diff --git a/simple-mind-map/index.js b/simple-mind-map/index.js index 504d27c7..132a67f6 100644 --- a/simple-mind-map/index.js +++ b/simple-mind-map/index.js @@ -8,6 +8,7 @@ import KeyCommand from './src/KeyCommand' import Command from './src/Command' import BatchExecution from './src/BatchExecution' import Export from './src/Export' +import Select from './src/Select' import { SVG } from '@svgdotjs/svg.js' @@ -47,17 +48,14 @@ class MindMap { // 容器元素 this.el = this.opt.el - let { - width, - height - } = this.el.getBoundingClientRect() + this.elRect = this.el.getBoundingClientRect() // 画布宽高 - this.width = width - this.height = height + this.width = this.elRect.width + this.height = this.elRect.height // 画布 - this.svg = SVG().addTo(this.el).size(width, height) + this.svg = SVG().addTo(this.el).size(this.width, this.height) this.draw = this.svg.group() // 节点id @@ -97,6 +95,11 @@ class MindMap { mindMap: this }) + // 选择类 + this.select = new Select({ + mindMap: this + }) + // 批量执行类 this.batchExecution = new BatchExecution() diff --git a/simple-mind-map/src/Event.js b/simple-mind-map/src/Event.js index 990f3dec..194cfedd 100644 --- a/simple-mind-map/src/Event.js +++ b/simple-mind-map/src/Event.js @@ -46,6 +46,7 @@ class Event extends EventEmitter { this.onMousemove = this.onMousemove.bind(this) this.onMouseup = this.onMouseup.bind(this) this.onMousewheel = this.onMousewheel.bind(this) + this.onContextmenu = this.onContextmenu.bind(this) } /** @@ -65,6 +66,7 @@ class Event extends EventEmitter { } else { this.mindMap.el.addEventListener('mousewheel', this.onMousewheel) } + this.mindMap.svg.on('contextmenu', this.onContextmenu) } /** @@ -74,10 +76,12 @@ class Event extends EventEmitter { * @Desc: 解绑事件 */ unbind() { + this.mindMap.svg.off('click', this.onDrawClick) this.mindMap.el.removeEventListener('mousedown', this.onMousedown) window.removeEventListener('mousemove', this.onMousemove) window.removeEventListener('mouseup', this.onMouseup) this.mindMap.el.removeEventListener('mousewheel', this.onMousewheel) + this.mindMap.svg.off('contextmenu', this.onContextmenu) } /** @@ -152,6 +156,16 @@ class Event extends EventEmitter { } this.emit('mousewheel', e, dir, this) } + + /** + * @Author: 王林 + * @Date: 2021-07-10 22:34:13 + * @Desc: 鼠标右键菜单事件 + */ + onContextmenu(e) { + e.preventDefault() + this.emit('contextmenu', e) + } } export default Event \ No newline at end of file diff --git a/simple-mind-map/src/Node.js b/simple-mind-map/src/Node.js index e493d7df..36d268bf 100644 --- a/simple-mind-map/src/Node.js +++ b/simple-mind-map/src/Node.js @@ -152,7 +152,7 @@ class Node { this._expandBtn.off(['mouseover', 'mouseout', 'click']) } if (this.group) { - this.group.off(['click', 'dblclick']) + this.group.off(['click', 'dblclick', 'contextmenu']) } } @@ -471,7 +471,7 @@ class Node { let { paddingY } = this.getPaddingVale() // 创建组 this.group = new G() - this.updatePos(false) + this.update(false) // 节点矩形 this.style.rect(this.group.rect(width, height)) // 图片节点 @@ -540,6 +540,11 @@ class Node { this.group.on('dblclick', () => { this.mindMap.emit('node_dblclick', this) }) + // 右键菜单事件 + this.group.on('contextmenu', (e) => { + e.stopPropagation() + e.preventDefault() + }) } /** @@ -555,7 +560,7 @@ class Node { this.mindMap.emit('before_node_active', this, this.renderer.activeNodeList) this.renderer.clearActive() this.mindMap.execCommand('SET_NODE_ACTIVE', this, !this.nodeData.data.isActive) - this.renderer.activeNodeList.push(this) + this.renderer.addActiveNode(this) this.mindMap.emit('node_active', this, this.renderer.activeNodeList) } @@ -576,12 +581,18 @@ class Node { /** * @Author: 王林 * @Date: 2021-07-04 22:47:01 - * @Desc: 更新节点位置 + * @Desc: 更新节点 */ - updatePos(animate = true) { + update(animate = true) { if (!this.group) { return; } + // 需要移除展开收缩按钮 + if (this._expandBtn && this.nodeData.children.length <= 0) { + this.removeExpandBtn() + } else if (!this._expandBtn && this.nodeData.children.length > 0) {// 需要添加展开收缩按钮 + this.renderExpandBtn() + } let t = this.group.transform() if (animate) { this.group.animate(300).translate(this.left - t.translateX, this.top - t.translateY) @@ -604,7 +615,7 @@ class Node { this._initRender = false this.renderNode() } else { - this.updatePos() + this.update() } // 子节点 if (this.children && this.children.length && this.nodeData.data.expand !== false) { @@ -730,6 +741,22 @@ class Node { this.renderer.layout.renderExpandBtn(this, this._expandBtn) } + /** + * @Author: 王林 + * @Date: 2021-07-11 13:26:00 + * @Desc: 移除展开收缩按钮 + */ + removeExpandBtn() { + if (this._expandBtn) { + this._expandBtn.off(['mouseover', 'mouseout', 'click']) + } + // 展开收缩按钮 + if (this._expandBtn) { + this._expandBtn.remove() + this._expandBtn = null + } + } + /** * @Author: 王林 * @Date: 2021-06-20 22:51:57 diff --git a/simple-mind-map/src/Render.js b/simple-mind-map/src/Render.js index cb45bab6..5d9432ea 100644 --- a/simple-mind-map/src/Render.js +++ b/simple-mind-map/src/Render.js @@ -139,16 +139,37 @@ class Render { this.activeNodeList = [] } + /** + * @Author: 王林 + * @Date: 2021-07-11 10:54:00 + * @Desc: 添加节点到激活列表里 + */ + addActiveNode(node) { + let index = this.findActiveNodeIndex(node) + if (index === -1) { + this.activeNodeList.push(node) + } + } + /** * @Author: 王林 * @Date: 2021-07-10 10:04:04 * @Desc: 在激活列表里移除某个节点 */ removeActiveNode(node) { - let index = this.activeNodeList.findIndex((item) => { + let index = this.findActiveNodeIndex(node) + this.activeNodeList.splice(index, 1) + } + + /** + * @Author: 王林 + * @Date: 2021-07-11 10:55:23 + * @Desc: 检索某个节点在激活列表里的索引 + */ + findActiveNodeIndex(node) { + return this.activeNodeList.findIndex((item) => { return item === node; }) - this.activeNodeList.splice(index, 1) } /** @@ -165,7 +186,7 @@ class Render { /** * @Author: 王林 * @Date: 2021-05-04 13:19:54 - * @Desc: 插入同级节点 + * @Desc: 插入同级节点,多个节点只会操作第一个节点 */ insertNode() { if (this.activeNodeList.length <= 0) { @@ -196,24 +217,24 @@ class Render { if (this.activeNodeList.length <= 0) { return; } - let node = this.activeNodeList[0] - if (!node.nodeData.children) { - node.nodeData.children = [] - } - let len = node.nodeData.children.length - node.nodeData.children.push({ - "data": { - "text": "分支主题", - "expand": true - }, - "children": [] + this.activeNodeList.forEach((node, index) => { + if (!node.nodeData.children) { + node.nodeData.children = [] + } + node.nodeData.children.push({ + "data": { + "text": "分支主题", + "expand": true + }, + "children": [] + }) + if (node.isRoot) { + this.mindMap.batchExecution.push('renderNode' + index, () => { + node.renderNode() + }) + } }) this.mindMap.render() - if (node.isRoot || len <= 0) { - this.mindMap.batchExecution.push('renderNode', () => { - node.renderNode() - }) - } } /** @@ -225,21 +246,24 @@ class Render { if (this.activeNodeList.length <= 0) { return; } - this.activeNodeList.forEach((item) => { - if (item.isRoot) { - item.children.forEach((child) => { + for (let i = 0; i < this.activeNodeList.length; i++) { + let node = this.activeNodeList[i] + if (node.isRoot) { + node.children.forEach((child) => { child.remove() }) - item.children = [] - item.nodeData.children = [] + node.children = [] + node.nodeData.children = [] + break } else { - this.removeActiveNode(item) - let index = this.getNodeIndex(item) - item.remove() - item.parent.children.splice(index, 1) - item.parent.nodeData.children.splice(index, 1) + this.removeActiveNode(node) + let index = this.getNodeIndex(node) + node.remove() + node.parent.children.splice(index, 1) + node.parent.nodeData.children.splice(index, 1) + i-- } - }) + } this.mindMap.render() } diff --git a/simple-mind-map/src/Select.js b/simple-mind-map/src/Select.js new file mode 100644 index 00000000..2474ba86 --- /dev/null +++ b/simple-mind-map/src/Select.js @@ -0,0 +1,123 @@ +import { bfsWalk, throttle } from './utils' + +/** + * @Author: 王林 + * @Date: 2021-07-10 22:34:51 + * @Desc: 选择节点类 + */ + +class Select { + /** + * @Author: 王林 + * @Date: 2021-07-10 22:35:16 + * @Desc: 构造函数 + */ + constructor({ mindMap }) { + this.mindMap = mindMap + this.rect = null + this.isMousedown = false + this.mouseDownX = 0 + this.mouseDownY = 0 + this.mouseMoveX = 0 + this.mouseMoveY = 0 + this.bindEvent() + } + + /** + * @Author: 王林 + * @Date: 2021-07-10 22:36:36 + * @Desc: 绑定事件 + */ + bindEvent() { + this.checkInNodes = throttle(this.checkInNodes, 500, this) + this.mindMap.on('contextmenu', (e) => { + this.isMousedown = true + let { x, y } = this.toPos(e.clientX, e.clientY) + this.mouseDownX = x + this.mouseDownY = y + this.createRect(x, y) + }) + this.mindMap.on('mousemove', (e) => { + if (!this.isMousedown) { + return + } + this.mouseMoveX = e.clientX + this.mouseMoveY = e.clientY + this.rect.plot([ + [this.mouseDownX, this.mouseDownY], + [this.mouseMoveX, this.mouseDownY], + [this.mouseMoveX, this.mouseMoveY], + [this.mouseDownX, this.mouseMoveY] + ]) + this.mindMap.batchExecution.push('checkInNodes', this.checkInNodes) + }) + this.mindMap.on('mouseup', (e) => { + if (!this.isMousedown) { + return; + } + this.mindMap.emit('node_active', null, this.mindMap.renderer.activeNodeList) + this.isMousedown = false + if (this.rect) this.rect.remove() + this.rect = null + }) + } + + /** + * @Author: 王林 + * @Date: 2021-07-11 10:19:37 + * @Desc: 创建矩形 + */ + createRect(x, y) { + this.rect = this.mindMap.svg.polygon().stroke({ + color: '#0984e3' + }).fill({ + color: 'rgba(9,132,227,0.3)' + }).plot([[x, y]]) + } + + /** + * @Author: 王林 + * @Date: 2021-07-11 09:20:03 + * @Desc: 转换位置 + */ + toPos(x, y) { + return { + x: x - this.mindMap.elRect.left, + y: y - this.mindMap.elRect.top + } + } + + /** + * @Author: 王林 + * @Date: 2021-07-11 10:20:43 + * @Desc: 检测在选区里的节点 + */ + checkInNodes() { + let { scaleX, scaleY, translateX, translateY } = this.mindMap.draw.transform() + let minx = Math.min(this.mouseDownX, this.mouseMoveX) + let miny = Math.min(this.mouseDownY, this.mouseMoveY) + let maxx = Math.max(this.mouseDownX, this.mouseMoveX) + let maxy = Math.max(this.mouseDownY, this.mouseMoveY) + bfsWalk(this.mindMap.renderer.root, (node) => { + let { left, top, width, height } = node + let right = (left + width) * scaleX + translateX + let bottom = (top + height) * scaleY + translateY + left = left * scaleX + translateX + top = top * scaleY + translateY + if ( + left >= minx && + right <= maxx && + top >= miny && + bottom <= maxy + ) { + this.mindMap.execCommand('SET_NODE_ACTIVE', node, true) + this.mindMap.renderer.addActiveNode(node) + } else if (node.nodeData.data.isActive) { + this.mindMap.execCommand('SET_NODE_ACTIVE', node, false) + this.mindMap.renderer.removeActiveNode(node) + } + }) + } +} + +export default Select \ No newline at end of file diff --git a/simple-mind-map/src/utils/index.js b/simple-mind-map/src/utils/index.js index a176e07e..b41baf47 100644 --- a/simple-mind-map/src/utils/index.js +++ b/simple-mind-map/src/utils/index.js @@ -8,7 +8,7 @@ export const walk = (root, parent, beforeCallback, afterCallback, isRoot, layerI let stop = false if (beforeCallback) { stop = beforeCallback(root, parent, isRoot, layerIndex, index) - } + } if (!stop && root.children && root.children.length > 0) { let _layerIndex = layerIndex + 1 root.children.forEach((node, nodeIndex) => { @@ -176,4 +176,22 @@ export const downloadFile = (file, fileName) => { a.href = file a.download = fileName a.click() +} + +/** + * @Author: 王林 + * @Date: 2021-07-11 10:36:47 + * @Desc: 节流函数 + */ +export const throttle = (fn, time = 300, ctx) => { + let timer = null + return () => { + if (timer) { + return + } + timer = setTimeout(() => { + fn.call(ctx) + timer = null + }, 300) + }; } \ No newline at end of file diff --git a/web/src/pages/Edit/components/NodeHyperlink.vue b/web/src/pages/Edit/components/NodeHyperlink.vue index 332a69d2..66fa6e91 100644 --- a/web/src/pages/Edit/components/NodeHyperlink.vue +++ b/web/src/pages/Edit/components/NodeHyperlink.vue @@ -6,13 +6,17 @@ width="500" >
- 链接 - -
+ 链接 + +
- 名称 - -
+ 名称 + + 取 消 确 定 @@ -21,10 +25,10 @@ \ No newline at end of file diff --git a/web/src/pages/Edit/components/NodeImage.vue b/web/src/pages/Edit/components/NodeImage.vue index 32758596..133329ba 100644 --- a/web/src/pages/Edit/components/NodeImage.vue +++ b/web/src/pages/Edit/components/NodeImage.vue @@ -35,20 +35,19 @@ export default { dialogVisible: false, img: "", imgTitle: "", - activeNode: null, + activeNodes: null, }; }, created() { this.$bus.$on("node_active", (...args) => { - let activeNodes = args[1]; - if (activeNodes.length > 0) { - this.activeNode = activeNodes[0]; - this.img = this.activeNode.getData("image"); - this.imgTitle = this.activeNode.getData("imageTitle"); + this.activeNodes = args[1]; + if (this.activeNodes.length > 0) { + let firstNode = this.activeNodes[0]; + this.img = firstNode.getData("image"); + this.imgTitle = firstNode.getData("imageTitle"); } else { this.img = ""; this.imgTitle = ""; - this.activeNode = null; } }); this.$bus.$on("showNodeImage", () => { @@ -73,11 +72,13 @@ export default { async confirm() { try { let { width, height } = await this.$refs.ImgUpload.getSize(); - this.activeNode.setImage({ - url: this.img || "none", - title: this.imgTitle, - width, - height, + this.activeNodes.forEach((node) => { + node.setImage({ + url: this.img || "none", + title: this.imgTitle, + width, + height, + }); }); this.cancel(); } catch (error) { diff --git a/web/src/pages/Edit/components/NodeNote.vue b/web/src/pages/Edit/components/NodeNote.vue index 4bc98cbb..849aec03 100644 --- a/web/src/pages/Edit/components/NodeNote.vue +++ b/web/src/pages/Edit/components/NodeNote.vue @@ -21,10 +21,10 @@