├── packages ├── css │ ├── theme.scss │ ├── edit.scss │ └── index.scss ├── js │ ├── interface │ │ ├── node │ │ │ ├── NeData.ts │ │ │ ├── Configure.ts │ │ │ └── NeNodeExport.ts │ │ ├── 2d │ │ │ └── Point.ts │ │ ├── NePanelInitIntf.ts │ │ └── NePanelConfigure.ts │ ├── uuid │ │ └── uuid4.ts │ ├── animate │ │ ├── animateIntf.ts │ │ └── animate.ts │ ├── browser.ts │ └── event │ │ └── eventProcesssor.ts ├── nodes │ ├── input │ │ └── NeInputNode │ │ │ ├── src │ │ │ ├── js │ │ │ │ ├── interface │ │ │ │ │ ├── NeInputData.ts │ │ │ │ │ └── neInputPanelIntf.ts │ │ │ │ ├── solution │ │ │ │ │ └── index.ts │ │ │ │ └── index.ts │ │ │ ├── configure.ts │ │ │ ├── NeInputNode.vue │ │ │ └── NeInputDetail.vue │ │ │ └── index.ts │ ├── base │ │ └── NeBaseNode │ │ │ ├── index.ts │ │ │ └── src │ │ │ ├── js │ │ │ ├── interface │ │ │ │ └── neInputPanelIntf.ts │ │ │ ├── index.ts │ │ │ └── event │ │ │ │ └── mouseEventProcessor.ts │ │ │ └── NeBaseNode.vue │ └── index.ts ├── NePanel │ ├── index.ts │ └── src │ │ ├── js │ │ ├── interface │ │ │ ├── nePanelIntf.ts │ │ │ └── NeNodeExportEx.ts │ │ ├── controller │ │ │ ├── panelInfoController.ts │ │ │ └── nodeController.ts │ │ ├── event │ │ │ ├── subEventProcessor.ts │ │ │ ├── dragEventProcessor.ts │ │ │ └── mouseEventProcessor.ts │ │ ├── format.ts │ │ └── index.ts │ │ ├── css │ │ └── index.scss │ │ └── NePanel.vue ├── components │ ├── NeCompSvg │ │ ├── index.ts │ │ └── src │ │ │ └── NeCompSvg.vue │ ├── NeListNode │ │ ├── index.ts │ │ └── src │ │ │ └── NeListNode.vue │ ├── NeSplitPanel │ │ ├── index.ts │ │ └── src │ │ │ ├── js │ │ │ └── event │ │ │ │ └── mouseEventProcessor.ts │ │ │ └── NeSplitPanel.vue │ └── NeDetailPanel │ │ ├── index.ts │ │ └── src │ │ └── NeDetailPanel.vue ├── edit │ ├── NeEditBase │ │ ├── index.ts │ │ └── src │ │ │ └── NeEditBase.vue │ └── NeEditText │ │ ├── index.ts │ │ └── src │ │ └── NeEditText.vue └── index.ts ├── babel.config.js ├── docs ├── .vuepress │ ├── public │ │ └── imgs │ │ │ └── ne-panel.png │ └── config.js ├── README.md └── guide │ ├── quick-start.md │ └── build.md ├── .editorconfig ├── examples ├── main.ts ├── App.vue └── views │ └── BasicUsage.vue ├── shims-vue.d.ts ├── jest.config.js ├── .npmignore ├── tests └── unit │ └── js │ └── uuid │ └── example.spec.ts ├── .gitignore ├── vue.config.js ├── public └── index.html ├── .eslintrc.js ├── tsconfig.json ├── LICENSE ├── README.md └── package.json /packages/css/theme.scss: -------------------------------------------------------------------------------- 1 | $theme-color: #003366; 2 | -------------------------------------------------------------------------------- /packages/js/interface/node/NeData.ts: -------------------------------------------------------------------------------- 1 | export interface NeData { 2 | } 3 | -------------------------------------------------------------------------------- /packages/js/interface/2d/Point.ts: -------------------------------------------------------------------------------- 1 | export interface Point { 2 | x: number 3 | y: number 4 | } 5 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | "@vue/cli-plugin-babel/preset" 4 | ] 5 | }; 6 | -------------------------------------------------------------------------------- /packages/js/interface/node/Configure.ts: -------------------------------------------------------------------------------- 1 | export interface Configure { 2 | name: string 3 | title: string 4 | color: string 5 | } 6 | -------------------------------------------------------------------------------- /docs/.vuepress/public/imgs/ne-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cambridgejames/NodeEditor3-vue/HEAD/docs/.vuepress/public/imgs/ne-panel.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js, jsx, ts, tsx, vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /examples/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./App.vue"; 3 | 4 | import NodeEditor from "../packages"; 5 | 6 | createApp(App) 7 | .use(NodeEditor) 8 | .mount("#app"); 9 | -------------------------------------------------------------------------------- /packages/nodes/input/NeInputNode/src/js/interface/NeInputData.ts: -------------------------------------------------------------------------------- 1 | import { NeData } from "@/js/interface/node/NeData"; 2 | 3 | export interface NeInputData extends NeData { 4 | input: string 5 | } 6 | -------------------------------------------------------------------------------- /shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | declare module "*.vue" { 3 | import type { DefineComponent } from "vue"; 4 | const component: DefineComponent<{}, {}, any>; 5 | export default component; 6 | } 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "@vue/cli-plugin-unit-jest/presets/typescript", 3 | moduleNameMapper: { 4 | "^@/(.*)$": "/packages/$1" 5 | }, 6 | coverageDirectory: "tests/coverage" 7 | }; 8 | -------------------------------------------------------------------------------- /packages/nodes/input/NeInputNode/src/configure.ts: -------------------------------------------------------------------------------- 1 | import { Configure } from "@/js/interface/node/Configure"; 2 | 3 | export default { 4 | name: "NeInputNode", 5 | title: "文字输入", 6 | color: "#bd2e2e" 7 | } as Configure; 8 | -------------------------------------------------------------------------------- /packages/NePanel/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from "vue"; 2 | import NePanel from "./src/NePanel.vue"; 3 | 4 | NePanel.install = (app: App): void => { 5 | app.component(NePanel.name, NePanel); 6 | }; 7 | 8 | export default NePanel; 9 | -------------------------------------------------------------------------------- /packages/components/NeCompSvg/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from "vue"; 2 | import NePanel from "./src/NeCompSvg.vue"; 3 | 4 | NePanel.install = (app: App): void => { 5 | app.component(NePanel.name, NePanel); 6 | }; 7 | 8 | export default NePanel; 9 | -------------------------------------------------------------------------------- /packages/edit/NeEditBase/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from "vue"; 2 | import NeEditBase from "./src/NeEditBase.vue"; 3 | 4 | NeEditBase.install = (app: App): void => { 5 | app.component(NeEditBase.name, NeEditBase); 6 | }; 7 | 8 | export default NeEditBase; 9 | -------------------------------------------------------------------------------- /packages/edit/NeEditText/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from "vue"; 2 | import NeEditText from "./src/NeEditText.vue"; 3 | 4 | NeEditText.install = (app: App): void => { 5 | app.component(NeEditText.name, NeEditText); 6 | }; 7 | 8 | export default NeEditText; 9 | -------------------------------------------------------------------------------- /packages/components/NeListNode/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from "vue"; 2 | import NeListNode from "./src/NeListNode.vue"; 3 | 4 | NeListNode.install = (app: App): void => { 5 | app.component(NeListNode.name, NeListNode); 6 | }; 7 | 8 | export default NeListNode; 9 | -------------------------------------------------------------------------------- /packages/nodes/base/NeBaseNode/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from "vue"; 2 | import NeBaseNode from "./src/NeBaseNode.vue"; 3 | 4 | NeBaseNode.install = (app: App): void => { 5 | app.component(NeBaseNode.name, NeBaseNode); 6 | }; 7 | 8 | export default NeBaseNode; 9 | -------------------------------------------------------------------------------- /packages/nodes/input/NeInputNode/src/js/solution/index.ts: -------------------------------------------------------------------------------- 1 | import { NeInputData } from "@/nodes/input/NeInputNode/src/js/interface/NeInputData"; 2 | 3 | export const solution = (data: NeInputData | null): string => { 4 | return data === null ? "" : data.input; 5 | }; 6 | -------------------------------------------------------------------------------- /packages/components/NeSplitPanel/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from "vue"; 2 | import NeSplitPanel from "./src/NeSplitPanel.vue"; 3 | 4 | NeSplitPanel.install = (app: App): void => { 5 | app.component(NeSplitPanel.name, NeSplitPanel); 6 | }; 7 | 8 | export default NeSplitPanel; 9 | -------------------------------------------------------------------------------- /packages/components/NeDetailPanel/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from "vue"; 2 | import NeDetailPanel from "./src/NeDetailPanel.vue"; 3 | 4 | NeDetailPanel.install = (app: App): void => { 5 | app.component(NeDetailPanel.name, NeDetailPanel); 6 | }; 7 | 8 | export default NeDetailPanel; 9 | -------------------------------------------------------------------------------- /packages/nodes/base/NeBaseNode/src/js/interface/neInputPanelIntf.ts: -------------------------------------------------------------------------------- 1 | export interface NeInputPanelIntf { 2 | x: number 3 | y: number 4 | width: number 5 | height: number 6 | minWidth: number 7 | minHeight: number 8 | title: string 9 | color: string 10 | } 11 | -------------------------------------------------------------------------------- /packages/nodes/input/NeInputNode/src/js/interface/neInputPanelIntf.ts: -------------------------------------------------------------------------------- 1 | export interface NeInputPanelIntf { 2 | x: number 3 | y: number 4 | width: number 5 | height: number 6 | minWidth: number 7 | minHeight: number 8 | title: string, 9 | color: string 10 | } 11 | -------------------------------------------------------------------------------- /packages/css/edit.scss: -------------------------------------------------------------------------------- 1 | @import "theme"; 2 | 3 | /** 4 | * 颜色定义 5 | */ 6 | $default-border-color: #999; 7 | $active-border-color: $theme-color; 8 | 9 | $border-radius: 4px; 10 | $ne-edit-line-height: 26px; 11 | 12 | /** 13 | * 文字输入 14 | */ 15 | $text-background-color: #fff; 16 | $text-color: #666; -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | title: NodeEditor3-vue 4 | actions: 5 | - text: 快速上手 → 6 | link: /guide/quick-start.md 7 | type: primary 8 | - text: 源码构建 9 | link: /guide/build.md 10 | type: default 11 | footer: MIT Licensed | Copyright © 2020 (可以根据实际情况写) 12 | --- 13 | 14 | ![NePanel](/imgs/ne-panel.png) 15 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # 忽略目录 2 | .idea 3 | .vscode 4 | examples/ 5 | build/ 6 | coverage/ 7 | docs/ 8 | imgs/ 9 | node_modules/ 10 | packages/ 11 | public/ 12 | tests/ 13 | typings/ 14 | 15 | # 忽略指定文件 16 | .editorconfig 17 | .eslintrc.js 18 | .gitignore 19 | .browserslistrc 20 | babel.config.js 21 | shims-vue.d.ts 22 | tsconfig.json 23 | tslint.json 24 | vue.config.js 25 | yarn.lock 26 | 27 | *.map 28 | -------------------------------------------------------------------------------- /tests/unit/js/uuid/example.spec.ts: -------------------------------------------------------------------------------- 1 | import UUID4 from "@/js/uuid/uuid4"; 2 | 3 | const UUID_REG = /^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/; 4 | 5 | describe("uuid4.ts", () => { 6 | it("Test create uuid4", () => { 7 | for (let i = 0; i < 10; i++) { 8 | const currentUuid = UUID4.create(); 9 | expect(UUID_REG.test(currentUuid)).toBeTruthy(); 10 | } 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/index.ts: -------------------------------------------------------------------------------- 1 | import { App, Component } from "vue"; 2 | import NePanel from "@/NePanel"; 3 | 4 | const components = [ 5 | NePanel 6 | ] as Component[]; 7 | 8 | const install = (app: App): void => { 9 | components.map(component => app.component(component.name === undefined ? "Undefined" : component.name, component)); 10 | }; 11 | 12 | export { 13 | NePanel 14 | }; 15 | 16 | export default { 17 | install 18 | }; 19 | -------------------------------------------------------------------------------- /packages/js/interface/NePanelInitIntf.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * NePanel配置入参 3 | */ 4 | import { Point } from "@/js/interface/2d/Point"; 5 | 6 | export interface NePanelInitIntf { 7 | /** 8 | * 节点Id 9 | */ 10 | nid: string 11 | 12 | /** 13 | * 节点类型 14 | */ 15 | name: string 16 | 17 | /** 18 | * 节点坐标(左上角) 19 | */ 20 | transform: Point 21 | 22 | /** 23 | * 数据原始信息 24 | */ 25 | data: string 26 | } 27 | -------------------------------------------------------------------------------- /packages/nodes/input/NeInputNode/src/NeInputNode.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | /lib 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | 25 | # vue docs 26 | .cache 27 | .temp 28 | /site 29 | 30 | # Test coverage 31 | /tests/coverage 32 | -------------------------------------------------------------------------------- /packages/NePanel/src/js/interface/nePanelIntf.ts: -------------------------------------------------------------------------------- 1 | export interface NePanelConf { 2 | x: number, 3 | y: number, 4 | width: number, 5 | height: number, 6 | gridDef: { 7 | largeGridSize: number, 8 | smallGridSize: number 9 | }, 10 | scale: number 11 | } 12 | 13 | export interface PanelInfo { 14 | ready: boolean, 15 | show: boolean, 16 | delay: number, 17 | timer: number, 18 | mouse: { 19 | realX: number, 20 | realY: number 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("path"); 2 | const { defineConfig } = require("@vue/cli-service"); 3 | 4 | const alias = { 5 | "@": resolve(__dirname, ".", "packages") 6 | }; 7 | 8 | module.exports = defineConfig({ 9 | transpileDependencies: true, 10 | pages: { 11 | index: { 12 | entry: "examples/main.ts", 13 | template: "public/index.html", 14 | filename: "index.html" 15 | } 16 | }, 17 | configureWebpack: { 18 | resolve: { 19 | alias 20 | } 21 | }, 22 | css: { 23 | extract: false 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /packages/nodes/index.ts: -------------------------------------------------------------------------------- 1 | import { NeNodeExport } from "@/js/interface/node/NeNodeExport"; 2 | 3 | import NeInputNodeExport from "@/nodes/input/NeInputNode"; 4 | 5 | /** 6 | * 节点列表,用于展示可选择的节点 7 | */ 8 | export const componentList = new Array(); 9 | componentList.push(NeInputNodeExport); 10 | 11 | /** 12 | * 节点Map,用于根据配置生成节点 13 | */ 14 | const COMPONENT_MAP = new Map(); 15 | for (const exportElement of componentList) { 16 | COMPONENT_MAP.set(exportElement.configure.name, exportElement); 17 | } 18 | export default COMPONENT_MAP; 19 | -------------------------------------------------------------------------------- /packages/js/interface/node/NeNodeExport.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "vue"; 2 | import { Configure } from "@/js/interface/node/Configure"; 3 | import { NeData } from "@/js/interface/node/NeData"; 4 | 5 | export interface NeNodeExport { 6 | /** 7 | * 节点配置信息 8 | */ 9 | configure: Configure 10 | 11 | /** 12 | * 节点组件 13 | */ 14 | node: Component 15 | 16 | /** 17 | * 节点信息组件 18 | */ 19 | detail: Component 20 | 21 | /** 22 | * 计算方法 23 | * 24 | * @param data 入参 25 | */ 26 | function: (data: T | null) => string 27 | } 28 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/NePanel/src/js/controller/panelInfoController.ts: -------------------------------------------------------------------------------- 1 | import { PanelInfo } from "@/NePanel/src/js/interface/nePanelIntf"; 2 | 3 | import { Ref } from "vue"; 4 | 5 | export const getPanelInfoController = (panelInfo: Ref) => { 6 | /** 7 | * 显示右上角的组件信息 8 | */ 9 | const showPanelInfo = (): void => { 10 | panelInfo.value.show = true; 11 | if (panelInfo.value.timer !== -1) { 12 | clearTimeout(panelInfo.value.timer); 13 | panelInfo.value.timer = -1; 14 | } 15 | panelInfo.value.timer = setTimeout(function () { 16 | panelInfo.value.show = false; 17 | }, panelInfo.value.delay); 18 | }; 19 | 20 | return { 21 | showPanelInfo 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /packages/js/interface/NePanelConfigure.ts: -------------------------------------------------------------------------------- 1 | import { Component, Ref } from "vue"; 2 | import { NePanelConf, PanelInfo } from "@/NePanel/src/js/interface/nePanelIntf"; 3 | import { NeNodeExportEx } from "@/NePanel/src/js/interface/NeNodeExportEx"; 4 | 5 | /** 6 | * 面板相关的变量 7 | */ 8 | export interface NePanelConfigure { 9 | /** 10 | * 面板对象 11 | */ 12 | nePanel: Ref 13 | 14 | /** 15 | * 面板配置信息 16 | */ 17 | nePanelConf: Ref 18 | 19 | /** 20 | * 实时信息 21 | */ 22 | panelInfo: Ref 23 | 24 | /** 25 | * 节点列表 26 | */ 27 | componentList: Ref 28 | 29 | /** 30 | * 节点详细信息 31 | */ 32 | rightElement: Ref 33 | } 34 | -------------------------------------------------------------------------------- /packages/js/uuid/uuid4.ts: -------------------------------------------------------------------------------- 1 | const DEFAULT_VALUE = "xxxxxxxx-xxxx-4xxx-xxxx-xxxxxxxxxxxx"; 2 | const DEFAULT_ELEMENT = "x"; 3 | 4 | const SEARCH_REG = /[xy]/g; 5 | 6 | /** 7 | * 从原始字符串中替换随机字符 8 | * 9 | * @param inputChar 字符串中的字符 10 | * @return 替换后的字符 11 | */ 12 | const replace = (inputChar: string): string => { 13 | const randomValue = Math.random() * 16 | 0; 14 | const solution = inputChar === DEFAULT_ELEMENT ? randomValue : (randomValue & 0x3 | 0x8); 15 | return solution.toString(16); 16 | }; 17 | 18 | /** 19 | * 创建UUID4 20 | * 21 | * @return UUID4 22 | */ 23 | const create = (): string => { 24 | return DEFAULT_VALUE.replace(SEARCH_REG, replace); 25 | }; 26 | 27 | export default { 28 | create 29 | }; 30 | -------------------------------------------------------------------------------- /examples/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | 17 | 38 | -------------------------------------------------------------------------------- /packages/nodes/input/NeInputNode/index.ts: -------------------------------------------------------------------------------- 1 | import { App, markRaw } from "vue"; 2 | import { NeNodeExport } from "@/js/interface/node/NeNodeExport"; 3 | 4 | import NeInputConfigure from "./src/configure"; 5 | import NeInputNode from "./src/NeInputNode.vue"; 6 | import NeInputDetail from "./src/NeInputDetail.vue"; 7 | import { solution } from "./src/js/solution"; 8 | 9 | NeInputNode.install = (app: App): void => { 10 | app.component(NeInputNode.name, NeInputNode); 11 | }; 12 | NeInputDetail.install = (app: App): void => { 13 | app.component(NeInputDetail.name, NeInputDetail); 14 | }; 15 | 16 | export default { 17 | configure: NeInputConfigure, 18 | node: markRaw(NeInputNode), 19 | detail: markRaw(NeInputDetail), 20 | function: solution 21 | } as NeNodeExport; 22 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | const { defaultTheme } = require("vuepress"); 2 | 3 | module.exports = { 4 | lang: "zh-CN", 5 | title: "NodeEditor3-vue", 6 | description: "基于 Vue.js 3.x 的前端可视化节点编辑器组件", 7 | base: "/", 8 | head: [], 9 | plugins: [], 10 | theme: defaultTheme({ 11 | repo: "https://github.com/cambridgejames/NodeEditor3-vue", 12 | navbar: [ 13 | { text: "首页", link: "/" }, 14 | { text: "文档", link: "/guide/" } 15 | ], 16 | sidebar: [ 17 | { text: "介绍", link: "/" }, 18 | { 19 | text: "开发指南", 20 | children: [ 21 | { text: "快速开始", link: "/guide/quick-start.md" }, 22 | { text: "从源码构建", link: "/guide/build.md" } 23 | ] 24 | } 25 | ], 26 | sidebarDepth: 2, 27 | lastUpdated: 'Last Updated' 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /packages/nodes/input/NeInputNode/src/NeInputDetail.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 29 | 30 | 32 | -------------------------------------------------------------------------------- /packages/edit/NeEditBase/src/NeEditBase.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 26 | 27 | 37 | -------------------------------------------------------------------------------- /examples/views/BasicUsage.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 38 | 39 | 42 | -------------------------------------------------------------------------------- /packages/NePanel/src/js/event/subEventProcessor.ts: -------------------------------------------------------------------------------- 1 | import { Ref } from "vue"; 2 | import { Point } from "@/js/interface/2d/Point"; 3 | import { NeNodeExportEx } from "@/NePanel/src/js/interface/NeNodeExportEx"; 4 | import { NePanelConfigure } from "@/js/interface/NePanelConfigure"; 5 | import { getNodeController } from "@/NePanel/src/js/controller/nodeController"; 6 | 7 | export const getSubEventProcessor = (rightContent: Ref, nePanelConfigure: NePanelConfigure) => { 8 | const NodeController = getNodeController(nePanelConfigure); 9 | const onNeLeftClick = (item: NeNodeExportEx): void => { 10 | NodeController.resetSelectedStatus(); 11 | const eventStr: Point = {} as Point; 12 | item.status.selected = true; 13 | rightContent.value.solutionValue = JSON.stringify(eventStr); 14 | nePanelConfigure.rightElement.value = item.detail; 15 | }; 16 | 17 | return { 18 | onNeLeftClick 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | "plugin:vue/vue3-essential", 8 | "@vue/standard", 9 | "@vue/typescript/recommended" 10 | ], 11 | parserOptions: { 12 | ecmaVersion: 2020, 13 | ecmaFeatures: { 14 | experimentalObjectRestSpread: true, 15 | jsx: true 16 | }, 17 | sourceType: "module" 18 | }, 19 | rules: { 20 | "@typescript-eslint/no-inferrable-types": "off", 21 | "@typescript-eslint/ban-types": "off", 22 | "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", 23 | "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", 24 | "node/no-callback-literal": 0, 25 | indent: ["error", 2], 26 | "space-before-function-paren": 0, 27 | quotes: ["error", "double"], 28 | "quote-props": ["error", "as-needed"], 29 | semi: ["error", "always"], 30 | "no-empty": "error", 31 | "no-unused-vars": "off", 32 | "operator-linebreak": ["error", "before"] 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /packages/js/animate/animateIntf.ts: -------------------------------------------------------------------------------- 1 | export declare type OnChangeCallBask = (value: number) => void; 2 | export declare type FinishCallback = (error: Error | null) => void; 3 | 4 | /** 5 | * 动画类型 6 | */ 7 | export enum AnimateType { 8 | /** 9 | * 线性 10 | */ 11 | LINER, 12 | 13 | /** 14 | * 缓入 15 | */ 16 | EASY_IN, 17 | 18 | /** 19 | * 缓出 20 | */ 21 | EASY_OUT, 22 | 23 | /** 24 | * 缓入缓出 25 | */ 26 | EASY_IN_EASY_OUT 27 | } 28 | 29 | /** 30 | * 动画元素 31 | */ 32 | export interface AnimateElement { 33 | /** 34 | * 起始值 35 | */ 36 | startValue: number, 37 | 38 | /** 39 | * 结束值 40 | */ 41 | endValue: number, 42 | 43 | /** 44 | * 起始时间,用于计算进度 45 | */ 46 | startTime: number, 47 | 48 | /** 49 | * 动画执行持续时间(毫秒) 50 | */ 51 | speed: number, 52 | 53 | /** 54 | * 回调函数,用于用户自定义更新变量的值 55 | */ 56 | onValueChange: OnChangeCallBask, 57 | 58 | /** 59 | * 动画类型 60 | */ 61 | type: AnimateType, 62 | 63 | /** 64 | * 动画元素执行完成之后的回调函数 65 | */ 66 | callback: FinishCallback | null 67 | } 68 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "moduleResolution": "node", 8 | "experimentalDecorators": true, 9 | "skipLibCheck": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "useDefineForClassFields": true, 14 | "sourceMap": true, 15 | "baseUrl": ".", 16 | "types": [ 17 | "webpack-env", 18 | "jest" 19 | ], 20 | "paths": { 21 | "@/*": [ 22 | "packages/*" 23 | ] 24 | }, 25 | "lib": [ 26 | "esnext", 27 | "dom", 28 | "dom.iterable", 29 | "scripthost" 30 | ] 31 | }, 32 | "include": [ 33 | "examples/**/*.ts", 34 | "examples/**/*.tsx", 35 | "examples/**/*.vue", 36 | "packages/**/*.ts", 37 | "packages/**/*.tsx", 38 | "packages/**/*.vue", 39 | "tests/**/*.ts", 40 | "tests/**/*.tsx" 41 | ], 42 | "exclude": [ 43 | "node_modules" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /packages/NePanel/src/js/controller/nodeController.ts: -------------------------------------------------------------------------------- 1 | import { NePanelInitIntf } from "@/js/interface/NePanelInitIntf"; 2 | import { neNodeExportEx, NeNodeExportEx } from "@/NePanel/src/js/interface/NeNodeExportEx"; 3 | import { NePanelConfigure } from "@/js/interface/NePanelConfigure"; 4 | 5 | /** 6 | * 初始化节点列表 7 | * 8 | * @param nodeInitList 9 | */ 10 | const initComponentList = (nodeInitList: NePanelInitIntf[]): NeNodeExportEx[] => { 11 | const solutionList = new Array(); 12 | for (const nodeInitElement of nodeInitList) { 13 | solutionList.push(neNodeExportEx(nodeInitElement)); 14 | } 15 | return solutionList; 16 | }; 17 | export default { initComponentList }; 18 | 19 | export const getNodeController = (nePanelConfigure: NePanelConfigure) => { 20 | const resetSelectedStatus = (): void => { 21 | for (const componentElement of nePanelConfigure.componentList.value) { 22 | componentElement.status.selected = false; 23 | } 24 | nePanelConfigure.rightElement.value = undefined; 25 | }; 26 | 27 | return { 28 | initComponentList, 29 | resetSelectedStatus 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Cambridge James 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/components/NeSplitPanel/src/js/event/mouseEventProcessor.ts: -------------------------------------------------------------------------------- 1 | import { Ref } from "vue"; 2 | import { Point } from "@/js/interface/2d/Point"; 3 | import { onMouseDown } from "@/js/event/eventProcesssor"; 4 | 5 | export const getMouseEventProcessor = (preConf: Ref, neSplitPanel: Ref, 6 | leftPanel: Ref) => { 7 | /** 8 | * 修改右边栏宽度事件响应方法 9 | * 10 | * @param event 鼠标事件 11 | */ 12 | const onRightResize = (event: MouseEvent): void => { 13 | const neSplitPanelElement = neSplitPanel.value; 14 | const leftPanelElement = leftPanel.value; 15 | if (neSplitPanelElement === undefined || leftPanelElement === undefined) { 16 | return; 17 | } 18 | const onDragFunc = (event: MouseEvent, startPoint: Point): void => { 19 | const rightMax = neSplitPanelElement.offsetWidth - leftPanelElement.offsetWidth - preConf.value.center; 20 | preConf.value.right = Math.max(Math.min(rightMax, preConf.value.right - event.clientX + startPoint.x), 50); 21 | startPoint.x = event.clientX; 22 | }; 23 | onMouseDown(event, null, onDragFunc, null, null); 24 | }; 25 | 26 | return { 27 | onRightResize 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /docs/guide/quick-start.md: -------------------------------------------------------------------------------- 1 | # 快速开始 2 | 3 | 本章介绍如何在项目中引入并使用 NodeEditor3 组件。 4 | 5 | ## 安装 6 | 7 | ### npm安装 8 | 9 | ```shell 10 | # 执行以下命令在当前项目中安装NodeEditor3 11 | npm install node-editor3-vue --save 12 | ``` 13 | 14 | ## 引入NodeEditor3 15 | 16 | ### 完整引入 17 | 18 | ```typescript 19 | import { createApp } from "vue"; 20 | import App from "./App.vue"; 21 | 22 | import NodeEditor from "node-editor3-vue"; 23 | import "node-editor3-vue/lib/bundle.min.css"; 24 | 25 | createApp(App).use(NodeEditor).mount('#app'); 26 | ``` 27 | 28 | 以上代码便完成了NodeEditor的引入。需要注意的是,样式文件需要单独引入。 29 | 30 | ## 开始使用 31 | 32 | 完成上述步骤之后,基于 Vue.js 3.0 和 NodeEditor3 的环境已经搭建完毕,接下来可以编写代码了。 33 | 34 | 下面是一个 NodeEditor3 组件的使用示例。 35 | 36 | ```vue 37 | 40 | 41 | 55 | ``` 56 | 57 | 需要注意的是,ne-panel组件、其父元素或其祖先元素中必须至少有一个元素拥有确定的宽度和高度,不能全都设置成`100%`,不然可能导致意料之外的问题。 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NodeEditor3-vue 2 | 3 | ![GitHub](https://img.shields.io/github/license/cambridgejames/NodeEditor3-vue) 4 | ![GitHub repo size](https://img.shields.io/github/repo-size/cambridgejames/NodeEditor3-vue) 5 | ![npm](https://img.shields.io/npm/v/node-editor3-vue) 6 | ![npm](https://img.shields.io/npm/dm/node-editor3-vue) 7 | ![npm bundle size](https://img.shields.io/bundlephobia/minzip/node-editor3-vue) 8 | ![GitHub Repo stars](https://img.shields.io/github/stars/cambridgejames/NodeEditor3-vue?style=social) 9 | 10 | --- 11 | 12 | ## 简介 13 | 14 | NodeEditor3-vue 是一个基于 Vue.js 3.x 的前端可视化节点编辑器组件。 15 | 16 | 若要着手进行本组件的开发,请先参考[组件开发构建指南](./docs/guide/build.md)。 17 | 18 | 若要快速上手并使用本组件,请参考[快速开始](./docs/guide/quick-start.md)。 19 | 20 | 若要为项目提交Issue,请转至右侧Github链接:[https://github.com/cambridgejames/NodeEditor3-vue/issues](https://github.com/cambridgejames/NodeEditor3-vue/issues) 21 | 22 | ## 版权说明 23 | 24 | 项目地址: 25 | 26 | - GitHub [https://github.com/cambridgejames/NodeEditor3-vue](https://github.com/cambridgejames/NodeEditor3-vue) 27 | - Gitee [https://gitee.com/powerinv/node-editor3-vue](https://gitee.com/powerinv/node-editor3-vue) 28 | 29 | 该项目签署了MIT授权许可,详情请参阅 [LICENSE](LICENSE) 文件 30 | 31 | 开发者邮箱:cambridge_james@foxmail.com 32 | 33 | ![](./docs/.vuepress/public/imgs/ne-panel.png) 34 | -------------------------------------------------------------------------------- /packages/nodes/input/NeInputNode/src/js/index.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, ref, SetupContext } from "vue"; 2 | import { NeInputPanelIntf } from "@/nodes/input/NeInputNode/src/js/interface/neInputPanelIntf"; 3 | import NeBaseNode from "@/nodes/base/NeBaseNode"; 4 | import Configure from "../configure"; 5 | 6 | export default defineComponent({ 7 | name: Configure.name, 8 | components: { 9 | NeBaseNode 10 | }, 11 | props: { 12 | x: { 13 | type: Number, 14 | default: 0, 15 | required: true 16 | }, 17 | y: { 18 | type: Number, 19 | default: 0, 20 | required: true 21 | } 22 | }, 23 | emits: { 24 | neLeftClick: null, 25 | neRightClick: null 26 | }, 27 | setup(propsData, context: SetupContext) { 28 | const nodePanel = ref(); 29 | const nodePanelConf = ref({ 30 | x: propsData.x, 31 | y: propsData.y, 32 | title: Configure.title, 33 | color: Configure.color 34 | } as NeInputPanelIntf); 35 | 36 | const onNeLeftClick = (): void => context.emit("neLeftClick"); 37 | const onNeRightClick = (): void => context.emit("neRightClick"); 38 | 39 | return { 40 | nodePanel, 41 | nodePanelConf, 42 | onNeLeftClick, 43 | onNeRightClick 44 | }; 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /docs/guide/build.md: -------------------------------------------------------------------------------- 1 | # NodeEditor3-vue 组件开发构建指南 2 | 3 | ## 环境依赖 4 | 5 | 1. Node.js v16+ 6 | 2. Vue.js 3.x 7 | 3. yarn v1 8 | 9 | ## 构建步骤 10 | 11 | NodeEditor3-vue使用yarn来管理依赖。因此在开始构建之前,你需要先全局安装yarn。 12 | 13 | ```shell 14 | npm install yarn -g 15 | ``` 16 | 17 | 你可以使用`yarn -v`命令来验证安装结果。确认安装完成后,就可以使用`yarn`命令来构建本组件了: 18 | 19 | ```shell 20 | # 安装依赖包 21 | yarn install 22 | 23 | # 运行示例 24 | yarn serve 25 | 26 | # 构建版本 27 | yarn build 28 | 29 | # 清理构建文件 30 | yarn build:clean 31 | ``` 32 | 33 | ## 目录结构 34 | 35 | 本节介绍主要的文件及目录结构。 36 | 37 | ```text 38 | node-editor3-vue - 项目根目录 39 | ├ build - 构建配置目录 40 | │ └ rollup.config.js - Rollup构建脚本 41 | ├ docs - 文档目录 42 | ├ examples - 演示组件,运行 npm run serve 时展示使用示例 43 | ├ lib - 组件构建的目标路径 44 | ├ packages - 节点编辑器组件源码目录 45 | │ ├ components - 通用组件目录 46 | │ │ └ NeCompSvg - SVG图标组件 47 | │ ├ css - 通用样式文件目录 48 | │ ├ js - 通用工具方法目录 49 | │ │ ├ animate - 动画引擎 50 | │ │ ├ browserFormat - 类型转换、通用数值计算工具方法 51 | │ │ ├ interface - 独立接口定义 52 | │ │ └ browser.ts - 浏览器及内核类型检测 53 | │ ├ NePanel - 面板组件 54 | │ ├ nodes - 节点组件目录 55 | │ │ ├ input - 输入组件目录 56 | │ │ │ └ NeInputNode - 通用输入组件 57 | │ │ └ index.ts - 节点组件汇总目录,提供动态加载功能的基础数据 58 | │ └ index.ts - 入口文件 59 | └ public - 演示用的HTML文件目录 60 | ``` 61 | -------------------------------------------------------------------------------- /packages/NePanel/src/js/interface/NeNodeExportEx.ts: -------------------------------------------------------------------------------- 1 | import { NeNodeExport } from "@/js/interface/node/NeNodeExport"; 2 | import { NePanelInitIntf } from "@/js/interface/NePanelInitIntf"; 3 | import { Point } from "@/js/interface/2d/Point"; 4 | import COMPONENT_MAP from "@/nodes"; 5 | import { NeData } from "@/js/interface/node/NeData"; 6 | 7 | export interface NeNodeExportStatus { 8 | id: string 9 | selected: boolean 10 | transform: Point 11 | } 12 | 13 | export interface NeNodeExportData { 14 | data: NeData | null 15 | solution: string 16 | } 17 | 18 | export interface NeNodeExportEx extends NeNodeExport { 19 | status: NeNodeExportStatus 20 | data: NeNodeExportData 21 | } 22 | 23 | /** 24 | * 节点初始化方法 25 | * 26 | * @param initConf 节点初始化对象 27 | */ 28 | export const neNodeExportEx = (initConf: NePanelInitIntf): NeNodeExportEx => { 29 | const solution = { ...COMPONENT_MAP.get(initConf.name) } as NeNodeExportEx; 30 | solution.status = { 31 | id: initConf.nid, 32 | selected: false, 33 | transform: initConf.transform 34 | } as NeNodeExportStatus; 35 | solution.data = { 36 | data: getNeData(initConf), 37 | solution: "" 38 | } as NeNodeExportData; 39 | return solution; 40 | }; 41 | 42 | const getNeData = (initConf: NePanelInitIntf): T | NeData | null => { 43 | if (initConf.data === undefined || initConf.data === null) { 44 | return null; 45 | } 46 | return JSON.parse(initConf.data) as T; 47 | }; 48 | -------------------------------------------------------------------------------- /packages/components/NeListNode/src/NeListNode.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 28 | 29 | 67 | -------------------------------------------------------------------------------- /packages/nodes/base/NeBaseNode/src/NeBaseNode.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 26 | 27 | 36 | -------------------------------------------------------------------------------- /packages/edit/NeEditText/src/NeEditText.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 48 | 49 | 69 | -------------------------------------------------------------------------------- /packages/nodes/base/NeBaseNode/src/js/index.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, inject, Ref, ref, SetupContext } from "vue"; 2 | import NeCompSvg from "@/components/NeCompSvg"; 3 | import { NeInputPanelIntf } from "./interface/neInputPanelIntf"; 4 | import { getMouseEventProcessor } from "./event/mouseEventProcessor"; 5 | 6 | export default defineComponent({ 7 | name: "NeBaseNode", 8 | components: { 9 | NeCompSvg 10 | }, 11 | props: { 12 | x: { 13 | type: Number, 14 | default: 0, 15 | required: true 16 | }, 17 | y: { 18 | type: Number, 19 | default: 0, 20 | required: true 21 | }, 22 | title: { 23 | type: String, 24 | default: "", 25 | required: true 26 | }, 27 | color: { 28 | type: String, 29 | default: "#eee", 30 | required: false 31 | } 32 | }, 33 | emits: { 34 | neLeftClick: null, 35 | neRightClick: null 36 | }, 37 | setup(propsData, context: SetupContext) { 38 | const NODE_WIDTH_DEFAULT = 120; 39 | const nodePanel = ref(); 40 | const nodePanelConf = ref({ 41 | x: propsData.x, 42 | y: propsData.y, 43 | width: NODE_WIDTH_DEFAULT, 44 | height: 40, 45 | minWidth: NODE_WIDTH_DEFAULT, 46 | minHeight: 40, 47 | title: propsData.title, 48 | color: propsData.color 49 | } as NeInputPanelIntf); 50 | 51 | /************************ 52 | * Imported Functions * 53 | ************************/ 54 | 55 | const panelConf = inject("panelConf") as Ref; 56 | const MouseEventProcessor = getMouseEventProcessor(nodePanel, nodePanelConf, panelConf, context); 57 | 58 | return { 59 | nodePanel, 60 | nodePanelConf, 61 | MouseEventProcessor 62 | }; 63 | } 64 | }); 65 | -------------------------------------------------------------------------------- /packages/css/index.scss: -------------------------------------------------------------------------------- 1 | @import "theme"; 2 | 3 | $grid-stroke-color: $theme-color; 4 | $selection-box-color: #ff4f81; 5 | $selected-node-border: #ffaa18; 6 | 7 | $node-background-color: #fbfbfb; 8 | $detail-title-background-color: $theme-color; 9 | 10 | $drag-info-panel-color: #ffaa18; 11 | 12 | $node-title-input: #bd2e2e; 13 | 14 | $tips-background: rgba(0, 0, 0, 0.3); 15 | 16 | * { 17 | margin: 0; 18 | padding: 0; 19 | } 20 | 21 | .none-selective { 22 | -webkit-user-select: none; 23 | -moz-user-select: none; 24 | -ms-user-select: none; 25 | user-select: none; 26 | } 27 | 28 | .ne-node { 29 | .background { 30 | width: calc(1px + var(--width)); 31 | height: calc(1px + var(--height)); 32 | fill: $node-background-color; 33 | stroke: $grid-stroke-color; 34 | stroke-width: 1px; 35 | } 36 | 37 | .title-group { 38 | cursor: pointer; 39 | 40 | .title-back { 41 | width: var(--width); 42 | height: 20px; 43 | outline: none; 44 | } 45 | 46 | .title-text { 47 | font-size: 12px; 48 | fill: white; 49 | text-anchor: start; 50 | } 51 | 52 | .output-point { 53 | fill: $selected-node-border; 54 | 55 | &:hover { 56 | fill: white; 57 | cursor: pointer; 58 | } 59 | } 60 | } 61 | 62 | .footer-group { 63 | width: var(--width); 64 | height: 20px; 65 | 66 | .resize { 67 | cursor: nwse-resize; 68 | } 69 | 70 | rect.resize { 71 | fill: $node-background-color; 72 | } 73 | } 74 | 75 | &:hover, &.selected { 76 | .background { 77 | stroke: $selected-node-border; 78 | } 79 | } 80 | } 81 | 82 | .ne-edit { 83 | display: block; 84 | width: 100%; 85 | box-sizing: border-box; 86 | font-size: 14px; 87 | 88 | &:not(:first-child) { 89 | margin-top: 6px; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /packages/NePanel/src/css/index.scss: -------------------------------------------------------------------------------- 1 | @import "../../../css/index.scss"; 2 | 3 | .node-list-container { 4 | box-sizing: border-box; 5 | width: 100%; 6 | padding: 3px; 7 | } 8 | 9 | .ne-panel { 10 | width: 100%; 11 | height: 100%; 12 | overflow: hidden; 13 | position: relative; 14 | 15 | .ne-svg-panel { 16 | width: 100%; 17 | height: 100%; 18 | 19 | .grid-defs { 20 | path { 21 | fill: none; 22 | stroke: $grid-stroke-color; 23 | stroke-opacity: 0.4; 24 | } 25 | } 26 | 27 | .grid-group { 28 | .coordinate-axis { 29 | stroke: $grid-stroke-color; 30 | stroke-opacity: 0.4; 31 | } 32 | } 33 | } 34 | } 35 | 36 | .ne-panel-reset { 37 | position: absolute; 38 | left: 10px; 39 | top: 10px; 40 | padding: 10px; 41 | background-color: $tips-background; 42 | fill: #fff; 43 | opacity: 0; 44 | transition: all .3s ease-in-out; 45 | cursor: pointer; 46 | 47 | &[class*=show] { 48 | opacity: 1; 49 | } 50 | } 51 | 52 | .ne-panel-info { 53 | position: absolute; 54 | right: 10px; 55 | top: 10px; 56 | padding: 10px; 57 | color: #fff; 58 | font-size: 12px; 59 | background-color: $tips-background; 60 | opacity: 0; 61 | transition: all .3s ease-in-out; 62 | 63 | &[class*=show] { 64 | opacity: 1; 65 | } 66 | } 67 | 68 | .drag-info-panel { 69 | position: absolute; 70 | top: 0; 71 | left: 0; 72 | width: 100%; 73 | height: 100%; 74 | opacity: 0.2; 75 | display: none; 76 | background-color: $drag-info-panel-color; 77 | text-align: center; 78 | 79 | &:after { 80 | width: 100%; 81 | height: min-content; 82 | display: block; 83 | vertical-align: middle; 84 | position: absolute; 85 | top: 50%; 86 | left: 0; 87 | transform: translateY(-50%); 88 | font-size: 50px; 89 | content: var(--dragInfo); 90 | } 91 | 92 | &[class*=show] { 93 | display: block; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /packages/NePanel/src/js/format.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 数值转换方法,在缩放坐标的同时保证保证线宽、尺寸等值不变 3 | * 4 | * @param number 期望显示出来的尺寸数值 5 | * @param scale 转换后的在画布上的实际尺寸 6 | * @return 元素在svg画布中的宽度 7 | */ 8 | const formatScale = (number: number, scale: number = 1.0): number => { 9 | return number / scale; 10 | }; 11 | 12 | /** 13 | * 获取缩放后的放大倍数 14 | * 15 | * @param currentScale 修改前的放大倍数 16 | * @param wheelDirection 滚轮方向:true:缩小,false:放大 17 | */ 18 | const formatScaleNumber = (currentScale: number, wheelDirection: boolean): number => { 19 | const confMap = [ 20 | [1000, 0], [100, 0], [10, 0], [1, 0], [0.1, 1], [0.01, 2], [0.001, 3] 21 | ]; 22 | const fixNumber = (number: number, digits: number): number => { 23 | return Number(number.toFixed(digits)); 24 | }; 25 | for (let i = 1; i < confMap.length; i++) { 26 | if (wheelDirection && currentScale <= confMap[i - 1][0] && currentScale > confMap[i][0]) { 27 | return fixNumber(currentScale - confMap[i][0], confMap[i][1]); // 缩小 28 | } else if (!wheelDirection && currentScale < confMap[i - 1][0] && currentScale >= confMap[i][0]) { 29 | return fixNumber(currentScale + confMap[i][0], confMap[i][1]); // 放大 30 | } 31 | } 32 | return 1; 33 | }; 34 | 35 | interface PanelGridDef { 36 | largeGridSize: number, 37 | smallGridSize: number 38 | } 39 | 40 | /** 41 | * 根据传入的放大倍数返回合适的格子尺寸 42 | * 43 | * @param scaleValue 放大倍数 44 | */ 45 | const formatGrid = (scaleValue: number): PanelGridDef => { 46 | const solutionMap = [ 47 | [80, 0.5], [8, 5], [0.8, 50], [0.08, 500], [0.008, 5000] 48 | ]; 49 | for (let i = 1; i < solutionMap.length; i++) { 50 | if (scaleValue >= solutionMap[i][0]) { 51 | return { 52 | largeGridSize: solutionMap[i][1], 53 | smallGridSize: solutionMap[i - 1][1] 54 | }; 55 | } 56 | } 57 | return { 58 | largeGridSize: 0, 59 | smallGridSize: 0 60 | }; 61 | }; 62 | 63 | export default { 64 | formatScale, 65 | formatScaleNumber, 66 | formatGrid 67 | }; 68 | -------------------------------------------------------------------------------- /packages/components/NeDetailPanel/src/NeDetailPanel.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 39 | 40 | 86 | -------------------------------------------------------------------------------- /packages/js/browser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 浏览器类型 3 | */ 4 | export enum BROWSER { 5 | IE = "IE", 6 | FIREFOX = "FIREFOX", 7 | OPERA = "OPERA", 8 | CHROME = "CHROME", 9 | SAFARI = "SAFARI" 10 | } 11 | 12 | /** 13 | * 浏览器内核类型 14 | */ 15 | export enum KERNEL { 16 | TRIDENT = "TRIDENT", 17 | WEBKIT = "WEBKIT", 18 | GECKO = "GECKO", 19 | PRESTO = "PRESTO" 20 | } 21 | 22 | /** 23 | * 用于浏览器类型匹配的正则表达式 24 | */ 25 | const BROWSER_REG_MAP: {[key in BROWSER]: RegExp} = { 26 | [BROWSER.IE]: /\b(?:msie |ie |trident\/\d.*rv[ :])([\d.]+)/, 27 | [BROWSER.FIREFOX]: /\bfirefox\/([\d.ab]+)/, 28 | [BROWSER.OPERA]: /\bopr\/([\d.]+)/, 29 | [BROWSER.CHROME]: / (?:chrome|crios|crmo)\/([\d.]+)/, 30 | [BROWSER.SAFARI]: /\bversion\/([\d.]+(?: beta)?)(?: mobile(?:\/[a-z\d]+)?)? safari\// 31 | }; 32 | 33 | /** 34 | * 用于浏览器内核类型匹配的正则表达式 35 | */ 36 | const KERNEL_REG_MAP: {[key in KERNEL]: RegExp} = { 37 | [KERNEL.TRIDENT]: /\b(?:msie |ie |trident\/\d.*rv[ :])([\d.]+)/, 38 | [KERNEL.WEBKIT]: /\bapplewebkit\/?([\d.+]+)/, 39 | [KERNEL.GECKO]: /\bgecko\/(\d+)/, 40 | [KERNEL.PRESTO]: /\bpresto\/([\d.]+)/ 41 | }; 42 | 43 | const DEFAULT_RESULT = "UNKNOWN"; 44 | 45 | /** 46 | * 匹配方法 47 | * 48 | * @param client 待匹配的类型 49 | * @param userAgent 用户代理 50 | * @return 匹配结果 51 | */ 52 | const detect = (client: {[key: string]: RegExp}, userAgent: string): string => { 53 | for (const clientKey in client) { 54 | if (client[clientKey].test(userAgent)) { 55 | return clientKey.toString(); 56 | } 57 | } 58 | return DEFAULT_RESULT; 59 | }; 60 | 61 | /** 62 | * 带默认值的匹配方法 63 | * 64 | * @param client 待匹配的类型 65 | * @param userAgent 用户代理 66 | * @param defaultValue 默认值 67 | * @return 匹配结果 68 | */ 69 | const detectWithDefault = (client: {[key: string]: RegExp}, userAgent: string, defaultValue: string = DEFAULT_RESULT): string => { 70 | const result = detect(client, userAgent); 71 | return result === DEFAULT_RESULT ? defaultValue : result; 72 | }; 73 | 74 | /** 75 | * 浏览器及内核类型类 76 | */ 77 | export interface BrowserType { 78 | browser: string // 浏览器类型 79 | kernel: string // 浏览器内核类型 80 | } 81 | 82 | /** 83 | * 获取浏览器及内核类型 84 | * 85 | * @return 浏览器及内核类型 86 | */ 87 | export const getBrowser = (): BrowserType => { 88 | const userAgent = navigator.userAgent.toLowerCase(); 89 | return { 90 | browser: detectWithDefault(BROWSER_REG_MAP, userAgent, BROWSER.CHROME), 91 | kernel: detectWithDefault(KERNEL_REG_MAP, userAgent, KERNEL.WEBKIT) 92 | }; 93 | }; 94 | -------------------------------------------------------------------------------- /packages/NePanel/src/js/event/dragEventProcessor.ts: -------------------------------------------------------------------------------- 1 | import { Ref } from "vue"; 2 | import { Configure } from "@/js/interface/node/Configure"; 3 | import { NePanelInitIntf } from "@/js/interface/NePanelInitIntf"; 4 | import { NePanelConfigure } from "@/js/interface/NePanelConfigure"; 5 | import { Point } from "@/js/interface/2d/Point"; 6 | import Format from "@/NePanel/src/js/format"; 7 | import { neNodeExportEx } from "@/NePanel/src/js/interface/NeNodeExportEx"; 8 | import Uuid4 from "@/js/uuid/uuid4"; 9 | 10 | const CONFIGURE_KEY = "configure"; 11 | const OFFSET_KEY = "offset"; 12 | const DEFAULT_OFFSET = JSON.stringify({ x: 0, y: 0 } as Point); 13 | 14 | /** 15 | * 获取拖拽事件中的数据 16 | * 17 | * @param event 拖拽事件 18 | * @param key 键 19 | * @param defaultValue 默认值 20 | * @return 数据 21 | */ 22 | const getOrDefault = (event: DragEvent, key: string, defaultValue: string): string => { 23 | const value = event.dataTransfer?.getData(key); 24 | return value === undefined ? defaultValue : value; 25 | }; 26 | 27 | export const getDragEventProcessor = (nodeDrag: Ref, nePanelConfigure: NePanelConfigure) => { 28 | /** 29 | * 拖拽开始事件监听方法 30 | * 31 | * @param event 拖拽事件 32 | * @param itemConfigure 被拖拽的元素信息 33 | */ 34 | const onDragStart = (event: DragEvent, itemConfigure: Configure): void => { 35 | if (event.dataTransfer === null) { 36 | return; 37 | } 38 | event.dataTransfer.setData(CONFIGURE_KEY, itemConfigure.name); 39 | event.dataTransfer.setData(OFFSET_KEY, JSON.stringify({ 40 | x: event.offsetX, 41 | y: event.offsetY 42 | } as Point)); 43 | nodeDrag.value.dragging = true; 44 | }; 45 | 46 | /** 47 | * 拖拽结束事件监听方法 48 | */ 49 | const onDragEnd = (): void => { 50 | nodeDrag.value.dragging = false; 51 | }; 52 | 53 | /** 54 | * 拖拽放置事件监听方法 55 | * 56 | * @param event 拖拽事件 57 | */ 58 | const onDrop = (event: DragEvent): void => { 59 | if (event.dataTransfer === null) { 60 | return; 61 | } 62 | const nePanelConf = nePanelConfigure.nePanelConf.value; 63 | const offset = JSON.parse(getOrDefault(event, OFFSET_KEY, DEFAULT_OFFSET)) as Point; 64 | nePanelConfigure.componentList.value.push(neNodeExportEx({ 65 | nid: Uuid4.create(), 66 | name: event.dataTransfer.getData(CONFIGURE_KEY), 67 | transform: { 68 | x: Format.formatScale(nePanelConf.x + event.offsetX - offset.x, nePanelConf.scale), 69 | y: Format.formatScale(nePanelConf.y + event.offsetY - offset.y, nePanelConf.scale) 70 | } 71 | } as NePanelInitIntf)); 72 | }; 73 | 74 | return { 75 | onDragStart, 76 | onDragEnd, 77 | onDrop 78 | }; 79 | }; 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-editor3-vue", 3 | "version": "0.2.1", 4 | "private": false, 5 | "description": "基于 Vue.js 3.x 的前端可视化节点编辑器组件", 6 | "author": { 7 | "name": "Cambridge James", 8 | "email": "cambridge_james@foxmail.com" 9 | }, 10 | "scripts": { 11 | "serve": "vue-cli-service serve", 12 | "build": "yarn clean && yarn build:lib && yarn build:esm-bundle && rimraf lib/demo.html", 13 | "test:unit": "vue-cli-service test:unit", 14 | "test:coverage": "vue-cli-service test:unit --coverage", 15 | "build:docs": "vuepress build docs --dest site --cache docs/.vuepress/.cache --temp docs/.vuepress/.temp", 16 | "build:esm-bundle": "rollup --config ./build/rollup.config.js", 17 | "build:lib": "vue-cli-service build --target lib --name index --dest lib ./packages/index.ts", 18 | "clean": "rimraf lib", 19 | "clean:docs": "rimraf docs/.vuepress/.cache && rimraf docs/.vuepress/.temp && rimraf site", 20 | "serve:doc": "vuepress dev docs --cache docs/.vuepress/.cache --temp docs/.vuepress/.temp" 21 | }, 22 | "main": "lib/index.cjs.js", 23 | "module": "lib/index.esm.js", 24 | "dependencies": { 25 | "vue": "^3.2.13" 26 | }, 27 | "devDependencies": { 28 | "@rollup/plugin-node-resolve": "^13.0.5", 29 | "@types/jest": "^27.0.1", 30 | "@types/node": "^18.0.3", 31 | "@typescript-eslint/eslint-plugin": "^5.4.0", 32 | "@typescript-eslint/parser": "^5.4.0", 33 | "@vue/cli-plugin-typescript": "~5.0.0", 34 | "@vue/cli-plugin-unit-jest": "~5.0.0", 35 | "@vue/cli-service": "~5.0.0", 36 | "@vue/eslint-config-standard": "^6.1.0", 37 | "@vue/eslint-config-typescript": "^9.1.0", 38 | "@vue/test-utils": "^2.0.0-0", 39 | "@vue/vue3-jest": "^27.0.0-alpha.1", 40 | "babel-jest": "^27.0.6", 41 | "core-js": "^3.8.3", 42 | "eslint": "^7.32.0", 43 | "eslint-plugin-import": "^2.25.3", 44 | "eslint-plugin-node": "^11.1.0", 45 | "eslint-plugin-promise": "^5.1.0", 46 | "eslint-plugin-vue": "^8.0.1", 47 | "jest": "^27.0.5", 48 | "rimraf": "^3.0.2", 49 | "rollup": "^2.76.0", 50 | "rollup-plugin-scss": "^3.0.0", 51 | "rollup-plugin-terser": "^7.0.2", 52 | "rollup-plugin-typescript2": "^0.32.1", 53 | "rollup-plugin-vue": "^6.0.0", 54 | "sass": "^1.32.7", 55 | "sass-loader": "^12.0.0", 56 | "ts-jest": "^27.0.4", 57 | "typescript": "~4.5.5", 58 | "vuepress": "^2.0.0-beta.49" 59 | }, 60 | "browserslist": [ 61 | "> 1%", 62 | "last 2 versions", 63 | "not dead", 64 | "not ie 11" 65 | ], 66 | "keywords": [ 67 | "node editor", 68 | "editor" 69 | ], 70 | "license": "MIT", 71 | "typings": "lib/index.d.ts" 72 | } 73 | -------------------------------------------------------------------------------- /packages/js/event/eventProcesssor.ts: -------------------------------------------------------------------------------- 1 | import { Point } from "@/js/interface/2d/Point"; 2 | 3 | /** 4 | * 判定鼠标拖拽的移动距离最小值(单位:像素) 5 | */ 6 | const MOUSE_DRAG_MIN_X = 5; 7 | const MOUSE_DRAG_MIN_Y = 5; 8 | 9 | /** 10 | * 判定鼠标拖拽的间隔时间最小值(单位:毫秒) 11 | */ 12 | const MOUSE_DRAG_MIN_TIME = 70; 13 | 14 | /** 15 | * 鼠标事件回调方法类型定义 16 | */ 17 | export declare type EventCallbackIntf = (event: MouseEvent, startPoint: Point) => void; 18 | export declare type EventCallback = EventCallbackIntf | null; 19 | export declare type ConfigCallbackIntf = () => void; 20 | export declare type ConfigCallback = ConfigCallbackIntf | null; 21 | 22 | interface StartPointMessage extends Point { 23 | time: number 24 | } 25 | 26 | /** 27 | * 判断鼠标是点击还是拖拽 28 | * 29 | * @param currentEvent 当前的鼠标事件 30 | * @param startPoint 鼠标按下时的坐标 31 | * @return 是(true) / 否(false) 32 | */ 33 | const isClickEvent = (currentEvent: MouseEvent, startPoint: StartPointMessage): boolean => { 34 | if (Math.abs(currentEvent.clientX - startPoint.x) < MOUSE_DRAG_MIN_X 35 | && Math.abs(currentEvent.clientY - startPoint.y) < MOUSE_DRAG_MIN_Y) { 36 | return true; 37 | } 38 | return new Date().getTime() - startPoint.time < MOUSE_DRAG_MIN_TIME; 39 | }; 40 | 41 | /** 42 | * 鼠标按下事件监听方法 43 | * 44 | * @param event 鼠标按下事件 45 | * @param onInit 鼠标按下事件初始化方法 46 | * @param onDrag 鼠标拖拽回调方法 47 | * @param onClick 鼠标点击回调方法 48 | * @param onClear 鼠标按下事件清理方法 49 | */ 50 | export const onMouseDown = (event: MouseEvent, onInit: ConfigCallback, onDrag: EventCallback, onClick: EventCallback, 51 | onClear: ConfigCallback): void => { 52 | if (onInit !== null) { 53 | onInit(); 54 | } 55 | const lastDragEndPoint: Point = { x: event.clientX, y: event.clientY }; 56 | const mouseDownPoint = { ...lastDragEndPoint } as StartPointMessage; 57 | mouseDownPoint.time = new Date().getTime(); 58 | 59 | /** 60 | * 鼠标移动事件监听方法 61 | * 62 | * @param subEvent 鼠标移动事件 63 | */ 64 | const mouseDragFunc = (subEvent: Event): void => { 65 | if (!(subEvent instanceof MouseEvent) || onDrag === null) { 66 | return; 67 | } 68 | const mouseEvent = subEvent as MouseEvent; 69 | if (isClickEvent(mouseEvent, mouseDownPoint)) { 70 | return; 71 | } 72 | onDrag(mouseEvent, lastDragEndPoint); 73 | }; 74 | 75 | /** 76 | * 鼠标抬起事件监听方法 77 | * 78 | * @param subEvent 鼠标抬起事件 79 | */ 80 | const mouseUpFunc = (subEvent: Event): void => { 81 | try { 82 | if (!(subEvent instanceof MouseEvent) || onClick === null) { 83 | return; 84 | } 85 | const mouseEvent = subEvent as MouseEvent; 86 | if (!isClickEvent(mouseEvent, mouseDownPoint)) { 87 | return; 88 | } 89 | onClick(mouseEvent, mouseDownPoint); 90 | } finally { 91 | if (onClear !== null) { 92 | onClear(); 93 | } 94 | window.removeEventListener("mousemove", mouseDragFunc); 95 | window.removeEventListener("mouseup", mouseUpFunc); 96 | } 97 | }; 98 | 99 | window.addEventListener("mousemove", mouseDragFunc); 100 | window.addEventListener("mouseup", mouseUpFunc); 101 | }; 102 | -------------------------------------------------------------------------------- /packages/components/NeSplitPanel/src/NeSplitPanel.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 62 | 63 | 125 | -------------------------------------------------------------------------------- /packages/nodes/base/NeBaseNode/src/js/event/mouseEventProcessor.ts: -------------------------------------------------------------------------------- 1 | import { NeInputPanelIntf } from "../interface/neInputPanelIntf"; 2 | import { Ref, SetupContext } from "vue"; 3 | import { Point } from "@/js/interface/2d/Point"; 4 | import Format from "@/NePanel/src/js/format"; 5 | import { EventCallback, onMouseDown } from "@/js/event/eventProcesssor"; 6 | 7 | /** 8 | * 根据节点获取整个面板 9 | * 10 | * @param nodeElement 11 | */ 12 | const getPanelElement = (nodeElement: HTMLElement | undefined): HTMLElement | undefined | null => { 13 | let parentElement = nodeElement as HTMLElement | undefined | null; 14 | while (parentElement !== undefined && parentElement !== null && parentElement.tagName !== "svg") { 15 | parentElement = parentElement.parentElement; 16 | } 17 | return parentElement; 18 | }; 19 | 20 | export const getMouseEventProcessor = (nodePanel: Ref, nodePanelConf: Ref, 21 | panelConf: Ref, context: SetupContext) => { 22 | /** 23 | * 右键点击事件响应方法 24 | * 25 | * @param event 鼠标事件 26 | */ 27 | const onRightDown = (event: MouseEvent): void => { 28 | const onClickFunc: EventCallback = (): void => { 29 | context.emit("neRightClick"); 30 | }; 31 | onMouseDown(event, null, null, onClickFunc, null); 32 | }; 33 | 34 | /** 35 | * 移动节点事件响应方法 36 | * 37 | * @param event 鼠标事件 38 | */ 39 | const onMoveNodeDown = (event: MouseEvent): void => { 40 | const onDragFunc: EventCallback = (event: MouseEvent, startPoint: Point): void => { 41 | nodePanelConf.value.x += Format.formatScale(event.clientX - startPoint.x, panelConf.value.scale); 42 | nodePanelConf.value.y += Format.formatScale(event.clientY - startPoint.y, panelConf.value.scale); 43 | startPoint.x = event.clientX; 44 | startPoint.y = event.clientY; 45 | }; 46 | const onClickFunc: EventCallback = (): void => { 47 | context.emit("neLeftClick"); 48 | }; 49 | onMouseDown(event, null, onDragFunc, onClickFunc, null); 50 | }; 51 | 52 | /** 53 | * 节点大小改变事件响应方法 54 | * 55 | * @param event 鼠标事件 56 | */ 57 | const onResizeDown = (event: MouseEvent): void => { 58 | const panelElement = getPanelElement(nodePanel.value); 59 | const starting: Point = { 60 | x: event.clientX, 61 | y: event.clientY 62 | }; 63 | const resizeFunc = (event: Event): void => { 64 | if (event instanceof MouseEvent) { 65 | const mouseEvent = event as MouseEvent; 66 | if (nodePanelConf.value.width + mouseEvent.clientX - starting.x >= nodePanelConf.value.minWidth) { 67 | nodePanelConf.value.width += Format.formatScale(mouseEvent.clientX - starting.x, panelConf.value.scale); 68 | starting.x = mouseEvent.clientX; 69 | } 70 | if (nodePanelConf.value.height + mouseEvent.clientY - starting.y >= nodePanelConf.value.minHeight) { 71 | nodePanelConf.value.height += Format.formatScale(mouseEvent.clientY - starting.y, panelConf.value.scale); 72 | starting.y = mouseEvent.clientY; 73 | } 74 | } 75 | }; 76 | const mouseUpFunc = (event: Event): void => { 77 | if (event instanceof MouseEvent) { 78 | panelElement?.removeEventListener("mousemove", resizeFunc); 79 | panelElement?.removeEventListener("mouseup", mouseUpFunc); 80 | } 81 | }; 82 | panelElement?.addEventListener("mousemove", resizeFunc); 83 | panelElement?.addEventListener("mouseup", mouseUpFunc); 84 | }; 85 | 86 | return { 87 | onRightDown, 88 | onMoveNodeDown, 89 | onResizeDown 90 | }; 91 | }; 92 | -------------------------------------------------------------------------------- /packages/js/animate/animate.ts: -------------------------------------------------------------------------------- 1 | import { FinishCallback, AnimateType, AnimateElement } from "@/js/animate/animateIntf"; 2 | 3 | /** 4 | * 计算线性动画指定进度的值 5 | * 6 | * @param element 动画元素 7 | * @param progress 进度(0~1) 8 | */ 9 | const liner = (element: AnimateElement, progress: number): void => { 10 | element.onValueChange(element.startValue + (element.endValue - element.startValue) * progress); 11 | }; 12 | 13 | /** 14 | * 计算缓入动画指定进度的值 15 | * 16 | * @param element 动画元素 17 | * @param progress 进度(0~1) 18 | */ 19 | const easyIn = (element: AnimateElement, progress: number): void => { 20 | element.onValueChange((element.endValue - element.startValue) * progress * progress + element.startValue); 21 | }; 22 | 23 | /** 24 | * 计算缓出动画指定进度的值 25 | * 26 | * @param element 动画元素 27 | * @param progress 进度(0~1) 28 | */ 29 | const easyOut = (element: AnimateElement, progress: number): void => { 30 | element.onValueChange((element.startValue - element.endValue) * (progress - 2) * progress + element.startValue); 31 | }; 32 | 33 | /** 34 | * 计算缓入缓出动画指定进度的值 35 | * 36 | * @param element 动画元素 37 | * @param progress 进度(0~1) 38 | */ 39 | const easyInEasyOut = (element: AnimateElement, progress: number): void => { 40 | const half = (element.startValue + element.endValue) / 2; 41 | const newElement = { ...element } as AnimateElement; 42 | if (progress < 0.5) { 43 | newElement.endValue = half; 44 | easyIn(newElement, progress * 2); 45 | } else { 46 | newElement.startValue = half; 47 | easyOut(newElement, (progress - 0.5) * 2); 48 | } 49 | }; 50 | 51 | declare type DoAnimateFunction = (element: AnimateElement, progress: number) => void; 52 | const ANIMATE_TYPE_MAP = new Map(); 53 | ANIMATE_TYPE_MAP.set(AnimateType.LINER, liner); 54 | ANIMATE_TYPE_MAP.set(AnimateType.EASY_IN, easyIn); 55 | ANIMATE_TYPE_MAP.set(AnimateType.EASY_OUT, easyOut); 56 | ANIMATE_TYPE_MAP.set(AnimateType.EASY_IN_EASY_OUT, easyInEasyOut); 57 | 58 | const ANIMATE_LIST = [] as AnimateElement[]; 59 | const FRAME_INTERVAL = 13 as number; 60 | 61 | let animateTimer = -1 as number; 62 | 63 | /** 64 | * 添加动画 65 | * 66 | * @param element 动画元素 67 | */ 68 | const push = (element: AnimateElement): void => { 69 | if (element.startTime <= 0) { 70 | element.startTime = new Date().getTime(); 71 | } 72 | ANIMATE_LIST.push(element); 73 | if (animateTimer !== -1) { 74 | return; // 若定时任务正在执行,则不需要新建定时任务 75 | } 76 | animateTimer = setInterval(() => { 77 | execute((error) => { 78 | if (!error) { 79 | clearInterval(animateTimer); 80 | animateTimer = -1; 81 | } 82 | }); 83 | }, FRAME_INTERVAL); // 每 FRAME_INTERVAL 毫秒执行一帧 84 | }; 85 | 86 | /** 87 | * 执行动画 88 | * 89 | * @param timerCleaner 所有动画元素全部执行完毕之后的回调 90 | */ 91 | const execute = (timerCleaner: FinishCallback): void => { 92 | for (let i = ANIMATE_LIST.length - 1; i >= 0; i--) { 93 | const item = ANIMATE_LIST[i]; 94 | const progress = (new Date().getTime() - item.startTime) / item.speed; 95 | if (progress < 0) { 96 | continue; // 支持延迟动画 97 | } 98 | const animateFunction = ANIMATE_TYPE_MAP.get(item.type); 99 | if (animateFunction !== undefined && animateFunction !== null) { 100 | animateFunction(item, Math.min(Math.max(progress, 0), 1)); 101 | } 102 | if (progress >= 1) { 103 | item.onValueChange(item.endValue); 104 | if (item.callback !== undefined && item.callback !== null) { 105 | item.callback(null); 106 | } 107 | ANIMATE_LIST.splice(i, 1); 108 | } 109 | } 110 | if (ANIMATE_LIST.length === 0) { 111 | timerCleaner(null); 112 | } 113 | }; 114 | 115 | export default { 116 | push 117 | }; 118 | -------------------------------------------------------------------------------- /packages/components/NeCompSvg/src/NeCompSvg.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 76 | 77 | 82 | -------------------------------------------------------------------------------- /packages/NePanel/src/NePanel.vue: -------------------------------------------------------------------------------- 1 | 67 | 68 | 72 | 73 | 76 | -------------------------------------------------------------------------------- /packages/NePanel/src/js/event/mouseEventProcessor.ts: -------------------------------------------------------------------------------- 1 | import { NePanelConf, PanelInfo } from "../interface/nePanelIntf"; 2 | import { getPanelInfoController } from "@/NePanel/src/js/controller/panelInfoController"; 3 | import Format from "@/NePanel/src/js/format"; 4 | 5 | import { NePanelConfigure } from "@/js/interface/NePanelConfigure"; 6 | import { Point } from "@/js/interface/2d/Point"; 7 | import { getNodeController } from "@/NePanel/src/js/controller/nodeController"; 8 | import { ConfigCallback, EventCallback, onMouseDown } from "@/js/event/eventProcesssor"; 9 | 10 | const scaleConfigure = { 11 | minValue: 0.01, 12 | maxValue: 50, 13 | speed: 0.1 14 | }; 15 | 16 | /** 17 | * 鼠标滚轮响应方法,对画布进行缩放 18 | * 19 | * @param event 鼠标事件 20 | * @param nePanelConf 面板属性 21 | */ 22 | const onMouseScrollFunc = (event: WheelEvent, nePanelConf: NePanelConf): void => { 23 | if (!(event.deltaY > 0 && nePanelConf.scale > scaleConfigure.minValue) 24 | && !(event.deltaY < 0 && nePanelConf.scale < scaleConfigure.maxValue)) { 25 | return; 26 | } 27 | const goalScale = Format.formatScaleNumber(nePanelConf.scale, event.deltaY > 0); 28 | const realX = Format.formatScale(nePanelConf.x + event.offsetX, nePanelConf.scale); 29 | const realY = Format.formatScale(nePanelConf.y + event.offsetY, nePanelConf.scale); 30 | nePanelConf.x += realX * (goalScale - nePanelConf.scale); 31 | nePanelConf.y += realY * (goalScale - nePanelConf.scale); 32 | nePanelConf.scale = goalScale; 33 | // 重新计算Grid网格 34 | nePanelConf.gridDef = Format.formatGrid(nePanelConf.scale); 35 | }; 36 | 37 | /** 38 | * 鼠标左键拖拽事件响应方法 39 | * 40 | * @param event 鼠标事件 41 | * @param starting 拖拽的起始坐标 42 | * @param nePanelConf 面板属性 43 | */ 44 | const onMouseLeftDrag = (event: MouseEvent, starting: Point, nePanelConf: NePanelConf): void => { 45 | console.log("onMouseLeftFrag", event); // TODO: 鼠标左键拖拽事件 46 | }; 47 | 48 | /** 49 | * 鼠标右键单击事件响应方法 50 | * 51 | * @param event 鼠标事件 52 | */ 53 | const onMouseRightClick = (event: MouseEvent): void => { 54 | console.log("onMouseRightClick", event); // TODO: 鼠标右键单击事件 55 | }; 56 | 57 | /** 58 | * 鼠标右键拖拽事件响应方法 59 | * 60 | * @param event 鼠标事件 61 | * @param starting 拖拽的起始坐标 62 | * @param nePanelConf 面板属性 63 | */ 64 | const onMouseRightDrag = (event: MouseEvent, starting: Point, nePanelConf: NePanelConf): void => { 65 | nePanelConf.x -= event.clientX - starting.x; 66 | nePanelConf.y -= event.clientY - starting.y; 67 | starting.x = event.clientX; 68 | starting.y = event.clientY; 69 | }; 70 | 71 | /** 72 | * 鼠标移动响应方法,当鼠标在面板上移动时触发,显示鼠标当前的世界坐标 73 | * 74 | * @param event 鼠标事件 75 | * @param nePanelConf 面板属性 76 | * @param panelInfo 面板信息 77 | */ 78 | const onMouseMoveFunc = (event: MouseEvent, nePanelConf: NePanelConf, panelInfo: PanelInfo): void => { 79 | panelInfo.mouse.realX = Format.formatScale(nePanelConf.x + event.offsetX, nePanelConf.scale); 80 | panelInfo.mouse.realY = Format.formatScale(nePanelConf.y + event.offsetY, nePanelConf.scale); 81 | }; 82 | 83 | /********************* 84 | ** Getter Function ** 85 | *********************/ 86 | 87 | /** 88 | * 鼠标事件响应方法 89 | * 90 | * @param nePanelConfigure 面板的全部信息 91 | */ 92 | export const getMouseEventProcessor = (nePanelConfigure: NePanelConfigure) => { 93 | const PanelInfoController = getPanelInfoController(nePanelConfigure.panelInfo); 94 | const NodeController = getNodeController(nePanelConfigure); 95 | 96 | /** 97 | * 鼠标滚轮响应方法,对画布进行缩放 98 | * 99 | * @param event 鼠标事件 100 | */ 101 | const onMouseScroll = (event: WheelEvent): void => { 102 | onMouseScrollFunc(event, nePanelConfigure.nePanelConf.value); 103 | PanelInfoController.showPanelInfo(); // 显示鼠标坐标 104 | }; 105 | 106 | /** 107 | * 鼠标左键事件响应方法 108 | * 109 | * @param event 110 | */ 111 | const onMouseLeftDown = (event: MouseEvent): void => { 112 | const onDragFunc: EventCallback = (event: MouseEvent, startPoint: Point): void => 113 | onMouseLeftDrag(event, startPoint, nePanelConfigure.nePanelConf.value); 114 | const onClickFunc: EventCallback = (): void => NodeController.resetSelectedStatus(); 115 | onMouseDown(event, null, onDragFunc, onClickFunc, null); 116 | }; 117 | 118 | /** 119 | * 鼠标右键事件响应方法 120 | * 121 | * @param event 122 | */ 123 | const onMouseRightDown = (event: MouseEvent): void => { 124 | const panelElement = nePanelConfigure.nePanel.value; 125 | const onDragFunc: EventCallback = (event: MouseEvent, startPoint: Point): void => { 126 | panelElement.style.cursor = "grab"; 127 | onMouseRightDrag(event, startPoint, nePanelConfigure.nePanelConf.value); 128 | }; 129 | const onClickFunc: EventCallback = (event: MouseEvent): void => onMouseRightClick(event); 130 | const onClearFunc: ConfigCallback = (): void => { 131 | panelElement.style.cursor = "inherit"; 132 | }; 133 | onMouseDown(event, null, onDragFunc, onClickFunc, onClearFunc); 134 | }; 135 | 136 | /** 137 | * 鼠标移动响应方法,当鼠标在面板上移动时触发,显示鼠标当前的世界坐标 138 | * 139 | * @param event 鼠标事件 140 | */ 141 | const onMouseMove = (event: MouseEvent): void => { 142 | onMouseMoveFunc(event, nePanelConfigure.nePanelConf.value, nePanelConfigure.panelInfo.value); 143 | PanelInfoController.showPanelInfo(); // 显示鼠标坐标 144 | }; 145 | 146 | return { 147 | onMouseScroll, 148 | onMouseLeftDown, 149 | onMouseRightDown, 150 | onMouseMove 151 | }; 152 | }; 153 | -------------------------------------------------------------------------------- /packages/NePanel/src/js/index.ts: -------------------------------------------------------------------------------- 1 | import Format from "./format"; 2 | import Animate from "@/js/animate/animate"; 3 | import { AnimateType, AnimateElement } from "@/js/animate/animateIntf"; 4 | 5 | import { getBrowser } from "@/js/browser"; 6 | import { getMouseEventProcessor } from "@/NePanel/src/js/event/mouseEventProcessor"; 7 | import { getPanelInfoController } from "@/NePanel/src/js/controller/panelInfoController"; 8 | import { NePanelInitIntf } from "@/js/interface/NePanelInitIntf"; 9 | 10 | import { Component, defineComponent, markRaw, onMounted, PropType, provide, ref } from "vue"; 11 | 12 | import NeCompSvg from "@/components/NeCompSvg"; 13 | import NeSplitPanel from "@/components/NeSplitPanel"; 14 | import NeListNode from "@/components/NeListNode"; 15 | import NeDetailPanel from "@/components/NeDetailPanel"; 16 | 17 | import { componentList } from "@/nodes"; 18 | import { NePanelConfigure } from "@/js/interface/NePanelConfigure"; 19 | import { getSubEventProcessor } from "@/NePanel/src/js/event/subEventProcessor"; 20 | import { getDragEventProcessor } from "@/NePanel/src/js/event/dragEventProcessor"; 21 | import StaticNodeController from "@/NePanel/src/js/controller/nodeController"; 22 | import { NeNodeExportEx } from "@/NePanel/src/js/interface/NeNodeExportEx"; 23 | 24 | export default defineComponent({ 25 | name: "ne-panel", 26 | components: { 27 | NeCompSvg: markRaw(NeCompSvg), 28 | NeSplitPanel: markRaw(NeSplitPanel), 29 | NeListNode: markRaw(NeListNode), 30 | NeDetailPanel: markRaw(NeDetailPanel) 31 | }, 32 | props: { 33 | init: { 34 | type: Array as PropType, 35 | required: false, 36 | default: [] as NePanelInitIntf[] 37 | } 38 | }, 39 | directives: { 40 | resize: { 41 | mounted(el, bindings) { 42 | const debounce = (callback: Function): ResizeObserverCallback => { 43 | return () => { 44 | callback.apply(this, []); 45 | }; 46 | }; 47 | el._resizer = new window.ResizeObserver(debounce(bindings.value)); 48 | el._resizer.observe(el); 49 | }, 50 | unmounted(el) { 51 | el._resizer.disconnect(); 52 | } 53 | } 54 | }, 55 | setup: (propsData) => { 56 | onMounted(() => { 57 | console.log(getBrowser()); 58 | initPanelSize(); 59 | reCalcGrid(); 60 | }); 61 | 62 | /** 63 | * Parameters 64 | */ 65 | const nePanel = ref(); 66 | const nePanelConf = ref({ 67 | x: -100, 68 | y: -100, 69 | width: 200, 70 | height: 200, 71 | gridDef: { 72 | largeGridSize: 0, 73 | smallGridSize: 0 74 | }, 75 | scale: 1 76 | }); 77 | const panelInfo = ref({ 78 | ready: false, 79 | show: false, 80 | delay: 1000, 81 | timer: -1, 82 | mouse: { 83 | realX: 0, 84 | realY: 0 85 | } 86 | }); 87 | const nodeDrag = ref({ 88 | dragging: false, 89 | dragInfo: "拖动到此处以添加节点" 90 | }); 91 | const rightContent = ref({ 92 | solutionValue: "" 93 | }); 94 | const components = ref(StaticNodeController.initComponentList(propsData.init)); 95 | const rightElement = ref(undefined); 96 | const SCALE_ANIMATE_SPEED = 300; 97 | 98 | /******************** 99 | * Provide Params * 100 | ********************/ 101 | 102 | provide("panelConf", nePanelConf); 103 | 104 | /********************* 105 | * Local Functions * 106 | *********************/ 107 | 108 | /** 109 | * 初始化主面板和Svg画布尺寸 110 | */ 111 | const initPanelSize = () : void => { 112 | const nePanelElement = nePanel.value; 113 | if (nePanelElement === undefined) { 114 | return; 115 | } 116 | nePanelConf.value.x = nePanelElement.offsetWidth / -2; 117 | nePanelConf.value.y = nePanelElement.offsetHeight / -2; 118 | nePanelConf.value.width = nePanelElement.offsetWidth; 119 | nePanelConf.value.height = nePanelElement.offsetHeight; 120 | }; 121 | 122 | /** 123 | * 判断视图是否处于初始状态 124 | * 125 | * @returns {Boolean} 视图是否处于初始状态 126 | */ 127 | const isInitialState = (): boolean => { 128 | return nePanelConf.value.scale === 1 129 | && nePanelConf.value.x === nePanelConf.value.width / -2 130 | && nePanelConf.value.y === nePanelConf.value.height / -2; 131 | }; 132 | 133 | /** 134 | * 重新计算主面板和Svg画布尺寸 135 | */ 136 | const reCalcPanelSize = (): void => { 137 | const nePanelElement = nePanel.value; 138 | if (nePanelElement === undefined) { 139 | return; 140 | } 141 | nePanelConf.value.x -= (nePanelElement.offsetWidth - nePanelConf.value.width) / 2; 142 | nePanelConf.value.y -= (nePanelElement.offsetHeight - nePanelConf.value.height) / 2; 143 | nePanelConf.value.width = nePanelElement.offsetWidth; 144 | nePanelConf.value.height = nePanelElement.offsetHeight; 145 | PanelInfoController.showPanelInfo(); 146 | }; 147 | 148 | /** 149 | * 重置缩放倍率 150 | */ 151 | const resetScale = (): void => { 152 | const timeNow = new Date().getTime(); 153 | Animate.push({ 154 | startValue: nePanelConf.value.scale, 155 | endValue: 1, 156 | startTime: timeNow, 157 | speed: SCALE_ANIMATE_SPEED, 158 | type: AnimateType.EASY_IN_EASY_OUT, 159 | onValueChange: (value) => { nePanelConf.value.scale = value; reCalcGrid(); PanelInfoController.showPanelInfo(); }, 160 | callback: null 161 | } as AnimateElement); 162 | 163 | Animate.push({ 164 | startValue: nePanelConf.value.x, 165 | endValue: nePanelConf.value.width / -2, 166 | startTime: timeNow, 167 | speed: SCALE_ANIMATE_SPEED, 168 | type: AnimateType.EASY_IN_EASY_OUT, 169 | onValueChange: (value) => { nePanelConf.value.x = value; }, 170 | callback: null 171 | } as AnimateElement); 172 | 173 | Animate.push({ 174 | startValue: nePanelConf.value.y, 175 | endValue: nePanelConf.value.height / -2, 176 | startTime: timeNow, 177 | speed: SCALE_ANIMATE_SPEED, 178 | type: AnimateType.EASY_IN_EASY_OUT, 179 | onValueChange: (value) => { nePanelConf.value.y = value; }, 180 | callback: null 181 | } as AnimateElement); 182 | }; 183 | 184 | /** 185 | * 重新计算网格参数 186 | */ 187 | const reCalcGrid = (): void => { 188 | nePanelConf.value.gridDef = Format.formatGrid(nePanelConf.value.scale); 189 | }; 190 | 191 | /** 192 | * 数值转换方法,在缩放坐标的同时保证保证线宽、尺寸等值不变 193 | * 194 | * @param number 期望显示出来的尺寸数值 195 | * @return 元素在svg画布中的宽度 196 | */ 197 | const formatScale = (number: number): number => { 198 | return Format.formatScale(number, nePanelConf.value.scale); 199 | }; 200 | 201 | /************************ 202 | * Imported Functions * 203 | ************************/ 204 | 205 | const configureParam = { 206 | nePanel: nePanel, 207 | nePanelConf: nePanelConf, 208 | panelInfo: panelInfo, 209 | componentList: components, 210 | rightElement: rightElement 211 | } as NePanelConfigure; 212 | 213 | const PanelInfoController = getPanelInfoController(panelInfo); 214 | const MouseEventProcessor = getMouseEventProcessor(configureParam); 215 | const SubEventProcessor = getSubEventProcessor(rightContent, configureParam); 216 | const DragEventProcessor = getDragEventProcessor(nodeDrag, configureParam); 217 | 218 | return { 219 | nePanel, 220 | nePanelConf, 221 | panelInfo, 222 | nodeDrag, 223 | components, 224 | rightContent, 225 | rightElement, 226 | componentList, 227 | isInitialState, 228 | reCalcPanelSize, 229 | resetScale, 230 | formatScale, 231 | MouseEventProcessor, 232 | SubEventProcessor, 233 | DragEventProcessor 234 | }; 235 | } 236 | }); 237 | --------------------------------------------------------------------------------