阅读 476

jsmind思维导图撤销和前进的实现

最近在做封装一个基于vue的思维导图的项目,项目中遇到了关于编辑思维导图节点的时候的撤销前进功能。

gif.gif该思维导图使用到的库是jsmind的库:hizzgdev.github.io/jsmind/deve…,然后在此基础上对其进行了二次封装。因为jsmind使用过的人不是很多,而且都没有这个需求,所以可能里面说的内容有点晦涩难懂,但是了解了这个文档之后,在此看着这篇文章就不会那么难了。

写这篇文章的目的,一是给以后要维护这个项目的小伙伴看,二是分享这个问题的解决方案。虽然这个解决方案的逻辑不是很难,但是里面有踩过的坑,希望能给以后使用到这个库的小伙伴有点帮助。

数据结构和解释

storeMap: new Map(), // 记录编辑操作的存储 , 你的每次编辑的操作,都会被记录在map集合里面
opearaoptorSetp: 0, // 记录操作的步长,也就说到哪个位置了

// 用来存储每一次操作数据结构
opreatorParams: {
  type: "", // 操作的类型
  selectedNode: {}, // 选中的节点id
  selectedParantNode: {}, // 选中的父节点的id
  opreatorContent: {}, //本次操作涉及到的内容
  oldNode:{}, // 更新该结点之后的老节点信息
},复制代码

image-20211001105032740.png(storeMap中存储的数据)

image-20211001105131800.png(每一步操作存储的数据)

selectedNodeselectedParantNode数据类型结构

