Feat:1.支持单独定义每条关联线的样式;2.新增取消激活关联线的事件;3.修复关联线文本存在空行时编辑框渲染异常的问题

This commit is contained in:
街角小林 2024-12-11 17:53:08 +08:00
parent ade7a95f3c
commit 732b6b50b0
5 changed files with 159 additions and 41 deletions

View File

@ -85,7 +85,7 @@ export const defaultOpt = {
// 是否只有当鼠标在画布内才响应快捷键事件 // 是否只有当鼠标在画布内才响应快捷键事件
enableShortcutOnlyWhenMouseInSvg: true, enableShortcutOnlyWhenMouseInSvg: true,
// 自定义判断是否响应快捷键事件优先级比enableShortcutOnlyWhenMouseInSvg选项高 // 自定义判断是否响应快捷键事件优先级比enableShortcutOnlyWhenMouseInSvg选项高
// 可以传递一个函数接收事件对象e为参数需要返回true或false返回true代表允许响应快捷键事件反之不允许库默认当事件目标为body或为文本编辑框元素时响应快捷键,其他不响应 // 可以传递一个函数接收事件对象e为参数需要返回true或false返回true代表允许响应快捷键事件反之不允许库默认当事件目标为body或为文本编辑框元素(普通文本编辑框、富文本编辑框、关联线文本编辑框)时响应快捷键,其他不响应
customCheckEnableShortcut: null, customCheckEnableShortcut: null,
// 初始根节点的位置 // 初始根节点的位置
initRootNodePosition: null, initRootNodePosition: null,

View File

@ -11,11 +11,25 @@ import {
import associativeLineControlsMethods from './associativeLine/associativeLineControls' import associativeLineControlsMethods from './associativeLine/associativeLineControls'
import associativeLineTextMethods from './associativeLine/associativeLineText' import associativeLineTextMethods from './associativeLine/associativeLineText'
const styleProps = [
'associativeLineWidth',
'associativeLineColor',
'associativeLineActiveWidth',
'associativeLineActiveColor',
'associativeLineDasharray',
'associativeLineTextColor',
'associativeLineTextFontSize',
'associativeLineTextLineHeight',
'associativeLineTextFontFamily'
]
// 关联线插件 // 关联线插件
class AssociativeLine { class AssociativeLine {
constructor(opt = {}) { constructor(opt = {}) {
this.mindMap = opt.mindMap this.mindMap = opt.mindMap
this.associativeLineDraw = this.mindMap.associativeLineDraw this.associativeLineDraw = this.mindMap.associativeLineDraw
// 本次不要重新渲染连线
this.isNotRenderAllLines = false
// 当前所有连接线 // 当前所有连接线
this.lineList = [] this.lineList = []
// 当前激活的连接线 // 当前激活的连接线
@ -27,9 +41,6 @@ class AssociativeLine {
this.overlapNode = null // 创建过程中的目标节点 this.overlapNode = null // 创建过程中的目标节点
// 是否有节点正在被拖拽 // 是否有节点正在被拖拽
this.isNodeDragging = false this.isNodeDragging = false
// 箭头图标
this.markerPath = null
this.marker = this.createMarker()
// 控制点 // 控制点
this.controlLine1 = null this.controlLine1 = null
this.controlLine2 = null this.controlLine2 = null
@ -112,6 +123,25 @@ class AssociativeLine {
this.mindMap.off('beforeDestroy', this.onBeforeDestroy) this.mindMap.off('beforeDestroy', this.onBeforeDestroy)
} }
// 获取关联线的样式配置
// 优先级:关联线自定义样式、节点自定义样式、主题的节点层级样式、主题的最外层样式
getStyleConfig(node, toNode) {
let lineStyle = {}
if (toNode) {
const associativeLineStyle = node.getData('associativeLineStyle') || {}
lineStyle = associativeLineStyle[toNode.getData('uid')] || {}
}
const res = {}
styleProps.forEach(prop => {
if (typeof lineStyle[prop] !== 'undefined') {
res[prop] = lineStyle[prop]
} else {
res[prop] = node.getStyle(prop)
}
})
return res
}
// 实例销毁时清除关联线文字编辑框 // 实例销毁时清除关联线文字编辑框
onBeforeDestroy() { onBeforeDestroy() {
this.hideEditTextBox() this.hideEditTextBox()
@ -140,12 +170,12 @@ class AssociativeLine {
} }
// 创建箭头 // 创建箭头
createMarker() { createMarker(callback = () => {}) {
return this.associativeLineDraw.marker(20, 20, add => { return this.associativeLineDraw.marker(20, 20, add => {
add.ref(12, 5) add.ref(12, 5)
add.size(10, 10) add.size(10, 10)
add.attr('orient', 'auto-start-reverse') add.attr('orient', 'auto-start-reverse')
this.markerPath = add.path('M0,0 L2,5 L0,10 L10,5 Z') callback(add.path('M0,0 L2,5 L0,10 L10,5 Z'))
}) })
} }
@ -172,6 +202,10 @@ class AssociativeLine {
// 渲染所有连线 // 渲染所有连线
renderAllLines() { renderAllLines() {
if (this.isNotRenderAllLines) {
this.isNotRenderAllLines = false
return
}
// 先移除 // 先移除
this.removeAllLines() this.removeAllLines()
this.removeControls() this.removeControls()
@ -223,11 +257,14 @@ class AssociativeLine {
associativeLineWidth, associativeLineWidth,
associativeLineColor, associativeLineColor,
associativeLineActiveWidth, associativeLineActiveWidth,
associativeLineActiveColor,
associativeLineDasharray associativeLineDasharray
} = this.mindMap.themeConfig } = this.getStyleConfig(node, toNode)
// 箭头 // 箭头
this.markerPath let markerPath = null
const marker = this.createMarker(p => {
markerPath = p
})
markerPath
.stroke({ color: associativeLineColor }) .stroke({ color: associativeLineColor })
.fill({ color: associativeLineColor }) .fill({ color: associativeLineColor })
// 路径 // 路径
@ -247,7 +284,7 @@ class AssociativeLine {
}) })
.fill({ color: 'none' }) .fill({ color: 'none' })
path.plot(pathStr) path.plot(pathStr)
path.marker('end', this.marker) path.marker('end', marker)
// 不可见的点击线 // 不可见的点击线
let clickPath = this.associativeLineDraw.path() let clickPath = this.associativeLineDraw.path()
clickPath clickPath
@ -258,6 +295,7 @@ class AssociativeLine {
let text = this.createText({ let text = this.createText({
path, path,
clickPath, clickPath,
markerPath,
node, node,
toNode, toNode,
startPoint, startPoint,
@ -270,6 +308,7 @@ class AssociativeLine {
this.setActiveLine({ this.setActiveLine({
path, path,
clickPath, clickPath,
markerPath,
text, text,
node, node,
toNode, toNode,
@ -284,14 +323,60 @@ class AssociativeLine {
this.showEditTextBox(text) this.showEditTextBox(text)
}) })
// 渲染关联线文字 // 渲染关联线文字
this.renderText(this.getText(node, toNode), path, text) this.renderText(this.getText(node, toNode), path, text, node, toNode)
this.lineList.push([path, clickPath, text, node, toNode]) this.lineList.push([path, clickPath, text, node, toNode])
} }
// 更新当前激活连线的样式,一般在自定义了节点关联线的样式后调用
// 直接调用node.setStyle方法更新样式会直接触发关联线更新但是关联线的激活状态会丢失
// 所以可以调用node.setData方法更新数据然后再调用该方法更新样式这样关联线激活状态不会丢失
updateActiveLineStyle() {
if (!this.activeLine) return
this.isNotRenderAllLines = true
const [path, clickPath, text, node, toNode, markerPath] = this.activeLine
const {
associativeLineWidth,
associativeLineColor,
associativeLineDasharray,
associativeLineActiveWidth,
associativeLineActiveColor,
associativeLineTextColor,
associativeLineTextFontFamily,
associativeLineTextFontSize
} = this.getStyleConfig(node, toNode)
path
.stroke({
width: associativeLineWidth,
color: associativeLineColor,
dasharray: associativeLineDasharray || [6, 4]
})
.fill({ color: 'none' })
clickPath
.stroke({
width: associativeLineActiveWidth,
color: associativeLineActiveColor
})
.fill({ color: 'none' })
markerPath
.stroke({ color: associativeLineColor })
.fill({ color: associativeLineColor })
text.find('text').forEach(textNode => {
textNode
.fill({
color: associativeLineTextColor
})
.css({
'font-family': associativeLineTextFontFamily,
'font-size': associativeLineTextFontSize + 'px'
})
})
}
// 激活某根关联线 // 激活某根关联线
setActiveLine({ setActiveLine({
path, path,
clickPath, clickPath,
markerPath,
text, text,
node, node,
toNode, toNode,
@ -299,25 +384,33 @@ class AssociativeLine {
endPoint, endPoint,
controlPoints controlPoints
}) { }) {
let { associativeLineActiveColor } = this.mindMap.themeConfig let { associativeLineActiveColor } = this.getStyleConfig(node, toNode)
// 如果当前存在激活节点,那么取消激活节点 // 如果当前存在激活节点,那么取消激活节点
this.mindMap.execCommand('CLEAR_ACTIVE_NODE') this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
// 否则清除当前的关联线的激活状态,如果有的话 // 否则清除当前的关联线的激活状态,如果有的话
this.clearActiveLine() this.clearActiveLine()
// 保存当前激活的关联线信息 // 保存当前激活的关联线信息
this.activeLine = [path, clickPath, text, node, toNode] this.activeLine = [path, clickPath, text, node, toNode, markerPath]
// 让不可见的点击线显示 // 让不可见的点击线显示
clickPath.stroke({ color: associativeLineActiveColor }) clickPath.stroke({ color: associativeLineActiveColor })
// 如果没有输入过关联线文字,那么显示默认文字 // 如果没有输入过关联线文字,那么显示默认文字
if (!this.getText(node, toNode)) { if (!this.getText(node, toNode)) {
this.renderText(this.mindMap.opt.defaultAssociativeLineText, path, text) this.renderText(
this.mindMap.opt.defaultAssociativeLineText,
path,
text,
node,
toNode
)
} }
// 渲染控制点和连线 // 渲染控制点和连线
this.renderControls( this.renderControls(
startPoint, startPoint,
endPoint, endPoint,
controlPoints[0], controlPoints[0],
controlPoints[1] controlPoints[1],
node,
toNode
) )
this.mindMap.emit('associative_line_click', path, clickPath, node, toNode) this.mindMap.emit('associative_line_click', path, clickPath, node, toNode)
this.front() this.front()
@ -346,7 +439,7 @@ class AssociativeLine {
associativeLineWidth, associativeLineWidth,
associativeLineColor, associativeLineColor,
associativeLineDasharray associativeLineDasharray
} = this.mindMap.themeConfig } = this.getStyleConfig(fromNode)
if (this.isCreatingLine || !fromNode) return if (this.isCreatingLine || !fromNode) return
this.front() this.front()
this.isCreatingLine = true this.isCreatingLine = true
@ -360,10 +453,14 @@ class AssociativeLine {
}) })
.fill({ color: 'none' }) .fill({ color: 'none' })
// 箭头 // 箭头
this.markerPath let markerPath = null
const marker = this.createMarker(p => {
markerPath = p
})
markerPath
.stroke({ color: associativeLineColor }) .stroke({ color: associativeLineColor })
.fill({ color: associativeLineColor }) .fill({ color: associativeLineColor })
this.creatingLine.marker('end', this.marker) this.creatingLine.marker('end', marker)
} }
// 取消创建关联线 // 取消创建关联线
@ -529,7 +626,8 @@ class AssociativeLine {
associativeLineTargets, associativeLineTargets,
associativeLinePoint, associativeLinePoint,
associativeLineTargetControlOffsets, associativeLineTargetControlOffsets,
associativeLineText associativeLineText,
associativeLineStyle
} = node.getData() } = node.getData()
associativeLinePoint = associativeLinePoint || [] associativeLinePoint = associativeLinePoint || []
let targetIndex = getAssociativeLineTargetIndex(node, toNode) let targetIndex = getAssociativeLineTargetIndex(node, toNode)
@ -542,6 +640,15 @@ class AssociativeLine {
} }
}) })
} }
// 更新关联线样式数据
let newAssociativeLineStyle = {}
if (associativeLineStyle) {
Object.keys(associativeLineStyle).forEach(item => {
if (item !== toNode.getData('uid')) {
newAssociativeLineStyle[item] = associativeLineStyle[item]
}
})
}
this.mindMap.execCommand('SET_NODE_DATA', node, { this.mindMap.execCommand('SET_NODE_DATA', node, {
// 目标 // 目标
associativeLineTargets: associativeLineTargets.filter((_, index) => { associativeLineTargets: associativeLineTargets.filter((_, index) => {
@ -558,7 +665,9 @@ class AssociativeLine {
}) })
: [], : [],
// 文本 // 文本
associativeLineText: newAssociativeLineText associativeLineText: newAssociativeLineText,
// 样式
associativeLineStyle: newAssociativeLineStyle
}) })
} }
@ -578,6 +687,7 @@ class AssociativeLine {
this.activeLine = null this.activeLine = null
this.removeControls() this.removeControls()
this.back() this.back()
this.mindMap.emit('associative_line_deactivate')
} }
} }

