From 876afb25041ecf4329559878317977a9409c5c8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A1=97=E8=A7=92=E5=B0=8F=E6=9E=97?= <1013335014@qq.com> Date: Mon, 1 Jul 2024 17:15:48 +0800 Subject: [PATCH] =?UTF-8?q?Feat=EF=BC=9A=E6=96=B0=E5=A2=9E=E5=A4=96?= =?UTF-8?q?=E6=A1=86=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple-mind-map/src/constants/constant.js | 3 +- simple-mind-map/src/plugins/OuterFrame.js | 391 ++++++++++++++++++++++ simple-mind-map/src/utils/index.js | 48 ++- 3 files changed, 439 insertions(+), 3 deletions(-) create mode 100644 simple-mind-map/src/plugins/OuterFrame.js diff --git a/simple-mind-map/src/constants/constant.js b/simple-mind-map/src/constants/constant.js index cccd5831..757c0c0a 100644 --- a/simple-mind-map/src/constants/constant.js +++ b/simple-mind-map/src/constants/constant.js @@ -315,7 +315,8 @@ export const nodeDataNoStylePropList = [ 'associativeLineText', 'attachmentUrl', 'attachmentName', - 'notation' + 'notation', + 'outerFrame' ] // 错误类型 diff --git a/simple-mind-map/src/plugins/OuterFrame.js b/simple-mind-map/src/plugins/OuterFrame.js new file mode 100644 index 00000000..f654f8a8 --- /dev/null +++ b/simple-mind-map/src/plugins/OuterFrame.js @@ -0,0 +1,391 @@ +import { + formatDataToArray, + walk, + getTopAncestorsFomNodeList, + getNodeListBoundingRect, + createUid +} from '../utils' + +// 解析要添加外框的节点实例列表 +const parseAddNodeList = list => { + // 找出顶层节点 + list = getTopAncestorsFomNodeList(list) + const cache = {} + const uidToParent = {} + // 找出列表中节点在兄弟节点中的索引,并和父节点关联起来 + list.forEach(node => { + const parent = node.parent + if (parent) { + const pUid = parent.uid + uidToParent[pUid] = parent + const index = node.getIndexInBrothers() + const data = { + node, + index + } + if (cache[pUid]) { + if ( + !cache[pUid].find(item => { + return item.index === data.index + }) + ) { + cache[pUid].push(data) + } + } else { + cache[pUid] = [data] + } + } + }) + const res = [] + Object.keys(cache).forEach(uid => { + const indexList = cache[uid] + const parentNode = uidToParent[uid] + if (indexList.length > 1) { + // 多个节点 + const rangeList = indexList + .map(item => { + return item.index + }) + .sort((a, b) => { + return a - b + }) + const minIndex = rangeList[0] + const maxIndex = rangeList[rangeList.length - 1] + let curStart = -1 + let curEnd = -1 + for (let i = minIndex; i <= maxIndex; i++) { + // 连续索引 + if (rangeList.includes(i)) { + if (curStart === -1) { + curStart = i + } + curEnd = i + } else { + // 连续断开 + if (curStart !== -1 && curEnd !== -1) { + res.push({ + node: parentNode, + range: [curStart, curEnd] + }) + } + curStart = -1 + curEnd = -1 + } + } + // 不要忘了最后一段索引 + if (curStart !== -1 && curEnd !== -1) { + res.push({ + node: parentNode, + range: [curStart, curEnd] + }) + } + } else { + // 单个节点 + res.push({ + node: parentNode, + range: [indexList[0].index, indexList[0].index] + }) + } + }) + return res +} + +// 解析获取节点的子节点生成的外框列表 +const getNodeOuterFrameList = node => { + const children = node.children + if (!children || children.length <= 0) return + const res = [] + const map = {} + children.forEach((item, index) => { + const outerFrameData = item.getData('outerFrame') + if (!outerFrameData) return + const groupId = outerFrameData.groupId + if (groupId) { + if (!map[groupId]) { + map[groupId] = [] + } + map[groupId].push({ + node: item, + index + }) + } else { + res.push({ + nodeList: [item], + range: [index, index] + }) + } + }) + Object.keys(map).forEach(id => { + const list = map[id] + res.push({ + nodeList: list.map(item => { + return item.node + }), + range: [list[0].index, list[list.length - 1].index] + }) + }) + return res +} + +// 默认外框样式 +const defaultStyle = { + radius: 5, + strokeWidth: 2, + strokeColor: '#0984e3', + strokeDasharray: '5,5', + fill: 'rgba(9,132,227,0.05)' +} + +// 外框插件 +class OuterFrame { + constructor(opt = {}) { + this.mindMap = opt.mindMap + this.draw = null + this.createDrawContainer() + this.outerFrameElList = [] + this.activeOuterFrame = null + this.paddingX = 10 + this.paddingY = 10 + this.bindEvent() + } + + // 创建容器 + createDrawContainer() { + this.draw = this.mindMap.draw.group() + this.draw.addClass('smm-outer-frame-container') + this.draw.back() // 最底层 + this.draw.forward() // 连线层上面 + } + + // 绑定事件 + bindEvent() { + this.renderOuterFrames = this.renderOuterFrames.bind(this) + this.mindMap.on('node_tree_render_end', this.renderOuterFrames) + this.mindMap.on('data_change', this.renderOuterFrames) + // 监听画布和节点点击事件,用于清除当前激活的连接线 + this.clearActiveOuterFrame = this.clearActiveOuterFrame.bind(this) + this.mindMap.on('draw_click', this.clearActiveOuterFrame) + this.mindMap.on('node_click', this.clearActiveOuterFrame) + + this.addOuterFrame = this.addOuterFrame.bind(this) + this.mindMap.command.add('ADD_OUTER_FRAME', this.addOuterFrame) + + this.removeActiveOuterFrame = this.removeActiveOuterFrame.bind(this) + this.mindMap.keyCommand.addShortcut( + 'Del|Backspace', + this.removeActiveOuterFrame + ) + } + + // 解绑事件 + unBindEvent() { + this.mindMap.off('node_tree_render_end', this.renderOuterFrames) + this.mindMap.off('data_change', this.renderOuterFrames) + this.mindMap.off('draw_click', this.clearActiveOuterFrame) + this.mindMap.off('node_click', this.clearActiveOuterFrame) + this.mindMap.command.remove('ADD_OUTER_FRAME', this.addOuterFrame) + this.mindMap.keyCommand.removeShortcut( + 'Del|Backspace', + this.removeActiveOuterFrame + ) + } + + // 给节点添加外框数据 + /* + config: { + text: '', + radius: 5, + strokeWidth: 2, + strokeColor: '#0984e3', + strokeDasharray: '5,5', + fill: 'rgba(9,132,227,0.05)' + } + */ + addOuterFrame(appointNodes, config = {}) { + appointNodes = formatDataToArray(appointNodes) + const activeNodeList = this.mindMap.renderer.activeNodeList + if (activeNodeList.length <= 0 && appointNodes.length <= 0) { + return + } + let nodeList = appointNodes.length > 0 ? appointNodes : activeNodeList + nodeList = nodeList.filter(node => { + return !node.isRoot && !node.isGeneralization + }) + const list = parseAddNodeList(nodeList) + list.forEach(({ node, range }) => { + const childNodeList = node.children.slice(range[0], range[1] + 1) + const groupId = createUid() + childNodeList.forEach(child => { + let outerFrame = child.getData('outerFrame') + // 检查该外框是否已存在 + if (outerFrame) { + outerFrame = { + ...outerFrame, + ...config, + groupId + } + } else { + outerFrame = { + ...config, + groupId + } + } + this.mindMap.execCommand('SET_NODE_DATA', child, { + outerFrame + }) + }) + }) + } + + // 获取当前激活的外框 + getActiveOuterFrame() { + return this.activeOuterFrame + ? { + ...this.activeOuterFrame + } + : null + } + + // 删除当前激活的外框 + removeActiveOuterFrame() { + if (!this.activeOuterFrame) return + const { node, range } = this.activeOuterFrame + this.getRangeNodeList(node, range).forEach(child => { + this.mindMap.execCommand('SET_NODE_DATA', child, { + outerFrame: null + }) + }) + this.mindMap.emit('outer_frame_delete') + } + + // 更新当前激活的外框 + // 执行了该方法后请立即隐藏你的样式面板,因为会清除当前激活的外框 + updateActiveOuterFrame(config = {}) { + if (!this.activeOuterFrame) return + const { node, range } = this.activeOuterFrame + this.getRangeNodeList(node, range).forEach(node => { + const outerFrame = node.getData('outerFrame') + this.mindMap.execCommand('SET_NODE_DATA', node, { + outerFrame: { + ...outerFrame, + ...config + } + }) + }) + } + + // 获取某个节点指定范围的带外框的子节点列表 + getRangeNodeList(node, range) { + return node.children.slice(range[0], range[1] + 1).filter(child => { + return child.getData('outerFrame') + }) + } + + // 渲染外框 + renderOuterFrames() { + this.clearOuterFrameElList() + let tree = this.mindMap.renderer.root + if (!tree) return + const t = this.mindMap.draw.transform() + walk( + tree, + null, + cur => { + if (!cur) return + const outerFrameList = getNodeOuterFrameList(cur) + if (outerFrameList && outerFrameList.length > 0) { + outerFrameList.forEach(({ nodeList, range }) => { + if (range[0] === -1 || range[1] === -1) return + const { left, top, width, height } = + getNodeListBoundingRect(nodeList) + const el = this.createOuterFrameEl( + (left - this.paddingX - this.mindMap.elRect.left - t.translateX) / + t.scaleX, + (top - this.paddingY - this.mindMap.elRect.top - t.translateY) / + t.scaleY, + (width + this.paddingX * 2) / t.scaleX, + (height + this.paddingY * 2) / t.scaleY, + nodeList[0].getData('outerFrame') // 使用第一个节点的外框样式 + ) + el.on('click', e => { + e.stopPropagation() + this.setActiveOuterFrame(el, cur, range) + }) + }) + } + }, + () => {}, + true, + 0 + ) + } + + // 激活外框 + setActiveOuterFrame(el, node, range) { + this.mindMap.execCommand('CLEAR_ACTIVE_NODE') + this.clearActiveOuterFrame() + this.activeOuterFrame = { + el, + node, + range + } + el.stroke({ + dasharray: 'none' + }) + this.mindMap.emit('outer_frame_active', el, node, range) + } + + // 清除当前激活的外框 + clearActiveOuterFrame() { + if (!this.activeOuterFrame) return + const { el } = this.activeOuterFrame + el.stroke({ + dasharray: '5,5' + }) + this.activeOuterFrame = null + } + + // 创建外框元素 + createOuterFrameEl(x, y, width, height, styleConfig = {}) { + styleConfig = { ...defaultStyle, ...styleConfig } + const el = this.draw + .rect() + .size(width, height) + .radius(styleConfig.radius) + .stroke({ + width: styleConfig.strokeWidth, + color: styleConfig.strokeColor, + dasharray: styleConfig.strokeDasharray + }) + .fill({ + color: styleConfig.fill + }) + .x(x) + .y(y) + this.outerFrameElList.push(el) + return el + } + + // 清除外框元素 + clearOuterFrameElList() { + this.outerFrameElList.forEach(item => { + item.remove() + }) + this.outerFrameElList = [] + this.activeOuterFrame = null + } + + // 插件被移除前做的事情 + beforePluginRemove() { + this.unBindEvent() + } + + // 插件被卸载前做的事情 + beforePluginDestroy() { + this.unBindEvent() + } +} + +OuterFrame.instanceName = 'outerFrame' + +export default OuterFrame diff --git a/simple-mind-map/src/utils/index.js b/simple-mind-map/src/utils/index.js index 78e7266d..c952b8e5 100644 --- a/simple-mind-map/src/utils/index.js +++ b/simple-mind-map/src/utils/index.js @@ -1368,7 +1368,8 @@ export const getNodeTreeBoundingRect = ( y = 0, paddingX = 0, paddingY = 0, - excludeSelf = false + excludeSelf = false, + excludeGeneralization = false ) => { let minX = Infinity let maxX = -Infinity @@ -1392,7 +1393,7 @@ export const getNodeTreeBoundingRect = ( maxY = y + height } } - if (root._generalizationList.length > 0) { + if (!excludeGeneralization && root._generalizationList.length > 0) { root._generalizationList.forEach(item => { walk(item.generalizationNode) }) @@ -1418,6 +1419,49 @@ export const getNodeTreeBoundingRect = ( } } +// 获取多个节点总的包围框 +export const getNodeListBoundingRect = ( + nodeList, + x = 0, + y = 0, + paddingX = 0, + paddingY = 0 +) => { + let minX = Infinity + let maxX = -Infinity + let minY = Infinity + let maxY = -Infinity + nodeList.forEach(node => { + const { left, top, width, height } = getNodeTreeBoundingRect( + node, + x, + y, + paddingX, + paddingY, + false, + true + ) + if (left < minX) { + minX = left + } + if (left + width > maxX) { + maxX = left + width + } + if (top < minY) { + minY = top + } + if (top + height > maxY) { + maxY = top + height + } + }) + return { + left: minX, + top: minY, + width: maxX - minX, + height: maxY - minY + } +} + // 全屏事件检测 const getOnfullscreEnevt = () => { if (document.documentElement.requestFullScreen) {