From 8c114dac02a0696e68fb61ba463d672811dffecb Mon Sep 17 00:00:00 2001 From: wanglin2 <1013335014@qq.com> Date: Fri, 24 Feb 2023 17:14:24 +0800 Subject: [PATCH] =?UTF-8?q?Feature=EF=BC=9A=E6=94=AF=E6=8C=81=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E5=AF=8C=E6=96=87=E6=9C=AC=E7=BC=96=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple-mind-map/index.js | 43 +- simple-mind-map/package-lock.json | 451 +++++++++++++++++- simple-mind-map/package.json | 4 +- simple-mind-map/src/Export.js | 31 +- simple-mind-map/src/Node.js | 44 +- simple-mind-map/src/Render.js | 14 +- simple-mind-map/src/RichText.js | 428 +++++++++++++++++ simple-mind-map/src/TextEdit.js | 10 +- simple-mind-map/src/css/quill.css | 9 + web/src/lang/en_us.js | 15 +- web/src/lang/zh_cn.js | 15 +- web/src/pages/Edit/components/BaseStyle.vue | 24 +- web/src/pages/Edit/components/Edit.vue | 48 +- web/src/pages/Edit/components/Export.vue | 68 ++- .../pages/Edit/components/RichTextToolbar.vue | 256 ++++++++++ web/src/pages/Edit/components/Style.vue | 11 +- web/src/store.js | 4 +- 17 files changed, 1422 insertions(+), 53 deletions(-) create mode 100644 simple-mind-map/src/RichText.js create mode 100644 simple-mind-map/src/css/quill.css create mode 100644 web/src/pages/Edit/components/RichTextToolbar.vue diff --git a/simple-mind-map/index.js b/simple-mind-map/index.js index 492c2317..1505df69 100644 --- a/simple-mind-map/index.js +++ b/simple-mind-map/index.js @@ -120,9 +120,7 @@ class MindMap { // 注册插件 MindMap.pluginList.forEach((plugin) => { - this[plugin.instanceName] = new plugin({ - mindMap: this - }) + this.initPlugin(plugin) }) // 初始渲染 @@ -374,14 +372,51 @@ class MindMap { scaleY: origTransform.scaleY // 思维导图图形的垂直缩放值 } } + + // 添加插件 + addPlugin(plugin, opt) { + let index = MindMap.hasPlugin(plugin) + if (index === -1) { + MindMap.usePlugin(plugin, opt) + this.initPlugin(plugin) + } + } + + // 移除插件 + removePlugin(plugin) { + let index = MindMap.hasPlugin(plugin) + if (index !== -1) { + MindMap.pluginList.splice(index, 1) + if (this[plugin.instanceName]) { + if (this[plugin.instanceName].beforePluginRemove) { + this[plugin.instanceName].beforePluginRemove() + } + delete this[plugin.instanceName] + } + } + } + + // 实例化插件 + initPlugin(plugin) { + this[plugin.instanceName] = new plugin({ + mindMap: this, + pluginOpt: plugin.pluginOpt + }) + } } // 插件列表 MindMap.pluginList = [] -MindMap.usePlugin = (plugin) => { +MindMap.usePlugin = (plugin, opt = {}) => { + plugin.pluginOpt = opt MindMap.pluginList.push(plugin) return MindMap } +MindMap.hasPlugin = (plugin) => { + return MindMap.pluginList.findIndex((item) => { + return item === plugin + }) +} // 定义新主题 MindMap.defineTheme = (name, config = {}) => { diff --git a/simple-mind-map/package-lock.json b/simple-mind-map/package-lock.json index e1937183..bbd2adeb 100644 --- a/simple-mind-map/package-lock.json +++ b/simple-mind-map/package-lock.json @@ -1,19 +1,21 @@ { "name": "simple-mind-map", - "version": "0.2.21", + "version": "0.3.4", "lockfileVersion": 2, "requires": true, "packages": { "": { - "version": "0.2.21", + "version": "0.3.4", "license": "MIT", "dependencies": { "@svgdotjs/svg.js": "^3.0.16", "canvg": "^3.0.7", "deepmerge": "^1.5.2", "eventemitter3": "^4.0.7", + "html2canvas": "^1.4.1", "jspdf": "^2.5.1", "jszip": "^3.10.1", + "quill": "^1.3.6", "xml-js": "^1.6.11" }, "devDependencies": { @@ -225,7 +227,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", - "optional": true, "engines": { "node": ">= 0.6.0" } @@ -251,6 +252,18 @@ "node": ">= 0.4.0" } }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -294,6 +307,14 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -351,7 +372,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", - "optional": true, "dependencies": { "utrie": "^1.0.2" } @@ -373,6 +393,22 @@ } } }, + "node_modules/deep-equal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "dependencies": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -387,6 +423,21 @@ "node": ">=0.10.0" } }, + "node_modules/define-properties": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -586,12 +637,22 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-diff": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", + "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -671,6 +732,32 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -724,6 +811,17 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -733,11 +831,46 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/html2canvas": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", - "optional": true, "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" @@ -800,6 +933,35 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -830,6 +992,21 @@ "node": ">=8" } }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -969,6 +1146,29 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1030,6 +1230,11 @@ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, + "node_modules/parchment": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz", + "integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -1132,6 +1337,37 @@ } ] }, + "node_modules/quill": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.6.tgz", + "integrity": "sha512-K0mvhimWZN6s+9OQ249CH2IEPZ9JmkFuCQeHAOQax3EZ2nDJ3wfGh59mnlQaZV2i7u8eFarx6wAtvQKgShojug==", + "dependencies": { + "clone": "^2.1.1", + "deep-equal": "^1.0.1", + "eventemitter3": "^2.0.3", + "extend": "^3.0.1", + "parchment": "^1.1.4", + "quill-delta": "^3.6.2" + } + }, + "node_modules/quill-delta": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz", + "integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==", + "dependencies": { + "deep-equal": "^1.0.1", + "extend": "^3.0.2", + "fast-diff": "1.1.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/quill/node_modules/eventemitter3": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", + "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==" + }, "node_modules/raf": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", @@ -1159,6 +1395,22 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, + "node_modules/regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/regexpp": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", @@ -1336,7 +1588,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", - "optional": true, "dependencies": { "utrie": "^1.0.2" } @@ -1389,7 +1640,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", - "optional": true, "dependencies": { "base64-arraybuffer": "^1.0.2" } @@ -1593,8 +1843,7 @@ "base64-arraybuffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", - "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", - "optional": true + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==" }, "brace-expansion": { "version": "1.1.11", @@ -1611,6 +1860,15 @@ "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==" }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1642,6 +1900,11 @@ "supports-color": "^7.1.0" } }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==" + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1688,7 +1951,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", - "optional": true, "requires": { "utrie": "^1.0.2" } @@ -1702,6 +1964,19 @@ "ms": "2.1.2" } }, + "deep-equal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "requires": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + } + }, "deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1713,6 +1988,15 @@ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-1.5.2.tgz", "integrity": "sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==" }, + "define-properties": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -1860,12 +2144,22 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "fast-diff": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", + "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==" + }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -1933,6 +2227,26 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==" + }, + "get-intrinsic": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -1971,17 +2285,45 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "requires": { + "get-intrinsic": "^1.1.1" + } + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "requires": { + "has-symbols": "^1.0.2" + } + }, "html2canvas": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", - "optional": true, "requires": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" @@ -2029,6 +2371,23 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2050,6 +2409,15 @@ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -2168,6 +2536,20 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2214,6 +2596,11 @@ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, + "parchment": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz", + "integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==" + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -2275,6 +2662,36 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, + "quill": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.6.tgz", + "integrity": "sha512-K0mvhimWZN6s+9OQ249CH2IEPZ9JmkFuCQeHAOQax3EZ2nDJ3wfGh59mnlQaZV2i7u8eFarx6wAtvQKgShojug==", + "requires": { + "clone": "^2.1.1", + "deep-equal": "^1.0.1", + "eventemitter3": "^2.0.3", + "extend": "^3.0.1", + "parchment": "^1.1.4", + "quill-delta": "^3.6.2" + }, + "dependencies": { + "eventemitter3": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", + "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==" + } + } + }, + "quill-delta": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz", + "integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==", + "requires": { + "deep-equal": "^1.0.1", + "extend": "^3.0.2", + "fast-diff": "1.1.2" + } + }, "raf": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", @@ -2302,6 +2719,16 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, + "regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + } + }, "regexpp": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", @@ -2419,7 +2846,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", - "optional": true, "requires": { "utrie": "^1.0.2" } @@ -2463,7 +2889,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", - "optional": true, "requires": { "base64-arraybuffer": "^1.0.2" } diff --git a/simple-mind-map/package.json b/simple-mind-map/package.json index 812c681f..00cb8d25 100644 --- a/simple-mind-map/package.json +++ b/simple-mind-map/package.json @@ -1,6 +1,6 @@ { "name": "simple-mind-map", - "version": "0.3.4", + "version": "0.4.0", "description": "一个简单的web在线思维导图", "authors": [ { @@ -28,8 +28,10 @@ "canvg": "^3.0.7", "deepmerge": "^1.5.2", "eventemitter3": "^4.0.7", + "html2canvas": "^1.4.1", "jspdf": "^2.5.1", "jszip": "^3.10.1", + "quill": "^1.3.6", "xml-js": "^1.6.11" }, "keywords": [ diff --git a/simple-mind-map/src/Export.js b/simple-mind-map/src/Export.js index 765dcd8e..2ade1789 100644 --- a/simple-mind-map/src/Export.js +++ b/simple-mind-map/src/Export.js @@ -26,7 +26,7 @@ class Export { } // 获取svg数据 - async getSvgData() { + async getSvgData(domToImage) { let { svg, svgHTML } = this.mindMap.getSvgData() // 把图片的url转换成data:url类型,否则导出会丢失图片 let imageList = svg.find('image') @@ -36,9 +36,17 @@ class Export { item.attr('href', imgData) }) await Promise.all(task) + // 如果开启了富文本编辑,需要把svg中的dom元素转换成图片 + let nodeWithDomToImg = null + if (domToImage && this.mindMap.richText) { + let res = await this.mindMap.richText.handleSvgDomElements(svg) + nodeWithDomToImg = res.svg + svgHTML = res.svgHTML + } return { node: svg, - str: svgHTML + str: svgHTML, + nodeWithDomToImg } } @@ -123,7 +131,7 @@ class Export { * 方法2.把svg的图片提取出来再挨个绘制到canvas里,最后一起转换 */ async png() { - let { str } = await this.getSvgData() + let { str } = await this.getSvgData(true) // 转换成blob数据 let blob = new Blob([str], { type: 'image/svg+xml' @@ -191,8 +199,21 @@ class Export { } // 导出为svg - async svg(name) { - let { node } = await this.getSvgData() + // domToImage:是否将svg中的dom节点转换成图片的形式 + // plusCssText:附加的css样式,如果svg中存在dom节点,想要设置一些针对节点的样式可以通过这个参数传入 + async svg(name, domToImage = false, plusCssText) { + let { node, nodeWithDomToImg } = await this.getSvgData(domToImage) + // 开启了节点富文本编辑 + if (this.mindMap.richText) { + if (domToImage) { + node = nodeWithDomToImg + } else if (plusCssText) { + let foreignObjectList = node.find('foreignObject') + if (foreignObjectList.length > 0) { + foreignObjectList[0].add(SVG(``)) + } + } + } node.first().before(SVG(`${name}`)) await this.drawBackgroundToSvg(node) let str = node.svg() diff --git a/simple-mind-map/src/Node.js b/simple-mind-map/src/Node.js index bc05048b..671a5e11 100644 --- a/simple-mind-map/src/Node.js +++ b/simple-mind-map/src/Node.js @@ -1,7 +1,7 @@ import Style from './Style' import Shape from './Shape' import { resizeImgSize, asyncRun, measureText } from './utils' -import { Image, SVG, Circle, A, G, Rect, Text } from '@svgdotjs/svg.js' +import { Image, SVG, Circle, A, G, Rect, Text, ForeignObject } from '@svgdotjs/svg.js' import btnsSvg from './svg/btns' import iconsSvg from './svg/icons' @@ -362,9 +362,42 @@ class Node { }) } + // 创建富文本节点 + createRichTextNode() { + let g = new G() + let html = `
${this.nodeData.data.text}
` + let div = document.createElement('div') + div.innerHTML = html + div.style.cssText = `position: fixed; left: -999999px;` + let el = div.children[0] + el.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml') + el.style.maxWidth = this.mindMap.opt.textAutoWrapWidth + 'px' + this.mindMap.el.appendChild(div) + let { width, height } = el.getBoundingClientRect() + width = Math.ceil(width) + height = Math.ceil(height) + g.attr('data-width', width) + g.attr('data-height', height) + html = div.innerHTML + this.mindMap.el.removeChild(div) + let foreignObject = new ForeignObject() + foreignObject.width(width) + foreignObject.height(height) + foreignObject.add(SVG(html)) + g.add(foreignObject) + return { + node: g, + width, + height + } + } + // 创建文本节点 createTextNode() { + if (this.nodeData.data.richText) { + return this.createRichTextNode() + } let g = new G() let fontSize = this.getStyle( 'fontSize', @@ -405,6 +438,10 @@ class Node { g.add(node) }) let { width, height } = g.bbox() + width = Math.ceil(width) + height = Math.ceil(height) + g.attr('data-width', width) + g.attr('data-height', height) return { node: g, width, @@ -582,6 +619,7 @@ class Node { } // 文字 if (this._textData) { + this._textData.node.attr('data-offsetx', textContentOffsetX) this._textData.node.x(textContentOffsetX).y(0) textContentNested.add(this._textData.node) textContentOffsetX += this._textData.width + textContentItemMargin @@ -760,7 +798,9 @@ class Node { if (this.nodeData.inserting) { delete this.nodeData.inserting this.active() - this.mindMap.emit('node_dblclick', this) + setTimeout(() => { + this.mindMap.emit('node_dblclick', this) + }, 0) } } diff --git a/simple-mind-map/src/Render.js b/simple-mind-map/src/Render.js index feca4b26..6d3c92e5 100644 --- a/simple-mind-map/src/Render.js +++ b/simple-mind-map/src/Render.js @@ -668,6 +668,15 @@ class Render { [prop]: value } } + // 如果开启了富文本,则需要应用到富文本上 + if (this.mindMap.richText) { + this.mindMap.richText.showEditText(node) + let config = this.mindMap.richText.normalStyleToRichTextStyle({ + [prop]: value + }) + this.mindMap.richText.formatAllText(config) + this.mindMap.richText.hideEditText() + } this.setNodeDataRender(node, data) // 更新了连线的样式 if (lineStyleProps.includes(prop)) { @@ -788,9 +797,10 @@ class Render { // 设置节点文本 - setNodeText(node, text) { + setNodeText(node, text, richText) { this.setNodeDataRender(node, { - text + text, + richText }) } diff --git a/simple-mind-map/src/RichText.js b/simple-mind-map/src/RichText.js new file mode 100644 index 00000000..c4fe780c --- /dev/null +++ b/simple-mind-map/src/RichText.js @@ -0,0 +1,428 @@ +import Quill from 'quill' +import 'quill/dist/quill.snow.css' +import './css/quill.css' +import html2canvas from 'html2canvas' +import { Image as SvgImage } from '@svgdotjs/svg.js' +import { walk } from './utils' + +let extended = false + +// 扩展quill的字体列表 +let fontFamilyList = [ + '宋体, SimSun, Songti SC', + '微软雅黑, Microsoft YaHei', + '楷体, 楷体_GB2312, SimKai, STKaiti', + '黑体, SimHei, Heiti SC', + '隶书, SimLi', + 'andale mono', + 'arial, helvetica, sans-serif', + 'arial black, avant garde', + 'comic sans ms', + 'impact, chicago', + 'times new roman', + 'sans-serif', + 'serif' +] + +// 扩展quill的字号列表 +let fontSizeList = new Array(100).fill(0).map((_, index) => { + return index + 'px' +}) + +// 节点支持富文本编辑功能 +class RichText { + constructor({ mindMap, pluginOpt }) { + this.mindMap = mindMap + this.pluginOpt = pluginOpt + this.textEditNode = null + this.showTextEdit = false + this.quill = null + this.range = null + this.lastRange = null + this.node = null + this.initOpt() + this.extendQuill() + } + + // 处理选项参数 + initOpt() { + if ( + this.pluginOpt.fontFamilyList && + Array.isArray(this.pluginOpt.fontFamilyList) + ) { + fontFamilyList = this.pluginOpt.fontFamilyList + } + if ( + this.pluginOpt.fontSizeList && + Array.isArray(this.pluginOpt.fontSizeList) + ) { + fontSizeList = this.pluginOpt.fontSizeList + } + } + + // 扩展quill编辑器 + extendQuill() { + if (extended) { + return + } + extended = true + + // 扩展quill的字体列表 + const FontAttributor = Quill.import('attributors/class/font') + FontAttributor.whitelist = fontFamilyList + Quill.register(FontAttributor, true) + + const FontStyle = Quill.import('attributors/style/font') + FontStyle.whitelist = fontFamilyList + Quill.register(FontStyle, true) + + // 扩展quill的字号列表 + const SizeAttributor = Quill.import('attributors/class/size') + SizeAttributor.whitelist = fontSizeList + Quill.register(SizeAttributor, true) + + const SizeStyle = Quill.import('attributors/style/size') + SizeStyle.whitelist = fontSizeList + Quill.register(SizeStyle, true) + } + + // 显示文本编辑控件 + showEditText(node, rect) { + if (this.showTextEdit) { + return + } + this.node = node + if (!rect) rect = node._textData.node.node.getBoundingClientRect() + this.mindMap.emit('before_show_text_edit') + this.mindMap.renderer.textEdit.registerTmpShortcut() + if (!this.textEditNode) { + this.textEditNode = document.createElement('div') + this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;background-color:#fff;box-shadow: 0 0 20px rgba(0,0,0,.5);outline: none; word-break: break-all;` + document.body.appendChild(this.textEditNode) + } + // 原始宽高 + let g = node._textData.node + let originWidth = g.attr('data-width') + let originHeight = g.attr('data-height') + console.log(`node`, node, rect, originWidth, originHeight) + this.textEditNode.style.minWidth = originWidth + 'px' + this.textEditNode.style.minHeight = originHeight + 'px' + this.textEditNode.style.left = + rect.left + (rect.width - originWidth) / 2 + 'px' + this.textEditNode.style.top = + rect.top + (rect.height - originHeight) / 2 + 'px' + this.textEditNode.style.display = 'block' + this.textEditNode.style.maxWidth = this.mindMap.opt.textAutoWrapWidth + 'px' + this.textEditNode.style.transform = `scale(${rect.width / originWidth}, ${ + rect.height / originHeight + })` + if (!node.nodeData.data.richText) { + // 还不是富文本的情况 + let text = node.nodeData.data.text.split(/\n/gim).join('
') + let html = `