View File

@ -6,8 +6,8 @@ import {
} from './associativeLineUtils' } from './associativeLineUtils'
// 创建控制点、连线节点 // 创建控制点、连线节点
function createControlNodes() { function createControlNodes(node, toNode) {
let { associativeLineActiveColor } = this.mindMap.themeConfig let { associativeLineActiveColor } = this.getStyleConfig(node, toNode)
// 连线 // 连线
this.controlLine1 = this.associativeLineDraw this.controlLine1 = this.associativeLineDraw
.line() .line()
@ -16,13 +16,13 @@ function createControlNodes() {
.line() .line()
.stroke({ color: associativeLineActiveColor, width: 2 }) .stroke({ color: associativeLineActiveColor, width: 2 })
// 控制点 // 控制点
this.controlPoint1 = this.createOneControlNode('controlPoint1') this.controlPoint1 = this.createOneControlNode('controlPoint1', node, toNode)
this.controlPoint2 = this.createOneControlNode('controlPoint2') this.controlPoint2 = this.createOneControlNode('controlPoint2', node, toNode)
} }
// 创建控制点 // 创建控制点
function createOneControlNode(pointKey) { function createOneControlNode(pointKey, node, toNode) {
let { associativeLineActiveColor } = this.mindMap.themeConfig let { associativeLineActiveColor } = this.getStyleConfig(node, toNode)
return this.associativeLineDraw return this.associativeLineDraw
.circle(this.controlPointDiameter) .circle(this.controlPointDiameter)
.stroke({ color: associativeLineActiveColor }) .stroke({ color: associativeLineActiveColor })
@ -221,10 +221,10 @@ function resetControlPoint() {
} }
// 渲染控制点 // 渲染控制点
function renderControls(startPoint, endPoint, point1, point2) { function renderControls(startPoint, endPoint, point1, point2, node, toNode) {
if (!this.mindMap.opt.enableAdjustAssociativeLinePoints) return if (!this.mindMap.opt.enableAdjustAssociativeLinePoints) return
if (!this.controlLine1) { if (!this.controlLine1) {
this.createControlNodes() this.createControlNodes(node, toNode)
} }
let radius = this.controlPointDiameter / 2 let radius = this.controlPointDiameter / 2
// 控制点和起终点的连线 // 控制点和起终点的连线

View File

@ -43,6 +43,7 @@ function showEditTextBox(g) {
// 输入框元素没有创建过,则先创建 // 输入框元素没有创建过,则先创建
if (!this.textEditNode) { if (!this.textEditNode) {
this.textEditNode = document.createElement('div') this.textEditNode = document.createElement('div')
this.textEditNode.className = 'associative-line-text-edit-warp'
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;` 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;`
this.textEditNode.setAttribute('contenteditable', true) this.textEditNode.setAttribute('contenteditable', true)
this.textEditNode.addEventListener('keyup', e => { this.textEditNode.addEventListener('keyup', e => {
@ -54,14 +55,14 @@ function showEditTextBox(g) {
const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body
targetNode.appendChild(this.textEditNode) targetNode.appendChild(this.textEditNode)
} }
let [, , , node, toNode] = this.activeLine
let { let {
associativeLineTextFontSize, associativeLineTextFontSize,
associativeLineTextFontFamily, associativeLineTextFontFamily,
associativeLineTextLineHeight associativeLineTextLineHeight
} = this.mindMap.themeConfig } = this.getStyleConfig(node, toNode)
let { defaultAssociativeLineText, nodeTextEditZIndex } = this.mindMap.opt let { defaultAssociativeLineText, nodeTextEditZIndex } = this.mindMap.opt
let scale = this.mindMap.view.scale let scale = this.mindMap.view.scale
let [, , , node, toNode] = this.activeLine
let text = this.getText(node, toNode) let text = this.getText(node, toNode)
let textLines = (text || defaultAssociativeLineText).split(/\n/gim) let textLines = (text || defaultAssociativeLineText).split(/\n/gim)
this.textEditNode.style.fontFamily = associativeLineTextFontFamily this.textEditNode.style.fontFamily = associativeLineTextFontFamily
@ -124,7 +125,7 @@ function hideEditTextBox() {
this.textEditNode.style.display = 'none' this.textEditNode.style.display = 'none'
this.textEditNode.innerHTML = '' this.textEditNode.innerHTML = ''
this.showTextEdit = false this.showTextEdit = false
this.renderText(str, path, text) this.renderText(str, path, text, node, toNode)
this.mindMap.emit('hide_text_edit') this.mindMap.emit('hide_text_edit')
} }
@ -138,29 +139,35 @@ function getText(node, toNode) {
} }
// 渲染关联线文字 // 渲染关联线文字
function renderText(str, path, text) { function renderText(str, path, text, node, toNode) {
if (!str) return if (!str) return
let { associativeLineTextFontSize, associativeLineTextLineHeight } = let { associativeLineTextFontSize, associativeLineTextLineHeight } =
this.mindMap.themeConfig this.getStyleConfig(node, toNode)
text.clear() text.clear()
let textArr = str.split(/\n/gim) let textArr = str.replace(/\n$/g, '').split(/\n/gim)
textArr.forEach((item, index) => { textArr.forEach((item, index) => {
let node = new Text().text(item) // 避免尾部的空行不占宽度,导致文本编辑框定位异常的问题
node.y(associativeLineTextFontSize * associativeLineTextLineHeight * index) if (item === '') {
this.styleText(node) item = ''
text.add(node) }
let textNode = new Text().text(item)
textNode.y(
associativeLineTextFontSize * associativeLineTextLineHeight * index
)
this.styleText(textNode, node, toNode)
text.add(textNode)
}) })
updateTextPos(path, text) updateTextPos(path, text)
} }
// 给文本设置样式 // 给文本设置样式
function styleText(node) { function styleText(textNode, node, toNode) {
let { let {
associativeLineTextColor, associativeLineTextColor,
associativeLineTextFontSize, associativeLineTextFontSize,
associativeLineTextFontFamily associativeLineTextFontFamily
} = this.mindMap.themeConfig } = this.getStyleConfig(node, toNode)
node textNode
.fill({ .fill({
color: associativeLineTextColor color: associativeLineTextColor
}) })

View File

@ -103,6 +103,7 @@ export default {
// lineFlow, // lineFlow,
// lineFlowDuration, // lineFlowDuration,
// lineFlowForward // lineFlowForward
// 关联线的所有样式
}, },
// 二级节点样式 // 二级节点样式
second: { second: {