使用prosemirror实现脑图大纲模式

ProseMirror实现脑图大纲

1. 安装依赖

npm install prosemirror-state prosemirror-view prosemirror-model prosemirror-keymap prosemirror-commands prosemirror-schema-list prosemirror-history prosemirror-transform

2. 创建脑图大纲的 Schema

import { Schema } from 'prosemirror-model';
import { orderedList, bulletList, listItem } from 'prosemirror-schema-list';

// 创建脑图专用的 schema
const mindmapSchema = new Schema({
  nodes: {
    doc: {
      content: 'mindmap'
    },
    mindmap: {
      content: 'mindmap_node+',
      toDOM: () => ['div', { class: 'mindmap-container' }, 0]
    },
    mindmap_node: {
      content: 'paragraph (mindmap_node*)?',
      defining: true,
      toDOM: () => ['div', { class: 'mindmap-node' }, 0],
      parseDOM: [{ tag: 'div.mindmap-node' }]
    },
    paragraph: {
      content: 'text*',
      toDOM: () => ['p', 0],
      parseDOM: [{ tag: 'p' }]
    },
    text: {
      group: 'inline'
    }
  }
});

3. 创建插件系统

import { Plugin } from 'prosemirror-state';
import { keymap } from 'prosemirror-keymap';
import { baseKeymap } from 'prosemirror-commands';
import { history, redo, undo } from 'prosemirror-history';

// 脑图专用按键绑定
const mindmapKeymap = keymap({
  'Tab': indentNode,
  'Shift-Tab': dedentNode,
  'Enter': splitNode,
  'Backspace': mergeWithPrevious,
  'Mod-z': undo,
  'Mod-y': redo,
  'Mod-Shift-z': redo
});

// 缩进节点(创建子节点)
function indentNode(state, dispatch) {
  const { $from, $to } = state.selection;
  if ($from.sameParent($to) && $from.depth > 1) {
    const tr = state.tr;
    const start = $from.before($from.depth);
    const end = $from.after($from.depth);
    
    // 将当前节点转换为前一个节点的子节点
    tr.wrap(tr.doc.resolve(start).blockRange(tr.doc.resolve(end)), {
      type: state.schema.nodes.mindmap_node
    });
    
    if (dispatch) dispatch(tr);
    return true;
  }
  return false;
}

// 取消缩进(提升节点层级)
function dedentNode(state, dispatch) {
  const { $from } = state.selection;
  if ($from.depth < 3) return false;
  
  const tr = state.tr;
  const start = $from.before($from.depth - 1);
  const end = $from.after($from.depth - 1);
  
  tr.lift(tr.doc.resolve(start).blockRange(tr.doc.resolve(end)), $from.depth - 2);
  
  if (dispatch) dispatch(tr);
  return true;
}

// 分割节点
function splitNode(state, dispatch) {
  const { $from, $to } = state.selection;
  if ($from.sameParent($to)) {
    const tr = state.tr.split($from.pos);
    if (dispatch) dispatch(tr);
    return true;
  }
  return false;
}

// 合并节点
function mergeWithPrevious(state, dispatch) {
  const { $from } = state.selection;
  if ($from.parentOffset === 0 && $from.depth > 2) {
    const before = $from.before($from.depth);
    if (before > 1) {
      const tr = state.tr.delete(before - 1, before + 1);
      if (dispatch) dispatch(tr);
      return true;
    }
  }
  return false;
}

// 节点选择插件
const nodeSelectionPlugin = new Plugin({
  props: {
    handleClick(view, pos) {
      const $pos = view.state.doc.resolve(pos);
      if ($pos.parent.type.name === 'mindmap_node') {
        view.dispatch(view.state.tr.setSelection(NodeSelection.create(view.state.doc, $pos.before())));
        return true;
      }
      return false;
    }
  }
});

4. 创建编辑器状态和视图

import { EditorState } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view';
import { NodeSelection } from 'prosemirror-state';

