UNPKG

leo-mind-map

Version:

一个简单的web在线思维导图

1,721 lines (1,616 loc) 44.5 kB
import { v4 as uuidv4 } from 'uuid' import { nodeDataNoStylePropList, selfCloseTagList, richTextSupportStyleList } from '../constants/constant' import MersenneTwister from './mersenneTwister' import { ForeignObject } from '@svgdotjs/svg.js' import merge from 'deepmerge' import { lineStyleProps } from '../theme/default' // 深度优先遍历树 export const walk = ( root, parent, beforeCallback, afterCallback, isRoot, layerIndex = 0, index = 0, ancestors = [] ) => { let stop = false if (beforeCallback) { stop = beforeCallback(root, parent, isRoot, layerIndex, index, ancestors) } if (!stop && root.children && root.children.length > 0) { let _layerIndex = layerIndex + 1 root.children.forEach((node, nodeIndex) => { walk( node, root, beforeCallback, afterCallback, false, _layerIndex, nodeIndex, [...ancestors, root] ) }) } afterCallback && afterCallback(root, parent, isRoot, layerIndex, index, ancestors) } // 广度优先遍历树 export const bfsWalk = (root, callback) => { let stack = [root] let isStop = false if (callback(root, null) === 'stop') { isStop = true } while (stack.length) { if (isStop) { break } let cur = stack.shift() if (cur.children && cur.children.length) { cur.children.forEach(item => { if (isStop) return stack.push(item) if (callback(item, cur) === 'stop') { isStop = true } }) } } } // 按原比例缩放图片 export const resizeImgSizeByOriginRatio = ( width, height, newWidth, newHeight ) => { let arr = [] let nRatio = width / height let mRatio = newWidth / newHeight if (nRatio > mRatio) { // 固定宽度 arr = [newWidth, newWidth / nRatio] } else { // 固定高度 arr = [nRatio * newHeight, newHeight] } return arr } // 缩放图片尺寸 export const resizeImgSize = (width, height, maxWidth, maxHeight) => { let nRatio = width / height let arr = [] if (maxWidth && maxHeight) { if (width <= maxWidth && height <= maxHeight) { arr = [width, height] } else { let mRatio = maxWidth / maxHeight if (nRatio > mRatio) { // 固定宽度 arr = [maxWidth, maxWidth / nRatio] } else { // 固定高度 arr = [nRatio * maxHeight, maxHeight] } } } else if (maxWidth) { if (width <= maxWidth) { arr = [width, height] } else { arr = [maxWidth, maxWidth / nRatio] } } else if (maxHeight) { if (height <= maxHeight) { arr = [width, height] } else { arr = [nRatio * maxHeight, maxHeight] } } return arr } // 缩放图片 export const resizeImg = (imgUrl, maxWidth, maxHeight) => { return new Promise((resolve, reject) => { let img = new Image() img.src = imgUrl img.onload = () => { let arr = resizeImgSize( img.naturalWidth, img.naturalHeight, maxWidth, maxHeight ) resolve(arr) } img.onerror = e => { reject(e) } }) } // 从头html结构字符串里获取带换行符的字符串 export const getStrWithBrFromHtml = str => { str = str.replace(/<br>/gim, '\n') let el = document.createElement('div') el.innerHTML = str str = el.textContent return str } // 极简的深拷贝 export const simpleDeepClone = data => { try { return JSON.parse(JSON.stringify(data)) } catch (error) { return null } } // 复制渲染树数据 export const copyRenderTree = (tree, root, removeActiveState = false) => { tree.data = simpleDeepClone(root.data) if (removeActiveState) { tree.data.isActive = false const generalizationList = formatGetNodeGeneralization(tree.data) generalizationList.forEach(item => { item.isActive = false }) } tree.children = [] if (root.children && root.children.length > 0) { root.children.forEach((item, index) => { tree.children[index] = copyRenderTree({}, item, removeActiveState) }) } // data、children外的其他字段 Object.keys(root).forEach(key => { if (!['data', 'children'].includes(key) && !/^_/.test(key)) { tree[key] = root[key] } }) return tree } // 复制节点树数据 export const copyNodeTree = ( tree, root, removeActiveState = false, removeId = true ) => { const rootData = root.nodeData ? root.nodeData : root tree.data = simpleDeepClone(rootData.data) // 移除节点uid if (removeId) { delete tree.data.uid } else if (!tree.data.uid) { // 否则保留或生成 tree.data.uid = createUid() } if (removeActiveState) { tree.data.isActive = false } tree.children = [] if (root.children && root.children.length > 0) { root.children.forEach((item, index) => { tree.children[index] = copyNodeTree({}, item, removeActiveState, removeId) }) } else if ( root.nodeData && root.nodeData.children && root.nodeData.children.length > 0 ) { root.nodeData.children.forEach((item, index) => { tree.children[index] = copyNodeTree({}, item, removeActiveState, removeId) }) } // data、children外的其他字段 Object.keys(rootData).forEach(key => { if (!['data', 'children'].includes(key) && !/^_/.test(key)) { tree[key] = rootData[key] } }) return tree } // 图片转成dataURL export const imgToDataUrl = (src, returnBlob = false) => { return new Promise((resolve, reject) => { const img = new Image() // 跨域图片需要添加这个属性,否则画布被污染了无法导出图片 img.setAttribute('crossOrigin', 'anonymous') img.onload = () => { try { let canvas = document.createElement('canvas') canvas.width = img.width canvas.height = img.height let ctx = canvas.getContext('2d') // 图片绘制到canvas里 ctx.drawImage(img, 0, 0, img.width, img.height) if (returnBlob) { canvas.toBlob(blob => { resolve(blob) }) } else { resolve(canvas.toDataURL()) } } catch (e) { reject(e) } } img.onerror = e => { reject(e) } img.src = src }) } // 解析dataUrl export const parseDataUrl = data => { if (!/^data:/.test(data)) return data let [typeStr, base64] = data.split(',') let res = /^data:[^/]+\/([^;]+);/.exec(typeStr) let type = res[1] return { type, base64 } } // 下载文件 export const downloadFile = (file, fileName) => { let a = document.createElement('a') a.href = file a.download = fileName a.click() } // 节流函数 export const throttle = (fn, time = 300, ctx) => { let timer = null return (...args) => { if (timer) { return } timer = setTimeout(() => { fn.call(ctx, ...args) timer = null }, time) } } // 防抖函数 export const debounce = (fn, wait = 300, ctx) => { let timeout = null return (...args) => { if (timeout) clearTimeout(timeout) timeout = setTimeout(() => { timeout = null fn.apply(ctx, args) }, wait) } } // 异步执行任务队列 export const asyncRun = (taskList, callback = () => {}) => { let index = 0 let len = taskList.length if (len <= 0) { return callback() } let loop = () => { if (index >= len) { callback() return } taskList[index]() setTimeout(() => { index++ loop() }, 0) } loop() } // 角度转弧度 export const degToRad = deg => { return deg * (Math.PI / 180) } // 驼峰转连字符 export const camelCaseToHyphen = str => { return str.replace(/([a-z])([A-Z])/g, (...args) => { return args[1] + '-' + args[2].toLowerCase() }) } //计算节点的文本长宽 let measureTextContext = null export const measureText = (text, { italic, bold, fontSize, fontFamily }) => { const font = joinFontStr({ italic, bold, fontSize, fontFamily }) if (!measureTextContext) { const canvas = document.createElement('canvas') measureTextContext = canvas.getContext('2d') } measureTextContext.save() measureTextContext.font = font const { width, actualBoundingBoxAscent, actualBoundingBoxDescent } = measureTextContext.measureText(text) measureTextContext.restore() const height = actualBoundingBoxAscent + actualBoundingBoxDescent return { width, height } } // 拼接font字符串 export const joinFontStr = ({ italic, bold, fontSize, fontFamily }) => { return `${italic ? 'italic ' : ''} ${ bold ? 'bold ' : '' } ${fontSize}px ${fontFamily} ` } // 在下一个事件循环里执行任务 export const nextTick = function (fn, ctx) { let pending = false let timerFunc = null let handle = () => { pending = false ctx ? fn.call(ctx) : fn() } // 支持MutationObserver接口的话使用MutationObserver if (typeof MutationObserver !== 'undefined') { let counter = 1 let observer = new MutationObserver(handle) let textNode = document.createTextNode(counter) observer.observe(textNode, { characterData: true // 设为 true 表示监视指定目标节点或子节点树中节点所包含的字符数据的变化 }) timerFunc = function () { counter = (counter + 1) % 2 // counter会在0和1两者循环变化 textNode.data = counter // 节点变化会触发回调handle, } } else { // 否则使用定时器 timerFunc = setTimeout } return function () { if (pending) return pending = true timerFunc(handle, 0) } } // 检查节点是否超出画布 export const checkNodeOuter = (mindMap, node, offsetX = 0, offsetY = 0) => { let elRect = mindMap.elRect let { scaleX, scaleY, translateX, translateY } = mindMap.draw.transform() 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 let offsetLeft = 0 let offsetTop = 0 if (left < 0 + offsetX) { offsetLeft = -left + offsetX } if (right > elRect.width - offsetX) { offsetLeft = -(right - elRect.width) - offsetX } if (top < 0 + offsetY) { offsetTop = -top + offsetY } if (bottom > elRect.height - offsetY) { offsetTop = -(bottom - elRect.height) - offsetY } return { isOuter: offsetLeft !== 0 || offsetTop !== 0, offsetLeft, offsetTop } } // 提取html字符串里的纯文本 let getTextFromHtmlEl = null export const getTextFromHtml = html => { if (!getTextFromHtmlEl) { getTextFromHtmlEl = document.createElement('div') } getTextFromHtmlEl.innerHTML = html return getTextFromHtmlEl.textContent } // 将blob转成data:url export const readBlob = blob => { return new Promise((resolve, reject) => { let reader = new FileReader() reader.onload = evt => { resolve(evt.target.result) } reader.onerror = err => { reject(err) } reader.readAsDataURL(blob) }) } // 将dom节点转换成html字符串 let nodeToHTMLWrapEl = null export const nodeToHTML = node => { if (!nodeToHTMLWrapEl) { nodeToHTMLWrapEl = document.createElement('div') } nodeToHTMLWrapEl.innerHTML = '' nodeToHTMLWrapEl.appendChild(node) return nodeToHTMLWrapEl.innerHTML } // 获取图片大小 export const getImageSize = src => { return new Promise(resolve => { let img = new Image() img.src = src img.onload = () => { resolve({ width: img.width, height: img.height }) } img.onerror = () => { resolve({ width: 0, height: 0 }) } }) } // 创建节点唯一的id export const createUid = () => { return uuidv4() } // 加载图片文件 export const loadImage = imgFile => { return new Promise((resolve, reject) => { let fr = new FileReader() fr.readAsDataURL(imgFile) fr.onload = async e => { let url = e.target.result let size = await getImageSize(url) resolve({ url, size }) } fr.onerror = error => { reject(error) } }) } // 移除字符串中的html实体 export const removeHTMLEntities = str => { [['&nbsp;', '&#160;']].forEach(item => { str = str.replace(new RegExp(item[0], 'g'), item[1]) }) return str } // 获取一个数据的类型 export const getType = data => { return Object.prototype.toString.call(data).slice(8, -1) } // 判断一个数据是否是null和undefined和空字符串 export const isUndef = data => { return data === null || data === undefined || data === '' } // 移除html字符串中节点的内联样式 export const removeHtmlStyle = html => { return html.replace(/(<[^\s]+)\s+style=["'][^'"]+["']\s*(>)/g, '$1$2') } // 给html标签中指定的标签添加内联样式 let addHtmlStyleEl = null export const addHtmlStyle = (html, tag, style) => { if (!addHtmlStyleEl) { addHtmlStyleEl = document.createElement('div') } const tags = Array.isArray(tag) ? tag : [tag] addHtmlStyleEl.innerHTML = html let walk = root => { let childNodes = root.childNodes childNodes.forEach(node => { if (node.nodeType === 1) { // 元素节点 if (tags.includes(node.tagName.toLowerCase())) { node.style.cssText = style } else { walk(node) } } }) } walk(addHtmlStyleEl) return addHtmlStyleEl.innerHTML } // 检查一个字符串是否是富文本字符 let checkIsRichTextEl = null export const checkIsRichText = str => { if (!checkIsRichTextEl) { checkIsRichTextEl = document.createElement('div') } checkIsRichTextEl.innerHTML = str for (let c = checkIsRichTextEl.childNodes, i = c.length; i--; ) { if (c[i].nodeType == 1) return true } return false } // 搜索和替换html字符串中指定的文本 let replaceHtmlTextEl = null export const replaceHtmlText = (html, searchText, replaceText) => { if (!replaceHtmlTextEl) { replaceHtmlTextEl = document.createElement('div') } replaceHtmlTextEl.innerHTML = html let walk = root => { let childNodes = root.childNodes childNodes.forEach(node => { if (node.nodeType === 1) { // 元素节点 walk(node) } else if (node.nodeType === 3) { // 文本节点 root.replaceChild( document.createTextNode( node.nodeValue.replace(new RegExp(searchText, 'g'), replaceText) ), node ) } }) } walk(replaceHtmlTextEl) return replaceHtmlTextEl.innerHTML } // 去除html字符串中指定选择器的节点,然后返回html字符串 let removeHtmlNodeByClassEl = null export const removeHtmlNodeByClass = (html, selector) => { if (!removeHtmlNodeByClassEl) { removeHtmlNodeByClassEl = document.createElement('div') } removeHtmlNodeByClassEl.innerHTML = html const node = removeHtmlNodeByClassEl.querySelector(selector) if (node) { node.parentNode.removeChild(node) } return removeHtmlNodeByClassEl.innerHTML } // 判断一个颜色是否是白色 export const isWhite = color => { color = String(color).replace(/\s+/g, '') return ( ['#fff', '#ffffff', '#FFF', '#FFFFFF', 'rgb(255,255,255)'].includes( color ) || /rgba\(255,255,255,[^)]+\)/.test(color) ) } // 判断一个颜色是否是透明 export const isTransparent = color => { color = String(color).replace(/\s+/g, '') return ( ['', 'transparent'].includes(color) || /rgba\(\d+,\d+,\d+,0\)/.test(color) ) } // 从当前主题里获取一个非透明非白色的颜色 export const getVisibleColorFromTheme = themeConfig => { let { lineColor, root, second, node } = themeConfig let list = [ lineColor, root.fillColor, root.color, second.fillColor, second.color, node.fillColor, node.color, root.borderColor, second.borderColor, node.borderColor ] for (let i = 0; i < list.length; i++) { let color = list[i] if (!isTransparent(color) && !isWhite(color)) { return color } } } // 去掉DOM节点中的公式标签 export const removeFormulaTags = node => { const walk = root => { const childNodes = root.childNodes childNodes.forEach(node => { if (node.nodeType === 1) { if (node.classList.contains('ql-formula')) { node.parentNode.removeChild(node) } else { walk(node) } } }) } walk(node) } // 将<p><span></span><p>形式的节点富文本内容转换成\n换行的文本 // 会过滤掉节点中的格式节点 let nodeRichTextToTextWithWrapEl = null export const nodeRichTextToTextWithWrap = html => { if (!nodeRichTextToTextWithWrapEl) { nodeRichTextToTextWithWrapEl = document.createElement('div') } nodeRichTextToTextWithWrapEl.innerHTML = html const childNodes = nodeRichTextToTextWithWrapEl.childNodes let res = '' for (let i = 0; i < childNodes.length; i++) { const node = childNodes[i] if (node.nodeType === 1) { // 元素节点 removeFormulaTags(node) if (node.tagName.toLowerCase() === 'p') { res += node.textContent + '\n' } else { res += node.textContent } } else if (node.nodeType === 3) { // 文本节点 res += node.nodeValue } } return res.replace(/\n$/, '') } // 将<br>换行的文本转换成<p><span></span><p>形式的节点富文本内容 let textToNodeRichTextWithWrapEl = null export const textToNodeRichTextWithWrap = html => { if (!textToNodeRichTextWithWrapEl) { textToNodeRichTextWithWrapEl = document.createElement('div') } textToNodeRichTextWithWrapEl.innerHTML = html const childNodes = textToNodeRichTextWithWrapEl.childNodes let list = [] let str = '' for (let i = 0; i < childNodes.length; i++) { const node = childNodes[i] if (node.nodeType === 1) { // 元素节点 if (node.tagName.toLowerCase() === 'br') { list.push(str) str = '' } else { str += node.textContent } } else if (node.nodeType === 3) { // 文本节点 str += node.nodeValue } } if (str) { list.push(str) } return list .map(item => { return `<p><span>${htmlEscape(item)}</span></p>` }) .join('') } // 去除富文本内容的样式,包括样式标签,比如strong、em、s等 // 但要保留数学公式内容 let removeRichTextStyesEl = null export const removeRichTextStyes = html => { if (!removeRichTextStyesEl) { removeRichTextStyesEl = document.createElement('div') } removeRichTextStyesEl.innerHTML = html // 首先用占位文本替换掉所有的公式 const formulaList = removeRichTextStyesEl.querySelectorAll('.ql-formula') Array.from(formulaList).forEach(el => { const placeholder = document.createTextNode('$smmformula$') el.parentNode.replaceChild(placeholder, el) }) // 然后遍历每行节点,去掉内部的所有标签,转为文本 const childNodes = removeRichTextStyesEl.childNodes let list = [] for (let i = 0; i < childNodes.length; i++) { const node = childNodes[i] if (node.nodeType === 1) { // 元素节点 list.push(node.textContent) } else if (node.nodeType === 3) { // 文本节点 list.push(node.nodeValue) } } // 拼接文本 html = list .map(item => { return `<p><span>${htmlEscape(item)}</span></p>` }) .join('') // 将公式添加回去 if (formulaList.length > 0) { html = html.replace(/\$smmformula\$/g, '<span class="smmformula"></span>') removeRichTextStyesEl.innerHTML = html const els = removeRichTextStyesEl.querySelectorAll('.smmformula') Array.from(els).forEach((el, index) => { el.parentNode.replaceChild(formulaList[index], el) }) html = removeRichTextStyesEl.innerHTML } return html } // 判断是否是移动端环境 export const isMobile = () => { return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( navigator.userAgent ) } // 获取对象改变了的的属性 export const getObjectChangedProps = (oldObject, newObject) => { const res = {} Object.keys(newObject).forEach(prop => { const oldVal = oldObject[prop] const newVal = newObject[prop] if (getType(oldVal) !== getType(newVal)) { res[prop] = newVal return } if (getType(oldVal) === 'Object') { if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) { res[prop] = newVal return } } else { if (oldVal !== newVal) { res[prop] = newVal return } } }) return res } // 判断一个字段是否是节点数据中的样式字段 export const checkIsNodeStyleDataKey = key => { // 用户自定义字段 if (/^_/.test(key)) return false // 不在节点非样式字段列表里,那么就是样式字段 if (!nodeDataNoStylePropList.includes(key)) { return true } return false } // 判断一个对象是否不需要触发节点重新创建 export const isNodeNotNeedRenderData = config => { const list = [...lineStyleProps] // 节点连线样式 const keys = Object.keys(config) for (let i = 0; i < keys.length; i++) { if (!list.includes(keys[i])) { return false } } return true } // 合并图标数组 // const data = [ // { type: 'priority', name: '优先级图标', list: [{ name: '1', icon: 'a' }, { name: 2, icon: 'b' }] }, // { type: 'priority', name: '优先级图标', list: [{ name: '2', icon: 'c' }, { name: 3, icon: 'd' }] }, // ]; // mergerIconList(data) 结果 // [ // { type: 'priority', name: '优先级图标', list: [{ name: '1', icon: 'a' }, { name: 2, icon: 'c' }, { name: 3, icon: 'd' }] }, // ] export const mergerIconList = list => { return list.reduce((result, item) => { const existingItem = result.find(x => x.type === item.type) if (existingItem) { item.list.forEach(newObj => { const existingObj = existingItem.list.find(x => x.name === newObj.name) if (existingObj) { existingObj.icon = newObj.icon } else { existingItem.list.push(newObj) } }) } else { result.push({ ...item }) } return result }, []) } // 从节点实例列表里找出顶层的节点 export const getTopAncestorsFomNodeList = list => { let res = [] list.forEach(node => { if ( !list.find(item => { return item.uid !== node.uid && item.isAncestor(node) }) ) { res.push(node) } }) return res } // 从给定的节点实例列表里判断是否存在上下级关系 export const checkHasSupSubRelation = list => { for (let i = 0; i < list.length; i++) { const cur = list[i] if ( list.find(item => { return item.uid !== cur.uid && cur.isAncestor(item) }) ) { return true } } return false } // 解析要添加概要的节点实例列表 export const parseAddGeneralizationNodeList = 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 => { if (cache[uid].length > 1) { const rangeList = cache[uid] .map(item => { return item.index }) .sort((a, b) => { return a - b }) res.push({ node: uidToParent[uid], range: [rangeList[0], rangeList[rangeList.length - 1]] }) } else { res.push({ node: cache[uid][0].node }) } }) return res } // 判断两个矩形是否重叠 export const checkTwoRectIsOverlap = ( minx1, maxx1, miny1, maxy1, minx2, maxx2, miny2, maxy2 ) => { return maxx1 > minx2 && maxx2 > minx1 && maxy1 > miny2 && maxy2 > miny1 } // 聚焦指定输入框 export const focusInput = el => { let selection = window.getSelection() let range = document.createRange() range.selectNodeContents(el) range.collapse() selection.removeAllRanges() selection.addRange(range) } // 聚焦全选指定输入框 export const selectAllInput = el => { let selection = window.getSelection() let range = document.createRange() range.selectNodeContents(el) selection.removeAllRanges() selection.addRange(range) } // 给指定的节点列表树数据添加附加数据,会修改原数据 export const addDataToAppointNodes = (appointNodes, data = {}) => { data = { ...data } const alreadyIsRichText = data && data.richText // 如果指定的数据就是富文本格式,那么不需要重新创建 if (alreadyIsRichText && data.resetRichText) { delete data.resetRichText } const walk = list => { list.forEach(node => { node.data = { ...node.data, ...data } if (node.children && node.children.length > 0) { walk(node.children) } }) } walk(appointNodes) return appointNodes } // 给指定的节点列表树数据添加uid,会修改原数据 // createNewId默认为false,即如果节点不存在uid的话,会创建新的uid。如果传true,那么无论节点数据原来是否存在uid,都会创建新的uid export const createUidForAppointNodes = ( appointNodes, createNewId = false, handle = null, handleGeneralization = false ) => { const walk = list => { list.forEach(node => { if (!node.data) { node.data = {} } if (createNewId || isUndef(node.data.uid)) { node.data.uid = createUid() } if (handleGeneralization) { const generalizationList = formatGetNodeGeneralization(node.data) generalizationList.forEach(gNode => { if (createNewId || isUndef(gNode.uid)) { gNode.uid = createUid() } }) } handle && handle(node) if (node.children && node.children.length > 0) { walk(node.children) } }) } walk(appointNodes) return appointNodes } // 传入一个数据,如果该数据是数组,那么返回该数组,否则返回一个以该数据为成员的数组 export const formatDataToArray = data => { if (!data) return [] return Array.isArray(data) ? data : [data] } // 获取节点在同级里的位置索引 export const getNodeDataIndex = node => { return node.parent ? node.parent.nodeData.children.findIndex(item => { return item.data.uid === node.uid }) : 0 } // 从一个节点列表里找出某个节点的索引 export const getNodeIndexInNodeList = (node, nodeList) => { return nodeList.findIndex(item => { return item.uid === node.uid }) } // 根据内容生成颜色 export const generateColorByContent = str => { let hash = 0 for (let i = 0; i < str.length; i++) { hash = str.charCodeAt(i) + ((hash << 5) - hash) } // 这里使用伪随机数的原因是因为 // 1. 如果字符串的内容差不多,根据hash生产的颜色就比较相近,不好区分,比如v1.1 v1.2,所以需要加入随机数来使得颜色能够区分开 // 2. 普通的随机数每次数值不一样,就会导致每次新增标签原来的标签颜色就会发生改变,所以加入了这个方法,使得内容不变随机数也不变 const rng = new MersenneTwister(hash) const h = rng.genrand_int32() % 360 return 'hsla(' + h + ', 50%, 50%, 1)' } // html转义 export const htmlEscape = str => { [ ['&', '&amp;'], ['<', '&lt;'], ['>', '&gt;'] ].forEach(item => { str = str.replace(new RegExp(item[0], 'g'), item[1]) }) return str } // 判断两个对象是否相同,只处理对象或数组 export const isSameObject = (a, b) => { const type = getType(a) // a、b类型不一致,那么肯定不相同 if (type !== getType(b)) return false // 如果都是对象 if (type === 'Object') { const keysa = Object.keys(a) const keysb = Object.keys(b) // 对象字段数量不一样,肯定不相同 if (keysa.length !== keysb.length) return false // 字段数量一样,那么需要遍历字段进行判断 for (let i = 0; i < keysa.length; i++) { const key = keysa[i] // b没有a的一个字段,那么肯定不相同 if (!keysb.includes(key)) return false // 字段名称一样,那么需要递归判断它们的值 const isSame = isSameObject(a[key], b[key]) if (!isSame) { return false } } return true } else if (type === 'Array') { // 如果都是数组 // 数组长度不一样,肯定不相同 if (a.length !== b.length) return false // 长度一样,那么需要遍历进行判断 for (let i = 0; i < a.length; i++) { const itema = a[i] const itemb = b[i] const typea = getType(itema) const typeb = getType(itemb) if (typea !== typeb) return false const isSame = isSameObject(itema, itemb) if (!isSame) { return false } } return true } else { // 其他类型,直接全等判断 return a === b } } // 检查navigator.clipboard对象的读取是否可用 export const checkClipboardReadEnable = () => { return navigator.clipboard && typeof navigator.clipboard.read === 'function' } // 将数据设置到用户剪切板中 export const setDataToClipboard = data => { if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(JSON.stringify(data)) } } // 从用户剪贴板中读取文字和图片 export const getDataFromClipboard = async () => { let text = null let img = null if (checkClipboardReadEnable()) { const items = await navigator.clipboard.read() if (items && items.length > 0) { for (const clipboardItem of items) { for (const type of clipboardItem.types) { if (/^image\//.test(type)) { img = await clipboardItem.getType(type) } else if (type === 'text/plain') { const blob = await clipboardItem.getType(type) text = await blob.text() } } } } } return { text, img } } // 从节点的父节点的nodeData.children列表中移除该节点的数据 export const removeFromParentNodeData = node => { if (!node || !node.parent) return const index = getNodeDataIndex(node) if (index === -1) return node.parent.nodeData.children.splice(index, 1) } // 给html自闭合标签添加闭合状态 export const handleSelfCloseTags = str => { selfCloseTagList.forEach(tagName => { str = str.replace( new RegExp(`<${tagName}([^>]*)>`, 'g'), `<${tagName} $1 />` ) }) return str } // 检查两个节点列表是否包含的节点是一样的 export const checkNodeListIsEqual = (list1, list2) => { if (list1.length !== list2.length) return false for (let i = 0; i < list1.length; i++) { if ( !list2.find(item => { return item.uid === list1[i].uid }) ) { return false } } return true } // 获取浏览器的chrome内核版本 export const getChromeVersion = () => { const match = navigator.userAgent.match(/\s+Chrome\/(.*)\s+/) if (match && match[1]) { return Number.parseFloat(match[1]) } return '' } // 创建smm粘贴的粘贴数据 export const createSmmFormatData = data => { return { simpleMindMap: true, data } } // 检查是否是smm粘贴格式的数据 export const checkSmmFormatData = data => { let smmData = null // 如果是字符串,则尝试解析为对象 if (typeof data === 'string') { try { const parsedData = JSON.parse(data) // 判断是否是对象,且存在属性标志 if (typeof parsedData === 'object' && parsedData.simpleMindMap) { smmData = parsedData.data } } catch (error) {} } else if (typeof data === 'object' && data.simpleMindMap) { // 否则如果是对象,则检查属性标志 smmData = data.data } const isSmm = !!smmData return { isSmm, data: isSmm ? smmData : String(data) } } // 处理输入框的粘贴事件,会去除文本的html格式、换行 export const handleInputPasteText = (e, text) => { e.preventDefault() const selection = window.getSelection() if (!selection.rangeCount) return selection.deleteFromDocument() text = text || e.clipboardData.getData('text') // 转义特殊字符 text = htmlEscape(text) // 去除格式 text = getTextFromHtml(text) // 去除换行 // text = text.replace(/\n/g, '') const textArr = text.split(/\n/g) const fragment = document.createDocumentFragment() textArr.forEach((item, index) => { const node = document.createTextNode(item) fragment.appendChild(node) if (index < textArr.length - 1) { const br = document.createElement('br') fragment.appendChild(br) } }) selection.getRangeAt(0).insertNode(fragment) selection.collapseToEnd() } // 将思维导图树结构转平级对象 /* { data: { uid: 'xxx' }, children: [ { data: { uid: 'xxx' }, children: [] } ] } 转为: { uid: { children: [uid1, uid2], data: {} } } */ export const transformTreeDataToObject = data => { const res = {} const walk = (root, parent) => { const uid = root.data.uid if (parent) { parent.children.push(uid) } res[uid] = { isRoot: !parent, data: { ...root.data }, children: [] } if (root.children && root.children.length > 0) { root.children.forEach(item => { walk(item, res[uid]) }) } } walk(data, null) return res } // 将平级对象转树结构 // transformTreeDataToObject方法的反向操作 // 找到父节点的uid const _findParentUid = (data, targetUid) => { const uids = Object.keys(data) let res = '' uids.forEach(uid => { const children = data[uid].children const isParent = children.findIndex(childUid => { return childUid === targetUid }) !== -1 if (isParent) { res = uid } }) return res } export const transformObjectToTreeData = data => { const uids = Object.keys(data) if (uids.length <= 0) return null const rootKey = uids.find(uid => { return data[uid].isRoot }) if (!rootKey || !data[rootKey]) return null // 根节点 const res = { data: simpleDeepClone(data[rootKey].data), children: [] } const map = {} map[rootKey] = res uids.forEach(uid => { const parentUid = _findParentUid(data, uid) const cur = data[uid] const node = map[uid] || { data: simpleDeepClone(cur.data), children: [] } if (!map[uid]) { map[uid] = node } if (parentUid) { const index = data[parentUid].children.findIndex(item => { return item === uid }) if (!map[parentUid]) { map[parentUid] = { data: simpleDeepClone(data[parentUid].data), children: [] } } map[parentUid].children[index] = node } }) return res } // 计算两个点的直线距离 export const getTwoPointDistance = (x1, y1, x2, y2) => { return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)) } // 判断两个矩形的相对位置 // 第一个矩形在第二个矩形的什么方向 export const getRectRelativePosition = (rect1, rect2) => { // 获取第一个矩形的中心点坐标 const rect1CenterX = rect1.x + rect1.width / 2 const rect1CenterY = rect1.y + rect1.height / 2 // 获取第二个矩形的中心点坐标 const rect2CenterX = rect2.x + rect2.width / 2 const rect2CenterY = rect2.y + rect2.height / 2 // 判断第一个矩形在第二个矩形的哪个方向 if (rect1CenterX < rect2CenterX && rect1CenterY < rect2CenterY) { return 'left-top' } else if (rect1CenterX > rect2CenterX && rect1CenterY < rect2CenterY) { return 'right-top' } else if (rect1CenterX > rect2CenterX && rect1CenterY > rect2CenterY) { return 'right-bottom' } else if (rect1CenterX < rect2CenterX && rect1CenterY > rect2CenterY) { return 'left-bottom' } else if (rect1CenterX < rect2CenterX && rect1CenterY === rect2CenterY) { return 'left' } else if (rect1CenterX > rect2CenterX && rect1CenterY === rect2CenterY) { return 'right' } else if (rect1CenterX === rect2CenterX && rect1CenterY < rect2CenterY) { return 'top' } else if (rect1CenterX === rect2CenterX && rect1CenterY > rect2CenterY) { return 'bottom' } else { return 'overlap' } } // 处理获取svg内容时添加额外内容 export const handleGetSvgDataExtraContent = ({ addContentToHeader, addContentToFooter }) => { // 追加内容 const cssTextList = [] let header = null let headerHeight = 0 let footer = null let footerHeight = 0 const handle = (fn, callback) => { if (typeof fn === 'function') { const res = fn() if (!res) return const { el, cssText, height } = res if (el instanceof HTMLElement) { addXmlns(el) const foreignObject = createForeignObjectNode({ el, height }) callback(foreignObject, height) } if (cssText) { cssTextList.push(cssText) } } } handle(addContentToHeader, (foreignObject, height) => { header = foreignObject headerHeight = height }) handle(addContentToFooter, (foreignObject, height) => { footer = foreignObject footerHeight = height }) return { cssTextList, header, headerHeight, footer, footerHeight } } // 获取指定节点的包围框信息 export const getNodeTreeBoundingRect = ( node, x = 0, y = 0, paddingX = 0, paddingY = 0, excludeSelf = false, excludeGeneralization = false ) => { let minX = Infinity let maxX = -Infinity let minY = Infinity let maxY = -Infinity const walk = (root, isRoot) => { if (!(isRoot && excludeSelf) && root.group) { try { 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 } } catch (e) {} } if (!excludeGeneralization && root._generalizationList.length > 0) { root._generalizationList.forEach(item => { walk(item.generalizationNode) }) } if (root.children) { root.children.forEach(item => { walk(item) }) } } walk(node, true) minX = minX - x + paddingX minY = minY - y + paddingY maxX = maxX - x + paddingX maxY = maxY - y + paddingY return { left: minX, top: minY, width: maxX - minX, height: maxY - minY } } // 获取多个节点总的包围框 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) { 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.fullscreenElement) return if (document.exitFullscreen) { document.exitFullscreen() } else if (document.webkitExitFullscreen) { document.webkitExitFullscreen() } else if (document.mozCancelFullScreen) { document.mozCancelFullScreen() } } // 创建foreignObject节点 export const createForeignObjectNode = ({ el, width, height }) => { const foreignObject = new ForeignObject() if (width !== undefined) { foreignObject.width(width) } if (height !== undefined) { foreignObject.height(height) } foreignObject.add(el) return foreignObject } // 格式化获取节点的概要数据 export const formatGetNodeGeneralization = data => { const generalization = data.generalization if (generalization) { return Array.isArray(generalization) ? generalization : [generalization] } else { return [] } } /** * 防御 XSS 攻击,过滤恶意 HTML 标签和属性 * @param {string} text 需要过滤的文本 * @returns {string} 过滤后的文本 */ export const defenseXSS = text => { text = String(text) // 初始化结果变量 let result = text // 使用正则表达式匹配 HTML 标签 const match = text.match(/<(\S*?)[^>]*>.*?|<.*? \/>/g) if (match == null) { // 如果没有匹配到任何标签,则直接返回原始文本 return text } // 遍历匹配到的标签 for (let value of match) { // 定义白名单属性正则表达式(style、target、href) const whiteAttrRegex = new RegExp(/(style|target|href)=["'][^"']*["']/g) // 定义黑名单href正则表达式(javascript:) const aHrefBlackRegex = new RegExp(/href=["']javascript:/g) // 过滤 HTML 标签 const filterHtml = value.replace( // 匹配属性键值对(如:key="value") /([a-zA-Z-]+)\s*=\s*["']([^"']*)["']/g, text => { // 如果属性值包含黑名单href或不在白名单中,则删除该属性 if (aHrefBlackRegex.test(text) || !whiteAttrRegex.test(text)) { return '' } // 否则,保留该属性 return text } ) // 将过滤后的标签替换回原始文本 result = result.replace(value, filterHtml) } // 返回最终结果 return result } // 给节点添加命名空间 export const addXmlns = el => { el.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml') } // 给一组节点实例升序排序,依据其sortIndex值 export const sortNodeList = nodeList => { nodeList = [...nodeList] nodeList.sort((a, b) => { return a.sortIndex - b.sortIndex }) return nodeList } // 合并主题配置 export const mergeTheme = (dest, source) => { return merge(dest, source, { arrayMerge: (destinationArray, sourceArray) => { return sourceArray } }) } // 获取节点实例的文本样式数据 export const getNodeRichTextStyles = node => { const res = {} richTextSupportStyleList.forEach(prop => { let value = node.style.merge(prop) if (prop === 'fontSize') { value = value + 'px' } res[prop] = value }) return res } // 判断两个版本号的关系 /* a > b 返回 > a < b 返回 < a = b 返回 = */ export const compareVersion = (a, b) => { const aArr = String(a).split('.') const bArr = String(b).split('.') const max = Math.max(aArr.length, bArr.length) for (let i = 0; i < max; i++) { const ai = aArr[i] || 0 const bi = bArr[i] || 0 if (ai > bi) { return '>' } else if (ai < bi) { return '<' } } return '=' }