{
  id: '',             // 节点的id
  topic: [],          // 节点的文字
  children: [],       // 该节点的子节点
  data: {},           // 该节点存储的数据
  parent: {          // 该节点的父节点
    id: '',    // 父节点的id
    topic: '', // 父节点的文字
    data:{},   // 父节点存储的数据
},复制代码

storeMap里面存储的是依据opearaoptorSetpopreatorParams,以opearaoptorSetp作为key值,以opreatorParams作为value。

但是有人就说了,既然你用数字作为key值,不如直接用数组的方式存储了。这样说也没毛病,当初设计的时候,考虑到的是,因为在编辑思维导图的时候,可能会涉及到的插入,删除,增加频率会很高,所以使用了Map进行了存储。

关于撤销和前进操作的,我们先理解一下大致的步骤

1、当增,删,改节点的时候,opearaoptorSetp也会增加一次步长 , 都会将该次的操作加入到storeMap中

2、如果是撤销的话,退回一步,然后取到storeMap在这一步的值,进行还原

撤销的过程中,如果下一次操作是删除,添加,或者是更新节点操作,会覆盖之前的操作,然后将这一步之后的所有操作清空。

假设:

增加:【1,2,3,4,5,6】

后退到2:【1,2】

再次添加一个节点:【1,2,3(覆盖了第一次的3步骤)】,之后清空3后的所有操作。

3、前进的话,根据步长去storeMap中去取就可以。

图解:

image-20211003205238334.png(图一 增加)

image-20211003211013340.png(图二 修改)

image-20211006122126339.png(图三 后退)

image-20211006122313409.png(图四 后退-删除)

opreatorParams的中的type是用来标识本次的操作类型

  1. add:增加 , 对立:remove删除

  2. delete:删除, 对立:add增加 (将remove和delete区别,remove是后退过程的设置,delete是主动的删除)

  3. modify:修改,modify没有对立,但是在修改的时候,会增加一个oldNode的属性。modify的撤销和返回和add/delete的操作是不同的。add/delete的撤回(前进)时候,会先退(前进)一步,取数据然后在处理逻辑,而modify的则是保持原有的步长,如果是后退,则去取oldNode里面的值,反之去selectedNode中取值。

主要代码实现:

1、新增节点

新增节点包含的是兄弟节点和子孙节点两种情况

image-20211003173250377.png

两种新增的节点,除了父节点(operatorNodedParant)的不同,其余数据相同。addNode的触发是通过键盘事件监听tab、enter或者是手动点击触发的。其实关于增加节点,根据文档上的api,只需要提供父节点,增加节点的id和内容就行了,一步操作this.jm.add_node(selectedNode, nodeid, topic);,剩下的代码就是维护storeMapopearatorSetp的,毕竟这两个记录了后退和前进的数据。

 addNode(selectedNode, type) {
      if (selectedNode.isroot && type === "sibling") return;
      if (type === "sibling") {
        selectedNode = selectedNode.parent;
      }
      let nodeid = jsMind.util.uuid.newid();
      let topic = "New Node";
      this.jm.add_node(selectedNode, nodeid, topic);
      // 预置前进后退操作
      // 1.先设置号操作的参数,分为添加兄弟和同级两种
      let params = {
        type: "add",
      };
      const tempNode = this.jm.get_node(nodeid);
      const tempAddNode = {
        id: tempNode.id,
        topic: tempNode.topic,
        children: this.copyNode(tempNode.children),
        data: tempNode.data,
      };
      if (!tempNode.isroot) {
        tempAddNode.parent = {
          id: tempNode.parent.id,
          topic: tempNode.parent.topic,
          data: tempNode.parent.data,
        };
      }
      params.operatorNode = tempAddNode;
      params.operatorNodedParant = tempAddNode.parent;
      params.opreatorContent = tempAddNode;

      // 2.缓存此步数据
      this.storeMap.set(this.opearatorSetp, params);
      // 3.步长+1
      this.opearatorSetp++;
      this.clearTailNode();
      console.log("新增,缓存", this.storeMap, this.opearatorSetp);
},复制代码

2、删除一个节点: 删除节点包含两种删除,一种是正常的删除,另一种是撤回的时候删除(撤回的时候,增加的相反就是删除)。删除的时候,先退回一步,然后取得应该删除的那个节点(这个函数没有这一步,传来的selectedNode就是要删除的节点)。

至于为什么要分为两种呢。是因为,当我们正常删除的时候我们要将删除节点这一步记录在storeMap中,并且操作步骤opearatorSetp要加一次,并且清理删除之后所有的操作(这个步骤参考上面的撤销步骤)。但是如果是撤回的时候删除,我们应该是opearatorSetp减去一次,拿到之前存储的那个上一步的存储信息,然后恢复上一步的操作。

fastGoBack是撤销,back是正常删除。

 // 删除节点
onRemoveNode(selectedNode, type) {
  // 先设置
  if (["fastGoBack", "delete"].includes(type)) {
  /**
  * 1.先缓存,如果不缓存直接使用selectedNode,那么删除之后,就没有selectedNode,而且设置在map中的数据又是selectedNode的引用地址,所以到时候this.storeMap.set(this.opearatorSetp, params)数据会出错
  *
  */
      let params = { type: type === "fastGoBack" ? "remove" : "delete" };
      let tempSelectedNode = {
        id: selectedNode.id,
        topic: selectedNode.topic,
        children: this.copyNode(selectedNode.children),
        data: selectedNode.data,
        parent: {
              id: selectedNode.parent.id,
              topic: selectedNode.parent.topic,
              data: selectedNode.parent.data,
          },
        };
        // 设置数据
        params.operatorNode = tempSelectedNode;
        params.operatorNodedParant = tempSelectedNode.parent;
        params.opreatorContent = tempSelectedNode;
        this.storeMap.set(this.opearatorSetp, params);
        // 右键删除也要增加一步,快捷键删除,上一步已经修改完opearatorSetp
        if (type === "delete") this.opearatorSetp++;
    }
    this.jm.remove_node(selectedNode.id);
    // 删除之后,清理后面的数据
    if (type === "delete") {
       this.clearTailNode();
     }

     console.log("删除,缓存", this.storeMap, this.opearatorSetp);
},复制代码

3、修改,修改节点暂时不提供,因为涉及到的这个库里面的默认修改方法

4、前进和后退

这两个函数是监听键盘事件,执行撤销和前进的回调。

  // 前进
    handleGoForward() {
      //  因为前进的时候opearatorSetpy一直处于累加的状态,所以有限制的长度,不能超过操作步骤的长度
      let limitLength = Array.from(this.storeMap).length;
      if (this.opearatorSetp > limitLength - 1) return;
      // 如果是增加的时候,撤回的时候应该是对应的是删除,注意此时传入的type是‘fastGoBack’
      const handleNode = this.storeMap.get(this.opearatorSetp);
      if (handleNode.type === "add") {
        this.onRemoveNode(handleNode.operatorNode, "fastGoBack");
      } else if (["remove", "delete"].includes(handleNode.type)) {
        // 删除的时候,对应的是插入
        handleNode.type = "add";
        const parentNodeId = handleNode.operatorNodedParant.id;
        // 退后的节点是删除节点,将其插入
        this.insertNode(parentNodeId, handleNode.operatorNode);
      } else if (handleNode.type === "modify") {
        // 修改的时候,修改应该是更新节点上的信息
        const tempMap = this.storeMap.get(this.opearatorSetp);
        this.jm.update_node(
          tempMap.operatorNode.id,
          tempMap.operatorNode.topic
        );
      }
      this.opearatorSetp++;
      console.log("前进,缓存", this.storeMap, this.opearatorSetp);
    },
    // 后退,后退和前进类似,只是操作相反而已!!!
    handleGoBack() {
      // 先拿到操作的节点
      this.opearatorSetp--;
      let handleNode = this.storeMap.get(this.opearatorSetp);
    
      if (handleNode.type === "add") {
        // 后退的节点是增加节点,将其删除
        this.onRemoveNode(handleNode.operatorNode, "fastGoBack");
      } else if (["remove", "delete"].includes(handleNode.type)) {
        const parentNodeId = handleNode.operatorNodedParant.id;
        handleNode.type = "add";
        // 退后的节点是删除节点,将其插入
        this.insertNode(parentNodeId, handleNode.operatorNode);
        //
      } else if (handleNode.type === "modify") {
        const tempMap = this.storeMap.get(this.opearatorSetp);
        this.jm.update_node(tempMap.operatorNode.id, tempMap.oldNode.topic);
      }
      console.log("后退,缓存", this.storeMap, this.opearatorSetp);
    },复制代码

工具函数:

// 获取选中标签的 ID
getNode() {
  let selectedNode = this.jm.get_selected_node();
  if (!selectedNode) {
      this.$Message.warning("请先选择一个节点");
      return false;
    }
   return selectedNode;
},
// 克隆一个节点,深入克隆一个节点,包含他的子节点
copyNode(node) {
      if (!node.length) return [];
      return node.map((item) => {
        return {
          id: item.id,
          topic: item.topic,
          children: this.copyNode(item.children),
          data: item.data,
          parent: {
            id: item.parent.id,
            topic: item.parent.topic,
            data: item.parent.data,
          },
        };
      });
    },
        
        
// 清除尾节点
    clearTailNode() {
      let arr = Array.from(this.storeMap);
      for (let i = this.opearatorSetp; i < arr.length; i++) {
        this.storeMap.delete(i);
      }
    },
        
// 插入节点
    insertNode(parentNodeId, node) {
      let parentNode = this.jm.get_node(parentNodeId);
      this.jm.add_node(parentNode, node.id, node.topic, node.data);
      if (!node.children) return;

      node.children.forEach((item) => {
        this.insertNode(item.parent.id, item);
      });
    },
 // 克隆一个节点
  copyNode(node) {
      if (!node.length) return [];
      return node.map((item) => {
        return {
          id: item.id,
          topic: item.topic,
          children: this.copyNode(item.children),
          data: item.data,
          parent: {
            id: item.parent.id,
            topic: item.parent.topic,
            data: item.parent.data,
          },
        };
      });
    },复制代码

总结一下吧:

有一说一,这篇文章大部分对于前端开发者来说,一点用处没有,只有特殊的场景才会用的到。

而且,对于实现一个前进和撤销的方案,应该有很多解决方案,定义上面这种数据结构进行存储的话,少之又少,所以就当图个乐看看吧。


作者:一起要跳舞吗
链接:https://juejin.cn/post/7015818820564549646


文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