function createMindmapState(schema) {
  return EditorState.create({
    doc: schema.node('doc', null, [
      schema.node('mindmap', null, [
        createMindmapNode(schema, '中心主题'),
        createMindmapNode(schema, '主要分支 1', [
          createMindmapNode(schema, '子主题 1.1'),
          createMindmapNode(schema, '子主题 1.2')
        ]),
        createMindmapNode(schema, '主要分支 2', [
          createMindmapNode(schema, '子主题 2.1'),
          createMindmapNode(schema, '子主题 2.2')
        ])
      ])
    ]),
    plugins: [
      history(),
      mindmapKeymap,
      nodeSelectionPlugin,
      keymap(baseKeymap)
    ]
  });
}

function createMindmapNode(schema, text, children = []) {
  const paragraph = schema.node('paragraph', null, [
    schema.text(text)
  ]);
  
  return schema.node('mindmap_node', null, [
    paragraph,
    ...children
  ]);
}

5. 样式和主题

// 创建带有样式的编辑器视图
function createMindmapEditor(placeholder) {
  const schema = mindmapSchema;
  const state = createMindmapState(schema);
  
  return new EditorView(placeholder, {
    state,
    dispatchTransaction(transaction) {
      const newState = this.state.apply(transaction);
      this.updateState(newState);
    },
    nodeViews: {
      mindmap_node(node, view, getPos) {
        return new MindmapNodeView(node, view, getPos);
      }
    }
  });
}

// 自定义节点视图
class MindmapNodeView {
  constructor(node, view, getPos) {
    this.node = node;
    this.view = view;
    this.getPos = getPos;
    this.dom = document.createElement('div');
    this.dom.className = 'mindmap-node';
    
    this.contentDOM = document.createElement('div');
    this.contentDOM.className = 'node-content';
    this.dom.appendChild(this.contentDOM);
    
    this.update(node);
  }
  
  update(node) {
    if (node.type !== this.node.type) return false;
    this.node = node;
    return true;
  }
  
  selectNode() {
    this.dom.classList.add('ProseMirror-selectednode');
  }
  
  deselectNode() {
    this.dom.classList.remove('ProseMirror-selectednode');
  }
}

6. 完整的初始化代码

import { Schema } from 'prosemirror-model';
import { EditorState, NodeSelection } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view';
import { keymap } from 'prosemirror-keymap';
import { baseKeymap } from 'prosemirror-commands';
import { history, redo, undo } from 'prosemirror-history';
import { Plugin } from 'prosemirror-state';

// 定义脑图 schema
const mindmapSchema = new Schema({
  nodes: {
    doc: { content: 'mindmap' },
    mindmap: {
      content: 'mindmap_node+',
      toDOM: () => ['div', { class: 'mindmap-container' }, 0]
    },
    mindmap_node: {
      content: 'paragraph (mindmap_node*)?',
      defining: true,
      toDOM: () => ['div', { class: 'mindmap-node' }, 0],
      parseDOM: [{ tag: 'div.mindmap-node' }]
    },
    paragraph: {
      content: 'text*',
      toDOM: () => ['p', 0],
      parseDOM: [{ tag: 'p' }]
    },
    text: { group: 'inline' }
  }
});

// 创建脑图编辑器
export function createMindmapEditor(element) {
  // 创建初始文档
  const initialDoc = mindmapSchema.node('doc', null, [
    mindmapSchema.node('mindmap', null, [
      createNode('中心主题'),
      createNode('主要分支 1', [
        createNode('子主题 1.1'),
        createNode('子主题 1.2')
      ]),
      createNode('主要分支 2', [
        createNode('子主题 2.1'),
        createNode('子主题 2.2', [
          createNode('子子主题 2.2.1')
        ])
      ])
    ])
  ]);

  function createNode(text, children = []) {
    const paragraph = mindmapSchema.node('paragraph', null, [
      mindmapSchema.text(text)
    ]);
    return mindmapSchema.node('mindmap_node', null, [
      paragraph,
      ...children
    ]);
  }

  // 创建编辑器状态
  const state = EditorState.create({
    doc: initialDoc,
    plugins: [
      history(),
      keymap(mindmapKeymap),
      keymap(baseKeymap),
      nodeSelectionPlugin
    ]
  });

  // 创建编辑器视图
  return new EditorView(element, {
    state,
    nodeViews: {
      mindmap_node: (node, view, getPos) => new MindmapNodeView(node, view, getPos)
    }
  });
}

