Merge pull request #1 from wanglin2/feature

Feature
This commit is contained in:
esky-wh 2023-09-14 10:59:57 +08:00 committed by GitHub
commit f19704ec41
48 changed files with 910 additions and 209 deletions

View File

@ -171,4 +171,12 @@ const mindMap = new MindMap({
<img src="./web/src/assets/avatar/南风.jpg" style="width: 50px;height: 50px;" />
<span>南风</span>
</span>
<span>
<img src="./web/src/assets/avatar/蜉蝣撼大叔.jpg" style="width: 50px;height: 50px;" />
<span>蜉蝣撼大叔</span>
</span>
<span>
<img src="./web/src/assets/avatar/乙.jpg" style="width: 50px;height: 50px;" />
<span></span>
</span>
</p>

View File

@ -448,6 +448,7 @@ class MindMap {
// 插件列表
MindMap.pluginList = []
MindMap.usePlugin = (plugin, opt = {}) => {
if (MindMap.hasPlugin(plugin) !== -1) return
plugin.pluginOpt = opt
MindMap.pluginList.push(plugin)
return MindMap

View File

@ -249,6 +249,10 @@ export const CONSTANTS = {
PASTE_TYPE: {
CLIP_BOARD: 'clipBoard',
CANVAS: 'canvas'
},
SCROLL_BAR_DIR: {
VERTICAL: 'vertical',
HORIZONTAL: 'horizontal'
}
}

View File

@ -382,7 +382,7 @@ class Render {
// 检索某个节点在激活列表里的索引
findActiveNodeIndex(node) {
return this.activeNodeList.findIndex(item => {
return item === node
return item.uid === node.uid
})
}
@ -390,7 +390,7 @@ class Render {
getNodeIndex(node) {
return node.parent
? node.parent.children.findIndex(item => {
return item === node
return item.uid === node.uid
})
: 0
}
@ -553,7 +553,7 @@ class Render {
let parent = node.parent
let childList = parent.children
let index = childList.findIndex(item => {
return item === node
return item.uid === node.uid
})
if (index === -1 || index === 0) {
return
@ -580,7 +580,7 @@ class Render {
let parent = node.parent
let childList = parent.children
let index = childList.findIndex(item => {
return item === node
return item.uid === node.uid
})
if (index === -1 || index === childList.length - 1) {
return
@ -752,7 +752,7 @@ class Render {
let nodeParent = node.parent
let nodeBorthers = nodeParent.children
let nodeIndex = nodeBorthers.findIndex(item => {
return item === node
return item.uid === node.uid
})
if (nodeIndex === -1) {
return
@ -764,7 +764,7 @@ class Render {
let existParent = exist.parent
let existBorthers = existParent.children
let existIndex = existBorthers.findIndex(item => {
return item === exist
return item.uid === exist.uid
})
if (existIndex === -1) {
return
@ -791,7 +791,7 @@ class Render {
let nodeParent = node.parent
let nodeBorthers = nodeParent.children
let nodeIndex = nodeBorthers.findIndex(item => {
return item === node
return item.uid === node.uid
})
if (nodeIndex === -1) {
return
@ -803,7 +803,7 @@ class Render {
let existParent = exist.parent
let existBorthers = existParent.children
let existIndex = existBorthers.findIndex(item => {
return item === exist
return item.uid === exist.uid
})
if (existIndex === -1) {
return

View File

@ -466,7 +466,7 @@ class Node {
e.stopPropagation()
e.preventDefault()
// 如果是多选节点结束,那么不要触发右键菜单事件
if(!useLeftKeySelectionRightKeyDrag && this.mindMap.select.hasSelectRange()) {
if(this.mindMap.select && !useLeftKeySelectionRightKeyDrag && this.mindMap.select.hasSelectRange()) {
return
}
if (this.nodeData.data.isActive) {
@ -794,12 +794,12 @@ class Node {
// 检测当前节点是否是某个节点的祖先节点
isParent(node) {
if (this === node) {
if (this.uid === node.uid) {
return false
}
let parent = node.parent
while (parent) {
if (this === parent) {
if (this.uid === parent.uid) {
return true
}
parent = parent.parent
@ -809,11 +809,11 @@ class Node {
// 检测当前节点是否是某个节点的兄弟节点
isBrother(node) {
if (!this.parent || this === node) {
if (!this.parent || this.uid === node.uid) {
return false
}
return this.parent.children.find(item => {
return item === node
return item.uid === node.uid
})
}

View File

@ -1,7 +1,6 @@
import {
tagColorList,
nodeDataNoStylePropList
} from '../../../constants/constant'
import { tagColorList } from '../../../constants/constant'
import { checkIsNodeStyleDataKey } from '../../../utils/index'
const rootProp = ['paddingX', 'paddingY']
const backgroundStyleProps = [
'backgroundColor',
@ -225,7 +224,7 @@ class Style {
hasCustomStyle() {
let res = false
Object.keys(this.ctx.nodeData.data).forEach(item => {
if (!nodeDataNoStylePropList.includes(item)) {
if (checkIsNodeStyleDataKey(item)) {
res = true
}
})

View File

@ -171,7 +171,6 @@ class View {
// 平移x方式到
translateXTo(x) {
if (x === 0) return
this.x = x
this.transform()
}
@ -185,7 +184,6 @@ class View {
// 平移y方向到
translateYTo(y) {
if (y === 0) return
this.y = y
this.transform()
}

View File

@ -202,9 +202,13 @@ class Base {
}
// 递归计算节点的宽度
getNodeAreaWidth(node) {
getNodeAreaWidth(node, withGeneralization = false) {
let widthArr = []
let totalGeneralizationNodeWidth = 0
let loop = (node, width) => {
if (withGeneralization && node.checkHasGeneralization()) {
totalGeneralizationNodeWidth += node._generalizationNodeWidth
}
if (node.children.length) {
width += node.width / 2
node.children.forEach(item => {
@ -216,7 +220,7 @@ class Base {
}
}
loop(node, 0)
return Math.max(...widthArr)
return Math.max(...widthArr) + totalGeneralizationNodeWidth
}
// 二次贝塞尔曲线

View File

@ -87,11 +87,11 @@ class CatalogOrganization extends Base {
totalLeft += cur.width + marginX
})
} else {
let totalTop = node.top + node.height + marginY + (this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
let totalTop = node.top + this.getNodeHeightWithGeneralization(node) + marginY + (this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
node.children.forEach(cur => {
cur.left = node.left + node.width * 0.5
cur.top = totalTop
totalTop += cur.height + marginY + (this.getNodeActChildrenLength(cur) > 0 ? cur.expandBtnSize : 0)
totalTop += this.getNodeHeightWithGeneralization(cur) + marginY + (this.getNodeActChildrenLength(cur) > 0 ? cur.expandBtnSize : 0)
})
}
}
@ -112,7 +112,7 @@ class CatalogOrganization extends Base {
}
// 调整left
if (parent && parent.isRoot) {
let areaWidth = this.getNodeAreaWidth(node)
let areaWidth = this.getNodeAreaWidth(node, true)
let difference = areaWidth - node.width
if (difference > 0) {
this.updateBrothersLeft(node, difference)
@ -124,7 +124,7 @@ class CatalogOrganization extends Base {
let marginY = this.getMarginY(layerIndex + 1)
let totalHeight =
node.children.reduce((h, item) => {
return h + item.height + (this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
return h + this.getNodeHeightWithGeneralization(item) + (this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
}, 0) +
len * marginY
this.updateBrothersTop(node, totalHeight)

View File

@ -56,6 +56,9 @@ class LogicalStructure extends Base {
}, 0) +
(len + 1) * this.getMarginY(layerIndex + 1)
: 0
// 如果存在概要,则和概要的高度取最大值
let generalizationNodeHeight = cur._node.checkHasGeneralization() ? cur._node._generalizationNodeHeight + this.getMarginY(layerIndex + 1) : 0
cur._node.childrenAreaHeight2 = Math.max(cur._node.childrenAreaHeight, generalizationNodeHeight)
},
true,
0
@ -99,9 +102,8 @@ class LogicalStructure extends Base {
}
// 判断子节点所占的高度之和是否大于该节点自身,大于则需要调整位置
let difference =
node.childrenAreaHeight -
this.getMarginY(layerIndex + 1) * 2 -
node.height
node.childrenAreaHeight2 -
this.getMarginY(layerIndex + 1) * 2 - node.height
if (difference > 0) {
this.updateBrothers(node, difference / 2)
}

View File

@ -90,6 +90,11 @@ class MindMap extends Base {
cur._node.rightChildrenAreaHeight =
rightChildrenAreaHeight +
(rightLen + 1) * this.getMarginY(layerIndex + 1)
// 如果存在概要,则和概要的高度取最大值
let generalizationNodeHeight = cur._node.checkHasGeneralization() ? cur._node._generalizationNodeHeight + this.getMarginY(layerIndex + 1) : 0
cur._node.leftChildrenAreaHeight2 = Math.max(cur._node.leftChildrenAreaHeight, generalizationNodeHeight)
cur._node.rightChildrenAreaHeight2 = Math.max(cur._node.rightChildrenAreaHeight, generalizationNodeHeight)
},
true,
0
@ -139,8 +144,8 @@ class MindMap extends Base {
}
// 判断子节点所占的高度之和是否大于该节点自身,大于则需要调整位置
let base = this.getMarginY(layerIndex + 1) * 2 + node.height
let leftDifference = node.leftChildrenAreaHeight - base
let rightDifference = node.rightChildrenAreaHeight - base
let leftDifference = node.leftChildrenAreaHeight2 - base
let rightDifference = node.rightChildrenAreaHeight2 - base
if (leftDifference > 0 || rightDifference > 0) {
this.updateBrothers(node, leftDifference / 2, rightDifference / 2)
}

View File

@ -57,6 +57,10 @@ class OrganizationStructure extends Base {
}, 0) +
(len + 1) * this.getMarginY(layerIndex + 1)
: 0
// 如果存在概要,则和概要的高度取最大值
let generalizationNodeWidth = cur._node.checkHasGeneralization() ? cur._node._generalizationNodeWidth + this.getMarginY(layerIndex + 1) : 0
cur._node.childrenAreaWidth2 = Math.max(cur._node.childrenAreaWidth, generalizationNodeWidth)
},
true,
0
@ -100,7 +104,7 @@ class OrganizationStructure extends Base {
}
// 判断子节点所占的宽度之和是否大于该节点自身,大于则需要调整位置
let difference =
node.childrenAreaWidth -
node.childrenAreaWidth2 -
this.getMarginY(layerIndex + 1) * 2 -
node.width
if (difference > 0) {

View File

@ -4,7 +4,8 @@ import {
getTextFromHtml,
imgToDataUrl,
parseDataUrl,
getImageSize
getImageSize,
isUndef
} from '../utils/index'
// 解析.xmind文件
@ -49,7 +50,7 @@ const transformXmind = async (content, files) => {
let walk = async (node, newNode) => {
newNode.data = {
// 节点内容
text: node.title
text: isUndef(node.title) ? '' : node.title
}
// 节点备注
if (node.notes) {
@ -146,9 +147,10 @@ const transformOldXmind = content => {
let walk = (node, newNode) => {
let nodeElements = node.elements
let nodeTitle = getItemByName(nodeElements, 'title')
nodeTitle = nodeTitle && nodeTitle.elements && nodeTitle.elements[0].text
newNode.data = {
// 节点内容
text: nodeTitle && nodeTitle.elements && nodeTitle.elements[0].text
text: isUndef(nodeTitle) ? '' : nodeTitle
}
try {
// 节点备注
@ -215,6 +217,7 @@ const transformToXmind = async (data, name) => {
let waitLoadImageList = []
let walk = async (node, newNode, isRoot) => {
let newData = {
id: node.data.uid,
structureClass: 'org.xmind.ui.logic.right',
title: getTextFromHtml(node.data.text), // 节点文本
children: {
@ -242,20 +245,20 @@ const transformToXmind = async (data, name) => {
}
// 图片
if (node.data.image) {
// 处理异步逻辑
let resolve = null
let promise = new Promise(_resolve => {
resolve = _resolve
})
waitLoadImageList.push(promise)
try {
// 处理异步逻辑
let resolve = null
let promise = new Promise(_resolve => {
resolve = _resolve
})
waitLoadImageList.push(promise)
let imgName = ''
let imgData = node.data.image
// 网络图片要先转换成data:url
if (/^https?:\/\//.test(node.data.image)) {
// base64之外的其他图片要先转换成data:url
if (!/^data:/.test(node.data.image)) {
imgData = await imgToDataUrl(node.data.image)
}
// 从data:url中解析出图片类型和base64
// 从data:url中解析出图片类型和ase64
let dataUrlRes = parseDataUrl(imgData)
imgName = 'image_' + imageList.length + '.' + dataUrlRes.type
imageList.push({

View File

@ -362,7 +362,7 @@ class AssociativeLine {
if (node.nodeData.data.isActive) {
this.mindMap.renderer.setNodeActive(node, false)
}
if (node === this.creatingStartNode || this.overlapNode) {
if (node.uid === this.creatingStartNode.uid || this.overlapNode) {
return
}
let { left, top, width, height } = node
@ -379,7 +379,7 @@ class AssociativeLine {
// 完成创建连接线
completeCreateLine(node) {
if (this.creatingStartNode === node) return
if (this.creatingStartNode.uid === node.uid) return
this.addLine(this.creatingStartNode, node)
if (this.overlapNode && this.overlapNode.nodeData.data.isActive) {
this.mindMap.renderer.setNodeActive(this.overlapNode, false)
@ -433,7 +433,7 @@ class AssociativeLine {
]
let associativeLinePoint = fromNode.nodeData.data.associativeLinePoint || []
// 记录关联的起始|结束坐标
associativeLinePoint[list.length - 1] = [{ startPoint, endPoint }]
associativeLinePoint[list.length - 1] = { startPoint, endPoint }
this.mindMap.execCommand('SET_NODE_DATA', fromNode, {
associativeLineTargets: list,
associativeLineTargetControlOffsets: offsetList,

View File

@ -13,6 +13,8 @@ class Drag extends Base {
// 复位
reset() {
// 当前画布节点列表
this.nodeList = []
// 当前拖拽节点
this.node = null
// 当前重叠节点
@ -68,6 +70,7 @@ class Drag extends Base {
this.isMousedown = true
this.mouseDownX = x
this.mouseDownY = y
this.nodeTreeToList()
})
this.mindMap.on('mousemove', e => {
if (this.mindMap.opt.readonly) {
@ -106,7 +109,9 @@ class Drag extends Base {
this.node.isDrag = false
this.node.show()
this.removeCloneNode()
let overlapNodeUid = this.overlapNode ? this.overlapNode.nodeData.data.uid : ''
let overlapNodeUid = this.overlapNode
? this.overlapNode.nodeData.data.uid
: ''
let prevNodeUid = this.prevNode ? this.prevNode.nodeData.data.uid : ''
let nextNodeUid = this.nextNode ? this.nextNode.nodeData.data.uid : ''
// 存在重叠子节点,则移动作为其子节点
@ -207,90 +212,47 @@ class Drag extends Base {
if (!this.drawTransform || !this.placeholder) {
return
}
const { nodeDragPlaceholderMaxSize } = this.mindMap.opt
let x = this.mouseMoveX
let y = this.mouseMoveY
this.overlapNode = null
this.prevNode = null
this.nextNode = null
this.placeholder.size(0, 0)
bfsWalk(this.mindMap.renderer.root, node => {
this.nodeList.forEach(node => {
if (node.nodeData.data.isActive) {
this.mindMap.renderer.setNodeActive(node, false)
}
if (node === this.node || this.node.isParent(node)) {
if (node.uid === this.node.uid) {
return
}
if (this.overlapNode || (this.prevNode && this.nextNode)) {
return
}
let nodeRect = this.getNodeRect(node)
let oneFourthHeight = nodeRect.height / 4
// 前一个和后一个节点
let checkList = node.parent ? node.parent.children.filter((item) => {
return item !== this.node
}) : []
let index = checkList.findIndex((item) => {
return item === node
})
let prevBrother = null
let nextBrother = null
if (index !== -1) {
if (index - 1 >= 0) {
prevBrother = checkList[index - 1]
}
if (index + 1 <= checkList.length - 1) {
nextBrother = checkList[index + 1]
}
}
// 和前一个兄弟节点的距离
let prevBrotherOffset = 0
if (prevBrother) {
let prevNodeRect = this.getNodeRect(prevBrother)
prevBrotherOffset = nodeRect.top - prevNodeRect.bottom
// 间距小于10就当它不存在
prevBrotherOffset = prevBrotherOffset >= this.minOffset ? prevBrotherOffset / 2 : 0
} else {
// 没有前一个兄弟节点那么假设和前一个节点的距离为20
prevBrotherOffset = this.minOffset
}
// 和后一个兄弟节点的距离
let nextBrotherOffset = 0
if (nextBrother) {
let nextNodeRect = this.getNodeRect(nextBrother)
nextBrotherOffset = nextNodeRect.top - nodeRect.bottom
nextBrotherOffset = nextBrotherOffset >= this.minOffset ? nextBrotherOffset / 2 : 0
} else {
nextBrotherOffset = this.minOffset
}
if (nodeRect.left <= x && nodeRect.right >= x) {
// 检测兄弟节点位置
if (!this.overlapNode && !this.prevNode && !this.nextNode && !node.isRoot) {
let checkIsPrevNode = nextBrotherOffset > 0 ? // 距离下一个兄弟节点的距离大于0
y > nodeRect.bottom && y <= (nodeRect.bottom + nextBrotherOffset) : // 那么在当前节点外底部判断
y >= nodeRect.bottom - oneFourthHeight && y <= nodeRect.bottom // 否则在当前节点内底部1/4区间判断
let checkIsNextNode = prevBrotherOffset > 0 ? // 距离上一个兄弟节点的距离大于0
y < nodeRect.top && y >= (nodeRect.top - prevBrotherOffset) : // 那么在当前节点外底部判断
y >= nodeRect.top && y <= nodeRect.top + oneFourthHeight
if (checkIsPrevNode) {
this.prevNode = node
let size = nextBrotherOffset > 0 ? Math.min(nextBrotherOffset, nodeDragPlaceholderMaxSize) : 5
this.placeholder.size(node.width, size).move(nodeRect.originLeft, nodeRect.originBottom)
} else if (checkIsNextNode) {
this.nextNode = node
let size = prevBrotherOffset > 0 ? Math.min(prevBrotherOffset, nodeDragPlaceholderMaxSize) : 5
this.placeholder.size(node.width, size).move(nodeRect.originLeft, nodeRect.originTop - size)
}
}
// 检测是否重叠
if (!this.overlapNode && !this.prevNode && !this.nextNode) {
if (
nodeRect.top + (prevBrotherOffset > 0 ? 0 : oneFourthHeight) <= y &&
nodeRect.bottom - (nextBrotherOffset > 0 ? 0 : oneFourthHeight) >= y
) {
this.overlapNode = node
}
}
switch (this.mindMap.opt.layout) {
case 'logicalStructure':
this.handleLogicalStructure(node)
break
case 'mindMap':
this.handleMindMap(node)
break
case 'organizationStructure':
this.handleOrganizationStructure(node)
break
case 'catalogOrganization':
this.handleCatalogOrganization(node)
break
case 'timeline':
this.handleTimeLine(node)
break
case 'timeline2':
this.handleTimeLine2(node)
break
case 'verticalTimeline':
this.handleLogicalStructure(node)
break
case 'fishbone':
this.handleFishbone(node)
break
default:
this.handleLogicalStructure(node)
}
})
if (this.overlapNode) {
@ -298,6 +260,299 @@ class Drag extends Base {
}
}
// 垂直方向比较
// isReverse是否反向
handleVerticalCheck(node, checkList, isReverse = false) {
let x = this.mouseMoveX
let y = this.mouseMoveY
let nodeRect = this.getNodeRect(node)
if (isReverse) {
checkList = checkList.reverse()
}
let oneFourthHeight = nodeRect.height / 4
let { prevBrotherOffset, nextBrotherOffset } =
this.getNodeDistanceToSiblingNode(checkList, node, nodeRect, 'v')
if (nodeRect.left <= x && nodeRect.right >= x) {
// 检测兄弟节点位置
if (
!this.overlapNode &&
!this.prevNode &&
!this.nextNode &&
!node.isRoot
) {
let checkIsPrevNode =
nextBrotherOffset > 0 // 距离下一个兄弟节点的距离大于0
? y > nodeRect.bottom && y <= nodeRect.bottom + nextBrotherOffset // 那么在当前节点外底部判断
: y >= nodeRect.bottom - oneFourthHeight && y <= nodeRect.bottom // 否则在当前节点内底部1/4区间判断
let checkIsNextNode =
prevBrotherOffset > 0 // 距离上一个兄弟节点的距离大于0
? y < nodeRect.top && y >= nodeRect.top - prevBrotherOffset // 那么在当前节点外底部判断
: y >= nodeRect.top && y <= nodeRect.top + oneFourthHeight
if (checkIsPrevNode) {
if (isReverse) {
this.nextNode = node
} else {
this.prevNode = node
}
let size = this.formatPlaceholderSize(nextBrotherOffset)
this.setPlaceholderRect(
node.width,
size,
nodeRect.originLeft,
nodeRect.originBottom
)
} else if (checkIsNextNode) {
if (isReverse) {
this.prevNode = node
} else {
this.nextNode = node
}
let size = this.formatPlaceholderSize(prevBrotherOffset)
this.setPlaceholderRect(
node.width,
size,
nodeRect.originLeft,
nodeRect.originTop - size
)
}
}
// 检测是否重叠
this.checkIsOverlap({
node,
dir: 'v',
prevBrotherOffset,
nextBrotherOffset,
size: oneFourthHeight,
pos: y,
nodeRect
})
}
}
// 水平方向比较
handleHorizontalCheck(node, checkList) {
let x = this.mouseMoveX
let y = this.mouseMoveY
let nodeRect = this.getNodeRect(node)
let oneFourthWidth = nodeRect.width / 4
let { prevBrotherOffset, nextBrotherOffset } =
this.getNodeDistanceToSiblingNode(checkList, node, nodeRect, 'h')
if (nodeRect.top <= y && nodeRect.bottom >= y) {
// 检测兄弟节点位置
if (
!this.overlapNode &&
!this.prevNode &&
!this.nextNode &&
!node.isRoot
) {
let checkIsPrevNode =
nextBrotherOffset > 0 // 距离下一个兄弟节点的距离大于0
? x < nodeRect.right + nextBrotherOffset && x >= nodeRect.right // 那么在当前节点外底部判断
: x <= nodeRect.right && x >= nodeRect.right - oneFourthWidth // 否则在当前节点内底部1/4区间判断
let checkIsNextNode =
prevBrotherOffset > 0 // 距离上一个兄弟节点的距离大于0
? x > nodeRect.left - prevBrotherOffset && x <= nodeRect.left // 那么在当前节点外底部判断
: x <= nodeRect.left + oneFourthWidth && x >= nodeRect.left
if (checkIsPrevNode) {
this.prevNode = node
let size = this.formatPlaceholderSize(nextBrotherOffset)
this.setPlaceholderRect(
size,
node.height,
nodeRect.originRight,
nodeRect.originTop
)
} else if (checkIsNextNode) {
this.nextNode = node
let size = this.formatPlaceholderSize(prevBrotherOffset)
this.setPlaceholderRect(
size,
node.height,
nodeRect.originLeft - size,
nodeRect.originTop
)
}
}
// 检测是否重叠
this.checkIsOverlap({
node,
dir: 'h',
prevBrotherOffset,
nextBrotherOffset,
size: oneFourthWidth,
pos: x,
nodeRect
})
}
}
// 获取节点距前一个和后一个节点的距离
getNodeDistanceToSiblingNode(checkList, node, nodeRect, dir) {
let dir1 = dir === 'v' ? 'top' : 'left'
let dir2 = dir === 'v' ? 'bottom' : 'right'
let index = checkList.findIndex(item => {
return item.uid === node.uid
})
let prevBrother = null
let nextBrother = null
if (index !== -1) {
if (index - 1 >= 0) {
prevBrother = checkList[index - 1]
}
if (index + 1 <= checkList.length - 1) {
nextBrother = checkList[index + 1]
}
}
// 和前一个兄弟节点的距离
let prevBrotherOffset = 0
if (prevBrother) {
let prevNodeRect = this.getNodeRect(prevBrother)
prevBrotherOffset = nodeRect[dir1] - prevNodeRect[dir2]
// 间距小于10就当它不存在
prevBrotherOffset =
prevBrotherOffset >= this.minOffset ? prevBrotherOffset / 2 : 0
} else {
// 没有前一个兄弟节点那么假设和前一个节点的距离为20
prevBrotherOffset = this.minOffset
}
// 和后一个兄弟节点的距离
let nextBrotherOffset = 0
if (nextBrother) {
let nextNodeRect = this.getNodeRect(nextBrother)
nextBrotherOffset = nextNodeRect[dir1] - nodeRect[dir2]
nextBrotherOffset =
nextBrotherOffset >= this.minOffset ? nextBrotherOffset / 2 : 0
} else {
nextBrotherOffset = this.minOffset
}
return {
prevBrotherOffset,
nextBrotherOffset
}
}
// 处理提示元素的大小
formatPlaceholderSize(size) {
const { nodeDragPlaceholderMaxSize } = this.mindMap.opt
return size > 0 ? Math.min(size, nodeDragPlaceholderMaxSize) : 5
}
// 设置提示元素的大小和位置
setPlaceholderRect(w, h, x, y) {
this.placeholder.size(w, h).move(x, y)
}
// 检测是否重叠
checkIsOverlap({
node,
dir,
prevBrotherOffset,
nextBrotherOffset,
size,
pos,
nodeRect
}) {
let dir1 = dir === 'v' ? 'top' : 'left'
let dir2 = dir === 'v' ? 'bottom' : 'right'
if (!this.overlapNode && !this.prevNode && !this.nextNode) {
if (
nodeRect[dir1] + (prevBrotherOffset > 0 ? 0 : size) <= pos &&
nodeRect[dir2] - (nextBrotherOffset > 0 ? 0 : size) >= pos
) {
this.overlapNode = node
}
}
}
// 处理逻辑结构图
handleLogicalStructure(node) {
const checkList = this.commonGetNodeCheckList(node)
this.handleVerticalCheck(node, checkList)
}
// 处理思维导图
handleMindMap(node) {
const checkList = node.parent
? node.parent.children.filter(item => {
let sameDir = true
if (node.layerIndex === 1) {
sameDir = item.dir === node.dir
}
return item !== this.node && sameDir
})
: []
this.handleVerticalCheck(node, checkList)
}
// 处理组织结构图
handleOrganizationStructure(node) {
const checkList = this.commonGetNodeCheckList(node)
this.handleHorizontalCheck(node, checkList)
}
// 处理目录组织图
handleCatalogOrganization(node) {
const checkList = this.commonGetNodeCheckList(node)
if (node.layerIndex === 1) {
this.handleHorizontalCheck(node, checkList)
} else {
this.handleVerticalCheck(node, checkList)
}
}
// 处理时间轴
handleTimeLine(node) {
let checkList = this.commonGetNodeCheckList(node)
if (node.layerIndex === 1) {
this.handleHorizontalCheck(node, checkList)
} else {
this.handleVerticalCheck(node, checkList)
}
}
// 处理时间轴2
handleTimeLine2(node) {
let checkList = this.commonGetNodeCheckList(node)
if (node.layerIndex === 1) {
this.handleHorizontalCheck(node, checkList)
} else {
// 处于上方的三级节点需要特殊处理,因为节点排列方向反向了
if (node.dir === 'top' && node.layerIndex === 2) {
this.handleVerticalCheck(node, checkList, true)
} else {
this.handleVerticalCheck(node, checkList)
}
}
}
// 处理鱼骨图
handleFishbone(node) {
let checkList = node.parent
? node.parent.children.filter(item => {
return item !== this.node && item.layerIndex > 1
})
: []
if (node.layerIndex === 1) {
this.handleHorizontalCheck(node, checkList)
} else {
// 处于上方的三级节点需要特殊处理,因为节点排列方向反向了
if (node.dir === 'top' && node.layerIndex === 2) {
this.handleVerticalCheck(node, checkList, true)
} else {
this.handleVerticalCheck(node, checkList)
}
}
}
// 获取节点的兄弟节点列表通用方法
commonGetNodeCheckList(node) {
return node.parent
? node.parent.children.filter(item => {
return item !== this.node
})
: []
}
// 计算节点的位置尺寸信息
getNodeRect(node) {
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
@ -305,6 +560,7 @@ class Drag extends Base {
let originLeft = left
let originTop = top
let originBottom = top + height
let originRight = left + width
let right = (left + width) * scaleX + translateX
let bottom = (top + height) * scaleY + translateY
left = left * scaleX + translateX
@ -318,9 +574,24 @@ class Drag extends Base {
bottom,
originLeft,
originTop,
originBottom
originBottom,
originRight
}
}
// 节点由树转换成数组,从子节点到根节点
nodeTreeToList() {
const list = []
bfsWalk(this.mindMap.renderer.root, node => {
if (!list[node.layerIndex]) {
list[node.layerIndex] = []
}
list[node.layerIndex].push(node)
})
this.nodeList = list.reduceRight((res, cur) => {
return [...res, ...cur]
}, [])
}
}
Drag.instanceName = 'drag'

View File

@ -94,7 +94,7 @@ class KeyboardNavigation {
// 遍历节点树
bfsWalk(this.mindMap.renderer.root, node => {
// 跳过当前聚焦的节点
if (node === currentActiveNode) return
if (node.uid === currentActiveNode.uid) return
// 当前遍历到的节点的位置信息
let rect = this.getNodeRect(node)
let { left, top, right, bottom } = rect
@ -131,7 +131,7 @@ class KeyboardNavigation {
checkNodeDis
}) {
bfsWalk(this.mindMap.renderer.root, node => {
if (node === currentActiveNode) return
if (node.uid === currentActiveNode.uid) return
let rect = this.getNodeRect(node)
let { left, top, right, bottom } = rect
let match = false
@ -173,7 +173,7 @@ class KeyboardNavigation {
let cX = (currentActiveNodeRect.right + currentActiveNodeRect.left) / 2
let cY = (currentActiveNodeRect.bottom + currentActiveNodeRect.top) / 2
bfsWalk(this.mindMap.renderer.root, node => {
if (node === currentActiveNode) return
if (node.uid === currentActiveNode.uid) return
let rect = this.getNodeRect(node)
let { left, top, right, bottom } = rect
// 遍历到的节点的中心点

View File

@ -49,7 +49,7 @@ class NodeImgAdjust {
// 如果当前正在拖动调整中那么直接返回
if (this.isMousedown || this.isAdjusted || this.mindMap.opt.readonly) return
// 如果在当前节点内移动,以及自定义元素已经是显示状态,那么直接返回
if (this.node === node && this.isShowHandleEl) return
if ((this.node && this.node.uid === node.uid) && this.isShowHandleEl) return
// 更新当前节点信息
this.node = node
this.img = img

View File

@ -1,4 +1,4 @@
import { nodeDataNoStylePropList } from '../constants/constant'
import { checkIsNodeStyleDataKey } from '../utils/index'
// 格式刷插件
class Painter {
@ -49,13 +49,13 @@ class Painter {
!this.isInPainter ||
!this.painterNode ||
!node ||
node === this.painterNode
node.uid === this.painterNode.uid
)
return
const style = {}
const painterNodeData = this.painterNode.nodeData.data
Object.keys(painterNodeData).forEach(key => {
if (!nodeDataNoStylePropList.includes(key)) {
if (checkIsNodeStyleDataKey(key)) {
style[key] = painterNodeData[key]
}
})

View File

@ -1,4 +1,5 @@
import { throttle } from '../utils/index'
import { CONSTANTS } from '../constants/constant'
// 滚动条插件
class Scrollbar {
@ -9,45 +10,13 @@ class Scrollbar {
width: 0, // 水平滚动条的容器宽度
height: 0 // 垂直滚动条的容器高度
}
// 思维导图实际高度
this.chartHeight = 0
this.chartWidth = 0
this.reset()
this.bindEvent()
}
// 绑定事件
bindEvent() {
this.onMousemove = this.onMousemove.bind(this)
this.onMouseup = this.onMouseup.bind(this)
this.onNodeTreeRenderEnd = this.onNodeTreeRenderEnd.bind(this)
this.onViewDataChange = throttle(this.onViewDataChange, 16, this) // 加个节流
this.mindMap.on('mousemove', this.onMousemove)
this.mindMap.on('mouseup', this.onMouseup)
this.mindMap.on('node_tree_render_end', this.onNodeTreeRenderEnd)
this.mindMap.on('view_data_change', this.onViewDataChange)
}
// 解绑事件
unBindEvent() {
this.mindMap.off('mousemove', this.onMousemove)
this.mindMap.off('mouseup', this.onMouseup)
this.mindMap.off('node_tree_render_end', this.onNodeTreeRenderEnd)
this.mindMap.off('view_data_change', this.onViewDataChange)
}
// 每次渲染后需要更新滚动条
onNodeTreeRenderEnd() {
this.emitEvent()
}
// 思维导图视图数据改变需要更新滚动条
onViewDataChange() {
this.emitEvent()
}
// 发送滚动条改变事件
emitEvent() {
this.mindMap.emit('scrollbar_change')
}
// 复位数据
reset() {
// 当前拖拽的滚动条类型
@ -57,13 +26,41 @@ class Scrollbar {
x: 0,
y: 0
}
this.startViewPos = {
x: 0,
y: 0
}
// 思维导图实际高度
this.chartHeight = 0
this.chartWidth = 0
// 鼠标按下时,滚动条位置
this.mousedownScrollbarPos = 0
}
// 绑定事件
bindEvent() {
this.onMousemove = this.onMousemove.bind(this)
this.onMouseup = this.onMouseup.bind(this)
this.updateScrollbar = this.updateScrollbar.bind(this)
this.updateScrollbar = throttle(this.updateScrollbar, 16, this) // 加个节流
this.mindMap.on('mousemove', this.onMousemove)
this.mindMap.on('mouseup', this.onMouseup)
this.mindMap.on('node_tree_render_end', this.updateScrollbar)
this.mindMap.on('view_data_change', this.updateScrollbar)
}
// 解绑事件
unBindEvent() {
this.mindMap.off('mousemove', this.onMousemove)
this.mindMap.off('mouseup', this.onMouseup)
this.mindMap.off('node_tree_render_end', this.updateScrollbar)
this.mindMap.off('view_data_change', this.updateScrollbar)
}
// 渲染后、数据改变需要更新滚动条
updateScrollbar() {
// 当前正在拖拽滚动条时不需要更新
if (this.isMousedown) return
const res = this.calculationScrollbar()
this.emitEvent(res)
}
// 发送滚动条改变事件
emitEvent(data) {
this.mindMap.emit('scrollbar_change', data)
}
// 设置滚动条容器的大小,指滚动条容器的大小,对于水平滚动条,即宽度,对于垂直滚动条,即高度
@ -78,9 +75,7 @@ class Scrollbar {
// 减去画布距离浏览器窗口左上角的距离
const elRect = this.mindMap.elRect
rect.x -= elRect.left
rect.x2 -= elRect.left
rect.y -= elRect.top
rect.y2 -= elRect.top
// 垂直滚动条
const canvasHeight = this.mindMap.height // 画布高度
@ -129,46 +124,129 @@ class Scrollbar {
return res
}
// 滚动条鼠标按下事件处理函数
onMousedown(e, type) {
e.preventDefault()
e.stopPropagation()
this.currentScrollType = type
this.isMousedown = true
this.mousedownPos = {
x: e.clientX,
y: e.clientY
}
// 保存视图当前的偏移量
let transformData = this.mindMap.view.getTransformData()
this.startViewPos = {
x: transformData.state.x,
y: transformData.state.y
// 保存滚动条当前的位置
const styles = window.getComputedStyle(e.target)
if (type === CONSTANTS.SCROLL_BAR_DIR.VERTICAL) {
this.mousedownScrollbarPos = Number.parseFloat(styles.top)
} else {
this.mousedownScrollbarPos = Number.parseFloat(styles.left)
}
}
// 鼠标移动事件处理函数
onMousemove(e) {
if (!this.isMousedown) {
return
}
if (this.currentScrollType === 'vertical') {
const oy = e.clientY - this.mousedownPos.y
const oyPercentage = -oy / this.scrollbarWrapSize.height
const oyPx = oyPercentage * this.chartHeight
// 在视图最初偏移量上累加更新量
this.mindMap.view.translateYTo(oyPx + this.startViewPos.y)
e.preventDefault()
e.stopPropagation()
if (this.currentScrollType === CONSTANTS.SCROLL_BAR_DIR.VERTICAL) {
const oy = e.clientY - this.mousedownPos.y + this.mousedownScrollbarPos
this.updateMindMapView(CONSTANTS.SCROLL_BAR_DIR.VERTICAL, oy)
} else {
const ox = e.clientX - this.mousedownPos.x
const oxPercentage = -ox / this.scrollbarWrapSize.width
const oxPx = oxPercentage * this.chartWidth
// 在视图最初偏移量上累加更新量
this.mindMap.view.translateXTo(oxPx + this.startViewPos.x)
const ox = e.clientX - this.mousedownPos.x + this.mousedownScrollbarPos
this.updateMindMapView(CONSTANTS.SCROLL_BAR_DIR.HORIZONTAL, ox)
}
}
// 鼠标松开事件处理函数
onMouseup() {
this.isMousedown = false
this.reset()
}
// 更新视图
updateMindMapView(type, offset) {
const scrollbarData = this.calculationScrollbar()
const t = this.mindMap.draw.transform()
const drawRect = this.mindMap.draw.rbox()
const rootRect = this.mindMap.renderer.root.group.rbox()
if (type === CONSTANTS.SCROLL_BAR_DIR.VERTICAL) {
// 滚动条新位置
let oy = offset
// 判断是否达到首尾
if (oy <= 0) {
oy = 0
}
let max =
((100 - scrollbarData.vertical.height) / 100) *
this.scrollbarWrapSize.height
if (oy >= max) {
oy = max
}
// 转换成百分比
const oyPercentage = (oy / this.scrollbarWrapSize.height) * 100
// 转换成相对于图形高度的距离
const oyPx = (-oyPercentage / 100) * this.chartHeight
// 节点中心点到图形最上方的距离
const yOffset = rootRect.y - drawRect.y
// 内边距
const paddingY = this.mindMap.height / 2
// 图形新位置
let chartTop = oyPx + yOffset - paddingY * t.scaleY + paddingY
this.mindMap.view.translateYTo(chartTop)
this.emitEvent({
horizontal: scrollbarData.horizontal,
vertical: {
top: oyPercentage,
height: scrollbarData.vertical.height
}
})
} else {
// 滚动条新位置
let ox = offset
// 判断是否达到首尾
if (ox <= 0) {
ox = 0
}
let max =
((100 - scrollbarData.horizontal.width) / 100) *
this.scrollbarWrapSize.width
if (ox >= max) {
ox = max
}
// 转换成百分比
const oxPercentage = (ox / this.scrollbarWrapSize.width) * 100
// 转换成相对于图形高度的距离
const oxPx = (-oxPercentage / 100) * this.chartWidth
// 节点中心点到图形最左边的距离
const xOffset = rootRect.x - drawRect.x
// 内边距
const paddingX = this.mindMap.width / 2
// 图形新位置
let chartLeft = oxPx + xOffset - paddingX * t.scaleX + paddingX
this.mindMap.view.translateXTo(chartLeft)
this.emitEvent({
vertical: scrollbarData.vertical,
horizontal: {
left: oxPercentage,
width: scrollbarData.horizontal.width
}
})
}
}
// 滚动条的点击事件
onClick(e, type) {
let offset = 0
if (type === CONSTANTS.SCROLL_BAR_DIR.VERTICAL) {
offset = e.clientY - e.currentTarget.getBoundingClientRect().top
} else {
offset = e.clientX - e.currentTarget.getBoundingClientRect().left
}
this.updateMindMapView(type, offset)
}
// 插件被卸载前做的事情
beforePluginDestroy() {
this.unBindEvent()

View File

@ -55,7 +55,7 @@ class Select {
return
}
clearTimeout(this.autoMoveTimer)
this.onMove(x, y)
this.onMove(e.clientX, e.clientY)
})
this.mindMap.on('mouseup', () => {
if (this.mindMap.opt.readonly) {

View File

@ -1,4 +1,5 @@
import { v4 as uuidv4 } from 'uuid'
import { nodeDataNoStylePropList } from '../constants/constant'
// 深度优先遍历树
export const walk = (
@ -656,4 +657,15 @@ export const getObjectChangedProps = (oldObject, newObject) => {
}
})
return res
}
// 判断一个字段是否是节点数据中的样式字段
export const checkIsNodeStyleDataKey = (key) => {
// 用户自定义字段
if (/^_/.test(key)) return false
// 不在节点非样式字段列表里,那么就是样式字段
if (!nodeDataNoStylePropList.includes(key)) {
return true
}
return false
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -39,7 +39,7 @@ let APIList = [
'markdown',
'utils'
]
let helpList = new Array(3).fill(0).map((_, index) => {
let helpList = new Array(5).fill(0).map((_, index) => {
return 'help' + (index + 1)
})

View File

@ -25,7 +25,7 @@ const mindMap = new MindMap({
| Field Name | Type | Default Value | Description | Required |
| -------------------------------- | ------- | ---------------- | ------------------------------------------------------------ | -------- |
| el | Element | | Container element, must be a DOM element | Yes |
| data | Object | {} | Mind map data, refer to: [exampleData.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js) | |
| data | Object | {} | Mind map data, Please refer to the introduction of 【Data structure】 below | |
| layout | String | logicalStructure | Layout type, options: logicalStructure (logical structure diagram), mindMap (mind map), catalogOrganization (catalog organization diagram), organizationStructure (organization structure diagram)、timelinev0.5.4+, timeline、timeline2v0.5.4+, up down alternating timeline、fishbonev0.5.4+, fishbone diagram | |
| fishboneDegv0.5.4+ | Number | 45 | Set the diagonal angle of the fishbone structure diagram | |
| theme | String | default | Theme, options: default, classic, minions, pinkGrape, mint, gold, vitalityOrange, greenLeaf, dark2, skyGreen, classic2, classic3, classic4(v0.2.0+), classicGreen, classicBlue, blueSky, brainImpairedPink, dark, earthYellow, freshGreen, freshRed, romanticPurple, simpleBlack(v0.5.4+), courseGreen(v0.5.4+), coffee(v0.5.4+), redSpirit(v0.5.4+), blackHumour(v0.5.4+), lateNightOffice(v0.5.4+), blackGold(v0.5.4+)、、avocado(v.5.10-fix.2+)、autumn(v.5.10-fix.2+)、orangeJuice(v.5.10-fix.2+) | |
@ -88,6 +88,47 @@ const mindMap = new MindMap({
| hoverRectPaddingv0.7.0+ | Number | 2 | The distance between the node mouse hover and the displayed rectangular border when activated and the node content | |
| selectTextOnEnterEditTextv0.7.0+ | Boolean | true | Is the text selected by default when double-clicking a node to enter node text editing? By default, it will only be selected when creating a new node | |
### Data structure
The basic data structure is as follows:
```js
{
data: {
text: '', // The text of the node can be rich text, which is in HTML format. In this case, richText should be set to true
richText: false, // Is the text of the node in rich text mode
expand: true, // Whether the node is expanded
uid: '',// The unique ID of the node, which may not be passed, will be generated internally
icon: [], // The format of the icon can be found in the "插入和扩展节点图标" section of the tutorial
image: '', // URL of the image
imageTitle: '', // The title of the image can be blank
imageSize: { // The size of the image
width: 100, // The width of the image, mandatory
height: 100, // The height of the image is mandatory
custom: false // If set to true, the display size of the image is not controlled by the theme, and is based on imageSize.width and imageSize.height
},
hyperlink: '', // Hyperlink address
hyperlinkTitle: '', // Title of hyperlink
note: '', // Content of remarks
tag: [], // Tag list
generalization: {// The summary of the node, if there is no summary, the generalization can be set to null
text: ''// Summary Text
},
associativeLineTargets: [''],// If there are associated lines, then it is the uid list of the target node
associativeLineText: '',// Association Line Text
// ...For other style fields, please refer to the topic
},
children [// Child nodes, with consistent structure and root nodes
{
data: {},
children: []
}
]
}
```
If you want to add custom fields, you can add them to the same level as 'data' and 'children'. If you want to add them to the 'data' object, please use the `_` Name your custom field at the beginning, and it will be used internally to determine whether it is a custom field.
### Watermark config
| Field Name | Type | Default Value | Description |

View File

@ -39,7 +39,7 @@
<td>data</td>
<td>Object</td>
<td>{}</td>
<td>Mind map data, refer to: <a href="https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js">exampleData.js</a></td>
<td>Mind map data, Please refer to the introduction of Data structure below</td>
<td></td>
</tr>
<tr>
@ -471,6 +471,42 @@
</tr>
</tbody>
</table>
<h3>Data structure</h3>
<p>The basic data structure is as follows:</p>
<pre class="hljs"><code>{
<span class="hljs-attr">data</span>: {
<span class="hljs-attr">text</span>: <span class="hljs-string">&#x27;&#x27;</span>, <span class="hljs-comment">// The text of the node can be rich text, which is in HTML format. In this case, richText should be set to true</span>
<span class="hljs-attr">richText</span>: <span class="hljs-literal">false</span>, <span class="hljs-comment">// Is the text of the node in rich text mode</span>
<span class="hljs-attr">expand</span>: <span class="hljs-literal">true</span>, <span class="hljs-comment">// Whether the node is expanded</span>
<span class="hljs-attr">uid</span>: <span class="hljs-string">&#x27;&#x27;</span>,<span class="hljs-comment">// The unique ID of the node, which may not be passed, will be generated internally</span>
<span class="hljs-attr">icon</span>: [], <span class="hljs-comment">// The format of the icon can be found in the &quot;&quot; section of the tutorial</span>
<span class="hljs-attr">image</span>: <span class="hljs-string">&#x27;&#x27;</span>, <span class="hljs-comment">// URL of the image</span>
<span class="hljs-attr">imageTitle</span>: <span class="hljs-string">&#x27;&#x27;</span>, <span class="hljs-comment">// The title of the image can be blank</span>
<span class="hljs-attr">imageSize</span>: { <span class="hljs-comment">// The size of the image</span>
<span class="hljs-attr">width</span>: <span class="hljs-number">100</span>, <span class="hljs-comment">// The width of the image, mandatory</span>
<span class="hljs-attr">height</span>: <span class="hljs-number">100</span>, <span class="hljs-comment">// The height of the image is mandatory</span>
<span class="hljs-attr">custom</span>: <span class="hljs-literal">false</span> <span class="hljs-comment">// If set to true, the display size of the image is not controlled by the theme, and is based on imageSize.width and imageSize.height</span>
},
<span class="hljs-attr">hyperlink</span>: <span class="hljs-string">&#x27;&#x27;</span>, <span class="hljs-comment">// Hyperlink address</span>
<span class="hljs-attr">hyperlinkTitle</span>: <span class="hljs-string">&#x27;&#x27;</span>, <span class="hljs-comment">// Title of hyperlink</span>
<span class="hljs-attr">note</span>: <span class="hljs-string">&#x27;&#x27;</span>, <span class="hljs-comment">// Content of remarks</span>
<span class="hljs-attr">tag</span>: [], <span class="hljs-comment">// Tag list</span>
<span class="hljs-attr">generalization</span>: {<span class="hljs-comment">// The summary of the node, if there is no summary, the generalization can be set to null</span>
<span class="hljs-attr">text</span>: <span class="hljs-string">&#x27;&#x27;</span><span class="hljs-comment">// Summary Text</span>
},
<span class="hljs-attr">associativeLineTargets</span>: [<span class="hljs-string">&#x27;&#x27;</span>],<span class="hljs-comment">// If there are associated lines, then it is the uid list of the target node</span>
<span class="hljs-attr">associativeLineText</span>: <span class="hljs-string">&#x27;&#x27;</span>,<span class="hljs-comment">// Association Line Text</span>
<span class="hljs-comment">// ...For other style fields, please refer to the topic</span>
},
children [<span class="hljs-comment">// Child nodes, with consistent structure and root nodes</span>
{
<span class="hljs-attr">data</span>: {},
<span class="hljs-attr">children</span>: []
}
]
}
</code></pre>
<p>If you want to add custom fields, you can add them to the same level as 'data' and 'children'. If you want to add them to the 'data' object, please use the <code>_</code> Name your custom field at the beginning, and it will be used internally to determine whether it is a custom field.</p>
<h3>Watermark config</h3>
<table>
<thead>

View File

@ -63,8 +63,6 @@ However, this requires backend support, as our application is a single page clie
## Docker
## Docker
> Thank you very much [水车](https://github.com/shuiche-it), This section is written by him, and the corresponding Docker package is also maintained by him.
Install directly from Docker Hub:
@ -79,6 +77,8 @@ After the installation is completed, check the container's running status throug
Open 127.0.0.1:8081 in the browser to use the Web mind map function.
[在群晖上以 Docker 方式安装](https://laosu.gq/2023/09/02/%E5%BC%BA%E5%A4%A7%E7%9A%84%E6%80%9D%E7%BB%B4%E5%AF%BC%E5%9B%BE%E5%BA%93SimpleMindMap/)
## Docking with one's own storage services
The application data is stored locally in the browser by default, and the local storage capacity of the browser is relatively small, so it is easy to trigger restrictions when inserting more images in the mind map. Therefore, a better choice is to dock with your own storage service, which usually has two ways:

View File

@ -39,7 +39,6 @@ npm link simple-mind-map
</code></pre>
<p>However, this requires backend support, as our application is a single page client application. If the backend is not properly configured, users will return 404 when accessing sub routes directly in the browser. Therefore, you need to add a candidate resource on the server that covers all situations: if the 'URL' cannot match any static resources, the same 'index. html' page should be returned.</p>
<h2>Docker</h2>
<h2>Docker</h2>
<blockquote>
<p>Thank you very much <a href="https://github.com/shuiche-it">水车</a>, This section is written by him, and the corresponding Docker package is also maintained by him.</p>
</blockquote>
@ -49,6 +48,7 @@ npm link simple-mind-map
<p>Mindmap has activated port 8080 as the web service entry point in the container. When running the container through Docker, it is necessary to specify a local mapping port. In the above case, we mapped the local port 8081 to the container port 8080.</p>
<p>After the installation is completed, check the container's running status through 'Docker PS'.</p>
<p>Open 127.0.0.1:8081 in the browser to use the Web mind map function.</p>
<p><a href="https://laosu.gq/2023/09/02/%E5%BC%BA%E5%A4%A7%E7%9A%84%E6%80%9D%E7%BB%B4%E5%AF%BC%E5%9B%BE%E5%BA%93SimpleMindMap/">在群晖上以 Docker 方式安装</a></p>
<h2>Docking with one's own storage services</h2>
<p>The application data is stored locally in the browser by default, and the local storage capacity of the browser is relatively small, so it is easy to trigger restrictions when inserting more images in the mind map. Therefore, a better choice is to dock with your own storage service, which usually has two ways:</p>
<h3>The first</h3>

View File

@ -192,4 +192,12 @@ Open source is not easy. If this project is helpful to you, you can invite the a
<img src="../../../../assets/avatar/南风.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>南风</p>
</div>
<div style="display: flex; flex-direction: column; align-items: center; width: fit-content; margin: 5px;">
<img src="../../../../assets/avatar/蜉蝣撼大叔.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>蜉蝣撼大叔</p>
</div>
<div style="display: flex; flex-direction: column; align-items: center; width: fit-content; margin: 5px;">
<img src="../../../../assets/avatar/乙.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p></p>
</div>
</div>

View File

@ -151,6 +151,14 @@ full screen, support mini map</li>
<img src="../../../../assets/avatar/南风.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>南风</p>
</div>
<div style="display: flex; flex-direction: column; align-items: center; width: fit-content; margin: 5px;">
<img src="../../../../assets/avatar/蜉蝣撼大叔.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>蜉蝣撼大叔</p>
</div>
<div style="display: flex; flex-direction: column; align-items: center; width: fit-content; margin: 5px;">
<img src="../../../../assets/avatar/乙.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p></p>
</div>
</div>
</div>
</template>

View File

@ -167,4 +167,14 @@ module. If you need it, you can try using other libraries to parse `xml` to
### Error `Getting bbox of element "text" is not possible: TypeError: Cannot read properties of undefined (reading 'apply')`
The reason is that the installed version of `@svgdotjs/svg.js` is too high. You can manually reduce it to the version of `3.0.16`.
The reason is that the installed version of `@svgdotjs/svg.js` is too high. You can manually reduce it to the version of `3.0.16`.
### TypeError: Cannot read properties of undefined (reading 'prototype') at sax.js:222:46
The following configurations can be added to the packaging configuration file:
```js
resolve: { alias: { stream: "stream-browserify" } }
```
Different packaging tools may have different specific configurations, with the principle of excluding 'stream' dependencies.

View File

@ -116,6 +116,11 @@ module. If you need it, you can try using other libraries to parse <code>xml</co
<code>json</code>.</p>
<h3>Error <code>Getting bbox of element &quot;text&quot; is not possible: TypeError: Cannot read properties of undefined (reading 'apply')</code></h3>
<p>The reason is that the installed version of <code>@svgdotjs/svg.js</code> is too high. You can manually reduce it to the version of <code>3.0.16</code>.</p>
<h3>TypeError: Cannot read properties of undefined (reading 'prototype') at sax.js:222:46</h3>
<p>The following configurations can be added to the packaging configuration file:</p>
<pre class="hljs"><code>resolve: { <span class="hljs-attr">alias</span>: { <span class="hljs-attr">stream</span>: <span class="hljs-string">&quot;stream-browserify&quot;</span> } }
</code></pre>
<p>Different packaging tools may have different specific configurations, with the principle of excluding 'stream' dependencies.</p>
</div>
</template>

View File

@ -58,7 +58,9 @@ export default [
{ path: 'scrollbar', title: 'Scrollbar插件' },
{ path: 'help1', title: '概要/关联线' },
{ path: 'help2', title: '客户端' },
{ path: 'help3', title: '打开预览在线文件' }
{ path: 'help3', title: '打开预览在线文件' },
{ path: 'help4', title: '复制粘贴' },
{ path: 'help5', title: '导出' }
]
},
{

View File

@ -25,7 +25,7 @@ const mindMap = new MindMap({
| 字段名称 | 类型 | 默认值 | 描述 | 是否必填 |
| -------------------------------- | ------- | ---------------- | ------------------------------------------------------------ | -------- |
| el | Element | | 容器元素必须为DOM元素 | 是 |
| data | Object | {} | 思维导图数据,可参考[exampleData.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js) | |
| data | Object | {} | 思维导图数据,可参考下方【数据结构】介绍 | |
| layout | String | logicalStructure | 布局类型可选列表logicalStructure逻辑结构图、mindMap思维导图、catalogOrganization目录组织图、organizationStructure组织结构图、timelinev0.5.4+时间轴、timeline2v0.5.4+上下交替型时间轴、fishbonev0.5.4+,鱼骨图) | |
| fishboneDegv0.5.4+ | Number | 45 | 设置鱼骨结构图的斜线角度 | |
| theme | String | default | 主题可选列表default默认、classic脑图经典、minions小黄人、pinkGrape粉红葡萄、mint薄荷、gold金色vip、vitalityOrange活力橙、greenLeaf绿叶、dark2暗色2、skyGreen天清绿、classic2脑图经典2、classic3脑图经典3、classic4脑图经典4v0.2.0+、classicGreen经典绿、classicBlue经典蓝、blueSky天空蓝、brainImpairedPink脑残粉、dark暗色、earthYellow泥土黄、freshGreen清新绿、freshRed清新红、romanticPurple浪漫紫、simpleBlackv0.5.4+简约黑、courseGreenv0.5.4+课程绿、coffeev0.5.4+咖啡、redSpiritv0.5.4+红色精神、blackHumourv0.5.4+黑色幽默、lateNightOfficev0.5.4+深夜办公室、blackGoldv0.5.4+黑金、avocadov.5.10-fix.2+牛油果、autumnv.5.10-fix.2+秋天、orangeJuicev.5.10-fix.2+橙汁) | |
@ -88,6 +88,47 @@ const mindMap = new MindMap({
| hoverRectPaddingv0.7.0+ | Number | 2 | 节点鼠标hover和激活时显示的矩形边框距节点内容的距离 | |
| selectTextOnEnterEditTextv0.7.0+ | Boolean | true | 双击节点进入节点文本编辑时是否默认选中文本,默认只在创建新节点时会选中 | |
### 数据结构
基本的数据结构如下:
```js
{
data: {
text: '', // 节点的文本可以是富文本也就是html格式的此时richText要设为true
richText: false, // 节点的文本是否是富文本模式
expand: true, // 节点是否展开
uid: '',// 节点唯一的id可不传内部会生成
icon: [], // 图标,格式可参考教程里的【插入和扩展节点图标】章节
image: '', // 图片的url
imageTitle: '', // 图片的标题,可为空
imageSize: { // 图片的尺寸
width: 100, // 图片的宽度,必传
height: 100, // 图片的高度,必传
custom: false // 如果设为true图片的显示大小不受主题控制以imageSize.width和imageSize.height为准
},
hyperlink: '', // 超链接地址
hyperlinkTitle: '', // 超链接的标题
note: '', // 备注的内容
tag: [], // 标签列表
generalization: {// 节点的概要如果没有概要generalization设为null即可
text: ''// 概要的文本
},
associativeLineTargets: [''],// 如果存在关联线那么为目标节点的uid列表
associativeLineText: '',// 关联线文本
// ...其他样式字段,可以参考主题
},
children [// 子节点,结构和根节点一致
{
data: {},
children: []
}
]
}
```
如果你要添加自定义的字段,可以添加到`data`、`children`同级,如果你要添加到`data`对象里,那么请使用`_`开头来命名你的自定义字段,内部会通过这个来判断是否是自定义字段。
### 水印配置
| 字段名称 | 类型 | 默认值 | 描述 |

View File

@ -39,7 +39,7 @@
<td>data</td>
<td>Object</td>
<td>{}</td>
<td>思维导图数据可参考<a href="https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js">exampleData.js</a></td>
<td>思维导图数据可参考下方数据结构介绍</td>
<td></td>
</tr>
<tr>
@ -471,6 +471,42 @@
</tr>
</tbody>
</table>
<h3>数据结构</h3>
<p>基本的数据结构如下</p>
<pre class="hljs"><code>{
<span class="hljs-attr">data</span>: {
<span class="hljs-attr">text</span>: <span class="hljs-string">&#x27;&#x27;</span>, <span class="hljs-comment">// htmlrichTexttrue</span>
<span class="hljs-attr">richText</span>: <span class="hljs-literal">false</span>, <span class="hljs-comment">// </span>
<span class="hljs-attr">expand</span>: <span class="hljs-literal">true</span>, <span class="hljs-comment">// </span>
<span class="hljs-attr">uid</span>: <span class="hljs-string">&#x27;&#x27;</span>,<span class="hljs-comment">// id</span>
<span class="hljs-attr">icon</span>: [], <span class="hljs-comment">// </span>
<span class="hljs-attr">image</span>: <span class="hljs-string">&#x27;&#x27;</span>, <span class="hljs-comment">// url</span>
<span class="hljs-attr">imageTitle</span>: <span class="hljs-string">&#x27;&#x27;</span>, <span class="hljs-comment">// </span>
<span class="hljs-attr">imageSize</span>: { <span class="hljs-comment">// </span>
<span class="hljs-attr">width</span>: <span class="hljs-number">100</span>, <span class="hljs-comment">// </span>
<span class="hljs-attr">height</span>: <span class="hljs-number">100</span>, <span class="hljs-comment">// </span>
<span class="hljs-attr">custom</span>: <span class="hljs-literal">false</span> <span class="hljs-comment">// trueimageSize.widthimageSize.height</span>
},
<span class="hljs-attr">hyperlink</span>: <span class="hljs-string">&#x27;&#x27;</span>, <span class="hljs-comment">// </span>
<span class="hljs-attr">hyperlinkTitle</span>: <span class="hljs-string">&#x27;&#x27;</span>, <span class="hljs-comment">// </span>
<span class="hljs-attr">note</span>: <span class="hljs-string">&#x27;&#x27;</span>, <span class="hljs-comment">// </span>
<span class="hljs-attr">tag</span>: [], <span class="hljs-comment">// </span>
<span class="hljs-attr">generalization</span>: {<span class="hljs-comment">// generalizationnull</span>
<span class="hljs-attr">text</span>: <span class="hljs-string">&#x27;&#x27;</span><span class="hljs-comment">// </span>
},
<span class="hljs-attr">associativeLineTargets</span>: [<span class="hljs-string">&#x27;&#x27;</span>],<span class="hljs-comment">// 线uid</span>
<span class="hljs-attr">associativeLineText</span>: <span class="hljs-string">&#x27;&#x27;</span>,<span class="hljs-comment">// 线</span>
<span class="hljs-comment">// ...</span>
},
children [<span class="hljs-comment">// </span>
{
<span class="hljs-attr">data</span>: {},
<span class="hljs-attr">children</span>: []
}
]
}
</code></pre>
<p>如果你要添加自定义的字段可以添加到<code>data</code><code>children</code>同级如果你要添加到<code>data</code>对象里那么请使用<code>_</code>开头来命名你的自定义字段内部会通过这个来判断是否是自定义字段</p>
<h3>水印配置</h3>
<table>
<thead>

View File

@ -77,6 +77,8 @@ mind-map在容器中启动了8080端口作为web服务入口通过docker运
浏览器打开 127.0.0.1:8081 即可使用Web 思维导图功能。
[在群晖上以 Docker 方式安装](https://laosu.gq/2023/09/02/%E5%BC%BA%E5%A4%A7%E7%9A%84%E6%80%9D%E7%BB%B4%E5%AF%BC%E5%9B%BE%E5%BA%93SimpleMindMap/)
## 对接自己的存储服务
应用数据默认存储在浏览器本地,浏览器本地存储容量是比较小的,所以当在思维导图中插入更多图片后很容易触发限制,所以更好的选择是对接你自己的存储服务,这通常有两种方式:

View File

@ -48,6 +48,7 @@ npm link simple-mind-map
<p>mind-map在容器中启动了8080端口作为web服务入口通过docker运行容器时需要指定本地映射端口上面案例中我们通过本地的8081端口映射到容器端口8080</p>
<p>安装完成后通过 <code>docker ps</code> 查看容器运行状态</p>
<p>浏览器打开 127.0.0.1:8081 即可使用Web 思维导图功能</p>
<p><a href="https://laosu.gq/2023/09/02/%E5%BC%BA%E5%A4%A7%E7%9A%84%E6%80%9D%E7%BB%B4%E5%AF%BC%E5%9B%BE%E5%BA%93SimpleMindMap/">在群晖上以 Docker 方式安装</a></p>
<h2>对接自己的存储服务</h2>
<p>应用数据默认存储在浏览器本地浏览器本地存储容量是比较小的所以当在思维导图中插入更多图片后很容易触发限制所以更好的选择是对接你自己的存储服务这通常有两种方式</p>
<h3>第一种</h3>

View File

@ -0,0 +1,23 @@
# 复制粘贴
## 复制粘贴画布内的节点
你可以通过选中某个节点,然后按`ctrl`+`c`快捷键来复制节点,如果选中多个节点,目前只支持复制第一个节点,然后点击你要复制到的节点,按`ctrl`+`v`快捷键粘贴即可。
除了快捷键,也可以通过右键菜单来操作。
## 支持粘贴剪贴板中的数据
当你选中某个节点后,直接按`ctrl`+`v`快捷键时,如果你的剪贴板中有数据,会粘贴你剪贴板中的数据,目前支持文本和图片两种格式,如果你的剪贴板中有图片,那么会将图片粘贴到你选中的节点里,如果存在文本,那么会以该文件创建一个子节点。
如果你的剪贴板中存在数据,又在画布里复制了节点数据,那么是以谁最后改变了为准。
## 支持跨浏览器标签、跨浏览器复制
如果你在多个浏览器里打开了思维导图编辑页面,那么可以复制一个页面的节点数据到另一个页面进行粘贴。
在客户端场景下打开了多个编辑页面时比较常用。
## 支持粘贴知犀思维导图数据
目前支持复制知犀思维导图的节点数据粘贴到本思维导图里。

View File

@ -0,0 +1,27 @@
<template>
<div>
<h1>复制粘贴</h1>
<h2>复制粘贴画布内的节点</h2>
<p>你可以通过选中某个节点然后按<code>ctrl</code>+<code>c</code>快捷键来复制节点如果选中多个节点目前只支持复制第一个节点然后点击你要复制到的节点<code>ctrl</code>+<code>v</code>快捷键粘贴即可</p>
<p>除了快捷键也可以通过右键菜单来操作</p>
<h2>支持粘贴剪贴板中的数据</h2>
<p>当你选中某个节点后直接按<code>ctrl</code>+<code>v</code>快捷键时如果你的剪贴板中有数据会粘贴你剪贴板中的数据目前支持文本和图片两种格式如果你的剪贴板中有图片那么会将图片粘贴到你选中的节点里如果存在文本那么会以该文件创建一个子节点</p>
<p>如果你的剪贴板中存在数据又在画布里复制了节点数据那么是以谁最后改变了为准</p>
<h2>支持跨浏览器标签跨浏览器复制</h2>
<p>如果你在多个浏览器里打开了思维导图编辑页面那么可以复制一个页面的节点数据到另一个页面进行粘贴</p>
<p>在客户端场景下打开了多个编辑页面时比较常用</p>
<h2>支持粘贴知犀思维导图数据</h2>
<p>目前支持复制知犀思维导图的节点数据粘贴到本思维导图里</p>
</div>
</template>
<script>
export default {
}
</script>
<style>
</style>

View File

@ -0,0 +1,5 @@
# 导出
## 导出为xmind
导出的xmind文件无法在xmind8及以下版本打开请使用最新版xmind软件。

View File

@ -0,0 +1,18 @@
<template>
<div>
<h1>导出</h1>
<h2>导出为xmind</h2>
<p>导出的xmind文件无法在xmind8及以下版本打开请使用最新版xmind软件</p>
</div>
</template>
<script>
export default {
}
</script>
<style>
</style>

View File

@ -185,4 +185,12 @@
<img src="../../../../assets/avatar/南风.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>南风</p>
</div>
<div style="display: flex; flex-direction: column; align-items: center; width: fit-content; margin: 5px;">
<img src="../../../../assets/avatar/蜉蝣撼大叔.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>蜉蝣撼大叔</p>
</div>
<div style="display: flex; flex-direction: column; align-items: center; width: fit-content; margin: 5px;">
<img src="../../../../assets/avatar/乙.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p></p>
</div>
</div>

View File

@ -145,6 +145,14 @@
<img src="../../../../assets/avatar/南风.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>南风</p>
</div>
<div style="display: flex; flex-direction: column; align-items: center; width: fit-content; margin: 5px;">
<img src="../../../../assets/avatar/蜉蝣撼大叔.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>蜉蝣撼大叔</p>
</div>
<div style="display: flex; flex-direction: column; align-items: center; width: fit-content; margin: 5px;">
<img src="../../../../assets/avatar/乙.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p></p>
</div>
</div>
</div>
</template>

View File

@ -151,4 +151,14 @@ import MindMap from "simple-mind-map/dist/simpleMindMap.umd.min"
### 2.报错`Getting bbox of element "text" is not possible: TypeError: Cannot read properties of undefined (reading 'apply')`
原因为安装的`@svgdotjs/svg.js`版本太高,手动降到`3.0.16`版本即可。
原因为安装的`@svgdotjs/svg.js`版本太高,手动降到`3.0.16`版本即可。
### 3.TypeError: Cannot read properties of undefined (reading 'prototype') at sax.js:222:46
可以在打包配置文件中增加如下配置:
```js
resolve: { alias: { stream: "stream-browserify" } }
```
不同的打包工具可能具体配置不一样,原理就是排除`stream`依赖。

View File

@ -99,6 +99,11 @@ npm run build
<p>如果需要二次开发也就是必须要使用未打包代码的话如果你不需要解析<code>xmind</code>文件的话可以去除<code>xmind</code>模块如果需要的话那么可以尝试换成其他的解析<code>xml</code><code>json</code>的库</p>
<h3>2.报错<code>Getting bbox of element &quot;text&quot; is not possible: TypeError: Cannot read properties of undefined (reading 'apply')</code></h3>
<p>原因为安装的<code>@svgdotjs/svg.js</code>版本太高手动降到<code>3.0.16</code>版本即可</p>
<h3>3.TypeError: Cannot read properties of undefined (reading 'prototype') at sax.js:222:46</h3>
<p>可以在打包配置文件中增加如下配置</p>
<pre class="hljs"><code>resolve: { <span class="hljs-attr">alias</span>: { <span class="hljs-attr">stream</span>: <span class="hljs-string">&quot;stream-browserify&quot;</span> } }
</code></pre>
<p>不同的打包工具可能具体配置不一样原理就是排除<code>stream</code>依赖</p>
</div>
</template>

View File

@ -80,6 +80,7 @@
<el-dropdown-item command="devDoc">开发文档</el-dropdown-item>
<el-dropdown-item command="site">官方网站</el-dropdown-item>
<el-dropdown-item command="issue">意见反馈</el-dropdown-item>
<el-dropdown-item disabled>当前v{{ version }}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
@ -94,6 +95,7 @@ import { langList } from '@/config'
import i18n from '@/i18n'
import { storeLang, getLang } from '@/api'
import { mapState, mapMutations } from 'vuex'
import pkg from 'simple-mind-map/package.json'
/**
* @Author: 王林
@ -114,6 +116,7 @@ export default {
},
data() {
return {
version: pkg.version,
langList,
lang: '',
isReadonly: false,

View File

@ -1,18 +1,28 @@
<template>
<div class="scrollbarContainer" :class="{ isDark: isDark }">
<!-- 竖向 -->
<div class="scrollbar verticalScrollbar" ref="verticalScrollbarRef">
<div
class="scrollbar verticalScrollbar"
ref="verticalScrollbarRef"
@click="onVerticalScrollbarClick"
>
<div
class="scrollbarInner"
:style="verticalScrollbarStyle"
@click.stop
@mousedown="onVerticalScrollbarMousedown"
></div>
</div>
<!-- 横向 -->
<div class="scrollbar horizontalScrollbar" ref="horizontalScrollbarRef">
<div
class="scrollbar horizontalScrollbar"
ref="horizontalScrollbarRef"
@click="onHorizontalScrollbarClick"
>
<div
class="scrollbarInner"
:style="horizontalScrollbarStyle"
@click.stop
@mousedown="onHorizontalScrollbarMousedown"
></div>
</div>
@ -67,11 +77,7 @@ export default {
},
//
updateScrollbar() {
const {
vertical,
horizontal
} = this.mindMap.scrollbar.calculationScrollbar()
updateScrollbar({ vertical, horizontal }) {
this.verticalScrollbarStyle = {
top: vertical.top + '%',
height: vertical.height + '%'
@ -87,9 +93,19 @@ export default {
this.mindMap.scrollbar.onMousedown(e, 'vertical')
},
//
onVerticalScrollbarClick(e) {
this.mindMap.scrollbar.onClick(e, 'vertical')
},
//
onHorizontalScrollbarMousedown(e) {
this.mindMap.scrollbar.onMousedown(e, 'horizontal')
},
//
onHorizontalScrollbarClick(e) {
this.mindMap.scrollbar.onClick(e, 'horizontal')
}
}
}

View File

@ -1,6 +1,5 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import EditPage from '@/pages/Edit/Index'
import DocPage from '@/pages/Doc/Index'
import routerList from '@/pages/Doc/routerList'
@ -58,7 +57,7 @@ const routes = [
{
path: '/',
name: 'Edit',
component: EditPage
component: () => import(`./pages/Edit/Index.vue`)
},
// 开发文档
...createTypeRouterList('doc', 'introduction'),