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;