Files
mind-map/simple-mind-map/src/utils/index.js
2024-08-21 22:01:19 +08:00

1603 lines
40 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { v4 as uuidv4 } from 'uuid'
import {
nodeDataNoStylePropList,
selfCloseTagList
} from '../constants/constant'
import MersenneTwister from './mersenneTwister'
import { ForeignObject } from '@svgdotjs/svg.js'
// 深度优先遍历树
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 = [nRatio * newHeight, newHeight]
} else {
// 固定宽度
arr = [newWidth, newWidth / nRatio]
}
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 = [nRatio * maxHeight, maxHeight]
} else {
// 固定宽度
arr = [maxWidth, maxWidth / nRatio]
}
}
} 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)
})
}
return tree
}
// 复制节点树数据
export const copyNodeTree = (
tree,
root,
removeActiveState = false,
removeId = true
) => {
tree.data = simpleDeepClone(root.nodeData ? root.nodeData.data : root.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)
})
}
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 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) => {
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) {
offsetLeft = -left
}
if (right > elRect.width) {
offsetLeft = -(right - elRect.width)
}
if (top < 0) {
offsetTop = -top
}
if (bottom > elRect.height) {
offsetTop = -(bottom - elRect.height)
}
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.replaceAll(item[0], 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.replaceAll(/(<[^\s]+)\s+style=["'][^'"]+["']\s*(>)/g, '$1$2')
}
// 给html标签中指定的标签添加内联样式
let addHtmlStyleEl = null
export const addHtmlStyle = (html, tag, style) => {
if (!addHtmlStyleEl) {
addHtmlStyleEl = document.createElement('div')
}
addHtmlStyleEl.innerHTML = html
let walk = root => {
let childNodes = root.childNodes
childNodes.forEach(node => {
if (node.nodeType === 1) {
// 元素节点
if (node.tagName.toLowerCase() === tag) {
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.replaceAll(searchText, 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).replaceAll(/\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).replaceAll(/\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
}
// 合并图标数组
// 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 = {}) => {
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
) => {
const walk = list => {
list.forEach(node => {
if (!node.data) {
node.data = {}
}
if (createNewId || isUndef(node.data.uid)) {
node.data.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
}
}
// 将数据设置到用户剪切板中
export const setDataToClipboard = data => {
if (navigator.clipboard) {
navigator.clipboard.writeText(JSON.stringify(data))
}
}
// 从用户剪贴板中读取文字和图片
export const getDataFromClipboard = async () => {
let text = null
let img = null
if (navigator.clipboard) {
text = await navigator.clipboard.readText()
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)
break
}
}
}
}
}
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.replaceAll(
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 = getTextFromHtml(text)
// 去除换行
text = text.replaceAll(/\n/g, '')
const node = document.createTextNode(text)
selection.getRangeAt(0).insertNode(node)
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
}