// 使用示例
const editorElement = document.getElementById('mindmap-editor');
const editor = createMindmapEditor(editorElement);

7. 扩展功能

添加序列化方法

// 导出脑图数据
export function exportMindmapData(editor) {
  const doc = editor.state.doc;
  const mindmap = doc.firstChild;
  const nodes = [];
  
  function traverseNode(node, depth = 0) {
    if (node.type.name === 'mindmap_node') {
      const text = node.firstChild.textContent;
      const children = [];
      
      // 遍历子节点
      for (let i = 1; i < node.childCount; i++) {
        const child = node.child(i);
        if (child.type.name === 'mindmap_node') {
          children.push(traverseNode(child, depth + 1));
        }
      }
      
      return {
        text,
        depth,
        children: children.length > 0 ? children : undefined
      };
    }
    return null;
  }
  
  mindmap.forEach(node => {
    const nodeData = traverseNode(node);
    if (nodeData) nodes.push(nodeData);
  });
  
  return nodes;
}

// 导入脑图数据
export function importMindmapData(editor, data) {
  const { schema } = editor.state;
  const tr = editor.state.tr;
  
  function createNodeFromData(nodeData) {
    const children = [schema.node('paragraph', null, [
      schema.text(nodeData.text)
    ])];
    
    if (nodeData.children) {
      nodeData.children.forEach(childData => {
        children.push(createNodeFromData(childData));
      });
    }
    
    return schema.node('mindmap_node', null, children);
  }
  
  const mindmapNodes = data.map(createNodeFromData);
  const mindmap = schema.node('mindmap', null, mindmapNodes);
  const doc = schema.node('doc', null, [mindmap]);
  
  tr.replaceWith(0, editor.state.doc.content.size, doc.content);
  editor.dispatch(tr);
}

添加工具栏

// 创建工具栏
export function createMindmapToolbar(editor, container) {
  const toolbar = document.createElement('div');
  toolbar.className = 'mindmap-toolbar';
  
  const buttons = [
    { text: '缩进', action: () => indentNode(editor.state, editor.dispatch) },
    { text: '取消缩进', action: () => dedentNode(editor.state, editor.dispatch) },
    { text: '添加子节点', action: addChildNode },
    { text: '添加兄弟节点', action: addSiblingNode },
    { text: '删除节点', action: deleteNode }
  ];
  
  buttons.forEach(btn => {
    const button = document.createElement('button');
    button.textContent = btn.text;
    button.addEventListener('click', btn.action);
    toolbar.appendChild(button);
  });
  
  container.appendChild(toolbar);
}

function addChildNode(state, dispatch) {
  const { $from } = state.selection;
  const node = $from.node($from.depth);
  
  if (node.type.name === 'mindmap_node') {
    const tr = state.tr;
    const newNode = createNode('新子节点');
    const insertPos = $from.after($from.depth);
    
    tr.insert(insertPos, newNode);
    if (dispatch) dispatch(tr);
    return true;
  }
  return false;
}

function addSiblingNode(state, dispatch) {
  const { $from } = state.selection;
  if ($from.depth < 2) return false;
  
  const tr = state.tr;
  const newNode = createNode('新兄弟节点');
  const insertPos = $from.after($from.depth);
  
  tr.insert(insertPos, newNode);
  if (dispatch) dispatch(tr);
  return true;
}

function deleteNode(state, dispatch) {
  const { $from } = state.selection;
  if ($from.depth < 2) return false;
  
  const tr = state.tr;
  const start = $from.before($from.depth);
  const end = $from.after($from.depth);
  
  tr.delete(start, end);
  if (dispatch) dispatch(tr);
  return true;
}

这个实现提供了完整的脑图大纲功能,包括:

  • 层级结构显示
  • 缩进/取消缩进
  • 节点选择
  • 添加/删除节点
  • 历史记录
  • 数据导入导出
    根据需要进一步扩展功能,如添加拖拽排序、折叠展开、样式定制等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值