├── config ├── prod.env.js ├── test.env.js ├── dev.env.js └── index.js ├── src ├── script │ ├── lang.js │ ├── hotbox.js │ ├── minder.js │ ├── expose-editor.js │ ├── tool │ │ ├── format.js │ │ ├── innertext.js │ │ ├── debug.js │ │ ├── keymap.js │ │ ├── key.js │ │ └── utils.js │ ├── store.js │ ├── protocol │ │ ├── json.js │ │ ├── svg.js │ │ ├── xmind.js │ │ ├── freemind.js │ │ ├── plain.js │ │ ├── markdown.js │ │ └── png.js │ ├── runtime │ │ ├── container.js │ │ ├── minder.js │ │ ├── tag.js │ │ ├── priority.js │ │ ├── hotbox.js │ │ ├── progress.js │ │ ├── exports.js │ │ ├── clipboard-mimetype.js │ │ ├── receiver.js │ │ ├── fsm.js │ │ ├── node.js │ │ ├── drag.js │ │ ├── jumping.js │ │ ├── history.js │ │ └── clipboard.js │ └── editor.js ├── assets │ └── minder │ │ ├── mold.png │ │ ├── icons.png │ │ ├── iconpriority.png │ │ └── iconprogress.png ├── components │ ├── main │ │ ├── footer.vue │ │ ├── header.vue │ │ ├── mainEditor.vue │ │ └── navigator.vue │ ├── menu │ │ ├── view │ │ │ ├── arrange.vue │ │ │ ├── viewMenu.vue │ │ │ ├── theme.vue │ │ │ ├── styleOperation.vue │ │ │ ├── mold.vue │ │ │ └── fontOperation.vue │ │ └── edit │ │ │ ├── editMenu.vue │ │ │ ├── expand.vue │ │ │ ├── insertBox.vue │ │ │ ├── moveBox.vue │ │ │ ├── editDel.vue │ │ │ ├── progressBox.vue │ │ │ ├── tagBox.vue │ │ │ ├── attachment.vue │ │ │ ├── selection.vue │ │ │ └── sequenceBox.vue │ └── minderEditor.vue ├── mixins │ └── locale.js ├── mixins.js ├── style │ ├── mixin.scss │ ├── dropdown-list.scss │ ├── editor.scss │ ├── navigator.scss │ ├── normalize.css │ ├── hotbox.scss │ └── header.scss ├── index.js ├── locale │ ├── format.js │ ├── index.js │ └── lang │ │ ├── zh-CN.js │ │ ├── zh-TW.js │ │ └── en-US.js └── props.js ├── .postcssrc.js ├── demo ├── test │ ├── i18n │ │ ├── zh-CN.js │ │ ├── zh-TW.js │ │ ├── en-US.js │ │ └── index.js │ ├── ms-test.vue │ ├── test-plugin.vue │ └── dev-test.vue ├── App.vue └── main.js ├── .gitignore ├── .editorconfig ├── index.html ├── .babelrc ├── LICENSE ├── package.json └── README.md /config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"' 3 | } 4 | -------------------------------------------------------------------------------- /src/script/lang.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | 3 | }); -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "plugins": { 3 | "autoprefixer": {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /demo/test/i18n/zh-CN.js: -------------------------------------------------------------------------------- 1 | export default { 2 | en_US: '英语', 3 | zh_CN: '中文简体', 4 | zh_TW: '中文繁体' 5 | } 6 | -------------------------------------------------------------------------------- /demo/test/i18n/zh-TW.js: -------------------------------------------------------------------------------- 1 | export default { 2 | en_US: '英語', 3 | zh_CN: '中文簡體', 4 | zh_TW: '中文繁體' 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/minder/mold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgAngle/vue-minder-editor-plus/HEAD/src/assets/minder/mold.png -------------------------------------------------------------------------------- /src/script/hotbox.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | return module.exports = window.HotBox; 3 | }); -------------------------------------------------------------------------------- /src/assets/minder/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgAngle/vue-minder-editor-plus/HEAD/src/assets/minder/icons.png -------------------------------------------------------------------------------- /src/assets/minder/iconpriority.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgAngle/vue-minder-editor-plus/HEAD/src/assets/minder/iconpriority.png -------------------------------------------------------------------------------- /src/assets/minder/iconprogress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgAngle/vue-minder-editor-plus/HEAD/src/assets/minder/iconprogress.png -------------------------------------------------------------------------------- /src/script/minder.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | return module.exports = window.kityminder.Minder; 3 | }); 4 | -------------------------------------------------------------------------------- /demo/test/i18n/en-US.js: -------------------------------------------------------------------------------- 1 | export default { 2 | en_US: 'English', 3 | zh_CN: 'Chinese simplified', 4 | zh_TW: 'Chinese traditional' 5 | } 6 | -------------------------------------------------------------------------------- /src/script/expose-editor.js: -------------------------------------------------------------------------------- 1 | define('expose-editor', function(require, exports, module) { 2 | return module.exports = kityminder.Editor = require('./editor'); 3 | }); 4 | -------------------------------------------------------------------------------- /config/test.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var devEnv = require('./dev.env') 3 | 4 | module.exports = merge(devEnv, { 5 | NODE_ENV: '"testing"' 6 | }) 7 | -------------------------------------------------------------------------------- /src/components/main/footer.vue: -------------------------------------------------------------------------------- 1 | 4 | 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log 4 | test/unit/coverage 5 | test/e2e/reports 6 | selenium-debug.log 7 | package-lock.json 8 | /.idea/ 9 | dist 10 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var prodEnv = require('./prod.env') 3 | 4 | module.exports = merge(prodEnv, { 5 | NODE_ENV: '"development"' 6 | }) 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /src/mixins/locale.js: -------------------------------------------------------------------------------- 1 | import { t } from '/src/locale'; 2 | 3 | export default { 4 | methods: { 5 | t(...args) { 6 | return t.apply(this, args); 7 | } 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/script/tool/format.js: -------------------------------------------------------------------------------- 1 | function format(template, args) { 2 | if (typeof(args) != 'object') { 3 | args = [].slice.call(arguments, 1); 4 | } 5 | return String(template).replace(/\{(\w+)\}/ig, function (match, $key) { 6 | return args[$key] || $key; 7 | }); 8 | } 9 | export {format} 10 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vue-Minder-Editor 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/mixins.js: -------------------------------------------------------------------------------- 1 | import Locale from "@/mixins/locale"; 2 | 3 | export default { 4 | ...Locale, 5 | computed: { 6 | operatorLabel() { 7 | for (let operator of this.operators) { 8 | if (operator.value === this.operator) { 9 | return this.t(operator.label) 10 | } 11 | } 12 | return this.operator 13 | } 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/style/mixin.scss: -------------------------------------------------------------------------------- 1 | $btn-hover-color: #eee; 2 | *[disabled] { 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/script/store.js: -------------------------------------------------------------------------------- 1 | export function setLocalStorage(k, v) { 2 | window.localStorage.setItem(k, JSON.stringify(v)); 3 | } 4 | 5 | export function getLocalStorage(k) { 6 | let v = window.localStorage.getItem(k); 7 | return JSON.parse(v); 8 | } 9 | 10 | export function rmLocalStorage(k) { 11 | window.localStorage.removeItem(k); 12 | } 13 | 14 | export function clearLocalStorage() { 15 | window.localStorage.clear(); 16 | } 17 | -------------------------------------------------------------------------------- /demo/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 24 | 25 | 30 | -------------------------------------------------------------------------------- /src/script/protocol/json.js: -------------------------------------------------------------------------------- 1 | function exportJson(minder) { 2 | var 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 { 19 | exportJson 20 | } 21 | -------------------------------------------------------------------------------- /src/script/runtime/container.js: -------------------------------------------------------------------------------- 1 | define(function (require, exports, module) { 2 | function ContainerRuntime() { 3 | var container; 4 | 5 | if (typeof (this.selector) == 'string') { 6 | container = document.querySelector(this.selector); 7 | } else { 8 | container = this.selector; 9 | } 10 | 11 | if (!container) throw new Error('Invalid selector: ' + this.selector); 12 | 13 | // 这个类名用于给编辑器添加样式 14 | container.classList.add('km-editor'); 15 | 16 | // 暴露容器给其他运行时使用 17 | this.container = container; 18 | } 19 | 20 | return module.exports = ContainerRuntime; 21 | }); 22 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "modules": false, 7 | "targets": { 8 | "browsers": [ 9 | "> 1%", 10 | "last 2 versions", 11 | "not ie <= 8" 12 | ] 13 | } 14 | } 15 | ], 16 | "stage-2", 17 | [ 18 | "es2015", 19 | { 20 | "modules": false 21 | } 22 | ] 23 | ], 24 | "plugins": [ 25 | "transform-vue-jsx", 26 | "syntax-dynamic-import" 27 | ], 28 | "env": { 29 | "test": { 30 | "presets": [ 31 | "env", 32 | "stage-2" 33 | ], 34 | "plugins": [ 35 | "istanbul" 36 | ] 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import mindEditor from './components/minderEditor' 2 | import * as locale from "./locale"; 3 | import PackageJSON from "../package.json" 4 | require('@7polo/kity/dist/kity.js'); 5 | require('hotbox-minder/hotbox.js'); 6 | require('@7polo/kityminder-core'); 7 | require('./script/expose-editor.js'); 8 | 9 | 10 | const install = function (Vue, options = {}) { 11 | locale.use(options.locale); 12 | locale.i18n(options.i18n); 13 | Vue.component(mindEditor.name, mindEditor); 14 | } 15 | 16 | const plugin = { 17 | name: "vueMinderEditorPlus", 18 | version: PackageJSON.version, 19 | locale: locale.use, 20 | i18n: locale.i18n, 21 | install, 22 | } 23 | 24 | if (typeof window !== 'undefined' && window.Vue) { 25 | window.Vue.use(plugin); 26 | } 27 | 28 | export default plugin; 29 | -------------------------------------------------------------------------------- /src/script/runtime/minder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * 4 | * 脑图示例运行时 5 | * 6 | * @author: techird 7 | * @copyright: Baidu FEX, 2014 8 | */ 9 | define(function (require, exports, module) { 10 | var Minder = require('../minder'); 11 | var {t} = require("../../locale"); 12 | 13 | function MinderRuntime() { 14 | 15 | // 不使用 kityminder 的按键处理,由 ReceiverRuntime 统一处理 16 | var 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 | } 30 | 31 | return module.exports = MinderRuntime; 32 | }); 33 | -------------------------------------------------------------------------------- /demo/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from './App'; 3 | import 'element-ui/lib/theme-chalk/index.css'; 4 | import ElementUI from 'element-ui'; 5 | import vueMinderEditorPlus from "../src/index" 6 | Vue.config.productionTip = true; 7 | 8 | // 方式一 9 | // import locale from '/src/locale/lang/en-US' 10 | // Vue.use(vueMinderEditorPlus, { 11 | // locale 12 | // }); 13 | 14 | // 方式二 15 | // import lang from '/src/locale/lang/en-US' 16 | // import locale from '/src/locale' 17 | // // 设置语言 18 | // locale.use(lang) 19 | // Vue.use(vueMinderEditorPlus); 20 | 21 | // 方式三 22 | import i18n from "./test/i18n/index"; 23 | Vue.use(vueMinderEditorPlus, { 24 | i18n: (key, value) => i18n.t(key, value) 25 | }); 26 | 27 | Vue.use(ElementUI, { 28 | i18n: (key, value) => i18n.t(key, value) 29 | }); 30 | 31 | new Vue({ 32 | el: '#app', 33 | template: '', 34 | components: { 35 | App 36 | }, 37 | i18n 38 | }) 39 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | 3 | module.exports = { 4 | build: { 5 | env: require('./prod.env'), 6 | index: path.resolve(__dirname, '../dist/index.html'), 7 | assetsRoot: path.resolve(__dirname, '../dist'), 8 | assetsSubDirectory: 'static', 9 | assetsPublicPath: '/', 10 | productionSourceMap: true, 11 | productionGzip: true, 12 | productionGzipExtensions: ['js', 'css'], 13 | bundleAnalyzerReport: process.env.npm_config_report, 14 | devtool: "eval" 15 | //devtool: 'cheap-module-eval-source-map' 16 | }, 17 | dev: { 18 | env: require('./dev.env'), 19 | port: 8088, 20 | autoOpenBrowser: true, 21 | assetsSubDirectory: 'static', 22 | assetsPublicPath: '/', 23 | proxyTable: {}, 24 | cssSourceMap: true, 25 | cacheBusting: true, 26 | devtool: 'cheap-module-eval-source-map', 27 | poll: false, 28 | errorOverlay: true, 29 | notifyOnErrors: true 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /src/script/tool/innertext.js: -------------------------------------------------------------------------------- 1 | define(function (require, exports, module) { 2 | if ((!('innerText' in document.createElement('a'))) && ('getSelection' in window)) { 3 | HTMLElement.prototype.__defineGetter__('innerText', function () { 4 | var selection = window.getSelection(), 5 | ranges = [], 6 | str, i; 7 | 8 | for (i = 0; i < selection.rangeCount; i++) { 9 | ranges[i] = selection.getRangeAt(i); 10 | } 11 | 12 | selection.removeAllRanges(); 13 | selection.selectAllChildren(this); 14 | str = selection.toString(); 15 | selection.removeAllRanges(); 16 | for (i = 0; i < ranges.length; i++) { 17 | selection.addRange(ranges[i]); 18 | } 19 | return str; 20 | }); 21 | HTMLElement.prototype.__defineSetter__('innerText', function (text) { 22 | this.innerHTML = (text || '').replace(//g, '>').replace(/\n/g, '
'); 23 | }); 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /src/components/menu/view/arrange.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 36 | -------------------------------------------------------------------------------- /src/script/tool/debug.js: -------------------------------------------------------------------------------- 1 | define(function (require, exports, module) { 2 | var format = require('./format'); 3 | 4 | function noop() {} 5 | 6 | function stringHash(str) { 7 | var hash = 0; 8 | for (var i = 0; i < str.length; i++) { 9 | hash += str.charCodeAt(i); 10 | } 11 | return hash; 12 | } 13 | 14 | function Debug(flag) { 15 | var debugMode = this.flaged = window.location.search.indexOf(flag) != -1; 16 | 17 | if (debugMode) { 18 | var h = stringHash(flag) % 360; 19 | 20 | var flagStyle = format( 21 | 'background: hsl({0}, 50%, 80%); ' + 22 | 'color: hsl({0}, 100%, 30%); ' + 23 | 'padding: 2px 3px; ' + 24 | 'margin: 1px 3px 0 0;' + 25 | 'border-radius: 2px;', h); 26 | 27 | var textStyle = 'background: none; color: black;'; 28 | this.log = function () { 29 | var output = format.apply(null, arguments); 30 | console.log(format('%c{0}%c{1}', flag, output), flagStyle, textStyle); 31 | }; 32 | } else { 33 | this.log = noop; 34 | } 35 | } 36 | 37 | return module.exports = Debug; 38 | }); 39 | -------------------------------------------------------------------------------- /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/locale/format.js: -------------------------------------------------------------------------------- 1 | import { hasOwn } from 'element-ui/src/utils/util'; 2 | 3 | const RE_NARGS = /(%|)\{([0-9a-zA-Z_]+)\}/g; 4 | /** 5 | * String format template 6 | * - Inspired: 7 | * https://github.com/Matt-Esch/string-template/index.js 8 | */ 9 | export default function(Vue) { 10 | 11 | /** 12 | * template 13 | * 14 | * @param {String} string 15 | * @param {Array} ...args 16 | * @return {String} 17 | */ 18 | 19 | function template(string, ...args) { 20 | if (args.length === 1 && typeof args[0] === 'object') { 21 | args = args[0]; 22 | } 23 | 24 | if (!args || !args.hasOwnProperty) { 25 | args = {}; 26 | } 27 | 28 | return string.replace(RE_NARGS, (match, prefix, i, index) => { 29 | let result; 30 | 31 | if (string[index - 1] === '{' && 32 | string[index + match.length] === '}') { 33 | return i; 34 | } else { 35 | result = hasOwn(args, i) ? args[i] : null; 36 | if (result === null || result === undefined) { 37 | return ''; 38 | } 39 | 40 | return result; 41 | } 42 | }); 43 | } 44 | 45 | return template; 46 | } 47 | -------------------------------------------------------------------------------- /src/script/runtime/tag.js: -------------------------------------------------------------------------------- 1 | 2 | define(function (require, exports, module) { 3 | 4 | function TagRuntime() { 5 | var minder = this.minder; 6 | var hotbox = this.hotbox; 7 | var {isDisableNode, isTagEnable} = require('../tool/utils'); 8 | var {t} = require("../../locale"); 9 | var main = hotbox.state('main'); 10 | 11 | main.button({ 12 | position: 'top', 13 | label: t('minder.main.tag'), 14 | key: 'H', 15 | next: 'tag', 16 | enable: function () { 17 | if (isDisableNode(minder) && !isTagEnable(minder)) { 18 | return false; 19 | } 20 | return minder.queryCommandState('tag') != -1; 21 | } 22 | }); 23 | 24 | let tag = hotbox.state('tag'); 25 | 26 | tag.button({ 27 | position: 'center', 28 | label: t('minder.commons.remove'), 29 | key: 'Del', 30 | action: function () { 31 | minder.execCommand('Tag', 0); 32 | } 33 | }); 34 | 35 | tag.button({ 36 | position: 'top', 37 | label: t('minder.commons.return'), 38 | key: 'esc', 39 | next: 'back' 40 | }); 41 | } 42 | 43 | return module.exports = TagRuntime; 44 | 45 | }); 46 | -------------------------------------------------------------------------------- /src/script/runtime/priority.js: -------------------------------------------------------------------------------- 1 | define(function (require, exports, module) { 2 | 3 | function PriorityRuntime() { 4 | var minder = this.minder; 5 | var hotbox = this.hotbox; 6 | var {isDisableNode} = require('../tool/utils'); 7 | var {t} = require("../../locale"); 8 | 9 | var main = hotbox.state('main'); 10 | 11 | main.button({ 12 | position: 'top', 13 | label: t('minder.main.priority'), 14 | key: 'P', 15 | next: 'priority', 16 | enable: function () { 17 | if (isDisableNode(minder)) { 18 | return false; 19 | } 20 | return minder.queryCommandState('priority') != -1; 21 | } 22 | }); 23 | 24 | let priority = hotbox.state('priority') 25 | 26 | priority.button({ 27 | position: 'center', 28 | label: t('minder.commons.remove'), 29 | key: 'Del', 30 | action: function () { 31 | minder.execCommand('Priority', 0); 32 | } 33 | }); 34 | 35 | priority.button({ 36 | position: 'top', 37 | label: t('minder.commons.return'), 38 | key: 'esc', 39 | next: 'back' 40 | }); 41 | 42 | } 43 | return module.exports = PriorityRuntime; 44 | }); 45 | -------------------------------------------------------------------------------- /src/components/menu/view/viewMenu.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 44 | 45 | 52 | -------------------------------------------------------------------------------- /src/locale/index.js: -------------------------------------------------------------------------------- 1 | import defaultLang from './lang/zh-CN'; 2 | import Vue from 'vue'; 3 | import deepmerge from 'deepmerge'; 4 | import Format from './format'; 5 | 6 | 7 | const format = Format(Vue); 8 | let lang = defaultLang; 9 | let merged = false; 10 | 11 | let i18nHandler = function() { 12 | const vuei18n = Object.getPrototypeOf(this || Vue).$t; 13 | if (typeof vuei18n === 'function' && !!Vue.locale) { 14 | if (!merged) { 15 | merged = true; 16 | Vue.locale( 17 | Vue.config.lang, 18 | deepmerge(lang, Vue.locale(Vue.config.lang) || {}, { clone: true }) 19 | ); 20 | } 21 | return vuei18n.apply(this, arguments); 22 | } 23 | }; 24 | 25 | export const t = function(path, options) { 26 | let value = i18nHandler.apply(this, arguments); 27 | if (value !== null && value !== undefined) return value; 28 | 29 | const array = path.split('.'); 30 | let current = lang; 31 | 32 | for (let i = 0, j = array.length; i < j; i++) { 33 | const property = array[i]; 34 | value = current[property]; 35 | if (i === j - 1) return format(value, options); 36 | if (!value) return ''; 37 | current = value; 38 | } 39 | return ''; 40 | }; 41 | 42 | export const use = function(l) { 43 | lang = l || lang; 44 | }; 45 | 46 | export const i18n = function(fn) { 47 | i18nHandler = fn || i18nHandler; 48 | }; 49 | 50 | export default { use, t, i18n}; 51 | -------------------------------------------------------------------------------- /src/script/runtime/hotbox.js: -------------------------------------------------------------------------------- 1 | define(function (require, exports, module) { 2 | var Hotbox = require('../hotbox'); 3 | 4 | function HotboxRuntime() { 5 | var fsm = this.fsm; 6 | var minder = this.minder; 7 | var receiver = this.receiver; 8 | var container = this.container; 9 | 10 | var hotbox = new Hotbox(container); 11 | 12 | hotbox.setParentFSM(fsm); 13 | 14 | fsm.when('normal -> hotbox', function (exit, enter, reason) { 15 | var node = minder.getSelectedNode(); 16 | var position; 17 | if (node) { 18 | var box = node.getRenderBox(); 19 | position = { 20 | x: box.cx, 21 | y: box.cy 22 | }; 23 | } 24 | hotbox.active('main', position); 25 | }); 26 | 27 | fsm.when('normal -> normal', function (exit, enter, reason, e) { 28 | if (reason == 'shortcut-handle') { 29 | var handleResult = hotbox.dispatch(e); 30 | if (handleResult) { 31 | e.preventDefault(); 32 | } else { 33 | minder.dispatchKeyEvent(e); 34 | } 35 | } 36 | }); 37 | 38 | fsm.when('modal -> normal', function (exit, enter, reason, e) { 39 | if (reason == 'import-text-finish') { 40 | receiver.element.focus(); 41 | } 42 | }); 43 | 44 | this.hotbox = hotbox; 45 | minder.hotbox = hotbox; 46 | } 47 | 48 | return module.exports = HotboxRuntime; 49 | }); 50 | -------------------------------------------------------------------------------- /src/script/runtime/progress.js: -------------------------------------------------------------------------------- 1 | define(function (require, exports, module) { 2 | 3 | function ProgressRuntime() { 4 | var minder = this.minder; 5 | var hotbox = this.hotbox; 6 | var {isDisableNode} = require('../tool/utils'); 7 | var {t} = require("../../locale"); 8 | 9 | var 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: function () { 17 | if (isDisableNode(minder)) { 18 | return false; 19 | } 20 | return minder.queryCommandState('progress') != -1; 21 | } 22 | }); 23 | 24 | var 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: function () { 31 | minder.execCommand('Progress', parseInt(p) + 1); 32 | } 33 | }); 34 | }); 35 | 36 | progress.button({ 37 | position: 'center', 38 | label: t('minder.commons.remove'), 39 | key: 'Del', 40 | action: function () { 41 | minder.execCommand('Progress', 0); 42 | } 43 | }); 44 | 45 | progress.button({ 46 | position: 'top', 47 | label: t('minder.commons.return'), 48 | key: 'esc', 49 | next: 'back' 50 | }); 51 | } 52 | 53 | return module.exports = ProgressRuntime; 54 | 55 | }); 56 | -------------------------------------------------------------------------------- /src/script/tool/keymap.js: -------------------------------------------------------------------------------- 1 | define(function (require, exports, module) { 2 | const keymap = { 3 | 4 | 'Shift': 16, 5 | 'Control': 17, 6 | 'Alt': 18, 7 | 'CapsLock': 20, 8 | 9 | 'BackSpace': 8, 10 | 'Tab': 9, 11 | 'Enter': 13, 12 | 'Esc': 27, 13 | 'Space': 32, 14 | 15 | 'PageUp': 33, 16 | 'PageDown': 34, 17 | 'End': 35, 18 | 'Home': 36, 19 | 20 | 'Insert': 45, 21 | 22 | 'Left': 37, 23 | 'Up': 38, 24 | 'Right': 39, 25 | 'Down': 40, 26 | 27 | 'Direction': { 28 | 37: 1, 29 | 38: 1, 30 | 39: 1, 31 | 40: 1 32 | }, 33 | 34 | 'Del': 46, 35 | 36 | 'NumLock': 144, 37 | 38 | 'Cmd': 91, 39 | 'CmdFF': 224, 40 | 'F1': 112, 41 | 'F2': 113, 42 | 'F3': 114, 43 | 'F4': 115, 44 | 'F5': 116, 45 | 'F6': 117, 46 | 'F7': 118, 47 | 'F8': 119, 48 | 'F9': 120, 49 | 'F10': 121, 50 | 'F11': 122, 51 | 'F12': 123, 52 | 53 | '`': 192, 54 | '=': 187, 55 | '-': 189, 56 | 57 | '/': 191, 58 | '.': 190 59 | }; 60 | 61 | for (var key in keymap) { 62 | if (keymap.hasOwnProperty(key)) { 63 | keymap[key.toLowerCase()] = keymap[key]; 64 | } 65 | } 66 | var aKeyCode = 65; 67 | var aCharCode = 'a'.charCodeAt(0); 68 | 69 | 'abcdefghijklmnopqrstuvwxyz'.split('').forEach(function (letter) { 70 | keymap[letter] = aKeyCode + (letter.charCodeAt(0) - aCharCode); 71 | }); 72 | 73 | var n = 9; 74 | do { 75 | keymap[n.toString()] = n + 48; 76 | } while (--n); 77 | 78 | module.exports = keymap; 79 | }); 80 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, 刘毅 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /src/script/tool/key.js: -------------------------------------------------------------------------------- 1 | define(function (require, exports, module) { 2 | var keymap = require('./keymap'); 3 | 4 | var CTRL_MASK = 0x1000; 5 | var ALT_MASK = 0x2000; 6 | var SHIFT_MASK = 0x4000; 7 | 8 | function hash(unknown) { 9 | if (typeof (unknown) == 'string') { 10 | return hashKeyExpression(unknown); 11 | } 12 | return hashKeyEvent(unknown); 13 | } 14 | 15 | function is(a, b) { 16 | return a && b && hash(a) == hash(b); 17 | } 18 | exports.hash = hash; 19 | exports.is = is; 20 | 21 | function hashKeyEvent(keyEvent) { 22 | var hashCode = 0; 23 | if (keyEvent.ctrlKey || keyEvent.metaKey) { 24 | hashCode |= CTRL_MASK; 25 | } 26 | if (keyEvent.altKey) { 27 | hashCode |= ALT_MASK; 28 | } 29 | if (keyEvent.shiftKey) { 30 | hashCode |= SHIFT_MASK; 31 | } 32 | if ([16, 17, 18, 91].indexOf(keyEvent.keyCode) === -1) { 33 | if (keyEvent.keyCode === 229 && keyEvent.keyIdentifier) { 34 | return hashCode |= parseInt(keyEvent.keyIdentifier.substr(2), 16); 35 | } 36 | hashCode |= keyEvent.keyCode; 37 | } 38 | return hashCode; 39 | } 40 | 41 | function hashKeyExpression(keyExpression) { 42 | var hashCode = 0; 43 | keyExpression.toLowerCase().split(/\s*\+\s*/).forEach(function (name) { 44 | switch (name) { 45 | case 'ctrl': 46 | case 'cmd': 47 | hashCode |= CTRL_MASK; 48 | break; 49 | case 'alt': 50 | hashCode |= ALT_MASK; 51 | break; 52 | case 'shift': 53 | hashCode |= SHIFT_MASK; 54 | break; 55 | default: 56 | hashCode |= keymap[name]; 57 | } 58 | }); 59 | return hashCode; 60 | } 61 | }); 62 | -------------------------------------------------------------------------------- /src/components/menu/edit/editMenu.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 61 | -------------------------------------------------------------------------------- /src/script/editor.js: -------------------------------------------------------------------------------- 1 | define(function (require, exports, module) { 2 | var runtimes = []; 3 | 4 | function assemble(runtime) { 5 | runtimes.push(runtime); 6 | } 7 | 8 | function KMEditor(selector, editMenuProps) { 9 | this.selector = selector; 10 | for (var i = 0; i < runtimes.length; i++) { 11 | if (typeof runtimes[i] == 'function' && isEnable(editMenuProps, runtimes[i])) { 12 | runtimes[i].call(this, this); 13 | } 14 | } 15 | } 16 | 17 | function isEnable(editMenuProps, runtime) { 18 | switch (runtime.name) { 19 | case "PriorityRuntime": 20 | return editMenuProps.sequenceEnable != true ? false : true; 21 | case "TagRuntime": 22 | return editMenuProps.tagEnable != true ? false : true; 23 | case "ProgressRuntime": 24 | return editMenuProps.progressEnable != true ? false : true; 25 | default: 26 | return true 27 | } 28 | } 29 | 30 | KMEditor.assemble = assemble; 31 | 32 | assemble(require('./runtime/container')); 33 | assemble(require('./runtime/fsm')); 34 | assemble(require('./runtime/minder')); 35 | assemble(require('./runtime/receiver')); 36 | assemble(require('./runtime/hotbox')); 37 | assemble(require('./runtime/input')); 38 | assemble(require('./runtime/clipboard-mimetype')); 39 | assemble(require('./runtime/clipboard')); 40 | assemble(require('./runtime/drag')); 41 | assemble(require('./runtime/node')); 42 | assemble(require('./runtime/history')); 43 | assemble(require('./runtime/jumping')); 44 | assemble(require('./runtime/priority')); 45 | assemble(require('./runtime/progress')); 46 | assemble(require('./runtime/exports')); 47 | assemble(require('./runtime/tag')); 48 | 49 | return module.exports = KMEditor; 50 | }); 51 | -------------------------------------------------------------------------------- /src/components/menu/view/theme.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 55 | -------------------------------------------------------------------------------- /demo/test/i18n/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueI18n from "vue-i18n"; 3 | import enLocale from "element-ui/lib/locale/lang/en"; 4 | import zh_CNLocale from "element-ui/lib/locale/lang/zh-CN"; 5 | import zh_TWLocale from "element-ui/lib/locale/lang/zh-TW"; 6 | import zh_CN from "./zh-CN"; 7 | import en_US from "./en-US"; 8 | import zh_TW from "./zh-TW"; 9 | 10 | import minder_zh_CN from "../../../src/locale/lang/zh-CN"; 11 | import minder_en_US from "../../../src/locale/lang/en-US"; 12 | import minder_zh_TW from "../../../src/locale/lang/zh-TW"; 13 | 14 | export const CURRENT_LANGUAGE = 'current_language'; 15 | 16 | Vue.use(VueI18n); 17 | 18 | const messages = { 19 | 'en_US': { 20 | ...enLocale, 21 | ...en_US, 22 | ...minder_en_US 23 | }, 24 | 'zh_CN': { 25 | ...zh_CNLocale, 26 | ...zh_CN, 27 | ...minder_zh_CN 28 | 29 | }, 30 | 'zh_TW': { 31 | ...zh_TWLocale, 32 | ...zh_TW, 33 | ...minder_zh_TW 34 | 35 | } 36 | }; 37 | 38 | const index = new VueI18n({ 39 | locale: 'zh_CN', 40 | messages, 41 | silentTranslationWarn: true 42 | }); 43 | 44 | const loadedLanguages = ['en_US', 'zh_CN', 'zh_TW']; 45 | 46 | function setI18nLanguage(lang) { 47 | index.locale = lang; 48 | document.querySelector('html').setAttribute('lang', lang); 49 | localStorage.setItem(CURRENT_LANGUAGE, lang); 50 | return lang; 51 | } 52 | 53 | Vue.prototype.$setLang = function (lang) { 54 | if (index.locale !== lang) { 55 | if (!loadedLanguages.includes(lang)) { 56 | let file = lang.replace("_", "-"); 57 | return import(`./${file}`).then(response => { 58 | index.mergeLocaleMessage(lang, response.default); 59 | loadedLanguages.push(lang); 60 | return setI18nLanguage(lang) 61 | }) 62 | } 63 | return Promise.resolve(setI18nLanguage(lang)) 64 | } 65 | return Promise.resolve(lang) 66 | }; 67 | 68 | export default index; 69 | -------------------------------------------------------------------------------- /src/components/menu/edit/expand.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 44 | -------------------------------------------------------------------------------- /src/components/menu/view/styleOperation.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 76 | -------------------------------------------------------------------------------- /src/script/runtime/exports.js: -------------------------------------------------------------------------------- 1 | 2 | define(function (require, exports, module) { 3 | var png = require("../protocol/png"); 4 | var svg = require("../protocol/svg"); 5 | var json = require("../protocol/json"); 6 | var plain = require("../protocol/plain"); 7 | var md = require("../protocol/markdown"); 8 | var mm = require("../protocol/freemind"); 9 | var {t} = require("../../locale"); 10 | 11 | function ExportRuntime() { 12 | var minder = this.minder; 13 | var hotbox = this.hotbox; 14 | var exps = [ 15 | {label: '.json', key: 'j', cmd: exportJson}, 16 | {label: '.png', key: 'p', cmd: exportImage}, 17 | {label: '.svg', key: 's', cmd: exportSVG}, 18 | {label: '.txt', key: 't', cmd: exportTextTree}, 19 | {label: '.md', key: 'm', cmd: exportMarkdown}, 20 | {label: '.mm', key: 'f', cmd: exportFreeMind} 21 | ]; 22 | 23 | 24 | var main = hotbox.state('main'); 25 | main.button({ 26 | position: 'top', 27 | label: t('minder.commons.export'), 28 | key: 'E', 29 | enable: canExp, 30 | next: 'exp' 31 | }); 32 | 33 | var exp = hotbox.state('exp'); 34 | exps.forEach(item => { 35 | exp.button({ 36 | position: 'ring', 37 | label: item.label, 38 | key: null, 39 | action: item.cmd 40 | }); 41 | }); 42 | 43 | exp.button({ 44 | position: 'center', 45 | label: t('minder.commons.cancel'), 46 | key: 'esc', 47 | next: 'back' 48 | }); 49 | 50 | function canExp() { 51 | return true; 52 | } 53 | 54 | function exportJson(){ 55 | json.exportJson(minder); 56 | } 57 | 58 | function exportImage (){ 59 | png.exportPNGImage(minder); 60 | } 61 | 62 | function exportSVG (){ 63 | svg.exportSVG(minder); 64 | } 65 | 66 | function exportTextTree (){ 67 | plain.exportTextTree(minder); 68 | } 69 | 70 | function exportMarkdown (){ 71 | md.exportMarkdown(minder); 72 | } 73 | 74 | function exportFreeMind (){ 75 | mm.exportFreeMind(minder); 76 | } 77 | } 78 | 79 | return module.exports = ExportRuntime; 80 | }); 81 | -------------------------------------------------------------------------------- /src/components/menu/view/mold.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 63 | 64 | 70 | -------------------------------------------------------------------------------- /src/locale/lang/zh-CN.js: -------------------------------------------------------------------------------- 1 | export default { 2 | minder: { 3 | commons: { 4 | confirm: '确定', 5 | clear: '清空', 6 | export: '导出', 7 | cancel: '取消', 8 | edit: '编辑', 9 | delete: '删除', 10 | remove: '移除', 11 | return: '返回', 12 | }, 13 | menu: { 14 | expand: { 15 | expand: '展开', 16 | folding: '收起', 17 | expand_one: '展开到一级节点', 18 | expand_tow: '展开到二级节点', 19 | expand_three: '展开到三级节点', 20 | expand_four: '展开到四级节点', 21 | expand_five: '展开到五级节点', 22 | expand_six: '展开到六级节点' 23 | }, 24 | insert: { 25 | down: '插入下级主题', 26 | up: '插入上级主题', 27 | same: '插入同级主题', 28 | _same: '同级', 29 | _down: '下级', 30 | _up: '上级', 31 | }, 32 | move: { 33 | up: '上移', 34 | down: '下移', 35 | forward: '前移', 36 | backward: '后移', 37 | }, 38 | progress: { 39 | progress: '进度', 40 | remove_progress: '移除进度', 41 | prepare: '未开始', 42 | complete_all: '全部完成', 43 | complete: '完成', 44 | }, 45 | selection: { 46 | all: '全选', 47 | invert: '反选', 48 | sibling: '选择兄弟节点', 49 | same: '选择同级节点', 50 | path: '选择路径', 51 | subtree: '选择子树', 52 | }, 53 | arrange: { 54 | arrange_layout: '整理布局' 55 | }, 56 | font: { 57 | font: '字体', 58 | size: '字号' 59 | }, 60 | style: { 61 | clear: '清除样式', 62 | copy: '复制样式', 63 | paste: '粘贴样式', 64 | } 65 | }, 66 | main: { 67 | header: { 68 | minder: '思维导图', 69 | style: '外观样式' 70 | }, 71 | main: { 72 | save: '保存' 73 | }, 74 | navigator: { 75 | amplification: '放大', 76 | narrow: '缩小', 77 | drag: '拖拽', 78 | locating_root: '定位根节点', 79 | navigator: '导航器', 80 | }, 81 | history: { 82 | undo: '撤销', 83 | redo: '重做' 84 | }, 85 | subject: { 86 | central: '中心主题', 87 | branch: '分支主题' 88 | }, 89 | priority: '优先级', 90 | tag: '标签' 91 | } 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /src/locale/lang/zh-TW.js: -------------------------------------------------------------------------------- 1 | export default { 2 | minder: { 3 | commons: { 4 | confirm: '確定', 5 | clear: '清空', 6 | export: '導出', 7 | cancel: '取消', 8 | edit: '編輯', 9 | delete: '刪除', 10 | remove: '移除', 11 | return: '返回', 12 | }, 13 | menu: { 14 | expand: { 15 | expand: '展開', 16 | folding: '收起', 17 | expand_one: '展開到一級節點', 18 | expand_tow: '展開到二級節點', 19 | expand_three: '展開到三級節點', 20 | expand_four: '展開到四級節點', 21 | expand_five: '展開到五級節點', 22 | expand_six: '展開到六級節點' 23 | }, 24 | insert: { 25 | down: '插入下級主題', 26 | up: '插入上級主題', 27 | same: '插入同級主題', 28 | _same: '同級', 29 | _down: '下級', 30 | _up: '上級', 31 | }, 32 | move: { 33 | up: '上移', 34 | down: '下移', 35 | forward: '前移', 36 | backward: '後移', 37 | }, 38 | progress: { 39 | progress: '進度', 40 | remove_progress: '移除進度', 41 | prepare: '未開始', 42 | complete_all: '全部完成', 43 | complete: '完成', 44 | }, 45 | selection: { 46 | all: '全選', 47 | invert: '反選', 48 | sibling: '選擇兄弟節點', 49 | same: '選擇同級節點', 50 | path: '選擇路徑', 51 | subtree: '選擇子樹', 52 | }, 53 | arrange: { 54 | arrange_layout: '整理布局' 55 | }, 56 | font: { 57 | font: '字體', 58 | size: '字號' 59 | }, 60 | style: { 61 | clear: '清除樣式', 62 | copy: '復製樣式', 63 | paste: '粘貼樣式', 64 | } 65 | }, 66 | main: { 67 | header: { 68 | minder: '思維導圖', 69 | style: '外觀樣式' 70 | }, 71 | main: { 72 | save: '保存' 73 | }, 74 | navigator: { 75 | amplification: '放大', 76 | narrow: '縮小', 77 | drag: '拖拽', 78 | locating_root: '定位根節點', 79 | navigator: '導航器', 80 | }, 81 | history: { 82 | undo: '撤銷', 83 | redo: '重做' 84 | }, 85 | subject: { 86 | central: '中心主題', 87 | branch: '分支主題' 88 | }, 89 | priority: '優先級', 90 | tag: '標簽' 91 | } 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /demo/test/ms-test.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 72 | 73 | 76 | -------------------------------------------------------------------------------- /src/script/protocol/svg.js: -------------------------------------------------------------------------------- 1 | function exportSVG(minder) { 2 | 3 | var paper = minder.getPaper(); 4 | var paperTransform = paper.shapeNode.getAttribute('transform'); 5 | var svgXml; 6 | var $svg; 7 | 8 | var renderContainer = minder.getRenderContainer(); 9 | var renderBox = renderContainer.getRenderBox(); 10 | var transform = renderContainer.getTransform(); 11 | var width = renderBox.width; 12 | var height = renderBox.height; 13 | var padding = 20; 14 | 15 | paper.shapeNode.setAttribute('transform', 'translate(0.5, 0.5)'); 16 | svgXml = paper.container.innerHTML; 17 | console.log(svgXml); 18 | paper.shapeNode.setAttribute('transform', paperTransform); 19 | 20 | let document = window.document; 21 | let el = document.createElement("div"); 22 | el.innerHTML = svgXml; 23 | $svg = el.getElementsByTagName('svg'); 24 | 25 | let index = $svg.length - 1; 26 | 27 | $svg[index].setAttribute('width', width + padding * 2 | 0); 28 | $svg[index].setAttribute('height', height + padding * 2 | 0); 29 | $svg[index].setAttribute('style', 'font-family: Arial, "Microsoft Yahei", "Heiti SC"; background: ' + minder.getStyle('background')); 30 | 31 | $svg[index].setAttribute('viewBox', [renderBox.x - padding | 0, 32 | renderBox.y - padding | 0, 33 | width + padding * 2 | 0, 34 | height + padding * 2 | 0 35 | ].join(' ')); 36 | 37 | let div = document.createElement("div"); 38 | div.appendChild($svg[index]); 39 | svgXml = div.innerHTML; 40 | svgXml = svgXml.replace(/ /g, ' '); 41 | 42 | var blob = new Blob([svgXml], { 43 | type: 'image/svg+xml' 44 | }); 45 | 46 | var DOMURL = window.URL || window.webkitURL || window; 47 | var svgUrl = DOMURL.createObjectURL(blob); 48 | 49 | var mind = editor.minder.exportJson(); 50 | downloadSVG(svgUrl, mind.root.data.text); 51 | } 52 | 53 | function downloadSVG(fileURI, fileName) { 54 | try { 55 | const link = document.createElement('a'); 56 | link.href = fileURI; 57 | link.download = `${fileName}.svg`; 58 | document.body.appendChild(link); 59 | link.click(); 60 | document.body.removeChild(link); 61 | } catch (err) { 62 | alert(err); 63 | } 64 | } 65 | 66 | export { 67 | exportSVG 68 | } 69 | -------------------------------------------------------------------------------- /src/script/protocol/xmind.js: -------------------------------------------------------------------------------- 1 | const priorities = [ 2 | {jp: 1, mp: 'full-1'}, 3 | {jp: 2, mp: 'full-2'}, 4 | {jp: 3, mp: 'full-3'}, 5 | {jp: 4, mp: 'full-4'}, 6 | {jp: 5, mp: 'full-5'}, 7 | {jp: 6, mp: 'full-6'}, 8 | {jp: 7, mp: 'full-7'}, 9 | {jp: 8, mp: 'full-8'} 10 | ]; 11 | const mmVersion = '\n'; 12 | const iconTextPrefix = '\n'; 14 | const nodeCreated = '\n'; 18 | const entityNode = '\n'; 19 | const entityMap = ''; 20 | 21 | function exportXMind(minder) { 22 | var minds = minder.exportJson(); 23 | var 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){ 40 | var result = ""; 41 | if (!node) { 42 | return; 43 | } 44 | result += concatNodes(node); 45 | if (node.children && node.children.length > 0) { 46 | for (var i = 0; i < node.children.length; i++) { 47 | result += traverseJson(node.children[i]); 48 | result += entityNode; 49 | } 50 | } 51 | return result; 52 | } 53 | 54 | function concatNodes(node) { 55 | var result = ""; 56 | var datas = node.data; 57 | result += nodeCreated + datas.created + nodeId + datas.id + nodeText + datas.text + nodeSuffix; 58 | if (datas.priority) { 59 | var mapped = priorities.find(d => { 60 | return d.jp == datas.priority 61 | }); 62 | if (mapped) { 63 | result += iconTextPrefix + mapped.mp + iconTextSuffix; 64 | } 65 | } 66 | return result; 67 | } 68 | 69 | export { 70 | exportXMind 71 | } 72 | -------------------------------------------------------------------------------- /src/script/protocol/freemind.js: -------------------------------------------------------------------------------- 1 | const priorities = [ 2 | {jp: 1, mp: 'full-1'}, 3 | {jp: 2, mp: 'full-2'}, 4 | {jp: 3, mp: 'full-3'}, 5 | {jp: 4, mp: 'full-4'}, 6 | {jp: 5, mp: 'full-5'}, 7 | {jp: 6, mp: 'full-6'}, 8 | {jp: 7, mp: 'full-7'}, 9 | {jp: 8, mp: 'full-8'} 10 | ]; 11 | const mmVersion = '\n'; 12 | const iconTextPrefix = '\n'; 14 | const nodeCreated = '\n'; 18 | const entityNode = '\n'; 19 | const entityMap = ''; 20 | 21 | function exportFreeMind(minder) { 22 | var minds = minder.exportJson(); 23 | var 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){ 40 | var result = ""; 41 | if (!node) { 42 | return; 43 | } 44 | result += concatNodes(node); 45 | if (node.children && node.children.length > 0) { 46 | for (var i = 0; i < node.children.length; i++) { 47 | result += traverseJson(node.children[i]); 48 | result += entityNode; 49 | } 50 | } 51 | return result; 52 | } 53 | 54 | function concatNodes(node) { 55 | var result = ""; 56 | var datas = node.data; 57 | result += nodeCreated + datas.created + nodeId + datas.id + nodeText + datas.text + nodeSuffix; 58 | if (datas.priority) { 59 | var mapped = priorities.find(d => { 60 | return d.jp == datas.priority 61 | }); 62 | if (mapped) { 63 | result += iconTextPrefix + mapped.mp + iconTextSuffix; 64 | } 65 | } 66 | return result; 67 | } 68 | 69 | export { 70 | exportFreeMind 71 | } 72 | -------------------------------------------------------------------------------- /src/components/menu/edit/insertBox.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 73 | -------------------------------------------------------------------------------- /src/components/menu/edit/moveBox.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 80 | -------------------------------------------------------------------------------- /src/script/runtime/clipboard-mimetype.js: -------------------------------------------------------------------------------- 1 | define(function (require, exports, module) { 2 | function MimeType() { 3 | var SPLITOR = '\uFEFF'; 4 | var MIMETYPE = { 5 | 'application/km': '\uFFFF' 6 | }; 7 | var SIGN = { 8 | '\uFEFF': 'SPLITOR', 9 | '\uFFFF': 'application/km' 10 | }; 11 | 12 | function process(mimetype, text) { 13 | if (!this.isPureText(text)) { 14 | var _mimetype = this.whichMimeType(text); 15 | if (!_mimetype) { 16 | throw new Error('unknow mimetype!'); 17 | }; 18 | text = this.getPureText(text); 19 | }; 20 | if (mimetype === false) { 21 | return text; 22 | }; 23 | return mimetype + SPLITOR + text; 24 | } 25 | 26 | this.registMimeTypeProtocol = function (type, sign) { 27 | if (sign && SIGN[sign]) { 28 | throw new Error('sing has registed!'); 29 | } 30 | if (type && !!MIMETYPE[type]) { 31 | throw new Error('mimetype has registed!'); 32 | }; 33 | SIGN[sign] = type; 34 | MIMETYPE[type] = sign; 35 | } 36 | 37 | this.getMimeTypeProtocol = function (type, text) { 38 | var mimetype = MIMETYPE[type] || false; 39 | 40 | if (text === undefined) { 41 | return process.bind(this, mimetype); 42 | }; 43 | 44 | return process(mimetype, text); 45 | } 46 | 47 | this.getSpitor = function () { 48 | return SPLITOR; 49 | } 50 | 51 | this.getMimeType = function (sign) { 52 | if (sign !== undefined) { 53 | return SIGN[sign] || null; 54 | }; 55 | return MIMETYPE; 56 | } 57 | } 58 | 59 | MimeType.prototype.isPureText = function (text) { 60 | return !(~text.indexOf(this.getSpitor())); 61 | } 62 | 63 | MimeType.prototype.getPureText = function (text) { 64 | if (this.isPureText(text)) { 65 | return text; 66 | }; 67 | return text.split(this.getSpitor())[1]; 68 | } 69 | 70 | MimeType.prototype.whichMimeType = function (text) { 71 | if (this.isPureText(text)) { 72 | return null; 73 | }; 74 | return this.getMimeType(text.split(this.getSpitor())[0]); 75 | } 76 | 77 | function MimeTypeRuntime() { 78 | if (this.minder.supportClipboardEvent && !kity.Browser.gecko) { 79 | this.MimeType = new MimeType(); 80 | }; 81 | } 82 | 83 | return module.exports = MimeTypeRuntime; 84 | }); 85 | -------------------------------------------------------------------------------- /demo/test/test-plugin.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 81 | 82 | 85 | -------------------------------------------------------------------------------- /src/components/minderEditor.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 84 | 85 | 87 | -------------------------------------------------------------------------------- /src/locale/lang/en-US.js: -------------------------------------------------------------------------------- 1 | export default { 2 | minder: { 3 | commons: { 4 | confirm: 'Confirm', 5 | clear: 'Clear', 6 | export: 'Export', 7 | cancel: 'Cancel', 8 | edit: 'Edit', 9 | delete: 'Delete', 10 | remove: 'Remove', 11 | return: 'Return', 12 | }, 13 | menu: { 14 | expand: { 15 | expand: 'Expand', 16 | folding: 'Folding', 17 | expand_one: 'Expand one level', 18 | expand_tow: 'Expand tow level', 19 | expand_three: 'Expand three level', 20 | expand_four: 'Expand four level', 21 | expand_five: 'Expand five level', 22 | expand_six: 'Expand six level' 23 | }, 24 | insert: { 25 | down: 'Subordinate', 26 | up: 'Superior', 27 | same: 'Same', 28 | _same: 'Same level', 29 | _down: 'Subordinate level', 30 | _up: 'Superior level', 31 | }, 32 | move: { 33 | up: 'Up', 34 | down: 'Down', 35 | forward: 'Forward', 36 | backward: 'Backward', 37 | }, 38 | progress: { 39 | progress: 'Progress', 40 | remove_progress: 'Remove progress', 41 | prepare: 'Prepare', 42 | complete_all: 'Complete all', 43 | complete: 'Complete', 44 | }, 45 | selection: { 46 | all: 'Select all', 47 | invert: 'Select invert', 48 | sibling: 'Select sibling node', 49 | same: 'Select same node', 50 | path: 'Select path', 51 | subtree: 'Select subtree', 52 | }, 53 | arrange: { 54 | arrange_layout: 'Arrange layout' 55 | }, 56 | font: { 57 | font: 'Font', 58 | size: 'Font size' 59 | }, 60 | style: { 61 | clear: 'Clear style', 62 | copy: 'Copy style', 63 | paste: 'Paste style', 64 | } 65 | }, 66 | main: { 67 | header: { 68 | minder: 'Minder', 69 | style: 'Appearance style' 70 | }, 71 | main: { 72 | save: 'Save' 73 | }, 74 | navigator: { 75 | amplification: 'Amplification', 76 | narrow: 'Narrow', 77 | drag: 'Drag', 78 | locating_root: 'Locating root node', 79 | navigator: 'Navigator', 80 | }, 81 | history: { 82 | undo: 'Undo', 83 | redo: 'Redo' 84 | }, 85 | subject: { 86 | central: 'Central subject', 87 | branch: 'Subject' 88 | }, 89 | priority: 'Priority', 90 | tag: 'Tag' 91 | } 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /src/components/menu/edit/editDel.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 93 | -------------------------------------------------------------------------------- /src/components/menu/edit/progressBox.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 83 | 84 | 89 | -------------------------------------------------------------------------------- /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 | overflow: hidden; 38 | word-break: break-all; 39 | word-wrap: break-word; 40 | border: none; 41 | -webkit-user-select: text; 42 | pointer-events: none; 43 | opacity: 0; 44 | z-index: -1000; 45 | 46 | &.debug { 47 | opacity: 1; 48 | outline: 1px solid green; 49 | background: none; 50 | z-index: 0; 51 | } 52 | 53 | &.input { 54 | pointer-events: all; 55 | opacity: 1; 56 | z-index: 999; 57 | background: white; 58 | outline: none; 59 | } 60 | } 61 | 62 | div.minder-editor-container { 63 | position: absolute; 64 | top: 40px; 65 | bottom: 0; 66 | left: 0; 67 | right: 0; 68 | font-family: Arial, "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif; 69 | } 70 | 71 | .minder-editor { 72 | position: absolute; 73 | top: 92px; 74 | left: 0; 75 | right: 0; 76 | bottom: 0; 77 | } 78 | 79 | .minder-viewer { 80 | position: absolute; 81 | top: 0; 82 | left: 0; 83 | right: 0; 84 | bottom: 0; 85 | } 86 | 87 | .control-panel { 88 | position: absolute; 89 | top: 0; 90 | right: 0; 91 | width: 250px; 92 | bottom: 0; 93 | border-left: 1px solid #ccc; 94 | } 95 | 96 | .minder-divider { 97 | position: absolute; 98 | top: 0; 99 | right: 250px; 100 | bottom: 0; 101 | width: 2px; 102 | background-color: rgb(251, 251, 251); 103 | cursor: ew-resize; 104 | } 105 | 106 | .hotbox .state .button.enabled.selected .key, 107 | .hotbox .state .ring .key { 108 | margin-top: 5px; 109 | font-size: 13px; 110 | } 111 | 112 | .hotbox .state .bottom .button .label, 113 | .hotbox .state .top .button .label { 114 | font-weight: 600; 115 | } 116 | 117 | .hotbox .exp .ring .button .label { 118 | margin-top: 28px; 119 | margin-left: -2px; 120 | } 121 | 122 | .hotbox .exp .ring .button .key { 123 | display: none; 124 | } 125 | -------------------------------------------------------------------------------- /src/script/protocol/plain.js: -------------------------------------------------------------------------------- 1 | const LINE_ENDING = '\r'; 2 | const LINE_ENDING_SPLITER = /\r\n|\r|\n/; 3 | const TAB_CHAR = '\t'; 4 | 5 | function exportTextTree(minder) { 6 | var 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, n) { 23 | var result = ''; 24 | while (n--) result += s; 25 | return result; 26 | } 27 | 28 | function encode(json, level) { 29 | var 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) { 35 | local += encode(child, level + 1); 36 | }); 37 | } 38 | return local; 39 | } 40 | 41 | function isEmpty(line) { 42 | return !/\S/.test(line); 43 | } 44 | 45 | function getLevel(line) { 46 | var level = 0; 47 | while (line.charAt(level) === TAB_CHAR) level++; 48 | return level; 49 | } 50 | 51 | function getNode(line) { 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, root) { 67 | var json, 68 | offset, 69 | parentMap = {}, 70 | lines = local.split(LINE_ENDING_SPLITER), 71 | line, level, node; 72 | 73 | function addChild(parent, child) { 74 | var children = parent.children || (parent.children = []); 75 | children.push(child); 76 | } 77 | if (root) { 78 | parentMap[0] = json = getNode('root'); 79 | offset = 1; 80 | } else { 81 | offset = 0; 82 | } 83 | 84 | for (var i = 0; i < lines.length; i++) { 85 | line = lines[i]; 86 | if (isEmpty(line)) continue; 87 | 88 | level = getLevel(line) + offset; 89 | node = getNode(line); 90 | 91 | if (level === 0) { 92 | if (json) { 93 | throw new Error('Invalid local format'); 94 | } 95 | json = node; 96 | } else { 97 | if (!parentMap[level - 1]) { 98 | throw new Error('Invalid local format'); 99 | } 100 | addChild(parentMap[level - 1], node); 101 | } 102 | parentMap[level] = node; 103 | } 104 | return json; 105 | } 106 | 107 | export { 108 | exportTextTree 109 | } 110 | -------------------------------------------------------------------------------- /src/props.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Api 列表 3 | */ 4 | 5 | export let 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 let priorityProps = { 40 | priorityCount: { 41 | type: Number, 42 | default: 4, 43 | validator: function (value) { 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 let tagProps = { 63 | tags: { 64 | // 自定义标签 65 | type: Array, 66 | default() { 67 | return [] 68 | } 69 | }, 70 | distinctTags: { 71 | // 个别标签二选一 72 | type: Array, 73 | default() { 74 | return [] 75 | } 76 | }, 77 | tagDisableCheck: Function, 78 | tagEditCheck: Function 79 | } 80 | 81 | export let 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 | moveConfirm: { 99 | type: Function, 100 | default: null 101 | }, 102 | } 103 | 104 | export let viewMenuProps = { 105 | viewMenuEnable: { 106 | type: Boolean, 107 | default: true 108 | }, 109 | moldEnable: { 110 | type: Boolean, 111 | default: true 112 | }, 113 | arrangeEnable: { 114 | type: Boolean, 115 | default: true 116 | }, 117 | styleEnable: { 118 | type: Boolean, 119 | default: true 120 | }, 121 | fontEnable: { 122 | type: Boolean, 123 | default: true 124 | }, 125 | } 126 | 127 | export let moleProps = { 128 | // 默认样式 129 | defaultMold: { 130 | type: Number, 131 | default: 3 132 | } 133 | } 134 | 135 | export let delProps = { 136 | delConfirm: { 137 | type: Function, 138 | default: null 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /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/components/menu/edit/tagBox.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 88 | 89 | 105 | 106 | 114 | -------------------------------------------------------------------------------- /src/script/runtime/receiver.js: -------------------------------------------------------------------------------- 1 | define(function (require, exports, module) { 2 | var key = require('../tool/key'); 3 | var hotbox = require('./hotbox'); 4 | 5 | function ReceiverRuntime() { 6 | var fsm = this.fsm; 7 | var minder = this.minder; 8 | var me = this; 9 | 10 | // 接收事件的 div 11 | var element = document.createElement('div'); 12 | element.contentEditable = true; 13 | element.setAttribute("tabindex", -1); 14 | element.classList.add('receiver'); 15 | element.onkeydown = element.onkeypress = element.onkeyup = dispatchKeyEvent; 16 | this.container.appendChild(element); 17 | 18 | // receiver 对象 19 | var receiver = { 20 | element: element, 21 | selectAll: function () { 22 | // 保证有被选中的 23 | if (!element.innerHTML) element.innerHTML = ' '; 24 | var range = document.createRange(); 25 | var selection = window.getSelection(); 26 | range.selectNodeContents(element); 27 | selection.removeAllRanges(); 28 | selection.addRange(range); 29 | element.focus(); 30 | }, 31 | enable: function () { 32 | element.setAttribute("contenteditable", true); 33 | }, 34 | disable: function () { 35 | element.setAttribute("contenteditable", false); 36 | }, 37 | fixFFCaretDisappeared: function () { 38 | element.removeAttribute("contenteditable"); 39 | element.setAttribute("contenteditable", "true"); 40 | element.blur(); 41 | element.focus(); 42 | }, 43 | onblur: function (handler) { 44 | element.onblur = handler; 45 | } 46 | }; 47 | receiver.selectAll(); 48 | minder.on('beforemousedown', receiver.selectAll); 49 | minder.on('receiverfocus', receiver.selectAll); 50 | minder.on('readonly', function () { 51 | // 屏蔽minder的事件接受,删除receiver和hotbox 52 | minder.disable(); 53 | editor.receiver.element.parentElement.removeChild(editor.receiver.element); 54 | editor.hotbox.$container.removeChild(editor.hotbox.$element); 55 | }); 56 | 57 | // 侦听器,接收到的事件会派发给所有侦听器 58 | var listeners = []; 59 | 60 | // 侦听指定状态下的事件,如果不传 state,侦听所有状态 61 | receiver.listen = function (state, listener) { 62 | if (arguments.length == 1) { 63 | listener = state; 64 | state = '*'; 65 | } 66 | listener.notifyState = state; 67 | listeners.push(listener); 68 | }; 69 | 70 | function dispatchKeyEvent(e) { 71 | e.is = function (keyExpression) { 72 | var subs = keyExpression.split('|'); 73 | for (var i = 0; i < subs.length; i++) { 74 | if (key.is(this, subs[i])) return true; 75 | } 76 | return false; 77 | }; 78 | var listener, jumpState; 79 | for (var i = 0; i < listeners.length; i++) { 80 | 81 | listener = listeners[i]; 82 | // 忽略不在侦听状态的侦听器 83 | if (listener.notifyState != '*' && listener.notifyState != fsm.state()) { 84 | continue; 85 | } 86 | 87 | if (listener.call(null, e)) { 88 | return; 89 | } 90 | } 91 | } 92 | 93 | this.receiver = receiver; 94 | } 95 | 96 | return module.exports = ReceiverRuntime; 97 | }); 98 | -------------------------------------------------------------------------------- /src/components/main/header.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 89 | 90 | 93 | 94 | 107 | -------------------------------------------------------------------------------- /src/script/runtime/fsm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * 4 | * 编辑器状态机 5 | * 6 | * @author: techird 7 | * @copyright: Baidu FEX, 2014 8 | */ 9 | define(function (require, exports, module) { 10 | 11 | var Debug = require('../tool/debug'); 12 | var debug = new Debug('fsm'); 13 | 14 | function handlerConditionMatch(condition, when, exit, enter) { 15 | if (condition.when != when) return false; 16 | if (condition.enter != '*' && condition.enter != enter) return false; 17 | if (condition.exit != '*' && condition.exit != exit) return; 18 | return true; 19 | } 20 | 21 | function FSM(defaultState) { 22 | var currentState = defaultState; 23 | var BEFORE_ARROW = ' - '; 24 | var AFTER_ARROW = ' -> '; 25 | var handlers = []; 26 | 27 | /** 28 | * 状态跳转 29 | * 30 | * 会通知所有的状态跳转监视器 31 | * 32 | * @param {string} newState 新状态名称 33 | * @param {any} reason 跳转的原因,可以作为参数传递给跳转监视器 34 | */ 35 | this.jump = function (newState, reason) { 36 | if (!reason) throw new Error('Please tell fsm the reason to jump'); 37 | 38 | var oldState = currentState; 39 | var notify = [oldState, newState].concat([].slice.call(arguments, 1)); 40 | var i, handler; 41 | 42 | // 跳转前 43 | for (i = 0; i < handlers.length; i++) { 44 | handler = handlers[i]; 45 | if (handlerConditionMatch(handler.condition, 'before', oldState, newState)) { 46 | if (handler.apply(null, notify)) return; 47 | } 48 | } 49 | 50 | currentState = newState; 51 | debug.log('[{0}] {1} -> {2}', reason, oldState, newState); 52 | 53 | // 跳转后 54 | for (i = 0; i < handlers.length; i++) { 55 | handler = handlers[i]; 56 | if (handlerConditionMatch(handler.condition, 'after', oldState, newState)) { 57 | handler.apply(null, notify); 58 | } 59 | } 60 | return currentState; 61 | }; 62 | 63 | /** 64 | * 返回当前状态 65 | * @return {string} 66 | */ 67 | this.state = function () { 68 | return currentState; 69 | }; 70 | 71 | /** 72 | * 添加状态跳转监视器 73 | * 74 | * @param {string} condition 75 | * 监视的时机 76 | * "* => *" (默认) 77 | * 78 | * @param {Function} handler 79 | * 监视函数,当状态跳转的时候,会接收三个参数 80 | * * from - 跳转前的状态 81 | * * to - 跳转后的状态 82 | * * reason - 跳转的原因 83 | */ 84 | this.when = function (condition, handler) { 85 | if (arguments.length == 1) { 86 | handler = condition; 87 | condition = '* -> *'; 88 | } 89 | 90 | var when, resolved, exit, enter; 91 | 92 | resolved = condition.split(BEFORE_ARROW); 93 | if (resolved.length == 2) { 94 | when = 'before'; 95 | } else { 96 | resolved = condition.split(AFTER_ARROW); 97 | if (resolved.length == 2) { 98 | when = 'after'; 99 | } 100 | } 101 | if (!when) throw new Error('Illegal fsm condition: ' + condition); 102 | 103 | exit = resolved[0]; 104 | enter = resolved[1]; 105 | 106 | handler.condition = { 107 | when: when, 108 | exit: exit, 109 | enter: enter 110 | }; 111 | 112 | handlers.push(handler); 113 | }; 114 | } 115 | 116 | function FSMRumtime() { 117 | this.fsm = new FSM('normal'); 118 | } 119 | 120 | return module.exports = FSMRumtime; 121 | }); 122 | -------------------------------------------------------------------------------- /src/script/tool/utils.js: -------------------------------------------------------------------------------- 1 | export function isDisableNode(minder) { 2 | let node = undefined; 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 isDisableForNode(node) { 13 | if (node && node.data.disable === true) { 14 | return true; 15 | } 16 | return false; 17 | } 18 | 19 | export function isDeleteDisableNode(minder) { 20 | let node = undefined; 21 | if (minder && minder.getSelectedNode) { 22 | node = minder.getSelectedNode(); 23 | } 24 | if (node && node.data.disable === true && !node.data.allowDelete) { 25 | return true; 26 | } 27 | return false; 28 | } 29 | 30 | export function isTagEnable(minder) { 31 | let node = undefined; 32 | if (minder && minder.getSelectedNode) { 33 | node = minder.getSelectedNode(); 34 | } 35 | if (isTagEnableNode(node)) { 36 | return true; 37 | } 38 | return false; 39 | } 40 | 41 | export function isTagEnableNode(node) { 42 | if (node && (node.data.tagEnable === true || node.data.allowDisabledTag === true)) { 43 | return true; 44 | } 45 | return false; 46 | } 47 | 48 | export function markChangeNode(node) { 49 | if (node && node.data) { 50 | // 修改的该节点标记为 contextChanged 51 | node.data.contextChanged = true; 52 | while (node) { 53 | // 该路径上的节点都标记为 changed 54 | node.data.changed = true; 55 | node = node.parent; 56 | } 57 | } 58 | } 59 | 60 | // 在父节点记录删除的节点 61 | export function markDeleteNode(minder) { 62 | if (minder) { 63 | let nodes = minder.getSelectedNodes(); 64 | nodes.forEach(node => { 65 | if (node && node.parent) { 66 | let pData = node.parent.data; 67 | if (!pData.deleteChild) { 68 | pData.deleteChild = []; 69 | } 70 | _markDeleteNode(node, pData.deleteChild); 71 | } 72 | }); 73 | } 74 | } 75 | 76 | function _markDeleteNode(node, deleteChild) { 77 | deleteChild.push(node.data); 78 | if (node.children) { 79 | node.children.forEach(child => { 80 | _markDeleteNode(child, deleteChild); 81 | }); 82 | } 83 | } 84 | 85 | export function isPriority(e) { 86 | if (e.getAttribute('text-rendering') === 'geometricPrecision' 87 | && e.getAttribute('text-anchor') === 'middle' 88 | ) { 89 | return true; 90 | } 91 | return false; 92 | } 93 | 94 | export function setPriorityView(priorityStartWithZero, priorityPrefix) { 95 | //手动将优先级前面加上P显示 96 | let items = document.getElementsByTagName('text'); 97 | if (items) { 98 | for (let i = 0; i < items.length; i++) { 99 | let item = items[i]; 100 | if (isPriority(item)) { 101 | let content = item.innerHTML; 102 | if (content.indexOf(priorityPrefix) < 0) { 103 | if (priorityStartWithZero) { 104 | content = parseInt(content) - 1 + ''; 105 | } 106 | item.innerHTML = priorityPrefix + content; 107 | } 108 | } 109 | } 110 | } 111 | } 112 | 113 | /** 114 | * 将节点及其子节点id置为null,changed 标记为true 115 | * @param node 116 | */ 117 | export function resetNodes(nodes) { 118 | if (nodes) { 119 | nodes.forEach(item => { 120 | if (item.data) { 121 | item.data.id = null; 122 | item.data.contextChanged = true; 123 | item.data.changed = true; 124 | resetNodes(item.children); 125 | } 126 | }); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/components/menu/edit/attachment.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 122 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-minder-editor-plus", 3 | "version": "1.1.9", 4 | "description": "A Vue2 project", 5 | "author": "AgAngle <1323481023@qq.com>", 6 | "scripts": { 7 | "dev": "node build/dev-server.js", 8 | "build": "node build/build.js", 9 | "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run", 10 | "e2e": "node test/e2e/runner.js", 11 | "test": "npm run unit && npm run e2e", 12 | "pub": "npm run build && npm publish" 13 | }, 14 | "main": "dist/static/vue-minder-editor-plus.js", 15 | "dependencies": { 16 | "@7polo/kity": "^2.0.8", 17 | "@7polo/kityminder-core": "^1.4.53", 18 | "element-ui": "^2.12.0", 19 | "hotbox-minder": "^1.0.15", 20 | "vue": "2.6.14", 21 | "vue-i18n": "^8.15.3" 22 | }, 23 | "devDependencies": { 24 | "chai": "^3.5.0", 25 | "cross-env": "^3.1.4", 26 | "cross-spawn": "^5.0.1", 27 | "function-bind": "^1.1.0", 28 | "inject-loader": "^2.0.1", 29 | "lolex": "^1.5.2", 30 | "nightwatch": "^0.9.12", 31 | "phantomjs-prebuilt": "^2.1.14", 32 | "sinon": "^1.17.7", 33 | "sinon-chai": "^2.8.0", 34 | "acorn": "^6.0.4", 35 | "assets-webpack-plugin": "^3.9.10", 36 | "autoprefixer": "^6.7.2", 37 | "babel-core": "^6.22.1", 38 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 39 | "babel-loader": "^7.1.5", 40 | "babel-plugin-syntax-dynamic-import": "^6.18.0", 41 | "babel-plugin-syntax-jsx": "^6.18.0", 42 | "babel-plugin-transform-imports": "1.5.0", 43 | "babel-plugin-transform-vue-jsx": "^3.7.0", 44 | "babel-polyfill": "^6.26.0", 45 | "babel-preset-env": "^1.3.2", 46 | "babel-preset-es2015": "^6.24.1", 47 | "babel-preset-stage-2": "^6.24.1", 48 | "babel-register": "^6.26.0", 49 | "chalk": "^1.1.3", 50 | "compression-webpack-plugin": "^1.1.12", 51 | "connect-history-api-fallback": "^1.3.0", 52 | "copy-webpack-plugin": "^4.0.1", 53 | "css-loader": "^0.28.8", 54 | "eslint": "^5.9.0", 55 | "eslint-config-standard": "^12.0.0", 56 | "eslint-plugin-import": "^2.14.0", 57 | "eslint-plugin-node": "^8.0.0", 58 | "eslint-plugin-promise": "^4.0.1", 59 | "eslint-plugin-standard": "^4.0.0", 60 | "eventsource-polyfill": "^0.9.6", 61 | "express": "^4.14.1", 62 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 63 | "extract-zip": "^1.6.6", 64 | "file-loader": "^2.0.0", 65 | "friendly-errors-webpack-plugin": "^1.1.3", 66 | "html-loader": "^0.5.5", 67 | "html-webpack-plugin": "^3.2.0", 68 | "http-proxy-middleware": "^0.17.3", 69 | "mini-css-extract-plugin": "^0.4.4", 70 | "node-sass": "^4.9.0", 71 | "opn": "^4.0.2", 72 | "optimize-css-assets-webpack-plugin": "^1.3.0", 73 | "ora": "^1.2.0", 74 | "postcss-import": "^12.0.1", 75 | "postcss-loader": "^3.0.0", 76 | "postcss-url": "^8.0.0", 77 | "rimraf": "^2.6.0", 78 | "sass-loader": "^7.0.1", 79 | "semver": "^5.3.0", 80 | "shelljs": "^0.7.6", 81 | "style-loader": "^0.21.0", 82 | "stylus": "^0.54.5", 83 | "stylus-loader": "^3.0.2", 84 | "text-loader": "0.0.1", 85 | "uglifyjs-webpack-plugin": "^2.0.1", 86 | "url-loader": "^0.5.8", 87 | "vue-loader": "^15.4.2", 88 | "vue-style-loader": "^2.0.5", 89 | "vue-template-compiler": "^2.6.10", 90 | "webpack": "^4.26.0", 91 | "webpack-bundle-analyzer": "^3.3.2", 92 | "webpack-cli": "^3.1.2", 93 | "webpack-dev-middleware": "^3.4.0", 94 | "webpack-dev-server": "^3.1.10", 95 | "webpack-hot-middleware": "^2.18.0", 96 | "webpack-merge": "^4.1.4" 97 | }, 98 | "engines": { 99 | "node": ">= 4.0.0", 100 | "npm": ">= 3.0.0" 101 | }, 102 | "browserslist": [ 103 | "> 1%", 104 | "last 2 versions", 105 | "not ie <= 8" 106 | ], 107 | "repository": { 108 | "type": "git", 109 | "url": "https://github.com/AgAngle/vue-minder-editor.git" 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/style/normalize.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: sans-serif; 3 | line-height: 1.15; 4 | -ms-text-size-adjust: 100%; 5 | -webkit-text-size-adjust: 100%; 6 | } 7 | 8 | body { 9 | margin: 0; 10 | } 11 | 12 | article, aside, footer, header, nav, section { 13 | display: block; 14 | } 15 | 16 | h1 { 17 | font-size: 2em; 18 | margin: .67em 0; 19 | } 20 | 21 | figcaption, figure, main { 22 | display: block; 23 | } 24 | 25 | figure { 26 | margin: 1em 40px; 27 | } 28 | 29 | hr { 30 | overflow: visible; 31 | box-sizing: content-box; 32 | height: 0; 33 | } 34 | 35 | pre { 36 | font-family: monospace, monospace; 37 | font-size: 1em; 38 | } 39 | 40 | a { 41 | background-color: transparent; 42 | -webkit-text-decoration-skip: objects; 43 | } 44 | 45 | a:active, a:hover { 46 | outline-width: 0; 47 | } 48 | 49 | abbr[title] { 50 | text-decoration: underline; 51 | text-decoration: underline dotted; 52 | border-bottom: none; 53 | } 54 | 55 | b, strong { 56 | font-weight: inherit; 57 | } 58 | 59 | b, strong { 60 | font-weight: bolder; 61 | } 62 | 63 | code, kbd, samp { 64 | font-family: monospace, monospace; 65 | font-size: 1em; 66 | } 67 | 68 | dfn { 69 | font-style: italic; 70 | } 71 | 72 | mark { 73 | color: #000; 74 | background-color: #ff0; 75 | } 76 | 77 | small { 78 | font-size: 80%; 79 | } 80 | 81 | sub, sup { 82 | font-size: 75%; 83 | line-height: 0; 84 | position: relative; 85 | vertical-align: baseline; 86 | } 87 | 88 | sub { 89 | bottom: -.25em; 90 | } 91 | 92 | sup { 93 | top: -.5em; 94 | } 95 | 96 | audio, video { 97 | display: inline-block; 98 | } 99 | 100 | audio:not([controls]) { 101 | display: none; 102 | height: 0; 103 | } 104 | 105 | img { 106 | border-style: none; 107 | } 108 | 109 | svg:not(:root) { 110 | overflow: hidden; 111 | } 112 | 113 | button, input, optgroup, select, textarea { 114 | font-family: sans-serif; 115 | font-size: 100%; 116 | line-height: 1.15; 117 | margin: 0; 118 | } 119 | 120 | button, input { 121 | overflow: visible; 122 | } 123 | 124 | button, select { 125 | text-transform: none; 126 | } 127 | 128 | button, html [type='button'], [type='reset'], [type='submit'] { 129 | -webkit-appearance: button; 130 | } 131 | 132 | [type='button']::-moz-focus-inner, [type='reset']::-moz-focus-inner, [type='submit']::-moz-focus-inner, button::-moz-focus-inner { 133 | padding: 0; 134 | border-style: none; 135 | } 136 | 137 | [type='button']:-moz-focusring, [type='reset']:-moz-focusring, [type='submit']:-moz-focusring, button:-moz-focusring { 138 | outline: 1px dotted ButtonText; 139 | } 140 | 141 | fieldset { 142 | margin: 0 2px; 143 | padding: .35em .625em .75em; 144 | border: 1px solid #c0c0c0; 145 | } 146 | 147 | legend { 148 | display: table; 149 | box-sizing: border-box; 150 | max-width: 100%; 151 | padding: 0; 152 | white-space: normal; 153 | color: inherit; 154 | } 155 | 156 | progress { 157 | display: inline-block; 158 | vertical-align: baseline; 159 | } 160 | 161 | textarea { 162 | overflow: auto; 163 | } 164 | 165 | [type='checkbox'], [type='radio'] { 166 | box-sizing: border-box; 167 | padding: 0; 168 | } 169 | 170 | [type='number']::-webkit-inner-spin-button, [type='number']::-webkit-outer-spin-button { 171 | height: auto; 172 | } 173 | 174 | [type='search'] { 175 | outline-offset: -2px; 176 | -webkit-appearance: textfield; 177 | } 178 | 179 | [type='search']::-webkit-search-cancel-button, [type='search']::-webkit-search-decoration { 180 | -webkit-appearance: none; 181 | } 182 | 183 | ::-webkit-file-upload-button { 184 | font: inherit; 185 | -webkit-appearance: button; 186 | } 187 | 188 | details, menu { 189 | display: block; 190 | } 191 | 192 | summary { 193 | display: list-item; 194 | } 195 | 196 | canvas { 197 | display: inline-block; 198 | } 199 | 200 | template { 201 | display: none; 202 | } 203 | 204 | [hidden] { 205 | display: none; 206 | } 207 | -------------------------------------------------------------------------------- /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 | vertical-align: middle; 31 | } 32 | 33 | .label { 34 | font-size: 16px; 35 | margin-top: 17px; 36 | color: black; 37 | font-weight: normal; 38 | line-height: 1em; 39 | } 40 | 41 | .key { 42 | font-size: 12px; 43 | color: #999; 44 | } 45 | } 46 | 47 | .ring-shape { 48 | position: absolute; 49 | left: -25px; 50 | top: -25px; 51 | border: 25px solid rgba(0, 0, 0, 0.3); 52 | border-radius: 100%; 53 | box-sizing: content-box; 54 | } 55 | 56 | .top, 57 | .bottom { 58 | position: absolute; 59 | white-space: nowrap; 60 | 61 | .button { 62 | display: inline-block; 63 | padding: 8px 15px; 64 | margin: 0 10px; 65 | border-radius: 15px; 66 | box-shadow: 0 0 30px rgba(0, 0, 0, 0.3); 67 | position: relative; 68 | 69 | .label { 70 | font-size: 14px; 71 | line-height: 14px; 72 | vertical-align: middle; 73 | color: black; 74 | line-height: 1em; 75 | } 76 | 77 | .key { 78 | font-size: 12px; 79 | line-height: 12px; 80 | vertical-align: middle; 81 | color: #999; 82 | margin-left: 3px; 83 | 84 | &:before { 85 | content: "("; 86 | } 87 | 88 | &:after { 89 | content: ")"; 90 | } 91 | } 92 | } 93 | } 94 | 95 | .button { 96 | background: #f9f9f9; 97 | overflow: hidden; 98 | cursor: default; 99 | 100 | .key, 101 | .label { 102 | opacity: 0.3; 103 | } 104 | } 105 | 106 | .button.enabled { 107 | background: white; 108 | 109 | .key, 110 | .label { 111 | opacity: 1; 112 | } 113 | 114 | &:hover { 115 | background: lighten(rgb(228, 93, 92), 5%); 116 | 117 | .label { 118 | color: white; 119 | } 120 | 121 | .key { 122 | color: lighten(rgb(228, 93, 92), 30%); 123 | } 124 | } 125 | 126 | &.selected { 127 | -webkit-animation: selected 0.1s ease; 128 | background: rgb(228, 93, 92); 129 | 130 | .label { 131 | color: white; 132 | } 133 | 134 | .key { 135 | color: lighten(rgb(228, 93, 92), 30%); 136 | } 137 | } 138 | 139 | &.pressed, 140 | &:active { 141 | background: #ff974d; 142 | 143 | .label { 144 | color: white; 145 | } 146 | 147 | .key { 148 | color: lighten(#ff974d, 30%); 149 | } 150 | } 151 | } 152 | } 153 | 154 | .state.active { 155 | display: block; 156 | } 157 | } 158 | 159 | @-webkit-keyframes selected { 160 | 0% { 161 | transform: scale(1); 162 | } 163 | 164 | 50% { 165 | transform: scale(1.1); 166 | } 167 | 168 | 100% { 169 | transform: scale(1); 170 | } 171 | } 172 | 173 | .hotbox-key-receiver { 174 | position: absolute; 175 | left: -999999px; 176 | top: -999999px; 177 | width: 20px; 178 | height: 20px; 179 | outline: none; 180 | margin: 0; 181 | } 182 | -------------------------------------------------------------------------------- /src/script/runtime/node.js: -------------------------------------------------------------------------------- 1 | define(function (require, exports, module) { 2 | function NodeRuntime() { 3 | var runtime = this; 4 | var minder = this.minder; 5 | var hotbox = this.hotbox; 6 | var fsm = this.fsm; 7 | var {t} = require("../../locale"); 8 | 9 | var main = hotbox.state('main'); 10 | var {isDisableNode, markDeleteNode, isDeleteDisableNode} = require('../tool/utils'); 11 | 12 | const buttons = [ 13 | t('minder.menu.move.forward') + ':Alt+Up:ArrangeUp', 14 | t('minder.menu.insert._down') + ':Tab|Insert:AppendChildNode', 15 | t('minder.menu.insert._same') + ':Enter:AppendSiblingNode', 16 | t('minder.menu.move.backward') + ':Alt+Down:ArrangeDown', 17 | t('minder.commons.delete') + ':Delete|Backspace:RemoveNode', 18 | t('minder.menu.insert._up') + ':Shift+Tab|Shift+Insert:AppendParentNode' 19 | ]; 20 | 21 | var AppendLock = 0; 22 | 23 | buttons.forEach(function (button) { 24 | var parts = button.split(':'); 25 | var label = parts.shift(); 26 | var key = parts.shift(); 27 | var command = parts.shift(); 28 | main.button({ 29 | position: 'ring', 30 | label: label, 31 | key: key, 32 | action: function () { 33 | if (command.indexOf('Append') === 0) { 34 | AppendLock++; 35 | minder.execCommand(command, t('minder.main.subject.branch')); 36 | 37 | function afterAppend() { 38 | if (!--AppendLock) { 39 | runtime.editText(); 40 | } 41 | minder.off('layoutallfinish', afterAppend); 42 | } 43 | minder.on('layoutallfinish', afterAppend); 44 | } else { 45 | if (command.indexOf('RemoveNode') > -1) { 46 | if (window.minderProps.delConfirm) { 47 | // 如果有删除确认,不删除,调用确认方法 48 | window.minderProps.delConfirm(); 49 | return; 50 | } 51 | markDeleteNode(minder); 52 | } else if (command.indexOf('ArrangeUp') > -1 || command.indexOf('ArrangeDown') > -1) { 53 | if (!window.minderProps.moveEnable || (window.minderProps.moveConfirm && !window.minderProps.moveConfirm())) { 54 | return; 55 | } 56 | } 57 | 58 | minder.execCommand(command); 59 | //fsm.jump('normal', 'command-executed'); 60 | } 61 | }, 62 | enable: function () { 63 | if (command.indexOf("RemoveNode") > -1) { 64 | if (isDeleteDisableNode(minder) && 65 | (command.indexOf("AppendChildNode") < 0 && command.indexOf("AppendSiblingNode") < 0) ) { 66 | return false; 67 | } 68 | } else if(command.indexOf("ArrangeUp") > 0 || command.indexOf("ArrangeDown") > 0) { 69 | if (!minder.moveEnable) { 70 | return false; 71 | } 72 | } else if (command.indexOf("AppendChildNode") < 0 && command.indexOf("AppendSiblingNode") < 0) { 73 | if (isDisableNode(minder)) return false; 74 | } 75 | let node = minder.getSelectedNode(); 76 | if (node && node.parent === null && command.indexOf("AppendSiblingNode") > -1) { 77 | return false; 78 | } 79 | return minder.queryCommandState(command) != -1; 80 | } 81 | }); 82 | }); 83 | 84 | main.button({ 85 | position: 'ring', 86 | key: '/', 87 | action: function () { 88 | if (!minder.queryCommandState('expand')) { 89 | minder.execCommand('expand'); 90 | } else if (!minder.queryCommandState('collapse')) { 91 | minder.execCommand('collapse'); 92 | } 93 | }, 94 | enable: function () { 95 | return minder.queryCommandState('expand') != -1 || minder.queryCommandState('collapse') != -1; 96 | }, 97 | beforeShow: function () { 98 | if (!minder.queryCommandState('expand')) { 99 | this.$button.children[0].innerHTML = t('minder.menu.expand.expand'); 100 | } else { 101 | this.$button.children[0].innerHTML = t('minder.menu.expand.folding'); 102 | } 103 | } 104 | }) 105 | } 106 | 107 | return module.exports = NodeRuntime; 108 | }); 109 | -------------------------------------------------------------------------------- /src/script/protocol/markdown.js: -------------------------------------------------------------------------------- 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) { 7 | var minds = minder.exportJson(); 8 | try { 9 | const link = document.createElement('a'); 10 | const blob = new Blob(["\ufeff" + encode(minds.root, 0)], { 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) { 24 | return _build(json, 1).join('\n'); 25 | } 26 | 27 | function _build(node, level) { 28 | var lines = []; 29 | 30 | level = level || 1; 31 | 32 | var sharps = _generateHeaderSharp(level); 33 | lines.push(sharps + ' ' + node.data.text); 34 | lines.push(EMPTY_LINE); 35 | 36 | var note = node.data.note; 37 | if (note) { 38 | var hasSharp = /^#/.test(note); 39 | if (hasSharp) { 40 | lines.push(NOTE_MARK_START); 41 | note = note.replace(/^#+/gm, function ($0) { 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) node.children.forEach(function (child) { 53 | lines = lines.concat(_build(child, level + 1)); 54 | }); 55 | 56 | return lines; 57 | } 58 | 59 | function _generateHeaderSharp(level) { 60 | var sharps = ''; 61 | while (level--) sharps += '#'; 62 | return sharps; 63 | } 64 | 65 | function decode(markdown) { 66 | 67 | var json, 68 | parentMap = {}, 69 | lines, line, lineInfo, level, node, parent, noteProgress, codeBlock; 70 | 71 | // 一级标题转换 `{title}\n===` => `# {title}` 72 | markdown = markdown.replace(/^(.+)\n={3,}/, function ($0, $1) { 73 | return '# ' + $1; 74 | }); 75 | 76 | lines = markdown.split(LINE_ENDING_SPLITER); 77 | 78 | // 按行分析 79 | for (var i = 0; i < lines.length; i++) { 80 | line = lines[i]; 81 | 82 | lineInfo = _resolveLine(line); 83 | 84 | // 备注标记处理 85 | if (lineInfo.noteClose) { 86 | noteProgress = false; 87 | continue; 88 | } else if (lineInfo.noteStart) { 89 | noteProgress = true; 90 | continue; 91 | } 92 | 93 | // 代码块处理 94 | codeBlock = lineInfo.codeBlock ? !codeBlock : codeBlock; 95 | 96 | // 备注条件:备注标签中,非标题定义,或标题越位 97 | if (noteProgress || codeBlock || !lineInfo.level || lineInfo.level > level + 1) { 98 | if (node) _pushNote(node, line); 99 | continue; 100 | } 101 | 102 | // 标题处理 103 | level = lineInfo.level; 104 | node = _initNode(lineInfo.content, parentMap[level - 1]); 105 | parentMap[level] = node; 106 | } 107 | 108 | _cleanUp(parentMap[1]); 109 | return parentMap[1]; 110 | } 111 | 112 | function _initNode(text, parent) { 113 | var node = { 114 | data: { 115 | text: text, 116 | note: '' 117 | } 118 | }; 119 | if (parent) { 120 | if (parent.children) parent.children.push(node); 121 | else parent.children = [node]; 122 | } 123 | return node; 124 | } 125 | 126 | function _pushNote(node, line) { 127 | node.data.note += line + '\n'; 128 | } 129 | 130 | function _isEmpty(line) { 131 | return !/\S/.test(line); 132 | } 133 | 134 | function _resolveLine(line) { 135 | var match = /^(#+)?\s*(.*)$/.exec(line); 136 | return { 137 | level: match[1] && match[1].length || null, 138 | content: match[2], 139 | noteStart: line == NOTE_MARK_START, 140 | noteClose: line == NOTE_MARK_CLOSE, 141 | codeBlock: /^\s*```/.test(line) 142 | }; 143 | } 144 | 145 | function _cleanUp(node) { 146 | if (!/\S/.test(node.data.note)) { 147 | node.data.note = null; 148 | delete node.data.note; 149 | } else { 150 | var notes = node.data.note.split('\n'); 151 | while (notes.length && !/\S/.test(notes[0])) notes.shift(); 152 | while (notes.length && !/\S/.test(notes[notes.length - 1])) notes.pop(); 153 | node.data.note = notes.join('\n'); 154 | } 155 | if (node.children) node.children.forEach(_cleanUp); 156 | } 157 | 158 | export { 159 | exportMarkdown 160 | } 161 | -------------------------------------------------------------------------------- /src/components/menu/edit/selection.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 136 | -------------------------------------------------------------------------------- /src/components/menu/edit/sequenceBox.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 78 | 169 | -------------------------------------------------------------------------------- /src/script/runtime/drag.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * 4 | * 用于拖拽节点时屏蔽键盘事件 5 | * 6 | * @author: techird 7 | * @copyright: Baidu FEX, 2014 8 | */ 9 | define(function (require, exports, module) { 10 | 11 | var Hotbox = require('../hotbox'); 12 | var Debug = require('../tool/debug'); 13 | var debug = new Debug('drag'); 14 | 15 | function DragRuntime() { 16 | var fsm = this.fsm; 17 | var minder = this.minder; 18 | var hotbox = this.hotbox; 19 | var receiver = this.receiver; 20 | var receiverElement = receiver.element; 21 | 22 | // setup everything to go 23 | setupFsm(); 24 | 25 | // listen the fsm changes, make action. 26 | function setupFsm() { 27 | 28 | // when jumped to drag mode, enter 29 | fsm.when('* -> drag', function () { 30 | // now is drag mode 31 | }); 32 | 33 | fsm.when('drag -> *', function (exit, enter, reason) { 34 | if (reason == 'drag-finish') { 35 | // now exit drag mode 36 | } 37 | }); 38 | } 39 | 40 | var downX, downY; 41 | var MOUSE_HAS_DOWN = 0; 42 | var MOUSE_HAS_UP = 1; 43 | var BOUND_CHECK = 20; 44 | var flag = MOUSE_HAS_UP; 45 | var maxX, maxY, osx, osy, containerY; 46 | var freeHorizen = false, 47 | freeVirtical = false; 48 | var frame; 49 | 50 | function move(direction, speed) { 51 | if (!direction) { 52 | freeHorizen = freeVirtical = false; 53 | frame && kity.releaseFrame(frame); 54 | frame = null; 55 | return; 56 | } 57 | if (!frame) { 58 | frame = kity.requestFrame((function (direction, speed, minder) { 59 | return function (frame) { 60 | switch (direction) { 61 | case 'left': 62 | minder._viewDragger.move({ 63 | x: -speed, 64 | y: 0 65 | }, 0); 66 | break; 67 | case 'top': 68 | minder._viewDragger.move({ 69 | x: 0, 70 | y: -speed 71 | }, 0); 72 | break; 73 | case 'right': 74 | minder._viewDragger.move({ 75 | x: speed, 76 | y: 0 77 | }, 0); 78 | break; 79 | case 'bottom': 80 | minder._viewDragger.move({ 81 | x: 0, 82 | y: speed 83 | }, 0); 84 | break; 85 | default: 86 | return; 87 | } 88 | frame.next(); 89 | }; 90 | })(direction, speed, minder)); 91 | } 92 | } 93 | 94 | minder.on('mousedown', function (e) { 95 | flag = MOUSE_HAS_DOWN; 96 | var rect = minder.getPaper().container.getBoundingClientRect(); 97 | downX = e.originEvent.clientX; 98 | downY = e.originEvent.clientY; 99 | containerY = rect.top; 100 | maxX = rect.width; 101 | maxY = rect.height; 102 | }); 103 | 104 | minder.on('mousemove', function (e) { 105 | if (fsm.state() === 'drag' && flag == MOUSE_HAS_DOWN && minder.getSelectedNode() && 106 | (Math.abs(downX - e.originEvent.clientX) > BOUND_CHECK || 107 | Math.abs(downY - e.originEvent.clientY) > BOUND_CHECK)) { 108 | osx = e.originEvent.clientX; 109 | osy = e.originEvent.clientY - containerY; 110 | 111 | if (osx < BOUND_CHECK) { 112 | move('right', BOUND_CHECK - osx); 113 | } else if (osx > maxX - BOUND_CHECK) { 114 | move('left', BOUND_CHECK + osx - maxX); 115 | } else { 116 | freeHorizen = true; 117 | } 118 | if (osy < BOUND_CHECK) { 119 | move('bottom', osy); 120 | } else if (osy > maxY - BOUND_CHECK) { 121 | move('top', BOUND_CHECK + osy - maxY); 122 | } else { 123 | freeVirtical = true; 124 | } 125 | if (freeHorizen && freeVirtical) { 126 | move(false); 127 | } 128 | } 129 | if (fsm.state() !== 'drag' && 130 | flag === MOUSE_HAS_DOWN && 131 | minder.getSelectedNode() && 132 | (Math.abs(downX - e.originEvent.clientX) > BOUND_CHECK || 133 | Math.abs(downY - e.originEvent.clientY) > BOUND_CHECK)) { 134 | 135 | if (fsm.state() === 'hotbox') { 136 | hotbox.active(Hotbox.STATE_IDLE); 137 | } 138 | 139 | return fsm.jump('drag', 'user-drag'); 140 | } 141 | }); 142 | 143 | window.addEventListener('mouseup', function () { 144 | flag = MOUSE_HAS_UP; 145 | if (fsm.state() === 'drag') { 146 | move(false); 147 | return fsm.jump('normal', 'drag-finish'); 148 | } 149 | }, false); 150 | } 151 | 152 | return module.exports = DragRuntime; 153 | }); 154 | -------------------------------------------------------------------------------- /src/components/main/mainEditor.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 139 | 140 | 143 | 144 | 157 | -------------------------------------------------------------------------------- /src/script/protocol/png.js: -------------------------------------------------------------------------------- 1 | var DOMURL = window.URL || window.webkitURL || window; 2 | 3 | function downloadImage(fileURI, fileName) { 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, callback) { 17 | return new Promise(function (resolve, reject) { 18 | var 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) { 31 | var paper = minder.getPaper(), 32 | paperTransform, 33 | domContainer = paper.container, 34 | svgXml, 35 | $svg, 36 | 37 | renderContainer = minder.getRenderContainer(), 38 | renderBox = renderContainer.getRenderBox(), 39 | width = renderBox.width + 1, 40 | height = renderBox.height + 1, 41 | 42 | blob, svgUrl, img; 43 | 44 | // 保存原始变换,并且移动到合适的位置 45 | paperTransform = paper.shapeNode.getAttribute('transform'); 46 | paper.shapeNode.setAttribute('transform', 'translate(0.5, 0.5)'); 47 | renderContainer.translate(-renderBox.x, -renderBox.y); 48 | 49 | // 获取当前的 XML 代码 50 | svgXml = paper.container.innerHTML; 51 | 52 | // 回复原始变换及位置 53 | renderContainer.translate(renderBox.x, renderBox.y); 54 | paper.shapeNode.setAttribute('transform', paperTransform); 55 | 56 | // 过滤内容 57 | let el = document.createElement("div"); 58 | el.innerHTML = svgXml; 59 | $svg = el.getElementsByTagName('svg'); 60 | 61 | let index = $svg.length - 1; 62 | 63 | $svg[index].setAttribute('width', renderBox.width + 1); 64 | $svg[index].setAttribute('height', renderBox.height + 1); 65 | $svg[index].setAttribute('style', 'font-family: Arial, "Microsoft Yahei","Heiti SC";'); 66 | 67 | let div = document.createElement("div"); 68 | div.appendChild($svg[index]); 69 | svgXml = div.innerHTML; 70 | 71 | // Dummy IE 72 | svgXml = svgXml.replace(' xmlns="http://www.w3.org/2000/svg" xmlns:NS1="" NS1:ns1:xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:NS2="" NS2:xmlns:ns1=""', ''); 73 | 74 | // svg 含有   符号导出报错 Entity 'nbsp' not defined 75 | svgXml = svgXml.replace(/ /g, ' '); 76 | 77 | blob = new Blob([svgXml], { 78 | type: 'image/svg+xml' 79 | }); 80 | 81 | svgUrl = DOMURL.createObjectURL(blob); 82 | 83 | return { 84 | width: width, 85 | height: height, 86 | dataUrl: svgUrl, 87 | xml: svgXml 88 | }; 89 | } 90 | 91 | function exportPNGImage(minder) { 92 | 93 | /* 绘制 PNG 的画布及上下文 */ 94 | var canvas = document.createElement('canvas'); 95 | var ctx = canvas.getContext('2d'); 96 | 97 | /* 尝试获取背景图片 URL 或背景颜色 */ 98 | var bgDeclare = minder.getStyle('background').toString(); 99 | var bgUrl = /url\((.+)\)/.exec(bgDeclare); 100 | var bgColor = kity.Color.parse(bgDeclare); 101 | 102 | /* 获取 SVG 文件内容 */ 103 | var svgInfo = getSVGInfo(minder); 104 | var width = svgInfo.width; 105 | var height = svgInfo.height; 106 | var svgDataUrl = svgInfo.dataUrl; 107 | 108 | /* 画布的填充大小 */ 109 | var padding = 20; 110 | 111 | canvas.width = width + padding * 2; 112 | canvas.height = height + padding * 2; 113 | 114 | function fillBackground(ctx, style) { 115 | ctx.save(); 116 | ctx.fillStyle = style; 117 | ctx.fillRect(0, 0, canvas.width, canvas.height); 118 | ctx.restore(); 119 | } 120 | 121 | function drawImage(ctx, image, x, y) { 122 | ctx.drawImage(image, x, y); 123 | } 124 | 125 | function generateDataUrl(canvas) { 126 | try { 127 | var url = canvas.toDataURL('png'); 128 | return url; 129 | } catch (e) { 130 | throw new Error('当前浏览器版本不支持导出 PNG 功能,请尝试升级到最新版本!'); 131 | } 132 | } 133 | 134 | function drawSVG(minder) { 135 | var mind = editor.minder.exportJson(); 136 | if (typeof (window.canvg) != 'undefined') { 137 | return new Promise(function (resolve) { 138 | window.canvg(canvas, svgInfo.xml, { 139 | ignoreMouse: true, 140 | ignoreAnimation: true, 141 | ignoreDimensions: true, 142 | ignoreClear: true, 143 | offsetX: padding, 144 | offsetY: padding, 145 | renderCallback: function () { 146 | downloadImage(generateDataUrl(canvas), mind.root.data.text); 147 | } 148 | }); 149 | }); 150 | } else { 151 | return loadImage(svgDataUrl).then(function (svgImage) { 152 | drawImage(ctx, svgImage, padding, padding); 153 | DOMURL.revokeObjectURL(svgDataUrl); 154 | downloadImage(generateDataUrl(canvas), mind.root.data.text); 155 | }); 156 | } 157 | } 158 | 159 | if (bgUrl) { 160 | loadImage(bgUrl[1]).then(function (image) { 161 | fillBackground(ctx, ctx.createPattern(image, 'repeat')); 162 | drawSVG(minder); 163 | }); 164 | } else { 165 | fillBackground(ctx, bgColor.toString()); 166 | drawSVG(minder); 167 | } 168 | } 169 | 170 | export { 171 | exportPNGImage 172 | } 173 | -------------------------------------------------------------------------------- /src/script/runtime/jumping.js: -------------------------------------------------------------------------------- 1 | define(function (require, exports, module) { 2 | 3 | var Hotbox = require('../hotbox'); 4 | 5 | // Nice: http://unixpapa.com/js/key.html 6 | function isIntendToInput(e) { 7 | if (e.ctrlKey || e.metaKey || e.altKey) return false; 8 | 9 | // a-zA-Z 10 | if (e.keyCode >= 65 && e.keyCode <= 90) return true; 11 | 12 | // 0-9 以及其上面的符号 13 | if (e.keyCode >= 48 && e.keyCode <= 57) return true; 14 | 15 | // 小键盘区域 (除回车外) 16 | if (e.keyCode != 108 && e.keyCode >= 96 && e.keyCode <= 111) return true; 17 | 18 | // 小键盘区域 (除回车外) 19 | // @yinheli from pull request 20 | if (e.keyCode != 108 && e.keyCode >= 96 && e.keyCode <= 111) return true; 21 | 22 | // 输入法 23 | if (e.keyCode == 229 || e.keyCode === 0) return true; 24 | 25 | return false; 26 | } 27 | /** 28 | * @Desc: 下方使用receiver.enable()和receiver.disable()通过 29 | * 修改div contenteditable属性的hack来解决开启热核后依然无法屏蔽浏览器输入的bug; 30 | * 特别: win下FF对于此种情况必须要先blur在focus才能解决,但是由于这样做会导致用户 31 | * 输入法状态丢失,因此对FF暂不做处理 32 | * @Editor: Naixor 33 | * @Date: 2015.09.14 34 | */ 35 | function JumpingRuntime() { 36 | var fsm = this.fsm; 37 | var minder = this.minder; 38 | var receiver = this.receiver; 39 | var container = this.container; 40 | var receiverElement = receiver.element; 41 | var hotbox = this.hotbox; 42 | 43 | // normal -> * 44 | receiver.listen('normal', function (e) { 45 | // 为了防止处理进入edit模式而丢失处理的首字母,此时receiver必须为enable 46 | receiver.enable(); 47 | // normal -> hotbox 48 | if (e.is('Space')) { 49 | e.preventDefault(); 50 | // safari下Space触发hotbox,然而这时Space已在receiver上留下作案痕迹,因此抹掉 51 | if (kity.Browser.safari) { 52 | receiverElement.innerHTML = ''; 53 | } 54 | return fsm.jump('hotbox', 'space-trigger'); 55 | } 56 | 57 | /** 58 | * check 59 | * @editor Naixor 60 | * @Date 2015-12-2 61 | */ 62 | switch (e.type) { 63 | case 'keydown': { 64 | if (minder.getSelectedNode()) { 65 | if (isIntendToInput(e)) { 66 | return fsm.jump('input', 'user-input'); 67 | }; 68 | } else { 69 | receiverElement.innerHTML = ''; 70 | } 71 | // normal -> normal shortcut 72 | fsm.jump('normal', 'shortcut-handle', e); 73 | break; 74 | } 75 | case 'keyup': { 76 | break; 77 | } 78 | default: {} 79 | } 80 | }); 81 | 82 | // hotbox -> normal 83 | receiver.listen('hotbox', function (e) { 84 | receiver.disable(); 85 | e.preventDefault(); 86 | var handleResult = hotbox.dispatch(e); 87 | if (hotbox.state() == Hotbox.STATE_IDLE && fsm.state() == 'hotbox') { 88 | return fsm.jump('normal', 'hotbox-idle'); 89 | } 90 | }); 91 | 92 | // input => normal 93 | receiver.listen('input', function (e) { 94 | receiver.enable(); 95 | if (e.type == 'keydown') { 96 | if (e.is('Enter')) { 97 | e.preventDefault(); 98 | return fsm.jump('normal', 'input-commit'); 99 | } 100 | if (e.is('Esc')) { 101 | e.preventDefault(); 102 | return fsm.jump('normal', 'input-cancel'); 103 | } 104 | if (e.is('Tab') || e.is('Shift + Tab')) { 105 | e.preventDefault(); 106 | } 107 | } else if (e.type == 'keyup' && e.is('Esc')) { 108 | e.preventDefault(); 109 | return fsm.jump('normal', 'input-cancel'); 110 | } 111 | }); 112 | 113 | 114 | ////////////////////////////////////////////// 115 | /// 右键呼出热盒 116 | /// 判断的标准是:按下的位置和结束的位置一致 117 | ////////////////////////////////////////////// 118 | var downX, downY; 119 | var MOUSE_RB = 2; // 右键 120 | 121 | container.addEventListener('mousedown', function (e) { 122 | if (e.button == MOUSE_RB) { 123 | e.preventDefault(); 124 | } 125 | if (fsm.state() == 'hotbox') { 126 | hotbox.active(Hotbox.STATE_IDLE); 127 | fsm.jump('normal', 'blur'); 128 | } else if (fsm.state() == 'normal' && e.button == MOUSE_RB) { 129 | downX = e.clientX; 130 | downY = e.clientY; 131 | } 132 | }, false); 133 | 134 | container.addEventListener('mousewheel', function (e) { 135 | if (fsm.state() == 'hotbox') { 136 | hotbox.active(Hotbox.STATE_IDLE); 137 | fsm.jump('normal', 'mousemove-blur'); 138 | } 139 | }, false); 140 | 141 | container.addEventListener('contextmenu', function (e) { 142 | e.preventDefault(); 143 | }); 144 | 145 | container.addEventListener('mouseup', function (e) { 146 | if (fsm.state() != 'normal') { 147 | return; 148 | } 149 | if (e.button != MOUSE_RB || e.clientX != downX || e.clientY != downY) { 150 | return; 151 | } 152 | if (!minder.getSelectedNode()) { 153 | return; 154 | } 155 | fsm.jump('hotbox', 'content-menu'); 156 | }, false); 157 | 158 | // 阻止热盒事件冒泡,在热盒正确执行前导致热盒关闭 159 | hotbox.$element.addEventListener('mousedown', function (e) { 160 | e.stopPropagation(); 161 | }); 162 | } 163 | 164 | return module.exports = JumpingRuntime; 165 | }); 166 | -------------------------------------------------------------------------------- /demo/test/dev-test.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 191 | 192 | 195 | -------------------------------------------------------------------------------- /src/script/runtime/history.js: -------------------------------------------------------------------------------- 1 | define(function (require, exports, module) { 2 | 3 | function HistoryRuntime() { 4 | var minder = this.minder; 5 | var hotbox = this.hotbox; 6 | var {isDisableNode} = require('../tool/utils'); 7 | var {t} = require("../../locale"); 8 | 9 | var MAX_HISTORY = 100; 10 | 11 | var lastSnap; 12 | var patchLock; 13 | var undoDiffs; 14 | var redoDiffs; 15 | 16 | function reset() { 17 | undoDiffs = []; 18 | redoDiffs = []; 19 | lastSnap = minder.exportJson(); 20 | } 21 | 22 | var _objectKeys = (function () { 23 | if (Object.keys) 24 | return Object.keys; 25 | 26 | return function (o) { 27 | var keys = []; 28 | for (var i in o) { 29 | if (o.hasOwnProperty(i)) { 30 | keys.push(i); 31 | } 32 | } 33 | return keys; 34 | }; 35 | })(); 36 | 37 | function escapePathComponent(str) { 38 | if (str.indexOf('/') === -1 && str.indexOf('~') === -1) 39 | return str; 40 | return str.replace(/~/g, '~0').replace(/\//g, '~1'); 41 | } 42 | 43 | function deepClone(obj) { 44 | if (typeof obj === "object") { 45 | return JSON.parse(JSON.stringify(obj)); 46 | } else { 47 | return obj; 48 | } 49 | } 50 | 51 | function _generate(mirror, obj, patches, path) { 52 | var newKeys = _objectKeys(obj); 53 | var oldKeys = _objectKeys(mirror); 54 | var changed = false; 55 | var deleted = false; 56 | 57 | for (var t = oldKeys.length - 1; t >= 0; t--) { 58 | var key = oldKeys[t]; 59 | var oldVal = mirror[key]; 60 | if (obj.hasOwnProperty(key)) { 61 | var newVal = obj[key]; 62 | if (typeof oldVal == "object" && oldVal != null && typeof newVal == "object" && newVal != null) { 63 | _generate(oldVal, newVal, patches, path + "/" + escapePathComponent(key)); 64 | } else { 65 | if (oldVal != newVal) { 66 | changed = true; 67 | patches.push({ 68 | op: "replace", 69 | path: path + "/" + escapePathComponent(key), 70 | value: deepClone(newVal) 71 | }); 72 | } 73 | } 74 | } else { 75 | patches.push({ 76 | op: "remove", 77 | path: path + "/" + escapePathComponent(key) 78 | }); 79 | deleted = true; // property has been deleted 80 | } 81 | } 82 | 83 | if (!deleted && newKeys.length == oldKeys.length) { 84 | return; 85 | } 86 | 87 | for (var t = 0; t < newKeys.length; t++) { 88 | var key = newKeys[t]; 89 | if (!mirror.hasOwnProperty(key)) { 90 | patches.push({ 91 | op: "add", 92 | path: path + "/" + escapePathComponent(key), 93 | value: deepClone(obj[key]) 94 | }); 95 | } 96 | } 97 | } 98 | 99 | function jsonDiff(tree1, tree2) { 100 | var patches = []; 101 | _generate(tree1, tree2, patches, ''); 102 | return patches; 103 | } 104 | 105 | function makeUndoDiff() { 106 | var headSnap = minder.exportJson(); 107 | var diff = jsonDiff(headSnap, lastSnap); 108 | if (diff.length) { 109 | undoDiffs.push(diff); 110 | while (undoDiffs.length > MAX_HISTORY) { 111 | undoDiffs.shift(); 112 | } 113 | lastSnap = headSnap; 114 | return true; 115 | } 116 | } 117 | 118 | function makeRedoDiff() { 119 | var revertSnap = minder.exportJson(); 120 | redoDiffs.push(jsonDiff(revertSnap, lastSnap)); 121 | lastSnap = revertSnap; 122 | } 123 | 124 | function undo() { 125 | patchLock = true; 126 | var undoDiff = undoDiffs.pop(); 127 | if (undoDiff) { 128 | minder.applyPatches(undoDiff); 129 | makeRedoDiff(); 130 | } 131 | patchLock = false; 132 | } 133 | 134 | function redo() { 135 | patchLock = true; 136 | var redoDiff = redoDiffs.pop(); 137 | if (redoDiff) { 138 | minder.applyPatches(redoDiff); 139 | makeUndoDiff(); 140 | } 141 | patchLock = false; 142 | } 143 | 144 | function changed() { 145 | if (patchLock) 146 | return; 147 | if (makeUndoDiff()) 148 | redoDiffs = []; 149 | } 150 | 151 | function hasUndo() { 152 | return !!undoDiffs.length; 153 | } 154 | 155 | function hasRedo() { 156 | return !!redoDiffs.length; 157 | } 158 | 159 | function updateSelection(e) { 160 | if (!patchLock) 161 | return; 162 | var patch = e.patch; 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 | } 174 | } 175 | 176 | this.history = { 177 | reset: reset, 178 | undo: undo, 179 | redo: redo, 180 | hasUndo: hasUndo, 181 | hasRedo: hasRedo 182 | }; 183 | reset(); 184 | minder.on('contentchange', changed); 185 | minder.on('import', reset); 186 | minder.on('patch', updateSelection); 187 | 188 | var main = hotbox.state('main'); 189 | main.button({ 190 | position: 'bottom', 191 | label: t('minder.main.history.undo'), 192 | key: 'Ctrl + Z', 193 | enable: function() { 194 | if (isDisableNode(minder)) { 195 | return false; 196 | } 197 | return hasUndo; 198 | }, 199 | action: undo, 200 | next: 'idle' 201 | }); 202 | main.button({ 203 | position: 'bottom', 204 | label: t('minder.main.history.redo'), 205 | key: 'Ctrl + Y', 206 | enable: function() { 207 | if (isDisableNode(minder)) { 208 | return false; 209 | } 210 | return hasRedo; 211 | }, 212 | action: redo, 213 | next: 'idle' 214 | }); 215 | } 216 | 217 | // window.diff = jsonDiff; 218 | 219 | return module.exports = HistoryRuntime; 220 | }); 221 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue-MindEditor based on fex-team/kityminder-core 2 | > 该项目是参考 [vue-mindeditor](https://github.com/fudax/vue-mindeditor) 以及 [kityminder-editor](https://github.com/fex-team/kityminder-editor) 3 | > 源码,基于 [kityminder-core](https://github.com/fex-team/kityminder-core) 实现 4 | 5 | ## install 6 | ``` bash 7 | npm install vue-minder-editor-plus --save 8 | ``` 9 | 10 | ## Usage 11 | ```javascript 12 | import vueMinderEditor from 'vue-minder-editor-plus' 13 | import Vue from 'vue' 14 | Vue.use(vueMinderEditor) 15 | ``` 16 | 17 | ## component 18 | ```html 19 | 24 | 25 | 90 | ``` 91 | 92 | ## Build Setup 93 | 94 | ``` bash 95 | # install npm dependencies 96 | npm install 97 | 98 | # serve with hot reload at localhost:8088 99 | npm run dev 100 | 101 | # build for plugin with minification 102 | npm run build 103 | 104 | # License 105 | BSD-3-Clause License 106 | ``` 107 | ## 国际化 108 | ``` 109 | // 方式一 110 | import locale from '/src/locale/lang/en-US' 111 | Vue.use(vueMinderEditorPlus, { 112 | locale 113 | }); 114 | 115 | // 方式二 116 | import lang from '/src/locale/lang/en-US' 117 | import locale from '/src/locale' 118 | locale.use(lang) 119 | Vue.use(vueMinderEditorPlus); 120 | 121 | // 方式三 122 | import Vue from 'vue'; 123 | import VueI18n from 'vue-i18n'; 124 | import enLocale from 'vue-minder-editor-plus/src/locale/lang/en-US'; 125 | import zhLocale from 'vue-minder-editor-plus/src/locale/lang/zh-CN'; 126 | import vueMinderEditor from 'vue-minder-editor-plus'; 127 | 128 | const messages = { 129 | en: { 130 | message: 'hello', 131 | ...enLocale 132 | }, 133 | zh: { 134 | message: '你好', 135 | ...zhLocale 136 | } 137 | } 138 | 139 | Vue.use(VueI18n); 140 | 141 | const i18n = new VueI18n({ 142 | locale: 'en', // set locale 143 | messages, // set locale messages 144 | }) 145 | 146 | Vue.use(vueMinderEditor, { 147 | i18n: (key, value) => i18n.t(key, value) 148 | }); 149 | ``` 150 | 151 | ## Props 152 | > 以下配置部分为 kityminder-core 扩展的功能,kityminder-core 本身的 minder 对象提供了丰富的功能,使用该组件时可通过 window.minder 对象获取 minder 对象具体的使用方法,可以参考它的文档扩展 [kityminder-core wiki](https://github.com/fex-team/kityminder-core/wiki) 153 | 154 | ### 基础配置 155 | 156 | #### importJson
157 | type Object
158 | Default: null 159 | 160 | 需要脑图解析的 js 对象,参数详情可参考上文 demo,或者调用 minder.exportJson() 查看具体参数 161 | 162 | #### height
163 | type: Number
164 | default: 500 165 | 166 | 显示高度,默认 500px 167 | 168 | #### disabled
169 | type: Boolean
170 | default: null 171 | 172 | 是否禁止编辑 173 | 174 | #### defaultMold 175 | type: Number
176 | default: 3 177 | 178 | 外观设置中样式的默认值 179 | 180 | ### 启用配置 181 | 182 | #### sequenceEnable 183 | type: Boolean
184 | default: true 185 | 186 | 是否启用优先级功能 187 | 188 | #### tagEnable 189 | type: Boolean
190 | default: true 191 | 192 | 是否启用标签功能 193 | 194 | #### progressEnable 195 | type: Boolean
196 | default: true 197 | 198 | 是否启用完成进度功能 199 | 200 | #### moveEnable 201 | type: Boolean
202 | default: true 203 | 204 | 是否启用上移下移功能 205 | 206 | #### viewMenuEnable 207 | type: Boolean
208 | default: true 209 | 210 | 是否启用外观样式 211 | 212 | #### moldEnable 213 | type: Boolean
214 | default: true 215 | 216 | 是否启用展示模式 217 | 218 | #### arrangeEnable 219 | type: Boolean
220 | default: true 221 | 222 | 是否启用整理布局 223 | 224 | #### styleEnable 225 | type: Boolean
226 | default: true 227 | 228 | 是否启用样式编辑 229 | 230 | #### fontEnable 231 | type: Boolean
232 | default: true 233 | 234 | 是否启用字体编辑 235 | 236 | ### 优先级配置 237 | #### priorityCount
238 | type Number
239 | default: 4 240 | 241 | 优先级最大显示数量,最多支持显示 9 个级别 242 | 243 | #### priorityStartWithZero
244 | type: Boolean
245 | default: true 246 | 247 | 优先级是否从 0 开始 248 | 249 | #### priorityPrefix 250 | type: String
251 | default: 'P' 252 | 优先级显示的前缀 253 | 254 | #### priorityDisableCheck 255 | type: Function
256 | default: null 257 | 258 | 优先级设置的回调函数,如果返回 false 则无法设置优先级 259 | 260 | ### 标签配置 261 | #### tags 262 | type: Array
263 | default: [] 264 | 265 | 标签选项 266 | 267 | #### distinctTags 268 | type: Array
269 | default: [] 270 | 271 | 定义排他标签,比如 ['tag1','tag2'] ,则 tag1 不能和 tag2 共存 272 | 273 | #### tagDisableCheck 274 | type: Function
275 | default: null 276 | 277 | 菜单栏是否允许打标签的回调函数,返回 false 则不允许打标签 278 | 279 | #### tagEditCheck 280 | type: Function
281 | default: null 282 | 283 | 打标签时的回调函数,返回 false 则打标签不成功 284 | 285 | 286 | 287 | -------------------------------------------------------------------------------- /src/script/runtime/clipboard.js: -------------------------------------------------------------------------------- 1 | define(function (require, exports, module) { 2 | 3 | function ClipboardRuntime() { 4 | var minder = this.minder; 5 | var Data = window.kityminder.data; 6 | 7 | var {markDeleteNode, resetNodes} = require('../tool/utils'); 8 | 9 | 10 | if (!minder.supportClipboardEvent || kity.Browser.gecko) { 11 | return; 12 | }; 13 | 14 | var fsm = this.fsm; 15 | var receiver = this.receiver; 16 | var MimeType = this.MimeType; 17 | 18 | var kmencode = MimeType.getMimeTypeProtocol('application/km'), 19 | decode = Data.getRegisterProtocol('json').decode; 20 | var _selectedNodes = []; 21 | 22 | /* 23 | * 增加对多节点赋值粘贴的处理 24 | */ 25 | function encode(nodes) { 26 | var _nodes = []; 27 | for (var i = 0, l = nodes.length; i < l; i++) { 28 | _nodes.push(minder.exportNode(nodes[i])); 29 | } 30 | return kmencode(Data.getRegisterProtocol('json').encode(_nodes)); 31 | } 32 | 33 | var beforeCopy = function (e) { 34 | if (document.activeElement == receiver.element) { 35 | var clipBoardEvent = e; 36 | var state = fsm.state(); 37 | 38 | switch (state) { 39 | case 'input': { 40 | break; 41 | } 42 | case 'normal': { 43 | var nodes = [].concat(minder.getSelectedNodes()); 44 | if (nodes.length) { 45 | // 这里由于被粘贴复制的节点的id信息也都一样,故做此算法 46 | // 这里有个疑问,使用node.getParent()或者node.parent会离奇导致出现非选中节点被渲染成选中节点,因此使用isAncestorOf,而没有使用自行回溯的方式 47 | if (nodes.length > 1) { 48 | var targetLevel; 49 | nodes.sort(function (a, b) { 50 | return a.getLevel() - b.getLevel(); 51 | }); 52 | targetLevel = nodes[0].getLevel(); 53 | if (targetLevel !== nodes[nodes.length - 1].getLevel()) { 54 | var plevel, pnode, 55 | idx = 0, 56 | l = nodes.length, 57 | pidx = l - 1; 58 | 59 | pnode = nodes[pidx]; 60 | 61 | while (pnode.getLevel() !== targetLevel) { 62 | idx = 0; 63 | while (idx < l && nodes[idx].getLevel() === targetLevel) { 64 | if (nodes[idx].isAncestorOf(pnode)) { 65 | nodes.splice(pidx, 1); 66 | break; 67 | } 68 | idx++; 69 | } 70 | pidx--; 71 | pnode = nodes[pidx]; 72 | } 73 | }; 74 | }; 75 | var str = encode(nodes); 76 | clipBoardEvent.clipboardData.setData('text/plain', str); 77 | } 78 | e.preventDefault(); 79 | break; 80 | } 81 | } 82 | } 83 | } 84 | 85 | var beforeCut = function (e) { 86 | if (document.activeElement == receiver.element) { 87 | if (minder.getStatus() !== 'normal') { 88 | e.preventDefault(); 89 | return; 90 | }; 91 | 92 | var clipBoardEvent = e; 93 | var state = fsm.state(); 94 | 95 | switch (state) { 96 | case 'input': { 97 | break; 98 | } 99 | case 'normal': { 100 | markDeleteNode(minder); 101 | var nodes = minder.getSelectedNodes(); 102 | if (nodes.length) { 103 | clipBoardEvent.clipboardData.setData('text/plain', encode(nodes)); 104 | minder.execCommand('removenode'); 105 | } 106 | e.preventDefault(); 107 | break; 108 | } 109 | } 110 | }; 111 | } 112 | 113 | var beforePaste = function (e) { 114 | if (document.activeElement == receiver.element) { 115 | if (minder.getStatus() !== 'normal') { 116 | e.preventDefault(); 117 | return; 118 | }; 119 | 120 | var clipBoardEvent = e; 121 | var state = fsm.state(); 122 | var textData = clipBoardEvent.clipboardData.getData('text/plain'); 123 | 124 | switch (state) { 125 | case 'input': { 126 | // input状态下如果格式为application/km则不进行paste操作 127 | if (!MimeType.isPureText(textData)) { 128 | e.preventDefault(); 129 | return; 130 | }; 131 | break; 132 | } 133 | case 'normal': { 134 | /* 135 | * 针对normal状态下通过对选中节点粘贴导入子节点文本进行单独处理 136 | */ 137 | var sNodes = minder.getSelectedNodes(); 138 | 139 | if (MimeType.whichMimeType(textData) === 'application/km') { 140 | var nodes = decode(MimeType.getPureText(textData)); 141 | resetNodes(nodes); 142 | var _node; 143 | sNodes.forEach(function (node) { 144 | // 由于粘贴逻辑中为了排除子节点重新排序导致逆序,因此复制的时候倒过来 145 | for (var i = nodes.length - 1; i >= 0; i--) { 146 | _node = minder.createNode(null, node); 147 | minder.importNode(_node, nodes[i]); 148 | _selectedNodes.push(_node); 149 | node.appendChild(_node); 150 | } 151 | }); 152 | minder.select(_selectedNodes, true); 153 | _selectedNodes = []; 154 | 155 | minder.refresh(); 156 | } else if (clipBoardEvent.clipboardData && clipBoardEvent.clipboardData.items[0].type.indexOf('image') > -1) { 157 | var imageFile = clipBoardEvent.clipboardData.items[0].getAsFile(); 158 | var serverService = angular.element(document.body).injector().get('server'); 159 | 160 | return serverService.uploadImage(imageFile).then(function (json) { 161 | var resp = json.data; 162 | if (resp.errno === 0) { 163 | minder.execCommand('image', resp.data.url); 164 | } 165 | }); 166 | } else { 167 | sNodes.forEach(function (node) { 168 | minder.Text2Children(node, textData); 169 | }); 170 | } 171 | e.preventDefault(); 172 | break; 173 | } 174 | } 175 | // 触发命令监听 176 | minder.execCommand('paste'); 177 | } 178 | } 179 | 180 | /** 181 | * 由editor的receiver统一处理全部事件,包括clipboard事件 182 | * @Editor: Naixor 183 | * @Date: 2015.9.24 184 | */ 185 | document.addEventListener('copy', beforeCopy); 186 | document.addEventListener('cut', beforeCut); 187 | document.addEventListener('paste', beforePaste); 188 | } 189 | 190 | return module.exports = ClipboardRuntime; 191 | }); 192 | -------------------------------------------------------------------------------- /src/components/menu/view/fontOperation.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 261 | 262 | 271 | -------------------------------------------------------------------------------- /src/style/header.scss: -------------------------------------------------------------------------------- 1 | @import "mixin.scss"; 2 | @import "dropdown-list.scss"; 3 | header { 4 | font-size: 12px; 5 | & > ul { 6 | display: flex; 7 | align-items: center; 8 | height: 30px; 9 | margin: 0; 10 | padding: 0; 11 | background-color: #e1e1e1; 12 | li { 13 | line-height: 30px; 14 | display: inline-flex; 15 | width: 80px; 16 | height: 100%; 17 | list-style: none; 18 | a { 19 | font-size: 14px; 20 | width: inherit; 21 | text-align: center; 22 | text-decoration: none; 23 | color: #337ab7; 24 | } 25 | a:hover, 26 | a:focus { 27 | color: #23527c; 28 | } 29 | } 30 | li.selected { 31 | background: #fff; 32 | a { 33 | color: #000; 34 | } 35 | } 36 | } 37 | } 38 | 39 | .mind-tab-panel { 40 | width: 100%; 41 | height: 100%; 42 | .menu-container { 43 | display: flex; 44 | height: inherit; 45 | & > div { 46 | display: inline-flex; 47 | overflow: hidden; 48 | align-items: center; 49 | flex-wrap: wrap; 50 | height: 100%; 51 | border-right: 1px dashed #eee; 52 | } 53 | & > div:last-of-type { 54 | border-right: none; 55 | } 56 | } 57 | .menu-btn { 58 | display: inline-flex; 59 | cursor: pointer; 60 | @include flexcenter; 61 | } 62 | .menu-btn:not([disabled]):hover { 63 | background-color: $btn-hover-color; 64 | } 65 | .tab-icons { 66 | display: inline-block; 67 | width: 20px; 68 | height: 20px; 69 | } 70 | .do-group { 71 | width: 40px; 72 | height: 100%; 73 | padding: 0 5px; 74 | p { 75 | height: 50%; 76 | margin: 0; 77 | @include flexcenter; 78 | } 79 | .undo i { 80 | background-position: 0 -1240px; 81 | } 82 | .redo i { 83 | background-position: 0 -1220px; 84 | } 85 | } 86 | .insert-group { 87 | width: 110px; 88 | & > div { 89 | height: 50%; 90 | margin: 0 5px; 91 | } 92 | .insert-sibling-box { 93 | i { 94 | background-position: 0 -20px; 95 | } 96 | } 97 | .insert-parent-box { 98 | i { 99 | background-position: 0 -40px; 100 | } 101 | } 102 | } 103 | .edit-del-group, 104 | .move-group { 105 | width: 70px; 106 | @include flexcenter; 107 | } 108 | .move-group { 109 | .move-up { 110 | i { 111 | background-position: 0 -280px; 112 | } 113 | } 114 | .move-down { 115 | i { 116 | background-position: 0 -300px; 117 | } 118 | } 119 | } 120 | .edit-del-group { 121 | .edit { 122 | i { 123 | background-position: 0 -60px; 124 | } 125 | } 126 | .del { 127 | i { 128 | background-position: 0 -80px; 129 | } 130 | } 131 | } 132 | .attachment-group { 133 | width: 185px; 134 | @include flexcenter; 135 | .el-dropdown-link { 136 | font-size: 12px; 137 | } 138 | button { 139 | font-size: inherit; 140 | width: 45px; 141 | height: 20px; 142 | padding: 0; 143 | background-repeat: no-repeat; 144 | background-position: right; 145 | @include button; 146 | @include flexcenter; 147 | span { 148 | margin-left: 15px; 149 | } 150 | } 151 | button:hover { 152 | background-color: $btn-hover-color; 153 | } 154 | & > div { 155 | font-size: inherit; 156 | flex-wrap: wrap; 157 | width: 60px; 158 | height: 100%; 159 | @include flexcenter; 160 | } 161 | .insert { 162 | height: 25px; 163 | background-repeat: no-repeat; 164 | } 165 | .link { 166 | .insert { 167 | background-position: 50% -100px; 168 | } 169 | } 170 | .img { 171 | .insert { 172 | background-position: 50% -125px; 173 | } 174 | } 175 | .remark { 176 | .insert { 177 | background-position: 50% -1150px; 178 | } 179 | } 180 | .el-dropdown { 181 | cursor: default; 182 | } 183 | } 184 | .progress-group, 185 | .sequence-group { 186 | width: 135px; 187 | @include flexcenter; 188 | ul { 189 | width: 120px; 190 | margin: 0; 191 | padding: 0; 192 | list-style: none; 193 | li { 194 | display: inline-block; 195 | width: 20px; 196 | height: 20px; 197 | margin: 2px; 198 | } 199 | } 200 | } 201 | .sequence-group { 202 | @for $i from 0 through 9 { 203 | .sequence-#{$i} { 204 | background-position: 0 -20px * (-1 + $i); 205 | } 206 | } 207 | } 208 | .progress-group { 209 | @for $i from 0 through 9 { 210 | .progress-#{$i} { 211 | background-position: 0 -20px * (-1 + $i); 212 | } 213 | } 214 | } 215 | .mold-group { 216 | width: 80px; 217 | @include flexcenter; 218 | @for $i from 1 through 6 { 219 | .mold-#{$i} { 220 | background-position: (1-$i) * 50px 0; 221 | } 222 | } 223 | .dropdown-toggle { 224 | display: block; 225 | width: 50px; 226 | height: 50px; 227 | margin: 5px 0 0 auto; 228 | span { 229 | display: inline-block; 230 | @include block; 231 | i { 232 | position: absolute; 233 | right: -20px; 234 | bottom: -5px; 235 | } 236 | } 237 | } 238 | } 239 | .arrange-group { 240 | width: 65px; 241 | .arrange { 242 | flex-wrap: wrap; 243 | @include flexcenter; 244 | } 245 | .tab-icons { 246 | display: inline-block; 247 | width: 25px; 248 | height: 25px; 249 | margin: 0; 250 | background-repeat: no-repeat; 251 | background-position: 0 -150px; 252 | } 253 | } 254 | .style-group { 255 | width: 150px; 256 | .clear-style-btn { 257 | flex-wrap: wrap; 258 | width: 65px; 259 | @include flexcenter; 260 | .tab-icons { 261 | display: inline-block; 262 | width: 25px; 263 | height: 25px; 264 | margin: 0; 265 | background-repeat: no-repeat; 266 | background-position: 0 -175px; 267 | } 268 | } 269 | .copy-paste-panel { 270 | width: 70px; 271 | .tab-icons { 272 | display: inline-block; 273 | width: 20px; 274 | height: 20px; 275 | } 276 | .copy-style { 277 | .tab-icons { 278 | background-position: 0 -200px; 279 | } 280 | } 281 | .paste-style { 282 | .tab-icons { 283 | background-position: 0 -220px; 284 | } 285 | } 286 | } 287 | } 288 | .font-group { 289 | width: 250px; 290 | * { 291 | font-size: 12px; 292 | } 293 | input { 294 | height: 30px !important; 295 | } 296 | .font-family-select { 297 | input { 298 | width: 150px; 299 | } 300 | .el-input__suffix { 301 | top: 12px; 302 | } 303 | } 304 | .font-size-select { 305 | input { 306 | width: 80px; 307 | } 308 | .el-input__suffix { 309 | top: 12px; 310 | } 311 | margin-left: 5px; 312 | } 313 | .font-bold, 314 | .font-italic { 315 | display: inline-block; 316 | width: 20px; 317 | height: 20px; 318 | margin: 0 3px; 319 | } 320 | .font-bold { 321 | background-position: 0 -242px; 322 | } 323 | .font-italic { 324 | background-position: 0 -262px; 325 | } 326 | } 327 | .expand-group, 328 | .selection-group { 329 | width: 60px; 330 | button { 331 | border: none; 332 | outline: none; 333 | } 334 | @include flexcenter; 335 | margin: 0 5px; 336 | span { 337 | font-size: 12px; 338 | } 339 | } 340 | .expand-group { 341 | .expand { 342 | width: 40px; 343 | height: 25px; 344 | background-position: center -995px; 345 | } 346 | i { 347 | font-size: 12px; 348 | } 349 | } 350 | .selection-group { 351 | .selection { 352 | width: 40px; 353 | height: 25px; 354 | background-position: 7px -1175px; 355 | } 356 | i { 357 | font-size: 12px; 358 | } 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /src/components/main/navigator.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 302 | 307 | --------------------------------------------------------------------------------