Feat:新增外框插件

This commit is contained in:
街角小林
2024-07-01 17:15:48 +08:00
parent 4d1608e8c4
commit 876afb2504
3 changed files with 439 additions and 3 deletions

View File

@@ -315,7 +315,8 @@ export const nodeDataNoStylePropList = [
'associativeLineText',
'attachmentUrl',
'attachmentName',
'notation'
'notation',
'outerFrame'
]
// 错误类型

View File

@@ -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

View File

@@ -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) {