├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── .stylelintrc.json ├── LICENSE ├── README.md ├── auto-imports.d.ts ├── components.d.ts ├── demo.html ├── env.d.ts ├── global.d.ts ├── index.html ├── package.json ├── pnpm-lock.yaml ├── public └── favicon.ico ├── src ├── assets │ └── minder │ │ ├── iconpriority.png │ │ ├── iconprogress.png │ │ ├── icons.png │ │ └── mold.png ├── components │ ├── main │ │ ├── header.vue │ │ ├── mainEditor.vue │ │ └── navigator.vue │ ├── menu │ │ ├── edit │ │ │ ├── editDel.vue │ │ │ ├── editMenu.vue │ │ │ ├── expand.vue │ │ │ ├── insertBox.vue │ │ │ ├── moveBox.vue │ │ │ ├── progressBox.vue │ │ │ ├── selection.vue │ │ │ ├── sequenceBox.vue │ │ │ └── tagBox.vue │ │ └── view │ │ │ ├── arrange.vue │ │ │ ├── fontOperation.vue │ │ │ ├── mold.vue │ │ │ ├── styleOperation.vue │ │ │ └── viewMenu.vue │ └── minderEditor.vue ├── demo.ts ├── demo │ └── index.vue ├── hooks │ └── useI18n.ts ├── locale │ ├── helper.ts │ ├── index.ts │ ├── lang │ │ ├── en-US.ts │ │ └── zh-CN.ts │ └── useLocale.ts ├── main.ts ├── props.ts ├── script │ ├── editor.ts │ ├── expose-editor.ts │ ├── protocol │ │ ├── freemind.ts │ │ ├── json.ts │ │ ├── markdown.ts │ │ ├── plain.ts │ │ ├── png.ts │ │ └── svg.ts │ ├── runtime │ │ ├── clipboard-mimetype.ts │ │ ├── clipboard.ts │ │ ├── container.ts │ │ ├── drag.ts │ │ ├── exports.ts │ │ ├── fsm.ts │ │ ├── history.ts │ │ ├── hotbox.ts │ │ ├── input.ts │ │ ├── jumping.ts │ │ ├── minder.ts │ │ ├── node.ts │ │ ├── priority.ts │ │ ├── progress.ts │ │ ├── receiver.ts │ │ └── tag.ts │ ├── store.ts │ └── tool │ │ ├── debug.ts │ │ ├── format.ts │ │ ├── innertext.ts │ │ ├── key.ts │ │ ├── keymap.ts │ │ ├── useLocaleNotVue.ts │ │ └── utils.ts └── style │ ├── dropdown-list.scss │ ├── editor.scss │ ├── header.scss │ ├── hotbox.scss │ ├── mixin.scss │ ├── navigator.scss │ └── normalize.css ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── types └── locale.d.ts └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | # JavaScript 13 | [*.js] 14 | # 支持一些 JS 新特性 15 | parser = babel-eslint 16 | 17 | # TypeScript 18 | [*.ts] 19 | parser = @typescript-eslint/parser 20 | plugins = @typescript-eslint/eslint-plugin 21 | 22 | # Vue 23 | [*.vue] 24 | parser = vue-eslint-parser 25 | plugins = vue 26 | 27 | # JSON 28 | [*.json] 29 | indent_size = 2 30 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: [ 7 | '@vue/eslint-config-typescript/recommended', 8 | 'airbnb-base', 9 | 'eslint:recommended', 10 | 'plugin:import/recommended', 11 | 'plugin:import/typescript', 12 | 'plugin:vue/vue3-recommended', 13 | 'plugin:vue-scoped-css/vue3-recommended', 14 | 'plugin:prettier/recommended', 15 | ], 16 | parserOptions: { 17 | ecmaVersion: 'latest', 18 | ecmaFeatures: { 19 | jsx: true, 20 | }, 21 | parser: '@typescript-eslint/parser', 22 | sourceType: 'module', 23 | }, 24 | plugins: ['@typescript-eslint', 'vue', 'prettier'], 25 | ignorePatterns: ['.config.cjs', '/src/.d.ts', '/*.json', '/node_modules', '/*.md'], 26 | rules: { 27 | 'prettier/prettier': 'error', 28 | 'consistent-return': 'off', 29 | 'import/prefer-default-export': 'off', 30 | 'import/extensions': 'off', 31 | 'import/no-unresolved': 'off', // 尽力了,真只能关掉:https://github.com/eslint/eslint/discussions/14667 32 | 'import/no-extraneous-dependencies': 'off', 33 | 'import/order': 'error', 34 | 'no-console': 'off', 35 | 'no-shadow': 'off', 36 | 'vue/multi-word-component-names': 'off', 37 | 'vue-scoped-css/no-unused-selector': 'off', 38 | 'vue/component-tags-order': ['error', { order: ['template', 'script', 'style'] }], // template统一在上 39 | 'vue/component-name-in-template-casing': ['error', 'kebab-case', { registeredComponentsOnly: false }], // 模版都用烤串命名 40 | // 强制使用三等号 41 | eqeqeq: 'error', 42 | // 不使用Function,尽量使用函数声明:(a: string) => string 43 | '@typescript-eslint/ban-types': 'warn', 44 | 'no-plusplus': 'off', 45 | 'no-use-before-define': 'off', 46 | 'no-unused-vars': 'warn', 47 | 'vue/html-self-closing': [ 48 | 'error', 49 | { 50 | html: { 51 | void: 'always', 52 | normal: 'always', 53 | component: 'always', 54 | }, 55 | svg: 'always', 56 | math: 'always', 57 | }, 58 | ], // 没包内容的标签都闭合 59 | }, 60 | }; 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | .DS_Store 3 | node_modules 4 | node/ 5 | /dist 6 | 7 | # local env files 8 | .env.local 9 | .env.*.local 10 | .history/ 11 | report.html 12 | 13 | # Log files 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | # Editor directories and files 19 | .idea/* 20 | !.idea/icon.png 21 | **/*.iml 22 | .vscode 23 | *.suo 24 | *.ntvs* 25 | *.njsproj 26 | *.sln 27 | *.sw? 28 | .mvn 29 | !.mvn/maven-wrapper.properties 30 | dist/ 31 | src/main/resources/static 32 | src/main/resources/templates 33 | target 34 | .settings 35 | .project 36 | .classpath 37 | .jython_cache 38 | qywx.json 39 | 40 | 41 | ### VS Code ### 42 | .vscode/ 43 | **/src/test/ 44 | ### flattened 45 | .flattened-pom.xml 46 | 47 | .node/ -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | semi: true, 4 | tabWidth: 2, 5 | useTabs: false, 6 | trailingComma: 'es5', 7 | printWidth: 120, 8 | arrowParens: 'always', 9 | endOfLine: 'auto', 10 | }; 11 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-standard", 4 | "stylelint-config-recommended-vue" 5 | ], 6 | "plugins": [ 7 | "stylelint-scss", 8 | "stylelint-order" 9 | ], 10 | "rules": { 11 | "at-rule-no-unknown": null, 12 | "scss/at-rule-no-unknown": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, ba1q1 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue3-minder-editor based on fex-team/kityminder-core 2 | 3 | > 该项目是 Vue3 版本的脑图,Vue2 版本请查看 [vue-minder-editor-plus](https://github.com/AgAngle/vue-minder-editor-plus) 4 | > 基于 Vue3+vite+TS+arco design 开发,支持国际化功能 5 | 6 | ## install 7 | 8 | ```bash 9 | npm install vue3-minder-editor --save 10 | ``` 11 | 12 | ## Usage 13 | 14 | ```javascript 15 | import mindEditor from 'vue3-minder-editor'; 16 | import { createApp } from 'vue'; 17 | 18 | const app = createApp(App); 19 | app.component(mindEditor.name, mindEditor); 20 | ``` 21 | 22 | ## component 23 | 24 | ```html 25 | 38 | 39 | 112 | ``` 113 | 114 | ## Build Setup 115 | 116 | ```bash 117 | # install npm dependencies 118 | npm install 119 | 120 | # serve with hot reload at localhost:8088 121 | npm run dev 122 | 123 | # build for plugin with minification 124 | npm run build 125 | 126 | # License 127 | BSD-3-Clause License 128 | ``` 129 | 130 | ## 国际化 131 | 132 | ``` 133 | 给组件传入language属性即可,例: 134 | 138 | ``` 139 | 140 | ## Props 141 | 142 | > 以下配置部分为 kityminder-core 扩展的功能,kityminder-core 本身的 minder 对象提供了丰富的功能,使用该组件时可通过 window.minder 对象获取 minder 对象具体的使用方法,可以参考它的文档扩展 [kityminder-core wiki](https://github.com/fex-team/kityminder-core/wiki) 143 | 144 | ### 基础配置 145 | 146 | #### importJson
147 | 148 | type Object
149 | Default: null 150 | 151 | 需要脑图解析的 js 对象,参数详情可参考上文 demo,或者调用 minder.exportJson() 查看具体参数 152 | 153 | #### height
154 | 155 | type: Number
156 | default: 500 157 | 158 | 显示高度,默认 500px 159 | 160 | #### disabled
161 | 162 | type: Boolean
163 | default: null 164 | 165 | 是否禁止编辑 166 | 167 | #### defaultMold 168 | 169 | type: Number
170 | default: 3 171 | 172 | 外观设置中样式的默认值 173 | 174 | ### 启用配置 175 | 176 | #### sequenceEnable 177 | 178 | type: Boolean
179 | default: true 180 | 181 | 是否优先级功能 182 | 183 | #### tagEnable 184 | 185 | type: Boolean
186 | default: true 187 | 188 | 是否启用标签功能 189 | 190 | #### progressEnable 191 | 192 | type: Boolean
193 | default: true 194 | 195 | 是否启用完成进度功能 196 | 197 | #### moveEnable 198 | 199 | type: Boolean
200 | default: true 201 | 202 | 是否启用上移下移功能 203 | 204 | ### 优先级配置 205 | 206 | #### priorityCount
207 | 208 | type Number
209 | Default: 4 210 | 211 | 优先级最大显示数量,最多支持显示 9 个级别 212 | 213 | #### priorityStartWithZero
214 | 215 | type: Boolean
216 | default: true 217 | 218 | 优先级是否从 0 开始 219 | 220 | #### priorityPrefix 221 | 222 | type: String
223 | default: 'P' 224 | 优先级显示的前缀 225 | 226 | #### priorityDisableCheck 227 | 228 | type: Function
229 | default: null 230 | 231 | 优先级设置的回调函数,如果返回 false 则无法设置优先级 232 | 233 | ### 标签配置 234 | 235 | #### tags 236 | 237 | type: Array
238 | default: [] 239 | 240 | 标签选项 241 | 242 | #### distinctTags 243 | 244 | type: Array
245 | default: [] 246 | 247 | 定义排他标签,比如 ['tag1','tag2'] ,则 tag1 不能和 tag2 共存 248 | 249 | #### tagDisableCheck 250 | 251 | type: Function
252 | default: null 253 | 254 | 菜单栏是否允许打标签的回调函数,返回 false 则不允许打标签 255 | 256 | #### tagEditCheck 257 | 258 | type: Function
259 | default: null 260 | 261 | 打标签时的回调函数,返回 false 则打标签不成功 262 | -------------------------------------------------------------------------------- /auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-auto-import 5 | export {} 6 | declare global { 7 | const EffectScope: typeof import('vue')['EffectScope'] 8 | const computed: typeof import('vue')['computed'] 9 | const createApp: typeof import('vue')['createApp'] 10 | const customRef: typeof import('vue')['customRef'] 11 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 12 | const defineComponent: typeof import('vue')['defineComponent'] 13 | const effectScope: typeof import('vue')['effectScope'] 14 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 15 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 16 | const h: typeof import('vue')['h'] 17 | const inject: typeof import('vue')['inject'] 18 | const isProxy: typeof import('vue')['isProxy'] 19 | const isReactive: typeof import('vue')['isReactive'] 20 | const isReadonly: typeof import('vue')['isReadonly'] 21 | const isRef: typeof import('vue')['isRef'] 22 | const markRaw: typeof import('vue')['markRaw'] 23 | const nextTick: typeof import('vue')['nextTick'] 24 | const onActivated: typeof import('vue')['onActivated'] 25 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 26 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 27 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 28 | const onDeactivated: typeof import('vue')['onDeactivated'] 29 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 30 | const onMounted: typeof import('vue')['onMounted'] 31 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 32 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 33 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 34 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 35 | const onUnmounted: typeof import('vue')['onUnmounted'] 36 | const onUpdated: typeof import('vue')['onUpdated'] 37 | const provide: typeof import('vue')['provide'] 38 | const reactive: typeof import('vue')['reactive'] 39 | const readonly: typeof import('vue')['readonly'] 40 | const ref: typeof import('vue')['ref'] 41 | const resolveComponent: typeof import('vue')['resolveComponent'] 42 | const shallowReactive: typeof import('vue')['shallowReactive'] 43 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 44 | const shallowRef: typeof import('vue')['shallowRef'] 45 | const toRaw: typeof import('vue')['toRaw'] 46 | const toRef: typeof import('vue')['toRef'] 47 | const toRefs: typeof import('vue')['toRefs'] 48 | const triggerRef: typeof import('vue')['triggerRef'] 49 | const unref: typeof import('vue')['unref'] 50 | const useAttrs: typeof import('vue')['useAttrs'] 51 | const useCssModule: typeof import('vue')['useCssModule'] 52 | const useCssVars: typeof import('vue')['useCssVars'] 53 | const useSlots: typeof import('vue')['useSlots'] 54 | const watch: typeof import('vue')['watch'] 55 | const watchEffect: typeof import('vue')['watchEffect'] 56 | const watchPostEffect: typeof import('vue')['watchPostEffect'] 57 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 58 | } 59 | // for type re-export 60 | declare global { 61 | // @ts-ignore 62 | export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode } from 'vue' 63 | } 64 | -------------------------------------------------------------------------------- /components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-vue-components 5 | // Read more: https://github.com/vuejs/core/pull/3399 6 | import '@vue/runtime-core' 7 | 8 | export {} 9 | 10 | declare module '@vue/runtime-core' { 11 | export interface GlobalComponents { 12 | Arrange: typeof import('./src/components/menu/view/arrange.vue')['default'] 13 | EditDel: typeof import('./src/components/menu/edit/editDel.vue')['default'] 14 | EditMenu: typeof import('./src/components/menu/edit/editMenu.vue')['default'] 15 | Expand: typeof import('./src/components/menu/edit/expand.vue')['default'] 16 | FontOperation: typeof import('./src/components/menu/view/fontOperation.vue')['default'] 17 | Header: typeof import('./src/components/main/header.vue')['default'] 18 | InsertBox: typeof import('./src/components/menu/edit/insertBox.vue')['default'] 19 | MainEditor: typeof import('./src/components/main/mainEditor.vue')['default'] 20 | MinderEditor: typeof import('./src/components/minderEditor.vue')['default'] 21 | Mold: typeof import('./src/components/menu/view/mold.vue')['default'] 22 | MoveBox: typeof import('./src/components/menu/edit/moveBox.vue')['default'] 23 | Navigator: typeof import('./src/components/main/navigator.vue')['default'] 24 | ProgressBox: typeof import('./src/components/menu/edit/progressBox.vue')['default'] 25 | RouterLink: typeof import('vue-router')['RouterLink'] 26 | RouterView: typeof import('vue-router')['RouterView'] 27 | Selection: typeof import('./src/components/menu/edit/selection.vue')['default'] 28 | SequenceBox: typeof import('./src/components/menu/edit/sequenceBox.vue')['default'] 29 | StyleOperation: typeof import('./src/components/menu/view/styleOperation.vue')['default'] 30 | TagBox: typeof import('./src/components/menu/edit/tagBox.vue')['default'] 31 | ViewMenu: typeof import('./src/components/menu/view/viewMenu.vue')['default'] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // interface ImportMetaEnv { 4 | // readonly VITE_APP_TITLE: string 5 | // // 更多环境变量... 6 | // } 7 | 8 | // interface ImportMeta { 9 | // readonly env: ImportMetaEnv 10 | // } 11 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | declare interface Window { 2 | kity: any; 3 | angular: any; 4 | HotBox: any; 5 | kityminder: Record; 6 | minderProps: Record; 7 | editor: Record; 8 | minder: Record; 9 | minderEditor: Record; 10 | km: Record; 11 | t: any; 12 | } 13 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3-minder-editor", 3 | "version": "0.3.0", 4 | "module": "./dist/vue3-minder-editor.es.js", 5 | "keywords": [ 6 | "minder", 7 | "vue3", 8 | "editor" 9 | ], 10 | "peerDependencies": { 11 | "vue": "^3.2.47", 12 | "@arco-design/web-vue": "^2.46.0" 13 | }, 14 | "scripts": { 15 | "dev": "vite", 16 | "build": "run-p type-check build-only", 17 | "build:report": "NODE_ENV=analyze run-p type-check build-only", 18 | "preview": "vite preview", 19 | "build-only": "vite build", 20 | "type-check": "vue-tsc --noEmit", 21 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore && node lint:styles", 22 | "lint:styles": "stylelint 'src/**/*.{vue,html,css,scss}' --fix", 23 | "format": "prettier --write src/", 24 | "prepublishOnly": "npm run build" 25 | }, 26 | "dependencies": { 27 | "@7polo/kity": "2.0.8", 28 | "@7polo/kityminder-core": "1.4.53", 29 | "@arco-design/web-vue": "^2.46.0", 30 | "@element-plus/icons-vue": "^2.1.0", 31 | "coolie.js": "2.4.1", 32 | "deepmerge": "^4.3.1", 33 | "element-plus": "^2.3.4", 34 | "hotbox-minder": "1.0.15", 35 | "lodash-unified": "^1.0.3", 36 | "pinia": "^2.0.36", 37 | "vue": "^3.2.47", 38 | "vue-i18n": "^9.2.2", 39 | "vue-router": "^4.1.6" 40 | }, 41 | "devDependencies": { 42 | "@arco-plugins/vite-vue": "^1.4.5", 43 | "@iconify-json/ep": "^1.1.10", 44 | "@rollup/plugin-replace": "^5.0.2", 45 | "@rushstack/eslint-patch": "^1.2.0", 46 | "@tsconfig/node18": "^2.0.0", 47 | "@types/deepmerge": "^2.2.0", 48 | "@types/node": "^18.16.3", 49 | "@typescript-eslint/eslint-plugin": "^5.59.2", 50 | "@typescript-eslint/parser": "^5.59.2", 51 | "@vitejs/plugin-legacy": "^4.0.3", 52 | "@vitejs/plugin-vue": "^4.2.1", 53 | "@vitejs/plugin-vue-jsx": "^3.0.1", 54 | "@vue/eslint-config-prettier": "^7.1.0", 55 | "@vue/eslint-config-typescript": "^11.0.3", 56 | "@vue/tsconfig": "^0.3.2", 57 | "@vueuse/core": "^10.1.2", 58 | "eslint": "^8.39.0", 59 | "eslint-config-airbnb-base": "^15.0.0", 60 | "eslint-config-prettier": "^8.8.0", 61 | "eslint-plugin-import": "^2.27.5", 62 | "eslint-plugin-prettier": "^4.2.1", 63 | "eslint-plugin-vue": "^9.11.0", 64 | "eslint-plugin-vue-scoped-css": "^2.4.0", 65 | "less": "^4.1.3", 66 | "npm-run-all": "^4.1.5", 67 | "prettier": "^2.8.8", 68 | "rollup-plugin-visualizer": "^5.9.0", 69 | "sass": "^1.62.1", 70 | "stylelint": "^15.6.1", 71 | "stylelint-config-recommended-vue": "^1.4.0", 72 | "stylelint-config-standard": "^33.0.0", 73 | "stylelint-order": "^6.0.3", 74 | "stylelint-scss": "^5.0.0", 75 | "typescript": "~5.0.4", 76 | "unplugin-auto-import": "^0.15.3", 77 | "unplugin-icons": "^0.14.1", 78 | "unplugin-vue-components": "^0.24.1", 79 | "vite": "^4.3.4", 80 | "vue-tsc": "^1.6.4" 81 | }, 82 | "repository": { 83 | "type": "git", 84 | "url": "https://github.com/ba1q1/vue3-minder-editor.git" 85 | }, 86 | "bugs": { 87 | "url": "https://github.com/ba1q1/vue3-minder-editor/issues", 88 | "email": "443543832@qq.com" 89 | }, 90 | "homepage": "https://github.com/ba1q1/vue3-minder-editor", 91 | "license": "BSD", 92 | "eslintConfig": { 93 | "extends": "./.eslintrc.js" 94 | }, 95 | "files": [ 96 | "dist", 97 | "src", 98 | "types", 99 | "README.md" 100 | ] 101 | } 102 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ba1q1/vue3-minder-editor/4c4aa05b2549d02a079cf85d7deb27b846373f2c/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/minder/iconpriority.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ba1q1/vue3-minder-editor/4c4aa05b2549d02a079cf85d7deb27b846373f2c/src/assets/minder/iconpriority.png -------------------------------------------------------------------------------- /src/assets/minder/iconprogress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ba1q1/vue3-minder-editor/4c4aa05b2549d02a079cf85d7deb27b846373f2c/src/assets/minder/iconprogress.png -------------------------------------------------------------------------------- /src/assets/minder/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ba1q1/vue3-minder-editor/4c4aa05b2549d02a079cf85d7deb27b846373f2c/src/assets/minder/icons.png -------------------------------------------------------------------------------- /src/assets/minder/mold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ba1q1/vue3-minder-editor/4c4aa05b2549d02a079cf85d7deb27b846373f2c/src/assets/minder/mold.png -------------------------------------------------------------------------------- /src/components/main/header.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 53 | 62 | 102 | -------------------------------------------------------------------------------- /src/components/main/mainEditor.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 130 | 131 | 143 | -------------------------------------------------------------------------------- /src/components/main/navigator.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 311 | 316 | -------------------------------------------------------------------------------- /src/components/menu/edit/editDel.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 84 | -------------------------------------------------------------------------------- /src/components/menu/edit/editMenu.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 39 | -------------------------------------------------------------------------------- /src/components/menu/edit/expand.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 34 | 39 | -------------------------------------------------------------------------------- /src/components/menu/edit/insertBox.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 66 | -------------------------------------------------------------------------------- /src/components/menu/edit/moveBox.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 61 | -------------------------------------------------------------------------------- /src/components/menu/edit/progressBox.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 90 | 95 | -------------------------------------------------------------------------------- /src/components/menu/edit/selection.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 119 | 124 | -------------------------------------------------------------------------------- /src/components/menu/edit/sequenceBox.vue: -------------------------------------------------------------------------------- 1 |  22 | 23 | 83 | 84 | 170 | -------------------------------------------------------------------------------- /src/components/menu/edit/tagBox.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 81 | 82 | 103 | -------------------------------------------------------------------------------- /src/components/menu/view/arrange.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 34 | -------------------------------------------------------------------------------- /src/components/menu/view/fontOperation.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 258 | 259 | 268 | -------------------------------------------------------------------------------- /src/components/menu/view/mold.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 56 | 57 | 67 | 68 | 103 | -------------------------------------------------------------------------------- /src/components/menu/view/styleOperation.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 71 | 78 | -------------------------------------------------------------------------------- /src/components/menu/view/viewMenu.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 27 | 28 | 38 | -------------------------------------------------------------------------------- /src/components/minderEditor.vue: -------------------------------------------------------------------------------- 1 | 40 | 88 | -------------------------------------------------------------------------------- /src/demo.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import { createRouter, createWebHashHistory } from 'vue-router'; 3 | import { setupI18n } from '@/locale'; 4 | import App from './demo/index.vue'; 5 | import mindEditor from './components/minderEditor.vue'; 6 | 7 | const routes = [{ path: '/', name: 'demo', component: App }]; 8 | 9 | // 3. 创建路由实例并传递 `routes` 配置 10 | // 你可以在这里输入更多的配置,但我们在这里 11 | // 暂时保持简单 12 | const router = createRouter({ 13 | // 4. 内部提供了 history 模式的实现。为了简单起见,我们在这里使用 hash 模式。 14 | history: createWebHashHistory(), 15 | routes, // `routes: routes` 的缩写 16 | }); 17 | 18 | const app = createApp(App); 19 | async function bootstrap() { 20 | await setupI18n(app); 21 | app.component(mindEditor.name, mindEditor); 22 | app.use(router).mount('#app'); 23 | } 24 | 25 | bootstrap(); 26 | -------------------------------------------------------------------------------- /src/demo/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 94 | -------------------------------------------------------------------------------- /src/hooks/useI18n.ts: -------------------------------------------------------------------------------- 1 | import { i18n } from '@/locale'; 2 | 3 | type I18nGlobalTranslation = { 4 | (key: string): string; 5 | (key: string, locale: string): string; 6 | (key: string, locale: string, list: unknown[]): string; 7 | (key: string, locale: string, named: Record): string; 8 | (key: string, list: unknown[]): string; 9 | (key: string, named: Record): string; 10 | }; 11 | 12 | type I18nTranslationRestParameters = [string, any]; 13 | 14 | function getKey(namespace: string | undefined, key: string) { 15 | if (!namespace) { 16 | return key; 17 | } 18 | if (key.startsWith(namespace)) { 19 | return key; 20 | } 21 | return `${namespace}.${key}`; 22 | } 23 | 24 | export function useI18n(namespace?: string): { 25 | t: I18nGlobalTranslation; 26 | } { 27 | const normalFn = { 28 | t: (key: string) => { 29 | return getKey(namespace, key); 30 | }, 31 | }; 32 | 33 | if (!i18n) { 34 | return normalFn; 35 | } 36 | 37 | const { t, ...methods } = i18n.global; 38 | 39 | const tFn: I18nGlobalTranslation = (key: string, ...arg: any[]) => { 40 | if (!key) return ''; 41 | if (!key.includes('.') && !namespace) return key; 42 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 43 | // @ts-ignore 44 | return t(getKey(namespace, key), ...(arg as I18nTranslationRestParameters)); 45 | }; 46 | window.t = tFn; 47 | return { 48 | ...methods, 49 | t: tFn, 50 | }; 51 | } 52 | 53 | // Why write this function? 54 | // Mainly to configure the vscode i18nn ally plugin. This function is only used for routing and menus. Please use useI18n for other places 55 | 56 | // 为什么要编写此函数? 57 | // 主要用于配合vscode i18nn ally插件。此功能仅用于路由和菜单。请在其他地方使用useI18n 58 | export const t = (key: string) => key; 59 | -------------------------------------------------------------------------------- /src/locale/helper.ts: -------------------------------------------------------------------------------- 1 | import type { LocaleType } from '#/locale'; 2 | 3 | export function setHtmlPageLang(locale: LocaleType) { 4 | document.querySelector('html')?.setAttribute('lang', locale); 5 | } 6 | 7 | export const loadLocalePool: LocaleType[] = []; 8 | 9 | export function setLoadLocalePool(cb: (lp: LocaleType[]) => void) { 10 | cb(loadLocalePool); 11 | } 12 | -------------------------------------------------------------------------------- /src/locale/index.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n'; 2 | 3 | import type { App } from 'vue'; 4 | import type { I18nOptions } from 'vue-i18n'; 5 | import { setHtmlPageLang, setLoadLocalePool } from './helper'; 6 | import zhCN from './lang/zh-CN'; 7 | 8 | // eslint-disable-next-line import/no-mutable-exports 9 | export let i18n: ReturnType; 10 | 11 | async function createI18nOptions(): Promise { 12 | const locale = 'zh-CN'; 13 | const defaultLocal = zhCN; 14 | const message = defaultLocal.message ?? {}; 15 | 16 | setHtmlPageLang(locale); 17 | setLoadLocalePool((loadLocalePool) => { 18 | loadLocalePool.push(locale); 19 | }); 20 | 21 | return { 22 | locale, 23 | fallbackLocale: 'zh-CN', 24 | legacy: false, 25 | allowComposition: true, 26 | messages: { 27 | [locale]: message, 28 | }, 29 | sync: true, // If you don’t want to inherit locale from global scope, you need to set sync of i18n component option to false. 30 | silentTranslationWarn: true, // true - warning off 31 | missingWarn: false, 32 | silentFallbackWarn: true, 33 | }; 34 | } 35 | 36 | // 创建国际化实例 37 | export async function setupI18n(app: App) { 38 | const options = await createI18nOptions(); 39 | 40 | i18n = createI18n(options); 41 | app.use(i18n); 42 | } 43 | -------------------------------------------------------------------------------- /src/locale/lang/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'en-US', 3 | message: { 4 | minder: { 5 | commons: { 6 | confirm: 'Confirm', 7 | clear: 'Clear', 8 | export: 'Export', 9 | cancel: 'Cancel', 10 | edit: 'Edit', 11 | delete: 'Delete', 12 | remove: 'Remove', 13 | return: 'Return', 14 | }, 15 | menu: { 16 | expand: { 17 | expand: 'Expand', 18 | folding: 'Folding', 19 | expand_one: 'Expand one level', 20 | expand_tow: 'Expand tow level', 21 | expand_three: 'Expand three level', 22 | expand_four: 'Expand four level', 23 | expand_five: 'Expand five level', 24 | expand_six: 'Expand six level', 25 | }, 26 | insert: { 27 | down: 'Subordinate', 28 | up: 'Superior', 29 | same: 'Same', 30 | _same: 'Same level', 31 | _down: 'Subordinate level', 32 | _up: 'Superior level', 33 | }, 34 | move: { 35 | up: 'Up', 36 | down: 'Down', 37 | forward: 'Forward', 38 | backward: 'Backward', 39 | }, 40 | progress: { 41 | progress: 'Progress', 42 | remove_progress: 'Remove progress', 43 | prepare: 'Prepare', 44 | complete_all: 'Complete all', 45 | complete: 'Complete', 46 | }, 47 | selection: { 48 | all: 'Select all', 49 | invert: 'Select invert', 50 | sibling: 'Select sibling node', 51 | same: 'Select same node', 52 | path: 'Select path', 53 | subtree: 'Select subtree', 54 | }, 55 | arrange: { 56 | arrange_layout: 'Arrange layout', 57 | }, 58 | font: { 59 | font: 'Font', 60 | size: 'Font size', 61 | }, 62 | style: { 63 | clear: 'Clear style', 64 | copy: 'Copy style', 65 | paste: 'Paste style', 66 | }, 67 | }, 68 | main: { 69 | header: { 70 | minder: 'Minder', 71 | style: 'Appearance style', 72 | }, 73 | main: { 74 | save: 'Save', 75 | }, 76 | navigator: { 77 | amplification: 'Amplification', 78 | narrow: 'Narrow', 79 | drag: 'Drag', 80 | locating_root: 'Locating root node', 81 | navigator: 'Navigator', 82 | }, 83 | history: { 84 | undo: 'Undo', 85 | redo: 'Redo', 86 | }, 87 | subject: { 88 | central: 'Central subject', 89 | branch: 'Subject', 90 | }, 91 | priority: 'Priority', 92 | tag: 'Tag', 93 | }, 94 | }, 95 | }, 96 | }; 97 | -------------------------------------------------------------------------------- /src/locale/lang/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'zh-CN', 3 | message: { 4 | minder: { 5 | commons: { 6 | confirm: '确定', 7 | clear: '清空', 8 | export: '导出', 9 | cancel: '取消', 10 | edit: '编辑', 11 | delete: '删除', 12 | remove: '移除', 13 | return: '返回', 14 | }, 15 | menu: { 16 | expand: { 17 | expand: '展开', 18 | folding: '收起', 19 | expand_one: '展开到一级节点', 20 | expand_tow: '展开到二级节点', 21 | expand_three: '展开到三级节点', 22 | expand_four: '展开到四级节点', 23 | expand_five: '展开到五级节点', 24 | expand_six: '展开到六级节点', 25 | }, 26 | insert: { 27 | down: '插入下级主题', 28 | up: '插入上级主题', 29 | same: '插入同级主题', 30 | _same: '同级', 31 | _down: '下级', 32 | _up: '上级', 33 | }, 34 | move: { 35 | up: '上移', 36 | down: '下移', 37 | forward: '前移', 38 | backward: '后移', 39 | }, 40 | progress: { 41 | progress: '进度', 42 | remove_progress: '移除进度', 43 | prepare: '未开始', 44 | complete_all: '全部完成', 45 | complete: '完成', 46 | }, 47 | selection: { 48 | all: '全选', 49 | invert: '反选', 50 | sibling: '选择兄弟节点', 51 | same: '选择同级节点', 52 | path: '选择路径', 53 | subtree: '选择子树', 54 | }, 55 | arrange: { 56 | arrange_layout: '整理布局', 57 | }, 58 | font: { 59 | font: '字体', 60 | size: '字号', 61 | }, 62 | style: { 63 | clear: '清除样式', 64 | copy: '复制样式', 65 | paste: '粘贴样式', 66 | }, 67 | }, 68 | main: { 69 | header: { 70 | minder: '思维导图', 71 | style: '外观样式', 72 | }, 73 | main: { 74 | save: '保存', 75 | }, 76 | navigator: { 77 | amplification: '放大', 78 | narrow: '缩小', 79 | drag: '拖拽', 80 | locating_root: '定位根节点', 81 | navigator: '导航器', 82 | }, 83 | history: { 84 | undo: '撤销', 85 | redo: '重做', 86 | }, 87 | subject: { 88 | central: '中心主题', 89 | branch: '分支主题', 90 | }, 91 | priority: '优先级', 92 | tag: '标签', 93 | }, 94 | }, 95 | }, 96 | }; 97 | -------------------------------------------------------------------------------- /src/locale/useLocale.ts: -------------------------------------------------------------------------------- 1 | import { computed, unref } from 'vue'; 2 | import { i18n } from '@/locale'; 3 | import { setHtmlPageLang, loadLocalePool } from '@/locale/helper'; 4 | 5 | import type { Recordable, LocaleType } from '#/locale'; 6 | 7 | interface LangModule { 8 | message: Recordable; 9 | } 10 | 11 | function setI18nLanguage(locale: LocaleType) { 12 | if (i18n.mode === 'legacy') { 13 | i18n.global.locale = locale; 14 | } else { 15 | (i18n.global.locale as any).value = locale; 16 | } 17 | localStorage.setItem('minder-locale', locale); 18 | setHtmlPageLang(locale); 19 | } 20 | 21 | async function changeLocale(locale: LocaleType) { 22 | const globalI18n = i18n.global; 23 | const currentLocale = unref(globalI18n.locale); 24 | if (currentLocale === locale) { 25 | return locale; 26 | } 27 | 28 | if (loadLocalePool.includes(locale)) { 29 | setI18nLanguage(locale); 30 | return locale; 31 | } 32 | const langModule = ((await import(`./lang/${locale}.ts`)) as any).default as LangModule; 33 | 34 | if (!langModule) return; 35 | 36 | const { message } = langModule; 37 | 38 | globalI18n.setLocaleMessage(locale, message); 39 | loadLocalePool.push(locale); 40 | 41 | setI18nLanguage(locale); 42 | return locale; 43 | } 44 | 45 | export default function useLocale() { 46 | const { locale } = i18n.global; 47 | const currentLocale = computed(() => { 48 | return locale; 49 | }); 50 | 51 | return { 52 | currentLocale, 53 | changeLocale, 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import mindEditor from './components/minderEditor.vue'; 2 | import { setupI18n } from '@/locale'; 3 | import '@7polo/kity/dist/kity.js'; 4 | import 'hotbox-minder/hotbox.js'; 5 | import '@7polo/kityminder-core'; 6 | import './script/expose-editor'; 7 | 8 | export default { 9 | install: async (app: any) => { 10 | await setupI18n(app); 11 | app.component(mindEditor.name, mindEditor); 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/props.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Api 列表 3 | */ 4 | 5 | export const mainEditorProps = { 6 | importJson: { 7 | type: Object, 8 | default() { 9 | return { 10 | root: { 11 | data: { 12 | text: 'test111', 13 | }, 14 | children: [ 15 | { 16 | data: { 17 | text: '地图', 18 | }, 19 | }, 20 | { 21 | data: { 22 | text: '百科', 23 | expandState: 'collapse', 24 | }, 25 | }, 26 | ], 27 | }, 28 | template: 'default', 29 | }; 30 | }, 31 | }, 32 | height: { 33 | type: Number, 34 | default: 500, 35 | }, 36 | disabled: Boolean, 37 | }; 38 | 39 | export const priorityProps = { 40 | priorityCount: { 41 | type: Number, 42 | default: 4, 43 | validator: (value: number) => { 44 | // 优先级最多支持 9 个级别 45 | return value <= 9; 46 | }, 47 | }, 48 | priorityStartWithZero: { 49 | // 优先级是否从0开始 50 | type: Boolean, 51 | default: true, 52 | }, 53 | priorityPrefix: { 54 | // 优先级显示的前缀 55 | type: String, 56 | default: 'P', 57 | }, 58 | priorityDisableCheck: Function, 59 | operators: [], 60 | }; 61 | 62 | export const tagProps = { 63 | tags: { 64 | // 自定义标签 65 | type: Array, 66 | default() { 67 | return [] as string[]; 68 | }, 69 | }, 70 | distinctTags: { 71 | // 个别标签二选一 72 | type: Array, 73 | default() { 74 | return [] as string[]; 75 | }, 76 | }, 77 | tagDisableCheck: Function, 78 | tagEditCheck: Function, 79 | }; 80 | 81 | export const editMenuProps = { 82 | sequenceEnable: { 83 | type: Boolean, 84 | default: true, 85 | }, 86 | tagEnable: { 87 | type: Boolean, 88 | default: true, 89 | }, 90 | progressEnable: { 91 | type: Boolean, 92 | default: true, 93 | }, 94 | moveEnable: { 95 | type: Boolean, 96 | default: true, 97 | }, 98 | }; 99 | 100 | export const moleProps = { 101 | // 默认样式 102 | defaultMold: { 103 | type: Number, 104 | default: 3, 105 | }, 106 | }; 107 | 108 | export const delProps = { 109 | delConfirm: { 110 | type: Function, 111 | default: null, 112 | }, 113 | }; 114 | -------------------------------------------------------------------------------- /src/script/editor.ts: -------------------------------------------------------------------------------- 1 | import container from './runtime/container'; 2 | import fsm from './runtime/fsm'; 3 | import minder from './runtime/minder'; 4 | import receiver from './runtime/receiver'; 5 | import hotbox from './runtime/hotbox'; 6 | import input from './runtime/input'; 7 | import clipboardMimetype from './runtime/clipboard-mimetype'; 8 | import clipboard from './runtime/clipboard'; 9 | import drag from './runtime/drag'; 10 | import node from './runtime/node'; 11 | import history from './runtime/history'; 12 | import jumping from './runtime/jumping'; 13 | import priority from './runtime/priority'; 14 | import progress from './runtime/progress'; 15 | import exportsRuntime from './runtime/exports'; 16 | import tag from './runtime/tag'; 17 | 18 | type EditMenuProps = { 19 | sequenceEnable: boolean; 20 | tagEnable: boolean; 21 | progressEnable: boolean; 22 | moveEnable: boolean; 23 | }; 24 | 25 | type Runtime = { 26 | name: string; 27 | call: (thisArg: KMEditor, editor: KMEditor) => void; 28 | }; 29 | 30 | const runtimes: Runtime[] = []; 31 | 32 | function assemble(runtime: Runtime) { 33 | runtimes.push(runtime); 34 | } 35 | 36 | class KMEditor { 37 | public selector: HTMLDivElement | null | string; 38 | 39 | public editMenuProps: EditMenuProps; 40 | 41 | constructor(selector: HTMLDivElement | null | string, editMenuPropsC: EditMenuProps) { 42 | this.selector = selector; 43 | this.editMenuProps = editMenuPropsC; 44 | this.init(); 45 | } 46 | 47 | public init() { 48 | for (let i = 0; i < runtimes.length; i++) { 49 | if (typeof runtimes[i].call === 'function' && isEnable(this.editMenuProps, runtimes[i])) { 50 | runtimes[i].call(this, this); 51 | } 52 | } 53 | } 54 | } 55 | 56 | function isEnable(editMenuProps: EditMenuProps, runtime: Runtime) { 57 | switch (runtime.name) { 58 | case 'PriorityRuntime': 59 | return editMenuProps.sequenceEnable === true; 60 | case 'TagRuntime': 61 | return editMenuProps.tagEnable === true; 62 | case 'ProgressRuntime': 63 | return editMenuProps.progressEnable === true; 64 | default: 65 | return true; 66 | } 67 | } 68 | 69 | assemble(container); 70 | assemble(fsm); 71 | assemble(minder); 72 | assemble(receiver); 73 | assemble(hotbox); 74 | assemble(input); 75 | assemble(clipboardMimetype); 76 | assemble(clipboard); 77 | assemble(drag); 78 | assemble(node); 79 | assemble(history); 80 | assemble(jumping); 81 | assemble(priority); 82 | assemble(progress); 83 | assemble(exportsRuntime); 84 | assemble(tag); 85 | 86 | export default KMEditor; 87 | -------------------------------------------------------------------------------- /src/script/expose-editor.ts: -------------------------------------------------------------------------------- 1 | import Editor from './editor'; 2 | 3 | export default window.kityminder.Editor = Editor; 4 | -------------------------------------------------------------------------------- /src/script/protocol/freemind.ts: -------------------------------------------------------------------------------- 1 | const priorities = [ 2 | { jp: 1, mp: 'full-1' }, 3 | { jp: 2, mp: 'full-2' }, 4 | { jp: 3, mp: 'full-3' }, 5 | { jp: 4, mp: 'full-4' }, 6 | { jp: 5, mp: 'full-5' }, 7 | { jp: 6, mp: 'full-6' }, 8 | { jp: 7, mp: 'full-7' }, 9 | { jp: 8, mp: 'full-8' }, 10 | ]; 11 | const mmVersion = '\n'; 12 | const iconTextPrefix = '\n'; 14 | const nodeCreated = '\n'; 18 | const entityNode = '\n'; 19 | const entityMap = ''; 20 | 21 | function exportFreeMind(minder: any) { 22 | const minds = minder.exportJson(); 23 | const mmContent = mmVersion + traverseJson(minds.root) + entityNode + entityMap; 24 | try { 25 | const link = document.createElement('a'); 26 | const blob = new Blob([`\ufeff${mmContent}`], { 27 | type: 'text/xml', 28 | }); 29 | link.href = window.URL.createObjectURL(blob); 30 | link.download = `${minds.root.data.text}.mm`; 31 | document.body.appendChild(link); 32 | link.click(); 33 | document.body.removeChild(link); 34 | } catch (err) { 35 | alert(err); 36 | } 37 | } 38 | 39 | function traverseJson(node: any) { 40 | let result = ''; 41 | if (!node) { 42 | return; 43 | } 44 | result += concatNodes(node); 45 | if (node.children && node.children.length > 0) { 46 | // eslint-disable-next-line no-restricted-syntax 47 | for (const element of node.children) { 48 | result += traverseJson(element); 49 | result += entityNode; 50 | } 51 | } 52 | return result; 53 | } 54 | 55 | function concatNodes(node: any) { 56 | let result = ''; 57 | const datas = node.data; 58 | result += nodeCreated + datas.created + nodeId + datas.id + nodeText + datas.text + nodeSuffix; 59 | if (datas.priority) { 60 | const mapped = priorities.find((d) => { 61 | return d.jp === datas.priority; 62 | }); 63 | if (mapped) { 64 | result += iconTextPrefix + mapped.mp + iconTextSuffix; 65 | } 66 | } 67 | return result; 68 | } 69 | 70 | export default { exportFreeMind }; 71 | -------------------------------------------------------------------------------- /src/script/protocol/json.ts: -------------------------------------------------------------------------------- 1 | function exportJson(minder: any) { 2 | const minds = minder.exportJson(); 3 | try { 4 | const link = document.createElement('a'); 5 | const blob = new Blob([`\ufeff${JSON.stringify(minds)}`], { 6 | type: 'text/json', 7 | }); 8 | link.href = window.URL.createObjectURL(blob); 9 | link.download = `${minds.root.data.text}.json`; 10 | document.body.appendChild(link); 11 | link.click(); 12 | document.body.removeChild(link); 13 | } catch (err) { 14 | alert(err); 15 | } 16 | } 17 | 18 | export default { exportJson }; 19 | -------------------------------------------------------------------------------- /src/script/protocol/markdown.ts: -------------------------------------------------------------------------------- 1 | const LINE_ENDING_SPLITER = /\r\n|\r|\n/; 2 | const EMPTY_LINE = ''; 3 | const NOTE_MARK_START = ''; 4 | const NOTE_MARK_CLOSE = ''; 5 | 6 | function exportMarkdown(minder: any) { 7 | const minds = minder.exportJson(); 8 | try { 9 | const link = document.createElement('a'); 10 | const blob = new Blob([`\ufeff${encode(minds.root)}`], { 11 | type: 'markdown', 12 | }); 13 | link.href = window.URL.createObjectURL(blob); 14 | link.download = `${minds.root.data.text}.md`; 15 | document.body.appendChild(link); 16 | link.click(); 17 | document.body.removeChild(link); 18 | } catch (err) { 19 | alert(err); 20 | } 21 | } 22 | 23 | function encode(json: any) { 24 | return _build(json, 1).join('\n'); 25 | } 26 | 27 | function _build(node: any, level: number) { 28 | let lines: string[] = []; 29 | 30 | level = level || 1; 31 | 32 | const sharps = _generateHeaderSharp(level); 33 | lines.push(`${sharps} ${node.data.text}`); 34 | lines.push(EMPTY_LINE); 35 | 36 | let { note } = node.data; 37 | if (note) { 38 | const hasSharp = /^#/.test(note); 39 | if (hasSharp) { 40 | lines.push(NOTE_MARK_START); 41 | note = note.replace(/^#+/gm, function ($0: string) { 42 | return sharps + $0; 43 | }); 44 | } 45 | lines.push(note); 46 | if (hasSharp) { 47 | lines.push(NOTE_MARK_CLOSE); 48 | } 49 | lines.push(EMPTY_LINE); 50 | } 51 | 52 | if (node.children) 53 | node.children.forEach(function (child: any) { 54 | lines = lines.concat(_build(child, level + 1)); 55 | }); 56 | 57 | return lines; 58 | } 59 | 60 | function _generateHeaderSharp(level: number) { 61 | let sharps = ''; 62 | while (level--) sharps += '#'; 63 | return sharps; 64 | } 65 | 66 | function decode(markdown: string) { 67 | const parentMap = []; 68 | let lines; 69 | let line; 70 | let lineInfo; 71 | let level = 0; 72 | let node; 73 | let noteProgress; 74 | let codeBlock; 75 | 76 | // 一级标题转换 `{title}\n===` => `# {title}` 77 | markdown = markdown.replace(/^(.+)\n={3,}/, function ($0, $1) { 78 | return `# ${$1}`; 79 | }); 80 | 81 | lines = markdown.split(LINE_ENDING_SPLITER); 82 | 83 | // 按行分析 84 | for (const element of lines) { 85 | line = element; 86 | 87 | lineInfo = _resolveLine(line); 88 | 89 | // 备注标记处理 90 | if (lineInfo.noteClose) { 91 | noteProgress = false; 92 | continue; 93 | } else if (lineInfo.noteStart) { 94 | noteProgress = true; 95 | continue; 96 | } 97 | 98 | // 代码块处理 99 | codeBlock = lineInfo.codeBlock ? !codeBlock : codeBlock; 100 | 101 | // 备注条件:备注标签中,非标题定义,或标题越位 102 | if (noteProgress || codeBlock || !lineInfo.level || lineInfo.level > level + 1) { 103 | if (node) _pushNote(node, line); 104 | continue; 105 | } 106 | 107 | // 标题处理 108 | level = lineInfo.level; 109 | node = _initNode(lineInfo.content, parentMap[level - 1]); 110 | parentMap[level] = node; 111 | } 112 | 113 | _cleanUp(parentMap[1]); 114 | return parentMap[1]; 115 | } 116 | 117 | function _initNode(text: string, parent: any) { 118 | const node = { 119 | data: { 120 | text, 121 | note: '', 122 | }, 123 | }; 124 | if (parent) { 125 | if (parent.children) parent.children.push(node); 126 | else parent.children = [node]; 127 | } 128 | return node; 129 | } 130 | 131 | function _pushNote(node: any, line: string) { 132 | node.data.note += `${line}\n`; 133 | } 134 | 135 | function _isEmpty(line: string) { 136 | return !/\S/.test(line); 137 | } 138 | 139 | function _resolveLine(line: string) { 140 | const match = /^(#+)?\s*(.*)$/.exec(line) || []; 141 | return { 142 | level: (match[1] && match[1].length) || null, 143 | content: match[2], 144 | noteStart: line == NOTE_MARK_START, 145 | noteClose: line == NOTE_MARK_CLOSE, 146 | codeBlock: /^\s*```/.test(line), 147 | }; 148 | } 149 | 150 | function _cleanUp(node: any) { 151 | if (!/\S/.test(node.data.note)) { 152 | node.data.note = null; 153 | delete node.data.note; 154 | } else { 155 | const notes = node.data.note.split('\n'); 156 | while (notes.length && !/\S/.test(notes[0])) notes.shift(); 157 | while (notes.length && !/\S/.test(notes[notes.length - 1])) notes.pop(); 158 | node.data.note = notes.join('\n'); 159 | } 160 | if (node.children) node.children.forEach(_cleanUp); 161 | } 162 | 163 | export default { exportMarkdown }; 164 | -------------------------------------------------------------------------------- /src/script/protocol/plain.ts: -------------------------------------------------------------------------------- 1 | const LINE_ENDING = '\r'; 2 | const LINE_ENDING_SPLITER = /\r\n|\r|\n/; 3 | const TAB_CHAR = '\t'; 4 | 5 | function exportTextTree(minder: any) { 6 | const minds = minder.exportJson(); 7 | try { 8 | const link = document.createElement('a'); 9 | const blob = new Blob([`\ufeff${encode(minds.root, 0)}`], { 10 | type: 'text/plain', 11 | }); 12 | link.href = window.URL.createObjectURL(blob); 13 | link.download = `${minds.root.data.text}.txt`; 14 | document.body.appendChild(link); 15 | link.click(); 16 | document.body.removeChild(link); 17 | } catch (err) { 18 | alert(err); 19 | } 20 | } 21 | 22 | function repeat(s: string, n: number) { 23 | let result = ''; 24 | while (n--) result += s; 25 | return result; 26 | } 27 | 28 | function encode(json: any, level: number) { 29 | let local = ''; 30 | level = level || 0; 31 | local += repeat(TAB_CHAR, level); 32 | local += json.data.text + LINE_ENDING; 33 | if (json.children) { 34 | json.children.forEach(function (child: any) { 35 | local += encode(child, level + 1); 36 | }); 37 | } 38 | return local; 39 | } 40 | 41 | function isEmpty(line: string) { 42 | return !/\S/.test(line); 43 | } 44 | 45 | function getLevel(line: string) { 46 | let level = 0; 47 | while (line.charAt(level) === TAB_CHAR) level++; 48 | return level; 49 | } 50 | 51 | function getNode(line: string) { 52 | return { 53 | data: { 54 | text: line.replace(new RegExp(`^${TAB_CHAR}*`), ''), 55 | }, 56 | }; 57 | } 58 | 59 | /** 60 | * 文本解码 61 | * 62 | * @param {string} local 文本内容 63 | * @param {=boolean} root 自动根节点 64 | * @return {Object} 返回解析后节点 65 | */ 66 | function decode(local: string, root: boolean) { 67 | let json; 68 | let offset; 69 | const parentMap = []; 70 | const lines = local.split(LINE_ENDING_SPLITER); 71 | let line; 72 | let level; 73 | let node; 74 | 75 | function addChild(parent: any, child: any) { 76 | const children = parent.children || (parent.children = []); 77 | children.push(child); 78 | } 79 | if (root) { 80 | parentMap[0] = json = getNode('root'); 81 | offset = 1; 82 | } else { 83 | offset = 0; 84 | } 85 | 86 | for (const element of lines) { 87 | line = element; 88 | if (isEmpty(line)) continue; 89 | 90 | level = getLevel(line) + offset; 91 | node = getNode(line); 92 | 93 | if (level === 0) { 94 | if (json) { 95 | throw new Error('Invalid local format'); 96 | } 97 | json = node; 98 | } else { 99 | if (!parentMap[level - 1]) { 100 | throw new Error('Invalid local format'); 101 | } 102 | addChild(parentMap[level - 1], node); 103 | } 104 | parentMap[level] = node; 105 | } 106 | return json; 107 | } 108 | 109 | export default { exportTextTree }; 110 | -------------------------------------------------------------------------------- /src/script/protocol/png.ts: -------------------------------------------------------------------------------- 1 | const DOMURL = window.URL || window.webkitURL || window; 2 | 3 | function downloadImage(fileURI: string, fileName: string) { 4 | try { 5 | const link = document.createElement('a'); 6 | link.href = fileURI; 7 | link.download = `${fileName}.png`; 8 | document.body.appendChild(link); 9 | link.click(); 10 | document.body.removeChild(link); 11 | } catch (err) { 12 | alert(err); 13 | } 14 | } 15 | 16 | function loadImage(url: string) { 17 | return new Promise(function (resolve, reject) { 18 | const image = document.createElement('img'); 19 | image.onload = function () { 20 | resolve(this); 21 | }; 22 | image.onerror = function (err) { 23 | reject(err); 24 | }; 25 | image.crossOrigin = ''; 26 | image.src = url; 27 | }); 28 | } 29 | 30 | function getSVGInfo(minder: any) { 31 | const paper = minder.getPaper(); 32 | let svgXml; 33 | let $svg; 34 | const renderContainer = minder.getRenderContainer(); 35 | const renderBox = renderContainer.getRenderBox(); 36 | const width = renderBox.width + 1; 37 | const height = renderBox.height + 1; 38 | let blob; 39 | let svgUrl; 40 | // 保存原始变换,并且移动到合适的位置 41 | const paperTransform = paper.shapeNode.getAttribute('transform'); 42 | paper.shapeNode.setAttribute('transform', 'translate(0.5, 0.5)'); 43 | renderContainer.translate(-renderBox.x, -renderBox.y); 44 | 45 | // 获取当前的 XML 代码 46 | svgXml = paper.container.innerHTML; 47 | 48 | // 回复原始变换及位置 49 | renderContainer.translate(renderBox.x, renderBox.y); 50 | paper.shapeNode.setAttribute('transform', paperTransform); 51 | 52 | // 过滤内容 53 | const el = document.createElement('div'); 54 | el.innerHTML = svgXml; 55 | $svg = el.getElementsByTagName('svg'); 56 | 57 | const index = $svg.length - 1; 58 | 59 | $svg[index].setAttribute('width', renderBox.width + 1); 60 | $svg[index].setAttribute('height', renderBox.height + 1); 61 | $svg[index].setAttribute('style', 'font-family: Arial, "Microsoft Yahei","Heiti SC";'); 62 | 63 | const div = document.createElement('div'); 64 | div.appendChild($svg[index]); 65 | svgXml = div.innerHTML; 66 | 67 | // Dummy IE 68 | svgXml = svgXml.replace( 69 | ' xmlns="http://www.w3.org/2000/svg" xmlns:NS1="" NS1:ns1:xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:NS2="" NS2:xmlns:ns1=""', 70 | '' 71 | ); 72 | 73 | // svg 含有   符号导出报错 Entity 'nbsp' not defined 74 | svgXml = svgXml.replace(/ /g, ' '); 75 | 76 | blob = new Blob([svgXml], { 77 | type: 'image/svg+xml', 78 | }); 79 | 80 | svgUrl = DOMURL.createObjectURL(blob); 81 | 82 | return { 83 | width, 84 | height, 85 | dataUrl: svgUrl, 86 | xml: svgXml, 87 | }; 88 | } 89 | 90 | function exportPNGImage(minder: any) { 91 | /* 绘制 PNG 的画布及上下文 */ 92 | const canvas = document.createElement('canvas'); 93 | const ctx = canvas.getContext('2d'); 94 | 95 | /* 尝试获取背景图片 URL 或背景颜色 */ 96 | const bgDeclare = minder.getStyle('background').toString(); 97 | const bgUrl = /url\((.+)\)/.exec(bgDeclare); 98 | const bgColor = window.kity.Color.parse(bgDeclare); 99 | 100 | /* 获取 SVG 文件内容 */ 101 | const svgInfo = getSVGInfo(minder); 102 | const { width } = svgInfo; 103 | const { height } = svgInfo; 104 | const svgDataUrl = svgInfo.dataUrl; 105 | 106 | /* 画布的填充大小 */ 107 | const padding = 20; 108 | 109 | canvas.width = width + padding * 2; 110 | canvas.height = height + padding * 2; 111 | 112 | function fillBackground(ctx: any, style: string) { 113 | ctx.save(); 114 | ctx.fillStyle = style; 115 | ctx.fillRect(0, 0, canvas.width, canvas.height); 116 | ctx.restore(); 117 | } 118 | 119 | function drawImage(ctx: any, image: any, x: number, y: number) { 120 | ctx.drawImage(image, x, y); 121 | } 122 | 123 | function generateDataUrl(canvas: any) { 124 | try { 125 | const url = canvas.toDataURL('png'); 126 | return url; 127 | } catch (e) { 128 | throw new Error('当前浏览器版本不支持导出 PNG 功能,请尝试升级到最新版本!'); 129 | } 130 | } 131 | 132 | function drawSVG() { 133 | const mind = window.editor.minder.exportJson(); 134 | if (typeof window.canvg !== 'undefined') { 135 | return new Promise(function (resolve) { 136 | window.canvg(canvas, svgInfo.xml, { 137 | ignoreMouse: true, 138 | ignoreAnimation: true, 139 | ignoreDimensions: true, 140 | ignoreClear: true, 141 | offsetX: padding, 142 | offsetY: padding, 143 | renderCallback() { 144 | downloadImage(generateDataUrl(canvas), mind.root.data.text); 145 | }, 146 | }); 147 | }); 148 | } 149 | return loadImage(svgDataUrl).then(function (svgImage) { 150 | drawImage(ctx, svgImage, padding, padding); 151 | DOMURL.revokeObjectURL(svgDataUrl); 152 | downloadImage(generateDataUrl(canvas), mind.root.data.text); 153 | }); 154 | } 155 | 156 | if (bgUrl) { 157 | loadImage(bgUrl[1]).then(function (image) { 158 | fillBackground(ctx, ctx.createPattern(image, 'repeat')); 159 | drawSVG(minder); 160 | }); 161 | } else { 162 | fillBackground(ctx, bgColor.toString()); 163 | drawSVG(minder); 164 | } 165 | } 166 | 167 | export default { exportPNGImage }; 168 | -------------------------------------------------------------------------------- /src/script/protocol/svg.ts: -------------------------------------------------------------------------------- 1 | function exportSVG(minder: any) { 2 | const paper = minder.getPaper(); 3 | const paperTransform = paper.shapeNode.getAttribute('transform'); 4 | let svgXml; 5 | let $svg; 6 | 7 | const renderContainer = minder.getRenderContainer(); 8 | const renderBox = renderContainer.getRenderBox(); 9 | const { width, height } = renderBox; 10 | const padding = 20; 11 | 12 | paper.shapeNode.setAttribute('transform', 'translate(0.5, 0.5)'); 13 | svgXml = paper.container.innerHTML; 14 | paper.shapeNode.setAttribute('transform', paperTransform); 15 | 16 | const { document } = window; 17 | const el = document.createElement('div'); 18 | el.innerHTML = svgXml; 19 | $svg = el.getElementsByTagName('svg'); 20 | 21 | const index = $svg.length - 1; 22 | 23 | $svg[index].setAttribute('width', width + padding * 2 || 0); 24 | $svg[index].setAttribute('height', height + padding * 2 || 0); 25 | $svg[index].setAttribute( 26 | 'style', 27 | `font-family: Arial, "Microsoft Yahei", "Heiti SC"; background: ${minder.getStyle('background')}` 28 | ); 29 | 30 | $svg[index].setAttribute( 31 | 'viewBox', 32 | [ 33 | (renderBox.x - padding) | 0, 34 | (renderBox.y - padding) | 0, 35 | (width + padding * 2) | 0, 36 | (height + padding * 2) | 0, 37 | ].join(' ') 38 | ); 39 | 40 | const div = document.createElement('div'); 41 | div.appendChild($svg[index]); 42 | svgXml = div.innerHTML; 43 | svgXml = svgXml.replace(/ /g, ' '); 44 | 45 | const blob = new Blob([svgXml], { 46 | type: 'image/svg+xml', 47 | }); 48 | 49 | const DOMURL = window.URL || window.webkitURL || window; 50 | const svgUrl = DOMURL.createObjectURL(blob); 51 | 52 | const mind = window.editor.minder.exportJson(); 53 | downloadSVG(svgUrl, mind.root.data.text); 54 | } 55 | 56 | function downloadSVG(fileURI: string, fileName: string) { 57 | try { 58 | const link = document.createElement('a'); 59 | link.href = fileURI; 60 | link.download = `${fileName}.svg`; 61 | document.body.appendChild(link); 62 | link.click(); 63 | document.body.removeChild(link); 64 | } catch (err) { 65 | alert(err); 66 | } 67 | } 68 | 69 | export default { exportSVG }; 70 | -------------------------------------------------------------------------------- /src/script/runtime/clipboard-mimetype.ts: -------------------------------------------------------------------------------- 1 | interface MimeTypes { 2 | [key: string]: string; 3 | } 4 | 5 | interface Signatures { 6 | [key: string]: string; 7 | } 8 | 9 | const MimeType = function () { 10 | const SPLITOR = '\uFEFF'; 11 | const MIMETYPE: MimeTypes = { 12 | 'application/km': '\uFFFF', 13 | }; 14 | const SIGN: Signatures = { 15 | '\uFEFF': 'SPLITOR', 16 | '\uFFFF': 'application/km', 17 | }; 18 | 19 | function process(mimetype: string | false, text: string): string { 20 | if (!isPureText(text)) { 21 | const _mimetype = whichMimeType(text); 22 | if (!_mimetype) { 23 | throw new Error('unknown mimetype!'); 24 | } 25 | text = getPureText(text); 26 | } 27 | if (mimetype === false) { 28 | return text; 29 | } 30 | return mimetype + SPLITOR + text; 31 | } 32 | 33 | function registMimeTypeProtocol(type: string, sign: string): void { 34 | if (sign && SIGN[sign]) { 35 | throw new Error('sign has registered!'); 36 | } 37 | if (type && !!MIMETYPE[type]) { 38 | throw new Error('mimetype has registered!'); 39 | } 40 | SIGN[sign] = type; 41 | MIMETYPE[type] = sign; 42 | } 43 | 44 | function getMimeTypeProtocol(type: string, text?: string): string | Function { 45 | const mimetype = MIMETYPE[type] || false; 46 | 47 | if (text === undefined) { 48 | return process.bind(null, mimetype); 49 | } 50 | 51 | return process(mimetype, text); 52 | } 53 | 54 | function getSpitor(): string { 55 | return SPLITOR; 56 | } 57 | 58 | function getMimeType(sign?: string): MimeTypes | string | null { 59 | if (sign !== undefined) { 60 | return SIGN[sign] || null; 61 | } 62 | return MIMETYPE; 63 | } 64 | 65 | function isPureText(text: string): boolean { 66 | return !~text.indexOf(getSpitor()); 67 | } 68 | 69 | function getPureText(text: string): string { 70 | if (isPureText(text)) { 71 | return text; 72 | } 73 | return text.split(getSpitor())[1]; 74 | } 75 | 76 | function whichMimeType(text: string): MimeTypes | string | null { 77 | if (isPureText(text)) { 78 | return null; 79 | } 80 | return getMimeType(text.split(getSpitor())[0]); 81 | } 82 | 83 | return { 84 | registMimeTypeProtocol, 85 | getMimeTypeProtocol, 86 | getSpitor, 87 | getMimeType, 88 | }; 89 | }; 90 | 91 | const MimeTypeRuntime = function (this: any) { 92 | if (this.minder.supportClipboardEvent && !window.kity.Browser.gecko) { 93 | this.MimeType = MimeType(); 94 | } 95 | }; 96 | 97 | export default MimeTypeRuntime; 98 | -------------------------------------------------------------------------------- /src/script/runtime/clipboard.ts: -------------------------------------------------------------------------------- 1 | import { markDeleteNode, resetNodes } from '../tool/utils'; 2 | 3 | interface INode { 4 | getLevel(): number; 5 | isAncestorOf(node: INode): boolean; 6 | appendChild(node: INode): INode; 7 | } 8 | 9 | interface IMimeType { 10 | getMimeTypeProtocol(mimeType: string): Function; 11 | isPureText(mimeType?: string): boolean; 12 | whichMimeType(mimeType?: string): string; 13 | getPureText(data?: string): string; 14 | } 15 | 16 | interface IData { 17 | getRegisterProtocol(protocol: string): { encode: Function; decode: Function }; 18 | } 19 | 20 | interface ICliboardEvent extends ClipboardEvent { 21 | clipboardData: DataTransfer; 22 | } 23 | 24 | export default function ClipboardRuntime(this: any) { 25 | const { minder } = this; 26 | const { receiver } = this; 27 | const Data: IData = window.kityminder.data; 28 | 29 | if (!minder.supportClipboardEvent || window.kity.Browser.gecko) { 30 | return; 31 | } 32 | 33 | const kmencode = this.MimeType.getMimeTypeProtocol('application/km'); 34 | const { decode } = Data.getRegisterProtocol('json'); 35 | let _selectedNodes: Array = []; 36 | 37 | function encode(nodes: Array): string { 38 | const _nodes = []; 39 | for (let i = 0, l = nodes.length; i < l; i++) { 40 | _nodes.push(minder.exportNode(nodes[i])); 41 | } 42 | return kmencode(Data.getRegisterProtocol('json').encode(_nodes)); 43 | } 44 | 45 | const beforeCopy = (e: ICliboardEvent) => { 46 | if (document.activeElement === receiver.element) { 47 | const clipBoardEvent = e; 48 | const state = this.fsm.state(); 49 | 50 | switch (state) { 51 | case 'input': { 52 | break; 53 | } 54 | case 'normal': { 55 | const nodes = [...minder.getSelectedNodes()]; 56 | if (nodes.length) { 57 | if (nodes.length > 1) { 58 | let targetLevel; 59 | nodes.sort(function (a: any, b: any) { 60 | return a.getLevel() - b.getLevel(); 61 | }); 62 | targetLevel = nodes[0].getLevel(); 63 | if (targetLevel !== nodes[nodes.length - 1].getLevel()) { 64 | let plevel; 65 | let pnode; 66 | let idx = 0; 67 | const l = nodes.length; 68 | let pidx = l - 1; 69 | 70 | pnode = nodes[pidx]; 71 | 72 | while (pnode.getLevel() !== targetLevel) { 73 | idx = 0; 74 | while (idx < l && nodes[idx].getLevel() === targetLevel) { 75 | if (nodes[idx].isAncestorOf(pnode)) { 76 | nodes.splice(pidx, 1); 77 | break; 78 | } 79 | idx++; 80 | } 81 | pidx--; 82 | pnode = nodes[pidx]; 83 | } 84 | } 85 | } 86 | const str = encode(nodes); 87 | clipBoardEvent.clipboardData.setData('text/plain', str); 88 | } 89 | e.preventDefault(); 90 | break; 91 | } 92 | } 93 | } 94 | }; 95 | 96 | const beforeCut = (e: ClipboardEvent) => { 97 | const { activeElement } = document; 98 | if (activeElement === receiver.element) { 99 | if (minder.getStatus() !== 'normal') { 100 | e.preventDefault(); 101 | return; 102 | } 103 | 104 | const clipBoardEvent = e; 105 | const state = this.fsm.state(); 106 | 107 | switch (state) { 108 | case 'input': { 109 | break; 110 | } 111 | case 'normal': { 112 | markDeleteNode(minder); 113 | const nodes = minder.getSelectedNodes(); 114 | if (nodes.length) { 115 | clipBoardEvent.clipboardData?.setData('text/plain', encode(nodes)); 116 | minder.execCommand('removenode'); 117 | } 118 | e.preventDefault(); 119 | break; 120 | } 121 | } 122 | } 123 | }; 124 | 125 | const beforePaste = (e: ClipboardEvent) => { 126 | if (document.activeElement === receiver.element) { 127 | if (minder.getStatus() !== 'normal') { 128 | e.preventDefault(); 129 | return; 130 | } 131 | 132 | const clipBoardEvent = e; 133 | const state = this.fsm.state(); 134 | const textData = clipBoardEvent.clipboardData?.getData('text/plain'); 135 | 136 | switch (state) { 137 | case 'input': { 138 | // input状态下如果格式为application/km则不进行paste操作 139 | if (!this.MimeType.isPureText(textData)) { 140 | e.preventDefault(); 141 | return; 142 | } 143 | break; 144 | } 145 | case 'normal': { 146 | /* 147 | * 针对normal状态下通过对选中节点粘贴导入子节点文本进行单独处理 148 | */ 149 | const sNodes = minder.getSelectedNodes(); 150 | 151 | if (this.MimeType.whichMimeType(textData) === 'application/km') { 152 | const nodes = decode(this.MimeType.getPureText(textData)); 153 | resetNodes(nodes); 154 | let _node; 155 | sNodes.forEach((node: INode) => { 156 | // 由于粘贴逻辑中为了排除子节点重新排序导致逆序,因此复制的时候倒过来 157 | for (let i = nodes.length - 1; i >= 0; i--) { 158 | _node = minder.createNode(null, node); 159 | minder.importNode(_node, nodes[i]); 160 | _selectedNodes.push(_node); 161 | node.appendChild(_node); 162 | } 163 | }); 164 | minder.select(_selectedNodes, true); 165 | _selectedNodes = []; 166 | 167 | minder.refresh(); 168 | } else if (clipBoardEvent.clipboardData && clipBoardEvent.clipboardData.items[0].type.indexOf('image') > -1) { 169 | const imageFile = clipBoardEvent.clipboardData.items[0].getAsFile(); 170 | const serverService = window.angular.element(document.body).injector().get('server'); 171 | 172 | return serverService.uploadImage(imageFile).then((json: Record) => { 173 | const resp = json.data; 174 | if (resp.errno === 0) { 175 | minder.execCommand('image', resp.data.url); 176 | } 177 | }); 178 | } else { 179 | sNodes.forEach((node: INode) => { 180 | minder.Text2Children(node, textData); 181 | }); 182 | } 183 | e.preventDefault(); 184 | break; 185 | } 186 | } 187 | // 触发命令监听 188 | minder.execCommand('paste'); 189 | } 190 | }; 191 | 192 | /** 193 | * 由editor的receiver统一处理全部事件,包括clipboard事件 194 | * @Editor: Naixor 195 | * @Date: 2015.9.24 196 | */ 197 | document.addEventListener('copy', () => beforeCopy); 198 | document.addEventListener('cut', () => beforeCut); 199 | document.addEventListener('paste', () => beforePaste); 200 | } 201 | -------------------------------------------------------------------------------- /src/script/runtime/container.ts: -------------------------------------------------------------------------------- 1 | function ContainerRuntime(this: { selector: string; container?: HTMLElement }) { 2 | let container: HTMLElement; 3 | 4 | if (typeof this.selector === 'string') { 5 | container = document.querySelector(this.selector)! as HTMLElement; 6 | } else { 7 | container = this.selector; 8 | } 9 | 10 | if (!container) throw new Error(`Invalid selector: ${this.selector}`); 11 | 12 | // 这个类名用于给编辑器添加样式 13 | container.classList.add('km-editor'); 14 | 15 | // 暴露容器给其他运行时使用 16 | this.container = container; 17 | } 18 | 19 | export default ContainerRuntime; 20 | -------------------------------------------------------------------------------- /src/script/runtime/drag.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * 4 | * 用于拖拽节点时屏蔽键盘事件 5 | * 6 | * @author: techird 7 | * @copyright: Baidu FEX, 2014 8 | */ 9 | interface DragRuntimeOptions { 10 | fsm: any; 11 | minder: any; 12 | hotbox: any; 13 | receiver: any; 14 | } 15 | 16 | function createDragRuntime(this: DragRuntimeOptions) { 17 | const { fsm, minder, hotbox } = this; 18 | 19 | // setup everything to go 20 | setupFsm(); 21 | 22 | // listen the fsm changes, make action. 23 | function setupFsm() { 24 | // when jumped to drag mode, enter 25 | fsm.when('* -> drag', function () { 26 | // now is drag mode 27 | }); 28 | 29 | fsm.when('drag -> *', function (exit: any, enter: any, reason: string) { 30 | if (reason == 'drag-finish') { 31 | // now exit drag mode 32 | } 33 | }); 34 | } 35 | 36 | let downX: number; 37 | let downY: number; 38 | const MOUSE_HAS_DOWN = 0; 39 | const MOUSE_HAS_UP = 1; 40 | const BOUND_CHECK = 20; 41 | let flag = MOUSE_HAS_UP; 42 | let maxX: number; 43 | let maxY: number; 44 | let containerY: number; 45 | let freeHorizen = false; 46 | let freeVirtical = false; 47 | let frame: number | null = null; 48 | 49 | function move(direction: 'left' | 'top' | 'right' | 'bottom' | false, speed?: number) { 50 | if (!direction) { 51 | freeHorizen = freeVirtical = false; 52 | frame && cancelAnimationFrame(frame); 53 | frame = null; 54 | return; 55 | } 56 | if (!frame) { 57 | frame = requestAnimationFrame( 58 | (function (direction: 'left' | 'top' | 'right' | 'bottom', speed: number, minder: any) { 59 | return function (frame) { 60 | switch (direction) { 61 | case 'left': 62 | minder._viewDragger.move( 63 | { 64 | x: -speed, 65 | y: 0, 66 | }, 67 | 0 68 | ); 69 | break; 70 | case 'top': 71 | minder._viewDragger.move( 72 | { 73 | x: 0, 74 | y: -speed, 75 | }, 76 | 0 77 | ); 78 | break; 79 | case 'right': 80 | minder._viewDragger.move( 81 | { 82 | x: speed, 83 | y: 0, 84 | }, 85 | 0 86 | ); 87 | break; 88 | case 'bottom': 89 | minder._viewDragger.move( 90 | { 91 | x: 0, 92 | y: speed, 93 | }, 94 | 0 95 | ); 96 | break; 97 | default: 98 | return; 99 | } 100 | frame && requestAnimationFrame(frame as any); 101 | }; 102 | })(direction, speed!, minder) 103 | ); 104 | } 105 | } 106 | 107 | minder.on('mousedown', function (e: any) { 108 | flag = MOUSE_HAS_DOWN; 109 | const rect = minder.getPaper().container.getBoundingClientRect(); 110 | downX = e.originEvent.clientX; 111 | downY = e.originEvent.clientY; 112 | containerY = rect.top; 113 | maxX = rect.width; 114 | maxY = rect.height; 115 | }); 116 | 117 | minder.on('mousemove', function (e: any) { 118 | if ( 119 | fsm.state() === 'drag' && 120 | flag === MOUSE_HAS_DOWN && 121 | minder.getSelectedNode() && 122 | (Math.abs(downX - e.originEvent.clientX) > BOUND_CHECK || Math.abs(downY - e.originEvent.clientY) > BOUND_CHECK) 123 | ) { 124 | const osx = e.originEvent.clientX; 125 | const osy = e.originEvent.clientY - containerY; 126 | 127 | if (osx < BOUND_CHECK) { 128 | move('right', BOUND_CHECK - osx); 129 | } else if (osx > maxX - BOUND_CHECK) { 130 | move('left', BOUND_CHECK + osx - maxX); 131 | } else { 132 | freeHorizen = true; 133 | } 134 | 135 | if (osy < BOUND_CHECK) { 136 | move('bottom', osy); 137 | } else if (osy > maxY - BOUND_CHECK) { 138 | move('top', BOUND_CHECK + osy - maxY); 139 | } else { 140 | freeVirtical = true; 141 | } 142 | 143 | if (freeHorizen && freeVirtical) { 144 | move(false); 145 | } 146 | } 147 | 148 | if ( 149 | fsm.state() !== 'drag' && 150 | flag === MOUSE_HAS_DOWN && 151 | minder.getSelectedNode() && 152 | (Math.abs(downX - e.originEvent.clientX) > BOUND_CHECK || Math.abs(downY - e.originEvent.clientY) > BOUND_CHECK) 153 | ) { 154 | if (fsm.state() === 'hotbox') { 155 | hotbox.active(window.HotBox.STATE_IDLE); 156 | } 157 | 158 | fsm.jump('drag', 'user-drag'); 159 | } 160 | }); 161 | 162 | window.addEventListener( 163 | 'mouseup', 164 | () => { 165 | flag = MOUSE_HAS_UP; 166 | if (fsm.state() === 'drag') { 167 | move(false); 168 | fsm.jump('normal', 'drag-finish'); 169 | } 170 | }, 171 | false 172 | ); 173 | } 174 | export default createDragRuntime; 175 | -------------------------------------------------------------------------------- /src/script/runtime/exports.ts: -------------------------------------------------------------------------------- 1 | import png from '../protocol/png'; 2 | import svg from '../protocol/svg'; 3 | import json from '../protocol/json'; 4 | import plain from '../protocol/plain'; 5 | import md from '../protocol/markdown'; 6 | import mm from '../protocol/freemind'; 7 | import useLocaleNotVue from '@/script/tool/useLocaleNotVue'; 8 | 9 | const tran = useLocaleNotVue; 10 | 11 | export default function ExportRuntime(this: any) { 12 | const { minder, hotbox } = this; 13 | const exps = [ 14 | { label: '.json', key: 'j', cmd: exportJson }, 15 | { label: '.png', key: 'p', cmd: exportImage }, 16 | { label: '.svg', key: 's', cmd: exportSVG }, 17 | { label: '.txt', key: 't', cmd: exportTextTree }, 18 | { label: '.md', key: 'm', cmd: exportMarkdown }, 19 | { label: '.mm', key: 'f', cmd: exportFreeMind }, 20 | ]; 21 | 22 | const main = hotbox.state('main'); 23 | main.button({ 24 | position: 'top', 25 | label: tran('minder.commons.export'), 26 | key: 'E', 27 | enable: canExp, 28 | beforeShow() { 29 | this.$button.children[0].innerHTML = tran('minder.commons.export'); 30 | }, 31 | next: 'exp', 32 | }); 33 | 34 | const exp = hotbox.state('exp'); 35 | exps.forEach((item) => { 36 | exp.button({ 37 | position: 'ring', 38 | label: item.label, 39 | key: null, 40 | action: item.cmd, 41 | beforeShow() { 42 | this.$button.children[0].innerHTML = tran(item.label); 43 | }, 44 | }); 45 | }); 46 | 47 | exp.button({ 48 | position: 'center', 49 | label: tran('minder.commons.cancel'), 50 | key: 'esc', 51 | beforeShow() { 52 | this.$button.children[0].innerHTML = tran('minder.commons.cancel'); 53 | }, 54 | next: 'back', 55 | }); 56 | 57 | function canExp() { 58 | return true; 59 | } 60 | 61 | function exportJson() { 62 | json.exportJson(minder); 63 | } 64 | 65 | function exportImage() { 66 | png.exportPNGImage(minder); 67 | } 68 | 69 | function exportSVG() { 70 | svg.exportSVG(minder); 71 | } 72 | 73 | function exportTextTree() { 74 | plain.exportTextTree(minder); 75 | } 76 | 77 | function exportMarkdown() { 78 | md.exportMarkdown(minder); 79 | } 80 | 81 | function exportFreeMind() { 82 | mm.exportFreeMind(minder); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/script/runtime/fsm.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-const */ 2 | /* eslint-disable prefer-destructuring */ 3 | /* eslint-disable no-param-reassign */ 4 | import Debug from '../tool/debug'; 5 | 6 | function handlerConditionMatch(condition: any, when: string, exit: string, enter: string): boolean { 7 | if (condition.when !== when) { 8 | return false; 9 | } 10 | if (condition.enter !== '*' && condition.enter !== enter) { 11 | return false; 12 | } 13 | if (condition.exit !== '*' && condition.exit !== exit) { 14 | return false; 15 | } 16 | return true; 17 | } 18 | 19 | type Handler = Function & { 20 | condition: { 21 | when: string; 22 | exit: string; 23 | enter: string; 24 | }; 25 | }; 26 | 27 | class FSM { 28 | private currentState: string; 29 | 30 | private readonly BEFORE_ARROW: string = ' - '; 31 | 32 | private readonly AFTER_ARROW: string = ' -> '; 33 | 34 | private handlers: any[] = []; 35 | 36 | private debug: Debug; 37 | 38 | constructor(defaultState: string) { 39 | this.currentState = defaultState; 40 | this.debug = new Debug('fsm'); 41 | } 42 | 43 | /** 44 | * 状态跳转 45 | * 46 | * 会通知所有的状态跳转监视器 47 | * 48 | * @param {string} newState 新状态名称 49 | * @param {any} reason 跳转的原因,可以作为参数传递给跳转监视器 50 | */ 51 | public jump(newState: string, reason: any, ...args: any): string { 52 | if (!reason) { 53 | throw new Error('Please tell fsm the reason to jump'); 54 | } 55 | 56 | const oldState = this.currentState; 57 | const notify = [oldState, newState].concat([].slice.call(args, 1)); 58 | let i; 59 | let handler; 60 | 61 | // 跳转前 62 | for (i = 0; i < this.handlers.length; i++) { 63 | handler = this.handlers[i]; 64 | if (handlerConditionMatch(handler.condition, 'before', oldState, newState)) { 65 | if (handler.apply(null, [...notify])) { 66 | return newState; 67 | } 68 | } 69 | } 70 | 71 | this.currentState = newState; 72 | this.debug.log('[{0}] {1} -> {2}', reason, oldState, newState); 73 | 74 | // 跳转后 75 | for (i = 0; i < this.handlers.length; i++) { 76 | handler = this.handlers[i]; 77 | if (handlerConditionMatch(handler.condition, 'after', oldState, newState)) { 78 | handler.apply(null, [...notify]); 79 | } 80 | } 81 | return newState; 82 | } 83 | 84 | /** 85 | * 返回当前状态 86 | * @return {string} 87 | */ 88 | public state(): string { 89 | return this.currentState; 90 | } 91 | 92 | /** 93 | * 添加状态跳转监视器 94 | * 95 | * @param {string} condition 96 | * 监视的时机 97 | * "* => *" (默认) 98 | * 99 | * @param {Handler} handler 100 | * 监视函数,当状态跳转的时候,会接收三个参数 101 | * * from - 跳转前的状态 102 | * * to - 跳转后的状态 103 | * * reason - 跳转的原因 104 | */ 105 | public when(condition: string, handler: Handler | string): void { 106 | if (arguments.length === 1) { 107 | handler = condition; 108 | condition = '* -> *'; 109 | } 110 | 111 | let whenVar = ''; 112 | let resolved: string[]; 113 | let exit: string; 114 | let enter: string; 115 | 116 | resolved = condition.split(this.BEFORE_ARROW); 117 | if (resolved.length === 2) { 118 | whenVar = 'before'; 119 | } else { 120 | resolved = condition.split(this.AFTER_ARROW); 121 | if (resolved.length === 2) { 122 | whenVar = 'after'; 123 | } 124 | } 125 | if (!whenVar) { 126 | throw new Error(`Illegal fsm condition: ${condition}`); 127 | } 128 | 129 | exit = resolved[0]; 130 | enter = resolved[1]; 131 | 132 | (handler as any).condition = { 133 | when: whenVar, 134 | exit, 135 | enter, 136 | }; 137 | 138 | this.handlers.push(handler); 139 | } 140 | } 141 | 142 | function FSMRumtime(this: any) { 143 | this.fsm = new FSM('normal'); 144 | } 145 | 146 | export default FSMRumtime; 147 | -------------------------------------------------------------------------------- /src/script/runtime/history.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import { isDisableNode } from '../tool/utils'; 3 | import useLocaleNotVue from '@/script/tool/useLocaleNotVue'; 4 | 5 | const tran = useLocaleNotVue; 6 | 7 | interface History { 8 | reset: () => void; 9 | undo: () => void; 10 | redo: () => void; 11 | hasUndo: () => boolean; 12 | hasRedo: () => boolean; 13 | } 14 | 15 | export default function HistoryRuntime(this: { minder: any; hotbox: any; editText: Function; history: History }) { 16 | const { minder, hotbox } = this; 17 | const MAX_HISTORY = 100; 18 | 19 | let lastSnap: string; 20 | let patchLock: boolean; 21 | let undoDiffs: any[]; 22 | let redoDiffs: any[]; 23 | 24 | function reset() { 25 | undoDiffs = []; 26 | redoDiffs = []; 27 | lastSnap = minder.exportJson(); 28 | } 29 | 30 | const _objectKeys = (function () { 31 | if (Object.keys) return Object.keys; 32 | 33 | return function (o: object) { 34 | const keys: string[] = []; 35 | Object.keys(o).forEach((i) => { 36 | keys.push(i); 37 | }); 38 | return keys; 39 | }; 40 | })(); 41 | 42 | function escapePathComponent(str: string): string { 43 | if (str.indexOf('/') === -1 && str.indexOf('~') === -1) return str; 44 | return str.replace(/~/g, '~0').replace(/\//g, '~1'); 45 | } 46 | 47 | function deepClone(obj: any): any { 48 | if (typeof obj === 'object') { 49 | return JSON.parse(JSON.stringify(obj)); 50 | } 51 | return obj; 52 | } 53 | 54 | function _generate(mirror: any, obj: any, patches: any[], path: string) { 55 | const newKeys = _objectKeys(obj); 56 | const oldKeys = _objectKeys(mirror); 57 | let changed = false; 58 | let deleted = false; 59 | 60 | for (let t = oldKeys.length - 1; t >= 0; t--) { 61 | const key = oldKeys[t]; 62 | const oldVal = mirror[key]; 63 | // eslint-disable-next-line no-prototype-builtins 64 | if (obj.hasOwnProperty(key)) { 65 | const newVal = obj[key]; 66 | if (typeof oldVal === 'object' && oldVal != null && typeof newVal === 'object' && newVal != null) { 67 | _generate(oldVal, newVal, patches, `${path}/${escapePathComponent(key)}`); 68 | } else if (oldVal !== newVal) { 69 | changed = true; 70 | patches.push({ 71 | op: 'replace', 72 | path: `${path}/${escapePathComponent(key)}`, 73 | value: deepClone(newVal), 74 | }); 75 | } 76 | } else { 77 | patches.push({ 78 | op: 'remove', 79 | path: `${path}/${escapePathComponent(key)}`, 80 | }); 81 | deleted = true; // property has been deleted 82 | } 83 | } 84 | 85 | if (!deleted && newKeys.length === oldKeys.length) { 86 | return; 87 | } 88 | 89 | for (let t = 0; t < newKeys.length; t++) { 90 | const key = newKeys[t]; 91 | // eslint-disable-next-line no-prototype-builtins 92 | if (!mirror.hasOwnProperty(key)) { 93 | patches.push({ 94 | op: 'add', 95 | path: `${path}/${escapePathComponent(key)}`, 96 | value: deepClone(obj[key]), 97 | }); 98 | } 99 | } 100 | } 101 | 102 | function jsonDiff(tree1: any, tree2: any): any[] { 103 | const patches: any = []; 104 | _generate(tree1, tree2, patches, ''); 105 | return patches; 106 | } 107 | 108 | function makeUndoDiff(): boolean | void { 109 | const headSnap = minder.exportJson(); 110 | const diff = jsonDiff(headSnap, lastSnap); 111 | if (diff.length) { 112 | undoDiffs.push(diff); 113 | while (undoDiffs.length > MAX_HISTORY) { 114 | undoDiffs.shift(); 115 | } 116 | lastSnap = headSnap; 117 | return true; 118 | } 119 | } 120 | 121 | function makeRedoDiff() { 122 | const revertSnap = minder.exportJson(); 123 | redoDiffs.push(jsonDiff(revertSnap, lastSnap)); 124 | lastSnap = revertSnap; 125 | } 126 | 127 | function undo() { 128 | patchLock = true; 129 | const undoDiff = undoDiffs.pop(); 130 | if (undoDiff) { 131 | minder.applyPatches(undoDiff); 132 | makeRedoDiff(); 133 | } 134 | patchLock = false; 135 | } 136 | 137 | function redo() { 138 | patchLock = true; 139 | const redoDiff = redoDiffs.pop(); 140 | if (redoDiff) { 141 | minder.applyPatches(redoDiff); 142 | makeUndoDiff(); 143 | } 144 | patchLock = false; 145 | } 146 | 147 | function changed() { 148 | if (patchLock) return; 149 | if (makeUndoDiff()) redoDiffs = []; 150 | } 151 | 152 | function hasUndo() { 153 | return !!undoDiffs.length; 154 | } 155 | 156 | function hasRedo() { 157 | return !!redoDiffs.length; 158 | } 159 | 160 | function updateSelection(e: any) { 161 | if (!patchLock) return; 162 | const { patch } = e; 163 | switch (patch.express) { 164 | case 'node.add': 165 | minder.select(patch.node.getChild(patch.index), true); 166 | break; 167 | case 'node.remove': 168 | case 'data.replace': 169 | case 'data.remove': 170 | case 'data.add': 171 | minder.select(patch.node, true); 172 | break; 173 | default: 174 | } 175 | } 176 | 177 | this.history = { 178 | reset, 179 | undo, 180 | redo, 181 | hasUndo, 182 | hasRedo, 183 | }; 184 | reset(); 185 | minder.on('contentchange', changed); 186 | minder.on('import', reset); 187 | minder.on('patch', updateSelection); 188 | 189 | const main = hotbox.state('main'); 190 | main.button({ 191 | position: 'bottom', 192 | label: tran('minder.main.history.undo'), 193 | key: 'Ctrl + Z', 194 | enable() { 195 | if (isDisableNode(minder)) { 196 | return false; 197 | } 198 | return hasUndo; 199 | }, 200 | action: undo, 201 | beforeShow() { 202 | this.$button.children[0].innerHTML = tran('minder.main.history.undo'); 203 | }, 204 | next: 'idle', 205 | }); 206 | main.button({ 207 | position: 'bottom', 208 | label: tran('minder.main.history.redo'), 209 | key: 'Ctrl + Y', 210 | enable() { 211 | if (isDisableNode(minder)) { 212 | return false; 213 | } 214 | return hasRedo; 215 | }, 216 | action: redo, 217 | beforeShow() { 218 | this.$button.children[0].innerHTML = tran('minder.main.history.undo'); 219 | }, 220 | next: 'idle', 221 | }); 222 | } 223 | -------------------------------------------------------------------------------- /src/script/runtime/hotbox.ts: -------------------------------------------------------------------------------- 1 | interface Position { 2 | x: number; 3 | y: number; 4 | } 5 | 6 | function HotboxRuntime(this: any) { 7 | const { fsm } = this; 8 | const { minder } = this; 9 | const { receiver } = this; 10 | const { container } = this; 11 | const { HotBox } = window; 12 | const hotbox = new HotBox(container); 13 | 14 | hotbox.setParentFSM(fsm); 15 | 16 | fsm.when('normal -> hotbox', (exit: any, enter: any, reason: any) => { 17 | const node = minder.getSelectedNode(); 18 | let position: Position | undefined; 19 | if (node) { 20 | const box = node.getRenderBox(); 21 | position = { 22 | x: box.cx, 23 | y: box.cy, 24 | }; 25 | } 26 | hotbox.active('main', position); 27 | }); 28 | 29 | fsm.when('normal -> normal', (exit: any, enter: any, reason: any, e: any) => { 30 | if (reason == 'shortcut-handle') { 31 | const handleResult = hotbox.dispatch(e); 32 | if (handleResult) { 33 | e.preventDefault(); 34 | } else { 35 | minder.dispatchKeyEvent(e); 36 | } 37 | } 38 | }); 39 | 40 | fsm.when('modal -> normal', (exit: any, enter: any, reason: any, e: any) => { 41 | if (reason == 'import-text-finish') { 42 | receiver.element.focus(); 43 | } 44 | }); 45 | 46 | this.hotbox = hotbox; 47 | minder.hotbox = hotbox; 48 | } 49 | 50 | export default HotboxRuntime; 51 | -------------------------------------------------------------------------------- /src/script/runtime/input.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | /* eslint-disable camelcase */ 3 | /** 4 | * @fileOverview 5 | * 6 | * 文本输入支持 7 | * 8 | * @author: techird 9 | * @copyright: Baidu FEX, 2014 10 | */ 11 | import '../tool/innertext'; 12 | import { isDisableNode, markChangeNode } from '../tool/utils'; 13 | import Debug from '../tool/debug'; 14 | import useLocaleNotVue from '@/script/tool/useLocaleNotVue'; 15 | 16 | const tran = useLocaleNotVue; 17 | const debug = new Debug('input') as any; 18 | 19 | function InputRuntime(this: any) { 20 | this.receiverElement = this.receiver.element; 21 | this.isGecko = window.kity.Browser.gecko; 22 | 23 | const updatePosition = (): void => { 24 | const planed = { 25 | timer: 0, 26 | }; 27 | const focusNode = this.minder.getSelectedNode(); 28 | if (!focusNode) return; 29 | 30 | if (!planed.timer) { 31 | planed.timer = setTimeout(() => { 32 | const box = focusNode.getRenderBox('TextRenderer'); 33 | this.receiverElement.style.left = `${Math.round(box.x)}px`; 34 | this.receiverElement.style.top = `${debug.flaged ? Math.round(box.bottom + 30) : Math.round(box.y)}px`; 35 | // receiverElement.focus(); 36 | planed.timer = 0; 37 | }); 38 | } 39 | }; 40 | 41 | // let the receiver follow the current selected node position 42 | // setup everything to go 43 | const setupReciverElement = () => { 44 | if (debug.flaged) { 45 | this.receiverElement.classList.add('debug'); 46 | } 47 | 48 | if (this.receiverElement) { 49 | this.receiverElement.onmousedown = (e: any) => { 50 | e.stopPropagation(); 51 | }; 52 | } 53 | if (this.minder.on) { 54 | this.minder.on('layoutallfinish viewchange viewchanged selectionchange', (e: any) => { 55 | // viewchange event is too frequenced, lazy it 56 | if (e.type === 'viewchange' && this.fsm.state() !== 'input') return; 57 | 58 | updatePosition(); 59 | }); 60 | } 61 | updatePosition(); 62 | }; 63 | 64 | setupReciverElement(); 65 | 66 | /** 67 | * 增加对字体的鉴别,以保证用户在编辑状态ctrl/cmd + b/i所触发的加粗斜体与显示一致 68 | * @editor Naixor 69 | * @Date 2015-12-2 70 | */ 71 | // edit for the selected node 72 | const editText = (): void => { 73 | const node = this.minder.getSelectedNode(); 74 | if (!node) { 75 | return; 76 | } 77 | 78 | markChangeNode(node); 79 | 80 | let textContainer = this.receiverElement; 81 | this.receiverElement.innerText = ''; 82 | if (node.getData('font-weight') === 'bold') { 83 | const b = document.createElement('b'); 84 | textContainer.appendChild(b); 85 | textContainer = b; 86 | } 87 | if (node.getData('font-style') === 'italic') { 88 | const i = document.createElement('i'); 89 | textContainer.appendChild(i); 90 | textContainer = i; 91 | } 92 | textContainer.innerText = this.minder.queryCommandValue('text'); 93 | 94 | if (this.isGecko) { 95 | this.receiver.fixFFCaretDisappeared(); 96 | } 97 | this.fsm.jump('input', 'input-request'); 98 | this.receiver.selectAll(); 99 | }; 100 | 101 | // expose editText() 102 | this.editText = editText.bind(this); 103 | 104 | /** 105 | * 增加对字体的鉴别,以保证用户在编辑状态ctrl/cmd + b/i所触发的加粗斜体与显示一致 106 | * @editor Naixor 107 | * @Date 2015-12-2 108 | */ 109 | const enterInputMode = (): void => { 110 | const node = this.minder.getSelectedNode(); 111 | if (node) { 112 | const fontSize = node.getData('font-size') || node.getStyle('font-size'); 113 | this.receiverElement.style.fontSize = `${fontSize}px`; 114 | this.receiverElement.style.minWidth = '0'; 115 | this.receiverElement.style.minWidth = `${this.receiverElement.clientWidth}px`; 116 | this.receiverElement.style.fontWeight = node.getData('font-weight') || ''; 117 | this.receiverElement.style.fontStyle = node.getData('font-style') || ''; 118 | this.receiverElement.classList.add('input'); 119 | this.receiverElement.focus(); 120 | } 121 | }; 122 | 123 | // listen the fsm changes, make action. 124 | const setupFsm = () => { 125 | // when jumped to input mode, enter 126 | this.fsm.when('* -> input', enterInputMode.bind(this)); 127 | 128 | // when exited, commit or exit depends on the exit reason 129 | this.fsm.when('input -> *', (exit: any, enter: any, reason: string) => { 130 | switch (reason) { 131 | case 'input-cancel': 132 | return exitInputMode(); 133 | case 'input-commit': 134 | default: 135 | return commitInputResult(); 136 | } 137 | }); 138 | 139 | // lost focus to commit 140 | this.receiver.onblur(() => { 141 | if (this.fsm.state() === 'input') { 142 | this.fsm.jump('normal', 'input-commit'); 143 | } 144 | }); 145 | 146 | this.minder.on('beforemousedown', () => { 147 | if (this.fsm.state() === 'input') { 148 | this.fsm.jump('normal', 'input-commit'); 149 | } 150 | }); 151 | 152 | this.minder.on('dblclick', () => { 153 | // eslint-disable-next-line no-underscore-dangle 154 | if (this.minder.getSelectedNode() && this.minder._status !== 'readonly' && !isDisableNode(this.minder)) { 155 | this.editText(); 156 | } 157 | }); 158 | }; 159 | 160 | setupFsm(); 161 | 162 | // edit entrance in hotbox 163 | const setupHotbox = () => { 164 | this.hotbox.state('main').button({ 165 | position: 'center', 166 | label: tran('minder.commons.edit'), 167 | key: 'F2', 168 | enable: () => { 169 | if (isDisableNode(this.minder)) { 170 | return false; 171 | } 172 | return this.minder.queryCommandState('text') !== -1; 173 | }, 174 | action: this.editText, 175 | beforeShow() { 176 | this.$button.children[0].innerHTML = tran('minder.commons.edit'); 177 | }, 178 | }); 179 | }; 180 | 181 | setupHotbox(); 182 | 183 | /** 184 | * 按照文本提交操作处理 185 | * @Desc: 从其他节点复制文字到另一个节点时部分浏览器(chrome)会自动包裹一个span标签,这样试用一下逻辑出来的就不是text节点二是span节点因此导致undefined的情况发生 186 | * @Warning: 下方代码使用[].slice.call来将HTMLDomCollection处理成为Array,ie8及以下会有问题 187 | * @Editor: Naixor 188 | * @Date: 2015.9.16 189 | */ 190 | const commitInputText = (textNodes: any): string => { 191 | let text = ''; 192 | const TAB_CHAR = '\t'; 193 | const ENTER_CHAR = '\n'; 194 | const STR_CHECK = /\S/; 195 | const SPACE_CHAR = '\u0020'; 196 | // 针对FF,SG,BD,LB,IE等浏览器下SPACE的charCode存在为32和160的情况做处理 197 | const SPACE_CHAR_REGEXP = new RegExp(`(\u0020|${String.fromCharCode(160)})`); 198 | const BR = document.createElement('br'); 199 | let isBold = false; 200 | let isItalic = false; 201 | 202 | // eslint-disable-next-line no-underscore-dangle, camelcase 203 | for (let str: any | string, _divChildNodes, space_l, i = 0, l = textNodes.length; i < l; i++) { 204 | str = textNodes[i]; 205 | 206 | switch (Object.prototype.toString.call(str)) { 207 | // 正常情况处理 208 | case '[object HTMLBRElement]': { 209 | text += ENTER_CHAR; 210 | break; 211 | } 212 | case '[object Text]': { 213 | // SG下会莫名其妙的加上 影响后续判断,干掉! 214 | /** 215 | * FF下的wholeText会导致如下问题: 216 | * |123| -> 在一个节点中输入一段字符,此时TextNode为[#Text 123] 217 | * 提交并重新编辑,在后面追加几个字符 218 | * |123abc| -> 此时123为一个TextNode为[#Text 123, #Text abc],但是对这两个任意取值wholeText均为全部内容123abc 219 | * 上述BUG仅存在在FF中,故将wholeText更改为textContent 220 | */ 221 | str = str.textContent?.replace(' ', ' '); 222 | 223 | if (!STR_CHECK.test(str)) { 224 | space_l = str.length; 225 | while (space_l--) { 226 | if (SPACE_CHAR_REGEXP.test(str[space_l])) { 227 | text += SPACE_CHAR; 228 | } else if (str[space_l] === TAB_CHAR) { 229 | text += TAB_CHAR; 230 | } 231 | } 232 | } else { 233 | text += str; 234 | } 235 | break; 236 | } 237 | // ctrl + b/i 会给字体加上/标签来实现黑体和斜体 238 | case '[object HTMLElement]': { 239 | switch (str.nodeName) { 240 | case 'B': { 241 | isBold = true; 242 | break; 243 | } 244 | case 'I': { 245 | isItalic = true; 246 | break; 247 | } 248 | default: 249 | } 250 | [].splice.apply(textNodes, [i, 1, ...[].slice.call(str.childNodes)]); 251 | l = textNodes.length; 252 | i--; 253 | break; 254 | } 255 | // 被增加span标签的情况会被处理成正常情况并会推交给上面处理 256 | case '[object HTMLSpanElement]': { 257 | [].splice.apply(textNodes, [i, 1, ...[].slice.call(str.childNodes)]); 258 | l = textNodes.length; 259 | i--; 260 | break; 261 | } 262 | // 若标签为image标签,则判断是否为合法url,是将其加载进来 263 | case '[object HTMLImageElement]': { 264 | if (str.src) { 265 | if (/http(|s):\/\//.test(str.src)) { 266 | this.minder.execCommand('Image', str.src, str.alt); 267 | } else { 268 | // data:image协议情况 269 | } 270 | } 271 | break; 272 | } 273 | // 被增加div标签的情况会被处理成正常情况并会推交给上面处理 274 | case '[object HTMLDivElement]': { 275 | _divChildNodes = []; 276 | for (let di = 0; di < l; di++) { 277 | _divChildNodes.push(str.childNodes[di]); 278 | } 279 | _divChildNodes.push(BR); 280 | [].splice.apply(textNodes, [i, 1, ...[].slice.call(_divChildNodes)]); 281 | l = textNodes.length; 282 | i--; 283 | break; 284 | } 285 | default: { 286 | if (str && str.childNodes.length) { 287 | _divChildNodes = []; 288 | for (let di = 0; di < l; di++) { 289 | _divChildNodes.push(str.childNodes[di]); 290 | } 291 | _divChildNodes.push(BR); 292 | [].splice.apply(textNodes, [i, 1, ...[].slice.call(_divChildNodes)]); 293 | l = textNodes.length; 294 | i--; 295 | } else if (str && str.textContent !== undefined) { 296 | text += str.textContent; 297 | } else { 298 | text += ''; 299 | } 300 | // // 其他带有样式的节点被粘贴进来,则直接取textContent,若取不出来则置空 301 | } 302 | } 303 | } 304 | 305 | text = text.replace(/^\n*|\n*$/g, ''); 306 | text = text.replace(new RegExp(`(\n|\r|\n\r)(\u0020|${String.fromCharCode(160)}){4}`, 'g'), '$1\t'); 307 | this.minder.getSelectedNode().setText(text); 308 | if (isBold) { 309 | this.minder.queryCommandState('bold') || this.minder.execCommand('bold'); 310 | } else { 311 | this.minder.queryCommandState('bold') && this.minder.execCommand('bold'); 312 | } 313 | 314 | if (isItalic) { 315 | this.minder.queryCommandState('italic') || this.minder.execCommand('italic'); 316 | } else { 317 | this.minder.queryCommandState('italic') && this.minder.execCommand('italic'); 318 | } 319 | exitInputMode(); 320 | return text; 321 | }; 322 | 323 | /** 324 | * 判断节点的文本信息是否是 325 | * @Desc: 从其他节点复制文字到另一个节点时部分浏览器(chrome)会自动包裹一个span标签,这样使用以下逻辑出来的就不是text节点二是span节点因此导致undefined的情况发生 326 | * @Notice: 此处逻辑应该拆分到 kityminder-core/core/data中去,单独增加一个对某个节点importJson的事件 327 | * @Editor: Naixor 328 | * @Date: 2015.9.16 329 | */ 330 | const commitInputNode = (node: any, text: string) => { 331 | try { 332 | this.minder.decodeData('text', text).then((json: any) => { 333 | function importText(node: any, json: any, minder: any) { 334 | const { data } = json; 335 | node.setText(data.text || ''); 336 | const childrenTreeData = json.children || []; 337 | for (let i = 0; i < childrenTreeData.length; i++) { 338 | const childNode = minder.createNode(null, node); 339 | importText(childNode, childrenTreeData[i], minder); 340 | } 341 | return node; 342 | } 343 | importText(node, json, this.minder); 344 | this.minder.fire('contentchange'); 345 | this.minder.getRoot().renderTree(); 346 | this.minder.layout(300); 347 | }); 348 | } catch (e: any) { 349 | this.minder.fire('contentchange'); 350 | this.minder.getRoot().renderTree(); 351 | // 无法被转换成脑图节点则不处理 352 | if (e.toString() !== 'Error: Invalid local format') { 353 | throw e; 354 | } 355 | } 356 | }; 357 | 358 | const commitInputResult = () => { 359 | /** 360 | * @desc 进行如下处理: 361 | * 根据用户的输入判断是否生成新的节点 362 | * fix #83 https://github.com/fex-team/kityminder-editor/issues/83 363 | * @editor Naixor 364 | * @date 2015.9.16 365 | */ 366 | const textNodes = Array.from(this.receiverElement.childNodes); 367 | 368 | /** 369 | * @desc 增加setTimeout的原因:ie下receiverElement.innerHTML=""会导致后 370 | * 面commitInputText中使用textContent报错,不要问我什么原因! 371 | * @editor Naixor 372 | * @date 2015.12.14 373 | */ 374 | setTimeout(() => { 375 | // 解决过大内容导致SVG窜位问题 376 | this.receiverElement.innerHTML = ''; 377 | }, 0); 378 | 379 | const node = this.minder.getSelectedNode(); 380 | const processedTextNodes = commitInputText(textNodes as any); 381 | 382 | commitInputNode(node, processedTextNodes as any); 383 | 384 | if (node.type === 'root') { 385 | const rootText = this.minder.getRoot().getText(); 386 | this.minder.fire('initChangeRoot', { 387 | text: rootText, 388 | }); 389 | } 390 | }; 391 | 392 | const exitInputMode = (): void => { 393 | this.receiverElement.classList.remove('input'); 394 | this.receiver.selectAll(); 395 | }; 396 | } 397 | 398 | export default InputRuntime; 399 | -------------------------------------------------------------------------------- /src/script/runtime/jumping.ts: -------------------------------------------------------------------------------- 1 | const { HotBox } = window; 2 | 3 | /** 4 | * @Desc: 下方使用receiver.enable()和receiver.disable()通过 5 | * 修改div contenteditable属性的hack来解决开启热核后依然无法屏蔽浏览器输入的bug; 6 | * 特别: win下FF对于此种情况必须要先blur在focus才能解决,但是由于这样做会导致用户 7 | * 输入法状态丢失,因此对FF暂不做处理 8 | * @Editor: Naixor 9 | * @Date: 2015.09.14 10 | */ 11 | interface IReceiver { 12 | element: HTMLElement; 13 | disable(): void; 14 | enable(): void; 15 | listen(type: string, callback: (e: KeyboardEvent) => void): void; 16 | } 17 | 18 | interface IHotbox { 19 | state(): number; 20 | $element: HTMLElement; 21 | active(state: number): void; 22 | dispatch(e: KeyboardEvent): void; 23 | } 24 | 25 | interface IMinder { 26 | getSelectedNode(): any; 27 | } 28 | 29 | interface IFsm { 30 | state(): string; 31 | jump(state: string, event: string, data?: any): void; 32 | } 33 | 34 | interface IJumpingRuntime { 35 | fsm: IFsm; 36 | minder: IMinder; 37 | receiver: IReceiver; 38 | container: HTMLElement; 39 | hotbox: IHotbox; 40 | } 41 | 42 | // Nice: http://unixpapa.com/js/key.html 43 | function isIntendToInput(e: KeyboardEvent): boolean { 44 | if (e.ctrlKey || e.metaKey || e.altKey) return false; 45 | 46 | // a-zA-Z 47 | if (e.keyCode >= 65 && e.keyCode <= 90) return true; 48 | 49 | // 0-9 以及其上面的符号 50 | if (e.keyCode >= 48 && e.keyCode <= 57) return true; 51 | 52 | // 小键盘区域 (除回车外) 53 | if (e.keyCode !== 108 && e.keyCode >= 96 && e.keyCode <= 111) return true; 54 | 55 | // 小键盘区域 (除回车外) 56 | // @yinheli from pull request 57 | if (e.keyCode !== 108 && e.keyCode >= 96 && e.keyCode <= 111) return true; 58 | 59 | // 输入法 60 | if (e.keyCode === 229 || e.keyCode === 0) return true; 61 | 62 | return false; 63 | } 64 | 65 | function JumpingRuntime(this: IJumpingRuntime): void { 66 | const { fsm, minder, receiver, container, hotbox } = this; 67 | const receiverElement = receiver.element; 68 | 69 | // normal -> * 70 | receiver.listen('normal', function (e: KeyboardEvent) { 71 | // 为了防止处理进入edit模式而丢失处理的首字母,此时receiver必须为enable 72 | receiver.enable(); 73 | // normal -> hotbox 74 | if (e.code === 'Space') { 75 | e.preventDefault(); 76 | // safari下Space触发hotbox,然而这时Space已在receiver上留下作案痕迹,因此抹掉 77 | if (window.kity.Browser.safari) { 78 | receiverElement.innerHTML = ''; 79 | } 80 | return fsm.jump('hotbox', 'space-trigger'); 81 | } 82 | 83 | /** 84 | * check 85 | * @editor Naixor 86 | * @Date 2015-12-2 87 | */ 88 | switch (e.type) { 89 | case 'keydown': { 90 | if (minder.getSelectedNode()) { 91 | if (isIntendToInput(e)) { 92 | return fsm.jump('input', 'user-input'); 93 | } 94 | } else { 95 | receiverElement.innerHTML = ''; 96 | } 97 | // normal -> normal shortcut 98 | fsm.jump('normal', 'shortcut-handle', e); 99 | break; 100 | } 101 | case 'keyup': { 102 | break; 103 | } 104 | default: { 105 | } 106 | } 107 | }); 108 | 109 | // hotbox -> normal 110 | receiver.listen('hotbox', function (e: KeyboardEvent) { 111 | receiver.disable(); 112 | e.preventDefault(); 113 | const handleResult = hotbox.dispatch(e); 114 | if (hotbox.state() === HotBox.STATE_IDLE && fsm.state() === 'hotbox') { 115 | return fsm.jump('normal', 'hotbox-idle'); 116 | } 117 | }); 118 | 119 | // input => normal 120 | receiver.listen('input', function (e: KeyboardEvent) { 121 | receiver.enable(); 122 | if (e.type === 'keydown') { 123 | if (e.code === 'Enter') { 124 | e.preventDefault(); 125 | return fsm.jump('normal', 'input-commit'); 126 | } 127 | if (e.code === 'Escape') { 128 | e.preventDefault(); 129 | return fsm.jump('normal', 'input-cancel'); 130 | } 131 | if (e.code === 'Tab' || (e.shiftKey && e.code === 'Tab')) { 132 | e.preventDefault(); 133 | } 134 | } else if (e.type === 'keyup' && e.code === 'Escape') { 135 | e.preventDefault(); 136 | return fsm.jump('normal', 'input-cancel'); 137 | } 138 | }); 139 | 140 | /// /////////////////////////////////////////// 141 | /// 右键呼出热盒 142 | /// 判断的标准是:按下的位置和结束的位置一致 143 | /// /////////////////////////////////////////// 144 | let downX: number; 145 | let downY: number; 146 | const MOUSE_RB = 2; // 右键 147 | 148 | container.addEventListener( 149 | 'mousedown', 150 | function (e: MouseEvent) { 151 | if (e.button == MOUSE_RB) { 152 | e.preventDefault(); 153 | } 154 | if (fsm.state() == 'hotbox') { 155 | hotbox.active(HotBox.STATE_IDLE); 156 | fsm.jump('normal', 'blur'); 157 | } else if (fsm.state() == 'normal' && e.button == MOUSE_RB) { 158 | downX = e.clientX; 159 | downY = e.clientY; 160 | } 161 | }, 162 | false 163 | ); 164 | 165 | container.addEventListener( 166 | 'mousewheel', 167 | function () { 168 | if (fsm.state() == 'hotbox') { 169 | hotbox.active(HotBox.STATE_IDLE); 170 | fsm.jump('normal', 'mousemove-blur'); 171 | } 172 | }, 173 | false 174 | ); 175 | 176 | container.addEventListener('contextmenu', function (e: MouseEvent) { 177 | e.preventDefault(); 178 | }); 179 | 180 | container.addEventListener( 181 | 'mouseup', 182 | function (e) { 183 | if (fsm.state() != 'normal') { 184 | return; 185 | } 186 | if (e.button !== MOUSE_RB || e.clientX !== downX || e.clientY !== downY) { 187 | return; 188 | } 189 | if (!minder.getSelectedNode()) { 190 | return; 191 | } 192 | fsm.jump('hotbox', 'content-menu'); 193 | }, 194 | false 195 | ); 196 | 197 | // 阻止热盒事件冒泡,在热盒正确执行前导致热盒关闭 198 | hotbox.$element.addEventListener('mousedown', function (e) { 199 | e.stopPropagation(); 200 | }); 201 | } 202 | 203 | export default JumpingRuntime; 204 | -------------------------------------------------------------------------------- /src/script/runtime/minder.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * 4 | * 脑图示例运行时 5 | * 6 | * @author: techird 7 | * @copyright: Baidu FEX, 2014 8 | */ 9 | import { useI18n } from '@/hooks/useI18n'; 10 | 11 | const { t } = useI18n(); 12 | 13 | export default function MinderRuntime(this: { selector: string; minder?: any }) { 14 | // 不使用 kityminder 的按键处理,由 ReceiverRuntime 统一处理 15 | const { Minder } = window.kityminder; 16 | const minder = new Minder({ 17 | enableKeyReceiver: false, 18 | enableAnimation: true, 19 | }); 20 | 21 | // 渲染,初始化 22 | minder.renderTo(this.selector); 23 | minder.setTheme(null); 24 | minder.select(minder.getRoot(), true); 25 | minder.execCommand('text', t('minder.main.subject.central')); 26 | 27 | // 导出给其它 Runtime 使用 28 | this.minder = minder; 29 | window.minder = minder; 30 | } 31 | -------------------------------------------------------------------------------- /src/script/runtime/node.ts: -------------------------------------------------------------------------------- 1 | import { isDisableNode, markDeleteNode, isDeleteDisableNode } from '../tool/utils'; 2 | import useLocaleNotVue from '@/script/tool/useLocaleNotVue'; 3 | 4 | const tran = useLocaleNotVue; 5 | 6 | const buttons = [ 7 | `minder.menu.move.forward:Alt+Up:ArrangeUp`, 8 | `minder.menu.insert._down:Tab|Insert:AppendChildNode`, 9 | `minder.menu.insert._same:Enter:AppendSiblingNode`, 10 | `minder.menu.move.backward:Alt+Down:ArrangeDown`, 11 | `minder.commons.delete:Delete|Backspace:RemoveNode`, 12 | `minder.menu.insert._up:Shift+Tab|Shift+Insert:AppendParentNode`, 13 | ]; 14 | 15 | // eslint-disable-next-line no-unused-vars 16 | export default function NodeRuntime(this: { minder: any; hotbox: any; editText: Function }) { 17 | const { minder, hotbox } = this; 18 | // eslint-disable-next-line @typescript-eslint/no-this-alias 19 | const runtime = this; 20 | 21 | const main = hotbox.state('main'); 22 | 23 | let AppendLock = 0; 24 | 25 | buttons.forEach((button: string) => { 26 | const parts = button.split(':'); 27 | const label = parts.shift(); 28 | const key = parts.shift(); 29 | const command = parts.shift() || ''; 30 | main.button({ 31 | position: 'ring', 32 | label: tran(label || ''), 33 | key, 34 | action() { 35 | if (command?.indexOf('Append') === 0) { 36 | AppendLock++; 37 | minder.execCommand(command, tran('minder.main.subject.branch')); 38 | 39 | const afterAppend = () => { 40 | if (!--AppendLock) { 41 | runtime.editText(); 42 | } 43 | minder.off('layoutallfinish', afterAppend); 44 | }; 45 | minder.on('layoutallfinish', afterAppend); 46 | } else { 47 | if (command.indexOf('RemoveNode') > -1) { 48 | if (window.minderProps?.delConfirm) { 49 | // 如果有删除确认,不删除,调用确认方法 50 | window.minderProps.delConfirm(); 51 | return; 52 | } 53 | markDeleteNode(minder); 54 | } 55 | minder.execCommand(command); 56 | } 57 | }, 58 | enable() { 59 | if (command.indexOf('RemoveNode') > -1) { 60 | if ( 61 | isDeleteDisableNode(minder) && 62 | command.indexOf('AppendChildNode') < 0 && 63 | command.indexOf('AppendSiblingNode') < 0 64 | ) { 65 | return false; 66 | } 67 | } else if (command.indexOf('ArrangeUp') > 0 || command.indexOf('ArrangeDown') > 0) { 68 | if (!minder.moveEnable) { 69 | return false; 70 | } 71 | } else if (command.indexOf('AppendChildNode') < 0 && command.indexOf('AppendSiblingNode') < 0) { 72 | if (isDisableNode(minder)) return false; 73 | } 74 | const node = minder.getSelectedNode(); 75 | if (node && node.parent === null && command.indexOf('AppendSiblingNode') > -1) { 76 | return false; 77 | } 78 | return minder.queryCommandState(command) !== -1; 79 | }, 80 | beforeShow() { 81 | this.$button.children[0].innerHTML = tran(label || ''); 82 | }, 83 | }); 84 | }); 85 | 86 | main.button({ 87 | position: 'ring', 88 | key: '/', 89 | action() { 90 | if (!minder.queryCommandState('expand')) { 91 | minder.execCommand('expand'); 92 | } else if (!minder.queryCommandState('collapse')) { 93 | minder.execCommand('collapse'); 94 | } 95 | }, 96 | enable() { 97 | return minder.queryCommandState('expand') !== -1 || minder.queryCommandState('collapse') !== -1; 98 | }, 99 | beforeShow() { 100 | if (!minder.queryCommandState('expand')) { 101 | this.$button.children[0].innerHTML = tran('minder.menu.expand.expand'); 102 | } else { 103 | this.$button.children[0].innerHTML = tran('minder.menu.expand.folding'); 104 | } 105 | }, 106 | }); 107 | } 108 | -------------------------------------------------------------------------------- /src/script/runtime/priority.ts: -------------------------------------------------------------------------------- 1 | import { isDisableNode } from '../tool/utils'; 2 | import useLocaleNotVue from '@/script/tool/useLocaleNotVue'; 3 | 4 | const tran = useLocaleNotVue; 5 | 6 | export default function PriorityRuntime(this: any): void { 7 | const { minder, hotbox } = this; 8 | 9 | const main = hotbox.state('main'); 10 | 11 | main.button({ 12 | position: 'top', 13 | label: tran('minder.main.priority'), 14 | key: 'P', 15 | next: 'priority', 16 | enable() { 17 | if (isDisableNode(minder)) { 18 | return false; 19 | } 20 | return minder.queryCommandState('priority') !== -1; 21 | }, 22 | beforeShow() { 23 | this.$button.children[0].innerHTML = tran('minder.main.priority'); 24 | }, 25 | }); 26 | 27 | const priority = hotbox.state('priority'); 28 | 29 | priority.button({ 30 | position: 'center', 31 | label: tran('minder.commons.remove'), 32 | key: 'Del', 33 | action() { 34 | minder.execCommand('Priority', 0); 35 | }, 36 | beforeShow() { 37 | this.$button.children[0].innerHTML = tran('minder.commons.remove'); 38 | }, 39 | }); 40 | 41 | priority.button({ 42 | position: 'top', 43 | label: tran('minder.commons.return'), 44 | key: 'esc', 45 | beforeShow() { 46 | this.$button.children[0].innerHTML = tran('minder.commons.return'); 47 | }, 48 | next: 'back', 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /src/script/runtime/progress.ts: -------------------------------------------------------------------------------- 1 | import { isDisableNode } from '../tool/utils'; 2 | import { useI18n } from '@/hooks/useI18n'; 3 | 4 | const { t } = useI18n(); 5 | 6 | export default function ProgressRuntime(this: any) { 7 | const { minder, hotbox } = this; 8 | 9 | const main = hotbox.state('main'); 10 | 11 | main.button({ 12 | position: 'top', 13 | label: t('minder.menu.progress.progress'), 14 | key: 'G', 15 | next: 'progress', 16 | enable() { 17 | if (isDisableNode(minder)) { 18 | return false; 19 | } 20 | return minder.queryCommandState('progress') !== -1; 21 | }, 22 | }); 23 | 24 | const progress = hotbox.state('progress'); 25 | '012345678'.replace(/./g, function (p) { 26 | progress.button({ 27 | position: 'ring', 28 | label: `G${p}`, 29 | key: p, 30 | action() { 31 | minder.execCommand('Progress', parseInt(p, 10) + 1); 32 | }, 33 | }); 34 | return p; 35 | }); 36 | 37 | progress.button({ 38 | position: 'center', 39 | label: t('minder.commons.remove'), 40 | key: 'Del', 41 | action() { 42 | minder.execCommand('Progress', 0); 43 | }, 44 | }); 45 | 46 | progress.button({ 47 | position: 'top', 48 | label: t('minder.commons.return'), 49 | key: 'esc', 50 | next: 'back', 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /src/script/runtime/receiver.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-multi-assign */ 2 | import * as key from '../tool/key'; 3 | 4 | // eslint-disable-next-line no-unused-vars 5 | function ReceiverRuntime(this: any) { 6 | // 接收事件的 div 7 | const element = document.createElement('div'); 8 | element.contentEditable = 'true'; 9 | element.setAttribute('tabindex', '-1'); 10 | element.classList.add('receiver'); 11 | 12 | const dispatchKeyEvent = (e: KeyboardEvent) => { 13 | (e as any).is = function (keyExpression: string) { 14 | const subs = keyExpression.split('|'); 15 | for (let i = 0; i < subs.length; i++) { 16 | if (key.is(this, subs[i])) return true; 17 | } 18 | return false; 19 | }; 20 | let listener; 21 | for (let i = 0; i < listeners.length; i++) { 22 | listener = listeners[i]; 23 | // 忽略不在侦听状态的侦听器 24 | if ((listener as any).notifyState !== '*' && (listener as any).notifyState !== this.fsm.state()) { 25 | // eslint-disable-next-line no-continue 26 | continue; 27 | } 28 | 29 | if (listener.call(null, e)) { 30 | return; 31 | } 32 | } 33 | }; 34 | 35 | element.onkeydown = element.onkeypress = element.onkeyup = dispatchKeyEvent; 36 | this.container.appendChild(element); 37 | 38 | interface Receiver { 39 | element: HTMLDivElement; 40 | selectAll(): void; 41 | enable(): void; 42 | disable(): void; 43 | fixFFCaretDisappeared(): void; 44 | // eslint-disable-next-line no-unused-vars 45 | onblur(handler: (event: FocusEvent) => void): void; 46 | // eslint-disable-next-line no-unused-vars 47 | listen?(state: string, listener: (event: KeyboardEvent) => boolean): void; 48 | } 49 | 50 | // receiver 对象 51 | const receiver: Receiver = { 52 | element, 53 | selectAll() { 54 | // 保证有被选中的 55 | if (!element.innerHTML) element.innerHTML = ' '; 56 | const range = document.createRange(); 57 | const selection = window.getSelection(); 58 | range.selectNodeContents(element); 59 | selection?.removeAllRanges(); 60 | selection?.addRange(range); 61 | element.focus(); 62 | }, 63 | enable() { 64 | element.setAttribute('contenteditable', 'true'); 65 | }, 66 | disable() { 67 | element.setAttribute('contenteditable', 'false'); 68 | }, 69 | fixFFCaretDisappeared() { 70 | element.removeAttribute('contenteditable'); 71 | element.setAttribute('contenteditable', 'true'); 72 | element.blur(); 73 | element.focus(); 74 | }, 75 | // eslint-disable-next-line no-unused-vars 76 | onblur(handler: (event: FocusEvent) => void) { 77 | element.onblur = handler; 78 | }, 79 | }; 80 | receiver.selectAll(); 81 | this.minder.on('beforemousedown', receiver.selectAll); 82 | this.minder.on('receiverfocus', receiver.selectAll); 83 | this.minder.on('readonly', () => { 84 | // 屏蔽 minder 的事件接受,删除 receiver 和 hotbox 85 | this.minder.disable(); 86 | window.editor.receiver.element.parentElement.removeChild(window.editor.receiver.element); 87 | window.editor.hotbox.$container.removeChild(window.editor.hotbox.$element); 88 | }); 89 | 90 | // 侦听器,接收到的事件会派发给所有侦听器 91 | // eslint-disable-next-line no-unused-vars 92 | const listeners: ((event: KeyboardEvent) => boolean)[] = []; 93 | 94 | // 侦听指定状态下的事件,如果不传 state,侦听所有状态 95 | // eslint-disable-next-line no-unused-vars 96 | receiver.listen = function (state: string, listener: ((event: KeyboardEvent) => boolean) | string) { 97 | if (arguments.length === 1) { 98 | // eslint-disable-next-line no-param-reassign 99 | listener = state; 100 | // eslint-disable-next-line no-param-reassign 101 | state = '*'; 102 | } 103 | // eslint-disable-next-line no-param-reassign 104 | (listener as any).notifyState = state; 105 | listeners.push(listener as any); 106 | }; 107 | 108 | this.receiver = receiver as any; 109 | } 110 | 111 | export default ReceiverRuntime; 112 | -------------------------------------------------------------------------------- /src/script/runtime/tag.ts: -------------------------------------------------------------------------------- 1 | import { isDisableNode, isTagEnable } from '../tool/utils'; 2 | import useLocaleNotVue from '@/script/tool/useLocaleNotVue'; 3 | 4 | const tran = useLocaleNotVue; 5 | 6 | // eslint-disable-next-line no-unused-vars 7 | export default function TagRuntime(this: any) { 8 | const { minder, hotbox } = this; 9 | const main = hotbox.state('main'); 10 | 11 | main.button({ 12 | position: 'top', 13 | label: tran('minder.main.tag'), 14 | key: 'H', 15 | next: 'tag', 16 | enable() { 17 | if (isDisableNode(minder) && !isTagEnable(minder)) { 18 | return false; 19 | } 20 | return minder.queryCommandState('tag') !== -1; 21 | }, 22 | beforeShow() { 23 | this.$button.children[0].innerHTML = tran('minder.main.tag'); 24 | }, 25 | }); 26 | 27 | const tag = hotbox.state('tag'); 28 | 29 | tag.button({ 30 | position: 'center', 31 | label: tran('minder.commons.remove'), 32 | key: 'Del', 33 | action() { 34 | minder.execCommand('Tag', 0); 35 | }, 36 | beforeShow() { 37 | this.$button.children[0].innerHTML = tran('minder.commons.remove'); 38 | }, 39 | }); 40 | 41 | tag.button({ 42 | position: 'top', 43 | label: tran('minder.commons.return'), 44 | key: 'esc', 45 | beforeShow() { 46 | this.$button.children[0].innerHTML = tran('minder.commons.return'); 47 | }, 48 | next: 'back', 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /src/script/store.ts: -------------------------------------------------------------------------------- 1 | export function setLocalStorage(k: string, v: any) { 2 | window.localStorage.setItem(k, JSON.stringify(v)); 3 | } 4 | 5 | export function getLocalStorage(k: string) { 6 | const v = window.localStorage.getItem(k); 7 | return JSON.parse(v || '"{}"'); 8 | } 9 | 10 | export function rmLocalStorage(k: string) { 11 | window.localStorage.removeItem(k); 12 | } 13 | 14 | export function clearLocalStorage() { 15 | window.localStorage.clear(); 16 | } 17 | -------------------------------------------------------------------------------- /src/script/tool/debug.ts: -------------------------------------------------------------------------------- 1 | import format from './format'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-empty-function 4 | function noop() {} 5 | 6 | function stringHash(str: string): number { 7 | let hash = 0; 8 | for (let i = 0; i < str.length; i++) { 9 | hash += str.charCodeAt(i); 10 | } 11 | return hash; 12 | } 13 | 14 | class Debug { 15 | private flaged: boolean; 16 | 17 | public log: (...args: any) => void; 18 | 19 | constructor(flag: string) { 20 | this.flaged = window.location.search.indexOf(flag) !== -1; 21 | 22 | if (this.flaged) { 23 | const h = stringHash(flag) % 360; 24 | const flagStyle = format( 25 | 'background: hsl({0}, 50%, 80%); ' + 26 | 'color: hsl({0}, 100%, 30%); ' + 27 | 'padding: 2px 3px; ' + 28 | 'margin: 1px 3px 0 0;' + 29 | 'border-radius: 2px;', 30 | h 31 | ); 32 | 33 | const textStyle = 'background: none; color: black;'; 34 | this.log = () => { 35 | console.log(format('%c{0}%c{1}', flag), flagStyle, textStyle); 36 | }; 37 | } else { 38 | this.log = noop; 39 | } 40 | } 41 | } 42 | 43 | export default Debug; 44 | -------------------------------------------------------------------------------- /src/script/tool/format.ts: -------------------------------------------------------------------------------- 1 | function format(template?: string | null, parm?: any, ...args: any[]) { 2 | let tmp = parm; 3 | if (typeof parm !== 'object') { 4 | tmp = [].slice.call(args, 1); 5 | } 6 | return String(template).replace(/\{(\w+)\}/gi, (match, $key) => { 7 | return tmp[$key] || $key; 8 | }); 9 | } 10 | export default format; 11 | -------------------------------------------------------------------------------- /src/script/tool/innertext.ts: -------------------------------------------------------------------------------- 1 | if (!('innerText' in document.createElement('a')) && 'getSelection' in window) { 2 | Object.defineProperty(HTMLElement.prototype, 'innerText', { 3 | get() { 4 | const selection = window.getSelection(); 5 | const ranges = []; 6 | let str; 7 | let i; 8 | if (selection) { 9 | for (i = 0; i < selection.rangeCount; i++) { 10 | ranges[i] = selection.getRangeAt(i); 11 | } 12 | 13 | selection.removeAllRanges(); 14 | selection.selectAllChildren(this); 15 | str = selection.toString(); 16 | selection.removeAllRanges(); 17 | 18 | for (i = 0; i < ranges.length; i++) { 19 | selection.addRange(ranges[i]); 20 | } 21 | 22 | return str; 23 | } 24 | return ''; 25 | }, 26 | 27 | set(text) { 28 | this.innerHTML = (text || '').replace(//g, '>').replace(/\n/g, '
'); 29 | }, 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /src/script/tool/key.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-bitwise */ 2 | import keymap from './keymap'; 3 | 4 | const CTRL_MASK = 0x1000; 5 | const ALT_MASK = 0x2000; 6 | const SHIFT_MASK = 0x4000; 7 | 8 | interface KeyboardEvent { 9 | keyIdentifier?: string; 10 | ctrlKey?: string; 11 | metaKey?: string; 12 | altKey?: string; 13 | shiftKey?: string; 14 | keyCode: number; 15 | } 16 | 17 | function hash(unknown: string | KeyboardEvent): number { 18 | if (typeof unknown === 'string') { 19 | return hashKeyExpression(unknown); 20 | } 21 | return hashKeyEvent(unknown); 22 | } 23 | 24 | function is(a: string | KeyboardEvent, b: string | KeyboardEvent): boolean { 25 | return !!a && !!b && hash(a) === hash(b); 26 | } 27 | 28 | function hashKeyEvent(keyEvent: KeyboardEvent): number { 29 | let hashCode = 0; 30 | if (keyEvent.ctrlKey || keyEvent.metaKey) { 31 | hashCode |= CTRL_MASK; 32 | } 33 | if (keyEvent.altKey) { 34 | hashCode |= ALT_MASK; 35 | } 36 | if (keyEvent.shiftKey) { 37 | hashCode |= SHIFT_MASK; 38 | } 39 | if ([16, 17, 18, 91].indexOf(keyEvent.keyCode) === -1) { 40 | if (keyEvent.keyCode === 229 && keyEvent.keyIdentifier) { 41 | hashCode |= parseInt(keyEvent.keyIdentifier.substr(2), 16); 42 | return hashCode; 43 | } 44 | hashCode |= keyEvent.keyCode; 45 | } 46 | return hashCode; 47 | } 48 | 49 | function hashKeyExpression(keyExpression: string): number { 50 | let hashCode = 0; 51 | keyExpression 52 | .toLowerCase() 53 | .split(/\s*\+\s*/) 54 | .forEach((name) => { 55 | switch (name) { 56 | case 'ctrl': 57 | case 'cmd': 58 | hashCode |= CTRL_MASK; 59 | break; 60 | case 'alt': 61 | hashCode |= ALT_MASK; 62 | break; 63 | case 'shift': 64 | hashCode |= SHIFT_MASK; 65 | break; 66 | default: 67 | hashCode |= keymap[name]; 68 | } 69 | }); 70 | return hashCode; 71 | } 72 | 73 | export { hash, is }; 74 | -------------------------------------------------------------------------------- /src/script/tool/keymap.ts: -------------------------------------------------------------------------------- 1 | const keymap: Record = { 2 | Shift: 16, 3 | Control: 17, 4 | Alt: 18, 5 | CapsLock: 20, 6 | 7 | BackSpace: 8, 8 | Tab: 9, 9 | Enter: 13, 10 | Esc: 27, 11 | Space: 32, 12 | 13 | PageUp: 33, 14 | PageDown: 34, 15 | End: 35, 16 | Home: 36, 17 | 18 | Insert: 45, 19 | 20 | Left: 37, 21 | Up: 38, 22 | Right: 39, 23 | Down: 40, 24 | 25 | Direction: { 26 | 37: 1, 27 | 38: 1, 28 | 39: 1, 29 | 40: 1, 30 | }, 31 | 32 | Del: 46, 33 | 34 | NumLock: 144, 35 | 36 | Cmd: 91, 37 | CmdFF: 224, 38 | F1: 112, 39 | F2: 113, 40 | F3: 114, 41 | F4: 115, 42 | F5: 116, 43 | F6: 117, 44 | F7: 118, 45 | F8: 119, 46 | F9: 120, 47 | F10: 121, 48 | F11: 122, 49 | F12: 123, 50 | 51 | '`': 192, 52 | '=': 187, 53 | '-': 189, 54 | 55 | '/': 191, 56 | '.': 190, 57 | }; 58 | 59 | Object.keys(keymap).forEach((key) => { 60 | if (Object.prototype.hasOwnProperty.call(keymap, key)) { 61 | keymap[key.toLowerCase()] = keymap[key]; 62 | } 63 | }); 64 | 65 | const aKeyCode = 65; 66 | const aCharCode = 'a'.charCodeAt(0); 67 | 68 | 'abcdefghijklmnopqrstuvwxyz'.split('').forEach((letter) => { 69 | keymap[letter] = aKeyCode + (letter.charCodeAt(0) - aCharCode); 70 | }); 71 | 72 | let n = 9; 73 | do { 74 | keymap[n.toString()] = n + 48; 75 | } while (--n); 76 | 77 | export default keymap; 78 | -------------------------------------------------------------------------------- /src/script/tool/useLocaleNotVue.ts: -------------------------------------------------------------------------------- 1 | import type { Recordable } from '#/locale'; 2 | import zhCN from '@/locale/lang/zh-CN'; 3 | import enUS from '@/locale/lang/en-US'; 4 | 5 | const findCode = (arr: string[], tree: Recordable) => { 6 | let curCode = arr.shift(); 7 | let curNode = tree[curCode || '']; 8 | while (curCode) { 9 | if (!curCode || !curNode) { 10 | return ''; 11 | } 12 | if (curNode && arr.length === 0) { 13 | return curNode; 14 | } 15 | curCode = arr.shift(); 16 | 17 | curNode = curNode[curCode || '']; 18 | } 19 | }; 20 | 21 | export default function useLocaleNotVue(key: string) { 22 | const locale = localStorage.getItem('minder-locale') || 'zh-CN'; 23 | 24 | const arr = key.split('.'); 25 | switch (locale) { 26 | case 'zh-CN': 27 | // eslint-disable-next-line no-eval 28 | return findCode(arr, zhCN.message); 29 | case 'en-US': 30 | return findCode(arr, enUS.message); 31 | default: 32 | return key; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/script/tool/utils.ts: -------------------------------------------------------------------------------- 1 | export function isDisableNode(minder: any) { 2 | let node; 3 | if (minder && minder.getSelectedNode) { 4 | node = minder.getSelectedNode(); 5 | } 6 | if (node && node.data.disable === true) { 7 | return true; 8 | } 9 | return false; 10 | } 11 | 12 | export function isDeleteDisableNode(minder: any) { 13 | let node; 14 | if (minder && minder.getSelectedNode) { 15 | node = minder.getSelectedNode(); 16 | } 17 | if (node && node.data.disable === true && !node.data.allowDelete) { 18 | return true; 19 | } 20 | return false; 21 | } 22 | 23 | export function isTagEnable(minder: any) { 24 | let node; 25 | if (minder && minder.getSelectedNode) { 26 | node = minder.getSelectedNode(); 27 | } 28 | if (isTagEnableNode(node)) { 29 | return true; 30 | } 31 | return false; 32 | } 33 | 34 | export function isTagEnableNode(node: any) { 35 | if (node && (node.data.tagEnable === true || node.data.allowDisabledTag === true)) { 36 | return true; 37 | } 38 | return false; 39 | } 40 | 41 | export function markChangeNode(node: any) { 42 | if (node && node.data) { 43 | // 修改的该节点标记为 contextChanged 44 | node.data.contextChanged = true; 45 | while (node) { 46 | // 该路径上的节点都标记为 changed 47 | node.data.changed = true; 48 | node = node.parent; 49 | } 50 | } 51 | } 52 | 53 | // 在父节点记录删除的节点 54 | export function markDeleteNode(minder: any) { 55 | if (minder) { 56 | const nodes = minder.getSelectedNodes(); 57 | nodes.forEach((node: any) => { 58 | if (node && node.parent) { 59 | const pData = node.parent.data; 60 | if (!pData.deleteChild) { 61 | pData.deleteChild = []; 62 | } 63 | _markDeleteNode(node, pData.deleteChild); 64 | } 65 | }); 66 | } 67 | } 68 | 69 | function _markDeleteNode(node: any, deleteChild: any) { 70 | deleteChild.push(node.data); 71 | if (node.children) { 72 | node.children.forEach((child: any) => { 73 | _markDeleteNode(child, deleteChild); 74 | }); 75 | } 76 | } 77 | 78 | export function isPriority(e: any) { 79 | if (e.getAttribute('text-rendering') === 'geometricPrecision' && e.getAttribute('text-anchor') === 'middle') { 80 | return true; 81 | } 82 | return false; 83 | } 84 | 85 | export function setPriorityView(priorityStartWithZero: boolean, priorityPrefix: string) { 86 | // 手动将优先级前面加上P显示 87 | const items = document.getElementsByTagName('text'); 88 | if (items) { 89 | for (let i = 0; i < items.length; i++) { 90 | const item = items[i]; 91 | if (isPriority(item)) { 92 | let content = item.innerHTML; 93 | if (content.indexOf(priorityPrefix) < 0) { 94 | if (priorityStartWithZero) { 95 | content = `${parseInt(content) - 1}`; 96 | } 97 | item.innerHTML = priorityPrefix + content; 98 | } 99 | } 100 | } 101 | } 102 | } 103 | 104 | /** 105 | * 将节点及其子节点id置为null,changed 标记为true 106 | * @param node 107 | */ 108 | export function resetNodes(nodes: any) { 109 | if (nodes) { 110 | nodes.forEach((item: any) => { 111 | if (item.data) { 112 | item.data.id = null; 113 | item.data.contextChanged = true; 114 | item.data.changed = true; 115 | resetNodes(item.children); 116 | } 117 | }); 118 | } 119 | } 120 | 121 | export function isDisableForNode(node: any) { 122 | if (node && node.data.disable === true) { 123 | return true; 124 | } 125 | return false; 126 | } 127 | -------------------------------------------------------------------------------- /src/style/dropdown-list.scss: -------------------------------------------------------------------------------- 1 | .link-dropdown-list, 2 | .img-dropdown-list, 3 | .remark-dropdown-list, 4 | .selection-dropdown-list, 5 | .expand-dropdown-list { 6 | font-size: 12px; 7 | } 8 | 9 | .mold-dropdown-list { 10 | width: 126px; 11 | height: 170px; 12 | font-size: 12px; 13 | .dropdown-item { 14 | display: inline-block; 15 | width: 50px; 16 | height: 40px; 17 | padding: 0; 18 | margin: 5px; 19 | } 20 | @for $i from 1 through 6 { 21 | .mold-#{$i} { 22 | background-position: (1 - $i) * 50px 0; 23 | } 24 | } 25 | } 26 | 27 | .theme-dropdown-list { 28 | width: 120px; 29 | font-size: 12px; 30 | .mold-icons { 31 | background-repeat: no-repeat; 32 | } 33 | .dropdown-item { 34 | display: inline-block; 35 | width: 100px; 36 | height: 30px; 37 | padding: 0; 38 | margin: 5px; 39 | } 40 | } 41 | 42 | .expand-dropdown-list { 43 | .dropdown-item { 44 | line-height: 25px; 45 | } 46 | } 47 | 48 | .selection-dropdown-list { 49 | .dropdown-item { 50 | line-height: 25px; 51 | } 52 | } 53 | 54 | .theme-group { 55 | //background-color: pink; 56 | padding: 0 10px; 57 | } 58 | -------------------------------------------------------------------------------- /src/style/editor.scss: -------------------------------------------------------------------------------- 1 | @import "@7polo/kityminder-core/dist/kityminder.core.css"; 2 | @import "navigator.scss"; 3 | @import "hotbox.scss"; 4 | 5 | .km-editor { 6 | overflow: hidden; 7 | z-index: 2; 8 | } 9 | 10 | .km-editor > .mask { 11 | display: block; 12 | position: absolute; 13 | left: 0; 14 | right: 0; 15 | top: 0; 16 | bottom: 0; 17 | background-color: transparent; 18 | } 19 | 20 | .km-editor > .receiver { 21 | position: absolute; 22 | background: white; 23 | outline: none; 24 | box-shadow: 0 0 20px; 25 | left: 0; 26 | top: 0; 27 | padding: 3px 5px; 28 | margin-left: -3px; 29 | margin-top: -5px; 30 | max-width: 300px; 31 | width: auto; 32 | overflow: hidden; 33 | font-size: 14px; 34 | line-height: 1.4em; 35 | min-height: 1.4em; 36 | box-sizing: border-box; 37 | word-break: break-all; 38 | word-wrap: break-word; 39 | border: none; 40 | user-select: text; 41 | pointer-events: none; 42 | opacity: 0; 43 | z-index: -1000; 44 | 45 | &.debug { 46 | opacity: 1; 47 | outline: 1px solid green; 48 | background: none; 49 | z-index: 0; 50 | } 51 | 52 | &.input { 53 | pointer-events: all; 54 | opacity: 1; 55 | z-index: 999; 56 | background: white; 57 | outline: none; 58 | } 59 | } 60 | 61 | div.minder-editor-container { 62 | position: absolute; 63 | top: 40px; 64 | bottom: 0; 65 | left: 0; 66 | right: 0; 67 | font-family: Arial, "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif; 68 | } 69 | 70 | .minder-editor { 71 | position: absolute; 72 | top: 92px; 73 | left: 0; 74 | right: 0; 75 | bottom: 0; 76 | } 77 | 78 | .minder-viewer { 79 | position: absolute; 80 | top: 0; 81 | left: 0; 82 | right: 0; 83 | bottom: 0; 84 | } 85 | 86 | .control-panel { 87 | position: absolute; 88 | top: 0; 89 | right: 0; 90 | width: 250px; 91 | bottom: 0; 92 | border-left: 1px solid #ccc; 93 | } 94 | 95 | .minder-divider { 96 | position: absolute; 97 | top: 0; 98 | right: 250px; 99 | bottom: 0; 100 | width: 2px; 101 | background-color: rgb(251, 251, 251); 102 | cursor: ew-resize; 103 | } 104 | 105 | .hotbox .state .button.enabled.selected .key, 106 | .hotbox .state .ring .key { 107 | margin-top: 5px; 108 | font-size: 13px; 109 | } 110 | 111 | .hotbox .state .bottom .button .label, 112 | .hotbox .state .top .button .label { 113 | font-weight: 600; 114 | } 115 | 116 | .hotbox .exp .ring .button .label { 117 | margin-top: 28px; 118 | margin-left: -2px; 119 | } 120 | 121 | .hotbox .exp .ring .button .key { 122 | display: none; 123 | } 124 | -------------------------------------------------------------------------------- /src/style/header.scss: -------------------------------------------------------------------------------- 1 | @import "mixin.scss"; 2 | @import "dropdown-list.scss"; 3 | 4 | .mind-tab-panel { 5 | width: 100%; 6 | height: 100%; 7 | .menu-container { 8 | display: flex; 9 | & > div { 10 | display: inline-flex; 11 | overflow: hidden; 12 | align-items: center; 13 | flex-wrap: wrap; 14 | border-right: 1px dashed #eee; 15 | } 16 | & > div:last-of-type { 17 | border-right: none; 18 | } 19 | } 20 | .menu-btn { 21 | display: inline-flex; 22 | cursor: pointer; 23 | @include flexcenter; 24 | } 25 | .menu-btn:not([disabled=true]):hover { 26 | background-color: $btn-hover-color; 27 | } 28 | .tab-icons { 29 | display: inline-block; 30 | width: 20px; 31 | height: 20px; 32 | } 33 | .do-group { 34 | width: 40px; 35 | height: 100%; 36 | padding: 0 5px; 37 | p { 38 | height: 50%; 39 | margin: 0; 40 | @include flexcenter; 41 | } 42 | .undo i { 43 | background-position: 0 -1240px; 44 | } 45 | .redo i { 46 | background-position: 0 -1220px; 47 | } 48 | } 49 | .insert-group { 50 | width: 110px; 51 | & > div { 52 | margin: 0 5px; 53 | } 54 | .insert-sibling-box { 55 | i { 56 | background-position: 0 -20px; 57 | } 58 | } 59 | .insert-parent-box { 60 | i { 61 | background-position: 0 -40px; 62 | } 63 | } 64 | } 65 | .edit-del-group, 66 | .move-group { 67 | width: 70px; 68 | @include flexcenter; 69 | } 70 | .move-group { 71 | .move-up { 72 | i { 73 | background-position: 0 -280px; 74 | } 75 | } 76 | .move-down { 77 | i { 78 | background-position: 0 -300px; 79 | } 80 | } 81 | } 82 | .edit-del-group { 83 | .edit { 84 | i { 85 | background-position: 0 -60px; 86 | } 87 | } 88 | .del { 89 | i { 90 | background-position: 0 -80px; 91 | } 92 | } 93 | } 94 | .attachment-group { 95 | width: 185px; 96 | @include flexcenter; 97 | button { 98 | font-size: inherit; 99 | width: 45px; 100 | height: 20px; 101 | padding: 0; 102 | background-repeat: no-repeat; 103 | background-position: right; 104 | @include button; 105 | @include flexcenter; 106 | span { 107 | margin-left: 15px; 108 | } 109 | } 110 | button:hover { 111 | background-color: $btn-hover-color; 112 | } 113 | & > div { 114 | font-size: inherit; 115 | flex-wrap: wrap; 116 | width: 60px; 117 | height: 100%; 118 | @include flexcenter; 119 | } 120 | .insert { 121 | height: 25px; 122 | background-repeat: no-repeat; 123 | } 124 | .link { 125 | .insert { 126 | background-position: 50% -100px; 127 | } 128 | } 129 | .img { 130 | .insert { 131 | background-position: 50% -125px; 132 | } 133 | } 134 | .remark { 135 | .insert { 136 | background-position: 50% -1150px; 137 | } 138 | } 139 | } 140 | .progress-group, 141 | .sequence-group { 142 | width: 135px; 143 | @include flexcenter; 144 | ul { 145 | width: 120px; 146 | margin: 0; 147 | padding: 0; 148 | list-style: none; 149 | li { 150 | display: inline-block; 151 | width: 20px; 152 | height: 20px; 153 | margin: 2px; 154 | } 155 | } 156 | } 157 | .sequence-group { 158 | @for $i from 0 through 9 { 159 | .sequence-#{$i} { 160 | background-position: 0 -20px * (-1 + $i); 161 | } 162 | } 163 | } 164 | .progress-group { 165 | @for $i from 0 through 9 { 166 | .progress-#{$i} { 167 | background-position: 0 -20px * (-1 + $i); 168 | } 169 | } 170 | } 171 | .arrange-group { 172 | width: 65px; 173 | .arrange { 174 | flex-wrap: wrap; 175 | @include flexcenter; 176 | } 177 | .tab-icons { 178 | display: inline-block; 179 | width: 25px; 180 | height: 25px; 181 | margin: 0; 182 | background-repeat: no-repeat; 183 | background-position: 0 -150px; 184 | } 185 | } 186 | .style-group { 187 | width: 150px; 188 | .clear-style-btn { 189 | flex-wrap: wrap; 190 | width: 65px; 191 | @include flexcenter; 192 | .tab-icons { 193 | display: inline-block; 194 | width: 25px; 195 | height: 25px; 196 | margin: 0; 197 | background-repeat: no-repeat; 198 | background-position: 0 -175px; 199 | } 200 | } 201 | .copy-paste-panel { 202 | width: 70px; 203 | .tab-icons { 204 | display: inline-block; 205 | width: 20px; 206 | height: 20px; 207 | } 208 | .copy-style { 209 | .tab-icons { 210 | background-position: 0 -200px; 211 | } 212 | } 213 | .paste-style { 214 | .tab-icons { 215 | background-position: 0 -220px; 216 | } 217 | } 218 | } 219 | } 220 | .font-group { 221 | width: 250px; 222 | * { 223 | font-size: 12px; 224 | } 225 | .font-family-select { 226 | width: 150px; 227 | } 228 | .font-size-select { 229 | width: 80px; 230 | margin-left: 5px; 231 | } 232 | .font-bold, 233 | .font-italic { 234 | display: inline-block; 235 | width: 20px; 236 | height: 20px; 237 | margin: 0 3px; 238 | } 239 | .font-bold { 240 | background-position: 0 -242px; 241 | } 242 | .font-italic { 243 | background-position: 0 -262px; 244 | } 245 | } 246 | .expand-group, 247 | .selection-group { 248 | width: 60px; 249 | button { 250 | border: none; 251 | outline: none; 252 | } 253 | @include flexcenter; 254 | margin: 0 5px; 255 | span { 256 | font-size: 12px; 257 | } 258 | } 259 | .expand-group { 260 | .expand { 261 | width: 40px; 262 | height: 25px; 263 | background-position: center -995px; 264 | } 265 | i { 266 | font-size: 12px; 267 | } 268 | } 269 | .selection-group { 270 | .selection { 271 | width: 40px; 272 | height: 25px; 273 | background-position: 7px -1175px; 274 | } 275 | i { 276 | font-size: 12px; 277 | } 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/style/hotbox.scss: -------------------------------------------------------------------------------- 1 | .hotbox { 2 | font-family: Arial, "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif; 3 | position: absolute; 4 | left: 0; 5 | top: 0; 6 | overflow: visible; 7 | 8 | .state { 9 | position: absolute; 10 | overflow: visible; 11 | display: none; 12 | 13 | .center, 14 | .ring { 15 | .button { 16 | position: absolute; 17 | width: 70px; 18 | height: 70px; 19 | margin-left: -35px; 20 | margin-top: -35px; 21 | border-radius: 100%; 22 | box-shadow: 0 0 30px rgba(0, 0, 0, 0.3); 23 | } 24 | 25 | .label, 26 | .key { 27 | display: block; 28 | text-align: center; 29 | line-height: 1.4em; 30 | } 31 | 32 | .label { 33 | font-size: 16px; 34 | margin-top: 17px; 35 | color: black; 36 | font-weight: normal; 37 | line-height: 1em; 38 | } 39 | 40 | .key { 41 | font-size: 12px; 42 | color: #999; 43 | } 44 | } 45 | 46 | .ring-shape { 47 | position: absolute; 48 | left: -25px; 49 | top: -25px; 50 | border: 25px solid rgba(0, 0, 0, 0.3); 51 | border-radius: 100%; 52 | box-sizing: content-box; 53 | } 54 | 55 | .top, 56 | .bottom { 57 | position: absolute; 58 | white-space: nowrap; 59 | 60 | .button { 61 | display: inline-block; 62 | padding: 8px 15px; 63 | margin: 0 10px; 64 | border-radius: 15px; 65 | box-shadow: 0 0 30px rgba(0, 0, 0, 0.3); 66 | position: relative; 67 | 68 | .label { 69 | font-size: 14px; 70 | line-height: 14px; 71 | vertical-align: middle; 72 | color: black; 73 | } 74 | 75 | .key { 76 | font-size: 12px; 77 | line-height: 12px; 78 | vertical-align: middle; 79 | color: #999; 80 | margin-left: 3px; 81 | 82 | &:before { 83 | content: "("; 84 | } 85 | 86 | &:after { 87 | content: ")"; 88 | } 89 | } 90 | } 91 | } 92 | 93 | .button { 94 | background: #f9f9f9; 95 | overflow: hidden; 96 | cursor: default; 97 | 98 | .key, 99 | .label { 100 | opacity: 0.3; 101 | } 102 | } 103 | 104 | .button.enabled { 105 | background: white; 106 | 107 | .key, 108 | .label { 109 | opacity: 1; 110 | } 111 | 112 | &:hover { 113 | background: lighten(rgb(228, 93, 92), 5%); 114 | 115 | .label { 116 | color: white; 117 | } 118 | 119 | .key { 120 | color: lighten(rgb(228, 93, 92), 30%); 121 | } 122 | } 123 | 124 | &.selected { 125 | animation: selected 0.1s ease; 126 | background: rgb(228, 93, 92); 127 | 128 | .label { 129 | color: white; 130 | } 131 | 132 | .key { 133 | color: lighten(rgb(228, 93, 92), 30%); 134 | } 135 | } 136 | 137 | &.pressed, 138 | &:active { 139 | background: #ff974d; 140 | 141 | .label { 142 | color: white; 143 | } 144 | 145 | .key { 146 | color: lighten(#ff974d, 30%); 147 | } 148 | } 149 | } 150 | } 151 | 152 | .state.active { 153 | display: block; 154 | } 155 | } 156 | 157 | @keyframes selected { 158 | 0% { 159 | transform: scale(1); 160 | } 161 | 162 | 50% { 163 | transform: scale(1.1); 164 | } 165 | 166 | 100% { 167 | transform: scale(1); 168 | } 169 | } 170 | 171 | .hotbox-key-receiver { 172 | position: absolute; 173 | left: -999999px; 174 | top: -999999px; 175 | width: 20px; 176 | height: 20px; 177 | outline: none; 178 | margin: 0; 179 | } 180 | -------------------------------------------------------------------------------- /src/style/mixin.scss: -------------------------------------------------------------------------------- 1 | $btn-hover-color: #eee; 2 | *[disabled=true] { 3 | opacity: 0.5; 4 | } 5 | 6 | @mixin block { 7 | width: 100%; 8 | height: 100%; 9 | } 10 | 11 | @mixin button { 12 | background: transparent; 13 | border: none; 14 | outline: none; 15 | } 16 | 17 | @mixin flexcenter { 18 | display: flex; 19 | justify-content: center; 20 | align-items: center; 21 | } 22 | -------------------------------------------------------------------------------- /src/style/navigator.scss: -------------------------------------------------------------------------------- 1 | .nav-bar { 2 | position: absolute; 3 | width: 35px; 4 | height: 200px; 5 | padding: 5px 0; 6 | left: 10px; 7 | bottom: 10px; 8 | background: #fc8383; 9 | color: #fff; 10 | border-radius: 4px; 11 | z-index: 10; 12 | box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.2); 13 | transition: -webkit-transform 0.7s 0.1s ease; 14 | transition: transform 0.7s 0.1s ease; 15 | 16 | .nav-btn { 17 | width: 35px; 18 | height: 24px; 19 | line-height: 24px; 20 | text-align: center; 21 | 22 | .icon { 23 | width: 20px; 24 | height: 20px; 25 | margin: 2px auto; 26 | display: block; 27 | } 28 | 29 | &.active { 30 | background-color: #5a6378; 31 | } 32 | } 33 | 34 | .zoom-in .icon { 35 | background-position: 0 -730px; 36 | } 37 | 38 | .zoom-out .icon { 39 | background-position: 0 -750px; 40 | } 41 | 42 | .hand .icon { 43 | background-position: 0 -770px; 44 | width: 25px; 45 | height: 25px; 46 | margin: 0 auto; 47 | } 48 | 49 | .camera .icon { 50 | background-position: 0 -870px; 51 | width: 25px; 52 | height: 25px; 53 | margin: 0 auto; 54 | } 55 | 56 | .nav-trigger .icon { 57 | background-position: 0 -845px; 58 | width: 25px; 59 | height: 25px; 60 | margin: 0 auto; 61 | } 62 | 63 | .zoom-pan { 64 | width: 2px; 65 | height: 70px; 66 | box-shadow: 0 1px #e50000; 67 | position: relative; 68 | background: white; 69 | margin: 3px auto; 70 | overflow: visible; 71 | 72 | .origin { 73 | position: absolute; 74 | width: 20px; 75 | height: 8px; 76 | left: -9px; 77 | margin-top: -4px; 78 | background: transparent; 79 | 80 | &:after { 81 | content: " "; 82 | display: block; 83 | width: 6px; 84 | height: 2px; 85 | background: white; 86 | left: 7px; 87 | top: 3px; 88 | position: absolute; 89 | } 90 | } 91 | 92 | .indicator { 93 | position: absolute; 94 | width: 8px; 95 | height: 8px; 96 | left: -3px; 97 | background: white; 98 | border-radius: 100%; 99 | margin-top: -4px; 100 | } 101 | } 102 | } 103 | 104 | .nav-previewer { 105 | background: #fff; 106 | width: 140px; 107 | height: 120px; 108 | position: absolute; 109 | left: 45px; 110 | bottom: 30px; 111 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.2); 112 | border-radius: 0 2px 2px 0; 113 | padding: 1px; 114 | z-index: 9; 115 | cursor: crosshair; 116 | transition: -webkit-transform 0.7s 0.1s ease; 117 | transition: transform 0.7s 0.1s ease; 118 | 119 | &.grab { 120 | cursor: move; 121 | cursor: -webkit-grabbing; 122 | cursor: -moz-grabbing; 123 | cursor: grabbing; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/style/normalize.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: sans-serif; 3 | line-height: 1.15; 4 | } 5 | 6 | body { 7 | margin: 0; 8 | } 9 | 10 | article, aside, footer, header, nav, section { 11 | display: block; 12 | } 13 | 14 | h1 { 15 | font-size: 2em; 16 | margin: .67em 0; 17 | } 18 | 19 | figcaption, figure, main { 20 | display: block; 21 | } 22 | 23 | figure { 24 | margin: 1em 40px; 25 | } 26 | 27 | hr { 28 | overflow: visible; 29 | box-sizing: content-box; 30 | height: 0; 31 | } 32 | 33 | pre { 34 | font-family: monospace; 35 | font-size: 1em; 36 | } 37 | 38 | a { 39 | background-color: transparent; 40 | text-decoration-skip: objects; 41 | } 42 | 43 | a:active, a:hover { 44 | outline-width: 0; 45 | } 46 | 47 | abbr[title] { 48 | text-decoration: underline; 49 | text-decoration: underline dotted; 50 | border-bottom: none; 51 | } 52 | 53 | b, strong { 54 | font-weight: bolder; 55 | } 56 | 57 | code, kbd, samp { 58 | font-family: monospace; 59 | font-size: 1em; 60 | } 61 | 62 | dfn { 63 | font-style: italic; 64 | } 65 | 66 | mark { 67 | color: #000; 68 | background-color: #ff0; 69 | } 70 | 71 | small { 72 | font-size: 80%; 73 | } 74 | 75 | sub, sup { 76 | font-size: 75%; 77 | line-height: 0; 78 | position: relative; 79 | vertical-align: baseline; 80 | } 81 | 82 | sub { 83 | bottom: -.25em; 84 | } 85 | 86 | sup { 87 | top: -.5em; 88 | } 89 | 90 | audio, video { 91 | display: inline-block; 92 | } 93 | 94 | audio:not([controls]) { 95 | display: none; 96 | height: 0; 97 | } 98 | 99 | img { 100 | border-style: none; 101 | } 102 | 103 | svg:not(:root) { 104 | overflow: hidden; 105 | } 106 | 107 | button, input, optgroup, select, textarea { 108 | font-family: sans-serif; 109 | font-size: 100%; 110 | line-height: 1.15; 111 | margin: 0; 112 | } 113 | 114 | button, input { 115 | overflow: visible; 116 | } 117 | 118 | button, select { 119 | text-transform: none; 120 | } 121 | 122 | button, html [type='button'], [type='reset'], [type='submit'] { 123 | appearance: button; 124 | } 125 | 126 | [type='button']::-moz-focus-inner, [type='reset']::-moz-focus-inner, [type='submit']::-moz-focus-inner, button::-moz-focus-inner { 127 | padding: 0; 128 | border-style: none; 129 | } 130 | 131 | [type='button']:-moz-focusring, [type='reset']:-moz-focusring, [type='submit']:-moz-focusring, button:-moz-focusring { 132 | outline: 1px dotted ButtonText; 133 | } 134 | 135 | fieldset { 136 | margin: 0 2px; 137 | padding: .35em .625em .75em; 138 | border: 1px solid #c0c0c0; 139 | } 140 | 141 | legend { 142 | display: table; 143 | box-sizing: border-box; 144 | max-width: 100%; 145 | padding: 0; 146 | white-space: normal; 147 | color: inherit; 148 | } 149 | 150 | progress { 151 | display: inline-block; 152 | vertical-align: baseline; 153 | } 154 | 155 | textarea { 156 | overflow: auto; 157 | } 158 | 159 | [type='checkbox'], [type='radio'] { 160 | box-sizing: border-box; 161 | padding: 0; 162 | } 163 | 164 | [type='number']::-webkit-inner-spin-button, [type='number']::-webkit-outer-spin-button { 165 | height: auto; 166 | } 167 | 168 | [type='search'] { 169 | outline-offset: -2px; 170 | appearance: textfield; 171 | } 172 | 173 | [type='search']::-webkit-search-cancel-button, [type='search']::-webkit-search-decoration { 174 | appearance: none; 175 | } 176 | 177 | ::-webkit-file-upload-button { 178 | font: inherit; 179 | appearance: button; 180 | } 181 | 182 | details, menu { 183 | display: block; 184 | } 185 | 186 | summary { 187 | display: list-item; 188 | } 189 | 190 | canvas { 191 | display: inline-block; 192 | } 193 | 194 | template { 195 | display: none; 196 | } 197 | 198 | [hidden] { 199 | display: none; 200 | } 201 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/**/*.ts", 4 | "src/**/*.d.ts", 5 | "src/**/*.tsx", 6 | "src/**/*.vue", 7 | "types/**/*.d.ts", 8 | "types/**/*.ts", 9 | "global.d.ts" 10 | ], // TS解析路径配置 11 | "exclude": [ 12 | "node_modules", 13 | ], 14 | "compilerOptions": { 15 | "allowJs": true, // 允许编译器编译JS,JSX文件 16 | "noEmit": true, 17 | "target": "esnext", // 使用es最新语法 18 | "useDefineForClassFields": true, 19 | "allowSyntheticDefaultImports": true, // 允许异步导入模块,配合自动导入插件使用 20 | "module": "esnext", // 使用ES模块语法 21 | "moduleResolution": "node", 22 | "strict": true, // 严格模式 23 | "jsx": "preserve", 24 | "sourceMap": true, // 代码映射 25 | "resolveJsonModule": true, 26 | "isolatedModules": true, 27 | "esModuleInterop": true, 28 | "lib": [ 29 | "esnext", 30 | "dom" 31 | ], 32 | "skipLibCheck": true, // 跳过node依赖包语法检查 33 | "types": [ 34 | // "vitest/globals", 35 | // "vite-plugin-svg-icons/client" 36 | ], // 手动导入TS类型声明文件 37 | "baseUrl": ".", 38 | "paths": { // 路径映射 39 | "@/*": [ 40 | "./src/*" 41 | ], 42 | "#/*": [ 43 | "types/*" 44 | ] 45 | } 46 | }, 47 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.node.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18/tsconfig.json", 3 | "include": [ 4 | "vite.config.*", 5 | "vitest.config.*", 6 | "cypress.config.*", 7 | "playwright.config.*" 8 | ], 9 | "compilerOptions": { 10 | "module": "ESNext", 11 | "composite": true, 12 | "types": [ 13 | "node", 14 | "element-plus/global", 15 | ] 16 | } 17 | } -------------------------------------------------------------------------------- /types/locale.d.ts: -------------------------------------------------------------------------------- 1 | declare type Recordable = Record; 2 | 3 | export type LocaleType = 'zh-CN' | 'en-US'; 4 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url'; 2 | 3 | import { defineConfig } from 'vite'; 4 | import vue from '@vitejs/plugin-vue'; 5 | import AutoImport from 'unplugin-auto-import/vite'; 6 | import Components from 'unplugin-vue-components/vite'; 7 | import { ArcoResolver } from 'unplugin-vue-components/resolvers'; 8 | import { vitePluginForArco } from '@arco-plugins/vite-vue'; 9 | 10 | import replace from '@rollup/plugin-replace'; 11 | import { visualizer } from 'rollup-plugin-visualizer'; 12 | 13 | export default () => 14 | defineConfig({ 15 | resolve: { 16 | alias: { 17 | '@': fileURLToPath(new URL('./src', import.meta.url)), 18 | }, 19 | }, 20 | plugins: [ 21 | vue(), 22 | AutoImport({ 23 | // Auto import functions from Vue, e.g. ref, reactive, toRef... 24 | // 自动导入 Vue 相关函数,如:ref, reactive, toRef 等 25 | imports: ['vue'], 26 | }), 27 | vitePluginForArco({}), 28 | Components({ 29 | resolvers: [ArcoResolver()], 30 | }), 31 | 32 | replace({ 33 | preventAssignment: true, 34 | // 将__VUE_I18N_FULL_INSTALL__替换为true 35 | __VUE_I18N_FULL_INSTALL__: true, 36 | // 将__VUE_I18N_LEGACY_API__替换为false 37 | __VUE_I18N_LEGACY_API__: false, 38 | // 将__VUE_I18N_PROD_DEVTOOLS__替换为false 39 | __VUE_I18N_PROD_DEVTOOLS__: false, 40 | }), 41 | // 这里通过--mode analyze配置为分析模式,使用visualizer插件分析方法,输出report.html分析报告 42 | process.env.NODE_ENV === 'analyze' ? visualizer({ open: true, brotliSize: true, filename: 'report.html' }) : null, 43 | ], 44 | optimizeDeps: { 45 | include: ['vue', '@vueuse/core'], 46 | }, 47 | build: { 48 | lib: { 49 | entry: 'src/main.ts', 50 | name: 'vue3-minder-editor', 51 | formats: ['es', 'umd', 'cjs'], 52 | fileName: (format) => `vue3-minder-editor.${format}.js`, 53 | }, 54 | rollupOptions: { 55 | external: ['vue', /@arco-design/], 56 | }, 57 | }, 58 | }); 59 | --------------------------------------------------------------------------------