From a31e93bacf27241f500f8b661d6c112607b27ec2 Mon Sep 17 00:00:00 2001 From: eseasky Date: Tue, 29 Aug 2023 19:46:22 +0800 Subject: [PATCH 1/4] =?UTF-8?q?Feat:=20=E6=94=B6=E8=B5=B7=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E6=97=B6=EF=BC=8C=E6=98=BE=E7=A4=BA=E6=8A=98=E5=8F=A0?= =?UTF-8?q?=E7=9A=84=E8=8A=82=E7=82=B9=E6=95=B0=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/core/render/node/nodeExpandBtn.js | 53 +++++++++++++++---- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/simple-mind-map/src/core/render/node/nodeExpandBtn.js b/simple-mind-map/src/core/render/node/nodeExpandBtn.js index 5b2675cd..99252eda 100644 --- a/simple-mind-map/src/core/render/node/nodeExpandBtn.js +++ b/simple-mind-map/src/core/render/node/nodeExpandBtn.js @@ -3,16 +3,29 @@ import { SVG, Circle, G } from '@svgdotjs/svg.js' // 创建展开收起按钮的内容节点 function createExpandNodeContent() { - if (this._openExpandNode) { - return - } - let { open, close } = this.mindMap.opt.expandBtnIcon || {} + // 实时更新收起节点数字 + // if (this._openExpandNode) { + // return + // } + let { close } = this.mindMap.opt.expandBtnIcon || {} + // 计算子节点数量 + const count = this.sumNode(this.nodeData.children) + // 生成按钮 + const node = SVG() + .text(count) + .font({ family: 'Inconsolata' }) + node.attr('font-size', 14) // 展开的节点 - this._openExpandNode = SVG(open || btnsSvg.open).size( - this.expandBtnSize, - this.expandBtnSize - ) - this._openExpandNode.x(0).y(-this.expandBtnSize / 2) + this._openExpandNode = node.size(this.expandBtnSize, this.expandBtnSize) + // 数字不同偏移量大小处理 + if (count < 10) { + this._openExpandNode.x(6).y(-this.expandBtnSize / 2) + } else if (count >= 10 && count < 100) { + this._openExpandNode.x(1).y(-this.expandBtnSize / 2) + } else { + this._openExpandNode.x(0).y(-this.expandBtnSize / 2) + node.attr('font-size', 12) + } // 收起的节点 this._closeExpandNode = SVG(close || btnsSvg.close).size( this.expandBtnSize, @@ -22,6 +35,7 @@ function createExpandNodeContent() { // 填充节点 this._fillExpandNode = new Circle().size(this.expandBtnSize) this._fillExpandNode.x(0).y(-this.expandBtnSize / 2) + // 设置样式 this.style.iconBtn( this._openExpandNode, @@ -30,6 +44,13 @@ function createExpandNodeContent() { ) } +// 统计折叠的子节点数量 +function sumNode(data = []) { + return data.reduce( + (total, cur) => total + this.sumNode(cur.children || []), + data.length + ) +} // 创建或更新展开收缩按钮内容 function updateExpandBtnNode() { let { expand } = this.nodeData.data @@ -47,7 +68,16 @@ function updateExpandBtnNode() { node = this._closeExpandNode this._lastExpandBtnType = true } - if (this._expandBtn) this._expandBtn.add(this._fillExpandNode).add(node) + + if (this._expandBtn) { + // 如果是收起按钮加上边框 + if (!expand) { + this._fillExpandNode.stroke({ + color: '#333333' + }) + } + this._expandBtn.add(this._fillExpandNode).add(node) + } } // 更新展开收缩按钮位置 @@ -138,5 +168,6 @@ export default { renderExpandBtn, removeExpandBtn, showExpandBtn, - hideExpandBtn + hideExpandBtn, + sumNode } From 3d86650f22e81ffc3844ee556f14dc392c0ea7bf Mon Sep 17 00:00:00 2001 From: eseasky Date: Tue, 29 Aug 2023 20:09:57 +0800 Subject: [PATCH 2/4] =?UTF-8?q?Feat:=20=E5=AE=9E=E7=8E=B0=E5=85=B3?= =?UTF-8?q?=E8=81=94=E7=BA=BF=E7=AB=AF=E7=82=B9=E4=BD=8D=E7=BD=AE=E9=9A=8F?= =?UTF-8?q?=E9=BC=A0=E6=A0=87=E6=8B=96=E6=8B=BD=E5=8F=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/plugins/AssociativeLine.js | 116 ++++++++++++++--- .../associativeLineControls.js | 73 ++++++++--- .../associativeLine/associativeLineText.js | 21 +-- .../associativeLine/associativeLineUtils.js | 123 +++++++++++++++++- 4 files changed, 280 insertions(+), 53 deletions(-) diff --git a/simple-mind-map/src/plugins/AssociativeLine.js b/simple-mind-map/src/plugins/AssociativeLine.js index 2a7ebba1..06dc6fa6 100644 --- a/simple-mind-map/src/plugins/AssociativeLine.js +++ b/simple-mind-map/src/plugins/AssociativeLine.js @@ -99,12 +99,32 @@ class AssociativeLine { // 创建箭头 createMarker() { return this.draw.marker(20, 20, add => { - add.ref(2, 5) + add.ref(12, 5) add.size(10, 10) add.attr('orient', 'auto-start-reverse') this.markerPath = add.path('M0,0 L2,5 L0,10 L10,5 Z') }) } + // 判断关联线坐标是否变更,有变更则使用变化后的坐标,无则默认坐标 + updateAllLinesPos(node, toNode, associativeLineTargets) { + let startPoint = {} + let endPoint = {} + let nodeRange = 0 + let nodeDir = 'right' + let toNodeRange = 0 + let toNodeDir = 'right' + if (associativeLineTargets.startPoint && associativeLineTargets.endPoint) { + nodeRange = associativeLineTargets.startPoint.range || 0 + nodeDir = associativeLineTargets.startPoint.dir || 'right' + startPoint = getNodePoint(node, nodeDir, nodeRange) + toNodeRange = associativeLineTargets.endPoint.range || 0 + toNodeDir = associativeLineTargets.endPoint.dir || 'right' + endPoint = getNodePoint(toNode, toNodeDir, toNodeRange) + } else { + ;[startPoint, endPoint] = computeNodePoints(node, toNode) + } + return [startPoint, endPoint] + } // 渲染所有连线 renderAllLines() { @@ -137,10 +157,17 @@ class AssociativeLine { 0 ) nodeToIds.forEach((ids, node) => { - ids.forEach(id => { - let toNode = idToNode.get(id) + ids.forEach((id, index) => { + let toNode = idToNode.get(id.id) if (!node || !toNode) return - let [startPoint, endPoint] = computeNodePoints(node, toNode) + const associativeLineTargets = + node.nodeData.data.associativeLineTargets[index] || {} + // 切换结构和布局,都会更新坐标 + const [startPoint, endPoint] = this.updateAllLinesPos( + node, + toNode, + associativeLineTargets + ) this.drawLine(startPoint, endPoint, node, toNode) }) }) @@ -183,11 +210,28 @@ class AssociativeLine { .fill({ color: 'none' }) clickPath.plot(pathStr) // 文字 - let text = this.createText({ path, clickPath, node, toNode, startPoint, endPoint, controlPoints }) + let text = this.createText({ + path, + clickPath, + node, + toNode, + startPoint, + endPoint, + controlPoints + }) // 点击事件 clickPath.click(e => { e.stopPropagation() - this.setActiveLine({ path, clickPath, text, node, toNode, startPoint, endPoint, controlPoints }) + this.setActiveLine({ + path, + clickPath, + text, + node, + toNode, + startPoint, + endPoint, + controlPoints + }) }) // 渲染关联线文字 this.renderText(this.getText(node, toNode), path, text) @@ -195,10 +239,17 @@ class AssociativeLine { } // 激活某根关联线 - setActiveLine({ path, clickPath, text, node, toNode, startPoint, endPoint, controlPoints }) { - let { - associativeLineActiveColor - } = this.mindMap.themeConfig + setActiveLine({ + path, + clickPath, + text, + node, + toNode, + startPoint, + endPoint, + controlPoints + }) { + let { associativeLineActiveColor } = this.mindMap.themeConfig // 如果当前存在激活节点,那么取消激活节点 if (this.mindMap.renderer.activeNodeList.length > 0) { this.clearActiveNodes() @@ -243,8 +294,10 @@ class AssociativeLine { // 创建连接线 createLine(fromNode) { - let { associativeLineWidth, associativeLineColor } = - this.mindMap.themeConfig + let { + associativeLineWidth, + associativeLineColor + } = this.mindMap.themeConfig if (this.isCreatingLine || !fromNode) return this.isCreatingLine = true this.creatingStartNode = fromNode @@ -279,14 +332,33 @@ class AssociativeLine { // 获取转换后的鼠标事件对象的坐标 getTransformedEventPos(e) { let { x, y } = this.mindMap.toPos(e.clientX, e.clientY) - let { scaleX, scaleY, translateX, translateY } = - this.mindMap.draw.transform() + let { + scaleX, + scaleY, + translateX, + translateY + } = this.mindMap.draw.transform() return { x: (x - translateX) / scaleX, y: (y - translateY) / scaleY } } + // 计算节点偏移位置 + getNodePos(node) { + const { translateX, translateY } = this.mindMap.draw.transform() + const { left, top, width, height } = node + let translateLeft = left + translateX + let translateTop = top + translateY + return { + left, + top, + translateLeft, + translateTop, + width, + height + } + } // 检测当前移动到的目标节点 checkOverlapNode(x, y) { this.overlapNode = null @@ -336,7 +408,12 @@ class AssociativeLine { } // 将目标节点id保存起来 let list = fromNode.nodeData.data.associativeLineTargets || [] - list.push(id) + // 连线节点是否存在相同的id,存在则阻止添加关联线 + const sameLine = list.some(item => item.id === id) + if (sameLine) { + return + } + list.push({ id }) // 保存控制点 let [startPoint, endPoint] = computeNodePoints(fromNode, toNode) let controlPoints = computeCubicBezierPathPoints( @@ -369,13 +446,16 @@ class AssociativeLine { if (!this.activeLine) return let [, , , node, toNode] = this.activeLine this.removeControls() - let { associativeLineTargets, associativeLineTargetControlOffsets, associativeLineText } = - node.nodeData.data + let { + associativeLineTargets, + associativeLineTargetControlOffsets, + associativeLineText + } = node.nodeData.data let targetIndex = getAssociativeLineTargetIndex(node, toNode) // 更新关联线文本数据 let newAssociativeLineText = {} if (associativeLineText) { - Object.keys(associativeLineText).forEach((item) => { + Object.keys(associativeLineText).forEach(item => { if (item !== toNode.nodeData.data.id) { newAssociativeLineText[item] = associativeLineText[item] } diff --git a/simple-mind-map/src/plugins/associativeLine/associativeLineControls.js b/simple-mind-map/src/plugins/associativeLine/associativeLineControls.js index 105a69ef..6e5c0641 100644 --- a/simple-mind-map/src/plugins/associativeLine/associativeLineControls.js +++ b/simple-mind-map/src/plugins/associativeLine/associativeLineControls.js @@ -1,7 +1,7 @@ import { getAssociativeLineTargetIndex, joinCubicBezierPath, - computeNodePoints, + getNodePoint, getDefaultControlPointOffsets } from './associativeLineUtils' @@ -62,14 +62,22 @@ function onControlPointMousemove(e) { // 更新当前拖拽的控制点的位置 this[this.mousedownControlPointKey].x(x - radius).y(y - radius) let [path, clickPath, text, node, toNode] = this.activeLine - let [startPoint, endPoint] = computeNodePoints(node, toNode) + let targetIndex = getAssociativeLineTargetIndex(node, toNode) + const { + associativeLineTargets, + associativeLineTargetControlOffsets + } = node.nodeData.data + const nodePos = this.getNodePos(node) + const toNodePos = this.getNodePos(toNode) + let [startPoint, endPoint] = this.updateAllLinesPos( + node, + toNode, + associativeLineTargets[targetIndex] + ) this.controlPointMousemoveState.startPoint = startPoint this.controlPointMousemoveState.endPoint = endPoint - let targetIndex = getAssociativeLineTargetIndex(node, toNode) this.controlPointMousemoveState.targetIndex = targetIndex let offsets = [] - let associativeLineTargetControlOffsets = - node.nodeData.data.associativeLineTargetControlOffsets if (!associativeLineTargetControlOffsets) { // 兼容0.4.5版本,没有associativeLineTargetControlOffsets的情况 offsets = getDefaultControlPointOffsets(startPoint, endPoint) @@ -80,6 +88,7 @@ function onControlPointMousemove(e) { let point2 = null // 拖拽的是控制点1 if (this.mousedownControlPointKey === 'controlPoint1') { + startPoint = getNodePoint(nodePos, '', 0, e) point1 = { x, y @@ -88,10 +97,22 @@ function onControlPointMousemove(e) { x: endPoint.x + offsets[1].x, y: endPoint.y + offsets[1].y } - // 更新控制点1的连线 - this.controlLine1.plot(startPoint.x, startPoint.y, point1.x, point1.y) + if (startPoint) { + // 保存更新后的坐标 + associativeLineTargets[targetIndex].startPoint = startPoint + this.controlPointMousemoveState.startPoint = startPoint + // 更新控制点1的连线 + this.controlLine1.plot(startPoint.x, startPoint.y, point1.x, point1.y) + // 更新关联线 + const pathStr = joinCubicBezierPath(startPoint, endPoint, point1, point2) + path.plot(pathStr) + clickPath.plot(pathStr) + this.updateTextPos(path, text) + this.updateTextEditBoxPos(text) + } } else { // 拖拽的是控制点2 + endPoint = getNodePoint(toNodePos, '', 0, e) point1 = { x: startPoint.x + offsets[0].x, y: startPoint.y + offsets[0].y @@ -100,15 +121,20 @@ function onControlPointMousemove(e) { x, y } - // 更新控制点2的连线 - this.controlLine2.plot(endPoint.x, endPoint.y, point2.x, point2.y) + if (endPoint) { + // 保存更新后结束节点的坐标 + associativeLineTargets[targetIndex].endPoint = endPoint + this.controlPointMousemoveState.endPoint = endPoint + // 更新控制点2的连线 + this.controlLine2.plot(endPoint.x, endPoint.y, point2.x, point2.y) + // 更新关联线 + const pathStr = joinCubicBezierPath(startPoint, endPoint, point1, point2) + path.plot(pathStr) + clickPath.plot(pathStr) + this.updateTextPos(path, text) + this.updateTextEditBoxPos(text) + } } - // 更新关联线 - let pathStr = joinCubicBezierPath(startPoint, endPoint, point1, point2) - path.plot(pathStr) - clickPath.plot(pathStr) - this.updateTextPos(path, text) - this.updateTextEditBoxPos(text) } // 控制点的鼠标移动事件 @@ -116,12 +142,18 @@ function onControlPointMouseup(e) { if (!this.isControlPointMousedown) return e.stopPropagation() e.preventDefault() - let { pos, startPoint, endPoint, targetIndex } = - this.controlPointMousemoveState + let { + pos, + startPoint, + endPoint, + targetIndex + } = this.controlPointMousemoveState let [, , , node] = this.activeLine let offsetList = [] - let associativeLineTargetControlOffsets = - node.nodeData.data.associativeLineTargetControlOffsets + const { + associativeLineTargets, + associativeLineTargetControlOffsets + } = node.nodeData.data if (!associativeLineTargetControlOffsets) { // 兼容0.4.5版本,没有associativeLineTargetControlOffsets的情况 offsetList[targetIndex] = getDefaultControlPointOffsets( @@ -150,7 +182,8 @@ function onControlPointMouseup(e) { } offsetList[targetIndex] = [offset1, offset2] this.mindMap.execCommand('SET_NODE_DATA', node, { - associativeLineTargetControlOffsets: offsetList + associativeLineTargetControlOffsets: offsetList, + associativeLineTargets }) // 这里要加个setTimeout0是因为draw_click事件比mouseup事件触发的晚,所以重置isControlPointMousedown需要等draw_click事件触发完以后 setTimeout(() => { diff --git a/simple-mind-map/src/plugins/associativeLine/associativeLineText.js b/simple-mind-map/src/plugins/associativeLine/associativeLineText.js index 26c31fdb..deb812d9 100644 --- a/simple-mind-map/src/plugins/associativeLine/associativeLineText.js +++ b/simple-mind-map/src/plugins/associativeLine/associativeLineText.js @@ -36,7 +36,7 @@ function showEditTextBox(g) { this.mindMap.keyCommand.addShortcut('Enter', () => { this.hideEditTextBox() }) - + 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);padding: 3px 5px;margin-left: -5px;margin-top: -3px;outline: none; word-break: break-all;` @@ -62,7 +62,8 @@ function showEditTextBox(g) { ).split(/\n/gim) this.textEditNode.style.fontFamily = associativeLineTextFontFamily this.textEditNode.style.fontSize = associativeLineTextFontSize * scale + 'px' - this.textEditNode.style.lineHeight = textLines.length > 1 ? associativeLineTextLineHeight : 'normal' + this.textEditNode.style.lineHeight = + textLines.length > 1 ? associativeLineTextLineHeight : 'normal' this.textEditNode.style.zIndex = this.mindMap.opt.nodeTextEditZIndex this.textEditNode.innerHTML = textLines.join('
') this.textEditNode.style.display = 'block' @@ -78,10 +79,12 @@ function onScale() { // 更新文本编辑框位置 function updateTextEditBoxPos(g) { let rect = g.node.getBoundingClientRect() - this.textEditNode.style.minWidth = rect.width + 10 + 'px' - this.textEditNode.style.minHeight = rect.height + 6 + 'px' - this.textEditNode.style.left = rect.left + 'px' - this.textEditNode.style.top = rect.top + 'px' + if (this.textEditNode) { + this.textEditNode.style.minWidth = `${rect.width + 10}px` + this.textEditNode.style.minHeight = `${rect.height + 6}px` + this.textEditNode.style.left = `${rect.left}px` + this.textEditNode.style.top = `${rect.top}px` + } } // 隐藏文本编辑框 @@ -116,8 +119,10 @@ function getText(node, toNode) { // 渲染关联线文字 function renderText(str, path, text) { if (!str) return - let { associativeLineTextFontSize, associativeLineTextLineHeight } = - this.mindMap.themeConfig + let { + associativeLineTextFontSize, + associativeLineTextLineHeight + } = this.mindMap.themeConfig text.clear() let textArr = str.split(/\n/gim) textArr.forEach((item, index) => { diff --git a/simple-mind-map/src/plugins/associativeLine/associativeLineUtils.js b/simple-mind-map/src/plugins/associativeLine/associativeLineUtils.js index 018c33b9..be1b619f 100644 --- a/simple-mind-map/src/plugins/associativeLine/associativeLineUtils.js +++ b/simple-mind-map/src/plugins/associativeLine/associativeLineUtils.js @@ -1,7 +1,7 @@ // 获取目标节点在起始节点的目标数组中的索引 export const getAssociativeLineTargetIndex = (node, toNode) => { return node.nodeData.data.associativeLineTargets.findIndex(item => { - return item === toNode.nodeData.data.id + return item.id === toNode.nodeData.data.id }) } @@ -54,28 +54,137 @@ export const cubicBezierPath = (x1, y1, x2, y2) => { ) } +// 计算关联线起始|结束坐标 +export const calcPoint = (node, e) => { + const { left, top, translateLeft, translateTop, width, height } = node + const clientX = e.clientX + const clientY = e.clientY + // 中心点的坐标 + const centerX = translateLeft + width / 2 + const centerY = translateTop + height / 2 + const translateCenterX = left + width / 2 + const translateCenterY = top + height / 2 + const theta = Math.atan(height / width) + // 矩形左上角坐标 + const deltaX = clientX - centerX + const deltaY = centerY - clientY + // 方向值 + const direction = Math.atan2(deltaY, deltaX) + // 默认坐标 + let x = left + width + let y = top + height + if (direction < theta && direction >= -theta) { + // 右边 + // 正切值 = 对边/邻边,对边 = 正切值*邻边 + const range = direction * (width / 2) + if (direction < theta && direction >= 0) { + // 中心点上边 + y = translateCenterY - range + } else if (direction >= -theta && direction < 0) { + // 中心点下方 + y = translateCenterY - range + } + return { + x, + y, + dir: 'right', + range + } + } else if (direction >= theta && direction < Math.PI - theta) { + // 上边 + y = top + let range = 0 + if (direction < Math.PI / 2 - theta && direction >= theta) { + // 正切值 = 对边/邻边,邻边 = 对边/正切值 + const side = height / 2 / direction + range = -side + // 中心点右侧 + x = translateCenterX + side + } else if ( + direction >= Math.PI / 2 - theta && + direction < Math.PI - theta + ) { + // 中心点左侧 + const tanValue = (centerX - clientX) / (centerY - clientY) + const side = (height / 2) * tanValue + range = side + x = translateCenterX - side + } + return { + x, + y, + dir: 'top', + range + } + } else if (direction < -theta && direction >= theta - Math.PI) { + // 下边 + let range = 0 + if (direction >= theta - Math.PI / 2 && direction < -theta) { + // 中心点右侧 + // 正切值 = 对边/邻边,邻边 = 对边/正切值 + const side = height / 2 / direction + range = side + x = translateCenterX - side + } else if ( + direction < theta - Math.PI / 2 && + direction >= theta - Math.PI + ) { + // 中心点左侧 + const tanValue = (centerX - clientX) / (centerY - clientY) + const side = (height / 2) * tanValue + range = -side + x = translateCenterX + side + } + return { + x, + y, + dir: 'bottom', + range + } + } + // 左边 + x = left + const tanValue = (centerY - clientY) / (centerX - clientX) + const range = tanValue * (width / 2) + if (direction >= -Math.PI && direction < theta - Math.PI) { + // 中心点右侧 + y = translateCenterY - range + } else if (direction < Math.PI && direction >= Math.PI - theta) { + // 中心点左侧 + y = translateCenterY - range + } + return { + x, + y, + dir: 'left', + range + } +} // 获取节点的连接点 -export const getNodePoint = (node, dir = 'right') => { +export const getNodePoint = (node, dir = 'right', range = 0, e = null) => { let { left, top, width, height } = node + if (e) { + return calcPoint(node, e) + } switch (dir) { case 'left': return { x: left, - y: top + height / 2 + y: top + height / 2 - range } case 'right': return { x: left + width, - y: top + height / 2 + y: top + height / 2 - range } case 'top': return { - x: left + width / 2, + x: left + width / 2 - range, y: top } case 'bottom': return { - x: left + width / 2, + x: left + width / 2 - range, y: top + height } default: @@ -179,4 +288,4 @@ export const getDefaultControlPointOffsets = (startPoint, endPoint) => { y: controlPoints[1].y - endPoint.y } ] -} \ No newline at end of file +} From df2dc96ba5fe4980c64a6cd13f8f81762b841da7 Mon Sep 17 00:00:00 2001 From: eseasky Date: Wed, 30 Aug 2023 20:09:16 +0800 Subject: [PATCH 3/4] =?UTF-8?q?Feat:=201.=E9=85=8D=E7=BD=AE=E5=BC=80?= =?UTF-8?q?=E5=90=AF=E6=94=B6=E8=B5=B7=E6=98=BE=E7=A4=BA=E8=8A=82=E7=82=B9?= =?UTF-8?q?=E6=95=B0=E9=87=8F=EF=BC=9B2.=E6=94=AF=E6=8C=81=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E4=BC=A0=E5=85=A5=E4=B8=80=E4=B8=AA=E5=87=BD=E6=95=B0?= =?UTF-8?q?=EF=BC=8C=E5=85=81=E8=AE=B8=E6=8E=A7=E5=88=B6=E6=94=B6=E8=B5=B7?= =?UTF-8?q?=E6=97=B6=E6=98=BE=E7=A4=BA=E7=9A=84=E5=86=85=E5=AE=B9=EF=BC=9B?= =?UTF-8?q?3.=E6=94=AF=E6=8C=81=E9=85=8D=E7=BD=AEexpandBtnSize=E5=B1=95?= =?UTF-8?q?=E5=BC=80=E6=94=B6=E8=B5=B7=E6=8C=89=E9=92=AE=E7=9A=84=E5=B0=BA?= =?UTF-8?q?=E5=AF=B8=EF=BC=9B4.=E6=94=B6=E8=B5=B7=E6=97=B6=E7=9A=84?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E5=8F=AF=E4=BB=A5=E4=BD=BF=E7=94=A8expandBtn?= =?UTF-8?q?Style=E9=85=8D=E7=BD=AE=E9=A2=9C=E8=89=B2=EF=BC=8C=E5=AD=97?= =?UTF-8?q?=E4=BD=93=EF=BC=8C=E8=BE=B9=E6=A1=86=E9=A2=9C=E8=89=B2=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/constants/defaultOptions.js | 10 +++- simple-mind-map/src/core/render/node/Style.js | 46 +++++++++++----- .../src/core/render/node/nodeExpandBtn.js | 52 ++++++++++--------- 3 files changed, 71 insertions(+), 37 deletions(-) diff --git a/simple-mind-map/src/constants/defaultOptions.js b/simple-mind-map/src/constants/defaultOptions.js index 0c8262e1..226ef97b 100644 --- a/simple-mind-map/src/constants/defaultOptions.js +++ b/simple-mind-map/src/constants/defaultOptions.js @@ -68,13 +68,21 @@ export const defaultOpt = { // 展开收起按钮的颜色 expandBtnStyle: { color: '#808080', - fill: '#fff' + fill: '#fff', + fontSize: 13, + strokeColor: '#333333' }, // 自定义展开收起按钮的图标 expandBtnIcon: { open: '', // svg字符串 close: '' }, + // 处理收起节点数量 + expandBtnNumHandler: num => { + return num + }, + // 是否显示带数量的收起按钮 + isShowExpandNum: true, // 是否只有当鼠标在画布内才响应快捷键事件 enableShortcutOnlyWhenMouseInSvg: true, // 初始根节点的位置 diff --git a/simple-mind-map/src/core/render/node/Style.js b/simple-mind-map/src/core/render/node/Style.js index 9cd452d2..a0f922ce 100644 --- a/simple-mind-map/src/core/render/node/Style.js +++ b/simple-mind-map/src/core/render/node/Style.js @@ -1,6 +1,15 @@ -import { tagColorList, nodeDataNoStylePropList } from '../../../constants/constant' +import { + tagColorList, + nodeDataNoStylePropList +} from '../../../constants/constant' const rootProp = ['paddingX', 'paddingY'] -const backgroundStyleProps = ['backgroundColor', 'backgroundImage', 'backgroundRepeat', 'backgroundPosition', 'backgroundSize'] +const backgroundStyleProps = [ + 'backgroundColor', + 'backgroundImage', + 'backgroundRepeat', + 'backgroundPosition', + 'backgroundSize' +] // 样式类 class Style { @@ -10,12 +19,18 @@ class Style { if (!Style.cacheStyle) { Style.cacheStyle = {} let style = window.getComputedStyle(el) - backgroundStyleProps.forEach((prop) => { + backgroundStyleProps.forEach(prop => { Style.cacheStyle[prop] = style[prop] }) } // 设置新样式 - let { backgroundColor, backgroundImage, backgroundRepeat, backgroundPosition, backgroundSize } = themeConfig + let { + backgroundColor, + backgroundImage, + backgroundRepeat, + backgroundPosition, + backgroundSize + } = themeConfig el.style.backgroundColor = backgroundColor if (backgroundImage && backgroundImage !== 'none') { el.style.backgroundImage = `url(${backgroundImage})` @@ -30,7 +45,7 @@ class Style { // 移除背景样式 static removeBackgroundStyle(el) { if (!Style.cacheStyle) return - backgroundStyleProps.forEach((prop) => { + backgroundStyleProps.forEach(prop => { el.style[prop] = Style.cacheStyle[prop] }) Style.cacheStyle = null @@ -142,10 +157,10 @@ class Style { // 获取文本样式 getTextFontStyle() { - return { - italic: this.merge('fontStyle') === 'italic', - bold: this.merge('fontWeight'), - fontSize: this.merge('fontSize'), + return { + italic: this.merge('fontStyle') === 'italic', + bold: this.merge('fontWeight'), + fontSize: this.merge('fontSize'), fontFamily: this.merge('fontFamily') } } @@ -201,19 +216,26 @@ class Style { // 展开收起按钮 iconBtn(node, node2, fillNode) { - let { color, fill } = this.ctx.mindMap.opt.expandBtnStyle || { + let { color, fill, fontSize, fontColor } = this.ctx.mindMap.opt + .expandBtnStyle || { color: '#808080', - fill: '#fff' + fill: '#fff', + fontSize: 12, + strokeColor: '#333333', + fontColor: '#333333' } node.fill({ color: color }) node2.fill({ color: color }) fillNode.fill({ color: fill }) + if (this.ctx.mindMap.opt.isShowExpandNum) { + node.attr({ 'font-size': fontSize, 'font-color': fontColor }) + } } // 是否设置了自定义的样式 hasCustomStyle() { let res = false - Object.keys(this.ctx.nodeData.data).forEach((item) => { + Object.keys(this.ctx.nodeData.data).forEach(item => { if (!nodeDataNoStylePropList.includes(item)) { res = true } diff --git a/simple-mind-map/src/core/render/node/nodeExpandBtn.js b/simple-mind-map/src/core/render/node/nodeExpandBtn.js index 99252eda..3fb13f16 100644 --- a/simple-mind-map/src/core/render/node/nodeExpandBtn.js +++ b/simple-mind-map/src/core/render/node/nodeExpandBtn.js @@ -3,28 +3,32 @@ import { SVG, Circle, G } from '@svgdotjs/svg.js' // 创建展开收起按钮的内容节点 function createExpandNodeContent() { - // 实时更新收起节点数字 - // if (this._openExpandNode) { - // return - // } - let { close } = this.mindMap.opt.expandBtnIcon || {} - // 计算子节点数量 - const count = this.sumNode(this.nodeData.children) - // 生成按钮 - const node = SVG() - .text(count) - .font({ family: 'Inconsolata' }) - node.attr('font-size', 14) - // 展开的节点 - this._openExpandNode = node.size(this.expandBtnSize, this.expandBtnSize) - // 数字不同偏移量大小处理 - if (count < 10) { - this._openExpandNode.x(6).y(-this.expandBtnSize / 2) - } else if (count >= 10 && count < 100) { - this._openExpandNode.x(1).y(-this.expandBtnSize / 2) + if (this._openExpandNode && !this.mindMap.opt.isShowExpandNum) { + return + } + let { close, open } = this.mindMap.opt.expandBtnIcon || {} + // 根据配置判断是否显示数量按钮 + if (this.mindMap.opt.isShowExpandNum) { + // 计算子节点数量 + let count = this.sumNode(this.nodeData.children) + count = this.mindMap.opt.expandBtnNumHandler(count) + // 展开的节点 + this._openExpandNode = SVG() + .text(count) + .size(this.expandBtnSize, this.expandBtnSize) + // 文本垂直居中 + this._openExpandNode.attr({ + 'text-anchor': 'middle', + 'dominant-baseline': 'middle', + x: this.expandBtnSize / 2, + y: 2 + }) } else { + this._openExpandNode = SVG(open || btnsSvg.open).size( + this.expandBtnSize, + this.expandBtnSize + ) this._openExpandNode.x(0).y(-this.expandBtnSize / 2) - node.attr('font-size', 12) } // 收起的节点 this._closeExpandNode = SVG(close || btnsSvg.close).size( @@ -43,8 +47,6 @@ function createExpandNodeContent() { this._fillExpandNode ) } - -// 统计折叠的子节点数量 function sumNode(data = []) { return data.reduce( (total, cur) => total + this.sumNode(cur.children || []), @@ -71,9 +73,11 @@ function updateExpandBtnNode() { if (this._expandBtn) { // 如果是收起按钮加上边框 - if (!expand) { + let opt = this.mindMap.opt + if (!expand && opt.isShowExpandNum) { + // 数字按钮添加边框 this._fillExpandNode.stroke({ - color: '#333333' + color: opt.expandBtnStyle.strokeColor }) } this._expandBtn.add(this._fillExpandNode).add(node) From 91bc0351b8ee88bcbfe6cecaa0bbccdebd6c103a Mon Sep 17 00:00:00 2001 From: eseasky Date: Wed, 30 Aug 2023 20:11:44 +0800 Subject: [PATCH 4/4] =?UTF-8?q?Feat:=20=E5=85=B3=E8=81=94=E7=BA=BF?= =?UTF-8?q?=E6=8B=96=E5=8A=A8=E6=95=88=E6=9E=9C=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/plugins/AssociativeLine.js | 56 ++++++++++++------- .../associativeLineControls.js | 52 ++++++++++------- .../associativeLine/associativeLineUtils.js | 7 +-- 3 files changed, 70 insertions(+), 45 deletions(-) diff --git a/simple-mind-map/src/plugins/AssociativeLine.js b/simple-mind-map/src/plugins/AssociativeLine.js index 06dc6fa6..637ba053 100644 --- a/simple-mind-map/src/plugins/AssociativeLine.js +++ b/simple-mind-map/src/plugins/AssociativeLine.js @@ -6,7 +6,8 @@ import { cubicBezierPath, getNodePoint, computeNodePoints, - getNodeLinePath + getNodeLinePath, + getDefaultControlPointOffsets } from './associativeLine/associativeLineUtils' import associativeLineControlsMethods from './associativeLine/associativeLineControls' import associativeLineTextMethods from './associativeLine/associativeLineText' @@ -106,22 +107,21 @@ class AssociativeLine { }) } // 判断关联线坐标是否变更,有变更则使用变化后的坐标,无则默认坐标 - updateAllLinesPos(node, toNode, associativeLineTargets) { - let startPoint = {} - let endPoint = {} + updateAllLinesPos(node, toNode, associativeLinePoint) { + let [startPoint, endPoint] = computeNodePoints(node, toNode) let nodeRange = 0 let nodeDir = 'right' let toNodeRange = 0 let toNodeDir = 'right' - if (associativeLineTargets.startPoint && associativeLineTargets.endPoint) { - nodeRange = associativeLineTargets.startPoint.range || 0 - nodeDir = associativeLineTargets.startPoint.dir || 'right' + if (associativeLinePoint.startPoint) { + nodeRange = associativeLinePoint.startPoint.range || 0 + nodeDir = associativeLinePoint.startPoint.dir || 'right' startPoint = getNodePoint(node, nodeDir, nodeRange) - toNodeRange = associativeLineTargets.endPoint.range || 0 - toNodeDir = associativeLineTargets.endPoint.dir || 'right' + } + if (associativeLinePoint.endPoint) { + toNodeRange = associativeLinePoint.endPoint.range || 0 + toNodeDir = associativeLinePoint.endPoint.dir || 'right' endPoint = getNodePoint(toNode, toNodeDir, toNodeRange) - } else { - ;[startPoint, endPoint] = computeNodePoints(node, toNode) } return [startPoint, endPoint] } @@ -158,15 +158,15 @@ class AssociativeLine { ) nodeToIds.forEach((ids, node) => { ids.forEach((id, index) => { - let toNode = idToNode.get(id.id) + let toNode = idToNode.get(id) if (!node || !toNode) return - const associativeLineTargets = - node.nodeData.data.associativeLineTargets[index] || {} + const associativeLinePoint = + node.nodeData.data.associativeLinePoint[index] || {} // 切换结构和布局,都会更新坐标 const [startPoint, endPoint] = this.updateAllLinesPos( node, toNode, - associativeLineTargets + associativeLinePoint ) this.drawLine(startPoint, endPoint, node, toNode) }) @@ -346,10 +346,15 @@ class AssociativeLine { // 计算节点偏移位置 getNodePos(node) { - const { translateX, translateY } = this.mindMap.draw.transform() + const { + scaleX, + scaleY, + translateX, + translateY + } = this.mindMap.draw.transform() const { left, top, width, height } = node - let translateLeft = left + translateX - let translateTop = top + translateY + let translateLeft = left * scaleX + translateX + let translateTop = top * scaleY + translateY return { left, top, @@ -409,11 +414,11 @@ class AssociativeLine { // 将目标节点id保存起来 let list = fromNode.nodeData.data.associativeLineTargets || [] // 连线节点是否存在相同的id,存在则阻止添加关联线 - const sameLine = list.some(item => item.id === id) + const sameLine = list.some(item => item === id) if (sameLine) { return } - list.push({ id }) + list.push(id) // 保存控制点 let [startPoint, endPoint] = computeNodePoints(fromNode, toNode) let controlPoints = computeCubicBezierPathPoints( @@ -435,9 +440,13 @@ class AssociativeLine { y: controlPoints[1].y - endPoint.y } ] + let associativeLinePoint = fromNode.nodeData.data.associativeLinePoint || [] + // 记录关联的起始|结束坐标 + associativeLinePoint[list.length - 1] = [{ startPoint, endPoint }] this.mindMap.execCommand('SET_NODE_DATA', fromNode, { associativeLineTargets: list, - associativeLineTargetControlOffsets: offsetList + associativeLineTargetControlOffsets: offsetList, + associativeLinePoint }) } @@ -448,6 +457,7 @@ class AssociativeLine { this.removeControls() let { associativeLineTargets, + associativeLinePoint, associativeLineTargetControlOffsets, associativeLineText } = node.nodeData.data @@ -466,6 +476,10 @@ class AssociativeLine { associativeLineTargets: associativeLineTargets.filter((_, index) => { return index !== targetIndex }), + // 连接线坐标 + associativeLinePoint: associativeLinePoint.filter((_, index) => { + return index !== targetIndex + }), // 偏移量 associativeLineTargetControlOffsets: associativeLineTargetControlOffsets ? associativeLineTargetControlOffsets.filter((_, index) => { diff --git a/simple-mind-map/src/plugins/associativeLine/associativeLineControls.js b/simple-mind-map/src/plugins/associativeLine/associativeLineControls.js index 6e5c0641..22a8dceb 100644 --- a/simple-mind-map/src/plugins/associativeLine/associativeLineControls.js +++ b/simple-mind-map/src/plugins/associativeLine/associativeLineControls.js @@ -61,10 +61,10 @@ function onControlPointMousemove(e) { } // 更新当前拖拽的控制点的位置 this[this.mousedownControlPointKey].x(x - radius).y(y - radius) - let [path, clickPath, text, node, toNode] = this.activeLine + let [, , , node, toNode] = this.activeLine let targetIndex = getAssociativeLineTargetIndex(node, toNode) const { - associativeLineTargets, + associativeLinePoint, associativeLineTargetControlOffsets } = node.nodeData.data const nodePos = this.getNodePos(node) @@ -72,7 +72,7 @@ function onControlPointMousemove(e) { let [startPoint, endPoint] = this.updateAllLinesPos( node, toNode, - associativeLineTargets[targetIndex] + associativeLinePoint[targetIndex] ) this.controlPointMousemoveState.startPoint = startPoint this.controlPointMousemoveState.endPoint = endPoint @@ -99,16 +99,10 @@ function onControlPointMousemove(e) { } if (startPoint) { // 保存更新后的坐标 - associativeLineTargets[targetIndex].startPoint = startPoint + associativeLinePoint[targetIndex].startPoint = startPoint this.controlPointMousemoveState.startPoint = startPoint // 更新控制点1的连线 this.controlLine1.plot(startPoint.x, startPoint.y, point1.x, point1.y) - // 更新关联线 - const pathStr = joinCubicBezierPath(startPoint, endPoint, point1, point2) - path.plot(pathStr) - clickPath.plot(pathStr) - this.updateTextPos(path, text) - this.updateTextEditBoxPos(text) } } else { // 拖拽的是控制点2 @@ -123,18 +117,35 @@ function onControlPointMousemove(e) { } if (endPoint) { // 保存更新后结束节点的坐标 - associativeLineTargets[targetIndex].endPoint = endPoint + associativeLinePoint[targetIndex].endPoint = endPoint this.controlPointMousemoveState.endPoint = endPoint // 更新控制点2的连线 this.controlLine2.plot(endPoint.x, endPoint.y, point2.x, point2.y) - // 更新关联线 - const pathStr = joinCubicBezierPath(startPoint, endPoint, point1, point2) - path.plot(pathStr) - clickPath.plot(pathStr) - this.updateTextPos(path, text) - this.updateTextEditBoxPos(text) } } + this.updataAassociativeLine( + startPoint, + endPoint, + point1, + point2, + this.activeLine + ) +} + +function updataAassociativeLine( + startPoint, + endPoint, + point1, + point2, + activeLine +) { + const [path, clickPath, text] = activeLine + // 更新关联线 + const pathStr = joinCubicBezierPath(startPoint, endPoint, point1, point2) + path.plot(pathStr) + clickPath.plot(pathStr) + this.updateTextPos(path, text) + this.updateTextEditBoxPos(text) } // 控制点的鼠标移动事件 @@ -151,7 +162,7 @@ function onControlPointMouseup(e) { let [, , , node] = this.activeLine let offsetList = [] const { - associativeLineTargets, + associativeLinePoint, associativeLineTargetControlOffsets } = node.nodeData.data if (!associativeLineTargetControlOffsets) { @@ -183,7 +194,7 @@ function onControlPointMouseup(e) { offsetList[targetIndex] = [offset1, offset2] this.mindMap.execCommand('SET_NODE_DATA', node, { associativeLineTargetControlOffsets: offsetList, - associativeLineTargets + associativeLinePoint }) // 这里要加个setTimeout0是因为draw_click事件比mouseup事件触发的晚,所以重置isControlPointMousedown需要等draw_click事件触发完以后 setTimeout(() => { @@ -270,5 +281,6 @@ export default { renderControls, removeControls, hideControls, - showControls + showControls, + updataAassociativeLine } diff --git a/simple-mind-map/src/plugins/associativeLine/associativeLineUtils.js b/simple-mind-map/src/plugins/associativeLine/associativeLineUtils.js index be1b619f..b68216e5 100644 --- a/simple-mind-map/src/plugins/associativeLine/associativeLineUtils.js +++ b/simple-mind-map/src/plugins/associativeLine/associativeLineUtils.js @@ -1,7 +1,7 @@ // 获取目标节点在起始节点的目标数组中的索引 export const getAssociativeLineTargetIndex = (node, toNode) => { return node.nodeData.data.associativeLineTargets.findIndex(item => { - return item.id === toNode.nodeData.data.id + return item === toNode.nodeData.data.id }) } @@ -54,7 +54,6 @@ export const cubicBezierPath = (x1, y1, x2, y2) => { ) } -// 计算关联线起始|结束坐标 export const calcPoint = (node, e) => { const { left, top, translateLeft, translateTop, width, height } = node const clientX = e.clientX @@ -220,8 +219,8 @@ export const computeNodePoints = (fromNode, toNode) => { toDir = 'bottom' } else if (offsetY > 0 && -offsetY < offsetX && offsetY > offsetX) { // down - fromDir = 'bottom' - toDir = 'top' + fromDir = 'right' + toDir = 'right' } return [getNodePoint(fromNode, fromDir), getNodePoint(toNode, toDir)] }