Merge branch 'main' into main

This commit is contained in:
街角小林
2023-08-07 16:25:08 +08:00
committed by GitHub
53 changed files with 939 additions and 276 deletions

View File

@@ -149,4 +149,8 @@ const mindMap = new MindMap({
<img src="./web/src/assets/avatar/千帆.jpg" style="width: 50px;height: 50px;" />
<span>千帆</span>
</span>
<span>
<img src="./web/src/assets/avatar/才镇.jpg" style="width: 50px;height: 50px;" />
<span>才镇</span>
</span>
</p>

View File

@@ -2,6 +2,7 @@ import MindMap from './index'
import MiniMap from './src/plugins/MiniMap.js'
import Watermark from './src/plugins/Watermark.js'
import KeyboardNavigation from './src/plugins/KeyboardNavigation.js'
import ExortXMind from './src/plugins/ExortXMind.js'
import ExportPDF from './src/plugins/ExportPDF.js'
import Export from './src/plugins/Export.js'
import Drag from './src/plugins/Drag.js'
@@ -11,6 +12,7 @@ import RichText from './src/plugins/RichText'
import NodeImgAdjust from './src/plugins/NodeImgAdjust.js'
import TouchEvent from './src/plugins/TouchEvent.js'
import Search from './src/plugins/Search.js'
import Painter from './src/plugins/Painter.js'
import xmind from './src/parse/xmind.js'
import markdown from './src/parse/markdown.js'
import icons from './src/svg/icons.js'
@@ -30,6 +32,7 @@ MindMap
.usePlugin(Watermark)
.usePlugin(Drag)
.usePlugin(KeyboardNavigation)
.usePlugin(ExortXMind)
.usePlugin(ExportPDF)
.usePlugin(Export)
.usePlugin(Select)
@@ -38,5 +41,6 @@ MindMap
.usePlugin(TouchEvent)
.usePlugin(NodeImgAdjust)
.usePlugin(Search)
.usePlugin(Painter)
export default MindMap

View File

@@ -1,11 +1,11 @@
{
"name": "simple-mind-map",
"version": "0.6.7",
"version": "0.6.11-fix.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"version": "0.6.7",
"version": "0.6.11-fix.1",
"license": "MIT",
"dependencies": {
"@svgdotjs/svg.js": "^3.0.16",

View File

@@ -323,7 +323,9 @@ export const nodeDataNoStylePropList = [
'richText',
'resetRichText',
'uid',
'activeStyle'
'activeStyle',
'associativeLineTargets',
'associativeLineTargetControlOffsets'
]
// 数据缓存

View File

@@ -122,5 +122,9 @@ export const defaultOpt = {
// 是否开启自定义节点内容
isUseCustomNodeContent: false,
// 自定义返回节点内容的方法
customCreateNodeContent: null
customCreateNodeContent: null,
// 指定内部一些元素节点文本编辑元素、节点备注显示元素、关联线文本编辑元素、节点图片调整按钮元素添加到的位置默认添加到document.body下
customInnerElsAppendTo: null,
// 拖拽元素时,指示元素新位置的块的最大高度
nodeDragPlaceholderMaxSize: 20
}

View File

@@ -153,9 +153,12 @@ class Render {
// 剪切节点
this.cutNode = this.cutNode.bind(this)
this.mindMap.command.add('CUT_NODE', this.cutNode)
// 修改节点样式
// 修改节点单个样式
this.setNodeStyle = this.setNodeStyle.bind(this)
this.mindMap.command.add('SET_NODE_STYLE', this.setNodeStyle)
// 修改节点多个样式
this.setNodeStyles = this.setNodeStyles.bind(this)
this.mindMap.command.add('SET_NODE_STYLES', this.setNodeStyles)
// 切换节点是否激活
this.setNodeActive = this.setNodeActive.bind(this)
this.mindMap.command.add('SET_NODE_ACTIVE', this.setNodeActive)
@@ -859,6 +862,42 @@ class Render {
}
}
// 设置节点多个样式
setNodeStyles(node, style, isActive) {
let data = {}
if (isActive) {
data = {
activeStyle: {
...(node.nodeData.data.activeStyle || {}),
...style
}
}
} else {
data = style
}
// 如果开启了富文本,则需要应用到富文本上
if (this.mindMap.richText) {
let config = this.mindMap.richText.normalStyleToRichTextStyle(style)
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)
// 更新了连线的样式
let props = Object.keys(style)
let hasLineStyleProps = false
props.forEach((key) => {
if (lineStyleProps.includes(key)) {
hasLineStyleProps = true
}
})
if (hasLineStyleProps) {
;(node.parent || node).renderLine(true)
}
}
// 设置节点是否激活
setNodeActive(node, active) {
this.setNodeData(node, {
@@ -1132,6 +1171,8 @@ class Render {
node.generalizationBelongNode.updateGeneralization()
}
if (!notRender) this.mindMap.render()
} else {
this.mindMap.emit('node_tree_render_end')
}
}

View File

@@ -12,6 +12,8 @@ export default class TextEdit {
this.textEditNode = null
// 隐藏的文本输入框
this.hiddenInputEl = null
// 节点激活时默认聚焦到隐藏输入框
this.enableFocus = true
// 文本编辑框是否显示
this.showTextEdit = false
// 如果编辑过程中缩放画布了,那么缓存当前编辑的内容
@@ -97,7 +99,17 @@ export default class TextEdit {
// 让隐藏的文本输入框聚焦
focusHiddenInput() {
if (this.hiddenInputEl) this.hiddenInputEl.focus()
if (this.hiddenInputEl && this.enableFocus) this.hiddenInputEl.focus()
}
// 关闭默认聚焦
stopFocusOnNodeActive() {
this.enableFocus = false
}
// 开启默认聚焦
openFocusOnNodeActive() {
this.enableFocus = true
}
// 注册临时快捷键
@@ -167,7 +179,11 @@ export default class TextEdit {
this.textEditNode.addEventListener('click', e => {
e.stopPropagation()
})
document.body.appendChild(this.textEditNode)
this.textEditNode.addEventListener('mousedown', (e) => {
e.stopPropagation()
})
const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body
targetNode.appendChild(this.textEditNode)
}
let scale = this.mindMap.view.scale
let lineHeight = node.style.merge('lineHeight')

View File

@@ -385,10 +385,10 @@ class Node {
this.active(e)
})
this.group.on('mousedown', e => {
if (this.isRoot && e.which === 3) {
if (this.isRoot && e.which === 3 && !this.mindMap.opt.readonly) {
e.stopPropagation()
}
if (!this.isRoot) {
if (!this.isRoot && !this.mindMap.opt.readonly) {
e.stopPropagation()
}
// 多选和取消多选
@@ -414,7 +414,7 @@ class Node {
this.mindMap.emit('node_mousedown', this, e)
})
this.group.on('mouseup', e => {
if (!this.isRoot) {
if (!this.isRoot && !this.mindMap.opt.readonly) {
e.stopPropagation()
}
this.mindMap.emit('node_mouseup', this, e)

View File

@@ -43,6 +43,11 @@ function setStyle(prop, value, isActive) {
this.mindMap.execCommand('SET_NODE_STYLE', this, prop, value, isActive)
}
// 修改多个样式
function setStyles(style, isActive) {
this.mindMap.execCommand('SET_NODE_STYLES', this, style, isActive)
}
export default {
setData,
setText,
@@ -52,5 +57,6 @@ export default {
setNote,
setTag,
setShape,
setStyle
setStyle,
setStyles
}

View File

@@ -268,7 +268,7 @@ function createNoteNode() {
if (!this.noteEl) {
this.noteEl = document.createElement('div')
this.noteEl.style.cssText = `
position: absolute;
position: fixed;
padding: 10px;
border-radius: 5px;
box-shadow: 0 2px 5px rgb(0 0 0 / 10%);
@@ -276,7 +276,8 @@ function createNoteNode() {
background-color: #fff;
z-index: ${ this.mindMap.opt.nodeNoteTooltipZIndex }
`
document.body.appendChild(this.noteEl)
const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body
targetNode.appendChild(this.noteEl)
}
this.noteEl.innerText = this.nodeData.data.note
}

View File

@@ -234,7 +234,7 @@ class LogicalStructure extends Base {
let nodeUseLineStylePath = nodeUseLineStyle
? ` L ${item.left + item.width},${y2}`
: ''
if (node.isRoot) {
if (node.isRoot && !this.mindMap.themeConfig.rootLineKeepSameInCurve) {
path = this.quadraticCurvePath(x1, y1, x2, y2) + nodeUseLineStylePath
} else {
path = this.cubicBezierPath(x1, y1, x2, y2) + nodeUseLineStylePath

View File

@@ -315,7 +315,7 @@ class MindMap extends Base {
nodeUseLineStylePath = ` L ${item.left + item.width},${y2}`
}
}
if (node.isRoot) {
if (node.isRoot && !this.mindMap.themeConfig.rootLineKeepSameInCurve) {
path = this.quadraticCurvePath(x1, y1, x2, y2) + nodeUseLineStylePath
} else {
path = this.cubicBezierPath(x1, y1, x2, y2) + nodeUseLineStylePath

View File

@@ -44,6 +44,7 @@ class Drag extends Base {
this.mouseMoveY = 0
// 鼠标移动的距离距鼠标按下的位置距离多少以上才认为是拖动事件
this.checkDragOffset = 10
this.minOffset = 10
}
// 绑定事件
@@ -105,6 +106,9 @@ class Drag extends Base {
this.node.isDrag = false
this.node.show()
this.removeCloneNode()
let overlapNodeUid = this.overlapNode ? this.overlapNode.nodeData.data.uid : ''
let prevNodeUid = this.prevNode ? this.prevNode.nodeData.data.uid : ''
let nextNodeUid = this.nextNode ? this.nextNode.nodeData.data.uid : ''
// 存在重叠子节点,则移动作为其子节点
if (this.overlapNode) {
this.mindMap.renderer.setNodeActive(this.overlapNode, false)
@@ -134,7 +138,11 @@ class Drag extends Base {
this.mindMap.render()
}
this.reset()
this.mindMap.emit('node_dragend')
this.mindMap.emit('node_dragend', {
overlapNodeUid,
prevNodeUid,
nextNodeUid
})
}
// 创建克隆节点
@@ -199,6 +207,7 @@ class Drag extends Base {
if (!this.drawTransform) {
return
}
const { nodeDragPlaceholderMaxSize } = this.mindMap.opt
let x = this.mouseMoveX
let y = this.mouseMoveY
this.overlapNode = null
@@ -240,19 +249,19 @@ class Drag extends Base {
let prevNodeRect = this.getNodeRect(prevBrother)
prevBrotherOffset = nodeRect.top - prevNodeRect.bottom
// 间距小于10就当它不存在
prevBrotherOffset = prevBrotherOffset >= 10 ? prevBrotherOffset / 2 : 0
prevBrotherOffset = prevBrotherOffset >= this.minOffset ? prevBrotherOffset / 2 : 0
} else {
// 没有前一个兄弟节点那么假设和前一个节点的距离为20
prevBrotherOffset = 10
prevBrotherOffset = this.minOffset
}
// 和后一个兄弟节点的距离
let nextBrotherOffset = 0
if (nextBrother) {
let nextNodeRect = this.getNodeRect(nextBrother)
nextBrotherOffset = nextNodeRect.top - nodeRect.bottom
nextBrotherOffset = nextBrotherOffset >= 10 ? nextBrotherOffset / 2 : 0
nextBrotherOffset = nextBrotherOffset >= this.minOffset ? nextBrotherOffset / 2 : 0
} else {
nextBrotherOffset = 10
nextBrotherOffset = this.minOffset
}
if (nodeRect.left <= x && nodeRect.right >= x) {
// 检测兄弟节点位置
@@ -265,11 +274,11 @@ class Drag extends Base {
y >= nodeRect.top && y <= nodeRect.top + oneFourthHeight
if (checkIsPrevNode) {
this.prevNode = node
let size = nextBrotherOffset > 0 ? nextBrotherOffset : 5
let size = nextBrotherOffset > 0 ? Math.min(nextBrotherOffset, nodeDragPlaceholderMaxSize) : 5
this.placeholder.size(node.width, size).move(nodeRect.originLeft, nodeRect.originBottom)
} else if (checkIsNextNode) {
this.nextNode = node
let size = prevBrotherOffset > 0 ? prevBrotherOffset : 5
let size = prevBrotherOffset > 0 ? Math.min(prevBrotherOffset, nodeDragPlaceholderMaxSize) : 5
this.placeholder.size(node.width, size).move(nodeRect.originLeft, nodeRect.originTop - size)
}
}

View File

@@ -25,6 +25,11 @@ class MiniMap {
let { svg, rect, origWidth, origHeight, scaleX, scaleY } =
this.mindMap.getSvgData()
// 计算数据
const elRect = this.mindMap.elRect
rect.x -= elRect.left
rect.x2 -= elRect.left
rect.y -= elRect.top
rect.y2 -= elRect.top
let boxRatio = boxWidth / boxHeight
let actWidth = 0
let actHeight = 0
@@ -55,19 +60,28 @@ class MiniMap {
bottom: 0
}
viewBoxStyle.left =
Math.max(0, (-_rectX / _rectWidth) * actWidth) + miniMapBoxLeft + 'px'
Math.max(0, (-_rectX / _rectWidth) * actWidth) + miniMapBoxLeft
viewBoxStyle.right =
Math.max(0, ((_rectX2 - origWidth) / _rectWidth) * actWidth) +
miniMapBoxLeft +
'px'
miniMapBoxLeft
viewBoxStyle.top =
Math.max(0, (-_rectY / _rectHeight) * actHeight) + miniMapBoxTop + 'px'
Math.max(0, (-_rectY / _rectHeight) * actHeight) + miniMapBoxTop
viewBoxStyle.bottom =
Math.max(0, ((_rectY2 - origHeight) / _rectHeight) * actHeight) +
miniMapBoxTop +
'px'
miniMapBoxTop
if (viewBoxStyle.top > miniMapBoxTop + actHeight) {
viewBoxStyle.top = miniMapBoxTop + actHeight
}
if (viewBoxStyle.left > miniMapBoxLeft + actWidth) {
viewBoxStyle.left = miniMapBoxLeft + actWidth
}
Object.keys(viewBoxStyle).forEach((key) => {
viewBoxStyle[key] = viewBoxStyle[key] + 'px'
})
this.removeNodeContent(svg)
return {
svgHTML: svg.svg(), // 小地图html

View File

@@ -191,6 +191,12 @@ class NodeImgAdjust {
btnRemove.addEventListener('click', e => {
this.mindMap.execCommand('SET_NODE_IMAGE',this.node,{url:null});
});
btnEl.addEventListener('click', e => {
e.stopPropagation()
})
btnEl.addEventListener('mousedown', (e) => {
e.stopPropagation()
})
}
// 鼠标按钮按下事件

View File

@@ -0,0 +1,76 @@
import { nodeDataNoStylePropList } from '../constants/constant'
// 格式刷插件
class Painter {
constructor({ mindMap }) {
this.mindMap = mindMap
this.isInPainter = false
this.painterNode = null
this.bindEvent()
}
bindEvent() {
this.painterOneNode = this.painterOneNode.bind(this)
this.onEndPainter = this.onEndPainter.bind(this)
this.mindMap.on('node_click', this.painterOneNode)
this.mindMap.on('draw_click', this.onEndPainter)
}
unBindEvent() {
this.mindMap.off('node_click', this.painterOneNode)
this.mindMap.off('draw_click', this.onEndPainter)
}
// 开始格式刷
startPainter() {
if (this.mindMap.opt.readonly) return
let activeNodeList = this.mindMap.renderer.activeNodeList
if (activeNodeList.length <= 0) return
this.painterNode = activeNodeList[0]
this.isInPainter = true
this.mindMap.emit('painter_start')
}
// 结束格式刷
endPainter() {
this.painterNode = null
this.isInPainter = false
}
onEndPainter() {
this.endPainter()
this.mindMap.emit('painter_end')
}
// 格式刷某个节点
painterOneNode(node) {
if (
!node ||
!this.isInPainter ||
!this.painterNode ||
!node ||
node === this.painterNode
)
return
const style = {}
const painterNodeData = this.painterNode.nodeData.data
Object.keys(painterNodeData).forEach(key => {
if (!nodeDataNoStylePropList.includes(key)) {
style[key] = painterNodeData[key]
}
})
node.setStyles(style)
if (painterNodeData.activeStyle) {
node.setStyles(painterNodeData.activeStyle, true)
}
}
// 插件被移除前做的事情
beforePluginRemove() {
this.unBindEvent()
}
}
Painter.instanceName = 'painter'
export default Painter

View File

@@ -172,7 +172,11 @@ class RichText {
this.textEditNode.addEventListener('click', e => {
e.stopPropagation()
})
document.body.appendChild(this.textEditNode)
this.textEditNode.addEventListener('mousedown', (e) => {
e.stopPropagation()
})
const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body
targetNode.appendChild(this.textEditNode)
}
// 使用节点的填充色,否则如果节点颜色是白色的话编辑时看不见
let bgColor = node.style.merge('fillColor')

View File

@@ -1,4 +1,4 @@
import { bfsWalk, getTextFromHtml, isUndef } from '../utils/index'
import { bfsWalk, getTextFromHtml, isUndef, replaceHtmlText } from '../utils/index'
// 搜索插件
class Search {
@@ -15,12 +15,19 @@ class Search {
this.currentIndex = -1
// 不要复位搜索文本
this.notResetSearchText = false
// 是否自动跳转下一个匹配节点
this.isJumpNext = false
this.onDataChange = this.onDataChange.bind(this)
this.mindMap.on('data_change', this.onDataChange)
}
// 节点数据改变了,需要重新搜索
onDataChange() {
if (this.isJumpNext) {
this.isJumpNext = false
this.search(this.searchText)
return
}
if (this.notResetSearchText) {
this.notResetSearchText = false
return
@@ -29,7 +36,7 @@ class Search {
}
// 搜索
search(text, callback) {
search(text, callback = () => {}) {
if (isUndef(text)) return this.endSearch()
text = String(text)
this.isSearching = true
@@ -88,13 +95,16 @@ class Search {
}
// 替换当前节点
replace(replaceText) {
replace(replaceText, jumpNext = false) {
if (
isUndef(replaceText) ||
replaceText === null ||
replaceText === undefined ||
!this.isSearching ||
this.matchNodeList.length <= 0
)
return
// 自动跳转下一个匹配节点
this.isJumpNext = jumpNext
replaceText = String(replaceText)
let currentNode = this.matchNodeList[this.currentIndex]
if (!currentNode) return
@@ -115,7 +125,8 @@ class Search {
// 替换所有
replaceAll(replaceText) {
if (
isUndef(replaceText) ||
replaceText === null ||
replaceText === undefined ||
!this.isSearching ||
this.matchNodeList.length <= 0
)
@@ -141,9 +152,10 @@ class Search {
getReplacedText(node, searchText, replaceText) {
let { richText, text } = node.nodeData.data
if (richText) {
text = getTextFromHtml(text)
return replaceHtmlText(text, searchText, replaceText)
} else {
return text.replaceAll(searchText, replaceText)
}
return text.replaceAll(searchText, replaceText)
}
// 发送事件

View File

@@ -59,10 +59,10 @@ class TouchEvent {
let cy = (touch1ClientY + touch2ClientY) / 2
if (distance > this.doubleTouchmoveDistance) {
// 放大
this.mindMap.view.enlarge(cx, cy)
this.mindMap.view.enlarge(cx, cy, true)
} else {
// 缩小
this.mindMap.view.narrow(cx, cy)
this.mindMap.view.narrow(cx, cy, true)
}
this.doubleTouchmoveDistance = distance
}

View File

@@ -47,7 +47,8 @@ function showEditTextBox(g) {
this.textEditNode.addEventListener('click', e => {
e.stopPropagation()
})
document.body.appendChild(this.textEditNode)
const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body
targetNode.appendChild(this.textEditNode)
}
let {
associativeLineTextFontSize,

View File

@@ -18,6 +18,8 @@ export default {
lineDasharray: 'none',
// 连线风格
lineStyle: 'straight', // 针对logicalStructure、mindMap两种结构。曲线curve、直线straight、直连direct
// 曲线连接时,根节点和其他节点的连接线样式保持统一,默认根节点为 ( 型,其他节点为 { 型设为true后都为 { 型
rootLineKeepSameInCurve: true,
// 概要连线的粗细
generalizationLineWidth: 1,
// 概要连线的颜色

View File

@@ -539,4 +539,59 @@ export const getVisibleColorFromTheme = (themeConfig) => {
return color
}
}
}
// 将<p><span></span><p>形式的节点富文本内容转换成<br>换行的文本
let nodeRichTextToTextWithWrapEl = null
export const nodeRichTextToTextWithWrap = (html) => {
if (!nodeRichTextToTextWithWrapEl) {
nodeRichTextToTextWithWrapEl = document.createElement('div')
}
nodeRichTextToTextWithWrapEl.innerHTML = html
const childNodes = nodeRichTextToTextWithWrapEl.childNodes
let res = ''
for(let i = 0; i < childNodes.length; i++) {
const node = childNodes[i]
if (node.nodeType === 1) {// 元素节点
if (node.tagName.toLowerCase() === 'p') {
res += node.textContent + '\n'
} else {
res += node.textContent
}
} else if (node.nodeType === 3) {// 文本节点
res += node.nodeValue
}
}
return res.replace(/\n$/, '')
}
// 将<br>换行的文本转换成<p><span></span><p>形式的节点富文本内容
let textToNodeRichTextWithWrapEl = null
export const textToNodeRichTextWithWrap = (html) => {
if (!textToNodeRichTextWithWrapEl) {
textToNodeRichTextWithWrapEl = document.createElement('div')
}
textToNodeRichTextWithWrapEl.innerHTML = html
const childNodes = textToNodeRichTextWithWrapEl.childNodes
let list = []
let str = ''
for(let i = 0; i < childNodes.length; i++) {
const node = childNodes[i]
if (node.nodeType === 1) {// 元素节点
if (node.tagName.toLowerCase() === 'br') {
list.push(str)
str = ''
} else {
str += node.textContent
}
} else if (node.nodeType === 3) {// 文本节点
str += node.nodeValue
}
}
if (str) {
list.push(str)
}
return list.map((item) => {
return `<p><span>${item}</span></p>`
}).join('')
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

View File

@@ -102,6 +102,18 @@ export const lineStyleList = [
}
]
// 曲线风格中,根节点样式是否和其他节点保持一致
export const rootLineKeepSameInCurveList = [
{
name: 'Bracket',
value: false
},
{
name: 'Brace',
value: true
}
]
// 图片重复方式
export const backgroundRepeatList = [
{

View File

@@ -10,6 +10,7 @@ import {
fontFamilyList as fontFamilyListZh,
borderDasharrayList as borderDasharrayListZh,
lineStyleList as lineStyleListZh,
rootLineKeepSameInCurveList as rootLineKeepSameInCurveListZh,
backgroundRepeatList as backgroundRepeatListZh,
backgroundPositionList as backgroundPositionListZh,
shortcutKeyList as shortcutKeyListZh,
@@ -22,6 +23,7 @@ import {
fontFamilyList as fontFamilyListEn,
borderDasharrayList as borderDasharrayListEn,
lineStyleList as lineStyleListEn,
rootLineKeepSameInCurveList as rootLineKeepSameInCurveListEn,
backgroundRepeatList as backgroundRepeatListEn,
backgroundPositionList as backgroundPositionListEn,
shortcutKeyList as shortcutKeyListEn,
@@ -46,6 +48,11 @@ const lineStyleList = {
en: lineStyleListEn
}
const rootLineKeepSameInCurveList = {
zh: rootLineKeepSameInCurveListZh,
en: rootLineKeepSameInCurveListEn
}
const backgroundRepeatList = {
zh: backgroundRepeatListZh,
en: backgroundRepeatListEn
@@ -93,6 +100,7 @@ export {
fontFamilyList,
borderDasharrayList,
lineStyleList,
rootLineKeepSameInCurveList,
backgroundRepeatList,
backgroundPositionList,
backgroundSizeList,

View File

@@ -157,6 +157,18 @@ export const lineStyleList = [
}
]
// 曲线风格中,根节点样式是否和其他节点保持一致
export const rootLineKeepSameInCurveList = [
{
name: '括号',
value: false
},
{
name: '大括号',
value: true
}
]
// 图片重复方式
export const backgroundRepeatList = [
{

View File

@@ -46,7 +46,8 @@ export default {
associativeLineActiveColor: 'Active color',
mousewheelZoomActionReverse: 'Mouse Wheel Zoom',
mousewheelZoomActionReverse1: 'Zoom out forward and zoom in back',
mousewheelZoomActionReverse2: 'Zoom in forward and zoom out back'
mousewheelZoomActionReverse2: 'Zoom in forward and zoom out back',
rootStyle: 'Root Node'
},
color: {
moreColor: 'More color'
@@ -202,6 +203,7 @@ export default {
export: 'Export',
shortcutKey: 'Shortcut key',
associativeLine: 'Associative line',
painter: 'Painter'
},
edit: {
newFeatureNoticeTitle: 'New feature reminder',

View File

@@ -46,7 +46,8 @@ export default {
associativeLineActiveColor: '激活颜色',
mousewheelZoomActionReverse: '鼠标滚轮缩放',
mousewheelZoomActionReverse1: '向前缩小向后放大',
mousewheelZoomActionReverse2: '向前放大向后缩小'
mousewheelZoomActionReverse2: '向前放大向后缩小',
rootStyle: '根节点'
},
color: {
moreColor: '更多颜色'
@@ -202,6 +203,7 @@ export default {
export: '导出',
shortcutKey: '快捷键',
associativeLine: '关联线',
painter: '格式刷'
},
edit: {
newFeatureNoticeTitle: '新特性提醒',

View File

@@ -71,6 +71,7 @@ const mindMap = new MindMap({
| isUseCustomNodeContentv0.6.3+ | Boolean | false | Whether to customize node content | |
| customCreateNodeContentv0.6.3+ | Function/null | null | If `isUseCustomNodeContent` is set to `true`, then this option needs to be used to pass in a method that receives the node instance `node` as a parameter (if you want to obtain data for that node, you can use `node.nodeData.data`). You need to return the custom node content element, which is the DOM node. If a node does not require customization, you can return `null` | |
| mouseScaleCenterUseMousePositionv0.6.4-fix.1+ | Boolean | true | Is the mouse zoom centered around the current position of the mouse, otherwise centered around the canvas | |
| customInnerElsAppendTov0.6.12+ | null/HTMLElement | null | Specify the location where some internal elements (node text editing element, node note display element, associated line text editing element, node image adjustment button element) are added, and default to document.body | |
### Watermark config
@@ -242,7 +243,7 @@ Listen to an event. Event list:
| rich_text_selection_changev0.4.0+ | Available when the `RichText` plugin is registered. Triggered when the text selection area changes when the node is edited | hasRangeWhether there is a selection、rectInfoSize and location information of the selected area、formatInfoText formatting information of the selected area |
| transforming-dom-to-imagesv0.4.0+ | Available when the `RichText` plugin is registered. When there is a `DOM` node in `svg`, the `DOM` node will be converted to an image when exporting to an image. This event will be triggered during the conversion process. You can use this event to prompt the user about the node to which you are currently converting | indexIndex of the node currently converted to、lenTotal number of nodes to be converted |
| node_draggingv0.4.5+ | Triggered when a node is dragged | node(The currently dragged node) |
| node_dragendv0.4.5+ | Triggered when the node is dragged and ends | |
| node_dragendv0.4.5+ | Triggered when the node is dragged and ends | { overlapNodeUid, prevNodeUid, nextNodeUid }v0.6.12+The node uid to which the node is moved this time, for example, if it is moved to node A, then the overlayNodeUid is the uid of node A. If it is moved to the front of node B, then the nextNodeUid is the uid of node B. You can obtain the node instance through the mindMap. extender.findNodeByUid(uid) method |
| associative_line_clickv0.4.5+ | Triggered when an associated line is clicked | path(Connector node)、clickPath(Invisible click line node)、node(Start node)、toNode(Target node) |
| svg_mouseenterv0.5.1+ | Triggered when the mouse moves into the SVG canvas | eevent object |
| svg_mouseleavev0.5.1+ | Triggered when the mouse moves out of the SVG canvas | eevent object |

View File

@@ -357,6 +357,13 @@
<td>Is the mouse zoom centered around the current position of the mouse, otherwise centered around the canvas</td>
<td></td>
</tr>
<tr>
<td>customInnerElsAppendTov0.6.12+</td>
<td>null/HTMLElement</td>
<td>null</td>
<td>Specify the location where some internal elements (node text editing element, node note display element, associated line text editing element, node image adjustment button element) are added, and default to document.body</td>
<td></td>
</tr>
</tbody>
</table>
<h3>Watermark config</h3>
@@ -697,7 +704,7 @@ poor performance and should be used sparingly.</p>
<tr>
<td>node_dragendv0.4.5+</td>
<td>Triggered when the node is dragged and ends</td>
<td></td>
<td>{ overlapNodeUid, prevNodeUid, nextNodeUid }v0.6.12+The node uid to which the node is moved this time, for example, if it is moved to node A, then the overlayNodeUid is the uid of node A. If it is moved to the front of node B, then the nextNodeUid is the uid of node B. You can obtain the node instance through the mindMap. extender.findNodeByUid(uid) method</td>
</tr>
<tr>
<td>associative_line_clickv0.4.5+</td>

View File

@@ -167,4 +167,8 @@ Open source is not easy. If this project is helpful to you, you can invite the a
<img src="../../../../assets/avatar/千帆.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>千帆</p>
</div>
<div style="display: flex; flex-direction: column; align-items: center; width: fit-content; margin: 5px;">
<img src="../../../../assets/avatar/才镇.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>才镇</p>
</div>
</div>

View File

@@ -126,6 +126,10 @@ full screen, support mini map</li>
<img src="../../../../assets/avatar/千帆.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>千帆</p>
</div>
<div style="display: flex; flex-direction: column; align-items: center; width: fit-content; margin: 5px;">
<img src="../../../../assets/avatar/才镇.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>才镇</p>
</div>
</div>
</div>
</template>

View File

@@ -71,6 +71,7 @@ const mindMap = new MindMap({
| isUseCustomNodeContentv0.6.3+ | Boolean | false | 是否自定义节点内容 | |
| customCreateNodeContentv0.6.3+ | Function/null | null | 如果`isUseCustomNodeContent`设为`true`,那么需要使用该选项传入一个方法,接收节点实例`node`为参数(如果要获取该节点的数据,可以通过`node.nodeData.data`需要返回自定义节点内容元素也就是DOM节点如果某个节点不需要自定义那么返回`null`即可 | |
| mouseScaleCenterUseMousePositionv0.6.4-fix.1+ | Boolean | true | 鼠标缩放是否以鼠标当前位置为中心点,否则以画布中心点 | |
| customInnerElsAppendTov0.6.12+ | null/HTMLElement | null | 指定内部一些元素节点文本编辑元素、节点备注显示元素、关联线文本编辑元素、节点图片调整按钮元素添加到的位置默认添加到document.body下 | |
### 水印配置
@@ -237,7 +238,7 @@ mindMap.setTheme('主题名称')
| rich_text_selection_changev0.4.0+ | 当注册了`RichText`插件时可用。当节点编辑时,文本选区发生改变时触发 | hasRange是否存在选区、rectInfo选区的尺寸和位置信息、formatInfo选区的文本格式化信息 |
| transforming-dom-to-imagesv0.4.0+ | 当注册了`RichText`插件时可用。当`svg`中存在`DOM`节点时,导出为图片时会将`DOM`节点转换为图片,转换过程中会触发该事件,可用通过该事件给用户提示,告知目前转换到的节点 | index当前转换到的节点索引、len一共需要转换的节点数量 |
| node_draggingv0.4.5+ | 当某个节点被拖拽时触发 | node当前被拖拽的节点 |
| node_dragendv0.4.5+ | 节点被拖拽结束时触发 | |
| node_dragendv0.4.5+ | 节点被拖拽结束时触发 | { overlapNodeUid, prevNodeUid, nextNodeUid }v0.6.12+本次节点移动到的节点uid比如本次移动到了节点A上那么overlapNodeUid就是节点A的uid如果移动到了B节点的前面那么nextNodeUid就是节点B的uid你可以通过mindMap.renderer.findNodeByUid(uid)方法来获取节点实例) |
| associative_line_clickv0.4.5+ | 点击某条关联线时触发 | path连接线节点、clickPath不可见的点击线节点、node起始节点、toNode目标节点 |
| svg_mouseenterv0.5.1+ | 鼠标移入svg画布时触发 | e事件对象 |
| svg_mouseleavev0.5.1+ | 鼠标移出svg画布时触发 | e事件对象 |

View File

@@ -357,6 +357,13 @@
<td>鼠标缩放是否以鼠标当前位置为中心点否则以画布中心点</td>
<td></td>
</tr>
<tr>
<td>customInnerElsAppendTov0.6.12+</td>
<td>null/HTMLElement</td>
<td>null</td>
<td>指定内部一些元素节点文本编辑元素节点备注显示元素关联线文本编辑元素节点图片调整按钮元素添加到的位置默认添加到document.body下</td>
<td></td>
</tr>
</tbody>
</table>
<h3>水印配置</h3>
@@ -690,7 +697,7 @@ mindMap.setTheme(<span class="hljs-string">&#x27;主题名称&#x27;</span>)
<tr>
<td>node_dragendv0.4.5+</td>
<td>节点被拖拽结束时触发</td>
<td></td>
<td>{ overlapNodeUid, prevNodeUid, nextNodeUid }v0.6.12+本次节点移动到的节点uid比如本次移动到了节点A上那么overlapNodeUid就是节点A的uid如果移动到了B节点的前面那么nextNodeUid就是节点B的uid你可以通过mindMap.renderer.findNodeByUid(uid)方法来获取节点实例</td>
</tr>
<tr>
<td>associative_line_clickv0.4.5+</td>

View File

@@ -160,4 +160,8 @@
<img src="../../../../assets/avatar/千帆.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>千帆</p>
</div>
<div style="display: flex; flex-direction: column; align-items: center; width: fit-content; margin: 5px;">
<img src="../../../../assets/avatar/才镇.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>才镇</p>
</div>
</div>

View File

@@ -8,16 +8,16 @@
</blockquote>
<h2>特性</h2>
<ul>
<li><input type="checkbox" id="checkbox0" checked="true" /><label for="checkbox0">插件化架构除核心功能外其他功能作为插件提供按需使用减小打包体积</label></li>
<li><input type="checkbox" id="checkbox1" checked="true" /><label for="checkbox1">支持逻辑结构图思维导图组织结构图目录组织图时间轴横向竖向鱼骨图等结构</label></li>
<li><input type="checkbox" id="checkbox2" checked="true" /><label for="checkbox2">内置多种主题允许高度自定义样式支持注册新主题</label></li>
<li><input type="checkbox" id="checkbox3" checked="true" /><label for="checkbox3">节点内容支持文本普通文本富文本图片图标超链接备注标签概要</label></li>
<li><input type="checkbox" id="checkbox4" checked="true" /><label for="checkbox4">节点支持拖拽拖拽移动自由调整多种节点形状支持使用 DDM 完全自定义节点内容</label></li>
<li><input type="checkbox" id="checkbox5" checked="true" /><label for="checkbox5">支持画布拖动缩放</label></li>
<li><input type="checkbox" id="checkbox6" checked="true" /><label for="checkbox6">支持鼠标按键拖动选择和Ctrl+左键两种多选节点方式</label></li>
<li><input type="checkbox" id="checkbox7" checked="true" /><label for="checkbox7">支持导出为</label><code>json</code><code>png</code><code>svg</code><code>pdf</code><code>markdown</code><code>xmind</code>支持从<code>json</code><code>xmind</code><code>markdown</code>导入</li>
<li><input type="checkbox" id="checkbox8" checked="true" /><label for="checkbox8">支持快捷键前进后退关联线搜索替换小地图水印</label></li>
<li><input type="checkbox" id="checkbox9" checked="true" /><label for="checkbox9">提供丰富的配置满足各种场景各种使用习惯</label></li>
<li><input type="checkbox" id="checkbox15" checked="true" /><label for="checkbox15">插件化架构除核心功能外其他功能作为插件提供按需使用减小打包体积</label></li>
<li><input type="checkbox" id="checkbox16" checked="true" /><label for="checkbox16">支持逻辑结构图思维导图组织结构图目录组织图时间轴横向竖向鱼骨图等结构</label></li>
<li><input type="checkbox" id="checkbox17" checked="true" /><label for="checkbox17">内置多种主题允许高度自定义样式支持注册新主题</label></li>
<li><input type="checkbox" id="checkbox18" checked="true" /><label for="checkbox18">节点内容支持文本普通文本富文本图片图标超链接备注标签概要</label></li>
<li><input type="checkbox" id="checkbox19" checked="true" /><label for="checkbox19">节点支持拖拽拖拽移动自由调整多种节点形状支持使用 DDM 完全自定义节点内容</label></li>
<li><input type="checkbox" id="checkbox20" checked="true" /><label for="checkbox20">支持画布拖动缩放</label></li>
<li><input type="checkbox" id="checkbox21" checked="true" /><label for="checkbox21">支持鼠标按键拖动选择和Ctrl+左键两种多选节点方式</label></li>
<li><input type="checkbox" id="checkbox22" checked="true" /><label for="checkbox22">支持导出为</label><code>json</code><code>png</code><code>svg</code><code>pdf</code><code>markdown</code><code>xmind</code>支持从<code>json</code><code>xmind</code><code>markdown</code>导入</li>
<li><input type="checkbox" id="checkbox23" checked="true" /><label for="checkbox23">支持快捷键前进后退关联线搜索替换小地图水印</label></li>
<li><input type="checkbox" id="checkbox24" checked="true" /><label for="checkbox24">提供丰富的配置满足各种场景各种使用习惯</label></li>
</ul>
<h2>仓库目录介绍</h2>
<p>1.<code>simple-mind-map</code></p>
@@ -25,11 +25,11 @@
<p>2.<code>web</code></p>
<p>使用<code>simple-mind-map</code>基于<code>vue2.x</code><code>ElementUI</code>搭建的在线思维导图特性</p>
<ul>
<li><input type="checkbox" id="checkbox10" checked="true" /><label for="checkbox10">工具栏支持插入节点删除节点编辑节点图片图标超链接备注标签概要</label></li>
<li><input type="checkbox" id="checkbox11" checked="true" /><label for="checkbox11">侧边栏基础样式设置面板节点样式设置面板大纲面板主题选择面板结构选择面板</label></li>
<li><input type="checkbox" id="checkbox12" checked="true" /><label for="checkbox12">导入导出功能数据默认保存在浏览器本地存储也支持直接创建打开编辑电脑本地文件</label></li>
<li><input type="checkbox" id="checkbox13" checked="true" /><label for="checkbox13">右键菜单支持展开收起整理布局等操作</label></li>
<li><input type="checkbox" id="checkbox14" checked="true" /><label for="checkbox14">底部栏支持节点数量字数统计支持切换编辑和只读模式支持放大缩小支持全屏切换支持小地图</label></li>
<li><input type="checkbox" id="checkbox25" checked="true" /><label for="checkbox25">工具栏支持插入节点删除节点编辑节点图片图标超链接备注标签概要</label></li>
<li><input type="checkbox" id="checkbox26" checked="true" /><label for="checkbox26">侧边栏基础样式设置面板节点样式设置面板大纲面板主题选择面板结构选择面板</label></li>
<li><input type="checkbox" id="checkbox27" checked="true" /><label for="checkbox27">导入导出功能数据默认保存在浏览器本地存储也支持直接创建打开编辑电脑本地文件</label></li>
<li><input type="checkbox" id="checkbox28" checked="true" /><label for="checkbox28">右键菜单支持展开收起整理布局等操作</label></li>
<li><input type="checkbox" id="checkbox29" checked="true" /><label for="checkbox29">底部栏支持节点数量字数统计支持切换编辑和只读模式支持放大缩小支持全屏切换支持小地图</label></li>
</ul>
<p>提供文档页面服务</p>
<p>3.<code>dist</code></p>
@@ -120,6 +120,10 @@
<img src="../../../../assets/avatar/千帆.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>千帆</p>
</div>
<div style="display: flex; flex-direction: column; align-items: center; width: fit-content; margin: 5px;">
<img src="../../../../assets/avatar/才镇.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>才镇</p>
</div>
</div>
</div>
</template>

View File

@@ -164,6 +164,28 @@
</el-option>
</el-select>
</div>
<div class="rowItem" v-if="style.lineStyle === 'curve'">
<span class="name">{{ $t('baseStyle.rootStyle') }}</span>
<el-select
size="mini"
style="width: 80px"
v-model="style.rootLineKeepSameInCurve"
placeholder=""
@change="
value => {
update('rootLineKeepSameInCurve', value)
}
"
>
<el-option
v-for="item in rootLineKeepSameInCurveList"
:key="item.value"
:label="item.name"
:value="item.value"
>
</el-option>
</el-select>
</div>
</div>
<!-- 概要连线 -->
<div class="title noTop">{{ $t('baseStyle.lineOfOutline') }}</div>
@@ -630,7 +652,7 @@
<script>
import Sidebar from './Sidebar'
import Color from './Color'
import { lineWidthList, lineStyleList, backgroundRepeatList, backgroundPositionList, backgroundSizeList, fontFamilyList, fontSizeList } from '@/config'
import { lineWidthList, lineStyleList, backgroundRepeatList, backgroundPositionList, backgroundSizeList, fontFamilyList, fontSizeList, rootLineKeepSameInCurveList } from '@/config'
import ImgUpload from '@/components/ImgUpload'
import { storeConfig } from '@/api'
import { mapState, mapMutations } from 'vuex'
@@ -667,6 +689,7 @@ export default {
lineColor: '',
lineWidth: '',
lineStyle: '',
rootLineKeepSameInCurve: '',
generalizationLineWidth: '',
generalizationLineColor: '',
associativeLineColor: '',
@@ -716,6 +739,9 @@ export default {
lineStyleList() {
return lineStyleList[this.$i18n.locale] || lineStyleList.zh
},
rootLineKeepSameInCurveList() {
return rootLineKeepSameInCurveList[this.$i18n.locale] || rootLineKeepSameInCurveList.zh
},
backgroundRepeatList() {
return backgroundRepeatList[this.$i18n.locale] || backgroundRepeatList.zh
},
@@ -759,6 +785,7 @@ export default {
'backgroundColor',
'lineWidth',
'lineStyle',
'rootLineKeepSameInCurve',
'lineColor',
'generalizationLineWidth',
'generalizationLineColor',

View File

@@ -4,7 +4,7 @@
<Count v-if="!isZenMode"></Count>
<Navigator :mindMap="mindMap"></Navigator>
<NavigatorToolbar :mindMap="mindMap" v-if="!isZenMode"></NavigatorToolbar>
<Outline :mindMap="mindMap"></Outline>
<OutlineSidebar :mindMap="mindMap"></OutlineSidebar>
<Style v-if="!isZenMode"></Style>
<BaseStyle :data="mindMapData" :mindMap="mindMap"></BaseStyle>
<Theme v-if="mindMap" :mindMap="mindMap"></Theme>
@@ -21,6 +21,7 @@
<Search v-if="mindMap" :mindMap="mindMap"></Search>
<NodeIconSidebar v-if="mindMap" :mindMap="mindMap"></NodeIconSidebar>
<NodeIconToolbar v-if="mindMap" :mindMap="mindMap"></NodeIconToolbar>
<OutlineEdit v-if="mindMap" :mindMap="mindMap"></OutlineEdit>
</div>
</template>
@@ -39,7 +40,8 @@ import AssociativeLine from 'simple-mind-map/src/plugins/AssociativeLine.js'
import TouchEvent from 'simple-mind-map/src/plugins/TouchEvent.js'
import NodeImgAdjust from 'simple-mind-map/src/plugins/NodeImgAdjust.js'
import SearchPlugin from 'simple-mind-map/src/plugins/Search.js'
import Outline from './Outline'
import Painter from 'simple-mind-map/src/plugins/Painter.js'
import OutlineSidebar from './OutlineSidebar'
import Style from './Style'
import BaseStyle from './BaseStyle'
import Theme from './Theme'
@@ -66,6 +68,7 @@ import i18n from '../../../i18n'
import Search from './Search.vue'
import NodeIconSidebar from './NodeIconSidebar.vue'
import NodeIconToolbar from './NodeIconToolbar.vue'
import OutlineEdit from './OutlineEdit.vue'
// 注册插件
MindMap
@@ -81,6 +84,7 @@ MindMap
.usePlugin(NodeImgAdjust)
.usePlugin(TouchEvent)
.usePlugin(SearchPlugin)
.usePlugin(Painter)
// 注册自定义主题
customThemeList.forEach((item) => {
@@ -95,7 +99,7 @@ customThemeList.forEach((item) => {
export default {
name: 'Edit',
components: {
Outline,
OutlineSidebar,
Style,
BaseStyle,
Theme,
@@ -111,14 +115,14 @@ export default {
SidebarTrigger,
Search,
NodeIconSidebar,
NodeIconToolbar
NodeIconToolbar,
OutlineEdit
},
data() {
return {
mindMap: null,
mindMapData: null,
prevImg: '',
openTest: false
prevImg: ''
}
},
computed: {
@@ -154,100 +158,14 @@ export default {
this.$bus.$on('createAssociativeLine', () => {
this.mindMap.associativeLine.createLineFromActiveNode()
})
this.$bus.$on('startPainter', () => {
this.mindMap.painter.startPainter()
})
window.addEventListener('resize', () => {
this.mindMap.resize()
})
if (this.openTest) {
setTimeout(() => {
this.test()
}, 5000)
}
},
methods: {
/**
* @Author: 王林25
* @Date: 2021-11-22 19:39:28
* @Desc: 数据更改测试
*/
test() {
let nodeData = {
data: { text: '根节点', expand: true, isActive: false },
children: []
}
setTimeout(() => {
nodeData.data.text = '理想青年实验室'
this.mindMap.setData(JSON.parse(JSON.stringify(nodeData)))
setTimeout(() => {
nodeData.children.push({
data: { text: '网站', expand: true, isActive: false },
children: []
})
this.mindMap.setData(JSON.parse(JSON.stringify(nodeData)))
setTimeout(() => {
nodeData.children.push({
data: { text: '博客', expand: true, isActive: false },
children: []
})
this.mindMap.setData(JSON.parse(JSON.stringify(nodeData)))
setTimeout(() => {
let viewData = {
transform: {
scaleX: 1,
scaleY: 1,
shear: 0,
rotate: 0,
translateX: 179,
translateY: 0,
originX: 0,
originY: 0,
a: 1,
b: 0,
c: 0,
d: 1,
e: 179,
f: 0
},
state: { scale: 1, x: 179, y: 0, sx: 0, sy: 0 }
}
this.mindMap.view.setTransformData(viewData)
setTimeout(() => {
let viewData = {
transform: {
scaleX: 1.6000000000000005,
scaleY: 1.6000000000000005,
shear: 0,
rotate: 0,
translateX: -373.3000000000004,
translateY: -281.10000000000025,
originX: 0,
originY: 0,
a: 1.6000000000000005,
b: 0,
c: 0,
d: 1.6000000000000005,
e: -373.3000000000004,
f: -281.10000000000025
},
state: {
scale: 1.6000000000000005,
x: 179,
y: 0,
sx: 0,
sy: 0
}
}
this.mindMap.view.setTransformData(viewData)
}, 1000)
}, 1000)
}, 1000)
}, 1000)
}, 1000)
},
/**
* @Author: 王林
* @Date: 2021-07-03 22:11:37
@@ -264,9 +182,6 @@ export default {
* @Desc: 存储数据当数据有变时
*/
bindSaveEvent() {
if (this.openTest) {
return
}
this.$bus.$on('data_change', data => {
storeData(data)
})
@@ -283,9 +198,6 @@ export default {
* @Desc: 手动保存
*/
manualSave() {
if (this.openTest) {
return
}
let data = this.mindMap.getData(true)
storeConfig(data)
},
@@ -317,6 +229,7 @@ export default {
...(config || {}),
iconList: icon,
useLeftKeySelectionRightKeyDrag: this.useLeftKeySelectionRightKeyDrag,
customInnerElsAppendTo: null,
// isUseCustomNodeContent: true,
// 示例1组件里用到了router、store、i18n等实例化vue组件时需要用到的东西
// customCreateNodeContent: (node) => {
@@ -363,7 +276,9 @@ export default {
'node_tree_render_end',
'rich_text_selection_change',
'transforming-dom-to-images',
'generalization_node_contextmenu'
'generalization_node_contextmenu',
'painter_start',
'painter_end'
].forEach(event => {
this.mindMap.on(event, (...args) => {
this.$bus.$emit(event, ...args)

View File

@@ -1,6 +1,6 @@
<template>
<el-dialog
class="nodeDialog"
class="nodeExportDialog"
:title="$t('export.title')"
:visible.sync="dialogVisible"
width="700px"
@@ -197,7 +197,7 @@ export default {
}
}
.nodeDialog {
.nodeExportDialog {
/deep/ .el-dialog__body {
background-color: #f2f4f7;
}

View File

@@ -1,6 +1,6 @@
<template>
<el-dialog
class="nodeDialog"
class="nodeImportDialog"
:title="$t('import.title')"
:visible.sync="dialogVisible"
width="300px"
@@ -245,6 +245,6 @@ export default {
</script>
<style lang="less" scoped>
.nodeDialog {
.nodeImportDialog {
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<el-dialog
class="nodeDialog"
class="nodeHyperlinkDialog"
:title="$t('nodeHyperlink.title')"
:visible.sync="dialogVisible"
width="500"
@@ -89,7 +89,7 @@ export default {
</script>
<style lang="less" scoped>
.nodeDialog {
.nodeHyperlinkDialog {
.item {
display: flex;
align-items: center;

View File

@@ -1,6 +1,6 @@
<template>
<el-dialog
class="nodeDialog"
class="nodeIconDialog"
:title="$t('nodeIcon.title')"
:visible.sync="dialogVisible"
width="500"
@@ -95,7 +95,7 @@ export default {
</script>
<style lang="less" scoped>
.nodeDialog {
.nodeIconDialog {
/deep/ .el-dialog__body {
padding: 0 20px;
}

View File

@@ -1,6 +1,6 @@
<template>
<el-dialog
class="nodeDialog"
class="nodeImageDialog"
:title="$t('nodeImage.title')"
:visible.sync="dialogVisible"
width="500"
@@ -112,7 +112,7 @@ export default {
</script>
<style lang="less" scoped>
.nodeDialog {
.nodeImageDialog {
.title {
font-size: 18px;
margin-bottom: 12px;

View File

@@ -1,6 +1,6 @@
<template>
<el-dialog
class="nodeDialog"
class="nodeNoteDialog"
:title="$t('nodeNote.title')"
:visible.sync="dialogVisible"
width="500"
@@ -105,7 +105,7 @@ export default {
</script>
<style lang="less" scoped>
.nodeDialog {
.nodeNoteDialog {
.tip {
margin-top: 5px;
color: #dcdfe6;

View File

@@ -1,6 +1,6 @@
<template>
<el-dialog
class="nodeDialog"
class="nodeTagDialog"
:title="$t('nodeTag.title')"
:visible.sync="dialogVisible"
width="500"
@@ -120,7 +120,7 @@ export default {
</script>
<style lang="less" scoped>
.nodeDialog {
.nodeTagDialog {
.tagList {
display: flex;
flex-wrap: wrap;

View File

@@ -1,42 +1,53 @@
<template>
<Sidebar ref="sidebar" :title="$t('outline.title')">
<el-tree
class="outlineTree"
:class="{ isDark: isDark }"
:data="data"
:props="defaultProps"
:expand-on-click-node="false"
default-expand-all
<el-tree
ref="tree"
class="outlineTree"
node-key="uid"
draggable
default-expand-all
:class="{ isDark: isDark }"
:data="data"
:props="defaultProps"
:highlight-current="true"
:expand-on-click-node="false"
:allow-drag="checkAllowDrag"
@node-drop="onNodeDrop"
@current-change="onCurrentChange"
@mouseenter.native="isInTreArea = true"
@mouseleave.native="isInTreArea = false"
>
<span
class="customNode"
slot-scope="{ node, data }"
:data-id="data.uid"
@click="onClick(data)"
>
<span class="customNode" slot-scope="{ node, data }" @click="onClick($event, node)">
<span
class="nodeEdit"
:key="getKey()"
contenteditable="true"
@keydown.stop="onKeydown($event, node)"
@keyup.stop
@blur="onBlur($event, node)"
v-html="node.label"
></span>
</span>
</el-tree>
</Sidebar>
<span
class="nodeEdit"
contenteditable="true"
:key="getKey()"
@keydown.stop="onNodeInputKeydown($event, node)"
@keyup.stop
@blur="onBlur($event, node)"
@paste="onPaste($event, node)"
v-html="node.label"
></span>
</span>
</el-tree>
</template>
<script>
import Sidebar from './Sidebar'
import { mapState } from 'vuex'
import {
nodeRichTextToTextWithWrap,
textToNodeRichTextWithWrap,
getTextFromHtml,
createUid
} from 'simple-mind-map/src/utils'
/**
* @Author: 王林
* @Date: 2021-06-24 22:54:14
* @Desc: 大纲内容
*/
// 大纲树
export default {
name: 'Outline',
components: {
Sidebar
},
props: {
mindMap: {
type: Object
@@ -46,79 +57,250 @@ export default {
return {
data: [],
defaultProps: {
label(data) {
return data.data.richText ? data.data.text : data.data.text.replaceAll(/\n/g, '</br>')
}
label: 'label'
},
notHandleDataChange: false
currentData: null,
notHandleDataChange: false,
handleNodeTreeRenderEnd: false,
beInsertNodeUid: '',
insertType: '',
isInTreArea: false,
isAfterCreateNewNode: false
}
},
computed: {
...mapState(['activeSidebar', 'isDark'])
},
watch: {
activeSidebar(val) {
if (val === 'outline') {
this.$refs.sidebar.show = true
} else {
this.$refs.sidebar.show = false
}
}
...mapState(['isDark', 'isOutlineEdit'])
},
created() {
this.$bus.$on('data_change', data => {
// 激活节点会让当前大纲失去焦点
window.addEventListener('keydown', this.onKeyDown)
this.$bus.$on('data_change', () => {
// 在大纲里操作节点时不要响应该事件,否则会重新刷新树
if (this.notHandleDataChange) {
this.notHandleDataChange = false
return
}
this.data = [this.mindMap.renderer.renderTree]
if (this.isAfterCreateNewNode) {
this.isAfterCreateNewNode = false
return
}
this.refresh()
})
this.$bus.$on('node_tree_render_end', () => {
// 当前存在未完成的节点插入操作
if (this.insertType) {
this[this.insertType]()
this.insertType = ''
return
}
// 插入了新节点后需要做一些操作
if (this.handleNodeTreeRenderEnd) {
this.handleNodeTreeRenderEnd = false
this.refresh()
this.$nextTick(() => {
this.afterCreateNewNode()
})
}
})
},
mounted() {
this.refresh()
},
beforeDestroy() {
window.removeEventListener('keydown', this.onKeyDown)
},
methods: {
// 刷新树数据
refresh() {
let data = this.mindMap.getData()
data.root = true // 标记根节点
let walk = root => {
const text = (root.data.richText
? nodeRichTextToTextWithWrap(root.data.text)
: root.data.text
).replaceAll(/\n/g, '<br>')
root.textCache = text // 保存一份修改前的数据,用于对比是否修改了
root.label = text
root.uid = root.data.uid
if (root.children && root.children.length > 0) {
root.children.forEach(item => {
walk(item)
})
}
}
walk(data)
this.data = [data]
},
// 插入了新节点之后
afterCreateNewNode() {
// 如果是新插入节点,那么需要手动高亮该节点、定位该节点及聚焦
let id = this.beInsertNodeUid
if (id && this.$refs.tree) {
try {
this.isAfterCreateNewNode = true
// 高亮树节点
this.$refs.tree.setCurrentKey(id)
let node = this.$refs.tree.getNode(id)
this.onCurrentChange(node.data)
// 定位该节点
this.onClick(node.data)
// 聚焦该树节点的编辑框
const el = document.querySelector(
`.customNode[data-id="${id}"] .nodeEdit`
)
if (el) {
let selection = window.getSelection()
let range = document.createRange()
range.selectNodeContents(el)
selection.removeAllRanges()
selection.addRange(range)
let offsetTop = el.offsetTop
this.$emit('scrollTo', offsetTop)
}
} catch (error) {
console.log(error)
}
}
this.beInsertNodeUid = ''
},
// 根节点不允许拖拽
checkAllowDrag(node) {
return !node.data.root
},
// 失去焦点更新节点文本
onBlur(e, node) {
// 节点数据没有修改
if (node.data.textCache === e.target.innerHTML) {
// 如果存在未执行的插入新节点操作,那么直接执行
if (this.insertType) {
this[this.insertType]()
this.insertType = ''
}
return
}
// 否则插入新节点操作需要等待当前修改事件渲染完成后再执行
const richText = node.data.data.richText
const text = richText ? e.target.innerHTML : e.target.innerText
const targetNode = this.mindMap.renderer.findNodeByUid(node.data.uid)
if (!targetNode) return
if (richText) {
node.data._node.setText(e.target.innerHTML, true)
targetNode.setText(textToNodeRichTextWithWrap(text), true, true)
} else {
node.data._node.setText(e.target.innerText)
targetNode.setText(text)
}
},
// 拦截粘贴事件
onPaste(e) {
e.preventDefault()
const selection = window.getSelection()
if (!selection.rangeCount) return
selection.deleteFromDocument()
let text = (e.clipboardData || window.clipboardData).getData('text')
// 去除格式
text = getTextFromHtml(text)
// 去除换行
text = text.replaceAll(/\n/g, '')
const node = document.createTextNode(text)
selection.getRangeAt(0).insertNode(node)
selection.collapseToEnd()
},
// 生成唯一的key
getKey() {
return Math.random()
},
onKeydown(e) {
// 节点输入区域按键事件
onNodeInputKeydown(e) {
if (e.keyCode === 13 && !e.shiftKey) {
e.preventDefault()
this.insertNode()
this.insertType = 'insertNode'
e.target.blur()
}
if (e.keyCode === 9) {
e.preventDefault()
this.insertChildNode()
this.insertType = 'insertChildNode'
e.target.blur()
}
},
// 插入兄弟节点
insertNode() {
this.notHandleDataChange = false
this.mindMap.execCommand('INSERT_NODE', false)
this.notHandleDataChange = true
this.handleNodeTreeRenderEnd = true
this.beInsertNodeUid = createUid()
this.mindMap.execCommand('INSERT_NODE', false, [], {
uid: this.beInsertNodeUid
})
},
// 插入下级节点
insertChildNode() {
this.notHandleDataChange = false
this.mindMap.execCommand('INSERT_CHILD_NODE', false)
this.notHandleDataChange = true
this.handleNodeTreeRenderEnd = true
this.beInsertNodeUid = createUid()
this.mindMap.execCommand('INSERT_CHILD_NODE', false, [], {
uid: this.beInsertNodeUid
})
},
// 激活当前节点且移动当前节点到画布中间
onClick(e, node) {
onClick(data) {
this.notHandleDataChange = true
let targetNode = node.data._node
const targetNode = this.mindMap.renderer.findNodeByUid(data.uid)
if (targetNode && targetNode.nodeData.data.isActive) return
this.mindMap.execCommand('GO_TARGET_NODE', node.data.data.uid)
this.mindMap.renderer.textEdit.stopFocusOnNodeActive()
this.mindMap.execCommand('GO_TARGET_NODE', data.uid, () => {
this.mindMap.renderer.textEdit.openFocusOnNodeActive()
this.notHandleDataChange = false
})
},
// 拖拽结束事件
onNodeDrop(data, target, postion) {
this.notHandleDataChange = true
const node = this.mindMap.renderer.findNodeByUid(data.data.uid)
const targetNode = this.mindMap.renderer.findNodeByUid(target.data.uid)
if (!node || !targetNode) {
return
}
switch (postion) {
case 'before':
this.mindMap.execCommand('INSERT_BEFORE', node, targetNode)
break
case 'after':
this.mindMap.execCommand('INSERT_AFTER', node, targetNode)
break
case 'inner':
this.mindMap.execCommand('MOVE_NODE_TO', node, targetNode)
break
default:
break
}
},
// 当前选中的树节点变化事件
onCurrentChange(data) {
this.currentData = data
},
// 删除节点
onKeyDown(e) {
if (!this.isInTreArea) return
if ([46, 8].includes(e.keyCode) && this.currentData) {
e.stopPropagation()
this.mindMap.renderer.textEdit.hideEditTextBox()
const node = this.mindMap.renderer.findNodeByUid(this.currentData.uid)
if (node && !node.isRoot) {
this.notHandleDataChange = true
this.$refs.tree.remove(this.currentData)
this.mindMap.execCommand('REMOVE_NODE', [node])
}
}
}
}
}
</script>
@@ -126,52 +308,47 @@ export default {
<style lang="less" scoped>
.customNode {
width: 100%;
overflow-x: auto;
&::-webkit-scrollbar {
width: 7px;
height: 7px;
}
&::-webkit-scrollbar-thumb {
border-radius: 7px;
background-color: rgba(0, 0, 0, 0.3);
cursor: pointer;
}
&::-webkit-scrollbar-track {
box-shadow: none;
background: transparent;
display: none;
}
color: rgba(0, 0, 0, 0.85);
font-weight: bold;
.nodeEdit {
outline: none;
white-space: normal;
padding-right: 20px;
}
}
.outlineTree {
&.isDark {
background-color: #262a2e;
}
/deep/ .el-tree-node > .el-tree-node__children {
overflow: inherit;
}
/deep/ .el-tree-node__content {
height: auto;
margin: 5px 0;
.el-tree-node__expand-icon.is-leaf {
position: relative;
.el-tree-node__expand-icon {
color: #262a2e;
&::after {
position: absolute;
content: '';
width: 5px;
height: 5px;
border-radius: 50%;
background-color: #c0c4cc;
left: 10px;
top: 50%;
transform: translateY(-50%);
&.is-leaf {
color: transparent;
position: relative;
&::after {
background-color: #262a2e;
position: absolute;
content: '';
width: 5px;
height: 5px;
border-radius: 50%;
left: 10px;
top: 50%;
transform: translateY(-50%);
}
}
}
}

View File

@@ -0,0 +1,107 @@
<template>
<div
class="outlineEditContainer"
ref="outlineEditContainer"
v-if="isOutlineEdit"
>
<div class="closeBtn" @click="onClose">
<span class="icon iconfont iconguanbi"></span>
</div>
<div class="outlineEditBox" ref="outlineEditBox">
<div class="outlineEdit">
<Outline :mindMap="mindMap" @scrollTo="onScrollTo"></Outline>
</div>
</div>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
import Outline from './Outline.vue'
// 大纲侧边栏
export default {
name: 'OutlineEdit',
components: {
Outline
},
props: {
mindMap: {
type: Object
}
},
computed: {
...mapState(['isOutlineEdit'])
},
watch: {
isOutlineEdit(val) {
if (val) {
this.$nextTick(() => {
document.body.appendChild(this.$refs.outlineEditContainer)
})
}
}
},
methods: {
...mapMutations(['setIsOutlineEdit']),
onClose() {
this.setIsOutlineEdit(false)
},
onScrollTo(y) {
let container = this.$refs.outlineEditBox
let height = container.offsetHeight
let top = container.scrollTop
y += 50
if (y > top + height) {
container.scrollTo(0, y - height / 2)
}
}
}
}
</script>
<style lang="less" scoped>
.outlineEditContainer {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 9999;
background-color: #fff;
overflow: hidden;
.closeBtn {
position: absolute;
right: 40px;
top: 20px;
cursor: pointer;
.icon {
font-size: 28px;
}
}
.outlineEditBox {
width: 100%;
height: 100%;
overflow-y: auto;
padding: 50px 0;
.outlineEdit {
width: 1000px;
height: 100%;
height: max-content;
margin: 0 auto;
/deep/ .customNode {
.nodeEdit {
max-width: 800px;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,70 @@
<template>
<Sidebar ref="sidebar" :title="$t('outline.title')">
<div class="changeBtn" @click="onChangeToOutlineEdit">
<span class="icon iconfont iconquanping1"></span>
</div>
<Outline
:mindMap="mindMap"
v-if="activeSidebar === 'outline'"
@scrollTo="onScrollTo"
></Outline>
</Sidebar>
</template>
<script>
import Sidebar from './Sidebar'
import { mapState, mapMutations } from 'vuex'
import Outline from './Outline.vue'
// 大纲侧边栏
export default {
name: 'OutlineSidebar',
components: {
Sidebar,
Outline
},
props: {
mindMap: {
type: Object
}
},
computed: {
...mapState(['activeSidebar', 'isOutlineEdit'])
},
watch: {
activeSidebar(val) {
if (val === 'outline') {
this.$refs.sidebar.show = true
} else {
this.$refs.sidebar.show = false
}
}
},
methods: {
...mapMutations(['setIsOutlineEdit', 'setActiveSidebar']),
onChangeToOutlineEdit() {
this.setActiveSidebar('')
this.setIsOutlineEdit(true)
},
onScrollTo(y) {
let container = this.$refs.sidebar.getEl()
let height = container.offsetHeight
let top = container.scrollTop
if (y > top + height) {
container.scrollTo(0, y - height / 2)
}
}
}
}
</script>
<style lang="less" scoped>
.changeBtn {
position: absolute;
right: 50px;
top: 12px;
cursor: pointer;
}
</style>

View File

@@ -117,7 +117,7 @@ export default {
},
replace() {
this.mindMap.search.replace(this.replaceText)
this.mindMap.search.replace(this.replaceText, true)
},
replaceAll() {

View File

@@ -9,7 +9,7 @@
<div class="sidebarHeader" v-if="title">
{{ title }}
</div>
<div class="sidebarContent">
<div class="sidebarContent" ref="sidebarContent">
<slot></slot>
</div>
</div>
@@ -59,6 +59,10 @@ export default {
close() {
this.show = false
this.setActiveSidebar('')
},
getEl() {
return this.$refs.sidebarContent
}
}
}

View File

@@ -23,6 +23,17 @@
<span class="icon iconfont iconqianjin1"></span>
<span class="text">{{ $t('toolbar.redo') }}</span>
</div>
<div
class="toolbarBtn"
:class="{
disabled: activeNodes.length <= 0 || hasGeneralization,
active: isInPainter
}"
@click="$bus.$emit('startPainter')"
>
<span class="icon iconfont iconjiedian"></span>
<span class="text">{{ $t('toolbar.painter') }}</span>
</div>
<div
class="toolbarBtn"
:class="{
@@ -195,7 +206,8 @@ export default {
forwardEnd: true,
readonly: false,
isFullDataFile: false,
timer: null
timer: null,
isInPainter: false
}
},
computed: {
@@ -227,12 +239,16 @@ export default {
this.$bus.$on('node_active', this.onNodeActive)
this.$bus.$on('back_forward', this.onBackForward)
this.$bus.$on('write_local_file', this.onWriteLocalFile)
this.$bus.$on('painter_start', this.onPainterStart)
this.$bus.$on('painter_end', this.onPainterEnd)
},
beforeDestroy() {
this.$bus.$off('mode_change', this.onModeChange)
this.$bus.$off('node_active', this.onNodeActive)
this.$bus.$off('back_forward', this.onBackForward)
this.$bus.$off('write_local_file', this.onWriteLocalFile)
this.$bus.$on('painter_start', this.onPainterStart)
this.$bus.$off('painter_end', this.onPainterEnd)
},
methods: {
...mapMutations(['setActiveSidebar']),
@@ -448,6 +464,14 @@ export default {
'你的浏览器可能不支持建议使用最新版本的Chrome浏览器'
)
}
},
onPainterStart() {
this.isInPainter = true
},
onPainterEnd() {
this.isInPainter = false
}
}
}
@@ -531,6 +555,12 @@ export default {
}
}
&.active {
.icon {
background: #f5f5f5;
}
}
&.disabled {
color: #bcbcbc;
cursor: not-allowed;

View File

@@ -19,6 +19,7 @@ const store = new Vuex.Store({
},
activeSidebar: '', // 当前显示的侧边栏
isDark: false,// 是否是暗黑模式
isOutlineEdit: false// 是否是大纲编辑模式
},
mutations: {
/**
@@ -67,6 +68,11 @@ const store = new Vuex.Store({
// 设置暗黑模式
setIsDark(state, data) {
state.isDark = data
},
// 设置大纲编辑模式
setIsOutlineEdit(state, data) {
state.isOutlineEdit = data
}
},
actions: {

View File

@@ -14,11 +14,13 @@ module.exports = {
// 移除 prefetch 插件
config.plugins.delete('prefetch')
// 支持运行时设置public path
config
.plugin('dynamicPublicPathPlugin')
.use(WebpackDynamicPublicPathPlugin, [
{ externalPublicPath: 'window.externalPublicPath' }
])
if (!isDev) {
config
.plugin('dynamicPublicPathPlugin')
.use(WebpackDynamicPublicPathPlugin, [
{ externalPublicPath: 'window.externalPublicPath' }
])
}
// 给插入html页面内的js和css添加hash参数
config.plugin('html').tap(args => {
args[0].hash = true