• prosemirror 学习记录(二)创建 apple 节点


    apple type

    向 schema 中添加 apple type

    const nodes = {
      apple: {
        inline: true,
        attrs: {
          name: { default: "unknown" },
        },
        group: "inline",
        draggable: true,
        parseDOM: [
          {
            tag: "span[custom-node-type=apple]",
            getAttrs(dom) {
              return {
                name: dom.getAttribute("name"),
              };
            },
          },
        ],
        toDOM(node) {
          let { name } = node.attrs;
          return ["span", { "custom-node-type": "apple", name }];
        },
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    加上样式:

    span[custom-node-type="apple"]::before {
      content: attr(name);
      background: pink;
      outline: 1px dashed red;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    效果:
    在这里插入图片描述

    在这里插入图片描述

    insertApple

    <template>
      <section>
        <input type="button" value="红富士" @click="handleClick" />
        <input type="button" value="国光" @click="handleClick" />
      section>
    template>
    
    <script setup>
    import { inject } from "vue";
    const editorView = inject("editorView");
    
    function handleClick(e) {
      const name = e.target.value;
      insertApple(name);
    }
    
    function insertApple(name) {
      const view = editorView.value;
      const appleType = view.state.schema.nodes.apple;
      const newAppleNode = appleType.create({ name });
      view.dispatch(view.state.tr.replaceSelectionWith(newAppleNode));
    }
    script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    点击按钮就可以在文档中插入一个apple节点
    在这里插入图片描述

    实时更新按钮状态

    增加功能:插入前需要判断,仅在文档中没有此类苹果时才能添加

    function insertApple(name) {
      const view = editorView.value;
      const appleType = view.state.schema.nodes.apple;
      const find = findNodeIndex(view.state.doc, (node) => {
        return node.type.name === appleType.name && node.attrs.name === name;
      });
      if (find !== -1) {
        return;
      }
      const newAppleNode = appleType.create({ name });
      view.dispatch(view.state.tr.replaceSelectionWith(newAppleNode));
    }
    
    function findNodeIndex(doc, isMyNode) {
      let found = -1;
      doc.nodesBetween(0, doc.content.size, (node, pos) => {
        if (found !== -1) return false;
        if (isMyNode(node)) found = pos;
      });
      return found;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在这里插入图片描述

    增加功能:按钮不可用时,将按钮禁用。

    改写 insertApple 方法:添加 just_check 参数
    在这里插入图片描述

    • insertApple(name, true) 只想看看命令是否可用,并不想真的插入一个苹果
    • insertApple(name) 确实是想插入一个苹果

    根据 insertApple(name, true) 的返回值更新 button 的 disabled 状态:

    const button1 = ref();
    const button2 = ref();
    function updateButtonState(el) {
      const name = el.value;
      const flag = insertApple(name, true);
      if (flag) {
        el.removeAttribute("disabled");
      } else {
        el.setAttribute("disabled", true);
      }
    }
    setInterval(() => updateButtonState(button1.value), 1000 / 60);
    setInterval(() => updateButtonState(button2.value), 1000 / 60);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    上面的代码用定时器调用 updateButtonState,很垃圾。

    如果能在 view 变化时才调用 updateButtonState 就好了 —— prosemirror 的 Plugin 提供了这个能力!!!

    用 Plugin 实现实时更新

    import {Plugin} from "prosemirror-state"
    
    new Plugin({
      view(view) {
        // 初始化时执行,只执行一次
    
        return {
          update(view, prevState) {
            // view 每次变化时都会执行 update
          },
          destroy() {},
        };
      },
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    使用 Plugin 重写插入苹果的功能:(伪代码)

    new Plugin({
      view() {
        appleMenus= [
          { name: "红苹果", active: true },
          { name: "绿苹果", active: true },
        ];
    
        return {
          update(view, prevState) {
            appleMenus.forEach((appleMenu) => {
              appleMenu.active = insertApple(appleMenu.name, true);
            });
          },
          destroy() {},
        };
      },
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    将 insertApple 改写成 command 形式

    prosemirror 的 command 格式为:

    function command_a(state, dispatch, view){
    	// When a command isn't applicable, it should return false and do nothing. 
    	// When applicable, it should dispatch a transaction and return true.
    }
    
    • 1
    • 2
    • 3
    • 4

    举例:toggleMark 是 prosemirror 的内置方法,返回一个 切换指定 mark 和 attrs 的 command

    function toggleMark(markType, attrs){
    	return function(state, dispatch){
    		if(无法切换) return false
    		if(dispatch){
    			dispatch(tr....)
    		}
    		return true
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    依样画葫芦改造 insertApple:(改造后 insertApple 本身不是 command,它返回一个 command)

    function insertApple(name) {
      return function (state, dispatch) {
        const appleType = state.schema.nodes.apple;
        const find = findNodeIndex(state.doc, (node) => {
          return node.type.name === appleType.name && node.attrs.name === name;
        });
        if (find !== -1) {
          return false;
        }
        if (dispatch) {
          const newAppleNode = appleType.create({ name });
          dispatch(state.tr.replaceSelectionWith(newAppleNode));
        }
        return true;
      };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    这样调用内置方法(toggleMark)和自定义方法(insertApple)就可以用统一的方式调用了

    自定义菜单

    MyCustomMenuPlugin.js

    import { setBlockType, toggleMark } from "prosemirror-commands";
    import { Plugin } from "prosemirror-state";
    import { ref } from "vue";
    import { mySchema } from "./schema";
    import { findNodeIndex } from "./utils/utils";
    
    export const MyCustomMenuPlugin = new Plugin({
      view(view) {
        function update(view) {
          // 按钮的 active 和 enable 状态需要即时更新
          menus.value.forEach((menu) => {
            if (menu.updateActive) {
              menu.active = menu.updateActive(view.state);
            }
            if (menu.updateEnable) {
              menu.enable = menu.updateEnable(view.state);	// 不传dispatch参数
            }
          });
        }
        update(view);
        return { update };
      },
    });
    export const menus = ref([
      {
        label: "加粗",
        run: toggleMark(mySchema.marks.strong),
        active: true,
        updateActive: (state) => markActive(state, mySchema.marks.strong),
        enable: true,
      },
      {
        label: "段落",
        run: setBlockType(mySchema.nodes.paragraph),
        active: true,
        updateActive: (state) => blockTypeActive(state, mySchema.nodes.paragraph),
        enable: true,
      },
      {
        label: "标题1",
        run: setBlockType(mySchema.nodes.heading, { attrs: { level: 1 } }),
        active: true,
        updateActive: (state) => blockTypeActive(state, mySchema.nodes.heading, { level: 1 }),
        enable: true,
      },
      {
        label: "插入大苹果",
        run: insertApple("大苹果"),
        enable: true,
        updateEnable: (state) => insertApple("大苹果")(state),
      },
      {
        label: "插入小苹果",
        run: insertApple("小苹果"),
        enable: true,
        updateEnable: (state) => insertApple("小苹果")(state),
      },
    ]);
    // 自定义命令
    function insertApple(name) {
      return function (state, dispatch) {
        const appleType = state.schema.nodes.apple;
        const find = findNodeIndex(state.doc, (node) => {
          return node.type.name === appleType.name && node.attrs.name === name;
        });
        if (find !== -1) {
          return false;
        }
        if (dispatch) {
          const newAppleNode = appleType.create({ name });
          dispatch(state.tr.replaceSelectionWith(newAppleNode));
        }
        return true;
      };
    }
    
    // mark 级别的按钮用来判断 active(从 prosemirror-example-setup 包中抄的)
    function markActive(state, type) {
      let { from, $from, to, empty } = state.selection;
      if (empty) return !!type.isInSet(state.storedMarks || $from.marks());
      else return state.doc.rangeHasMark(from, to, type);
    }
    // block 级别的按钮用来判断 active(从 prosemirror-example-setup 包中抄的)
    function blockTypeActive(state, nodeType, attrs) {
      let { $from, to, node } = state.selection;
      if (node) return node.hasMarkup(nodeType, attrs);
      return to <= $from.end() && $from.parent.hasMarkup(nodeType, attrs);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88

    TestEditor.vue:

    <script setup>
    import { exampleSetup } from "prosemirror-example-setup";
    import { EditorState } from "prosemirror-state";
    import { EditorView } from "prosemirror-view";
    import { onMounted, shallowRef } from "vue";
    import "./editor.css";
    import { MyCustomMenuPlugin, menus } from "./MyCustomMenuPlugin";
    import { mySchema } from "./schema";
    
    
    const editorView = shallowRef(); // 不能用ref
    
    onMounted(() => {
      editorView.value = new EditorView(document.querySelector("#editor"), {
        state: EditorState.create({
          schema: mySchema,
          plugins: exampleSetup({
            schema: mySchema,
            menuBar: false,	// 不使用 exampleSetup 提供的 menu
          }).concat(MyCustomMenuPlugin),	// 用 concat 加上我们自定义的 menu 插件
        }),
      });
    });
    
    function handleClick(e, o) {
      e.preventDefault();
      o.run(editorView.value.state, editorView.value.dispatch);
    }
    script>
    
    <template>
      <section class="custom-menu">
        <input
          v-for="o in menus"
          :key="o.label"
          type="button"
          :value="o.label"
          @click="(e) => handleClick(e, o)"
          :class="{ active: o.active }"
          :disabled="!o.enable"
        />
      section>
      <section id="editor">section>
    template>
    
    <style>
    span[custom-node-type="apple"]::before {
      content: attr(name);
      background: pink;
      outline: 1px dashed red;
    }
    input[type="button"].active {
      font-weight: bold;
      background: gray;
      color: white;
    }
    style>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    效果:
    在这里插入图片描述

  • 相关阅读:
    传统算法与神经网络算法,进化算法优化神经网络
    Pandas处理dataframe的文本数据列:使用str属性获取数据列的字符串方法类、contains函数判断数据列是否包含指定字符串生成布尔值序列
    网络地址转换NAT
    空间地理数据可视化之 leaflet 包及其拓展
    【AI】机器学习——绪论
    如何在Docker环境下安装Firefox浏览器并结合内网穿透工具实现公网访问
    3.Android高仿网易云音乐-首页复杂发现界面布局和功能/RecyclerView复杂布局
    QT 排序操作
    【数据结构】哈希表
    Leetcode | 二叉树
  • 原文地址:https://blog.csdn.net/tangran0526/article/details/134010170