|
@@ -0,0 +1,251 @@
|
|
|
+import React from "react";
|
|
|
+import { createRoot, Root } from "react-dom/client";
|
|
|
+import { Dropdown } from "antd";
|
|
|
+import { Graph, ToolsView, EdgeView } from "@antv/x6";
|
|
|
+import type { MenuProps } from "antd";
|
|
|
+import { menuHander } from "./contentMenuHander";
|
|
|
+
|
|
|
+export class ContextMenuTool extends ToolsView.ToolItem<
|
|
|
+ EdgeView,
|
|
|
+ ContextMenuToolOptions
|
|
|
+> {
|
|
|
+ private timer: number | null = null;
|
|
|
+ private root: Root | null = null;
|
|
|
+
|
|
|
+ private toggleContextMenu(visible: boolean, e?: MouseEvent) {
|
|
|
+ this.root?.unmount();
|
|
|
+ document.removeEventListener("mousedown", this.onMouseDown);
|
|
|
+ if (visible && e) {
|
|
|
+ const { sx, sy } = this.graph.scale();
|
|
|
+ let offsetX = e.offsetX * sx,
|
|
|
+ offsetY = e.offsetY * sy;
|
|
|
+ // 非页面节点需要获取当前节点位置 + 节点本身偏移位置
|
|
|
+ if (this.cell.isNode() && !this.cell.getData()?.isPage) {
|
|
|
+ const { x, y } = this.cell.getPosition();
|
|
|
+ offsetX = x * sx + e.offsetX * sx;
|
|
|
+ offsetY = y * sy + e.offsetY * sy;
|
|
|
+ }
|
|
|
+ this.root = createRoot(this.container);
|
|
|
+ const items = this.options.menu?.map((item: any) => {
|
|
|
+ if (!item) return item;
|
|
|
+ return {
|
|
|
+ ...item,
|
|
|
+ onClick: () => {
|
|
|
+ setTimeout(() => {
|
|
|
+ item?.onClick?.call(this, this, e);
|
|
|
+ }, 200);
|
|
|
+ },
|
|
|
+ };
|
|
|
+ });
|
|
|
+ this.root.render(
|
|
|
+ <Dropdown
|
|
|
+ open={true}
|
|
|
+ trigger={["contextMenu"]}
|
|
|
+ menu={{ items }}
|
|
|
+ align={{ offset: [offsetX, offsetY] }}
|
|
|
+ >
|
|
|
+ <span />
|
|
|
+ </Dropdown>
|
|
|
+ );
|
|
|
+ document.addEventListener("mousedown", this.onMouseDown);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private onMouseDown = () => {
|
|
|
+ this.timer = window.setTimeout(() => {
|
|
|
+ this.toggleContextMenu(false);
|
|
|
+ }, 200);
|
|
|
+ };
|
|
|
+
|
|
|
+ private onContextMenu({ e }: { e: MouseEvent }) {
|
|
|
+ if (this.timer) {
|
|
|
+ clearTimeout(this.timer);
|
|
|
+ this.timer = 0;
|
|
|
+ }
|
|
|
+ console.log(e, this);
|
|
|
+ this.toggleContextMenu(true, e);
|
|
|
+ }
|
|
|
+
|
|
|
+ delegateEvents() {
|
|
|
+ this.cellView.on("cell:contextmenu", this.onContextMenu, this);
|
|
|
+ this.graph.on("blank:contextmenu", this.onContextMenu, this);
|
|
|
+ return super.delegateEvents();
|
|
|
+ }
|
|
|
+
|
|
|
+ protected onRemove() {
|
|
|
+ this.cellView.off("cell:contextmenu", this.onContextMenu, this);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+ContextMenuTool.config({
|
|
|
+ tagName: "div",
|
|
|
+ isSVGElement: false,
|
|
|
+});
|
|
|
+
|
|
|
+export interface ContextMenuToolOptions extends ToolsView.ToolItem.Options {
|
|
|
+ menu: MenuProps["items"];
|
|
|
+}
|
|
|
+
|
|
|
+Graph.registerEdgeTool("contextmenu", ContextMenuTool, true);
|
|
|
+Graph.registerNodeTool("contextmenu", ContextMenuTool, true);
|
|
|
+
|
|
|
+interface MenuItem {
|
|
|
+ key?: string;
|
|
|
+ label?: string;
|
|
|
+ type?: "divider";
|
|
|
+ icon?: string;
|
|
|
+ fastKey?: string;
|
|
|
+ handler?: (tool: ContextMenuTool, e: MouseEvent) => void;
|
|
|
+}
|
|
|
+
|
|
|
+// [复制、剪切、粘贴、复用、删除、设为默认样式],[置于顶层、置于底层、上移一层、下移一层],[锁定],[全选],[导出所选图形为PNG、复制所选图形为图片]
|
|
|
+const commonMenuData: MenuItem[] = [
|
|
|
+ { key: "copy", label: "复制", fastKey: "Ctrl+C", handler: menuHander.copy },
|
|
|
+ { key: "cut", label: "剪切", fastKey: "Ctrl+X", handler: menuHander.cut },
|
|
|
+ { key: "paste", label: "粘贴", fastKey: "Ctrl+V", handler: menuHander.paste },
|
|
|
+ {
|
|
|
+ key: "duplicate",
|
|
|
+ label: "复用",
|
|
|
+ fastKey: "Ctrl+D",
|
|
|
+ handler: menuHander.duplicate,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: "delete",
|
|
|
+ label: "删除",
|
|
|
+ fastKey: "Delete/Backspace",
|
|
|
+ handler: menuHander.delete,
|
|
|
+ },
|
|
|
+ { key: "setDefaultStyle", label: "设为默认样式" },
|
|
|
+ { key: "resetDefaultStyle", label: "恢复默认样式" },
|
|
|
+ { type: "divider" },
|
|
|
+ {
|
|
|
+ key: "top",
|
|
|
+ label: "置于顶层",
|
|
|
+ fastKey: "Ctrl+]",
|
|
|
+ icon: "icon-zhiding1",
|
|
|
+ handler: menuHander.top,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: "bottom",
|
|
|
+ label: "置于底层",
|
|
|
+ fastKey: "Ctrl+[",
|
|
|
+ icon: "icon-zhidi1",
|
|
|
+ handler: menuHander.bottom,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: "up",
|
|
|
+ label: "上移一层",
|
|
|
+ fastKey: "Ctrl+Shift+]",
|
|
|
+ icon: "icon-shangyiyiceng1",
|
|
|
+ handler: menuHander.up,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: "down",
|
|
|
+ label: "下移一层",
|
|
|
+ fastKey: "Ctrl+Shift+[",
|
|
|
+ icon: "icon-xiayiyiceng1",
|
|
|
+ handler: menuHander.down,
|
|
|
+ },
|
|
|
+ { type: "divider" },
|
|
|
+ { key: "lock", label: "锁定", fastKey: "Ctrl+L", icon: "icon-lock" },
|
|
|
+ { type: "divider" },
|
|
|
+ { key: "selectAll", label: "全选", fastKey: "A" },
|
|
|
+ { type: "divider" },
|
|
|
+ { key: "export", label: "导出所选图形为PNG", icon: "icon-tupian" },
|
|
|
+ { key: "copyAsImage", label: "复制所选图形为图片" },
|
|
|
+];
|
|
|
+
|
|
|
+const edgeMenuData: MenuItem[] = [...commonMenuData];
|
|
|
+
|
|
|
+const nodeMenuData: MenuItem[] = [...commonMenuData];
|
|
|
+
|
|
|
+const lockMenuData: MenuItem[] = [
|
|
|
+ { key: "paste", label: "粘贴", fastKey: "Ctrl+V" },
|
|
|
+ { type: "divider" },
|
|
|
+ { key: "unlock", label: "解锁", fastKey: "Ctrl+Shift+L", icon: "icon-lock" },
|
|
|
+ { type: "divider" },
|
|
|
+ { key: "selectAll", label: "全选", fastKey: "A" },
|
|
|
+];
|
|
|
+
|
|
|
+const pageMenuData: MenuItem[] = [
|
|
|
+ {
|
|
|
+ key: "paste",
|
|
|
+ label: "粘贴",
|
|
|
+ fastKey: "Ctrl+V",
|
|
|
+ handler: menuHander.paste,
|
|
|
+ },
|
|
|
+ { key: "1", type: "divider" },
|
|
|
+ {
|
|
|
+ key: "zoomIn",
|
|
|
+ label: "放大",
|
|
|
+ fastKey: "Ctrl+(+)",
|
|
|
+ icon: "icon-fangda",
|
|
|
+ handler: menuHander.zoomIn,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: "zoomOut",
|
|
|
+ label: "缩小",
|
|
|
+ fastKey: "Ctrl+(-)",
|
|
|
+ icon: "icon-suoxiao",
|
|
|
+ handler: menuHander.zoomOut,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: "resetView",
|
|
|
+ label: "重置视图缩放",
|
|
|
+ handler: menuHander.resetView,
|
|
|
+ },
|
|
|
+ { key: "2", type: "divider" },
|
|
|
+ {
|
|
|
+ key: "selectAll",
|
|
|
+ label: "全选",
|
|
|
+ fastKey: "A",
|
|
|
+ handler: menuHander.selectAll,
|
|
|
+ },
|
|
|
+ { key: "3", type: "divider" },
|
|
|
+ {
|
|
|
+ key: "createLine",
|
|
|
+ label: "创建连线",
|
|
|
+ fastKey: "L",
|
|
|
+ handler: menuHander.createLine,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: "insertImage",
|
|
|
+ label: "插入图片",
|
|
|
+ fastKey: "I",
|
|
|
+ handler: menuHander.insertImage,
|
|
|
+ },
|
|
|
+];
|
|
|
+
|
|
|
+const LabelComponent = ({ item }: { item: MenuItem }) => {
|
|
|
+ return (
|
|
|
+ <div className="w-150px flex items-center justify-between">
|
|
|
+ <span>
|
|
|
+ <span className="inline-block w-20px">
|
|
|
+ {item.icon && <i className={`iconfont mr-8px ${item.icon}`} />}
|
|
|
+ </span>
|
|
|
+ {item.label}
|
|
|
+ </span>
|
|
|
+ <span className="text-12px color-#a6b9cd">{item.fastKey}</span>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+const getMenuData = (menuData: MenuItem[]) => {
|
|
|
+ return menuData.map((item) => {
|
|
|
+ if (item.type === "divider") return item;
|
|
|
+ return {
|
|
|
+ key: item.key,
|
|
|
+ label: <LabelComponent item={item} />,
|
|
|
+ onClick: item.handler,
|
|
|
+ };
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// 节点右键菜单
|
|
|
+export const nodeMenu = getMenuData(nodeMenuData);
|
|
|
+// 边线右键菜单
|
|
|
+export const edgeMenu = getMenuData(edgeMenuData);
|
|
|
+// 页面右键菜单
|
|
|
+export const pageMenu = getMenuData(pageMenuData);
|
|
|
+// 上锁节点菜单
|
|
|
+export const lockMenu = getMenuData(lockMenuData);
|