Compare commits

...

42 Commits

Author SHA1 Message Date
wanglin2
14bd7c3705 Merge branch 'feature' into main 2023-07-28 16:47:17 +08:00
wanglin2
5d0c9dcab1 打包0.6.9-fix.1 2023-07-28 16:46:38 +08:00
wanglin2
def2f02eea 1.修复搜索进行一次单个替换后再全部替换不正确的问题;2.不会再直接修改传入的data对象,内部会深拷贝一份 2023-07-28 16:36:56 +08:00
wanglin2
a74a60c22d 更新群二维码 2023-07-28 09:38:00 +08:00
wanglin2
74d37f2cbc Merge branch 'feature' into main 2023-07-28 09:36:54 +08:00
wanglin2
6ca9a116c2 打包0.6.9 2023-07-28 09:36:25 +08:00
wanglin2
8e59677623 Doc: update 2023-07-28 09:33:18 +08:00
wanglin2
d06f57a5dc Demo:优化暗黑模式 2023-07-28 08:55:35 +08:00
wanglin2
07be48d342 Feat:支持搜索和替换 2023-07-28 08:50:29 +08:00
wanglin2
5a5c7702f5 修改注释 2023-07-28 08:48:51 +08:00
wanglin2
7e1a43143d Demo:切换主题时支持选择是否覆盖设置过的基础样式 2023-07-26 09:17:48 +08:00
wanglin2
5ff9137745 Fix:1.修复节点处于编辑中时添加新节点时新节点的焦点丢失问题;2.修复连续按tab键无法连续创建子节点的问题 2023-07-26 09:04:24 +08:00
wanglin2
8e30d4a94f Fix:修复自定义节点内容时,二次创建根实例时节点内容不渲染的问题 2023-07-25 18:35:41 +08:00
wanglin2
4b2ebb2e1c Doc: update 2023-07-25 16:29:14 +08:00
wanglin2
8c79ffd723 Feat:导出svg时替换svg中存在的 字符,防止导出的svg报错 2023-07-25 09:38:50 +08:00
wanglin2
839c79405f Fix:修复给概要节点设置样式概要节点会消失的问题 2023-07-25 09:25:42 +08:00
wanglin2
d645adfd37 Merge branch 'feature' into main 2023-07-24 14:20:43 +08:00
wanglin2
1b1551c6e3 Demo:完善暗黑模式 2023-07-24 14:20:29 +08:00
wanglin2
78638dc291 更新群二维码 2023-07-24 11:26:55 +08:00
wanglin2
e972924143 打包0.6.8 2023-07-24 11:25:10 +08:00
wanglin2
8871d8727b Doc: update 2023-07-24 11:21:09 +08:00
wanglin2
55945a1a2c Demo:支持根据主题自动切换为暗黑模式 2023-07-24 10:25:02 +08:00
wanglin2
d5e4044fb2 Demo:支持夜间模式 2023-07-24 10:12:53 +08:00
wanglin2
4ee458c509 Demo:修复节点正在编辑时切换富文本编辑配置输入框出现异常的问题 2023-07-24 08:53:08 +08:00
wanglin2
a76ec0dad8 Feat:修改复制、剪切、粘贴逻辑;支持粘贴剪切板中的数据 2023-07-21 17:40:09 +08:00
wanglin2
f2b247e85c Demo:修复基础样式-设置节点外边距未保存的问题 2023-07-21 14:10:40 +08:00
wanglin2
3aed09a8c3 Feat:修改插入概要的快捷键为Ctrl+G 2023-07-21 11:28:49 +08:00
wanglin2
17b7a023ba Merge branch 'main' of https://github.com/wanglin2/mind-map into feature 2023-07-19 09:02:05 +08:00
wanglin2
10cb36829f Doc: update 2023-07-19 09:01:47 +08:00
wanglin2
90a55bb995 Merge branch 'feature' into main 2023-07-18 10:55:57 +08:00
wanglin2
08a37d8971 Doc: update 2023-07-18 10:55:28 +08:00
wanglin2
89983f9f47 Merge branch 'feature' into main 2023-07-18 09:30:32 +08:00
wanglin2
b71a80e383 打包0.6.7 2023-07-18 09:29:19 +08:00
wanglin2
2bf146816b Doc:update 2023-07-18 09:21:58 +08:00
wanglin2
daf9888da4 Fix:修复节点收起再展开后展开收起按钮占位元素丢失的问题 2023-07-18 08:44:44 +08:00
wanglin2
b42cee7a2f Feat:支持根据id定位到某个节点、优化大纲的节点定位 2023-07-18 08:35:40 +08:00
wanglin2
adccef5699 Feat:修改节点uid的创建 2023-07-17 09:59:47 +08:00
wanglin2
94230f8ec6 Fix:修复大纲里创建新节点时节点样式丢失的问题 2023-07-17 09:43:22 +08:00
wanglin2
a161661c6b 删除节点过渡动画效果 2023-07-17 09:07:58 +08:00
wanglin2
08b971cd9a Demo:优化大纲编辑 2023-07-16 22:20:09 +08:00
wanglin2
b64c8f132b Demo:优化页面:侧边栏按钮支持收起;优化小屏适配 2023-07-15 17:29:26 +08:00
wanglin2
d6181591c5 Fix:修复只读模式下可以缩放图片的bug 2023-07-15 15:26:57 +08:00
111 changed files with 2547 additions and 296 deletions

View File

