import { ArrowDownOutlined, ArrowUpOutlined, CloseOutlined } from "@ant-design/icons"; import G6, { G6GraphEvent, Item, TreeGraph } from "@antv/g6"; import { dialog } from "@electron/remote"; import { useSize } from "ahooks"; import { Button, Dropdown, Flex, FlexProps, Input, InputRef, MenuProps } from "antd"; import { clipboard } from "electron"; import * as fs from "fs"; import React, { FC, KeyboardEvent, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { FiDelete } from "react-icons/fi"; import { IoMdReturnLeft } from "react-icons/io"; import { RiFocus3Line } from "react-icons/ri"; import { VscCaseSensitive } from "react-icons/vsc"; import { mergeRefs } from "react-merge-refs"; import { useDebounceCallback } from "usehooks-ts"; import { useShallow } from "zustand/react/shallow"; import { EditEvent, EditNode, EditorStore, EditTree, useWorkspace, } from "../contexts/workspace-context"; import { isExprType, NodeModel, TreeGraphData, TreeModel } from "../misc/b3type"; import * as b3util from "../misc/b3util"; import { message } from "../misc/hooks"; import i18n from "../misc/i18n"; import { Hotkey, isMacos, useKeyDown } from "../misc/keys"; import Path from "../misc/path"; import { mergeClassNames, readJson } from "../misc/util"; import "./register-node"; import { calcTreeDataSize } from "./register-node"; export interface EditorProps extends React.HTMLAttributes { data: EditorStore; onUpdate: () => void; } interface FilterOption { results: string[]; index: number; filterStr: string; filterCase: boolean; filterFocus: boolean; filterType: "content" | "id"; placeholder: string; isFocused: boolean; } const createTreeData = (node: NodeModel, parent?: string) => { return b3util.createTreeData(node, parent, calcTreeDataSize); }; const refreshTreeData = (data: TreeGraphData) => { data.size = calcTreeDataSize(data); if (data.children) { for (const child of data.children) { refreshTreeData(child); } } }; const isSubtreeUpdated = (data: TreeGraphData) => { if (data.path) { try { const subtreePath = useWorkspace.getState().workdir + "/" + data.path; if (fs.statSync(subtreePath).mtimeMs !== data.lastModified) { return true; } } catch (error) { return true; } } if (data.children) { for (const child of data.children) { if (isSubtreeUpdated(child)) { return true; } } } return false; }; const createMenu = () => { const t = i18n.t; const MenuItem: FC = (itemProps) => { return ( ); }; const arr: MenuProps["items"] = [ { label: (
{t("copy")}
{isMacos ? "⌘ C" : "Ctrl+C"}
), key: "copy", }, { label: (
{t("paste")}
{isMacos ? "⌘ V" : "Ctrl+V"}
), key: "paste", }, { label: (
{t("replace")}
{isMacos ? "⇧ ⌘ V" : "Ctrl+Shift+V"}
), key: "replace", }, { label: (
{t("insertNode")}
{isMacos ? : "Enter"}
), key: "insert", }, { label: (
{t("deleteNode")}
{isMacos ? : "Backspace"}
), key: "delete", }, { label: (
{t("editSubtree")}
), key: "editSubtree", }, { label: (
{t("saveAsSubtree")}
), key: "saveAsSubtree", }, ]; return arr; }; export const Editor: FC = ({ onUpdate: updateState, data: editor, ...props }) => { const workspace = useWorkspace( useShallow((state) => ({ editing: state.editing, onEditingNode: state.onEditingNode, onEditingTree: state.onEditingTree, open: state.open, workdir: state.workdir, relative: state.relative, updateFileMeta: state.updateFileMeta, nodeDefs: state.nodeDefs, })) ); const searchInputRef = useRef(null); const graphRef = useRef(null); const sizeRef = useRef(null); const editorSize = useSize(sizeRef); const { t } = useTranslation(); const menuItems = useMemo(() => createMenu(), [t]); const [showingSearch, setShowingSearch] = useState(false); const [filterOption, setFilterOption] = useState({ results: [], index: 0, filterStr: "", filterCase: false, filterFocus: true, filterType: "content", placeholder: "", isFocused: false, }); const onSearchChange = (option: FilterOption) => { option.results.length = 0; editor.searchingText = option.filterStr; filterNodes(option, editor.data); setFilterOption({ ...option, }); if (option.results.length > 0) { const idx = option.index < option.results.length ? option.index : 0; recursiveOpenCollapsed(option.results[idx]); editor.graph.focusItem(option.results[idx]); selectNode(option.results[idx]); } else { selectNode(null); } editor.graph.findAll("node", () => true).forEach((item) => item.draw()); }; const updateSearchState = () => { const option = { ...filterOption }; option.results.length = 0; filterNodes(option, editor.data); setFilterOption({ ...option, }); editor.graph.findAll("node", () => true).forEach((item) => item.draw()); }; const onDebounceSearchChange = useDebounceCallback(onSearchChange, 200); const onChange = () => { if (!editor.unsave) { editor.unsave = true; updateState(); } }; const findParent = (node: TreeGraphData) => { if (node.parent) { return findDataById(node.parent); } else { return null; } }; const findHightlight = (node: TreeGraphData, highlight: string[], changed?: TreeGraphData[]) => { changed ||= []; if (node.highlightInput || node.highlightOutput || node.highlightArgs) { node.highlightInput = false; node.highlightOutput = false; node.highlightArgs = false; changed.push(node); } if (node.input) { for (const v of node.input) { if (highlight.includes(v)) { node.highlightInput = true; changed.push(node); break; } } } if (node.output) { for (const v of node.output) { if (highlight.includes(v)) { node.highlightOutput = true; changed.push(node); break; } } } node.def.args?.forEach((arg) => { if (isExprType(arg.type)) { const expr: string | string[] = node.args?.[arg.name]; if (typeof expr === "string") { for (const v of b3util.parseExpr(expr)) { if (highlight.includes(v)) { node.highlightArgs = true; changed.push(node); break; } } } else if (expr instanceof Array) { loop: for (const str of expr) { for (const v of b3util.parseExpr(str)) { if (highlight.includes(v)) { node.highlightArgs = true; changed.push(node); break loop; } } } } } }); node.children?.forEach((child: TreeGraphData) => findHightlight(child, highlight, changed)); return changed; }; const findDataById = (id: string) => { const traverse = (node: TreeGraphData): TreeGraphData | undefined => { if (node.id === id) { return node; } if (node.children) { for (const child of node.children) { const result = traverse(child); if (result) { return result; } } } }; return traverse(editor.data) as TreeGraphData; }; const includeString = (content: string | undefined, option: FilterOption) => { if (!content) { return false; } else if (option.filterCase) { return content.includes(option.filterStr); } else { return content.toLowerCase().includes(option.filterStr.toLowerCase()); } }; const filterNodes = (option: FilterOption, node: TreeGraphData | null) => { if (node) { node.highlightGray = option.filterFocus && !!option.filterStr; if (option.filterStr) { let found = false; if (option.filterType === "id") { if (option.filterStr === node.id) { found = true; } } else { if ( includeString(node.name, option) || includeString(node.desc || node.def.desc, option) ) { found = true; } if (!found && node.input) { for (const str of node.input) { if (includeString(str, option)) { found = true; break; } } } if (!found && node.args) { for (const str in node.args) { if (includeString(str, option) || includeString(node.args[str], option)) { found = true; break; } } } if (!found && node.output) { for (const str of node.output) { if (includeString(str, option)) { found = true; break; } } } if (!found && node.path) { if (includeString(node.path, option)) { found = true; } } } if (found) { option.results.push(node.id); node.highlightGray = false; } } node.children?.forEach((child: TreeGraphData) => filterNodes(option, child)); } }; const isAncestor = (ancestor: TreeGraphData, node: TreeGraphData): boolean => { if (ancestor.id === node.parent) { return true; } else if (node.parent) { return isAncestor(ancestor, findDataById(node.parent)); } else { return false; } }; const isSubtreeNode = (node: TreeGraphData | null): boolean => { if (node?.path) { return true; } else if (node?.parent) { return isSubtreeNode(findParent(node)); } else { return false; } }; const findSubtree = (node: TreeGraphData | null): TreeGraphData | null => { if (node?.path) { return node; } else if (node?.parent) { return findSubtree(findParent(node)); } else { return null; } }; const refresh = () => { const rootNode = b3util.createNode(editor.data); editor.modifiedTime = fs.statSync(editor.path).mtimeMs; editor.data = createTreeData(rootNode); editor.autoId = b3util.refreshTreeDataId(editor.data, editor.firstid); editor.graph.changeData(editor.data); editor.graph.layout(); restoreViewport(); }; const reload = () => { const file = readJson(editor.path) as TreeModel; editor.data = createTreeData(file.root); editor.autoId = b3util.refreshTreeDataId(editor.data, editor.firstid); editor.unsave = false; updateState(); if (editor.graph) { editor.graph.changeData(editor.data); editor.graph.layout(); restoreViewport(); } }; const checkSubtree = () => { if (editor.graph) { if (isSubtreeUpdated(editor.data)) { refresh(); pushHistory(); onChange(); } } }; const clearDragDstState = () => { if (editor.dragDstId) { setItemState(editor.dragDstId, "dragRight", false); setItemState(editor.dragDstId, "dragDown", false); setItemState(editor.dragDstId, "dragUp", false); editor.dragDstId = undefined; } }; const clearDragSrcState = () => { if (editor.dragSrcId) { setItemState(editor.dragSrcId, "dragSrc", false); editor.dragSrcId = undefined; } }; const setItemState = (item: string | Item, state: string, value: string | boolean) => { if (typeof item === "string") { item = editor.graph.findById(item); } if (item) { editor.graph.setItemState(item, state, value); } }; const updateNode = (editNode: EditNode) => { const newNode = editNode.data; const data = findDataById(editNode.data.id.toString()); const oldNode = b3util.createNode(data, false); if (b3util.isNodeEqual(oldNode, newNode)) { return; } b3util.copyFromNode(data, newNode); data.def = workspace.nodeDefs.get(data.name)!; data.size = calcTreeDataSize(data); if (oldNode.path !== newNode.path) { refresh(); selectNode(null); selectNode(data.id); } else { const item = editor.graph.findById(data.id); item.draw(); } pushHistory(); onChange(); }; const updateTree = (editTree: EditTree) => { if (editor.desc !== editTree.data.desc || editor.export !== editTree.data.export) { editor.desc = editTree.data.desc || ""; editor.export = editTree.data.export !== false; editor.group = editTree.data.group || []; if (editor.data.firstid !== editTree.data.firstid) { editor.firstid = editTree.data.firstid ?? 1; editor.declare.vars = editTree.data.declare.vars || []; editor.declare.imports = editTree.data.declare.imports || []; refresh(); } onChange(); } }; const selectNode = (id: string | null) => { if (editor.selectedId) { setItemState(editor.selectedId, "selected", false); setItemState(editor.selectedId, "hover", false); } editor.selectedId = id; if (editor.selectedId) { const data = findDataById(editor.selectedId); workspace.onEditingNode({ data: b3util.createNode(data, false), editable: !isSubtreeNode(data), limit_error: !b3util.checkChildrenLimit(data), }); setItemState(editor.selectedId, "selected", true); } else { workspace.onEditingTree(editor); } }; const restoreViewport = () => { if (editor.graphMatrix) { editor.graph.getGroup().setMatrix(editor.graphMatrix); } }; const pushHistory = () => { editor.historyStack.length = ++editor.historyIndex; editor.historyStack.push(b3util.createNode(editor.data)); }; const copyNode = () => { if (editor.selectedId) { const data = findDataById(editor.selectedId); if (data) { const str = JSON.stringify(b3util.createNode(data), null, 2); clipboard.writeText(str); } } }; const pasteNode = () => { if (!editor.selectedId) { message.warning(t("node.noNodeSelected")); return; } if (isSubtreeNode(findDataById(editor.selectedId))) { message.warning(t("node.editSubtreeDenied")); return; } try { const str = clipboard.readText(); if (!str || str === "") { return; } const curNodeData = findDataById(editor.selectedId); const data = createTreeData(JSON.parse(str), editor.selectedId); editor.autoId = b3util.refreshTreeDataId(data, editor.autoId); selectNode(null); curNodeData.children ||= []; curNodeData.children.push(data); refreshItem(curNodeData); updateGrahp(); pushHistory(); onChange(); } catch (error) { message.error(t("node.pasteDataError")); console.log(error); } }; const replaceNode = () => { if (!editor.selectedId) { message.warning(t("node.noNodeSelected")); return; } if (isSubtreeNode(findDataById(editor.selectedId))) { message.warning(t("node.editSubtreeDenied")); return; } try { const str = clipboard.readText(); if (!str || str === "") { return; } const curNodeData = findDataById(editor.selectedId); if (curNodeData.parent) { const parentData = findDataById(curNodeData.parent); const idx = parentData.children!.indexOf(curNodeData); const data = createTreeData(JSON.parse(str), editor.selectedId); editor.autoId = b3util.refreshTreeDataId(data, editor.autoId); parentData.children![idx] = data; refreshItem(parentData); updateGrahp(); } else { editor.data = createTreeData(JSON.parse(str), editor.selectedId); editor.autoId = b3util.refreshTreeDataId(editor.data, editor.firstid); editor.graph.changeData(editor.data); editor.graph.render(); editor.graph.layout(); } selectNode(null); pushHistory(); onChange(); } catch (error) { message.error(t("node.pasteDataError")); console.log(error); } }; const createNode = () => { if (filterOption.isFocused) { return; } if (!editor.selectedId) { message.warning(t("node.noNodeSelected")); return; } if (isSubtreeNode(findDataById(editor.selectedId))) { message.warning(t("node.editSubtreeDenied")); return; } const curNodeData = findDataById(editor.selectedId); const newNodeData: NodeModel = { id: editor.autoId++, name: "unknow", }; curNodeData.children ||= []; curNodeData.children.push(createTreeData(newNodeData, editor.selectedId)); refreshItem(curNodeData); updateGrahp(); pushHistory(); onChange(); }; const deleteNode = () => { if (!editor.selectedId) { return; } if (editor.selectedId === "1") { message.warning(t("node.deleteRootNodeDenied")); return; } const data = findDataById(editor.selectedId); if (isSubtreeNode(data) && !data.path) { message.warning(t("node.editSubtreeDenied")); return; } const parentData = findParent(data)!; parentData.children = parentData.children!.filter((e) => e.id !== editor.selectedId); refreshItem(parentData); selectNode(null); updateGrahp(); pushHistory(); onChange(); }; const refreshItem = (data: TreeGraphData) => { editor.graph.findById(data.id)?.draw(); }; const useStackData = (data: NodeModel) => { editor.data = createTreeData(data); editor.autoId = b3util.refreshTreeDataId(editor.data, editor.firstid); editor.graph.changeData(editor.data); editor.graph.layout(); restoreViewport(); onChange(); }; const updateGrahp = () => { editor.graph.changeData(); editor.graph.layout(); }; const undo = () => { if (editor.historyIndex > 0) { const data = editor.historyStack[--editor.historyIndex]; useStackData(data); updateSearchState(); } }; const redo = () => { if (editor.historyIndex < editor.historyStack.length - 1) { const data = editor.historyStack[++editor.historyIndex]; useStackData(data); updateSearchState(); } }; const rename = (newPath: string) => { editor.path = newPath; editor.size.width = 0; editor.size.height = 0; }; const save = () => { if (editor.unsave || !fs.existsSync(editor.path)) { const path = editor.path; editor.autoId = b3util.refreshTreeDataId(editor.data, editor.firstid); const root = b3util.createFileData(editor.data); const treeModel = { name: Path.basenameWithoutExt(path), desc: editor.desc, firstid: editor.firstid ?? 1, export: editor.export, group: editor.group ?? [], declare: { imports: editor.declare.imports.map((v) => v.path).sort(), vars: editor.declare.vars.sort((a, b) => a.name.localeCompare(b.name)), }, root, } as TreeModel; editor.modifiedTime = Date.now(); fs.writeFileSync(path, JSON.stringify(treeModel, null, 2)); workspace.updateFileMeta(editor); editor.unsave = false; editor.graph.changeData(editor.data); editor.graph.layout(); restoreViewport(); updateSearchState(); updateState(); } }; const editSubtree = () => { if (!editor.selectedId) { message.warning(t("node.noNodeSelected")); return; } const data = findSubtree(findDataById(editor.selectedId)); if (data?.path) { const path = `${workspace.workdir}/${data.path}`; workspace.open(path, Number(editor.selectedId) - Number(data.id) + 1); } }; const saveAsSubtree = () => { if (!editor.selectedId) { message.warning(t("node.noNodeSelected")); return; } if (editor.selectedId === "1") { message.warning(t("node.saveAsSubtree")); return; } setTimeout(() => { let subpath = dialog.showSaveDialogSync({ defaultPath: workspace.workdir, properties: ["showOverwriteConfirmation"], filters: [{ name: "Json", extensions: ["json"] }], }); if (!subpath) { return; } subpath = subpath.replaceAll(Path.sep, "/"); if (subpath.indexOf(workspace.workdir) === -1) { message.warning(t("node.subtreePathError")); return; } const data = findDataById(editor.selectedId!); const subroot = b3util.createFileData(data); const subtreeModel = { name: Path.basename(subpath).slice(0, -5), root: subroot, desc: data.desc, } as TreeModel; fs.writeFileSync(subpath, JSON.stringify(subtreeModel, null, 2)); data.path = workspace.relative(subpath); refresh(); pushHistory(); onChange(); }, 200); }; const destroyGraph = () => { if (editor.graph) { editor.graph.destroy(); editor.graph = undefined!; } }; const createGraph = (ref: React.MutableRefObject) => { editor.graph = new TreeGraph({ container: ref.current!, animate: false, maxZoom: 2, minZoom: 0.4, modes: { default: [ "drag-canvas", "zoom-canvas", "click-select", "hover", { type: "collapse-expand", trigger: "dblclick", onChange: (item, collapsed) => { if (!item) { return; } selectNode(item.getID()); const data = item.getModel(); data.collapsed = collapsed; setItemState(item, "collapsed", data.collapsed as boolean); const icon = data.collapsed ? G6.Marker.expand : G6.Marker.collapse; const marker = item .get("group") .find((ele: any) => ele.get("name") === "collapse-icon"); editor.graph.refresh(); marker.attr("symbol", icon); return true; }, }, ], }, defaultEdge: { type: "cubic-horizontal", size: 2.5, style: { stroke: "#A3B1BF", }, }, defaultNode: { type: "TreeNode", }, layout: { type: "compactBox", direction: "LR", getHGap: () => 60, getWidth: (d: TreeGraphData) => { return 160; }, getHeight: (d: TreeGraphData) => { if (d.size) { return d.size[1]; } else { return 50; } }, }, }); editor.graph.on("viewportchange", (data: any) => { if (data.action === "translate" || data.action === "zoom") { editor.graphMatrix = data.matrix; } }); editor.graph.on("node:click", (e: G6GraphEvent) => { const highlight: string[] = []; if (e.shape.cfg.name === "input-text") { const data = findDataById(e.item.getID()); data.input?.forEach((v) => v && highlight.push(v)); } else if (e.shape.cfg.name === "output-text") { const data = findDataById(e.item.getID()); data.output?.forEach((v) => v && highlight.push(v)); } const changed = findHightlight(editor.data, highlight); if (changed.length > 0) { const refreshHighlight = (node: TreeGraphData) => { const item = editor.graph.findById(node.id); if (highlight.length > 0) { node.highlightGray = !( node.highlightInput || node.highlightOutput || node.highlightArgs ); } else { node.highlightGray = false; } item.draw(); if (node.children) { node.children.forEach(refreshHighlight); } }; refreshHighlight(editor.data); } if (highlight.length === 0 && editor.searchingText) { onSearchChange({ ...filterOption, filterType: "content", filterStr: editor.searchingText, }); } }); editor.graph.on("node:contextmenu", (e: G6GraphEvent) => { selectNode(e.item.getID()); }); editor.graph.on("node:mouseenter", (e: G6GraphEvent) => { if (!e.item.hasState("selected")) { setItemState(e.item, "hover", true); } }); editor.graph.on("node:mouseleave", (e: G6GraphEvent) => { if (!e.item.hasState("selected")) { setItemState(e.item, "hover", false); } }); editor.graph.on("nodeselectchange", (e: G6GraphEvent) => { if (e.target) { selectNode((e.target as unknown as Item).getID()); } else { selectNode(null); } }); editor.graph.on("node:dragstart", (e: G6GraphEvent) => { editor.dragSrcId = e.item.getID(); setItemState(editor.dragSrcId, "dragSrc", true); }); editor.graph.on("node:dragend", (e: G6GraphEvent) => { if (editor.dragSrcId) { setItemState(editor.dragSrcId, "dragSrc", false); editor.dragSrcId = undefined; } }); editor.graph.on("node:dragover", (e: G6GraphEvent) => { const dstNodeId = e.item.getID(); if (dstNodeId === editor.dragSrcId) { return; } clearDragDstState(); const box = e.item.getBBox(); if (e.x > box.minX + box.width * 0.6) { setItemState(dstNodeId, "dragRight", true); } else if (e.y > box.minY + box.height * 0.5) { setItemState(dstNodeId, "dragDown", true); } else { setItemState(dstNodeId, "dragUp", true); } editor.dragDstId = dstNodeId; }); editor.graph.on("node:dragleave", (e: G6GraphEvent) => { clearDragDstState(); }); editor.graph.on("node:drop", (e: G6GraphEvent) => { const dstNode = e.item; let srcNodeId = editor.dragSrcId; let dragDir; if (dstNode.hasState("dragRight")) { dragDir = "dragRight"; } else if (dstNode.hasState("dragDown")) { dragDir = "dragDown"; } else if (dstNode.hasState("dragUp")) { dragDir = "dragUp"; } clearDragSrcState(); clearDragDstState(); if (e.originalEvent instanceof DragEvent) { const dragEvent = e.originalEvent as DragEvent; const exploreFile = dragEvent.dataTransfer?.getData("explore-file"); const exploreNode = dragEvent.dataTransfer?.getData("explore-node"); const dstData = findDataById(dstNode.getID()); if (exploreNode) { const newTreeData: TreeGraphData = createTreeData( { id: editor.autoId++, name: exploreNode, }, dstData.id ); dstData.children ||= []; dstData.children.push(newTreeData); refreshItem(dstData); srcNodeId = newTreeData.id; } else if (exploreFile && exploreFile !== editor.path) { const newTreeData: TreeGraphData = createTreeData( { id: editor.autoId++, name: "unknow", path: workspace.relative(exploreFile), }, dstData.id ); dstData.children ||= []; dstData.children.push(newTreeData); refreshItem(dstData); srcNodeId = newTreeData.id; editor.autoId = b3util.refreshTreeDataId(newTreeData, Number(srcNodeId)); } } if (!srcNodeId) { console.log("no drag src"); return; } if (srcNodeId === dstNode.getID()) { console.log("drop same node"); return; } const srcData = findDataById(srcNodeId); const srcParent = findParent(srcData); const dstData = findDataById(dstNode.getID()); const dstParent = findParent(dstData); if (!srcParent) { console.log("no parent!"); return; } if (isAncestor(srcData, dstData)) { console.log("cannot move to child"); return; } const removeSrc = () => { srcParent.children = srcParent.children!.filter((value) => value.id !== srcData.id); }; if (dragDir === "dragRight") { removeSrc(); if (!dstData.children) { dstData.children = []; } srcData.parent = dstData.id; dstData.children.push(srcData); } else if (dragDir === "dragUp") { if (!dstParent) { return; } removeSrc(); const idx = dstParent.children!.findIndex((value) => value.id === dstData.id); srcData.parent = dstParent.id; dstParent.children!.splice(idx, 0, srcData); } else if (dragDir === "dragDown") { if (!dstParent) { return; } removeSrc(); const idx = dstParent.children!.findIndex((value) => value.id === dstData.id); srcData.parent = dstParent.id; dstParent.children!.splice(idx + 1, 0, srcData); } else { return; } refreshItem(srcParent); refreshItem(dstData); updateGrahp(); pushHistory(); onChange(); }); refreshTreeData(editor.data); editor.graph.data(editor.data); editor.graph.render(); if (editor.graphMatrix) { restoreViewport(); } else { editor.graph.fitCenter(); } }; const keysRef = useRef(null); useKeyDown(Hotkey.Copy, keysRef, () => copyNode()); useKeyDown(Hotkey.Replace, keysRef, () => replaceNode()); useKeyDown(Hotkey.Paste, keysRef, () => pasteNode()); useKeyDown(Hotkey.Undo, keysRef, () => undo()); useKeyDown(Hotkey.Redo, keysRef, () => redo()); useKeyDown([Hotkey.Insert, Hotkey.Enter], keysRef, () => createNode()); useKeyDown([Hotkey.Delete, Hotkey.Backspace], keysRef, () => deleteNode()); editor.dispatch = (event: EditEvent, data: unknown) => { switch (event) { case "copy": { copyNode(); break; } case "paste": { pasteNode(); break; } case "delete": { deleteNode(); break; } case "insert": { createNode(); break; } case "replace": { replaceNode(); break; } case "save": { save(); break; } case "undo": { undo(); break; } case "redo": { redo(); break; } case "refresh": { refresh(); break; } case "reload": { reload(); break; } case "rename": { rename(data as string); break; } case "updateTree": { updateTree(data as EditTree); break; } case "updateNode": { updateNode(data as EditNode); break; } case "searchNode": { searchByType("content"); break; } case "jumpNode": { searchByType("id"); break; } case "editSubtree": { editSubtree(); break; } case "saveAsSubtree": { saveAsSubtree(); break; } } }; // check should rebuild graph useEffect(() => { if (!editorSize || (editorSize.width === 0 && editorSize.height === 0)) { return; } if (editor.size.width !== editorSize.width || editor.size.height !== editorSize.height) { destroyGraph(); } if (!editor.graph) { createGraph(graphRef); editor.size.width = editorSize.width; editor.size.height = editorSize.height; } if (typeof editor.editNode === "number") { const id = editor.editNode.toString(); selectNode(null); setTimeout(() => { editor.graph.focusItem(id); selectNode(id); }, 100); } }); // check should subtree useEffect(() => { if (workspace.editing === editor) { checkSubtree(); } }, [workspace.editing]); // check should repaint node useEffect(() => { if (editor.graph) { editor.graph.changeData(editor.data); editor.graph.layout(); restoreViewport(); } }, [t]); const nextResult = () => { const { results, index } = filterOption; if (results.length > 0) { const idx = (index + 1) % results.length; recursiveOpenCollapsed(results[idx]); editor.graph.focusItem(results[idx]); selectNode(results[idx]); setFilterOption({ ...filterOption, index: idx }); } }; const prevResult = () => { const { results, index } = filterOption; if (results.length > 0) { const idx = (index + results.length - 1) % results.length; recursiveOpenCollapsed(results[idx]); editor.graph.focusItem(results[idx]); selectNode(results[idx]); setFilterOption({ ...filterOption, index: idx }); } }; const searchByType = (type: FilterOption["filterType"]) => { let placeholder = ""; const filterType = type; // todo multiple parameter format judgment switch (type) { case "id": placeholder = t("jumpNode"); break; default: placeholder = t("searchNode"); break; } if (!showingSearch) { setFilterOption({ ...filterOption, placeholder, filterType }); setShowingSearch(true); return; } if (filterOption.filterType === type) { return searchInputRef.current?.focus(); } setShowingSearch(false); setTimeout(() => { setShowingSearch(true); setFilterOption({ ...filterOption, placeholder, filterType }); searchInputRef.current?.focus(); }, 50); }; const handleKeyDown = (e: KeyboardEvent) => { if (e.code === Hotkey.Enter) { nextResult(); } else if ((e.ctrlKey || e.metaKey) && e.code === "KeyF") { searchByType("content"); } else if ((e.ctrlKey || e.metaKey) && e.code === "KeyG") { searchByType("id"); } e.stopPropagation(); }; const recursiveOpenCollapsed = (id: string) => { const recursiveFun = (searchId: string) => { const data = findDataById(searchId); if (searchId !== id && data.collapsed) { data.collapsed = undefined; } if (data.parent) { recursiveFun(data.parent); } }; recursiveFun(id); updateGrahp(); }; return (
{showingSearch && ( onDebounceSearchChange({ ...filterOption, filterStr: e.currentTarget.value, index: 0, }) } onFocus={() => setFilterOption({ ...filterOption, isFocused: true })} onBlur={() => setFilterOption({ ...filterOption, isFocused: false })} onKeyDownCapture={handleKeyDown} suffix={ {filterOption.filterType !== "id" && (