├── .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 |
26 |
36 |
37 |
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 |
2 |
3 |
4 |
5 |
6 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
53 |
62 |
102 |
--------------------------------------------------------------------------------
/src/components/main/mainEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{
4 | t('minder.main.main.save')
5 | }}
6 |
7 |
8 |
9 |
10 |
130 |
131 |
143 |
--------------------------------------------------------------------------------
/src/components/main/navigator.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
22 |
30 |
33 |
36 |
44 |
45 |
46 |
47 |
48 |
49 |
311 |
316 |
--------------------------------------------------------------------------------
/src/components/menu/edit/editDel.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
15 |
16 |
17 |
18 |
84 |
--------------------------------------------------------------------------------
/src/components/menu/edit/editMenu.vue:
--------------------------------------------------------------------------------
1 |
2 |
24 |
25 |
26 |
39 |
--------------------------------------------------------------------------------
/src/components/menu/edit/expand.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ t('minder.menu.expand.expand') }}
7 |
8 |
9 |
10 | {{ t('minder.menu.expand.expand_one') }}
11 | {{ t('minder.menu.expand.expand_tow') }}
12 | {{ t('minder.menu.expand.expand_three') }}
13 | {{ t('minder.menu.expand.expand_four') }}
14 | {{ t('minder.menu.expand.expand_five') }}
15 | {{ t('minder.menu.expand.expand_six') }}
16 |
17 |
18 |
19 |
20 |
21 |
34 |
39 |
--------------------------------------------------------------------------------
/src/components/menu/edit/insertBox.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
15 |
23 |
24 |
25 |
26 |
66 |
--------------------------------------------------------------------------------
/src/components/menu/edit/moveBox.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
11 |
12 |
13 |
14 |
61 |
--------------------------------------------------------------------------------
/src/components/menu/edit/progressBox.vue:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
90 |
95 |
--------------------------------------------------------------------------------
/src/components/menu/edit/selection.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ t('minder.menu.selection.all') }}
7 |
8 |
9 |
10 | {{ t('minder.menu.selection.invert') }}
11 | {{ t('minder.menu.selection.sibling') }}
12 | {{ t('minder.menu.selection.same') }}
13 | {{ t('minder.menu.selection.path') }}
14 | {{ t('minder.menu.selection.subtree') }}
15 |
16 |
17 |
18 |
19 |
20 |
119 |
124 |
--------------------------------------------------------------------------------
/src/components/menu/edit/sequenceBox.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
17 | {{ priorityPrefix }}{{ priorityStartWithZero ? pIndex - 1 : pIndex }}
18 |
19 |
20 |
21 |
22 |
23 |
83 |
84 |
170 |
--------------------------------------------------------------------------------
/src/components/menu/edit/tagBox.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
81 |
82 |
103 |
--------------------------------------------------------------------------------
/src/components/menu/view/arrange.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
34 |
--------------------------------------------------------------------------------
/src/components/menu/view/fontOperation.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
18 |
19 |
27 |
39 |
40 |
41 |
47 |
53 |
54 |
55 |
56 |
57 |
258 |
259 |
268 |
--------------------------------------------------------------------------------
/src/components/menu/view/mold.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
56 |
57 |
67 |
68 |
103 |
--------------------------------------------------------------------------------
/src/components/menu/view/styleOperation.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
16 |
22 |
23 |
24 |
25 |
26 |
71 |
78 |
--------------------------------------------------------------------------------
/src/components/menu/view/viewMenu.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
27 |
28 |
38 |
--------------------------------------------------------------------------------
/src/components/minderEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
20 |
38 |
39 |
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 |
2 |
13 |
14 |
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 = '';
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 |
--------------------------------------------------------------------------------