@@ -136,4 +136,16 @@ const mindMap = new MindMap({
<img src="./web/src/assets/avatar/suka.jpg" style="width: 50px;height: 50px;" />
<span>suka</span>
</span>
<span>
<img src="./web/src/assets/avatar/Chris.jpg" style="width: 50px;height: 50px;" />
<span>Chris</span>
</span>
<span>
<img src="./web/src/assets/avatar/水车.jpg" style="width: 50px;height: 50px;" />
<span>水车</span>
</span>
<span>
<img src="./web/src/assets/avatar/仓鼠.jpg" style="width: 50px;height: 50px;" />
<span>仓鼠</span>
</span>
</p>

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -10,6 +10,7 @@ import AssociativeLine from './src/plugins/AssociativeLine'
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 xmind from './src/parse/xmind.js'
import markdown from './src/parse/markdown.js'
import icons from './src/svg/icons.js'
@@ -36,5 +37,6 @@ MindMap
.usePlugin(RichText)
.usePlugin(TouchEvent)
.usePlugin(NodeImgAdjust)
.usePlugin(Search)
export default MindMap

View File

@@ -7,9 +7,9 @@ import Style from './src/core/render/node/Style'
import KeyCommand from './src/core/command/KeyCommand'
import Command from './src/core/command/Command'
import BatchExecution from './src/utils/BatchExecution'
import { layoutValueList, CONSTANTS } from './src/constants/constant'
import { layoutValueList, CONSTANTS, commonCaches } from './src/constants/constant'
import { SVG } from '@svgdotjs/svg.js'
import { simpleDeepClone } from './src/utils'
import { simpleDeepClone, getType } from './src/utils'
import defaultTheme, { checkIsNodeSizeIndependenceConfig } from './src/themes/default'
import { defaultOpt } from './src/constants/defaultOptions'
@@ -32,12 +32,12 @@ class MindMap {
this.svg = SVG().addTo(this.el).size(this.width, this.height)
this.draw = this.svg.group()
// 节点id
this.uid = 1
// 初始化主题
this.initTheme()
// 初始化缓存数据
this.initCache()
// 事件类
this.event = new Event({
mindMap: this
@@ -132,6 +132,23 @@ class MindMap {
this.event.off(event, fn)
}
// 初始化缓存数据
initCache() {
Object.keys(commonCaches).forEach((key) => {
let type = getType(commonCaches[key])
let value = ''
switch(type) {
case 'Boolean':
value = false
break
default:
value = null
break
}
commonCaches[key] = value
})
}
// 设置主题
initTheme() {
// 合并主题配置
@@ -238,7 +255,7 @@ class MindMap {
// 获取思维导图数据,节点树、主题、布局等
getData(withConfig) {
let nodeData = this.command.removeDataUid(this.command.getCopyData())
let nodeData = this.command.getCopyData()
let data = {}
if (withConfig) {
data = {

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "simple-mind-map",
"version": "0.6.6",
"version": "0.6.9-fix.1",
"description": "一个简单的web在线思维导图",
"authors": [
{

View File

@@ -27,130 +27,162 @@ export const themeList = [
{
name: '默认',
value: 'default',
dark: false
},
{
name: '暗色2',
value: 'dark2',
dark: true
},
{
name: '天清绿',
value: 'skyGreen',
dark: false
},
{
name: '脑图经典2',
value: 'classic2',
dark: false
},
{
name: '脑图经典3',
value: 'classic3',
dark: false
},
{
name: '经典绿',
value: 'classicGreen',
dark: false
},
{
name: '经典蓝',
value: 'classicBlue',
dark: false
},
{
name: '天空蓝',
value: 'blueSky',
dark: false
},
{
name: '脑残粉',
value: 'brainImpairedPink',
dark: false
},
{
name: '暗色',
value: 'dark',
dark: true
},
{
name: '泥土黄',
value: 'earthYellow',
dark: false
},
{
name: '清新绿',
value: 'freshGreen',
dark: false
},
{
name: '清新红',
value: 'freshRed',
dark: false
},
{
name: '浪漫紫',
value: 'romanticPurple',
dark: false
},
{
name: '粉红葡萄',
value: 'pinkGrape',
dark: false
},
{
name: '薄荷',
value: 'mint',
dark: false
},
{
name: '金色vip',
value: 'gold',
dark: false
},
{
name: '活力橙',
value: 'vitalityOrange',
dark: false
},
{
name: '绿叶',
value: 'greenLeaf',
dark: false
},
{
name: '脑图经典',
value: 'classic',
dark: true
},
{
name: '脑图经典4',
value: 'classic4',
dark: false
},
{
name: '小黄人',
value: 'minions',
dark: false
},
{
name: '简约黑',
value: 'simpleBlack',
dark: true
},
{
name: '课程绿',
value: 'courseGreen',
dark: false
},
{
name: '咖啡',
value: 'coffee',
dark: false
},
{
name: '红色精神',
value: 'redSpirit',
dark: false
},
{
name: '黑色幽默',
value: 'blackHumour',
dark: true
},
{
name: '深夜办公室',
value: 'lateNightOffice',
dark: true
},
{
name: '黑金',
value: 'blackGold',
dark: true
},
{
name: '牛油果',
value: 'avocado',
dark: false
},
{
name: '秋天',
value: 'autumn',
dark: false
},
{
name: '橙汁',
value: 'orangeJuice',
dark: true
}
]
@@ -213,6 +245,10 @@ export const CONSTANTS = {
TOP: 'top',
RIGHT: 'right',
BOTTOM: 'bottom'
},
PASTE_TYPE: {
CLIP_BOARD: 'clipBoard',
CANVAS: 'canvas'
}
}
@@ -288,4 +324,9 @@ export const nodeDataNoStylePropList = [
'resetRichText',
'uid',
'activeStyle'
]
]
// 数据缓存
export const commonCaches = {
measureCustomNodeContentSizeEl: null
}

View File

@@ -79,10 +79,6 @@ export const defaultOpt = {
},
// 是否只有当鼠标在画布内才响应快捷键事件
enableShortcutOnlyWhenMouseInSvg: true,
// 是否开启节点动画过渡
enableNodeTransitionMove: true,
// 如果开启节点动画过渡可以通过该属性设置过渡的时间单位ms
nodeTransitionMoveDuration: 300,
// 初始根节点的位置
initRootNodePosition: null,
// 导出png、svg、pdf时的图形内边距

View File

@@ -89,7 +89,7 @@ class Command {
this.history.shift()
}
this.activeHistoryIndex = this.history.length - 1
this.mindMap.emit('data_change', this.removeDataUid(data))
this.mindMap.emit('data_change', data)
this.mindMap.emit(
'back_forward',
this.activeHistoryIndex,
@@ -110,7 +110,7 @@ class Command {
this.history.length
)
let data = simpleDeepClone(this.history[this.activeHistoryIndex])
this.mindMap.emit('data_change', this.removeDataUid(data))
this.mindMap.emit('data_change', data)
return data
}
}
@@ -125,7 +125,7 @@ class Command {
this.activeHistoryIndex += step
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))
this.mindMap.emit('data_change', data)
return data
}
}

View File

@@ -57,8 +57,11 @@ export default class KeyCommand {
}
Object.keys(this.shortcutMap).forEach(key => {
if (this.checkKey(e, key)) {
e.stopPropagation()
e.preventDefault()
// 粘贴事件不组织因为要监听paste事件
if (!this.checkKey(e, 'Control+v')) {
e.stopPropagation()
e.preventDefault()
}
this.shortcutMap[key].forEach(fn => {
fn()
})

View File

@@ -7,7 +7,13 @@ import Timeline from '../../layouts/Timeline'
import VerticalTimeline from '../../layouts/VerticalTimeline'
import Fishbone from '../../layouts/Fishbone'
import TextEdit from './TextEdit'
import { copyNodeTree, simpleDeepClone, walk } from '../../utils'
import {
copyNodeTree,
simpleDeepClone,
walk,
bfsWalk,
loadImage
} from '../../utils'
import { shapeList } from './node/Shape'
import { lineStyleProps } from '../../themes/default'
import { CONSTANTS } from '../../constants/constant'
@@ -29,7 +35,7 @@ const layouts = {
// 竖向时间轴
[CONSTANTS.LAYOUT.VERTICAL_TIMELINE]: VerticalTimeline,
// 鱼骨图
[CONSTANTS.LAYOUT.FISHBONE]: Fishbone,
[CONSTANTS.LAYOUT.FISHBONE]: Fishbone
}
// 渲染
@@ -42,7 +48,7 @@ class Render {
this.themeConfig = this.mindMap.themeConfig
this.draw = this.mindMap.draw
// 渲染树,操作过程中修改的都是这里的数据
this.renderTree = merge({}, this.mindMap.opt.data || {})
this.renderTree = merge({}, simpleDeepClone(this.mindMap.opt.data) || {})
// 是否重新渲染
this.reRender = false
// 是否正在渲染中
@@ -60,6 +66,12 @@ class Render {
this.root = null
// 文本编辑框需要再bindEvent之前实例化否则单击事件只能触发隐藏文本编辑框而无法保存文本修改
this.textEdit = new TextEdit(this)
// 当前复制的数据
this.lastBeingCopyData = null
this.beingCopyData = null
this.beingPasteText = ''
this.beingPasteImgSize = 0
this.currentBeingPasteType = ''
// 布局
this.setLayout()
// 绑定事件
@@ -82,18 +94,24 @@ class Render {
// 绑定事件
bindEvent() {
// 点击事件
this.mindMap.on('draw_click', (e) => {
this.mindMap.on('draw_click', e => {
// 清除激活状态
let isTrueClick = true
let { useLeftKeySelectionRightKeyDrag } = this.mindMap.opt
if (useLeftKeySelectionRightKeyDrag) {
let mousedownPos = this.mindMap.event.mousedownPos
isTrueClick = Math.abs(e.clientX - mousedownPos.x) <= 5 && Math.abs(e.clientY - mousedownPos.y) <= 5
isTrueClick =
Math.abs(e.clientX - mousedownPos.x) <= 5 &&
Math.abs(e.clientY - mousedownPos.y) <= 5
}
if (isTrueClick && this.activeNodeList.length > 0) {
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
}
})
// 粘贴事件
this.mindMap.on('paste', data => {
this.onPaste(data)
})
}
// 注册命令
@@ -195,6 +213,9 @@ class Render {
// 设置节点形状
this.setNodeShape = this.setNodeShape.bind(this)
this.mindMap.command.add('SET_NODE_SHAPE', this.setNodeShape)
// 定位节点
this.goTargetNode = this.goTargetNode.bind(this)
this.mindMap.command.add('GO_TARGET_NODE', this.goTargetNode)
}
// 注册快捷键
@@ -212,7 +233,7 @@ class Render {
}
this.mindMap.keyCommand.addShortcut('Enter', this.insertNodeWrap)
// 插入概要
this.mindMap.keyCommand.addShortcut('Control+s', this.addGeneralization)
this.mindMap.keyCommand.addShortcut('Control+g', this.addGeneralization)
// 展开/收起节点
this.toggleActiveExpand = this.toggleActiveExpand.bind(this)
this.mindMap.keyCommand.addShortcut('/', this.toggleActiveExpand)
@@ -239,6 +260,14 @@ class Render {
// 下移节点
this.mindMap.keyCommand.addShortcut('Control+Down', this.downNode)
// 复制节点、剪切节点、粘贴节点的快捷键需开发者自行注册实现可参考demo
this.copy = this.copy.bind(this)
this.mindMap.keyCommand.addShortcut('Control+c', this.copy)
this.mindMap.keyCommand.addShortcut('Control+v', () => {
// 隐藏输入框可能会失去焦点,所以要重新聚焦
this.textEdit.focusHiddenInput()
})
this.cut = this.cut.bind(this)
this.mindMap.keyCommand.addShortcut('Control+x', this.cut)
}
// 开启文字编辑,会禁用回车键和删除键相关快捷键防止冲突
@@ -259,7 +288,6 @@ class Render {
// 渲染
render(callback = () => {}, source) {
let t = Date.now()
// 如果当前还没有渲染完毕,不再触发渲染
if (this.isRendering) {
// 等待当前渲染完毕后再进行一次渲染
@@ -279,7 +307,7 @@ class Render {
// 计算布局
this.layout.doLayout(root => {
// 删除本次渲染时不再需要的节点
Object.keys(this.lastNodeCache).forEach((uid) => {
Object.keys(this.lastNodeCache).forEach(uid => {
if (!this.nodeCache[uid]) {
this.lastNodeCache[uid].destroy()
if (this.lastNodeCache[uid].parent) {
@@ -290,7 +318,7 @@ class Render {
// 更新根节点
this.root = root
// 渲染节点
const onEnd = () => {
this.root.render(() => {
this.isRendering = false
this.mindMap.emit('node_tree_render_end')
callback && callback()
@@ -299,22 +327,13 @@ class Render {
this.render(callback, source)
} else {
// 触发一次保存,因为修改了渲染树的数据
if (this.mindMap.richText && [CONSTANTS.CHANGE_THEME, CONSTANTS.SET_DATA].includes(source)) {
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)
@@ -416,7 +435,7 @@ class Render {
// 规范指定节点数据
formatAppointNodes(appointNodes) {
if (!appointNodes) return []
return Array.isArray(appointNodes) ? appointNodes: [appointNodes]
return Array.isArray(appointNodes) ? appointNodes : [appointNodes]
}
// 插入同级节点,多个节点只会操作第一个节点
@@ -425,7 +444,11 @@ class Render {
if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
return
}
let { defaultInsertSecondLevelNodeText, defaultInsertBelowSecondLevelNodeText } = this.mindMap.opt
this.textEdit.hideEditTextBox()
let {
defaultInsertSecondLevelNodeText,
defaultInsertBelowSecondLevelNodeText
} = this.mindMap.opt
let list = appointNodes.length > 0 ? appointNodes : this.activeNodeList
let first = list[0]
if (first.isGeneralization) {
@@ -434,16 +457,22 @@ class Render {
if (first.isRoot) {
this.insertChildNode(openEdit, appointNodes, appointData)
} else {
let text = first.layerIndex === 1 ? defaultInsertSecondLevelNodeText : defaultInsertBelowSecondLevelNodeText
let text =
first.layerIndex === 1
? defaultInsertSecondLevelNodeText
: defaultInsertBelowSecondLevelNodeText
if (first.layerIndex === 1) {
first.parent.destroy()
}
let index = this.getNodeIndex(first)
let isRichText = !!this.mindMap.richText
first.parent.nodeData.children.splice(index + 1, 0, {
inserting: openEdit,
data: {
text: text,
expand: true,
richText: isRichText,
resetRichText: isRichText,
...(appointData || {})
},
children: []
@@ -458,7 +487,11 @@ class Render {
if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
return
}
let { defaultInsertSecondLevelNodeText, defaultInsertBelowSecondLevelNodeText } = this.mindMap.opt
this.textEdit.hideEditTextBox()
let {
defaultInsertSecondLevelNodeText,
defaultInsertBelowSecondLevelNodeText
} = this.mindMap.opt
let list = appointNodes.length > 0 ? appointNodes : this.activeNodeList
list.forEach(node => {
if (node.isGeneralization) {
@@ -467,12 +500,17 @@ class Render {
if (!node.nodeData.children) {
node.nodeData.children = []
}
let text = node.isRoot ? defaultInsertSecondLevelNodeText : defaultInsertBelowSecondLevelNodeText
let text = node.isRoot
? defaultInsertSecondLevelNodeText
: defaultInsertBelowSecondLevelNodeText
let isRichText = !!this.mindMap.richText
node.nodeData.children.push({
inserting: openEdit,
data: {
text: text,
expand: true,
richText: isRichText,
resetRichText: isRichText,
...(appointData || {})
},
children: []
@@ -540,13 +578,81 @@ class Render {
this.mindMap.render()
}
// 复制节点
copy() {
this.beingCopyData = this.copyNode()
}
// 剪切节点
cut() {
this.mindMap.execCommand('CUT_NODE', copyData => {
this.beingCopyData = copyData
})
}
// 粘贴节点
paste() {
if (this.beingCopyData) {
this.mindMap.execCommand('PASTE_NODE', this.beingCopyData)
}
}
// 粘贴事件
async onPaste({ text, img }) {
// 检查剪切板数据是否有变化
// 通过图片大小来判断图片是否发生变化,可能是不准确的,但是目前没有其他好方法
const imgSize = img ? img.size : 0
if (this.beingPasteText !== text || this.beingPasteImgSize !== imgSize) {
this.currentBeingPasteType = CONSTANTS.PASTE_TYPE.CLIP_BOARD
this.beingPasteText = text
this.beingPasteImgSize = imgSize
}
// 检查要粘贴的节点数据是否有变化,节点优先级高于剪切板
if (this.lastBeingCopyData !== this.beingCopyData) {
this.lastBeingCopyData = this.beingCopyData
this.currentBeingPasteType = CONSTANTS.PASTE_TYPE.CANVAS
}
// 粘贴剪切板的数据
if (this.currentBeingPasteType === CONSTANTS.PASTE_TYPE.CLIP_BOARD) {
// 存在文本,则创建子节点
if (text) {
this.mindMap.execCommand('INSERT_CHILD_NODE', false, [], {
text
})
}
// 存在图片,则添加到当前激活节点
if (img) {
try {
let imgData = await loadImage(img)
if (this.activeNodeList.length > 0) {
this.activeNodeList.forEach(node => {
this.mindMap.execCommand('SET_NODE_IMAGE', node, {
url: imgData.url,
title: '',
width: imgData.size.width,
height: imgData.size.height
})
})
}
} catch (error) {
console.log(error)
}
}
} else {
// 粘贴节点数据
this.paste()
}
}
// 将节点移动到另一个节点的前面
insertBefore(node, exist) {
if (node.isRoot) {
return
}
// 如果是二级节点变成了下级节点,或是下级节点变成了二级节点,节点样式需要更新
let nodeLayerChanged = (node.layerIndex === 1 && exist.layerIndex !== 1) || (node.layerIndex !== 1 && exist.layerIndex === 1)
let nodeLayerChanged =
(node.layerIndex === 1 && exist.layerIndex !== 1) ||
(node.layerIndex !== 1 && exist.layerIndex === 1)
// 移动节点
let nodeParent = node.parent
let nodeBorthers = nodeParent.children
@@ -583,7 +689,9 @@ class Render {
return
}
// 如果是二级节点变成了下级节点,或是下级节点变成了二级节点,节点样式需要更新
let nodeLayerChanged = (node.layerIndex === 1 && exist.layerIndex !== 1) || (node.layerIndex !== 1 && exist.layerIndex === 1)
let nodeLayerChanged =
(node.layerIndex === 1 && exist.layerIndex !== 1) ||
(node.layerIndex !== 1 && exist.layerIndex === 1)
// 移动节点
let nodeParent = node.parent
let nodeBorthers = nodeParent.children
@@ -623,7 +731,7 @@ class Render {
}
let isAppointNodes = appointNodes.length > 0
let list = isAppointNodes ? appointNodes : this.activeNodeList
let root = list.find((node) => {
let root = list.find(node => {
return node.isRoot
})
if (root) {
@@ -709,7 +817,7 @@ class Render {
// 粘贴节点到节点
pasteNode(data) {
if (this.activeNodeList.length <= 0) {
if (this.activeNodeList.length <= 0 || !data) {
return
}
this.activeNodeList.forEach(item => {
@@ -867,7 +975,8 @@ class Render {
setNodeText(node, text, richText) {
this.setNodeDataRender(node, {
text,
richText
richText,
resetRichText: richText
})
}
@@ -992,6 +1101,20 @@ class Render {
})
}
// 定位到指定节点
goTargetNode(node, callback = () => {}) {
let uid = typeof node === 'string' ? node : node.nodeData.data.uid
if (!uid) return
this.expandToNodeUid(uid, () => {
let targetNode = this.findNodeByUid(uid)
if (targetNode) {
targetNode.active()
this.moveNodeToCenter(targetNode)
callback()
}
})
}
// 更新节点数据
setNodeData(node, data) {
Object.keys(data).forEach(key => {
@@ -1000,7 +1123,7 @@ class Render {
}
// 设置节点数据,并判断是否渲染
setNodeDataRender(node, data) {
setNodeDataRender(node, data, notRender = false) {
this.setNodeData(node, data)
let changed = node.reRender()
if (changed) {
@@ -1008,7 +1131,7 @@ class Render {
// 概要节点
node.generalizationBelongNode.updateGeneralization()
}
this.mindMap.render()
if (!notRender) this.mindMap.render()
}
}
@@ -1028,6 +1151,44 @@ class Render {
this.mindMap.view.translateY(offsetY)
this.mindMap.view.setScale(1)
}
// 展开到指定uid的节点
expandToNodeUid(uid, callback = () => {}) {
let parentsList = []
const cache = {}
bfsWalk(this.renderTree, (node, parent) => {
if (node.data.uid === uid) {
parentsList = parent ? [...cache[parent.data.uid], parent] : []
return 'stop'
} else {
cache[node.data.uid] = parent ? [...cache[parent.data.uid], parent] : []
}
})
let needRender = false
parentsList.forEach(node => {
if (!node.data.expand) {
needRender = true
node.data.expand = true
}
})
if (needRender) {
this.mindMap.render(callback)
} else {
callback()
}
}
// 根据uid找到对应的节点实例
findNodeByUid(uid) {
let res = null
walk(this.root, null, node => {
if (node.nodeData.data.uid === uid) {
res = node
return true
}
})
return res
}
}
export default Render

View File

@@ -10,11 +10,14 @@ export default class TextEdit {
this.currentNode = null
// 文本编辑框
this.textEditNode = null
// 隐藏的文本输入框
this.hiddenInputEl = null
// 文本编辑框是否显示
this.showTextEdit = false
// 如果编辑过程中缩放画布了,那么缓存当前编辑的内容
this.cacheEditingText = ''
this.bindEvent()
this.createHiddenInput()
}
// 事件
@@ -46,6 +49,10 @@ export default class TextEdit {
this.mindMap.on('before_node_active', () => {
this.hideEditTextBox()
})
// 节点激活事件
this.mindMap.on('node_active', () => {
this.focusHiddenInput()
})
// 注册编辑快捷键
this.mindMap.keyCommand.addShortcut('F2', () => {
if (this.renderer.activeNodeList.length <= 0) {
@@ -56,12 +63,52 @@ export default class TextEdit {
this.mindMap.on('scale', this.onScale)
}
// 创建一个隐藏的文本输入框
createHiddenInput() {
if (this.hiddenInputEl) return
this.hiddenInputEl = document.createElement('input')
this.hiddenInputEl.type = 'text'
this.hiddenInputEl.style.cssText = `
position: fixed;
left: -99999px;
top: -99999px;
`
// 监听粘贴事件
this.hiddenInputEl.addEventListener('paste', async event => {
event.preventDefault()
const text = (event.clipboardData || window.clipboardData).getData('text')
const files = event.clipboardData.files
let img = null
if (files.length > 0) {
for (let i = 0; i < files.length; i++) {
if (/^image\//.test(files[i].type)) {
img = files[i]
break
}
}
}
this.mindMap.emit('paste', {
text,
img
})
})
document.body.appendChild(this.hiddenInputEl)
}
// 让隐藏的文本输入框聚焦
focusHiddenInput() {
if (this.hiddenInputEl) this.hiddenInputEl.focus()
}
// 注册临时快捷键
registerTmpShortcut() {
// 注册回车快捷键
this.mindMap.keyCommand.addShortcut('Enter', () => {
this.hideEditTextBox()
})
this.mindMap.keyCommand.addShortcut('Tab', () => {
this.hideEditTextBox()
})
}
// 显示文本编辑框
@@ -96,7 +143,8 @@ export default class TextEdit {
onScale() {
if (!this.currentNode) return
if (this.mindMap.richText) {
this.mindMap.richText.cacheEditingText = this.mindMap.richText.getEditText()
this.mindMap.richText.cacheEditingText =
this.mindMap.richText.getEditText()
this.mindMap.richText.showTextEdit = false
} else {
this.cacheEditingText = this.getEditText()
@@ -124,7 +172,9 @@ export default class TextEdit {
let scale = this.mindMap.view.scale
let lineHeight = node.style.merge('lineHeight')
let fontSize = node.style.merge('fontSize')
let textLines = (this.cacheEditingText || node.nodeData.data.text).split(/\n/gim)
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
@@ -134,9 +184,12 @@ export default class TextEdit {
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.maxWidth =
this.mindMap.opt.textAutoWrapWidth * scale + 'px'
if (isMultiLine && lineHeight !== 1) {
this.textEditNode.style.transform = `translateY(${-((lineHeight * fontSize - fontSize) / 2) * scale}px)`
this.textEditNode.style.transform = `translateY(${
-((lineHeight * fontSize - fontSize) / 2) * scale
}px)`
}
this.showTextEdit = true
// 选中文本

View File

@@ -365,8 +365,8 @@ class Node {
this._unVisibleRectRegionNode.fill({
color: 'transparent'
})
this.group.add(this._unVisibleRectRegionNode)
}
this.group.add(this._unVisibleRectRegionNode)
this.renderer.layout.renderExpandBtnRect(this._unVisibleRectRegionNode, this.expandBtnSize, width, height, this)
}
}
@@ -476,8 +476,6 @@ class Node {
return
}
let {
enableNodeTransitionMove,
nodeTransitionMoveDuration,
alwaysShowExpandBtn
} = this.mindMap.opt
if (alwaysShowExpandBtn) {
@@ -503,13 +501,7 @@ class Node {
let t = this.group.transform()
// 如果节点位置没有变化,则返回
if (this.left === t.translateX && this.top === t.translateY) return
if (!isLayout && enableNodeTransitionMove) {
this.group
.animate(nodeTransitionMoveDuration)
.translate(this.left - t.translateX, this.top - t.translateY)
} else {
this.group.translate(this.left - t.translateX, this.top - t.translateY)
}
this.group.translate(this.left - t.translateX, this.top - t.translateY)
}
// 重新渲染节点,即重新创建节点内容、计算节点大小、计算节点内容布局、更新展开收起按钮,概要及位置
@@ -531,8 +523,6 @@ class Node {
// 递归渲染
render(callback = () => {}) {
let { enableNodeTransitionMove, nodeTransitionMoveDuration } =
this.mindMap.opt
// 节点
// 重新渲染连线
this.renderLine()
@@ -580,13 +570,7 @@ class Node {
})
)
} else {
if (enableNodeTransitionMove && !isLayout) {
setTimeout(() => {
callback()
}, nodeTransitionMoveDuration)
} else {
callback()
}
callback()
}
// 手动插入的节点立即获得焦点并且开启编辑模式
if (this.nodeData.inserting) {

View File

@@ -1,7 +1,7 @@
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 '../../../constants/constant'
import { CONSTANTS, commonCaches } from '../../../constants/constant'
// 创建图片节点
function createImgNode() {
@@ -293,20 +293,19 @@ function createNoteNode() {
}
// 测量自定义节点内容元素的宽高
let warpEl = null
function measureCustomNodeContentSize (content) {
if (!warpEl) {
warpEl = document.createElement('div')
warpEl.style.cssText = `
if (!commonCaches.measureCustomNodeContentSizeEl) {
commonCaches.measureCustomNodeContentSizeEl = document.createElement('div')
commonCaches.measureCustomNodeContentSizeEl.style.cssText = `
position: fixed;
left: -99999px;
top: -99999px;
`
this.mindMap.el.appendChild(warpEl)
this.mindMap.el.appendChild(commonCaches.measureCustomNodeContentSizeEl)
}
warpEl.innerHTML = ''
warpEl.appendChild(content)
let rect = warpEl.getBoundingClientRect()
commonCaches.measureCustomNodeContentSizeEl.innerHTML = ''
commonCaches.measureCustomNodeContentSizeEl.appendChild(content)
let rect = commonCaches.measureCustomNodeContentSizeEl.getBoundingClientRect()
return {
width: rect.width,
height: rect.height

View File

@@ -1,4 +1,5 @@
import Node from './Node'
import { createUid } from '../../../utils/index'
// 检查是否存在概要
function checkHasGeneralization () {
@@ -18,7 +19,7 @@ function createGeneralizationNode () {
data: {
data: this.nodeData.data.generalization
},
uid: this.mindMap.uid++,
uid: createUid(),
renderer: this.renderer,
mindMap: this.mindMap,
draw: this.draw,
@@ -35,15 +36,14 @@ function createGeneralizationNode () {
// 更新概要节点
function updateGeneralization () {
if (this.isGeneralization) return
this.removeGeneralization()
this.createGeneralizationNode()
}
// 渲染概要节点
function renderGeneralization () {
if (this.isGeneralization) {
return
}
if (this.isGeneralization) return
if (!this.checkHasGeneralization()) {
this.removeGeneralization()
this._generalizationNodeWidth = 0
@@ -66,6 +66,7 @@ function renderGeneralization () {
// 删除概要节点
function removeGeneralization () {
if (this.isGeneralization) return
if (this._generalizationLine) {
this._generalizationLine.remove()
this._generalizationLine = null
@@ -86,6 +87,7 @@ function removeGeneralization () {
// 隐藏概要节点
function hideGeneralization () {
if (this.isGeneralization) return
if (this._generalizationLine) {
this._generalizationLine.hide()
}
@@ -96,6 +98,7 @@ function hideGeneralization () {
// 显示概要节点
function showGeneralization () {
if (this.isGeneralization) return
if (this._generalizationLine) {
this._generalizationLine.show()
}

View File

@@ -1,6 +1,7 @@
import Node from '../core/render/node/Node'
import { CONSTANTS, initRootNodePositionMap } from '../constants/constant'
import Lru from '../utils/Lru'
import { createUid } from '../utils/index'
// 布局基类
class Base {
@@ -101,7 +102,7 @@ class Base {
}
} else {
// 创建新节点
let uid = this.mindMap.uid++
let uid = data.data.uid || createUid()
newNode = new Node({
data,
uid,

View File

@@ -11,7 +11,7 @@ import {
import associativeLineControlsMethods from './associativeLine/associativeLineControls'
import associativeLineTextMethods from './associativeLine/associativeLineText'
// 关联线
// 关联线插件
class AssociativeLine {
constructor(opt = {}) {
this.mindMap = opt.mindMap

View File

@@ -1,8 +1,7 @@
import { bfsWalk, throttle } from '../utils'
import Base from '../layouts/Base'
// 节点拖动
// 节点拖动插件
class Drag extends Base {
// 构造函数
constructor({ mindMap }) {

View File

@@ -1,9 +1,9 @@
import { imgToDataUrl, downloadFile, readBlob } from '../utils'
import { imgToDataUrl, downloadFile, readBlob, removeHTMLEntities } from '../utils'
import { SVG } from '@svgdotjs/svg.js'
import drawBackgroundImageToCanvas from '../utils/simulateCSSBackgroundInCanvas'
import { transformToMarkdown } from '../parse/toMarkdown'
// 导出
// 导出插件
class Export {
// 构造函数
constructor(opt) {
@@ -154,6 +154,7 @@ class Export {
*/
async png(name, transparent = false) {
let { node, str } = await this.getSvgData()
str = removeHTMLEntities(str)
// 如果开启了富文本则使用htmltocanvas转换为图片
if (this.mindMap.richText) {
let res = await this.mindMap.richText.handleExportPng(node.node)
@@ -207,6 +208,7 @@ class Export {
node.first().before(SVG(`<title>${name}</title>`))
await this.drawBackgroundToSvg(node)
let str = node.svg()
str = removeHTMLEntities(str)
// 转换成blob数据
let blob = new Blob([str], {
type: 'image/svg+xml'

View File

@@ -1,6 +1,6 @@
import JsPDF from 'jspdf'
// 导出PDF需要通过Export插件使用
// 导出PDF插件需要通过Export插件使用
class ExportPDF {
// 构造函数
constructor(opt) {

View File

@@ -1,6 +1,6 @@
import xmind from '../parse/xmind'
// 导出XMind需要通过Export插件使用
// 导出XMind插件需要通过Export插件使用
class ExportXMind {
// 构造函数
constructor(opt) {

View File

@@ -1,7 +1,7 @@
import { bfsWalk } from '../utils'
import { CONSTANTS } from '../constants/constant'
// 键盘导航
// 键盘导航插件
class KeyboardNavigation {
// 构造函数
constructor(opt) {
@@ -28,8 +28,7 @@ class KeyboardNavigation {
this.focus(dir)
} else {
let root = this.mindMap.renderer.root
this.mindMap.renderer.moveNodeToCenter(root)
root.active()
this.mindMap.execCommand('GO_TARGET_NODE', root)
}
}
@@ -81,8 +80,7 @@ class KeyboardNavigation {
// 找到了则让目标节点聚焦
if (targetNode) {
this.mindMap.renderer.moveNodeToCenter(targetNode)
targetNode.active()
this.mindMap.execCommand('GO_TARGET_NODE', targetNode)
}
}

View File

@@ -1,4 +1,4 @@
// 小地图
// 小地图插件
class MiniMap {
// 构造函数
constructor(opt) {

View File

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

View File

@@ -28,7 +28,7 @@ let fontSizeList = new Array(100).fill(0).map((_, index) => {
return index + 'px'
})
// 节点支持富文本编辑功能
// 富文本编辑插件
class RichText {
constructor({ mindMap, pluginOpt }) {
this.mindMap = mindMap
@@ -268,6 +268,12 @@ class RichText {
handler: function () {
// 覆盖默认的回车键换行
}
},
tab: {
key: 9,
handler: function () {
// 覆盖默认的tab键
}
}
}
}

View File

@@ -0,0 +1,151 @@
import { bfsWalk, getTextFromHtml } from '../utils/index'
// 搜索插件
class Search {
// 构造函数
constructor({ mindMap }) {
this.mindMap = mindMap
// 是否正在搜索
this.isSearching = false
// 搜索文本
this.searchText = ''
// 匹配的节点列表
this.matchNodeList = []
// 当前所在的节点列表索引
this.currentIndex = -1
// 不要复位搜索文本
this.notResetSearchText = false
this.onDataChange = this.onDataChange.bind(this)
this.mindMap.on('data_change', this.onDataChange)
}
// 节点数据改变了,需要重新搜索
onDataChange() {
if (this.notResetSearchText) {
this.notResetSearchText = false
return
}
this.searchText = ''
}
// 搜索
search(text, callback) {
text = String(text).trim()
if (!text) return this.endSearch()
this.isSearching = true
if (this.searchText === text) {
// 和上一次搜索文本一样,那么搜索下一个
this.searchNext(callback)
} else {
// 和上次搜索文本不一样,那么重新开始
this.searchText = text
this.doSearch()
this.searchNext(callback)
}
this.emitEvent()
}
// 结束搜索
endSearch() {
if (!this.isSearching) return
this.searchText = ''
this.matchNodeList = []
this.currentIndex = -1
this.notResetSearchText = false
this.isSearching = false
this.emitEvent()
}
// 搜索匹配的节点
doSearch() {
this.matchNodeList = []
this.currentIndex = -1
bfsWalk(this.mindMap.renderer.root, node => {
let { richText, text } = node.nodeData.data
if (richText) {
text = getTextFromHtml(text)
}
if (text.includes(this.searchText)) {
this.matchNodeList.push(node)
}
})
}
// 搜索下一个,定位到下一个匹配节点
searchNext(callback) {
if (!this.isSearching || this.matchNodeList.length <= 0) return
if (this.currentIndex < this.matchNodeList.length - 1) {
this.currentIndex++
} else {
this.currentIndex = 0
}
let currentNode = this.matchNodeList[this.currentIndex]
this.notResetSearchText = true
this.mindMap.execCommand('GO_TARGET_NODE', currentNode, () => {
callback()
})
}
// 替换当前节点
replace(replaceText) {
replaceText = String(replaceText).trim()
if (!replaceText || !this.isSearching || this.matchNodeList.length <= 0)
return
let currentNode = this.matchNodeList[this.currentIndex]
if (!currentNode) return
let text = this.getReplacedText(currentNode, this.searchText, replaceText)
this.notResetSearchText = true
currentNode.setText(text, currentNode.nodeData.data.richText)
this.matchNodeList = this.matchNodeList.filter(node => {
return currentNode !== node
})
if (this.currentIndex > this.matchNodeList.length - 1) {
this.currentIndex = -1
} else {
this.currentIndex--
}
this.emitEvent()
}
// 替换所有
replaceAll(replaceText) {
replaceText = String(replaceText).trim()
if (!replaceText || !this.isSearching || this.matchNodeList.length <= 0)
return
this.matchNodeList.forEach(node => {
let text = this.getReplacedText(node, this.searchText, replaceText)
this.mindMap.renderer.setNodeDataRender(
node,
{
text,
resetRichText: !!node.nodeData.data.richText
},
true
)
})
this.mindMap.render()
this.mindMap.command.addHistory()
this.endSearch()
}
// 获取某个节点替换后的文本
getReplacedText(node, searchText, replaceText) {
let { richText, text } = node.nodeData.data
if (richText) {
text = getTextFromHtml(text)
}
return text.replaceAll(searchText, replaceText)
}
// 发送事件
emitEvent() {
this.mindMap.emit('search_info_change', {
currentIndex: this.currentIndex,
total: this.matchNodeList.length
})
}
}
Search.instanceName = 'search'
export default Search

View File

@@ -1,7 +1,6 @@
import { bfsWalk, throttle } from '../utils'
// 选择节点类
// 节点选择插件
class Select {
// 构造函数
constructor({ mindMap }) {

View File

@@ -1,5 +1,4 @@
// 手势事件支持
// 手势事件支持插件
class TouchEvent {
// 构造函数
constructor({ mindMap }) {

View File

@@ -2,7 +2,7 @@ import { Text, G } from '@svgdotjs/svg.js'
import { degToRad, camelCaseToHyphen } from '../utils'
import merge from 'deepmerge'
// 水印
// 水印插件
class Watermark {
constructor(opt = {}) {
this.mindMap = opt.mindMap

View File

@@ -1,3 +1,5 @@
import { v4 as uuidv4 } from 'uuid'
// 深度优先遍历树
export const walk = (
root,
@@ -31,9 +33,11 @@ export const walk = (
// 广度优先遍历树
export const bfsWalk = (root, callback) => {
callback(root)
let stack = [root]
let isStop = false
if (callback(root, null) === 'stop') {
isStop = true
}
while (stack.length) {
if (isStop) {
break
@@ -41,8 +45,9 @@ export const bfsWalk = (root, callback) => {
let cur = stack.shift()
if (cur.children && cur.children.length) {
cur.children.forEach(item => {
if (isStop) return
stack.push(item)
if (callback(item) === 'stop') {
if (callback(item, cur) === 'stop') {
isStop = true
}
})
@@ -424,3 +429,40 @@ export const getImageSize = src => {
}
})
}
// 创建节点唯一的id
export const createUid = () => {
return uuidv4()
}
// 加载图片文件
export const loadImage = imgFile => {
return new Promise((resolve, reject) => {
let fr = new FileReader()
fr.readAsDataURL(imgFile)
fr.onload = async e => {
let url = e.target.result
let size = await getImageSize(url)
resolve({
url,
size
})
}
fr.onerror = error => {
reject(error)
}
})
}
// 移除字符串中的html实体
export const removeHTMLEntities = (str) => {
[['&nbsp;', '&#160;']].forEach((item) => {
str = str.replaceAll(item[0], item[1])
})
return str
}
// 获取一个数据的类型
export const getType = (data) => {
return Object.prototype.toString.call(data).slice(7, -1)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 2479351 */
src: url('iconfont.woff2?t=1689210173189') format('woff2'),
url('iconfont.woff?t=1689210173189') format('woff'),
url('iconfont.ttf?t=1689210173189') format('truetype');
src: url('iconfont.woff2?t=1690506335310') format('woff2'),
url('iconfont.woff?t=1690506335310') format('woff'),
url('iconfont.ttf?t=1690506335310') format('truetype');
}
.iconfont {
@@ -13,6 +13,26 @@
-moz-osx-font-smoothing: grayscale;
}
.iconsousuo:before {
content: "\e693";
}
.iconjiantouyou:before {
content: "\e62d";
}
.iconbianji1:before {
content: "\e60a";
}
.icondaohang1:before {
content: "\e632";
}
.iconyanjing:before {
content: "\e8bf";
}
.iconwangzhan:before {
content: "\e628";
}

View File

@@ -210,7 +210,7 @@ export const shortcutKeyList = [
{
icon: 'icongaikuozonglan',
name: 'Insert summary',
value: 'Ctrl + S'
value: 'Ctrl + G'
},
{
icon: 'iconzhankai',
@@ -271,6 +271,11 @@ export const shortcutKeyList = [
icon: 'iconzhengli',
name: 'Arrange layout',
value: 'Ctrl + L'
},
{
icon: 'iconsousuo',
name: 'Search and Replace',
value: 'Ctrl + F'
}
]
},

View File

@@ -270,7 +270,7 @@ export const shortcutKeyList = [
{
icon: 'icongaikuozonglan',
name: '插入概要',
value: 'Ctrl + S'
value: 'Ctrl + G'
},
{
icon: 'iconzhankai',
@@ -331,6 +331,11 @@ export const shortcutKeyList = [
icon: 'iconzhengli',
name: '一键整理布局',
value: 'Ctrl + L'
},
{
icon: 'iconsousuo',
name: '搜索和替换',
value: 'Ctrl + F'
}
]
},

View File

@@ -114,8 +114,9 @@ export default {
},
navigatorToolbar: {
openMiniMap: 'Open mini map',
readonly: 'Readonly',
edit: 'Edit'
closeMiniMap: 'Close mini map',
readonly: 'Change to eadonly',
edit: 'Change to edit'
},
nodeHyperlink: {
title: 'Link',
@@ -209,5 +210,12 @@ export default {
mouseAction: {
tip1: 'Current: Left click to drag the canvas, right click to box select nodes',
tip2: 'Current: Left click to box select nodes, right click to drag the canvas',
},
search: {
searchPlaceholder: 'Please enter the search content',
replacePlaceholder: 'Please enter replacement content',
replace: 'Replace',
replaceAll: 'Replace all',
cancel: 'Cancel'
}
}

View File

@@ -114,8 +114,9 @@ export default {
},
navigatorToolbar: {
openMiniMap: '开启小地图',
readonly: '只读模式',
edit: '编辑模式'
closeMiniMap: '关闭小地图',
readonly: '切换为只读模式',
edit: '切换为编辑模式'
},
nodeHyperlink: {
title: '超链接',
@@ -209,5 +210,12 @@ export default {
mouseAction: {
tip1: '当前:左键拖动画布,右键框选节点',
tip2: '当前:左键框选节点,右键拖动画布',
},
search: {
searchPlaceholder: '请输入查找内容',
replacePlaceholder: '请输入替换内容',
replace: '替换',
replaceAll: '全部替换',
cancel: '取消'
}
}

View File

@@ -11,7 +11,7 @@ let langList = [
}
]
let StartList = ['introduction', 'start', 'deploy', 'client', 'translate', 'changelog']
let CourseList = new Array(20).fill(0).map((_, index) => {
let CourseList = new Array(21).fill(0).map((_, index) => {
return 'course' + (index + 1)
})
let APIList = [
@@ -32,10 +32,14 @@ let APIList = [
'associativeLine',
'touchEvent',
'nodeImgAdjust',
'search',
'xmind',
'markdown',
'utils'
]
let helpList = new Array(2).fill(0).map((_, index) => {
return 'help' + (index + 1)
})
const createList = (lang, list) => {
let langRouter = routerList.find(item => {
@@ -62,28 +66,39 @@ export default {
zh: [
{
groupName: '开始',
type: 'doc',
list: createList('zh', StartList)
},
{
groupName: '教程',
type: 'doc',
list: createList('zh', CourseList)
},
{
groupName: 'API',
type: 'doc',
list: createList('zh', APIList)
},
{
groupName: '使用帮助',
type: 'help',
list: createList('zh', helpList)
}
],
en: [
{
groupName: 'Start',
type: 'doc',
list: createList('en', StartList)
},
{
groupName: 'Course',
type: 'doc',
list: createList('zh', CourseList)
},
{
groupName: 'API',
type: 'doc',
list: createList('en', APIList)
}
]

View File

@@ -66,18 +66,18 @@ export default {
methods: {
// 获取当前语言
initLang() {
let lang = /^\/doc\/([^\/]+)\//.exec(this.$route.path)
if (lang && lang[1]) {
this.lang = lang[1]
let lang = /^\/(doc|help)\/([^\/]+)\//.exec(this.$route.path)
if (lang && lang[2]) {
this.lang = lang[2]
}
},
// 初始化二级标题目录
initCatalogList(newPath, oldPath) {
let newPathRes = /^\/doc\/[^\/]+\/([^\/]+)/.exec(newPath)
let oldPathRes = /^\/doc\/[^\/]+\/([^\/]+)/.exec(oldPath)
let newPathRes = /^\/(doc|help)\/[^\/]+\/([^\/]+)/.exec(newPath)
let oldPathRes = /^\/(doc|help)\/[^\/]+\/([^\/]+)/.exec(oldPath)
// 语言变了、章节变了,需要重新获取二级标题目录
if ((!newPath && !oldPath) || newPathRes[1] !== oldPathRes[1]) {
if ((!newPath && !oldPath) || newPathRes[2] !== oldPathRes[2]) {
this.$emit('scroll', 0)
this.resetActive()
let container = document.getElementById('doc')
@@ -93,9 +93,9 @@ export default {
// 如果url中存在二级标题那么滚动到该标题所在位置
scrollToCatalog() {
let url = /^\/doc\/[^\/]+\/[^\/]+\/([^\/]+)($|\/)/.exec(this.$route.path)
if (url && url[1]) {
let h = decodeURIComponent(url[1])
let url = /^\/(doc|help)\/[^\/]+\/[^\/]+\/([^\/]+)($|\/)/.exec(this.$route.path)
if (url && url[2]) {
let h = decodeURIComponent(url[2])
let item = this.list.find(item => {
return item.title === h
})
@@ -126,15 +126,15 @@ export default {
let path = this.$route.path
let url = ''
if (!title) {
url = path.replace(/^(\/doc\/[^\/]+\/[^\/]+)($|\/|.*)$/, '$1')
} else if (/^\/doc\/[^\/]+\/[^\/]+($|\/)$/.test(path)) {
url = path.replace(/^(\/(doc|help)\/[^\/]+\/[^\/]+)($|\/|.*)$/, '$1')
} else if (/^\/(doc|help)\/[^\/]+\/[^\/]+($|\/)$/.test(path)) {
url = path.replace(
/^(\/doc\/[^\/]+\/[^\/]+)($|\/)$/,
/^(\/(doc|help)\/[^\/]+\/[^\/]+)($|\/)$/,
'$1/' + encodeURIComponent(title)
)
} else {
url = path.replace(
/^(\/doc\/[^\/]+\/[^\/]+\/)([^\/]+)($|\/)/,
/^(\/(doc|help)\/[^\/]+\/[^\/]+\/)([^\/]+)($|\/)/,
(...args) => {
return args[1] + encodeURIComponent(title)
}

View File

@@ -1,7 +1,7 @@
<template>
<div class="headerContainer">
<div class="left">
<div class="title">
<div class="title" @click="toIndex">
<img src="../../../assets/img/logo2.png" alt="">
SimpleMindMap
</div>
@@ -74,6 +74,10 @@ export default {
}
},
toIndex() {
this.$router.push('/index')
},
toDemo() {
this.$router.push('/')
},
@@ -108,6 +112,7 @@ export default {
font-weight: bold;
display: flex;
align-items: center;
cursor: pointer;
img {
width: 30px;

View File

@@ -31,7 +31,8 @@ export default {
return {
groupList: [],
lang: '',
currentPath: ''
currentPath: '',
type: ''// doc、help
}
},
created() {
@@ -47,20 +48,24 @@ export default {
if (item.path === this.currentPath) {
return
}
this.$router.push(`/doc/${this.lang}/${item.path}`)
this.$router.push(`/${this.type}/${this.lang}/${item.path}`)
},
initCatalog() {
// 目录列表
let lang = /^\/doc\/([^\/]+)\//.exec(this.$route.path)
if (lang && lang[1]) {
this.lang = lang[1]
this.groupList = catalogList[this.lang]
let lang = /^\/(doc|help)\/([^\/]+)\//.exec(this.$route.path)
if (lang && lang[2]) {
this.type = lang[1]// 判断是开发文档还是帮助文档
this.lang = lang[2]
// 过滤出对应文档的章节
this.groupList = catalogList[this.lang].filter((item) => {
return item.type === this.type
})
}
// 当前所在路径
let path = /^\/doc\/[^\/]+\/([^\/]+)(\/|$)/.exec(this.$route.path)
if (path && path[1]) {
this.currentPath = path[1]
let path = /^\/(doc|help)\/[^\/]+\/([^\/]+)(\/|$)/.exec(this.$route.path)
if (path && path[2]) {
this.currentPath = path[2]
}
}
}

View File

@@ -1,5 +1,37 @@
# Changelog
## 0.6.9-fix.1
Fix: 1.Fix the issue of incorrect replacement after a single search.
New: 1.We will no longer directly modify the incoming data object, but will make a deep copy internally.
## 0.6.9
Fix: 1.Fixed an issue where setting styles to summary nodes would cause summary nodes to disappear. 2.Fixed the issue of node content not rendering when creating a root instance again when customizing node content. 3.Fix the issue of losing focus when adding a new node while the node is in editing. 2.Fix the issue of continuously pressing the tab key not being able to continuously create child nodes.
New: 1.Replace existing `&nbsp;` in SVG when exporting Characters to avoid exporting SVG errors. 2.Support for search and replace.
Demo: 1.When switching themes, it is supported to choose whether to overwrite the set basic style.
## 0.6.8
Fix: 1.Change the shortcut key for inserting a summary to Ctrl+G to avoid conflicts with the save shortcut key. 2.Fix the issue of abnormal switching between rich text editing configuration input boxes while nodes are being edited.
New: 1.Modify the copy, cut, and paste logic, and support pasting data from the clipboard.
Demo: 1.Fix the issue of not saving the outer margin of the basic style setting node. 2.Supports automatic switching to dark mode based on the theme.
## 0.6.7
Fix: 1.Fixed the issue of missing placeholder elements for the expand and collapse button after node collapse and expansion. 2.Fixed the issue of being able to scale images in read-only mode.
New: 1.Support locating to a node based on node instance or node uid. 2.Modify the creation method of node uids and export data to add node uids.
Remove: 1.Remove the node transition effect.
Demo: 1.Add website homepage. 2.Fixed the issue of missing node styles when creating new nodes in the outline. 3.Fixed the issue of missing edited text after pressing Enter or Tab after editing nodes in the outline. 4.Optimize the node positioning of the outline, and the collapsed nodes will automatically expand. 5.The sidebar button supports folding. 6.Optimize small screen adaptation.
## 0.6.6
New: 1.Support exporting to Xmind new version files. 2.Importing the new version of Xmind file supports importing images from nodes. 3.Add a vertical timeline structure.

View File

@@ -1,6 +1,22 @@
<template>
<div>
<h1>Changelog</h1>
<h2>0.6.9-fix.1</h2>
<p>Fix: 1.Fix the issue of incorrect replacement after a single search.</p>
<p>New: 1.We will no longer directly modify the incoming data object, but will make a deep copy internally.</p>
<h2>0.6.9</h2>
<p>Fix: 1.Fixed an issue where setting styles to summary nodes would cause summary nodes to disappear. 2.Fixed the issue of node content not rendering when creating a root instance again when customizing node content. 3.Fix the issue of losing focus when adding a new node while the node is in editing. 2.Fix the issue of continuously pressing the tab key not being able to continuously create child nodes.</p>
<p>New: 1.Replace existing <code>&amp;nbsp;</code> in SVG when exporting Characters to avoid exporting SVG errors. 2.Support for search and replace.</p>
<p>Demo: 1.When switching themes, it is supported to choose whether to overwrite the set basic style.</p>
<h2>0.6.8</h2>
<p>Fix: 1.Change the shortcut key for inserting a summary to Ctrl+G to avoid conflicts with the save shortcut key. 2.Fix the issue of abnormal switching between rich text editing configuration input boxes while nodes are being edited.</p>
<p>New: 1.Modify the copy, cut, and paste logic, and support pasting data from the clipboard.</p>
<p>Demo: 1.Fix the issue of not saving the outer margin of the basic style setting node. 2.Supports automatic switching to dark mode based on the theme.</p>
<h2>0.6.7</h2>
<p>Fix: 1.Fixed the issue of missing placeholder elements for the expand and collapse button after node collapse and expansion. 2.Fixed the issue of being able to scale images in read-only mode.</p>
<p>New: 1.Support locating to a node based on node instance or node uid. 2.Modify the creation method of node uids and export data to add node uids.</p>
<p>Remove: 1.Remove the node transition effect.</p>
<p>Demo: 1.Add website homepage. 2.Fixed the issue of missing node styles when creating new nodes in the outline. 3.Fixed the issue of missing edited text after pressing Enter or Tab after editing nodes in the outline. 4.Optimize the node positioning of the outline, and the collapsed nodes will automatically expand. 5.The sidebar button supports folding. 6.Optimize small screen adaptation.</p>
<h2>0.6.6</h2>
<p>New: 1.Support exporting to Xmind new version files. 2.Importing the new version of Xmind file supports importing images from nodes. 3.Add a vertical timeline structure.</p>
<p>Fix: 1.The TouchEvent plugin no longer sends click events, solving the problem of two windows opening when clicking on a hyperlink on the mobile end. 2.Fix the issue of dragging and moving a node to become a child node of another node, where the parent node of that node points to not being updated. 3.Fixed an issue where the node border style was not updated when dragging a second level node into a third level node. 4.Fix the issue where the mouse will not trigger the button display when moving into the unfolded or retracted button position, except for the structure growing to the right.</p>

View File

@@ -51,8 +51,8 @@ const mindMap = new MindMap({
| expandBtnStylev0.5.0+ | Object | { color: '#808080', fill: '#fff' } | Expand the color of the stow button | |
| expandBtnIconv0.5.0+ | Object | { open: '', close: '' } | Customize the icon of the expand/collapse button, and you can transfer the svg string of the icon | |
| enableShortcutOnlyWhenMouseInSvgv0.5.1+ | Boolean | true | Only respond to shortcut key events when the mouse is inside the canvas | |
| enableNodeTransitionMovev0.5.1+ | Boolean | true | Whether to enable node animation transition | |
| nodeTransitionMoveDurationv0.5.1+ | Number | 300 | If node animation transition is enabled, the transition time can be set using this attribute, in milliseconds | |
| enableNodeTransitionMovev0.5.1+v0.6.7+ is remove this feature | Boolean | true | Whether to enable node animation transition | |
| nodeTransitionMoveDurationv0.5.1+v0.6.7+ is remove this feature | Number | 300 | If node animation transition is enabled, the transition time can be set using this attribute, in milliseconds | |
| initRootNodePositionv0.5.3+ | Array | null | The position of the initial root node can be passed as an array, default is `['center', 'center']`, Represents the root node at the center of the canvas, In addition to `center`, keywords can also be set to `left`, `top`, `right`, and `bottom`, In addition to passing keywords, each item in the array can also pass a number representing a specific pixel, Can pass a percentage string, such as `['40%', '60%']`, Represents a horizontal position at `40%` of the canvas width, and a vertical position at `60%` of the canvas height | |
| exportPaddingXv0.5.5+ | Number | 10 | Horizontal padding of graphics when exporting PNG, SVG, and PDF | |
| exportPaddingYv0.5.5+ | Number | 10 | Vertical padding of graphics when exporting PNG, SVG, and PDF | |
@@ -347,6 +347,7 @@ redo. All commands are as follows:
| SET_NODE_CUSTOM_POSITION (v0.2.0+) | Set a custom position for a node | node (the node to set), left (custom x coordinate, default is undefined), top (custom y coordinate, default is undefined) |
| RESET_LAYOUT (v0.2.0+) | Arrange layout with one click | |
| SET_NODE_SHAPE (v0.2.4+) | Set the shape of a node | node (the node to set), shape (the shape, all shapes: [Shape.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/core/render/node/Shape.js)) |
| GO_TARGET_NODEv0.6.7+ | Navigate to a node, and if the node is collapsed, it will automatically expand to that node | nodeNode instance or node uid to locate、callbackv0.6.9+, Callback function after positioning completion |
### setData(data)

View File

@@ -218,14 +218,14 @@
<td></td>
</tr>
<tr>
<td>enableNodeTransitionMovev0.5.1+</td>
<td>enableNodeTransitionMovev0.5.1+v0.6.7+ is remove this feature</td>
<td>Boolean</td>
<td>true</td>
<td>Whether to enable node animation transition</td>
<td></td>
</tr>
<tr>
<td>nodeTransitionMoveDurationv0.5.1+</td>
<td>nodeTransitionMoveDurationv0.5.1+v0.6.7+ is remove this feature</td>
<td>Number</td>
<td>300</td>
<td>If node animation transition is enabled, the transition time can be set using this attribute, in milliseconds</td>
@@ -920,6 +920,11 @@ redo. All commands are as follows:</p>
<td>Set the shape of a node</td>
<td>node (the node to set), shape (the shape, all shapes: <a href="https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/core/render/node/Shape.js">Shape.js</a>)</td>
</tr>
<tr>
<td>GO_TARGET_NODEv0.6.7+</td>
<td>Navigate to a node, and if the node is collapsed, it will automatically expand to that node</td>
<td>nodeNode instance or node uid to locatecallbackv0.6.9+, Callback function after positioning completion</td>
</tr>
</tbody>
</table>
<h3>setData(data)</h3>

View File

@@ -1 +1,66 @@
# Deploy
# Deploy
The 'web' directory of this project provides a complete project developed based on the 'simple mind map' library, 'Vue2. x', and 'ElementUI'. The data is stored locally on the computer by default, and can also be manipulated locally on the computer. Originally intended as an online 'demo', it can also be directly used as an online version of the mind map application, online address: [https://wanglin2.github.io/mind-map/](https://wanglin2.github.io/mind-map/).
If your network environment is slow to access the 'GitHub' service, you can also deploy it to your server.
## Deploying to a static file server
The project itself does not rely on the backend, so it can be deployed to a static file server. The following commands can be executed in sequence:
```bash
git clone https://github.com/wanglin2/mind-map.git
cd mind-map
cd simple-mind-map
npm i
npm link
cd ..
cd web
npm i
npm link simple-mind-map
```
Then you can choose to start the local service:
```bash
npm run serve
```
You can also directly package and generate construction products:
```bash
npm run build
```
The packaged entry page 'index.html' can be found in the project root directory, and the corresponding static resources are located in the 'dist' directory under the root directory. The 'html' file will access the resources in the 'dist' directory through relative paths, such as 'dist/xxx'. You can directly upload these two files or directories to your static file server. In fact, this project is deployed to 'GitHub Pages' in this way.
If you do not have any code modification requirements, it is also possible to directly copy these files from this repository.
If you want to package 'index.html' into the 'dist' directory as well, you can modify the 'scripts.build' command in the 'web/package.json' file to delete '&& node ../copy.js' from 'vue-cli-service build && node ../copy.js'.
If you want to modify the directory for packaging output, you can modify the 'outputDir' configuration of the 'web/vue.config.js' file to the path you want to output.
If you want to modify the path of the 'index. html' file referencing static resources, you can modify the 'publicPath' configuration of the 'web/vue.config.js' file.
In addition, the default route used is 'hash ', which means that there will be '#'in the path. If you want to use the 'history' route, you can modify the 'web/src/router.js' file to:
```js
const router = new VueRouter({
routes
})
```
Change to:
```js
const router = new VueRouter({
mode: 'history',
routes
})
```
However, this requires backend support, as our application is a single page client application. If the backend is not properly configured, users will return 404 when accessing sub routes directly in the browser. Therefore, you need to add a candidate resource on the server that covers all situations: if the 'URL' cannot match any static resources, the same 'index. html' page should be returned.
## Docker
In writing...

View File

@@ -0,0 +1,55 @@
<template>
<div>
<h1>Deploy</h1>
<p>The 'web' directory of this project provides a complete project developed based on the 'simple mind map' library, 'Vue2. x', and 'ElementUI'. The data is stored locally on the computer by default, and can also be manipulated locally on the computer. Originally intended as an online 'demo', it can also be directly used as an online version of the mind map application, online address: <a href="https://wanglin2.github.io/mind-map/">https://wanglin2.github.io/mind-map/</a>.</p>
<p>If your network environment is slow to access the 'GitHub' service, you can also deploy it to your server.</p>
<h2>Deploying to a static file server</h2>
<p>The project itself does not rely on the backend, so it can be deployed to a static file server. The following commands can be executed in sequence:</p>
<pre class="hljs"><code>git <span class="hljs-built_in">clone</span> https://github.com/wanglin2/mind-map.git
<span class="hljs-built_in">cd</span> mind-map
<span class="hljs-built_in">cd</span> simple-mind-map
npm i
npm link
<span class="hljs-built_in">cd</span> ..
<span class="hljs-built_in">cd</span> web
npm i
npm link simple-mind-map
</code></pre>
<p>Then you can choose to start the local service:</p>
<pre class="hljs"><code>npm run serve
</code></pre>
<p>You can also directly package and generate construction products:</p>
<pre class="hljs"><code>npm run build
</code></pre>
<p>The packaged entry page 'index.html' can be found in the project root directory, and the corresponding static resources are located in the 'dist' directory under the root directory. The 'html' file will access the resources in the 'dist' directory through relative paths, such as 'dist/xxx'. You can directly upload these two files or directories to your static file server. In fact, this project is deployed to 'GitHub Pages' in this way.</p>
<p>If you do not have any code modification requirements, it is also possible to directly copy these files from this repository.</p>
<p>If you want to package 'index.html' into the 'dist' directory as well, you can modify the 'scripts.build' command in the 'web/package.json' file to delete '&amp;&amp; node ../copy.js' from 'vue-cli-service build &amp;&amp; node ../copy.js'.</p>
<p>If you want to modify the directory for packaging output, you can modify the 'outputDir' configuration of the 'web/vue.config.js' file to the path you want to output.</p>
<p>If you want to modify the path of the 'index. html' file referencing static resources, you can modify the 'publicPath' configuration of the 'web/vue.config.js' file.</p>
<p>In addition, the default route used is 'hash ', which means that there will be '#'in the path. If you want to use the 'history' route, you can modify the 'web/src/router.js' file to:</p>
<pre class="hljs"><code><span class="hljs-keyword">const</span> router = <span class="hljs-keyword">new</span> VueRouter({
routes
})
</code></pre>
<p>Change to:</p>
<pre class="hljs"><code><span class="hljs-keyword">const</span> router = <span class="hljs-keyword">new</span> VueRouter({
<span class="hljs-attr">mode</span>: <span class="hljs-string">&#x27;history&#x27;</span>,
routes
})
</code></pre>
<p>However, this requires backend support, as our application is a single page client application. If the backend is not properly configured, users will return 404 when accessing sub routes directly in the browser. Therefore, you need to add a candidate resource on the server that covers all situations: if the 'URL' cannot match any static resources, the same 'index. html' page should be returned.</p>
<h2>Docker</h2>
<p>In writing...</p>
</div>
</template>
<script>
export default {
}
</script>
<style>
</style>

View File

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

View File

@@ -115,6 +115,18 @@ full screen, support mini map</li>
<img src="../../../../assets/avatar/suka.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>suka</p>
</div>
<div style="display: flex; flex-direction: column; align-items: center; width: fit-content; margin: 5px;">
<img src="../../../../assets/avatar/Chris.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>Chris</p>
</div>
<div style="display: flex; flex-direction: column; align-items: center; width: fit-content; margin: 5px;">
<img src="../../../../assets/avatar/水车.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>水车</p>
</div>
<div style="display: flex; flex-direction: column; align-items: center; width: fit-content; margin: 5px;">
<img src="../../../../assets/avatar/仓鼠.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>仓鼠</p>
</div>
</div>
</div>
</template>

View File

@@ -61,7 +61,9 @@ Delete a specific node
Copy a node, the active node is the node to be operated on, if there are
multiple active nodes, only the first node will be operated on
### setNodeDataRender(node, data)
### setNodeDataRender(node, data, notRender)
- `notRender`: v0.6.9+, `Boolean`, Default is `false`, Do not trigger rendering.
Set node `data`, i.e. the data in the data field, and will determine whether the
node needs to be re-rendered based on whether the node size has changed, `data`
@@ -91,4 +93,40 @@ Move a node behind another node
Move a node to the center of the canvas.
Currently, if there is zoom, returning to the center will reset the zoom.
Currently, if there is zoom, returning to the center will reset the zoom.
### expandToNodeUid(uid, callback)
> v0.6.7+
- `uid`: uid of node
- `callback`: Expand completed callback function
Expand to the node of the specified uid.
### findNodeByUid(uid)
> v0.6.7+
- `uid`: uid of node
Find the corresponding node instance based on the uid.
### copy()
> v0.6.8+
Copy nodes. After calling this method, the current activated node data will be stored. Multiple activated nodes will only operate on the first node, and subsequent calls to the 'paste()' method can be pasted.
### cut()
> v0.6.8+
Cut a node. After calling this method, the currently active node will be cut and the node data will be stored. Multiple nodes will only operate on the first node, and subsequent calls to the 'paste()' method can be pasted.
### paste()
> v0.6.8+
Pasting nodes can be done by calling the 'copy()' or 'cut()' method after calling it. This method does not support pasting data from the user's clipboard. Please use the built-in 'Ctrl+v' shortcut key.

View File

@@ -37,7 +37,10 @@ disable the enter key and delete key related shortcuts to prevent conflicts</p>
<h3>copyNode()</h3>
<p>Copy a node, the active node is the node to be operated on, if there are
multiple active nodes, only the first node will be operated on</p>
<h3>setNodeDataRender(node, data)</h3>
<h3>setNodeDataRender(node, data, notRender)</h3>
<ul>
<li><code>notRender</code>: v0.6.9+, <code>Boolean</code>, Default is <code>false</code>, Do not trigger rendering.</li>
</ul>
<p>Set node <code>data</code>, i.e. the data in the data field, and will determine whether the
node needs to be re-rendered based on whether the node size has changed, <code>data</code>
is an object, e.g. <code>{text: 'I am new text'}</code></p>
@@ -62,6 +65,42 @@ is an object, e.g. <code>{text: 'I am new text'}</code></p>
</blockquote>
<p>Move a node to the center of the canvas.</p>
<p>Currently, if there is zoom, returning to the center will reset the zoom.</p>
<h3>expandToNodeUid(uid, callback)</h3>
<blockquote>
<p>v0.6.7+</p>
</blockquote>
<ul>
<li>
<p><code>uid</code>: uid of node</p>
</li>
<li>
<p><code>callback</code>: Expand completed callback function</p>
</li>
</ul>
<p>Expand to the node of the specified uid.</p>
<h3>findNodeByUid(uid)</h3>
<blockquote>
<p>v0.6.7+</p>
</blockquote>
<ul>
<li><code>uid</code>: uid of node</li>
</ul>
<p>Find the corresponding node instance based on the uid.</p>
<h3>copy()</h3>
<blockquote>
<p>v0.6.8+</p>
</blockquote>
<p>Copy nodes. After calling this method, the current activated node data will be stored. Multiple activated nodes will only operate on the first node, and subsequent calls to the 'paste()' method can be pasted.</p>
<h3>cut()</h3>
<blockquote>
<p>v0.6.8+</p>
</blockquote>
<p>Cut a node. After calling this method, the currently active node will be cut and the node data will be stored. Multiple nodes will only operate on the first node, and subsequent calls to the 'paste()' method can be pasted.</p>
<h3>paste()</h3>
<blockquote>
<p>v0.6.8+</p>
</blockquote>
<p>Pasting nodes can be done by calling the 'copy()' or 'cut()' method after calling it. This method does not support pasting data from the user's clipboard. Please use the built-in 'Ctrl+v' shortcut key.</p>
</div>
</template>

View File

@@ -0,0 +1,68 @@
# Search plugin
> v0.6.9+
This plugin provides the ability to search and replace node content.
## Register
```js
import MindMap from 'simple-mind-map'
import Search from 'simple-mind-map/src/plugins/Search.js'
MindMap.usePlugin(Search)
```
After registration and instantiation of `MindMap`, the instance can be obtained through `mindMap.Search`.
## Event
### search_info_change
You can listen to 'search_info_change' event to get the number of current search results and the index currently located.
```js
mindMap.on('search_info_change', (data) => {
/*
data: {
currentIndex,// Index, from zero
total
}
*/
})
```
## Method
### search(searchText, callback)
- `searchText`: Text to search for
- `callback`: The callback function that completes this search will be triggered after jumping to the node
Search for node content, which can be called repeatedly. Each call will search and locate to the next matching node. If the search text changes, it will be searched again.
### endSearch()
End search.
### replace(replaceText)
- `replaceText`: Text to be replaced
To replace the content of the current node, call the 'search' method after calling it to replace the content of the currently located matching node.
### replaceAll(replaceText)
- `replaceText`: Text to be replaced
Replace all matching node contents, and call it after calling the 'search' method.
### getReplacedText(node, searchText, replaceText)
- `node`: Node instance
- `searchText`: Text to search for
- `replaceText`: Text to be replaced
Return the text content of the node after search and replacement. Note that the node content will not be actually changed, but is only used to calculate the content of a node after replacement.

View File

@@ -0,0 +1,74 @@
<template>
<div>
<h1>Search plugin</h1>
<blockquote>
<p>v0.6.9+</p>
</blockquote>
<p>This plugin provides the ability to search and replace node content.</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> Search <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;simple-mind-map/src/plugins/Search.js&#x27;</span>
MindMap.usePlugin(Search)
</code></pre>
<p>After registration and instantiation of <code>MindMap</code>, the instance can be obtained through <code>mindMap.Search</code>.</p>
<h2>Event</h2>
<h3>search_info_change</h3>
<p>You can listen to 'search_info_change' event to get the number of current search results and the index currently located.</p>
<pre class="hljs"><code>mindMap.on(<span class="hljs-string">&#x27;search_info_change&#x27;</span>, <span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> {
<span class="hljs-comment">/*
data: {
currentIndex,// Index, from zero
total
}
*/</span>
})
</code></pre>
<h2>Method</h2>
<h3>search(searchText, callback)</h3>
<ul>
<li>
<p><code>searchText</code>: Text to search for</p>
</li>
<li>
<p><code>callback</code>: The callback function that completes this search will be triggered after jumping to the node</p>
</li>
</ul>
<p>Search for node content, which can be called repeatedly. Each call will search and locate to the next matching node. If the search text changes, it will be searched again.</p>
<h3>endSearch()</h3>
<p>End search.</p>
<h3>replace(replaceText)</h3>
<ul>
<li><code>replaceText</code>: Text to be replaced</li>
</ul>
<p>To replace the content of the current node, call the 'search' method after calling it to replace the content of the currently located matching node.</p>
<h3>replaceAll(replaceText)</h3>
<ul>
<li><code>replaceText</code>: Text to be replaced</li>
</ul>
<p>Replace all matching node contents, and call it after calling the 'search' method.</p>
<h3>getReplacedText(node, searchText, replaceText)</h3>
<ul>
<li>
<p><code>node</code>: Node instance</p>
</li>
<li>
<p><code>searchText</code>: Text to search for</p>
</li>
<li>
<p><code>replaceText</code>: Text to be replaced</p>
</li>
</ul>
<p>Return the text content of the node after search and replacement. Note that the node content will not be actually changed, but is only used to calculate the content of a node after replacement.</p>
</div>
</template>
<script>
export default {
}
</script>
<style>
</style>

View File

@@ -181,6 +181,27 @@ Get the size of image, return:
}
```
#### loadImage(imgFile)
> v0.6.8+
- `imgFile`: File object of image type
Load image, return:
```js
{
url,// DataUrl
size// { width, height } width and height of image
}
```
#### getType(data)
> v0.6.9+
Get the type of a data, such as `Boolean``Array`.
## Simulate CSS background in Canvas
Import:

View File

@@ -121,6 +121,24 @@ and copying the <code>data</code> of the data object, example:</p>
height
}
</code></pre>
<h4>loadImage(imgFile)</h4>
<blockquote>
<p>v0.6.8+</p>
</blockquote>
<ul>
<li><code>imgFile</code>: File object of image type</li>
</ul>
<p>Load image, return:</p>
<pre class="hljs"><code>{
url,<span class="hljs-comment">// DataUrl</span>
size<span class="hljs-comment">// { width, height } width and height of image</span>
}
</code></pre>
<h4>getType(data)</h4>
<blockquote>
<p>v0.6.9+</p>
</blockquote>
<p>Get the type of a data, such as <code>Boolean</code><code>Array</code>.</p>
<h2>Simulate CSS background in Canvas</h2>
<p>Import:</p>
<pre class="hljs"><code><span class="hljs-keyword">import</span> drawBackgroundImageToCanvas <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;simple-mind-map/src/utils/simulateCSSBackgroundInCanvas&#x27;</span>

View File

@@ -27,6 +27,7 @@ export default [
{ path: 'course18', title: '如何持久化数据' },
{ path: 'course19', title: '插入和扩展节点图标' },
{ path: 'course20', title: '如何自定义节点内容' },
{ path: 'course21', title: '如何复制、剪切、粘贴' },
{ path: 'doExport', title: 'Export 插件' },
{ path: 'drag', title: 'Drag插件' },
{ path: 'introduction', title: '简介' },
@@ -47,7 +48,10 @@ export default [
{ path: 'deploy', title: '部署' },
{ path: 'client', title: '客户端' },
{ path: 'touchEvent', title: 'TouchEvent插件' },
{ path: 'nodeImgAdjust', title: 'NodeImgAdjust插件' }
{ path: 'nodeImgAdjust', title: 'NodeImgAdjust插件' },
{ path: 'search', title: 'Search插件' },
{ path: 'help1', title: '概要/关联线' },
{ path: 'help2', title: '客户端' }
]
},
{
@@ -77,7 +81,8 @@ export default [
{ path: 'xmind', title: 'XMind parse' },
{ path: 'deploy', title: 'Deploy' },
{ path: 'touchEvent', title: 'TouchEvent plugin' },
{ path: 'nodeImgAdjust', title: 'NodeImgAdjust plugin' }
{ path: 'nodeImgAdjust', title: 'NodeImgAdjust plugin' },
{ path: 'search', title: 'Search plugin' }
]
}
]

View File

@@ -1,5 +1,37 @@
# Changelog
## 0.6.9-fix.1
修复1.修复搜索进行一次单个替换后再全部替换不正确的问题。
新增1.不会再直接修改传入的data对象内部会深拷贝一份。
## 0.6.9
修复1.修复给概要节点设置样式概要节点会消失的问题。2.修复自定义节点内容时二次创建根实例时节点内容不渲染的问题。3.修复节点处于编辑中时添加新节点时新节点的焦点丢失问题。 2.修复连续按tab键无法连续创建子节点的问题。
新增1.导出svg时替换svg中存在的`&nbsp;`字符避免导出的svg报错。 2.支持搜索和替换。
Demo1.切换主题时支持选择是否覆盖设置过的基础样式。
## 0.6.8
修复1.修改插入概要的快捷键为Ctrl+G避免和保存快捷键冲突。 2.修复节点正在编辑时切换富文本编辑配置输入框出现异常的问题。
新增1.修改复制、剪切、粘贴逻辑,支持粘贴剪切板中的数据。
Demo1.修复基础样式-设置节点外边距未保存的问题。 2.支持根据主题自动切换为暗黑模式。
## 0.6.7
修复1.修复节点收起再展开后展开收起按钮占位元素丢失的问题。 2.修复只读模式下可以缩放图片的问题。
新增1.支持根据节点实例或节点uid定位到某个节点。 2.修改节点uid的创建方式导出数据添加节点的uid。
移除1.移除节点过渡效果。
Demo1.添加网站首页。 2.修复大纲里创建新节点时节点样式丢失的问题。 3.修复大纲里编辑节点后按回车或Tab键后编辑文本丢失的问题。 4.优化大纲的节点定位,被收起的节点会自动展开。 5.侧边栏按钮支持收起。 6.优化小屏适配。
## 0.6.6
新增1.支持导出为Xmind新版文件。2.导入Xmind新版文件支持导入节点中的图片。 3.新增竖向时间轴结构。

View File

@@ -1,6 +1,22 @@
<template>
<div>
<h1>Changelog</h1>
<h2>0.6.9-fix.1</h2>
<p>修复1.修复搜索进行一次单个替换后再全部替换不正确的问题</p>
<p>新增1.不会再直接修改传入的data对象内部会深拷贝一份</p>
<h2>0.6.9</h2>
<p>修复1.修复给概要节点设置样式概要节点会消失的问题2.修复自定义节点内容时二次创建根实例时节点内容不渲染的问题3.修复节点处于编辑中时添加新节点时新节点的焦点丢失问题 2.修复连续按tab键无法连续创建子节点的问题</p>
<p>新增1.导出svg时替换svg中存在的<code>&amp;nbsp;</code>字符避免导出的svg报错 2.支持搜索和替换</p>
<p>Demo1.切换主题时支持选择是否覆盖设置过的基础样式</p>
<h2>0.6.8</h2>
<p>修复1.修改插入概要的快捷键为Ctrl+G避免和保存快捷键冲突 2.修复节点正在编辑时切换富文本编辑配置输入框出现异常的问题</p>
<p>新增1.修改复制剪切粘贴逻辑支持粘贴剪切板中的数据</p>
<p>Demo1.修复基础样式-设置节点外边距未保存的问题 2.支持根据主题自动切换为暗黑模式</p>
<h2>0.6.7</h2>
<p>修复1.修复节点收起再展开后展开收起按钮占位元素丢失的问题 2.修复只读模式下可以缩放图片的问题</p>
<p>新增1.支持根据节点实例或节点uid定位到某个节点 2.修改节点uid的创建方式导出数据添加节点的uid</p>
<p>移除1.移除节点过渡效果</p>
<p>Demo1.添加网站首页 2.修复大纲里创建新节点时节点样式丢失的问题 3.修复大纲里编辑节点后按回车或Tab键后编辑文本丢失的问题 4.优化大纲的节点定位被收起的节点会自动展开 5.侧边栏按钮支持收起 6.优化小屏适配</p>
<h2>0.6.6</h2>
<p>新增1.支持导出为Xmind新版文件2.导入Xmind新版文件支持导入节点中的图片 3.新增竖向时间轴结构</p>
<p>修复1.TouchEvent插件不再派发click事件解决移动端点击超链接会打开两个窗口的问题 2.修复拖拽移动一个节点成为另一个节点的子节点时该节点的父节点指向未更新的问题 3.修复二级节点拖拽成三级节点时节点边框样式未更新的问题 4.修复向右生长的结构外其他结构鼠标移入展开收起按钮位置时不会触发按钮显示的问题</p>

View File

@@ -51,8 +51,8 @@ const mindMap = new MindMap({
| expandBtnStylev0.5.0+ | Object | { color: '#808080', fill: '#fff' } | 展开收起按钮的颜色 | |
| expandBtnIconv0.5.0+ | Object | { open: '', close: '' } | 自定义展开收起按钮的图标可以传图标的svg字符串 | |
| enableShortcutOnlyWhenMouseInSvgv0.5.1+ | Boolean | true | 是否只有当鼠标在画布内才响应快捷键事件 | |
| enableNodeTransitionMovev0.5.1+ | Boolean | true | 是否开启节点动画过渡 | |
| nodeTransitionMoveDurationv0.5.1+ | Number | 300 | 如果开启节点动画过渡可以通过该属性设置过渡的时间单位ms | |
| enableNodeTransitionMovev0.5.1+v0.6.7+已去除该特性) | Boolean | true | 是否开启节点动画过渡 | |
| nodeTransitionMoveDurationv0.5.1+v0.6.7+已去除该特性) | Number | 300 | 如果开启节点动画过渡可以通过该属性设置过渡的时间单位ms | |
| initRootNodePositionv0.5.3+ | Array | null | 初始根节点的位置,可传一个数组,默认为`['center', 'center']`,代表根节点处于画布中心位置,除了`center`,关键词还可以设置`left``top``right``bottom`,除了可以传关键词,数组的每项还可以传递一个数字,代表具体的像素,可以传递一个百分比字符串,比如`['40%', '60%']`,代表水平位置在画布宽度的`40%`的位置,垂直位置在画布高度的`60%`的位置 | |
| exportPaddingXv0.5.5+ | Number | 10 | 导出png、svg、pdf时的图形水平内边距 | |
| exportPaddingYv0.5.5+ | Number | 10 | 导出png、svg、pdf时的图形垂直内边距 | |
@@ -340,6 +340,7 @@ mindMap.updateConfig({
| SET_NODE_CUSTOM_POSITIONv0.2.0+ | 设置节点自定义位置 | node要设置的节点、 left自定义的x坐标默认为undefined、 top自定义的y坐标默认为undefined |
| RESET_LAYOUTv0.2.0+ | 一键整理布局 | |
| SET_NODE_SHAPEv0.2.4+ | 设置节点形状 | node要设置的节点、shape形状全部形状[Shape.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/core/render/node/Shape.js) |
| GO_TARGET_NODEv0.6.7+ | 定位到某个节点,如果该节点被收起,那么会自动展开到该节点 | node要定位到的节点实例或节点uid、callbackv0.6.9+,定位完成后的回调函数) |
### setData(data)

View File

@@ -218,14 +218,14 @@
<td></td>
</tr>
<tr>
<td>enableNodeTransitionMovev0.5.1+</td>
<td>enableNodeTransitionMovev0.5.1+v0.6.7+已去除该特性</td>
<td>Boolean</td>
<td>true</td>
<td>是否开启节点动画过渡</td>
<td></td>
</tr>
<tr>
<td>nodeTransitionMoveDurationv0.5.1+</td>
<td>nodeTransitionMoveDurationv0.5.1+v0.6.7+已去除该特性</td>
<td>Number</td>
<td>300</td>
<td>如果开启节点动画过渡可以通过该属性设置过渡的时间单位ms</td>
@@ -915,6 +915,11 @@ mindMap.setTheme(<span class="hljs-string">&#x27;主题名称&#x27;</span>)
<td>设置节点形状</td>
<td>node要设置的节点shape形状全部形状<a href="https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/core/render/node/Shape.js">Shape.js</a></td>
</tr>
<tr>
<td>GO_TARGET_NODEv0.6.7+</td>
<td>定位到某个节点如果该节点被收起那么会自动展开到该节点</td>
<td>node要定位到的节点实例或节点uidcallbackv0.6.9+定位完成后的回调函数</td>
</tr>
</tbody>
</table>
<h3>setData(data)</h3>

View File

@@ -9,6 +9,8 @@ import { themeList } from 'simple-mind-map/src/constants/constant'
// import { themeList } from 'simple-mind-map/src/utils/constant' v0.6.0以下版本使用该路径
```
> v0.6.8+主题列表增加了代表是否是暗黑主题的字段dark你可以根据这个字段来将界面切换为暗黑模式。
可以在实例化`simple-mind-map`时指定使用的主题:
```js

View File

@@ -6,6 +6,9 @@
<pre class="hljs"><code><span class="hljs-keyword">import</span> { themeList } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;simple-mind-map/src/constants/constant&#x27;</span>
<span class="hljs-comment">// import { themeList } from &#x27;simple-mind-map/src/utils/constant&#x27; v0.6.0以下版本使用该路径</span>
</code></pre>
<blockquote>
<p>v0.6.8+主题列表增加了代表是否是暗黑主题的字段dark你可以根据这个字段来将界面切换为暗黑模式</p>
</blockquote>
<p>可以在实例化<code>simple-mind-map</code>时指定使用的主题</p>
<pre class="hljs"><code><span class="hljs-keyword">new</span> MindMap({
<span class="hljs-attr">theme</span>: <span class="hljs-string">&#x27;minions&#x27;</span>

View File

@@ -18,6 +18,9 @@ mindMap.on('data_change', (data) => {
const node = data._node
mindMap.renderer.moveNodeToCenter(node)
node.active()
// 在v0.6.7+版本可以这么做:
mindMap.execCommand('GO_TARGET_NODE', node)// 或者传节点的uid
```
当在大纲树上编辑了某个节点的内容,需要同步到思维导图树上:

View File

@@ -13,6 +13,9 @@
<pre class="hljs"><code><span class="hljs-keyword">const</span> node = data._node
mindMap.renderer.moveNodeToCenter(node)
node.active()
<span class="hljs-comment">// 在v0.6.7+版本可以这么做:</span>
mindMap.execCommand(<span class="hljs-string">&#x27;GO_TARGET_NODE&#x27;</span>, node)<span class="hljs-comment">// 或者传节点的uid</span>
</code></pre>
<p>当在大纲树上编辑了某个节点的内容需要同步到思维导图树上</p>
<pre class="hljs"><code>data._node.setText(<span class="hljs-string">&#x27;xxx&#x27;</span>)

View File

@@ -25,7 +25,7 @@
},
{
name: '插入概要',
value: 'Ctrl + S'
value: 'Ctrl + S'// v0.6.8+改为Ctrl + G
},
{
name: '展开/收起节点',

View File

@@ -24,7 +24,7 @@
},
{
<span class="hljs-attr">name</span>: <span class="hljs-string">&#x27;插入概要&#x27;</span>,
<span class="hljs-attr">value</span>: <span class="hljs-string">&#x27;Ctrl + S&#x27;</span>
<span class="hljs-attr">value</span>: <span class="hljs-string">&#x27;Ctrl + S&#x27;</span><span class="hljs-comment">// v0.6.8+改为Ctrl + G</span>
},
{
<span class="hljs-attr">name</span>: <span class="hljs-string">&#x27;展开/收起节点&#x27;</span>,

View File

@@ -116,4 +116,20 @@ import CustomNodeContent from './CustomNodeContent.vue'
## 示例4渲染react组件
如果你成功渲染了`react`组件,欢迎[提交](https://github.com/wanglin2/mind-map/issues/new)示例代码给我~
```js
import { createRoot } from 'react-dom/client'
{
customCreateNodeContent: (node) => {
const el = document.createElement('div')
el.style.width = '227px'
el.style.height = '60px'
const currentNode = node.nodeData.data
const root = createRoot(el)
root.render({currentNode.text})
return el
}
}
```
> 感谢[h5chenhang](https://github.com/h5chenhang)贡献的[示例代码](https://github.com/wanglin2/mind-map/issues/192)。

View File

@@ -94,7 +94,23 @@
}
</code></pre>
<h2>示例4渲染react组件</h2>
<p>如果你成功渲染了<code>react</code>组件欢迎<a href="https://github.com/wanglin2/mind-map/issues/new">提交</a>示例代码给我~</p>
<pre class="hljs"><code><span class="hljs-keyword">import</span> { createRoot } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;react-dom/client&#x27;</span>
{
<span class="hljs-attr">customCreateNodeContent</span>: <span class="hljs-function">(<span class="hljs-params">node</span>) =&gt;</span> {
<span class="hljs-keyword">const</span> el = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">&#x27;div&#x27;</span>)
el.style.width = <span class="hljs-string">&#x27;227px&#x27;</span>
el.style.height = <span class="hljs-string">&#x27;60px&#x27;</span>
<span class="hljs-keyword">const</span> currentNode = node.nodeData.data
<span class="hljs-keyword">const</span> root = createRoot(el)
root.render({currentNode.text})
<span class="hljs-keyword">return</span> el
}
}
</code></pre>
<blockquote>
<p>感谢<a href="https://github.com/h5chenhang">h5chenhang</a>贡献的<a href="https://github.com/wanglin2/mind-map/issues/192">示例代码</a></p>
</blockquote>
</div>
</template>

View File

@@ -0,0 +1,40 @@
# 如何复制、剪切、粘贴
## 使用快捷键
核心库内部默认支持`Ctrl+c``Ctrl+x``Ctrl+v`三个快捷键来执行复制、剪切、粘贴操作。
当激活了某个节点,按`Ctrl+c`会复制当前激活节点的数据,按`Ctrl+x`会删除当前激活节点,同时保存该节点的数据,这两个操作当同时存在多个激活节点,只会对第一个生效。
当按了`Ctrl+c``Ctrl+x`后,按`Ctrl+v`会在当前激活的节点粘贴复制或剪切的数据,也就是被复制或剪切的节点会作为该节点的子节点。
除了支持粘贴在画布中复制或剪切的节点数据外,如果你再其他地方复制了文本或图片,也支持进行粘贴,也就是会粘贴你当前剪贴板中的数据,如果你的剪切板中存在文本数据,那么会粘贴作为当前激活节点的子节点,如果存在图片数据,那么会直接给当前激活节点添加或替换图片。
如果复制或剪切了画布数据,同时剪切板中也存在数据,那么默认会以最新的`Ctrl+c``Ctrl+x`操作为准,比如你先复制了节点,然后又复制了其他地方的文本,那么会粘贴最后一次的操作,也就是其他地方复制的文本数据。
## 使用按钮触发
一般会在右键菜单上下文中显然复制、剪切、粘贴三个按钮,当点击了这三个按钮也需要能执行复制、剪切、粘贴操作,这需要调用内部的一些方法来完成:
```js
// 点击了复制按钮
const onCopyBtnClick = () => {
mindMap.renderer.copy()
}
// 点击了剪切按钮
const onCutBtnClick = () => {
mindMap.renderer.cut()
}
// 点击了粘贴按钮
const onPasteBtnClick = () => {
mindMap.renderer.paste()
}
```
需要注意的是,这三个方法只能复制、剪切、粘贴画布中的节点数据,不支持操作用户的剪切板数据。
### 完整示例
<iframe style="width: 100%; height: 455px; border: none;" src="https://wanglin2.github.io/playground/#eNrFVc1OGzEQfpWRqypJFXaD1FMaEIX20AMV4lpz2Ow6ianXXq29BIRy4VCgopeeeuyhUqUeeqhQJejrEKBv0fH+k0RVbkSKtJ75vm/Gnhn7mLyMIucgYaRLetqPeWRAM5NE61TyMFKxgWOI2aANSm6rRBoWtEGPPCHUeJcNYAKDWIXQQIVGydjmMtj2osxFiUazYCshWldCL6KESgAqBTNgbRa5BjIRgkoqXRfuTq6mp39urj9Mv32anv2+vTj/+/knlb6S2mAaWyo62jRyS3D/PRKbLVhbh2OrCYWeEzMZsJjFjo/gZovKyZz2+Y/p2emcdmKWl07MQuW7X1/uLy9nlXc8bdjS2pFFl+rl0TdrrNrRsXFx5M1ckIkuBMpPQiaNM2TmtWD2c/PoTdBs5MwtJY3HJYsbrXbGCjzjdYucsETEGiipmTKzYYfGmim5/Xp1//EEt54V1f4muZgF+iMuAtyUBb+rNGbkFkaZjXRzfXF3/X022MOAC4LuVb467pEyKD5zW8HjkptdpcxbFbAdpbnhSiKzIdjANNrQ8LF0WKa9FD5pvcC2wNboudm84qTiwjAcMs8wXAH0An4AvvC0XqMkL/crFipKUncO4EHlLZsBIT0XvXVgoWSUEn3PQoqN9PqJMUrChm/bGiEPxxOR2Qz33Az4X2I1e5aXzucyvAeThcxs/maY1Z6Kr55bOzJcanMkstPbyK8xShw3u7vy6XKYDh1fa0qwBPYKA3Bqp1u0z5gHZtSF1U7naYoDiMqixgwj8gOWOtJ+sP8ns1UopCqi19dKJCYjAtjW6EInXxkVVYv58CPGhyOEP+90osMi8uK4z8oryYuHHOMWqpEXBFwOC0OZupM3xZIZrxYZ5EmXaxTEhk5rQNokq4B9LZx9rSQ+Tqk8zR1YgXJYKcG3J5tQx8VPJ8arkofMFmulH6uxxvt0Hxn5sC14jzLufKktK89tQib/AMXMadM=" />

View File

@@ -0,0 +1,41 @@
<template>
<div>
<h1>如何复制剪切粘贴</h1>
<h2>使用快捷键</h2>
<p>核心库内部默认支持<code>Ctrl+c</code><code>Ctrl+x</code><code>Ctrl+v</code>三个快捷键来执行复制剪切粘贴操作</p>
<p>当激活了某个节点<code>Ctrl+c</code>会复制当前激活节点的数据<code>Ctrl+x</code>会删除当前激活节点同时保存该节点的数据这两个操作当同时存在多个激活节点只会对第一个生效</p>
<p>当按了<code>Ctrl+c</code><code>Ctrl+x</code><code>Ctrl+v</code>会在当前激活的节点粘贴复制或剪切的数据也就是被复制或剪切的节点会作为该节点的子节点</p>
<p>除了支持粘贴在画布中复制或剪切的节点数据外如果你再其他地方复制了文本或图片也支持进行粘贴也就是会粘贴你当前剪贴板中的数据如果你的剪切板中存在文本数据那么会粘贴作为当前激活节点的子节点如果存在图片数据那么会直接给当前激活节点添加或替换图片</p>
<p>如果复制或剪切了画布数据同时剪切板中也存在数据那么默认会以最新的<code>Ctrl+c</code><code>Ctrl+x</code>操作为准比如你先复制了节点然后又复制了其他地方的文本那么会粘贴最后一次的操作也就是其他地方复制的文本数据</p>
<h2>使用按钮触发</h2>
<p>一般会在右键菜单上下文中显然复制剪切粘贴三个按钮当点击了这三个按钮也需要能执行复制剪切粘贴操作这需要调用内部的一些方法来完成</p>
<pre class="hljs"><code><span class="hljs-comment">// 点击了复制按钮</span>
<span class="hljs-keyword">const</span> onCopyBtnClick = <span class="hljs-function">() =&gt;</span> {
mindMap.renderer.copy()
}
<span class="hljs-comment">// 点击了剪切按钮</span>
<span class="hljs-keyword">const</span> onCutBtnClick = <span class="hljs-function">() =&gt;</span> {
mindMap.renderer.cut()
}
<span class="hljs-comment">// 点击了粘贴按钮</span>
<span class="hljs-keyword">const</span> onPasteBtnClick = <span class="hljs-function">() =&gt;</span> {
mindMap.renderer.paste()
}
</code></pre>
<p>需要注意的是这三个方法只能复制剪切粘贴画布中的节点数据不支持操作用户的剪切板数据</p>
<h3>完整示例</h3>
<iframe style="width: 100%; height: 455px; border: none;" src="https://wanglin2.github.io/playground/#eNrFVc1OGzEQfpWRqypJFXaD1FMaEIX20AMV4lpz2Ow6ianXXq29BIRy4VCgopeeeuyhUqUeeqhQJejrEKBv0fH+k0RVbkSKtJ75vm/Gnhn7mLyMIucgYaRLetqPeWRAM5NE61TyMFKxgWOI2aANSm6rRBoWtEGPPCHUeJcNYAKDWIXQQIVGydjmMtj2osxFiUazYCshWldCL6KESgAqBTNgbRa5BjIRgkoqXRfuTq6mp39urj9Mv32anv2+vTj/+/knlb6S2mAaWyo62jRyS3D/PRKbLVhbh2OrCYWeEzMZsJjFjo/gZovKyZz2+Y/p2emcdmKWl07MQuW7X1/uLy9nlXc8bdjS2pFFl+rl0TdrrNrRsXFx5M1ckIkuBMpPQiaNM2TmtWD2c/PoTdBs5MwtJY3HJYsbrXbGCjzjdYucsETEGiipmTKzYYfGmim5/Xp1//EEt54V1f4muZgF+iMuAtyUBb+rNGbkFkaZjXRzfXF3/X022MOAC4LuVb467pEyKD5zW8HjkptdpcxbFbAdpbnhSiKzIdjANNrQ8LF0WKa9FD5pvcC2wNboudm84qTiwjAcMs8wXAH0An4AvvC0XqMkL/crFipKUncO4EHlLZsBIT0XvXVgoWSUEn3PQoqN9PqJMUrChm/bGiEPxxOR2Qz33Az4X2I1e5aXzucyvAeThcxs/maY1Z6Kr55bOzJcanMkstPbyK8xShw3u7vy6XKYDh1fa0qwBPYKA3Bqp1u0z5gHZtSF1U7naYoDiMqixgwj8gOWOtJ+sP8ns1UopCqi19dKJCYjAtjW6EInXxkVVYv58CPGhyOEP+90osMi8uK4z8oryYuHHOMWqpEXBFwOC0OZupM3xZIZrxYZ5EmXaxTEhk5rQNokq4B9LZx9rSQ+Tqk8zR1YgXJYKcG3J5tQx8VPJ8arkofMFmulH6uxxvt0Hxn5sC14jzLufKktK89tQib/AMXMadM=" />
</div>
</template>
<script>
export default {
}
</script>
<style>
</style>

View File

@@ -0,0 +1,11 @@
# 概要/关联线
## 概要
可以选中一个节点添加概要,如果想给多个节点添加一个概要,只能通过给它们的父节点添加来实现。
概要节点后面无法再添加节点,后续该特性也不会支持。
## 关联线
关联线添加完后要删除,需要先点击选中关联线,然后按删除键即可。

View File

@@ -0,0 +1,21 @@
<template>
<div>
<h1>概要/关联线</h1>
<h2>概要</h2>
<p>可以选中一个节点添加概要如果想给多个节点添加一个概要只能通过给它们的父节点添加来实现</p>
<p>概要节点后面无法再添加节点后续该特性也不会支持</p>
<h2>关联线</h2>
<p>关联线添加完后要删除需要先点击选中关联线然后按删除键即可</p>
</div>
</template>
<script>
export default {
}
</script>
<style>
</style>

View File

@@ -0,0 +1,26 @@
# 客户端
## 安装
目前提供了两个客户端下载方式:
1.百度云:[https://pan.baidu.com/s/1huasEbKsGNH2Af68dvWiOg?pwd=3bp3](https://pan.baidu.com/s/1huasEbKsGNH2Af68dvWiOg?pwd=3bp3)
2.Github[https://github.com/wanglin2/mind-map/releases](https://github.com/wanglin2/mind-map/releases)
Windows用户请下载`Setup.[版本].exe``[版本].exe`文件。
Mac用户请下载`.dmg`文件。
Linux用户请下载`.AppImage`文件。
如果Mac安装出现`已损坏`问题,可以参考这篇文章:[苹果M1芯片安装xxx.app 显示已损坏,无法打开,你应该将它移到废纸篓/打不开 xxx因为它来自身份不明的开发者 如何解决?](https://www.jianshu.com/p/031efc52ccea)。
## 使用
客户端名称为`思绪`,对应的思维导图文件后缀名为`.smm`
客户端相比在线版,多了一个显示最近文件列表的页面,你打开过的文件都会显示在最近列表里。
除了点击打开文件按钮打开文件外,你也可以直接将文件拖入到列表页面,如果拖入一个文件,那么会直接打开进行编辑,如果同时拖入了多个文件,那么会直接添加到列表里,不会打开编辑。

View File

@@ -0,0 +1,28 @@
<template>
<div>
<h1>客户端</h1>
<h2>安装</h2>
<p>目前提供了两个客户端下载方式</p>
<p>1.百度云<a href="https://pan.baidu.com/s/1huasEbKsGNH2Af68dvWiOg?pwd=3bp3">https://pan.baidu.com/s/1huasEbKsGNH2Af68dvWiOg?pwd=3bp3</a></p>
<p>2.Github<a href="https://github.com/wanglin2/mind-map/releases">https://github.com/wanglin2/mind-map/releases</a></p>
<p>Windows用户请下载<code>Setup.[版本].exe</code><code>[版本].exe</code>文件</p>
<p>Mac用户请下载<code>.dmg</code>文件</p>
<p>Linux用户请下载<code>.AppImage</code>文件</p>
<p>如果Mac安装出现<code>已损坏</code>问题可以参考这篇文章<a href="https://www.jianshu.com/p/031efc52ccea">苹果M1芯片安装xxx.app 显示已损坏无法打开你应该将它移到废纸篓/打不开 xxx因为它来自身份不明的开发者 如何解决</a></p>
<h2>使用</h2>
<p>客户端名称为<code>思绪</code>对应的思维导图文件后缀名为<code>.smm</code></p>
<p>客户端相比在线版多了一个显示最近文件列表的页面你打开过的文件都会显示在最近列表里</p>
<p>除了点击打开文件按钮打开文件外你也可以直接将文件拖入到列表页面如果拖入一个文件那么会直接打开进行编辑如果同时拖入了多个文件那么会直接添加到列表里不会打开编辑</p>
</div>
</template>
<script>
export default {
}
</script>
<style>
</style>

View File

@@ -147,4 +147,16 @@
<img src="../../../../assets/avatar/suka.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>suka</p>
</div>
<div style="display: flex; flex-direction: column; align-items: center; width: fit-content; margin: 5px;">
<img src="../../../../assets/avatar/Chris.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>Chris</p>
</div>
<div style="display: flex; flex-direction: column; align-items: center; width: fit-content; margin: 5px;">
<img src="../../../../assets/avatar/水车.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>水车</p>
</div>
<div style="display: flex; flex-direction: column; align-items: center; width: fit-content; margin: 5px;">
<img src="../../../../assets/avatar/仓鼠.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>仓鼠</p>
</div>
</div>

View File

@@ -107,6 +107,18 @@
<img src="../../../../assets/avatar/suka.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>suka</p>
</div>
<div style="display: flex; flex-direction: column; align-items: center; width: fit-content; margin: 5px;">
<img src="../../../../assets/avatar/Chris.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>Chris</p>
</div>
<div style="display: flex; flex-direction: column; align-items: center; width: fit-content; margin: 5px;">
<img src="../../../../assets/avatar/水车.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>水车</p>
</div>
<div style="display: flex; flex-direction: column; align-items: center; width: fit-content; margin: 5px;">
<img src="../../../../assets/avatar/仓鼠.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>仓鼠</p>
</div>
</div>
</div>
</template>

View File

@@ -54,7 +54,9 @@
复制节点,操作节点为当前激活节点,有多个激活节点只会操作第一个节点
### setNodeDataRender(node, data)
### setNodeDataRender(node, data, notRender)
- `notRender`v0.6.9+`Boolean`,默认为`false`,是否不要触发渲染。
设置节点数据,即`data`字段的数据,并会根据节点大小是否变化来判断是否需要重新渲染该节点,`data`为对象,如:`{text: '我是新文本'}`
@@ -82,4 +84,40 @@
移动节点到画布中心。
目前如果是存在缩放的情况下回到中心会重置缩放。
目前如果是存在缩放的情况下回到中心会重置缩放。
### expandToNodeUid(uid, callback)
> v0.6.7+
- `uid`节点uid
- `callback`:展开完成的回调函数
展开到指定uid的节点。
### findNodeByUid(uid)
> v0.6.7+
- `uid`节点uid
根据uid找到对应的节点实例。
### copy()
> v0.6.8+
复制节点,调用该方法后会存储当前激活的节点数据,多个激活节点只会操作第一个节点,后续调用`paste()`方法时可以进行粘贴。
### cut()
> v0.6.8+
剪切节点,调用该方法后会剪切当前激活的节点,并且存储该节点数据,多个节点只会操作第一个节点,后续调用`paste()`方法时可以进行粘贴。
### paste()
> v0.6.8+
粘贴节点,在调用了`copy()``cut()`方法后可以调用该方法进行粘贴节点。该方法不支持粘贴用户剪贴板中的数据,请使用内置的`Ctrl+v`快捷键。

View File

@@ -28,7 +28,10 @@
<p>删除某个指定节点</p>
<h3>copyNode()</h3>
<p>复制节点操作节点为当前激活节点有多个激活节点只会操作第一个节点</p>
<h3>setNodeDataRender(node, data)</h3>
<h3>setNodeDataRender(node, data, notRender)</h3>
<ul>
<li><code>notRender</code>v0.6.9+<code>Boolean</code>默认为<code>false</code>是否不要触发渲染</li>
</ul>
<p>设置节点数据<code>data</code>字段的数据并会根据节点大小是否变化来判断是否需要重新渲染该节点<code>data</code>为对象<code>{text: '我是新文本'}</code></p>
<h3>moveNodeTo(node, toNode)</h3>
<blockquote>
@@ -51,6 +54,42 @@
</blockquote>
<p>移动节点到画布中心</p>
<p>目前如果是存在缩放的情况下回到中心会重置缩放</p>
<h3>expandToNodeUid(uid, callback)</h3>
<blockquote>
<p>v0.6.7+</p>
</blockquote>
<ul>
<li>
<p><code>uid</code>节点uid</p>
</li>
<li>
<p><code>callback</code>展开完成的回调函数</p>
</li>
</ul>
<p>展开到指定uid的节点</p>
<h3>findNodeByUid(uid)</h3>
<blockquote>
<p>v0.6.7+</p>
</blockquote>
<ul>
<li><code>uid</code>节点uid</li>
</ul>
<p>根据uid找到对应的节点实例</p>
<h3>copy()</h3>
<blockquote>
<p>v0.6.8+</p>
</blockquote>
<p>复制节点调用该方法后会存储当前激活的节点数据多个激活节点只会操作第一个节点后续调用<code>paste()</code>方法时可以进行粘贴</p>
<h3>cut()</h3>
<blockquote>
<p>v0.6.8+</p>
</blockquote>
<p>剪切节点调用该方法后会剪切当前激活的节点并且存储该节点数据多个节点只会操作第一个节点后续调用<code>paste()</code>方法时可以进行粘贴</p>
<h3>paste()</h3>
<blockquote>
<p>v0.6.8+</p>
</blockquote>
<p>粘贴节点在调用了<code>copy()</code><code>cut()</code>方法后可以调用该方法进行粘贴节点该方法不支持粘贴用户剪贴板中的数据请使用内置的<code>Ctrl+v</code>快捷键</p>
</div>
</template>

View File

@@ -0,0 +1,68 @@
# Search 插件
> v0.6.9+
该插件提供搜索和替换节点内容的功能。
## 注册
```js
import MindMap from 'simple-mind-map'
import Search from 'simple-mind-map/src/plugins/Search.js'
MindMap.usePlugin(Search)
```
注册完且实例化`MindMap`后可通过`mindMap.search`获取到该实例。
## 事件
### search_info_change
可以通过监听`search_info_change`事件来获取当前搜索结果的数量和当前定位到的索引。
```js
mindMap.on('search_info_change', (data) => {
/*
data: {
currentIndex,// 索引从0开始
total
}
*/
})
```
## 方法
### search(searchText, callback)
- `searchText`:要进行搜索的文本
- `callback`:本次搜索完成的回调函数,会在跳转到节点后触发
搜索节点内容,可以重复调用,每调一次,会搜索和定位到下一个匹配的节点。如果搜索文本改变了,那么会重新搜索。
### endSearch()
结束搜索。
### replace(replaceText)
- `replaceText`:要进行替换的文本
替换当前节点内容,要在调用了`search`方法之后调用,会替换当前定位到的匹配节点内容。
### replaceAll(replaceText)
- `replaceText`:要进行替换的文本
替换所有匹配的节点内容,要在调用了`search`方法之后调用。
### getReplacedText(node, searchText, replaceText)
- `node`:节点实例
- `searchText`:要进行搜索的文本
- `replaceText`:要进行替换的文本
返回该节点搜索和替换后的文本内容,注意,不会实际改变节点内容,只是用来计算一个节点替换后的内容。

View File

@@ -0,0 +1,74 @@
<template>
<div>
<h1>Search 插件</h1>
<blockquote>
<p>v0.6.9+</p>
</blockquote>
<p>该插件提供搜索和替换节点内容的功能</p>
<h2>注册</h2>
<pre class="hljs"><code><span class="hljs-keyword">import</span> MindMap <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;simple-mind-map&#x27;</span>
<span class="hljs-keyword">import</span> Search <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;simple-mind-map/src/plugins/Search.js&#x27;</span>
MindMap.usePlugin(Search)
</code></pre>
<p>注册完且实例化<code>MindMap</code>后可通过<code>mindMap.search</code>获取到该实例</p>
<h2>事件</h2>
<h3>search_info_change</h3>
<p>可以通过监听<code>search_info_change</code>事件来获取当前搜索结果的数量和当前定位到的索引</p>
<pre class="hljs"><code>mindMap.on(<span class="hljs-string">&#x27;search_info_change&#x27;</span>, <span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> {
<span class="hljs-comment">/*
data: {
currentIndex,// 索引从0开始
total
}
*/</span>
})
</code></pre>
<h2>方法</h2>
<h3>search(searchText, callback)</h3>
<ul>
<li>
<p><code>searchText</code>要进行搜索的文本</p>
</li>
<li>
<p><code>callback</code>本次搜索完成的回调函数会在跳转到节点后触发</p>
</li>
</ul>
<p>搜索节点内容可以重复调用每调一次会搜索和定位到下一个匹配的节点如果搜索文本改变了那么会重新搜索</p>
<h3>endSearch()</h3>
<p>结束搜索</p>
<h3>replace(replaceText)</h3>
<ul>
<li><code>replaceText</code>要进行替换的文本</li>
</ul>
<p>替换当前节点内容要在调用了<code>search</code>方法之后调用会替换当前定位到的匹配节点内容</p>
<h3>replaceAll(replaceText)</h3>
<ul>
<li><code>replaceText</code>要进行替换的文本</li>
</ul>
<p>替换所有匹配的节点内容要在调用了<code>search</code>方法之后调用</p>
<h3>getReplacedText(node, searchText, replaceText)</h3>
<ul>
<li>
<p><code>node</code>节点实例</p>
</li>
<li>
<p><code>searchText</code>要进行搜索的文本</p>
</li>
<li>
<p><code>replaceText</code>要进行替换的文本</p>
</li>
</ul>
<p>返回该节点搜索和替换后的文本内容注意不会实际改变节点内容只是用来计算一个节点替换后的内容</p>
</div>
</template>
<script>
export default {
}
</script>
<style>
</style>

View File

@@ -176,6 +176,27 @@ copyNodeTree({}, node)
}
```
#### loadImage(imgFile)
> v0.6.8+
- `imgFile`图片类型的File对象
加载图片,返回:
```js
{
url,// DataUrl
size// { width, height } 图片宽高
}
```
#### getType(data)
> v0.6.9+
获取一个数据的类型,比如`Boolean``Array`等。
## 在canvas中模拟css的背景属性
引入:

View File

@@ -116,6 +116,24 @@
height
}
</code></pre>
<h4>loadImage(imgFile)</h4>
<blockquote>
<p>v0.6.8+</p>
</blockquote>
<ul>
<li><code>imgFile</code>图片类型的File对象</li>
</ul>
<p>加载图片返回</p>
<pre class="hljs"><code>{
url,<span class="hljs-comment">// DataUrl</span>
size<span class="hljs-comment">// { width, height } 图片宽高</span>
}
</code></pre>
<h4>getType(data)</h4>
<blockquote>
<p>v0.6.9+</p>
</blockquote>
<p>获取一个数据的类型比如<code>Boolean</code><code>Array</code></p>
<h2>在canvas中模拟css的背景属性</h2>
<p>引入</p>
<pre class="hljs"><code><span class="hljs-keyword">import</span> drawBackgroundImageToCanvas <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;simple-mind-map/src/utils/simulateCSSBackgroundInCanvas&#x27;</span>

View File

@@ -1,5 +1,5 @@
<template>
<div class="container">
<div class="container" :class="{ isDark: isDark }">
<template v-if="show">
<Toolbar v-if="!isZenMode"></Toolbar>
<Edit></Edit>
@@ -26,9 +26,15 @@ export default {
},
computed: {
...mapState({
isZenMode: state => state.localConfig.isZenMode
isZenMode: state => state.localConfig.isZenMode,
isDark: state => state.isDark
})
},
watch: {
isDark() {
this.setBodyDark()
}
},
async created() {
this.initLocalConfig()
const loading = this.$loading({
@@ -38,6 +44,7 @@ export default {
await this.getUserMindMapData()
this.show = true
loading.close()
this.setBodyDark()
},
methods: {
...mapActions(['getUserMindMapData']),
@@ -56,12 +63,158 @@ export default {
...config
})
}
},
setBodyDark() {
this.isDark
? document.body.classList.add('isDark')
: document.body.classList.remove('isDark')
}
}
}
</script>
<style lang="less" scoped>
.container {
<style lang="less">
.container {}
body {
&.isDark {
/* el-button */
.el-button {
background-color: #363b3f;
color: hsla(0,0%,100%,.9);
border-color: hsla(0, 0%, 100%, 0.1);
}
/* el-input */
.el-input__inner {
background-color: #363b3f;
border-color: hsla(0, 0%, 100%, 0.1);
color: hsla(0, 0%, 100%, 0.9);
}
.el-input.is-disabled .el-input__inner {
background-color: #363b3f;
border-color: hsla(0, 0%, 100%, 0.1);
color: hsla(0,0%,100%,.3);
}
.el-input-group__append, .el-input-group__prepend {
background-color: #363b3f;
border-color: hsla(0, 0%, 100%, 0.1);
}
.el-input-group__append button.el-button {
color: hsla(0, 0%, 100%, 0.9);
}
/* el-select */
.el-select-dropdown {
background-color: #36393d;
border-color: hsla(0, 0%, 100%, 0.1);
.el-select-dropdown__item {
color: hsla(0, 0%, 100%, 0.6);
}
.el-select-dropdown__item.selected {
color: #409eff;
}
.el-select-dropdown__item.hover,
.el-select-dropdown__item:hover {
background-color: hsla(0, 0%, 100%, 0.05);
}
}
.el-select .el-input.is-disabled .el-input__inner:hover {
border-color: hsla(0, 0%, 100%, 0.1);
}
/* el-popper*/
.el-popper {
background-color: #36393d;
border-color: hsla(0, 0%, 100%, 0.1);
}
.el-popper[x-placement^='bottom'] .popper__arrow {
background-color: #36393d;
}
.el-popper[x-placement^='bottom'] .popper__arrow::after {
border-bottom-color: #36393d;
}
.el-popper[x-placement^=top] .popper__arrow {
background-color: #36393d;
}
.el-popper[x-placement^=top] .popper__arrow::after {
border-top-color: #36393d;
}
/* el-tabs */
.el-tabs__item {
color: hsla(0, 0%, 100%, 0.6);
&:hover,
&.is-active {
color: #409EFF;
}
}
.el-tabs__nav-wrap::after {
background-color: hsla(0, 0%, 100%, 0.6);
}
/* el-slider */
.el-slider__runway {
background-color: hsla(0, 0%, 100%, 0.6);
}
/* el-radio-group */
.el-radio-group {
.el-radio-button__inner {
background-color: #36393d;
color: hsla(0, 0%, 100%, 0.6);
}
.el-radio-button__orig-radio:checked+.el-radio-button__inner {
color: #FFF;
background-color: #409EFF;
}
}
/* el-dialog */
.el-dialog {
background-color: #262a2e;
.el-dialog__header {
border-bottom: 1px solid hsla(0,0%,100%,.1);
}
.el-dialog__title {
color: hsla(0,0%,100%,.9);
}
.el-dialog__body {
background-color: #262a2e;
}
.el-dialog__footer {
border-top: 1px solid hsla(0,0%,100%,.1);
}
}
/* el-upload */
.el-upload__tip {
color: #999;
}
/* 富文本编辑器 */
.toastui-editor-main-container {
background-color: #fff;
}
}
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<Sidebar ref="sidebar" :title="$t('baseStyle.title')">
<div class="sidebarContent" v-if="data">
<div class="sidebarContent" :class="{ isDark: isDark }" v-if="data">
<!-- 背景 -->
<div class="title noTop">{{ $t('baseStyle.background') }}</div>
<div class="row">
@@ -711,7 +711,7 @@ export default {
}
},
computed: {
...mapState(['activeSidebar', 'localConfig']),
...mapState(['activeSidebar', 'localConfig', 'isDark']),
lineStyleList() {
return lineStyleList[this.$i18n.locale] || lineStyleList.zh
@@ -879,6 +879,12 @@ export default {
}
this.data.theme.config[this.marginActiveTab][type] = value
this.mindMap.setThemeConfig(this.data.theme.config)
storeConfig({
theme: {
template: this.mindMap.getTheme(),
config: this.data.theme.config
}
})
},
// 切换显示水印与否
@@ -894,6 +900,7 @@ export default {
// 切换是否开启节点富文本编辑
enableNodeRichTextChange(e) {
this.mindMap.renderer.textEdit.hideEditTextBox()
this.setLocalConfig({
openNodeRichText: e
})
@@ -915,6 +922,20 @@ export default {
padding: 20px;
padding-top: 10px;
&.isDark {
.title {
color: #fff;
}
.row {
.rowItem {
.name {
color: hsla(0,0%,100%,.6);
}
}
}
}
.title {
font-size: 16px;
font-family: PingFangSC-Medium, PingFang SC;

View File

@@ -1,5 +1,5 @@
<template>
<div>
<div class="colorContainer" :class="{ isDark: isDark }">
<div class="colorList">
<span
class="colorItem"
@@ -22,6 +22,7 @@
<script>
import { colorList } from '@/config'
import { mapState } from 'vuex'
/**
* @Author: 王林
@@ -42,6 +43,9 @@ export default {
selectColor: ''
}
},
computed: {
...mapState(['isDark']),
},
watch: {
color() {
this.selectColor = this.color
@@ -73,6 +77,14 @@ export default {
</script>
<style lang="less" scoped>
.colorContainer {
&.isDark {
.moreColor {
color: hsla(0, 0%, 100%, 0.6);
}
}
}
.colorList {
width: 240px;

View File

@@ -23,7 +23,7 @@
:class="{ disabled: insertNodeBtnDisabled }"
>
{{ $t('contextmenu.insertSummary') }}
<span class="desc">Ctrl + S</span>
<span class="desc">Ctrl + G</span>
</div>
<div
class="item"
@@ -55,7 +55,6 @@
</div>
<div
class="item"
:class="{ disabled: copyData === null }"
@click="exec('PASTE_NODE')"
>
{{ $t('contextmenu.pasteNode') }}
@@ -123,7 +122,6 @@ export default {
left: 0,
top: 0,
node: null,
copyData: null,
type: '',
isMousedown: false,
mosuedownX: 0,
@@ -180,10 +178,6 @@ export default {
this.$bus.$on('expand_btn_click', this.hide)
this.$bus.$on('svg_mousedown', this.onMousedown)
this.$bus.$on('mouseup', this.onMouseup)
// 注册快捷键
this.mindMap.keyCommand.addShortcut('Control+c', this.copy)
this.mindMap.keyCommand.addShortcut('Control+v', this.paste)
this.mindMap.keyCommand.addShortcut('Control+x', this.cut)
},
beforeDestroy() {
this.$bus.$off('node_contextmenu', this.show)
@@ -192,10 +186,6 @@ export default {
this.$bus.$off('expand_btn_click', this.hide)
this.$bus.$on('svg_mousedown', this.onMousedown)
this.$bus.$on('mouseup', this.onMouseup)
// 移除快捷键
this.mindMap.keyCommand.removeShortcut('Control+c', this.copy)
this.mindMap.keyCommand.removeShortcut('Control+v', this.paste)
this.mindMap.keyCommand.removeShortcut('Control+x', this.cut)
},
methods: {
...mapMutations(['setLocalConfig']),
@@ -282,15 +272,13 @@ export default {
}
switch (key) {
case 'COPY_NODE':
this.copyData = this.mindMap.renderer.copyNode()
this.mindMap.renderer.copy()
break
case 'CUT_NODE':
this.$bus.$emit('execCommand', key, copyData => {
this.copyData = copyData
})
this.mindMap.renderer.cut()
break
case 'PASTE_NODE':
this.$bus.$emit('execCommand', key, this.copyData)
this.mindMap.renderer.paste()
break
case 'RETURN_CENTER':
this.mindMap.view.reset()
@@ -308,33 +296,6 @@ export default {
break
}
this.hide()
},
/**
* @Author: 王林25
* @Date: 2022-08-04 14:25:45
* @Desc: 复制
*/
copy() {
this.exec('COPY_NODE')
},
/**
* @Author: 王林25
* @Date: 2022-08-04 14:26:43
* @Desc: 粘贴
*/
paste() {
this.exec('PASTE_NODE')
},
/**
* @Author: 王林25
* @Date: 2022-08-04 14:27:32
* @Desc: 剪切
*/
cut() {
this.exec('CUT_NODE')
}
}
}

View File

@@ -1,5 +1,5 @@
<template>
<div class="countContainer">
<div class="countContainer" :class="{ isDark: isDark }">
<div class="item">
<span class="name">{{ $t('count.words') }}</span>
<span class="value">{{ words }}</span>
@@ -12,6 +12,8 @@
</template>
<script>
import { mapState } from 'vuex'
/**
* @Author: 王林
* @Date: 2021-06-24 22:53:10
@@ -29,6 +31,9 @@ export default {
num: 0
}
},
computed: {
...mapState(['isDark']),
},
created() {
this.$bus.$on('data_change', this.onDataChange)
},
@@ -82,6 +87,14 @@ export default {
font-size: 12px;
display: flex;
&.isDark {
background: #262a2e;
.item {
color: hsla(0,0%,100%,.6);
}
}
.item {
color: #555;
margin-right: 15px;
@@ -95,4 +108,10 @@ export default {
}
}
}
@media screen and (max-width: 635px) {
.countContainer {
display: none;
}
}
</style>

View File

@@ -7,7 +7,7 @@
<Outline :mindMap="mindMap"></Outline>
<Style v-if="!isZenMode"></Style>
<BaseStyle :data="mindMapData" :mindMap="mindMap"></BaseStyle>
<Theme :mindMap="mindMap"></Theme>
<Theme v-if="mindMap" :mindMap="mindMap"></Theme>
<Structure :mindMap="mindMap"></Structure>
<ShortcutKey></ShortcutKey>
<Contextmenu v-if="mindMap" :mindMap="mindMap"></Contextmenu>
@@ -18,6 +18,7 @@
></NodeNoteContentShow>
<NodeImgPreview v-if="mindMap" :mindMap="mindMap"></NodeImgPreview>
<SidebarTrigger v-if="!isZenMode"></SidebarTrigger>
<Search v-if="mindMap" :mindMap="mindMap"></Search>
</div>
</template>
@@ -35,6 +36,7 @@ import RichText from 'simple-mind-map/src/plugins/RichText.js'
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 Style from './Style'
import BaseStyle from './BaseStyle'
@@ -59,6 +61,7 @@ import Vue from 'vue'
import router from '../../../router'
import store from '../../../store'
import i18n from '../../../i18n'
import Search from './Search.vue'
// 注册插件
MindMap
@@ -73,6 +76,7 @@ MindMap
.usePlugin(AssociativeLine)
.usePlugin(NodeImgAdjust)
.usePlugin(TouchEvent)
.usePlugin(SearchPlugin)
// 注册自定义主题
// customThemeList.forEach((item) => {
@@ -100,7 +104,8 @@ export default {
NodeNoteContentShow,
Navigator,
NodeImgPreview,
SidebarTrigger
SidebarTrigger,
Search
},
data() {
return {

View File

@@ -9,7 +9,7 @@
element-loading-spinner="el-icon-loading"
element-loading-background="rgba(0, 0, 0, 0.8)"
>
<div>
<div class="exportContainer" :class="{ isDark: isDark }">
<div class="nameInputBox">
<span class="name">{{ $t('export.filename') }}</span>
<el-input
@@ -99,6 +99,7 @@ export default {
computed: {
...mapState({
openNodeRichText: state => state.localConfig.openNodeRichText,
isDark: state => state.isDark
}),
downTypeList() {
@@ -180,6 +181,22 @@ export default {
</script>
<style lang="less" scoped>
.exportContainer {
&.isDark {
.downloadTypeList {
.downloadTypeItem {
background-color: #363b3f;
.info {
.name {
color: hsla(0,0%,100%,.9);
}
}
}
}
}
}
.nodeDialog {
/deep/ .el-dialog__body {
background-color: #f2f4f7;

View File

@@ -1,5 +1,5 @@
<template>
<div class="fullscreenContainer">
<div class="fullscreenContainer" :class="{ isDark: isDark }">
<el-tooltip
class="item"
effect="dark"
@@ -32,6 +32,9 @@ export default {
props: {
mindMap: {
type: Object
},
isDark: {
type: Boolean
}
},
data() {
@@ -63,6 +66,12 @@ export default {
display: flex;
align-items: center;
&.isDark {
.btn {
color: hsla(0,0%,100%,.6);
}
}
.item {
margin-right: 12px;

View File

@@ -1,5 +1,5 @@
<template>
<div class="mouseActionContainer">
<div class="mouseActionContainer" :class="{ isDark: isDark }">
<el-tooltip
class="item"
effect="dark"
@@ -32,6 +32,9 @@ export default {
props: {
mindMap: {
type: Object
},
isDark: {
type: Boolean
}
},
data() {
@@ -64,6 +67,12 @@ export default {
display: flex;
align-items: center;
&.isDark{
.btn {
color: hsla(0,0%,100%,.6);
}
}
.item {
margin-right: 12px;
@@ -74,6 +83,7 @@ export default {
.btn {
cursor: pointer;
font-size: 18px;
}
}
</style>

View File

@@ -2,6 +2,7 @@
<div
v-if="showMiniMap"
class="navigatorBox"
:class="{ isDark: isDark }"
ref="navigatorBox"
@mousedown="onMousedown"
@mousemove="onMousemove"
@@ -21,6 +22,8 @@
</template>
<script>
import { mapState } from 'vuex'
export default {
props: {
mindMap: {
@@ -44,6 +47,9 @@ export default {
}
}
},
computed: {
...mapState(['isDark']),
},
mounted() {
this.$bus.$on('toggle_mini_map', this.toggle_mini_map)
this.$bus.$on('data_change', this.data_change)
@@ -128,6 +134,10 @@ export default {
cursor: pointer;
user-select: none;
&.isDark {
background-color: #262a2e;
}
.svgBox {
position: absolute;
left: 0;

View File

@@ -1,5 +1,5 @@
<template>
<div class="navigatorContainer">
<div class="navigatorContainer" :class="{ isDark: isDark }">
<div class="item">
<el-select
v-model="lang"
@@ -16,27 +16,53 @@
</el-select>
</div>
<div class="item">
<MouseAction :mindMap="mindMap"></MouseAction>
<MouseAction :isDark="isDark" :mindMap="mindMap"></MouseAction>
</div>
<div class="item">
<el-checkbox v-model="openMiniMap" @change="toggleMiniMap">{{
$t('navigatorToolbar.openMiniMap')
}}</el-checkbox>
<el-tooltip
effect="dark"
:content="
openMiniMap
? $t('navigatorToolbar.closeMiniMap')
: $t('navigatorToolbar.openMiniMap')
"
placement="top"
>
<div
class="btn iconfont icondaohang1"
@click="toggleMiniMap"
></div>
</el-tooltip>
</div>
<div class="item">
<el-switch
<!-- <el-switch
v-model="isReadonly"
:active-text="$t('navigatorToolbar.readonly')"
:inactive-text="$t('navigatorToolbar.edit')"
@change="readonlyChange"
>
</el-switch>
</el-switch> -->
<el-tooltip
effect="dark"
:content="
isReadonly
? $t('navigatorToolbar.edit')
: $t('navigatorToolbar.readonly')
"
placement="top"
>
<div
class="btn iconfont"
:class="[isReadonly ? 'iconyanjing' : 'iconbianji1']"
@click="readonlyChange"
></div>
</el-tooltip>
</div>
<div class="item">
<Scale :mindMap="mindMap"></Scale>
<Fullscreen :isDark="isDark" :mindMap="mindMap"></Fullscreen>
</div>
<div class="item">
<Fullscreen :mindMap="mindMap"></Fullscreen>
<Scale :isDark="isDark" :mindMap="mindMap"></Scale>
</div>
<div class="item">
<a href="https://github.com/wanglin2/mind-map" target="_blank">
@@ -53,6 +79,7 @@ import MouseAction from './MouseAction.vue'
import { langList } from '@/config'
import i18n from '@/i18n'
import { storeLang, getLang } from '@/api'
import { mapState } from 'vuex'
/**
* @Author: 王林
@@ -79,16 +106,18 @@ export default {
openMiniMap: false
}
},
mounted() {
this.toggleMiniMap(this.openMiniMap)
computed: {
...mapState(['isDark']),
},
methods: {
readonlyChange(value) {
this.mindMap.setMode(value ? 'readonly' : 'edit')
readonlyChange() {
this.isReadonly = !this.isReadonly
this.mindMap.setMode(this.isReadonly ? 'readonly' : 'edit')
},
toggleMiniMap(show) {
this.$bus.$emit('toggle_mini_map', show)
toggleMiniMap() {
this.openMiniMap = !this.openMiniMap
this.$bus.$emit('toggle_mini_map', this.openMiniMap)
},
onLangChange(lang) {
@@ -113,6 +142,20 @@ export default {
display: flex;
align-items: center;
&.isDark {
background: #262a2e;
.item {
a {
color: hsla(0,0%,100%,.6);
}
.btn {
color: hsla(0,0%,100%,.6);
}
}
}
.item {
margin-right: 20px;
@@ -124,6 +167,18 @@ export default {
color: #303133;
text-decoration: none;
}
.btn {
cursor: pointer;
font-size: 18px;
}
}
}
@media screen and (max-width: 502px) {
.navigatorContainer {
left: 20px;
overflow-x: auto;
}
}
</style>

View File

@@ -2,6 +2,7 @@
<Sidebar ref="sidebar" :title="$t('outline.title')">
<el-tree
class="outlineTree"
:class="{ isDark: isDark }"
:data="data"
:props="defaultProps"
:expand-on-click-node="false"
@@ -46,15 +47,14 @@ export default {
data: [],
defaultProps: {
label(data) {
return data.data.text.replaceAll(/\n/g, '</br>')
return data.data.richText ? data.data.text : data.data.text.replaceAll(/\n/g, '</br>')
}
},
notHandleDataChange: false,
isCreateNode: false
notHandleDataChange: false
}
},
computed: {
...mapState(['activeSidebar'])
...mapState(['activeSidebar', 'isDark'])
},
watch: {
activeSidebar(val) {
@@ -77,11 +77,12 @@ export default {
},
methods: {
onBlur(e, node) {
if (this.isCreateNode) {
this.isCreateNode = false
return
const richText = node.data.data.richText
if (richText) {
node.data._node.setText(e.target.innerHTML, true)
} else {
node.data._node.setText(e.target.innerText)
}
node.data._node.setText(e.target.innerText)
},
getKey() {
@@ -102,24 +103,21 @@ export default {
// 插入兄弟节点
insertNode() {
this.notHandleDataChange = false
this.isCreateNode = true
this.mindMap.execCommand('INSERT_NODE', false)
},
// 插入下级节点
insertChildNode() {
this.notHandleDataChange = false
this.isCreateNode = true
this.mindMap.execCommand('INSERT_CHILD_NODE', false)
},
// 激活当前节点且移动当前节点到画布中间
onClick(e, data) {
onClick(e, node) {
this.notHandleDataChange = true
let node = data.data._node
if (node.nodeData.data.isActive) return
node.mindMap.renderer.moveNodeToCenter(node)
node.active()
let targetNode = node.data._node
if (targetNode && targetNode.nodeData.data.isActive) return
this.mindMap.execCommand('GO_TARGET_NODE', node.data.data.uid)
},
}
}
@@ -153,6 +151,10 @@ export default {
}
.outlineTree {
&.isDark {
background-color: #262a2e;
}
/deep/ .el-tree-node__content {
height: auto;
margin: 5px 0;

View File

@@ -1,5 +1,5 @@
<template>
<div class="scaleContainer">
<div class="scaleContainer" :class="{ isDark: isDark }">
<el-tooltip
class="item"
effect="dark"
@@ -31,6 +31,9 @@ export default {
props: {
mindMap: {
type: Object
},
isDark: {
type: Boolean
}
},
data() {
@@ -84,6 +87,16 @@ export default {
display: flex;
align-items: center;
&.isDark {
.btn {
color: hsla(0,0%,100%,.6);
}
.scaleInfo {
color: hsla(0,0%,100%,.6);
}
}
.btn {
cursor: pointer;
}

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