This commit is contained in:
wanglin2 2023-05-03 21:26:13 +08:00
commit 27d2238d20
203 changed files with 16484 additions and 11471 deletions

3
.gitignore vendored
View File

@ -1,2 +1,3 @@
node_modules
.DS_Store
.DS_Store
dist_electron

View File

@ -7,15 +7,22 @@
[![GitHub forks](https://img.shields.io/github/forks/wanglin2/mind-map)](https://github.com/wanglin2/mind-map/network/members)
![license](https://img.shields.io/npm/l/express.svg)
> 一个简单&强大的Web思维导图
> 一个简单&强大的Web思维导图
Demo[https://wanglin2.github.io/mind-map/](https://wanglin2.github.io/mind-map/)
本项目包含两部分:
文档:[https://wanglin2.github.io/mind-map/#/doc/zh/](https://wanglin2.github.io/mind-map/#/doc/zh/)
1.一个js思维导图库不依赖任何框架你可以使用它来快速完成Web思维导图产品的开发。
开发文档:[https://wanglin2.github.io/mind-map/#/doc/zh/](https://wanglin2.github.io/mind-map/#/doc/zh/)
2.一个Web思维导图基于思维导图库、Vue2.x、ElementUI开发可以操作电脑本地文件所以你可以直接把它当做一个在线版思维导图应用使用如果觉得github的响应速度慢你也可以部署到你的服务器上。
在线地址:[https://wanglin2.github.io/mind-map/](https://wanglin2.github.io/mind-map/)
# 特性
- [x] 支持逻辑结构图、思维导图、组织结构图、目录组织图四种结构
- [x] 插件化架构,除核心功能外,其他功能作为插件提供,按需使用,减小打包体积
- [x] 支持逻辑结构图、思维导图、组织结构图、目录组织图、时间轴、鱼骨图六种结构
- [x] 内置多种主题,允许高度自定义样式,支持注册新主题
- [x] 支持快捷键
- [x] 节点内容支持图片、图标、超链接、备注、标签、概要
@ -24,8 +31,9 @@ Demo[https://wanglin2.github.io/mind-map/](https://wanglin2.github.io/mind-ma
- [x] 支持右键和Ctrl+左键两种多选方式
- [x] 支持节点自由拖拽、拖拽调整
- [x] 支持多种节点形状
- [x] 支持导出为`json`、`png`、`svg`、`pdf`,支持从`json`、`xmind`导入
- [x] 支持小地图
- [x] 支持导出为`json`、`png`、`svg`、`pdf`、`markdown`,支持从`json`、`xmind`、`markdown`导入
- [x] 支持小地图、支持水印
- [x] 支持关联线
# 安装
@ -35,10 +43,23 @@ npm i simple-mind-map
# 使用
提供一个宽高不为0的容器元素
```html
<div id="mindMapContainer"></div>
```
另外再设置一下`css`样式:
```css
#mindMapContainer * {
margin: 0;
padding: 0;
}
```
然后创建一个实例:
```js
import MindMap from "simple-mind-map";
@ -53,6 +74,10 @@ const mindMap = new MindMap({
});
```
即可得到一个思维导图。
想要实现更多功能?可以查看[开发文档](https://wanglin2.github.io/mind-map/#/doc/zh/)。
# License
MIT

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -5,9 +5,15 @@ import KeyboardNavigation from './src/KeyboardNavigation.js'
import Export from './src/Export.js'
import Drag from './src/Drag.js'
import Select from './src/Select.js'
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)
@ -16,5 +22,7 @@ MindMap
.usePlugin(KeyboardNavigation)
.usePlugin(Export)
.usePlugin(Select)
.usePlugin(AssociativeLine)
.usePlugin(RichText)
export default MindMap

View File

@ -7,17 +7,19 @@ import Style from './src/Style'
import KeyCommand from './src/KeyCommand'
import Command from './src/Command'
import BatchExecution from './src/BatchExecution'
import { layoutValueList } from './src/utils/constant'
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 = {
// 是否只读
readonly: false,
// 布局
layout: 'logicalStructure',
layout: CONSTANTS.LAYOUT.LOGICAL_STRUCTURE,
// 如果结构为鱼骨图,那么可以通过该选项控制倾斜角度
fishboneDeg: 45,
// 主题
theme: 'default', // 内置主题default默认主题
// 主题配置,会和所选择的主题进行合并
@ -61,7 +63,66 @@ const defaultOpt = {
}
},
// 达到该宽度文本自动换行
textAutoWrapWidth: 500
textAutoWrapWidth: 500,
// 自定义鼠标滚轮事件处理
// 可以传一个函数,回调参数为事件对象
customHandleMousewheel: null,
// 鼠标滚动的行为如果customHandleMousewheel传了自定义函数这个属性不生效
mousewheelAction: CONSTANTS.MOUSE_WHEEL_ACTION.ZOOM,// zoom放大缩小、move上下移动
// 当mousewheelAction设为move时可以通过该属性控制鼠标滚动一下视图移动的步长单位px
mousewheelMoveStep: 100,
// 默认插入的二级节点的文字
defaultInsertSecondLevelNodeText: '二级节点',
// 默认插入的二级以下节点的文字
defaultInsertBelowSecondLevelNodeText: '分支主题',
// 展开收起按钮的颜色
expandBtnStyle: {
color: '#808080',
fill: '#fff'
},
// 自定义展开收起按钮的图标
expandBtnIcon: {
open: '',// svg字符串
close: ''
},
// 是否只有当鼠标在画布内才响应快捷键事件
enableShortcutOnlyWhenMouseInSvg: true,
// 是否开启节点动画过渡
enableNodeTransitionMove: true,
// 如果开启节点动画过渡可以通过该属性设置过渡的时间单位ms
nodeTransitionMoveDuration: 300,
// 初始根节点的位置
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: '关联'
}
// 思维导图
@ -84,7 +145,7 @@ class MindMap {
this.draw = this.svg.group()
// 节点id
this.uid = 0
this.uid = 1
// 初始化主题
this.initTheme()
@ -124,7 +185,7 @@ class MindMap {
})
// 初始渲染
this.reRender()
this.render()
setTimeout(() => {
this.command.addHistory()
}, 0)
@ -134,7 +195,7 @@ class MindMap {
handleOpt(opt) {
// 检查布局配置
if (!layoutValueList.includes(opt.layout)) {
opt.layout = 'logicalStructure'
opt.layout = CONSTANTS.LAYOUT.LOGICAL_STRUCTURE
}
// 检查主题配置
opt.theme = opt.theme && theme[opt.theme] ? opt.theme : 'default'
@ -142,21 +203,21 @@ class MindMap {
}
// 渲染,部分渲染
render(callback) {
render(callback, source = '') {
this.batchExecution.push('render', () => {
this.initTheme()
this.renderer.reRender = false
this.renderer.render(callback)
this.renderer.render(callback, source)
})
}
// 重新渲染
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)
})
}
@ -195,7 +256,7 @@ class MindMap {
setTheme(theme) {
this.renderer.clearAllActive()
this.opt.theme = theme
this.reRender()
this.render(null, CONSTANTS.CHANGE_THEME)
}
// 获取当前主题
@ -206,7 +267,9 @@ class MindMap {
// 设置主题配置
setThemeConfig(config) {
this.opt.themeConfig = config
this.reRender()
// 检查改变的是否是节点大小无关的主题属性
let res = checkIsNodeSizeIndependenceConfig(config)
this.render(null, res ? '' : CONSTANTS.CHANGE_THEME)
}
// 获取自定义主题配置
@ -238,9 +301,10 @@ class MindMap {
setLayout(layout) {
// 检查布局配置
if (!layoutValueList.includes(layout)) {
layout = 'logicalStructure'
layout = CONSTANTS.LAYOUT.LOGICAL_STRUCTURE
}
this.opt.layout = layout
this.view.reset()
this.renderer.setLayout()
this.render()
}
@ -254,8 +318,13 @@ class MindMap {
setData(data) {
this.execCommand('CLEAR_ACTIVE_NODE')
this.command.clearHistory()
this.renderer.renderTree = data
this.reRender()
this.command.addHistory()
if (this.richText) {
this.renderer.renderTree = this.richText.handleSetData(data)
} else {
this.renderer.renderTree = data
}
this.reRender(() => {}, CONSTANTS.SET_DATA)
}
// 动态设置思维导图数据,包括节点数据、布局、主题、视图
@ -281,7 +350,7 @@ class MindMap {
// 获取思维导图数据,节点树、主题、布局等
getData(withConfig) {
let nodeData = this.command.getCopyData()
let nodeData = this.command.removeDataUid(this.command.getCopyData())
let data = {}
if (withConfig) {
data = {
@ -315,10 +384,10 @@ class MindMap {
// 设置只读模式、编辑模式
setMode(mode) {
if (!['readonly', 'edit'].includes(mode)) {
if (![CONSTANTS.MODE.READONLY, CONSTANTS.MODE.EDIT].includes(mode)) {
return
}
this.opt.readonly = mode === 'readonly'
this.opt.readonly = mode === CONSTANTS.MODE.READONLY
if (this.opt.readonly) {
// 取消当前激活的元素
this.renderer.clearAllActive()
@ -327,7 +396,7 @@ class MindMap {
}
// 获取svg数据
getSvgData() {
getSvgData({ paddingX = 0, paddingY = 0 } = {}) {
const svg = this.svg
const draw = this.draw
// 保存原始信息
@ -339,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)
// 把实际内容变换

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "simple-mind-map",
"version": "0.4.2",
"version": "0.5.11",
"description": "一个简单的web在线思维导图",
"authors": [
{
@ -31,7 +31,9 @@
"html2canvas": "^1.4.1",
"jspdf": "^2.5.1",
"jszip": "^3.10.1",
"mdast-util-from-markdown": "^1.3.0",
"quill": "^1.3.6",
"uuid": "^9.0.0",
"xml-js": "^1.6.11"
},
"keywords": [

View File

@ -0,0 +1,456 @@
import { walk, bfsWalk, throttle } from './utils/'
import { v4 as uuid } from 'uuid'
import {
getAssociativeLineTargetIndex,
computeCubicBezierPathPoints,
cubicBezierPath,
getNodePoint,
computeNodePoints,
getNodeLinePath
} from './utils/associativeLineUtils'
import associativeLineControlsMethods from './utils/associativeLineControls'
import associativeLineTextMethods from './utils/associativeLineText'
// 关联线类
class AssociativeLine {
constructor(opt = {}) {
this.mindMap = opt.mindMap
this.draw = this.mindMap.draw
// 当前所有连接线
this.lineList = []
// 当前激活的连接线
this.activeLine = null
// 当前正在创建连接线
this.isCreatingLine = false // 是否正在创建连接线中
this.creatingStartNode = null // 起始节点
this.creatingLine = null // 创建过程中的连接线
this.overlapNode = null // 创建过程中的目标节点
// 是否有节点正在被拖拽
this.isNodeDragging = false
// 箭头图标
this.markerPath = null
this.marker = this.createMarker()
// 控制点
this.controlLine1 = null
this.controlLine2 = null
this.controlPoint1 = null
this.controlPoint2 = null
this.controlPointDiameter = 10
this.isControlPointMousedown = false
this.mousedownControlPointKey = ''
this.controlPointMousemoveState = {
pos: null,
startPoint: null,
endPoint: null,
targetIndex: ''
}
// 节流一下,不然很卡
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()
}
// 监听事件
bindEvent() {
// 节点树渲染完毕后渲染连接线
this.renderAllLines = this.renderAllLines.bind(this)
this.mindMap.on('node_tree_render_end', this.renderAllLines)
// 状态改变后重新渲染连接线
this.mindMap.on('data_change', this.renderAllLines)
// 监听画布和节点点击事件,用于清除当前激活的连接线
this.mindMap.on('draw_click', () => {
if (this.isControlPointMousedown) {
return
}
this.clearActiveLine()
})
this.mindMap.on('node_click', node => {
if (this.isCreatingLine) {
this.completeCreateLine(node)
} else {
this.clearActiveLine()
}
})
// 注册删除快捷键
this.mindMap.keyCommand.addShortcut(
'Del|Backspace',
this.removeLine.bind(this)
)
// 注册添加连接线的命令
this.mindMap.command.add('ADD_ASSOCIATIVE_LINE', this.addLine.bind(this))
// 监听鼠标移动事件
this.mindMap.on('mousemove', this.onMousemove.bind(this))
// 节点拖拽事件
this.mindMap.on('node_dragging', this.onNodeDragging.bind(this))
this.mindMap.on('node_dragend', this.onNodeDragend.bind(this))
// 拖拽控制点
window.addEventListener('mousemove', e => {
this.onControlPointMousemove(e)
})
window.addEventListener('mouseup', e => {
this.onControlPointMouseup(e)
})
// 缩放事件
this.mindMap.on('scale', this.onScale)
}
// 创建箭头
createMarker() {
return this.draw.marker(20, 20, add => {
add.ref(2, 5)
add.size(10, 10)
add.attr('orient', 'auto-start-reverse')
this.markerPath = add.path('M0,0 L2,5 L0,10 L10,5 Z')
})
}
// 渲染所有连线
renderAllLines() {
// 先移除
this.removeAllLines()
this.removeControls()
this.clearActiveLine()
let tree = this.mindMap.renderer.root
if (!tree) return
let idToNode = new Map()
let nodeToIds = new Map()
walk(
tree,
null,
cur => {
if (!cur) return
let data = cur.nodeData.data
if (
data.associativeLineTargets &&
data.associativeLineTargets.length > 0
) {
nodeToIds.set(cur, data.associativeLineTargets)
}
if (data.id) {
idToNode.set(data.id, cur)
}
},
() => {},
true,
0
)
nodeToIds.forEach((ids, node) => {
ids.forEach(id => {
let toNode = idToNode.get(id)
if (!node || !toNode) return
let [startPoint, endPoint] = computeNodePoints(node, toNode)
this.drawLine(startPoint, endPoint, node, toNode)
})
})
}
// 绘制连接线
drawLine(startPoint, endPoint, node, toNode) {
let {
associativeLineWidth,
associativeLineColor,
associativeLineActiveWidth,
associativeLineActiveColor
} = this.mindMap.themeConfig
// 箭头
this.markerPath
.stroke({ color: associativeLineColor })
.fill({ color: associativeLineColor })
// 路径
let { path: pathStr, controlPoints } = getNodeLinePath(
startPoint,
endPoint,
node,
toNode
)
// 虚线
let path = this.draw.path()
path
.stroke({
width: associativeLineWidth,
color: associativeLineColor,
dasharray: [6, 4]
})
.fill({ color: 'none' })
path.plot(pathStr)
path.marker('end', this.marker)
// 不可见的点击线
let clickPath = this.draw.path()
clickPath
.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()
this.setActiveLine({ path, clickPath, text, node, toNode, startPoint, endPoint, controlPoints })
})
// 渲染关联线文字
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)
}
}
// 移除所有连接线
removeAllLines() {
this.lineList.forEach(line => {
line[0].remove()
line[1].remove()
line[2].remove()
})
this.lineList = []
}
// 从当前激活节点开始创建连接线
createLineFromActiveNode() {
if (this.mindMap.renderer.activeNodeList.length <= 0) return
let node = this.mindMap.renderer.activeNodeList[0]
this.createLine(node)
}
// 创建连接线
createLine(fromNode) {
let { associativeLineWidth, associativeLineColor } =
this.mindMap.themeConfig
if (this.isCreatingLine || !fromNode) return
this.isCreatingLine = true
this.creatingStartNode = fromNode
this.creatingLine = this.draw.path()
this.creatingLine
.stroke({
width: associativeLineWidth,
color: associativeLineColor,
dasharray: [6, 4]
})
.fill({ color: 'none' })
this.creatingLine.marker('end', this.marker)
}
// 鼠标移动事件
onMousemove(e) {
if (!this.isCreatingLine) return
this.updateCreatingLine(e)
}
// 更新创建过程中的连接线
updateCreatingLine(e) {
let { x, y } = this.getTransformedEventPos(e)
let startPoint = getNodePoint(this.creatingStartNode)
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)
}
// 获取转换后的鼠标事件对象的坐标
getTransformedEventPos(e) {
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
let { scaleX, scaleY, translateX, translateY } =
this.mindMap.draw.transform()
return {
x: (x - translateX) / scaleX,
y: (y - translateY) / scaleY
}
}
// 检测当前移动到的目标节点
checkOverlapNode(x, y) {
this.overlapNode = null
bfsWalk(this.mindMap.renderer.root, node => {
if (node.nodeData.data.isActive) {
this.mindMap.renderer.setNodeActive(node, false)
}
if (node === this.creatingStartNode || this.overlapNode) {
return
}
let { left, top, width, height } = node
let right = left + width
let bottom = top + height
if (x >= left && x <= right && y >= top && y <= bottom) {
this.overlapNode = node
}
})
if (this.overlapNode && !this.overlapNode.nodeData.data.isActive) {
this.mindMap.renderer.setNodeActive(this.overlapNode, true)
}
}
// 完成创建连接线
completeCreateLine(node) {
if (this.creatingStartNode === node) return
this.addLine(this.creatingStartNode, node)
if (this.overlapNode && this.overlapNode.nodeData.data.isActive) {
this.mindMap.renderer.setNodeActive(this.overlapNode, false)
}
this.isCreatingLine = false
this.creatingStartNode = null
this.creatingLine.remove()
this.creatingLine = null
this.overlapNode = null
}
// 添加连接线
addLine(fromNode, toNode) {
if (!fromNode || !toNode) return
// 目标节点如果没有id则生成一个id
let id = toNode.nodeData.data.id
if (!id) {
id = uuid()
this.mindMap.execCommand('SET_NODE_DATA', toNode, {
id
})
}
// 将目标节点id保存起来
let list = fromNode.nodeData.data.associativeLineTargets || []
list.push(id)
// 保存控制点
let [startPoint, endPoint] = computeNodePoints(fromNode, toNode)
let controlPoints = computeCubicBezierPathPoints(
startPoint.x,
startPoint.y,
endPoint.x,
endPoint.y
)
let offsetList =
fromNode.nodeData.data.associativeLineTargetControlOffsets || []
// 保存的实际是控制点和端点的差值,否则当节点位置改变了,控制点还是原来的位置,连线就不对了
offsetList[list.length - 1] = [
{
x: controlPoints[0].x - startPoint.x,
y: controlPoints[0].y - startPoint.y
},
{
x: controlPoints[1].x - endPoint.x,
y: controlPoints[1].y - endPoint.y
}
]
this.mindMap.execCommand('SET_NODE_DATA', fromNode, {
associativeLineTargets: list,
associativeLineTargetControlOffsets: offsetList
})
}
// 删除连接线
removeLine() {
if (!this.activeLine) return
let [, , , node, toNode] = this.activeLine
this.removeControls()
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
})
}
// 清除当前激活的节点
clearActiveNodes() {
if (this.mindMap.renderer.activeNodeList.length > 0) {
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
}
}
// 清除激活的线
clearActiveLine() {
if (this.activeLine) {
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()
}
}
// 处理节点正在拖拽事件
onNodeDragging() {
if (this.isNodeDragging) return
this.isNodeDragging = true
this.lineList.forEach(line => {
line[0].hide()
line[1].hide()
line[2].hide()
})
this.hideControls()
}
// 处理节点拖拽完成事件
onNodeDragend() {
if (!this.isNodeDragging) return
this.lineList.forEach(line => {
line[0].show()
line[1].show()
line[2].show()
})
this.showControls()
this.isNodeDragging = false
}
}
AssociativeLine.instanceName = 'associativeLine'
export default AssociativeLine

View File

@ -1,33 +1,4 @@
// 在下一个事件循环里执行任务
const nextTick = function (fn, ctx) {
let pending = false
let timerFunc = null
let handle = () => {
pending = false
ctx ? fn.call(ctx) : fn()
}
// 支持MutationObserver接口的话使用MutationObserver
if (typeof MutationObserver !== 'undefined') {
let counter = 1
let observer = new MutationObserver(handle)
let textNode = document.createTextNode(counter)
observer.observe(textNode, {
characterData: true // 设为 true 表示监视指定目标节点或子节点树中节点所包含的字符数据的变化
})
timerFunc = function () {
counter = (counter + 1) % 2 // counter会在0和1两者循环变化
textNode.data = counter // 节点变化会触发回调handle
}
} else {
// 否则使用定时器
timerFunc = setTimeout
}
return function () {
if (pending) return
pending = true
timerFunc(handle, 0)
}
}
import { nextTick } from './utils'
// 批量执行
class BatchExecution {

View File

@ -1,4 +1,4 @@
import { copyRenderTree, simpleDeepClone } from './utils'
import { copyRenderTree, simpleDeepClone, nextTick } from './utils'
// 命令类
class Command {
@ -11,6 +11,7 @@ class Command {
this.activeHistoryIndex = 0
// 注册快捷键
this.registerShortcutKeys()
this.addHistory = nextTick(this.addHistory, this)
}
// 清空历史数据
@ -36,7 +37,7 @@ class Command {
this.commands[name].forEach(fn => {
fn(...args)
})
if (name === 'BACK' || name === 'FORWARD') {
if (['BACK', 'FORWARD', 'SET_NODE_ACTIVE', 'CLEAR_ACTIVE_NODE'].includes(name)) {
return
}
this.addHistory()
@ -76,9 +77,19 @@ class Command {
return
}
let data = this.getCopyData()
// 此次数据和上次一样则不重复添加
if (this.history.length > 0 && JSON.stringify(this.history[this.history.length - 1]) === JSON.stringify(data)) {
return
}
// 删除当前历史指针后面的数据
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', data)
this.mindMap.emit('data_change', this.removeDataUid(data))
this.mindMap.emit(
'back_forward',
this.activeHistoryIndex,
@ -98,7 +109,9 @@ class Command {
this.activeHistoryIndex,
this.history.length
)
return simpleDeepClone(this.history[this.activeHistoryIndex])
let data = simpleDeepClone(this.history[this.activeHistoryIndex])
this.mindMap.emit('data_change', this.removeDataUid(data))
return data
}
}
@ -110,14 +123,31 @@ class Command {
let len = this.history.length
if (this.activeHistoryIndex + step <= len - 1) {
this.activeHistoryIndex += step
this.mindMap.emit('back_forward', this.activeHistoryIndex)
return simpleDeepClone(this.history[this.activeHistoryIndex])
this.mindMap.emit('back_forward', this.activeHistoryIndex, this.history.length)
let data = simpleDeepClone(this.history[this.activeHistoryIndex])
this.mindMap.emit('data_change', this.removeDataUid(data))
return data
}
}
// 获取渲染树数据副本
getCopyData() {
return copyRenderTree({}, this.mindMap.renderer.renderTree)
return copyRenderTree({}, this.mindMap.renderer.renderTree, true)
}
// 移除节点数据中的uid
removeDataUid(data) {
data = simpleDeepClone(data)
let walk = (root) => {
delete root.data.uid
if (root.children && root.children.length > 0) {
root.children.forEach((item) => {
walk(item)
})
}
}
walk(data)
return data
}
}

View File

@ -5,7 +5,6 @@ import Base from './layouts/Base'
class Drag extends Base {
// 构造函数
constructor({ mindMap }) {
super(mindMap.renderer)
this.mindMap = mindMap
@ -14,7 +13,6 @@ class Drag extends Base {
}
// 复位
reset() {
// 当前拖拽节点
this.node = null
@ -45,10 +43,11 @@ class Drag extends Base {
this.mouseDownY = 0
this.mouseMoveX = 0
this.mouseMoveY = 0
// 鼠标移动的距离距鼠标按下的位置距离多少以上才认为是拖动事件
this.checkDragOffset = 10
}
// 绑定事件
bindEvent() {
this.checkOverlapNode = throttle(this.checkOverlapNode, 300, this)
this.mindMap.on('node_mousedown', (node, e) => {
@ -77,13 +76,14 @@ class Drag extends Base {
if (!this.isMousedown) {
return
}
this.mindMap.emit('node_dragging', this.node)
e.preventDefault()
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
this.mouseMoveX = x
this.mouseMoveY = y
if (
Math.abs(x - this.mouseDownX) <= 10 &&
Math.abs(y - this.mouseDownY) <= 10 &&
Math.abs(x - this.mouseDownX) <= this.checkDragOffset &&
Math.abs(y - this.mouseDownY) <= this.checkDragOffset &&
!this.node.isDrag
) {
return
@ -97,7 +97,6 @@ class Drag extends Base {
}
// 鼠标松开事件
onMouseup(e) {
if (!this.isMousedown) {
return
@ -136,10 +135,10 @@ class Drag extends Base {
this.mindMap.render()
}
this.reset()
this.mindMap.emit('node_dragend')
}
// 创建克隆节点
createCloneNode() {
if (!this.clone) {
// 节点
@ -161,7 +160,6 @@ class Drag extends Base {
}
// 移除克隆节点
removeCloneNode() {
if (!this.clone) {
return
@ -172,7 +170,6 @@ class Drag extends Base {
}
// 拖动中
onMove(x, y) {
if (!this.isMousedown) {
return
@ -199,14 +196,12 @@ class Drag extends Base {
}
// 检测重叠节点
checkOverlapNode() {
if (!this.drawTransform) {
return
}
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
let checkRight = this.cloneNodeLeft + this.node.width * scaleX
let checkBottom = this.cloneNodeTop + this.node.height * scaleX
let x = this.mouseMoveX
let y = this.mouseMoveY
this.overlapNode = null
this.prevNode = null
this.nextNode = null
@ -221,35 +216,71 @@ class Drag extends Base {
if (this.overlapNode || (this.prevNode && this.nextNode)) {
return
}
let { left, top, width, height } = node
let _left = left
let _top = top
let _bottom = top + height
let right = (left + width) * scaleX + translateX
let bottom = (top + height) * scaleY + translateY
left = left * scaleX + translateX
top = top * scaleY + translateY
// 检测是否重叠
if (!this.overlapNode) {
if (
left <= checkRight &&
right >= this.cloneNodeLeft &&
top <= checkBottom &&
bottom >= this.cloneNodeTop
) {
this.overlapNode = node
let nodeRect = this.getNodeRect(node)
let oneFourthHeight = nodeRect.height / 4
// 前一个和后一个节点
let checkList = node.parent ? node.parent.children.filter((item) => {
return item !== this.node
}) : []
let index = checkList.findIndex((item) => {
return item === node
})
let prevBrother = null
let nextBrother = null
if (index !== -1) {
if (index - 1 >= 0) {
prevBrother = checkList[index - 1]
}
if (index + 1 <= checkList.length - 1) {
nextBrother = checkList[index + 1]
}
}
// 检测兄弟节点位置
if (!this.prevNode && !this.nextNode && !node.isRoot) {
// && this.node.isBrother(node)
if (left <= checkRight && right >= this.cloneNodeLeft) {
if (this.cloneNodeTop > bottom && this.cloneNodeTop <= bottom + 10) {
// 和前一个兄弟节点的距离
let prevBrotherOffset = 0
if (prevBrother) {
let prevNodeRect = this.getNodeRect(prevBrother)
prevBrotherOffset = nodeRect.top - prevNodeRect.bottom
// 间距小于10就当它不存在
prevBrotherOffset = prevBrotherOffset >= 10 ? prevBrotherOffset / 2 : 0
} else {
// 没有前一个兄弟节点那么假设和前一个节点的距离为20
prevBrotherOffset = 10
}
// 和后一个兄弟节点的距离
let nextBrotherOffset = 0
if (nextBrother) {
let nextNodeRect = this.getNodeRect(nextBrother)
nextBrotherOffset = nextNodeRect.top - nodeRect.bottom
nextBrotherOffset = nextBrotherOffset >= 10 ? nextBrotherOffset / 2 : 0
} else {
nextBrotherOffset = 10
}
if (nodeRect.left <= x && nodeRect.right >= x) {
// 检测兄弟节点位置
if (!this.overlapNode && !this.prevNode && !this.nextNode && !node.isRoot) {
let checkIsPrevNode = nextBrotherOffset > 0 ? // 距离下一个兄弟节点的距离大于0
y > nodeRect.bottom && y <= (nodeRect.bottom + nextBrotherOffset) : // 那么在当前节点外底部判断
y >= nodeRect.bottom - oneFourthHeight && y <= nodeRect.bottom // 否则在当前节点内底部1/4区间判断
let checkIsNextNode = prevBrotherOffset > 0 ? // 距离上一个兄弟节点的距离大于0
y < nodeRect.top && y >= (nodeRect.top - prevBrotherOffset) : // 那么在当前节点外底部判断
y >= nodeRect.top && y <= nodeRect.top + oneFourthHeight
if (checkIsPrevNode) {
this.prevNode = node
this.placeholder.size(node.width, 10).move(_left, _bottom)
} else if (checkBottom < top && checkBottom >= top - 10) {
let size = nextBrotherOffset > 0 ? nextBrotherOffset : 5
this.placeholder.size(node.width, size).move(nodeRect.originLeft, nodeRect.originBottom)
} else if (checkIsNextNode) {
this.nextNode = node
this.placeholder.size(node.width, 10).move(_left, _top - 10)
let size = prevBrotherOffset > 0 ? prevBrotherOffset : 5
this.placeholder.size(node.width, size).move(nodeRect.originLeft, nodeRect.originTop - size)
}
}
// 检测是否重叠
if (!this.overlapNode && !this.prevNode && !this.nextNode) {
if (
nodeRect.top + (prevBrotherOffset > 0 ? 0 : oneFourthHeight) <= y &&
nodeRect.bottom - (nextBrotherOffset > 0 ? 0 : oneFourthHeight) >= y
) {
this.overlapNode = node
}
}
}
@ -258,6 +289,30 @@ class Drag extends Base {
this.mindMap.renderer.setNodeActive(this.overlapNode, true)
}
}
// 计算节点的位置尺寸信息
getNodeRect(node) {
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
let { left, top, width, height } = node
let originLeft = left
let originTop = top
let originBottom = top + height
let right = (left + width) * scaleX + translateX
let bottom = (top + height) * scaleY + translateY
left = left * scaleX + translateX
top = top * scaleY + translateY
return {
width,
height,
left,
top,
right,
bottom,
originLeft,
originTop,
originBottom
}
}
}
Drag.instanceName = 'drag'

View File

@ -1,4 +1,5 @@
import EventEmitter from 'eventemitter3'
import { CONSTANTS } from './utils/constant'
// 事件类
class Event extends EventEmitter {
@ -26,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)
@ -34,10 +36,13 @@ class Event extends EventEmitter {
this.onContextmenu = this.onContextmenu.bind(this)
this.onSvgMousedown = this.onSvgMousedown.bind(this)
this.onKeyup = this.onKeyup.bind(this)
this.onMouseenter = this.onMouseenter.bind(this)
this.onMouseleave = this.onMouseleave.bind(this)
}
// 绑定事件
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)
@ -45,17 +50,22 @@ class Event extends EventEmitter {
window.addEventListener('mouseup', this.onMouseup)
this.mindMap.el.addEventListener('wheel', this.onMousewheel)
this.mindMap.svg.on('contextmenu', this.onContextmenu)
this.mindMap.svg.on('mouseenter', this.onMouseenter)
this.mindMap.svg.on('mouseleave', this.onMouseleave)
window.addEventListener('keyup', this.onKeyup)
}
// 解绑事件
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)
window.removeEventListener('mouseup', this.onMouseup)
this.mindMap.el.removeEventListener('wheel', this.onMousewheel)
this.mindMap.svg.off('contextmenu', this.onContextmenu)
this.mindMap.svg.off('mouseenter', this.onMouseenter)
this.mindMap.svg.off('mouseleave', this.onMouseleave)
window.removeEventListener('keyup', this.onKeyup)
}
@ -64,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)
@ -71,7 +86,6 @@ class Event extends EventEmitter {
// 鼠标按下事件
onMousedown(e) {
// e.preventDefault()
// 鼠标左键
if (e.which === 1) {
this.isLeftMousedown = true
@ -83,13 +97,13 @@ class Event extends EventEmitter {
// 鼠标移动事件
onMousemove(e) {
// e.preventDefault()
this.mousemovePos.x = e.clientX
this.mousemovePos.y = e.clientY
this.mousemoveOffset.x = e.clientX - this.mousedownPos.x
this.mousemoveOffset.y = e.clientY - this.mousedownPos.y
this.emit('mousemove', e, this)
if (this.isLeftMousedown) {
e.preventDefault()
this.emit('drag', e, this)
}
}
@ -107,14 +121,15 @@ class Event extends EventEmitter {
let dir
// 解决mac触控板双指缩放方向相反的问题
if (e.ctrlKey) {
if (e.deltaY > 0) dir = 'up'
if (e.deltaY < 0) dir = 'down'
if (e.deltaY > 0) dir = CONSTANTS.DIR.UP
if (e.deltaY < 0) dir = CONSTANTS.DIR.DOWN
if (e.deltaX > 0) dir = CONSTANTS.DIR.LEFT
if (e.deltaX < 0) dir = CONSTANTS.DIR.RIGHT
} else {
if ((e.wheelDeltaY || e.detail) > 0) {
dir = 'up'
} else {
dir = 'down'
}
if ((e.wheelDeltaY || e.detail) > 0) dir = CONSTANTS.DIR.UP
if ((e.wheelDeltaY || e.detail) < 0) dir = CONSTANTS.DIR.DOWN
if ((e.wheelDeltaX || e.detail) > 0) dir = CONSTANTS.DIR.LEFT
if ((e.wheelDeltaX || e.detail) < 0) dir = CONSTANTS.DIR.RIGHT
}
this.emit('mousewheel', e, dir, this)
}
@ -129,6 +144,16 @@ class Event extends EventEmitter {
onKeyup(e) {
this.emit('keyup', e)
}
// 进入
onMouseenter(e) {
this.emit('svg_mouseenter', e)
}
// 离开
onMouseleave(e) {
this.emit('svg_mouseleave', e)
}
}
export default Event

View File

@ -1,7 +1,8 @@
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'
import { transformToMarkdown } from './parse/toMarkdown'
const URL = window.URL || window.webkitURL || window
// 导出类
@ -26,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 => {
@ -36,22 +41,17 @@ class Export {
item.attr('href', imgData)
})
await Promise.all(task)
// 如果开启了富文本编辑需要把svg中的dom元素转换成图片
let nodeWithDomToImg = null
if (domToImage && this.mindMap.richText) {
let res = await this.mindMap.richText.handleSvgDomElements(svg)
nodeWithDomToImg = res.svg
svgHTML = res.svgHTML
if (imageList.length > 0) {
svgHTML = svg.svg()
}
return {
node: svg,
str: svgHTML,
nodeWithDomToImg
str: svgHTML
}
}
// svg转png
svgToPng(svgSrc) {
svgToPng(svgSrc, transparent) {
return new Promise((resolve, reject) => {
const img = new Image()
// 跨域图片需要添加这个属性,否则画布被污染了无法导出图片
@ -63,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,
@ -125,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
@ -176,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>`))
@ -221,20 +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文件
async md() {
let data = this.mindMap.getData()
let content = transformToMarkdown(data)
let blob = new Blob([content])
let res = await readBlob(blob)
return res
}
}

View File

@ -10,6 +10,7 @@ export default class KeyCommand {
}
this.shortcutMapCache = {}
this.isPause = false
this.isInSvg = false
this.bindEvent()
}
@ -37,8 +38,21 @@ export default class KeyCommand {
// 绑定事件
bindEvent() {
// 只有当鼠标在画布内才响应快捷键
this.mindMap.on('svg_mouseenter', () => {
this.isInSvg = true
})
this.mindMap.on('svg_mouseleave', () => {
if (this.mindMap.richText && this.mindMap.richText.showTextEdit) {
return
}
if (this.mindMap.renderer.textEdit.showTextEdit || (this.mindMap.associativeLine && this.mindMap.associativeLine.showTextEdit)) {
return
}
this.isInSvg = false
})
window.addEventListener('keydown', e => {
if (this.isPause) {
if (this.isPause || (this.mindMap.opt.enableShortcutOnlyWhenMouseInSvg && !this.isInSvg)) {
return
}
Object.keys(this.shortcutMap).forEach(key => {

View File

@ -1,5 +1,5 @@
import { isKey } from './utils/keyMap'
import { bfsWalk } from './utils'
import { CONSTANTS } from './utils/constant'
// 键盘导航类
class KeyboardNavigation {
@ -8,22 +8,29 @@ class KeyboardNavigation {
this.opt = opt
this.mindMap = opt.mindMap
this.onKeyup = this.onKeyup.bind(this)
this.mindMap.on('keyup', this.onKeyup)
this.mindMap.keyCommand.addShortcut(CONSTANTS.KEY_DIR.LEFT, () => {
this.onKeyup(CONSTANTS.KEY_DIR.LEFT)
})
this.mindMap.keyCommand.addShortcut(CONSTANTS.KEY_DIR.UP, () => {
this.onKeyup(CONSTANTS.KEY_DIR.UP)
})
this.mindMap.keyCommand.addShortcut(CONSTANTS.KEY_DIR.RIGHT, () => {
this.onKeyup(CONSTANTS.KEY_DIR.RIGHT)
})
this.mindMap.keyCommand.addShortcut(CONSTANTS.KEY_DIR.DOWN, () => {
this.onKeyup(CONSTANTS.KEY_DIR.DOWN)
})
}
// 处理按键事件
onKeyup(e) {
;['Left', 'Up', 'Right', 'Down'].forEach(dir => {
if (isKey(e, dir)) {
if (this.mindMap.renderer.activeNodeList.length > 0) {
this.focus(dir)
} else {
let root = this.mindMap.renderer.root
this.mindMap.renderer.moveNodeToCenter(root)
root.active()
}
}
})
onKeyup(dir) {
if (this.mindMap.renderer.activeNodeList.length > 0) {
this.focus(dir)
} else {
let root = this.mindMap.renderer.root
this.mindMap.renderer.moveNodeToCenter(root)
root.active()
}
}
// 聚焦到下一个节点
@ -95,19 +102,19 @@ class KeyboardNavigation {
let { left, top, right, bottom } = rect
let match = false
// 按下了左方向键
if (dir === 'Left') {
if (dir === CONSTANTS.KEY_DIR.LEFT) {
// 判断节点是否在当前节点的左侧
match = right <= currentActiveNodeRect.left
// 按下了右方向键
} else if (dir === 'Right') {
} else if (dir === CONSTANTS.KEY_DIR.RIGHT) {
// 判断节点是否在当前节点的右侧
match = left >= currentActiveNodeRect.right
// 按下了上方向键
} else if (dir === 'Up') {
} else if (dir === CONSTANTS.KEY_DIR.UP) {
// 判断节点是否在当前节点的上面
match = bottom <= currentActiveNodeRect.top
// 按下了下方向键
} else if (dir === 'Down') {
} else if (dir === CONSTANTS.KEY_DIR.DOWN) {
// 判断节点是否在当前节点的下面
match = top >= currentActiveNodeRect.bottom
}
@ -130,22 +137,22 @@ class KeyboardNavigation {
let rect = this.getNodeRect(node)
let { left, top, right, bottom } = rect
let match = false
if (dir === 'Left') {
if (dir === CONSTANTS.KEY_DIR.LEFT) {
match =
left < currentActiveNodeRect.left &&
top < currentActiveNodeRect.bottom &&
bottom > currentActiveNodeRect.top
} else if (dir === 'Right') {
} else if (dir === CONSTANTS.KEY_DIR.RIGHT) {
match =
right > currentActiveNodeRect.right &&
top < currentActiveNodeRect.bottom &&
bottom > currentActiveNodeRect.top
} else if (dir === 'Up') {
} else if (dir === CONSTANTS.KEY_DIR.UP) {
match =
top < currentActiveNodeRect.top &&
left < currentActiveNodeRect.right &&
right > currentActiveNodeRect.left
} else if (dir === 'Down') {
} else if (dir === CONSTANTS.KEY_DIR.DOWN) {
match =
bottom > currentActiveNodeRect.bottom &&
left < currentActiveNodeRect.right &&
@ -179,13 +186,13 @@ class KeyboardNavigation {
let offsetY = ccY - cY
if (offsetX === 0 && offsetY === 0) return
let match = false
if (dir === 'Left') {
if (dir === CONSTANTS.KEY_DIR.LEFT) {
match = offsetX <= 0 && offsetX <= offsetY && offsetX <= -offsetY
} else if (dir === 'Right') {
} else if (dir === CONSTANTS.KEY_DIR.RIGHT) {
match = offsetX > 0 && offsetX >= -offsetY && offsetX >= offsetY
} else if (dir === 'Up') {
} else if (dir === CONSTANTS.KEY_DIR.UP) {
match = offsetY <= 0 && offsetY < offsetX && offsetY < -offsetX
} else if (dir === 'Down') {
} else if (dir === CONSTANTS.KEY_DIR.DOWN) {
match = offsetY > 0 && -offsetY < offsetX && offsetY > offsetX
}
if (match) {

File diff suppressed because it is too large Load Diff

View File

@ -3,28 +3,36 @@ 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'
import { lineStyleProps } from './themes/default'
import { CONSTANTS } from './utils/constant'
// 布局列表
const layouts = {
// 逻辑结构图
logicalStructure: LogicalStructure,
[CONSTANTS.LAYOUT.LOGICAL_STRUCTURE]: LogicalStructure,
// 思维导图
mindMap: MindMap,
[CONSTANTS.LAYOUT.MIND_MAP]: MindMap,
// 目录组织图
catalogOrganization: CatalogOrganization,
[CONSTANTS.LAYOUT.CATALOG_ORGANIZATION]: CatalogOrganization,
// 组织结构图
organizationStructure: OrganizationStructure
[CONSTANTS.LAYOUT.ORGANIZATION_STRUCTURE]: OrganizationStructure,
// 时间轴
[CONSTANTS.LAYOUT.TIMELINE]: Timeline,
// 时间轴2
[CONSTANTS.LAYOUT.TIMELINE2]: Timeline,
// 鱼骨图
[CONSTANTS.LAYOUT.FISHBONE]: Fishbone,
}
// 渲染
class Render {
// 构造函数
constructor(opt = {}) {
this.opt = opt
this.mindMap = opt.mindMap
@ -34,6 +42,15 @@ class Render {
this.renderTree = merge({}, this.mindMap.opt.data || {})
// 是否重新渲染
this.reRender = false
// 是否正在渲染中
this.isRendering = false
// 是否存在等待渲染
this.hasWaitRendering = false
// 用于缓存节点
this.nodeCache = {}
this.lastNodeCache = {}
// 触发render的来源
this.renderSource = ''
// 当前激活的节点列表
this.activeNodeList = []
// 根节点
@ -51,17 +68,15 @@ class Render {
}
// 设置布局结构
setLayout() {
this.layout = new (
layouts[this.mindMap.opt.layout]
? layouts[this.mindMap.opt.layout]
: layouts.logicalStructure
)(this)
: layouts[CONSTANTS.LAYOUT.LOGICAL_STRUCTURE]
)(this, this.mindMap.opt.layout)
}
// 绑定事件
bindEvent() {
// 点击事件
this.mindMap.on('draw_click', () => {
@ -73,7 +88,6 @@ class Render {
}
// 注册命令
registerCommands() {
// 全选
this.selectAll = this.selectAll.bind(this)
@ -175,7 +189,6 @@ class Render {
}
// 注册快捷键
registerShortcutKeys() {
// 插入下级节点
this.mindMap.keyCommand.addShortcut('Tab', () => {
@ -220,7 +233,6 @@ class Render {
}
// 开启文字编辑,会禁用回车键和删除键相关快捷键防止冲突
startTextEdit() {
this.mindMap.keyCommand.save()
// this.mindMap.keyCommand.removeShortcut('Del|Backspace')
@ -229,7 +241,6 @@ class Render {
}
// 结束文字编辑,会恢复回车键和删除键相关快捷键
endTextEdit() {
this.mindMap.keyCommand.restore()
// this.mindMap.keyCommand.addShortcut('Del|Backspace', this.removeNodeWrap)
@ -238,23 +249,69 @@ class Render {
}
// 渲染
render(callback = () => {}) {
render(callback = () => {}, source) {
let t = Date.now()
// 如果当前还没有渲染完毕,不再触发渲染
if (this.isRendering) {
// 等待当前渲染完毕后再进行一次渲染
this.hasWaitRendering = true
return
}
this.isRendering = true
// 触发当前重新渲染的来源
this.renderSource = source
// 节点缓存
this.lastNodeCache = this.nodeCache
this.nodeCache = {}
// 重新渲染需要清除激活状态
if (this.reRender) {
this.clearActive()
}
// 计算布局
this.layout.doLayout(root => {
// 删除本次渲染时不再需要的节点
Object.keys(this.lastNodeCache).forEach((uid) => {
if (!this.nodeCache[uid]) {
this.lastNodeCache[uid].destroy()
if (this.lastNodeCache[uid].parent) {
this.lastNodeCache[uid].parent.removeLine()
}
}
})
// 更新根节点
this.root = root
this.root.render(() => {
// 渲染节点
const onEnd = () => {
this.isRendering = false
this.mindMap.emit('node_tree_render_end')
callback()
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()
}
})
})
this.mindMap.emit('node_active', null, this.activeNodeList)
}
// 清除当前激活的节点
clearActive() {
this.activeNodeList.forEach(item => {
this.setNodeActive(item, false)
@ -263,7 +320,6 @@ class Render {
}
// 清除当前所有激活节点,并会触发事件
clearAllActive() {
if (this.activeNodeList.length <= 0) {
return
@ -273,7 +329,6 @@ class Render {
}
// 添加节点到激活列表里
addActiveNode(node) {
let index = this.findActiveNodeIndex(node)
if (index === -1) {
@ -282,7 +337,6 @@ class Render {
}
// 在激活列表里移除某个节点
removeActiveNode(node) {
let index = this.findActiveNodeIndex(node)
if (index === -1) {
@ -292,7 +346,6 @@ class Render {
}
// 检索某个节点在激活列表里的索引
findActiveNodeIndex(node) {
return this.activeNodeList.findIndex(item => {
return item === node
@ -300,7 +353,6 @@ class Render {
}
// 获取节点在同级里的索引位置
getNodeIndex(node) {
return node.parent
? node.parent.children.findIndex(item => {
@ -310,7 +362,6 @@ class Render {
}
// 全选
selectAll() {
walk(
this.root,
@ -319,8 +370,10 @@ class Render {
if (!node.nodeData.data.isActive) {
node.nodeData.data.isActive = true
this.addActiveNode(node)
// 激活节点需要显示展开收起按钮
node.showExpandBtn()
setTimeout(() => {
node.renderNode()
node.updateNodeShape()
}, 0)
}
},
@ -332,47 +385,54 @@ class Render {
}
// 回退
back(step) {
this.clearAllActive()
let data = this.mindMap.command.back(step)
if (data) {
this.renderTree = data
this.mindMap.reRender()
this.mindMap.render()
}
}
// 前进
forward(step) {
this.clearAllActive()
let data = this.mindMap.command.forward(step)
if (data) {
this.renderTree = data
this.mindMap.reRender()
this.mindMap.render()
}
}
// 插入同级节点,多个节点只会操作第一个节点
// 规范指定节点数据
formatAppointNodes(appointNodes) {
if (!appointNodes) return []
return Array.isArray(appointNodes) ? appointNodes: [appointNodes]
}
insertNode() {
if (this.activeNodeList.length <= 0) {
// 插入同级节点,多个节点只会操作第一个节点
insertNode(openEdit = true, appointNodes = [], appointData = null) {
appointNodes = this.formatAppointNodes(appointNodes)
if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
return
}
let first = this.activeNodeList[0]
let { defaultInsertSecondLevelNodeText, defaultInsertBelowSecondLevelNodeText } = this.mindMap.opt
let list = appointNodes.length > 0 ? appointNodes : this.activeNodeList
let first = list[0]
if (first.isRoot) {
this.insertChildNode()
this.insertChildNode(openEdit, appointNodes, appointData)
} else {
let text = first.layerIndex === 1 ? '二级节点' : '分支主题'
let text = first.layerIndex === 1 ? defaultInsertSecondLevelNodeText : defaultInsertBelowSecondLevelNodeText
if (first.layerIndex === 1) {
first.parent.initRender = true
first.parent.destroy()
}
let index = this.getNodeIndex(first)
first.parent.nodeData.children.splice(index + 1, 0, {
inserting: true,
inserting: openEdit,
data: {
text: text,
expand: true
expand: true,
...(appointData || {})
},
children: []
})
@ -381,38 +441,37 @@ class Render {
}
// 插入子节点
insertChildNode() {
if (this.activeNodeList.length <= 0) {
insertChildNode(openEdit = true, appointNodes = [], appointData = null) {
appointNodes = this.formatAppointNodes(appointNodes)
if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
return
}
this.activeNodeList.forEach(node => {
let { defaultInsertSecondLevelNodeText, defaultInsertBelowSecondLevelNodeText } = this.mindMap.opt
let list = appointNodes.length > 0 ? appointNodes : this.activeNodeList
list.forEach(node => {
if (!node.nodeData.children) {
node.nodeData.children = []
}
let text = node.isRoot ? '二级节点' : '分支主题'
let text = node.isRoot ? defaultInsertSecondLevelNodeText : defaultInsertBelowSecondLevelNodeText
node.nodeData.children.push({
inserting: true,
inserting: openEdit,
data: {
text: text,
expand: true
expand: true,
...(appointData || {})
},
children: []
})
// 插入子节点时自动展开子节点
node.nodeData.data.expand = true
if (node.isRoot) {
node.initRender = true
// this.mindMap.batchExecution.push('renderNode' + index, () => {
// node.renderNode()
// })
node.destroy()
}
})
this.mindMap.render()
}
// 上移节点,多个节点只会操作第一个节点
upNode() {
if (this.activeNodeList.length <= 0) {
return
@ -440,7 +499,6 @@ class Render {
}
// 下移节点,多个节点只会操作第一个节点
downNode() {
if (this.activeNodeList.length <= 0) {
return
@ -468,7 +526,6 @@ class Render {
}
// 将节点移动到另一个节点的前面
insertBefore(node, exist) {
if (node.isRoot) {
return
@ -500,14 +557,12 @@ class Render {
existParent.nodeData.children.splice(existIndex, 0, node.nodeData)
this.mindMap.render(() => {
if (nodeLayerChanged) {
node.getSize()
node.renderNode()
node.reRender()
}
})
}
// 将节点移动到另一个节点的后面
insertAfter(node, exist) {
if (node.isRoot) {
return
@ -540,19 +595,20 @@ class Render {
existParent.nodeData.children.splice(existIndex, 0, node.nodeData)
this.mindMap.render(() => {
if (nodeLayerChanged) {
node.getSize()
node.renderNode()
node.reRender()
}
})
}
// 移除节点
removeNode() {
if (this.activeNodeList.length <= 0) {
removeNode(appointNodes = []) {
appointNodes = this.formatAppointNodes(appointNodes)
if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
return
}
let root = this.activeNodeList.find((node) => {
let isAppointNodes = appointNodes.length > 0
let list = isAppointNodes ? appointNodes : this.activeNodeList
let root = list.find((node) => {
return node.isRoot
})
if (root) {
@ -563,8 +619,9 @@ class Render {
root.children = []
root.nodeData.children = []
} else {
for (let i = 0; i < this.activeNodeList.length; i++) {
let node = this.activeNodeList[i]
for (let i = 0; i < list.length; i++) {
let node = list[i]
if (isAppointNodes) list.splice(i, 1)
if (node.isGeneralization) {
// 删除概要节点
this.setNodeData(node.generalizationBelongNode, {
@ -580,13 +637,11 @@ class Render {
}
}
}
this.activeNodeList = []
this.mindMap.emit('node_active', null, [])
this.mindMap.emit('node_active', null, this.activeNodeList)
this.mindMap.render()
}
// 移除某个指定节点
removeOneNode(node) {
let index = this.getNodeIndex(node)
node.remove()
@ -595,7 +650,6 @@ class Render {
}
// 复制节点,多个节点只会操作第一个节点
copyNode() {
if (this.activeNodeList.length <= 0) {
return
@ -604,7 +658,6 @@ class Render {
}
// 剪切节点,多个节点只会操作第一个节点
cutNode(callback) {
if (this.activeNodeList.length <= 0) {
return
@ -624,24 +677,22 @@ class Render {
}
// 移动一个节点作为另一个节点的子节点
moveNodeTo(node, toNode) {
if (node.isRoot) {
return
}
let copyData = copyNodeTree({}, node)
let copyData = copyNodeTree({}, node, false, true)
this.removeActiveNode(node)
this.removeOneNode(node)
this.mindMap.emit('node_active', null, this.activeNodeList)
toNode.nodeData.children.push(copyData)
this.mindMap.render()
if (toNode.isRoot) {
toNode.renderNode()
toNode.destroy()
}
}
// 粘贴节点到节点
pasteNode(data) {
if (this.activeNodeList.length <= 0) {
return
@ -653,7 +704,6 @@ class Render {
}
// 设置节点样式
setNodeStyle(node, prop, value, isActive) {
let data = {}
if (isActive) {
@ -670,12 +720,14 @@ class Render {
}
// 如果开启了富文本,则需要应用到富文本上
if (this.mindMap.richText) {
this.mindMap.richText.showEditText(node)
let config = this.mindMap.richText.normalStyleToRichTextStyle({
[prop]: value
})
this.mindMap.richText.formatAllText(config)
this.mindMap.richText.hideEditText()
if (Object.keys(config).length > 0) {
this.mindMap.richText.showEditText(node)
this.mindMap.richText.formatAllText(config)
this.mindMap.richText.hideEditText([node])
}
}
this.setNodeDataRender(node, data)
// 更新了连线的样式
@ -685,16 +737,20 @@ class Render {
}
// 设置节点是否激活
setNodeActive(node, active) {
this.setNodeData(node, {
isActive: active
})
node.renderNode()
// 切换激活状态,需要切换展开收起按钮的显隐
if (active) {
node.showExpandBtn()
} else {
node.hideExpandBtn()
}
node.updateNodeShape()
}
// 设置节点是否展开
setNodeExpand(node, expand) {
this.setNodeData(node, {
expand
@ -705,20 +761,19 @@ 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()
}
// 展开所有
expandAllNode() {
walk(
this.renderTree,
@ -733,11 +788,10 @@ class Render {
0,
0
)
this.mindMap.reRender()
this.mindMap.render()
}
// 收起所有
unexpandAllNode() {
walk(
this.renderTree,
@ -753,11 +807,12 @@ class Render {
0,
0
)
this.mindMap.reRender()
this.mindMap.render(() => {
this.mindMap.view.reset()
})
}
// 展开到指定层级
expandToLevel(level) {
walk(
this.renderTree,
@ -771,11 +826,10 @@ class Render {
0,
0
)
this.mindMap.reRender()
this.mindMap.render()
}
// 切换激活节点的展开状态
toggleActiveExpand() {
this.activeNodeList.forEach(node => {
if (node.nodeData.children.length <= 0) {
@ -786,7 +840,6 @@ class Render {
}
// 切换节点展开状态
toggleNodeExpand(node) {
this.mindMap.execCommand(
'SET_NODE_EXPAND',
@ -796,7 +849,6 @@ class Render {
}
// 设置节点文本
setNodeText(node, text, richText) {
this.setNodeDataRender(node, {
text,
@ -805,7 +857,6 @@ class Render {
}
// 设置节点图片
setNodeImage(node, { url, title, width, height }) {
this.setNodeDataRender(node, {
image: url,
@ -818,7 +869,6 @@ class Render {
}
// 设置节点图标
setNodeIcon(node, icons) {
this.setNodeDataRender(node, {
icon: icons
@ -826,7 +876,6 @@ class Render {
}
// 设置节点超链接
setNodeHyperlink(node, link, title = '') {
this.setNodeDataRender(node, {
hyperlink: link,
@ -835,7 +884,6 @@ class Render {
}
// 设置节点备注
setNodeNote(node, note) {
this.setNodeDataRender(node, {
note
@ -843,7 +891,6 @@ class Render {
}
// 设置节点标签
setNodeTag(node, tag) {
this.setNodeDataRender(node, {
tag
@ -851,7 +898,6 @@ class Render {
}
// 添加节点概要
addGeneralization(data) {
if (this.activeNodeList.length <= 0) {
return
@ -871,7 +917,6 @@ class Render {
}
// 删除节点概要
removeGeneralization() {
if (this.activeNodeList.length <= 0) {
return
@ -889,7 +934,6 @@ class Render {
}
// 设置节点自定义位置
setNodeCustomPosition(node, left = undefined, top = undefined) {
let nodeList = [node] || this.activeNodeList
nodeList.forEach(item => {
@ -901,7 +945,6 @@ class Render {
}
// 一键整理布局,即去除自定义位置
resetLayout() {
walk(
this.root,
@ -923,7 +966,6 @@ class Render {
}
// 设置节点形状
setNodeShape(node, shape) {
if (!shape || !shapeList.includes(shape)) {
return
@ -935,7 +977,6 @@ class Render {
}
// 更新节点数据
setNodeData(node, data) {
Object.keys(data).forEach(key => {
node.nodeData.data[key] = data[key]
@ -943,11 +984,9 @@ class Render {
}
// 设置节点数据,并判断是否渲染
setNodeDataRender(node, data) {
this.setNodeData(node, data)
let changed = node.getSize()
node.renderNode()
let changed = node.reRender()
if (changed) {
if (node.isGeneralization) {
// 概要节点
@ -958,7 +997,6 @@ class Render {
}
// 移动节点到画布中心
moveNodeToCenter(node) {
let halfWidth = this.mindMap.width / 2
let halfHeight = this.mindMap.height / 2

View File

@ -1,9 +1,8 @@
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
@ -40,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)
}
// 处理选项参数
@ -95,42 +135,56 @@ 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;background-color:#fff;box-shadow: 0 0 20px rgba(0,0,0,.5);outline: none; word-break: break-all;`
document.body.appendChild(this.textEditNode)
}
// 原始宽高
let g = node._textData.node
let originWidth = g.attr('data-width')
let originHeight = g.attr('data-height')
this.textEditNode.style.minWidth = originWidth + 'px'
// 缩放值
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 + 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'
this.showTextEdit = true
this.selectAll()
this.focus()
if (!node.nodeData.data.richText) {
// 如果是非富文本的情况,需要手动应用文本样式
this.setTextStyleIfNotRichText(node)
}
this.cacheEditingText = ''
}
// 如果是非富文本的情况,需要手动应用文本样式
@ -144,18 +198,24 @@ class RichText {
underline: node.style.merge('textDecoration') === 'underline',
strike: node.style.merge('textDecoration') === 'line-through'
}
this.formatText(style)
this.formatAllText(style)
}
// 获取当前正在编辑的内容
getEditText() {
let html = this.quill.container.firstChild.innerHTML
// 去除最后的空行
return html.replace(/<p><br><\/p>$/, '')
}
// 隐藏文本编辑控件,即完成编辑
hideEditText() {
hideEditText(nodes) {
if (!this.showTextEdit) {
return
}
let html = this.quill.container.firstChild.innerHTML
// 去除最后的空行
html = html.replace(/<p><br><\/p>$/, '')
this.mindMap.renderer.activeNodeList.forEach(node => {
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)
if (node.isGeneralization) {
// 概要节点
@ -166,7 +226,7 @@ class RichText {
this.mindMap.emit(
'hide_text_edit',
this.textEditNode,
this.mindMap.renderer.activeNodeList
list
)
this.textEditNode.style.display = 'none'
this.showTextEdit = false
@ -228,6 +288,12 @@ class RichText {
this.quill.setSelection(0, this.quill.getLength())
}
// 聚焦
focus() {
let len = this.quill.getLength()
this.quill.setSelection(len, len)
}
// 格式化当前选中的文本
formatText(config = {}, clear = false) {
if (!this.range && !this.lastRange) return
@ -338,90 +404,41 @@ 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()
}
}
// 将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)
}
// 处理导出为图片
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)
})
}
if (len > 0) transform()
}
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
}
},
null,
@ -429,12 +446,33 @@ class RichText {
0,
0
)
this.mindMap.reRender()
// 清空历史数据,并且触发数据变化
this.mindMap.command.clearHistory()
this.mindMap.command.addHistory()
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)
}
}

View File

@ -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)
})
// })
}
})
}

View File

@ -1,3 +1,6 @@
import { Rect, Polygon, Path } from '@svgdotjs/svg.js'
import { CONSTANTS } from './utils/constant'
// 节点形状类
export default class Shape {
constructor(node) {
@ -13,37 +16,37 @@ export default class Shape {
const actHeight = height + paddingY * 2
const actOffset = Math.abs(actWidth - actHeight)
switch (shape) {
case 'roundedRectangle':
case CONSTANTS.SHAPE.ROUNDED_RECTANGLE:
return {
paddingX: height > width ? (height - width) / 2 : 0,
paddingY: 0
}
case 'diamond':
case CONSTANTS.SHAPE.DIAMOND:
return {
paddingX: width / 2,
paddingY: height / 2
}
case 'parallelogram':
case CONSTANTS.SHAPE.PARALLELOGRAM:
return {
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
paddingY: 0
}
case 'outerTriangularRectangle':
case CONSTANTS.SHAPE.OUTER_TRIANGULAR_RECTANGLE:
return {
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
paddingY: 0
}
case 'innerTriangularRectangle':
case CONSTANTS.SHAPE.INNER_TRIANGULAR_RECTANGLE:
return {
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
paddingY: 0
}
case 'ellipse':
case CONSTANTS.SHAPE.ELLIPSE:
return {
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
paddingY: paddingY <= 0 ? defaultPaddingY : 0
}
case 'circle':
case CONSTANTS.SHAPE.CIRCLE:
return {
paddingX: actHeight > actWidth ? actOffset / 2 : 0,
paddingY: actHeight < actWidth ? actOffset / 2 : 0
@ -62,30 +65,30 @@ export default class Shape {
let { width, height } = this.node
let node = null
// 矩形
if (shape === 'rectangle') {
node = this.node.group.rect(width, height)
} else if (shape === 'diamond') {
if (shape === CONSTANTS.SHAPE.RECTANGLE) {
node = new Rect().size(width, height)
} else if (shape === CONSTANTS.SHAPE.DIAMOND) {
// 菱形
node = this.createDiamond()
} else if (shape === 'parallelogram') {
} else if (shape === CONSTANTS.SHAPE.PARALLELOGRAM) {
// 平行四边形
node = this.createParallelogram()
} else if (shape === 'roundedRectangle') {
} else if (shape === CONSTANTS.SHAPE.ROUNDED_RECTANGLE) {
// 圆角矩形
node = this.createRoundedRectangle()
} else if (shape === 'octagonalRectangle') {
} else if (shape === CONSTANTS.SHAPE.OCTAGONAL_RECTANGLE) {
// 八角矩形
node = this.createOctagonalRectangle()
} else if (shape === 'outerTriangularRectangle') {
} else if (shape === CONSTANTS.SHAPE.OUTER_TRIANGULAR_RECTANGLE) {
// 外三角矩形
node = this.createOuterTriangularRectangle()
} else if (shape === 'innerTriangularRectangle') {
} else if (shape === CONSTANTS.SHAPE.INNER_TRIANGULAR_RECTANGLE) {
// 内三角矩形
node = this.createInnerTriangularRectangle()
} else if (shape === 'ellipse') {
} else if (shape === CONSTANTS.SHAPE.ELLIPSE) {
// 椭圆
node = this.createEllipse()
} else if (shape === 'circle') {
} else if (shape === CONSTANTS.SHAPE.CIRCLE) {
// 圆
node = this.createCircle()
}
@ -105,12 +108,12 @@ export default class Shape {
let bottomY = height
let leftX = 0
let leftY = halfHeight
return this.node.group.polygon(`
${topX}, ${topY}
${rightX}, ${rightY}
${bottomX}, ${bottomY}
${leftX}, ${leftY}
`)
return new Polygon().plot([
[topX, topY],
[rightX, rightY],
[bottomX, bottomY],
[leftX, leftY]
])
}
// 创建平行四边形
@ -118,41 +121,41 @@ export default class Shape {
let { paddingX } = this.node.getPaddingVale()
paddingX = paddingX || this.node.shapePadding.paddingX
let { width, height } = this.node
return this.node.group.polygon(`
${paddingX}, ${0}
${width}, ${0}
${width - paddingX}, ${height}
${0}, ${height}
`)
return new Polygon().plot([
[paddingX, 0],
[width, 0],
[width - paddingX, height],
[0, height]
])
}
// 创建圆角矩形
createRoundedRectangle() {
let { width, height } = this.node
let halfHeight = height / 2
return this.node.group.path(`
M${halfHeight},0
L${width - halfHeight},0
A${height / 2},${height / 2} 0 0,1 ${width - halfHeight},${height}
L${halfHeight},${height}
A${height / 2},${height / 2} 0 0,1 ${halfHeight},${0}
`)
return new Path().plot(`
M${halfHeight},0
L${width - halfHeight},0
A${height / 2},${height / 2} 0 0,1 ${width - halfHeight},${height}
L${halfHeight},${height}
A${height / 2},${height / 2} 0 0,1 ${halfHeight},${0}
`)
}
// 创建八角矩形
createOctagonalRectangle() {
let w = 5
let { width, height } = this.node
return this.node.group.polygon(`
${0}, ${w}
${w}, ${0}
${width - w}, ${0}
${width}, ${w}
${width}, ${height - w}
${width - w}, ${height}
${w}, ${height}
${0}, ${height - w}
`)
return new Polygon().plot([
[0, w],
[w, 0],
[width - w, 0],
[width, w],
[width, height - w],
[width - w, height],
[w, height],
[0, height - w]
])
}
// 创建外三角矩形
@ -160,14 +163,14 @@ export default class Shape {
let { paddingX } = this.node.getPaddingVale()
paddingX = paddingX || this.node.shapePadding.paddingX
let { width, height } = this.node
return this.node.group.polygon(`
${paddingX}, ${0}
${width - paddingX}, ${0}
${width}, ${height / 2}
${width - paddingX}, ${height}
${paddingX}, ${height}
${0}, ${height / 2}
`)
return new Polygon().plot([
[paddingX, 0],
[width - paddingX, 0],
[width, height / 2],
[width - paddingX, height],
[paddingX, height],
[0, height / 2]
])
}
// 创建内三角矩形
@ -175,14 +178,14 @@ export default class Shape {
let { paddingX } = this.node.getPaddingVale()
paddingX = paddingX || this.node.shapePadding.paddingX
let { width, height } = this.node
return this.node.group.polygon(`
${0}, ${0}
${width}, ${0}
${width - paddingX / 2}, ${height / 2}
${width}, ${height}
${0}, ${height}
${paddingX / 2}, ${height / 2}
`)
return new Polygon().plot([
[0, 0],
[width, 0],
[width - paddingX / 2, height / 2],
[width, height],
[0, height],
[paddingX / 2, height / 2]
])
}
// 创建椭圆
@ -190,12 +193,12 @@ export default class Shape {
let { width, height } = this.node
let halfWidth = width / 2
let halfHeight = height / 2
return this.node.group.path(`
M${halfWidth},0
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${height}
M${halfWidth},${height}
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${0}
`)
return new Path().plot(`
M${halfWidth},0
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${height}
M${halfWidth},${height}
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${0}
`)
}
// 创建圆
@ -203,24 +206,24 @@ export default class Shape {
let { width, height } = this.node
let halfWidth = width / 2
let halfHeight = height / 2
return this.node.group.path(`
M${halfWidth},0
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${height}
M${halfWidth},${height}
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${0}
`)
return new Path().plot(`
M${halfWidth},0
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${height}
M${halfWidth},${height}
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${0}
`)
}
}
// 形状列表
export const shapeList = [
'rectangle',
'diamond',
'parallelogram',
'roundedRectangle',
'octagonalRectangle',
'outerTriangularRectangle',
'innerTriangularRectangle',
'ellipse',
'circle'
CONSTANTS.SHAPE.RECTANGLE,
CONSTANTS.SHAPE.DIAMOND,
CONSTANTS.SHAPE.PARALLELOGRAM,
CONSTANTS.SHAPE.ROUNDED_RECTANGLE,
CONSTANTS.SHAPE.OCTAGONAL_RECTANGLE,
CONSTANTS.SHAPE.OUTER_TRIANGULAR_RECTANGLE,
CONSTANTS.SHAPE.INNER_TRIANGULAR_RECTANGLE,
CONSTANTS.SHAPE.ELLIPSE,
CONSTANTS.SHAPE.CIRCLE
]

View File

@ -2,10 +2,8 @@ import { tagColorList } from './utils/constant'
const rootProp = ['paddingX', 'paddingY']
// 样式类
class Style {
// 设置背景样式
static setBackgroundStyle(el, themeConfig) {
let { backgroundColor, backgroundImage, backgroundRepeat, backgroundPosition, backgroundSize } = themeConfig
el.style.backgroundColor = backgroundColor
@ -20,35 +18,27 @@ class Style {
}
// 构造函数
constructor(ctx, themeConfig) {
constructor(ctx) {
this.ctx = ctx
this.themeConfig = themeConfig
}
// 更新主题配置
updateThemeConfig(themeConfig) {
this.themeConfig = themeConfig
}
// 合并样式
merge(prop, root, isActive) {
let themeConfig = this.ctx.mindMap.themeConfig
// 三级及以下节点
let defaultConfig = this.themeConfig.node
let defaultConfig = themeConfig.node
if (root || rootProp.includes(prop)) {
// 直接使用最外层样式
defaultConfig = this.themeConfig
defaultConfig = themeConfig
} else if (this.ctx.isGeneralization) {
// 概要节点
defaultConfig = this.themeConfig.generalization
defaultConfig = themeConfig.generalization
} else if (this.ctx.layerIndex === 0) {
// 根节点
defaultConfig = this.themeConfig.root
defaultConfig = themeConfig.root
} else if (this.ctx.layerIndex === 1) {
// 二级节点
defaultConfig = this.themeConfig.second
defaultConfig = themeConfig.second
}
// 激活状态
if (isActive !== undefined ? isActive : this.ctx.nodeData.data.isActive) {
@ -68,39 +58,35 @@ class Style {
}
// 获取某个样式值
getStyle(prop, root, isActive) {
return this.merge(prop, root, isActive)
}
// 获取自身自定义样式
getSelfStyle(prop) {
return this.ctx.nodeData.data[prop]
}
// 矩形
rect(node) {
this.shape(node)
node.radius(this.merge('borderRadius'))
}
// 矩形外的其他形状
shape(node) {
node.fill({
color: this.merge('fillColor')
})
// 节点使用横线样式,不需要渲染非激活状态的边框样式
if (
!this.ctx.isRoot &&
!this.ctx.isGeneralization &&
this.themeConfig.nodeUseLineStyle &&
!this.ctx.nodeData.data.isActive
) {
return
}
// if (
// !this.ctx.isRoot &&
// !this.ctx.isGeneralization &&
// this.ctx.mindMap.themeConfig.nodeUseLineStyle &&
// !this.ctx.nodeData.data.isActive
// ) {
// return
// }
node.stroke({
color: this.merge('borderColor'),
width: this.merge('borderWidth'),
@ -109,7 +95,6 @@ class Style {
}
// 文字
text(node) {
node
.fill({
@ -124,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 {
@ -135,17 +132,15 @@ class Style {
}
// html文字节点
domText(node, fontSizeScale = 1) {
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 = this.merge('lineHeight')
node.style.lineHeight = !isMultiLine ? 'normal' : this.merge('lineHeight')
node.style.fontStyle = this.merge('fontStyle')
}
// 标签文字
tagText(node, index) {
node
.fill({
@ -157,7 +152,6 @@ class Style {
}
// 标签矩形
tagRect(node, index) {
node.fill({
color: tagColorList[index].background
@ -165,7 +159,6 @@ class Style {
}
// 内置图标
iconNode(node) {
node.attr({
fill: this.merge('color')
@ -173,13 +166,11 @@ class Style {
}
// 连线
line(node, { width, color, dasharray } = {}) {
node.stroke({ width, color, dasharray }).fill({ color: 'none' })
}
// 概要连线
generalizationLine(node) {
node
.stroke({
@ -189,11 +180,15 @@ class Style {
.fill({ color: 'none' })
}
// 按钮
iconBtn(node, fillNode) {
node.fill({ color: '#808080' })
fillNode.fill({ color: '#fff' })
// 展开收起按钮
iconBtn(node, node2, fillNode) {
let { color, fill } = this.ctx.mindMap.opt.expandBtnStyle || {
color: '#808080',
fill: '#fff'
}
node.fill({ color: color })
node2.fill({ color: color })
fillNode.fill({ color: fill })
}
}

View File

@ -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,16 @@ 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()
})
// 展开收缩按钮点击事件
this.mindMap.on('expand_btn_click', () => {
this.hideEditTextBox()
@ -38,6 +53,7 @@ export default class TextEdit {
}
this.show(this.renderer.activeNodeList[0])
})
this.mindMap.on('scale', this.onScale)
}
// 注册临时快捷键
@ -50,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)
@ -58,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')
@ -69,25 +101,34 @@ 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')
node.style.domText(this.textEditNode, scale)
this.textEditNode.innerHTML = node.nodeData.data.text
.split(/\n/gim)
.join('<br>')
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'
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 * scale + 'px'
this.textEditNode.style.transform = `translateY(${-(lineHeight * fontSize - fontSize) / 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 = ''
}
// 选中文本
@ -99,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()
}
@ -108,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) {
// 概要节点
@ -126,6 +173,7 @@ export default class TextEdit {
this.textEditNode.style.fontFamily = 'inherit'
this.textEditNode.style.fontSize = 'inherit'
this.textEditNode.style.fontWeight = 'normal'
this.textEditNode.style.transform = 'translateY(0)'
this.showTextEdit = false
}
}

View File

@ -1,3 +1,5 @@
import { CONSTANTS } from './utils/constant'
// 视图操作类
class View {
// 构造函数
@ -55,12 +57,41 @@ class View {
})
// 放大缩小视图
this.mindMap.event.on('mousewheel', (e, dir) => {
// // 放大
if (dir === 'down') {
this.enlarge()
if (this.mindMap.opt.customHandleMousewheel && typeof this.mindMap.opt.customHandleMousewheel === 'function') {
return this.mindMap.opt.customHandleMousewheel(e)
}
if (this.mindMap.opt.mousewheelAction === CONSTANTS.MOUSE_WHEEL_ACTION.ZOOM) {
switch (dir) {
// 鼠标滚轮,向上和向左,都是缩小
case CONSTANTS.DIR.UP:
case CONSTANTS.DIR.LEFT:
this.narrow()
break
// 鼠标滚轮,向下和向右,都是放大
case CONSTANTS.DIR.DOWN:
case CONSTANTS.DIR.RIGHT:
this.enlarge()
break
}
} else {
// 缩小
this.narrow()
switch (dir){
// 上移
case CONSTANTS.DIR.DOWN:
this.translateY(-this.mindMap.opt.mousewheelMoveStep)
break
// 下移
case CONSTANTS.DIR.UP:
this.translateY(this.mindMap.opt.mousewheelMoveStep)
break
// 右移
case CONSTANTS.DIR.LEFT:
this.translateX(-this.mindMap.opt.mousewheelMoveStep)
break
// 左移
case CONSTANTS.DIR.RIGHT:
this.translateX(this.mindMap.opt.mousewheelMoveStep)
break
}
}
})
}
@ -93,6 +124,13 @@ class View {
}
}
// 平移x,y方向
translateXY(x, y) {
this.x += x
this.y += y
this.transform()
}
// 平移x方向
translateX(step) {
this.x += step
@ -129,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)
}
}
// 缩小

View File

@ -1,10 +0,0 @@
.ql-editor {
overflow: hidden;
padding: 0;
height: auto;
}
.ql-container {
height: auto;
font-size: inherit;
}

View File

@ -1,4 +1,6 @@
import Node from '../Node'
import { CONSTANTS, initRootNodePositionMap } from '../utils/constant'
import Lru from '../utils/Lru'
// 布局基类
class Base {
@ -12,6 +14,7 @@ class Base {
this.draw = this.mindMap.draw
// 根节点
this.root = null
this.lru = new Lru(this.mindMap.opt.maxNodeCacheCount)
}
// 计算节点位置
@ -32,26 +35,66 @@ class Base {
// 概要节点
renderGeneralization() {}
// 通过uid缓存节点
cacheNode(uid, node) {
// 记录本次渲染时的节点
this.renderer.nodeCache[uid] = node
// 记录所有渲染时的节点
this.lru.add(uid, node)
}
// 检查当前来源是否需要重新计算节点大小
checkIsNeedResizeSources() {
return [CONSTANTS.CHANGE_THEME, CONSTANTS.TRANSFORM_TO_NORMAL_NODE].includes(this.renderer.renderSource)
}
// 创建节点实例
createNode(data, parent, isRoot, layerIndex) {
// 创建节点
let newNode = null
// 复用节点
// 数据上保存了节点引用,那么直接复用节点
if (data && data._node && !this.renderer.reRender) {
newNode = data._node
newNode.reset()
newNode.layerIndex = layerIndex
this.cacheNode(data._node.uid, newNode)
// 主题或主题配置改变了需要重新计算节点大小和布局
if (this.checkIsNeedResizeSources()) {
newNode.getSize()
newNode.needLayout = true
}
} else if (this.lru.has(data.data.uid) && !this.renderer.reRender) {
// 数据上没有保存节点引用但是通过uid找到了缓存的节点也可以复用
newNode = this.lru.get(data.data.uid)
// 保存该节点上一次的数据
let lastData = JSON.stringify(newNode.nodeData.data)
newNode.reset()
newNode.nodeData = newNode.handleData(data || {})
newNode.layerIndex = layerIndex
this.cacheNode(data.data.uid, newNode)
data._node = newNode
// 主题或主题配置改变了需要重新计算节点大小和布局
let isResizeSource = this.checkIsNeedResizeSources()
// 节点数据改变了需要重新计算节点大小和布局
let isNodeDataChange = lastData !== JSON.stringify(data.data)
if (isResizeSource || isNodeDataChange) {
newNode.getSize()
newNode.needLayout = true
}
} else {
// 创建新节点
let uid = this.mindMap.uid++
newNode = new Node({
data,
uid: this.mindMap.uid++,
uid,
renderer: this.renderer,
mindMap: this.mindMap,
draw: this.draw,
layerIndex
})
newNode.getSize()
// uid保存到数据上为了节点复用
data.data.uid = uid
this.cacheNode(uid, newNode)
// 数据关联实际节点
data._node = newNode
if (data.data.isActive) {
@ -70,10 +113,28 @@ class Base {
return newNode
}
// 格式化节点位置
formatPosition(value, size, nodeSize) {
if (typeof value === 'number') {
return value
} else if (initRootNodePositionMap[value] !== undefined) {
return size * initRootNodePositionMap[value]
} else if (/^\d\d*%$/.test(value)) {
return Number.parseFloat(value) / 100 * size
} else {
return (size - nodeSize) / 2
}
}
// 定位节点到画布中间
setNodeCenter(node) {
node.left = (this.mindMap.width - node.width) / 2
node.top = (this.mindMap.height - node.height) / 2
let { initRootNodePosition } = this.mindMap.opt
let { CENTER }= CONSTANTS.INIT_ROOT_NODE_POSITION
if (!initRootNodePosition || !Array.isArray(initRootNodePosition) || initRootNodePosition.length < 2) {
initRootNodePosition = [CENTER, CENTER]
}
node.left = this.formatPosition(initRootNodePosition[0], this.mindMap.width, node.width)
node.top = this.formatPosition(initRootNodePosition[1], this.mindMap.height, node.height)
}
// 更新子节点属性
@ -87,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
@ -196,6 +288,11 @@ class Base {
generalizationNodeMargin
}
}
// 获取节点实际存在几个子节点
getNodeActChildrenLength(node) {
return node.nodeData.children && node.nodeData.children.length
}
}
export default Base

View File

@ -1,386 +1,354 @@
import Base from './Base'
import { walk, asyncRun } from '../utils'
// 目录组织图
class CatalogOrganization extends Base {
// 构造函数
constructor(opt = {}) {
super(opt)
}
// 布局
doLayout(callback) {
let task = [
() => {
this.computedBaseValue()
},
() => {
this.computedLeftTopValue()
},
() => {
this.adjustLeftTopValue()
},
() => {
callback(this.root)
}
]
asyncRun(task)
}
// 遍历数据计算节点的left、width、height
computedBaseValue() {
walk(
this.renderer.renderTree,
null,
(cur, parent, isRoot, layerIndex) => {
let newNode = this.createNode(cur, parent, isRoot, layerIndex)
// 根节点定位在画布中心位置
if (isRoot) {
this.setNodeCenter(newNode)
} else {
// 非根节点
if (parent._node.isRoot) {
newNode.top =
parent._node.top +
parent._node.height +
this.getMarginX(layerIndex)
}
}
if (!cur.data.expand) {
return true
}
},
(cur, parent, isRoot, layerIndex) => {
if (isRoot) {
let len = cur.data.expand === false ? 0 : cur._node.children.length
cur._node.childrenAreaWidth = len
? cur._node.children.reduce((h, item) => {
return h + item.width
}, 0) +
(len + 1) * this.getMarginX(layerIndex + 1)
: 0
}
},
true,
0
)
}
// 遍历节点树计算节点的left、top
computedLeftTopValue() {
walk(
this.root,
null,
(node, parent, isRoot, layerIndex) => {
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 / 2 - node.childrenAreaWidth / 2
let totalLeft = left + marginX
node.children.forEach(cur => {
cur.left = totalLeft
totalLeft += cur.width + marginX
})
} else {
let totalTop = node.top + node.height + marginY + node.expandBtnSize
node.children.forEach(cur => {
cur.left = node.left + node.width * 0.5
cur.top = totalTop
totalTop += cur.height + marginY + node.expandBtnSize
})
}
}
},
null,
true
)
}
// 调整节点left、top
adjustLeftTopValue() {
walk(
this.root,
null,
(node, parent, isRoot, layerIndex) => {
if (!node.nodeData.data.expand) {
return
}
// 调整left
if (parent && parent.isRoot) {
let areaWidth = this.getNodeAreaWidth(node)
let difference = areaWidth - node.width
if (difference > 0) {
this.updateBrothersLeft(node, difference / 2)
}
}
// 调整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
}, 0) +
(len + 1) * marginY +
len * node.expandBtnSize
this.updateBrothersTop(node, totalHeight)
}
},
null,
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) {
let childrenList = node.parent.children
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()) {
// 适配自定义位置
return
}
let _offset = 0
if (_index < index) {
// 左边的节点往左移
_offset = -addWidth
} else if (_index > index) {
// 右边的节点往右移
_offset = addWidth
}
item.left += _offset
// 同步更新子节点的位置
if (item.children && item.children.length) {
this.updateChildren(item.children, 'left', _offset)
}
})
// 更新父节点的位置
this.updateBrothersLeft(node.parent, addWidth)
}
}
// 调整兄弟节点的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
let marginX = this.getMarginX(node.layerIndex + 1)
if (node.isRoot) {
// 根节点
let x1 = left + width / 2
let y1 = top + height
let s1 = marginX * 0.7
let minx = Infinity
let maxx = -Infinity
node.children.forEach((item, index) => {
let x2 = item.left + item.width / 2
let y2 = item.top
if (x2 < minx) {
minx = x2
}
if (x2 > maxx) {
maxx = x2
}
// 节点使用横线风格,需要额外渲染横线
let nodeUseLineStylePath = this.mindMap.themeConfig.nodeUseLineStyle
? ` L ${item.left},${y2} L ${item.left + item.width},${y2}`
: ''
let path =
`M ${x2},${y1 + s1} L ${x2},${y1 + s1 > y2 ? y2 + item.height : y2}` +
nodeUseLineStylePath
// 竖线
lines[index].plot(path)
style && style(lines[index], item)
})
minx = Math.min(minx, x1)
maxx = Math.max(maxx, x1)
// 父节点的竖线
let line1 = this.draw.path()
node.style.line(line1)
line1.plot(`M ${x1},${y1} L ${x1},${y1 + s1}`)
node._lines.push(line1)
style && style(line1, node)
// 水平线
if (len > 0) {
let lin2 = this.draw.path()
node.style.line(lin2)
lin2.plot(`M ${minx},${y1 + s1} L ${maxx},${y1 + s1}`)
node._lines.push(lin2)
style && style(lin2, node)
}
} else {
// 非根节点
let y1 = top + height
let maxy = -Infinity
let x2 = node.left + node.width * 0.3
node.children.forEach((item, index) => {
// 为了适配自定义位置,下面做了各种位置的兼容
let y2 = item.top + item.height / 2
if (y2 > maxy) {
maxy = y2
}
// 水平线
let path = ''
let _left = item.left
let _isLeft = item.left + item.width < x2
let _isXCenter = false
if (_isLeft) {
// 水平位置在父节点左边
_left = item.left + item.width
} else if (item.left < x2 && item.left + item.width > x2) {
// 水平位置在父节点之间
_isXCenter = true
y2 = item.top
maxy = y2
}
if (y2 > top && y2 < y1) {
// 自定义位置的情况:垂直位置节点在父节点之间
path = `M ${
_isLeft ? node.left : node.left + node.width
},${y2} L ${_left},${y2}`
} else if (y2 < y1) {
// 自定义位置的情况:垂直位置节点在父节点上面
if (_isXCenter) {
y2 = item.top + item.height
_left = x2
}
path = `M ${x2},${top} L ${x2},${y2} L ${_left},${y2}`
} else {
if (_isXCenter) {
_left = x2
}
path = `M ${x2},${y2} L ${_left},${y2}`
}
// 节点使用横线风格,需要额外渲染横线
let nodeUseLineStylePath = this.mindMap.themeConfig.nodeUseLineStyle
? ` L ${_left},${y2 - item.height / 2} L ${_left},${
y2 + item.height / 2
}`
: ''
path += nodeUseLineStylePath
lines[index].plot(path)
style && style(lines[index], item)
})
// 竖线
if (len > 0) {
let lin2 = this.draw.path()
expandBtnSize = len > 0 ? expandBtnSize : 0
node.style.line(lin2)
if (maxy < y1 + expandBtnSize) {
lin2.hide()
} else {
lin2.plot(`M ${x2},${y1 + expandBtnSize} L ${x2},${maxy}`)
lin2.show()
}
node._lines.push(lin2)
style && style(lin2, node)
}
}
}
// 渲染按钮
renderExpandBtn(node, btn) {
let { width, height, expandBtnSize, isRoot } = node
if (!isRoot) {
let { translateX, translateY } = btn.transform()
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 CatalogOrganization
import Base from './Base'
import { walk, asyncRun } from '../utils'
// 目录组织图
class CatalogOrganization extends Base {
// 构造函数
constructor(opt = {}) {
super(opt)
}
// 布局
doLayout(callback) {
let task = [
() => {
this.computedBaseValue()
},
() => {
this.computedLeftTopValue()
},
() => {
this.adjustLeftTopValue()
},
() => {
callback(this.root)
}
]
asyncRun(task)
}
// 遍历数据计算节点的left、width、height
computedBaseValue() {
walk(
this.renderer.renderTree,
null,
(cur, parent, isRoot, layerIndex) => {
let newNode = this.createNode(cur, parent, isRoot, layerIndex)
// 根节点定位在画布中心位置
if (isRoot) {
this.setNodeCenter(newNode)
} else {
// 非根节点
if (parent._node.isRoot) {
newNode.top =
parent._node.top +
parent._node.height +
this.getMarginX(layerIndex)
}
}
if (!cur.data.expand) {
return true
}
},
(cur, parent, isRoot, layerIndex) => {
if (isRoot) {
let len = cur.data.expand === false ? 0 : cur._node.children.length
cur._node.childrenAreaWidth = len
? cur._node.children.reduce((h, item) => {
return h + item.width
}, 0) +
(len + 1) * this.getMarginX(layerIndex + 1)
: 0
}
},
true,
0
)
}
// 遍历节点树计算节点的left、top
computedLeftTopValue() {
walk(
this.root,
null,
(node, parent, isRoot, layerIndex) => {
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 / 2 - node.childrenAreaWidth / 2
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 (parent && parent.isRoot) {
let areaWidth = this.getNodeAreaWidth(node)
let difference = areaWidth - node.width
if (difference > 0) {
this.updateBrothersLeft(node, difference)
}
}
// 调整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) => {
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
)
}
// 调整兄弟节点的left
updateBrothersLeft(node, addWidth) {
if (node.parent) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
})
childrenList.forEach((item, _index) => {
if (item.hasCustomPosition() || _index <= index) {
// 适配自定义位置
return
}
item.left += addWidth
// 同步更新子节点的位置
if (item.children && item.children.length) {
this.updateChildren(item.children, 'left', addWidth)
}
})
// 更新父节点的位置
this.updateBrothersLeft(node.parent, addWidth)
}
}
// 调整兄弟节点的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
let marginX = this.getMarginX(node.layerIndex + 1)
if (node.isRoot) {
// 根节点
let x1 = left + width / 2
let y1 = top + height
let s1 = marginX * 0.7
let minx = Infinity
let maxx = -Infinity
node.children.forEach((item, index) => {
let x2 = item.left + item.width / 2
let y2 = item.top
if (x2 < minx) {
minx = x2
}
if (x2 > maxx) {
maxx = x2
}
// 节点使用横线风格,需要额外渲染横线
let nodeUseLineStylePath = this.mindMap.themeConfig.nodeUseLineStyle
? ` L ${item.left},${y2} L ${item.left + item.width},${y2}`
: ''
let path =
`M ${x2},${y1 + s1} L ${x2},${y1 + s1 > y2 ? y2 + item.height : y2}` +
nodeUseLineStylePath
// 竖线
lines[index].plot(path)
style && style(lines[index], item)
})
minx = Math.min(minx, x1)
maxx = Math.max(maxx, x1)
// 父节点的竖线
let line1 = this.draw.path()
node.style.line(line1)
line1.plot(`M ${x1},${y1} L ${x1},${y1 + s1}`)
node._lines.push(line1)
style && style(line1, node)
// 水平线
if (len > 0) {
let lin2 = this.draw.path()
node.style.line(lin2)
lin2.plot(`M ${minx},${y1 + s1} L ${maxx},${y1 + s1}`)
node._lines.push(lin2)
style && style(lin2, node)
}
} else {
// 非根节点
let y1 = top + height
let maxy = -Infinity
let x2 = node.left + node.width * 0.3
node.children.forEach((item, index) => {
// 为了适配自定义位置,下面做了各种位置的兼容
let y2 = item.top + item.height / 2
if (y2 > maxy) {
maxy = y2
}
// 水平线
let path = ''
let _left = item.left
let _isLeft = item.left + item.width < x2
let _isXCenter = false
if (_isLeft) {
// 水平位置在父节点左边
_left = item.left + item.width
} else if (item.left < x2 && item.left + item.width > x2) {
// 水平位置在父节点之间
_isXCenter = true
y2 = item.top
maxy = y2
}
if (y2 > top && y2 < y1) {
// 自定义位置的情况:垂直位置节点在父节点之间
path = `M ${
_isLeft ? node.left : node.left + node.width
},${y2} L ${_left},${y2}`
} else if (y2 < y1) {
// 自定义位置的情况:垂直位置节点在父节点上面
if (_isXCenter) {
y2 = item.top + item.height
_left = x2
}
path = `M ${x2},${top} L ${x2},${y2} L ${_left},${y2}`
} else {
if (_isXCenter) {
_left = x2
}
path = `M ${x2},${y2} L ${_left},${y2}`
}
// 节点使用横线风格,需要额外渲染横线
let nodeUseLineStylePath = this.mindMap.themeConfig.nodeUseLineStyle
? ` L ${_left},${y2 - item.height / 2} L ${_left},${
y2 + item.height / 2
}`
: ''
path += nodeUseLineStylePath
lines[index].plot(path)
style && style(lines[index], item)
})
// 竖线
if (len > 0) {
let lin2 = this.draw.path()
expandBtnSize = len > 0 ? expandBtnSize : 0
node.style.line(lin2)
if (maxy < y1 + expandBtnSize) {
lin2.hide()
} else {
lin2.plot(`M ${x2},${y1 + expandBtnSize} L ${x2},${maxy}`)
lin2.show()
}
node._lines.push(lin2)
style && style(lin2, node)
}
}
}
// 渲染按钮
renderExpandBtn(node, btn) {
let { width, height, expandBtnSize, isRoot } = node
if (!isRoot) {
let { translateX, translateY } = btn.transform()
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 CatalogOrganization

View 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

View 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

View 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

View File

@ -2,16 +2,13 @@ import Base from './Base'
import { walk, asyncRun } from '../utils'
// 逻辑结构图
class LogicalStructure extends Base {
// 构造函数
constructor(opt = {}) {
super(opt)
}
// 布局
doLayout(callback) {
let task = [
() => {
@ -31,7 +28,6 @@ class LogicalStructure extends Base {
}
// 遍历数据计算节点的left、width、height
computedBaseValue() {
walk(
this.renderer.renderTree,
@ -67,7 +63,6 @@ class LogicalStructure extends Base {
}
// 遍历节点树计算节点的top
computedTopValue() {
walk(
this.root,
@ -94,7 +89,6 @@ class LogicalStructure extends Base {
}
// 调整节点top
adjustTopValue() {
walk(
this.root,
@ -118,7 +112,6 @@ class LogicalStructure extends Base {
}
// 更新兄弟节点的top
updateBrothers(node, addHeight) {
if (node.parent) {
let childrenList = node.parent.children
@ -150,7 +143,6 @@ class LogicalStructure extends Base {
}
// 绘制连线,连接该节点到其子节点
renderLine(node, lines, style, lineStyle) {
if (lineStyle === 'curve') {
this.renderLineCurve(node, lines, style)
@ -162,12 +154,14 @@ class LogicalStructure extends Base {
}
// 直线风格连线
renderLineStraight(node, lines, style) {
if (node.children.length <= 0) {
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
@ -192,12 +186,14 @@ class LogicalStructure extends Base {
}
// 直连风格
renderLineDirect(node, lines, style) {
if (node.children.length <= 0) {
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 =
@ -218,12 +214,14 @@ class LogicalStructure extends Base {
}
// 曲线风格连线
renderLineCurve(node, lines, style) {
if (node.children.length <= 0) {
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 =
@ -249,7 +247,6 @@ class LogicalStructure extends Base {
}
// 渲染按钮
renderExpandBtn(node, btn) {
let { width, height } = node
let { translateX, translateY } = btn.transform()
@ -257,14 +254,19 @@ 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
)
}
// 创建概要节点
renderGeneralization(node, gLine, gNode) {
let {
top,

View File

@ -2,7 +2,6 @@ import Base from './Base'
import { walk, asyncRun } from '../utils'
// 思维导图
class MindMap extends Base {
// 构造函数
// 在逻辑结构图的基础上增加一个变量来记录生长方向向左还是向右同时在计算left的时候根据方向来计算、调整top时只考虑同方向的节点即可
@ -11,7 +10,6 @@ class MindMap extends Base {
}
// 布局
doLayout(callback) {
let task = [
() => {
@ -31,7 +29,6 @@ class MindMap extends Base {
}
// 遍历数据计算节点的left、width、height
computedBaseValue() {
walk(
this.renderer.renderTree,
@ -96,7 +93,6 @@ class MindMap extends Base {
}
// 遍历节点树计算节点的top
computedTopValue() {
walk(
this.root,
@ -129,7 +125,6 @@ class MindMap extends Base {
}
// 调整节点top
adjustTopValue() {
walk(
this.root,
@ -152,7 +147,6 @@ class MindMap extends Base {
}
// 更新兄弟节点的top
updateBrothers(node, leftAddHeight, rightAddHeight) {
if (node.parent) {
// 过滤出和自己同方向的节点
@ -188,7 +182,6 @@ class MindMap extends Base {
}
// 绘制连线,连接该节点到其子节点
renderLine(node, lines, style, lineStyle) {
if (lineStyle === 'curve') {
this.renderLineCurve(node, lines, style)
@ -200,12 +193,14 @@ class MindMap extends Base {
}
// 直线风格连线
renderLineStraight(node, lines, style) {
if (node.children.length <= 0) {
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
@ -238,12 +233,14 @@ class MindMap extends Base {
}
// 直连风格
renderLineDirect(node, lines, style) {
if (node.children.length <= 0) {
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 =
@ -273,12 +270,14 @@ class MindMap extends Base {
}
// 曲线风格连线
renderLineCurve(node, lines, style) {
if (node.children.length <= 0) {
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 =
@ -286,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
@ -313,7 +312,6 @@ class MindMap extends Base {
}
// 渲染按钮
renderExpandBtn(node, btn) {
let { width, height, expandBtnSize } = node
let { translateX, translateY } = btn.transform()
@ -321,13 +319,18 @@ 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)
}
// 创建概要节点
renderGeneralization(node, gLine, gNode) {
let isLeft = node.dir === 'left'
let {

View File

@ -5,13 +5,11 @@ import { walk, asyncRun } from '../utils'
// 和逻辑结构图基本一样只是方向变成向下生长所以先计算节点的top后计算节点的left、最后调整节点的left即可
class OrganizationStructure extends Base {
// 构造函数
constructor(opt = {}) {
super(opt)
}
// 布局
doLayout(callback) {
let task = [
() => {
@ -31,7 +29,6 @@ class OrganizationStructure extends Base {
}
// 遍历数据计算节点的left、width、height
computedBaseValue() {
walk(
this.renderer.renderTree,
@ -67,7 +64,6 @@ class OrganizationStructure extends Base {
}
// 遍历节点树计算节点的left
computedLeftValue() {
walk(
this.root,
@ -94,7 +90,6 @@ class OrganizationStructure extends Base {
}
// 调整节点left
adjustLeftValue() {
walk(
this.root,
@ -118,7 +113,6 @@ class OrganizationStructure extends Base {
}
// 更新兄弟节点的left
updateBrothers(node, addWidth) {
if (node.parent) {
let childrenList = node.parent.children
@ -150,7 +144,6 @@ class OrganizationStructure extends Base {
}
// 绘制连线,连接该节点到其子节点
renderLine(node, lines, style, lineStyle) {
if (lineStyle === 'direct') {
this.renderLineDirect(node, lines, style)
@ -160,7 +153,6 @@ class OrganizationStructure extends Base {
}
// 直连风格
renderLineDirect(node, lines, style) {
if (node.children.length <= 0) {
return []
@ -182,12 +174,14 @@ class OrganizationStructure extends Base {
}
// 直线风格连线
renderLineStraight(node, lines, style) {
if (node.children.length <= 0) {
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)
@ -232,7 +226,6 @@ class OrganizationStructure extends Base {
}
// 渲染按钮
renderExpandBtn(node, btn) {
let { width, height, expandBtnSize } = node
let { translateX, translateY } = btn.transform()
@ -243,7 +236,6 @@ class OrganizationStructure extends Base {
}
// 创建概要节点
renderGeneralization(node, gLine, gNode) {
let {
bottom,

View 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

View 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
})
})
}
}
}
}

View File

@ -0,0 +1,7 @@
import { transformToMarkdown } from './toMarkdown'
import { transformMarkdownTo } from './markdownTo'
export default {
transformToMarkdown,
transformMarkdownTo
}

View File

@ -0,0 +1,98 @@
import { fromMarkdown } from 'mdast-util-from-markdown'
// 处理list的情况
const handleList = node => {
let list = []
let walk = (arr, newArr) => {
for (let i = 0; i < arr.length; i++) {
let cur = arr[i]
let node = {}
node.data = {
// 节点内容
text: cur.children[0].children[0].value
}
node.children = []
newArr.push(node)
if (cur.children.length > 1) {
for (let j = 1; j < cur.children.length; j++) {
let cur2 = cur.children[j]
if (cur2.type === 'list') {
walk(cur2.children, node.children)
}
}
}
}
}
walk(node.children, list)
return list
}
// 将markdown转换成节点树
export const transformMarkdownTo = async md => {
const tree = fromMarkdown(md)
let root = {
children: []
}
let childrenQueue = [root.children]
let currentChildren = root.children
let depthQueue = [-1]
let currentDepth = -1
for (let i = 0; i < tree.children.length; i++) {
let cur = tree.children[i]
if (cur.type === 'heading') {
if (!cur.children[0]) continue
// 创建新节点
let node = {}
node.data = {
// 节点内容
text: cur.children[0].value
}
node.children = []
// 如果当前的层级大于上一个节点的层级,那么是其子节点
if (cur.depth > currentDepth) {
// 添加到上一个节点的子节点列表里
currentChildren.push(node)
// 更新当前栈和数据
childrenQueue.push(node.children)
currentChildren = node.children
depthQueue.push(cur.depth)
currentDepth = cur.depth
} else if (cur.depth === currentDepth) {
// 如果当前层级等于上一个节点的层级,说明它们是同级节点
// 将上一个节点出栈
childrenQueue.pop()
currentChildren = childrenQueue[childrenQueue.length - 1]
depthQueue.pop()
currentDepth = depthQueue[depthQueue.length - 1]
// 追加到上上个节点的子节点列表里
currentChildren.push(node)
// 更新当前栈和数据
childrenQueue.push(node.children)
currentChildren = node.children
depthQueue.push(cur.depth)
currentDepth = cur.depth
} else {
// 如果当前层级小于上一个节点的层级,那么一直出栈,直到遇到比当前层级小的节点
while (depthQueue.length) {
childrenQueue.pop()
currentChildren = childrenQueue[childrenQueue.length - 1]
depthQueue.pop()
currentDepth = depthQueue[depthQueue.length - 1]
if (currentDepth < cur.depth) {
// 追加到该节点的子节点列表里
currentChildren.push(node)
// 更新当前栈和数据
childrenQueue.push(node.children)
currentChildren = node.children
depthQueue.push(cur.depth)
currentDepth = cur.depth
break
}
}
}
} else if (cur.type === 'list') {
currentChildren.push(...handleList(cur))
}
}
return root.children[0]
}

View File

@ -0,0 +1,53 @@
import { walk } from '../utils'
let el = null
const getText = str => {
if (!el) {
el = document.createElement('div')
}
el.innerHTML = str
return el.textContent
}
const getTitleMark = level => {
return new Array(level).fill('#').join('')
}
const getIndentMark = level => {
return new Array(level - 6).fill(' ').join('') + '*'
}
// 转换成markdown格式
export const transformToMarkdown = root => {
let content = ''
walk(
root,
null,
(node, parent, isRoot, layerIndex) => {
let level = layerIndex + 1
let text = node.data.richText ? getText(node.data.text) : node.data.text
if (level <= 6) {
content += getTitleMark(level)
} else {
content += getIndentMark(level)
}
content += ' ' + text
// 概要
let generalization = node.data.generalization
if (generalization && generalization.text) {
let generalizationText = generalization.richText
? getText(generalization.text)
: generalization.text
content += `[${generalizationText}]`
}
content += '\n\n'
// 备注
if (node.data.note) {
content += node.data.note + '\n\n'
}
},
() => {},
true
)
return content
}

View File

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

View 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'
}
}
})

View 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'
}
}
})

View File

@ -0,0 +1,57 @@
import defaultTheme from './default'
import merge from 'deepmerge'
// 黑金
export default merge(defaultTheme, {
// 背景颜色
backgroundColor: 'rgb(18, 20, 20)',
// 连线的颜色
lineColor: 'rgb(205, 186, 156)',
lineWidth: 3,
// 概要连线的粗细
generalizationLineWidth: 3,
// 概要连线的颜色
generalizationLineColor: 'rgb(245, 224, 191)',
// 根节点样式
root: {
fillColor: 'rgb(255, 208, 124)',
color: 'rgb(111, 61, 6)',
borderColor: '',
borderWidth: 0,
fontSize: 24,
active: {
borderColor: '#fff',
borderWidth: 3
}
},
// 二级节点样式
second: {
fillColor: 'rgb(66, 57, 46)',
color: 'rgb(225, 201, 158)',
borderColor: 'rgb(245, 224, 191)',
borderWidth: 2,
fontSize: 18,
active: {
borderColor: 'rgb(255, 208, 124)'
}
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: 'rgb(231, 203, 155)',
active: {
borderColor: 'rgb(255, 208, 124)'
}
},
// 概要节点样式
generalization: {
fontSize: 14,
fillColor: 'rgb(56, 45, 34)',
borderColor: 'rgb(104, 84, 61)',
borderWidth: 2,
color: 'rgb(242, 216, 176)',
active: {
borderColor: 'rgb(255, 208, 124)'
}
}
})

View File

@ -0,0 +1,58 @@
import defaultTheme from './default'
import merge from 'deepmerge'
// 黑色幽默
export default merge(defaultTheme, {
// 背景颜色
backgroundColor: 'rgb(27, 31, 34)',
// 连线的颜色
lineColor: 'rgb(75, 81, 78)',
lineWidth: 3,
// 概要连线的粗细
generalizationLineWidth: 3,
// 概要连线的颜色
generalizationLineColor: 'rgb(255, 119, 34)',
// 根节点样式
root: {
fillColor: 'rgb(36, 179, 96)',
color: '#fff',
borderColor: '',
borderWidth: 0,
fontSize: 24,
active: {
borderColor: 'rgb(254, 199, 13)',
borderWidth: 3
}
},
// 二级节点样式
second: {
fillColor: 'rgb(254, 199, 13)',
color: 'rgb(0, 0, 0)',
borderColor: '',
borderWidth: 0,
fontSize: 18,
active: {
borderColor: 'rgb(36, 179, 96)',
borderWidth: 3
}
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: 'rgb(204, 204, 204)',
active: {
borderColor: 'rgb(254, 199, 13)'
}
},
// 概要节点样式
generalization: {
fontSize: 14,
fillColor: 'rgb(27, 31, 34)',
borderColor: 'rgb(255, 119, 34)',
borderWidth: 2,
color: 'rgb(204, 204, 204)',
active: {
borderColor: 'rgb(36, 179, 96)'
}
}
})

View File

@ -0,0 +1,55 @@
import defaultTheme from './default'
import merge from 'deepmerge'
// 咖啡
export default merge(defaultTheme, {
// 连线的颜色
lineColor: 'rgb(173, 123, 91)',
lineWidth: 4,
// 概要连线的粗细
generalizationLineWidth: 4,
// 概要连线的颜色
generalizationLineColor: 'rgb(173, 123, 91)',
// 根节点样式
root: {
fillColor: 'rgb(202, 117, 79)',
color: '#fff',
borderColor: '',
borderWidth: 0,
fontSize: 24,
active: {
borderColor: 'rgb(173, 123, 91)',
borderWidth: 3
}
},
// 二级节点样式
second: {
fillColor: 'rgb(245, 231, 216)',
color: 'rgb(125, 86, 42)',
borderColor: '',
borderWidth: 0,
fontSize: 18,
active: {
borderColor: 'rgb(173, 123, 91)'
}
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: 'rgb(96, 71, 47)',
active: {
borderColor: 'rgb(173, 123, 91)'
}
},
// 概要节点样式
generalization: {
fontSize: 14,
fillColor: 'rgb(255, 249, 239)',
borderColor: 'rgb(173, 123, 91)',
borderWidth: 2,
color: 'rgb(122, 83, 44)',
active: {
borderColor: 'rgb(202, 117, 79)'
}
}
})

View File

@ -0,0 +1,55 @@
import defaultTheme from './default'
import merge from 'deepmerge'
// 课程绿
export default merge(defaultTheme, {
// 连线的颜色
lineColor: 'rgb(113, 195, 169)',
lineWidth: 3,
// 概要连线的粗细
generalizationLineWidth: 3,
// 概要连线的颜色
generalizationLineColor: 'rgb(113, 195, 169)',
// 根节点样式
root: {
fillColor: 'rgb(16, 160, 121)',
color: '#fff',
borderColor: '',
borderWidth: 0,
fontSize: 24,
active: {
borderColor: 'rgb(173, 91, 12)',
borderWidth: 3
}
},
// 二级节点样式
second: {
fillColor: 'rgb(240, 252, 249)',
color: 'rgb(50, 113, 96)',
borderColor: 'rgb(113, 195, 169)',
borderWidth: 2,
fontSize: 18,
active: {
borderColor: 'rgb(173, 91, 12)'
}
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: 'rgb(10, 59, 43)',
active: {
borderColor: 'rgb(173, 91, 12)'
}
},
// 概要节点样式
generalization: {
fontSize: 14,
fillColor: 'rgb(246, 238, 211)',
borderColor: '',
borderWidth: 0,
color: 'rgb(173, 91, 12)',
active: {
borderColor: 'rgb(113, 195, 169)'
}
}
})

View File

@ -26,6 +26,22 @@ export default {
generalizationLineMargin: 0,
// 概要节点距节点的距离
generalizationNodeMargin: 20,
// 关联线默认状态的粗细
associativeLineWidth: 2,
// 关联线默认状态的颜色
associativeLineColor: 'rgb(51, 51, 51)',
// 关联线激活状态的粗细
associativeLineActiveWidth: 8,
// 关联线激活状态的颜色
associativeLineActiveColor: 'rgba(2, 167, 240, 1)',
// 关联线文字颜色
associativeLineTextColor: 'rgb(51, 51, 51)',
// 关联线文字大小
associativeLineTextFontSize: 14,
// 关联线文字行高
associativeLineTextLineHeight: 1.2,
// 关联线文字字体
associativeLineTextFontFamily: '微软雅黑, Microsoft YaHei',
// 背景颜色
backgroundColor: '#fafafa',
// 背景图片
@ -134,14 +150,44 @@ export default {
// 简单来说,会改变节点大小的都不支持在激活时设置,为了性能考虑,节点切换激活态时不会重新计算节点大小
export const supportActiveStyle = [
'fillColor',
'color',
'fontWeight',
'fontStyle',
'borderColor',
'borderWidth',
'borderDasharray',
'borderRadius',
'textDecoration'
'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']

View File

@ -20,6 +20,16 @@ import vitalityOrange from './vitalityOrange'
import greenLeaf from './greenLeaf'
import dark2 from './dark2'
import skyGreen from './skyGreen'
import simpleBlack from './simpleBlack'
import courseGreen from './courseGreen'
import coffee from './coffee'
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,
@ -43,5 +53,15 @@ export default {
vitalityOrange,
greenLeaf,
dark2,
skyGreen
skyGreen,
simpleBlack,
courseGreen,
coffee,
redSpirit,
blackHumour,
lateNightOffice,
blackGold,
avocado,
autumn,
orangeJuice
}

View File

@ -0,0 +1,58 @@
import defaultTheme from './default'
import merge from 'deepmerge'
// 深夜办公室
export default merge(defaultTheme, {
// 背景颜色
backgroundColor: 'rgb(32, 37, 49)',
// 连线的颜色
lineColor: 'rgb(137, 167, 196)',
lineWidth: 3,
// 概要连线的粗细
generalizationLineWidth: 3,
// 概要连线的颜色
generalizationLineColor: 'rgb(255, 119, 34)',
// 根节点样式
root: {
fillColor: 'rgb(23, 153, 243)',
color: 'rgb(255, 255, 255)',
borderColor: '',
borderWidth: 0,
fontSize: 24,
active: {
borderColor: 'rgb(255, 119, 34)',
borderWidth: 3
}
},
// 二级节点样式
second: {
fillColor: 'rgb(70, 78, 94)',
color: 'rgb(209, 210, 210)',
borderColor: '',
borderWidth: 0,
fontSize: 18,
active: {
borderColor: 'rgb(255, 119, 34)',
borderWidth: 3
}
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: 'rgb(204, 204, 204)',
active: {
borderColor: 'rgb(255, 119, 34)'
}
},
// 概要节点样式
generalization: {
fontSize: 14,
fillColor: 'rgb(255, 119, 34)',
borderColor: '',
borderWidth: 2,
color: '#fff',
active: {
borderColor: 'rgb(23, 153, 243)'
}
}
})

View 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'
}
}
})

View File

@ -0,0 +1,57 @@
import defaultTheme from './default'
import merge from 'deepmerge'
// 红色精神
export default merge(defaultTheme, {
// 背景颜色
backgroundColor: 'rgb(255, 238, 228)',
// 连线的颜色
lineColor: 'rgb(230, 138, 131)',
lineWidth: 3,
// 概要连线的粗细
generalizationLineWidth: 3,
// 概要连线的颜色
generalizationLineColor: 'rgb(222, 101, 85)',
// 根节点样式
root: {
fillColor: 'rgb(207, 44, 44)',
color: 'rgb(255, 233, 157)',
borderColor: '',
borderWidth: 0,
fontSize: 24,
active: {
borderColor: 'rgb(255, 233, 157)',
borderWidth: 3
}
},
// 二级节点样式
second: {
fillColor: 'rgb(255, 255, 255)',
color: 'rgb(211, 58, 21)',
borderColor: 'rgb(222, 101, 85)',
borderWidth: 2,
fontSize: 18,
active: {
borderColor: 'rgb(255, 233, 157)'
}
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: 'rgb(144, 71, 43)',
active: {
borderColor: 'rgb(255, 233, 157)'
}
},
// 概要节点样式
generalization: {
fontSize: 14,
fillColor: 'rgb(255, 247, 211)',
borderColor: 'rgb(255, 202, 162)',
borderWidth: 2,
color: 'rgb(187, 101, 69)',
active: {
borderColor: 'rgb(222, 101, 85)'
}
}
})

View File

@ -0,0 +1,54 @@
import defaultTheme from './default'
import merge from 'deepmerge'
// 简约黑
export default merge(defaultTheme, {
// 连线的颜色
lineColor: 'rgb(34, 34, 34)',
lineWidth: 4,
// 概要连线的粗细
generalizationLineWidth: 4,
// 概要连线的颜色
generalizationLineColor: 'rgb(34, 34, 34)',
// 根节点样式
root: {
fillColor: '#fff',
color: 'rgb(34, 34, 34)',
borderColor: 'rgb(34, 34, 34)',
borderWidth: 3,
fontSize: 24,
active: {
borderColor: '#a13600'
}
},
// 二级节点样式
second: {
fillColor: 'rgb(241, 246, 248)',
color: 'rgb(34, 34, 34)',
borderColor: 'rgb(34, 34, 34)',
borderWidth: 3,
fontSize: 18,
active: {
borderColor: '#a13600'
}
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: 'rgb(34, 34, 34)',
active: {
borderColor: '#a13600'
}
},
// 概要节点样式
generalization: {
fontSize: 14,
fillColor: 'transparent',
borderColor: 'rgb(34, 34, 34)',
borderWidth: 2,
color: 'rgb(34, 34, 34)',
active: {
borderColor: '#a13600'
}
}
})

View 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)
}
}
}

View 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
}

View 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
}

View File

@ -0,0 +1,182 @@
// 获取目标节点在起始节点的目标数组中的索引
export const getAssociativeLineTargetIndex = (node, toNode) => {
return node.nodeData.data.associativeLineTargets.findIndex(item => {
return item === toNode.nodeData.data.id
})
}
// 计算贝塞尔曲线的控制点
export const computeCubicBezierPathPoints = (x1, y1, x2, y2) => {
let cx1 = x1 + (x2 - x1) / 2
let cy1 = y1
let cx2 = cx1
let cy2 = y2
if (Math.abs(x1 - x2) <= 5) {
cx1 = x1 + (y2 - y1) / 2
cx2 = cx1
}
return [
{
x: cx1,
y: cy1
},
{
x: cx2,
y: cy2
}
]
}
// 拼接贝塞尔曲线路径
export const joinCubicBezierPath = (startPoint, endPoint, point1, point2) => {
return `M ${startPoint.x},${startPoint.y} C ${point1.x},${point1.y} ${point2.x},${point2.y} ${endPoint.x},${endPoint.y}`
}
// 获取节点的位置信息
const getNodeRect = node => {
let { left, top, width, height } = node
return {
right: left + width,
bottom: top + height,
left,
top
}
}
// 三次贝塞尔曲线
export const cubicBezierPath = (x1, y1, x2, y2) => {
let points = computeCubicBezierPathPoints(x1, y1, x2, y2)
return joinCubicBezierPath(
{ x: x1, y: y1 },
{ x: x2, y: y2 },
points[0],
points[1]
)
}
// 获取节点的连接点
export const getNodePoint = (node, dir = 'right') => {
let { left, top, width, height } = node
switch (dir) {
case 'left':
return {
x: left,
y: top + height / 2
}
case 'right':
return {
x: left + width,
y: top + height / 2
}
case 'top':
return {
x: left + width / 2,
y: top
}
case 'bottom':
return {
x: left + width / 2,
y: top + height
}
default:
break
}
}
// 根据两个节点的位置计算节点的连接点
export const computeNodePoints = (fromNode, toNode) => {
let fromRect = getNodeRect(fromNode)
let fromCx = (fromRect.right + fromRect.left) / 2
let fromCy = (fromRect.bottom + fromRect.top) / 2
let toRect = getNodeRect(toNode)
let toCx = (toRect.right + toRect.left) / 2
let toCy = (toRect.bottom + toRect.top) / 2
// 中心点坐标的差值
let offsetX = toCx - fromCx
let offsetY = toCy - fromCy
if (offsetX === 0 && offsetY === 0) return
let fromDir = ''
let toDir = ''
if (offsetX <= 0 && offsetX <= offsetY && offsetX <= -offsetY) {
// left
fromDir = 'left'
toDir = 'right'
} else if (offsetX > 0 && offsetX >= -offsetY && offsetX >= offsetY) {
// right
fromDir = 'right'
toDir = 'left'
} else if (offsetY <= 0 && offsetY < offsetX && offsetY < -offsetX) {
// up
fromDir = 'top'
toDir = 'bottom'
} else if (offsetY > 0 && -offsetY < offsetX && offsetY > offsetX) {
// down
fromDir = 'bottom'
toDir = 'top'
}
return [getNodePoint(fromNode, fromDir), getNodePoint(toNode, toDir)]
}
// 获取节点的关联线路径
export const getNodeLinePath = (startPoint, endPoint, node, toNode) => {
let targetIndex = getAssociativeLineTargetIndex(node, toNode)
// 控制点
let controlPoints = []
let associativeLineTargetControlOffsets =
node.nodeData.data.associativeLineTargetControlOffsets
if (
associativeLineTargetControlOffsets &&
associativeLineTargetControlOffsets[targetIndex]
) {
// 节点保存了控制点差值
let offsets = associativeLineTargetControlOffsets[targetIndex]
controlPoints = [
{
x: startPoint.x + offsets[0].x,
y: startPoint.y + offsets[0].y
},
{
x: endPoint.x + offsets[1].x,
y: endPoint.y + offsets[1].y
}
]
} else {
// 没有保存控制点则生成默认的
controlPoints = computeCubicBezierPathPoints(
startPoint.x,
startPoint.y,
endPoint.x,
endPoint.y
)
}
// 根据控制点拼接贝塞尔曲线路径
return {
path: joinCubicBezierPath(
startPoint,
endPoint,
controlPoints[0],
controlPoints[1]
),
controlPoints
}
}
// 获取默认的控制点差值
export const getDefaultControlPointOffsets = (startPoint, endPoint) => {
let controlPoints = computeCubicBezierPathPoints(
startPoint.x,
startPoint.y,
endPoint.x,
endPoint.y
)
return [
{
x: controlPoints[0].x - startPoint.x,
y: controlPoints[0].y - startPoint.y
},
{
x: controlPoints[1].x - endPoint.x,
y: controlPoints[1].y - endPoint.y
}
]
}

View File

@ -22,66 +22,12 @@ export const tagColorList = [
}
]
// 布局结构列表
export const layoutList = [
{
name: '逻辑结构图',
value: 'logicalStructure',
},
{
name: '思维导图',
value: 'mindMap',
},
{
name: '组织结构图',
value: 'organizationStructure',
},
{
name: '目录组织图',
value: 'catalogOrganization',
}
]
export const layoutValueList = [
'logicalStructure',
'mindMap',
'catalogOrganization',
'organizationStructure'
]
// 主题列表
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',
@ -98,10 +44,6 @@ export const themeList = [
name: '脑图经典3',
value: 'classic3',
},
{
name: '脑图经典4',
value: 'classic4',
},
{
name: '经典绿',
value: 'classicGreen',
@ -137,5 +79,184 @@ 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',
},
{
name: '课程绿',
value: 'courseGreen',
},
{
name: '咖啡',
value: 'coffee',
},
{
name: '红色精神',
value: 'redSpirit',
},
{
name: '黑色幽默',
value: 'blackHumour',
},
{
name: '深夜办公室',
value: 'lateNightOffice',
},
{
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',
EDIT: 'edit'
},
LAYOUT: {
LOGICAL_STRUCTURE: 'logicalStructure',
MIND_MAP: 'mindMap',
ORGANIZATION_STRUCTURE: 'organizationStructure',
CATALOG_ORGANIZATION: 'catalogOrganization',
TIMELINE: 'timeline',
TIMELINE2: 'timeline2',
FISHBONE: 'fishbone'
},
DIR: {
UP: 'up',
LEFT: 'left',
DOWN: 'down',
RIGHT: 'right'
},
KEY_DIR: {
LEFT: 'Left',
UP: 'Up',
RIGHT: 'Right',
DOWN: 'Down'
},
SHAPE: {
RECTANGLE: 'rectangle',
DIAMOND: 'diamond',
PARALLELOGRAM: 'parallelogram',
ROUNDED_RECTANGLE: 'roundedRectangle',
OCTAGONAL_RECTANGLE: 'octagonalRectangle',
OUTER_TRIANGULAR_RECTANGLE: 'outerTriangularRectangle',
INNER_TRIANGULAR_RECTANGLE: 'innerTriangularRectangle',
ELLIPSE: 'ellipse',
CIRCLE: 'circle'
},
MOUSE_WHEEL_ACTION: {
ZOOM: 'zoom',
MOVE: 'move'
},
INIT_ROOT_NODE_POSITION: {
LEFT: 'left',
TOP: 'top',
RIGHT: 'right',
BOTTOM: 'bottom',
CENTER: 'center'
},
TIMELINE_DIR: {
TOP: 'top',
BOTTOM: 'bottom'
}
}
export const initRootNodePositionMap = {
[CONSTANTS.INIT_ROOT_NODE_POSITION.LEFT]: 0,
[CONSTANTS.INIT_ROOT_NODE_POSITION.TOP]: 0,
[CONSTANTS.INIT_ROOT_NODE_POSITION.RIGHT]: 1,
[CONSTANTS.INIT_ROOT_NODE_POSITION.BOTTOM]: 1,
[CONSTANTS.INIT_ROOT_NODE_POSITION.CENTER]: 0.5,
}
// 布局结构列表
export const layoutList = [
{
name: '逻辑结构图',
value: CONSTANTS.LAYOUT.LOGICAL_STRUCTURE,
},
{
name: '思维导图',
value: CONSTANTS.LAYOUT.MIND_MAP,
},
{
name: '组织结构图',
value: CONSTANTS.LAYOUT.ORGANIZATION_STRUCTURE,
},
{
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.TIMELINE,
CONSTANTS.LAYOUT.TIMELINE2,
CONSTANTS.LAYOUT.FISHBONE
]

View File

@ -122,27 +122,33 @@ export const simpleDeepClone = data => {
}
// 复制渲染树数据
export const copyRenderTree = (tree, root) => {
export const copyRenderTree = (tree, root, removeActiveState = false) => {
tree.data = simpleDeepClone(root.data)
tree.children = []
if (root.children && root.children.length > 0) {
root.children.forEach((item, index) => {
tree.children[index] = copyRenderTree({}, item)
})
}
return tree
}
// 复制节点树数据
export const copyNodeTree = (tree, root, removeActiveState = false) => {
tree.data = simpleDeepClone(root.nodeData ? root.nodeData.data : root.data)
if (removeActiveState) {
tree.data.isActive = false
}
tree.children = []
if (root.children && root.children.length > 0) {
root.children.forEach((item, index) => {
tree.children[index] = copyNodeTree({}, item, removeActiveState)
tree.children[index] = copyRenderTree({}, item, removeActiveState)
})
}
return tree
}
// 复制节点树数据
export const copyNodeTree = (tree, root, removeActiveState = false, keepId = false) => {
tree.data = simpleDeepClone(root.nodeData ? root.nodeData.data : root.data)
// 去除节点id因为节点id不能重复
if (tree.data.id && !keepId) delete tree.data.id
if (tree.data.uid) delete tree.data.uid
if (removeActiveState) {
tree.data.isActive = false
}
tree.children = []
if (root.children && root.children.length > 0) {
root.children.forEach((item, index) => {
tree.children[index] = copyNodeTree({}, item, removeActiveState, keepId)
})
} else if (
root.nodeData &&
@ -150,7 +156,7 @@ export const copyNodeTree = (tree, root, removeActiveState = false) => {
root.nodeData.children.length > 0
) {
root.nodeData.children.forEach((item, index) => {
tree.children[index] = copyNodeTree({}, item, removeActiveState)
tree.children[index] = copyNodeTree({}, item, removeActiveState, keepId)
})
}
return tree
@ -193,12 +199,12 @@ export const downloadFile = (file, fileName) => {
// 节流函数
export const throttle = (fn, time = 300, ctx) => {
let timer = null
return () => {
return (...args) => {
if (timer) {
return
}
timer = setTimeout(() => {
fn.call(ctx)
fn.call(ctx, ...args)
timer = null
}, time)
}
@ -265,4 +271,89 @@ export const measureText = (text, { italic, bold, fontSize, fontFamily }) => {
// 拼接font字符串
export const joinFontStr = ({ italic, bold, fontSize, fontFamily }) => {
return `${italic ? 'italic ' : ''} ${bold ? 'bold ' : ''} ${fontSize}px ${fontFamily} `
}
// 在下一个事件循环里执行任务
export const nextTick = function (fn, ctx) {
let pending = false
let timerFunc = null
let handle = () => {
pending = false
ctx ? fn.call(ctx) : fn()
}
// 支持MutationObserver接口的话使用MutationObserver
if (typeof MutationObserver !== 'undefined') {
let counter = 1
let observer = new MutationObserver(handle)
let textNode = document.createTextNode(counter)
observer.observe(textNode, {
characterData: true // 设为 true 表示监视指定目标节点或子节点树中节点所包含的字符数据的变化
})
timerFunc = function () {
counter = (counter + 1) % 2 // counter会在0和1两者循环变化
textNode.data = counter // 节点变化会触发回调handle
}
} else {
// 否则使用定时器
timerFunc = setTimeout
}
return function () {
if (pending) return
pending = true
timerFunc(handle, 0)
}
}
// 检查节点是否超出画布
export const checkNodeOuter = (mindMap, node) => {
let elRect = mindMap.elRect
let { scaleX, scaleY, translateX, translateY } = mindMap.draw.transform()
let { left, top, width, height } = node
let right = (left + width) * scaleX + translateX
let bottom = (top + height) * scaleY + translateY
left = left * scaleX + translateX
top = top * scaleY + translateY
let offsetLeft = 0
let offsetTop = 0
if (left < 0) {
offsetLeft = -left
}
if (right > elRect.width) {
offsetLeft = -(right - elRect.width)
}
if (top < 0) {
offsetTop = -top
}
if (bottom > elRect.height) {
offsetTop = -(bottom - elRect.height)
}
return {
isOuter: offsetLeft !== 0 || offsetTop !== 0,
offsetLeft,
offsetTop
}
}
// 提取html字符串里的纯文本
let getTextFromHtmlEl = null
export const getTextFromHtml = (html) => {
if (!getTextFromHtmlEl) {
getTextFromHtmlEl = document.createElement('div')
}
getTextFromHtmlEl.innerHTML = html
return getTextFromHtmlEl.textContent
}
// 将blob转成data:url
export const readBlob = (blob) => {
return new Promise((resolve, reject) => {
let reader = new FileReader()
reader.onload = (evt) => {
resolve(evt.target.result)
}
reader.onerror = (err) => {
reject(err)
}
reader.readAsDataURL(blob)
})
}

View File

@ -0,0 +1,56 @@
// 设置数据
function setData(data = {}) {
this.mindMap.execCommand('SET_NODE_DATA', this, data)
}
// 设置文本
function setText(text, richText) {
this.mindMap.execCommand('SET_NODE_TEXT', this, text, richText)
}
// 设置图片
function setImage(imgData) {
this.mindMap.execCommand('SET_NODE_IMAGE', this, imgData)
}
// 设置图标
function setIcon(icons) {
this.mindMap.execCommand('SET_NODE_ICON', this, icons)
}
// 设置超链接
function setHyperlink(link, title) {
this.mindMap.execCommand('SET_NODE_HYPERLINK', this, link, title)
}
// 设置备注
function setNote(note) {
this.mindMap.execCommand('SET_NODE_NOTE', this, note)
}
// 设置标签
function setTag(tag) {
this.mindMap.execCommand('SET_NODE_TAG', this, tag)
}
// 设置形状
function setShape(shape) {
this.mindMap.execCommand('SET_NODE_SHAPE', this, shape)
}
// 修改某个样式
function setStyle(prop, value, isActive) {
this.mindMap.execCommand('SET_NODE_STYLE', this, prop, value, isActive)
}
export default {
setData,
setText,
setImage,
setIcon,
setHyperlink,
setNote,
setTag,
setShape,
setStyle
}

View File

@ -0,0 +1,282 @@
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() {
let img = this.nodeData.data.image
if (!img) {
return
}
let imgSize = this.getImgShowSize()
let node = new Image().load(img).size(...imgSize)
if (this.nodeData.data.imageTitle) {
node.attr('title', this.nodeData.data.imageTitle)
}
node.on('dblclick', e => {
this.mindMap.emit('node_img_dblclick', this, e)
})
return {
node,
width: imgSize[0],
height: imgSize[1]
}
}
// 获取图片显示宽高
function getImgShowSize() {
return resizeImgSize(
this.nodeData.data.imageSize.width,
this.nodeData.data.imageSize.height,
this.mindMap.themeConfig.imgMaxWidth,
this.mindMap.themeConfig.imgMaxHeight
)
}
// 创建icon节点
function createIconNode() {
let _data = this.nodeData.data
if (!_data.icon || _data.icon.length <= 0) {
return []
}
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,
width: iconSize,
height: iconSize
}
})
}
// 创建富文本节点
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)
let { width, height } = el.getBoundingClientRect()
width = Math.ceil(width)
height = Math.ceil(height)
g.attr('data-width', width)
g.attr('data-height', height)
html = div.innerHTML
this.mindMap.el.removeChild(div)
let foreignObject = new ForeignObject()
foreignObject.width(width)
foreignObject.height(height)
foreignObject.add(SVG(html))
g.add(foreignObject)
return {
node: g,
width,
height
}
}
// 创建文本节点
function createTextNode() {
if (this.nodeData.data.richText) {
return this.createRichTextNode()
}
let g = new G()
let fontSize = this.getStyle('fontSize', false, this.nodeData.data.isActive)
let lineHeight = this.getStyle(
'lineHeight',
false,
this.nodeData.data.isActive
)
// 文本超长自动换行
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) {
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)
textArr.forEach((item, index) => {
let node = new Text().text(item)
this.style.text(node)
node.y(fontSize * lineHeight * index)
g.add(node)
})
let { width, height } = g.bbox()
width = Math.ceil(width)
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,
height
}
}
// 创建超链接节点
function createHyperlinkNode() {
let { hyperlink, hyperlinkTitle } = this.nodeData.data
if (!hyperlink) {
return
}
let iconSize = this.mindMap.themeConfig.iconSize
let node = new SVG()
// 超链接节点
let a = new A().to(hyperlink).target('_blank')
a.node.addEventListener('click', e => {
e.stopPropagation()
})
if (hyperlinkTitle) {
a.attr('title', hyperlinkTitle)
}
// 添加一个透明的层,作为鼠标区域
a.rect(iconSize, iconSize).fill({ color: 'transparent' })
// 超链接图标
let iconNode = SVG(iconsSvg.hyperlink).size(iconSize, iconSize)
this.style.iconNode(iconNode)
a.add(iconNode)
node.add(a)
return {
node,
width: iconSize,
height: iconSize
}
}
// 创建标签节点
function createTagNode() {
let tagData = this.nodeData.data.tag
if (!tagData || tagData.length <= 0) {
return []
}
let nodes = []
tagData.slice(0, this.mindMap.opt.maxTag).forEach((item, index) => {
let tag = new G()
// 标签文本
let text = new Text().text(item).x(8).cy(10)
this.style.tagText(text, index)
let { width } = text.bbox()
// 标签矩形
let rect = new Rect().size(width + 16, 20)
this.style.tagRect(rect, index)
tag.add(rect).add(text)
nodes.push({
node: tag,
width: width + 16,
height: 20
})
})
return nodes
}
// 创建备注节点
function createNoteNode() {
if (!this.nodeData.data.note) {
return null
}
let iconSize = this.mindMap.themeConfig.iconSize
let node = new SVG().attr('cursor', 'pointer')
// 透明的层,用来作为鼠标区域
node.add(new Rect().size(iconSize, iconSize).fill({ color: 'transparent' }))
// 备注图标
let iconNode = SVG(iconsSvg.note).size(iconSize, iconSize)
this.style.iconNode(iconNode)
node.add(iconNode)
// 备注tooltip
if (!this.mindMap.opt.customNoteContentShow) {
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;
z-index: ${ this.mindMap.opt.nodeNoteTooltipZIndex }
`
document.body.appendChild(this.noteEl)
}
this.noteEl.innerText = this.nodeData.data.note
}
node.on('mouseover', () => {
let { left, top } = node.node.getBoundingClientRect()
if (!this.mindMap.opt.customNoteContentShow) {
this.noteEl.style.left = left + 'px'
this.noteEl.style.top = top + iconSize + 'px'
this.noteEl.style.display = 'block'
} else {
this.mindMap.opt.customNoteContentShow.show(
this.nodeData.data.note,
left,
top + iconSize
)
}
})
node.on('mouseout', () => {
if (!this.mindMap.opt.customNoteContentShow) {
this.noteEl.style.display = 'none'
} else {
this.mindMap.opt.customNoteContentShow.hide()
}
})
return {
node,
width: iconSize,
height: iconSize
}
}
export default {
createImgNode,
getImgShowSize,
createIconNode,
createRichTextNode,
createTextNode,
createHyperlinkNode,
createTagNode,
createNoteNode
}

View File

@ -0,0 +1,142 @@
import btnsSvg from '../svg/btns'
import { SVG, Circle, G } from '@svgdotjs/svg.js'
// 创建展开收起按钮的内容节点
function createExpandNodeContent() {
if (this._openExpandNode) {
return
}
let { open, close } = this.mindMap.opt.expandBtnIcon || {}
// 展开的节点
this._openExpandNode = SVG(open || btnsSvg.open).size(
this.expandBtnSize,
this.expandBtnSize
)
this._openExpandNode.x(0).y(-this.expandBtnSize / 2)
// 收起的节点
this._closeExpandNode = SVG(close || btnsSvg.close).size(
this.expandBtnSize,
this.expandBtnSize
)
this._closeExpandNode.x(0).y(-this.expandBtnSize / 2)
// 填充节点
this._fillExpandNode = new Circle().size(this.expandBtnSize)
this._fillExpandNode.x(0).y(-this.expandBtnSize / 2)
// 设置样式
this.style.iconBtn(
this._openExpandNode,
this._closeExpandNode,
this._fillExpandNode
)
}
// 创建或更新展开收缩按钮内容
function updateExpandBtnNode() {
let { expand } = this.nodeData.data
// 如果本次和上次的展开状态一样则返回
if (expand === this._lastExpandBtnType) return
if (this._expandBtn) {
this._expandBtn.clear()
}
this.createExpandNodeContent()
let node
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)
}
// 更新展开收缩按钮位置
function updateExpandBtnPos() {
if (!this._expandBtn) {
return
}
this.renderer.layout.renderExpandBtn(this, this._expandBtn)
}
// 创建展开收缩按钮
function renderExpandBtn() {
if (
!this.nodeData.children ||
this.nodeData.children.length <= 0 ||
this.isRoot
) {
return
}
if (this._expandBtn) {
this.group.add(this._expandBtn)
} else {
this._expandBtn = new G()
this._expandBtn.on('mouseover', e => {
e.stopPropagation()
this._expandBtn.css({
cursor: 'pointer'
})
})
this._expandBtn.on('mouseout', e => {
e.stopPropagation()
this._expandBtn.css({
cursor: 'auto'
})
})
this._expandBtn.on('click', e => {
e.stopPropagation()
// 展开收缩
this.mindMap.execCommand(
'SET_NODE_EXPAND',
this,
!this.nodeData.data.expand
)
this.mindMap.emit('expand_btn_click', this)
})
this._expandBtn.on('dblclick', e => {
e.stopPropagation()
})
this.group.add(this._expandBtn)
}
this._showExpandBtn = true
this.updateExpandBtnNode()
this.updateExpandBtnPos()
}
// 移除展开收缩按钮
function removeExpandBtn() {
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)
}
}
export default {
createExpandNodeContent,
updateExpandBtnNode,
updateExpandBtnPos,
renderExpandBtn,
removeExpandBtn,
showExpandBtn,
hideExpandBtn
}

View File

@ -0,0 +1,115 @@
import Node from '../Node'
// 检查是否存在概要
function checkHasGeneralization () {
return !!this.nodeData.data.generalization
}
// 创建概要节点
function createGeneralizationNode () {
if (this.isGeneralization || !this.checkHasGeneralization()) {
return
}
if (!this._generalizationLine) {
this._generalizationLine = this.draw.path()
}
if (!this._generalizationNode) {
this._generalizationNode = new Node({
data: {
data: this.nodeData.data.generalization
},
uid: this.mindMap.uid++,
renderer: this.renderer,
mindMap: this.mindMap,
draw: this.draw,
isGeneralization: true
})
this._generalizationNodeWidth = this._generalizationNode.width
this._generalizationNodeHeight = this._generalizationNode.height
this._generalizationNode.generalizationBelongNode = this
if (this.nodeData.data.generalization.isActive) {
this.renderer.addActiveNode(this._generalizationNode)
}
}
}
// 更新概要节点
function updateGeneralization () {
this.removeGeneralization()
this.createGeneralizationNode()
}
// 渲染概要节点
function renderGeneralization () {
if (this.isGeneralization) {
return
}
if (!this.checkHasGeneralization()) {
this.removeGeneralization()
this._generalizationNodeWidth = 0
this._generalizationNodeHeight = 0
return
}
if (this.nodeData.data.expand === false) {
this.removeGeneralization()
return
}
this.createGeneralizationNode()
this.renderer.layout.renderGeneralization(
this,
this._generalizationLine,
this._generalizationNode
)
this.style.generalizationLine(this._generalizationLine)
this._generalizationNode.render()
}
// 删除概要节点
function removeGeneralization () {
if (this._generalizationLine) {
this._generalizationLine.remove()
this._generalizationLine = null
}
if (this._generalizationNode) {
// 删除概要节点时要同步从激活节点里删除
this.renderer.removeActiveNode(this._generalizationNode)
this._generalizationNode.remove()
this._generalizationNode = null
}
// hack修复当激活一个节点时创建概要然后立即激活创建的概要节点后会重复创建概要节点并且无法删除的问题
if (this.generalizationBelongNode) {
this.draw
.find('.generalization_' + this.generalizationBelongNode.uid)
.remove()
}
}
// 隐藏概要节点
function hideGeneralization () {
if (this._generalizationLine) {
this._generalizationLine.hide()
}
if (this._generalizationNode) {
this._generalizationNode.hide()
}
}
// 显示概要节点
function showGeneralization () {
if (this._generalizationLine) {
this._generalizationLine.show()
}
if (this._generalizationNode) {
this._generalizationNode.show()
}
}
export default {
checkHasGeneralization,
createGeneralizationNode,
updateGeneralization,
renderGeneralization,
removeGeneralization,
hideGeneralization,
showGeneralization
}

File diff suppressed because one or more lines are too long

View File

@ -8,13 +8,12 @@
"lint": "vue-cli-service lint",
"autoBuildDoc": "node ./scripts/autoBuildDoc.js",
"buildDoc": "node ./scripts/buildDoc.js",
"buildLibrary": "vue-cli-service build --target lib --name simpleMindMap ../simple-mind-map/full.js --dest ../simple-mind-map/dist",
"electron:build": "vue-cli-service electron:build",
"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",
"start": "electron ."
"postuninstall": "electron-builder install-app-deps"
},
"main": "background.js",
"dependencies": {
@ -38,6 +37,7 @@
"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",

8630
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -8,9 +8,9 @@
"lint": "vue-cli-service lint",
"autoBuildDoc": "node ./scripts/autoBuildDoc.js",
"buildDoc": "node ./scripts/buildDoc.js",
"buildLibrary": "vue-cli-service build --target lib --name simpleMindMap ../simple-mind-map/full.js --dest ../simple-mind-map/dist",
"electron:build": "vue-cli-service electron:build",
"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"
@ -37,6 +37,7 @@
"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",

View File

@ -2,6 +2,8 @@ const path = require('path')
const fs = require('fs')
const hljs = require('highlight.js')
const md = require('markdown-it')({
html: true,
xhtmlOut: true,
highlight: function(str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,539 +0,0 @@
/* Logo 字体 */
@font-face {
font-family: "iconfont logo";
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg');
}
.logo {
font-family: "iconfont logo";
font-size: 160px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* tabs */
.nav-tabs {
position: relative;
}
.nav-tabs .nav-more {
position: absolute;
right: 0;
bottom: 0;
height: 42px;
line-height: 42px;
color: #666;
}
#tabs {
border-bottom: 1px solid #eee;
}
#tabs li {
cursor: pointer;
width: 100px;
height: 40px;
line-height: 40px;
text-align: center;
font-size: 16px;
border-bottom: 2px solid transparent;
position: relative;
z-index: 1;
margin-bottom: -1px;
color: #666;
}
#tabs .active {
border-bottom-color: #f00;
color: #222;
}
.tab-container .content {
display: none;
}
/* 页面布局 */
.main {
padding: 30px 100px;
width: 960px;
margin: 0 auto;
}
.main .logo {
color: #333;
text-align: left;
margin-bottom: 30px;
line-height: 1;
height: 110px;
margin-top: -50px;
overflow: hidden;
*zoom: 1;
}
.main .logo a {
font-size: 160px;
color: #333;
}
.helps {
margin-top: 40px;
}
.helps pre {
padding: 20px;
margin: 10px 0;
border: solid 1px #e7e1cd;
background-color: #fffdef;
overflow: auto;
}
.icon_lists {
width: 100% !important;
overflow: hidden;
*zoom: 1;
}
.icon_lists li {
width: 100px;
margin-bottom: 10px;
margin-right: 20px;
text-align: center;
list-style: none !important;
cursor: default;
}
.icon_lists li .code-name {
line-height: 1.2;
}
.icon_lists .icon {
display: block;
height: 100px;
line-height: 100px;
font-size: 42px;
margin: 10px auto;
color: #333;
-webkit-transition: font-size 0.25s linear, width 0.25s linear;
-moz-transition: font-size 0.25s linear, width 0.25s linear;
transition: font-size 0.25s linear, width 0.25s linear;
}
.icon_lists .icon:hover {
font-size: 100px;
}
.icon_lists .svg-icon {
/* 通过设置 font-size 来改变图标大小 */
width: 1em;
/* 图标和文字相邻时,垂直对齐 */
vertical-align: -0.15em;
/* 通过设置 color 来改变 SVG 的颜色/fill */
fill: currentColor;
/* path stroke 溢出 viewBox 部分在 IE 下会显示
normalize.css 中也包含这行 */
overflow: hidden;
}
.icon_lists li .name,
.icon_lists li .code-name {
color: #666;
}
/* markdown 样式 */
.markdown {
color: #666;
font-size: 14px;
line-height: 1.8;
}
.highlight {
line-height: 1.5;
}
.markdown img {
vertical-align: middle;
max-width: 100%;
}
.markdown h1 {
color: #404040;
font-weight: 500;
line-height: 40px;
margin-bottom: 24px;
}
.markdown h2,
.markdown h3,
.markdown h4,
.markdown h5,
.markdown h6 {
color: #404040;
margin: 1.6em 0 0.6em 0;
font-weight: 500;
clear: both;
}
.markdown h1 {
font-size: 28px;
}
.markdown h2 {
font-size: 22px;
}
.markdown h3 {
font-size: 16px;
}
.markdown h4 {
font-size: 14px;
}
.markdown h5 {
font-size: 12px;
}
.markdown h6 {
font-size: 12px;
}
.markdown hr {
height: 1px;
border: 0;
background: #e9e9e9;
margin: 16px 0;
clear: both;
}
.markdown p {
margin: 1em 0;
}
.markdown>p,
.markdown>blockquote,
.markdown>.highlight,
.markdown>ol,
.markdown>ul {
width: 80%;
}
.markdown ul>li {
list-style: circle;
}
.markdown>ul li,
.markdown blockquote ul>li {
margin-left: 20px;
padding-left: 4px;
}
.markdown>ul li p,
.markdown>ol li p {
margin: 0.6em 0;
}
.markdown ol>li {
list-style: decimal;
}
.markdown>ol li,
.markdown blockquote ol>li {
margin-left: 20px;
padding-left: 4px;
}
.markdown code {
margin: 0 3px;
padding: 0 5px;
background: #eee;
border-radius: 3px;
}
.markdown strong,
.markdown b {
font-weight: 600;
}
.markdown>table {
border-collapse: collapse;
border-spacing: 0px;
empty-cells: show;
border: 1px solid #e9e9e9;
width: 95%;
margin-bottom: 24px;
}
.markdown>table th {
white-space: nowrap;
color: #333;
font-weight: 600;
}
.markdown>table th,
.markdown>table td {
border: 1px solid #e9e9e9;
padding: 8px 16px;
text-align: left;
}
.markdown>table th {
background: #F7F7F7;
}
.markdown blockquote {
font-size: 90%;
color: #999;
border-left: 4px solid #e9e9e9;
padding-left: 0.8em;
margin: 1em 0;
}
.markdown blockquote p {
margin: 0;
}
.markdown .anchor {
opacity: 0;
transition: opacity 0.3s ease;
margin-left: 8px;
}
.markdown .waiting {
color: #ccc;
}
.markdown h1:hover .anchor,
.markdown h2:hover .anchor,
.markdown h3:hover .anchor,
.markdown h4:hover .anchor,
.markdown h5:hover .anchor,
.markdown h6:hover .anchor {
opacity: 1;
display: inline-block;
}
.markdown>br,
.markdown>p>br {
clear: both;
}
.hljs {
display: block;
background: white;
padding: 0.5em;
color: #333333;
overflow-x: auto;
}
.hljs-comment,
.hljs-meta {
color: #969896;
}
.hljs-string,
.hljs-variable,
.hljs-template-variable,
.hljs-strong,
.hljs-emphasis,
.hljs-quote {
color: #df5000;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-type {
color: #a71d5d;
}
.hljs-literal,
.hljs-symbol,
.hljs-bullet,
.hljs-attribute {
color: #0086b3;
}
.hljs-section,
.hljs-name {
color: #63a35c;
}
.hljs-tag {
color: #333333;
}
.hljs-title,
.hljs-attr,
.hljs-selector-id,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo {
color: #795da3;
}
.hljs-addition {
color: #55a532;
background-color: #eaffea;
}
.hljs-deletion {
color: #bd2c00;
background-color: #ffecec;
}
.hljs-link {
text-decoration: underline;
}
/* 代码高亮 */
/* PrismJS 1.15.0
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
/**
* prism.js default theme for JavaScript, CSS and HTML
* Based on dabblet (http://dabblet.com)
* @author Lea Verou
*/
code[class*="language-"],
pre[class*="language-"] {
color: black;
background: none;
text-shadow: 0 1px white;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*="language-"]::-moz-selection,
pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection,
code[class*="language-"] ::-moz-selection {
text-shadow: none;
background: #b3d4fc;
}
pre[class*="language-"]::selection,
pre[class*="language-"] ::selection,
code[class*="language-"]::selection,
code[class*="language-"] ::selection {
text-shadow: none;
background: #b3d4fc;
}
@media print {
code[class*="language-"],
pre[class*="language-"] {
text-shadow: none;
}
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
}
:not(pre)>code[class*="language-"],
pre[class*="language-"] {
background: #f5f2f0;
}
/* Inline code */
:not(pre)>code[class*="language-"] {
padding: .1em;
border-radius: .3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: slategray;
}
.token.punctuation {
color: #999;
}
.namespace {
opacity: .7;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #905;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #690;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #9a6e3a;
background: hsla(0, 0%, 100%, .5);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #07a;
}
.token.function,
.token.class-name {
color: #DD4A68;
}
.token.regex,
.token.important,
.token.variable {
color: #e90;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 2479351 */
src: url('iconfont.woff2?t=1677915953390') format('woff2'),
url('iconfont.woff?t=1677915953390') format('woff'),
url('iconfont.ttf?t=1677915953390') format('truetype');
src: url('iconfont.woff2?t=1679621707211') format('woff2'),
url('iconfont.woff?t=1679621707211') format('woff'),
url('iconfont.ttf?t=1679621707211') format('truetype');
}
.iconfont {
@ -13,6 +13,66 @@
-moz-osx-font-smoothing: grayscale;
}
.iconwenjian:before {
content: "\e607";
}
.iconpdf:before {
content: "\e740";
}
.iconPNG:before {
content: "\ec18";
}
.iconSVG:before {
content: "\e621";
}
.iconmarkdown:before {
content: "\ec04";
}
.iconjson:before {
content: "\ea42";
}
.iconlianjiexian:before {
content: "\e75b";
}
.iconbangzhu:before {
content: "\e620";
}
.iconshezhi:before {
content: "\e8b7";
}
.iconwushuju:before {
content: "\e643";
}
.iconzuijinliulan:before {
content: "\e62f";
}
.icon3zuidahua-3:before {
content: "\e692";
}
.iconzuixiaohua:before {
content: "\e650";
}
.iconzuidahua:before {
content: "\e651";
}
.iconguanbi:before {
content: "\e652";
}
.icondiannao:before {
content: "\eac0";
}

File diff suppressed because one or more lines are too long

View File

@ -1,415 +0,0 @@
{
"id": "2479351",
"name": "思绪",
"font_family": "iconfont",
"css_prefix_text": "icon",
"description": "思维导图",
"glyphs": [
{
"icon_id": "5387383",
"name": "电脑",
"font_class": "diannao",
"unicode": "eac0",
"unicode_decimal": 60096
},
{
"icon_id": "6940837",
"name": "主页",
"font_class": "zhuye",
"unicode": "e65c",
"unicode_decimal": 58972
},
{
"icon_id": "17859315",
"name": "本地@1x",
"font_class": "bendi1x",
"unicode": "e606",
"unicode_decimal": 58886
},
{
"icon_id": "1790495",
"name": "背景颜色",
"font_class": "beijingyanse",
"unicode": "e6f8",
"unicode_decimal": 59128
},
{
"icon_id": "11321310",
"name": "清除",
"font_class": "qingchu",
"unicode": "e605",
"unicode_decimal": 58885
},
{
"icon_id": "586787",
"name": "case",
"font_class": "case",
"unicode": "e6c6",
"unicode_decimal": 59078
},
{
"icon_id": "4354254",
"name": "形状-文字",
"font_class": "xingzhuang-wenzi",
"unicode": "eb99",
"unicode_decimal": 60313
},
{
"icon_id": "6337466",
"name": "字体加粗",
"font_class": "zitijiacu",
"unicode": "ec83",
"unicode_decimal": 60547
},
{
"icon_id": "6337470",
"name": "字体下划线",
"font_class": "zitixiahuaxian",
"unicode": "ec85",
"unicode_decimal": 60549
},
{
"icon_id": "6337471",
"name": "字体斜体",
"font_class": "zitixieti",
"unicode": "ec86",
"unicode_decimal": 60550
},
{
"icon_id": "11975179",
"name": "删除线",
"font_class": "shanchuxian",
"unicode": "e612",
"unicode_decimal": 58898
},
{
"icon_id": "34198316",
"name": "字体颜色",
"font_class": "zitiyanse",
"unicode": "e854",
"unicode_decimal": 59476
},
{
"icon_id": "8760187",
"name": "github",
"font_class": "github",
"unicode": "e64f",
"unicode_decimal": 58959
},
{
"icon_id": "1009019",
"name": "选择",
"font_class": "choose1",
"unicode": "e6c5",
"unicode_decimal": 59077
},
{
"icon_id": "493507",
"name": "主题",
"font_class": "zhuti",
"unicode": "e7aa",
"unicode_decimal": 59306
},
{
"icon_id": "1305460",
"name": "导出",
"font_class": "daochu1",
"unicode": "e63e",
"unicode_decimal": 58942
},
{
"icon_id": "4784101",
"name": "另存为",
"font_class": "lingcunwei",
"unicode": "e657",
"unicode_decimal": 58967
},
{
"icon_id": "9929033",
"name": "export",
"font_class": "export",
"unicode": "e642",
"unicode_decimal": 58946
},
{
"icon_id": "4570294",
"name": "打开",
"font_class": "dakai",
"unicode": "ebdf",
"unicode_decimal": 60383
},
{
"icon_id": "5086088",
"name": "新建",
"font_class": "xinjian",
"unicode": "e64e",
"unicode_decimal": 58958
},
{
"icon_id": "1117",
"name": "剪切",
"font_class": "jianqie",
"unicode": "e601",
"unicode_decimal": 58881
},
{
"icon_id": "1415523",
"name": "整理",
"font_class": "zhengli",
"unicode": "e83b",
"unicode_decimal": 59451
},
{
"icon_id": "2815710",
"name": "复制",
"font_class": "fuzhi",
"unicode": "e604",
"unicode_decimal": 58884
},
{
"icon_id": "11121506",
"name": "粘贴",
"font_class": "niantie",
"unicode": "e63f",
"unicode_decimal": 58943
},
{
"icon_id": "11383392",
"name": "上移",
"font_class": "shangyi",
"unicode": "e6be",
"unicode_decimal": 59070
},
{
"icon_id": "11383396",
"name": "下移",
"font_class": "xiayi",
"unicode": "e6bf",
"unicode_decimal": 59071
},
{
"icon_id": "14843439",
"name": "概括总览",
"font_class": "gaikuozonglan",
"unicode": "e609",
"unicode_decimal": 58889
},
{
"icon_id": "19738998",
"name": "全选",
"font_class": "quanxuan",
"unicode": "f199",
"unicode_decimal": 61849
},
{
"icon_id": "17606306",
"name": "导入",
"font_class": "daoru",
"unicode": "e6a3",
"unicode_decimal": 59043
},
{
"icon_id": "5110748",
"name": "后退-实",
"font_class": "houtui-shi",
"unicode": "e656",
"unicode_decimal": 58966
},
{
"icon_id": "14420971",
"name": "前进",
"font_class": "qianjin1",
"unicode": "e654",
"unicode_decimal": 58964
},
{
"icon_id": "1368553",
"name": "撤回",
"font_class": "withdraw",
"unicode": "e603",
"unicode_decimal": 58883
},
{
"icon_id": "15006636",
"name": "前进",
"font_class": "qianjin",
"unicode": "e600",
"unicode_decimal": 58880
},
{
"icon_id": "19980541",
"name": "恢复默认",
"font_class": "huifumoren",
"unicode": "e60e",
"unicode_decimal": 58894
},
{
"icon_id": "1616783",
"name": "换行",
"font_class": "huanhang",
"unicode": "e61e",
"unicode_decimal": 58910
},
{
"icon_id": "4777227",
"name": "缩小",
"font_class": "suoxiao",
"unicode": "ec13",
"unicode_decimal": 60435
},
{
"icon_id": "18811980",
"name": "编辑",
"font_class": "bianji",
"unicode": "e626",
"unicode_decimal": 58918
},
{
"icon_id": "21188137",
"name": "放大",
"font_class": "fangda",
"unicode": "e663",
"unicode_decimal": 58979
},
{
"icon_id": "21189639",
"name": "全屏",
"font_class": "quanping1",
"unicode": "e664",
"unicode_decimal": 58980
},
{
"icon_id": "397753",
"name": "定位",
"font_class": "dingwei",
"unicode": "e616",
"unicode_decimal": 58902
},
{
"icon_id": "2605158",
"name": "导航",
"font_class": "daohang",
"unicode": "e611",
"unicode_decimal": 58897
},
{
"icon_id": "6528451",
"name": "键盘",
"font_class": "jianpan",
"unicode": "e64d",
"unicode_decimal": 58957
},
{
"icon_id": "7556170",
"name": "全屏",
"font_class": "quanping",
"unicode": "e602",
"unicode_decimal": 58882
},
{
"icon_id": "788015",
"name": "导出",
"font_class": "daochu",
"unicode": "e63d",
"unicode_decimal": 58941
},
{
"icon_id": "2678575",
"name": "标签",
"font_class": "biaoqian",
"unicode": "e63c",
"unicode_decimal": 58940
},
{
"icon_id": "6265396",
"name": "流程-备注",
"font_class": "flow-Mark",
"unicode": "e65b",
"unicode_decimal": 58971
},
{
"icon_id": "1790486",
"name": "超链接",
"font_class": "chaolianjie",
"unicode": "e6f4",
"unicode_decimal": 59124
},
{
"icon_id": "4608986",
"name": "主题",
"font_class": "jingzi",
"unicode": "e610",
"unicode_decimal": 58896
},
{
"icon_id": "11903017",
"name": "笑脸",
"font_class": "xiaolian",
"unicode": "e60f",
"unicode_decimal": 58895
},
{
"icon_id": "19657962",
"name": "图 片",
"font_class": "image",
"unicode": "e629",
"unicode_decimal": 58921
},
{
"icon_id": "20784489",
"name": "结构",
"font_class": "jiegou",
"unicode": "e61d",
"unicode_decimal": 58909
},
{
"icon_id": "15969341",
"name": "样式",
"font_class": "yangshi",
"unicode": "e631",
"unicode_decimal": 58929
},
{
"icon_id": "2967176",
"name": "符号-大纲树",
"font_class": "fuhao-dagangshu",
"unicode": "e71f",
"unicode_decimal": 59167
},
{
"icon_id": "12316668",
"name": "添加子节点",
"font_class": "tianjiazijiedian",
"unicode": "e622",
"unicode_decimal": 58914
},
{
"icon_id": "14435368",
"name": "节点",
"font_class": "jiedian",
"unicode": "e655",
"unicode_decimal": 58965
},
{
"icon_id": "15765352",
"name": "删 除",
"font_class": "shanchu",
"unicode": "e696",
"unicode_decimal": 59030
},
{
"icon_id": "9592600",
"name": "HTSCIT_展开",
"font_class": "zhankai",
"unicode": "e64c",
"unicode_decimal": 58956
},
{
"icon_id": "9900009",
"name": "HTSCIT_展开2",
"font_class": "zhankai1",
"unicode": "e673",
"unicode_decimal": 58995
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

View File

@ -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'),
}

View File

@ -369,3 +369,43 @@ export const sidebarTriggerList = [
icon: 'iconjianpan'
}
]
// 下载类型列表
export const downTypeList = [
{
name: 'Dedicated file',
type: 'smm',
icon: 'iconwenjian',
desc: 'Available for import'
},
{
name: 'JSON',
type: 'json',
icon: 'iconjson',
desc: 'Popular data exchange formats, Available for import'
},
{
name: 'Image',
type: 'png',
icon: 'iconPNG',
desc: 'Suitable for viewing and sharing'
},
{
name: 'SVG',
type: 'svg',
icon: 'iconSVG',
desc: 'Scalable Vector Graphics'
},
{
name: 'PDF',
type: 'pdf',
icon: 'iconpdf',
desc: 'Suitable for printing'
},
{
name: 'Markdown',
type: 'md',
icon: 'iconmarkdown',
desc: 'Easy for other software to open'
}
]

64
web/src/config/icon.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -15,7 +15,8 @@ import {
shortcutKeyList as shortcutKeyListZh,
shapeList as shapeListZh,
sidebarTriggerList as sidebarTriggerListZh,
backgroundSizeList as backgroundSizeListZh
backgroundSizeList as backgroundSizeListZh,
downTypeList as downTypeListZh
} from './zh'
import {
fontFamilyList as fontFamilyListEn,
@ -26,7 +27,8 @@ import {
shortcutKeyList as shortcutKeyListEn,
shapeList as shapeListEn,
sidebarTriggerList as sidebarTriggerListEn,
backgroundSizeList as backgroundSizeListEn
backgroundSizeList as backgroundSizeListEn,
downTypeList as downTypeListEn
} from './en'
const fontFamilyList = {
@ -74,6 +76,11 @@ const sidebarTriggerList = {
en: sidebarTriggerListEn
}
const downTypeList = {
zh: downTypeListZh,
en: downTypeListEn
}
export {
fontSizeList,
lineHeightList,
@ -91,5 +98,6 @@ export {
backgroundSizeList,
shortcutKeyList,
shapeList,
sidebarTriggerList
sidebarTriggerList,
downTypeList
}

View File

@ -441,3 +441,43 @@ export const sidebarTriggerList = [
icon: 'iconjianpan'
}
]
// 下载类型列表
export const downTypeList = [
{
name: '专有文件',
type: 'smm',
icon: 'iconwenjian',
desc: '可用于导入'
},
{
name: 'JSON',
type: 'json',
icon: 'iconjson',
desc: '流行的数据交换格式,可用于导入'
},
{
name: '图片',
type: 'png',
icon: 'iconPNG',
desc: '适合查看分享'
},
{
name: 'SVG',
type: 'svg',
icon: 'iconSVG',
desc: '可缩放矢量图形'
},
{
name: 'PDF',
type: 'pdf',
icon: 'iconpdf',
desc: '适合打印'
},
{
name: 'Markdown',
type: 'md',
icon: 'iconmarkdown',
desc: '便于其他软件打开'
}
]

View File

@ -1,55 +0,0 @@
// 黑金
export default {
// 背景颜色
backgroundColor: 'rgb(18, 20, 20)',
// 连线的颜色
lineColor: 'rgb(205, 186, 156)',
lineWidth: 3,
// 概要连线的粗细
generalizationLineWidth: 3,
// 概要连线的颜色
generalizationLineColor: 'rgb(245, 224, 191)',
// 根节点样式
root: {
fillColor: 'rgb(255, 208, 124)',
color: 'rgb(111, 61, 6)',
borderColor: '',
borderWidth: 0,
fontSize: 24,
active: {
borderColor: '#fff',
borderWidth: 3,
}
},
// 二级节点样式
second: {
fillColor: 'rgb(66, 57, 46)',
color: 'rgb(225, 201, 158)',
borderColor: 'rgb(245, 224, 191)',
borderWidth: 2,
fontSize: 18,
active: {
borderColor: 'rgb(255, 208, 124)',
}
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: 'rgb(231, 203, 155)',
active: {
borderColor: 'rgb(255, 208, 124)'
}
},
// 概要节点样式
generalization: {
fontSize: 14,
fillColor: 'rgb(56, 45, 34)',
borderColor: 'rgb(104, 84, 61)',
borderWidth: 2,
color: 'rgb(242, 216, 176)',
active: {
borderColor: 'rgb(255, 208, 124)'
}
}
}

View File

@ -1,56 +0,0 @@
// 黑色幽默
export default {
// 背景颜色
backgroundColor: 'rgb(27, 31, 34)',
// 连线的颜色
lineColor: 'rgb(75, 81, 78)',
lineWidth: 3,
// 概要连线的粗细
generalizationLineWidth: 3,
// 概要连线的颜色
generalizationLineColor: 'rgb(255, 119, 34)',
// 根节点样式
root: {
fillColor: 'rgb(36, 179, 96)',
color: '#fff',
borderColor: '',
borderWidth: 0,
fontSize: 24,
active: {
borderColor: 'rgb(254, 199, 13)',
borderWidth: 3,
}
},
// 二级节点样式
second: {
fillColor: 'rgb(254, 199, 13)',
color: 'rgb(0, 0, 0)',
borderColor: '',
borderWidth: 0,
fontSize: 18,
active: {
borderColor: 'rgb(36, 179, 96)',
borderWidth: 3,
}
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: 'rgb(204, 204, 204)',
active: {
borderColor: 'rgb(254, 199, 13)'
}
},
// 概要节点样式
generalization: {
fontSize: 14,
fillColor: 'rgb(27, 31, 34)',
borderColor: 'rgb(255, 119, 34)',
borderWidth: 2,
color: 'rgb(204, 204, 204)',
active: {
borderColor: 'rgb(36, 179, 96)'
}
}
}

View File

@ -1,53 +0,0 @@
// 咖啡
export default {
// 连线的颜色
lineColor: 'rgb(173, 123, 91)',
lineWidth: 4,
// 概要连线的粗细
generalizationLineWidth: 4,
// 概要连线的颜色
generalizationLineColor: 'rgb(173, 123, 91)',
// 根节点样式
root: {
fillColor: 'rgb(202, 117, 79)',
color: '#fff',
borderColor: '',
borderWidth: 0,
fontSize: 24,
active: {
borderColor: 'rgb(173, 123, 91)',
borderWidth: 3,
}
},
// 二级节点样式
second: {
fillColor: 'rgb(245, 231, 216)',
color: 'rgb(125, 86, 42)',
borderColor: '',
borderWidth: 0,
fontSize: 18,
active: {
borderColor: 'rgb(173, 123, 91)',
}
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: 'rgb(96, 71, 47)',
active: {
borderColor: 'rgb(173, 123, 91)'
}
},
// 概要节点样式
generalization: {
fontSize: 14,
fillColor: 'rgb(255, 249, 239)',
borderColor: 'rgb(173, 123, 91)',
borderWidth: 2,
color: 'rgb(122, 83, 44)',
active: {
borderColor: 'rgb(202, 117, 79)'
}
}
}

View File

@ -1,53 +0,0 @@
// 课程绿
export default {
// 连线的颜色
lineColor: 'rgb(113, 195, 169)',
lineWidth: 3,
// 概要连线的粗细
generalizationLineWidth: 3,
// 概要连线的颜色
generalizationLineColor: 'rgb(113, 195, 169)',
// 根节点样式
root: {
fillColor: 'rgb(16, 160, 121)',
color: '#fff',
borderColor: '',
borderWidth: 0,
fontSize: 24,
active: {
borderColor: 'rgb(173, 91, 12)',
borderWidth: 3,
}
},
// 二级节点样式
second: {
fillColor: 'rgb(240, 252, 249)',
color: 'rgb(50, 113, 96)',
borderColor: 'rgb(113, 195, 169)',
borderWidth: 2,
fontSize: 18,
active: {
borderColor: 'rgb(173, 91, 12)',
}
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: 'rgb(10, 59, 43)',
active: {
borderColor: 'rgb(173, 91, 12)'
}
},
// 概要节点样式
generalization: {
fontSize: 14,
fillColor: 'rgb(246, 238, 211)',
borderColor: '',
borderWidth: 0,
color: 'rgb(173, 91, 12)',
active: {
borderColor: 'rgb(113, 195, 169)'
}
}
}

View File

@ -1,45 +1,9 @@
import simpleBlack from './simpleBlack'
import courseGreen from './courseGreen'
import coffee from './coffee'
import redSpirit from './redSpirit'
import blackHumour from './blackHumour'
import lateNightOffice from './lateNightOffice'
import blackGold from './blackGold'
export default [
{
name: '简约黑',
value: 'simpleBlack',
theme: simpleBlack
},
{
name: '课程绿',
value: 'courseGreen',
theme: courseGreen
},
{
name: '咖啡',
value: 'coffee',
theme: coffee
},
{
name: '红色精神',
value: 'redSpirit',
theme: redSpirit
},
{
name: '黑色幽默',
value: 'blackHumour',
theme: blackHumour
},
{
name: '深夜办公室',
value: 'lateNightOffice',
theme: lateNightOffice
},
{
name: '黑金',
value: 'blackGold',
theme: blackGold
}
]

View File

@ -1,56 +0,0 @@
// 深夜办公室
export default {
// 背景颜色
backgroundColor: 'rgb(32, 37, 49)',
// 连线的颜色
lineColor: 'rgb(137, 167, 196)',
lineWidth: 3,
// 概要连线的粗细
generalizationLineWidth: 3,
// 概要连线的颜色
generalizationLineColor: 'rgb(255, 119, 34)',
// 根节点样式
root: {
fillColor: 'rgb(23, 153, 243)',
color: 'rgb(255, 255, 255)',
borderColor: '',
borderWidth: 0,
fontSize: 24,
active: {
borderColor: 'rgb(255, 119, 34)',
borderWidth: 3,
}
},
// 二级节点样式
second: {
fillColor: 'rgb(70, 78, 94)',
color: 'rgb(209, 210, 210)',
borderColor: '',
borderWidth: 0,
fontSize: 18,
active: {
borderColor: 'rgb(255, 119, 34)',
borderWidth: 3,
}
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: 'rgb(204, 204, 204)',
active: {
borderColor: 'rgb(255, 119, 34)'
}
},
// 概要节点样式
generalization: {
fontSize: 14,
fillColor: 'rgb(255, 119, 34)',
borderColor: '',
borderWidth: 2,
color: '#fff',
active: {
borderColor: 'rgb(23, 153, 243)'
}
}
}

View File

@ -1,55 +0,0 @@
// 红色精神
export default {
// 背景颜色
backgroundColor: 'rgb(255, 238, 228)',
// 连线的颜色
lineColor: 'rgb(230, 138, 131)',
lineWidth: 3,
// 概要连线的粗细
generalizationLineWidth: 3,
// 概要连线的颜色
generalizationLineColor: 'rgb(222, 101, 85)',
// 根节点样式
root: {
fillColor: 'rgb(207, 44, 44)',
color: 'rgb(255, 233, 157)',
borderColor: '',
borderWidth: 0,
fontSize: 24,
active: {
borderColor: 'rgb(255, 233, 157)',
borderWidth: 3,
}
},
// 二级节点样式
second: {
fillColor: 'rgb(255, 255, 255)',
color: 'rgb(211, 58, 21)',
borderColor: 'rgb(222, 101, 85)',
borderWidth: 2,
fontSize: 18,
active: {
borderColor: 'rgb(255, 233, 157)',
}
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: 'rgb(144, 71, 43)',
active: {
borderColor: 'rgb(255, 233, 157)'
}
},
// 概要节点样式
generalization: {
fontSize: 14,
fillColor: 'rgb(255, 247, 211)',
borderColor: 'rgb(255, 202, 162)',
borderWidth: 2,
color: 'rgb(187, 101, 69)',
active: {
borderColor: 'rgb(222, 101, 85)'
}
}
}

View File

@ -35,7 +35,15 @@ export default {
watermarkAngle: 'Angle',
watermarkTextOpacity: 'Text opacity',
watermarkTextFontSize: 'Font size',
isEnableNodeRichText: 'Enable node rich text editing'
isEnableNodeRichText: 'Enable node rich text editing',
mousewheelAction: 'Mouse wheel behavior',
zoomView: 'Zoom view',
moveViewUpDown: 'Move view up and down',
associativeLine: 'Associative line',
associativeLineWidth: 'Width',
associativeLineColor: 'Color',
associativeLineActiveWidth: 'Active width',
associativeLineActiveColor: 'Active color'
},
color: {
moreColor: 'More color'
@ -80,13 +88,16 @@ export default {
imageFile: 'Image file',
svgFile: 'svg file',
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',
@ -95,7 +106,7 @@ export default {
import: {
title: 'Import',
selectFile: 'Select file',
supportFile: 'Support .smm、.json、.xmind、.xlsx file'
supportFile: 'Support .smm、.json、.xmind、.xlsx、.md file'
},
navigatorToolbar: {
openMiniMap: 'Open mini map',
@ -184,7 +195,8 @@ export default {
saveAs: 'Save as',
import: 'Import',
export: 'Export',
shortcutKey: 'Shortcut key'
shortcutKey: 'Shortcut key',
associativeLine: 'Associative line',
},
edit: {
newFeatureNoticeTitle: 'New feature reminder',

View File

@ -35,7 +35,15 @@ export default {
watermarkAngle: '旋转角度',
watermarkTextOpacity: '文字透明度',
watermarkTextFontSize: '文字字号',
isEnableNodeRichText: '是否开启节点富文本编辑'
isEnableNodeRichText: '是否开启节点富文本编辑',
mousewheelAction: '鼠标滚轮行为',
zoomView: '缩放视图',
moveViewUpDown: '上下移动视图',
associativeLine: '关联线',
associativeLineWidth: '粗细',
associativeLineColor: '颜色',
associativeLineActiveWidth: '激活粗细',
associativeLineActiveColor: '激活颜色'
},
color: {
moreColor: '更多颜色'
@ -80,13 +88,16 @@ export default {
imageFile: '图片文件',
svgFile: 'svg文件',
pdfFile: 'pdf文件',
markdownFile: 'markdown文件',
tips: 'tips.smm和.json文件可用于导入',
domToImage: '是否将svg中富文本节点转换成图片',
isTransparent: '背景是否透明',
pngTips: 'tips富文本模式导出图片非常耗时建议导出为svg格式',
svgTips: 'tips富文本模式导出图片非常耗时',
transformingDomToImages: '正在转换节点:',
notifyTitle: '消息',
notifyMessage: '如果没有触发下载,请检查是否被浏览器拦截了'
notifyMessage: '如果没有触发下载,请检查是否被浏览器拦截了',
paddingX: '水平内边距',
paddingY: '垂直内边距'
},
fullscreen: {
fullscreenShow: '全屏查看',
@ -95,7 +106,7 @@ export default {
import: {
title: '导入',
selectFile: '选取文件',
supportFile: '支持.smm、.json、.xmind、.xlsx文件'
supportFile: '支持.smm、.json、.xmind、.xlsx、.md文件'
},
navigatorToolbar: {
openMiniMap: '开启小地图',
@ -184,7 +195,8 @@ export default {
saveAs: '另存为',
import: '导入',
export: '导出',
shortcutKey: '快捷键'
shortcutKey: '快捷键',
associativeLine: '关联线',
},
edit: {
newFeatureNoticeTitle: '新特性提醒',

View File

@ -92,6 +92,10 @@ export default {
margin: 10px 0;
}
h4 {
margin-bottom: 10px;
}
p {
margin-bottom: 20px;
}

View File

@ -10,7 +10,10 @@ let langList = [
path: 'en'
}
]
let StartList = ['introduction', 'start', 'translate', 'changelog']
let StartList = ['introduction', 'start', 'deploy', 'translate', 'changelog']
let CourseList = new Array(19).fill(0).map((_, index) => {
return 'course' + (index + 1)
})
let APIList = [
'constructor',
'node',
@ -26,7 +29,9 @@ let APIList = [
'doExport',
'miniMap',
'watermark',
'associativeLine',
'xmind',
'markdown',
'utils'
]
@ -57,6 +62,10 @@ export default {
groupName: '开始',
list: createList('zh', StartList)
},
{
groupName: '教程',
list: createList('zh', CourseList)
},
{
groupName: 'API',
list: createList('zh', APIList)
@ -67,6 +76,10 @@ export default {
groupName: 'Start',
list: createList('en', StartList)
},
{
groupName: 'Course',
list: createList('zh', CourseList)
},
{
groupName: 'API',
list: createList('en', APIList)

View File

@ -0,0 +1,89 @@
# AssociativeLine plugin
> v0.4.5+
> The function of adjusting associated line control points is supported from v0.4.6+
> Relevance support for text editing starting from v0.5.11+
This plugin is used to support the addition of associative lines.
## Register
```js
import MindMap from 'simple-mind-map'
import AssociativeLine from 'simple-mind-map/src/AssociativeLine.js'
MindMap.usePlugin(AssociativeLine)
```
After registration and instantiation of `MindMap`, the instance can be obtained through `mindMap.associativeLine`.
## Config
Support for modifying the thickness and color of associated lines, divided into default and active states. The configuration is as follows:
- `associativeLineWidth`: The thickness of the default state of the associated line. The default value is `2`
- `associativeLineColor`: Color of the default state of associative lines. The default value is `rgb(51, 51, 51)`
- `associativeLineActiveWidth`: The thickness of the active state of the associated line. The default value is `8`
- `associativeLineActiveColor`: The color of the active state of the associated line. The default value is `rgba(2, 167, 240, 1)`
The configuration is provided as a theme, so if you want to modify these four properties, you can modify them using the `mindMap.setThemeConfig(config)` method.
## Props
### mindMap.associativeLine.lineList
Currently, all connection line data, array types, and each item of the array are also an array:
```js
[
path, // Connector node
clickPath, // Invisible click line node
node, // Start node
toNode // Target node
]
```
### mindMap.associativeLine.activeLine
The currently active connection line and array type are the same as the structure of each item in the `lineList` array.
## Methods
### renderAllLines()
Re-render all associated lines.
### removeAllLines()
Remove all associated lines.
### createLineFromActiveNode()
Create an associated line from the current active node. If there are multiple active nodes, the default is the first node.
After calling this method, an association line will be rendered from the first active node to the current mouse real-time position. When a target node is clicked, it represents completion of creation. An association line will be rendered between the first active node and the clicked node.
### createLine(fromNode)
Creates an associative line starting at the specified node.
After calling this method, an association line will be rendered from the specified node to the current mouse real-time position. When a target node is clicked, it represents completion of creation, and an association line will be rendered between the specified node and the clicked node.
### addLine(fromNode, toNode)
Add an associative line directly.
Calling this method will directly create an association line from the `fromNode` to the `toNode` node.
### removeLine()
Deletes the currently active associative line. Clicking on an associated line is considered active.
### clearActiveLine()
Clears the active state of the currently active association line.

View File

@ -0,0 +1,80 @@
<template>
<div>
<h1>AssociativeLine plugin</h1>
<blockquote>
<p>v0.4.5+</p>
</blockquote>
<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>
<h2>Register</h2>
<pre class="hljs"><code><span class="hljs-keyword">import</span> MindMap <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;simple-mind-map&#x27;</span>
<span class="hljs-keyword">import</span> AssociativeLine <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;simple-mind-map/src/AssociativeLine.js&#x27;</span>
MindMap.usePlugin(AssociativeLine)
</code></pre>
<p>After registration and instantiation of <code>MindMap</code>, the instance can be obtained through <code>mindMap.associativeLine</code>.</p>
<h2>Config</h2>
<p>Support for modifying the thickness and color of associated lines, divided into default and active states. The configuration is as follows:</p>
<ul>
<li>
<p><code>associativeLineWidth</code>: The thickness of the default state of the associated line. The default value is <code>2</code></p>
</li>
<li>
<p><code>associativeLineColor</code>: Color of the default state of associative lines. The default value is <code>rgb(51, 51, 51)</code></p>
</li>
<li>
<p><code>associativeLineActiveWidth</code>: The thickness of the active state of the associated line. The default value is <code>8</code></p>
</li>
<li>
<p><code>associativeLineActiveColor</code>: The color of the active state of the associated line. The default value is <code>rgba(2, 167, 240, 1)</code></p>
</li>
</ul>
<p>The configuration is provided as a theme, so if you want to modify these four properties, you can modify them using the <code>mindMap.setThemeConfig(config)</code> method.</p>
<h2>Props</h2>
<h3>mindMap.associativeLine.lineList</h3>
<p>Currently, all connection line data, array types, and each item of the array are also an array:</p>
<pre class="hljs"><code>[
path, <span class="hljs-comment">// Connector node</span>
clickPath, <span class="hljs-comment">// Invisible click line node</span>
node, <span class="hljs-comment">// Start node</span>
toNode <span class="hljs-comment">// Target node</span>
]
</code></pre>
<h3>mindMap.associativeLine.activeLine</h3>
<p>The currently active connection line and array type are the same as the structure of each item in the <code>lineList</code> array.</p>
<h2>Methods</h2>
<h3>renderAllLines()</h3>
<p>Re-render all associated lines.</p>
<h3>removeAllLines()</h3>
<p>Remove all associated lines.</p>
<h3>createLineFromActiveNode()</h3>
<p>Create an associated line from the current active node. If there are multiple active nodes, the default is the first node.</p>
<p>After calling this method, an association line will be rendered from the first active node to the current mouse real-time position. When a target node is clicked, it represents completion of creation. An association line will be rendered between the first active node and the clicked node.</p>
<h3>createLine(fromNode)</h3>
<p>Creates an associative line starting at the specified node.</p>
<p>After calling this method, an association line will be rendered from the specified node to the current mouse real-time position. When a target node is clicked, it represents completion of creation, and an association line will be rendered between the specified node and the clicked node.</p>
<h3>addLine(fromNode, toNode)</h3>
<p>Add an associative line directly.</p>
<p>Calling this method will directly create an association line from the <code>fromNode</code> to the <code>toNode</code> node.</p>
<h3>removeLine()</h3>
<p>Deletes the currently active associative line. Clicking on an associated line is considered active.</p>
<h3>clearActiveLine()</h3>
<p>Clears the active state of the currently active association line.</p>
</div>
</template>
<script>
export default {
}
</script>
<style>
</style>

View File

@ -1,5 +1,157 @@
# 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 changeIn 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. 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
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.
New: 1.Support setting the position of the initial central node.
### 0.5.3-fix.1
Fix: 1.Fix the issue where setting the position of the initial central node does not take effect.
### 0.5.3-fix.2
Fix: 1.Fix the issue of not displaying images in nodes when exporting as images.
## 0.5.2
Fix: 1.Remove `uid` from exported `JSON` data; 2.Clear the node cache pool when re rendering.
## 0.5.1
optimization: 1.Only respond to shortcut key events when the mouse is inside the canvas
Fix: 1.Fix the issue of incorrect node position during fast operation
## 0.5.0
This version is mainly about code level changes and optimization, with the core goal of improving rendering performance and reducing stuck issues.
New: 1.Support custom expansion and collapse node icons and colors;
optimization: 1.Optimize rendering logic, set the theme, move forward and backward, and other operations no longer require full rendering;
2.Optimize node drag logic, and fix the problem of being unable to drag between two nodes;
3.Collapse all nodes adds logic to return to the center point;
4.Fix the problem of nodes flying and scrambling caused by triggering rendering multiple times in a short time;
5.Optimize the experience of node editing;
Fix: 1.Fix the issue where the setData method does not trigger history;
modify: Starting from version 0.5.0, considering performance issues, the node activation state can only modify shape related styles:
```js
[
'fillColor',
'borderColor',
'borderWidth',
'borderDasharray',
'borderRadius'
]
```
## 0.4.7
optimization: 1.During rich text editing, when initially focusing, all are no longer selected by default; 2.When editing rich text, use the node fill color as the background color to avoid being invisible when the node color is white. 3.Node activation state switching no longer triggers history. 4.Triggering history multiple times in a short time will only add the last data. 5.Optimize the addition of historical records. When there is a rollback, delete the historical data after the current pointer when adding a new record again.
New: 1.Support for importing and exporting Markdown format files. 2.Support for configuring initial text when inserting nodes. 3.Expand the commands for inserting and deleting nodes to support specifying nodes.
## 0.4.6
New: 1.Associated lines support adjusting control points.
optimization: 1.When adding historical data, filter data that has not changed compared to the previous time.
Fix: 1.Fixed a conflict between the direction keys and the navigation function of the direction keys during node editing. 2.Fixed the issue of node id loss when dragging a mobile node, which can cause associated lines to be lost.
## 0.4.5
New: 1.Supports associative lines. 2.You can also drag the canvas by holding down the root node. 3. Hold down the ctrl key to adjust multiple selected nodes.
## 0.4.4
New: Support horizontal scrolling in response to the mouse.
## 0.4.3
Fix: No trigger after forward and backward `data_ Change` event.
New: Support user-defined mouse wheel events; The mouse wheel is adjusted to support zooming and moving the view up and down.
## 0.4.2
New: The `setText` method of the Node class adds a second parameter to support setting rich text content.

Some files were not shown because too many files have changed in this diff Show More