From 6539a87cb2f0cf9bd657379dda3cddab7317dcd3 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: Fri, 19 Apr 2024 09:29:48 +0800 Subject: [PATCH] =?UTF-8?q?Feat=EF=BC=9A=E6=96=B0=E5=A2=9E=E6=BC=94?= =?UTF-8?q?=E7=A4=BA=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/constants/defaultOptions.js | 4 +- simple-mind-map/src/core/command/Command.js | 14 +- simple-mind-map/src/core/render/Render.js | 6 +- simple-mind-map/src/core/view/View.js | 11 +- simple-mind-map/src/plugins/Demonstrate.js | 309 ++++++++++++++++++ simple-mind-map/src/utils/index.js | 77 ++++- 6 files changed, 397 insertions(+), 24 deletions(-) create mode 100644 simple-mind-map/src/plugins/Demonstrate.js diff --git a/simple-mind-map/src/constants/defaultOptions.js b/simple-mind-map/src/constants/defaultOptions.js index 7e8d6346..c13b3b05 100644 --- a/simple-mind-map/src/constants/defaultOptions.js +++ b/simple-mind-map/src/constants/defaultOptions.js @@ -318,5 +318,7 @@ export const defaultOpt = { } */ addContentToHeader: null, - addContentToFooter: null + addContentToFooter: null, + // 演示插件配置 + demonstrateConfig: null } diff --git a/simple-mind-map/src/core/command/Command.js b/simple-mind-map/src/core/command/Command.js index 5c51b4d8..eddc24d2 100644 --- a/simple-mind-map/src/core/command/Command.js +++ b/simple-mind-map/src/core/command/Command.js @@ -23,6 +23,18 @@ class Command { this.mindMap.opt.addHistoryTime, this ) + // 是否暂停收集历史数据 + this.isPause = false + } + + // 暂停收集历史数据 + pause() { + this.isPause = true + } + + // 恢复收集历史数据 + recovery() { + this.isPause = false } // 清空历史数据 @@ -88,7 +100,7 @@ class Command { // 添加回退数据 addHistory() { - if (this.mindMap.opt.readonly) { + if (this.mindMap.opt.readonly || this.isPause) { return } const lastData = diff --git a/simple-mind-map/src/core/render/Render.js b/simple-mind-map/src/core/render/Render.js index 95197a7b..42e1779c 100644 --- a/simple-mind-map/src/core/render/Render.js +++ b/simple-mind-map/src/core/render/Render.js @@ -1506,7 +1506,7 @@ class Render { } // 收起所有 - unexpandAllNode() { + unexpandAllNode(isSetRootNodeCenter = true) { if (!this.renderTree) return walk( this.renderTree, @@ -1522,7 +1522,9 @@ class Render { 0 ) this.mindMap.render(() => { - this.setRootNodeCenter() + if (isSetRootNodeCenter) { + this.setRootNodeCenter() + } }) } diff --git a/simple-mind-map/src/core/view/View.js b/simple-mind-map/src/core/view/View.js index 1394f570..93875b6f 100644 --- a/simple-mind-map/src/core/view/View.js +++ b/simple-mind-map/src/core/view/View.js @@ -280,11 +280,12 @@ class View { } // 适应画布大小 - fit() { - const { fitPadding } = this.mindMap.opt + fit(getRbox = () => {}, enlarge = false, fitPadding) { + fitPadding = + fitPadding === undefined ? this.mindMap.opt.fitPadding : fitPadding const draw = this.mindMap.draw const origTransform = draw.transform() - const rect = draw.rbox() + const rect = getRbox() || draw.rbox() const drawWidth = rect.width / origTransform.scaleX const drawHeight = rect.height / origTransform.scaleY const drawRatio = drawWidth / drawHeight @@ -294,7 +295,7 @@ class View { const elRatio = elWidth / elHeight let newScale = 0 let flag = '' - if (drawWidth <= elWidth && drawHeight <= elHeight) { + if (drawWidth <= elWidth && drawHeight <= elHeight && !enlarge) { newScale = 1 flag = 1 } else { @@ -312,7 +313,7 @@ class View { newScale = newWidth / drawWidth } this.setScale(newScale) - const newRect = draw.rbox() + const newRect = getRbox() || draw.rbox() // 需要考虑画布容器距浏览器窗口左上角的距离 newRect.x -= this.mindMap.elRect.left newRect.y -= this.mindMap.elRect.top diff --git a/simple-mind-map/src/plugins/Demonstrate.js b/simple-mind-map/src/plugins/Demonstrate.js new file mode 100644 index 00000000..68159a45 --- /dev/null +++ b/simple-mind-map/src/plugins/Demonstrate.js @@ -0,0 +1,309 @@ +import { + walk, + getNodeTreeBoundingRect, + fullscrrenEvent, + fullScreen, + exitFullScreen +} from '../utils/index' +import { keyMap } from '../core/command/keyMap' + +const defaultConfig = { + boxShadowColor: 'rgba(0, 0, 0, 0.8)', // 高亮框四周的区域颜色 + borderRadius: '5px', // 高亮框的圆角大小 + transition: 'all 0.3s ease-out', // 高亮框动画的过渡 + zIndex: 9999, // 高亮框元素的层级 + padding: 20, // 高亮框的内边距 + margin: 50 // 高亮框的外边距 +} + +// 演示插件 +class Demonstrate { + constructor(opt) { + this.mindMap = opt.mindMap + this.stepList = [] + this.currentStepIndex = 0 + this.maskEl = null + this.highlightEl = null + this.transformState = null + this.renderTree = null + this.config = Object.assign( + { ...defaultConfig }, + this.mindMap.opt.demonstrateConfig || {} + ) + } + + // 进入演示模式 + enter() { + // 全屏 + this.bindFullscreenEvent() + // 如果已经全屏了 + if (document.fullscreenElement === this.mindMap.el) { + this._enter() + } else { + // 否则申请全屏 + fullScreen(this.mindMap.el) + } + } + + _enter() { + // 记录演示前的画布状态 + this.transformState = this.mindMap.view.getTransformData() + // 记录演示前的画布数据 + this.renderTree = this.mindMap.getData() + // 暂停收集历史记录 + this.mindMap.command.pause() + // 暂停思维导图快捷键响应 + this.mindMap.keyCommand.pause() + // 创建高亮元素 + this.createHighlightEl() + // 计算步骤数据 + this.getStepList() + // 收起所有节点 + this.mindMap.execCommand('UNEXPAND_ALL', false) + const onRenderEnd = () => { + this.mindMap.off('node_tree_render_end', onRenderEnd) + // 聚焦到第一步 + this.jump(this.currentStepIndex) + this.bindEvent() + } + this.mindMap.on('node_tree_render_end', onRenderEnd) + } + + // 退出演示模式 + exit() { + exitFullScreen(this.mindMap.el) + this.mindMap.updateData(this.renderTree) + this.mindMap.view.setTransformData(this.transformState) + this.renderTree = null + this.transformState = null + this.stepList = [] + this.currentStepIndex = 0 + this.unBindEvent() + this.removeHighlightEl() + this.mindMap.command.recovery() + this.mindMap.keyCommand.recovery() + this.mindMap.emit('exit_demonstrate') + } + + // 创建高亮元素 + createHighlightEl() { + if (!this.highlightEl) { + // 遮罩元素 + this.maskEl = document.createElement('div') + this.maskEl.style.cssText = ` + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + z-index: ${this.config.zIndex}; + ` + this.mindMap.el.appendChild(this.maskEl) + // 高亮元素 + this.highlightEl = document.createElement('div') + this.highlightEl.style.cssText = ` + position: absolute; + box-shadow: 0 0 0 5000px ${this.config.boxShadowColor}; + border-radius: ${this.config.borderRadius}; + transition: ${this.config.transition}; + z-index: ${this.config.zIndex + 1}; + ` + this.mindMap.el.appendChild(this.highlightEl) + } + } + + // 移除高亮元素 + removeHighlightEl() { + if (this.highlightEl) { + this.mindMap.el.removeChild(this.highlightEl) + this.highlightEl = null + } + if (this.maskEl) { + this.mindMap.el.removeChild(this.maskEl) + this.maskEl = null + } + } + + // 更新高亮元素的位置和大小 + updateHighlightEl({ left, top, width, height }) { + const padding = this.config.padding + if (left) { + this.highlightEl.style.left = left - padding + 'px' + } + if (top) { + this.highlightEl.style.top = top - padding + 'px' + } + if (width) { + this.highlightEl.style.width = width + padding * 2 + 'px' + } + if (height) { + this.highlightEl.style.height = height + padding * 2 + 'px' + } + } + + // 绑定事件 + bindEvent() { + this.onKeydown = this.onKeydown.bind(this) + window.addEventListener('keydown', this.onKeydown) + } + + // 绑定全屏事件 + bindFullscreenEvent() { + this.onFullscreenChange = this.onFullscreenChange.bind(this) + document.addEventListener(fullscrrenEvent, this.onFullscreenChange) + } + + // 解绑事件 + unBindEvent() { + window.removeEventListener('keydown', this.onKeydown) + document.removeEventListener(fullscrrenEvent, this.onFullscreenChange) + } + + // 全屏状态改变 + onFullscreenChange() { + if (!document.fullscreenElement) { + this.exit() + } else if (document.fullscreenElement === this.mindMap.el) { + this._enter() + } + } + + // 按键事件 + onKeydown(e) { + // 上一个 + if (e.keyCode === keyMap.Left) { + this.prev() + } else if (e.keyCode === keyMap.Right) { + // 下一个 + this.next() + } else if (e.keyCode === keyMap.Esc) { + // 退出演示 + this.exit() + } + } + + // 上一张 + prev() { + if (this.currentStepIndex > 0) { + this.jump(this.currentStepIndex - 1) + } + } + + // 下一张 + next() { + const stepLength = this.stepList.length + if (this.currentStepIndex < stepLength - 1) { + this.jump(this.currentStepIndex + 1) + } + } + + // 跳转到某一张 + jump(index) { + this.currentStepIndex = index + this.mindMap.emit( + 'demonstrate_jump', + this.currentStepIndex, + this.stepList.length + ) + const step = this.stepList[index] + // 这一步的节点数据 + const nodeData = step.node + // 该节点的uid + const uid = nodeData.data.uid + // 根据uid在画布上找到该节点实例 + const node = this.mindMap.renderer.findNodeByUid(uid) + // 如果该节点实例不存在,那么先展开到该节点 + if (!node) { + this.mindMap.renderer.expandToNodeUid(uid, () => { + this.jump(index) + }) + return + } + // 1.聚焦到某个节点 + if (step.type === 'node') { + // 适应画布大小 + this.mindMap.view.fit( + () => { + return node.group.rbox() + }, + true, + this.config.padding + this.config.margin + ) + const rect = node.group.rbox() + this.updateHighlightEl({ + left: rect.x, + top: rect.y, + width: rect.width, + height: rect.height + }) + } else { + // 2.聚焦到某个节点的所有子节点 + // 聚焦该节点的所有子节点 + const task = () => { + // 先收起该节点所有子节点的子节点 + nodeData.children.forEach(item => { + item.data.expand = false + }) + this.mindMap.render(() => { + // 适应画布大小 + this.mindMap.view.fit( + () => { + const res = getNodeTreeBoundingRect(node, 0, 0, 0, 0, true) + return { + ...res, + x: res.left, + y: res.top + } + }, + true, + this.config.padding + this.config.margin + ) + const res = getNodeTreeBoundingRect(node, 0, 0, 0, 0, true) + this.updateHighlightEl(res) + }) + } + // 如果该节点是收起状态,那么需要先展开 + if (!nodeData.data.expand) { + this.mindMap.execCommand('SET_NODE_EXPAND', node, true) + const onRenderEnd = () => { + this.mindMap.off('node_tree_render_end', onRenderEnd) + task() + } + this.mindMap.on('node_tree_render_end', onRenderEnd) + } else { + // 否则直接聚焦 + task() + } + } + } + + // 深度度优先遍历所有节点,返回步骤列表 + getStepList() { + walk(this.mindMap.renderer.renderTree, null, node => { + this.stepList.push({ + type: 'node', + node + }) + if (node.children.length > 1) { + this.stepList.push({ + type: 'children', + node + }) + } + }) + } + + // 插件被移除前做的事情 + beforePluginRemove() { + this.unBindEvent() + } + + // 插件被卸载前做的事情 + beforePluginDestroy() { + this.unBindEvent() + } +} + +Demonstrate.instanceName = 'demonstrate' + +export default Demonstrate diff --git a/simple-mind-map/src/utils/index.js b/simple-mind-map/src/utils/index.js index 0b9e2d47..1b212724 100644 --- a/simple-mind-map/src/utils/index.js +++ b/simple-mind-map/src/utils/index.js @@ -1370,24 +1370,35 @@ export const handleGetSvgDataExtraContent = ({ } // 获取指定节点的包围框信息 -export const getNodeTreeBoundingRect = (node, x, y, paddingX, paddingY) => { +export const getNodeTreeBoundingRect = ( + node, + x = 0, + y = 0, + paddingX = 0, + paddingY = 0, + excludeSelf = false +) => { let minX = Infinity let maxX = -Infinity let minY = Infinity let maxY = -Infinity - const walk = root => { - const { x, y, width, height } = root.group.findOne('.smm-node-shape').rbox() - if (x < minX) { - minX = x - } - if (x + width > maxX) { - maxX = x + width - } - if (y < minY) { - minY = y - } - if (y + height > maxY) { - maxY = y + height + const walk = (root, isRoot) => { + if (!(isRoot && excludeSelf)) { + const { x, y, width, height } = root.group + .findOne('.smm-node-shape') + .rbox() + if (x < minX) { + minX = x + } + if (x + width > maxX) { + maxX = x + width + } + if (y < minY) { + minY = y + } + if (y + height > maxY) { + maxY = y + height + } } if (root._generalizationList.length > 0) { root._generalizationList.forEach(item => { @@ -1400,7 +1411,7 @@ export const getNodeTreeBoundingRect = (node, x, y, paddingX, paddingY) => { }) } } - walk(node) + walk(node, true) minX = minX - x + paddingX minY = minY - y + paddingY @@ -1414,3 +1425,39 @@ export const getNodeTreeBoundingRect = (node, x, y, paddingX, paddingY) => { height: maxY - minY } } + +// 全屏事件检测 +const getOnfullscreEnevt = () => { + if (document.documentElement.requestFullScreen) { + return 'fullscreenchange' + } else if (document.documentElement.webkitRequestFullScreen) { + return 'webkitfullscreenchange' + } else if (document.documentElement.mozRequestFullScreen) { + return 'mozfullscreenchange' + } else if (document.documentElement.msRequestFullscreen) { + return 'msfullscreenchange' + } +} +export const fullscrrenEvent = getOnfullscreEnevt() + +// 全屏 +export const fullScreen = element => { + if (element.requestFullScreen) { + element.requestFullScreen() + } else if (element.webkitRequestFullScreen) { + element.webkitRequestFullScreen() + } else if (element.mozRequestFullScreen) { + element.mozRequestFullScreen() + } +} + +// 退出全屏 +export const exitFullScreen = () => { + if (document.exitFullscreen) { + document.exitFullscreen() + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen() + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen() + } +}