├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── bug_report_zh.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ci.yaml ├── .gitignore ├── .npmignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── element ├── core │ ├── .npmignore │ ├── package.json │ ├── scripts │ │ ├── build.js │ │ └── dev.js │ ├── source │ │ ├── element.ts │ │ ├── index.ts │ │ └── theme.ts │ ├── test │ │ └── base.spec.js │ └── tsconfig.json ├── graph │ ├── .npmignore │ ├── README.MD │ ├── design │ │ └── event.md │ ├── example │ │ ├── class-data.esm.js │ │ ├── class.html │ │ ├── custom-data.esm.js │ │ ├── custom.html │ │ ├── flow-data.esm.js │ │ └── flow.html │ ├── package.json │ ├── scripts │ │ ├── build.js │ │ └── dev.js │ ├── source │ │ ├── element │ │ │ ├── event-interface.ts │ │ │ ├── graph-line.ts │ │ │ ├── graph-node-param.ts │ │ │ ├── graph-node.ts │ │ │ ├── graph │ │ │ │ ├── data.ts │ │ │ │ ├── index.ts │ │ │ │ └── utils.ts │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ ├── event.ts │ │ ├── index.ts │ │ ├── interface.ts │ │ ├── manager.ts │ │ └── theme │ │ │ ├── class-diagram.ts │ │ │ └── flow-chart.ts │ ├── test │ │ └── base.spec.js │ └── tsconfig.json └── tree │ ├── .npmignore │ ├── README.MD │ ├── example │ ├── custom.esm.js │ ├── custom.html │ ├── list.esm.js │ ├── list.html │ ├── tree.esm.js │ └── tree.html │ ├── package.json │ ├── scripts │ ├── build.js │ └── dev.js │ ├── source │ ├── element-item.ts │ ├── element-tree.ts │ ├── index.ts │ ├── interface.ts │ └── utils.ts │ ├── test │ └── base.spec.js │ └── tsconfig.json ├── package.json └── scripts ├── build.js ├── server.js ├── server ├── index.html └── public │ ├── base.css │ ├── index.css │ └── item.css └── test.js /.eslintignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itharbors/ui/93f93e9bb1574f8d821a8fcaa8ec0cdf7bef4c05/.eslintignore -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['prettier'], 3 | extends: ['plugin:prettier/recommended'], 4 | }; -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | 15 | Steps to reproduce the behavior: 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | 21 | **Expected behavior** 22 | 23 | A clear and concise description of what you expected to happen. 24 | 25 | **Screenshots** 26 | 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | **Desktop (please complete the following information):** 30 | 31 | - OS: [e.g. iOS] 32 | - Browser [e.g. chrome, safari] 33 | - Version [e.g. 22] 34 | 35 | **Additional context** 36 | 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report_zh.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 问题反馈 3 | about: 创建报告以帮助我们改进 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | --- 8 | 9 | **描述错误** 10 | 11 | 对 bug 的简要概述。 12 | 13 | **复现** 14 | 15 | 再现行为的步骤: 16 | 1. 转到“…” 17 | 2. 单击“….” 18 | 3. 向下滚动至“….” 19 | 4. 参见错误 20 | 21 | **预期行为** 22 | 23 | 对你期望发生的事情进行清晰且简要的概述。 24 | 25 | **屏幕截图** 26 | 27 | 如果可以,请添加屏幕截图以帮助解释您的问题。 28 | 29 | **桌面(请填写以下信息):** 30 | 31 | - 操作系统:[例如iOS] 32 | - 系统版本:[例如windows11] 33 | - 浏览器:[例如chrome、safari] 34 | - 浏览器版本:[例如100] 35 | 36 | **附加上下文** 37 | 38 | 在此处添加有关该问题的任何其他上下文。 39 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | ## PR 类型 3 | 4 | - [ ] 问题修复 5 | - [ ] 优化 6 | - [ ] 新功能 7 | - [ ] 测试用例 8 | 9 | 10 | ## PR 描述 11 | 12 | 13 | ### 修改说明 14 | 1. 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: {} 5 | merge_group: 6 | pull_request: 7 | types: 8 | - opened 9 | - synchronize 10 | - ready_for_review 11 | paths: 12 | - 'source/**.ts' 13 | - 'source/scripts/*.js' 14 | - '.github/workfows/ci.yaml' 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | check-tests: 21 | runs-on: ubuntu-latest 22 | strategy: 23 | matrix: 24 | node-version: [18.x] 25 | steps: 26 | - uses: actions/checkout@v3 27 | 28 | - name: Setup Node.js ${{ matrix.node-version }} 29 | uses: actions/setup-node@v3 30 | with: 31 | node-version: ${{ matrix.node-version }} 32 | 33 | - name: Run CI 34 | run: npm run ci 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 忽略操作系统生成的文件 2 | .DS_Store 3 | Thumbs.db 4 | 5 | # 忽略 Node.js 生成的文件夹 6 | node_modules/ 7 | npm-debug.log 8 | yarn-error.log 9 | 10 | # 忽略本地配置文件(例如,配置文件包含敏感信息) 11 | config.js 12 | .env 13 | 14 | # 忽略依赖管理文件(可以用于避免冲突) 15 | package-lock.json 16 | yarn.lock 17 | 18 | # 忽略构建输出和临时文件 19 | dist/ 20 | bundle/ 21 | build/ 22 | .tmp/ 23 | .cache/ 24 | 25 | # 忽略日志文件 26 | logs/ 27 | *.log 28 | npm-debug.log* 29 | 30 | # 忽略测试结果 31 | coverage/ 32 | 33 | # 忽略编辑器生成的文件 34 | .vscode/ 35 | .idea/ 36 | *.swp 37 | 38 | # 忽略自动生成的文档和注释 39 | docs/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # 使用 .gitignore 中的规则排除不需要包含在 npm 包中的文件 2 | /source 3 | /test 4 | /script 5 | 6 | # 如果需要在 npm 包中包含特定文件,请在下面取消注释并添加文件路径 7 | !/build 8 | !/dist -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tabWidth: 4, 3 | singleQuote: true, 4 | }; 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 itharbors 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UI 2 | 3 | [![CI Status](https://github.com/itharbors/ui/actions/workflows/ci.yaml/badge.svg)](https://github.com/itharbors/ui/actions/workflows/ci.yaml) 4 | ![Typescript](https://img.shields.io/badge/Language-Typescript-blue.svg) 5 | 6 | 一个通用的 Web UI 库。包含一些常用的基础组件,以及一些较为复杂的功能组件 7 | 8 | ## Install 9 | 10 | ```bash 11 | npm install @itharbors/ui 12 | ``` 13 | 14 | ## Usage 15 | 16 | ### core 17 | [![NPM](https://img.shields.io/npm/v/@itharbors/ui-core)](https://www.npmjs.com/package/@itharbors/ui-core) 18 | 19 | 这是一个没有功能的元素,所有 ui 元素都从这里开始创建 20 | 21 | ```bash 22 | npm install @itharbors/ui-core 23 | ``` 24 | 25 | ### graph 26 | [![NPM](https://img.shields.io/npm/v/@itharbors/ui-graph)](https://www.npmjs.com/package/@itharbors/ui-graph) 27 | 28 | 图组件,用于绘制简单的流程图、类图 29 | 30 | ```bash 31 | npm install @itharbors/ui-graph 32 | ``` 33 | 34 | ### tree 35 | [![NPM](https://img.shields.io/npm/v/@itharbors/ui-tree)](https://www.npmjs.com/package/@itharbors/ui-tree) 36 | 37 | 基于原生的树形组件 38 | 39 | ```bash 40 | npm install @itharbors/ui-tree 41 | ``` 42 | 43 | ## Develop 44 | 45 | clone 仓库后,需要安装依赖仓库 46 | 47 | ```bash 48 | npm install 49 | ``` 50 | 51 | 编译仓库代码 52 | 53 | ```bash 54 | npm run build 55 | ``` 56 | 57 | 启动网页预览、运行测试用例 58 | 59 | ```bash 60 | npm run server 61 | ``` 62 | 63 | 如果开发过程中以及准备提交代码的时候需要测试,则执行 64 | 65 | ```bash 66 | npm run test 67 | ``` 68 | 69 | ## Contributing 70 | 71 | Please contribute! [Look at the issues.](https://github.com/itharbors/ui/issues) 72 | -------------------------------------------------------------------------------- /element/core/.npmignore: -------------------------------------------------------------------------------- 1 | /design 2 | /scripts 3 | /playwright 4 | /source 5 | /test 6 | /example 7 | /tsconfig.json 8 | .* -------------------------------------------------------------------------------- /element/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@itharbors/ui-core", 3 | "version": "1.0.2", 4 | "description": "", 5 | "main": "./dist/index.js", 6 | "scripts": { 7 | "dev": "node ./scripts/dev", 8 | "build": "node ./scripts/build" 9 | }, 10 | "author": "VisualSJ", 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "license": "ISC" 15 | } 16 | -------------------------------------------------------------------------------- /element/core/scripts/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { spawn } = require('child_process'); 4 | 5 | const spawnAsync = function(...cmd) { 6 | return new Promise((resolve) => { 7 | const child = spawn('npx', [...cmd], { 8 | stdio: [0, 1, 2], 9 | }); 10 | child.on('exit', () => { 11 | resolve(); 12 | }); 13 | }); 14 | }; 15 | 16 | const exec = async function() { 17 | await spawnAsync('tsc'); 18 | }; 19 | 20 | exec(); 21 | -------------------------------------------------------------------------------- /element/core/scripts/dev.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { spawn } = require('child_process'); 4 | 5 | const spawnAsync = function(...cmd) { 6 | return new Promise((resolve) => { 7 | const child = spawn('npx', [...cmd], { 8 | stdio: [0, 1, 2], 9 | }); 10 | child.on('exit', () => { 11 | resolve(); 12 | }); 13 | }); 14 | }; 15 | 16 | const exec = async function() { 17 | await spawnAsync('tsc', '-w'); 18 | }; 19 | 20 | exec(); 21 | -------------------------------------------------------------------------------- /element/core/source/element.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * 框架基础元素 5 | * 其他元素都继承自这个元素,职责包含: 6 | * 1. 管理定义生命周期函数 7 | * 2. 数据管理(data、attribute) 8 | */ 9 | export class BaseElement extends HTMLElement { 10 | protected DEBUG = true; 11 | 12 | /** 13 | * @description 监听 attribute 修改,需要继承的元素自己重写这个方法 14 | */ 15 | static get observedAttributes(): string[] { 16 | return []; 17 | } 18 | 19 | /** 20 | * 元素 shadowDOM 内的 HTML 21 | */ 22 | protected get HTMLTemplate(): string { 23 | return ''; 24 | }; 25 | 26 | /** 27 | * 元素 shadowDOM 内的 STYLE 28 | */ 29 | protected get HTMLStyle(): string { 30 | return ''; 31 | }; 32 | 33 | /** 34 | * 默认数据,这份数据需要完整的给出定义 35 | * 会在 getProperty 的时候自动识别类型 36 | */ 37 | get defaultData(): { 38 | [key: string]: object | number | string | boolean | null; 39 | } { 40 | return {}; 41 | } 42 | 43 | /** 44 | * 在这个 UI 元素内查询一个内部元素 45 | * @param selector 46 | * @returns 47 | */ 48 | deepQuerySelector(selector: string) { 49 | return this.shadowRoot.querySelector(selector); 50 | } 51 | 52 | /** 53 | * 在这个 UI 元素内查询所有符合条件的内部元素 54 | * @param selector 55 | * @returns 56 | */ 57 | deepQueryInternalSelectorAll(selector: string) { 58 | return this.shadowRoot.querySelectorAll(selector); 59 | } 60 | 61 | /** 62 | * 获取一个存储的属性 63 | * @param key 64 | * @returns 65 | */ 66 | getProperty(key: K): this['defaultData'][K] { 67 | return this.data.getProperty(key); 68 | } 69 | 70 | /** 71 | * 设置一个属性的值 72 | * @param key 73 | * @param value 74 | * @returns 75 | */ 76 | setProperty(key: K, value: this['defaultData'][K]) { 77 | return this.data.setProperty(key, value); 78 | } 79 | 80 | /** 81 | * 触发一个自定义事件 82 | * @param eventName 83 | * @param options 84 | */ 85 | dispatch(eventName: string, options?: EventInit & { detail: T }) { 86 | const targetOptions = { 87 | bubbles: true, 88 | cancelable: true, 89 | }; 90 | if (options) { 91 | Object.assign(targetOptions, options); 92 | } 93 | const event = new CustomEvent(eventName, targetOptions); 94 | this.dispatchEvent(event); 95 | } 96 | 97 | protected initialize() { 98 | this.shadowRoot.innerHTML = `${this.HTMLTemplate}`; 99 | // for (let key in this.listener.attrs) { 100 | // this.data.addAttributeListener(key, this.listener.attrs[key]); 101 | // } 102 | 103 | this.onInit(); 104 | if (this.isConnected) { 105 | this.onMounted(); 106 | } 107 | } 108 | 109 | protected onInit() {}; 110 | protected onMounted() {}; 111 | protected onRemoved() {}; 112 | 113 | public data = new DataManager(this, this.defaultData); 114 | 115 | public shadowRoot!: ShadowRoot; 116 | 117 | constructor() { 118 | super(); 119 | this.attachShadow({ mode: 'open' }); 120 | this.initialize(); 121 | } 122 | 123 | attributeChangedCallback(key: string, legacy: string, value: string) { 124 | this.data.emitAttribute(key, value, legacy); 125 | } 126 | 127 | connectedCallback() { 128 | this.onMounted(); 129 | } 130 | 131 | disconnectedCallback() { 132 | this.onRemoved(); 133 | } 134 | } 135 | 136 | class DataManager { 137 | private root: BaseElement; 138 | 139 | public stash: T['defaultData']; 140 | 141 | constructor(root: T, data: T['defaultData']) { 142 | this.root = root; 143 | const stash: Partial = {}; 144 | 145 | for (let key in data) { 146 | stash[key] = JSON.parse(JSON.stringify(data[key])); 147 | } 148 | this.stash = stash as T['defaultData']; 149 | } 150 | 151 | private propertyEventMap: Partial void)[]>> = {}; 152 | touchProperty(key: K) { 153 | const legacy = this.getProperty(key); 154 | this.emitProperty(key, legacy, legacy); 155 | } 156 | getProperty(key: K): T['defaultData'][K] { 157 | return this.stash[key]; 158 | } 159 | setProperty(key: K, value: T['defaultData'][K]) { 160 | const legacy = this.stash[key]; 161 | if (this.stash[key] === value) { 162 | return; 163 | } 164 | this.stash[key] = value; 165 | this.emitProperty(key, value, legacy); 166 | } 167 | addPropertyListener(key: K, handle: (value: T['defaultData'][K], legacy: T['defaultData'][K]) => void) { 168 | const list = this.propertyEventMap[key] = this.propertyEventMap[key] || []; 169 | list.push(handle); 170 | } 171 | removePropertyListener(key: K, handle: (value: T['defaultData'][K], legacy: T['defaultData'][K]) => void) { 172 | const list = this.propertyEventMap[key]; 173 | if (!list) { 174 | return; 175 | } 176 | const index = list.indexOf(handle); 177 | if (index !== -1) { 178 | list.splice(index, 1); 179 | } 180 | } 181 | emitProperty(key: K, value: T['defaultData'][K], legacy: T['defaultData'][K]) { 182 | const list = this.propertyEventMap[key]; 183 | if (!list) { 184 | return; 185 | } 186 | list.forEach((func) => { 187 | func.call(this.root, value, legacy); 188 | }); 189 | } 190 | 191 | private attributeEventMap: { [key: string]: ((value: any, legacy: any) => void)[]} = {}; 192 | touchAttribute(key: string) { 193 | const legacy = this.getAttribute(key); 194 | this.emitAttribute(key, legacy, legacy); 195 | } 196 | getAttribute(key: string) { 197 | return this.root.getAttribute(key) || ''; 198 | } 199 | setAttribute(key: string, value: string) { 200 | const legacy = this.getAttribute(key); 201 | this.root.setAttribute(key, value); 202 | this.emitAttribute(key, value, legacy); 203 | } 204 | addAttributeListener(key: string, handle: (value: string, legacy: string) => void) { 205 | const list = this.attributeEventMap[key] = this.attributeEventMap[key] || []; 206 | list.push(handle); 207 | } 208 | removeAttributeListener(key: string, handle: (value: any, legacy: any) => void) { 209 | const list = this.attributeEventMap[key]; 210 | if (!list) { 211 | return; 212 | } 213 | const index = list.indexOf(handle); 214 | if (index !== -1) { 215 | list.splice(index, 1); 216 | } 217 | } 218 | emitAttribute(key: string, value: string, legacy: string) { 219 | const list = this.attributeEventMap[key]; 220 | if (!list) { 221 | return; 222 | } 223 | list.forEach((func) => { 224 | func.call(this.root, value, legacy); 225 | }); 226 | } 227 | 228 | // setSchema(schema: any) {} 229 | } 230 | -------------------------------------------------------------------------------- /element/core/source/index.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import './theme.js'; 4 | import { BaseElement } from './element.js'; 5 | 6 | export { BaseElement }; 7 | 8 | export class CustomElementOption { 9 | template: string = ''; 10 | style: string = ''; 11 | 12 | static attrListenList: string[] = []; 13 | 14 | attrs: { 15 | [key: string]: (this: BaseElement, value: string, legacy: string) => void; 16 | } = {}; 17 | 18 | data: { 19 | [key: string]: string | number | boolean | object; 20 | } = {}; 21 | 22 | methods: { 23 | [key: string]: (...args: any[]) => void; 24 | } = {}; 25 | 26 | element: BaseElement; 27 | constructor(elem: BaseElement) { 28 | this.element = elem; 29 | } 30 | } 31 | 32 | export function registerElement(name: string, element: typeof BaseElement) { 33 | window.customElements.define(`v-${name}`, element); 34 | } 35 | 36 | export const style = { 37 | /** 38 | * 实心样式 39 | */ 40 | solid: /*css*/` 41 | :host { 42 | --background-color: var(--ui-color-default); 43 | --font-color: var(--ui-color-default-contrast); 44 | --border-color: var(--ui-color-default-line); 45 | } 46 | :host([color="primary"]) { 47 | --background-color: var(--ui-color-primary); 48 | --font-color: var(--ui-color-primary-contrast); 49 | --border-color: var(--ui-color-primary-line); 50 | } 51 | :host([color="success"]) { 52 | --background-color: var(--ui-color-success); 53 | --font-color: var(--ui-color-success-contrast); 54 | --border-color: var(--ui-color-success-line); 55 | } 56 | :host([color="danger"]) { 57 | --background-color: var(--ui-color-danger); 58 | --font-color: var(--ui-color-danger-contrast); 59 | --border-color: var(--ui-color-danger-line); 60 | } 61 | :host([color="warn"]) { 62 | --background-color: var(--ui-color-warn); 63 | --font-color: var(--ui-color-warn-contrast); 64 | --border-color: var(--ui-color-warn-line); 65 | } 66 | :host([disabled]) { 67 | opacity: 0.4; 68 | } 69 | :host([disabled]), :host([readonly]) { 70 | cursor: not-allowed; 71 | } 72 | `, 73 | 74 | /** 75 | * 空心样式 76 | */ 77 | hollow: /*css*/` 78 | :host { 79 | --background-color: transparent; 80 | --font-color: var(--ui-color-default-contrast); 81 | --border-color: var(--ui-color-default-line); 82 | } 83 | :host([color="primary"]) { 84 | --font-color: var(--ui-color-primary); 85 | --border-color: var(--ui-color-primary-line); 86 | } 87 | :host([color="success"]) { 88 | --font-color: var(--ui-color-success); 89 | --border-color: var(--ui-color-success-line); 90 | } 91 | :host([color="danger"]) { 92 | --font-color: var(--ui-color-danger); 93 | --border-color: var(--ui-color-danger-line); 94 | } 95 | :host([color="warn"]) { 96 | --font-color: var(--ui-color-warn); 97 | --border-color: var(--ui-color-warn-line); 98 | } 99 | :host([disabled]) { 100 | opacity: 0.4; 101 | } 102 | :host([disabled]), :host([readonly]) { 103 | cursor: not-allowed; 104 | } 105 | `, 106 | 107 | line: /*css*/` 108 | :host { 109 | display: inline-flex; 110 | 111 | --line-height: calc(var(--ui-size-line) * 1px); 112 | --font-size: calc(var(--ui-size-font) * 1px); 113 | 114 | --padding-row: calc((var(--ui-size-line) - var(--ui-size-font)) * 0.8px); 115 | --padding-column: calc((var(--ui-size-line) - var(--ui-size-font)) * 0.2px); 116 | } 117 | `, 118 | }; 119 | -------------------------------------------------------------------------------- /element/core/source/theme.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const $style = document.createElement('style'); 4 | $style.innerHTML = /*css*/` 5 | body { 6 | /* 基础颜色 */ 7 | --ui-color-default: #fff; 8 | --ui-color-primary: #1677ff; 9 | --ui-color-success: #00b578; 10 | --ui-color-danger: #ff3141; 11 | --ui-color-warn: #ff8f1f; 12 | 13 | /* 基础颜色作为背景色的时候,需要一个对比色 */ 14 | --ui-color-default-contrast: #333; 15 | --ui-color-primary-contrast: #fff; 16 | --ui-color-success-contrast: #fff; 17 | --ui-color-danger-contrast: #fff; 18 | --ui-color-warn-contrast: #fff; 19 | 20 | /* 基础颜色作为背景色的时候,需要一个对比色 */ 21 | --ui-color-default-line: #333; 22 | --ui-color-primary-line: #1677ff; 23 | --ui-color-success-line: #00b578; 24 | --ui-color-danger-line: #ff3141; 25 | --ui-color-warn-line: #ff8f1f; 26 | 27 | --ui-size-line: 24; 28 | --ui-size-font: 12; 29 | --ui-size-radius: 4; 30 | 31 | --ui-anim-duration: 0.3s; 32 | } 33 | `; 34 | 35 | document.head.appendChild($style); 36 | -------------------------------------------------------------------------------- /element/core/test/base.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; -------------------------------------------------------------------------------- /element/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "commonjs", /* Specify what module code is generated. */ 28 | "rootDir": "./source", /* Specify the root folder within your source files. */ 29 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | // "resolveJsonModule": true, /* Enable importing .json files */ 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 50 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 51 | // "removeComments": true, /* Disable emitting comments. */ 52 | // "noEmit": true, /* Disable emitting files from a compilation. */ 53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 68 | 69 | /* Interop Constraints */ 70 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 71 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 72 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 73 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 74 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 75 | 76 | /* Type Checking */ 77 | "strict": true, /* Enable all strict type-checking options. */ 78 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 79 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 80 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 81 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 82 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 83 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 84 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 85 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 86 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 87 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 88 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 89 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 90 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 91 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 92 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 93 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 94 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 95 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 96 | 97 | /* Completeness */ 98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 99 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /element/graph/.npmignore: -------------------------------------------------------------------------------- 1 | /design 2 | /scripts 3 | /playwright 4 | /source 5 | /test 6 | /example 7 | /tsconfig.json 8 | .* -------------------------------------------------------------------------------- /element/graph/README.MD: -------------------------------------------------------------------------------- 1 | # UI Graph 2 | 3 | Graph 图组件 4 | 5 | 图组件是一个较为复杂的 UI 组件,主要用于制作流程图、框架图、组织架构图等的基础组件。 6 | 7 | 为了满足多种图的需求,我们需要确定出公共功能,将这些功能组合到 Graph 内。通过对上述功能的调研,我们拆分出了以下功能点: 8 | 9 | 1. 基础 Viewer,允许移动、缩放、控制网格样式、背景样式 10 | 2. 基础 Node,允许选中(可控制)、移动、自定义内容、自定义输入输出参数 11 | 3. 基础 Node-Param,允许控制一个 Node 的输入/输出样式、位置、方向(例如参数在右侧,曲线需要先向右移动一定距离) 12 | 4. 基础 Line,允许选中(可控制)、自定义连线样式、自定义连线内容 13 | 14 | ## 概要 15 | 16 | 因为图是一个基础功能,从图里可以衍生出很多不同的业务场景,所以技术层面主要考虑可扩展性,满足未来定制需求。初期满足产品概要内的几种图形式。 17 | 18 | 目标: 19 | 20 | - 满足未来多种 Graph 的显示(初期满足 animGraph、自定义材质编辑器、UML 类图需求) 21 | 22 | - 节点定制 23 | - [x] 输入/输出为横向的图 24 | - [x] 输入/输出为竖向的图 25 | - [x] 无输入输出的图 26 | - [x] 自定义图内节点的内容 27 | - [x] 自定义图内参数的样式、位置 28 | - [ ] 可选中节点 29 | - [ ] 框选节点 30 | - [x] 可控制节点可拖拽移动的区域 31 | - [ ] 可控制节点是否可选中 32 | 33 | - 连接线定制 34 | - [x] 自定义图内连接线的内容 35 | - [ ] 可选中连接线 36 | - [ ] 框选连接线 37 | - [x] 不同图允许不同的连接线规则(例如鼠标按下开始拖拽或者单击节点开始拖拽) 38 | - [ ] 可控制连接线是否可选中 39 | - [ ] 可定制多条连接线布局 40 | - [ ] 可控制是否允许新建连接线 41 | 42 | - 性能验收指标 43 | - [x] 一个图内,同时显示 1k 个节点操作不卡顿 44 | - [x] 同屏显示 4 个 1k 节点的 Graph 不卡顿 45 | 46 | - 其他功能 47 | - [x] 同一页面,显示多种不同 Graph 48 | - [x] 制作了一种 Graph 后,方便其他开发者使用定制好的 Graph 49 | - [ ] 子图嵌套 50 | 51 | 为了达到兼容各种图的目的,我们需要内置几个管理器: 52 | 53 | 1. Graph 管理器,可以预设 Graph,以及部分限制条件函数,设置好后,其他地方可以直接使用 type attribute,给数据就自动渲染成对应的图 54 | 2. NodeManager,给每种 Node 分配一个 Type,根据 Type 决定 Node 应该如何渲染,不同 Graph 类型需要隔离 55 | 3. LineManager,给每种 Line 分配一个 Type,根据 Type 决定 Line 应该如何渲染,不同 Graph 类型需要隔离 56 | 57 | ### 基础程序结构 58 | 59 | ```mermaid 60 | classDiagram 61 | class GraphElement { 62 | +scale: number; 63 | ++option: GraphOption; 64 | +offset: Position; 65 | +calibration: Position; 66 | +nodes: Node[]; 67 | +lines: Line[]; 68 | 69 | +createNode(nodeInfo): void; 70 | +createLine(lineInfo): void; 71 | +startMoveNode(node): void; 72 | +stopMoveNode(node?): void; 73 | +startConnect(type, node, param?): void; 74 | +stopConnect(): void; 75 | } 76 | class NodeElement { 77 | +type: string; 78 | +details: Object; 79 | +position: Position; 80 | 81 | +startMove(): void; 82 | +stopMove(): void; 83 | +startConnect(type, param?): void; 84 | +stopConnect(): void; 85 | } 86 | class NodeParamElement { 87 | +name: string; 88 | +direction: string; 89 | +type: string; 90 | +role: string; 91 | 92 | +startConnect(type): void; 93 | +stopConnect(): void; 94 | } 95 | class LineElement { 96 | +type: string; 97 | +details: Object; 98 | +input: LineParam; 99 | +output: LineParam; 100 | } 101 | 102 | GraphElement *-- NodeElement 103 | NodeElement *-- NodeParamElement 104 | GraphElement *-- LineElement 105 | ``` 106 | 107 | ### 基础渲染流程 108 | 109 | ```mermaid 110 | flowchart TD 111 | A001[设置 Graph 数据] 112 | A002[设置 Mesh 数据] 113 | A003[设置/更新 Nodes 数据] 114 | A004[设置/更新 Lines 数据] 115 | 116 | B001[更新 Graph 渲染的缓存数据] 117 | B002[重绘 Mesh] 118 | B003[重绘所有 Nodes] 119 | B004[重绘所有 Lines] 120 | 121 | A001 --> B001 122 | A002 --> B002 123 | A004 --> B004 124 | 125 | C001[移动节点] 126 | C002[缩放 Graph] 127 | C003[移动 Graph] 128 | 129 | C001 --> A003 --> B003 130 | C001 --> A004 --> B004 131 | 132 | C002 --> B001 133 | C003 --> B001 134 | B001 --> B002 135 | B001 --> B003 136 | B001 --> B004 137 | ``` 138 | 139 | ### Node/Line 渲染流程 140 | 141 | ```mermaid 142 | flowchart TD 143 | A001[设置/更新 Nodes 数据] 144 | A002[循环每个数据] 145 | A003[新建对应的 NodeElement] 146 | A004[更新对应的 NodeElement 内的数据] 147 | 148 | A010[NodeElement 根据 type 查找对应的注册数据] 149 | A011[触发 NodeElement update 流程] 150 | A012[update 流程内操作对应的 HTML 元素] 151 | 152 | B001{对应节点是否存在} 153 | 154 | A001 --> A002 --> B001 155 | B001 -->|不存在| A003 --> A004 156 | B001 -->|存在| A004 157 | 158 | A004 --> A010 --> A011 --> A012 159 | 160 | 161 | 162 | C001[设置/更新 Lines 数据] 163 | C002[循环每个数据] 164 | C003[新建对应的 SVGGElement] 165 | C004[更新对应的 SVGGElement 内的数据] 166 | 167 | C010[SVGGElement 根据 Type 查找对应的注册数据] 168 | C011[将 SVGGElement 丢入注册数据内的 update 函数] 169 | C012[update 流程内操作对应的 SVGGElement 元素] 170 | 171 | D001{对应节点是否存在} 172 | 173 | C001 --> C002 --> D001 174 | D001 -->|不存在| C003 --> C004 175 | D001 -->|存在| C004 176 | 177 | C004 --> C010 --> C011 --> C012 178 | ``` 179 | 180 | ### 连接、操作流程 181 | 182 | ```mermaid 183 | sequenceDiagram 184 | participant ParamElement 185 | participant NodeElement 186 | participant GraphElement 187 | 188 | ParamElement ->> NodeElement: 鼠标按下触发 connect 事件 189 | NodeElement ->> NodeElement: shadowRoot 收到 connect 190 | NodeElement ->> NodeElement: 用户自定义处理,发出 start-connect 事件 191 | NodeElement ->> GraphElement: 收到 start-connect 事件 192 | GraphElement ->> GraphElement: 开始拖拽 193 | GraphElement ->> GraphElement: 收到点击事件 194 | GraphElement ->> NodeElement: 结束点击,更新 Node 数据 195 | ``` 196 | 197 | ### 组件数据 198 | 199 | Graph 需要的数据: 200 | 201 | 1. mesh 开关、间距、颜色 202 | 2. 0 点参考线开关、颜色、是否加粗 0 点 203 | 3. 是否可移动 204 | 4. 是否可缩放、最大/最小缩放比例 205 | 206 | Node 需要的数据: 207 | 208 | 1. 相对 Graph 0 点的坐标 209 | 2. Node 自身的内容(template、style) 210 | 3. Node 携带的附加数据 211 | 212 | Node-Param 需要的数据: 213 | 214 | 1. 连线方向限制 215 | 2. 输入 / 输出标记 216 | 3. 参数类型,用于显示是否可连接 217 | 4. 参数名字 218 | 219 | Line 需要的数据: 220 | 221 | 1. Line 自身的内容(SVGElement) 222 | 2. Line 携带的附加数据 223 | 3. Line 起始点位置 224 | 4. Line 结束点位置 225 | 226 | 除了以上基础功能,还可以实现一些附加功能: 227 | 228 | 1. 自动布局,忽略 Node Position,根据图信息尝试自动布局 229 | 2. 多图混合互联 230 | 3. 子图嵌套 -------------------------------------------------------------------------------- /element/graph/design/event.md: -------------------------------------------------------------------------------- 1 | # 事件 2 | 3 | 这里统计所有通过 HTML 发送的事件 4 | 5 | ## 列表 6 | 7 | 主要分成了节点和线段两个类型,其中事件类型分成了 `内部` 和 `外部` 两个。 8 | 9 | 内部事件会发送到 graph 元素的 shadowRoot 上,不会传递到外部。 10 | 11 | 而外部事件,则直接从 graph 元素向上发送。 12 | 13 | ### Node 14 | 15 | | 消息名 | 类型 | 说明 | 16 | |----|----|----| 17 | | node-added | 通知 | 新增一个节点之后发出的消息 18 | | node-changed | 通知 | 修改一个节点之后发出的消息 19 | | node-position-changed | 通知 | 修改一个节点之后发出的消息 20 | | node-removed | 通知 | 删除一个节点之后发出的消息 21 | | node-selected | 通知 | 一个节点被选中的时候触发 22 | | node-unselected | 通知 | 一个节点被取消选中的时候触发 23 | | node-connected | 通知 | 确定用户操作准备连接一个节点 24 | | select-node | 内部 | 选中一个节点 25 | | unselect-node | 内部 | 取消选中一个节点 26 | | clear-select-node | 内部 | 取消选中所有节点 27 | | move-node | 内部 | 开始拖拽一个节点 28 | | interrupt-move-node | 内部 | 中断拖拽一个节点 29 | | connect-node | 内部 | 开始连接一个节点 30 | | interrupt-connect-node | 内部 | 中断连接一个节点 31 | 32 | ### Line 33 | 34 | | 消息名 | 类型 | 说明 | 35 | |----|----|----| 36 | | line-added | 通知 | 新增一个线段之后发出的消息 37 | | line-changed | 通知 | 修改一个线段之后发出的消息 38 | | line-removed | 通知 | 删除一个线段之后发出的消息 39 | | line-selected | 通知 | 一个线段被选中的时候触发 40 | | line-unselected | 通知 | 一个线段被取消选中的时候触发 41 | | select-line | 内部 | 选中一个线段 42 | | unselect-line | 内部 | 取消选中一个线段 43 | | clear-select-line | 内部 | 取消选中所有线段 44 | 45 | ```mermaid 46 | flowchart TD 47 | A01[Graph] 48 | A03[Graph] 49 | A02[Node] 50 | A11[select-node] 51 | A12[unselect-node] 52 | A21[node-selected] 53 | A22[node-unselected] 54 | A02 -->|mousedown| A11 -->|冒泡| A01 --> A21 55 | A02 --> A12 --> A03 --> A22 56 | 57 | B01[Graph] 58 | B02[Graph] 59 | B03[Line] 60 | B11[select-line] 61 | B12[unselect-line] 62 | B21[line-selected] 63 | B22[line-unselected] 64 | B03 -->|mousedown| B11 -->|冒泡| B01 --> B21 65 | B03 --> B12 --> B02 --> B22 66 | 67 | C01[Graph] 68 | C11[clear-all-selected] 69 | C21[node-unslected] 70 | C22[node-unslected] 71 | C01 -->|mousedown| C11 72 | C11 --> C21 73 | C11 --> C22 74 | ``` -------------------------------------------------------------------------------- /element/graph/example/class-data.esm.js: -------------------------------------------------------------------------------- 1 | export const data = { 2 | scale: 1, 3 | offset: { 4 | x: 0, 5 | y: 0, 6 | }, 7 | option: { 8 | type: 'pure', 9 | meshSize: 0, 10 | meshColor: '#333', 11 | backgroundColor: '#333', 12 | }, 13 | nodes: { 14 | 'test-1': { 15 | type: 'class-node', 16 | position: { 17 | x: 0, 18 | y: -300, 19 | }, 20 | details: { 21 | name: 'GraphElement', 22 | property: [ 23 | '+scale: number;', 24 | '+option: GraphOption', 25 | '+offset: Position;', 26 | '+calibration: Position;', 27 | '+nodes: Node[];', 28 | '+lines: Line[];', 29 | ], 30 | function: [ 31 | '+createNode(nodeInfo): void;', 32 | '+createLine(lineInfo): void;', 33 | '+startMoveNode(node): void;', 34 | '+stopMoveNode(node?): void;', 35 | '+startConnect(type, node, param?): void;', 36 | '+stopConnect(): void;', 37 | ], 38 | }, 39 | }, 40 | 'test-2': { 41 | type: 'class-node', 42 | position: { 43 | x: -200, 44 | y: 80, 45 | }, 46 | details: { 47 | name: 'NodeElement', 48 | property: [ 49 | '+type: string;', 50 | '+details: Object;', 51 | '+position: Position;', 52 | ], 53 | function: [ 54 | '+startMove(): void;', 55 | '+stopMove(): void;', 56 | '+startConnect(type, param?): void;', 57 | '+stopConnect(): void;', 58 | ], 59 | }, 60 | }, 61 | 'test-3': { 62 | type: 'class-node', 63 | position: { 64 | x: 200, 65 | y: 80, 66 | }, 67 | details: { 68 | name: 'LineElement', 69 | property: [ 70 | '+type: string;', 71 | '+details: Object;', 72 | '+input: LineParam;', 73 | '+output: LineParam;', 74 | ], 75 | function: [], 76 | }, 77 | }, 78 | 'test-4': { 79 | type: 'class-node', 80 | position: { 81 | x: -200, 82 | y: 380, 83 | }, 84 | details: { 85 | name: 'NodeParamElement', 86 | property: [ 87 | '+name: string;', 88 | '+direction: string;', 89 | '+type: string;', 90 | '+role: string;', 91 | ], 92 | function: [ 93 | '+startConnect(type): void;', 94 | '+stopConnect(): void;', 95 | ], 96 | }, 97 | }, 98 | 'test-5': { 99 | type: 'class-node', 100 | position: { 101 | x: 120, 102 | y: 300, 103 | }, 104 | details: { 105 | name: 'Inheritance', 106 | property: [], 107 | function: [], 108 | }, 109 | }, 110 | 'test-6': { 111 | type: 'class-node', 112 | position: { 113 | x: 120, 114 | y: 340, 115 | }, 116 | details: { 117 | name: 'Realization', 118 | property: [], 119 | function: [], 120 | }, 121 | }, 122 | 'test-7': { 123 | type: 'class-node', 124 | position: { 125 | x: 120, 126 | y: 380, 127 | }, 128 | details: { 129 | name: 'Association', 130 | property: [], 131 | function: [], 132 | }, 133 | }, 134 | 'test-8': { 135 | type: 'class-node', 136 | position: { 137 | x: 120, 138 | y: 420, 139 | }, 140 | details: { 141 | name: 'Aggregation', 142 | property: [], 143 | function: [], 144 | }, 145 | }, 146 | 'test-9': { 147 | type: 'class-node', 148 | position: { 149 | x: 120, 150 | y: 460, 151 | }, 152 | details: { 153 | name: 'Composition', 154 | property: [], 155 | function: [], 156 | }, 157 | }, 158 | 'test-10': { 159 | type: 'class-node', 160 | position: { 161 | x: 120, 162 | y: 500, 163 | }, 164 | details: { 165 | name: 'Dependency', 166 | property: [], 167 | function: [], 168 | }, 169 | }, 170 | }, 171 | lines: { 172 | line1: { 173 | type: 'composition', 174 | details: {}, 175 | input: { 176 | node: 'test-2', 177 | }, 178 | output: { 179 | node: 'test-1', 180 | }, 181 | }, 182 | line2: { 183 | type: 'composition', 184 | details: {}, 185 | input: { 186 | node: 'test-3', 187 | }, 188 | output: { 189 | node: 'test-1', 190 | }, 191 | }, 192 | line3: { 193 | type: 'composition', 194 | details: {}, 195 | input: { 196 | node: 'test-4', 197 | }, 198 | output: { 199 | node: 'test-2', 200 | }, 201 | }, 202 | line4: { 203 | type: 'inheritance', 204 | details: {}, 205 | input: { 206 | node: 'test-5', 207 | }, 208 | output: { 209 | node: 'test-4', 210 | }, 211 | }, 212 | line5: { 213 | type: 'realization', 214 | details: {}, 215 | input: { 216 | node: 'test-6', 217 | }, 218 | output: { 219 | node: 'test-4', 220 | }, 221 | }, 222 | line6: { 223 | type: 'association', 224 | details: {}, 225 | input: { 226 | node: 'test-7', 227 | }, 228 | output: { 229 | node: 'test-4', 230 | }, 231 | }, 232 | line7: { 233 | type: 'aggregation', 234 | details: {}, 235 | input: { 236 | node: 'test-8', 237 | }, 238 | output: { 239 | node: 'test-4', 240 | }, 241 | }, 242 | line8: { 243 | type: 'composition', 244 | details: {}, 245 | input: { 246 | node: 'test-9', 247 | }, 248 | output: { 249 | node: 'test-4', 250 | }, 251 | }, 252 | line9: { 253 | type: 'dependency', 254 | details: {}, 255 | input: { 256 | node: 'test-10', 257 | }, 258 | output: { 259 | node: 'test-4', 260 | }, 261 | }, 262 | }, 263 | }; 264 | -------------------------------------------------------------------------------- /element/graph/example/class.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | My Website 7 | 22 | 23 | 24 | 25 | 26 | 27 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /element/graph/example/custom-data.esm.js: -------------------------------------------------------------------------------- 1 | export const data = { 2 | scale: 1, 3 | offset: { 4 | x: 0, 5 | y: 0, 6 | }, 7 | nodes: { 8 | 'test-c-1': { 9 | type: 'test-c', 10 | position: { 11 | x: -20, 12 | y: -60, 13 | }, 14 | details: { 15 | label: 'test-c-1', 16 | }, 17 | }, 18 | 'test-c-2': { 19 | type: 'test-c', 20 | position: { 21 | x: 100, 22 | y: 200, 23 | }, 24 | details: { 25 | label: 'test-c-2', 26 | }, 27 | }, 28 | 'test-c-3': { 29 | type: 'test-c', 30 | position: { 31 | x: 170, 32 | y: -160, 33 | }, 34 | details: { 35 | label: 'test-c-3', 36 | }, 37 | }, 38 | 'test-a-1': { 39 | type: 'test-a', 40 | position: { 41 | x: -230.9754901960791, 42 | y: -263.76470588235316, 43 | }, 44 | details: { 45 | label: 'test-a-1', 46 | }, 47 | }, 48 | 'test-a-2': { 49 | type: 'test-a', 50 | position: { 51 | x: 86.00000000000063, 52 | y: -303.0000000000002, 53 | }, 54 | details: { 55 | label: 'test-a-2', 56 | }, 57 | }, 58 | 'test-b-1': { 59 | type: 'test-b', 60 | position: { 61 | x: -230.9754901960791, 62 | y: -128.1348039215685, 63 | }, 64 | details: { 65 | label: 'test-b-1', 66 | }, 67 | }, 68 | 'test-b-2': { 69 | type: 'test-b', 70 | position: { 71 | x: -77.66221033868112, 72 | y: 176.37990196078474, 73 | }, 74 | details: { 75 | label: 'test-b-2', 76 | }, 77 | }, 78 | }, 79 | lines: { 80 | line1: { 81 | type: 'straight', 82 | details: {}, 83 | input: { 84 | node: 'test-c-1', 85 | }, 86 | output: { 87 | node: 'test-c-2', 88 | }, 89 | }, 90 | line2: { 91 | type: 'curve', 92 | details: {}, 93 | input: { 94 | node: 'test-a-1', 95 | param: 'output1', 96 | }, 97 | output: { 98 | node: 'test-a-2', 99 | param: 'input1', 100 | }, 101 | }, 102 | line3: { 103 | type: 'curve', 104 | details: {}, 105 | input: { 106 | node: 'test-a-1', 107 | param: 'output2', 108 | }, 109 | output: { 110 | node: 'test-b-2', 111 | param: 'input2', 112 | }, 113 | }, 114 | line4: { 115 | type: 'curve', 116 | details: {}, 117 | input: { 118 | node: 'test-b-1', 119 | param: 'output1', 120 | }, 121 | output: { 122 | node: 'test-b-2', 123 | param: 'input1', 124 | }, 125 | }, 126 | } 127 | }; 128 | 129 | for (let i = 0; i < 20; i++) { 130 | for (let j = 0; j < 50; j++) { 131 | data.nodes[`test-${i}-t-${j}`] = { 132 | type: 'test-c', 133 | position: { 134 | x: j * 100 - 3000, 135 | y: i * 60 - 2000, 136 | }, 137 | details: { 138 | label: 'test-c-1', 139 | }, 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /element/graph/example/custom.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | My Website 7 | 22 | 23 | 24 | 25 | 26 | 27 | 291 | 292 | 293 | -------------------------------------------------------------------------------- /element/graph/example/flow-data.esm.js: -------------------------------------------------------------------------------- 1 | export const data = { 2 | scale: 1, 3 | offset: { 4 | x: 0, 5 | y: 0, 6 | }, 7 | nodes: { 8 | 'test-1-1': { 9 | type: 'node', 10 | position: { 11 | x: -360, 12 | y: 0, 13 | }, 14 | details: { 15 | label: '设置 Mesh 数据', 16 | }, 17 | }, 18 | 'test-1-2': { 19 | type: 'node', 20 | position: { 21 | x: -120, 22 | y: 0, 23 | }, 24 | details: { 25 | label: '更新 Graph 渲染的缓存数据', 26 | }, 27 | }, 28 | 'test-1-3': { 29 | type: 'node', 30 | position: { 31 | x: 120, 32 | y: 0, 33 | }, 34 | details: { 35 | label: '设置/更新 Nodes 数据', 36 | }, 37 | }, 38 | 'test-1-4': { 39 | type: 'node', 40 | position: { 41 | x: 360, 42 | y: 0, 43 | }, 44 | details: { 45 | label: '设置/更新 Lines 数据', 46 | }, 47 | }, 48 | 'test-2-1': { 49 | type: 'node', 50 | position: { 51 | x: -200, 52 | y: 200, 53 | }, 54 | details: { 55 | label: '重绘 Mesh', 56 | }, 57 | }, 58 | 'test-2-2': { 59 | type: 'node', 60 | position: { 61 | x: 0, 62 | y: 200, 63 | }, 64 | details: { 65 | label: '重绘所有的 Nodes', 66 | }, 67 | }, 68 | 'test-2-3': { 69 | type: 'node', 70 | position: { 71 | x: 200, 72 | y: 200, 73 | }, 74 | details: { 75 | label: '重绘所有的 Lines', 76 | }, 77 | }, 78 | 'test-3-1': { 79 | type: 'node', 80 | position: { 81 | x: -360, 82 | y: -200, 83 | }, 84 | details: { 85 | label: '移动 Graph', 86 | }, 87 | }, 88 | 'test-3-2': { 89 | type: 'node', 90 | position: { 91 | x: -200, 92 | y: -200, 93 | }, 94 | details: { 95 | label: '缩放 Graph', 96 | }, 97 | }, 98 | 'test-3-3': { 99 | type: 'node', 100 | position: { 101 | x: -40, 102 | y: -200, 103 | }, 104 | details: { 105 | label: '设置 Graph 数据', 106 | }, 107 | }, 108 | 'test-3-4': { 109 | type: 'node', 110 | position: { 111 | x: 120, 112 | y: -200, 113 | }, 114 | details: { 115 | label: '移动节点', 116 | }, 117 | }, 118 | 'test-3-5': { 119 | type: 'node', 120 | position: { 121 | x: 280, 122 | y: -200, 123 | }, 124 | details: { 125 | label: '连接节点', 126 | }, 127 | }, 128 | }, 129 | lines: { 130 | line1: { 131 | type: 'curve', 132 | details: {}, 133 | input: { 134 | node: 'test-1-1', 135 | }, 136 | output: { 137 | node: 'test-2-1', 138 | }, 139 | }, 140 | line2: { 141 | type: 'curve', 142 | details: {}, 143 | input: { 144 | node: 'test-1-2', 145 | }, 146 | output: { 147 | node: 'test-2-1', 148 | }, 149 | }, 150 | line3: { 151 | type: 'curve', 152 | details: {}, 153 | input: { 154 | node: 'test-1-2', 155 | }, 156 | output: { 157 | node: 'test-2-2', 158 | }, 159 | }, 160 | line4: { 161 | type: 'curve', 162 | details: {}, 163 | input: { 164 | node: 'test-1-2', 165 | }, 166 | output: { 167 | node: 'test-2-3', 168 | }, 169 | }, 170 | line5: { 171 | type: 'curve', 172 | details: {}, 173 | input: { 174 | node: 'test-1-3', 175 | }, 176 | output: { 177 | node: 'test-2-2', 178 | }, 179 | }, 180 | line6: { 181 | type: 'curve', 182 | details: {}, 183 | input: { 184 | node: 'test-1-4', 185 | }, 186 | output: { 187 | node: 'test-2-3', 188 | }, 189 | }, 190 | line7: { 191 | type: 'curve', 192 | details: {}, 193 | input: { 194 | node: 'test-3-1', 195 | }, 196 | output: { 197 | node: 'test-1-2', 198 | }, 199 | }, 200 | line8: { 201 | type: 'curve', 202 | details: {}, 203 | input: { 204 | node: 'test-3-2', 205 | }, 206 | output: { 207 | node: 'test-1-2', 208 | }, 209 | }, 210 | line9: { 211 | type: 'curve', 212 | details: {}, 213 | input: { 214 | node: 'test-3-3', 215 | }, 216 | output: { 217 | node: 'test-1-2', 218 | }, 219 | }, 220 | line10: { 221 | type: 'curve', 222 | details: {}, 223 | input: { 224 | node: 'test-3-4', 225 | }, 226 | output: { 227 | node: 'test-1-3', 228 | }, 229 | }, 230 | line11: { 231 | type: 'curve', 232 | details: {}, 233 | input: { 234 | node: 'test-3-4', 235 | }, 236 | output: { 237 | node: 'test-1-4', 238 | }, 239 | }, 240 | line12: { 241 | type: 'curve', 242 | details: {}, 243 | input: { 244 | node: 'test-3-5', 245 | }, 246 | output: { 247 | node: 'test-1-4', 248 | }, 249 | }, 250 | }, 251 | }; 252 | -------------------------------------------------------------------------------- /element/graph/example/flow.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | My Website 7 | 22 | 23 | 24 | 25 | 26 | 27 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /element/graph/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@itharbors/ui-graph", 3 | "version": "0.3.2", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "dev": "node ./scripts/dev", 8 | "build": "node ./scripts/build" 9 | }, 10 | "author": "VisualSJ", 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "license": "ISC", 15 | "dependencies": { 16 | "@itharbors/ui-core": "^1.0.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /element/graph/scripts/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { join } = require('path'); 4 | const { spawn } = require('child_process'); 5 | 6 | const spawnAsync = function(...cmd) { 7 | return new Promise((resolve) => { 8 | const child = spawn('npx', [...cmd], { 9 | stdio: [0, 1, 2], 10 | cwd: join(__dirname, '..'), 11 | }); 12 | child.on('exit', () => { 13 | resolve(); 14 | }); 15 | }); 16 | }; 17 | 18 | const exec = async function() { 19 | await spawnAsync('tsc'); 20 | await spawnAsync('esbuild', './source/index.js', '--outfile=./bundle/ui-graph.esm.js', '--bundle', '--format=esm', '--platform=node'); 21 | await spawnAsync('esbuild', './source/theme/class-diagram.js', '--outfile=./bundle/class-diagram.esm.js', '--bundle', '--format=esm', '--platform=node'); 22 | await spawnAsync('esbuild', './source/theme/flow-chart.js', '--outfile=./bundle/flow-chart.esm.js', '--bundle', '--format=esm', '--platform=node'); 23 | }; 24 | 25 | exec(); 26 | -------------------------------------------------------------------------------- /element/graph/scripts/dev.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { spawn } = require('child_process'); 4 | 5 | const spawnAsync = function(...cmd) { 6 | return new Promise((resolve) => { 7 | const child = spawn('npx', [...cmd], { 8 | stdio: [0, 1, 2], 9 | }); 10 | child.on('exit', () => { 11 | resolve(); 12 | }); 13 | }); 14 | }; 15 | 16 | const exec = async function() { 17 | await spawnAsync('tsc', '-w'); 18 | }; 19 | 20 | exec(); 21 | -------------------------------------------------------------------------------- /element/graph/source/element/event-interface.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { GraphNodeElement } from './index'; 4 | import { LineInfo, NodeInfo } from '../interface'; 5 | 6 | // ---- Node Private 7 | 8 | export interface SelectNodeDetail { 9 | target: GraphNodeElement; 10 | clearLines: boolean; 11 | clearNodes: boolean; 12 | } 13 | 14 | export interface UnselectNodeDetail { 15 | target: GraphNodeElement; 16 | } 17 | 18 | export interface ClearSelectNodeDetail { 19 | 20 | } 21 | 22 | export interface MoveNodeDetail { 23 | node: GraphNodeElement; 24 | } 25 | 26 | export interface InterruptMoveNodeDetail { 27 | 28 | } 29 | 30 | export interface InterruptMoveNodeDetail { 31 | 32 | } 33 | 34 | export interface ConnectNodeDetail { 35 | lineType: string; 36 | node: string; 37 | param?: string; 38 | paramDirection?: 'input' | 'output'; 39 | details?: { [key: string]: any }; 40 | } 41 | 42 | export interface InterruptConnectNodeDetail { 43 | 44 | } 45 | 46 | // ---- Node Public 47 | 48 | export interface NodeAddedDetail { 49 | node: NodeInfo; 50 | } 51 | 52 | export interface NodeRemovedDetail { 53 | node: NodeInfo; 54 | } 55 | 56 | export interface NodeChangedDetail { 57 | id: string; 58 | node: NodeInfo; 59 | } 60 | 61 | export interface NodePositionChangedDetail { 62 | moveList: { 63 | id: string, 64 | source: { x: number, y: number }, 65 | target: { x: number, y: number }, 66 | }[]; 67 | } 68 | 69 | export interface NodeSelectedDetail { 70 | 71 | } 72 | 73 | export interface NodeUnselectedDetail { 74 | 75 | } 76 | 77 | // ---- Line 78 | 79 | export interface SelectLineDetail { 80 | target: SVGGElement; 81 | } 82 | 83 | export interface UnselectLineDetail { 84 | target: SVGGElement; 85 | } 86 | 87 | // ---- Node Public 88 | 89 | export interface LineAddedDetail { 90 | line: LineInfo; 91 | } 92 | 93 | export interface LineRemovedDetail { 94 | line: LineInfo; 95 | } 96 | 97 | export interface LineChangedDetail { 98 | line: LineInfo; 99 | } 100 | 101 | export interface LineSelectedDetail { 102 | line: LineInfo; 103 | } 104 | 105 | export interface LineUnselectedDetail { 106 | line: LineInfo; 107 | } 108 | -------------------------------------------------------------------------------- /element/graph/source/element/graph-line.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import type { GraphElement } from './graph'; 4 | import { registerElement, BaseElement } from '@itharbors/ui-core'; 5 | import { queryNode } from '../manager'; 6 | 7 | type GraphNodeElementData = { 8 | scale: number; 9 | // 图类型 10 | graphType: string; 11 | // 类型 12 | type: string; 13 | // 是否选中 14 | selected: boolean; 15 | // 附加描述信息 16 | details: { [key: string]: any }; 17 | // 节点所在的坐标 18 | position: { x: number, y: number }; 19 | 20 | // 拖拽过程中需要使用的临时变量,拖拽 21 | moveStartPoint: { x: number, y: number, pageX: number, pageY: number }; 22 | }; 23 | 24 | // export class GraphLineElement extends BaseElement { 25 | 26 | // } 27 | 28 | // 创建一个新的原型对象,继承自 SVGGElement 的原型 29 | const MyCustomSVGElementPrototype = Object.create(SVGGElement.prototype); 30 | 31 | // 添加新的方法到原型对象 32 | MyCustomSVGElementPrototype.sayHello = function() { 33 | console.log("Hello from custom SVG element!"); 34 | }; 35 | 36 | // 注册自定义元素 37 | // document.registerElement('my-custom-svg', { 38 | // prototype: MyCustomSVGElementPrototype, 39 | // extends: 'g' 40 | // }); 41 | // registerElement('graph-line', GraphLineElement); 42 | 43 | // 创建一个新的类,继承自 SVGGElement 44 | class MyCustomSVGElement extends SVGGElement { 45 | constructor() { 46 | super(); 47 | // 在构造函数中可以进行初始化操作 48 | // 例如添加子元素、事件监听等 49 | } 50 | } 51 | 52 | // @ts-ignore 定义自定义元素 53 | // customElements.define('v-graph-line', MyCustomSVGElement, { extends: 'g' }); 54 | -------------------------------------------------------------------------------- /element/graph/source/element/graph-node-param.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { registerElement, BaseElement } from '@itharbors/ui-core'; 4 | 5 | export class GraphNodeParamElement extends BaseElement { 6 | get HTMLTemplate() { return /*html*/``; } 7 | get HTMLStyle() { return /*css*/`:host { display: block; position: relative; }`; } 8 | } 9 | 10 | registerElement('graph-node-param', GraphNodeParamElement); 11 | -------------------------------------------------------------------------------- /element/graph/source/element/graph-node.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import type { SelectNodeDetail, MoveNodeDetail, InterruptMoveNodeDetail, UnselectNodeDetail, ClearSelectNodeDetail, ConnectNodeDetail, InterruptConnectNodeDetail } from './event-interface'; 4 | import type { GraphElement } from './graph'; 5 | 6 | import { registerElement, BaseElement } from '@itharbors/ui-core'; 7 | import { queryNode } from '../manager'; 8 | 9 | type GraphNodeElementData = { 10 | scale: number; 11 | // 图类型 12 | graphType: string; 13 | // 类型 14 | type: string; 15 | // 是否选中 16 | selected: boolean; 17 | // 附加描述信息 18 | details: { [key: string]: any }; 19 | // 节点所在的坐标 20 | position: { x: number, y: number }; 21 | 22 | // 拖拽过程中需要使用的临时变量,拖拽 23 | moveStartPoint: { x: number, y: number, pageX: number, pageY: number }; 24 | }; 25 | 26 | export class GraphNodeElement extends BaseElement { 27 | get HTMLTemplate() { 28 | return /*html*/``; 29 | } 30 | 31 | get HTMLStyle() { 32 | return /*css*/``; 33 | } 34 | 35 | get defaultData(): GraphNodeElementData { 36 | return { 37 | scale: 1, 38 | graphType: '', 39 | type: '', 40 | selected: false, 41 | details: {}, 42 | position: { x: 0, y: 0 }, 43 | moveStartPoint: { x: 0, y: 0, pageX: 0, pageY: 0, }, 44 | }; 45 | } 46 | 47 | getHost() { 48 | const $shadom = this.getRootNode() as ShadowRoot; 49 | return $shadom?.host; 50 | } 51 | 52 | /** 53 | * 开始拖拽节点 54 | * 运行后,节点开始随着鼠标移动 55 | * 直到执行 stopMove 或者点击一下页面 56 | */ 57 | startMove() { 58 | const custom = new CustomEvent('move-node', { 59 | bubbles: false, 60 | cancelable: false, 61 | detail: { 62 | node: this, 63 | }, 64 | }); 65 | (this.getRootNode() as ShadowRoot).dispatchEvent(custom); 66 | } 67 | 68 | /** 69 | * 停止拖拽 70 | * 在没有开始拖拽的时候执行无效 71 | */ 72 | stopMove() { 73 | const custom = new CustomEvent('interrupt-move-node', { 74 | bubbles: false, 75 | cancelable: false, 76 | detail: {}, 77 | }); 78 | (this.getRootNode() as ShadowRoot).dispatchEvent(custom); 79 | } 80 | 81 | /** 82 | * 开始连接其他节点 83 | * 运行后,连接线从起始位置到鼠标位置结束 84 | * 直到另一个节点上触发 startConnect,或者在空白区域点击 85 | * @param type 86 | * @param param 87 | * @param paramDirection 88 | * @returns 89 | */ 90 | startConnect(type: string, param?: string, paramDirection?: 'input' | 'output', details?: { [key: string]: any }) { 91 | const uuid = this.data.getAttribute('node-uuid'); 92 | if (!uuid) { 93 | return; 94 | } 95 | 96 | const custom = new CustomEvent('connect-node', { 97 | bubbles: false, 98 | cancelable: false, 99 | detail: { 100 | lineType: type, 101 | node: uuid, 102 | param, 103 | paramDirection, 104 | details, 105 | }, 106 | }); 107 | (this.getRootNode() as ShadowRoot).dispatchEvent(custom); 108 | } 109 | 110 | /** 111 | * 判断当前是否在连接过程中 112 | * @returns 113 | */ 114 | hasConnect() { 115 | const $graph = this.getRootNode() as ShadowRoot; 116 | return ($graph.host as GraphElement).hasConnect(); 117 | } 118 | 119 | /** 120 | * 停止连接动作 121 | * 没有开始连接的时候执行无效 122 | */ 123 | stopConnect() { 124 | const custom = new CustomEvent('interrupt-connect-node', { 125 | bubbles: false, 126 | cancelable: false, 127 | detail: {}, 128 | }); 129 | (this.getRootNode() as ShadowRoot).dispatchEvent(custom); 130 | } 131 | 132 | /** 133 | * 选中当前节点 134 | */ 135 | select(option: Omit) { 136 | const custom = new CustomEvent('select-node', { 137 | bubbles: false, 138 | cancelable: false, 139 | detail: { 140 | target: this, 141 | clearLines: option.clearLines, 142 | clearNodes: option.clearNodes, 143 | }, 144 | }); 145 | (this.getRootNode() as ShadowRoot).dispatchEvent(custom); 146 | } 147 | 148 | /** 149 | * 取消选中当前节点 150 | */ 151 | unselect() { 152 | const custom = new CustomEvent('unselect-node', { 153 | bubbles: false, 154 | cancelable: false, 155 | detail: { 156 | target: this, 157 | }, 158 | }); 159 | (this.getRootNode() as ShadowRoot).dispatchEvent(custom); 160 | } 161 | 162 | /** 163 | * 清空所有选中的元素 164 | */ 165 | clearOtherSelected() { 166 | const custom = new CustomEvent('clear-select-node', { 167 | bubbles: false, 168 | cancelable: false, 169 | detail: {}, 170 | }); 171 | (this.getRootNode() as ShadowRoot).dispatchEvent(custom); 172 | } 173 | 174 | /** 175 | * 绑定默认的参数连接事件 176 | */ 177 | bindDefaultParamEvent() { 178 | const $paramList = this.shadowRoot.querySelectorAll(`v-graph-node-param`); 179 | Array.prototype.forEach.call($paramList, ($param) => { 180 | $param.addEventListener('mousedown', (event: MouseEvent) => { 181 | event.stopPropagation(); 182 | event.preventDefault(); 183 | 184 | const name = $param.getAttribute('name'); 185 | if (!name) { 186 | return; 187 | } 188 | const paramDirection = $param.getAttribute('direction'); 189 | if (paramDirection !== 'input' && paramDirection !== 'output') { 190 | return; 191 | } 192 | this.startConnect('', name, paramDirection); 193 | }); 194 | }); 195 | } 196 | 197 | /** 198 | * 绑定默认的鼠标点击后移动事件 199 | */ 200 | bindDefaultMoveEvent() { 201 | // 拖拽移动 202 | this.addEventListener('mousedown', (event) => { 203 | event.stopPropagation(); 204 | event.preventDefault(); 205 | if (!this.hasAttribute('selected')) { 206 | if (!(event as MouseEvent).metaKey && !(event as MouseEvent).ctrlKey) { 207 | this.clearOtherSelected(); 208 | } 209 | const clear = event.metaKey || event.ctrlKey; 210 | this.select({ 211 | clearLines: !clear, 212 | clearNodes: !clear, 213 | }); 214 | } 215 | this.startMove(); 216 | }); 217 | } 218 | 219 | onInit() { 220 | let inited = false; 221 | this.data.addPropertyListener('type', (type, legacy) => { 222 | const graphType = this.data.getProperty('graphType'); 223 | const panel = queryNode(graphType, type); 224 | const details = this.data.getProperty('details'); 225 | 226 | if (panel) { 227 | this.shadowRoot.innerHTML = `\n${panel.template}`; 228 | } 229 | inited && panel.onUpdate.call(this, details); 230 | }); 231 | 232 | this.data.addPropertyListener('details', (details) => { 233 | const type = this.data.getProperty('type'); 234 | const graphType = this.data.getProperty('graphType'); 235 | const panel = queryNode(graphType, type); 236 | panel.onInit.call(this, details); 237 | inited = true; 238 | inited && panel.onUpdate.call(this, details); 239 | }); 240 | 241 | this.data.addPropertyListener('position', (position, legacy) => { 242 | this.setAttribute('style', `--offset-x: ${position.x}px; --offset-y: ${position.y}px;`); 243 | }); 244 | 245 | this.data.addPropertyListener('selected', (selected, legacy) => { 246 | if (selected) { 247 | this.setAttribute('selected', ''); 248 | } else { 249 | this.removeAttribute('selected'); 250 | } 251 | }); 252 | } 253 | 254 | onUpdate() { 255 | const type = this.data.getProperty('type'); 256 | const graphType = this.data.getProperty('graphType'); 257 | const details = this.data.getProperty('details'); 258 | const panel = queryNode(graphType, type); 259 | panel.onUpdate.call(this, details); 260 | } 261 | } 262 | registerElement('graph-node', GraphNodeElement); 263 | -------------------------------------------------------------------------------- /element/graph/source/element/graph/data.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import type { PathParamRole, LineInfo, NodeInfo } from '../../interface'; 4 | 5 | /** 6 | * 连接点计算方式 7 | * normal: 默认在元素中心点 8 | * snap: 吸附到边框 9 | * shortest: 变换到连线与边框的焦点 10 | */ 11 | type ParamPointType = 'normal' | 'snap' | 'shortest'; 12 | 13 | export class ParamConnectData { 14 | 15 | // 起始点 16 | x1: number = 0; 17 | y1: number = 0; 18 | 19 | // 连线结束点 20 | x2: number = 0; 21 | y2: number = 0; 22 | 23 | // 起始点开始的线段的朝向 24 | r1: PathParamRole = 'all'; 25 | // 终点开始的线段的朝向 26 | r2: PathParamRole = 'all'; 27 | 28 | // 当 r1 为全向的时候,起始点优先以横向还是竖向显示 29 | d1: 0 | 1 = 1; 30 | // 当 r2 为全向的时候,终点点优先以横向还是竖向显示 31 | d2: 0 | 1 = 1; 32 | 33 | line: LineInfo; 34 | 35 | private scale: number = 1; 36 | private $nodeA?: HTMLElement; 37 | private $nodeB?: HTMLElement; 38 | nodeA?: NodeInfo; 39 | nodeB?: NodeInfo; 40 | 41 | constructor(line: LineInfo, scale: number, $nodeA?: HTMLElement, $nodeB?: HTMLElement, nodeA?: NodeInfo, nodeB?: NodeInfo) { 42 | this.line = line; 43 | this.scale = scale; 44 | 45 | this.$nodeA = $nodeA; 46 | this.$nodeB = $nodeB; 47 | this.nodeA = nodeA; 48 | this.nodeB = nodeB; 49 | 50 | if (nodeA) { 51 | this.x1 = nodeA.position.x; 52 | this.y1 = nodeA.position.y; 53 | 54 | if ($nodeA) { 55 | const bound = $nodeA.getBoundingClientRect(); 56 | this.x1 += bound.width / 2 / scale; 57 | this.y1 += bound.height / 2 / scale; 58 | } 59 | } 60 | 61 | if (nodeB) { 62 | this.x2 = nodeB.position.x; 63 | this.y2 = nodeB.position.y; 64 | 65 | if ($nodeB) { 66 | const bound = $nodeB.getBoundingClientRect(); 67 | this.x2 += bound.width / 2 / scale; 68 | this.y2 += bound.height / 2 / scale; 69 | } 70 | } 71 | } 72 | 73 | getNodeABoundingClientRect() { 74 | return this.$nodeA!.getBoundingClientRect(); 75 | } 76 | 77 | getNodeBBoundingClientRect() { 78 | return this.$nodeB!.getBoundingClientRect(); 79 | } 80 | 81 | transform(startType: ParamPointType, endType: ParamPointType) { 82 | switch(startType) { 83 | case 'snap': 84 | snapBorderInput(this, this.$nodeA, this.$nodeB, this.nodeA, this.nodeB, this.scale); 85 | break; 86 | case 'shortest': 87 | if (this.$nodeA !== this.$nodeB) { 88 | shortestInput(this, this.$nodeA, this.$nodeB, this.nodeA, this.nodeB, this.scale); 89 | } 90 | break; 91 | } 92 | switch(endType) { 93 | case 'snap': 94 | snapBorderOutput(this, this.$nodeA, this.$nodeB, this.nodeA, this.nodeB, this.scale); 95 | break; 96 | case 'shortest': 97 | if (this.$nodeA !== this.$nodeB) { 98 | shortestOutput(this, this.$nodeA, this.$nodeB, this.nodeA, this.nodeB, this.scale); 99 | } 100 | break; 101 | } 102 | } 103 | } 104 | 105 | /** 106 | * 检测一条线和一个矩形相交的点 107 | * 返回他们相交的点坐标,以及该点的朝向 [x, y, d]; 108 | * @param x1 线段起始点的 x 坐标 109 | * @param y1 线段起始点的 y 坐标 110 | * @param x2 线段终点的 x 坐标 111 | * @param y2 线段终点的 y 坐标 112 | * @param x3 矩形的左上角 x 坐标 113 | * @param y3 矩形的左上角 y 坐标 114 | * @param w 矩形的宽度 115 | * @param h 矩形的高度 116 | * @returns [number, number, 0 | 1]; 117 | */ 118 | function intersect(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, w: number, h: number): [number, number, 0 | 1] { 119 | // 计算矩形的四个顶点坐标 120 | const x4 = x3 + w; 121 | const y4 = y3 + h; 122 | 123 | const xa = (y4 - y1) / (y2 - y1) * (x2 - x1) + x1; 124 | if (xa > x4 || xa < x3) { 125 | const ya = (x4 - x1) / (x2 - x1) * (y2 - y1) + y1; 126 | if (x2 > x1) { 127 | return [x4, ya, 0]; 128 | } else { 129 | return [x3, y4 - ya + y3, 0]; 130 | } 131 | } else { 132 | if (y2 > y1) { 133 | return [xa, y4, 1]; 134 | } else { 135 | return [x4 - xa + x3, y3, 1]; 136 | } 137 | } 138 | } 139 | 140 | /** 141 | * 将起始点吸附到边框 142 | */ 143 | function snapBorderInput(data: ParamConnectData, $nodeA?: HTMLElement, $nodeB?: HTMLElement, nodeA?: NodeInfo, nodeB?: NodeInfo, scale?: number) { 144 | if (!$nodeA || !nodeA || !nodeB) { 145 | return; 146 | } 147 | let r1 = data.r1; 148 | if (r1 === 'all') { 149 | const xd = data.x1 - data.x2; 150 | const yd = data.y1 - data.y2; 151 | const tl = Math.abs(xd / yd); 152 | if (tl <= 1) { // up down 153 | r1 = yd <= 0 ? 'up' : 'down'; 154 | } else { // left right 155 | r1 = xd <= 0 ? 'right' : 'left'; 156 | } 157 | } 158 | const boundA = $nodeA.getBoundingClientRect(); 159 | switch (r1) { 160 | case 'right': 161 | data.x1 += boundA.width / 2; 162 | break; 163 | case 'left': 164 | data.x1 -= boundA.width / 2; 165 | break; 166 | case 'up': 167 | data.y1 += boundA.height / 2; 168 | break; 169 | case 'down': 170 | data.y1 -= boundA.height / 2; 171 | break; 172 | } 173 | } 174 | 175 | /** 176 | * 将结束点吸附到边框 177 | */ 178 | function snapBorderOutput(data: ParamConnectData, $nodeA?: HTMLElement, $nodeB?: HTMLElement, nodeA?: NodeInfo, nodeB?: NodeInfo, scale?: number) { 179 | if (!$nodeB || !nodeA || !nodeB) { 180 | return; 181 | } 182 | const boundB = $nodeB.getBoundingClientRect(); 183 | let r2 = data.r2; 184 | if (r2 === 'all') { 185 | const xd = data.x1 - data.x2; 186 | const yd = data.y1 - data.y2; 187 | const tl = Math.abs(xd / yd); 188 | if (tl <= 1) { // up down 189 | r2 = yd <= 0 ? 'up' : 'down'; 190 | } else { // left right 191 | r2 = xd <= 0 ? 'right' : 'left'; 192 | } 193 | } 194 | switch (r2) { 195 | case 'right': 196 | data.x2 -= boundB.width / 2; 197 | break; 198 | case 'left': 199 | data.x2 += boundB.width / 2; 200 | break; 201 | case 'up': 202 | data.y2 -= boundB.height / 2; 203 | break; 204 | case 'down': 205 | data.y2 += boundB.height / 2; 206 | break; 207 | } 208 | } 209 | 210 | /** 211 | * 两点连接计算最短连接线上的两个交点 212 | */ 213 | function shortestInput(data: ParamConnectData, $nodeA?: HTMLElement, $nodeB?: HTMLElement, nodeA?: NodeInfo, nodeB?: NodeInfo, scale: number = 1) { 214 | if (!$nodeA || !nodeA || !nodeB) { 215 | return; 216 | } 217 | const boundA = $nodeA!.getBoundingClientRect(); 218 | boundA.width /= scale; 219 | boundA.height /= scale; 220 | const pa = intersect(data.x1, data.y1, data.x2, data.y2, nodeA.position.x, nodeA.position.y, boundA.width, boundA.height)!; 221 | data.x1 = pa[0]; 222 | data.y1 = pa[1]; 223 | data.d1 = pa[2]; 224 | } 225 | 226 | /** 227 | * 两点连接计算最短连接线上的两个交点 228 | */ 229 | function shortestOutput(data: ParamConnectData, $nodeA?: HTMLElement, $nodeB?: HTMLElement, nodeA?: NodeInfo, nodeB?: NodeInfo, scale: number = 1) { 230 | if (!$nodeB || !nodeA || !nodeB) { 231 | return; 232 | } 233 | const boundB = $nodeB!.getBoundingClientRect(); 234 | boundB.width /= scale; 235 | boundB.height /= scale; 236 | const pb = intersect(data.x2, data.y2, data.x1, data.y1, nodeB.position.x, nodeB.position.y, boundB.width, boundB.height)!; 237 | data.x2 = pb[0]; 238 | data.y2 = pb[1]; 239 | data.d2 = pb[2]; 240 | } 241 | -------------------------------------------------------------------------------- /element/graph/source/element/index.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export { 4 | GraphNodeParamElement 5 | } from './graph-node-param'; 6 | 7 | export { 8 | GraphNodeElement 9 | } from './graph-node'; 10 | 11 | // export { 12 | // GraphLineElement 13 | // } from './graph-line'; 14 | 15 | export { 16 | GraphElement 17 | } from './graph'; -------------------------------------------------------------------------------- /element/graph/source/element/utils.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import type { PathParamRole } from '../interface'; 4 | 5 | /** 6 | * 生成一个临时的 ID 7 | * @returns 8 | */ 9 | export function generateUUID() { 10 | return 't_' + Date.now() + (Math.random() + '').substring(10); 11 | } 12 | 13 | /** 14 | * 获取一个 param 元素相对 node 的偏移坐标 15 | * @param $node 16 | * @param selector 17 | * @returns 18 | */ 19 | export function getParamElementOffset($node: HTMLElement, selector: string) { 20 | const $param = $node.shadowRoot!.querySelector(selector); 21 | if (!$param) { 22 | return null; 23 | } 24 | if ($param.hasAttribute('hidden')) { 25 | return null; 26 | } 27 | const nodeBBound = $node.getBoundingClientRect(); 28 | const paramBBound = $param.getBoundingClientRect(); 29 | return { 30 | x: (paramBBound.width / 2 + paramBBound.x) - (nodeBBound.width / 2 + nodeBBound.x), 31 | y: (paramBBound.height / 2 + paramBBound.y) - (nodeBBound.height / 2 + nodeBBound.y), 32 | role: $param.getAttribute('role') as PathParamRole, 33 | }; 34 | } 35 | 36 | /** 37 | * 在元素里找到 param 的一些信息 38 | * @param $root 39 | * @param node 40 | * @param param 41 | * @returns 42 | */ 43 | export function queryParamInfo($root: HTMLElement, node: string, param?: string) { 44 | const $node = $root.shadowRoot!.querySelector(`#nodes > v-graph-node[node-uuid="${node}"]`); 45 | if (!$node) { 46 | return; 47 | } 48 | const $param = $node.shadowRoot!.querySelector(`v-graph-node-param[name="${param}"]`); 49 | if (!$param) { 50 | return; 51 | } 52 | return { 53 | direction: $param.getAttribute('direction'), 54 | type: $param.getAttribute('type'), 55 | name: $param.getAttribute('name'), 56 | role: $param.getAttribute('role'), 57 | }; 58 | } 59 | 60 | /** 61 | * 基于 requestAnimtionFrame 的节流 62 | * @param func 63 | * @returns 64 | */ 65 | // export function requestAnimtionFrameThrottling any>(func: T): T { 66 | // let exec = false; 67 | // let wait: any[] | null = null; 68 | 69 | // const handle = async function(...args: any[]) { 70 | // if (exec) { 71 | // wait = args; 72 | // return; 73 | // } 74 | // exec = true; 75 | // await func(...args); 76 | // requestAnimationFrame(() => { 77 | // exec = false; 78 | // if (wait) { 79 | // handle(...wait); 80 | // wait = null; 81 | // } 82 | // }); 83 | // } as T; 84 | 85 | // return handle; 86 | // } 87 | export function requestAnimtionFrameThrottling any>(func: T): T { 88 | let wait: any[] | null = null; 89 | 90 | const handle = async function(...args: any[]) { 91 | if (wait) { 92 | wait = args; 93 | return; 94 | } 95 | wait = args; 96 | requestAnimationFrame(() => { 97 | func(...wait!); 98 | wait = null; 99 | }); 100 | } as T; 101 | 102 | return handle; 103 | } 104 | -------------------------------------------------------------------------------- /element/graph/source/event.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | import type { GraphNodeElement } from './element/graph-node'; 5 | import type { GraphElement } from './element/graph'; 6 | import type { NodeInfo, LineInfo } from './interface'; 7 | 8 | type TNodeMap = { 9 | [key: string]: NodeInfo; 10 | }; 11 | 12 | type TLineMap = { 13 | [key: string]: LineInfo; 14 | }; 15 | 16 | class CustomEvent { 17 | 18 | blocks: TNodeMap; 19 | lines: TLineMap; 20 | 21 | constructor(blocks: TNodeMap, lines: TLineMap) { 22 | this.blocks = blocks; 23 | this.lines = lines; 24 | } 25 | } 26 | 27 | class MouseEvent extends CustomEvent { 28 | // 点击点在页面的坐标 29 | pageX = 0; 30 | pageY = 0; 31 | 32 | // 点击点在 Graph 里的坐标 33 | graphX = 0; 34 | graphY = 0; 35 | 36 | constructor( 37 | blocks: TNodeMap, 38 | lines: TLineMap, 39 | ) { 40 | super(blocks, lines); 41 | } 42 | 43 | initPagePosition(x: number, y: number) { 44 | this.pageX = x; 45 | this.pageY = y; 46 | } 47 | 48 | initGraphPosition(x: number, y: number) { 49 | this.graphX = x; 50 | this.graphY = y; 51 | } 52 | } 53 | 54 | export class GraphMouseEvent extends MouseEvent { 55 | target: GraphElement; 56 | 57 | constructor( 58 | blocks: TNodeMap, 59 | lines: TLineMap, 60 | target: GraphElement, 61 | ) { 62 | super(blocks, lines); 63 | this.target = target; 64 | } 65 | } 66 | 67 | export class BlockMouseEvent extends MouseEvent { 68 | block: NodeInfo; 69 | target: GraphNodeElement; 70 | constructor( 71 | blocks: TNodeMap, 72 | lines: TLineMap, 73 | target: GraphNodeElement, 74 | block: NodeInfo, 75 | ) { 76 | super(blocks, lines); 77 | this.block = block; 78 | this.target = target; 79 | } 80 | } 81 | 82 | export class LineMouseEvent extends MouseEvent { 83 | target: SVGGElement; 84 | line: LineInfo; 85 | constructor( 86 | blocks: TNodeMap, 87 | lines: TLineMap, 88 | target: SVGGElement, 89 | line: LineInfo, 90 | ) { 91 | super(blocks, lines); 92 | this.line = line; 93 | this.target = target; 94 | } 95 | } 96 | 97 | export class BlockEvent extends CustomEvent{ 98 | block: NodeInfo; 99 | target: GraphNodeElement; 100 | constructor( 101 | blocks: TNodeMap, 102 | lines: TLineMap, 103 | target: GraphNodeElement, 104 | block: NodeInfo, 105 | ) { 106 | super(blocks, lines); 107 | this.block = block; 108 | this.target = target; 109 | } 110 | } 111 | 112 | export class LineEvent extends CustomEvent { 113 | line: LineInfo; 114 | target: SVGGElement; 115 | constructor( 116 | blocks: TNodeMap, 117 | lines: TLineMap, 118 | target: SVGGElement, 119 | line: LineInfo, 120 | ) { 121 | super(blocks, lines); 122 | this.line = line; 123 | this.target = target; 124 | } 125 | } 126 | 127 | type EventEmitterType = string; 128 | type EventEmitterHandler = (...args: T) => void; 129 | 130 | export class EventEmitter { 131 | private _handleMap: Map = new Map(); 132 | 133 | public emit(channel: EventEmitterType, ...args: any[]) { 134 | if (!this._handleMap.has(channel)) { 135 | this._handleMap.set(channel, []); 136 | } 137 | const handlerList = this._handleMap.get(channel)!; 138 | Promise.all(handlerList.map((handler) => { 139 | return handler(...args); 140 | })); 141 | } 142 | 143 | public addListener(channel: EventEmitterType, handler: EventEmitterHandler) { 144 | if (!this._handleMap.has(channel)) { 145 | this._handleMap.set(channel, []); 146 | } 147 | const handlerList = this._handleMap.get(channel)!; 148 | if (!handlerList.includes(handler)) { 149 | handlerList.push(handler); 150 | } 151 | } 152 | 153 | public removeListener(channel: EventEmitterType, handler: EventEmitterHandler) { 154 | if (!this._handleMap.has(channel)) { 155 | return; 156 | } 157 | const handlerList = this._handleMap.get(channel)!; 158 | const index = handlerList.indexOf(handler); 159 | if (index !== -1) { 160 | handlerList.splice(index, 1); 161 | } 162 | } 163 | } -------------------------------------------------------------------------------- /element/graph/source/index.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export { GraphElement } from './element'; 4 | export { GraphNodeElement } from './element'; 5 | 6 | export { ParamConnectData } from './element/graph/data'; 7 | 8 | export { registerNode, queryNode, registerLine, queryLine, registerGraphOption } from './manager'; 9 | 10 | export * from './event'; 11 | 12 | export * from './interface'; 13 | 14 | export type { 15 | NodeAddedDetail, 16 | LineAddedDetail, 17 | NodeChangedDetail, 18 | NodePositionChangedDetail, 19 | LineChangedDetail, 20 | NodeRemovedDetail, 21 | LineRemovedDetail, 22 | } from './element/event-interface'; 23 | -------------------------------------------------------------------------------- /element/graph/source/interface.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // 图文件的格式 4 | export interface GraphInfo { 5 | // 数据版本,为以后数据兼容预留 6 | version: number; 7 | // 缩放比例,会传递到 attribute 上 8 | scale: number; 9 | // 绘制设置 10 | option: GraphOption; 11 | // 节点列表 12 | nodes: { [uuid: string]: NodeInfo } 13 | // 线段列表 14 | lines: { [uuid: string]: LineInfo } 15 | } 16 | 17 | // 图配置 18 | export interface GraphOption { 19 | // 背景颜色 20 | backgroundColor?: string; 21 | 22 | // 网格尺寸 23 | gridSize?: number; 24 | // mesh 颜色 25 | gridColor?: string; 26 | 27 | // 原点坐标 28 | showOriginPoint?: boolean; 29 | // origin 颜色 30 | originPointColor?: string; 31 | } 32 | 33 | // 图里节点的信息 34 | export interface NodeInfo { 35 | // 节点类型 36 | type: string; 37 | // 节点所在的坐标 38 | position: { x: number, y: number }; 39 | // 附加描述信息 40 | details: { [key: string]: any }; 41 | } 42 | 43 | // 图里的线段信息 44 | export interface LineInfo { 45 | // 线条类型,曲线,直线 46 | type: string; 47 | // 附加描述信息 48 | details: { [key: string]: any }; 49 | // 线段开始连接的节点 50 | input: { 51 | node: string; 52 | param?: string; 53 | __fake?: NodeInfo; 54 | }; 55 | // 线段结束连接的节点 56 | output: { 57 | node: string; 58 | param?: string; 59 | __fake?: NodeInfo; 60 | }; 61 | } 62 | 63 | // 选中 64 | export interface SelectNodeInfo { 65 | id: string; 66 | target: NodeInfo; 67 | } 68 | export interface SelectLineInfo { 69 | id: string; 70 | target: LineInfo; 71 | } 72 | 73 | // 曲线生成规则,允许单向或者全向绘制 74 | export type PathParamRole = 'up' | 'down' | 'left' | 'right' | 'all'; 75 | -------------------------------------------------------------------------------- /element/graph/source/manager.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { LineInfo, NodeInfo, GraphOption } from './interface'; 4 | import type { ParamConnectData } from './element/graph/data'; 5 | import type { GraphNodeElement } from './element/graph-node'; 6 | import { EventEmitter } from './event'; 7 | 8 | export const eventEmitter = new EventEmitter(); 9 | 10 | /** 11 | * Type 管理器 12 | * 注册一个 type 怎么渲染 13 | */ 14 | 15 | interface NodeTypeOption { 16 | template: string; 17 | style: string; 18 | onInit(this: GraphNodeElement, ...args: any[]): void; 19 | onUpdate(this: GraphNodeElement, ...args: any[]): void; 20 | } 21 | 22 | interface LineTypeOption { 23 | template: string; 24 | style: string; 25 | updateSVGPath( 26 | $path: SVGGElement, 27 | scale: number, 28 | data: ParamConnectData, 29 | line: LineInfo, 30 | lines: { [key: string]: LineInfo | undefined }, 31 | ): void; 32 | } 33 | 34 | interface ParamInfo { 35 | direction: string | null; 36 | type: string | null; 37 | name: string | null; 38 | role: string | null; 39 | }; 40 | 41 | interface GraphFliter { 42 | lineFilter?( 43 | nodes: { [key: string]: NodeInfo | undefined }, 44 | lines: { [key: string]: LineInfo | undefined }, 45 | line: LineInfo, 46 | input?: ParamInfo, 47 | output?: ParamInfo, 48 | ): boolean; 49 | } 50 | 51 | interface GraphInfo { 52 | nodeMap: Map; 53 | lineMap: Map; 54 | graphFilter: GraphFliter; 55 | option: GraphOption; 56 | } 57 | const graphTypeMap: Map = new Map(); 58 | function generateDefaultGraph(): GraphInfo { 59 | return { 60 | nodeMap: new Map(), 61 | lineMap: new Map(), 62 | graphFilter: {}, 63 | option: {}, 64 | } 65 | } 66 | 67 | export function registerGraphOption(graphType: string, option: GraphOption) { 68 | if (!graphTypeMap.has(graphType)) { 69 | graphTypeMap.set(graphType, generateDefaultGraph()); 70 | } 71 | const graphInfo = graphTypeMap.get(graphType)!; 72 | graphInfo.option = option; 73 | 74 | eventEmitter.emit('graph-registered', graphType, option); 75 | } 76 | 77 | export function queryGraphOption(graphType: string) { 78 | if (!graphTypeMap.has(graphType)) { 79 | graphTypeMap.set(graphType, generateDefaultGraph()); 80 | } 81 | return graphTypeMap.get(graphType)!.option; 82 | } 83 | 84 | // Node 85 | 86 | export function registerNode(graphType: string, nodeType: string, option: NodeTypeOption) { 87 | if (!graphTypeMap.has(graphType)) { 88 | graphTypeMap.set(graphType, generateDefaultGraph()); 89 | } 90 | const graphInfo = graphTypeMap.get(graphType)!; 91 | graphInfo.nodeMap.set(nodeType, option); 92 | 93 | eventEmitter.emit('node-registered', graphType, nodeType, option); 94 | } 95 | 96 | /** 97 | * 查询自定义节点信息 98 | * 如果没有信息,则返回 graphType:* nodeType:* 位置的数据 99 | * @param graphType 100 | * @param nodeType 101 | * @returns 102 | */ 103 | export function queryNode(graphType: string, nodeType: string): NodeTypeOption { 104 | const graphInfo = graphTypeMap.get(graphType); 105 | if (!graphInfo) { 106 | const defaultGraphInfo = graphTypeMap.get('*')!; 107 | return defaultGraphInfo!.nodeMap.get(nodeType) || defaultGraphInfo!.nodeMap.get('*')!; 108 | } 109 | const nodeTypeOption = graphInfo.nodeMap.get(nodeType); 110 | if (!nodeTypeOption) { 111 | const defaultGraphInfo = graphTypeMap.get('*')!; 112 | return defaultGraphInfo!.nodeMap.get(nodeType) || defaultGraphInfo!.nodeMap.get('*')!; 113 | } 114 | return nodeTypeOption; 115 | } 116 | 117 | registerNode('*', 'unknown', { 118 | template: /*html*/`
Unknown
`, 119 | style: /*css*/`div { background: #77777799; color: #eee; padding: 6px 12px; }`, 120 | onInit() {}, 121 | onUpdate() {}, 122 | }); 123 | registerNode('*', '*', queryNode('*', 'unknown')); 124 | 125 | // Line 126 | 127 | export function registerLine(graphType: string, lineType: string, option: LineTypeOption) { 128 | if (!graphTypeMap.has(graphType)) { 129 | graphTypeMap.set(graphType, generateDefaultGraph()); 130 | } 131 | const graphInfo = graphTypeMap.get(graphType)!; 132 | graphInfo.lineMap.set(lineType, option); 133 | 134 | eventEmitter.emit('node-registered', graphType, lineType, option); 135 | } 136 | 137 | export function queryLine(graphType: string, lineType: string): LineTypeOption { 138 | const graphInfo = graphTypeMap.get(graphType); 139 | if (!graphInfo) { 140 | const defaultGraphInfo = graphTypeMap.get('*')!; 141 | return defaultGraphInfo!.lineMap.get(lineType) || defaultGraphInfo!.lineMap.get('*')!; 142 | } 143 | const nodeTypeOption = graphInfo.lineMap.get(lineType); 144 | if (!nodeTypeOption) { 145 | const defaultGraphInfo = graphTypeMap.get('*')!; 146 | return defaultGraphInfo!.lineMap.get(lineType) || defaultGraphInfo!.lineMap.get('*')!; 147 | } 148 | return nodeTypeOption; 149 | } 150 | 151 | function getAngle(x1: number, y1: number, x2: number, y2: number) { 152 | const deltaX = x2 - x1; 153 | const deltaY = y2 - y1; 154 | const angleRadians = Math.atan2(deltaY, deltaX); 155 | const angleDegrees = angleRadians * 180 / Math.PI; 156 | return angleDegrees; 157 | } 158 | 159 | registerLine('*', 'straight', { 160 | template: /*svg*/` 161 | 162 | 163 | `, 164 | // 4 165 | style: /*css*/` 166 | g[type="straight"] > path, g[type="straight"] > polygon { 167 | fill: none; 168 | stroke: #fafafa; 169 | stroke-width: 2px; 170 | transition: stroke 0.3s, fill 0.3s; 171 | } 172 | g[type="straight"]:hover > path, g[type="straight"]:hover > polygon, g[type="straight"]:hover > text { 173 | stroke: #666; 174 | } 175 | g[type="straight"][selected] > path, g[type="straight"][selected] > polygon, g[type="straight"][selected] > text { 176 | stroke: #666; 177 | } 178 | g[type="straight"] > polygon { 179 | fill: #fafafa; 180 | } 181 | g[type="straight"]:hover > polygon { 182 | fill: #666; 183 | } 184 | g[type="straight"][selected] > polygon { 185 | fill: #666; 186 | } 187 | g[type="straight"] > text { 188 | fill: #fafafa; 189 | } 190 | `, 191 | updateSVGPath($g, scale, data) { 192 | if (data.nodeA === data.nodeB) { 193 | const rect = data.getNodeABoundingClientRect(); 194 | const position = { 195 | x: data.x1 + rect.width / scale / 2, 196 | y: data.y1 - rect.height / scale / 2, 197 | } 198 | const $path = $g.querySelector(`path`)!; 199 | $path.setAttribute('d', `M${position.x - 20},${position.y} A20,20 1 1 1 ${position.x},${position.y + 20}`); 200 | const c1x = position.x - 4; // 三角形顶点坐标 201 | const c1y = position.y - 20; 202 | const c2x = c1x + 6; 203 | const c2y = c1y - 7; 204 | const c3x = c1x - 6; 205 | const c3y = c1y - 7; 206 | const $polygon = $g.querySelector(`polygon`)!; 207 | $polygon.setAttribute('points', `${c1x},${c1y} ${c2x},${c2y} ${c3x},${c3y}`); 208 | $polygon.setAttribute('style', `transform-origin: ${c1x}px ${c1y}px; transform: rotate(${90}deg)`); 209 | 210 | // const $text = $g.querySelector(`text`)!; 211 | // $text.setAttribute('x', String(c1x - 4)); 212 | // $text.setAttribute('y', String(c1y - 12)); 213 | return; 214 | } 215 | data.transform( 216 | !data.line.input.param ? 'shortest' : 'normal', 217 | !data.line.output.param ? 'shortest' : 'normal', 218 | ); 219 | const ct1x = (data.x2 - data.x1) / 2; 220 | const ct1y = (data.y2 - data.y1) / 2 221 | const angle = getAngle(data.x1, data.y1, data.x2, data.y2); 222 | const c1x = data.x2 - ct1x; // 三角形顶点坐标 223 | const c1y = data.y2 - ct1y; 224 | const c2x = c1x + 6; 225 | const c2y = c1y - 7; 226 | const c3x = c1x - 6; 227 | const c3y = c1y - 7; 228 | const $path = $g.querySelector(`path`)!; 229 | $path.setAttribute('d', `M${data.x1},${data.y1} L${data.x2},${data.y2}`); 230 | 231 | const $polygon = $g.querySelector(`polygon`)!; 232 | $polygon.setAttribute('points', `${c1x},${c1y} ${c2x},${c2y} ${c3x},${c3y}`); 233 | $polygon.setAttribute('style', `transform-origin: ${c1x}px ${c1y}px; transform: rotate(${angle - 90}deg)`); 234 | 235 | // const oAngle = -Math.PI/(180/angle); 236 | 237 | // const xa = -4 * Math.cos(oAngle) - -16 * Math.sin(oAngle); 238 | // const ya = -4 * Math.sin(oAngle) - -16 * Math.cos(oAngle); 239 | 240 | // const $text = $g.querySelector(`text`)!; 241 | // $text.setAttribute('x', String(c1x + xa)); 242 | // $text.setAttribute('y', String(c1y + ya)); 243 | // $text.innerHTML = '1'; 244 | }, 245 | }); 246 | 247 | registerLine('*', 'curve', { 248 | template: /*svg*/` 249 | 250 | 251 | `, 252 | style: /*css*/` 253 | @keyframes strokeMove { 254 | from { 255 | stroke-dashoffset: 360; 256 | } 257 | to { 258 | stroke-dashoffset: 0; 259 | } 260 | } 261 | g[type="curve"] > path.stroke { 262 | fill: none; 263 | stroke: #fafafa; 264 | stroke-width: 2px; 265 | stroke-dasharray: 20, 5, 5, 5, 5, 5; 266 | animation: strokeMove 30s linear infinite; 267 | transition: stroke 0.3s, fill 0.3s; 268 | } 269 | g[type="curve"] > path.background { 270 | fill: none; 271 | stroke: transparent; 272 | stroke-width: 10px; 273 | } 274 | g[type="curve"][selected] > path.stroke, g[type="curve"]:hover > path.stroke { 275 | stroke: #666; 276 | } 277 | `, 278 | updateSVGPath($g, scale, data) { 279 | data.transform( 280 | !data.line.input.param ? 'shortest' : 'normal', 281 | !data.line.output.param ? 'shortest' : 'normal', 282 | ); 283 | let cpx1 = 0; // 起始点的控制点上的 x 坐标 284 | let cpy1 = 0; // 起始点的控制点上的 y 坐标 285 | let cpx2 = 0; // 终点的控制点上的 x 坐标 286 | let cpy2 = 0; // 终点的控制点上的 y 坐标 287 | 288 | if (data.d1 === 1) { 289 | cpx1 = data.x1; 290 | cpy1 = (data.y1 + data.y2) / 2; 291 | } else { 292 | cpx1 = (data.x1 + data.x2) / 2; 293 | cpy1 = data.y1; 294 | } 295 | if (data.d2 === 1) { 296 | cpx2 = data.x2; 297 | cpy2 = (data.y1 + data.y2) / 2; 298 | } else { 299 | cpx2 = (data.x1 + data.x2) / 2; 300 | cpy2 = data.y2; 301 | } 302 | 303 | // 生成曲线的时候,如果是单向曲线,需要预留的最低宽度 304 | const cm = 100; 305 | 306 | switch (data.r1) { 307 | case 'left': 308 | if (cpx1 - data.x1 > -cm) { 309 | cpx1 = data.x1 - cm; 310 | } 311 | cpy1 = data.y1; 312 | break; 313 | case 'right': 314 | if (cpx1 - data.x1 < cm) { 315 | cpx1 = data.x1 + cm; 316 | } 317 | cpy1 = data.y1; 318 | break; 319 | case 'down': 320 | if (cpy1 - data.y1 < cm) { 321 | cpy1 = data.y1 + cm; 322 | } 323 | cpx1 = data.x1; 324 | break; 325 | case 'up': 326 | if (cpy1 - data.y1 > -cm) { 327 | cpy1 = data.y1 - cm; 328 | } 329 | cpx1 = data.x1; 330 | break; 331 | } 332 | switch (data.r2) { 333 | case 'left': 334 | if (cpx2 - data.x2 > -cm) { 335 | cpx2 = data.x2 - cm; 336 | } 337 | cpy2 = data.y2; 338 | break; 339 | case 'right': 340 | if (cpx2 - data.x2 < cm) { 341 | cpx2 = data.x2 + cm; 342 | } 343 | cpy2 = data.y2; 344 | break; 345 | case 'down': 346 | if (cpy2 - data.y2 < cm) { 347 | cpy2 = data.y2 + cm; 348 | } 349 | cpx2 = data.x2; 350 | break; 351 | case 'up': 352 | if (cpy2 - data.y2 > -cm) { 353 | cpy2 = data.y2 - cm; 354 | } 355 | cpx2 = data.x2; 356 | break; 357 | } 358 | 359 | const $pathList = $g.querySelectorAll(`path`)!; 360 | $pathList[0].setAttribute('d', `M${data.x1},${data.y1} C${cpx1},${cpy1} ${cpx2},${cpy2} ${data.x2},${data.y2}`); 361 | $pathList[1].setAttribute('d', `M${data.x1},${data.y1} C${cpx1},${cpy1} ${cpx2},${cpy2} ${data.x2},${data.y2}`); 362 | }, 363 | }); 364 | registerLine('*', '*', queryLine('*', 'curve')); 365 | 366 | // Graph 367 | 368 | export function registerGraphFilter(graphType: string, info: GraphFliter) { 369 | if (!graphTypeMap.has(graphType)) { 370 | graphTypeMap.set(graphType, generateDefaultGraph()); 371 | } 372 | const graphInfo = graphTypeMap.get(graphType)!; 373 | const keys = Object.keys(info) as unknown as (keyof GraphFliter)[]; 374 | keys.forEach((key) => { 375 | graphInfo.graphFilter[key] = info[key]; 376 | }); 377 | } 378 | 379 | export function queryGraphFliter(graphType: string, key: keyof GraphFliter) { 380 | const info = graphTypeMap.get(graphType) || graphTypeMap.get('*'); 381 | const filter = info!.graphFilter[key] || graphTypeMap.get('*')!.graphFilter[key]; 382 | return filter; 383 | } 384 | 385 | registerGraphFilter('*', { 386 | lineFilter(nodes, lines, line, input, output) { 387 | if (input && output && (input.type !== output.type)) { 388 | return false; 389 | } 390 | return true; 391 | }, 392 | }); 393 | -------------------------------------------------------------------------------- /element/graph/source/theme/class-diagram.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import '../index'; 4 | import { BaseElement } from '@itharbors/ui-core'; 5 | import { registerLine, registerNode, registerGraphOption } from '../manager'; 6 | 7 | function getAngle(x1: number, y1: number, x2: number, y2: number) { 8 | const deltaX = x2 - x1; 9 | const deltaY = y2 - y1; 10 | const angleRadians = Math.atan2(deltaY, deltaX); 11 | const angleDegrees = angleRadians * 180 / Math.PI; 12 | return angleDegrees; 13 | } 14 | 15 | registerGraphOption('class-diagram', { 16 | backgroundColor: '#2f2f2f', 17 | }); 18 | 19 | // 继承关系:用一个带空心三角形的实线箭头表示,箭头指向父类 20 | registerLine('class-diagram', 'inheritance', { 21 | template: /*svg*/` 22 | 23 | 24 | `, 25 | style: /*css*/` 26 | g[type="inheritance"] > path { 27 | fill: none; 28 | stroke: #fafafa; 29 | stroke-width: 1px; 30 | } 31 | g[type="inheritance"] > polygon { 32 | fill: none; 33 | stroke: #fafafa; 34 | stroke-width: 1px; 35 | } 36 | `, 37 | updateSVGPath($g, scale, info) { 38 | info.transform( 39 | !info.line.input.param ? 'shortest' : 'normal', 40 | !info.line.output.param ? 'shortest' : 'normal', 41 | ); 42 | const angle = getAngle(info.x1, info.y1, info.x2, info.y2); 43 | 44 | const a = 10 * Math.cos((180 - angle) * Math.PI / 180); 45 | const b = 10 * Math.sin((180 - angle) * Math.PI / 180); 46 | 47 | const $path = $g.querySelector(`path`)!; 48 | $path.setAttribute('d', `M${info.x1},${info.y1} L${info.x2 + a},${info.y2 - b}`); 49 | 50 | const c1x = info.x2; // 三角形顶点坐标 51 | const c1y = info.y2; 52 | 53 | const c2x = c1x + 6; 54 | const c2y = c1y - 10; 55 | 56 | const c3x = c1x - 6; 57 | const c3y = c1y - 10; 58 | 59 | const $polygon = $g.querySelector(`polygon`)!; 60 | $polygon.setAttribute('points', `${c1x},${c1y} ${c2x},${c2y} ${c3x},${c3y}`); 61 | $polygon.setAttribute('style', `transform-origin: ${c1x}px ${c1y}px; transform: rotate(${angle - 90}deg)`); 62 | }, 63 | }); 64 | 65 | // 实现关系:用一个带空心三角形的虚线箭头表示,箭头指向实现的接口 66 | registerLine('class-diagram', 'realization', { 67 | template: /*svg*/` 68 | 69 | 70 | `, 71 | style: /*css*/` 72 | g[type="realization"] > path { 73 | fill: none; 74 | stroke: #fafafa; 75 | stroke-width: 1px; 76 | stroke-dasharray: 5, 5; 77 | } 78 | g[type="realization"] > polygon { 79 | fill: none; 80 | stroke: #fafafa; 81 | stroke-width: 1px; 82 | } 83 | `, 84 | updateSVGPath($g, scale, info) { 85 | info.transform( 86 | !info.line.input.param ? 'shortest' : 'normal', 87 | !info.line.output.param ? 'shortest' : 'normal', 88 | ); 89 | const angle = getAngle(info.x1, info.y1, info.x2, info.y2); 90 | 91 | const c1x = info.x2; // 三角形顶点坐标 92 | const c1y = info.y2; 93 | 94 | const c2x = c1x + 6; 95 | const c2y = c1y - 10; 96 | 97 | const c3x = c1x - 6; 98 | const c3y = c1y - 10; 99 | 100 | const a = 10 * Math.cos((180 - angle) * Math.PI / 180); 101 | const b = 10 * Math.sin((180 - angle) * Math.PI / 180); 102 | 103 | const $path = $g.querySelector(`path`)!; 104 | $path.setAttribute('d', `M${info.x1},${info.y1} L${info.x2 + a},${info.y2 - b}`); 105 | 106 | const $polygon = $g.querySelector(`polygon`)!; 107 | $polygon.setAttribute('points', `${c1x},${c1y} ${c2x},${c2y} ${c3x},${c3y}`); 108 | $polygon.setAttribute('style', `transform-origin: ${c1x}px ${c1y}px; transform: rotate(${angle - 90}deg)`); 109 | }, 110 | }); 111 | 112 | // 关联关系:用一个带实心箭头的实线表示,箭头指向关联的类 113 | registerLine('class-diagram', 'association', { 114 | template: /*svg*/` 115 | 116 | 117 | `, 118 | style: /*css*/` 119 | g[type="association"] > path { 120 | fill: none; 121 | stroke: #fafafa; 122 | stroke-width: 1px; 123 | } 124 | g[type="association"] > polygon { 125 | fill: #fafafa; 126 | stroke: none; 127 | } 128 | `, 129 | updateSVGPath($g, scale, info) { 130 | info.transform( 131 | !info.line.input.param ? 'shortest' : 'normal', 132 | !info.line.output.param ? 'shortest' : 'normal', 133 | ); 134 | const angle = getAngle(info.x1, info.y1, info.x2, info.y2); 135 | const c1x = info.x2; // 三角形顶点坐标 136 | const c1y = info.y2; 137 | 138 | const c2x = c1x + 6; 139 | const c2y = c1y - 10; 140 | 141 | const c3x = c1x - 6; 142 | const c3y = c1y - 10; 143 | 144 | const c4x = c1x; 145 | const c4y = c1y - 6; 146 | 147 | const a = 6 * Math.cos((180 - angle) * Math.PI / 180); 148 | const b = 6 * Math.sin((180 - angle) * Math.PI / 180); 149 | 150 | const $path = $g.querySelector(`path`)!; 151 | $path.setAttribute('d', `M${info.x1},${info.y1} L${info.x2 + a},${info.y2 - b}`); 152 | 153 | const $polygon = $g.querySelector(`polygon`)!; 154 | $polygon.setAttribute('points', `${c1x},${c1y} ${c2x},${c2y} ${c4x},${c4y} ${c3x},${c3y}`); 155 | $polygon.setAttribute('style', `transform-origin: ${c1x}px ${c1y}px; transform: rotate(${angle - 90}deg)`); 156 | }, 157 | }); 158 | 159 | // 聚合关系:用一个带空心菱形的实线表示,菱形指向整体,箭头指向局部 160 | registerLine('class-diagram', 'aggregation', { 161 | template: /*svg*/` 162 | 163 | 164 | `, 165 | style: /*css*/` 166 | g[type="aggregation"] > path { 167 | fill: none; 168 | stroke: #fafafa; 169 | stroke-width: 1px; 170 | } 171 | g[type="aggregation"] > polygon { 172 | fill: none; 173 | stroke: #fafafa; 174 | } 175 | `, 176 | updateSVGPath($g, scale, info) { 177 | info.transform( 178 | !info.line.input.param ? 'shortest' : 'normal', 179 | !info.line.output.param ? 'shortest' : 'normal', 180 | ); 181 | const angle = getAngle(info.x1, info.y1, info.x2, info.y2); 182 | const c1x = info.x2; // 三角形顶点坐标 183 | const c1y = info.y2; 184 | 185 | const c2x = c1x + 6; 186 | const c2y = c1y - 10; 187 | 188 | const c3x = c1x - 6; 189 | const c3y = c1y - 10; 190 | 191 | const c4x = c1x; 192 | const c4y = c1y - 20; 193 | 194 | const a = 20 * Math.cos((180 - angle) * Math.PI / 180); 195 | const b = 20 * Math.sin((180 - angle) * Math.PI / 180); 196 | 197 | const $path = $g.querySelector(`path`)!; 198 | $path.setAttribute('d', `M${info.x1},${info.y1} L${info.x2 + a},${info.y2 - b}`); 199 | 200 | const $polygon = $g.querySelector(`polygon`)!; 201 | $polygon.setAttribute('points', `${c1x},${c1y} ${c2x},${c2y} ${c4x},${c4y} ${c3x},${c3y}`); 202 | $polygon.setAttribute('style', `transform-origin: ${c1x}px ${c1y}px; transform: rotate(${angle - 90}deg)`); 203 | }, 204 | }); 205 | 206 | // 组合关系:用一个带实心菱形的实线表示,菱形指向整体,箭头指向局部 207 | registerLine('class-diagram', 'composition', { 208 | template: /*svg*/` 209 | 210 | 211 | `, 212 | style: /*css*/` 213 | g[type="composition"] > path { 214 | fill: none; 215 | stroke: #fafafa; 216 | stroke-width: 1px; 217 | } 218 | g[type="composition"] > polygon { 219 | fill: #fafafa; 220 | stroke: none; 221 | } 222 | `, 223 | updateSVGPath($g, scale, info) { 224 | info.transform( 225 | !info.line.input.param ? 'shortest' : 'normal', 226 | !info.line.output.param ? 'shortest' : 'normal', 227 | ); 228 | const angle = getAngle(info.x1, info.y1, info.x2, info.y2); 229 | const c1x = info.x2; // 三角形顶点坐标 230 | const c1y = info.y2; 231 | 232 | const c2x = c1x + 6; 233 | const c2y = c1y - 10; 234 | 235 | const c3x = c1x - 6; 236 | const c3y = c1y - 10; 237 | 238 | const c4x = c1x; 239 | const c4y = c1y - 20; 240 | 241 | const a = 20 * Math.cos((180 - angle) * Math.PI / 180); 242 | const b = 20 * Math.sin((180 - angle) * Math.PI / 180); 243 | 244 | const $path = $g.querySelector(`path`)!; 245 | $path.setAttribute('d', `M${info.x1},${info.y1} L${info.x2 + a},${info.y2 - b}`); 246 | 247 | const $polygon = $g.querySelector(`polygon`)!; 248 | $polygon.setAttribute('points', `${c1x},${c1y} ${c2x},${c2y} ${c4x},${c4y} ${c3x},${c3y}`); 249 | $polygon.setAttribute('style', `transform-origin: ${c1x}px ${c1y}px; transform: rotate(${angle - 90}deg)`); 250 | }, 251 | }); 252 | 253 | // 依赖关系:用一个带箭头的虚线表示,箭头指向被依赖的类 254 | registerLine('class-diagram', 'dependency', { 255 | template: /*svg*/` 256 | 257 | 258 | `, 259 | style: /*css*/` 260 | g[type="dependency"] > path { 261 | fill: none; 262 | stroke: #fafafa; 263 | stroke-width: 1px; 264 | stroke-dasharray: 5, 5; 265 | } 266 | g[type="dependency"] > polygon { 267 | fill: #fafafa; 268 | stroke: none; 269 | } 270 | `, 271 | updateSVGPath($g, scale, info) { 272 | info.transform( 273 | !info.line.input.param ? 'shortest' : 'normal', 274 | !info.line.output.param ? 'shortest' : 'normal', 275 | ); 276 | const angle = getAngle(info.x1, info.y1, info.x2, info.y2); 277 | const c1x = info.x2; // 三角形顶点坐标 278 | const c1y = info.y2; 279 | 280 | const c2x = c1x + 6; 281 | const c2y = c1y - 10; 282 | 283 | const c3x = c1x - 6; 284 | const c3y = c1y - 10; 285 | 286 | const c4x = c1x; 287 | const c4y = c1y - 6; 288 | 289 | const a = 6 * Math.cos((180 - angle) * Math.PI / 180); 290 | const b = 6 * Math.sin((180 - angle) * Math.PI / 180); 291 | 292 | const $path = $g.querySelector(`path`)!; 293 | $path.setAttribute('d', `M${info.x1},${info.y1} L${info.x2 + a},${info.y2 - b}`); 294 | 295 | const $polygon = $g.querySelector(`polygon`)!; 296 | $polygon.setAttribute('points', `${c1x},${c1y} ${c2x},${c2y} ${c4x},${c4y} ${c3x},${c3y}`); 297 | $polygon.setAttribute('style', `transform-origin: ${c1x}px ${c1y}px; transform: rotate(${angle - 90}deg)`); 298 | }, 299 | }); 300 | 301 | // 节点 302 | registerNode('class-diagram', 'class-node', { 303 | template: /*html*/` 304 |
305 |
306 |
307 | 308 | 316 | `, 317 | 318 | style: /*css*/` 319 | :host { 320 | background: #2b2b2bcc; 321 | border: 1px solid #333; 322 | border-radius: 4px; 323 | color: #ccc; 324 | transition: box-shadow 0.2s, border 0.2s; 325 | white-space: nowrap; 326 | } 327 | :host(:hover) { 328 | border-color: white; 329 | box-shadow: 0px 0px 14px 2px white; 330 | } 331 | header { 332 | background: #227f9b; 333 | border-radius: 4px 4px 0 0; 334 | padding: 4px 10px; 335 | text-align: center; 336 | } 337 | section { 338 | min-height: 20px; 339 | border-left: 1px solid #666; 340 | border-right: 1px solid #666; 341 | border-bottom: 1px solid #666; 342 | padding: 4px 0; 343 | } 344 | section > div { 345 | padding: 2px 10px; 346 | } 347 | .menu { 348 | position: absolute; 349 | left: 0; 350 | top: 0; 351 | background: #2b2b2bcc; 352 | border: 1px solid #ccc; 353 | border-radius: 4px; 354 | } 355 | .menu > div { 356 | padding: 4px 10px; 357 | border-bottom: 1px solid #ccc; 358 | } 359 | .menu > div:last-child { 360 | border: none; 361 | } 362 | `, 363 | 364 | onInit(details) { 365 | const $menu = this.shadowRoot!.querySelector('.menu')!; 366 | this.shadowRoot!.querySelector('.class-name')!.innerHTML = details.name; 367 | 368 | this.addEventListener('mousedown', (event) => { 369 | event.stopPropagation(); 370 | event.preventDefault(); 371 | 372 | if (event.button === 0) { 373 | this.startMove(); 374 | return; 375 | } 376 | 377 | if (this.hasConnect()) { 378 | this.startConnect(''); 379 | return; 380 | } 381 | $menu.removeAttribute('hidden'); 382 | function mousedown(event: MouseEvent) { 383 | $menu.setAttribute('hidden', ''); 384 | document.removeEventListener('mousedown', mousedown, true); 385 | } 386 | document.addEventListener('mousedown', mousedown, true); 387 | }); 388 | 389 | $menu.addEventListener('mousedown', (event) => { 390 | event.stopPropagation(); 391 | event.preventDefault(); 392 | const type = (event.target! as HTMLDivElement).innerHTML.toLocaleLowerCase(); 393 | 394 | this.startConnect(type); 395 | }); 396 | }, 397 | onUpdate(details) { 398 | const updateHTML = (type: string, list: string[]) => { 399 | if (!Array.isArray(list)) { 400 | return; 401 | } 402 | let HTML = ``; 403 | for (const item of list) { 404 | HTML += `
${item}
`; 405 | } 406 | this.shadowRoot!.querySelector(`.${type}`)!.innerHTML = HTML; 407 | } 408 | this.data.addPropertyListener('details', (details) => { 409 | updateHTML('property', details.property); 410 | }); 411 | updateHTML('property', details.property); 412 | 413 | this.data.addPropertyListener('details', (details) => { 414 | updateHTML('function', details.function); 415 | }); 416 | updateHTML('function', details.function); 417 | }, 418 | }); 419 | -------------------------------------------------------------------------------- /element/graph/source/theme/flow-chart.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import '../index'; 4 | import { BaseElement } from '@itharbors/ui-core'; 5 | import { registerLine, registerNode, registerGraphFilter, registerGraphOption } from '../manager'; 6 | 7 | function getAngle(x1: number, y1: number, x2: number, y2: number) { 8 | const deltaX = x2 - x1; 9 | const deltaY = y2 - y1; 10 | const angleRadians = Math.atan2(deltaY, deltaX); 11 | const angleDegrees = angleRadians * 180 / Math.PI; 12 | return angleDegrees; 13 | } 14 | 15 | registerGraphOption('flow-chart', { 16 | backgroundColor: '#2f2f2f', 17 | }); 18 | 19 | registerLine('flow-chart', 'curve', { 20 | template: /*svg*/` 21 | 22 | 23 | `, 24 | style: /*css*/` 25 | g[type="curve"] > path { 26 | fill: none; 27 | stroke: #fafafa; 28 | stroke-width: 1px; 29 | transition: stroke 0.3s; 30 | } 31 | g[type="curve"] > polygon { 32 | fill: none; 33 | stroke: #fafafa; 34 | stroke-width: 1px; 35 | } 36 | `, 37 | updateSVGPath($g, scale, info) { 38 | info.transform( 39 | !info.line.input.param ? 'shortest' : 'normal', 40 | !info.line.output.param ? 'shortest' : 'normal', 41 | ); 42 | const angle = getAngle(info.x1, info.y1, info.x2, info.y2); 43 | 44 | const c1x = info.x2; // 三角形顶点坐标 45 | const c1y = info.y2; 46 | 47 | const c2x = c1x + 6; 48 | const c2y = c1y - 10; 49 | 50 | const c3x = c1x - 6; 51 | const c3y = c1y - 10; 52 | 53 | const a = 10 * Math.cos((180 - angle) * Math.PI / 180); 54 | const b = 10 * Math.sin((180 - angle) * Math.PI / 180); 55 | 56 | const $path = $g.querySelector(`path`)!; 57 | $path.setAttribute('d', `M${info.x1},${info.y1} L${info.x2 + a},${info.y2 - b}`); 58 | 59 | const $polygon = $g.querySelector(`polygon`)!; 60 | $polygon.setAttribute('points', `${c1x},${c1y} ${c2x},${c2y} ${c3x},${c3y}`); 61 | $polygon.setAttribute('style', `transform-origin: ${c1x}px ${c1y}px; transform: rotate(${angle - 90}deg)`); 62 | }, 63 | }); 64 | 65 | // 节点 66 | registerNode('flow-chart', 'node', { 67 | template: /*html*/` 68 |
69 | `, 70 | 71 | style: /*css*/` 72 | :host { 73 | background: #2b2b2bcc; 74 | border: 1px solid #333; 75 | border-radius: 4px; 76 | color: #ccc; 77 | transition: box-shadow 0.2s, border 0.2s; 78 | } 79 | :host(:hover) { 80 | border-color: white; 81 | box-shadow: 0px 0px 14px 2px white; 82 | } 83 | section { 84 | min-height: 20px; 85 | border: 1px solid #999; 86 | border-radius: 4px; 87 | padding: 4px 10px; 88 | text-align: center; 89 | } 90 | `, 91 | 92 | onInit(details) { 93 | this.bindDefaultMoveEvent(); 94 | 95 | const updateHTML = (HTML: string) => { 96 | this.shadowRoot!.querySelector(`.content`)!.innerHTML = HTML; 97 | } 98 | this.data.addPropertyListener('details', (details) => { 99 | updateHTML(details.label); 100 | }); 101 | }, 102 | onUpdate(details) { 103 | this.shadowRoot!.querySelector(`.content`)!.innerHTML = details.label; 104 | }, 105 | }); 106 | -------------------------------------------------------------------------------- /element/graph/test/base.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; -------------------------------------------------------------------------------- /element/graph/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "commonjs", /* Specify what module code is generated. */ 28 | "rootDir": "./source", /* Specify the root folder within your source files. */ 29 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | // "resolveJsonModule": true, /* Enable importing .json files */ 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 50 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 51 | // "removeComments": true, /* Disable emitting comments. */ 52 | // "noEmit": true, /* Disable emitting files from a compilation. */ 53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 68 | 69 | /* Interop Constraints */ 70 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 71 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 72 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 73 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 74 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 75 | 76 | /* Type Checking */ 77 | "strict": true, /* Enable all strict type-checking options. */ 78 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 79 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 80 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 81 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 82 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 83 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 84 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 85 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 86 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 87 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 88 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 89 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 90 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 91 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 92 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 93 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 94 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 95 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 96 | 97 | /* Completeness */ 98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 99 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /element/tree/.npmignore: -------------------------------------------------------------------------------- 1 | /design 2 | /scripts 3 | /playwright 4 | /source 5 | /test 6 | /example 7 | /tsconfgi.json 8 | .* -------------------------------------------------------------------------------- /element/tree/README.MD: -------------------------------------------------------------------------------- 1 | # UI TREE 2 | 3 | ```html 4 | 5 | 6 | 17 | ``` -------------------------------------------------------------------------------- /element/tree/example/custom.esm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export function initCustomElement($tree) { 4 | const treeList = [ 5 | { 6 | name: 'test-1', 7 | fold: false, 8 | details: {}, 9 | children: [ 10 | { 11 | name: 'test-1.1', 12 | fold: false, 13 | details: { 14 | // 自定义附加数据 15 | color: 'green', 16 | }, 17 | children: [], 18 | }, 19 | { 20 | name: 'test-1.2', 21 | fold: false, 22 | details: { 23 | // 自定义附加数据 24 | color: 'red', 25 | }, 26 | }, 27 | ], 28 | }, 29 | { 30 | name: 'test-2', 31 | fold: false, 32 | details: {}, 33 | children: [ 34 | { 35 | name: 'test-2.1', 36 | fold: false, 37 | details: { 38 | // 自定义附加数据 39 | color: 'blue', 40 | }, 41 | }, 42 | ], 43 | }, 44 | ]; 45 | 46 | $tree.setTreeData(treeList); 47 | 48 | $tree.registerStyle(/*css*/` 49 | v-tree-item { 50 | display: flex; 51 | } 52 | v-tree-item[color=green] { 53 | color: green; 54 | } 55 | v-tree-item[color=red] { 56 | color: red; 57 | } 58 | v-tree-item[color=blue] { 59 | color: blue; 60 | } 61 | svg[fold] { 62 | transform: rotate(-90deg); 63 | } 64 | span[ref=tab] { 65 | display: inline-block; 66 | padding-left: calc(var(--item-tab) * 24px); 67 | } 68 | span[ref=name] { 69 | flex: 1; 70 | } 71 | span[ref=right] { 72 | cursor: pointer; 73 | } 74 | `); 75 | 76 | $tree.registerUpdateItem(($item, data) => { 77 | const $tab = $item.querySelector('[ref=tab]'); 78 | const $name = $item.querySelector('[ref=name]'); 79 | const $arrow = $item.querySelector('[ref=arrow]'); 80 | const $right = $item.querySelector('[ref=right]'); 81 | 82 | // 名字 83 | $name.innerHTML = data.name; 84 | // 缩进 85 | $item.setAttribute('style', `--item-tab: ${data.tab}`); 86 | // 箭头 87 | if (data.fold) { 88 | $arrow.setAttribute('fold', ''); 89 | } else { 90 | $arrow.removeAttribute('fold'); 91 | } 92 | if (data.arrow) { 93 | $arrow.removeAttribute('hidden'); 94 | } else { 95 | $arrow.setAttribute('hidden', ''); 96 | } 97 | 98 | if (data.data.details.color) { 99 | $right.innerHTML = `${data.data.details.color}`; 100 | const $span = $right.querySelector('span'); 101 | $span.addEventListener('click', () => { 102 | alert(data.data.details.color); 103 | }); 104 | $item.setAttribute('color', data.data.details.color); 105 | } else { 106 | $right.innerHTML = ''; 107 | $item.setAttribute('color', ''); 108 | } 109 | }); 110 | } 111 | -------------------------------------------------------------------------------- /element/tree/example/custom.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | UI Tree - Custom 7 | 8 | 9 | 20 | 21 | 22 | 23 | 24 |
25 |

树形列表展示

26 |
27 | 28 | 29 | 30 | 31 | 33 | 34 | 35 | 36 | 37 | 38 |
39 |
40 | 41 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /element/tree/example/list.esm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export function initConsoleElement($tree) { 4 | const treeList = []; 5 | let black = false; 6 | for (let i = 0; i < 1000000; i++) { 7 | black = !black; 8 | treeList.push({ 9 | name: `message_${i}`, 10 | fold: true, 11 | details: { 12 | black: black, 13 | }, 14 | }); 15 | } 16 | 17 | $tree.setTreeData(treeList); 18 | } 19 | -------------------------------------------------------------------------------- /element/tree/example/list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | UI Tree - List 7 | 8 | 9 | 19 | 20 | 21 | 22 |
23 |

100w 条数据展示

24 |
25 | 26 |
27 | 28 |
29 | 30 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /element/tree/example/tree.esm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export function initTreeElement($tree) { 4 | const treeList = [ 5 | { 6 | name: 'test-1', 7 | fold: false, 8 | details: {}, 9 | children: [ 10 | { 11 | name: 'test-1.1', 12 | fold: true, 13 | details: {}, 14 | children: [ 15 | { 16 | name: 'test-1.1.1', 17 | fold: false, 18 | details: {}, 19 | children: [], 20 | }, 21 | ], 22 | }, 23 | { 24 | name: 'test-1.2', 25 | fold: false, 26 | details: {}, 27 | children: [ 28 | { 29 | name: 'test-1.2.1', 30 | fold: false, 31 | details: {}, 32 | children: [ 33 | { 34 | name: 'test-1.2.1.1', 35 | fold: false, 36 | details: {}, 37 | children: [], 38 | }, 39 | ], 40 | }, 41 | ], 42 | }, 43 | ], 44 | }, 45 | { 46 | name: 'test-2', 47 | fold: false, 48 | details: {}, 49 | children: [ 50 | { 51 | name: 'test-2.1', 52 | fold: false, 53 | details: {}, 54 | children: [ 55 | { 56 | name: 'test-2.1.1', 57 | fold: false, 58 | details: {}, 59 | children: [ 60 | { 61 | name: 'test-2.1.1.1', 62 | fold: false, 63 | details: {}, 64 | children: [ 65 | { 66 | name: 'test-2.1.1.1.1', 67 | fold: false, 68 | details: {}, 69 | children: [ 70 | { 71 | name: 'test-2.1.1.1.1.1', 72 | fold: false, 73 | details: {}, 74 | children: [ 75 | { 76 | name: 'test-2.1.1.1.1.1.1', 77 | fold: false, 78 | details: {}, 79 | children: [ 80 | { 81 | name: 'test-2.1.1.1.1.1.1.1', 82 | fold: false, 83 | details: {}, 84 | children: [ 85 | { 86 | name: 'test-2.1.1.1.1.1.1.1.1', 87 | fold: false, 88 | details: {}, 89 | children: [], 90 | }, 91 | ], 92 | }, 93 | ], 94 | }, 95 | ], 96 | }, 97 | ], 98 | }, 99 | ], 100 | }, 101 | ], 102 | }, 103 | ], 104 | }, 105 | { 106 | name: 'test-2.2', 107 | fold: true, 108 | details: {}, 109 | children: [ 110 | { 111 | name: 'test-2.2.1', 112 | fold: false, 113 | details: {}, 114 | children: [ 115 | { 116 | name: 'test-2.2.1.1', 117 | fold: false, 118 | details: {}, 119 | children: [], 120 | }, 121 | ], 122 | }, 123 | ], 124 | }, 125 | ], 126 | }, 127 | ]; 128 | $tree.setTreeData(treeList); 129 | } 130 | -------------------------------------------------------------------------------- /element/tree/example/tree.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | UI Tree - Tree 7 | 8 | 9 | 20 | 21 | 22 | 23 | 24 |
25 |

树形列表展示

26 |
27 | 28 |
29 |
30 | 31 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /element/tree/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@itharbors/ui-tree", 3 | "version": "0.2.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "node ./scripts/dev", 8 | "build": "node ./scripts/build" 9 | }, 10 | "author": "VisualSJ", 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "license": "ISC", 15 | "dependencies": { 16 | "@itharbors/ui-core": "^1.0.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /element/tree/scripts/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { spawn } = require('child_process'); 4 | 5 | const spawnAsync = function(...cmd) { 6 | return new Promise((resolve) => { 7 | const child = spawn('npx', [...cmd], { 8 | stdio: [0, 1, 2], 9 | }); 10 | child.on('exit', () => { 11 | resolve(); 12 | }); 13 | }); 14 | }; 15 | 16 | const exec = async function() { 17 | await spawnAsync('tsc'); 18 | await spawnAsync('esbuild', './dist/index.js', '--bundle', '--outfile=./bundle/ui-tree.esm.js'); 19 | }; 20 | 21 | exec(); 22 | -------------------------------------------------------------------------------- /element/tree/scripts/dev.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { spawn } = require('child_process'); 4 | 5 | const spawnAsync = function(...cmd) { 6 | return new Promise((resolve) => { 7 | const child = spawn('npx', [...cmd], { 8 | stdio: [0, 1, 2], 9 | }); 10 | child.on('exit', () => { 11 | resolve(); 12 | }); 13 | }); 14 | }; 15 | 16 | const exec = async function() { 17 | await spawnAsync('tsc', '-w'); 18 | }; 19 | 20 | exec(); 21 | -------------------------------------------------------------------------------- /element/tree/source/element-item.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import type { 4 | TreeItemRenderData, 5 | DetailChangeFold, 6 | DetailItemDataUpdated, 7 | } from './interface'; 8 | 9 | import { 10 | BaseElement, 11 | registerElement, 12 | style, 13 | } from '@itharbors/ui-core'; 14 | 15 | const HTML = /*html*/` 16 | 17 |
18 | 19 | 20 | 21 |
22 | `; 23 | 24 | const STYLE = /*css*/` 25 | ${style.solid} 26 | :host { 27 | display: block; 28 | box-sizing: border-box; 29 | } 30 | :host([custom]) > section { 31 | display: none; 32 | } 33 | [ref=tab] { 34 | display: inline-block; 35 | padding-left: calc(var(--item-tab) * 24px); 36 | } 37 | [ref=arrow] { 38 | position: relative; 39 | top: 1px; 40 | display: inline-block; 41 | cursor: pointer; 42 | } 43 | [ref=arrow][fold] { 44 | transform: rotate(-90deg); 45 | } 46 | [ref=arrow][hidden] { 47 | visibility: hidden; 48 | } 49 | [ref=arrow]:before { 50 | content: ''; 51 | display: inline-block; 52 | width: calc(var(--item-element-height) / 4); 53 | height: calc(var(--item-element-height) / 4); 54 | position: relative; 55 | top: calc(var(--item-element-height) / -8); 56 | transform: rotate(-135deg); 57 | border-left: 2px solid #000; 58 | border-top: 2px solid #000; 59 | } 60 | `; 61 | 62 | export class TreeItemElement extends BaseElement { 63 | get HTMLTemplate() { 64 | return HTML; 65 | } 66 | 67 | get HTMLStyle() { 68 | return STYLE; 69 | } 70 | 71 | get defaultData(): { 72 | // 显示数据 73 | data: TreeItemRenderData, 74 | } { 75 | return { 76 | data: { 77 | name: '', 78 | fold: false, 79 | arrow: false, 80 | tab: 0, 81 | data: undefined 82 | }, 83 | }; 84 | } 85 | 86 | public root!: HTMLElement | ShadowRoot; 87 | 88 | private bindEvent() { 89 | const $arrow = this.root.querySelector('[ref=arrow]'); 90 | 91 | this.data.addPropertyListener('data', (data) => { 92 | this.dispatch('item-data-updated', { 93 | detail: { 94 | elem: this, 95 | data 96 | }, 97 | }); 98 | }); 99 | 100 | if ($arrow) { 101 | $arrow.addEventListener('mousedown', (event) => { 102 | event.stopPropagation(); 103 | event.preventDefault(); 104 | const data = this.data.getProperty('data'); 105 | this.dispatch('change-fold', { 106 | detail: { 107 | elem: this, 108 | fold: !data.fold, 109 | }, 110 | }); 111 | }); 112 | } 113 | } 114 | 115 | onInit() { 116 | if (this.innerHTML) { 117 | this.setAttribute('custom', ''); 118 | this.root = this; 119 | } else { 120 | this.removeAttribute('custom'); 121 | this.root = this.shadowRoot; 122 | } 123 | this.bindEvent(); 124 | } 125 | 126 | onMounted() { 127 | 128 | } 129 | 130 | onRemoved() { 131 | 132 | } 133 | } 134 | 135 | registerElement('tree-item', TreeItemElement); 136 | -------------------------------------------------------------------------------- /element/tree/source/element-tree.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import type { 4 | DetailChangeFold, 5 | DetailItemDataUpdated, 6 | TreeItem, 7 | TreeItemRenderData, 8 | } from './interface'; 9 | 10 | import type { 11 | TreeItemElement, 12 | } from './element-item'; 13 | 14 | import { 15 | BaseElement, 16 | registerElement, 17 | style, 18 | } from '@itharbors/ui-core'; 19 | 20 | import { 21 | convertTreeToList, 22 | } from './utils'; 23 | 24 | const HTML = /*html*/` 25 | 26 | 27 |
28 | `; 29 | 30 | const STYLE = /*css*/` 31 | ${style.solid} 32 | :host { 33 | display: block; 34 | border: 1px solid; 35 | box-sizing: border-box; 36 | overflow: auto; 37 | border-radius: calc(var(--ui-size-radius) * 1px); 38 | border-color: var(--ui-color-default-line); 39 | color: var(--ui-color-default-contrast); 40 | } 41 | [hidden] { 42 | display: none; 43 | } 44 | :host > section { 45 | border: none; 46 | outline: none; 47 | background: none; 48 | padding: 0; 49 | line-height: var(--item-element-height); 50 | overflow: hidden; 51 | } 52 | :host v-tree-item { 53 | line-height: var(--item-element-height); 54 | height: var(--item-element-height); 55 | transform: translateY(calc(var(--item-offset-line) * var(--item-offset))) 56 | } 57 | slot { 58 | display: none; 59 | } 60 | `; 61 | 62 | export class TreeElement extends BaseElement { 63 | get HTMLTemplate() { 64 | return HTML; 65 | } 66 | 67 | get HTMLStyle() { 68 | return STYLE; 69 | } 70 | 71 | get defaultData(): { 72 | // 每一行的高度 73 | lineHeight: number; 74 | // 滚动的时候每一行的偏移量 75 | lineOffset: number; 76 | // 顶部到渲染区域的偏移行数 77 | offsetLine: number; 78 | // 现在的区域最多可以显示几行 79 | maxDisplayLine: number; 80 | // 用于显示的数据,会比总数据少 81 | list: TreeItemRenderData[], 82 | // 将 tree 转换成的列表,包含了所有可能显示的数据 83 | cacheList: TreeItemRenderData[], 84 | } { 85 | return { 86 | lineHeight: 0, 87 | lineOffset: 0, 88 | offsetLine: 0, 89 | maxDisplayLine: 0, 90 | list: [], 91 | cacheList: [], 92 | }; 93 | } 94 | 95 | private itemArray: TreeItemElement[] = []; 96 | private $section!: HTMLElement; 97 | 98 | /** 99 | * 绑定内部事件 100 | */ 101 | private bindEvent() { 102 | 103 | this.addEventListener('scroll', (event) => { 104 | event.stopPropagation(); 105 | event.preventDefault(); 106 | 107 | const lineHeight = this.getProperty('lineHeight'); 108 | const maxDisplayLine = this.getProperty('maxDisplayLine'); 109 | const list = this.getProperty('list'); 110 | 111 | const offsetScroll = this.scrollTop / (this.scrollHeight - this.clientHeight); 112 | const maxOffsetLine = list.length - maxDisplayLine; 113 | 114 | let offsetLine = Math.round(maxOffsetLine * offsetScroll)// Math.floor(this.scrollTop / lineHeight); 115 | if (offsetLine > list.length - this.itemArray.length) { 116 | offsetLine = list.length - this.itemArray.length; 117 | } 118 | if (offsetLine < 0) { 119 | offsetLine = 0; 120 | } 121 | this.setProperty('offsetLine', offsetLine); 122 | }); 123 | 124 | this.data.addPropertyListener('offsetLine', (offsetLine) => { 125 | const lineHeight = this.getProperty('lineHeight'); 126 | const lineOffset = this.getProperty('lineOffset'); 127 | const height = this.$section.style.height; 128 | this.$section.setAttribute('style', `--item-element-height: ${lineHeight}px; --item-offset: ${lineOffset}px; --item-offset-line: ${offsetLine}; height: ${height};`); 129 | this.applyItemData(); 130 | }); 131 | 132 | this.data.addPropertyListener('lineHeight', (lineHeight) => { 133 | const offsetLine = this.getProperty('offsetLine'); 134 | const lineOffset = this.getProperty('lineOffset'); 135 | const height = this.$section.style.height; 136 | this.$section.setAttribute('style', `--item-element-height: ${lineHeight}px; --item-offset: ${lineOffset}px; --item-offset-line: ${offsetLine}; height: ${height};`); 137 | }); 138 | 139 | this.data.addPropertyListener('lineOffset', (lineOffset) => { 140 | const offsetLine = this.getProperty('offsetLine'); 141 | const lineHeight = this.getProperty('lineHeight'); 142 | const height = this.$section.style.height; 143 | this.$section.setAttribute('style', `--item-element-height: ${lineHeight}px; --item-offset: ${lineOffset}px; --item-offset-line: ${offsetLine}; height: ${height};`); 144 | }); 145 | 146 | this.data.addPropertyListener('maxDisplayLine', (maxDisplayLine) => { 147 | const length = maxDisplayLine; 148 | const $template = this.querySelector('v-tree-item'); 149 | while (this.itemArray.length < length) { 150 | const $item = $template ? $template.cloneNode(true) as TreeItemElement : document.createElement('v-tree-item') as TreeItemElement; 151 | this.itemArray.push($item); 152 | this.$section.appendChild($item); 153 | } 154 | while (this.itemArray.length > length) { 155 | const elem = this.itemArray.pop(); 156 | elem?.remove(); 157 | } 158 | this.updateContentHeight(); 159 | this.applyItemData() 160 | }); 161 | 162 | this.data.addPropertyListener('list', () => { 163 | this.updateContentHeight(); 164 | this.applyItemData(); 165 | }); 166 | 167 | // item 事件处理 168 | this.shadowRoot.addEventListener('item-data-updated', (event) => { 169 | const cEvent = event as CustomEvent; 170 | const data = cEvent.detail.data; 171 | const $elem = cEvent.detail.elem; 172 | this.updateItem($elem, data); 173 | }); 174 | this.shadowRoot.addEventListener('change-fold', (event) => { 175 | const cEvent = event as CustomEvent; 176 | const data = cEvent.detail.elem.getProperty('data'); 177 | data.fold = cEvent.detail.fold; 178 | cEvent.detail.elem.data.touchProperty('data'); 179 | this.updateList(); 180 | }); 181 | } 182 | 183 | /** 184 | * 更新当前元素可以显示区域内,最多显示的行数 185 | */ 186 | private updateMaxDisplayLine() { 187 | const bounding = this.getBoundingClientRect(); 188 | const lineHeight = this.getProperty('lineHeight'); 189 | this.setProperty('maxDisplayLine', Math.ceil(bounding.height / lineHeight)); 190 | } 191 | 192 | /** 193 | * 更新内部元素的高度 194 | * 更新后将显示/隐藏滚动条 195 | */ 196 | private updateContentHeight() { 197 | const lineHeight = this.getProperty('lineHeight'); 198 | const list = this.getProperty('list'); 199 | let height = lineHeight * list.length; 200 | let lineOffset = lineHeight; 201 | let itemHeight = this.itemArray.length * lineHeight; 202 | if (height > 1000000) { 203 | lineOffset = (1000000 - itemHeight) / (height - itemHeight) * lineHeight; 204 | height = 1000000; 205 | } 206 | this.setProperty('lineOffset', lineOffset); 207 | this.$section.style.height = `${height}px`; 208 | } 209 | 210 | /** 211 | * 更新用于显示的 list 数据 212 | * 折叠状态变化的时候会重新生成 list 数组 213 | */ 214 | private updateList() { 215 | const cacheList = this.getProperty('cacheList'); 216 | const list: TreeItemRenderData[] = []; 217 | let tab = 9999; 218 | for (let item of cacheList) { 219 | if (item.tab > tab) { 220 | continue; 221 | } 222 | item.fold ? (tab = item.tab) : (tab = 9999); 223 | list.push(item); 224 | } 225 | this.setProperty('list', list); 226 | } 227 | 228 | /** 229 | * 将数据应用到所有 item 元素上 230 | */ 231 | private applyItemDateLock = false; 232 | private applyItemData() { 233 | if (this.applyItemDateLock) { 234 | return; 235 | } 236 | this.applyItemDateLock = true; 237 | requestAnimationFrame(() => { 238 | this.applyItemDateLock = false; 239 | const offsetLine = this.getProperty('offsetLine'); 240 | const list = this.getProperty('list'); 241 | this.itemArray.forEach(($item, index) => { 242 | const data = list[offsetLine + index]; 243 | if (data) { 244 | $item.removeAttribute('hidden'); 245 | data && $item.setProperty('data', data); 246 | } else { 247 | $item.setAttribute('hidden', ''); 248 | } 249 | }); 250 | }); 251 | } 252 | 253 | /** 254 | * 默认的更新 Item 元素的函数 255 | * 执行 registerUpdateItem 可以被替换 256 | * @param $item 257 | * @param data 258 | */ 259 | private updateItem($item: TreeItemElement, data: TreeItemRenderData) { 260 | const $name = $item.root.querySelector('[ref=name]'); 261 | const $arrow = $item.root.querySelector('[ref=arrow]'); 262 | 263 | // 名字 264 | $name && ($name.innerHTML = data.name + ''); 265 | // 缩进 266 | $item.setAttribute('style', `--item-tab: ${data.tab}`); 267 | // 箭头 268 | if ($arrow) { 269 | if (data.fold) { 270 | $arrow.setAttribute('fold', ''); 271 | } else { 272 | $arrow.removeAttribute('fold'); 273 | } 274 | if (data.arrow) { 275 | $arrow.removeAttribute('hidden'); 276 | } else { 277 | $arrow.setAttribute('hidden', ''); 278 | } 279 | } 280 | } 281 | 282 | onInit() { 283 | this.$section = this.shadowRoot.querySelector('section.content')! as HTMLElement; 284 | 285 | this.setProperty('lineHeight', 24); 286 | this.bindEvent(); 287 | requestAnimationFrame(() => { 288 | this.updateContentHeight(); 289 | this.updateMaxDisplayLine(); 290 | }); 291 | } 292 | 293 | onMounted() { 294 | 295 | } 296 | 297 | onRemoved() { 298 | 299 | } 300 | 301 | /** 302 | * 设置 Tree 数据 303 | * @param tree 304 | */ 305 | public setTreeData(tree: TreeItem[]) { 306 | const cacheList = convertTreeToList(tree); 307 | this.setProperty('cacheList', cacheList); 308 | this.updateList(); 309 | } 310 | 311 | /** 312 | * 注册自定义的样式 313 | * 这个样式只影响当前的 tree 元素 314 | * tree 使用 :host,相信信息参考 shadowDOM 文档 315 | * @param style 316 | */ 317 | public registerStyle(style: string) { 318 | const $style = this.shadowRoot.querySelector('style.custom')!; 319 | $style.innerHTML = style; 320 | } 321 | 322 | /** 323 | * 注册一个 Item 的更新函数 324 | * 使用了自定义 item 才会需要定义这个函数 325 | * @param func 326 | */ 327 | public registerUpdateItem(func: ($item: TreeItemElement, data: TreeItemRenderData) => void) { 328 | this.updateItem = func; 329 | } 330 | } 331 | 332 | registerElement('tree', TreeElement); 333 | -------------------------------------------------------------------------------- /element/tree/source/index.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export { 4 | TreeElement, 5 | } from './element-tree'; 6 | 7 | export { 8 | TreeItemElement, 9 | } from './element-item'; 10 | 11 | export { 12 | convertTreeToList, 13 | } from './utils'; 14 | -------------------------------------------------------------------------------- /element/tree/source/interface.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import type { TreeItemElement } from './element-item'; 4 | 5 | /** 6 | * 树形数据中每一个对象的结构 7 | */ 8 | export type TreeItem = { 9 | // 显示在界面上的文本 10 | name?: string; 11 | // 是否折叠子元素 12 | fold?: boolean; 13 | // 一些可供用户自定义的附加数据 14 | details?: T; 15 | // 子元素 16 | children?: TreeItem[]; 17 | } 18 | 19 | /** 20 | * 树形数据传入后,转换成用于渲染的数据 21 | */ 22 | export type TreeItemRenderData = { 23 | // 显示在界面上的文本 24 | name: string; 25 | // 是否折叠子元素 26 | fold: boolean; 27 | // 是否显示下拉箭头 28 | arrow: boolean; 29 | // 缩进层级 30 | tab: number; 31 | // 对象原始数据 32 | data?: TreeItem; 33 | } 34 | 35 | export interface DetailChangeFold { 36 | elem: TreeItemElement; 37 | fold: boolean; 38 | } 39 | 40 | export interface DetailItemDataUpdated { 41 | elem: TreeItemElement; 42 | data: TreeItemRenderData; 43 | } 44 | -------------------------------------------------------------------------------- /element/tree/source/utils.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import type { TreeItem, TreeItemRenderData } from './interface'; 4 | 5 | /** 6 | * 将一个树形数据,转成一个平级的列表 7 | * 传入非数组,返回空数组 8 | * 数组内传入错误数据,会转换成一个空的 item 对象 9 | * @param tree 10 | */ 11 | export function convertTreeToList(treeList: TreeItem[]): TreeItemRenderData[] { 12 | const list: TreeItemRenderData[] = []; 13 | if (!Array.isArray(treeList)) { 14 | return list; 15 | } 16 | for (let treeItem of treeList) { 17 | convertItemToRenderDataWithIndex(treeItem, 0, list); 18 | } 19 | return list; 20 | } 21 | 22 | function convertItemToRenderDataWithIndex(item: TreeItem, index: number, list: TreeItemRenderData[]) { 23 | if (typeof item !== 'object' || item === null) { 24 | list.push({ 25 | name: '', 26 | fold: false, 27 | tab: index, 28 | arrow: false, 29 | data: {}, 30 | }); 31 | return; 32 | } 33 | list.push({ 34 | name: item.name || '', 35 | fold: !!item.fold, 36 | tab: index, 37 | arrow: !!item.children, 38 | data: item, 39 | }); 40 | item.children && item.children.forEach((item) => { 41 | convertItemToRenderDataWithIndex(item, index + 1, list); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /element/tree/test/base.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { convertTreeToList } = require('../dist/utils'); 4 | const { deepEqual, equal } = require('assert'); 5 | 6 | describe('工具函数测试', () => { 7 | 8 | describe('convertTreeToList', () => { 9 | const illegalList = [ 10 | true, false, 11 | 0, 1, 100, 12 | null, undefined, 13 | '', 'abc', 14 | {}, new ArrayBuffer, new Function, 15 | ]; 16 | 17 | // 非法对象测试 18 | illegalList.forEach((item) => { 19 | const str = (item + '').replace(/\n/g, ''); 20 | it(`传入非法对象: ${str}`, () => { 21 | const list = convertTreeToList(item); 22 | deepEqual(list, []); 23 | }); 24 | }); 25 | 26 | // 数字内有非法对象 27 | illegalList.forEach((item) => { 28 | const str = (item + '').replace(/\n/g, ''); 29 | it(`数组内传入非法对象: ${str}`, () => { 30 | const list = convertTreeToList([item]); 31 | deepEqual(list[0], { 32 | name: '', 33 | fold: false, 34 | arrow: false, 35 | tab: 0, 36 | data: (typeof item === 'object' && item !== null) ? item : {}, 37 | }); 38 | }); 39 | }); 40 | 41 | it('传入一个只有 name 的对象', () => { 42 | const item = { 43 | name: 'test', 44 | }; 45 | const list = convertTreeToList([item]); 46 | deepEqual(list[0], { 47 | name: 'test', 48 | fold: false, 49 | arrow: false, 50 | tab: 0, 51 | data: item, 52 | }); 53 | equal(list.length, 1); 54 | }); 55 | 56 | it('传入一个只有 details 的对象', () => { 57 | const item = { 58 | details: {}, 59 | }; 60 | const list = convertTreeToList([item]); 61 | deepEqual(list[0], { 62 | name: '', 63 | fold: false, 64 | arrow: false, 65 | tab: 0, 66 | data: item, 67 | }); 68 | equal(list.length, 1); 69 | }); 70 | 71 | it('传入一个只有 fold 的对象', () => { 72 | const item = { 73 | fold: true, 74 | }; 75 | const list = convertTreeToList([item]); 76 | deepEqual(list[0], { 77 | name: '', 78 | fold: true, 79 | arrow: false, 80 | tab: 0, 81 | data: item, 82 | }); 83 | equal(list.length, 1); 84 | }); 85 | 86 | it('传入一个只有 children 的对象', () => { 87 | const item = { 88 | children: [], 89 | }; 90 | const list = convertTreeToList([item]); 91 | deepEqual(list[0], { 92 | name: '', 93 | fold: false, 94 | arrow: true, 95 | tab: 0, 96 | data: item, 97 | }); 98 | equal(list.length, 1); 99 | }); 100 | 101 | it('传入一个完整的对象', () => { 102 | const item = { 103 | name: 'test', 104 | fold: true, 105 | details: {}, 106 | children: [], 107 | }; 108 | const list = convertTreeToList([item]); 109 | deepEqual(list[0], { 110 | name: 'test', 111 | fold: true, 112 | arrow: true, 113 | tab: 0, 114 | data: item, 115 | }); 116 | equal(list.length, 1); 117 | }); 118 | 119 | it('传入一个带有多级子对象的对象', () => { 120 | const treeList = [ 121 | { 122 | name: 'test0', 123 | fold: true, 124 | details: {}, 125 | children: [ 126 | { 127 | name: 'test1', 128 | fold: true, 129 | details: {}, 130 | children: [ 131 | { 132 | name: 'test2', 133 | fold: true, 134 | details: {}, 135 | children: [], 136 | }, 137 | ], 138 | }, 139 | { 140 | name: 'test3', 141 | fold: true, 142 | details: {}, 143 | children: [], 144 | }, 145 | ], 146 | }, 147 | { 148 | name: 'test4', 149 | fold: true, 150 | details: {}, 151 | children: [], 152 | }, 153 | ]; 154 | const list = convertTreeToList(treeList); 155 | deepEqual(list[0], { 156 | name: 'test0', 157 | fold: true, 158 | arrow: true, 159 | tab: 0, 160 | data: treeList[0], 161 | }); 162 | deepEqual(list[1], { 163 | name: 'test1', 164 | fold: true, 165 | arrow: true, 166 | tab: 1, 167 | data: treeList[0].children[0], 168 | }); 169 | deepEqual(list[2], { 170 | name: 'test2', 171 | fold: true, 172 | arrow: true, 173 | tab: 2, 174 | data: treeList[0].children[0].children[0], 175 | }); 176 | deepEqual(list[3], { 177 | name: 'test3', 178 | fold: true, 179 | arrow: true, 180 | tab: 1, 181 | data: treeList[0].children[1], 182 | }); 183 | deepEqual(list[4], { 184 | name: 'test4', 185 | fold: true, 186 | arrow: true, 187 | tab: 0, 188 | data: treeList[1], 189 | }); 190 | equal(list.length, 5); 191 | }); 192 | }); 193 | }); 194 | -------------------------------------------------------------------------------- /element/tree/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "commonjs", /* Specify what module code is generated. */ 28 | "rootDir": "./source", /* Specify the root folder within your source files. */ 29 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | // "resolveJsonModule": true, /* Enable importing .json files */ 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 50 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 51 | // "removeComments": true, /* Disable emitting comments. */ 52 | // "noEmit": true, /* Disable emitting files from a compilation. */ 53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 68 | 69 | /* Interop Constraints */ 70 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 71 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 72 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 73 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 74 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 75 | 76 | /* Type Checking */ 77 | "strict": true, /* Enable all strict type-checking options. */ 78 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 79 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 80 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 81 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 82 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 83 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 84 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 85 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 86 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 87 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 88 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 89 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 90 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 91 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 92 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 93 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 94 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 95 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 96 | 97 | /* Completeness */ 98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 99 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@itharbors/ui", 3 | "version": "0.1.0", 4 | "description": "", 5 | "main": "./dist/index.js", 6 | "module": "./bundle/index.mjs", 7 | "scripts": { 8 | "build": "node ./scripts/build.js", 9 | "server": "node ./scripts/server.js", 10 | "test": "node ./scripts/test.js", 11 | "ci": "npm install && npm run build && npm run test" 12 | }, 13 | "author": "VisualSJ", 14 | "publishConfig": { 15 | "access": "public" 16 | }, 17 | "license": "ISC", 18 | "devDependencies": { 19 | "@swc/cli": "^0.1.57", 20 | "@swc/core": "^1.3.15", 21 | "@types/mocha": "^10.0.1", 22 | "@types/node": "^20.6.0", 23 | "esbuild": "^0.15.13", 24 | "express": "^4.18.2", 25 | "mocha": "^10.2.0", 26 | "open": "^9.1.0", 27 | "typescript": "^5.2.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { readdirSync } = require('fs'); 4 | const { join } = require('path'); 5 | const { spawn } = require('child_process'); 6 | 7 | const spawnNPMAsync = function(cwd, ...cmd) { 8 | return new Promise((resolve) => { 9 | const child = spawn('npm', [...cmd], { 10 | stdio: [0, 1, 2], 11 | cwd: cwd, 12 | }); 13 | child.on('exit', () => { 14 | resolve(); 15 | }); 16 | }); 17 | }; 18 | 19 | const exec = async function() { 20 | const elementDir = join(__dirname, '../element'); 21 | const names = readdirSync(elementDir); 22 | 23 | for (let name of names) { 24 | const dir = join(elementDir, name); 25 | await spawnNPMAsync(dir, 'install'); 26 | await spawnNPMAsync(dir, 'run', 'build'); 27 | } 28 | 29 | }; 30 | 31 | exec(); 32 | -------------------------------------------------------------------------------- /scripts/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const PORT = 4000; 4 | 5 | const express = require('express'); 6 | const path = require('path'); 7 | const fs = require('fs'); 8 | 9 | const app = express(); 10 | app.use(`/public`, express.static(path.join(__dirname, './server/public'))); 11 | 12 | const HTMLPath = path.join(__dirname, './server/index.html'); 13 | const navList = []; 14 | 15 | // 获取元素列表 16 | const list = fs.readdirSync(path.join(__dirname, '../element')); 17 | list.forEach((name) => { 18 | const example = path.join(__dirname, '../element', name, 'example'); 19 | app.use(`/${name}`, express.static(example)); 20 | const bundle = path.join(__dirname, '../element', name, 'bundle'); 21 | app.use(`/${name}`, express.static(bundle)); 22 | 23 | if (fs.existsSync(example)) { 24 | const htmlList = fs.readdirSync(example).filter(htmlName => htmlName.endsWith('.html')); 25 | 26 | navList.push({ 27 | name: name, 28 | list: htmlList.map((htmlName) => { 29 | return { 30 | name: path.basename(htmlName, '.html'), 31 | href: `./${name}/${htmlName}`, 32 | }; 33 | }), 34 | }); 35 | } 36 | 37 | }); 38 | 39 | // 主页上的参数 40 | app.get('/', (req, res) => { 41 | let html = fs.readFileSync(HTMLPath, 'utf8'); 42 | let HTML = ''; 43 | navList.forEach((item) => { 44 | HTML += `
`; 45 | HTML += `

${item.name}

`; 46 | item.list.forEach((navItem) => { 47 | HTML += `
`; 48 | HTML += `${navItem.name}`; 49 | HTML += `
`; 50 | }); 51 | HTML += `
`; 52 | }); 53 | html = html.replace('{{CONTENT}}', HTML); 54 | res.send(html); 55 | }); 56 | 57 | app.listen(PORT, async () => { 58 | console.log(`Static server listening on port ${PORT}!`); 59 | const open = await import('open'); 60 | open.default('http://localhost:4000'); 61 | }); 62 | -------------------------------------------------------------------------------- /scripts/server/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ITHARBORS UI 7 | 8 | 9 | 10 | 11 |
12 |
13 |

ITHARBORS UI EXAMPLE

14 |
15 | {{CONTENT}} 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /scripts/server/public/base.css: -------------------------------------------------------------------------------- 1 | html, body { height: 100%; margin: 0; } 2 | body { 3 | /* 基础颜色 */ 4 | --ui-color-primary: #1677ff; 5 | 6 | --ui-color-default: #fff; 7 | --ui-color-success: #00b578; 8 | --ui-color-danger: #ff3141; 9 | --ui-color-warn: #ff8f1f; 10 | 11 | /* 基础颜色作为背景色的时候,需要一个对比色 */ 12 | --ui-color-primary-contrast: #fff; 13 | 14 | --ui-color-default-contrast: #333; 15 | --ui-color-success-contrast: #fff; 16 | --ui-color-danger-contrast: #fff; 17 | --ui-color-warn-contrast: #fff; 18 | 19 | /* 基础颜色作为背景色的时候,需要边框的颜色 */ 20 | --ui-color-primary-line: #1677ff; 21 | 22 | --ui-color-default-line: #333; 23 | --ui-color-success-line: #00b578; 24 | --ui-color-danger-line: #ff3141; 25 | --ui-color-warn-line: #ff8f1f; 26 | 27 | --ui-size-line: 24; 28 | --ui-size-font: 12; 29 | --ui2-size-radius: 40; 30 | 31 | --ui-anim-duration: 0.3s; 32 | } 33 | 34 | body { 35 | color: var(--ui-color-default-contrast); 36 | background: var(--ui-color-default); 37 | } 38 | 39 | h1 { 40 | line-height: calc(var(--ui-size-line) * 2px); 41 | } 42 | 43 | h2 { 44 | line-height: calc(var(--ui-size-line) * 1.5px); 45 | } 46 | 47 | a { 48 | text-decoration: none; 49 | color: var(--ui-color-default-contrast); 50 | } 51 | 52 | a:visited { 53 | color: var(--ui-color-default-contrast); 54 | } 55 | -------------------------------------------------------------------------------- /scripts/server/public/index.css: -------------------------------------------------------------------------------- 1 | section { 2 | width: 960px; 3 | margin: auto; 4 | } 5 | a { 6 | color: #ccc; 7 | } 8 | h2 { 9 | margin-bottom: 4px; 10 | } 11 | main > div { 12 | margin-left: 20px; 13 | } -------------------------------------------------------------------------------- /scripts/server/public/item.css: -------------------------------------------------------------------------------- 1 | section[name="content"] { 2 | width: 960px; 3 | margin: 0 auto; 4 | border-radius: calc(var(--ui-size-radius) * 1px); 5 | overflow: hidden; 6 | } 7 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { readdirSync } = require('fs'); 4 | const { join } = require('path'); 5 | const { spawn } = require('child_process'); 6 | 7 | const spawnNPMAsync = function(cwd, ...cmd) { 8 | return new Promise((resolve) => { 9 | const child = spawn('npm', [...cmd], { 10 | stdio: [0, 1, 2], 11 | cwd: cwd, 12 | }); 13 | child.on('exit', (code) => { 14 | resolve(code); 15 | }); 16 | }); 17 | }; 18 | 19 | const spawnNPXAsync = function(cwd, ...cmd) { 20 | return new Promise((resolve) => { 21 | const child = spawn('npx', [...cmd], { 22 | stdio: [0, 1, 2], 23 | cwd: cwd, 24 | }); 25 | child.on('exit', (code) => { 26 | resolve(code); 27 | }); 28 | }); 29 | }; 30 | 31 | const exec = async function() { 32 | const elementDir = join(__dirname, '../element'); 33 | const names = readdirSync(elementDir); 34 | 35 | for (let name of names) { 36 | const dir = join(elementDir, name); 37 | console.log(dir); 38 | const installCode = await spawnNPMAsync(dir, 'install'); 39 | const mochaCode = await spawnNPXAsync(dir, 'mocha'); 40 | if (installCode !== 0 || mochaCode !== 0) { 41 | process.exit(-1); 42 | } 43 | } 44 | 45 | }; 46 | 47 | exec(); 48 | --------------------------------------------------------------------------------