${text}

` + this.textEditNode.innerHTML = html + } else { + this.textEditNode.innerHTML = node.nodeData.data.text + } + this.initQuillEditor() + document.querySelector('.ql-editor').style.minHeight = originHeight + 'px' + this.showTextEdit = true + this.selectAll() + if (!node.nodeData.data.richText) { + // 如果是非富文本的情况,需要手动应用文本样式 + this.setTextStyleIfNotRichText(node) + } + } + + // 如果是非富文本的情况,需要手动应用文本样式 + setTextStyleIfNotRichText(node) { + let style = { + font: node.style.merge('fontFamily'), + color: node.style.merge('color'), + italic: node.style.merge('fontStyle') === 'italic', + bold: node.style.merge('fontWeight') === 'bold', + size: node.style.merge('fontSize') + 'px', + underline: node.style.merge('textDecoration') === 'underline', + strike: node.style.merge('textDecoration') === 'line-through' + } + this.formatText(style) + } + + // 隐藏文本编辑控件,即完成编辑 + hideEditText() { + if (!this.showTextEdit) { + return + } + let html = this.quill.container.firstChild.innerHTML + // 去除最后的空行 + html = html.replace(/


<\/p>$/, '') + this.mindMap.renderer.activeNodeList.forEach(node => { + this.mindMap.execCommand('SET_NODE_TEXT', node, html, true) + if (node.isGeneralization) { + // 概要节点 + node.generalizationBelongNode.updateGeneralization() + } + this.mindMap.render() + }) + this.mindMap.emit( + 'hide_text_edit', + this.textEditNode, + this.mindMap.renderer.activeNodeList + ) + this.textEditNode.style.display = 'none' + this.showTextEdit = false + this.mindMap.emit('rich_text_selection_change', false) + this.node = null + } + + // 初始化Quill富文本编辑器 + initQuillEditor() { + this.quill = new Quill(this.textEditNode, { + modules: { + toolbar: false, + keyboard: { + bindings: { + enter: { + key: 13, + handler: function () { + // 覆盖默认的回车键换行 + } + } + } + } + }, + theme: 'snow' + }) + this.quill.on('selection-change', range => { + this.lastRange = this.range + this.range = null + if (range) { + let bounds = this.quill.getBounds(range.index, range.length) + let rect = this.textEditNode.getBoundingClientRect() + let rectInfo = { + left: bounds.left + rect.left, + top: bounds.top + rect.top, + right: bounds.right + rect.left, + bottom: bounds.bottom + rect.top, + width: bounds.width + } + let formatInfo = this.quill.getFormat(range.index, range.length) + let hasRange = false + if (range.length == 0) { + hasRange = false + } else { + this.range = range + hasRange = true + } + this.mindMap.emit( + 'rich_text_selection_change', + hasRange, + rectInfo, + formatInfo + ) + } + }) + } + + // 选中全部 + selectAll() { + this.quill.setSelection(0, this.quill.getLength()) + } + + // 格式化当前选中的文本 + formatText(config = {}) { + if (!this.range && !this.lastRange) return + this.syncFormatToNodeConfig(config) + let rangeLost = !this.range + let range = rangeLost ? this.lastRange : this.range + this.quill.formatText(range.index, range.length, config) + if (rangeLost) { + this.quill.setSelection(this.lastRange.index, this.lastRange.length) + } + } + + // 格式化指定范围的文本 + formatRangeText(range, config = {}) { + if (!range) return + this.syncFormatToNodeConfig(config) + this.quill.formatText(range.index, range.length, config) + } + + // 格式化所有文本 + formatAllText(config = {}) { + this.syncFormatToNodeConfig(config) + this.quill.formatText(0, this.quill.getLength(), config) + } + + // 同步格式化到节点样式配置 + syncFormatToNodeConfig(config) { + if (!this.node) return + let data = this.richTextStyleToNormalStyle(config) + this.mindMap.renderer.setNodeData(this.node, data) + } + + // 将普通节点样式对象转换成富文本样式对象 + normalStyleToRichTextStyle(style) { + let config = {} + Object.keys(style).forEach(prop => { + let value = style[prop] + switch (prop) { + case 'fontFamily': + config.font = value + break + case 'fontSize': + config.size = value + 'px' + break + case 'fontWeight': + config.bold = value === 'bold' + break + case 'fontStyle': + config.italic = value === 'italic' + break + case 'textDecoration': + config.underline = value === 'underline' + config.strike = value === 'line-through' + case 'color': + config.color = value + break + default: + break + } + }) + return config + } + + // 将富文本样式对象转换成普通节点样式对象 + richTextStyleToNormalStyle(config) { + let data = {} + Object.keys(config).forEach(prop => { + let value = config[prop] + switch (prop) { + case 'font': + data.fontFamily = value + break + case 'size': + data.fontSize = parseFloat(value) + break + case 'bold': + data.fontWeight = value ? 'bold' : 'normal' + break + case 'italic': + data.fontStyle = value ? 'italic' : 'normal' + break + case 'underline': + data.textDecoration = value ? 'underline' : 'none' + break + case 'strike': + data.textDecoration = value ? 'line-through' : 'none' + break + case 'color': + data.color = value + break + default: + break + } + }) + return data + } + + // 将svg中嵌入的dom元素转换成图片 + async _handleSvgDomElements(svg) { + svg = svg.clone() + let foreignObjectList = svg.find('foreignObject') + let task = foreignObjectList.map(async item => { + let clone = item.first().node.cloneNode(true) + let div = document.createElement('div') + div.style.cssText = `position: fixed; left: -999999px;` + div.appendChild(clone) + this.mindMap.el.appendChild(div) + let canvas = await html2canvas(clone, { + backgroundColor: null + }) + this.mindMap.el.removeChild(div) + let imgNode = new SvgImage() + .load(canvas.toDataURL()) + .size(canvas.width, canvas.height) + item.replace(imgNode) + }) + await Promise.all(task) + return { + svg: svg, + svgHTML: svg.svg() + } + } + + // 将svg中嵌入的dom元素转换成图片 + handleSvgDomElements(svg) { + return new Promise((resolve, reject) => { + svg = svg.clone() + let foreignObjectList = svg.find('foreignObject') + let index = 0 + let len = foreignObjectList.length + let transform = async () => { + this.mindMap.emit('transforming-dom-to-images', index, len) + try { + let item = foreignObjectList[index++] + let parent = item.parent() + let clone = item.first().node.cloneNode(true) + let div = document.createElement('div') + div.style.cssText = `position: fixed; left: -999999px;` + div.appendChild(clone) + this.mindMap.el.appendChild(div) + let canvas = await html2canvas(clone, { + backgroundColor: null + }) + this.mindMap.el.removeChild(div) + let imgNode = new SvgImage() + .load(canvas.toDataURL()) + .size(canvas.width, canvas.height) + .x((parent ? parent.attr('data-offsetx') : 0) || 0) + item.replace(imgNode) + if (index <= len - 1) { + setTimeout(() => { + transform() + }, 0) + } else { + resolve({ + svg: svg, + svgHTML: svg.svg() + }) + } + } catch (error) { + reject(error) + } + } + if (len > 0) transform() + }) + } + + // 将所有节点转换成非富文本节点 + transformAllNodesToNormalNode() { + let div = document.createElement('div') + walk( + this.mindMap.renderer.renderTree, + null, + node => { + if (node.data.richText) { + node.data.richText = false + div.innerHTML = node.data.text + node.data.text = div.textContent + } + }, + null, + true, + 0, + 0 + ) + this.mindMap.reRender() + } + + // 插件被移除前做的事情 + beforePluginRemove() { + this.transformAllNodesToNormalNode() + } +} + +RichText.instanceName = 'richText' + +export default RichText diff --git a/simple-mind-map/src/TextEdit.js b/simple-mind-map/src/TextEdit.js index 20fb603a..a71e31b0 100644 --- a/simple-mind-map/src/TextEdit.js +++ b/simple-mind-map/src/TextEdit.js @@ -50,7 +50,12 @@ export default class TextEdit { // 显示文本编辑框 show(node) { - this.showEditTextBox(node, node._textData.node.node.getBoundingClientRect()) + let rect = node._textData.node.node.getBoundingClientRect() + if (this.mindMap.richText) { + this.mindMap.richText.showEditText(node, rect) + return + } + this.showEditTextBox(node, rect) } // 显示文本编辑框 @@ -96,6 +101,9 @@ export default class TextEdit { // 隐藏文本编辑框 hideEditTextBox() { + if (this.mindMap.richText) { + return this.mindMap.richText.hideEditText() + } if (!this.showTextEdit) { return } diff --git a/simple-mind-map/src/css/quill.css b/simple-mind-map/src/css/quill.css new file mode 100644 index 00000000..78787c19 --- /dev/null +++ b/simple-mind-map/src/css/quill.css @@ -0,0 +1,9 @@ +.ql-editor { + overflow: hidden; + padding: 0; + height: auto; +} + +.ql-container { + height: auto; +} \ No newline at end of file diff --git a/web/src/lang/en_us.js b/web/src/lang/en_us.js index c3eafdde..fe0f116f 100644 --- a/web/src/lang/en_us.js +++ b/web/src/lang/en_us.js @@ -34,7 +34,8 @@ export default { watermarkTextSpacing: 'Text spacing', watermarkAngle: 'Angle', watermarkTextOpacity: 'Text opacity', - watermarkTextFontSize: 'Font size' + watermarkTextFontSize: 'Font size', + isEnableNodeRichText: 'Enable node rich text editing' }, color: { moreColor: 'More color' @@ -79,7 +80,13 @@ export default { imageFile: 'Image file', svgFile: 'svg file', pdfFile: 'pdf file', - tips: 'tips:.smm and .json file can be import' + tips: 'tips: .smm and .json file can be import', + domToImage: 'Whether to convert rich text nodes in svg into pictures', + pngTips: 'tips: Exporting pictures in rich text mode is time-consuming. It is recommended to export to svg format', + svgTips: 'tips: Exporting pictures in rich text mode is time-consuming', + transformingDomToImages: 'Converting nodes: ', + notifyTitle: 'Info', + notifyMessage: 'If the download is not triggered, check whether it is blocked by the browser' }, fullscreen: { fullscreenShow: 'Full screen show', @@ -178,5 +185,9 @@ export default { import: 'Import', export: 'Export', shortcutKey: 'Shortcut key' + }, + edit: { + newFeatureNoticeTitle: 'New feature reminder', + newFeatureNoticeMessage: 'This update supports node rich text editing, But there are some defects, The most important impact is that the time to export the image is proportional to the number of nodes, Therefore, if you are more dependent on export requirements, you can use【Base style】-【Other config】-【Enable node rich text editing】Set to turn off rich text editing mode.' } } diff --git a/web/src/lang/zh_cn.js b/web/src/lang/zh_cn.js index 250d2999..9596aefb 100644 --- a/web/src/lang/zh_cn.js +++ b/web/src/lang/zh_cn.js @@ -34,7 +34,8 @@ export default { watermarkTextSpacing: '水印文字间距', watermarkAngle: '旋转角度', watermarkTextOpacity: '文字透明度', - watermarkTextFontSize: '文字字号' + watermarkTextFontSize: '文字字号', + isEnableNodeRichText: '是否开启节点富文本编辑' }, color: { moreColor: '更多颜色' @@ -79,7 +80,13 @@ export default { imageFile: '图片文件', svgFile: 'svg文件', pdfFile: 'pdf文件', - tips: 'tips:.smm和.json文件可用于导入' + tips: 'tips:.smm和.json文件可用于导入', + domToImage: '是否将svg中富文本节点转换成图片', + pngTips: 'tips:富文本模式导出图片非常耗时,建议导出为svg格式', + svgTips: 'tips:富文本模式导出图片非常耗时', + transformingDomToImages: '正在转换节点:', + notifyTitle: '消息', + notifyMessage: '如果没有触发下载,请检查是否被浏览器拦截了' }, fullscreen: { fullscreenShow: '全屏查看', @@ -178,5 +185,9 @@ export default { import: '导入', export: '导出', shortcutKey: '快捷键' + }, + edit: { + newFeatureNoticeTitle: '新特性提醒', + newFeatureNoticeMessage: '本次更新支持了节点富文本编辑,但是存在一定缺陷,最主要的影响是导出为图片的时间和节点数量成正比,所以对导出需求比较依赖的话可以通过【基础样式】-【其他配置】-【是否开启节点富文本编辑】设置关掉富文本编辑模式。' } } diff --git a/web/src/pages/Edit/components/BaseStyle.vue b/web/src/pages/Edit/components/BaseStyle.vue index 015f870b..7177a3f8 100644 --- a/web/src/pages/Edit/components/BaseStyle.vue +++ b/web/src/pages/Edit/components/BaseStyle.vue @@ -429,6 +429,11 @@ ">{{ $t('baseStyle.enableFreeDrag') }} +

+
+ {{ this.$t('baseStyle.isEnableNodeRichText') }} +
+
@@ -439,7 +444,7 @@ import Color from './Color' import { lineWidthList, lineStyleList, backgroundRepeatList, backgroundPositionList, backgroundSizeList } from '@/config' import ImgUpload from '@/components/ImgUpload' import { storeConfig } from '@/api' -import { mapState } from 'vuex' +import { mapState, mapMutations } from 'vuex' /** * @Author: 王林 @@ -502,11 +507,12 @@ export default { fontSize: 1 } }, - updateWatermarkTimer: null + updateWatermarkTimer: null, + enableNodeRichText: true } }, computed: { - ...mapState(['activeSidebar']), + ...mapState(['activeSidebar', 'localConfig']), lineStyleList() { return lineStyleList[this.$i18n.locale] || lineStyleList.zh @@ -533,7 +539,12 @@ export default { } } }, + created () { + this.enableNodeRichText = this.localConfig.openNodeRichText + }, methods: { + ...mapMutations(['setLocalConfig']), + /** * @Author: 王林 * @Date: 2021-05-05 14:02:12 @@ -668,6 +679,13 @@ export default { this.watermarkConfig.text = '' } this.updateWatermarkConfig() + }, + + // 切换是否开启节点富文本编辑 + enableNodeRichTextChange(e) { + this.setLocalConfig({ + openNodeRichText: e + }) } } } diff --git a/web/src/pages/Edit/components/Edit.vue b/web/src/pages/Edit/components/Edit.vue index b2147ac9..80141975 100644 --- a/web/src/pages/Edit/components/Edit.vue +++ b/web/src/pages/Edit/components/Edit.vue @@ -11,6 +11,7 @@ + state.localConfig.isZenMode + isZenMode: state => state.localConfig.isZenMode, + openNodeRichText: state => state.localConfig.openNodeRichText, }) }, + watch: { + openNodeRichText() { + if (this.openNodeRichText) { + this.addRichTextPlugin() + } else { + this.removeRichTextPlugin() + } + } + }, mounted() { + this.showNewFeatureInfo() this.getData() this.init() this.$bus.$on('execCommand', this.execCommand) @@ -263,6 +278,7 @@ export default { }, ...(config || {}) }) + if (this.openNodeRichText) this.addRichTextPlugin() this.mindMap.keyCommand.addShortcut('Control+s', () => { this.manualSave() }) @@ -279,7 +295,9 @@ export default { 'svg_mousedown', 'mouseup', 'mode_change', - 'node_tree_render_end' + 'node_tree_render_end', + 'rich_text_selection_change', + 'transforming-dom-to-images' ].forEach(event => { this.mindMap.on(event, (...args) => { this.$bus.$emit(event, ...args) @@ -331,6 +349,32 @@ export default { } catch (error) { console.log(error) } + }, + + // 显示新特性提示 + showNewFeatureInfo() { + let showed = localStorage.getItem('SIMPLE_MIND_MAP_NEW_FEATURE_TIP_1') + if (!showed) { + this.$notify.info({ + title: this.$t('edit.newFeatureNoticeTitle'), + message: this.$t('edit.newFeatureNoticeMessage'), + duration: 0, + onClose: () => { + localStorage.setItem('SIMPLE_MIND_MAP_NEW_FEATURE_TIP_1', true) + } + }) + } + }, + + // 加载节点富文本编辑插件 + addRichTextPlugin() { + if (!this.mindMap) return + this.mindMap.addPlugin(RichText) + }, + + // 移除节点富文本编辑插件 + removeRichTextPlugin() { + this.mindMap.removePlugin(RichText) } } } diff --git a/web/src/pages/Edit/components/Export.vue b/web/src/pages/Edit/components/Export.vue index cc6d5ca1..cafe31fa 100644 --- a/web/src/pages/Edit/components/Export.vue +++ b/web/src/pages/Edit/components/Export.vue @@ -4,6 +4,10 @@ :title="$t('export.title')" :visible.sync="dialogVisible" width="700px" + v-loading.fullscreen.lock="loading" + :element-loading-text="loadingText" + element-loading-spinner="el-icon-loading" + element-loading-background="rgba(0, 0, 0, 0.8)" >
@@ -19,6 +23,12 @@ style="margin-left: 12px" >{{ $t('export.include') }} + {{ $t('export.domToImage') }}
{{ $t('export.tips') }}
+
{{ $t('export.pngTips') }}
+
{{ $t('export.svgTips') }}
{{ $t('dialog.cancel') }} @@ -49,6 +61,8 @@ + + diff --git a/web/src/pages/Edit/components/Style.vue b/web/src/pages/Edit/components/Style.vue index 70fb3a8d..b066a65a 100644 --- a/web/src/pages/Edit/components/Style.vue +++ b/web/src/pages/Edit/components/Style.vue @@ -45,6 +45,7 @@ :key="item" :label="item" :value="item" + :style="{ fontSize: item + 'px' }" > @@ -125,7 +126,7 @@ @@ -133,7 +134,7 @@ @@ -294,7 +295,7 @@ diff --git a/web/src/store.js b/web/src/store.js index ad4f748d..091183b0 100644 --- a/web/src/store.js +++ b/web/src/store.js @@ -11,7 +11,9 @@ const store = new Vuex.Store({ isHandleLocalFile: false, // 是否操作的是本地文件 localConfig: { // 本地配置 - isZenMode: false // 是否是禅模式 + isZenMode: false, // 是否是禅模式 + // 是否开启节点富文本 + openNodeRichText: true }, activeSidebar: '' // 当前显示的侧边栏 },