vian 5 лет назад
Родитель
Сommit
998e3d8e81
8 измененных файлов с 598 добавлено и 571 удалено
  1. 1 0
      tree/.eslintrc.js
  2. 1 1
      tree/package-lock.json
  3. 1 1
      tree/package.json
  4. 2 530
      tree/src/index.ts
  5. 20 16
      tree/src/nodeCtx.ts
  6. 550 0
      tree/src/tree.ts
  7. 22 22
      tree/tests/nodeCtx.ts
  8. 1 1
      tree/tests/tree.ts

+ 1 - 0
tree/.eslintrc.js

@@ -40,5 +40,6 @@ module.exports = {
     'no-param-reassign': 'off',
     'no-plusplus': 'off',
     'class-methods-use-this': 'off',
+    'import/prefer-default-export': 'off',
   },
 };

+ 1 - 1
tree/package-lock.json

@@ -1,6 +1,6 @@
 {
   "name": "@sc/tree",
-  "version": "1.0.0",
+  "version": "1.0.1",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {

+ 1 - 1
tree/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@sc/tree",
-  "version": "1.0.0",
+  "version": "1.0.1",
   "description": "a template for npm package coding",
   "main": "./dist/index.cjs.js",
   "module": "./dist/index.esm.js",

+ 2 - 530
tree/src/index.ts

@@ -1,530 +1,2 @@
-import NodeContext from './nodeCtx';
-
-export interface TreeRaw {
-  ID: string;
-  parentID: string;
-  seq: number;
-  [propName: string]: any;
-}
-
-export interface TreeNode extends TreeRaw {
-  ctx: NodeContext;
-}
-
-export interface TreeIDMap {
-  [propName: string]: TreeNode;
-}
-
-export interface TreeParentMap {
-  [propName: string]: TreeNode[];
-}
-
-export interface ParentMap {
-  [propName: string]: (TreeRaw | TreeNode)[];
-}
-
-export interface UpdateItem {
-  parentID?: string;
-  seq?: number;
-  [propName: string]: any;
-}
-
-export interface UpdateData {
-  ID: string;
-  update: UpdateItem;
-}
-
-export type None = null | undefined;
-
-export class Tree {
-  // 原始数据,不经过排序的数据
-  rawData: TreeNode[];
-
-  // 按照树结构拼装好、排好序的数据。实际只是原始数据进行排序,内部元素跟原始数据内部元素的引用是一致的
-  data: TreeNode[];
-
-  readonly rootID: string = '-1';
-
-  // 默认初始顺序号。在对树结构进行操作后,没必要保证seq连号,只需保证正确排序就行,以减少需要更新的节点。
-  readonly seqStartIndex = 0;
-
-  // ID与原始数据条目映射
-  IDMap: TreeIDMap;
-
-  // parentID与多条同parentID的原始数据条目映射,parentMap内部数组要始终保持正确排序
-  parentMap: TreeParentMap;
-
-  constructor(rawData: TreeRaw[]) {
-    this.rawData = this.genNodeContext(rawData);
-    this.rawData = Tree.sort(this.rawData);
-    this.data = [];
-    this.IDMap = {};
-    this.parentMap = {};
-    this.genMap();
-    this.genData();
-  }
-
-  // 根据seq进行排序
-  static sort<T extends TreeNode | TreeRaw>(nodes: T[]): T[] {
-    return nodes.sort((a, b) => a.seq - b.seq);
-  }
-
-  // 获取节点的所有parentID
-  static getParentIDList(nodes: TreeNode[]): Set<string> {
-    const parentIDList: Set<string> = new Set();
-    nodes.forEach(node => parentIDList.add(node.parentID));
-    return parentIDList;
-  }
-
-  // 生成节点ctx上下文,挂载相关节点方法,TreeRaw转换为TreeNode
-  genNodeContext(rawData: TreeRaw[]): TreeNode[] {
-    rawData.forEach(item => {
-      item.ctx = new NodeContext(item as TreeNode, this);
-    });
-    return rawData as TreeNode[];
-  }
-
-  // 生成映射表
-  genMap(): void {
-    this.rawData.forEach(raw => {
-      this.IDMap[raw.ID] = raw;
-      (
-        this.parentMap[raw.parentID] || (this.parentMap[raw.parentID] = [])
-      ).push(raw);
-    });
-  }
-
-  // 获取顶层原始数据
-  getRoots(): TreeNode[] {
-    return this.parentMap[this.rootID] || [];
-  }
-
-  // 生成按照树结构排好序的数据
-  genData(): void {
-    // genData时不需要排序,因为rawData已经排好序
-    const roots = this.getRoots();
-    const pushNodesToData = (nodes: TreeNode[]): void => {
-      nodes.forEach(node => {
-        this.data.push(node);
-        const children = this.parentMap[node.ID];
-        if (children && children.length) {
-          pushNodesToData(children);
-        }
-      });
-    };
-    pushNodesToData(roots);
-  }
-
-  // 重新生成排序序好的数据,不改变原引用
-  reGenData(): void {
-    this.data.splice(0, this.data.length);
-    this.genData();
-  }
-
-  // 将相关parentMap内的数据、data进行重新排序。新增、删除等操作进行完后,数据需要重新排序
-  reSortData(nodes: TreeNode[]): void {
-    const toSortList = Tree.getParentIDList(nodes);
-    toSortList.forEach(parentID => {
-      if (this.parentMap[parentID] && this.parentMap[parentID].length) {
-        Tree.sort(this.parentMap[parentID]);
-      }
-    });
-    this.reGenData();
-  }
-
-  // 根据parentID对parentMap进行重新排序,并重新排序生成data
-  resortDataByID(parentIDList: string[]): void {
-    parentIDList.forEach(parentID => {
-      if (this.parentMap[parentID] && this.parentMap[parentID].length) {
-        Tree.sort(this.parentMap[parentID]);
-      }
-    });
-    this.reGenData();
-  }
-
-  // 查找ID节点
-  find(ID: string): TreeNode | None {
-    return this.IDMap[ID];
-  }
-
-  // 查找ID节点的父节点
-  findParent(ID: string): TreeNode | None {
-    const node = this.find(ID);
-    if (!node) {
-      return null;
-    }
-    return this.find(node.parentID);
-  }
-
-  // 查找ID节点的下一个节点
-  findNext(ID: string): TreeNode | None {
-    const node = this.find(ID);
-    if (!node) {
-      return null;
-    }
-    const nodes = this.parentMap[node.parentID];
-    const nodeIndex = nodes.indexOf(node);
-    if (nodeIndex < 0) {
-      return null;
-    }
-    return nodes[nodeIndex + 1];
-  }
-
-  // 查找ID节点的上一个节点
-  findPrev(ID: string): TreeNode | None {
-    const node = this.find(ID);
-    if (!node) {
-      return null;
-    }
-    const nodes = this.parentMap[node.parentID];
-    const nodeIndex = nodes.indexOf(node);
-    if (nodeIndex < 0) {
-      return null;
-    }
-    return nodes[nodeIndex - 1];
-  }
-
-  // 查询ID节点的子节点
-  findChilren(ID: string): TreeNode[] {
-    return this.parentMap[ID] || [];
-  }
-
-  updateValue(updateData: UpdateData[]): void {
-    if (updateData.length) {
-      updateData.forEach(updateItem => {
-        const node = this.find(updateItem.ID);
-        if (node) {
-          Object.assign(node, updateItem.update);
-        }
-      });
-    }
-  }
-
-  // 递归获取节点
-  getNodesPosterity(treeNodes: TreeNode[], includeSelf = true): TreeNode[] {
-    const rst: TreeNode[] = [];
-    const pushNodes = (nodes: TreeNode[]): void => {
-      nodes.forEach(node => {
-        if (includeSelf || !treeNodes.includes(node)) {
-          rst.push(node);
-        }
-        const children = this.findChilren(node.ID);
-        if (children.length) {
-          pushNodes(children);
-        }
-      });
-    };
-    pushNodes(treeNodes);
-    return rst;
-  }
-
-  // 节选中点块是否是可操作的节点块,选中一批节点进行升降、上下移的时候可能需要用到这个判断。
-  // 以第一个节点为基准,如果后续的节点深度均大于于于等于第一个节点的深度,且节点间是连续的,则此为可操作节点块
-  isOperable(nodes: TreeNode[]): boolean {
-    const baseDepth = nodes[0].ctx.depth();
-    for (let i = 0; i < nodes.length; i++) {
-      const node = nodes[i];
-      const depth = node.ctx.depth();
-      if (depth < baseDepth) {
-        return false;
-      }
-      const nextRowNode = nodes[i + 1];
-      if (nextRowNode && node.ctx.row() + 1 !== nextRowNode.ctx.row()) {
-        return false;
-      }
-    }
-    return true;
-  }
-
-  // 从节点块中获取相同深度的节点
-  sameDepthNodes(nodes: TreeNode[], depth: number | None): TreeNode[] {
-    if (!depth) {
-      depth = nodes[0].ctx.depth();
-    }
-    return nodes.filter(node => node.ctx.depth() === depth);
-  }
-
-  // 准备插入节点,插入节点前,计算出需要更新的数据。(可能会直接覆盖插入数据的seq)
-  // 可调用完此方法后,将需要更新、插入的数据提交至数据库,成功响应后调用插入节点更新缓存的方法
-  prepareInsert(rawData: TreeRaw[]): UpdateData[] {
-    const updateData: UpdateData[] = [];
-    const insertParentMap: ParentMap = {};
-    // 将相同父项的插入数据和已存在数据进行合并
-    rawData.forEach(item => {
-      if (typeof item.seq === 'undefined') {
-        item.seq = this.seqStartIndex;
-      }
-      (
-        insertParentMap[item.parentID] || (insertParentMap[item.parentID] = [])
-      ).push(item);
-    });
-    Object.entries(insertParentMap).forEach(([parentID, insertItems]) => {
-      const items = this.parentMap[parentID];
-      if (items) {
-        insertParentMap[parentID].push(...items);
-      }
-      // 重新排序
-      const combineItems = insertParentMap[parentID];
-      Tree.sort(combineItems);
-      combineItems.forEach((item, index) => {
-        // 插入数据重新赋值
-        if (insertItems.includes(item)) {
-          item.seq = index;
-        } else if (item.seq !== index) {
-          // 需要更新的原数据
-          updateData.push({
-            ID: item.ID,
-            update: {
-              seq: index,
-            },
-          });
-        }
-      });
-    });
-    return updateData;
-  }
-
-  // 插入节点数据
-  insert(items: TreeRaw[], updateData: UpdateData[] = []): TreeNode[] {
-    // 更新需要更新的节点(一般为更新seq)
-    this.updateValue(updateData);
-    // 建立映射、插入数据
-    const nodes = this.genNodeContext(items);
-    nodes.forEach(node => {
-      this.IDMap[node.ID] = node;
-      (
-        this.parentMap[node.parentID] || (this.parentMap[node.parentID] = [])
-      ).push(node);
-      this.rawData.push(node);
-    });
-    // 排序
-    this.reSortData(nodes);
-    return this.data;
-  }
-
-  // 准备删除,返回所有需要删除的节点,包括嵌套节点
-  prepareDelete(deleteNodes: TreeNode[]): TreeNode[] {
-    return this.getNodesPosterity(deleteNodes);
-  }
-
-  /**
-   * 删除节点
-   * @param {TreeNode[]} treeNodes - 要删除的节点,不需要包含嵌套节点
-   * @return {TreeNode[]} 返回删除节点后,树的data
-   */
-  delete(treeNodes: TreeNode[]): TreeNode[] {
-    const allDeletedNodes: TreeNode[] = [];
-    // 递归删除节点
-    const deleteNodes = (nodes: TreeNode[]): void => {
-      // 删除映射、删除数据
-      nodes.forEach(node => {
-        allDeletedNodes.push(node);
-        delete this.IDMap[node.ID];
-        const children = this.parentMap[node.ID];
-        delete this.parentMap[node.ID];
-        const nodesInParentMap = this.parentMap[node.parentID];
-        if (nodesInParentMap && nodesInParentMap.length) {
-          const nIndex = nodesInParentMap.indexOf(node);
-          if (nIndex >= 0) {
-            nodesInParentMap.splice(nIndex, 1);
-          }
-        }
-        const index = this.rawData.indexOf(node);
-        if (index >= 0) {
-          this.rawData.splice(index, 1);
-        }
-        if (children && children.length) {
-          deleteNodes(children);
-        }
-      });
-    };
-    deleteNodes(treeNodes);
-    // 排序
-    this.reSortData(allDeletedNodes);
-    return this.data;
-  }
-
-  // IDList 返回所有需要删除的节点,包括嵌套节点
-  prepareDeleteByID(IDList: string[]): TreeNode[] {
-    const deleteNodes: TreeNode[] = [];
-    IDList.forEach(ID => {
-      const node = this.find(ID);
-      if (node) {
-        deleteNodes.push(node);
-      }
-    });
-    return this.prepareDelete(deleteNodes);
-  }
-
-  /**
-   * 根据ID删除节点
-   * @param {string[]} IDList - 要删除的节点的ID列表(不包含嵌套节点ID)
-   * @return {TreeNode[]} - 返回被删除的所有节点
-   */
-  deleteByID(IDList: string[]): TreeNode[] {
-    const deleteNodes: TreeNode[] = [];
-    IDList.forEach(ID => {
-      const node = this.find(ID);
-      if (node) {
-        deleteNodes.push(node);
-      }
-    });
-    this.delete(deleteNodes);
-    return deleteNodes;
-  }
-
-  // 准备上移节点块(连续的兄弟节点),注意节点的seq可能不连号
-  prepareUpMove(nodes: TreeNode[]): UpdateData[] {
-    const updateData: UpdateData[] = [];
-    const firstNode = nodes[0];
-    const firstNodePrev = this.findPrev(firstNode.ID);
-    if (!firstNodePrev) {
-      return [];
-    }
-    let tempSeq = firstNodePrev.seq;
-    nodes.forEach(node => {
-      const orgSeq = node.seq;
-      updateData.push({
-        ID: node.ID,
-        update: { seq: tempSeq },
-      });
-      tempSeq = orgSeq;
-    });
-    updateData.push({
-      ID: firstNodePrev.ID,
-      update: { seq: tempSeq },
-    });
-    return updateData;
-  }
-
-  // 准备下移节点块(连续的兄弟节点),注意节点的seq可能不连号
-  prepareDownMove(nodes: TreeNode[]): UpdateData[] {
-    const updateData: UpdateData[] = [];
-    const lastNode = nodes[nodes.length - 1];
-    const lastNodeNext = this.findNext(lastNode.ID);
-    if (!lastNodeNext) {
-      return [];
-    }
-    let tempSeq = lastNodeNext.seq;
-    for (let i = nodes.length - 1; i >= 0; i--) {
-      const node = nodes[i];
-      const orgSeq = node.seq;
-      updateData.push({
-        ID: node.ID,
-        update: { seq: tempSeq },
-      });
-      tempSeq = orgSeq;
-    }
-    updateData.push({
-      ID: lastNodeNext.ID,
-      update: { seq: tempSeq },
-    });
-    return updateData;
-  }
-
-  // 上下移
-  move(nodes: TreeNode[], updateData: UpdateData[]): void {
-    this.updateValue(updateData);
-    this.reSortData(nodes);
-  }
-
-  // 准备升级节点块(连续的兄弟节点),不维护seq连号
-  prepareUpLevel(nodes: TreeNode[]): UpdateData[] {
-    const updateData: UpdateData[] = [];
-    const firstNode = nodes[0];
-    const lastNode = nodes[nodes.length - 1];
-    const parent = this.findParent(firstNode.ID);
-    if (!parent) {
-      return [];
-    }
-    const baseSeq = parent.seq + 1;
-    nodes.forEach((node, index) => {
-      updateData.push({
-        ID: node.ID,
-        update: { parentID: parent.parentID, seq: baseSeq + index },
-      });
-    });
-    const parentNextBrothers = parent.ctx.nextBrothers();
-    // 因为seq可能是不连号的,如果上移的最末节点seq,小于下一节点的seq,那就不更新所有下兄弟节点的seq,减少更新的数据量
-    const lastNodeCurSeq = updateData[updateData.length - 1].update.seq;
-    const firstBrohter = parentNextBrothers[0];
-    if (lastNodeCurSeq && lastNodeCurSeq >= firstBrohter.seq) {
-      parentNextBrothers.forEach((node, index) => {
-        updateData.push({
-          ID: node.ID,
-          update: { seq: baseSeq + index + nodes.length },
-        });
-      });
-    }
-    // 最末节点的所有后兄弟节点,成为最末节点的子节点
-    const lastNodeNextBrothers = lastNode.ctx.nextBrothers();
-    lastNodeNextBrothers.forEach((node, index) => {
-      updateData.push({
-        ID: node.ID,
-        update: { parentID: lastNode.ID, seq: index },
-      });
-    });
-    return updateData;
-  }
-
-  upLevel(nodes: TreeNode[], updateData: UpdateData[]): void {
-    const firstNode = nodes[0];
-    const lastNode = nodes[nodes.length - 1];
-    if (!firstNode.ctx.canUpLevel()) {
-      return;
-    }
-    const orgParentID = firstNode.parentID;
-    const orgBrothers = this.parentMap[orgParentID];
-    const lastNodeNextBrothers = lastNode.ctx.nextBrothers();
-    orgBrothers.splice(
-      orgBrothers.indexOf(firstNode),
-      nodes.length + lastNodeNextBrothers.length
-    );
-    (this.parentMap[lastNode.ID] || (this.parentMap[lastNode.ID] = [])).push(
-      ...lastNodeNextBrothers
-    );
-    this.updateValue(updateData);
-    const newParentID = firstNode.parentID;
-    this.parentMap[newParentID].push(...nodes);
-    this.resortDataByID([orgParentID, newParentID, lastNode.ID]);
-  }
-
-  // 准备降级节点块(连续的兄弟节点),不维护seq连号
-  prepareDownLevel(nodes: TreeNode[]): UpdateData[] {
-    const updateData: UpdateData[] = [];
-    const firstNode = nodes[0];
-    const prevNode = this.findPrev(firstNode.ID);
-    if (!prevNode) {
-      return [];
-    }
-    // 节点块成为前节点的子节点
-    const prevNodeLastChild = prevNode.ctx.lastChild();
-    const baseSeq = prevNodeLastChild
-      ? prevNodeLastChild.seq + 1
-      : this.seqStartIndex;
-    nodes.forEach((node, index) => {
-      updateData.push({
-        ID: node.ID,
-        update: { parentID: prevNode.ID, seq: baseSeq + index },
-      });
-    });
-    return updateData;
-  }
-
-  downLevel(nodes: TreeNode[], updateData: UpdateData[]): void {
-    const firstNode = nodes[0];
-    if (!firstNode.ctx.canDownLevel()) {
-      return;
-    }
-    const prevNode = this.findPrev(firstNode.ID);
-    if (!prevNode) {
-      return;
-    }
-    const orgBrothers = this.parentMap[firstNode.parentID];
-    orgBrothers.splice(orgBrothers.indexOf(firstNode), nodes.length);
-    (this.parentMap[prevNode.ID] || (this.parentMap[prevNode.ID] = [])).push(
-      ...nodes
-    );
-    this.updateValue(updateData);
-  }
-}
+export * from './tree';
+export * from './nodeCtx';

+ 20 - 16
tree/src/nodeCtx.ts

@@ -1,6 +1,6 @@
-import { TreeNode, Tree, None } from './index';
+import { TreeNode, Tree, None } from './tree';
 
-class NodeContext {
+export class NodeContext {
   // 对树节点数据的引用
   ref: TreeNode;
 
@@ -22,29 +22,35 @@ class NodeContext {
     return this.ref.parentID;
   }
 
-  // 节点在同层数据中的顺序
-  seq(): number {
-    return this.ref.seq;
-  }
-
   // 节点在完整、排好序的树数据中的行号
   row(): number {
     return this.tree.data.indexOf(this.ref);
   }
 
+  // 节点在相同父节点下的行号
+  rowInParent(): number {
+    return this.brothers().indexOf(this.ref);
+  }
+
+  // 节点的原始seq数据
+  seq(): number {
+    return this.ref.seq;
+  }
+
+  // 节点深度,根节点深度为0
   depth(): number {
     const parent = this.parent();
-    return parent ? parent.ctx.depth() + 1 : 0;
+    return parent ? parent.getCtx().depth() + 1 : 0;
   }
 
   // 节点是否可见,根据先代节点的expanded就可以计算出来,不需要维护visible属性
   visible(): boolean {
     let parent = this.parent();
     while (parent) {
-      if (parent.ctx.expanded) {
+      if (parent.getCtx().expanded) {
         return false;
       }
-      parent = parent.ctx.parent();
+      parent = parent.getCtx().parent();
     }
     return true;
   }
@@ -81,7 +87,7 @@ class NodeContext {
     const getChild = (nodes: TreeNode[]): void => {
       nodes.forEach(node => {
         posterity.push(node);
-        const children = node.ctx.chilren();
+        const children = node.getCtx().chilren();
         if (children.length) {
           getChild(children);
         }
@@ -98,8 +104,8 @@ class NodeContext {
   // 获取节点最上层的父项(起源)
   progenitor(): TreeNode | None {
     let parent = this.parent();
-    while (parent && parent.ctx.parent()) {
-      parent = parent.ctx.parent();
+    while (parent && parent.getCtx().parent()) {
+      parent = parent.getCtx().parent();
     }
     return parent;
   }
@@ -110,7 +116,7 @@ class NodeContext {
     let parent = this.parent();
     while (parent) {
       ancestor.push(parent);
-      parent = parent.ctx.parent();
+      parent = parent.getCtx().parent();
     }
     return ancestor;
   }
@@ -164,5 +170,3 @@ class NodeContext {
     return !!this.prev();
   }
 }
-
-export default NodeContext;

+ 550 - 0
tree/src/tree.ts

@@ -0,0 +1,550 @@
+import { NodeContext } from './nodeCtx';
+
+export interface TreeRaw {
+  ID: string;
+  parentID: string;
+  seq: number;
+  [propName: string]: any;
+}
+
+export interface TreeNode extends TreeRaw {
+  getCtx: () => NodeContext; // ctx对象改为getCtx(),因为handsontable数据循环引用会报错
+}
+
+export interface TreeIDMap {
+  [propName: string]: TreeNode;
+}
+
+export interface CtxIDMap {
+  [propName: string]: NodeContext;
+}
+
+export interface TreeParentMap {
+  [propName: string]: TreeNode[];
+}
+
+export interface ParentMap {
+  [propName: string]: (TreeRaw | TreeNode)[];
+}
+
+export interface UpdateItem {
+  parentID?: string;
+  seq?: number;
+  [propName: string]: any;
+}
+
+export interface UpdateData {
+  ID: string;
+  update: UpdateItem;
+}
+
+export type None = null | undefined;
+
+export class Tree {
+  // 原始数据,不经过排序的数据
+  rawData: TreeNode[];
+
+  // 按照树结构拼装好、排好序的数据。实际只是原始数据进行排序,内部元素跟原始数据内部元素的引用是一致的
+  data: TreeNode[];
+
+  readonly rootID: string = '-1';
+
+  // 默认初始顺序号。在对树结构进行操作后,没必要保证seq连号,只需保证正确排序就行,以减少需要更新的节点。
+  readonly seqStartIndex = 0;
+
+  // ID与原始数据条目映射
+  IDMap: TreeIDMap;
+
+  // parentID与多条同parentID的原始数据条目映射,parentMap内部数组要始终保持正确排序
+  parentMap: TreeParentMap;
+
+  // 节点上下文与节点ID映射
+  ctxMap: CtxIDMap;
+
+  constructor(rawData: TreeRaw[]) {
+    this.rawData = this.genNodeContext(rawData);
+    this.rawData = Tree.sort(this.rawData);
+    this.data = [];
+    this.IDMap = {};
+    this.parentMap = {};
+    this.ctxMap = {};
+    this.genMap(this.rawData);
+    this.genData();
+  }
+
+  // 根据seq进行排序
+  static sort<T extends TreeNode | TreeRaw>(nodes: T[]): T[] {
+    return nodes.sort((a, b) => a.seq - b.seq);
+  }
+
+  // 获取节点的所有parentID
+  static getParentIDList(nodes: TreeNode[]): Set<string> {
+    const parentIDList: Set<string> = new Set();
+    nodes.forEach(node => parentIDList.add(node.parentID));
+    return parentIDList;
+  }
+
+  // 生成节点getCtx方法(由于handsontable循环引用bug,将ctx对象改为getCtx方法),用于获取节点上下文,挂载相关节点方法,TreeRaw转换为TreeNode
+  private genNodeContext(rawData: TreeRaw[]): TreeNode[] {
+    rawData.forEach(item => {
+      item.getCtx = () => this.ctxMap[item.ID];
+    });
+    return rawData as TreeNode[];
+  }
+  /* genNodeContext(rawData: TreeRaw[]): TreeNode[] {
+    rawData.forEach(item => {
+      item.getCtx() = new NodeContext(item as TreeNode, this);
+    });
+    return rawData as TreeNode[];
+  } */
+
+  // 生成映射表
+  private genMap(data: TreeNode[]): void {
+    data.forEach(item => {
+      this.IDMap[item.ID] = item;
+      this.ctxMap[item.ID] = new NodeContext(item, this);
+      (
+        this.parentMap[item.parentID] || (this.parentMap[item.parentID] = [])
+      ).push(item);
+    });
+  }
+
+  // 获取顶层原始数据
+  getRoots(): TreeNode[] {
+    return this.parentMap[this.rootID] || [];
+  }
+
+  // 生成按照树结构排好序的数据
+  private genData(): void {
+    // genData时不需要排序,因为rawData已经排好序
+    const roots = this.getRoots();
+    const pushNodesToData = (nodes: TreeNode[]): void => {
+      nodes.forEach(node => {
+        this.data.push(node);
+        const children = this.parentMap[node.ID];
+        if (children && children.length) {
+          pushNodesToData(children);
+        }
+      });
+    };
+    pushNodesToData(roots);
+  }
+
+  // 重新生成排序序好的数据,不改变原引用
+  private reGenData(): void {
+    this.data.splice(0, this.data.length);
+    this.genData();
+  }
+
+  // 将相关parentMap内的数据、data进行重新排序。新增、删除等操作进行完后,数据需要重新排序
+  reSortData(nodes: TreeNode[]): void {
+    const toSortList = Tree.getParentIDList(nodes);
+    toSortList.forEach(parentID => {
+      if (this.parentMap[parentID] && this.parentMap[parentID].length) {
+        Tree.sort(this.parentMap[parentID]);
+      }
+    });
+    this.reGenData();
+  }
+
+  // 根据parentID对parentMap进行重新排序,并重新排序生成data
+  resortDataByID(parentIDList: string[]): void {
+    parentIDList.forEach(parentID => {
+      if (this.parentMap[parentID] && this.parentMap[parentID].length) {
+        Tree.sort(this.parentMap[parentID]);
+      }
+    });
+    this.reGenData();
+  }
+
+  // 查找ID节点
+  find(ID: string): TreeNode | None {
+    return this.IDMap[ID];
+  }
+
+  // 查找ID节点的父节点
+  findParent(ID: string): TreeNode | None {
+    const node = this.find(ID);
+    if (!node) {
+      return null;
+    }
+    return this.find(node.parentID);
+  }
+
+  // 查找ID节点的下一个节点
+  findNext(ID: string): TreeNode | None {
+    const node = this.find(ID);
+    if (!node) {
+      return null;
+    }
+    const nodes = this.parentMap[node.parentID];
+    const nodeIndex = nodes.indexOf(node);
+    if (nodeIndex < 0) {
+      return null;
+    }
+    return nodes[nodeIndex + 1];
+  }
+
+  // 查找ID节点的上一个节点
+  findPrev(ID: string): TreeNode | None {
+    const node = this.find(ID);
+    if (!node) {
+      return null;
+    }
+    const nodes = this.parentMap[node.parentID];
+    const nodeIndex = nodes.indexOf(node);
+    if (nodeIndex < 0) {
+      return null;
+    }
+    return nodes[nodeIndex - 1];
+  }
+
+  // 查询ID节点的子节点
+  findChilren(ID: string): TreeNode[] {
+    return this.parentMap[ID] || [];
+  }
+
+  updateValue(updateData: UpdateData[]): void {
+    if (updateData.length) {
+      updateData.forEach(updateItem => {
+        const node = this.find(updateItem.ID);
+        if (node) {
+          Object.assign(node, updateItem.update);
+        }
+      });
+    }
+  }
+
+  // 递归获取节点
+  getNodesPosterity(treeNodes: TreeNode[], includeSelf = true): TreeNode[] {
+    const rst: TreeNode[] = [];
+    const pushNodes = (nodes: TreeNode[]): void => {
+      nodes.forEach(node => {
+        if (includeSelf || !treeNodes.includes(node)) {
+          rst.push(node);
+        }
+        const children = this.findChilren(node.ID);
+        if (children.length) {
+          pushNodes(children);
+        }
+      });
+    };
+    pushNodes(treeNodes);
+    return rst;
+  }
+
+  // 节选中点块是否是可操作的节点块,选中一批节点进行升降、上下移的时候可能需要用到这个判断。
+  // 以第一个节点为基准,如果后续的节点深度均大于于于等于第一个节点的深度,且节点间是连续的,则此为可操作节点块
+  isOperable(nodes: TreeNode[]): boolean {
+    const baseDepth = nodes[0].getCtx().depth();
+    for (let i = 0; i < nodes.length; i++) {
+      const node = nodes[i];
+      const depth = node.getCtx().depth();
+      if (depth < baseDepth) {
+        return false;
+      }
+      const nextRowNode = nodes[i + 1];
+      if (
+        nextRowNode &&
+        node.getCtx().row() + 1 !== nextRowNode.getCtx().row()
+      ) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  // 从节点块中获取相同深度的节点
+  sameDepthNodes(nodes: TreeNode[], depth: number | None): TreeNode[] {
+    if (!depth) {
+      depth = nodes[0].getCtx().depth();
+    }
+    return nodes.filter(node => node.getCtx().depth() === depth);
+  }
+
+  // 准备插入节点,插入节点前,计算出需要更新的数据。(可能会直接覆盖插入数据的seq)
+  // 可调用完此方法后,将需要更新、插入的数据提交至数据库,成功响应后调用插入节点更新缓存的方法
+  prepareInsert(rawData: TreeRaw[]): UpdateData[] {
+    const updateData: UpdateData[] = [];
+    const insertParentMap: ParentMap = {};
+    // 将相同父项的插入数据和已存在数据进行合并
+    rawData.forEach(item => {
+      if (typeof item.seq === 'undefined') {
+        item.seq = this.seqStartIndex;
+      }
+      (
+        insertParentMap[item.parentID] || (insertParentMap[item.parentID] = [])
+      ).push(item);
+    });
+    Object.entries(insertParentMap).forEach(([parentID, insertItems]) => {
+      const items = this.parentMap[parentID];
+      if (items) {
+        insertParentMap[parentID].push(...items);
+      }
+      // 重新排序
+      const combineItems = insertParentMap[parentID];
+      Tree.sort(combineItems);
+      combineItems.forEach((item, index) => {
+        // 插入数据重新赋值
+        if (insertItems.includes(item)) {
+          item.seq = index;
+        } else if (item.seq !== index) {
+          // 需要更新的原数据
+          updateData.push({
+            ID: item.ID,
+            update: {
+              seq: index,
+            },
+          });
+        }
+      });
+    });
+    return updateData;
+  }
+
+  // 插入节点数据
+  insert(items: TreeRaw[], updateData: UpdateData[] = []): TreeNode[] {
+    // 更新需要更新的节点(一般为更新seq)
+    this.updateValue(updateData);
+    // 建立映射、插入数据
+    const nodes = this.genNodeContext(items);
+    this.genMap(nodes);
+    /* nodes.forEach(node => {
+      this.IDMap[node.ID] = node;
+      (
+        this.parentMap[node.parentID] || (this.parentMap[node.parentID] = [])
+      ).push(node);
+    }); */
+    this.rawData.push(...nodes);
+    // 排序
+    this.reSortData(nodes);
+    return this.data;
+  }
+
+  // 准备删除,返回所有需要删除的节点,包括嵌套节点
+  prepareDelete(deleteNodes: TreeNode[]): TreeNode[] {
+    return this.getNodesPosterity(deleteNodes);
+  }
+
+  /**
+   * 删除节点
+   * @param {TreeNode[]} treeNodes - 要删除的节点,不需要包含嵌套节点
+   * @return {TreeNode[]} 返回删除节点后,树的data
+   */
+  delete(treeNodes: TreeNode[]): TreeNode[] {
+    const allDeletedNodes: TreeNode[] = [];
+    // 递归删除节点
+    const deleteNodes = (nodes: TreeNode[]): void => {
+      // 删除映射、删除数据
+      nodes.forEach(node => {
+        allDeletedNodes.push(node);
+        delete this.IDMap[node.ID];
+        delete this.ctxMap[node.ID];
+        const children = this.parentMap[node.ID];
+        delete this.parentMap[node.ID];
+        const nodesInParentMap = this.parentMap[node.parentID];
+        if (nodesInParentMap && nodesInParentMap.length) {
+          const nIndex = nodesInParentMap.indexOf(node);
+          if (nIndex >= 0) {
+            nodesInParentMap.splice(nIndex, 1);
+          }
+        }
+        const index = this.rawData.indexOf(node);
+        if (index >= 0) {
+          this.rawData.splice(index, 1);
+        }
+        if (children && children.length) {
+          deleteNodes(children);
+        }
+      });
+    };
+    deleteNodes(treeNodes);
+    // 排序
+    this.reSortData(allDeletedNodes);
+    return this.data;
+  }
+
+  // IDList 返回所有需要删除的节点,包括嵌套节点
+  prepareDeleteByID(IDList: string[]): TreeNode[] {
+    const deleteNodes: TreeNode[] = [];
+    IDList.forEach(ID => {
+      const node = this.find(ID);
+      if (node) {
+        deleteNodes.push(node);
+      }
+    });
+    return this.prepareDelete(deleteNodes);
+  }
+
+  /**
+   * 根据ID删除节点
+   * @param {string[]} IDList - 要删除的节点的ID列表(不包含嵌套节点ID)
+   * @return {TreeNode[]} - 返回被删除的所有节点
+   */
+  deleteByID(IDList: string[]): TreeNode[] {
+    const deleteNodes: TreeNode[] = [];
+    IDList.forEach(ID => {
+      const node = this.find(ID);
+      if (node) {
+        deleteNodes.push(node);
+      }
+    });
+    this.delete(deleteNodes);
+    return deleteNodes;
+  }
+
+  // 准备上移节点块(连续的兄弟节点),注意节点的seq可能不连号
+  prepareUpMove(nodes: TreeNode[]): UpdateData[] {
+    const updateData: UpdateData[] = [];
+    const firstNode = nodes[0];
+    const firstNodePrev = this.findPrev(firstNode.ID);
+    if (!firstNodePrev) {
+      return [];
+    }
+    let tempSeq = firstNodePrev.seq;
+    nodes.forEach(node => {
+      const orgSeq = node.seq;
+      updateData.push({
+        ID: node.ID,
+        update: { seq: tempSeq },
+      });
+      tempSeq = orgSeq;
+    });
+    updateData.push({
+      ID: firstNodePrev.ID,
+      update: { seq: tempSeq },
+    });
+    return updateData;
+  }
+
+  // 准备下移节点块(连续的兄弟节点),注意节点的seq可能不连号
+  prepareDownMove(nodes: TreeNode[]): UpdateData[] {
+    const updateData: UpdateData[] = [];
+    const lastNode = nodes[nodes.length - 1];
+    const lastNodeNext = this.findNext(lastNode.ID);
+    if (!lastNodeNext) {
+      return [];
+    }
+    let tempSeq = lastNodeNext.seq;
+    for (let i = nodes.length - 1; i >= 0; i--) {
+      const node = nodes[i];
+      const orgSeq = node.seq;
+      updateData.push({
+        ID: node.ID,
+        update: { seq: tempSeq },
+      });
+      tempSeq = orgSeq;
+    }
+    updateData.push({
+      ID: lastNodeNext.ID,
+      update: { seq: tempSeq },
+    });
+    return updateData;
+  }
+
+  // 上下移
+  move(nodes: TreeNode[], updateData: UpdateData[]): void {
+    this.updateValue(updateData);
+    this.reSortData(nodes);
+  }
+
+  // 准备升级节点块(连续的兄弟节点),不维护seq连号
+  prepareUpLevel(nodes: TreeNode[]): UpdateData[] {
+    const updateData: UpdateData[] = [];
+    const firstNode = nodes[0];
+    const lastNode = nodes[nodes.length - 1];
+    const parent = this.findParent(firstNode.ID);
+    if (!parent) {
+      return [];
+    }
+    const baseSeq = parent.seq + 1;
+    nodes.forEach((node, index) => {
+      updateData.push({
+        ID: node.ID,
+        update: { parentID: parent.parentID, seq: baseSeq + index },
+      });
+    });
+    const parentNextBrothers = parent.getCtx().nextBrothers();
+    // 因为seq可能是不连号的,如果上移的最末节点seq,小于下一节点的seq,那就不更新所有下兄弟节点的seq,减少更新的数据量
+    const lastNodeCurSeq = updateData[updateData.length - 1].update.seq;
+    const firstBrohter = parentNextBrothers[0];
+    if (lastNodeCurSeq && lastNodeCurSeq >= firstBrohter.seq) {
+      parentNextBrothers.forEach((node, index) => {
+        updateData.push({
+          ID: node.ID,
+          update: { seq: baseSeq + index + nodes.length },
+        });
+      });
+    }
+    // 最末节点的所有后兄弟节点,成为最末节点的子节点
+    const lastNodeNextBrothers = lastNode.getCtx().nextBrothers();
+    lastNodeNextBrothers.forEach((node, index) => {
+      updateData.push({
+        ID: node.ID,
+        update: { parentID: lastNode.ID, seq: index },
+      });
+    });
+    return updateData;
+  }
+
+  upLevel(nodes: TreeNode[], updateData: UpdateData[]): void {
+    const firstNode = nodes[0];
+    const lastNode = nodes[nodes.length - 1];
+    if (!firstNode.getCtx().canUpLevel()) {
+      return;
+    }
+    const orgParentID = firstNode.parentID;
+    const orgBrothers = this.parentMap[orgParentID];
+    const lastNodeNextBrothers = lastNode.getCtx().nextBrothers();
+    orgBrothers.splice(
+      orgBrothers.indexOf(firstNode),
+      nodes.length + lastNodeNextBrothers.length
+    );
+    (this.parentMap[lastNode.ID] || (this.parentMap[lastNode.ID] = [])).push(
+      ...lastNodeNextBrothers
+    );
+    this.updateValue(updateData);
+    const newParentID = firstNode.parentID;
+    this.parentMap[newParentID].push(...nodes);
+    this.resortDataByID([orgParentID, newParentID, lastNode.ID]);
+  }
+
+  // 准备降级节点块(连续的兄弟节点),不维护seq连号
+  prepareDownLevel(nodes: TreeNode[]): UpdateData[] {
+    const updateData: UpdateData[] = [];
+    const firstNode = nodes[0];
+    const prevNode = this.findPrev(firstNode.ID);
+    if (!prevNode) {
+      return [];
+    }
+    // 节点块成为前节点的子节点
+    const prevNodeLastChild = prevNode.getCtx().lastChild();
+    const baseSeq = prevNodeLastChild
+      ? prevNodeLastChild.seq + 1
+      : this.seqStartIndex;
+    nodes.forEach((node, index) => {
+      updateData.push({
+        ID: node.ID,
+        update: { parentID: prevNode.ID, seq: baseSeq + index },
+      });
+    });
+    return updateData;
+  }
+
+  downLevel(nodes: TreeNode[], updateData: UpdateData[]): void {
+    const firstNode = nodes[0];
+    if (!firstNode.getCtx().canDownLevel()) {
+      return;
+    }
+    const prevNode = this.findPrev(firstNode.ID);
+    if (!prevNode) {
+      return;
+    }
+    const orgBrothers = this.parentMap[firstNode.parentID];
+    orgBrothers.splice(orgBrothers.indexOf(firstNode), nodes.length);
+    (this.parentMap[prevNode.ID] || (this.parentMap[prevNode.ID] = [])).push(
+      ...nodes
+    );
+    this.updateValue(updateData);
+  }
+}

+ 22 - 22
tree/tests/nodeCtx.ts

@@ -1,5 +1,5 @@
 import { expect } from 'chai';
-import { Tree, TreeNode, TreeRaw } from '../src';
+import { Tree, TreeNode, TreeRaw } from '../src/tree';
 
 function getIDList(nodes: TreeNode[]): string[] {
   return nodes.map(node => node.ID);
@@ -23,7 +23,7 @@ describe('NodeCtx', () => {
   it('row', () => {
     const node = tree.find('4');
     if (node) {
-      const row = node.ctx.row();
+      const row = node.getCtx().row();
       expect(row).to.equal(7);
     }
   });
@@ -31,12 +31,12 @@ describe('NodeCtx', () => {
   it('depth', () => {
     const node = tree.find('1');
     if (node) {
-      const depth = node.ctx.depth();
+      const depth = node.getCtx().depth();
       expect(depth).to.equal(0);
     }
     const deepNode = tree.find('8');
     if (deepNode) {
-      const depth = deepNode.ctx.depth();
+      const depth = deepNode.getCtx().depth();
       expect(depth).to.equal(2);
     }
   });
@@ -44,13 +44,13 @@ describe('NodeCtx', () => {
   it('visible', () => {
     const node = tree.find('1');
     if (node) {
-      node.ctx.expanded = true;
+      node.getCtx().expanded = true;
       const testNode = tree.find('8');
       if (testNode) {
-        const visibleA = testNode.ctx.visible();
+        const visibleA = testNode.getCtx().visible();
         expect(visibleA).to.equal(false);
-        node.ctx.expanded = false;
-        const visibleB = testNode.ctx.visible();
+        node.getCtx().expanded = false;
+        const visibleB = testNode.getCtx().visible();
         expect(visibleB).to.equal(true);
       }
     }
@@ -59,7 +59,7 @@ describe('NodeCtx', () => {
   it('parent', () => {
     const node = tree.find('6');
     if (node) {
-      const parent = node.ctx.parent();
+      const parent = node.getCtx().parent();
       expect(parent).to.have.property('ID', '1');
     }
   });
@@ -67,7 +67,7 @@ describe('NodeCtx', () => {
   it('parent', () => {
     const node = tree.find('6');
     if (node) {
-      const parent = node.ctx.parent();
+      const parent = node.getCtx().parent();
       expect(parent).to.have.property('ID', '1');
     }
   });
@@ -75,7 +75,7 @@ describe('NodeCtx', () => {
   it('next', () => {
     const node = tree.find('1');
     if (node) {
-      const next = node.ctx.next();
+      const next = node.getCtx().next();
       expect(next).to.have.property('ID', '3');
     }
   });
@@ -83,7 +83,7 @@ describe('NodeCtx', () => {
   it('prev', () => {
     const node = tree.find('3');
     if (node) {
-      const prev = node.ctx.prev();
+      const prev = node.getCtx().prev();
       expect(prev).to.have.property('ID', '1');
     }
   });
@@ -91,7 +91,7 @@ describe('NodeCtx', () => {
   it('children', () => {
     const node = tree.find('1');
     if (node) {
-      const children = node.ctx.chilren();
+      const children = node.getCtx().chilren();
       const IDList = getIDList(children);
       expect(IDList).to.have.ordered.members(['7', '6']);
     }
@@ -100,7 +100,7 @@ describe('NodeCtx', () => {
   it('firstChild', () => {
     const node = tree.find('1');
     if (node) {
-      const firstChild = node.ctx.firstChild();
+      const firstChild = node.getCtx().firstChild();
       expect(firstChild).to.have.property('ID', '7');
     }
   });
@@ -108,7 +108,7 @@ describe('NodeCtx', () => {
   it('lastChild', () => {
     const node = tree.find('1');
     if (node) {
-      const lastChild = node.ctx.lastChild();
+      const lastChild = node.getCtx().lastChild();
       expect(lastChild).to.have.property('ID', '6');
     }
   });
@@ -116,7 +116,7 @@ describe('NodeCtx', () => {
   it('posterity', () => {
     const node = tree.find('1');
     if (node) {
-      const posterity = node.ctx.posterity();
+      const posterity = node.getCtx().posterity();
       const IDList = getIDList(posterity);
       expect(IDList).to.have.ordered.members(['7', '8', '6']);
     }
@@ -125,7 +125,7 @@ describe('NodeCtx', () => {
   it('progenitor', () => {
     const node = tree.find('8');
     if (node) {
-      const progenitor = node.ctx.progenitor();
+      const progenitor = node.getCtx().progenitor();
       if (progenitor) {
         expect(progenitor).to.have.property('ID', '1');
       }
@@ -135,7 +135,7 @@ describe('NodeCtx', () => {
   it('ancestor', () => {
     const node = tree.find('8');
     if (node) {
-      const ancestor = node.ctx.ancestor();
+      const ancestor = node.getCtx().ancestor();
       const IDList = getIDList(ancestor);
       expect(IDList).to.have.ordered.members(['7', '1']);
     }
@@ -144,11 +144,11 @@ describe('NodeCtx', () => {
   it('brothers', () => {
     const node = tree.find('6');
     if (node) {
-      const brothers = node.ctx.brothers();
+      const brothers = node.getCtx().brothers();
       const IDList = getIDList(brothers);
       expect(IDList).to.have.ordered.members(['7', '6']);
 
-      const excludeBrothers = node.ctx.brothers(false);
+      const excludeBrothers = node.getCtx().brothers(false);
       const exIDList = getIDList(excludeBrothers);
       expect(exIDList).to.have.ordered.members(['7']);
     }
@@ -157,7 +157,7 @@ describe('NodeCtx', () => {
   it('nextBrothers', () => {
     const node = tree.find('2');
     if (node) {
-      const nextBrothers = node.ctx.nextBrothers();
+      const nextBrothers = node.getCtx().nextBrothers();
       const IDList = getIDList(nextBrothers);
       expect(IDList).to.have.ordered.members(['4', '5']);
     }
@@ -166,7 +166,7 @@ describe('NodeCtx', () => {
   it('prevBrothers', () => {
     const node = tree.find('2');
     if (node) {
-      const prevBrothers = node.ctx.prevBrothers();
+      const prevBrothers = node.getCtx().prevBrothers();
       const IDList = getIDList(prevBrothers);
       expect(IDList).to.have.ordered.members(['1', '3']);
     }

+ 1 - 1
tree/tests/tree.ts

@@ -1,6 +1,6 @@
 import { expect } from 'chai';
 import cloneDeep from 'lodash/cloneDeep';
-import { Tree, TreeNode, TreeRaw } from '../src';
+import { Tree, TreeNode, TreeRaw } from '../src/tree';
 
 function getIDList(nodes: TreeNode[]): string[] {
   return nodes.map(node => node.ID);