DivBucket / src / store / reducers / treeReducer.js
treeReducer.js
Raw
import { createSlice } from "@reduxjs/toolkit";

const createCopy = (id, state) => {
  const uid = Math.floor(Math.random() * 1000000);
  state.styleMap[uid] = state.styleMap[id];
  state.dataMap[uid] = state.dataMap[id];
  state.tree[uid] = state.tree[id].map((_id) => createCopy(_id, state));
  return uid;
};

const getParent = (tree, start, id) => {
  if (tree[start].includes(id)) return start;
  for (const node of tree[start]) {
    const result = getParent(tree, node, id);
    if (result) return result;
  }
  return null;
};

const isRelation = ({ tree, parent, child }) => {
  if (!tree[parent]) return false;
  if (tree[parent].includes(child)) return true;
  for (const _child of tree[parent]) {
    if (isRelation({ tree, parent: _child, child })) return true;
  }
  return false;
};

const treeSlice = createSlice({
  name: "tree",
  initialState: {
    tree: {},
    activeNodeId: 936385,
    activeTab: 936385,
    styleMap: {},
    dataMap: {},
    bgContentRect: {
      width: 0,
      height: 0,
      top: 0,
      bottom: 0,
      left: 0,
      right: 0,
    },
    clipboard: {
      cut: null,
      copy: null,
    },
  },
  reducers: {
    addNode: (state, { payload }) => {
      if (state.dataMap[payload.parent].unit) return;
      state.tree[payload.parent].push(Number(payload.child));
      state.tree[payload.child] = state.tree[payload.child] || [];
    },
    addTemplate: (state, { payload }) => {
      state.tree = { ...state.tree, ...payload.tree };
      state.dataMap = { ...state.dataMap, ...payload.dataMap };
      state.styleMap = { ...state.styleMap, ...payload.styleMap };
    },
    deleteNode: (state, { payload }) => {
      state.activeNodeId = getParent(state.tree, "tabs", state.activeNodeId);
      const deleteWork = (id) => {
        state.tree[id].map((child) => deleteWork(child));
        delete state.dataMap[id];
        delete state.styleMap[id];
        const { [id]: ___, ...newTree } = state.tree;
        state.tree = newTree;
      };
      deleteWork(payload.id);
      treeSlice.caseReducers.deleteFromParent(state, { payload });
    },
    deleteFromParent: (state, { payload }) => {
      if (!payload.id) return;
      state.tree = Object.keys(state.tree).reduce((acc, key) => {
        acc[key] = state.tree[key].filter((_id) => _id !== Number(payload.id));
        return acc;
      }, {});
    },
    updateActiveNode: (state, { payload }) => {
      state.activeNodeId = payload.id;
    },
    updateActiveTab: (state, { payload }) => {
      state.activeTab = payload.tab;
      state.activeNodeId = payload.tab;
      if (!state.dataMap[payload.tab].open)
        state.dataMap[payload.tab].open = true;
    },
    updateTabOpenStatus: (state, { payload }) => {
      state.dataMap[payload.tab].open = payload.open;
      if (payload.tab !== state.activeTab) return;
      state.activeTab =
        state.tree.tabs.filter((tab) => state.dataMap[tab].open)[0] || null;
      state.activeNodeId = state.activeTab;
    },
    updateStyleMap: (state, { payload }) => {
      state.styleMap[payload.id] = payload.style;
    },
    updateDataMap: (state, { payload }) => {
      state.dataMap[payload.id] = payload.data;
    },
    updateRootWidth: (state, { payload }) => {
      state.styleMap[state.activeTab].width = payload.width;
    },
    updateBgContentRect: (state, { payload }) => {
      state.bgContentRect = payload.bgContentRect;
    },
    updateClipboard: (state, { payload }) => {
      if (state.clipboard.cut) {
        treeSlice.caseReducers.deleteNode(state, {
          payload: { id: state.clipboard.cut },
        });
        state.clipboard.cut = null;
      }
      if (state.tree.tabs.includes(payload.cut || payload.copy)) return;
      state.clipboard = payload;
    },
    paste: (state) => {
      const parent = state.activeNodeId;
      if (state.dataMap[parent].unit) return;
      if (state.clipboard.cut) {
        treeSlice.caseReducers.addNode(state, {
          payload: { parent, child: state.clipboard.cut },
        });
        state.activeNodeId = state.clipboard.cut;
        state.clipboard.copy = state.clipboard.cut;
        state.clipboard.cut = null;
      } else if (state.clipboard.copy) {
        const newChild = createCopy(state.clipboard.copy, state);
        state.tree[parent].push(newChild);
        state.activeNodeId = newChild;
      }
    },
    duplicate: (state) => {
      if (state.tree.tabs.includes(state.activeNodeId)) return;
      const duplicate = createCopy(state.activeNodeId, state);
      treeSlice.caseReducers.splice(state, {
        payload: { referenceNode: state.activeNodeId, pos: 1, node: duplicate },
      });
      state.activeNodeId = duplicate;
    },
    revealParent: (state) => {
      state.activeNodeId = getParent(
        state.tree,
        state.activeTab,
        state.activeNodeId
      );
    },
    splice: (state, { payload }) => {
      if (state.tree.tabs.includes(Number(payload.referenceNode))) {
        state.tree[payload.referenceNode].splice(0, 0, Number(payload.node));
      } else {
        const parent =
          payload.parent ||
          getParent(state.tree, "tabs", Number(payload.referenceNode));
        const index = state.tree[parent].indexOf(Number(payload.referenceNode));
        state.tree[parent].splice(index + payload.pos, 0, Number(payload.node));
      }
    },
    moveItem: (state, { payload }) => {
      const { node, referenceNode, pos } = payload;
      if (payload.pos === -1 && state.dataMap[payload.referenceNode].unit)
        return;
      if (
        !state.tree.tabs.includes(referenceNode) &&
        isRelation({
          tree: state.tree,
          parent: Number(node),
          child: Number(referenceNode),
        })
      )
        return;
      treeSlice.caseReducers.deleteFromParent(state, { payload: { id: node } });
      if (pos === -1)
        treeSlice.caseReducers.addNode(state, {
          payload: { parent: referenceNode, child: node },
        });
      else
        treeSlice.caseReducers.splice(state, {
          payload: { ...payload },
        });
      state.activeNodeId = Number(node);
    },
    cut: (state) => {
      const cutNode = state.activeNodeId;
      const parent = getParent(state.tree, "tabs", cutNode);
      if (state.tree.tabs.includes(cutNode)) return;
      treeSlice.caseReducers.updateClipboard(state, {
        payload: { cut: cutNode, copy: null },
      });
      treeSlice.caseReducers.deleteFromParent(state, {
        payload: { id: cutNode },
      });
      state.activeNodeId = parent || state.activeTab;
    },
    copy: (state) => {
      if (state.tree.tabs.includes(state.activeNodeId)) return;
      treeSlice.caseReducers.updateClipboard(state, {
        payload: { copy: state.activeNodeId, cut: null },
      });
    },
  },
});

export const {
  updateActiveNode,
  updateHoverNode,
  addNode,
  updateStyleMap,
  updateDataMap,
  deleteNode,
  deleteFromParent,
  updateRootWidth,
  updateBgContentRect,
  updateClipboard,
  paste,
  duplicate,
  revealParent,
  splice,
  moveItem,
  addTemplate,
  updateActiveTab,
  updateTabOpenStatus,
  cut,
  copy,
} = treeSlice.actions;
export default treeSlice.reducer;