Compare commits
104 Commits
0.5.4-fix.
...
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c789a95e4e | ||
|
|
b0532072c2 | ||
|
|
7d21ae4618 | ||
|
|
0e608de9da | ||
|
|
fbff68c635 | ||
|
|
ec9517c491 | ||
|
|
26e3158bd8 | ||
|
|
c2c9de1c03 | ||
|
|
27d2238d20 | ||
|
|
01dc98f1f8 | ||
|
|
cbd07246bd | ||
|
|
5bb23ca738 | ||
|
|
0886ba7698 | ||
|
|
a65cffa58b | ||
|
|
02e2d432dd | ||
|
|
5c9c3d7934 | ||
|
|
4ca6713675 | ||
|
|
3d18404fd6 | ||
|
|
98dda26bf8 | ||
|
|
cd4c5ecd83 | ||
|
|
fdc0017ccb | ||
|
|
20a5c12bbb | ||
|
|
4eacec125e | ||
|
|
34322ba6d1 | ||
|
|
8210151a7b | ||
|
|
1e00088608 | ||
|
|
ee59b8002a | ||
|
|
b2ca5d0fba | ||
|
|
470604e567 | ||
|
|
f5f665ec0a | ||
|
|
5e865a4e33 | ||
|
|
38ad33b604 | ||
|
|
e1b4146171 | ||
|
|
65151f4b0a | ||
|
|
942706fb63 | ||
|
|
5f4492d4b7 | ||
|
|
ace1f62a40 | ||
|
|
706c88c7d5 | ||
|
|
4512fb16eb | ||
|
|
e446ff12e7 | ||
|
|
4318646abe | ||
|
|
2cbfe4f0e7 | ||
|
|
b7910c4665 | ||
|
|
fc1ba24834 | ||
|
|
cf16937160 | ||
|
|
c3393abed6 | ||
|
|
2abff3e21b | ||
|
|
e39a94c5e2 | ||
|
|
a6fff7f7a3 | ||
|
|
e584081b41 | ||
|
|
b91dde8084 | ||
|
|
077478d654 | ||
|
|
2be97cc1a0 | ||
|
|
0f7dc949a4 | ||
|
|
c7e91cc9eb | ||
|
|
6eacfab9c2 | ||
|
|
e36a408275 | ||
|
|
abda5b7d06 | ||
|
|
f815f71dd7 | ||
|
|
fa2c5b420c | ||
|
|
4c19bc76a7 | ||
|
|
d08a317920 | ||
|
|
bd805836cd | ||
|
|
e804a8f2f7 | ||
|
|
8bf876d446 | ||
|
|
f2521f663e | ||
|
|
e676bff453 | ||
|
|
8f2cc72d3c | ||
|
|
ec22656bee | ||
|
|
4acf8ba2ac | ||
|
|
d45a18904e | ||
|
|
9fc2dbabd4 | ||
|
|
b83b81f52e | ||
|
|
d1e2db993e | ||
|
|
ab931901e2 | ||
|
|
9879a25f9b | ||
|
|
16fb8eb092 | ||
|
|
de859927ed | ||
|
|
7bde59f664 | ||
|
|
be9668c7b8 | ||
|
|
95fe3189d5 | ||
|
|
9c60857c6a | ||
|
|
3b7cea9ee3 | ||
|
|
d959420d6e | ||
|
|
79d81b92e6 | ||
|
|
940c60f23d | ||
|
|
965ab8151e | ||
|
|
87d55b31ca | ||
|
|
bb68575aca | ||
|
|
e561e804be | ||
|
|
5a8c3aa9d3 | ||
|
|
f84639debd | ||
|
|
de77a2b613 | ||
|
|
3825c3769f | ||
|
|
876908e922 | ||
|
|
25ecde8948 | ||
|
|
2de0334e3b | ||
|
|
d6e625efa6 | ||
|
|
fba0ece8fd | ||
|
|
78ed038012 | ||
|
|
58f7be77ed | ||
|
|
362160c67b | ||
|
|
c8a3c569e1 | ||
|
|
6bc309e3fa |
@@ -22,7 +22,7 @@
|
||||
# 特性
|
||||
|
||||
- [x] 插件化架构,除核心功能外,其他功能作为插件提供,按需使用,减小打包体积
|
||||
- [x] 支持逻辑结构图、思维导图、组织结构图、目录组织图四种结构
|
||||
- [x] 支持逻辑结构图、思维导图、组织结构图、目录组织图、时间轴、鱼骨图六种结构
|
||||
- [x] 内置多种主题,允许高度自定义样式,支持注册新主题
|
||||
- [x] 支持快捷键
|
||||
- [x] 节点内容支持图片、图标、超链接、备注、标签、概要
|
||||
|
||||
BIN
qrcode.jpg
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
@@ -9,9 +9,11 @@ import AssociativeLine from './src/AssociativeLine'
|
||||
import RichText from './src/RichText'
|
||||
import xmind from './src/parse/xmind.js'
|
||||
import markdown from './src/parse/markdown.js'
|
||||
import icons from './src/svg/icons.js'
|
||||
|
||||
MindMap.xmind = xmind
|
||||
MindMap.markdown = markdown
|
||||
MindMap.iconList = icons.nodeIconList
|
||||
|
||||
MindMap
|
||||
.usePlugin(MiniMap)
|
||||
|
||||
@@ -10,7 +10,7 @@ import BatchExecution from './src/BatchExecution'
|
||||
import { layoutValueList, CONSTANTS } from './src/utils/constant'
|
||||
import { SVG } from '@svgdotjs/svg.js'
|
||||
import { simpleDeepClone } from './src/utils'
|
||||
import defaultTheme from './src/themes/default'
|
||||
import defaultTheme, { checkIsNodeSizeIndependenceConfig } from './src/themes/default'
|
||||
|
||||
// 默认选项配置
|
||||
const defaultOpt = {
|
||||
@@ -18,6 +18,8 @@ const defaultOpt = {
|
||||
readonly: false,
|
||||
// 布局
|
||||
layout: CONSTANTS.LAYOUT.LOGICAL_STRUCTURE,
|
||||
// 如果结构为鱼骨图,那么可以通过该选项控制倾斜角度
|
||||
fishboneDeg: 45,
|
||||
// 主题
|
||||
theme: 'default', // 内置主题:default(默认主题)
|
||||
// 主题配置,会和所选择的主题进行合并
|
||||
@@ -90,7 +92,37 @@ const defaultOpt = {
|
||||
// 如果开启节点动画过渡,可以通过该属性设置过渡的时间,单位ms
|
||||
nodeTransitionMoveDuration: 300,
|
||||
// 初始根节点的位置
|
||||
initRootNodePosition: null
|
||||
initRootNodePosition: null,
|
||||
// 导出png、svg、pdf时的图形内边距
|
||||
exportPaddingX: 10,
|
||||
exportPaddingY: 10,
|
||||
// 节点文本编辑框的z-index
|
||||
nodeTextEditZIndex: 3000,
|
||||
// 节点备注浮层的z-index
|
||||
nodeNoteTooltipZIndex: 3000,
|
||||
// 是否在点击了画布外的区域时结束节点文本的编辑状态
|
||||
isEndNodeTextEditOnClickOuter: true,
|
||||
// 最大历史记录数
|
||||
maxHistoryCount: 1000,
|
||||
// 是否一直显示节点的展开收起按钮,默认为鼠标移上去和激活时才显示
|
||||
alwaysShowExpandBtn: false,
|
||||
// 扩展节点可插入的图标
|
||||
iconList: [
|
||||
// {
|
||||
// name: '',// 分组名称
|
||||
// type: '',// 分组的值
|
||||
// list: [// 分组下的图标列表
|
||||
// {
|
||||
// name: '',// 图标名称
|
||||
// icon:''// 图标,可以传svg或图片
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
],
|
||||
// 节点最大缓存数量
|
||||
maxNodeCacheCount: 1000,
|
||||
// 关联线默认文字
|
||||
defaultAssociativeLineText: '关联'
|
||||
}
|
||||
|
||||
// 思维导图
|
||||
@@ -180,12 +212,12 @@ class MindMap {
|
||||
}
|
||||
|
||||
// 重新渲染
|
||||
reRender(callback) {
|
||||
reRender(callback, source = '') {
|
||||
this.batchExecution.push('render', () => {
|
||||
this.draw.clear()
|
||||
this.initTheme()
|
||||
this.renderer.reRender = true
|
||||
this.renderer.render(callback)
|
||||
this.renderer.render(callback, source)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -235,7 +267,9 @@ class MindMap {
|
||||
// 设置主题配置
|
||||
setThemeConfig(config) {
|
||||
this.opt.themeConfig = config
|
||||
this.render(null, CONSTANTS.CHANGE_THEME)
|
||||
// 检查改变的是否是节点大小无关的主题属性
|
||||
let res = checkIsNodeSizeIndependenceConfig(config)
|
||||
this.render(null, res ? '' : CONSTANTS.CHANGE_THEME)
|
||||
}
|
||||
|
||||
// 获取自定义主题配置
|
||||
@@ -270,6 +304,7 @@ class MindMap {
|
||||
layout = CONSTANTS.LAYOUT.LOGICAL_STRUCTURE
|
||||
}
|
||||
this.opt.layout = layout
|
||||
this.view.reset()
|
||||
this.renderer.setLayout()
|
||||
this.render()
|
||||
}
|
||||
@@ -284,8 +319,12 @@ class MindMap {
|
||||
this.execCommand('CLEAR_ACTIVE_NODE')
|
||||
this.command.clearHistory()
|
||||
this.command.addHistory()
|
||||
this.renderer.renderTree = data
|
||||
this.reRender()
|
||||
if (this.richText) {
|
||||
this.renderer.renderTree = this.richText.handleSetData(data)
|
||||
} else {
|
||||
this.renderer.renderTree = data
|
||||
}
|
||||
this.reRender(() => {}, CONSTANTS.SET_DATA)
|
||||
}
|
||||
|
||||
// 动态设置思维导图数据,包括节点数据、布局、主题、视图
|
||||
@@ -357,7 +396,7 @@ class MindMap {
|
||||
}
|
||||
|
||||
// 获取svg数据
|
||||
getSvgData() {
|
||||
getSvgData({ paddingX = 0, paddingY = 0 } = {}) {
|
||||
const svg = this.svg
|
||||
const draw = this.draw
|
||||
// 保存原始信息
|
||||
@@ -369,6 +408,10 @@ class MindMap {
|
||||
draw.scale(1 / origTransform.scaleX, 1 / origTransform.scaleY)
|
||||
// 获取变换后的位置尺寸信息,其实是getBoundingClientRect方法的包装方法
|
||||
const rect = draw.rbox()
|
||||
// 内边距
|
||||
rect.width += paddingX
|
||||
rect.height += paddingY
|
||||
draw.translate(paddingX / 2, paddingY / 2)
|
||||
// 将svg设置为实际内容的宽高
|
||||
svg.size(rect.width, rect.height)
|
||||
// 把实际内容变换
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "simple-mind-map",
|
||||
"version": "0.5.4",
|
||||
"version": "0.5.11",
|
||||
"description": "一个简单的web在线思维导图",
|
||||
"authors": [
|
||||
{
|
||||
|
||||
@@ -3,13 +3,13 @@ import { v4 as uuid } from 'uuid'
|
||||
import {
|
||||
getAssociativeLineTargetIndex,
|
||||
computeCubicBezierPathPoints,
|
||||
joinCubicBezierPath,
|
||||
cubicBezierPath,
|
||||
getNodePoint,
|
||||
computeNodePoints,
|
||||
getNodeLinePath,
|
||||
getDefaultControlPointOffsets
|
||||
getNodeLinePath
|
||||
} from './utils/associativeLineUtils'
|
||||
import associativeLineControlsMethods from './utils/associativeLineControls'
|
||||
import associativeLineTextMethods from './utils/associativeLineText'
|
||||
|
||||
// 关联线类
|
||||
class AssociativeLine {
|
||||
@@ -46,6 +46,14 @@ class AssociativeLine {
|
||||
}
|
||||
// 节流一下,不然很卡
|
||||
this.checkOverlapNode = throttle(this.checkOverlapNode, 100, this)
|
||||
// 控制点相关方法
|
||||
Object.keys(associativeLineControlsMethods).forEach(item => {
|
||||
this[item] = associativeLineControlsMethods[item].bind(this)
|
||||
})
|
||||
// 关联线文字相关方法
|
||||
Object.keys(associativeLineTextMethods).forEach(item => {
|
||||
this[item] = associativeLineTextMethods[item].bind(this)
|
||||
})
|
||||
this.bindEvent()
|
||||
}
|
||||
|
||||
@@ -89,6 +97,8 @@ class AssociativeLine {
|
||||
window.addEventListener('mouseup', e => {
|
||||
this.onControlPointMouseup(e)
|
||||
})
|
||||
// 缩放事件
|
||||
this.mindMap.on('scale', this.onScale)
|
||||
}
|
||||
|
||||
// 创建箭头
|
||||
@@ -177,36 +187,46 @@ class AssociativeLine {
|
||||
.stroke({ width: associativeLineActiveWidth, color: 'transparent' })
|
||||
.fill({ color: 'none' })
|
||||
clickPath.plot(pathStr)
|
||||
// 文字
|
||||
let text = this.createText({ path, clickPath, node, toNode, startPoint, endPoint, controlPoints })
|
||||
// 点击事件
|
||||
clickPath.click(e => {
|
||||
e.stopPropagation()
|
||||
// 如果当前存在激活节点,那么取消激活节点
|
||||
if (this.mindMap.renderer.activeNodeList.length > 0) {
|
||||
this.clearActiveNodes()
|
||||
} else {
|
||||
// 否则清除当前的关联线的激活状态,如果有的话
|
||||
this.clearActiveLine()
|
||||
// 保存当前激活的关联线信息
|
||||
this.activeLine = [path, clickPath, node, toNode]
|
||||
// 让不可见的点击线显示
|
||||
clickPath.stroke({ color: associativeLineActiveColor })
|
||||
// 渲染控制点和连线
|
||||
this.renderControls(
|
||||
startPoint,
|
||||
endPoint,
|
||||
controlPoints[0],
|
||||
controlPoints[1]
|
||||
)
|
||||
this.mindMap.emit(
|
||||
'associative_line_click',
|
||||
path,
|
||||
clickPath,
|
||||
node,
|
||||
toNode
|
||||
)
|
||||
}
|
||||
this.setActiveLine({ path, clickPath, text, node, toNode, startPoint, endPoint, controlPoints })
|
||||
})
|
||||
this.lineList.push([path, clickPath, node, toNode])
|
||||
// 渲染关联线文字
|
||||
this.renderText(this.getText(node, toNode), path, text)
|
||||
this.lineList.push([path, clickPath, text, node, toNode])
|
||||
}
|
||||
|
||||
// 激活某根关联线
|
||||
setActiveLine({ path, clickPath, text, node, toNode, startPoint, endPoint, controlPoints }) {
|
||||
let {
|
||||
associativeLineActiveColor
|
||||
} = this.mindMap.themeConfig
|
||||
// 如果当前存在激活节点,那么取消激活节点
|
||||
if (this.mindMap.renderer.activeNodeList.length > 0) {
|
||||
this.clearActiveNodes()
|
||||
} else {
|
||||
// 否则清除当前的关联线的激活状态,如果有的话
|
||||
this.clearActiveLine()
|
||||
// 保存当前激活的关联线信息
|
||||
this.activeLine = [path, clickPath, text, node, toNode]
|
||||
// 让不可见的点击线显示
|
||||
clickPath.stroke({ color: associativeLineActiveColor })
|
||||
// 如果没有输入过关联线文字,那么显示默认文字
|
||||
if (!this.getText(node, toNode)) {
|
||||
this.renderText(this.mindMap.opt.defaultAssociativeLineText, path, text)
|
||||
}
|
||||
// 渲染控制点和连线
|
||||
this.renderControls(
|
||||
startPoint,
|
||||
endPoint,
|
||||
controlPoints[0],
|
||||
controlPoints[1]
|
||||
)
|
||||
this.mindMap.emit('associative_line_click', path, clickPath, node, toNode)
|
||||
}
|
||||
}
|
||||
|
||||
// 移除所有连接线
|
||||
@@ -214,6 +234,7 @@ class AssociativeLine {
|
||||
this.lineList.forEach(line => {
|
||||
line[0].remove()
|
||||
line[1].remove()
|
||||
line[2].remove()
|
||||
})
|
||||
this.lineList = []
|
||||
}
|
||||
@@ -253,7 +274,8 @@ class AssociativeLine {
|
||||
updateCreatingLine(e) {
|
||||
let { x, y } = this.getTransformedEventPos(e)
|
||||
let startPoint = getNodePoint(this.creatingStartNode)
|
||||
let pathStr = cubicBezierPath(startPoint.x, startPoint.y, x, y)
|
||||
let offsetX = x > startPoint.x ? -10 : 10
|
||||
let pathStr = cubicBezierPath(startPoint.x, startPoint.y, x + offsetX, y)
|
||||
this.creatingLine.plot(pathStr)
|
||||
this.checkOverlapNode(x, y)
|
||||
}
|
||||
@@ -349,20 +371,33 @@ class AssociativeLine {
|
||||
// 删除连接线
|
||||
removeLine() {
|
||||
if (!this.activeLine) return
|
||||
let [, , node, toNode] = this.activeLine
|
||||
let [, , , node, toNode] = this.activeLine
|
||||
this.removeControls()
|
||||
let { associativeLineTargets, associativeLineTargetControlOffsets } =
|
||||
let { associativeLineTargets, associativeLineTargetControlOffsets, associativeLineText } =
|
||||
node.nodeData.data
|
||||
let targetIndex = getAssociativeLineTargetIndex(node, toNode)
|
||||
// 更新关联线文本数据
|
||||
let newAssociativeLineText = {}
|
||||
if (associativeLineText) {
|
||||
Object.keys(associativeLineText).forEach((item) => {
|
||||
if (item !== toNode.nodeData.data.id) {
|
||||
newAssociativeLineText[item] = associativeLineText[item]
|
||||
}
|
||||
})
|
||||
}
|
||||
this.mindMap.execCommand('SET_NODE_DATA', node, {
|
||||
// 目标
|
||||
associativeLineTargets: associativeLineTargets.filter((_, index) => {
|
||||
return index !== targetIndex
|
||||
}),
|
||||
// 偏移量
|
||||
associativeLineTargetControlOffsets: associativeLineTargetControlOffsets
|
||||
? associativeLineTargetControlOffsets.filter((_, index) => {
|
||||
return index !== targetIndex
|
||||
})
|
||||
: []
|
||||
: [],
|
||||
// 文本
|
||||
associativeLineText: newAssociativeLineText
|
||||
})
|
||||
}
|
||||
|
||||
@@ -376,9 +411,16 @@ class AssociativeLine {
|
||||
// 清除激活的线
|
||||
clearActiveLine() {
|
||||
if (this.activeLine) {
|
||||
this.activeLine[1].stroke({
|
||||
let [, clickPath, text, node, toNode] = this.activeLine
|
||||
clickPath.stroke({
|
||||
color: 'transparent'
|
||||
})
|
||||
// 隐藏关联线文本编辑框
|
||||
this.hideEditTextBox()
|
||||
// 如果当前关联线没有文字,则清空文字节点
|
||||
if (!this.getText(node, toNode)) {
|
||||
text.clear()
|
||||
}
|
||||
this.activeLine = null
|
||||
this.removeControls()
|
||||
}
|
||||
@@ -391,6 +433,7 @@ class AssociativeLine {
|
||||
this.lineList.forEach(line => {
|
||||
line[0].hide()
|
||||
line[1].hide()
|
||||
line[2].hide()
|
||||
})
|
||||
this.hideControls()
|
||||
}
|
||||
@@ -401,225 +444,11 @@ class AssociativeLine {
|
||||
this.lineList.forEach(line => {
|
||||
line[0].show()
|
||||
line[1].show()
|
||||
line[2].show()
|
||||
})
|
||||
this.showControls()
|
||||
this.isNodeDragging = false
|
||||
}
|
||||
|
||||
// 创建控制点、连线节点
|
||||
createControlNodes() {
|
||||
let { associativeLineActiveColor } = this.mindMap.themeConfig
|
||||
// 连线
|
||||
this.controlLine1 = this.draw
|
||||
.line()
|
||||
.stroke({ color: associativeLineActiveColor, width: 2 })
|
||||
this.controlLine2 = this.draw
|
||||
.line()
|
||||
.stroke({ color: associativeLineActiveColor, width: 2 })
|
||||
// 控制点
|
||||
this.controlPoint1 = this.createOneControlNode('controlPoint1')
|
||||
this.controlPoint2 = this.createOneControlNode('controlPoint2')
|
||||
}
|
||||
|
||||
// 创建控制点
|
||||
createOneControlNode(pointKey) {
|
||||
let { associativeLineActiveColor } = this.mindMap.themeConfig
|
||||
return this.draw
|
||||
.circle(this.controlPointDiameter)
|
||||
.stroke({ color: associativeLineActiveColor })
|
||||
.fill({ color: '#fff' })
|
||||
.click(e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
.mousedown(e => {
|
||||
this.onControlPointMousedown(e, pointKey)
|
||||
})
|
||||
}
|
||||
|
||||
// 控制点的鼠标按下事件
|
||||
onControlPointMousedown(e, pointKey) {
|
||||
e.stopPropagation()
|
||||
this.isControlPointMousedown = true
|
||||
this.mousedownControlPointKey = pointKey
|
||||
}
|
||||
|
||||
// 控制点的鼠标移动事件
|
||||
onControlPointMousemove(e) {
|
||||
if (
|
||||
!this.isControlPointMousedown ||
|
||||
!this.mousedownControlPointKey ||
|
||||
!this[this.mousedownControlPointKey]
|
||||
)
|
||||
return
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
let radius = this.controlPointDiameter / 2
|
||||
// 转换鼠标当前的位置
|
||||
let { x, y } = this.getTransformedEventPos(e)
|
||||
this.controlPointMousemoveState.pos = {
|
||||
x,
|
||||
y
|
||||
}
|
||||
// 更新当前拖拽的控制点的位置
|
||||
this[this.mousedownControlPointKey].x(x - radius).y(y - radius)
|
||||
let [path, clickPath, node, toNode] = this.activeLine
|
||||
let [startPoint, endPoint] = computeNodePoints(node, toNode)
|
||||
this.controlPointMousemoveState.startPoint = startPoint
|
||||
this.controlPointMousemoveState.endPoint = endPoint
|
||||
let targetIndex = getAssociativeLineTargetIndex(node, toNode)
|
||||
this.controlPointMousemoveState.targetIndex = targetIndex
|
||||
let offsets = []
|
||||
let associativeLineTargetControlOffsets = node.nodeData.data.associativeLineTargetControlOffsets
|
||||
if (!associativeLineTargetControlOffsets) {
|
||||
// 兼容0.4.5版本,没有associativeLineTargetControlOffsets的情况
|
||||
offsets = getDefaultControlPointOffsets(startPoint, endPoint)
|
||||
} else {
|
||||
offsets = associativeLineTargetControlOffsets[targetIndex]
|
||||
}
|
||||
let point1 = null
|
||||
let point2 = null
|
||||
// 拖拽的是控制点1
|
||||
if (this.mousedownControlPointKey === 'controlPoint1') {
|
||||
point1 = {
|
||||
x,
|
||||
y
|
||||
}
|
||||
point2 = {
|
||||
x: endPoint.x + offsets[1].x,
|
||||
y: endPoint.y + offsets[1].y
|
||||
}
|
||||
// 更新控制点1的连线
|
||||
this.controlLine1.plot(startPoint.x, startPoint.y, point1.x, point1.y)
|
||||
} else {
|
||||
// 拖拽的是控制点2
|
||||
point1 = {
|
||||
x: startPoint.x + offsets[0].x,
|
||||
y: startPoint.y + offsets[0].y
|
||||
}
|
||||
point2 = {
|
||||
x,
|
||||
y
|
||||
}
|
||||
// 更新控制点2的连线
|
||||
this.controlLine2.plot(endPoint.x, endPoint.y, point2.x, point2.y)
|
||||
}
|
||||
// 更新关联线
|
||||
let pathStr = joinCubicBezierPath(startPoint, endPoint, point1, point2)
|
||||
path.plot(pathStr)
|
||||
clickPath.plot(pathStr)
|
||||
}
|
||||
|
||||
// 控制点的鼠标移动事件
|
||||
onControlPointMouseup(e) {
|
||||
if (!this.isControlPointMousedown) return
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
let { pos, startPoint, endPoint, targetIndex } =
|
||||
this.controlPointMousemoveState
|
||||
let [, , node] = this.activeLine
|
||||
let offsetList = []
|
||||
let associativeLineTargetControlOffsets = node.nodeData.data.associativeLineTargetControlOffsets
|
||||
if (!associativeLineTargetControlOffsets) {
|
||||
// 兼容0.4.5版本,没有associativeLineTargetControlOffsets的情况
|
||||
offsetList[targetIndex] = getDefaultControlPointOffsets(startPoint, endPoint)
|
||||
} else {
|
||||
offsetList = associativeLineTargetControlOffsets
|
||||
}
|
||||
let offset1 = null
|
||||
let offset2 = null
|
||||
if (this.mousedownControlPointKey === 'controlPoint1') {
|
||||
// 更新控制点1数据
|
||||
offset1 = {
|
||||
x: pos.x - startPoint.x,
|
||||
y: pos.y - startPoint.y
|
||||
}
|
||||
offset2 = offsetList[targetIndex][1]
|
||||
} else {
|
||||
// 更新控制点2数据
|
||||
offset1 = offsetList[targetIndex][0]
|
||||
offset2 = {
|
||||
x: pos.x - endPoint.x,
|
||||
y: pos.y - endPoint.y
|
||||
}
|
||||
}
|
||||
offsetList[targetIndex] = [offset1, offset2]
|
||||
this.mindMap.execCommand('SET_NODE_DATA', node, {
|
||||
associativeLineTargetControlOffsets: offsetList
|
||||
})
|
||||
// 这里要加个setTimeout0是因为draw_click事件比mouseup事件触发的晚,所以重置isControlPointMousedown需要等draw_click事件触发完以后
|
||||
setTimeout(() => {
|
||||
this.resetControlPoint()
|
||||
}, 0)
|
||||
}
|
||||
|
||||
// 复位控制点移动
|
||||
resetControlPoint() {
|
||||
this.isControlPointMousedown = false
|
||||
this.mousedownControlPointKey = ''
|
||||
this.controlPointMousemoveState = {
|
||||
pos: null,
|
||||
startPoint: null,
|
||||
endPoint: null,
|
||||
targetIndex: ''
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染控制点
|
||||
renderControls(startPoint, endPoint, point1, point2) {
|
||||
if (!this.controlLine1) {
|
||||
this.createControlNodes()
|
||||
}
|
||||
let radius = this.controlPointDiameter / 2
|
||||
// 控制点和起终点的连线
|
||||
this.controlLine1.plot(startPoint.x, startPoint.y, point1.x, point1.y)
|
||||
this.controlLine2.plot(endPoint.x, endPoint.y, point2.x, point2.y)
|
||||
// 控制点
|
||||
this.controlPoint1.x(point1.x - radius).y(point1.y - radius)
|
||||
this.controlPoint2.x(point2.x - radius).y(point2.y - radius)
|
||||
}
|
||||
|
||||
// 删除控制点
|
||||
removeControls() {
|
||||
if (!this.controlLine1) return
|
||||
;[
|
||||
this.controlLine1,
|
||||
this.controlLine2,
|
||||
this.controlPoint1,
|
||||
this.controlPoint2
|
||||
].forEach(item => {
|
||||
item.remove()
|
||||
})
|
||||
this.controlLine1 = null
|
||||
this.controlLine2 = null
|
||||
this.controlPoint1 = null
|
||||
this.controlPoint2 = null
|
||||
}
|
||||
|
||||
// 隐藏控制点
|
||||
hideControls() {
|
||||
if (!this.controlLine1) return
|
||||
;[
|
||||
this.controlLine1,
|
||||
this.controlLine2,
|
||||
this.controlPoint1,
|
||||
this.controlPoint2
|
||||
].forEach(item => {
|
||||
item.hide()
|
||||
})
|
||||
}
|
||||
|
||||
// 显示控制点
|
||||
showControls() {
|
||||
if (!this.controlLine1) return
|
||||
;[
|
||||
this.controlLine1,
|
||||
this.controlLine2,
|
||||
this.controlPoint1,
|
||||
this.controlPoint2
|
||||
].forEach(item => {
|
||||
item.show()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
AssociativeLine.instanceName = 'associativeLine'
|
||||
|
||||
@@ -84,6 +84,10 @@ class Command {
|
||||
// 删除当前历史指针后面的数据
|
||||
this.history = this.history.slice(0, this.activeHistoryIndex + 1)
|
||||
this.history.push(simpleDeepClone(data))
|
||||
// 历史记录数超过最大数量
|
||||
if (this.history.length > this.mindMap.opt.maxHistoryCount) {
|
||||
this.history.shift()
|
||||
}
|
||||
this.activeHistoryIndex = this.history.length - 1
|
||||
this.mindMap.emit('data_change', this.removeDataUid(data))
|
||||
this.mindMap.emit(
|
||||
|
||||
@@ -27,6 +27,7 @@ class Event extends EventEmitter {
|
||||
|
||||
// 绑定函数上下文
|
||||
bindFn() {
|
||||
this.onBodyClick = this.onBodyClick.bind(this)
|
||||
this.onDrawClick = this.onDrawClick.bind(this)
|
||||
this.onMousedown = this.onMousedown.bind(this)
|
||||
this.onMousemove = this.onMousemove.bind(this)
|
||||
@@ -41,6 +42,7 @@ class Event extends EventEmitter {
|
||||
|
||||
// 绑定事件
|
||||
bind() {
|
||||
document.body.addEventListener('click', this.onBodyClick)
|
||||
this.mindMap.svg.on('click', this.onDrawClick)
|
||||
this.mindMap.el.addEventListener('mousedown', this.onMousedown)
|
||||
this.mindMap.svg.on('mousedown', this.onSvgMousedown)
|
||||
@@ -55,6 +57,7 @@ class Event extends EventEmitter {
|
||||
|
||||
// 解绑事件
|
||||
unbind() {
|
||||
document.body.removeEventListener('click', this.onBodyClick)
|
||||
this.mindMap.svg.off('click', this.onDrawClick)
|
||||
this.mindMap.el.removeEventListener('mousedown', this.onMousedown)
|
||||
window.removeEventListener('mousemove', this.onMousemove)
|
||||
@@ -71,6 +74,11 @@ class Event extends EventEmitter {
|
||||
this.emit('draw_click', e)
|
||||
}
|
||||
|
||||
// 页面的单击事件
|
||||
onBodyClick(e) {
|
||||
this.emit('body_click', e)
|
||||
}
|
||||
|
||||
// svg画布的鼠标按下事件
|
||||
onSvgMousedown(e) {
|
||||
this.emit('svg_mousedown', e)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { imgToDataUrl, downloadFile } from './utils'
|
||||
import { imgToDataUrl, downloadFile, readBlob } from './utils'
|
||||
import JsPDF from 'jspdf'
|
||||
import { SVG } from '@svgdotjs/svg.js'
|
||||
import drawBackgroundImageToCanvas from './utils/simulateCSSBackgroundInCanvas'
|
||||
@@ -27,8 +27,12 @@ class Export {
|
||||
}
|
||||
|
||||
// 获取svg数据
|
||||
async getSvgData(domToImage) {
|
||||
let { svg, svgHTML } = this.mindMap.getSvgData()
|
||||
async getSvgData() {
|
||||
let { exportPaddingX, exportPaddingY } = this.mindMap.opt
|
||||
let { svg, svgHTML } = this.mindMap.getSvgData({
|
||||
paddingX: exportPaddingX,
|
||||
paddingY: exportPaddingY
|
||||
})
|
||||
// 把图片的url转换成data:url类型,否则导出会丢失图片
|
||||
let imageList = svg.find('image')
|
||||
let task = imageList.map(async item => {
|
||||
@@ -40,24 +44,14 @@ class Export {
|
||||
if (imageList.length > 0) {
|
||||
svgHTML = svg.svg()
|
||||
}
|
||||
// 如果开启了富文本编辑,需要把svg中的dom元素转换成图片
|
||||
let nodeWithDomToImg = null
|
||||
if (domToImage && this.mindMap.richText) {
|
||||
let res = await this.mindMap.richText.handleSvgDomElements(svg)
|
||||
if (res) {
|
||||
nodeWithDomToImg = res.svg
|
||||
svgHTML = res.svgHTML
|
||||
}
|
||||
}
|
||||
return {
|
||||
node: svg,
|
||||
str: svgHTML,
|
||||
nodeWithDomToImg
|
||||
str: svgHTML
|
||||
}
|
||||
}
|
||||
|
||||
// svg转png
|
||||
svgToPng(svgSrc) {
|
||||
svgToPng(svgSrc, transparent) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
// 跨域图片需要添加这个属性,否则画布被污染了无法导出图片
|
||||
@@ -69,7 +63,9 @@ class Export {
|
||||
canvas.height = img.height + this.exportPadding * 2
|
||||
let ctx = canvas.getContext('2d')
|
||||
// 绘制背景
|
||||
await this.drawBackgroundToCanvas(ctx, canvas.width, canvas.height)
|
||||
if (!transparent) {
|
||||
await this.drawBackgroundToCanvas(ctx, canvas.width, canvas.height)
|
||||
}
|
||||
// 图片绘制到canvas里
|
||||
ctx.drawImage(
|
||||
img,
|
||||
@@ -131,23 +127,50 @@ class Export {
|
||||
})
|
||||
}
|
||||
|
||||
// 在svg上绘制思维导图背景
|
||||
drawBackgroundToSvg(svg) {
|
||||
return new Promise(async resolve => {
|
||||
let {
|
||||
backgroundColor = '#fff',
|
||||
backgroundImage,
|
||||
backgroundRepeat = 'repeat'
|
||||
} = this.mindMap.themeConfig
|
||||
// 背景颜色
|
||||
svg.css('background-color', backgroundColor)
|
||||
// 背景图片
|
||||
if (backgroundImage && backgroundImage !== 'none') {
|
||||
let imgDataUrl = await imgToDataUrl(backgroundImage)
|
||||
svg.css('background-image', `url(${imgDataUrl})`)
|
||||
svg.css('background-repeat', backgroundRepeat)
|
||||
resolve()
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 导出为png
|
||||
/**
|
||||
* 方法1.把svg的图片都转化成data:url格式,再转换
|
||||
* 方法2.把svg的图片提取出来再挨个绘制到canvas里,最后一起转换
|
||||
*/
|
||||
async png() {
|
||||
let { str } = await this.getSvgData(true)
|
||||
async png(name, transparent = false) {
|
||||
let { node, str } = await this.getSvgData()
|
||||
// 如果开启了富文本,则使用htmltocanvas转换为图片
|
||||
if (this.mindMap.richText) {
|
||||
let res = await this.mindMap.richText.handleExportPng(node.node)
|
||||
let imgDataUrl = await this.svgToPng(res, transparent)
|
||||
return imgDataUrl
|
||||
}
|
||||
// 转换成blob数据
|
||||
let blob = new Blob([str], {
|
||||
type: 'image/svg+xml'
|
||||
})
|
||||
// 转换成data:url数据
|
||||
let svgUrl = URL.createObjectURL(blob)
|
||||
let svgUrl = await readBlob(blob)
|
||||
// 绘制到canvas上
|
||||
let imgDataUrl = await this.svgToPng(svgUrl)
|
||||
URL.revokeObjectURL(svgUrl)
|
||||
return imgDataUrl
|
||||
let res = await this.svgToPng(svgUrl, transparent)
|
||||
return res
|
||||
}
|
||||
|
||||
// 导出为pdf
|
||||
@@ -182,38 +205,13 @@ class Export {
|
||||
image.src = img
|
||||
}
|
||||
|
||||
// 在svg上绘制思维导图背景
|
||||
drawBackgroundToSvg(svg) {
|
||||
return new Promise(async resolve => {
|
||||
let {
|
||||
backgroundColor = '#fff',
|
||||
backgroundImage,
|
||||
backgroundRepeat = 'repeat'
|
||||
} = this.mindMap.themeConfig
|
||||
// 背景颜色
|
||||
svg.css('background-color', backgroundColor)
|
||||
// 背景图片
|
||||
if (backgroundImage && backgroundImage !== 'none') {
|
||||
let imgDataUrl = await imgToDataUrl(backgroundImage)
|
||||
svg.css('background-image', `url(${imgDataUrl})`)
|
||||
svg.css('background-repeat', backgroundRepeat)
|
||||
resolve()
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 导出为svg
|
||||
// domToImage:是否将svg中的dom节点转换成图片的形式
|
||||
// plusCssText:附加的css样式,如果svg中存在dom节点,想要设置一些针对节点的样式可以通过这个参数传入
|
||||
async svg(name, domToImage = false, plusCssText) {
|
||||
let { node, nodeWithDomToImg } = await this.getSvgData(domToImage)
|
||||
async svg(name, plusCssText) {
|
||||
let { node } = await this.getSvgData()
|
||||
// 开启了节点富文本编辑
|
||||
if (this.mindMap.richText) {
|
||||
if (domToImage) {
|
||||
node = nodeWithDomToImg
|
||||
} else if (plusCssText) {
|
||||
if (plusCssText) {
|
||||
let foreignObjectList = node.find('foreignObject')
|
||||
if (foreignObjectList.length > 0) {
|
||||
foreignObjectList[0].add(SVG(`<style>${plusCssText}</style>`))
|
||||
@@ -227,28 +225,32 @@ class Export {
|
||||
let blob = new Blob([str], {
|
||||
type: 'image/svg+xml'
|
||||
})
|
||||
return URL.createObjectURL(blob)
|
||||
let res = await readBlob(blob)
|
||||
return res
|
||||
}
|
||||
|
||||
// 导出为json
|
||||
json(name, withConfig = true) {
|
||||
async json(name, withConfig = true) {
|
||||
let data = this.mindMap.getData(withConfig)
|
||||
let str = JSON.stringify(data)
|
||||
let blob = new Blob([str])
|
||||
return URL.createObjectURL(blob)
|
||||
let res = await readBlob(blob)
|
||||
return res
|
||||
}
|
||||
|
||||
// 专有文件,其实就是json文件
|
||||
smm(name, withConfig) {
|
||||
return this.json(name, withConfig)
|
||||
async smm(name, withConfig) {
|
||||
let res = await this.json(name, withConfig)
|
||||
return res
|
||||
}
|
||||
|
||||
// markdown文件
|
||||
md() {
|
||||
async md() {
|
||||
let data = this.mindMap.getData()
|
||||
let content = transformToMarkdown(data)
|
||||
let blob = new Blob([content])
|
||||
return URL.createObjectURL(blob)
|
||||
let res = await readBlob(blob)
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ export default class KeyCommand {
|
||||
if (this.mindMap.richText && this.mindMap.richText.showTextEdit) {
|
||||
return
|
||||
}
|
||||
if (this.mindMap.renderer.textEdit.showTextEdit) {
|
||||
if (this.mindMap.renderer.textEdit.showTextEdit || (this.mindMap.associativeLine && this.mindMap.associativeLine.showTextEdit)) {
|
||||
return
|
||||
}
|
||||
this.isInSvg = false
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import Style from './Style'
|
||||
import Shape from './Shape'
|
||||
import { asyncRun } from './utils'
|
||||
import { G } from '@svgdotjs/svg.js'
|
||||
import { G, Rect } from '@svgdotjs/svg.js'
|
||||
import nodeGeneralizationMethods from './utils/nodeGeneralization'
|
||||
import nodeExpandBtnMethods from './utils/nodeExpandBtn'
|
||||
import nodeCommandWrapsMethods from './utils/nodeCommandWraps'
|
||||
import nodeCreateContentsMethods from './utils/nodeCreateContents'
|
||||
import { CONSTANTS } from './utils/constant'
|
||||
|
||||
|
||||
// 节点类
|
||||
class Node {
|
||||
// 构造函数
|
||||
@@ -58,7 +57,7 @@ class Node {
|
||||
this.children = opt.children || []
|
||||
// 节点内容的容器
|
||||
this.group = null
|
||||
this.shapeNode = null// 节点形状节点
|
||||
this.shapeNode = null // 节点形状节点
|
||||
// 节点内容对象
|
||||
this._imgData = null
|
||||
this._iconData = null
|
||||
@@ -68,12 +67,16 @@ class Node {
|
||||
this._noteData = null
|
||||
this.noteEl = null
|
||||
this._expandBtn = null
|
||||
this._lastExpandBtnType = null
|
||||
this._showExpandBtn = false
|
||||
this._openExpandNode = null
|
||||
this._closeExpandNode = null
|
||||
this._fillExpandNode = null
|
||||
this._lines = []
|
||||
this._generalizationLine = null
|
||||
this._generalizationNode = null
|
||||
this._unVisibleRectRegionNode = null
|
||||
this._isMouseenter = false
|
||||
// 尺寸信息
|
||||
this._rectInfo = {
|
||||
imgContentWidth: 0,
|
||||
@@ -95,19 +98,19 @@ class Node {
|
||||
// 是否需要重新layout
|
||||
this.needLayout = false
|
||||
// 概要相关方法
|
||||
Object.keys(nodeGeneralizationMethods).forEach((item) => {
|
||||
Object.keys(nodeGeneralizationMethods).forEach(item => {
|
||||
this[item] = nodeGeneralizationMethods[item].bind(this)
|
||||
})
|
||||
// 展开收起按钮相关方法
|
||||
Object.keys(nodeExpandBtnMethods).forEach((item) => {
|
||||
Object.keys(nodeExpandBtnMethods).forEach(item => {
|
||||
this[item] = nodeExpandBtnMethods[item].bind(this)
|
||||
})
|
||||
// 命令的相关方法
|
||||
Object.keys(nodeCommandWrapsMethods).forEach((item) => {
|
||||
Object.keys(nodeCommandWrapsMethods).forEach(item => {
|
||||
this[item] = nodeCommandWrapsMethods[item].bind(this)
|
||||
})
|
||||
// 创建节点内容的相关方法
|
||||
Object.keys(nodeCreateContentsMethods).forEach((item) => {
|
||||
Object.keys(nodeCreateContentsMethods).forEach(item => {
|
||||
this[item] = nodeCreateContentsMethods[item].bind(this)
|
||||
})
|
||||
// 初始化
|
||||
@@ -242,13 +245,23 @@ class Node {
|
||||
layout() {
|
||||
// 清除之前的内容
|
||||
this.group.clear()
|
||||
let { width, textContentItemMargin } = this
|
||||
let { width, height, textContentItemMargin } = this
|
||||
let { paddingY } = this.getPaddingVale()
|
||||
paddingY += this.shapePadding.paddingY
|
||||
// 节点形状
|
||||
this.shapeNode = this.shapeInstance.createShape()
|
||||
this.group.add(this.shapeNode)
|
||||
this.updateNodeShape()
|
||||
// 渲染一个隐藏的矩形区域,用来触发展开收起按钮的显示
|
||||
if (!this.mindMap.opt.alwaysShowExpandBtn) {
|
||||
if (!this._unVisibleRectRegionNode) {
|
||||
this._unVisibleRectRegionNode = new Rect()
|
||||
}
|
||||
this._unVisibleRectRegionNode.fill({
|
||||
color: 'transparent'
|
||||
}).size(this.expandBtnSize, height).x(width).y(0)
|
||||
this.group.add(this._unVisibleRectRegionNode)
|
||||
}
|
||||
// 概要节点添加一个带所属节点id的类名
|
||||
if (this.isGeneralization && this.generalizationBelongNode) {
|
||||
this.group.addClass('generalization_' + this.generalizationBelongNode.uid)
|
||||
@@ -339,6 +352,9 @@ class Node {
|
||||
this.active(e)
|
||||
})
|
||||
this.group.on('mousedown', e => {
|
||||
if (this.isRoot && e.which === 3) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
if (!this.isRoot) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
@@ -346,9 +362,16 @@ class Node {
|
||||
if (e.ctrlKey) {
|
||||
this.isMultipleChoice = true
|
||||
let isActive = this.nodeData.data.isActive
|
||||
if (!isActive) this.mindMap.emit('before_node_active', this, this.renderer.activeNodeList)
|
||||
if (!isActive)
|
||||
this.mindMap.emit(
|
||||
'before_node_active',
|
||||
this,
|
||||
this.renderer.activeNodeList
|
||||
)
|
||||
this.mindMap.execCommand('SET_NODE_ACTIVE', this, !isActive)
|
||||
this.mindMap.renderer[isActive ? 'removeActiveNode' : 'addActiveNode'](this)
|
||||
this.mindMap.renderer[isActive ? 'removeActiveNode' : 'addActiveNode'](
|
||||
this
|
||||
)
|
||||
this.mindMap.emit(
|
||||
'node_active',
|
||||
isActive ? null : this,
|
||||
@@ -364,9 +387,14 @@ class Node {
|
||||
this.mindMap.emit('node_mouseup', this, e)
|
||||
})
|
||||
this.group.on('mouseenter', e => {
|
||||
this._isMouseenter = true
|
||||
// 显示展开收起按钮
|
||||
this.showExpandBtn()
|
||||
this.mindMap.emit('node_mouseenter', this, e)
|
||||
})
|
||||
this.group.on('mouseleave', e => {
|
||||
this._isMouseenter = false
|
||||
this.hideExpandBtn()
|
||||
this.mindMap.emit('node_mouseleave', this, e)
|
||||
})
|
||||
// 双击事件
|
||||
@@ -380,7 +408,7 @@ class Node {
|
||||
// 右键菜单事件
|
||||
this.group.on('contextmenu', e => {
|
||||
// 按住ctrl键点击鼠标左键不知为何触发的是contextmenu事件
|
||||
if (this.mindMap.opt.readonly || this.isGeneralization || e.ctrlKey) {
|
||||
if (this.mindMap.opt.readonly || e.ctrlKey) {// || this.isGeneralization
|
||||
return
|
||||
}
|
||||
e.stopPropagation()
|
||||
@@ -414,30 +442,37 @@ class Node {
|
||||
if (!this.group) {
|
||||
return
|
||||
}
|
||||
let { enableNodeTransitionMove, nodeTransitionMoveDuration } = this.mindMap.opt
|
||||
// 需要移除展开收缩按钮
|
||||
if (this._expandBtn && this.nodeData.children.length <= 0) {
|
||||
this.removeExpandBtn()
|
||||
let { enableNodeTransitionMove, nodeTransitionMoveDuration, alwaysShowExpandBtn } =
|
||||
this.mindMap.opt
|
||||
if (alwaysShowExpandBtn) {
|
||||
// 需要移除展开收缩按钮
|
||||
if (this._expandBtn && this.nodeData.children.length <= 0) {
|
||||
this.removeExpandBtn()
|
||||
} else {
|
||||
// 更新展开收起按钮
|
||||
this.renderExpandBtn()
|
||||
}
|
||||
} else {
|
||||
// 更新展开收起按钮
|
||||
this.renderExpandBtn()
|
||||
let { isActive, expand } = this.nodeData.data
|
||||
// 展开状态且非激活状态,且当前鼠标不在它上面,才隐藏
|
||||
if (expand && !isActive && !this._isMouseenter) {
|
||||
this.hideExpandBtn()
|
||||
} else {
|
||||
this.showExpandBtn()
|
||||
}
|
||||
}
|
||||
// 更新概要
|
||||
this.renderGeneralization()
|
||||
// 更新节点位置
|
||||
let t = this.group.transform()
|
||||
// 如果节点位置没有变化,则返回
|
||||
if (this.left === t.translateX && this.top === t.translateY) return
|
||||
if (!isLayout && enableNodeTransitionMove) {
|
||||
this.group
|
||||
.animate(nodeTransitionMoveDuration)
|
||||
.translate(
|
||||
this.left - t.translateX,
|
||||
this.top - t.translateY
|
||||
)
|
||||
.translate(this.left - t.translateX, this.top - t.translateY)
|
||||
} else {
|
||||
this.group.translate(
|
||||
this.left - t.translateX,
|
||||
this.top - t.translateY
|
||||
)
|
||||
this.group.translate(this.left - t.translateX, this.top - t.translateY)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,12 +488,15 @@ class Node {
|
||||
updateNodeShape() {
|
||||
if (!this.shapeNode) return
|
||||
const shape = this.getShape()
|
||||
this.style[shape === CONSTANTS.SHAPE.RECTANGLE ? 'rect' : 'shape'](this.shapeNode)
|
||||
this.style[shape === CONSTANTS.SHAPE.RECTANGLE ? 'rect' : 'shape'](
|
||||
this.shapeNode
|
||||
)
|
||||
}
|
||||
|
||||
// 递归渲染
|
||||
render(callback = () => {}) {
|
||||
let { enableNodeTransitionMove, nodeTransitionMoveDuration } = this.mindMap.opt
|
||||
let { enableNodeTransitionMove, nodeTransitionMoveDuration } =
|
||||
this.mindMap.opt
|
||||
// 节点
|
||||
// 重新渲染连线
|
||||
this.renderLine()
|
||||
@@ -553,7 +591,10 @@ class Node {
|
||||
this.hideGeneralization()
|
||||
if (this.parent) {
|
||||
let index = this.parent.children.indexOf(this)
|
||||
this.parent._lines[index].hide()
|
||||
this.parent._lines[index] && this.parent._lines[index].hide()
|
||||
this._lines.forEach(item => {
|
||||
item.hide()
|
||||
})
|
||||
}
|
||||
// 子节点
|
||||
if (this.children && this.children.length) {
|
||||
@@ -577,6 +618,9 @@ class Node {
|
||||
if (this.parent) {
|
||||
let index = this.parent.children.indexOf(this)
|
||||
this.parent._lines[index] && this.parent._lines[index].show()
|
||||
this._lines.forEach(item => {
|
||||
item.show()
|
||||
})
|
||||
}
|
||||
// 子节点
|
||||
if (this.children && this.children.length) {
|
||||
@@ -596,6 +640,13 @@ class Node {
|
||||
return
|
||||
}
|
||||
let childrenLen = this.nodeData.children.length
|
||||
// 切换为鱼骨结构时,清空根节点和二级节点的连线
|
||||
if (
|
||||
this.mindMap.opt.layout === CONSTANTS.LAYOUT.FISHBONE &&
|
||||
(this.isRoot || this.layerIndex === 1)
|
||||
) {
|
||||
childrenLen = 0
|
||||
}
|
||||
if (childrenLen > this._lines.length) {
|
||||
// 创建缺少的线
|
||||
new Array(childrenLen - this._lines.length).fill(0).forEach(() => {
|
||||
@@ -707,9 +758,10 @@ class Node {
|
||||
|
||||
// 获取padding值
|
||||
getPaddingVale() {
|
||||
let { isActive }= this.nodeData.data
|
||||
return {
|
||||
paddingX: this.getStyle('paddingX', true, this.nodeData.data.isActive),
|
||||
paddingY: this.getStyle('paddingY', true, this.nodeData.data.isActive)
|
||||
paddingX: this.getStyle('paddingX', true, isActive),
|
||||
paddingY: this.getStyle('paddingY', true, isActive)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import LogicalStructure from './layouts/LogicalStructure'
|
||||
import MindMap from './layouts/MindMap'
|
||||
import CatalogOrganization from './layouts/CatalogOrganization'
|
||||
import OrganizationStructure from './layouts/OrganizationStructure'
|
||||
import Timeline from './layouts/Timeline'
|
||||
import Fishbone from './layouts/Fishbone'
|
||||
import TextEdit from './TextEdit'
|
||||
import { copyNodeTree, simpleDeepClone, walk } from './utils'
|
||||
import { shapeList } from './Shape'
|
||||
@@ -18,7 +20,13 @@ const layouts = {
|
||||
// 目录组织图
|
||||
[CONSTANTS.LAYOUT.CATALOG_ORGANIZATION]: CatalogOrganization,
|
||||
// 组织结构图
|
||||
[CONSTANTS.LAYOUT.ORGANIZATION_STRUCTURE]: OrganizationStructure
|
||||
[CONSTANTS.LAYOUT.ORGANIZATION_STRUCTURE]: OrganizationStructure,
|
||||
// 时间轴
|
||||
[CONSTANTS.LAYOUT.TIMELINE]: Timeline,
|
||||
// 时间轴2
|
||||
[CONSTANTS.LAYOUT.TIMELINE2]: Timeline,
|
||||
// 鱼骨图
|
||||
[CONSTANTS.LAYOUT.FISHBONE]: Fishbone,
|
||||
}
|
||||
|
||||
// 渲染
|
||||
@@ -65,7 +73,7 @@ class Render {
|
||||
layouts[this.mindMap.opt.layout]
|
||||
? layouts[this.mindMap.opt.layout]
|
||||
: layouts[CONSTANTS.LAYOUT.LOGICAL_STRUCTURE]
|
||||
)(this)
|
||||
)(this, this.mindMap.opt.layout)
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
@@ -242,6 +250,7 @@ class Render {
|
||||
|
||||
// 渲染
|
||||
render(callback = () => {}, source) {
|
||||
let t = Date.now()
|
||||
// 如果当前还没有渲染完毕,不再触发渲染
|
||||
if (this.isRendering) {
|
||||
// 等待当前渲染完毕后再进行一次渲染
|
||||
@@ -257,7 +266,6 @@ class Render {
|
||||
// 重新渲染需要清除激活状态
|
||||
if (this.reRender) {
|
||||
this.clearActive()
|
||||
this.layout.clearNodePool()
|
||||
}
|
||||
// 计算布局
|
||||
this.layout.doLayout(root => {
|
||||
@@ -273,13 +281,30 @@ class Render {
|
||||
// 更新根节点
|
||||
this.root = root
|
||||
// 渲染节点
|
||||
this.root.render(() => {
|
||||
const onEnd = () => {
|
||||
this.isRendering = false
|
||||
this.mindMap.emit('node_tree_render_end')
|
||||
callback && callback()
|
||||
if (this.hasWaitRendering) {
|
||||
this.hasWaitRendering = false
|
||||
this.render(callback, source)
|
||||
} else {
|
||||
// 触发一次保存,因为修改了渲染树的数据
|
||||
if (this.mindMap.richText && [CONSTANTS.CHANGE_THEME, CONSTANTS.SET_DATA].includes(source)) {
|
||||
this.mindMap.command.addHistory()
|
||||
}
|
||||
}
|
||||
}
|
||||
let { enableNodeTransitionMove, nodeTransitionMoveDuration } =
|
||||
this.mindMap.opt
|
||||
this.root.render(() => {
|
||||
let dur = Date.now() - t
|
||||
if (enableNodeTransitionMove && dur <= nodeTransitionMoveDuration) {
|
||||
setTimeout(() => {
|
||||
onEnd()
|
||||
}, nodeTransitionMoveDuration - dur);
|
||||
} else {
|
||||
onEnd()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -345,6 +370,8 @@ class Render {
|
||||
if (!node.nodeData.data.isActive) {
|
||||
node.nodeData.data.isActive = true
|
||||
this.addActiveNode(node)
|
||||
// 激活节点需要显示展开收起按钮
|
||||
node.showExpandBtn()
|
||||
setTimeout(() => {
|
||||
node.updateNodeShape()
|
||||
}, 0)
|
||||
@@ -714,6 +741,12 @@ class Render {
|
||||
this.setNodeData(node, {
|
||||
isActive: active
|
||||
})
|
||||
// 切换激活状态,需要切换展开收起按钮的显隐
|
||||
if (active) {
|
||||
node.showExpandBtn()
|
||||
} else {
|
||||
node.hideExpandBtn()
|
||||
}
|
||||
node.updateNodeShape()
|
||||
}
|
||||
|
||||
@@ -728,14 +761,14 @@ class Render {
|
||||
item.render()
|
||||
})
|
||||
node.renderLine()
|
||||
node.updateExpandBtnNode()
|
||||
// node.updateExpandBtnNode()
|
||||
} else {
|
||||
// 收缩
|
||||
node.children.forEach(item => {
|
||||
item.remove()
|
||||
})
|
||||
node.removeLine()
|
||||
node.updateExpandBtnNode()
|
||||
// node.updateExpandBtnNode()
|
||||
}
|
||||
this.mindMap.render()
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import Quill from 'quill'
|
||||
import 'quill/dist/quill.snow.css'
|
||||
import './css/quill.css'
|
||||
import html2canvas from 'html2canvas'
|
||||
import { Image as SvgImage } from '@svgdotjs/svg.js'
|
||||
import { walk } from './utils'
|
||||
import { walk, getTextFromHtml } from './utils'
|
||||
import { CONSTANTS } from './utils/constant'
|
||||
|
||||
let extended = false
|
||||
@@ -41,8 +39,49 @@ class RichText {
|
||||
this.range = null
|
||||
this.lastRange = null
|
||||
this.node = null
|
||||
this.styleEl = null
|
||||
this.cacheEditingText = ''
|
||||
this.initOpt()
|
||||
this.extendQuill()
|
||||
this.appendCss()
|
||||
|
||||
// 处理数据,转成富文本格式
|
||||
if (this.mindMap.opt.data) {
|
||||
this.mindMap.opt.data = this.handleSetData(this.mindMap.opt.data)
|
||||
}
|
||||
}
|
||||
|
||||
// 插入样式
|
||||
appendCss() {
|
||||
let cssText = `
|
||||
.ql-editor {
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.ql-container {
|
||||
height: auto;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.ql-container.ql-snow {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.smm-richtext-node-wrap p {
|
||||
font-family: auto;
|
||||
}
|
||||
|
||||
.smm-richtext-node-edit-wrap p {
|
||||
font-family: auto;
|
||||
}
|
||||
`
|
||||
this.styleEl = document.createElement('style')
|
||||
this.styleEl.type = 'text/css'
|
||||
this.styleEl.innerHTML = cssText
|
||||
document.head.appendChild(this.styleEl)
|
||||
}
|
||||
|
||||
// 处理选项参数
|
||||
@@ -96,36 +135,46 @@ class RichText {
|
||||
if (!rect) rect = node._textData.node.node.getBoundingClientRect()
|
||||
this.mindMap.emit('before_show_text_edit')
|
||||
this.mindMap.renderer.textEdit.registerTmpShortcut()
|
||||
if (!this.textEditNode) {
|
||||
this.textEditNode = document.createElement('div')
|
||||
this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;box-shadow: 0 0 20px rgba(0,0,0,.5);outline: none; word-break: break-all;padding: 3px 5px;margin-left: -5px;margin-top: -3px;`
|
||||
document.body.appendChild(this.textEditNode)
|
||||
}
|
||||
// 原始宽高
|
||||
let g = node._textData.node
|
||||
let originWidth = g.attr('data-width')
|
||||
let originHeight = g.attr('data-height')
|
||||
// 缩放值
|
||||
let scaleX = rect.width / originWidth
|
||||
let scaleY = rect.height / originHeight
|
||||
// 内边距
|
||||
const paddingX = 6
|
||||
const paddingY = 4
|
||||
if (!this.textEditNode) {
|
||||
this.textEditNode = document.createElement('div')
|
||||
this.textEditNode.classList.add('smm-richtext-node-edit-wrap')
|
||||
this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;box-shadow: 0 0 20px rgba(0,0,0,.5);outline: none; word-break: break-all;padding: ${paddingY}px ${paddingX}px;`
|
||||
this.textEditNode.addEventListener('click', e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
document.body.appendChild(this.textEditNode)
|
||||
}
|
||||
// 使用节点的填充色,否则如果节点颜色是白色的话编辑时看不见
|
||||
let bgColor = node.style.merge('fillColor')
|
||||
this.textEditNode.style.marginLeft = `-${paddingX * scaleX}px`
|
||||
this.textEditNode.style.marginTop = `-${paddingY * scaleY}px`
|
||||
this.textEditNode.style.zIndex = this.mindMap.opt.nodeTextEditZIndex
|
||||
this.textEditNode.style.backgroundColor = bgColor === 'transparent' ? '#fff' : bgColor
|
||||
this.textEditNode.style.minWidth = originWidth + 'px'
|
||||
this.textEditNode.style.minWidth = originWidth + paddingX * 2 + 'px'
|
||||
this.textEditNode.style.minHeight = originHeight + 'px'
|
||||
this.textEditNode.style.left =
|
||||
rect.left + (rect.width - originWidth) / 2 + 'px'
|
||||
this.textEditNode.style.top =
|
||||
rect.top + (rect.height - originHeight) / 2 + 'px'
|
||||
this.textEditNode.style.left = rect.left + 'px'
|
||||
this.textEditNode.style.top = rect.top + 'px'
|
||||
this.textEditNode.style.display = 'block'
|
||||
this.textEditNode.style.maxWidth = this.mindMap.opt.textAutoWrapWidth + 'px'
|
||||
this.textEditNode.style.transform = `scale(${rect.width / originWidth}, ${
|
||||
rect.height / originHeight
|
||||
})`
|
||||
this.textEditNode.style.maxWidth = this.mindMap.opt.textAutoWrapWidth + paddingX * 2 + 'px'
|
||||
this.textEditNode.style.transform = `scale(${scaleX}, ${scaleY})`
|
||||
this.textEditNode.style.transformOrigin = 'left top'
|
||||
if (!node.nodeData.data.richText) {
|
||||
// 还不是富文本的情况
|
||||
let text = node.nodeData.data.text.split(/\n/gim).join('<br>')
|
||||
let html = `<p>${text}</p>`
|
||||
this.textEditNode.innerHTML = html
|
||||
this.textEditNode.innerHTML = this.cacheEditingText || html
|
||||
} else {
|
||||
this.textEditNode.innerHTML = node.nodeData.data.text
|
||||
this.textEditNode.innerHTML = this.cacheEditingText || node.nodeData.data.text
|
||||
}
|
||||
this.initQuillEditor()
|
||||
document.querySelector('.ql-editor').style.minHeight = originHeight + 'px'
|
||||
@@ -135,6 +184,7 @@ class RichText {
|
||||
// 如果是非富文本的情况,需要手动应用文本样式
|
||||
this.setTextStyleIfNotRichText(node)
|
||||
}
|
||||
this.cacheEditingText = ''
|
||||
}
|
||||
|
||||
// 如果是非富文本的情况,需要手动应用文本样式
|
||||
@@ -151,14 +201,19 @@ class RichText {
|
||||
this.formatAllText(style)
|
||||
}
|
||||
|
||||
// 获取当前正在编辑的内容
|
||||
getEditText() {
|
||||
let html = this.quill.container.firstChild.innerHTML
|
||||
// 去除最后的空行
|
||||
return html.replace(/<p><br><\/p>$/, '')
|
||||
}
|
||||
|
||||
// 隐藏文本编辑控件,即完成编辑
|
||||
hideEditText(nodes) {
|
||||
if (!this.showTextEdit) {
|
||||
return
|
||||
}
|
||||
let html = this.quill.container.firstChild.innerHTML
|
||||
// 去除最后的空行
|
||||
html = html.replace(/<p><br><\/p>$/, '')
|
||||
let html = this.getEditText()
|
||||
let list = nodes && nodes.length > 0 ? nodes : this.mindMap.renderer.activeNodeList
|
||||
list.forEach(node => {
|
||||
this.mindMap.execCommand('SET_NODE_TEXT', node, html, true)
|
||||
@@ -349,94 +404,40 @@ class RichText {
|
||||
return data
|
||||
}
|
||||
|
||||
// 将svg中嵌入的dom元素转换成图片
|
||||
async _handleSvgDomElements(svg) {
|
||||
svg = svg.clone()
|
||||
let foreignObjectList = svg.find('foreignObject')
|
||||
let task = foreignObjectList.map(async item => {
|
||||
let clone = item.first().node.cloneNode(true)
|
||||
let div = document.createElement('div')
|
||||
div.style.cssText = `position: fixed; left: -999999px;`
|
||||
div.appendChild(clone)
|
||||
this.mindMap.el.appendChild(div)
|
||||
let canvas = await html2canvas(clone, {
|
||||
backgroundColor: null
|
||||
})
|
||||
this.mindMap.el.removeChild(div)
|
||||
let imgNode = new SvgImage()
|
||||
.load(canvas.toDataURL())
|
||||
.size(canvas.width, canvas.height)
|
||||
item.replace(imgNode)
|
||||
})
|
||||
await Promise.all(task)
|
||||
return {
|
||||
svg: svg,
|
||||
svgHTML: svg.svg()
|
||||
// 处理导出为图片
|
||||
async handleExportPng(node) {
|
||||
let el = document.createElement('div')
|
||||
el.style.position = 'absolute'
|
||||
el.style.left = '-9999999px'
|
||||
el.appendChild(node)
|
||||
this.mindMap.el.appendChild(el)
|
||||
// 遍历所有节点,将它们的margin和padding设为0
|
||||
let walk = (root) => {
|
||||
root.style.margin = 0
|
||||
root.style.padding = 0
|
||||
if (root.hasChildNodes()) {
|
||||
Array.from(root.children).forEach((item) => {
|
||||
walk(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 将svg中嵌入的dom元素转换成图片
|
||||
handleSvgDomElements(svg) {
|
||||
return new Promise((resolve, reject) => {
|
||||
svg = svg.clone()
|
||||
let foreignObjectList = svg.find('foreignObject')
|
||||
let index = 0
|
||||
let len = foreignObjectList.length
|
||||
let transform = async () => {
|
||||
this.mindMap.emit('transforming-dom-to-images', index, len)
|
||||
try {
|
||||
let item = foreignObjectList[index++]
|
||||
let parent = item.parent()
|
||||
let clone = item.first().node.cloneNode(true)
|
||||
let div = document.createElement('div')
|
||||
div.style.cssText = `position: fixed; left: -999999px;`
|
||||
div.appendChild(clone)
|
||||
this.mindMap.el.appendChild(div)
|
||||
let canvas = await html2canvas(clone, {
|
||||
backgroundColor: null
|
||||
})
|
||||
// 优先使用原始宽高,因为当设备的window.devicePixelRatio不为1时,html2canvas输出的图片会更大
|
||||
let imgNodeWidth = parent.attr('data-width') || canvas.width
|
||||
let imgNodeHeight = parent.attr('data-height') || canvas.height
|
||||
this.mindMap.el.removeChild(div)
|
||||
let imgNode = new SvgImage()
|
||||
.load(canvas.toDataURL())
|
||||
.size(imgNodeWidth, imgNodeHeight)
|
||||
.x((parent ? parent.attr('data-offsetx') : 0) || 0)
|
||||
item.replace(imgNode)
|
||||
if (index <= len - 1) {
|
||||
setTimeout(() => {
|
||||
transform()
|
||||
}, 0)
|
||||
} else {
|
||||
resolve({
|
||||
svg: svg,
|
||||
svgHTML: svg.svg()
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
}
|
||||
if (len > 0) {
|
||||
transform()
|
||||
} else {
|
||||
resolve(null)
|
||||
}
|
||||
walk(node)
|
||||
let canvas = await html2canvas(el, {
|
||||
backgroundColor: null
|
||||
})
|
||||
this.mindMap.el.removeChild(el)
|
||||
return canvas.toDataURL()
|
||||
}
|
||||
|
||||
// 将所有节点转换成非富文本节点
|
||||
transformAllNodesToNormalNode() {
|
||||
let div = document.createElement('div')
|
||||
walk(
|
||||
this.mindMap.renderer.renderTree,
|
||||
null,
|
||||
node => {
|
||||
if (node.data.richText) {
|
||||
node.data.richText = false
|
||||
div.innerHTML = node.data.text
|
||||
node.data.text = div.textContent
|
||||
node.data.text = getTextFromHtml(node.data.text)
|
||||
// delete node.data.uid
|
||||
}
|
||||
},
|
||||
@@ -451,9 +452,27 @@ class RichText {
|
||||
this.mindMap.render(null, CONSTANTS.TRANSFORM_TO_NORMAL_NODE)
|
||||
}
|
||||
|
||||
// 处理导入数据
|
||||
handleSetData(data) {
|
||||
let walk = (root) => {
|
||||
if (!root.data.richText) {
|
||||
root.data.richText = true
|
||||
root.data.resetRichText = true
|
||||
}
|
||||
if (root.children && root.children.length > 0) {
|
||||
Array.from(root.children).forEach((item) => {
|
||||
walk(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
walk(data)
|
||||
return data
|
||||
}
|
||||
|
||||
// 插件被移除前做的事情
|
||||
beforePluginRemove() {
|
||||
this.transformAllNodesToNormalNode()
|
||||
document.head.removeChild(this.styleEl)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ class Select {
|
||||
|
||||
// 绑定事件
|
||||
bindEvent() {
|
||||
this.checkInNodes = throttle(this.checkInNodes, 500, this)
|
||||
this.checkInNodes = throttle(this.checkInNodes, 300, this)
|
||||
this.mindMap.on('mousedown', e => {
|
||||
if (this.mindMap.opt.readonly) {
|
||||
return
|
||||
@@ -146,22 +146,26 @@ class Select {
|
||||
let bottom = (top + height) * scaleY + translateY
|
||||
left = left * scaleX + translateX
|
||||
top = top * scaleY + translateY
|
||||
if (left >= minx && right <= maxx && top >= miny && bottom <= maxy) {
|
||||
this.mindMap.batchExecution.push('activeNode' + node.uid, () => {
|
||||
if ((left >= minx && left <= maxx ||
|
||||
right >= minx && right <= maxx) &&
|
||||
(top >= miny && top <= maxy ||
|
||||
bottom >= miny && bottom <= maxy)
|
||||
) {
|
||||
// this.mindMap.batchExecution.push('activeNode' + node.uid, () => {
|
||||
if (node.nodeData.data.isActive) {
|
||||
return
|
||||
}
|
||||
this.mindMap.renderer.setNodeActive(node, true)
|
||||
this.mindMap.renderer.addActiveNode(node)
|
||||
})
|
||||
// })
|
||||
} else if (node.nodeData.data.isActive) {
|
||||
this.mindMap.batchExecution.push('activeNode' + node.uid, () => {
|
||||
// this.mindMap.batchExecution.push('activeNode' + node.uid, () => {
|
||||
if (!node.nodeData.data.isActive) {
|
||||
return
|
||||
}
|
||||
this.mindMap.renderer.setNodeActive(node, false)
|
||||
this.mindMap.renderer.removeActiveNode(node)
|
||||
})
|
||||
// })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -109,6 +109,18 @@ class Style {
|
||||
})
|
||||
}
|
||||
|
||||
// 生成内联样式
|
||||
createStyleText() {
|
||||
return `
|
||||
color: ${this.merge('color')};
|
||||
font-family: ${this.merge('fontFamily')};
|
||||
font-size: ${this.merge('fontSize') + 'px'};
|
||||
font-weight: ${this.merge('fontWeight')};
|
||||
font-style: ${this.merge('fontStyle')};
|
||||
text-decoration: ${this.merge('textDecoration')}
|
||||
`
|
||||
}
|
||||
|
||||
// 获取文本样式
|
||||
getTextFontStyle() {
|
||||
return {
|
||||
@@ -120,11 +132,11 @@ class Style {
|
||||
}
|
||||
|
||||
// html文字节点
|
||||
domText(node, fontSizeScale = 1, textLines) {
|
||||
domText(node, fontSizeScale = 1, isMultiLine) {
|
||||
node.style.fontFamily = this.merge('fontFamily')
|
||||
node.style.fontSize = this.merge('fontSize') * fontSizeScale + 'px'
|
||||
node.style.fontWeight = this.merge('fontWeight') || 'normal'
|
||||
node.style.lineHeight = textLines === 1 ? 'normal' : this.merge('lineHeight')
|
||||
node.style.lineHeight = !isMultiLine ? 'normal' : this.merge('lineHeight')
|
||||
node.style.fontStyle = this.merge('fontStyle')
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getStrWithBrFromHtml } from './utils'
|
||||
import { getStrWithBrFromHtml, checkNodeOuter } from './utils'
|
||||
|
||||
// 节点文字编辑类
|
||||
export default class TextEdit {
|
||||
@@ -6,16 +6,21 @@ export default class TextEdit {
|
||||
constructor(renderer) {
|
||||
this.renderer = renderer
|
||||
this.mindMap = renderer.mindMap
|
||||
// 当前编辑的节点
|
||||
this.currentNode = null
|
||||
// 文本编辑框
|
||||
this.textEditNode = null
|
||||
// 文本编辑框是否显示
|
||||
this.showTextEdit = false
|
||||
// 如果编辑过程中缩放画布了,那么缓存当前编辑的内容
|
||||
this.cacheEditingText = ''
|
||||
this.bindEvent()
|
||||
}
|
||||
|
||||
// 事件
|
||||
bindEvent() {
|
||||
this.show = this.show.bind(this)
|
||||
this.onScale = this.onScale.bind(this)
|
||||
// 节点双击事件
|
||||
this.mindMap.on('node_dblclick', this.show)
|
||||
// 点击事件
|
||||
@@ -23,6 +28,12 @@ export default class TextEdit {
|
||||
// 隐藏文本编辑框
|
||||
this.hideEditTextBox()
|
||||
})
|
||||
this.mindMap.on('body_click', () => {
|
||||
// 隐藏文本编辑框
|
||||
if (this.mindMap.opt.isEndNodeTextEditOnClickOuter) {
|
||||
this.hideEditTextBox()
|
||||
}
|
||||
})
|
||||
this.mindMap.on('svg_mousedown', () => {
|
||||
// 隐藏文本编辑框
|
||||
this.hideEditTextBox()
|
||||
@@ -42,6 +53,7 @@ export default class TextEdit {
|
||||
}
|
||||
this.show(this.renderer.activeNodeList[0])
|
||||
})
|
||||
this.mindMap.on('scale', this.onScale)
|
||||
}
|
||||
|
||||
// 注册临时快捷键
|
||||
@@ -54,6 +66,9 @@ export default class TextEdit {
|
||||
|
||||
// 显示文本编辑框
|
||||
show(node) {
|
||||
this.currentNode = node
|
||||
let { offsetLeft, offsetTop } = checkNodeOuter(this.mindMap, node)
|
||||
this.mindMap.view.translateXY(offsetLeft, offsetTop)
|
||||
let rect = node._textData.node.node.getBoundingClientRect()
|
||||
if (this.mindMap.richText) {
|
||||
this.mindMap.richText.showEditText(node, rect)
|
||||
@@ -62,6 +77,19 @@ export default class TextEdit {
|
||||
this.showEditTextBox(node, rect)
|
||||
}
|
||||
|
||||
// 处理画布缩放
|
||||
onScale() {
|
||||
if (!this.currentNode) return
|
||||
if (this.mindMap.richText) {
|
||||
this.mindMap.richText.cacheEditingText = this.mindMap.richText.getEditText()
|
||||
this.mindMap.richText.showTextEdit = false
|
||||
} else {
|
||||
this.cacheEditingText = this.getEditText()
|
||||
this.showTextEdit = false
|
||||
}
|
||||
this.show(this.currentNode)
|
||||
}
|
||||
|
||||
// 显示文本编辑框
|
||||
showEditTextBox(node, rect) {
|
||||
this.mindMap.emit('before_show_text_edit')
|
||||
@@ -73,13 +101,18 @@ export default class TextEdit {
|
||||
this.textEditNode.addEventListener('keyup', e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
this.textEditNode.addEventListener('click', e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
document.body.appendChild(this.textEditNode)
|
||||
}
|
||||
let scale = this.mindMap.view.scale
|
||||
let lineHeight = node.style.merge('lineHeight')
|
||||
let fontSize = node.style.merge('fontSize')
|
||||
let textLines = node.nodeData.data.text.split(/\n/gim)
|
||||
node.style.domText(this.textEditNode, scale, textLines.length)
|
||||
let textLines = (this.cacheEditingText || node.nodeData.data.text).split(/\n/gim)
|
||||
let isMultiLine = node._textData.node.attr('data-ismultiLine') === 'true'
|
||||
node.style.domText(this.textEditNode, scale, isMultiLine)
|
||||
this.textEditNode.style.zIndex = this.mindMap.opt.nodeTextEditZIndex
|
||||
this.textEditNode.innerHTML = textLines.join('<br>')
|
||||
this.textEditNode.style.minWidth = rect.width + 10 + 'px'
|
||||
this.textEditNode.style.minHeight = rect.height + 6 + 'px'
|
||||
@@ -87,12 +120,15 @@ export default class TextEdit {
|
||||
this.textEditNode.style.top = rect.top + 'px'
|
||||
this.textEditNode.style.display = 'block'
|
||||
this.textEditNode.style.maxWidth = this.mindMap.opt.textAutoWrapWidth * scale + 'px'
|
||||
if (textLines.length > 1 && lineHeight !== 1) {
|
||||
this.textEditNode.style.transform = `translateY(${-((lineHeight * fontSize - fontSize) / 2 - 2) * scale}px)`
|
||||
if (isMultiLine && lineHeight !== 1) {
|
||||
this.textEditNode.style.transform = `translateY(${-((lineHeight * fontSize - fontSize) / 2) * scale}px)`
|
||||
}
|
||||
this.showTextEdit = true
|
||||
// 选中文本
|
||||
this.selectNodeText()
|
||||
if (!this.cacheEditingText) {
|
||||
this.selectNodeText()
|
||||
}
|
||||
this.cacheEditingText = ''
|
||||
}
|
||||
|
||||
// 选中文本
|
||||
@@ -104,8 +140,14 @@ export default class TextEdit {
|
||||
selection.addRange(range)
|
||||
}
|
||||
|
||||
// 获取当前正在编辑的内容
|
||||
getEditText() {
|
||||
return getStrWithBrFromHtml(this.textEditNode.innerHTML)
|
||||
}
|
||||
|
||||
// 隐藏文本编辑框
|
||||
hideEditTextBox() {
|
||||
this.currentNode = null
|
||||
if (this.mindMap.richText) {
|
||||
return this.mindMap.richText.hideEditText()
|
||||
}
|
||||
@@ -113,7 +155,7 @@ export default class TextEdit {
|
||||
return
|
||||
}
|
||||
this.renderer.activeNodeList.forEach(node => {
|
||||
let str = getStrWithBrFromHtml(this.textEditNode.innerHTML)
|
||||
let str = this.getEditText()
|
||||
this.mindMap.execCommand('SET_NODE_TEXT', node, str)
|
||||
if (node.isGeneralization) {
|
||||
// 概要节点
|
||||
|
||||
@@ -124,6 +124,13 @@ class View {
|
||||
}
|
||||
}
|
||||
|
||||
// 平移x,y方向
|
||||
translateXY(x, y) {
|
||||
this.x += x
|
||||
this.y += y
|
||||
this.transform()
|
||||
}
|
||||
|
||||
// 平移x方向
|
||||
translateX(step) {
|
||||
this.x += step
|
||||
@@ -160,10 +167,14 @@ class View {
|
||||
|
||||
// 恢复
|
||||
reset() {
|
||||
let scaleChange = this.scale !== 1
|
||||
this.scale = 1
|
||||
this.x = 0
|
||||
this.y = 0
|
||||
this.transform()
|
||||
if (scaleChange) {
|
||||
this.mindMap.emit('scale', this.scale)
|
||||
}
|
||||
}
|
||||
|
||||
// 缩小
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
.ql-editor {
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.ql-container {
|
||||
height: auto;
|
||||
font-size: inherit;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import Node from '../Node'
|
||||
import { CONSTANTS, initRootNodePositionMap } from '../utils/constant'
|
||||
import Lru from '../utils/Lru'
|
||||
|
||||
// 布局基类
|
||||
class Base {
|
||||
@@ -13,8 +14,7 @@ class Base {
|
||||
this.draw = this.mindMap.draw
|
||||
// 根节点
|
||||
this.root = null
|
||||
// 保存所有uid和节点,用于复用
|
||||
this.nodePool = {}
|
||||
this.lru = new Lru(this.mindMap.opt.maxNodeCacheCount)
|
||||
}
|
||||
|
||||
// 计算节点位置
|
||||
@@ -40,16 +40,7 @@ class Base {
|
||||
// 记录本次渲染时的节点
|
||||
this.renderer.nodeCache[uid] = node
|
||||
// 记录所有渲染时的节点
|
||||
this.nodePool[uid] = node
|
||||
// 如果总缓存数量达到1000,直接清空
|
||||
if (Object.keys(this.nodePool).length > 1000) {
|
||||
this.clearNodePool()
|
||||
}
|
||||
}
|
||||
|
||||
// 清空节点存储池
|
||||
clearNodePool() {
|
||||
this.nodePool = {}
|
||||
this.lru.add(uid, node)
|
||||
}
|
||||
|
||||
// 检查当前来源是否需要重新计算节点大小
|
||||
@@ -72,9 +63,9 @@ class Base {
|
||||
newNode.getSize()
|
||||
newNode.needLayout = true
|
||||
}
|
||||
} else if (this.nodePool[data.data.uid] && !this.renderer.reRender) {
|
||||
} else if (this.lru.has(data.data.uid) && !this.renderer.reRender) {
|
||||
// 数据上没有保存节点引用,但是通过uid找到了缓存的节点,也可以复用
|
||||
newNode = this.nodePool[data.data.uid]
|
||||
newNode = this.lru.get(data.data.uid)
|
||||
// 保存该节点上一次的数据
|
||||
let lastData = JSON.stringify(newNode.nodeData.data)
|
||||
newNode.reset()
|
||||
@@ -157,6 +148,37 @@ class Base {
|
||||
})
|
||||
}
|
||||
|
||||
// 更新子节点多个属性
|
||||
updateChildrenPro(children, props) {
|
||||
children.forEach(item => {
|
||||
Object.keys(props).forEach((prop) => {
|
||||
item[prop] += props[prop]
|
||||
})
|
||||
if (item.children && item.children.length && !item.hasCustomPosition()) {
|
||||
// 适配自定义位置
|
||||
this.updateChildrenPro(item.children, props)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 递归计算节点的宽度
|
||||
getNodeAreaWidth(node) {
|
||||
let widthArr = []
|
||||
let loop = (node, width) => {
|
||||
if (node.children.length) {
|
||||
width += node.width / 2
|
||||
node.children.forEach(item => {
|
||||
loop(item, width)
|
||||
})
|
||||
} else {
|
||||
width += node.width
|
||||
widthArr.push(width)
|
||||
}
|
||||
}
|
||||
loop(node, 0)
|
||||
return Math.max(...widthArr)
|
||||
}
|
||||
|
||||
// 二次贝塞尔曲线
|
||||
quadraticCurvePath(x1, y1, x2, y2) {
|
||||
let cx = x1 + (x2 - x1) * 0.2
|
||||
@@ -266,6 +288,11 @@ class Base {
|
||||
generalizationNodeMargin
|
||||
}
|
||||
}
|
||||
|
||||
// 获取节点实际存在几个子节点
|
||||
getNodeActChildrenLength(node) {
|
||||
return node.nodeData.children && node.nodeData.children.length
|
||||
}
|
||||
}
|
||||
|
||||
export default Base
|
||||
|
||||
@@ -87,11 +87,11 @@ class CatalogOrganization extends Base {
|
||||
totalLeft += cur.width + marginX
|
||||
})
|
||||
} else {
|
||||
let totalTop = node.top + node.height + marginY + node.expandBtnSize
|
||||
let totalTop = node.top + node.height + 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 + node.expandBtnSize
|
||||
totalTop += cur.height + marginY + (this.getNodeActChildrenLength(cur) > 0 ? cur.expandBtnSize : 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -115,7 +115,7 @@ class CatalogOrganization extends Base {
|
||||
let areaWidth = this.getNodeAreaWidth(node)
|
||||
let difference = areaWidth - node.width
|
||||
if (difference > 0) {
|
||||
this.updateBrothersLeft(node, difference / 2)
|
||||
this.updateBrothersLeft(node, difference)
|
||||
}
|
||||
}
|
||||
// 调整top
|
||||
@@ -124,36 +124,24 @@ class CatalogOrganization extends Base {
|
||||
let marginY = this.getMarginY(layerIndex + 1)
|
||||
let totalHeight =
|
||||
node.children.reduce((h, item) => {
|
||||
return h + item.height
|
||||
return h + item.height + (this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
}, 0) +
|
||||
(len + 1) * marginY +
|
||||
len * node.expandBtnSize
|
||||
len * marginY
|
||||
this.updateBrothersTop(node, totalHeight)
|
||||
}
|
||||
},
|
||||
null,
|
||||
(node, parent, isRoot) => {
|
||||
if (isRoot) {
|
||||
let { right, left } = this.getNodeBoundaries(node, 'h')
|
||||
let childrenWidth = right - left
|
||||
let offset = (node.left - left) - (childrenWidth - node.width) / 2
|
||||
this.updateChildren(node.children, 'left', offset)
|
||||
}
|
||||
},
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 递归计算节点的宽度
|
||||
getNodeAreaWidth(node) {
|
||||
let widthArr = []
|
||||
let loop = (node, width) => {
|
||||
if (node.children.length) {
|
||||
width += node.width / 2
|
||||
node.children.forEach(item => {
|
||||
loop(item, width)
|
||||
})
|
||||
} else {
|
||||
width += node.width
|
||||
widthArr.push(width)
|
||||
}
|
||||
}
|
||||
loop(node, 0)
|
||||
return Math.max(...widthArr)
|
||||
}
|
||||
|
||||
// 调整兄弟节点的left
|
||||
updateBrothersLeft(node, addWidth) {
|
||||
if (node.parent) {
|
||||
@@ -161,38 +149,15 @@ class CatalogOrganization extends Base {
|
||||
let index = childrenList.findIndex(item => {
|
||||
return item === node
|
||||
})
|
||||
// 存在大于一个节点时,第一个或最后一个节点自身也需要移动,否则两边不对称
|
||||
if (
|
||||
(index === 0 || index === childrenList.length - 1) &&
|
||||
childrenList.length > 1
|
||||
) {
|
||||
let _offset = index === 0 ? -addWidth : addWidth
|
||||
node.left += _offset
|
||||
if (
|
||||
node.children &&
|
||||
node.children.length &&
|
||||
!node.hasCustomPosition()
|
||||
) {
|
||||
this.updateChildren(node.children, 'left', _offset)
|
||||
}
|
||||
}
|
||||
childrenList.forEach((item, _index) => {
|
||||
if (item.hasCustomPosition()) {
|
||||
if (item.hasCustomPosition() || _index <= index) {
|
||||
// 适配自定义位置
|
||||
return
|
||||
}
|
||||
let _offset = 0
|
||||
if (_index < index) {
|
||||
// 左边的节点往左移
|
||||
_offset = -addWidth
|
||||
} else if (_index > index) {
|
||||
// 右边的节点往右移
|
||||
_offset = addWidth
|
||||
}
|
||||
item.left += _offset
|
||||
item.left += addWidth
|
||||
// 同步更新子节点的位置
|
||||
if (item.children && item.children.length) {
|
||||
this.updateChildren(item.children, 'left', _offset)
|
||||
this.updateChildren(item.children, 'left', addWidth)
|
||||
}
|
||||
})
|
||||
// 更新父节点的位置
|
||||
@@ -234,6 +199,9 @@ class CatalogOrganization extends Base {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize } = node
|
||||
if (!this.mindMap.opt.alwaysShowExpandBtn) {
|
||||
expandBtnSize = 0
|
||||
}
|
||||
let len = node.children.length
|
||||
let marginX = this.getMarginX(node.layerIndex + 1)
|
||||
if (node.isRoot) {
|
||||
|
||||
378
simple-mind-map/src/layouts/Fishbone.js
Normal file
@@ -0,0 +1,378 @@
|
||||
import Base from './Base'
|
||||
import { walk, asyncRun, degToRad } from '../utils'
|
||||
import { CONSTANTS } from '../utils/constant'
|
||||
import utils from './fishboneUtils'
|
||||
|
||||
// 鱼骨图
|
||||
class Fishbone extends Base {
|
||||
// 构造函数
|
||||
constructor(opt = {}) {
|
||||
super(opt)
|
||||
this.indent = 0.3
|
||||
this.childIndent = 0.5
|
||||
}
|
||||
|
||||
// 布局
|
||||
doLayout(callback) {
|
||||
let task = [
|
||||
() => {
|
||||
this.computedBaseValue()
|
||||
},
|
||||
() => {
|
||||
this.computedLeftTopValue()
|
||||
},
|
||||
() => {
|
||||
this.adjustLeftTopValue()
|
||||
},
|
||||
() => {
|
||||
callback(this.root)
|
||||
}
|
||||
]
|
||||
asyncRun(task)
|
||||
}
|
||||
|
||||
// 遍历数据创建节点、计算根节点的位置,计算根节点的子节点的top值
|
||||
computedBaseValue() {
|
||||
walk(
|
||||
this.renderer.renderTree,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex, index) => {
|
||||
// 创建节点
|
||||
let newNode = this.createNode(node, parent, isRoot, layerIndex)
|
||||
// 根节点定位在画布中心位置
|
||||
if (isRoot) {
|
||||
this.setNodeCenter(newNode)
|
||||
} else {
|
||||
// 非根节点
|
||||
// 三级及以下节点以上级方向为准
|
||||
if (parent._node.dir) {
|
||||
newNode.dir = parent._node.dir
|
||||
} else {
|
||||
// 节点生长方向
|
||||
newNode.dir =
|
||||
index % 2 === 0
|
||||
? CONSTANTS.TIMELINE_DIR.TOP
|
||||
: CONSTANTS.TIMELINE_DIR.BOTTOM
|
||||
}
|
||||
// 计算二级节点的top值
|
||||
if (parent._node.isRoot) {
|
||||
if (this.checkIsTop(newNode)) {
|
||||
newNode.top = parent._node.top - newNode.height
|
||||
} else {
|
||||
newNode.top = parent._node.top + parent._node.height
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!node.data.expand) {
|
||||
return true
|
||||
}
|
||||
},
|
||||
null,
|
||||
true,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
// 遍历节点树计算节点的left、top
|
||||
computedLeftTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (node.isRoot) {
|
||||
let topTotalLeft = node.left + node.width + node.height
|
||||
let bottomTotalLeft = node.left + node.width + node.height
|
||||
node.children.forEach(item => {
|
||||
if (this.checkIsTop(item)) {
|
||||
item.left = topTotalLeft
|
||||
topTotalLeft += item.width
|
||||
} else {
|
||||
item.left = bottomTotalLeft + 20
|
||||
bottomTotalLeft += item.width
|
||||
}
|
||||
})
|
||||
}
|
||||
let params = { layerIndex, node, ctx: this }
|
||||
if (this.checkIsTop(node)) {
|
||||
utils.top.computedLeftTopValue(params)
|
||||
} else {
|
||||
utils.bottom.computedLeftTopValue(params)
|
||||
}
|
||||
},
|
||||
null,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 调整节点left、top
|
||||
adjustLeftTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (!node.nodeData.data.expand) {
|
||||
return
|
||||
}
|
||||
let params = { node, parent, layerIndex, ctx: this }
|
||||
if (this.checkIsTop(node)) {
|
||||
utils.top.adjustLeftTopValueBefore(params)
|
||||
} else {
|
||||
utils.bottom.adjustLeftTopValueBefore(params)
|
||||
}
|
||||
},
|
||||
(node, parent) => {
|
||||
let params = { parent, node, ctx: this }
|
||||
if (this.checkIsTop(node)) {
|
||||
utils.top.adjustLeftTopValueAfter(params)
|
||||
} else {
|
||||
utils.bottom.adjustLeftTopValueAfter(params)
|
||||
}
|
||||
// 调整二级节点的子节点的left值
|
||||
if (node.isRoot) {
|
||||
let topTotalLeft = 0
|
||||
let bottomTotalLeft = 0
|
||||
node.children.forEach(item => {
|
||||
if (this.checkIsTop(item)) {
|
||||
item.left += topTotalLeft
|
||||
this.updateChildren(item.children, 'left', topTotalLeft)
|
||||
let { left, right } = this.getNodeBoundaries(item, 'h')
|
||||
topTotalLeft += right - left
|
||||
} else {
|
||||
item.left += bottomTotalLeft
|
||||
this.updateChildren(item.children, 'left', bottomTotalLeft)
|
||||
let { left, right } = this.getNodeBoundaries(item, 'h')
|
||||
bottomTotalLeft += right - left
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 递归计算节点的宽度
|
||||
getNodeAreaHeight(node) {
|
||||
let totalHeight = 0
|
||||
let loop = node => {
|
||||
totalHeight +=
|
||||
node.height +
|
||||
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
if (node.children.length) {
|
||||
node.children.forEach(item => {
|
||||
loop(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
loop(node)
|
||||
return totalHeight
|
||||
}
|
||||
|
||||
// 调整兄弟节点的left
|
||||
updateBrothersLeft(node) {
|
||||
let childrenList = node.children
|
||||
let totalAddWidth = 0
|
||||
childrenList.forEach(item => {
|
||||
item.left += totalAddWidth
|
||||
if (item.children && item.children.length) {
|
||||
this.updateChildren(item.children, 'left', totalAddWidth)
|
||||
}
|
||||
let { left, right } = this.getNodeBoundaries(item, 'h')
|
||||
let areaWidth = right - left
|
||||
let difference = areaWidth - item.width
|
||||
if (difference > 0) {
|
||||
totalAddWidth += difference
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 调整兄弟节点的top
|
||||
updateBrothersTop(node, addHeight) {
|
||||
if (node.parent && !node.parent.isRoot) {
|
||||
let childrenList = node.parent.children
|
||||
let index = childrenList.findIndex(item => {
|
||||
return item === node
|
||||
})
|
||||
childrenList.forEach((item, _index) => {
|
||||
if (item.hasCustomPosition()) {
|
||||
// 适配自定义位置
|
||||
return
|
||||
}
|
||||
let _offset = 0
|
||||
// 下面的节点往下移
|
||||
if (_index > index) {
|
||||
_offset = addHeight
|
||||
}
|
||||
item.top += _offset
|
||||
// 同步更新子节点的位置
|
||||
if (item.children && item.children.length) {
|
||||
this.updateChildren(item.children, 'top', _offset)
|
||||
}
|
||||
})
|
||||
// 更新父节点的位置
|
||||
if (this.checkIsTop(node)) {
|
||||
this.updateBrothersTop(node.parent, addHeight)
|
||||
} else {
|
||||
this.updateBrothersTop(
|
||||
node.parent,
|
||||
node.layerIndex === 3 ? 0 : addHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查节点是否是上方节点
|
||||
checkIsTop(node) {
|
||||
return node.dir === CONSTANTS.TIMELINE_DIR.TOP
|
||||
}
|
||||
|
||||
// 绘制连线,连接该节点到其子节点
|
||||
renderLine(node, lines, style) {
|
||||
if (node.layerIndex !== 1 && node.children.length <= 0) {
|
||||
return []
|
||||
}
|
||||
let { top, height, expandBtnSize } = node
|
||||
if (!this.mindMap.opt.alwaysShowExpandBtn) {
|
||||
expandBtnSize = 0
|
||||
}
|
||||
let len = node.children.length
|
||||
if (node.isRoot) {
|
||||
// 当前节点是根节点
|
||||
// 根节点的子节点是和根节点同一水平线排列
|
||||
let maxx = -Infinity
|
||||
node.children.forEach((item) => {
|
||||
if (item.left > maxx) {
|
||||
maxx = item.left
|
||||
}
|
||||
// 水平线段到二级节点的连线
|
||||
let nodeLineX = item.left
|
||||
let offset = node.height / 2
|
||||
let offsetX = offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
|
||||
let line = this.draw.path()
|
||||
if (this.checkIsTop(item)) {
|
||||
line.plot(
|
||||
`M ${nodeLineX - offsetX},${item.top + item.height + offset} L ${item.left},${
|
||||
item.top + item.height
|
||||
}`
|
||||
)
|
||||
} else {
|
||||
line.plot(
|
||||
`M ${nodeLineX - offsetX},${
|
||||
item.top - offset
|
||||
} L ${nodeLineX},${item.top}`
|
||||
)
|
||||
}
|
||||
node.style.line(line)
|
||||
node._lines.push(line)
|
||||
style && style(line, node)
|
||||
})
|
||||
// 从根节点出发的水平线
|
||||
let nodeHalfTop = node.top + node.height / 2
|
||||
let offset = node.height / 2
|
||||
let line = this.draw.path()
|
||||
line.plot(
|
||||
`M ${node.left + node.width},${nodeHalfTop} L ${
|
||||
maxx - offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
|
||||
},${nodeHalfTop}`
|
||||
)
|
||||
node.style.line(line)
|
||||
node._lines.push(line)
|
||||
style && style(line, node)
|
||||
} else {
|
||||
// 当前节点为非根节点
|
||||
let maxy = -Infinity
|
||||
let miny = Infinity
|
||||
let maxx = -Infinity
|
||||
let x = node.left + node.width * this.indent
|
||||
node.children.forEach((item, index) => {
|
||||
if (item.left > maxx) {
|
||||
maxx = item.left
|
||||
}
|
||||
let y = item.top + item.height / 2
|
||||
if (y > maxy) {
|
||||
maxy = y
|
||||
}
|
||||
if (y < miny) {
|
||||
miny = y
|
||||
}
|
||||
// 水平线
|
||||
if (node.layerIndex > 1) {
|
||||
let path = `M ${x},${y} L ${item.left},${y}`
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
}
|
||||
})
|
||||
// 斜线
|
||||
if (len >= 0) {
|
||||
let line = this.draw.path()
|
||||
expandBtnSize = len > 0 ? expandBtnSize : 0
|
||||
let lineLength = maxx - node.left - node.width * this.indent
|
||||
lineLength = Math.max(lineLength, 0)
|
||||
let params = {
|
||||
node,
|
||||
line,
|
||||
top,
|
||||
x,
|
||||
lineLength,
|
||||
height,
|
||||
expandBtnSize,
|
||||
maxy,
|
||||
miny,
|
||||
ctx: this
|
||||
}
|
||||
if (this.checkIsTop(node)) {
|
||||
utils.top.renderLine(params)
|
||||
} else {
|
||||
utils.bottom.renderLine(params)
|
||||
}
|
||||
node.style.line(line)
|
||||
node._lines.push(line)
|
||||
style && style(line, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染按钮
|
||||
renderExpandBtn(node, btn) {
|
||||
let { width, height, expandBtnSize, isRoot } = node
|
||||
if (!isRoot) {
|
||||
let { translateX, translateY } = btn.transform()
|
||||
let params = {
|
||||
node,
|
||||
btn,
|
||||
expandBtnSize,
|
||||
translateX,
|
||||
translateY,
|
||||
width,
|
||||
height
|
||||
}
|
||||
if (this.checkIsTop(node)) {
|
||||
utils.top.renderExpandBtn(params)
|
||||
} else {
|
||||
utils.bottom.renderExpandBtn(params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建概要节点
|
||||
renderGeneralization(node, gLine, gNode) {
|
||||
let {
|
||||
top,
|
||||
bottom,
|
||||
right,
|
||||
generalizationLineMargin,
|
||||
generalizationNodeMargin
|
||||
} = this.getNodeBoundaries(node, 'h')
|
||||
let x1 = right + generalizationLineMargin
|
||||
let y1 = top
|
||||
let x2 = right + generalizationLineMargin
|
||||
let y2 = bottom
|
||||
let cx = x1 + 20
|
||||
let cy = y1 + (y2 - y1) / 2
|
||||
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
||||
gLine.plot(path)
|
||||
gNode.left = right + generalizationNodeMargin
|
||||
gNode.top = top + (bottom - top - gNode.height) / 2
|
||||
}
|
||||
}
|
||||
|
||||
export default Fishbone
|
||||
369
simple-mind-map/src/layouts/FishboneBottom.js
Normal file
@@ -0,0 +1,369 @@
|
||||
import Base from './Base'
|
||||
import { walk, asyncRun } from '../utils'
|
||||
import { CONSTANTS } from '../utils/constant'
|
||||
|
||||
const degToRad = deg => {
|
||||
return (Math.PI / 180) * deg
|
||||
}
|
||||
|
||||
// 下方鱼骨图
|
||||
class Fishbone extends Base {
|
||||
// 构造函数
|
||||
constructor(opt = {}) {
|
||||
super(opt)
|
||||
}
|
||||
|
||||
// 布局
|
||||
doLayout(callback) {
|
||||
let task = [
|
||||
() => {
|
||||
this.computedBaseValue()
|
||||
},
|
||||
() => {
|
||||
this.computedLeftTopValue()
|
||||
},
|
||||
() => {
|
||||
this.adjustLeftTopValue()
|
||||
},
|
||||
() => {
|
||||
callback(this.root)
|
||||
}
|
||||
]
|
||||
asyncRun(task)
|
||||
}
|
||||
|
||||
// 遍历数据创建节点、计算根节点的位置,计算根节点的子节点的top值
|
||||
computedBaseValue() {
|
||||
walk(
|
||||
this.renderer.renderTree,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex, index) => {
|
||||
// 创建节点
|
||||
let newNode = this.createNode(node, parent, isRoot, layerIndex)
|
||||
// 根节点定位在画布中心位置
|
||||
if (isRoot) {
|
||||
this.setNodeCenter(newNode)
|
||||
} else {
|
||||
// 非根节点
|
||||
// 三级及以下节点以上级方向为准
|
||||
if (parent._node.dir) {
|
||||
newNode.dir = parent._node.dir
|
||||
} else {
|
||||
// 节点生长方向
|
||||
newNode.dir =
|
||||
index % 2 === 0
|
||||
? CONSTANTS.TIMELINE_DIR.TOP
|
||||
: CONSTANTS.TIMELINE_DIR.BOTTOM
|
||||
}
|
||||
// 计算二级节点的top值
|
||||
if (parent._node.isRoot) {
|
||||
newNode.top = parent._node.top + parent._node.height
|
||||
}
|
||||
}
|
||||
if (!node.data.expand) {
|
||||
return true
|
||||
}
|
||||
},
|
||||
null,
|
||||
true,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
// 遍历节点树计算节点的left、top
|
||||
computedLeftTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex, index) => {
|
||||
if (node.isRoot) {
|
||||
let totalLeft = node.left + node.width
|
||||
node.children.forEach(item => {
|
||||
item.left = totalLeft
|
||||
totalLeft += item.width
|
||||
})
|
||||
}
|
||||
if (layerIndex === 1 && node.children) {
|
||||
// 遍历二级节点的子节点
|
||||
let startLeft = node.left + node.width * 0.5
|
||||
let totalTop =
|
||||
node.top +
|
||||
node.height +
|
||||
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
|
||||
node.children.forEach(item => {
|
||||
item.left = startLeft
|
||||
item.top =
|
||||
totalTop +
|
||||
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
totalTop +=
|
||||
item.height +
|
||||
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
})
|
||||
}
|
||||
if (layerIndex > 1 && node.children) {
|
||||
// 遍历三级及以下节点的子节点
|
||||
let startLeft = node.left + node.width * 0.5
|
||||
let totalTop =
|
||||
node.top -
|
||||
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
node.children.forEach(item => {
|
||||
item.left = startLeft
|
||||
item.top = totalTop - item.height
|
||||
totalTop -=
|
||||
item.height +
|
||||
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
})
|
||||
}
|
||||
},
|
||||
null,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 调整节点left、top
|
||||
adjustLeftTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (!node.nodeData.data.expand) {
|
||||
return
|
||||
}
|
||||
// 调整top
|
||||
let len = node.children.length
|
||||
// 调整三级节点的top
|
||||
// if (layerIndex === 2 && len > 0) {
|
||||
// let totalHeight = node.children.reduce((h, item) => {
|
||||
// return h + item.height
|
||||
// }, 0)
|
||||
// this.updateBrothersTop(node, totalHeight)
|
||||
// }
|
||||
if (layerIndex > 2 && len > 0) {
|
||||
let totalHeight = node.children.reduce((h, item) => {
|
||||
return (
|
||||
h +
|
||||
item.height +
|
||||
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
)
|
||||
}, 0)
|
||||
this.updateBrothersTop(node, -totalHeight)
|
||||
}
|
||||
},
|
||||
(node, parent) => {
|
||||
// 将二级节点的子节点移到上方
|
||||
if (parent && parent.isRoot) {
|
||||
// 遍历二级节点的子节点
|
||||
let totalHeight = 0
|
||||
let totalHeight2 = 0
|
||||
node.children.forEach(item => {
|
||||
// 调整top
|
||||
let hasChildren = this.getNodeActChildrenLength(item) > 0
|
||||
let nodeTotalHeight = this.getNodeAreaHeight(item)
|
||||
let offset =
|
||||
hasChildren > 0
|
||||
? nodeTotalHeight -
|
||||
item.height -
|
||||
(hasChildren ? item.expandBtnSize : 0)
|
||||
: 0
|
||||
let _top = totalHeight + offset
|
||||
item.top += _top
|
||||
// 调整left
|
||||
let offsetLeft =
|
||||
(totalHeight2 + nodeTotalHeight) / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
|
||||
item.left += offsetLeft
|
||||
totalHeight += offset
|
||||
totalHeight2 += nodeTotalHeight
|
||||
// 同步更新后代节点
|
||||
this.updateChildrenPro(item.children, {
|
||||
top: _top,
|
||||
left: offsetLeft
|
||||
})
|
||||
})
|
||||
}
|
||||
// 调整二级节点的子节点的left值
|
||||
if (node.isRoot) {
|
||||
let totalLeft = 0
|
||||
node.children.forEach(item => {
|
||||
item.left += totalLeft
|
||||
this.updateChildren(item.children, 'left', totalLeft)
|
||||
let { left, right } = this.getNodeBoundaries(item, 'h')
|
||||
totalLeft += right - left
|
||||
})
|
||||
}
|
||||
},
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 递归计算节点的宽度
|
||||
getNodeAreaHeight(node) {
|
||||
let totalHeight = 0
|
||||
let loop = node => {
|
||||
totalHeight +=
|
||||
node.height +
|
||||
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
if (node.children.length) {
|
||||
node.children.forEach(item => {
|
||||
loop(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
loop(node)
|
||||
return totalHeight
|
||||
}
|
||||
|
||||
// 调整兄弟节点的left
|
||||
updateBrothersLeft(node) {
|
||||
let childrenList = node.children
|
||||
let totalAddWidth = 0
|
||||
childrenList.forEach(item => {
|
||||
item.left += totalAddWidth
|
||||
if (item.children && item.children.length) {
|
||||
this.updateChildren(item.children, 'left', totalAddWidth)
|
||||
}
|
||||
// let areaWidth = this.getNodeAreaWidth(item)
|
||||
let { left, right } = this.getNodeBoundaries(item, 'h')
|
||||
let areaWidth = right - left
|
||||
let difference = areaWidth - item.width
|
||||
if (difference > 0) {
|
||||
totalAddWidth += difference
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 调整兄弟节点的top
|
||||
updateBrothersTop(node, addHeight) {
|
||||
if (node.parent && !node.parent.isRoot) {
|
||||
let childrenList = node.parent.children
|
||||
let index = childrenList.findIndex(item => {
|
||||
return item === node
|
||||
})
|
||||
childrenList.forEach((item, _index) => {
|
||||
if (item.hasCustomPosition()) {
|
||||
// 适配自定义位置
|
||||
return
|
||||
}
|
||||
let _offset = 0
|
||||
// 下面的节点往下移
|
||||
if (_index > index) {
|
||||
_offset = addHeight
|
||||
}
|
||||
item.top += _offset
|
||||
// 同步更新子节点的位置
|
||||
if (item.children && item.children.length) {
|
||||
this.updateChildren(item.children, 'top', _offset)
|
||||
}
|
||||
})
|
||||
// 更新父节点的位置
|
||||
this.updateBrothersTop(node.parent, node.layerIndex === 3 ? 0 : addHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制连线,连接该节点到其子节点
|
||||
renderLine(node, lines, style) {
|
||||
if (node.children.length <= 0) {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize } = node
|
||||
let len = node.children.length
|
||||
if (node.isRoot) {
|
||||
// 当前节点是根节点
|
||||
let prevBother = node
|
||||
// 根节点的子节点是和根节点同一水平线排列
|
||||
node.children.forEach((item, index) => {
|
||||
let x1 = prevBother.left + prevBother.width
|
||||
let x2 = item.left
|
||||
let y = node.top + node.height / 2
|
||||
let path = `M ${x1},${y} L ${x2},${y}`
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
prevBother = item
|
||||
})
|
||||
} else {
|
||||
// 当前节点为非根节点
|
||||
let maxy = -Infinity
|
||||
let miny = Infinity
|
||||
let maxx = -Infinity
|
||||
let x = node.left + node.width * 0.3
|
||||
node.children.forEach((item, index) => {
|
||||
if (item.left > maxx) {
|
||||
maxx = item.left
|
||||
}
|
||||
let y = item.top + item.height / 2
|
||||
if (y > maxy) {
|
||||
maxy = y
|
||||
}
|
||||
if (y < miny) {
|
||||
miny = y
|
||||
}
|
||||
// 水平线
|
||||
if (node.layerIndex > 1) {
|
||||
let path = `M ${x},${y} L ${item.left},${y}`
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
}
|
||||
})
|
||||
// 竖线
|
||||
if (len > 0) {
|
||||
let line = this.draw.path()
|
||||
expandBtnSize = len > 0 ? expandBtnSize : 0
|
||||
let lineLength = maxx - node.left - node.width * 0.3
|
||||
if (node.parent && node.parent.isRoot) {
|
||||
line.plot(
|
||||
`M ${x},${top + height} L ${x + lineLength},${
|
||||
top + height + Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) * lineLength
|
||||
}`
|
||||
)
|
||||
} else {
|
||||
line.plot(`M ${x},${top} L ${x},${miny}`)
|
||||
}
|
||||
node.style.line(line)
|
||||
node._lines.push(line)
|
||||
style && style(line, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染按钮
|
||||
renderExpandBtn(node, btn) {
|
||||
let { width, height, expandBtnSize, isRoot } = node
|
||||
if (!isRoot) {
|
||||
let { translateX, translateY } = btn.transform()
|
||||
if (node.parent && node.parent.isRoot) {
|
||||
btn.translate(
|
||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
||||
height + expandBtnSize / 2 - translateY
|
||||
)
|
||||
} else {
|
||||
btn.translate(
|
||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
||||
-expandBtnSize / 2 - translateY
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建概要节点
|
||||
renderGeneralization(node, gLine, gNode) {
|
||||
let {
|
||||
top,
|
||||
bottom,
|
||||
right,
|
||||
generalizationLineMargin,
|
||||
generalizationNodeMargin
|
||||
} = this.getNodeBoundaries(node, 'h')
|
||||
let x1 = right + generalizationLineMargin
|
||||
let y1 = top
|
||||
let x2 = right + generalizationLineMargin
|
||||
let y2 = bottom
|
||||
let cx = x1 + 20
|
||||
let cy = y1 + (y2 - y1) / 2
|
||||
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
||||
gLine.plot(path)
|
||||
gNode.left = right + generalizationNodeMargin
|
||||
gNode.top = top + (bottom - top - gNode.height) / 2
|
||||
}
|
||||
}
|
||||
|
||||
export default Fishbone
|
||||
350
simple-mind-map/src/layouts/FishboneTop.js
Normal file
@@ -0,0 +1,350 @@
|
||||
import Base from './Base'
|
||||
import { walk, asyncRun } from '../utils'
|
||||
import { CONSTANTS } from '../utils/constant'
|
||||
|
||||
const degToRad = deg => {
|
||||
return (Math.PI / 180) * deg
|
||||
}
|
||||
|
||||
// 上方鱼骨图
|
||||
class Fishbone extends Base {
|
||||
// 构造函数
|
||||
constructor(opt = {}) {
|
||||
super(opt)
|
||||
}
|
||||
|
||||
// 布局
|
||||
doLayout(callback) {
|
||||
let task = [
|
||||
() => {
|
||||
this.computedBaseValue()
|
||||
},
|
||||
() => {
|
||||
this.computedLeftTopValue()
|
||||
},
|
||||
() => {
|
||||
this.adjustLeftTopValue()
|
||||
},
|
||||
() => {
|
||||
callback(this.root)
|
||||
}
|
||||
]
|
||||
asyncRun(task)
|
||||
}
|
||||
|
||||
// 遍历数据创建节点、计算根节点的位置,计算根节点的子节点的top值
|
||||
computedBaseValue() {
|
||||
walk(
|
||||
this.renderer.renderTree,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex, index) => {
|
||||
// 创建节点
|
||||
let newNode = this.createNode(node, parent, isRoot, layerIndex)
|
||||
// 根节点定位在画布中心位置
|
||||
if (isRoot) {
|
||||
this.setNodeCenter(newNode)
|
||||
} else {
|
||||
// 非根节点
|
||||
// 三级及以下节点以上级方向为准
|
||||
if (parent._node.dir) {
|
||||
newNode.dir = parent._node.dir
|
||||
} else {
|
||||
// 节点生长方向
|
||||
newNode.dir =
|
||||
index % 2 === 0
|
||||
? CONSTANTS.TIMELINE_DIR.TOP
|
||||
: CONSTANTS.TIMELINE_DIR.BOTTOM
|
||||
}
|
||||
// 计算二级节点的top值
|
||||
if (parent._node.isRoot) {
|
||||
newNode.top = parent._node.top - newNode.height
|
||||
}
|
||||
}
|
||||
if (!node.data.expand) {
|
||||
return true
|
||||
}
|
||||
},
|
||||
null,
|
||||
true,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
// 遍历节点树计算节点的left、top
|
||||
computedLeftTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex, index) => {
|
||||
if (node.isRoot) {
|
||||
let totalLeft = node.left + node.width
|
||||
node.children.forEach(item => {
|
||||
item.left = totalLeft
|
||||
totalLeft += item.width
|
||||
})
|
||||
}
|
||||
if (layerIndex >= 1 && node.children) {
|
||||
// 遍历三级及以下节点的子节点
|
||||
let startLeft = node.left + node.width * 0.5
|
||||
let totalTop =
|
||||
node.top +
|
||||
node.height +
|
||||
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
node.children.forEach(item => {
|
||||
item.left = startLeft
|
||||
item.top += totalTop
|
||||
totalTop +=
|
||||
item.height +
|
||||
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
})
|
||||
}
|
||||
},
|
||||
null,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 调整节点left、top
|
||||
adjustLeftTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (!node.nodeData.data.expand) {
|
||||
return
|
||||
}
|
||||
// 调整top
|
||||
let len = node.children.length
|
||||
// 调整三级及以下节点的top
|
||||
if (parent && !parent.isRoot && len > 0) {
|
||||
let totalHeight = node.children.reduce((h, item) => {
|
||||
return (
|
||||
h +
|
||||
item.height +
|
||||
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
)
|
||||
}, 0)
|
||||
this.updateBrothersTop(node, totalHeight)
|
||||
}
|
||||
},
|
||||
(node, parent) => {
|
||||
// 将二级节点的子节点移到上方
|
||||
if (parent && parent.isRoot) {
|
||||
// 遍历二级节点的子节点
|
||||
let totalHeight = 0
|
||||
node.children.forEach(item => {
|
||||
// 调整top
|
||||
let nodeTotalHeight = this.getNodeAreaHeight(item)
|
||||
let _top = item.top
|
||||
item.top =
|
||||
node.top - (item.top - node.top) - nodeTotalHeight + node.height
|
||||
// 调整left
|
||||
let offsetLeft =
|
||||
(nodeTotalHeight + totalHeight) / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
|
||||
item.left += offsetLeft
|
||||
totalHeight += nodeTotalHeight
|
||||
// 同步更新后代节点
|
||||
this.updateChildrenPro(item.children, {
|
||||
top: item.top - _top,
|
||||
left: offsetLeft
|
||||
})
|
||||
})
|
||||
}
|
||||
// 调整二级节点的子节点的left值
|
||||
if (node.isRoot) {
|
||||
let totalLeft = 0
|
||||
node.children.forEach(item => {
|
||||
item.left += totalLeft
|
||||
this.updateChildren(item.children, 'left', totalLeft)
|
||||
let { left, right } = this.getNodeBoundaries(item, 'h')
|
||||
totalLeft += right - left
|
||||
})
|
||||
}
|
||||
},
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 递归计算节点的宽度
|
||||
getNodeAreaHeight(node) {
|
||||
let totalHeight = 0
|
||||
let loop = node => {
|
||||
totalHeight +=
|
||||
node.height +
|
||||
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
if (node.children.length) {
|
||||
node.children.forEach(item => {
|
||||
loop(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
loop(node)
|
||||
return totalHeight
|
||||
}
|
||||
|
||||
// 调整兄弟节点的left
|
||||
updateBrothersLeft(node) {
|
||||
let childrenList = node.children
|
||||
let totalAddWidth = 0
|
||||
childrenList.forEach(item => {
|
||||
item.left += totalAddWidth
|
||||
if (item.children && item.children.length) {
|
||||
this.updateChildren(item.children, 'left', totalAddWidth)
|
||||
}
|
||||
// let areaWidth = this.getNodeAreaWidth(item)
|
||||
let { left, right } = this.getNodeBoundaries(item, 'h')
|
||||
let areaWidth = right - left
|
||||
let difference = areaWidth - item.width
|
||||
if (difference > 0) {
|
||||
totalAddWidth += difference
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 调整兄弟节点的top
|
||||
updateBrothersTop(node, addHeight) {
|
||||
if (node.parent && !node.parent.isRoot) {
|
||||
let childrenList = node.parent.children
|
||||
let index = childrenList.findIndex(item => {
|
||||
return item === node
|
||||
})
|
||||
childrenList.forEach((item, _index) => {
|
||||
if (item.hasCustomPosition()) {
|
||||
// 适配自定义位置
|
||||
return
|
||||
}
|
||||
let _offset = 0
|
||||
// 下面的节点往下移
|
||||
if (_index > index) {
|
||||
_offset = addHeight
|
||||
}
|
||||
item.top += _offset
|
||||
// 同步更新子节点的位置
|
||||
if (item.children && item.children.length) {
|
||||
this.updateChildren(item.children, 'top', _offset)
|
||||
}
|
||||
})
|
||||
// 更新父节点的位置
|
||||
this.updateBrothersTop(node.parent, addHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制连线,连接该节点到其子节点
|
||||
renderLine(node, lines, style) {
|
||||
if (node.children.length <= 0) {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize } = node
|
||||
let len = node.children.length
|
||||
if (node.isRoot) {
|
||||
// 当前节点是根节点
|
||||
let prevBother = node
|
||||
// 根节点的子节点是和根节点同一水平线排列
|
||||
node.children.forEach((item, index) => {
|
||||
let x1 = prevBother.left + prevBother.width
|
||||
let x2 = item.left
|
||||
let y = node.top + node.height / 2
|
||||
let path = `M ${x1},${y} L ${x2},${y}`
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
prevBother = item
|
||||
})
|
||||
} else {
|
||||
// 当前节点为非根节点
|
||||
let maxy = -Infinity
|
||||
let miny = Infinity
|
||||
let maxx = -Infinity
|
||||
let x = node.left + node.width * 0.3
|
||||
node.children.forEach((item, index) => {
|
||||
if (item.left > maxx) {
|
||||
maxx = item.left
|
||||
}
|
||||
let y = item.top + item.height / 2
|
||||
if (y > maxy) {
|
||||
maxy = y
|
||||
}
|
||||
if (y < miny) {
|
||||
miny = y
|
||||
}
|
||||
// 水平线
|
||||
if (node.layerIndex > 1) {
|
||||
let path = `M ${x},${y} L ${item.left},${y}`
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
}
|
||||
})
|
||||
// 竖线
|
||||
if (len > 0) {
|
||||
let line = this.draw.path()
|
||||
expandBtnSize = len > 0 ? expandBtnSize : 0
|
||||
let lineLength = maxx - node.left - node.width * 0.3
|
||||
if (
|
||||
node.parent &&
|
||||
node.parent.isRoot &&
|
||||
node.dir === CONSTANTS.TIMELINE_DIR.TOP
|
||||
) {
|
||||
line.plot(
|
||||
`M ${x},${top} L ${x + lineLength},${
|
||||
top - Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) * lineLength
|
||||
}`
|
||||
)
|
||||
} else {
|
||||
if (node.parent && node.parent.isRoot) {
|
||||
line.plot(
|
||||
`M ${x},${top} L ${x + lineLength},${
|
||||
top - Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) * lineLength
|
||||
}`
|
||||
)
|
||||
} else {
|
||||
line.plot(`M ${x},${top + height + expandBtnSize} L ${x},${maxy}`)
|
||||
}
|
||||
}
|
||||
node.style.line(line)
|
||||
node._lines.push(line)
|
||||
style && style(line, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染按钮
|
||||
renderExpandBtn(node, btn) {
|
||||
let { width, height, expandBtnSize, isRoot } = node
|
||||
if (!isRoot) {
|
||||
let { translateX, translateY } = btn.transform()
|
||||
if (node.parent && node.parent.isRoot) {
|
||||
btn.translate(
|
||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
||||
-expandBtnSize / 2 - translateY
|
||||
)
|
||||
} else {
|
||||
btn.translate(
|
||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
||||
height + expandBtnSize / 2 - translateY
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建概要节点
|
||||
renderGeneralization(node, gLine, gNode) {
|
||||
let {
|
||||
top,
|
||||
bottom,
|
||||
right,
|
||||
generalizationLineMargin,
|
||||
generalizationNodeMargin
|
||||
} = this.getNodeBoundaries(node, 'h')
|
||||
let x1 = right + generalizationLineMargin
|
||||
let y1 = top
|
||||
let x2 = right + generalizationLineMargin
|
||||
let y2 = bottom
|
||||
let cx = x1 + 20
|
||||
let cy = y1 + (y2 - y1) / 2
|
||||
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
||||
gLine.plot(path)
|
||||
gNode.left = right + generalizationNodeMargin
|
||||
gNode.top = top + (bottom - top - gNode.height) / 2
|
||||
}
|
||||
}
|
||||
|
||||
export default Fishbone
|
||||
@@ -159,6 +159,9 @@ class LogicalStructure extends Base {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize } = node
|
||||
if (!this.mindMap.opt.alwaysShowExpandBtn) {
|
||||
expandBtnSize = 0
|
||||
}
|
||||
let marginX = this.getMarginX(node.layerIndex + 1)
|
||||
let s1 = (marginX - expandBtnSize) * 0.6
|
||||
let nodeUseLineStyle = this.mindMap.themeConfig.nodeUseLineStyle
|
||||
@@ -188,6 +191,9 @@ class LogicalStructure extends Base {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize } = node
|
||||
if (!this.mindMap.opt.alwaysShowExpandBtn) {
|
||||
expandBtnSize = 0
|
||||
}
|
||||
let nodeUseLineStyle = this.mindMap.themeConfig.nodeUseLineStyle
|
||||
node.children.forEach((item, index) => {
|
||||
let x1 =
|
||||
@@ -213,6 +219,9 @@ class LogicalStructure extends Base {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize } = node
|
||||
if (!this.mindMap.opt.alwaysShowExpandBtn) {
|
||||
expandBtnSize = 0
|
||||
}
|
||||
let nodeUseLineStyle = this.mindMap.themeConfig.nodeUseLineStyle
|
||||
node.children.forEach((item, index) => {
|
||||
let x1 =
|
||||
@@ -245,9 +254,15 @@ class LogicalStructure extends Base {
|
||||
let nodeUseLineStyleOffset = this.mindMap.themeConfig.nodeUseLineStyle
|
||||
? height / 2
|
||||
: 0
|
||||
// 位置没有变化则返回
|
||||
let _x = width
|
||||
let _y = height / 2 + nodeUseLineStyleOffset
|
||||
if (_x === translateX && _y === translateY) {
|
||||
return
|
||||
}
|
||||
btn.translate(
|
||||
width - translateX,
|
||||
height / 2 - translateY + nodeUseLineStyleOffset
|
||||
_x - translateX,
|
||||
_y - translateY
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -198,6 +198,9 @@ class MindMap extends Base {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize } = node
|
||||
if (!this.mindMap.opt.alwaysShowExpandBtn) {
|
||||
expandBtnSize = 0
|
||||
}
|
||||
let marginX = this.getMarginX(node.layerIndex + 1)
|
||||
let s1 = (marginX - expandBtnSize) * 0.6
|
||||
let nodeUseLineStyle = this.mindMap.themeConfig.nodeUseLineStyle
|
||||
@@ -235,6 +238,9 @@ class MindMap extends Base {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize } = node
|
||||
if (!this.mindMap.opt.alwaysShowExpandBtn) {
|
||||
expandBtnSize = 0
|
||||
}
|
||||
let nodeUseLineStyle = this.mindMap.themeConfig.nodeUseLineStyle
|
||||
node.children.forEach((item, index) => {
|
||||
let x1 =
|
||||
@@ -269,6 +275,9 @@ class MindMap extends Base {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize } = node
|
||||
if (!this.mindMap.opt.alwaysShowExpandBtn) {
|
||||
expandBtnSize = 0
|
||||
}
|
||||
let nodeUseLineStyle = this.mindMap.themeConfig.nodeUseLineStyle
|
||||
node.children.forEach((item, index) => {
|
||||
let x1 =
|
||||
@@ -276,7 +285,7 @@ class MindMap extends Base {
|
||||
? left + width / 2
|
||||
: item.dir === 'left'
|
||||
? left - expandBtnSize
|
||||
: left + width + 20
|
||||
: left + width + expandBtnSize
|
||||
let y1 = top + height / 2
|
||||
let x2 = item.dir === 'left' ? item.left + item.width : item.left
|
||||
let y2 = item.top + item.height / 2
|
||||
@@ -310,8 +319,14 @@ class MindMap extends Base {
|
||||
let nodeUseLineStyleOffset = this.mindMap.themeConfig.nodeUseLineStyle
|
||||
? height / 2
|
||||
: 0
|
||||
let x = (node.dir === 'left' ? 0 - expandBtnSize : width) - translateX
|
||||
let y = height / 2 - translateY + nodeUseLineStyleOffset
|
||||
// 位置没有变化则返回
|
||||
let _x = (node.dir === 'left' ? 0 - expandBtnSize : width)
|
||||
let _y = height / 2 + nodeUseLineStyleOffset
|
||||
if (_x === translateX && _y === translateY) {
|
||||
return
|
||||
}
|
||||
let x = _x - translateX
|
||||
let y = _y - translateY
|
||||
btn.translate(x, y)
|
||||
}
|
||||
|
||||
|
||||
@@ -179,6 +179,9 @@ class OrganizationStructure extends Base {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize, isRoot } = node
|
||||
if (!this.mindMap.opt.alwaysShowExpandBtn) {
|
||||
expandBtnSize = 0
|
||||
}
|
||||
let x1 = left + width / 2
|
||||
let y1 = top + height
|
||||
let marginX = this.getMarginX(node.layerIndex + 1)
|
||||
|
||||
341
simple-mind-map/src/layouts/Timeline.js
Normal file
@@ -0,0 +1,341 @@
|
||||
import Base from './Base'
|
||||
import { walk, asyncRun } from '../utils'
|
||||
import { CONSTANTS } from '../utils/constant'
|
||||
|
||||
// 时间轴
|
||||
class Timeline extends Base {
|
||||
// 构造函数
|
||||
constructor(opt = {}, layout) {
|
||||
super(opt)
|
||||
this.layout = layout
|
||||
}
|
||||
|
||||
// 布局
|
||||
doLayout(callback) {
|
||||
let task = [
|
||||
() => {
|
||||
this.computedBaseValue()
|
||||
},
|
||||
() => {
|
||||
this.computedLeftTopValue()
|
||||
},
|
||||
() => {
|
||||
this.adjustLeftTopValue()
|
||||
},
|
||||
() => {
|
||||
callback(this.root)
|
||||
}
|
||||
]
|
||||
asyncRun(task)
|
||||
}
|
||||
|
||||
// 遍历数据创建节点、计算根节点的位置,计算根节点的子节点的top值
|
||||
computedBaseValue() {
|
||||
walk(
|
||||
this.renderer.renderTree,
|
||||
null,
|
||||
(cur, parent, isRoot, layerIndex, index) => {
|
||||
let newNode = this.createNode(cur, parent, isRoot, layerIndex)
|
||||
// 根节点定位在画布中心位置
|
||||
if (isRoot) {
|
||||
this.setNodeCenter(newNode)
|
||||
} else {
|
||||
// 非根节点
|
||||
// 时间轴2类型需要交替显示
|
||||
if (this.layout === CONSTANTS.LAYOUT.TIMELINE2) {
|
||||
// 三级及以下节点以上级为准
|
||||
if (parent._node.dir) {
|
||||
newNode.dir = parent._node.dir
|
||||
} else {
|
||||
// 节点生长方向
|
||||
newNode.dir =
|
||||
index % 2 === 0
|
||||
? CONSTANTS.TIMELINE_DIR.BOTTOM
|
||||
: CONSTANTS.TIMELINE_DIR.TOP
|
||||
}
|
||||
} else {
|
||||
newNode.dir = ''
|
||||
}
|
||||
if (parent._node.isRoot) {
|
||||
newNode.top =
|
||||
parent._node.top +
|
||||
(cur._node.height > parent._node.height
|
||||
? -(cur._node.height - parent._node.height) / 2
|
||||
: (parent._node.height - cur._node.height) / 2)
|
||||
}
|
||||
}
|
||||
if (!cur.data.expand) {
|
||||
return true
|
||||
}
|
||||
},
|
||||
null,
|
||||
true,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
// 遍历节点树计算节点的left、top
|
||||
computedLeftTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex, index) => {
|
||||
if (
|
||||
node.nodeData.data.expand &&
|
||||
node.children &&
|
||||
node.children.length
|
||||
) {
|
||||
let marginX = this.getMarginX(layerIndex + 1)
|
||||
let marginY = this.getMarginY(layerIndex + 1)
|
||||
if (isRoot) {
|
||||
let left = node.left + node.width
|
||||
let totalLeft = left + marginX
|
||||
node.children.forEach(cur => {
|
||||
cur.left = totalLeft
|
||||
totalLeft += cur.width + marginX
|
||||
})
|
||||
} else {
|
||||
let totalTop =
|
||||
node.top +
|
||||
node.height +
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
null,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 调整节点left、top
|
||||
adjustLeftTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (!node.nodeData.data.expand) {
|
||||
return
|
||||
}
|
||||
// 调整left
|
||||
if (node.isRoot) {
|
||||
this.updateBrothersLeft(node)
|
||||
}
|
||||
// 调整top
|
||||
let len = node.children.length
|
||||
if (parent && !parent.isRoot && len > 0) {
|
||||
let marginY = this.getMarginY(layerIndex + 1)
|
||||
let totalHeight =
|
||||
node.children.reduce((h, item) => {
|
||||
return (
|
||||
h +
|
||||
item.height +
|
||||
(this.getNodeActChildrenLength(item) > 0
|
||||
? item.expandBtnSize
|
||||
: 0)
|
||||
)
|
||||
}, 0) +
|
||||
len * marginY
|
||||
this.updateBrothersTop(node, totalHeight)
|
||||
}
|
||||
},
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (
|
||||
parent &&
|
||||
parent.isRoot &&
|
||||
node.dir === CONSTANTS.TIMELINE_DIR.TOP
|
||||
) {
|
||||
// 遍历二级节点的子节点
|
||||
node.children.forEach(item => {
|
||||
let totalHeight = this.getNodeAreaHeight(item)
|
||||
let _top = item.top
|
||||
item.top =
|
||||
node.top - (item.top - node.top) - totalHeight + node.height
|
||||
this.updateChildren(item.children, 'top', item.top - _top)
|
||||
})
|
||||
}
|
||||
},
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 递归计算节点的宽度
|
||||
getNodeAreaHeight(node) {
|
||||
let totalHeight = 0
|
||||
let loop = node => {
|
||||
totalHeight +=
|
||||
node.height +
|
||||
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) +
|
||||
this.getMarginY(node.layerIndex)
|
||||
if (node.children.length) {
|
||||
node.children.forEach(item => {
|
||||
loop(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
loop(node)
|
||||
return totalHeight
|
||||
}
|
||||
|
||||
// 调整兄弟节点的left
|
||||
updateBrothersLeft(node) {
|
||||
let childrenList = node.children
|
||||
let totalAddWidth = 0
|
||||
childrenList.forEach(item => {
|
||||
item.left += totalAddWidth
|
||||
if (item.children && item.children.length) {
|
||||
this.updateChildren(item.children, 'left', totalAddWidth)
|
||||
}
|
||||
// let areaWidth = this.getNodeAreaWidth(item)
|
||||
let { left, right } = this.getNodeBoundaries(item, 'h')
|
||||
let areaWidth = right - left
|
||||
let difference = areaWidth - item.width
|
||||
if (difference > 0) {
|
||||
totalAddWidth += difference
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 调整兄弟节点的top
|
||||
updateBrothersTop(node, addHeight) {
|
||||
if (node.parent && !node.parent.isRoot) {
|
||||
let childrenList = node.parent.children
|
||||
let index = childrenList.findIndex(item => {
|
||||
return item === node
|
||||
})
|
||||
childrenList.forEach((item, _index) => {
|
||||
if (item.hasCustomPosition()) {
|
||||
// 适配自定义位置
|
||||
return
|
||||
}
|
||||
let _offset = 0
|
||||
// 下面的节点往下移
|
||||
if (_index > index) {
|
||||
_offset = addHeight
|
||||
}
|
||||
item.top += _offset
|
||||
// 同步更新子节点的位置
|
||||
if (item.children && item.children.length) {
|
||||
this.updateChildren(item.children, 'top', _offset)
|
||||
}
|
||||
})
|
||||
// 更新父节点的位置
|
||||
this.updateBrothersTop(node.parent, addHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制连线,连接该节点到其子节点
|
||||
renderLine(node, lines, style) {
|
||||
if (node.children.length <= 0) {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize } = node
|
||||
if (!this.mindMap.opt.alwaysShowExpandBtn) {
|
||||
expandBtnSize = 0
|
||||
}
|
||||
let len = node.children.length
|
||||
if (node.isRoot) {
|
||||
// 当前节点是根节点
|
||||
let prevBother = node
|
||||
// 根节点的子节点是和根节点同一水平线排列
|
||||
node.children.forEach((item, index) => {
|
||||
let x1 = prevBother.left + prevBother.width
|
||||
let x2 = item.left
|
||||
let y = node.top + node.height / 2
|
||||
let path = `M ${x1},${y} L ${x2},${y}`
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
prevBother = item
|
||||
})
|
||||
} else {
|
||||
// 当前节点为非根节点
|
||||
let maxy = -Infinity
|
||||
let miny = Infinity
|
||||
let x = node.left + node.width * 0.3
|
||||
node.children.forEach((item, index) => {
|
||||
let y = item.top + item.height / 2
|
||||
if (y > maxy) {
|
||||
maxy = y
|
||||
}
|
||||
if (y < miny) {
|
||||
miny = y
|
||||
}
|
||||
// 水平线
|
||||
let path = `M ${x},${y} L ${item.left},${y}`
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
})
|
||||
// 竖线
|
||||
if (len > 0) {
|
||||
let line = this.draw.path()
|
||||
expandBtnSize = len > 0 ? expandBtnSize : 0
|
||||
if (
|
||||
node.parent &&
|
||||
node.parent.isRoot &&
|
||||
node.dir === CONSTANTS.TIMELINE_DIR.TOP
|
||||
) {
|
||||
line.plot(`M ${x},${top} L ${x},${miny}`)
|
||||
} else {
|
||||
line.plot(`M ${x},${top + height + expandBtnSize} L ${x},${maxy}`)
|
||||
}
|
||||
node.style.line(line)
|
||||
node._lines.push(line)
|
||||
style && style(line, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染按钮
|
||||
renderExpandBtn(node, btn) {
|
||||
let { width, height, expandBtnSize, isRoot } = node
|
||||
if (!isRoot) {
|
||||
let { translateX, translateY } = btn.transform()
|
||||
if (
|
||||
node.parent &&
|
||||
node.parent.isRoot &&
|
||||
node.dir === CONSTANTS.TIMELINE_DIR.TOP
|
||||
) {
|
||||
btn.translate(
|
||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
||||
-expandBtnSize / 2 - translateY
|
||||
)
|
||||
} else {
|
||||
btn.translate(
|
||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
||||
height + expandBtnSize / 2 - translateY
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建概要节点
|
||||
renderGeneralization(node, gLine, gNode) {
|
||||
let {
|
||||
top,
|
||||
bottom,
|
||||
right,
|
||||
generalizationLineMargin,
|
||||
generalizationNodeMargin
|
||||
} = this.getNodeBoundaries(node, 'h')
|
||||
let x1 = right + generalizationLineMargin
|
||||
let y1 = top
|
||||
let x2 = right + generalizationLineMargin
|
||||
let y2 = bottom
|
||||
let cx = x1 + 20
|
||||
let cy = y1 + (y2 - y1) / 2
|
||||
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
||||
gLine.plot(path)
|
||||
gNode.left = right + generalizationNodeMargin
|
||||
gNode.top = top + (bottom - top - gNode.height) / 2
|
||||
}
|
||||
}
|
||||
|
||||
export default Timeline
|
||||
216
simple-mind-map/src/layouts/fishboneUtils.js
Normal file
@@ -0,0 +1,216 @@
|
||||
import { degToRad } from '../utils/'
|
||||
|
||||
export default {
|
||||
top: {
|
||||
renderExpandBtn({
|
||||
node,
|
||||
btn,
|
||||
expandBtnSize,
|
||||
translateX,
|
||||
translateY,
|
||||
width,
|
||||
height
|
||||
}) {
|
||||
if (node.parent && node.parent.isRoot) {
|
||||
btn.translate(
|
||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
||||
-expandBtnSize / 2 - translateY
|
||||
)
|
||||
} else {
|
||||
btn.translate(
|
||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
||||
height + expandBtnSize / 2 - translateY
|
||||
)
|
||||
}
|
||||
},
|
||||
renderLine({
|
||||
node,
|
||||
line,
|
||||
top,
|
||||
x,
|
||||
lineLength,
|
||||
height,
|
||||
expandBtnSize,
|
||||
maxy,
|
||||
ctx
|
||||
}) {
|
||||
if (node.parent && node.parent.isRoot) {
|
||||
line.plot(
|
||||
`M ${x},${top} L ${x + lineLength},${
|
||||
top - Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg)) * lineLength
|
||||
}`
|
||||
)
|
||||
} else {
|
||||
line.plot(`M ${x},${top + height + expandBtnSize} L ${x},${maxy}`)
|
||||
}
|
||||
},
|
||||
computedLeftTopValue({ layerIndex, node, ctx }) {
|
||||
if (layerIndex >= 1 && node.children) {
|
||||
// 遍历三级及以下节点的子节点
|
||||
let startLeft = node.left + node.width * ctx.childIndent
|
||||
let totalTop =
|
||||
node.top +
|
||||
node.height +
|
||||
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
node.children.forEach(item => {
|
||||
item.left = startLeft
|
||||
item.top += totalTop
|
||||
totalTop +=
|
||||
item.height +
|
||||
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
})
|
||||
}
|
||||
},
|
||||
adjustLeftTopValueBefore({ node, parent, ctx }) {
|
||||
// 调整top
|
||||
let len = node.children.length
|
||||
// 调整三级及以下节点的top
|
||||
if (parent && !parent.isRoot && len > 0) {
|
||||
let totalHeight = node.children.reduce((h, item) => {
|
||||
return (
|
||||
h +
|
||||
item.height +
|
||||
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
)
|
||||
}, 0)
|
||||
ctx.updateBrothersTop(node, totalHeight)
|
||||
}
|
||||
},
|
||||
adjustLeftTopValueAfter({ parent, node, ctx }) {
|
||||
// 将二级节点的子节点移到上方
|
||||
if (parent && parent.isRoot) {
|
||||
// 遍历二级节点的子节点
|
||||
let totalHeight = node.expandBtnSize
|
||||
node.children.forEach(item => {
|
||||
// 调整top
|
||||
let nodeTotalHeight = ctx.getNodeAreaHeight(item)
|
||||
let _top = item.top
|
||||
let _left = item.left
|
||||
item.top =
|
||||
node.top - (item.top - node.top) - nodeTotalHeight + node.height
|
||||
// 调整left
|
||||
item.left = node.left + node.width * ctx.indent + (nodeTotalHeight + totalHeight) / Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg))
|
||||
totalHeight += nodeTotalHeight
|
||||
// 同步更新后代节点
|
||||
ctx.updateChildrenPro(item.children, {
|
||||
top: item.top - _top,
|
||||
left: item.left - _left
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
bottom: {
|
||||
renderExpandBtn({
|
||||
node,
|
||||
btn,
|
||||
expandBtnSize,
|
||||
translateX,
|
||||
translateY,
|
||||
width,
|
||||
height
|
||||
}) {
|
||||
if (node.parent && node.parent.isRoot) {
|
||||
btn.translate(
|
||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
||||
height + expandBtnSize / 2 - translateY
|
||||
)
|
||||
} else {
|
||||
btn.translate(
|
||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
||||
-expandBtnSize / 2 - translateY
|
||||
)
|
||||
}
|
||||
},
|
||||
renderLine({ node, line, top, x, lineLength, height, miny, ctx }) {
|
||||
if (node.parent && node.parent.isRoot) {
|
||||
line.plot(
|
||||
`M ${x},${top + height} L ${x + lineLength},${
|
||||
top + height + Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg)) * lineLength
|
||||
}`
|
||||
)
|
||||
} else {
|
||||
line.plot(`M ${x},${top} L ${x},${miny}`)
|
||||
}
|
||||
},
|
||||
computedLeftTopValue({ layerIndex, node, ctx }) {
|
||||
if (layerIndex === 1 && node.children) {
|
||||
// 遍历二级节点的子节点
|
||||
let startLeft = node.left + node.width * ctx.childIndent
|
||||
let totalTop =
|
||||
node.top +
|
||||
node.height +
|
||||
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
|
||||
node.children.forEach(item => {
|
||||
item.left = startLeft
|
||||
item.top =
|
||||
totalTop +
|
||||
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
totalTop +=
|
||||
item.height +
|
||||
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
})
|
||||
}
|
||||
if (layerIndex > 1 && node.children) {
|
||||
// 遍历三级及以下节点的子节点
|
||||
let startLeft = node.left + node.width * ctx.childIndent
|
||||
let totalTop =
|
||||
node.top -
|
||||
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
node.children.forEach(item => {
|
||||
item.left = startLeft
|
||||
item.top = totalTop - item.height
|
||||
totalTop -=
|
||||
item.height +
|
||||
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
})
|
||||
}
|
||||
},
|
||||
adjustLeftTopValueBefore({ node, ctx, layerIndex }) {
|
||||
// 调整top
|
||||
let len = node.children.length
|
||||
if (layerIndex > 2 && len > 0) {
|
||||
let totalHeight = node.children.reduce((h, item) => {
|
||||
return (
|
||||
h +
|
||||
item.height +
|
||||
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
)
|
||||
}, 0)
|
||||
ctx.updateBrothersTop(node, -totalHeight)
|
||||
}
|
||||
},
|
||||
adjustLeftTopValueAfter({ parent, node, ctx }) {
|
||||
// 将二级节点的子节点移到上方
|
||||
if (parent && parent.isRoot) {
|
||||
// 遍历二级节点的子节点
|
||||
let totalHeight = 0
|
||||
let totalHeight2 = node.expandBtnSize
|
||||
node.children.forEach(item => {
|
||||
// 调整top
|
||||
let hasChildren = ctx.getNodeActChildrenLength(item) > 0
|
||||
let nodeTotalHeight = ctx.getNodeAreaHeight(item)
|
||||
let offset =
|
||||
hasChildren > 0
|
||||
? nodeTotalHeight -
|
||||
item.height -
|
||||
(hasChildren ? item.expandBtnSize : 0)
|
||||
: 0
|
||||
let _top = totalHeight + offset
|
||||
let _left = item.left
|
||||
item.top += _top
|
||||
// 调整left
|
||||
item.left = node.left + node.width * ctx.indent + (nodeTotalHeight + totalHeight2) / Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg))
|
||||
totalHeight += offset
|
||||
totalHeight2 += nodeTotalHeight
|
||||
// 同步更新后代节点
|
||||
ctx.updateChildrenPro(item.children, {
|
||||
top: _top,
|
||||
left: item.left - _left
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -279,9 +279,9 @@ export const nodeIconList = [
|
||||
]
|
||||
|
||||
// 获取nodeIconList icon内容
|
||||
const getNodeIconListIcon = name => {
|
||||
const getNodeIconListIcon = (name, extendIconList = []) => {
|
||||
let arr = name.split('_')
|
||||
let typeData = nodeIconList.find(item => {
|
||||
let typeData = [...nodeIconList, ...extendIconList].find(item => {
|
||||
return item.type === arr[0]
|
||||
})
|
||||
return typeData.list.find(item => {
|
||||
|
||||
57
simple-mind-map/src/themes/autumn.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import defaultTheme from './default'
|
||||
import merge from 'deepmerge'
|
||||
|
||||
// 秋天
|
||||
export default merge(defaultTheme, {
|
||||
// 背景颜色
|
||||
backgroundColor: '#fff2df',
|
||||
// 连线的颜色
|
||||
lineColor: '#b0bc47',
|
||||
lineWidth: 3,
|
||||
// 概要连线的粗细
|
||||
generalizationLineWidth: 3,
|
||||
// 概要连线的颜色
|
||||
generalizationLineColor: '#b0bc47',
|
||||
// 根节点样式
|
||||
root: {
|
||||
fillColor: '#e68112',
|
||||
color: '#fff',
|
||||
borderColor: '#e68112',
|
||||
borderWidth: 0,
|
||||
fontSize: 24,
|
||||
active: {
|
||||
borderColor: '#b0bc47',
|
||||
borderWidth: 3
|
||||
}
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
fillColor: '#ffd683',
|
||||
color: '#8c5416',
|
||||
borderColor: '#b0bc47',
|
||||
borderWidth: 2,
|
||||
fontSize: 18,
|
||||
active: {
|
||||
borderColor: '#e68112'
|
||||
}
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 14,
|
||||
color: '#8c5416',
|
||||
active: {
|
||||
borderColor: '#b0bc47'
|
||||
}
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
fontSize: 14,
|
||||
fillColor: '#ffd683',
|
||||
borderColor: '#b0bc47',
|
||||
borderWidth: 2,
|
||||
color: '#8c5416',
|
||||
active: {
|
||||
borderColor: '#e68112'
|
||||
}
|
||||
}
|
||||
})
|
||||
57
simple-mind-map/src/themes/avocado.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import defaultTheme from './default'
|
||||
import merge from 'deepmerge'
|
||||
|
||||
// 牛油果
|
||||
export default merge(defaultTheme, {
|
||||
// 背景颜色
|
||||
backgroundColor: '#e6f1de',
|
||||
// 连线的颜色
|
||||
lineColor: '#f5ffad',
|
||||
lineWidth: 4,
|
||||
// 概要连线的粗细
|
||||
generalizationLineWidth: 3,
|
||||
// 概要连线的颜色
|
||||
generalizationLineColor: '#749336',
|
||||
// 根节点样式
|
||||
root: {
|
||||
fillColor: '#94c143',
|
||||
color: '#fff',
|
||||
borderColor: '#94c143',
|
||||
borderWidth: 0,
|
||||
fontSize: 24,
|
||||
active: {
|
||||
borderColor: '#749336',
|
||||
borderWidth: 3
|
||||
}
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
fillColor: '#cee498',
|
||||
color: '#749336',
|
||||
borderColor: '#aec668',
|
||||
borderWidth: 2,
|
||||
fontSize: 18,
|
||||
active: {
|
||||
borderColor: '#749336'
|
||||
}
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 14,
|
||||
color: '#749336',
|
||||
active: {
|
||||
borderColor: '#749336'
|
||||
}
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
fontSize: 14,
|
||||
fillColor: '#cee498',
|
||||
borderColor: '#aec668',
|
||||
borderWidth: 2,
|
||||
color: '#749336',
|
||||
active: {
|
||||
borderColor: '#749336'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -34,6 +34,14 @@ export default {
|
||||
associativeLineActiveWidth: 8,
|
||||
// 关联线激活状态的颜色
|
||||
associativeLineActiveColor: 'rgba(2, 167, 240, 1)',
|
||||
// 关联线文字颜色
|
||||
associativeLineTextColor: 'rgb(51, 51, 51)',
|
||||
// 关联线文字大小
|
||||
associativeLineTextFontSize: 14,
|
||||
// 关联线文字行高
|
||||
associativeLineTextLineHeight: 1.2,
|
||||
// 关联线文字字体
|
||||
associativeLineTextFontFamily: '微软雅黑, Microsoft YaHei',
|
||||
// 背景颜色
|
||||
backgroundColor: '#fafafa',
|
||||
// 背景图片
|
||||
@@ -148,4 +156,38 @@ export const supportActiveStyle = [
|
||||
'borderRadius'
|
||||
]
|
||||
|
||||
// 检测主题配置是否是节点大小无关的
|
||||
const nodeSizeIndependenceList = [
|
||||
'lineWidth',
|
||||
'lineColor',
|
||||
'lineDasharray',
|
||||
'lineStyle',
|
||||
'generalizationLineWidth',
|
||||
'generalizationLineColor',
|
||||
'associativeLineWidth',
|
||||
'associativeLineColor',
|
||||
'associativeLineActiveWidth',
|
||||
'associativeLineActiveColor',
|
||||
'associativeLineTextColor',
|
||||
'associativeLineTextFontSize',
|
||||
'associativeLineTextLineHeight',
|
||||
'associativeLineTextFontFamily',
|
||||
'backgroundColor',
|
||||
'backgroundImage',
|
||||
'backgroundRepeat',
|
||||
'backgroundPosition',
|
||||
'backgroundSize'
|
||||
]
|
||||
export const checkIsNodeSizeIndependenceConfig = (config) => {
|
||||
let keys = Object.keys(config)
|
||||
for(let i = 0; i < keys.length; i++) {
|
||||
if (!nodeSizeIndependenceList.find((item) => {
|
||||
return item === keys[i]
|
||||
})) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export const lineStyleProps = ['lineColor', 'lineDasharray', 'lineWidth']
|
||||
|
||||
@@ -27,6 +27,9 @@ import redSpirit from './redSpirit'
|
||||
import blackHumour from './blackHumour'
|
||||
import lateNightOffice from './lateNightOffice'
|
||||
import blackGold from './blackGold'
|
||||
import avocado from './avocado'
|
||||
import autumn from './autumn'
|
||||
import orangeJuice from './orangeJuice'
|
||||
|
||||
export default {
|
||||
default: defaultTheme,
|
||||
@@ -57,5 +60,8 @@ export default {
|
||||
redSpirit,
|
||||
blackHumour,
|
||||
lateNightOffice,
|
||||
blackGold
|
||||
blackGold,
|
||||
avocado,
|
||||
autumn,
|
||||
orangeJuice
|
||||
}
|
||||
|
||||
57
simple-mind-map/src/themes/orangeJuice.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import defaultTheme from './default'
|
||||
import merge from 'deepmerge'
|
||||
|
||||
// 橙汁
|
||||
export default merge(defaultTheme, {
|
||||
// 背景颜色
|
||||
backgroundColor: '#070616',
|
||||
// 连线的颜色
|
||||
lineColor: '#fff',
|
||||
lineWidth: 3,
|
||||
// 概要连线的粗细
|
||||
generalizationLineWidth: 3,
|
||||
// 概要连线的颜色
|
||||
generalizationLineColor: '#fff',
|
||||
// 根节点样式
|
||||
root: {
|
||||
fillColor: '#ff6811',
|
||||
color: '#110501',
|
||||
borderColor: '#ff6811',
|
||||
borderWidth: 0,
|
||||
fontSize: 24,
|
||||
active: {
|
||||
borderColor: '#a9a4a9',
|
||||
borderWidth: 3
|
||||
}
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
fillColor: '#070616',
|
||||
color: '#a9a4a9',
|
||||
borderColor: '#ff6811',
|
||||
borderWidth: 2,
|
||||
fontSize: 18,
|
||||
active: {
|
||||
borderColor: '#110501'
|
||||
}
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 14,
|
||||
color: '#a9a4a9',
|
||||
active: {
|
||||
borderColor: '#ff6811'
|
||||
}
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
fontSize: 14,
|
||||
fillColor: '',
|
||||
borderColor: '#ff6811',
|
||||
borderWidth: 2,
|
||||
color: '#a9a4a9',
|
||||
active: {
|
||||
borderColor: '#110501'
|
||||
}
|
||||
}
|
||||
})
|
||||
39
simple-mind-map/src/utils/Lru.js
Normal file
@@ -0,0 +1,39 @@
|
||||
// LRU缓存类
|
||||
export default class CRU {
|
||||
constructor(max) {
|
||||
this.max = max || 1000
|
||||
this.size = 0
|
||||
this.pool = new Map()
|
||||
}
|
||||
|
||||
add(key, value) {
|
||||
// 如果该key是否已经存在,则先删除
|
||||
this.delete(key)
|
||||
this.pool.set(key, value)
|
||||
this.size++
|
||||
// 如果数量超出最大值,则删除最早的
|
||||
if (this.size > this.max) {
|
||||
let keys = this.pool.keys()
|
||||
let last = keys.next()
|
||||
this.delete(last.value)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
delete(key) {
|
||||
if (this.pool.has(key)) {
|
||||
this.pool.delete(key)
|
||||
this.size--
|
||||
}
|
||||
}
|
||||
|
||||
has(key) {
|
||||
return this.pool.has(key)
|
||||
}
|
||||
|
||||
get(key) {
|
||||
if (this.pool.has(key)) {
|
||||
return this.pool.get(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
241
simple-mind-map/src/utils/associativeLineControls.js
Normal file
@@ -0,0 +1,241 @@
|
||||
import {
|
||||
getAssociativeLineTargetIndex,
|
||||
joinCubicBezierPath,
|
||||
computeNodePoints,
|
||||
getDefaultControlPointOffsets
|
||||
} from './associativeLineUtils'
|
||||
|
||||
// 创建控制点、连线节点
|
||||
function createControlNodes() {
|
||||
let { associativeLineActiveColor } = this.mindMap.themeConfig
|
||||
// 连线
|
||||
this.controlLine1 = this.draw
|
||||
.line()
|
||||
.stroke({ color: associativeLineActiveColor, width: 2 })
|
||||
this.controlLine2 = this.draw
|
||||
.line()
|
||||
.stroke({ color: associativeLineActiveColor, width: 2 })
|
||||
// 控制点
|
||||
this.controlPoint1 = this.createOneControlNode('controlPoint1')
|
||||
this.controlPoint2 = this.createOneControlNode('controlPoint2')
|
||||
}
|
||||
|
||||
// 创建控制点
|
||||
function createOneControlNode(pointKey) {
|
||||
let { associativeLineActiveColor } = this.mindMap.themeConfig
|
||||
return this.draw
|
||||
.circle(this.controlPointDiameter)
|
||||
.stroke({ color: associativeLineActiveColor })
|
||||
.fill({ color: '#fff' })
|
||||
.click(e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
.mousedown(e => {
|
||||
this.onControlPointMousedown(e, pointKey)
|
||||
})
|
||||
}
|
||||
|
||||
// 控制点的鼠标按下事件
|
||||
function onControlPointMousedown(e, pointKey) {
|
||||
e.stopPropagation()
|
||||
this.isControlPointMousedown = true
|
||||
this.mousedownControlPointKey = pointKey
|
||||
}
|
||||
|
||||
// 控制点的鼠标移动事件
|
||||
function onControlPointMousemove(e) {
|
||||
if (
|
||||
!this.isControlPointMousedown ||
|
||||
!this.mousedownControlPointKey ||
|
||||
!this[this.mousedownControlPointKey]
|
||||
)
|
||||
return
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
let radius = this.controlPointDiameter / 2
|
||||
// 转换鼠标当前的位置
|
||||
let { x, y } = this.getTransformedEventPos(e)
|
||||
this.controlPointMousemoveState.pos = {
|
||||
x,
|
||||
y
|
||||
}
|
||||
// 更新当前拖拽的控制点的位置
|
||||
this[this.mousedownControlPointKey].x(x - radius).y(y - radius)
|
||||
let [path, clickPath, text, node, toNode] = this.activeLine
|
||||
let [startPoint, endPoint] = computeNodePoints(node, toNode)
|
||||
this.controlPointMousemoveState.startPoint = startPoint
|
||||
this.controlPointMousemoveState.endPoint = endPoint
|
||||
let targetIndex = getAssociativeLineTargetIndex(node, toNode)
|
||||
this.controlPointMousemoveState.targetIndex = targetIndex
|
||||
let offsets = []
|
||||
let associativeLineTargetControlOffsets =
|
||||
node.nodeData.data.associativeLineTargetControlOffsets
|
||||
if (!associativeLineTargetControlOffsets) {
|
||||
// 兼容0.4.5版本,没有associativeLineTargetControlOffsets的情况
|
||||
offsets = getDefaultControlPointOffsets(startPoint, endPoint)
|
||||
} else {
|
||||
offsets = associativeLineTargetControlOffsets[targetIndex]
|
||||
}
|
||||
let point1 = null
|
||||
let point2 = null
|
||||
// 拖拽的是控制点1
|
||||
if (this.mousedownControlPointKey === 'controlPoint1') {
|
||||
point1 = {
|
||||
x,
|
||||
y
|
||||
}
|
||||
point2 = {
|
||||
x: endPoint.x + offsets[1].x,
|
||||
y: endPoint.y + offsets[1].y
|
||||
}
|
||||
// 更新控制点1的连线
|
||||
this.controlLine1.plot(startPoint.x, startPoint.y, point1.x, point1.y)
|
||||
} else {
|
||||
// 拖拽的是控制点2
|
||||
point1 = {
|
||||
x: startPoint.x + offsets[0].x,
|
||||
y: startPoint.y + offsets[0].y
|
||||
}
|
||||
point2 = {
|
||||
x,
|
||||
y
|
||||
}
|
||||
// 更新控制点2的连线
|
||||
this.controlLine2.plot(endPoint.x, endPoint.y, point2.x, point2.y)
|
||||
}
|
||||
// 更新关联线
|
||||
let pathStr = joinCubicBezierPath(startPoint, endPoint, point1, point2)
|
||||
path.plot(pathStr)
|
||||
clickPath.plot(pathStr)
|
||||
this.updateTextPos(path, text)
|
||||
this.updateTextEditBoxPos(text)
|
||||
}
|
||||
|
||||
// 控制点的鼠标移动事件
|
||||
function onControlPointMouseup(e) {
|
||||
if (!this.isControlPointMousedown) return
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
let { pos, startPoint, endPoint, targetIndex } =
|
||||
this.controlPointMousemoveState
|
||||
let [, , , node] = this.activeLine
|
||||
let offsetList = []
|
||||
let associativeLineTargetControlOffsets =
|
||||
node.nodeData.data.associativeLineTargetControlOffsets
|
||||
if (!associativeLineTargetControlOffsets) {
|
||||
// 兼容0.4.5版本,没有associativeLineTargetControlOffsets的情况
|
||||
offsetList[targetIndex] = getDefaultControlPointOffsets(
|
||||
startPoint,
|
||||
endPoint
|
||||
)
|
||||
} else {
|
||||
offsetList = associativeLineTargetControlOffsets
|
||||
}
|
||||
let offset1 = null
|
||||
let offset2 = null
|
||||
if (this.mousedownControlPointKey === 'controlPoint1') {
|
||||
// 更新控制点1数据
|
||||
offset1 = {
|
||||
x: pos.x - startPoint.x,
|
||||
y: pos.y - startPoint.y
|
||||
}
|
||||
offset2 = offsetList[targetIndex][1]
|
||||
} else {
|
||||
// 更新控制点2数据
|
||||
offset1 = offsetList[targetIndex][0]
|
||||
offset2 = {
|
||||
x: pos.x - endPoint.x,
|
||||
y: pos.y - endPoint.y
|
||||
}
|
||||
}
|
||||
offsetList[targetIndex] = [offset1, offset2]
|
||||
this.mindMap.execCommand('SET_NODE_DATA', node, {
|
||||
associativeLineTargetControlOffsets: offsetList
|
||||
})
|
||||
// 这里要加个setTimeout0是因为draw_click事件比mouseup事件触发的晚,所以重置isControlPointMousedown需要等draw_click事件触发完以后
|
||||
setTimeout(() => {
|
||||
this.resetControlPoint()
|
||||
}, 0)
|
||||
}
|
||||
|
||||
// 复位控制点移动
|
||||
function resetControlPoint() {
|
||||
this.isControlPointMousedown = false
|
||||
this.mousedownControlPointKey = ''
|
||||
this.controlPointMousemoveState = {
|
||||
pos: null,
|
||||
startPoint: null,
|
||||
endPoint: null,
|
||||
targetIndex: ''
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染控制点
|
||||
function renderControls(startPoint, endPoint, point1, point2) {
|
||||
if (!this.controlLine1) {
|
||||
this.createControlNodes()
|
||||
}
|
||||
let radius = this.controlPointDiameter / 2
|
||||
// 控制点和起终点的连线
|
||||
this.controlLine1.plot(startPoint.x, startPoint.y, point1.x, point1.y)
|
||||
this.controlLine2.plot(endPoint.x, endPoint.y, point2.x, point2.y)
|
||||
// 控制点
|
||||
this.controlPoint1.x(point1.x - radius).y(point1.y - radius)
|
||||
this.controlPoint2.x(point2.x - radius).y(point2.y - radius)
|
||||
}
|
||||
|
||||
// 删除控制点
|
||||
function removeControls() {
|
||||
if (!this.controlLine1) return
|
||||
;[
|
||||
this.controlLine1,
|
||||
this.controlLine2,
|
||||
this.controlPoint1,
|
||||
this.controlPoint2
|
||||
].forEach(item => {
|
||||
item.remove()
|
||||
})
|
||||
this.controlLine1 = null
|
||||
this.controlLine2 = null
|
||||
this.controlPoint1 = null
|
||||
this.controlPoint2 = null
|
||||
}
|
||||
|
||||
// 隐藏控制点
|
||||
function hideControls() {
|
||||
if (!this.controlLine1) return
|
||||
;[
|
||||
this.controlLine1,
|
||||
this.controlLine2,
|
||||
this.controlPoint1,
|
||||
this.controlPoint2
|
||||
].forEach(item => {
|
||||
item.hide()
|
||||
})
|
||||
}
|
||||
|
||||
// 显示控制点
|
||||
function showControls() {
|
||||
if (!this.controlLine1) return
|
||||
;[
|
||||
this.controlLine1,
|
||||
this.controlLine2,
|
||||
this.controlPoint1,
|
||||
this.controlPoint2
|
||||
].forEach(item => {
|
||||
item.show()
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
createControlNodes,
|
||||
createOneControlNode,
|
||||
onControlPointMousedown,
|
||||
onControlPointMousemove,
|
||||
onControlPointMouseup,
|
||||
resetControlPoint,
|
||||
renderControls,
|
||||
removeControls,
|
||||
hideControls,
|
||||
showControls
|
||||
}
|
||||
167
simple-mind-map/src/utils/associativeLineText.js
Normal file
@@ -0,0 +1,167 @@
|
||||
import { Text } from '@svgdotjs/svg.js'
|
||||
import { getStrWithBrFromHtml } from './index'
|
||||
|
||||
// 创建文字节点
|
||||
function createText(data) {
|
||||
let g = this.draw.group()
|
||||
const setActive = () => {
|
||||
if (
|
||||
!this.activeLine ||
|
||||
this.activeLine[3] !== data.node ||
|
||||
this.activeLine[4] !== data.toNode
|
||||
) {
|
||||
this.setActiveLine({
|
||||
...data,
|
||||
text: g
|
||||
})
|
||||
}
|
||||
}
|
||||
g.click(e => {
|
||||
e.stopPropagation()
|
||||
setActive()
|
||||
})
|
||||
g.on('dblclick', e => {
|
||||
e.stopPropagation()
|
||||
setActive()
|
||||
if (!this.activeLine) return
|
||||
this.showEditTextBox(g)
|
||||
})
|
||||
return g
|
||||
}
|
||||
|
||||
// 显示文本编辑框
|
||||
function showEditTextBox(g) {
|
||||
this.mindMap.emit('before_show_text_edit')
|
||||
// 注册回车快捷键
|
||||
this.mindMap.keyCommand.addShortcut('Enter', () => {
|
||||
this.hideEditTextBox()
|
||||
})
|
||||
|
||||
if (!this.textEditNode) {
|
||||
this.textEditNode = document.createElement('div')
|
||||
this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;background-color:#fff;box-shadow: 0 0 20px rgba(0,0,0,.5);padding: 3px 5px;margin-left: -5px;margin-top: -3px;outline: none; word-break: break-all;`
|
||||
this.textEditNode.setAttribute('contenteditable', true)
|
||||
this.textEditNode.addEventListener('keyup', e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
this.textEditNode.addEventListener('click', e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
document.body.appendChild(this.textEditNode)
|
||||
}
|
||||
let {
|
||||
associativeLineTextFontSize,
|
||||
associativeLineTextFontFamily,
|
||||
associativeLineTextLineHeight
|
||||
} = this.mindMap.themeConfig
|
||||
let scale = this.mindMap.view.scale
|
||||
let [, , , node, toNode] = this.activeLine
|
||||
let textLines = (
|
||||
this.getText(node, toNode) || this.mindMap.opt.defaultAssociativeLineText
|
||||
).split(/\n/gim)
|
||||
this.textEditNode.style.fontFamily = associativeLineTextFontFamily
|
||||
this.textEditNode.style.fontSize = associativeLineTextFontSize * scale + 'px'
|
||||
this.textEditNode.style.lineHeight = textLines.length > 1 ? associativeLineTextLineHeight : 'normal'
|
||||
this.textEditNode.style.zIndex = this.mindMap.opt.nodeTextEditZIndex
|
||||
this.textEditNode.innerHTML = textLines.join('<br>')
|
||||
this.textEditNode.style.display = 'block'
|
||||
this.updateTextEditBoxPos(g)
|
||||
this.showTextEdit = true
|
||||
}
|
||||
|
||||
// 处理画布缩放
|
||||
function onScale() {
|
||||
this.hideEditTextBox()
|
||||
}
|
||||
|
||||
// 更新文本编辑框位置
|
||||
function updateTextEditBoxPos(g) {
|
||||
let rect = g.node.getBoundingClientRect()
|
||||
this.textEditNode.style.minWidth = rect.width + 10 + 'px'
|
||||
this.textEditNode.style.minHeight = rect.height + 6 + 'px'
|
||||
this.textEditNode.style.left = rect.left + 'px'
|
||||
this.textEditNode.style.top = rect.top + 'px'
|
||||
}
|
||||
|
||||
// 隐藏文本编辑框
|
||||
function hideEditTextBox() {
|
||||
if (!this.showTextEdit) {
|
||||
return
|
||||
}
|
||||
let [path, , text, node, toNode] = this.activeLine
|
||||
let str = getStrWithBrFromHtml(this.textEditNode.innerHTML)
|
||||
this.mindMap.execCommand('SET_NODE_DATA', node, {
|
||||
associativeLineText: {
|
||||
...(node.nodeData.data.associativeLineText || {}),
|
||||
[toNode.nodeData.data.id]: str
|
||||
}
|
||||
})
|
||||
this.textEditNode.style.display = 'none'
|
||||
this.textEditNode.innerHTML = ''
|
||||
this.showTextEdit = false
|
||||
this.renderText(str, path, text)
|
||||
this.mindMap.emit('hide_text_edit')
|
||||
}
|
||||
|
||||
// 获取某根关联线的文字
|
||||
function getText(node, toNode) {
|
||||
let obj = node.nodeData.data.associativeLineText
|
||||
if (!obj) {
|
||||
return ''
|
||||
}
|
||||
return obj[toNode.nodeData.data.id] || ''
|
||||
}
|
||||
|
||||
// 渲染关联线文字
|
||||
function renderText(str, path, text) {
|
||||
if (!str) return
|
||||
let { associativeLineTextFontSize, associativeLineTextLineHeight } =
|
||||
this.mindMap.themeConfig
|
||||
text.clear()
|
||||
let textArr = str.split(/\n/gim)
|
||||
textArr.forEach((item, index) => {
|
||||
let node = new Text().text(item)
|
||||
node.y(associativeLineTextFontSize * associativeLineTextLineHeight * index)
|
||||
this.styleText(node)
|
||||
text.add(node)
|
||||
})
|
||||
updateTextPos(path, text)
|
||||
}
|
||||
|
||||
// 给文本设置样式
|
||||
function styleText(node) {
|
||||
let {
|
||||
associativeLineTextColor,
|
||||
associativeLineTextFontSize,
|
||||
associativeLineTextFontFamily
|
||||
} = this.mindMap.themeConfig
|
||||
node
|
||||
.fill({
|
||||
color: associativeLineTextColor
|
||||
})
|
||||
.css({
|
||||
'font-family': associativeLineTextFontFamily,
|
||||
'font-size': associativeLineTextFontSize
|
||||
})
|
||||
}
|
||||
|
||||
// 更新关联线文字位置
|
||||
function updateTextPos(path, text) {
|
||||
let pathLength = path.length()
|
||||
let centerPoint = path.pointAt(pathLength / 2)
|
||||
let { width: textWidth, height: textHeight } = text.bbox()
|
||||
text.x(centerPoint.x - textWidth / 2)
|
||||
text.y(centerPoint.y - textHeight / 2)
|
||||
}
|
||||
|
||||
export default {
|
||||
getText,
|
||||
createText,
|
||||
styleText,
|
||||
onScale,
|
||||
showEditTextBox,
|
||||
hideEditTextBox,
|
||||
updateTextEditBoxPos,
|
||||
renderText,
|
||||
updateTextPos
|
||||
}
|
||||
@@ -28,34 +28,6 @@ export const themeList = [
|
||||
name: '默认',
|
||||
value: 'default',
|
||||
},
|
||||
{
|
||||
name: '脑图经典',
|
||||
value: 'classic',
|
||||
},
|
||||
{
|
||||
name: '小黄人',
|
||||
value: 'minions',
|
||||
},
|
||||
{
|
||||
name: '粉红葡萄',
|
||||
value: 'pinkGrape',
|
||||
},
|
||||
{
|
||||
name: '薄荷',
|
||||
value: 'mint',
|
||||
},
|
||||
{
|
||||
name: '金色vip',
|
||||
value: 'gold',
|
||||
},
|
||||
{
|
||||
name: '活力橙',
|
||||
value: 'vitalityOrange',
|
||||
},
|
||||
{
|
||||
name: '绿叶',
|
||||
value: 'greenLeaf',
|
||||
},
|
||||
{
|
||||
name: '暗色2',
|
||||
value: 'dark2',
|
||||
@@ -72,10 +44,6 @@ export const themeList = [
|
||||
name: '脑图经典3',
|
||||
value: 'classic3',
|
||||
},
|
||||
{
|
||||
name: '脑图经典4',
|
||||
value: 'classic4',
|
||||
},
|
||||
{
|
||||
name: '经典绿',
|
||||
value: 'classicGreen',
|
||||
@@ -112,6 +80,38 @@ export const themeList = [
|
||||
name: '浪漫紫',
|
||||
value: 'romanticPurple',
|
||||
},
|
||||
{
|
||||
name: '粉红葡萄',
|
||||
value: 'pinkGrape',
|
||||
},
|
||||
{
|
||||
name: '薄荷',
|
||||
value: 'mint',
|
||||
},
|
||||
{
|
||||
name: '金色vip',
|
||||
value: 'gold',
|
||||
},
|
||||
{
|
||||
name: '活力橙',
|
||||
value: 'vitalityOrange',
|
||||
},
|
||||
{
|
||||
name: '绿叶',
|
||||
value: 'greenLeaf',
|
||||
},
|
||||
{
|
||||
name: '脑图经典',
|
||||
value: 'classic',
|
||||
},
|
||||
{
|
||||
name: '脑图经典4',
|
||||
value: 'classic4',
|
||||
},
|
||||
{
|
||||
name: '小黄人',
|
||||
value: 'minions',
|
||||
},
|
||||
{
|
||||
name: '简约黑',
|
||||
value: 'simpleBlack',
|
||||
@@ -139,12 +139,25 @@ export const themeList = [
|
||||
{
|
||||
name: '黑金',
|
||||
value: 'blackGold',
|
||||
},
|
||||
{
|
||||
name: '牛油果',
|
||||
value: 'avocado',
|
||||
},
|
||||
{
|
||||
name: '秋天',
|
||||
value: 'autumn',
|
||||
},
|
||||
{
|
||||
name: '橙汁',
|
||||
value: 'orangeJuice',
|
||||
}
|
||||
]
|
||||
|
||||
// 常量
|
||||
export const CONSTANTS = {
|
||||
CHANGE_THEME: 'changeTheme',
|
||||
SET_DATA: 'setData',
|
||||
TRANSFORM_TO_NORMAL_NODE: 'transformAllNodesToNormalNode',
|
||||
MODE: {
|
||||
READONLY: 'readonly',
|
||||
@@ -154,7 +167,10 @@ export const CONSTANTS = {
|
||||
LOGICAL_STRUCTURE: 'logicalStructure',
|
||||
MIND_MAP: 'mindMap',
|
||||
ORGANIZATION_STRUCTURE: 'organizationStructure',
|
||||
CATALOG_ORGANIZATION: 'catalogOrganization'
|
||||
CATALOG_ORGANIZATION: 'catalogOrganization',
|
||||
TIMELINE: 'timeline',
|
||||
TIMELINE2: 'timeline2',
|
||||
FISHBONE: 'fishbone'
|
||||
},
|
||||
DIR: {
|
||||
UP: 'up',
|
||||
@@ -189,6 +205,10 @@ export const CONSTANTS = {
|
||||
RIGHT: 'right',
|
||||
BOTTOM: 'bottom',
|
||||
CENTER: 'center'
|
||||
},
|
||||
TIMELINE_DIR: {
|
||||
TOP: 'top',
|
||||
BOTTOM: 'bottom'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,11 +237,26 @@ export const layoutList = [
|
||||
{
|
||||
name: '目录组织图',
|
||||
value: CONSTANTS.LAYOUT.CATALOG_ORGANIZATION,
|
||||
},
|
||||
{
|
||||
name: '时间轴',
|
||||
value: CONSTANTS.LAYOUT.TIMELINE,
|
||||
},
|
||||
{
|
||||
name: '时间轴2',
|
||||
value: CONSTANTS.LAYOUT.TIMELINE2,
|
||||
},
|
||||
{
|
||||
name: '鱼骨图',
|
||||
value: CONSTANTS.LAYOUT.FISHBONE,
|
||||
}
|
||||
]
|
||||
export const layoutValueList = [
|
||||
CONSTANTS.LAYOUT.LOGICAL_STRUCTURE,
|
||||
CONSTANTS.LAYOUT.MIND_MAP,
|
||||
CONSTANTS.LAYOUT.CATALOG_ORGANIZATION,
|
||||
CONSTANTS.LAYOUT.ORGANIZATION_STRUCTURE
|
||||
CONSTANTS.LAYOUT.ORGANIZATION_STRUCTURE,
|
||||
CONSTANTS.LAYOUT.TIMELINE,
|
||||
CONSTANTS.LAYOUT.TIMELINE2,
|
||||
CONSTANTS.LAYOUT.FISHBONE
|
||||
]
|
||||
@@ -302,4 +302,58 @@ export const nextTick = function (fn, ctx) {
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { measureText, resizeImgSize } from '../utils'
|
||||
import { measureText, resizeImgSize, getTextFromHtml } from '../utils'
|
||||
import { Image, SVG, A, G, Rect, Text, ForeignObject } from '@svgdotjs/svg.js'
|
||||
import iconsSvg from '../svg/icons'
|
||||
import { CONSTANTS } from './constant'
|
||||
|
||||
// 创建图片节点
|
||||
function createImgNode() {
|
||||
@@ -41,8 +42,18 @@ function createIconNode() {
|
||||
}
|
||||
let iconSize = this.mindMap.themeConfig.iconSize
|
||||
return _data.icon.map(item => {
|
||||
let src = iconsSvg.getNodeIconListIcon(item, this.mindMap.opt.iconList || [])
|
||||
let node = null
|
||||
// svg图标
|
||||
if (/^<svg/.test(src)) {
|
||||
node = SVG(src)
|
||||
} else {
|
||||
// 图片图标
|
||||
node = new Image().load(src)
|
||||
}
|
||||
node.size(iconSize, iconSize)
|
||||
return {
|
||||
node: SVG(iconsSvg.getNodeIconListIcon(item)).size(iconSize, iconSize),
|
||||
node,
|
||||
width: iconSize,
|
||||
height: iconSize
|
||||
}
|
||||
@@ -52,11 +63,18 @@ function createIconNode() {
|
||||
// 创建富文本节点
|
||||
function createRichTextNode() {
|
||||
let g = new G()
|
||||
// 重新设置富文本节点内容
|
||||
if (this.nodeData.data.resetRichText || [CONSTANTS.CHANGE_THEME].includes(this.mindMap.renderer.renderSource)) {
|
||||
delete this.nodeData.data.resetRichText
|
||||
let text = getTextFromHtml(this.nodeData.data.text)
|
||||
this.nodeData.data.text = `<p><span style="${this.style.createStyleText()}">${text}</span></p>`
|
||||
}
|
||||
let html = `<div>${this.nodeData.data.text}</div>`
|
||||
let div = document.createElement('div')
|
||||
div.innerHTML = html
|
||||
div.style.cssText = `position: fixed; left: -999999px;`
|
||||
let el = div.children[0]
|
||||
el.classList.add('smm-richtext-node-wrap')
|
||||
el.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml')
|
||||
el.style.maxWidth = this.mindMap.opt.textAutoWrapWidth + 'px'
|
||||
this.mindMap.el.appendChild(div)
|
||||
@@ -95,21 +113,27 @@ function createTextNode() {
|
||||
let textStyle = this.style.getTextFontStyle()
|
||||
let textArr = this.nodeData.data.text.split(/\n/gim)
|
||||
let maxWidth = this.mindMap.opt.textAutoWrapWidth
|
||||
let isMultiLine = false
|
||||
textArr.forEach((item, index) => {
|
||||
let arr = item.split('')
|
||||
let lines = []
|
||||
let line = []
|
||||
while (arr.length) {
|
||||
line.push(arr.shift())
|
||||
let text = line.join('')
|
||||
if (measureText(text, textStyle).width >= maxWidth) {
|
||||
lines.push(text)
|
||||
line = []
|
||||
let str = arr.shift()
|
||||
let text = [...line, str].join('')
|
||||
if (measureText(text, textStyle).width <= maxWidth) {
|
||||
line.push(str)
|
||||
} else {
|
||||
lines.push(line.join(''))
|
||||
line = [str]
|
||||
}
|
||||
}
|
||||
if (line.length > 0) {
|
||||
lines.push(line.join(''))
|
||||
}
|
||||
if (lines.length > 1) {
|
||||
isMultiLine = true
|
||||
}
|
||||
textArr[index] = lines.join('\n')
|
||||
})
|
||||
textArr = textArr.join('\n').split(/\n/gim)
|
||||
@@ -124,6 +148,7 @@ function createTextNode() {
|
||||
height = Math.ceil(height)
|
||||
g.attr('data-width', width)
|
||||
g.attr('data-height', height)
|
||||
g.attr('data-ismultiLine', isMultiLine || textArr.length > 1)
|
||||
return {
|
||||
node: g,
|
||||
width,
|
||||
@@ -205,13 +230,14 @@ function createNoteNode() {
|
||||
if (!this.noteEl) {
|
||||
this.noteEl = document.createElement('div')
|
||||
this.noteEl.style.cssText = `
|
||||
position: absolute;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 5px rgb(0 0 0 / 10%);
|
||||
display: none;
|
||||
background-color: #fff;
|
||||
`
|
||||
position: absolute;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 5px rgb(0 0 0 / 10%);
|
||||
display: none;
|
||||
background-color: #fff;
|
||||
z-index: ${ this.mindMap.opt.nodeNoteTooltipZIndex }
|
||||
`
|
||||
document.body.appendChild(this.noteEl)
|
||||
}
|
||||
this.noteEl.innerText = this.nodeData.data.note
|
||||
|
||||
@@ -32,15 +32,20 @@ function createExpandNodeContent() {
|
||||
|
||||
// 创建或更新展开收缩按钮内容
|
||||
function updateExpandBtnNode() {
|
||||
let { expand } = this.nodeData.data
|
||||
// 如果本次和上次的展开状态一样则返回
|
||||
if (expand === this._lastExpandBtnType) return
|
||||
if (this._expandBtn) {
|
||||
this._expandBtn.clear()
|
||||
}
|
||||
this.createExpandNodeContent()
|
||||
let node
|
||||
if (this.nodeData.data.expand === false) {
|
||||
if (expand === false) {
|
||||
node = this._openExpandNode
|
||||
this._lastExpandBtnType = false
|
||||
} else {
|
||||
node = this._closeExpandNode
|
||||
this._lastExpandBtnType = true
|
||||
}
|
||||
if (this._expandBtn) this._expandBtn.add(this._fillExpandNode).add(node)
|
||||
}
|
||||
@@ -93,14 +98,36 @@ function renderExpandBtn() {
|
||||
})
|
||||
this.group.add(this._expandBtn)
|
||||
}
|
||||
this._showExpandBtn = true
|
||||
this.updateExpandBtnNode()
|
||||
this.updateExpandBtnPos()
|
||||
}
|
||||
|
||||
// 移除展开收缩按钮
|
||||
function removeExpandBtn() {
|
||||
if (this._expandBtn) {
|
||||
if (this._expandBtn && this._showExpandBtn) {
|
||||
this._expandBtn.remove()
|
||||
this._showExpandBtn = false
|
||||
}
|
||||
}
|
||||
|
||||
// 显示展开收起按钮
|
||||
function showExpandBtn() {
|
||||
if (this.mindMap.opt.alwaysShowExpandBtn) return
|
||||
setTimeout(() => {
|
||||
this.renderExpandBtn()
|
||||
}, 0)
|
||||
}
|
||||
|
||||
// 隐藏展开收起按钮
|
||||
function hideExpandBtn() {
|
||||
if (this.mindMap.opt.alwaysShowExpandBtn || this._isMouseenter) return
|
||||
// 非激活状态且展开状态鼠标移出才隐藏按钮
|
||||
let { isActive, expand } = this.nodeData.data
|
||||
if (!isActive && expand) {
|
||||
setTimeout(() => {
|
||||
this.removeExpandBtn()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,5 +136,7 @@ export default {
|
||||
updateExpandBtnNode,
|
||||
updateExpandBtnPos,
|
||||
renderExpandBtn,
|
||||
removeExpandBtn
|
||||
removeExpandBtn,
|
||||
showExpandBtn,
|
||||
hideExpandBtn
|
||||
}
|
||||
|
||||
BIN
web/build/icons/icon.icns
Normal file
BIN
web/build/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
web/build/icons/icon.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
2023
web/dist_electron/index.js
Normal file
84
web/dist_electron/package.json
Normal file
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"name": "thoughts",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "一个简洁的思维导图",
|
||||
"author": "街角小林<1013335014@qq.com>",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/wanglin2/mind-map"
|
||||
},
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build && node ../copy.js",
|
||||
"lint": "vue-cli-service lint",
|
||||
"autoBuildDoc": "node ./scripts/autoBuildDoc.js",
|
||||
"buildDoc": "node ./scripts/buildDoc.js",
|
||||
"electron:build": "vue-cli-service electron:build",
|
||||
"electron:build-all": "vue-cli-service electron:build -p never -mwl",
|
||||
"electron:build-mac": "vue-cli-service electron:build -p never -m",
|
||||
"electron:build-win": "vue-cli-service electron:build -p never -w",
|
||||
"electron:build-linux": "vue-cli-service electron:build -p never -l",
|
||||
"electron:serve": "vue-cli-service electron:serve",
|
||||
"buildLibrary": "vue-cli-service build --target lib --name simpleMindMap ../simple-mind-map/full.js --dest ../simple-mind-map/dist && esbuild ../simple-mind-map/full.js --bundle --external:buffer --format=esm --outfile=../simple-mind-map/dist/simpleMindMap.esm.js",
|
||||
"format": "prettier --write src/* src/*/* src/*/*/* src/*/*/*/*",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"postuninstall": "electron-builder install-app-deps"
|
||||
},
|
||||
"main": "background.js",
|
||||
"dependencies": {
|
||||
"@toast-ui/editor": "^3.1.5",
|
||||
"core-js": "^3.6.5",
|
||||
"electron-json-storage": "^4.6.0",
|
||||
"element-ui": "^2.15.1",
|
||||
"fs-extra": "^7.0.1",
|
||||
"highlight.js": "^10.7.3",
|
||||
"open": "^6.4.0",
|
||||
"uuid": "^3.4.0",
|
||||
"v-viewer": "^1.6.4",
|
||||
"vue": "^2.6.11",
|
||||
"vue-i18n": "^8.27.2",
|
||||
"vue-router": "^3.5.1",
|
||||
"vuex": "^3.6.2",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^4.5.0",
|
||||
"@vue/cli-plugin-eslint": "^4.5.0",
|
||||
"@vue/cli-service": "^4.5.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"chokidar": "^3.5.3",
|
||||
"electron": "^23.1.1",
|
||||
"electron-devtools-installer": "^3.1.0",
|
||||
"esbuild": "^0.17.15",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"less": "^3.12.2",
|
||||
"less-loader": "^7.1.0",
|
||||
"markdown-it": "^13.0.1",
|
||||
"markdown-it-checkbox": "^1.1.0",
|
||||
"prettier": "^1.19.1",
|
||||
"vue-cli-plugin-electron-builder": "~2.1.1",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"webpack": "^4.44.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/essential",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
},
|
||||
"rules": {}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
]
|
||||
}
|
||||
122
web/dist_electron/preload.js
Normal file
@@ -0,0 +1,122 @@
|
||||
/******/ (function(modules) { // webpackBootstrap
|
||||
/******/ // The module cache
|
||||
/******/ var installedModules = {};
|
||||
/******/
|
||||
/******/ // The require function
|
||||
/******/ function __webpack_require__(moduleId) {
|
||||
/******/
|
||||
/******/ // Check if module is in cache
|
||||
/******/ if(installedModules[moduleId]) {
|
||||
/******/ return installedModules[moduleId].exports;
|
||||
/******/ }
|
||||
/******/ // Create a new module (and put it into the cache)
|
||||
/******/ var module = installedModules[moduleId] = {
|
||||
/******/ i: moduleId,
|
||||
/******/ l: false,
|
||||
/******/ exports: {}
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // Execute the module function
|
||||
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
||||
/******/
|
||||
/******/ // Flag the module as loaded
|
||||
/******/ module.l = true;
|
||||
/******/
|
||||
/******/ // Return the exports of the module
|
||||
/******/ return module.exports;
|
||||
/******/ }
|
||||
/******/
|
||||
/******/
|
||||
/******/ // expose the modules object (__webpack_modules__)
|
||||
/******/ __webpack_require__.m = modules;
|
||||
/******/
|
||||
/******/ // expose the module cache
|
||||
/******/ __webpack_require__.c = installedModules;
|
||||
/******/
|
||||
/******/ // define getter function for harmony exports
|
||||
/******/ __webpack_require__.d = function(exports, name, getter) {
|
||||
/******/ if(!__webpack_require__.o(exports, name)) {
|
||||
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
|
||||
/******/ }
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // define __esModule on exports
|
||||
/******/ __webpack_require__.r = function(exports) {
|
||||
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
|
||||
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
||||
/******/ }
|
||||
/******/ Object.defineProperty(exports, '__esModule', { value: true });
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // create a fake namespace object
|
||||
/******/ // mode & 1: value is a module id, require it
|
||||
/******/ // mode & 2: merge all properties of value into the ns
|
||||
/******/ // mode & 4: return value when already ns object
|
||||
/******/ // mode & 8|1: behave like require
|
||||
/******/ __webpack_require__.t = function(value, mode) {
|
||||
/******/ if(mode & 1) value = __webpack_require__(value);
|
||||
/******/ if(mode & 8) return value;
|
||||
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
|
||||
/******/ var ns = Object.create(null);
|
||||
/******/ __webpack_require__.r(ns);
|
||||
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
|
||||
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
|
||||
/******/ return ns;
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
||||
/******/ __webpack_require__.n = function(module) {
|
||||
/******/ var getter = module && module.__esModule ?
|
||||
/******/ function getDefault() { return module['default']; } :
|
||||
/******/ function getModuleExports() { return module; };
|
||||
/******/ __webpack_require__.d(getter, 'a', getter);
|
||||
/******/ return getter;
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // Object.prototype.hasOwnProperty.call
|
||||
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
|
||||
/******/
|
||||
/******/ // __webpack_public_path__
|
||||
/******/ __webpack_require__.p = "";
|
||||
/******/
|
||||
/******/
|
||||
/******/ // Load entry module and return exports
|
||||
/******/ return __webpack_require__(__webpack_require__.s = 0);
|
||||
/******/ })
|
||||
/************************************************************************/
|
||||
/******/ ({
|
||||
|
||||
/***/ "./src/electron/preload.js":
|
||||
/*!*********************************!*\
|
||||
!*** ./src/electron/preload.js ***!
|
||||
\*********************************/
|
||||
/*! no static exports found */
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
eval("const { contextBridge, ipcRenderer } = __webpack_require__(/*! electron */ \"electron\")\r\n\r\ncontextBridge.exposeInMainWorld('platform', process.platform)\r\ncontextBridge.exposeInMainWorld('IS_ELECTRON', true)\r\n\r\ncontextBridge.exposeInMainWorld('electronAPI', {\r\n minimize: () => ipcRenderer.send('minimize'),\r\n maximize: () => ipcRenderer.send('maximize'),\r\n unmaximize: () => ipcRenderer.send('unmaximize'),\r\n close: () => ipcRenderer.send('close'),\r\n destroy: () => ipcRenderer.send('destroy'),\r\n create: id => ipcRenderer.send('create', id),\r\n getFileContent: id => ipcRenderer.invoke('getFileContent', id),\r\n save: (id, data, fileName) => ipcRenderer.invoke('save', id, data, fileName),\r\n rename: (id, name) => ipcRenderer.invoke('rename', id, name),\r\n openUrl: url => ipcRenderer.send('openUrl', url),\r\n addRecentFileList: (fileList) => ipcRenderer.invoke('addRecentFileList', fileList),\r\n getRecentFileList: () => ipcRenderer.invoke('getRecentFileList'),\r\n clearRecentFileList: () => ipcRenderer.invoke('clearRecentFileList'),\r\n openFileInDir: file => ipcRenderer.send('openFileInDir', file),\r\n deleteFile: file => ipcRenderer.invoke('deleteFile', file),\r\n onRefreshRecentFileList: callback =>\r\n ipcRenderer.on('refreshRecentFileList', callback),\r\n openFile: file => ipcRenderer.send('openFile', file),\r\n selectOpenFile: () => ipcRenderer.send('selectOpenFile'),\r\n copyFile: file => ipcRenderer.invoke('copyFile', file)\r\n})\r\n\n\n//# sourceURL=webpack:///./src/electron/preload.js?");
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 0:
|
||||
/*!***************************************!*\
|
||||
!*** multi ./src/electron/preload.js ***!
|
||||
\***************************************/
|
||||
/*! no static exports found */
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
eval("module.exports = __webpack_require__(/*! E:\\wanglin\\mind-map\\web\\src\\electron\\preload.js */\"./src/electron/preload.js\");\n\n\n//# sourceURL=webpack:///multi_./src/electron/preload.js?");
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "electron":
|
||||
/*!***************************!*\
|
||||
!*** external "electron" ***!
|
||||
\***************************/
|
||||
/*! no static exports found */
|
||||
/***/ (function(module, exports) {
|
||||
|
||||
eval("module.exports = require(\"electron\");\n\n//# sourceURL=webpack:///external_%22electron%22?");
|
||||
|
||||
/***/ })
|
||||
|
||||
/******/ });
|
||||
18421
web/package-lock.json
generated
@@ -2,20 +2,39 @@
|
||||
"name": "thoughts",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "一个简洁的思维导图",
|
||||
"author": "街角小林<1013335014@qq.com>",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/wanglin2/mind-map"
|
||||
},
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build && node ../copy.js",
|
||||
"lint": "vue-cli-service lint",
|
||||
"autoBuildDoc": "node ./scripts/autoBuildDoc.js",
|
||||
"buildDoc": "node ./scripts/buildDoc.js",
|
||||
"electron:build": "vue-cli-service electron:build",
|
||||
"electron:build-all": "vue-cli-service electron:build -p never -mwl",
|
||||
"electron:build-mac": "vue-cli-service electron:build -p never -m",
|
||||
"electron:build-win": "vue-cli-service electron:build -p never -w",
|
||||
"electron:build-linux": "vue-cli-service electron:build -p never -l",
|
||||
"electron:serve": "vue-cli-service electron:serve",
|
||||
"buildLibrary": "vue-cli-service build --target lib --name simpleMindMap ../simple-mind-map/full.js --dest ../simple-mind-map/dist && esbuild ../simple-mind-map/full.js --bundle --external:buffer --format=esm --outfile=../simple-mind-map/dist/simpleMindMap.esm.js",
|
||||
"format": "prettier --write src/* src/*/* src/*/*/* src/*/*/*/*",
|
||||
"buildDoc": "node ./scripts/buildDoc.js",
|
||||
"autoBuildDoc": "node ./scripts/autoBuildDoc.js"
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"postuninstall": "electron-builder install-app-deps"
|
||||
},
|
||||
"main": "background.js",
|
||||
"dependencies": {
|
||||
"@toast-ui/editor": "^3.1.5",
|
||||
"core-js": "^3.6.5",
|
||||
"electron-json-storage": "^4.6.0",
|
||||
"element-ui": "^2.15.1",
|
||||
"fs-extra": "^7.0.1",
|
||||
"highlight.js": "^10.7.3",
|
||||
"open": "^6.4.0",
|
||||
"uuid": "^3.4.0",
|
||||
"v-viewer": "^1.6.4",
|
||||
"vue": "^2.6.11",
|
||||
"vue-i18n": "^8.27.2",
|
||||
@@ -29,6 +48,8 @@
|
||||
"@vue/cli-service": "^4.5.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"chokidar": "^3.5.3",
|
||||
"electron": "^23.1.1",
|
||||
"electron-devtools-installer": "^3.1.0",
|
||||
"esbuild": "^0.17.15",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
@@ -37,6 +58,7 @@
|
||||
"markdown-it": "^13.0.1",
|
||||
"markdown-it-checkbox": "^1.1.0",
|
||||
"prettier": "^1.19.1",
|
||||
"vue-cli-plugin-electron-builder": "~2.1.1",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"webpack": "^4.44.2"
|
||||
},
|
||||
|
||||
BIN
web/src/.DS_Store
vendored
@@ -34,7 +34,17 @@ export const getData = () => {
|
||||
return simpleDeepClone(exampleData)
|
||||
} else {
|
||||
try {
|
||||
return JSON.parse(store)
|
||||
let parsedData = JSON.parse(store)
|
||||
if (window.IS_ELECTRON) {
|
||||
return simpleDeepClone(exampleData)
|
||||
let { root, ...rest } = parsedData
|
||||
return {
|
||||
...rest,
|
||||
root: simpleDeepClone(exampleData).root
|
||||
}
|
||||
} else {
|
||||
return parsedData
|
||||
}
|
||||
} catch (error) {
|
||||
return simpleDeepClone(exampleData)
|
||||
}
|
||||
|
||||
BIN
web/src/assets/.DS_Store
vendored
BIN
web/src/assets/avatar/Think.jpg
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
web/src/assets/img/autumn.jpg
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
web/src/assets/img/avocado.jpg
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
web/src/assets/img/fishbone.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
web/src/assets/img/icon.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
web/src/assets/img/iconList.jpg
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
web/src/assets/img/orangeJuice.jpg
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
web/src/assets/img/timeline.jpg
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
web/src/assets/img/timeline2.jpg
Normal file
|
After Width: | Height: | Size: 26 KiB |
114
web/src/background.js
Normal file
@@ -0,0 +1,114 @@
|
||||
'use strict'
|
||||
|
||||
import { app, protocol, BrowserWindow } from 'electron'
|
||||
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'
|
||||
import path from 'path'
|
||||
import { bindFileHandleEvent } from './electron/fileHandle'
|
||||
import { bindOtherHandleEvent } from './electron/otherHandle'
|
||||
|
||||
const isDevelopment = process.env.NODE_ENV !== 'production'
|
||||
|
||||
// 在应用程序准备就绪之前,必须注册方案
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{ scheme: 'app', privileges: { secure: true, standard: true } }
|
||||
])
|
||||
|
||||
// 创建主页面
|
||||
let mainWindow = null
|
||||
async function createMainWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
frame: false,
|
||||
titleBarStyle: 'hiddenInset',
|
||||
webPreferences: {
|
||||
webSecurity: false,
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
}
|
||||
})
|
||||
|
||||
if (process.env.WEBPACK_DEV_SERVER_URL) {
|
||||
// 如果处于开发模式,则加载开发服务器的url
|
||||
await mainWindow.loadURL(
|
||||
process.env.WEBPACK_DEV_SERVER_URL + '/#/workbenche'
|
||||
)
|
||||
// if (!process.env.IS_TEST) mainWindow.webContents.openDevTools()
|
||||
} else {
|
||||
createProtocol('app')
|
||||
// 非开发环境时加载index.html
|
||||
mainWindow.loadURL('app://./index.html/#/workbenche')
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
const bindEvent = () => {
|
||||
bindFileHandleEvent({ mainWindow, initOpenFileQueue })
|
||||
bindOtherHandleEvent()
|
||||
}
|
||||
|
||||
// 关闭所有窗口后退出
|
||||
app.on('window-all-closed', () => {
|
||||
// 在macOS上,应用程序及其菜单栏通常保持活动状态,直到用户使用Cmd+Q明确退出
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
app.on('activate', () => {
|
||||
// 在macOS上,当点击dock图标且没有其他窗口打开时,通常会在应用程序中重新创建一个窗口。
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createMainWindow()
|
||||
// bindEvent()
|
||||
}
|
||||
})
|
||||
|
||||
// Attempt to bind file opening #2
|
||||
// https://stackoverflow.com/questions/62420427/how-do-i-make-my-electron-app-the-default-for-opening-files
|
||||
// https://github.com/rchrd2/example-electron-file-association
|
||||
// https://www.jianshu.com/p/a32542277b83
|
||||
const initOpenFileQueue = []
|
||||
app.on('will-finish-launching', () => {
|
||||
// Event fired When someone drags files onto the icon while your app is running
|
||||
if (process.platform == 'win32') {
|
||||
const argv = process.argv
|
||||
if (argv) {
|
||||
argv.forEach(filePath => {
|
||||
if (filePath.indexOf('.smm') >= 0) {
|
||||
initOpenFileQueue.push(filePath)
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
app.on('open-file', (event, file) => {
|
||||
if (app.isReady() === false) {
|
||||
initOpenFileQueue.push(file)
|
||||
} else {
|
||||
console.log(file)
|
||||
}
|
||||
event.preventDefault()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
app.on('ready', async () => {
|
||||
createMainWindow()
|
||||
bindEvent()
|
||||
})
|
||||
|
||||
// 在开发模式下,应父进程的请求干净地退出。
|
||||
if (isDevelopment) {
|
||||
if (process.platform === 'win32') {
|
||||
process.on('message', data => {
|
||||
if (data === 'graceful-exit') {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
process.on('SIGTERM', () => {
|
||||
app.quit()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,10 @@ export const layoutImgMap = {
|
||||
logicalStructure: require('../assets/img/logicalStructure.jpg'),
|
||||
mindMap: require('../assets/img/mindMap.jpg'),
|
||||
organizationStructure: require('../assets/img/organizationStructure.jpg'),
|
||||
catalogOrganization: require('../assets/img/catalogOrganization.jpg')
|
||||
catalogOrganization: require('../assets/img/catalogOrganization.jpg'),
|
||||
timeline: require('../assets/img/timeline.jpg'),
|
||||
timeline2: require('../assets/img/timeline2.jpg'),
|
||||
fishbone: require('../assets/img/fishbone.jpg'),
|
||||
}
|
||||
|
||||
// 主题图片映射
|
||||
@@ -37,5 +40,8 @@ export const themeMap = {
|
||||
blackHumour: require('../assets/img/blackHumour.jpg'),
|
||||
lateNightOffice: require('../assets/img/lateNightOffice.jpg'),
|
||||
blackGold: require('../assets/img/blackGold.jpg'),
|
||||
autumn: require('../assets/img/autumn.jpg'),
|
||||
avocado: require('../assets/img/avocado.jpg'),
|
||||
orangeJuice: require('../assets/img/orangeJuice.jpg'),
|
||||
}
|
||||
|
||||
64
web/src/config/icon.js
Normal file
3
web/src/css/global.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.noDrag {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
251
web/src/electron/fileHandle.js
Normal file
@@ -0,0 +1,251 @@
|
||||
import { BrowserWindow, ipcMain, dialog, shell } from 'electron'
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import {
|
||||
saveToRecent,
|
||||
clearRecent,
|
||||
removeFileInRecent,
|
||||
replaceFileInRecent,
|
||||
getRecent,
|
||||
saveFileListToRecent
|
||||
} from './storage'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
export const bindFileHandleEvent = ({ mainWindow, initOpenFileQueue }) => {
|
||||
// 通知主页面刷新最近文件列表
|
||||
const notifyMainWindowRefreshRecentFileList = () => {
|
||||
mainWindow.webContents.send('refreshRecentFileList')
|
||||
}
|
||||
|
||||
// 新建编辑页面
|
||||
const openIds = []
|
||||
const createEditWindow = async (event, id) => {
|
||||
openIds.push(id)
|
||||
const win = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
frame: false,
|
||||
titleBarStyle: 'hiddenInset',
|
||||
webPreferences: {
|
||||
webSecurity: false,
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
}
|
||||
})
|
||||
win.on('closed', () => {
|
||||
// 从openIds数组中删除
|
||||
let index = openIds.find(item => {
|
||||
return item === id
|
||||
})
|
||||
if (index !== -1) {
|
||||
openIds.splice(index, 1)
|
||||
}
|
||||
// 从idToFilePath中删除
|
||||
delete idToFilePath[id]
|
||||
})
|
||||
if (process.env.WEBPACK_DEV_SERVER_URL) {
|
||||
// Load the url of the dev server if in development mode
|
||||
win.loadURL(
|
||||
process.env.WEBPACK_DEV_SERVER_URL + '/#/workbenche/edit/' + id
|
||||
)
|
||||
// if (!process.env.IS_TEST) win.webContents.openDevTools()
|
||||
} else {
|
||||
// Load the index.html when not in development
|
||||
win.loadURL('app://./index.html/#/workbenche/edit/' + id)
|
||||
}
|
||||
}
|
||||
ipcMain.on('create', createEditWindow)
|
||||
|
||||
// 保存文件
|
||||
const idToFilePath = {}
|
||||
ipcMain.handle('save', async (event, id, data, fileName = '未命名') => {
|
||||
if (!idToFilePath[id]) {
|
||||
const webContents = event.sender
|
||||
const win = BrowserWindow.fromWebContents(webContents)
|
||||
const res = dialog.showSaveDialogSync(win, {
|
||||
title: '保存',
|
||||
defaultPath: fileName + '.smm',
|
||||
filters: [{ name: '思维导图', extensions: ['smm'] }]
|
||||
})
|
||||
if (res) {
|
||||
idToFilePath[id] = res
|
||||
fs.writeFile(res, data)
|
||||
saveToRecent(res).then(() => {
|
||||
notifyMainWindowRefreshRecentFileList()
|
||||
})
|
||||
return path.parse(idToFilePath[id]).name
|
||||
}
|
||||
} else {
|
||||
fs.writeFile(idToFilePath[id], data)
|
||||
}
|
||||
})
|
||||
|
||||
// 打开文件
|
||||
const openFile = (event, file) => {
|
||||
let id = uuid()
|
||||
idToFilePath[id] = file
|
||||
saveToRecent(file).then(() => {
|
||||
notifyMainWindowRefreshRecentFileList()
|
||||
})
|
||||
createEditWindow(null, id)
|
||||
}
|
||||
ipcMain.on('openFile', openFile)
|
||||
|
||||
// 选择打开本地文件
|
||||
ipcMain.on('selectOpenFile', event => {
|
||||
const res = dialog.showOpenDialogSync({
|
||||
title: '选择',
|
||||
filters: [{ name: '思维导图', extensions: ['smm'] }]
|
||||
})
|
||||
if (res && res[0]) {
|
||||
openFile(null, res[0])
|
||||
}
|
||||
})
|
||||
|
||||
// 获取文件内容
|
||||
ipcMain.handle('getFileContent', (event, id) => {
|
||||
return new Promise(resolve => {
|
||||
let file = idToFilePath[id]
|
||||
if (!file) {
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
fs.readFile(file, { encoding: 'utf-8' }, (err, data) => {
|
||||
resolve({
|
||||
name: path.parse(file).name,
|
||||
content: JSON.parse(data)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// 重命名文件
|
||||
ipcMain.handle('rename', (event, id, name) => {
|
||||
return new Promise(resolve => {
|
||||
if (!idToFilePath[id]) {
|
||||
resolve('文件不存在')
|
||||
return
|
||||
}
|
||||
let oldPath = idToFilePath[id]
|
||||
let { base, ...oldPathData } = path.parse(oldPath)
|
||||
oldPathData.name = name
|
||||
let newPath = path.format(oldPathData)
|
||||
idToFilePath[id] = newPath
|
||||
fs.rename(oldPath, newPath, err => {
|
||||
if (err) {
|
||||
resolve('重命名失败')
|
||||
} else {
|
||||
replaceFileInRecent(oldPath, newPath).then(() => {
|
||||
notifyMainWindowRefreshRecentFileList()
|
||||
resolve()
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// 获取最近文件列表
|
||||
ipcMain.handle('getRecentFileList', () => {
|
||||
return getRecent().map(item => {
|
||||
let data = path.parse(item)
|
||||
return {
|
||||
url: item,
|
||||
dir: data.dir,
|
||||
name: data.name
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 清空最近文件列表
|
||||
ipcMain.handle('clearRecentFileList', async () => {
|
||||
try {
|
||||
clearRecent()
|
||||
return ''
|
||||
} catch (error) {
|
||||
return '清空失败'
|
||||
}
|
||||
})
|
||||
|
||||
// 添加到最近文件列表
|
||||
ipcMain.handle('addRecentFileList', async (event, fileList) => {
|
||||
try {
|
||||
console.log(fileList);
|
||||
await saveFileListToRecent(fileList)
|
||||
notifyMainWindowRefreshRecentFileList()
|
||||
} catch (error) {
|
||||
return error
|
||||
}
|
||||
})
|
||||
|
||||
// 打开指定目录
|
||||
ipcMain.on('openFileInDir', (event, file) => {
|
||||
shell.showItemInFolder(file)
|
||||
})
|
||||
|
||||
// 删除指定文件
|
||||
ipcMain.handle('deleteFile', (event, file) => {
|
||||
let res = ''
|
||||
let id = Object.keys(idToFilePath).find(item => {
|
||||
return idToFilePath[item] === file
|
||||
})
|
||||
let index = -1
|
||||
if (id) {
|
||||
index = openIds.findIndex(item => {
|
||||
return item === id
|
||||
})
|
||||
}
|
||||
if (index === -1) {
|
||||
try {
|
||||
fs.rmSync(file)
|
||||
} catch (error) {}
|
||||
removeFileInRecent(file)
|
||||
} else {
|
||||
res = '该文件正在编辑,请关闭后再试'
|
||||
}
|
||||
return res
|
||||
})
|
||||
|
||||
// 复制文件
|
||||
ipcMain.handle('copyFile', async (event, file) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.pathExists(file, (err, exists) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
if (exists) {
|
||||
let { base, ...oldPathData } = path.parse(file)
|
||||
let newName = oldPathData.name + '-复制'
|
||||
let index = 1
|
||||
oldPathData.name = newName
|
||||
let newPath = path.format(oldPathData)
|
||||
// 检查新路径是否已存在
|
||||
while (fs.pathExistsSync(newPath)) {
|
||||
oldPathData.name = newName + index
|
||||
newPath = path.format(oldPathData)
|
||||
index++
|
||||
}
|
||||
fs.copy(file, newPath, err => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
saveToRecent(newPath).then(() => {
|
||||
notifyMainWindowRefreshRecentFileList()
|
||||
})
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
reject('文件不存在')
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// 直接双击文件打开应用时,需要直接打开该文件编辑
|
||||
initOpenFileQueue.forEach((file) => {
|
||||
openFile(null, file)
|
||||
})
|
||||
}
|
||||
18
web/src/electron/otherHandle.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { BrowserWindow, ipcMain } from 'electron'
|
||||
import open from 'open'
|
||||
|
||||
export const bindOtherHandleEvent = () => {
|
||||
// 处理缩放事件
|
||||
;['minimize', 'maximize', 'unmaximize', 'close', 'destroy'].forEach(item => {
|
||||
ipcMain.on(item, event => {
|
||||
const webContents = event.sender
|
||||
const win = BrowserWindow.fromWebContents(webContents)
|
||||
win[item]()
|
||||
})
|
||||
})
|
||||
|
||||
// 使用默认浏览器打开指定url
|
||||
ipcMain.on('openUrl', (event, url) => {
|
||||
open(url)
|
||||
})
|
||||
}
|
||||
27
web/src/electron/preload.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const { contextBridge, ipcRenderer } = require('electron')
|
||||
|
||||
contextBridge.exposeInMainWorld('platform', process.platform)
|
||||
contextBridge.exposeInMainWorld('IS_ELECTRON', true)
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
minimize: () => ipcRenderer.send('minimize'),
|
||||
maximize: () => ipcRenderer.send('maximize'),
|
||||
unmaximize: () => ipcRenderer.send('unmaximize'),
|
||||
close: () => ipcRenderer.send('close'),
|
||||
destroy: () => ipcRenderer.send('destroy'),
|
||||
create: id => ipcRenderer.send('create', id),
|
||||
getFileContent: id => ipcRenderer.invoke('getFileContent', id),
|
||||
save: (id, data, fileName) => ipcRenderer.invoke('save', id, data, fileName),
|
||||
rename: (id, name) => ipcRenderer.invoke('rename', id, name),
|
||||
openUrl: url => ipcRenderer.send('openUrl', url),
|
||||
addRecentFileList: (fileList) => ipcRenderer.invoke('addRecentFileList', fileList),
|
||||
getRecentFileList: () => ipcRenderer.invoke('getRecentFileList'),
|
||||
clearRecentFileList: () => ipcRenderer.invoke('clearRecentFileList'),
|
||||
openFileInDir: file => ipcRenderer.send('openFileInDir', file),
|
||||
deleteFile: file => ipcRenderer.invoke('deleteFile', file),
|
||||
onRefreshRecentFileList: callback =>
|
||||
ipcRenderer.on('refreshRecentFileList', callback),
|
||||
openFile: file => ipcRenderer.send('openFile', file),
|
||||
selectOpenFile: () => ipcRenderer.send('selectOpenFile'),
|
||||
copyFile: file => ipcRenderer.invoke('copyFile', file)
|
||||
})
|
||||
109
web/src/electron/storage.js
Normal file
@@ -0,0 +1,109 @@
|
||||
import storage from 'electron-json-storage'
|
||||
|
||||
export const RECENT_FILE_LIST = 'recentFileList'
|
||||
|
||||
// 保存到最近文件
|
||||
export const saveToRecent = file => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let list = getRecent()
|
||||
let index = list.findIndex(item => {
|
||||
return item === file
|
||||
})
|
||||
if (index !== -1) {
|
||||
list.splice(index, 1)
|
||||
}
|
||||
list.push(file)
|
||||
storage.set(RECENT_FILE_LIST, list, err => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 保存到最近文件
|
||||
export const saveFileListToRecent = fileList => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let list = getRecent()
|
||||
fileList.forEach(file => {
|
||||
let index = list.findIndex(item => {
|
||||
return item === file
|
||||
})
|
||||
if (index !== -1) {
|
||||
list.splice(index, 1)
|
||||
}
|
||||
list.push(file)
|
||||
})
|
||||
storage.set(RECENT_FILE_LIST, list, err => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 获取最近文件列表
|
||||
export const getRecent = () => {
|
||||
let res = storage.getSync(RECENT_FILE_LIST)
|
||||
return (Array.isArray(res) ? res : []).filter(item => {
|
||||
return !!item
|
||||
})
|
||||
}
|
||||
|
||||
// 清除最近文件列表
|
||||
export const clearRecent = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
storage.remove(RECENT_FILE_LIST, err => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 从最近文件列表中移除指定文件
|
||||
export const removeFileInRecent = file => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let list = getRecent()
|
||||
let index = list.findIndex(item => {
|
||||
return item === file
|
||||
})
|
||||
if (index !== -1) {
|
||||
list.splice(index, 1)
|
||||
}
|
||||
storage.set(RECENT_FILE_LIST, list, err => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 替换指定文件
|
||||
export const replaceFileInRecent = (oldFile, newFile) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let list = getRecent()
|
||||
let index = list.findIndex(item => {
|
||||
return item === oldFile
|
||||
})
|
||||
if (index !== -1) {
|
||||
list.splice(index, 1)
|
||||
}
|
||||
list.push(newFile)
|
||||
storage.set(RECENT_FILE_LIST, list, err => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -90,12 +90,14 @@ export default {
|
||||
pdfFile: 'pdf file',
|
||||
markdownFile: 'markdown file',
|
||||
tips: 'tips: .smm and .json file can be import',
|
||||
domToImage: 'Whether to convert rich text nodes in svg into pictures',
|
||||
isTransparent: 'Background is transparent',
|
||||
pngTips: 'tips: Exporting pictures in rich text mode is time-consuming. It is recommended to export to svg format',
|
||||
svgTips: 'tips: Exporting pictures in rich text mode is time-consuming',
|
||||
transformingDomToImages: 'Converting nodes: ',
|
||||
notifyTitle: 'Info',
|
||||
notifyMessage: 'If the download is not triggered, check whether it is blocked by the browser'
|
||||
notifyMessage: 'If the download is not triggered, check whether it is blocked by the browser',
|
||||
paddingX: 'Padding x',
|
||||
paddingY: 'Padding y'
|
||||
},
|
||||
fullscreen: {
|
||||
fullscreenShow: 'Full screen show',
|
||||
|
||||
@@ -90,12 +90,14 @@ export default {
|
||||
pdfFile: 'pdf文件',
|
||||
markdownFile: 'markdown文件',
|
||||
tips: 'tips:.smm和.json文件可用于导入',
|
||||
domToImage: '是否将svg中富文本节点转换成图片',
|
||||
isTransparent: '背景是否透明',
|
||||
pngTips: 'tips:富文本模式导出图片非常耗时,建议导出为svg格式',
|
||||
svgTips: 'tips:富文本模式导出图片非常耗时',
|
||||
transformingDomToImages: '正在转换节点:',
|
||||
notifyTitle: '消息',
|
||||
notifyMessage: '如果没有触发下载,请检查是否被浏览器拦截了'
|
||||
notifyMessage: '如果没有触发下载,请检查是否被浏览器拦截了',
|
||||
paddingX: '水平内边距',
|
||||
paddingY: '垂直内边距'
|
||||
},
|
||||
fullscreen: {
|
||||
fullscreenShow: '全屏查看',
|
||||
|
||||
@@ -8,11 +8,21 @@ import '@/assets/icon-font/iconfont.css'
|
||||
import 'viewerjs/dist/viewer.css'
|
||||
import VueViewer from 'v-viewer'
|
||||
import i18n from './i18n'
|
||||
import './css/global.css'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
Vue.prototype.$bus = new Vue()
|
||||
Vue.use(ElementUI)
|
||||
Vue.use(VueViewer)
|
||||
Vue.mixin({
|
||||
data () {
|
||||
return {
|
||||
IS_ELECTRON: window.IS_ELECTRON,
|
||||
IS_MAC: window.platform === 'darwin',
|
||||
IS_WIN: window.platform === 'win32'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
new Vue({
|
||||
render: h => h(App),
|
||||
|
||||
@@ -10,8 +10,8 @@ let langList = [
|
||||
path: 'en'
|
||||
}
|
||||
]
|
||||
let StartList = ['introduction', 'start', 'translate', 'changelog']
|
||||
let CourseList = new Array(18).fill(0).map((_, index) => {
|
||||
let StartList = ['introduction', 'start', 'deploy', 'translate', 'changelog']
|
||||
let CourseList = new Array(19).fill(0).map((_, index) => {
|
||||
return 'course' + (index + 1)
|
||||
})
|
||||
let APIList = [
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
> The function of adjusting associated line control points is supported from v0.4.6+
|
||||
|
||||
This plugin is used to support the addition of associative lines.
|
||||
> Relevance support for text editing starting from v0.5.11+
|
||||
|
||||
The plugin is currently not fully functional, and does not support adding text to association lines.
|
||||
This plugin is used to support the addition of associative lines.
|
||||
|
||||
## Register
|
||||
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
<blockquote>
|
||||
<p>The function of adjusting associated line control points is supported from v0.4.6+</p>
|
||||
</blockquote>
|
||||
<blockquote>
|
||||
<p>Relevance support for text editing starting from v0.5.11+</p>
|
||||
</blockquote>
|
||||
<p>This plugin is used to support the addition of associative lines.</p>
|
||||
<p>The plugin is currently not fully functional, and does not support adding text to association lines.</p>
|
||||
<h2>Register</h2>
|
||||
<pre class="hljs"><code><span class="hljs-keyword">import</span> MindMap <span class="hljs-keyword">from</span> <span class="hljs-string">'simple-mind-map'</span>
|
||||
<span class="hljs-keyword">import</span> AssociativeLine <span class="hljs-keyword">from</span> <span class="hljs-string">'simple-mind-map/src/AssociativeLine.js'</span>
|
||||
|
||||
@@ -1,8 +1,74 @@
|
||||
# Changelog
|
||||
|
||||
## 0.5.11
|
||||
|
||||
New: Supports associative text editing.
|
||||
|
||||
optimization: Optimizing theme configuration updates, changing configurations that do not involve node size does not trigger node recalculation.
|
||||
|
||||
## 0.5.10
|
||||
|
||||
New: Optimize node reuse logic using LRU caching algorithm.
|
||||
|
||||
## 0.5.10-fix.1
|
||||
|
||||
Fix: Fix the issue of import errors.
|
||||
|
||||
## 0.5.10-fix.2
|
||||
|
||||
Fix: Fixed the issue of switching themes and importing data without triggering data changes in rich text mode.
|
||||
|
||||
New: Add three new themes.
|
||||
|
||||
## 0.5.9
|
||||
|
||||
Change: Unified export method format, using `FileReader` instead of `URL.createObjectURL` to convert `blob` data.
|
||||
|
||||
## 0.5.8
|
||||
|
||||
optimization: 1.The position setting is not triggered when the node position does not change. 2.The unfolding and folding status does not change and does not trigger button updates.
|
||||
|
||||
New: 1.The default setting is to move the mouse over the node to display the expand and collapse buttons. 2.Support the list of icons that can be inserted into extended nodes.
|
||||
|
||||
## 0.5.7
|
||||
|
||||
Breaking change:In rich text mode, exporting png has been changed to using html2canvas to convert the entire svg, greatly improving the export speed. However, html2canvas has a bug where the text color inline with the dom node in the foreignObject element cannot be recognized. Therefore, the text color of the exported node is fixed. However, compared to the previously unavailable state of the export, it can at least be exported quickly and smoothly.
|
||||
|
||||
optimization: Optimize the rich text node editing experience.
|
||||
|
||||
New: In rich text mode, importing data, initializing data, and switching theme scene node styles support following theme changes.
|
||||
|
||||
## 0.5.6
|
||||
|
||||
Fix: 1.Fix the issue of node position disorder during fast and multiple renderings in a short period of time. 2.Fix the issue of dragging the canvas while the node is being edited, causing the edit box and node to separate.
|
||||
|
||||
New: 1.Add a maximum history limit.
|
||||
|
||||
## 0.5.5-fix.1
|
||||
|
||||
Fix: 1.Fix the issue where the edit box is also outside the canvas when editing nodes outside the canvas. 2.After modifying the structure, reset the transformation to prevent the problem of sudden position changes during the first drag after switching the structure during scaling.
|
||||
|
||||
optimization: 1.When multiple nodes are selected, as long as there is a cross between the node and the selection area, it is considered selected.
|
||||
|
||||
## 0.5.5-fix.2
|
||||
|
||||
Fix: 1.Fix mini map error.
|
||||
|
||||
## 0.5.5
|
||||
|
||||
New: 1.Supports configuring the padding when exporting to PNG, SVG, or PDF. 2.Support the configuration of z-index for node text editing boxes and node comment floating layer elements. 3.Support clicking on areas outside the canvas to end node editing status.
|
||||
|
||||
## 0.5.4
|
||||
|
||||
New: 1.Add new themes.
|
||||
New: 1.Add new themes. 2.Added timeline and fishbone structure.
|
||||
|
||||
Fix: 1.Fix the conflict issue between node right-click and canvas right-click. 2.Fix the bug that the line segment is not hidden when dragging nodes such as organizational chart and directory organization chart.
|
||||
|
||||
optimization: 1.Optimize the layout of organizational chart. 2.Optimize the layout of the directory organization chart.
|
||||
|
||||
## 0.5.4-fix.1
|
||||
|
||||
optimization: 1.Optimize fishbone layout.
|
||||
|
||||
## 0.5.3
|
||||
|
||||
|
||||
@@ -1,8 +1,41 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Changelog</h1>
|
||||
<h2>0.5.11</h2>
|
||||
<p>New: Supports associative text editing.</p>
|
||||
<p>optimization: Optimizing theme configuration updates, changing configurations that do not involve node size does not trigger node recalculation.</p>
|
||||
<h2>0.5.10</h2>
|
||||
<p>New: Optimize node reuse logic using LRU caching algorithm.</p>
|
||||
<h2>0.5.10-fix.1</h2>
|
||||
<p>Fix: Fix the issue of import errors.</p>
|
||||
<h2>0.5.10-fix.2</h2>
|
||||
<p>Fix: Fixed the issue of switching themes and importing data without triggering data changes in rich text mode.</p>
|
||||
<p>New: Add three new themes.</p>
|
||||
<h2>0.5.9</h2>
|
||||
<p>Change: Unified export method format, using <code>FileReader</code> instead of <code>URL.createObjectURL</code> to convert <code>blob</code> data.</p>
|
||||
<h2>0.5.8</h2>
|
||||
<p>optimization: 1.The position setting is not triggered when the node position does not change. 2.The unfolding and folding status does not change and does not trigger button updates.</p>
|
||||
<p>New: 1.The default setting is to move the mouse over the node to display the expand and collapse buttons. 2.Support the list of icons that can be inserted into extended nodes.</p>
|
||||
<h2>0.5.7</h2>
|
||||
<p>Breaking change:In rich text mode, exporting png has been changed to using html2canvas to convert the entire svg, greatly improving the export speed. However, html2canvas has a bug where the text color inline with the dom node in the foreignObject element cannot be recognized. Therefore, the text color of the exported node is fixed. However, compared to the previously unavailable state of the export, it can at least be exported quickly and smoothly.</p>
|
||||
<p>optimization: Optimize the rich text node editing experience.</p>
|
||||
<p>New: In rich text mode, importing data, initializing data, and switching theme scene node styles support following theme changes.</p>
|
||||
<h2>0.5.6</h2>
|
||||
<p>Fix: 1.Fix the issue of node position disorder during fast and multiple renderings in a short period of time. 2.Fix the issue of dragging the canvas while the node is being edited, causing the edit box and node to separate.</p>
|
||||
<p>New: 1.Add a maximum history limit.</p>
|
||||
<h2>0.5.5-fix.1</h2>
|
||||
<p>Fix: 1.Fix the issue where the edit box is also outside the canvas when editing nodes outside the canvas. 2.After modifying the structure, reset the transformation to prevent the problem of sudden position changes during the first drag after switching the structure during scaling.</p>
|
||||
<p>optimization: 1.When multiple nodes are selected, as long as there is a cross between the node and the selection area, it is considered selected.</p>
|
||||
<h2>0.5.5-fix.2</h2>
|
||||
<p>Fix: 1.Fix mini map error.</p>
|
||||
<h2>0.5.5</h2>
|
||||
<p>New: 1.Supports configuring the padding when exporting to PNG, SVG, or PDF. 2.Support the configuration of z-index for node text editing boxes and node comment floating layer elements. 3.Support clicking on areas outside the canvas to end node editing status.</p>
|
||||
<h2>0.5.4</h2>
|
||||
<p>New: 1.Add new themes.</p>
|
||||
<p>New: 1.Add new themes. 2.Added timeline and fishbone structure.</p>
|
||||
<p>Fix: 1.Fix the conflict issue between node right-click and canvas right-click. 2.Fix the bug that the line segment is not hidden when dragging nodes such as organizational chart and directory organization chart.</p>
|
||||
<p>optimization: 1.Optimize the layout of organizational chart. 2.Optimize the layout of the directory organization chart.</p>
|
||||
<h2>0.5.4-fix.1</h2>
|
||||
<p>optimization: 1.Optimize fishbone layout.</p>
|
||||
<h2>0.5.3</h2>
|
||||
<p>Fix: 1.Fixed the issue of setting the text style when multiple nodes were selected in rich text mode, which would change the text of all selected nodes to the text of the last selected node.</p>
|
||||
<p>New: 1.Support setting the position of the initial central node.</p>
|
||||
|
||||
@@ -26,8 +26,9 @@ const mindMap = new MindMap({
|
||||
| -------------------------------- | ------- | ---------------- | ------------------------------------------------------------ | -------- |
|
||||
| el | Element | | Container element, must be a DOM element | Yes |
|
||||
| data | Object | {} | Mind map data, refer to: https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js | |
|
||||
| layout | String | logicalStructure | Layout type, options: logicalStructure (logical structure diagram), mindMap (mind map), catalogOrganization (catalog organization diagram), organizationStructure (organization 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+) | |
|
||||
| layout | String | logicalStructure | Layout type, options: logicalStructure (logical structure diagram), mindMap (mind map), catalogOrganization (catalog organization diagram), organizationStructure (organization structure diagram)、timeline(v0.5.4+, timeline)、timeline2(v0.5.4+, up down alternating timeline)、fishbone(v0.5.4+, fishbone diagram) | |
|
||||
| fishboneDeg(v0.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+) | |
|
||||
| themeConfig | Object | {} | Theme configuration, will be merged with the selected theme, available fields refer to: https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/themes/default.js | |
|
||||
| scaleRatio | Number | 0.1 | The incremental scaling ratio | |
|
||||
| maxTag | Number | 5 | The maximum number of tags displayed in the node, any additional tags will be discarded | |
|
||||
@@ -52,6 +53,15 @@ const mindMap = new MindMap({
|
||||
| enableNodeTransitionMove(v0.5.1+) | Boolean | true | Whether to enable node animation transition | |
|
||||
| nodeTransitionMoveDuration(v0.5.1+) | Number | 300 | If node animation transition is enabled, the transition time can be set using this attribute, in milliseconds | |
|
||||
| initRootNodePosition(v0.5.3+) | Array | null | The position of the initial root node can be passed as an array, default is `['center', 'center']`, Represents the root node at the center of the canvas, In addition to `center`, keywords can also be set to `left`, `top`, `right`, and `bottom`, In addition to passing keywords, each item in the array can also pass a number representing a specific pixel, Can pass a percentage string, such as `['40%', '60%']`, Represents a horizontal position at `40%` of the canvas width, and a vertical position at `60%` of the canvas height | |
|
||||
| exportPaddingX(v0.5.5+) | Number | 10 | Horizontal padding of graphics when exporting PNG, SVG, and PDF | |
|
||||
| exportPaddingY(v0.5.5+) | Number | 10 | Vertical padding of graphics when exporting PNG, SVG, and PDF | |
|
||||
| nodeTextEditZIndex(v0.5.5+) | Number | 3000 | | z-index of node text edit box elements |
|
||||
| nodeNoteTooltipZIndex(v0.5.5+) | Number | 3000 | z-index of floating layer elements in node comments | |
|
||||
| isEndNodeTextEditOnClickOuter(v0.5.5+) | Boolean | true | Whether to end the editing status of node text when clicking on an area outside the canvas | |
|
||||
| maxHistoryCount(v0.5.6+) | Number | 1000 | | Maximum number of history records |
|
||||
| alwaysShowExpandBtn(v0.5.8+) | Boolean | false | Whether to always display the expand and collapse buttons of nodes, which are only displayed when the mouse is moved up and activated by default | |
|
||||
| iconList(v0.5.8+) | Array | [] | The icons that can be inserted into the extension node, and each item in the array is an object. Please refer to the "Icon Configuration" table below for the detailed structure of the object | |
|
||||
| maxNodeCacheCount(v0.5.10+) | Number | 1000 | The maximum number of cached nodes. To optimize performance, an internal node cache pool is maintained to reuse nodes. This attribute allows you to specify the maximum number of caches in the pool | |
|
||||
|
||||
### Watermark config
|
||||
|
||||
@@ -63,6 +73,14 @@ const mindMap = new MindMap({
|
||||
| angle | Number | 30 | Tilt angle of watermark, range: [0, 90] |
|
||||
| textStyle | Object | {color: '#999', opacity: 0.5, fontSize: 14} | Watermark text style |
|
||||
|
||||
### Icon Configuration
|
||||
|
||||
| Field Name | Type | Default Value | Description |
|
||||
| ----------- | ------ | ------------------------------------------- | ------------------------------------------------------------ |
|
||||
| name | String | | The name of the icon group |
|
||||
| type | String | | Values for icon grouping |
|
||||
| list | Array | | A list of icons under grouping, with each item in the array being an object, `{ name: '', icon: '' }`,`name`represents the name of the icon, `icon`represents the icon, Can be an `svg` icon, such as `<svg ...><path></path></svg>`, also can be a image `url`, or `base64` icon, such as `data:image/png;base64,...` |
|
||||
|
||||
## Static methods
|
||||
|
||||
### defineTheme(name, config)
|
||||
@@ -122,10 +140,14 @@ List of all currently registered plugins.
|
||||
|
||||
## Instance methods
|
||||
|
||||
### getSvgData()
|
||||
### getSvgData({ paddingX = 0, paddingY = 0 })
|
||||
|
||||
> v0.3.0+
|
||||
|
||||
`paddingX`: Padding x
|
||||
|
||||
`paddingY`: Padding y
|
||||
|
||||
Get the `svg` data and return an object. The detailed structure is as follows:
|
||||
|
||||
```js
|
||||
@@ -348,6 +370,18 @@ smm (essentially also json)
|
||||
`fileName`: (v0.1.6+) the name of the exported file, default is `思维导图` (mind
|
||||
map).
|
||||
|
||||
If it is exported as `png`, the fourth parameter can be passed:
|
||||
|
||||
`transparent`: v0.5.7+, `Boolean`, default is `false`, Specify whether the background of the exported image is transparent
|
||||
|
||||
If it is exported as `svg`, the fourth parameter can be passed:
|
||||
|
||||
`plusCssText`: Additional `CSS` style. If there is a `dom` node in `svg`, you can pass in some styles specific to the node through this parameter
|
||||
|
||||
If it is exported as `json` or `smm`, the fourth parameter can be passed:
|
||||
|
||||
`withConfig`: `Boolean`, default is `true`, Specify whether the exported data includes configuration data, otherwise only pure node tree data will be exported
|
||||
|
||||
### toPos(x, y)
|
||||
|
||||
> v0.1.5+
|
||||
|
||||
@@ -46,14 +46,21 @@
|
||||
<td>layout</td>
|
||||
<td>String</td>
|
||||
<td>logicalStructure</td>
|
||||
<td>Layout type, options: logicalStructure (logical structure diagram), mindMap (mind map), catalogOrganization (catalog organization diagram), organizationStructure (organization structure diagram)</td>
|
||||
<td>Layout type, options: logicalStructure (logical structure diagram), mindMap (mind map), catalogOrganization (catalog organization diagram), organizationStructure (organization structure diagram)、timeline(v0.5.4+, timeline)、timeline2(v0.5.4+, up down alternating timeline)、fishbone(v0.5.4+, fishbone diagram)</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>fishboneDeg(v0.5.4+)</td>
|
||||
<td>Number</td>
|
||||
<td>45</td>
|
||||
<td>Set the diagonal angle of the fishbone structure diagram</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>theme</td>
|
||||
<td>String</td>
|
||||
<td>default</td>
|
||||
<td>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+)</td>
|
||||
<td>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+)</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -224,6 +231,69 @@
|
||||
<td>The position of the initial root node can be passed as an array, default is <code>['center', 'center']</code>, Represents the root node at the center of the canvas, In addition to <code>center</code>, keywords can also be set to <code>left</code>, <code>top</code>, <code>right</code>, and <code>bottom</code>, In addition to passing keywords, each item in the array can also pass a number representing a specific pixel, Can pass a percentage string, such as <code>['40%', '60%']</code>, Represents a horizontal position at <code>40%</code> of the canvas width, and a vertical position at <code>60%</code> of the canvas height</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>exportPaddingX(v0.5.5+)</td>
|
||||
<td>Number</td>
|
||||
<td>10</td>
|
||||
<td>Horizontal padding of graphics when exporting PNG, SVG, and PDF</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>exportPaddingY(v0.5.5+)</td>
|
||||
<td>Number</td>
|
||||
<td>10</td>
|
||||
<td>Vertical padding of graphics when exporting PNG, SVG, and PDF</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>nodeTextEditZIndex(v0.5.5+)</td>
|
||||
<td>Number</td>
|
||||
<td>3000</td>
|
||||
<td></td>
|
||||
<td>z-index of node text edit box elements</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>nodeNoteTooltipZIndex(v0.5.5+)</td>
|
||||
<td>Number</td>
|
||||
<td>3000</td>
|
||||
<td>z-index of floating layer elements in node comments</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>isEndNodeTextEditOnClickOuter(v0.5.5+)</td>
|
||||
<td>Boolean</td>
|
||||
<td>true</td>
|
||||
<td>Whether to end the editing status of node text when clicking on an area outside the canvas</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>maxHistoryCount(v0.5.6+)</td>
|
||||
<td>Number</td>
|
||||
<td>1000</td>
|
||||
<td></td>
|
||||
<td>Maximum number of history records</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>alwaysShowExpandBtn(v0.5.8+)</td>
|
||||
<td>Boolean</td>
|
||||
<td>false</td>
|
||||
<td>Whether to always display the expand and collapse buttons of nodes, which are only displayed when the mouse is moved up and activated by default</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>iconList(v0.5.8+)</td>
|
||||
<td>Array</td>
|
||||
<td>[]</td>
|
||||
<td>The icons that can be inserted into the extension node, and each item in the array is an object. Please refer to the "Icon Configuration" table below for the detailed structure of the object</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>maxNodeCacheCount(v0.5.10+)</td>
|
||||
<td>Number</td>
|
||||
<td>1000</td>
|
||||
<td>The maximum number of cached nodes. To optimize performance, an internal node cache pool is maintained to reuse nodes. This attribute allows you to specify the maximum number of caches in the pool</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>Watermark config</h3>
|
||||
@@ -269,6 +339,37 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>Icon Configuration</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field Name</th>
|
||||
<th>Type</th>
|
||||
<th>Default Value</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>name</td>
|
||||
<td>String</td>
|
||||
<td></td>
|
||||
<td>The name of the icon group</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>String</td>
|
||||
<td></td>
|
||||
<td>Values for icon grouping</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>list</td>
|
||||
<td>Array</td>
|
||||
<td></td>
|
||||
<td>A list of icons under grouping, with each item in the array being an object, <code>{ name: '', icon: '' }</code>,<code>name</code>represents the name of the icon, <code>icon</code>represents the icon, Can be an <code>svg</code> icon, such as <code><svg ...><path></path></svg></code>, also can be a image <code>url</code>, or <code>base64</code> icon, such as <code>data:image/png;base64,...</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2>Static methods</h2>
|
||||
<h3>defineTheme(name, config)</h3>
|
||||
<blockquote>
|
||||
@@ -312,10 +413,12 @@ mindMap.setTheme(<span class="hljs-string">'Theme name'</span>)
|
||||
</blockquote>
|
||||
<p>List of all currently registered plugins.</p>
|
||||
<h2>Instance methods</h2>
|
||||
<h3>getSvgData()</h3>
|
||||
<h3>getSvgData({ paddingX = 0, paddingY = 0 })</h3>
|
||||
<blockquote>
|
||||
<p>v0.3.0+</p>
|
||||
</blockquote>
|
||||
<p><code>paddingX</code>: Padding x</p>
|
||||
<p><code>paddingY</code>: Padding y</p>
|
||||
<p>Get the <code>svg</code> data and return an object. The detailed structure is as follows:</p>
|
||||
<pre class="hljs"><code>{
|
||||
svg, <span class="hljs-comment">// Element, the overall svg element of the mind map graphics, including: svg (canvas container), g (actual mind map group)</span>
|
||||
@@ -765,6 +868,12 @@ smm (essentially also json)</p>
|
||||
<code>false</code></p>
|
||||
<p><code>fileName</code>: (v0.1.6+) the name of the exported file, default is <code>思维导图</code> (mind
|
||||
map).</p>
|
||||
<p>If it is exported as <code>png</code>, the fourth parameter can be passed:</p>
|
||||
<p><code>transparent</code>: v0.5.7+, <code>Boolean</code>, default is <code>false</code>, Specify whether the background of the exported image is transparent</p>
|
||||
<p>If it is exported as <code>svg</code>, the fourth parameter can be passed:</p>
|
||||
<p><code>plusCssText</code>: Additional <code>CSS</code> style. If there is a <code>dom</code> node in <code>svg</code>, you can pass in some styles specific to the node through this parameter</p>
|
||||
<p>If it is exported as <code>json</code> or <code>smm</code>, the fourth parameter can be passed:</p>
|
||||
<p><code>withConfig</code>: <code>Boolean</code>, default is <code>true</code>, Specify whether the exported data includes configuration data, otherwise only pure node tree data will be exported</p>
|
||||
<h3>toPos(x, y)</h3>
|
||||
<blockquote>
|
||||
<p>v0.1.5+</p>
|
||||
|
||||
1
web/src/pages/Doc/en/deploy/index.md
Normal file
@@ -0,0 +1 @@
|
||||
# Deploy
|
||||
@@ -15,17 +15,40 @@ After registration and instantiation of `MindMap`, the instance can be obtained
|
||||
|
||||
## Methods
|
||||
|
||||
### png()
|
||||
All exported methods are asynchronous and return an instance of `Promise`. You can use the `then` method to obtain data, or use the `async await` function to obtain:
|
||||
|
||||
Exports as `png`, an async method that returns image data, `data:url` data which
|
||||
can be downloaded or displayed.
|
||||
```js
|
||||
mindMap.doExport.png().then((data) => {
|
||||
// ...
|
||||
})
|
||||
|
||||
### svg(name, domToImage = false, plusCssText)
|
||||
const export = async () => {
|
||||
let data = await mindMap.doExport.png()
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
The returned data is in the format of `data:URL`. You can create an `a` tag to trigger the download:
|
||||
|
||||
```js
|
||||
let a = document.createElement('a')
|
||||
a.href = 'xxx.png'// .png、.svg、.pdf、.md、.json、.smm
|
||||
a.download = 'xxx'
|
||||
a.click()
|
||||
```
|
||||
|
||||
### png(name, transparent = false)
|
||||
|
||||
- `name`: Name, optional
|
||||
|
||||
- `transparent`: v0.5.7+, Specify whether the background of the exported image is transparent
|
||||
|
||||
Exports as `png`.
|
||||
|
||||
### svg(name, plusCssText)
|
||||
|
||||
- `name`:`svg` title
|
||||
|
||||
- `domToImage`:v0.4.0+, When node rich text editing is enabled, you can use this parameter to specify whether to convert the `dom` node in the `svg` into a picture
|
||||
|
||||
- `plusCssText`:v0.4.0+, When node rich text editing is enabled and `domToImage` passes `false`, additional `css` styles can be added. If there is a `dom` node in `svg`, you can set some styles for the node through this parameter, such as:
|
||||
|
||||
```js
|
||||
@@ -40,22 +63,7 @@ svg(
|
||||
)
|
||||
```
|
||||
|
||||
Exports as `svg`, an async method that returns `svg` data, `data:url` data which
|
||||
can be downloaded or displayed.
|
||||
|
||||
### getSvgData(domToImage)
|
||||
|
||||
- `domToImage`:v0.4.0+, If node rich text is enabled, you can use this parameter to specify whether to convert the `DOM` node embedded in `svg` into a picture.
|
||||
|
||||
Gets `svg` data, an async method that returns an object:
|
||||
|
||||
```js
|
||||
{
|
||||
node; // svg object
|
||||
str; // svg string, if rich text editing is enabled and domToImage is set to true, the dom node in the svg character returned by this value will be converted into the form of an image
|
||||
nodeWithDomToImg// v0.4.0+,The svg object after the DOM node is converted to an image has a value only when rich text editing is enabled and domToImage is set to true, otherwise null
|
||||
}
|
||||
```
|
||||
Exports as `svg`.
|
||||
|
||||
### pdf(name)
|
||||
|
||||
@@ -63,7 +71,7 @@ Gets `svg` data, an async method that returns an object:
|
||||
|
||||
`name`:File name
|
||||
|
||||
Export as `pdf`
|
||||
Export as `pdf`. Unlike other export methods, this method does not return data and directly triggers the download.
|
||||
|
||||
### json(name, withConfig)
|
||||
|
||||
@@ -71,4 +79,21 @@ Export as `pdf`
|
||||
|
||||
`withConfig``:Boolean`, default `true`, Whether the data contains configuration, otherwise it is pure mind map node data
|
||||
|
||||
Return `json` data, `data:url` type, you can download it yourself
|
||||
Return `json` data.
|
||||
|
||||
### md()
|
||||
|
||||
> v0.4.7+
|
||||
|
||||
Export as `markdown` file.
|
||||
|
||||
### getSvgData()
|
||||
|
||||
Gets `svg` data, an async method that returns an object:
|
||||
|
||||
```js
|
||||
{
|
||||
node // svg node
|
||||
str // svg string
|
||||
}
|
||||
```
|
||||
@@ -10,18 +10,38 @@ MindMap.usePlugin(Export)
|
||||
</code></pre>
|
||||
<p>After registration and instantiation of <code>MindMap</code>, the instance can be obtained through <code>mindMap.doExport</code>.</p>
|
||||
<h2>Methods</h2>
|
||||
<h3>png()</h3>
|
||||
<p>Exports as <code>png</code>, an async method that returns image data, <code>data:url</code> data which
|
||||
can be downloaded or displayed.</p>
|
||||
<h3>svg(name, domToImage = false, plusCssText)</h3>
|
||||
<p>All exported methods are asynchronous and return an instance of <code>Promise</code>. You can use the <code>then</code> method to obtain data, or use the <code>async await</code> function to obtain:</p>
|
||||
<pre class="hljs"><code>mindMap.doExport.png().then(<span class="hljs-function">(<span class="hljs-params">data</span>) =></span> {
|
||||
<span class="hljs-comment">// ...</span>
|
||||
})
|
||||
|
||||
<span class="hljs-keyword">const</span> <span class="hljs-keyword">export</span> = <span class="hljs-keyword">async</span> () => {
|
||||
<span class="hljs-keyword">let</span> data = <span class="hljs-keyword">await</span> mindMap.doExport.png()
|
||||
<span class="hljs-comment">// ...</span>
|
||||
}
|
||||
</code></pre>
|
||||
<p>The returned data is in the format of <code>data:URL</code>. You can create an <code>a</code> tag to trigger the download:</p>
|
||||
<pre class="hljs"><code><span class="hljs-keyword">let</span> a = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'a'</span>)
|
||||
a.href = <span class="hljs-string">'xxx.png'</span><span class="hljs-comment">// .png、.svg、.pdf、.md、.json、.smm</span>
|
||||
a.download = <span class="hljs-string">'xxx'</span>
|
||||
a.click()
|
||||
</code></pre>
|
||||
<h3>png(name, transparent = false)</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<p><code>name</code>: Name, optional</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>transparent</code>: v0.5.7+, Specify whether the background of the exported image is transparent</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>Exports as <code>png</code>.</p>
|
||||
<h3>svg(name, plusCssText)</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<p><code>name</code>:<code>svg</code> title</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>domToImage</code>:v0.4.0+, When node rich text editing is enabled, you can use this parameter to specify whether to convert the <code>dom</code> node in the <code>svg</code> into a picture</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>plusCssText</code>:v0.4.0+, When node rich text editing is enabled and <code>domToImage</code> passes <code>false</code>, additional <code>css</code> styles can be added. If there is a <code>dom</code> node in <code>svg</code>, you can set some styles for the node through this parameter, such as:</p>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -35,29 +55,29 @@ can be downloaded or displayed.</p>
|
||||
}`</span>
|
||||
)
|
||||
</code></pre>
|
||||
<p>Exports as <code>svg</code>, an async method that returns <code>svg</code> data, <code>data:url</code> data which
|
||||
can be downloaded or displayed.</p>
|
||||
<h3>getSvgData(domToImage)</h3>
|
||||
<ul>
|
||||
<li><code>domToImage</code>:v0.4.0+, If node rich text is enabled, you can use this parameter to specify whether to convert the <code>DOM</code> node embedded in <code>svg</code> into a picture.</li>
|
||||
</ul>
|
||||
<p>Gets <code>svg</code> data, an async method that returns an object:</p>
|
||||
<pre class="hljs"><code>{
|
||||
node; <span class="hljs-comment">// svg object</span>
|
||||
str; <span class="hljs-comment">// svg string, if rich text editing is enabled and domToImage is set to true, the dom node in the svg character returned by this value will be converted into the form of an image</span>
|
||||
nodeWithDomToImg<span class="hljs-comment">// v0.4.0+,The svg object after the DOM node is converted to an image has a value only when rich text editing is enabled and domToImage is set to true, otherwise null</span>
|
||||
}
|
||||
</code></pre>
|
||||
<p>Exports as <code>svg</code>.</p>
|
||||
<h3>pdf(name)</h3>
|
||||
<blockquote>
|
||||
<p>v0.2.1+</p>
|
||||
</blockquote>
|
||||
<p><code>name</code>:File name</p>
|
||||
<p>Export as <code>pdf</code></p>
|
||||
<p>Export as <code>pdf</code>. Unlike other export methods, this method does not return data and directly triggers the download.</p>
|
||||
<h3>json(name, withConfig)</h3>
|
||||
<p><code>name</code>:It is temporarily useless, just pass an empty string</p>
|
||||
<p><code>withConfig``:Boolean</code>, default <code>true</code>, Whether the data contains configuration, otherwise it is pure mind map node data</p>
|
||||
<p>Return <code>json</code> data, <code>data:url</code> type, you can download it yourself</p>
|
||||
<p>Return <code>json</code> data.</p>
|
||||
<h3>md()</h3>
|
||||
<blockquote>
|
||||
<p>v0.4.7+</p>
|
||||
</blockquote>
|
||||
<p>Export as <code>markdown</code> file.</p>
|
||||
<h3>getSvgData()</h3>
|
||||
<p>Gets <code>svg</code> data, an async method that returns an object:</p>
|
||||
<pre class="hljs"><code>{
|
||||
node <span class="hljs-comment">// svg node</span>
|
||||
str <span class="hljs-comment">// svg string</span>
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
## Features
|
||||
|
||||
- [x] Plugin architecture. In addition to core functions, other functions are provided as plugins, which can be used as needed to reduce the overall volume
|
||||
- [x] Supports four types of structures: logical structure diagrams, mind maps,
|
||||
organizational structure diagrams, and directory organization diagrams
|
||||
- [x] Supports six types of structures: logical structure diagrams, mind maps,
|
||||
organizational structure diagrams, directory organization diagrams, timeline, and fishbone diagrams
|
||||
- [x] Built-in multiple themes and allows for highly customized styles, and support register new themes
|
||||
- [x] Supports shortcuts
|
||||
- [x] Node content supports images, icons, hyperlinks, notes, tags, and
|
||||
@@ -97,6 +97,18 @@ These open-source mind maps are also good, each with its own characteristics, bu
|
||||
|
||||
In summary, in open-source mind maps, it is difficult to find a better choice than `simple-mind-map`. Of course, `simple-mind-map` is far from being the best, and it also has many shortcomings, as you saw in the previous [special note]. However, `simple-mind-map` has always been in a fast iteration process, and we welcome you to join and improve it together.
|
||||
|
||||
## Browser Compatibility
|
||||
|
||||
We recommend using the latest version of the `Chrome` browser.
|
||||
|
||||
Limited testing situation:
|
||||
|
||||
Normal operation: `360` extreme speed browser(v13.5.2036.0)、`opera` browser(v71.0.3770.284)。
|
||||
|
||||
Abnormal operation: `Firefox`(v98.0.2), Node content cannot be displayed in rich text mode. If you want to support the `Firefox` browser, please use non rich text mode.
|
||||
|
||||
Unsupported: `IE` browser.
|
||||
|
||||
## License
|
||||
|
||||
[MIT](https://opensource.org/licenses/MIT)
|
||||
|
||||
@@ -7,21 +7,21 @@
|
||||
</blockquote>
|
||||
<h2>Features</h2>
|
||||
<ul>
|
||||
<li><input type="checkbox" id="checkbox18" checked="true" /><label for="checkbox18">Plugin architecture. In addition to core functions, other functions are provided as plugins, which can be used as needed to reduce the overall volume</label></li>
|
||||
<li><input type="checkbox" id="checkbox19" checked="true" /><label for="checkbox19">Supports four types of structures: logical structure diagrams, mind maps,</label>
|
||||
organizational structure diagrams, and directory organization diagrams</li>
|
||||
<li><input type="checkbox" id="checkbox20" checked="true" /><label for="checkbox20">Built-in multiple themes and allows for highly customized styles, and support register new themes</label></li>
|
||||
<li><input type="checkbox" id="checkbox21" checked="true" /><label for="checkbox21">Supports shortcuts</label></li>
|
||||
<li><input type="checkbox" id="checkbox22" checked="true" /><label for="checkbox22">Node content supports images, icons, hyperlinks, notes, tags, and</label>
|
||||
<li><input type="checkbox" id="checkbox216" checked="true" /><label for="checkbox216">Plugin architecture. In addition to core functions, other functions are provided as plugins, which can be used as needed to reduce the overall volume</label></li>
|
||||
<li><input type="checkbox" id="checkbox217" checked="true" /><label for="checkbox217">Supports six types of structures: logical structure diagrams, mind maps,</label>
|
||||
organizational structure diagrams, directory organization diagrams, timeline, and fishbone diagrams</li>
|
||||
<li><input type="checkbox" id="checkbox218" checked="true" /><label for="checkbox218">Built-in multiple themes and allows for highly customized styles, and support register new themes</label></li>
|
||||
<li><input type="checkbox" id="checkbox219" checked="true" /><label for="checkbox219">Supports shortcuts</label></li>
|
||||
<li><input type="checkbox" id="checkbox220" checked="true" /><label for="checkbox220">Node content supports images, icons, hyperlinks, notes, tags, and</label>
|
||||
summaries</li>
|
||||
<li><input type="checkbox" id="checkbox23" checked="true" /><label for="checkbox23">Supports forward and backward navigation</label></li>
|
||||
<li><input type="checkbox" id="checkbox24" checked="true" /><label for="checkbox24">Supports dragging and scaling</label></li>
|
||||
<li><input type="checkbox" id="checkbox25" checked="true" /><label for="checkbox25">Supports right-click and Ctrl + left-click to select multiple items</label></li>
|
||||
<li><input type="checkbox" id="checkbox26" checked="true" /><label for="checkbox26">Supports free dragging and dragging to adjust nodes</label></li>
|
||||
<li><input type="checkbox" id="checkbox27" checked="true" /><label for="checkbox27">Supports various node shapes</label></li>
|
||||
<li><input type="checkbox" id="checkbox28" checked="true" /><label for="checkbox28">Supports export to json, png, svg, pdf markdown, and import from json, xmind, markdown</label></li>
|
||||
<li><input type="checkbox" id="checkbox29" checked="true" /><label for="checkbox29">Supports mini map、support watermark</label></li>
|
||||
<li><input type="checkbox" id="checkbox30" checked="true" /><label for="checkbox30">Supports associative lines</label></li>
|
||||
<li><input type="checkbox" id="checkbox221" checked="true" /><label for="checkbox221">Supports forward and backward navigation</label></li>
|
||||
<li><input type="checkbox" id="checkbox222" checked="true" /><label for="checkbox222">Supports dragging and scaling</label></li>
|
||||
<li><input type="checkbox" id="checkbox223" checked="true" /><label for="checkbox223">Supports right-click and Ctrl + left-click to select multiple items</label></li>
|
||||
<li><input type="checkbox" id="checkbox224" checked="true" /><label for="checkbox224">Supports free dragging and dragging to adjust nodes</label></li>
|
||||
<li><input type="checkbox" id="checkbox225" checked="true" /><label for="checkbox225">Supports various node shapes</label></li>
|
||||
<li><input type="checkbox" id="checkbox226" checked="true" /><label for="checkbox226">Supports export to json, png, svg, pdf markdown, and import from json, xmind, markdown</label></li>
|
||||
<li><input type="checkbox" id="checkbox227" checked="true" /><label for="checkbox227">Supports mini map、support watermark</label></li>
|
||||
<li><input type="checkbox" id="checkbox228" checked="true" /><label for="checkbox228">Supports associative lines</label></li>
|
||||
</ul>
|
||||
<h2>Repository Catalog Introduction</h2>
|
||||
<p>1.<code>simple-mind-map</code></p>
|
||||
@@ -31,16 +31,16 @@ frameworks such as Vue and React, or without a framework.</p>
|
||||
<p>This is an online mind map built using the <code>simple-mind-map</code> library and based
|
||||
on <code>Vue2.x</code> and <code>ElementUI</code>. Features include:</p>
|
||||
<ul>
|
||||
<li><input type="checkbox" id="checkbox31" checked="true" /><label for="checkbox31">Toolbar, which supports inserting and deleting nodes, and editing node</label>
|
||||
<li><input type="checkbox" id="checkbox229" checked="true" /><label for="checkbox229">Toolbar, which supports inserting and deleting nodes, and editing node</label>
|
||||
images, icons, hyperlinks, notes, tags, and summaries</li>
|
||||
<li><input type="checkbox" id="checkbox32" checked="true" /><label for="checkbox32">Sidebar, with panels for basic style settings, node style settings,</label>
|
||||
<li><input type="checkbox" id="checkbox230" checked="true" /><label for="checkbox230">Sidebar, with panels for basic style settings, node style settings,</label>
|
||||
outline, theme selection, and structure selection</li>
|
||||
<li><input type="checkbox" id="checkbox33" checked="true" /><label for="checkbox33">Import and export functionality; data is saved in the browser's local</label>
|
||||
<li><input type="checkbox" id="checkbox231" checked="true" /><label for="checkbox231">Import and export functionality; data is saved in the browser's local</label>
|
||||
storage by default, but it also supports creating, opening, and editing
|
||||
local files on the computer directly</li>
|
||||
<li><input type="checkbox" id="checkbox34" checked="true" /><label for="checkbox34">Right-click menu, which supports operations such as expanding, collapsing,</label>
|
||||
<li><input type="checkbox" id="checkbox232" checked="true" /><label for="checkbox232">Right-click menu, which supports operations such as expanding, collapsing,</label>
|
||||
and organizing layout</li>
|
||||
<li><input type="checkbox" id="checkbox35" checked="true" /><label for="checkbox35">Bottom bar, which supports node and word count statistics, switching</label>
|
||||
<li><input type="checkbox" id="checkbox233" checked="true" /><label for="checkbox233">Bottom bar, which supports node and word count statistics, switching</label>
|
||||
between edit and read-only modes, zooming in and out, and switching to
|
||||
full screen, support mini map</li>
|
||||
</ul>
|
||||
@@ -69,6 +69,12 @@ full screen, support mini map</li>
|
||||
<p>3.<a href="https://github.com/hizzgdev/jsmind">jsmind</a>、<a href="https://github.com/ssshooter/mind-elixir-core">Mind-elixir</a>、<a href="https://github.com/ondras/my-mind">my-mind</a>、<a href="https://github.com/awehook/blink-mind">blink-mind</a>、<a href="https://github.com/luvsic3/remind">remind</a>、<a href="https://github.com/hellowuxin/vue3-mindmap">vue3-mindmap</a>、<a href="https://github.com/zyascend/ZMindMap">ZMindMap</a>...</p>
|
||||
<p>These open-source mind maps are also good, each with its own characteristics, but they also have certain drawbacks, such as stopping updates, average interface aesthetics, less functionality, relying on a certain framework, and so on.</p>
|
||||
<p>In summary, in open-source mind maps, it is difficult to find a better choice than <code>simple-mind-map</code>. Of course, <code>simple-mind-map</code> is far from being the best, and it also has many shortcomings, as you saw in the previous [special note]. However, <code>simple-mind-map</code> has always been in a fast iteration process, and we welcome you to join and improve it together.</p>
|
||||
<h2>Browser Compatibility</h2>
|
||||
<p>We recommend using the latest version of the <code>Chrome</code> browser.</p>
|
||||
<p>Limited testing situation:</p>
|
||||
<p>Normal operation: <code>360</code> extreme speed browser(v13.5.2036.0)、<code>opera</code> browser(v71.0.3770.284)。</p>
|
||||
<p>Abnormal operation: <code>Firefox</code>(v98.0.2), Node content cannot be displayed in rich text mode. If you want to support the <code>Firefox</code> browser, please use non rich text mode.</p>
|
||||
<p>Unsupported: <code>IE</code> browser.</p>
|
||||
<h2>License</h2>
|
||||
<p><a href="https://opensource.org/licenses/MIT">MIT</a></p>
|
||||
<h2>Invite the author to a cup of coffee</h2>
|
||||
|
||||
@@ -17,7 +17,7 @@ If you are using the file in the format of `umd`, you can obtain it in the follo
|
||||
```
|
||||
|
||||
```js
|
||||
MindMap.markdown
|
||||
simpleMindMap.markdown
|
||||
```
|
||||
|
||||
## Methods
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<p>If you are using the file in the format of <code>umd</code>, you can obtain it in the following way:</p>
|
||||
<pre class="hljs"><code><span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"simple-mind-map/dist/simpleMindMap.umd.min.js"</span>></span><span class="hljs-tag"></<span class="hljs-name">script</span>></span>
|
||||
</code></pre>
|
||||
<pre class="hljs"><code>MindMap.markdown
|
||||
<pre class="hljs"><code>simpleMindMap.markdown
|
||||
</code></pre>
|
||||
<h2>Methods</h2>
|
||||
<h3>transformToMarkdown(data)</h3>
|
||||
|
||||
@@ -10,9 +10,11 @@ By default, node editing can only uniformly apply styles to all text in the node
|
||||
|
||||
The principle of this plugin is to use [Quill](https://github.com/quilljs/quill) editor implements rich text editing, and then uses the edited `DOM` node directly as the text data of the node, and embeds the `DOM` node through the `svg` `foreignObject` tag during rendering.
|
||||
|
||||
This also caused a problem, that is, the function of exporting as a picture was affected, The original principle of exporting `svg` as an image is very simple, Get the `svg` string, and then create the `blob` data of the `type=image/svg+xml` type. Then use the `URL.createObjectURL` method to generate the `data:url` data. Then create a `Image` tag, use the `data:url` as the `src` of the image, and finally draw the image on the `canvas` object for export, However, after testing, when the `DOM` node is embedded in the `svg`, this method of export will cause errors, and after trying many ways, the perfect export effect cannot be achieved, The current method is to traverse the `foreignObject` node in `svg`, using [html2canvas](https://github.com/niklasvh/html2canvas) Convert the `DOM` node in the `foreignObject` node into an image and then replace the `foreignObject` node. This method can work, but it is very time-consuming. Because the `html2canvas` conversion takes a long time, it takes about 2 seconds to convert a node. This leads to the more nodes, the slower the conversion time. Therefore, it is recommended not to use this plugin if you cannot tolerate the long time of export.
|
||||
> The following prompts exist in versions prior to v0.5.6:
|
||||
>
|
||||
> This also caused a problem, that is, the function of exporting as a picture was affected, The original principle of exporting `svg` as an image is very simple, Get the `svg` string, and then create the `blob` data of the `type=image/svg+xml` type. Then use the `URL.createObjectURL` method to generate the `data:url` data. Then create a `Image` tag, use the `data:url` as the `src` of the image, and finally draw the image on the `canvas` object for export, However, after testing, when the `DOM` node is embedded in the `svg`, this method of export will cause errors, and after trying many ways, the perfect export effect cannot be achieved, The current method is to traverse the `foreignObject` node in `svg`, using [html2canvas](https://github.com/niklasvh/html2canvas) Convert the `DOM` node in the `foreignObject` node into an image and then replace the `foreignObject` node. This method can work, but it is very time-consuming. Because the `html2canvas` conversion takes a long time, it takes about 2 seconds to convert a node. This leads to the more nodes, the slower the conversion time. Therefore, it is recommended not to use this plugin if you cannot tolerate the long time of export.
|
||||
|
||||
If you have a better way, please leave a message.
|
||||
The version of `v0.5.7+` directly uses `html2canvas` to convert the entire `svg`, which is no longer an issue with speed. However, there is currently a bug where the color of the node does not take effect after export.
|
||||
|
||||
## Register
|
||||
|
||||
|
||||
@@ -10,8 +10,11 @@
|
||||
<p>This plugin provides the ability to edit rich text of nodes, and takes effect after registration.</p>
|
||||
<p>By default, node editing can only uniformly apply styles to all text in the node. This plugin can support rich text editing effects. Currently, it supports bold, italic, underline, strikethrough, font, font size, color, and backgroundColor. Underline and line height are not supported.</p>
|
||||
<p>The principle of this plugin is to use <a href="https://github.com/quilljs/quill">Quill</a> editor implements rich text editing, and then uses the edited <code>DOM</code> node directly as the text data of the node, and embeds the <code>DOM</code> node through the <code>svg</code> <code>foreignObject</code> tag during rendering.</p>
|
||||
<blockquote>
|
||||
<p>The following prompts exist in versions prior to v0.5.6:</p>
|
||||
<p>This also caused a problem, that is, the function of exporting as a picture was affected, The original principle of exporting <code>svg</code> as an image is very simple, Get the <code>svg</code> string, and then create the <code>blob</code> data of the <code>type=image/svg+xml</code> type. Then use the <code>URL.createObjectURL</code> method to generate the <code>data:url</code> data. Then create a <code>Image</code> tag, use the <code>data:url</code> as the <code>src</code> of the image, and finally draw the image on the <code>canvas</code> object for export, However, after testing, when the <code>DOM</code> node is embedded in the <code>svg</code>, this method of export will cause errors, and after trying many ways, the perfect export effect cannot be achieved, The current method is to traverse the <code>foreignObject</code> node in <code>svg</code>, using <a href="https://github.com/niklasvh/html2canvas">html2canvas</a> Convert the <code>DOM</code> node in the <code>foreignObject</code> node into an image and then replace the <code>foreignObject</code> node. This method can work, but it is very time-consuming. Because the <code>html2canvas</code> conversion takes a long time, it takes about 2 seconds to convert a node. This leads to the more nodes, the slower the conversion time. Therefore, it is recommended not to use this plugin if you cannot tolerate the long time of export.</p>
|
||||
<p>If you have a better way, please leave a message.</p>
|
||||
</blockquote>
|
||||
<p>The version of <code>v0.5.7+</code> directly uses <code>html2canvas</code> to convert the entire <code>svg</code>, which is no longer an issue with speed. However, there is currently a bug where the color of the node does not take effect after export.</p>
|
||||
<h2>Register</h2>
|
||||
<pre class="hljs"><code><span class="hljs-keyword">import</span> MindMap <span class="hljs-keyword">from</span> <span class="hljs-string">'simple-mind-map'</span>
|
||||
<span class="hljs-keyword">import</span> RichText <span class="hljs-keyword">from</span> <span class="hljs-string">'simple-mind-map/src/RichText.js'</span>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
# Participate in translation
|
||||
|
||||
Thanks for the first version English translation provided by [Emircan ERKUL](https://github.com/emircanerkul).
|
||||
> Thanks for the first version English translation provided by [Emircan ERKUL](https://github.com/emircanerkul).
|
||||
>
|
||||
> Due to limited energy, most translations currently use machine translation, so accuracy is inevitably problematic.
|
||||
>
|
||||
> At present, the 【Course】 section is not translated. If you are interested, please join us.
|
||||
|
||||
If you want to participate in the translation of this document, you can clone this repository first.
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Participate in translation</h1>
|
||||
<blockquote>
|
||||
<p>Thanks for the first version English translation provided by <a href="https://github.com/emircanerkul">Emircan ERKUL</a>.</p>
|
||||
<p>Due to limited energy, most translations currently use machine translation, so accuracy is inevitably problematic.</p>
|
||||
<p>At present, the 【Course】 section is not translated. If you are interested, please join us.</p>
|
||||
</blockquote>
|
||||
<p>If you want to participate in the translation of this document, you can clone this repository first.</p>
|
||||
<p>The translated documents are in the <code>/web/src/pages/Doc/</code> directory, and currently support English(<code>en</code>) and Simplified Chinese(<code>zh</code>).</p>
|
||||
<p>If you are adding a new language type, you can create a new directory under the <code>/web/src/pages/Doc/</code> directory, Then create a folder for each chapter, You can also directly copy all chapter directories under the existing language directory for translation, Note that you only need to write the <code>index.md</code> file, The <code>index.vue</code> file under the chapter directory is automatically generated by the script according to <code>index.md</code>.</p>
|
||||
|
||||
@@ -129,6 +129,16 @@ Measure the width and height of the text, return value:
|
||||
{ width, height }
|
||||
```
|
||||
|
||||
#### getTextFromHtml(html)
|
||||
|
||||
Extract plain text content from an HTML string.
|
||||
|
||||
#### readBlob(blob)
|
||||
|
||||
> v0.5.9+
|
||||
|
||||
Convert `blob` data to `data:url` data.
|
||||
|
||||
## Simulate CSS background in Canvas
|
||||
|
||||
Import:
|
||||
@@ -157,4 +167,50 @@ drawBackgroundImageToCanvas(ctx, width, height, img, {
|
||||
// success
|
||||
}
|
||||
})
|
||||
```
|
||||
```
|
||||
|
||||
## LRU cache class
|
||||
|
||||
> v0.5.10+
|
||||
|
||||
Import:
|
||||
|
||||
```js
|
||||
import Lru from 'simple-mind-map/src/utils/Lru.js'
|
||||
```
|
||||
|
||||
### Constructor
|
||||
|
||||
```js
|
||||
let lru = new Lru(max)
|
||||
```
|
||||
|
||||
`max`: Specify the maximum number of caches.
|
||||
|
||||
### Instance properties
|
||||
|
||||
#### size
|
||||
|
||||
The current number of caches.
|
||||
|
||||
#### pool
|
||||
|
||||
Get cache pool.
|
||||
|
||||
### Instance methods
|
||||
|
||||
#### add(key, value)
|
||||
|
||||
Add cache.
|
||||
|
||||
#### delete(key)
|
||||
|
||||
Delete cache.
|
||||
|
||||
#### has(key)
|
||||
|
||||
Check if a cache exists.
|
||||
|
||||
#### get(key)
|
||||
|
||||
Gets the value of a cache.
|
||||
@@ -82,6 +82,13 @@ and copying the <code>data</code> of the data object, example:</p>
|
||||
<p>Measure the width and height of the text, return value:</p>
|
||||
<pre class="hljs"><code>{ width, height }
|
||||
</code></pre>
|
||||
<h4>getTextFromHtml(html)</h4>
|
||||
<p>Extract plain text content from an HTML string.</p>
|
||||
<h4>readBlob(blob)</h4>
|
||||
<blockquote>
|
||||
<p>v0.5.9+</p>
|
||||
</blockquote>
|
||||
<p>Convert <code>blob</code> data to <code>data:url</code> data.</p>
|
||||
<h2>Simulate CSS background in Canvas</h2>
|
||||
<p>Import:</p>
|
||||
<pre class="hljs"><code><span class="hljs-keyword">import</span> drawBackgroundImageToCanvas <span class="hljs-keyword">from</span> <span class="hljs-string">'simple-mind-map/src/utils/simulateCSSBackgroundInCanvas'</span>
|
||||
@@ -105,6 +112,31 @@ drawBackgroundImageToCanvas(ctx, width, height, img, {
|
||||
}
|
||||
})
|
||||
</code></pre>
|
||||
<h2>LRU cache class</h2>
|
||||
<blockquote>
|
||||
<p>v0.5.10+</p>
|
||||
</blockquote>
|
||||
<p>Import:</p>
|
||||
<pre class="hljs"><code><span class="hljs-keyword">import</span> Lru <span class="hljs-keyword">from</span> <span class="hljs-string">'simple-mind-map/src/utils/Lru.js'</span>
|
||||
</code></pre>
|
||||
<h3>Constructor</h3>
|
||||
<pre class="hljs"><code><span class="hljs-keyword">let</span> lru = <span class="hljs-keyword">new</span> Lru(max)
|
||||
</code></pre>
|
||||
<p><code>max</code>: Specify the maximum number of caches.</p>
|
||||
<h3>Instance properties</h3>
|
||||
<h4>size</h4>
|
||||
<p>The current number of caches.</p>
|
||||
<h4>pool</h4>
|
||||
<p>Get cache pool.</p>
|
||||
<h3>Instance methods</h3>
|
||||
<h4>add(key, value)</h4>
|
||||
<p>Add cache.</p>
|
||||
<h4>delete(key)</h4>
|
||||
<p>Delete cache.</p>
|
||||
<h4>has(key)</h4>
|
||||
<p>Check if a cache exists.</p>
|
||||
<h4>get(key)</h4>
|
||||
<p>Gets the value of a cache.</p>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -17,16 +17,15 @@ If you are using the file in the format of `umd`, you can obtain it in the follo
|
||||
```
|
||||
|
||||
```js
|
||||
MindMap.xmind
|
||||
simpleMindMap.xmind
|
||||
```
|
||||
|
||||
## Methods
|
||||
|
||||
### xmind.parseXmindFile(file)
|
||||
|
||||
Parsing the `.xmind` file and returning the parsed data. Note that this is
|
||||
complete data, including the node tree, theme, and structure. You can use
|
||||
`mindMap.setFullData(data)` to render the returned data to the canvas.
|
||||
Parsing the `.xmind` file and returning the parsed data. You can use
|
||||
`mindMap.setData(data)` to render the returned data to the canvas.
|
||||
|
||||
`file`: `File` object
|
||||
|
||||
@@ -35,9 +34,8 @@ complete data, including the node tree, theme, and structure. You can use
|
||||
Convert `xmind` data. The `.xmind` file is essentially a `zip` file that can be
|
||||
decompressed by changing the suffix to zip. Inside, there is a `content.json`
|
||||
file. If you have parsed this file yourself, you can pass the contents of this
|
||||
file to this method for conversion. The converted data is the complete data,
|
||||
including the node tree, theme, structure, etc. You can use
|
||||
`mindMap.setFullData(data)` to render the returned data to the canvas.
|
||||
file to this method for conversion. You can use
|
||||
`mindMap.setData(data)` to render the returned data to the canvas.
|
||||
|
||||
`content`: the contents of the `content.json` file within the `.xmind` zip
|
||||
package
|
||||
|
||||
@@ -11,21 +11,19 @@
|
||||
<p>If you are using the file in the format of <code>umd</code>, you can obtain it in the following way:</p>
|
||||
<pre class="hljs"><code><span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"simple-mind-map/dist/simpleMindMap.umd.min.js"</span>></span><span class="hljs-tag"></<span class="hljs-name">script</span>></span>
|
||||
</code></pre>
|
||||
<pre class="hljs"><code>MindMap.xmind
|
||||
<pre class="hljs"><code>simpleMindMap.xmind
|
||||
</code></pre>
|
||||
<h2>Methods</h2>
|
||||
<h3>xmind.parseXmindFile(file)</h3>
|
||||
<p>Parsing the <code>.xmind</code> file and returning the parsed data. Note that this is
|
||||
complete data, including the node tree, theme, and structure. You can use
|
||||
<code>mindMap.setFullData(data)</code> to render the returned data to the canvas.</p>
|
||||
<p>Parsing the <code>.xmind</code> file and returning the parsed data. You can use
|
||||
<code>mindMap.setData(data)</code> to render the returned data to the canvas.</p>
|
||||
<p><code>file</code>: <code>File</code> object</p>
|
||||
<h3>xmind.transformXmind(content)</h3>
|
||||
<p>Convert <code>xmind</code> data. The <code>.xmind</code> file is essentially a <code>zip</code> file that can be
|
||||
decompressed by changing the suffix to zip. Inside, there is a <code>content.json</code>
|
||||
file. If you have parsed this file yourself, you can pass the contents of this
|
||||
file to this method for conversion. The converted data is the complete data,
|
||||
including the node tree, theme, structure, etc. You can use
|
||||
<code>mindMap.setFullData(data)</code> to render the returned data to the canvas.</p>
|
||||
file to this method for conversion. You can use
|
||||
<code>mindMap.setData(data)</code> to render the returned data to the canvas.</p>
|
||||
<p><code>content</code>: the contents of the <code>content.json</code> file within the <code>.xmind</code> zip
|
||||
package</p>
|
||||
<h3>xmind.transformOldXmind(content)</h3>
|
||||
|
||||
@@ -25,6 +25,7 @@ export default [
|
||||
{ path: 'course16', title: '如何渲染富文本的悬浮工具栏' },
|
||||
{ path: 'course17', title: '导入和导出' },
|
||||
{ path: 'course18', title: '如何持久化数据' },
|
||||
{ path: 'course19', title: '插入和扩展节点图标' },
|
||||
{ path: 'doExport', title: 'Export 插件' },
|
||||
{ path: 'drag', title: 'Drag插件' },
|
||||
{ path: 'introduction', title: '简介' },
|
||||
@@ -41,7 +42,8 @@ export default [
|
||||
{ path: 'utils', title: '内置工具方法' },
|
||||
{ path: 'view', title: 'View实例' },
|
||||
{ path: 'watermark', title: 'Watermark插件' },
|
||||
{ path: 'xmind', title: 'XMind解析' }
|
||||
{ path: 'xmind', title: 'XMind解析' },
|
||||
{ path: 'deploy', title: '部署' }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -68,7 +70,8 @@ export default [
|
||||
{ path: 'utils', title: 'Utility Methods' },
|
||||
{ path: 'view', title: 'View instance' },
|
||||
{ path: 'watermark', title: 'Watermark plugin' },
|
||||
{ path: 'xmind', title: 'XMind parse' }
|
||||
{ path: 'xmind', title: 'XMind parse' },
|
||||
{ path: 'deploy', title: 'Deploy' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
> 调整关联线控制点的功能从v0.4.6+开始支持
|
||||
|
||||
该插件用于支持添加关联线。
|
||||
> 关联性支持文本编辑从v0.5.11+开始支持
|
||||
|
||||
该插件目前功能还不完善,不支持在关联线上添加文字。
|
||||
该插件用于支持添加关联线。
|
||||
|
||||
## 注册
|
||||
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
<blockquote>
|
||||
<p>调整关联线控制点的功能从v0.4.6+开始支持</p>
|
||||
</blockquote>
|
||||
<blockquote>
|
||||
<p>关联性支持文本编辑从v0.5.11+开始支持</p>
|
||||
</blockquote>
|
||||
<p>该插件用于支持添加关联线。</p>
|
||||
<p>该插件目前功能还不完善,不支持在关联线上添加文字。</p>
|
||||
<h2>注册</h2>
|
||||
<pre class="hljs"><code><span class="hljs-keyword">import</span> MindMap <span class="hljs-keyword">from</span> <span class="hljs-string">'simple-mind-map'</span>
|
||||
<span class="hljs-keyword">import</span> AssociativeLine <span class="hljs-keyword">from</span> <span class="hljs-string">'simple-mind-map/src/AssociativeLine.js'</span>
|
||||
|
||||
@@ -1,8 +1,74 @@
|
||||
# Changelog
|
||||
|
||||
## 0.5.11
|
||||
|
||||
新增:支持关联性文本编辑。
|
||||
|
||||
优化:优化主题配置更新,改变不涉及节点大小的配置不触发节点重新计算。
|
||||
|
||||
## 0.5.10
|
||||
|
||||
新增:使用LRU缓存算法优化节点复用逻辑。
|
||||
|
||||
## 0.5.10-fix.1
|
||||
|
||||
修复:修复导入出错的问题。
|
||||
|
||||
## 0.5.10-fix.2
|
||||
|
||||
修复:修复富文本模式下,切换主题、导入数据后没有触发数据改变的问题。
|
||||
|
||||
新增:新增三种主题。
|
||||
|
||||
## 0.5.9
|
||||
|
||||
修改:统一导出方法的格式,使用`FileReader`代替`URL.createObjectURL`转换`blob`数据。
|
||||
|
||||
## 0.5.8
|
||||
|
||||
优化:1.节点位置没有变化不触发位置设置。 2.展开收起状态没有变化不触发按钮更新。
|
||||
|
||||
新增:1.默认改为鼠标移上节点才显示展开收起按钮。 2.支持扩展节点可插入的图标列表。
|
||||
|
||||
## 0.5.7
|
||||
|
||||
破坏性更新:富文本模式下导出png改为使用html2canvas转换整个svg,大幅提高导出速度,不过html2canvas存在一个bug,foreignObject元素中的dom节点内联的文字颜色无法识别,所以导出节点的文字颜色是固定的,不过相对于之前的导出基本不可用状态,目前至少能快速顺利的导出。
|
||||
|
||||
优化:优化富文本节点编辑体验。
|
||||
|
||||
新增:富文本模式下,导入数据、初始化数据、切换主题场景节点样式支持跟随主题变化。
|
||||
|
||||
## 0.5.6
|
||||
|
||||
修复:1.修复短时间快速多次渲染时节点位置错乱的问题。 2.修复节点正在编辑中时拖动画布导致编辑框和节点分离的问题。
|
||||
|
||||
新增:1.添加最大历史记录数限制。
|
||||
|
||||
## 0.5.5
|
||||
|
||||
新增:1.支持配置导出为png、svg、pdf时的内边距。 2.支持配置节点文本编辑框、节点备注浮层元素的z-index。 3.支持点击画布外的区域结束节点编辑状态。
|
||||
|
||||
## 0.5.5-fix.1
|
||||
|
||||
修复:1.修复节点在画布外编辑时编辑框也在画布外的问题。 2.修改结构后复位变换,防止存在缩放时切换结构后第一次拖动时会发生位置突变的问题。
|
||||
|
||||
优化:1.节点多选时只要节点和选区存在交叉即认为被选中。
|
||||
|
||||
## 0.5.5-fix.2
|
||||
|
||||
修复:1.修复小地图报错。
|
||||
|
||||
## 0.5.4
|
||||
|
||||
新增:1.添加新主题。
|
||||
新增:1.添加新主题。 2.新增时间轴和鱼骨结构。
|
||||
|
||||
修复:1.修复节点右键和画布右键的冲突问题。 2.修复组织结构图、目录组织图等节点拖拽时存在线段未隐藏的bug。
|
||||
|
||||
优化:1.优化组织结构图布局。2.优化目录组织图布局。
|
||||
|
||||
## 0.5.4-fix.1
|
||||
|
||||
优化:1.优化鱼骨图布局。
|
||||
|
||||
## 0.5.3
|
||||
|
||||
|
||||
@@ -1,8 +1,41 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Changelog</h1>
|
||||
<h2>0.5.11</h2>
|
||||
<p>新增:支持关联性文本编辑。</p>
|
||||
<p>优化:优化主题配置更新,改变不涉及节点大小的配置不触发节点重新计算。</p>
|
||||
<h2>0.5.10</h2>
|
||||
<p>新增:使用LRU缓存算法优化节点复用逻辑。</p>
|
||||
<h2>0.5.10-fix.1</h2>
|
||||
<p>修复:修复导入出错的问题。</p>
|
||||
<h2>0.5.10-fix.2</h2>
|
||||
<p>修复:修复富文本模式下,切换主题、导入数据后没有触发数据改变的问题。</p>
|
||||
<p>新增:新增三种主题。</p>
|
||||
<h2>0.5.9</h2>
|
||||
<p>修改:统一导出方法的格式,使用<code>FileReader</code>代替<code>URL.createObjectURL</code>转换<code>blob</code>数据。</p>
|
||||
<h2>0.5.8</h2>
|
||||
<p>优化:1.节点位置没有变化不触发位置设置。 2.展开收起状态没有变化不触发按钮更新。</p>
|
||||
<p>新增:1.默认改为鼠标移上节点才显示展开收起按钮。 2.支持扩展节点可插入的图标列表。</p>
|
||||
<h2>0.5.7</h2>
|
||||
<p>破坏性更新:富文本模式下导出png改为使用html2canvas转换整个svg,大幅提高导出速度,不过html2canvas存在一个bug,foreignObject元素中的dom节点内联的文字颜色无法识别,所以导出节点的文字颜色是固定的,不过相对于之前的导出基本不可用状态,目前至少能快速顺利的导出。</p>
|
||||
<p>优化:优化富文本节点编辑体验。</p>
|
||||
<p>新增:富文本模式下,导入数据、初始化数据、切换主题场景节点样式支持跟随主题变化。</p>
|
||||
<h2>0.5.6</h2>
|
||||
<p>修复:1.修复短时间快速多次渲染时节点位置错乱的问题。 2.修复节点正在编辑中时拖动画布导致编辑框和节点分离的问题。</p>
|
||||
<p>新增:1.添加最大历史记录数限制。</p>
|
||||
<h2>0.5.5</h2>
|
||||
<p>新增:1.支持配置导出为png、svg、pdf时的内边距。 2.支持配置节点文本编辑框、节点备注浮层元素的z-index。 3.支持点击画布外的区域结束节点编辑状态。</p>
|
||||
<h2>0.5.5-fix.1</h2>
|
||||
<p>修复:1.修复节点在画布外编辑时编辑框也在画布外的问题。 2.修改结构后复位变换,防止存在缩放时切换结构后第一次拖动时会发生位置突变的问题。</p>
|
||||
<p>优化:1.节点多选时只要节点和选区存在交叉即认为被选中。</p>
|
||||
<h2>0.5.5-fix.2</h2>
|
||||
<p>修复:1.修复小地图报错。</p>
|
||||
<h2>0.5.4</h2>
|
||||
<p>新增:1.添加新主题。</p>
|
||||
<p>新增:1.添加新主题。 2.新增时间轴和鱼骨结构。</p>
|
||||
<p>修复:1.修复节点右键和画布右键的冲突问题。 2.修复组织结构图、目录组织图等节点拖拽时存在线段未隐藏的bug。</p>
|
||||
<p>优化:1.优化组织结构图布局。2.优化目录组织图布局。</p>
|
||||
<h2>0.5.4-fix.1</h2>
|
||||
<p>优化:1.优化鱼骨图布局。</p>
|
||||
<h2>0.5.3</h2>
|
||||
<p>修复:1.修复富文本模式下,如果选择了多个节点时设置文本样式,会将所有多选节点的文本改成最后一个多选节点的文本的问题。</p>
|
||||
<p>新增:1.支持设置初始中心节点的位置。</p>
|
||||
|
||||