Compare commits

..

46 Commits

Author SHA1 Message Date
wanglin2
731a5a504d Merge branch 'feature' into main 2023-08-18 15:13:42 +08:00
wanglin2
008e697b74 打包0.6.15-fix.1 2023-08-18 15:13:24 +08:00
wanglin2
fe1745e779 Merge branch 'feature' into main 2023-08-18 14:55:15 +08:00
wanglin2
e6ac9be402 打包0.6.15 2023-08-18 14:54:49 +08:00
wanglin2
b427a9ed1b Doc: update 2023-08-18 14:51:42 +08:00
wanglin2
a4dc9210b3 Fix:修复节点边框会重合的问题 2023-08-18 14:25:24 +08:00
wanglin2
fb681de1f5 Feat:增加错误处理 2023-08-18 11:02:24 +08:00
wanglin2
3757622521 Fix:修复画布距浏览器窗口左上角不为0时鼠标缩放时不以鼠标为中心的问题 2023-08-18 10:33:52 +08:00
wanglin2
12265be7d4 Feat:新增禁止鼠标滚轮缩放的配置 2023-08-18 10:16:19 +08:00
wanglin2
df1aed7e04 Fix:优化展开收起按钮的占位元素:1.没有子节点的节点不渲染该元素;2.根据是否存在子节点动态更新该元素 2023-08-18 10:09:23 +08:00
wanglin2
c32c9d1ba1 更新群二维码 2023-08-17 10:06:16 +08:00
wanglin2
26d75c9203 打包 2023-08-17 10:04:49 +08:00
wanglin2
8d1e9fa8e9 Demo:侧边栏涉及图形的选项增加可视化效果 2023-08-17 10:00:33 +08:00
wanglin2
ebc99e97af Feat:导出pdf支持根据长宽比自动调整方向 2023-08-17 08:49:40 +08:00
wanglin2
efe4aa0ec2 Feat:导出pdf支持根据图片大小分页导出 2023-08-16 17:45:34 +08:00
wanglin2
3f659af1e1 Featc:删除重复的exportPadding配置,改为使用exportPaddingX和exportPaddingY配置;去除导出图片时的双重padding设置 2023-08-16 17:20:05 +08:00
wanglin2
64209a6392 Featc:删除重复的exportPadding配置,改为使用exportPaddingX和exportPaddingY配置;去除导出图片时的双重padding设置 2023-08-16 17:19:35 +08:00
wanglin2
8d1e5dd2e9 Feat:导出svg的图形的paddingX和paddingY改为单侧padding 2023-08-16 17:17:18 +08:00
wanglin2
d218122752 '打包' 2023-08-16 09:31:03 +08:00
wanglin2
9af8afca22 Demo:单独编辑大纲不再和画布联动,优化大数据量下的编辑体验 2023-08-16 09:25:33 +08:00
wanglin2
002ec41ba8 Fix:修复节点文本为空时显示异常问题 2023-08-15 09:30:00 +08:00
wanglin2
ffff257f57 Demo:修复打开本地文件右上角的提示无法关闭的问 2023-08-15 09:03:26 +08:00
街角小林
41d0b675a0 Merge pull request #269 from suka233/main
fix: 修复尝试赋值给const常量导致的vite启动dev服务报错
2023-08-15 08:48:43 +08:00
suka233
7601d6730d Merge pull request #1 from suka233/suka233-patch-1
fix: 修复尝试赋值给const常量导致的vite启动dev服务报错
2023-08-14 17:07:29 +08:00
suka233
f0e9b76bb5 fix: 修复尝试赋值给const常量导致的vite启动dev服务报错 2023-08-14 15:13:42 +08:00
wanglin2
4ec33062a4 Merge branch 'feature' into main 2023-08-14 09:39:03 +08:00
wanglin2
edc63b1293 打包 2023-08-14 09:38:49 +08:00
wanglin2
ec83976818 Doc:调整首页文档 2023-08-14 09:36:10 +08:00
wanglin2
393410757b Demo:调整小地图位置,解决被侧边按钮遮挡的问题 2023-08-14 09:29:11 +08:00
wanglin2
f20748744a Demo:右下角支持跳转相关链接 2023-08-14 09:27:59 +08:00
wanglin2
067131f76d Merge branch 'feature' into main 2023-08-13 12:21:16 +08:00
wanglin2
f689d333f9 打包0.6.14 2023-08-13 12:20:31 +08:00
wanglin2
b3059bb6a3 Doc: update 2023-08-13 12:18:25 +08:00
wanglin2
b135f6a61c Demo:支持粘贴知犀思维导图的节点数据 2023-08-13 11:43:44 +08:00
wanglin2
2a8b71497f Feat:自定义处理剪贴板函数支持返回Promise实例 2023-08-13 09:49:29 +08:00
wanglin2
726ebc5e88 Feat:支持自定义处理剪贴板中的文本数据 2023-08-13 08:47:35 +08:00
wanglin2
2c56cd453c Demo:支持从右键菜单删除节点的超链接,备注 2023-08-12 22:45:41 +08:00
wanglin2
bf2f9b2697 Demo:修复退出禅模式后左下角的节点和字数统计未更新的问题 2023-08-12 22:17:38 +08:00
wanglin2
fbd061e8b3 Demo:修复大纲修改一个节点的文本后再点击其他节点时不聚焦的问题 2023-08-12 15:43:10 +08:00
wanglin2
680320ba76 Demo:修复基础样式侧边栏打开状态下,导入思维导图数据后侧边栏数据未更新的问题 2023-08-12 15:10:46 +08:00
wanglin2
0d992bd6b1 Demo:颜色选择器支持选择透明颜色 2023-08-12 14:58:32 +08:00
wanglin2
c80eacc5e7 Demo:修复在节点样式侧边栏里无法去除节点的文本修饰线样式的问题 2023-08-12 14:42:14 +08:00
wanglin2
6d202b4b7d Demo:修复无法在节点图片弹窗里删除节点图片的问题 2023-08-12 14:21:11 +08:00
wanglin2
94478fe9f3 Demo:修复开启输入自动进入文本编辑模式和其他输入框冲突的问题 2023-08-12 11:12:11 +08:00
wanglin2
5745e4567b Feat:修改复制粘贴实现方式,去除创建隐藏输入框,支持跨浏览器粘贴思维导图数据 2023-08-12 10:57:51 +08:00
wanglin2
c6a3f4ac7b Feat:去除创建隐藏输入框,通过navigator.clipboard实现复制粘贴 2023-08-11 17:39:04 +08:00
64 changed files with 1676 additions and 329 deletions

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1"><link rel="icon" href="dist/logo.ico"><title>思绪思维导图</title><script>// 自定义静态资源的路径
window.externalPublicPath = './dist/'
// 接管应用
window.takeOverApp = false</script><link href="dist/css/chunk-vendors.css?5afd1a8b2213b3355dce" rel="stylesheet"><link href="dist/css/app.css?5afd1a8b2213b3355dce" rel="stylesheet"></head><body><noscript><strong>We're sorry but thoughts doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script>const getDataFromBackend = () => {
window.takeOverApp = false</script><link href="dist/css/chunk-vendors.css?c6f9dda3bfa8385d51f4" rel="stylesheet"><link href="dist/css/app.css?c6f9dda3bfa8385d51f4" rel="stylesheet"></head><body><noscript><strong>We're sorry but thoughts doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script>const getDataFromBackend = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
@@ -66,4 +66,4 @@
// 可以通过window.$bus.$on()来监听应用的一些事件
// 实例化页面
window.initApp()
}</script><script src="dist/js/chunk-vendors.js?5afd1a8b2213b3355dce"></script><script src="dist/js/app.js?5afd1a8b2213b3355dce"></script></body></html>
}</script><script src="dist/js/chunk-vendors.js?c6f9dda3bfa8385d51f4"></script><script src="dist/js/app.js?c6f9dda3bfa8385d51f4"></script></body></html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -7,10 +7,17 @@ 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, commonCaches } from './src/constants/constant'
import {
layoutValueList,
CONSTANTS,
commonCaches,
ERROR_TYPES
} from './src/constants/constant'
import { SVG } from '@svgdotjs/svg.js'
import { simpleDeepClone, getType } from './src/utils'
import defaultTheme, { checkIsNodeSizeIndependenceConfig } from './src/themes/default'
import defaultTheme, {
checkIsNodeSizeIndependenceConfig
} from './src/themes/default'
import { defaultOpt } from './src/constants/defaultOptions'
// 思维导图
@@ -22,11 +29,13 @@ class MindMap {
// 容器元素
this.el = this.opt.el
if (!this.el) throw new Error('缺少容器元素el')
this.elRect = this.el.getBoundingClientRect()
// 画布宽高
this.width = this.elRect.width
this.height = this.elRect.height
if (this.width <= 0 || this.height <= 0) throw new Error('容器元素el的宽高不能为0')
// 画布
this.svg = SVG().addTo(this.el).size(this.width, this.height)
@@ -68,7 +77,7 @@ class MindMap {
this.batchExecution = new BatchExecution()
// 注册插件
MindMap.pluginList.forEach((plugin) => {
MindMap.pluginList.forEach(plugin => {
this.initPlugin(plugin)
})
@@ -136,10 +145,10 @@ class MindMap {
// 初始化缓存数据
initCache() {
Object.keys(commonCaches).forEach((key) => {
Object.keys(commonCaches).forEach(key => {
let type = getType(commonCaches[key])
let value = ''
switch(type) {
switch (type) {
case 'Boolean':
value = false
break
@@ -164,7 +173,7 @@ class MindMap {
this.renderer.clearAllActive()
this.opt.theme = theme
this.render(null, CONSTANTS.CHANGE_THEME)
this.emit('view_theme_change', theme)
this.emit('view_theme_change', theme)
}
// 获取当前主题
@@ -278,8 +287,12 @@ class MindMap {
// 导出
async export(...args) {
let result = await this.doExport.export(...args)
return result
try {
let result = await this.doExport.export(...args)
return result
} catch (error) {
this.mindMap.opt.errorHandler(ERROR_TYPES.EXPORT_ERROR, error)
}
}
// 转换位置
@@ -317,9 +330,9 @@ class MindMap {
// 获取变换后的位置尺寸信息其实是getBoundingClientRect方法的包装方法
const rect = draw.rbox()
// 内边距
rect.width += paddingX
rect.height += paddingY
draw.translate(paddingX / 2, paddingY / 2)
rect.width += paddingX * 2
rect.height += paddingY * 2
draw.translate(paddingX, paddingY)
// 将svg设置为实际内容的宽高
svg.size(rect.width, rect.height)
// 把实际内容变换
@@ -327,7 +340,11 @@ class MindMap {
// 克隆一份数据
let clone = svg.clone()
// 如果实际图形宽高超出了屏幕宽高,且存在水印的话需要重新绘制水印,否则会出现超出部分没有水印的问题
if ((rect.width > origWidth || rect.height > origHeight) && this.watermark && this.watermark.hasWatermark()) {
if (
(rect.width > origWidth || rect.height > origHeight) &&
this.watermark &&
this.watermark.hasWatermark()
) {
this.width = rect.width
this.height = rect.height
this.watermark.draw()
@@ -388,7 +405,7 @@ class MindMap {
// 销毁
destroy() {
// 移除插件
[...MindMap.pluginList].forEach((plugin) => {
;[...MindMap.pluginList].forEach(plugin => {
this[plugin.instanceName] = null
})
// 解绑事件
@@ -408,8 +425,8 @@ MindMap.usePlugin = (plugin, opt = {}) => {
MindMap.pluginList.push(plugin)
return MindMap
}
MindMap.hasPlugin = (plugin) => {
return MindMap.pluginList.findIndex((item) => {
MindMap.hasPlugin = plugin => {
return MindMap.pluginList.findIndex(item => {
return item === plugin
})
}

View File

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

View File

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

View File

@@ -330,5 +330,16 @@ export const nodeDataNoStylePropList = [
// 数据缓存
export const commonCaches = {
measureCustomNodeContentSizeEl: null
measureCustomNodeContentSizeEl: null,
measureRichtextNodeTextSizeEl: null
}
// 错误类型
export const ERROR_TYPES = {
READ_CLIPBOARD_ERROR: 'read_clipboard_error',
PARSE_PASTE_DATA_ERROR: 'parse_paste_data_error',
CUSTOM_HANDLE_CLIPBOARD_TEXT_ERROR: 'custom_handle_clipboard_text_error',
LOAD_CLIPBOARD_IMAGE_ERROR: 'load_clipboard_image_error',
BEFORE_TEXT_EDIT_ERROR: 'before_text_edit_error',
EXPORT_ERROR: 'export_error'
}

View File

@@ -18,8 +18,6 @@ export const defaultOpt = {
mouseScaleCenterUseMousePosition: true,
// 最多显示几个标签
maxTag: 5,
// 导出图片时的内边距
exportPadding: 20,
// 展开收缩按钮尺寸
expandBtnSize: 20,
// 节点里图片和文字的间距
@@ -81,7 +79,7 @@ export const defaultOpt = {
enableShortcutOnlyWhenMouseInSvg: true,
// 初始根节点的位置
initRootNodePosition: null,
// 导出png、svg、pdf时的图形内边距
// 导出png、svg、pdf时的图形内边距,注意是单侧内边距
exportPaddingX: 10,
exportPaddingY: 10,
// 节点文本编辑框的z-index
@@ -127,12 +125,35 @@ export const defaultOpt = {
customInnerElsAppendTo: null,
// 拖拽元素时,指示元素新位置的块的最大高度
nodeDragPlaceholderMaxSize: 20,
// 是否允许创建一个隐藏的输入框,该输入框会在节点激活时聚焦,用于粘贴数据和自动进入文本编辑状态
enableCreateHiddenInput: true,
// 是否在存在一个激活节点时,当按下中文、英文、数字按键时自动进入文本编辑模式
// 该配置在enableCreateHiddenInput设为true时生效
enableAutoEnterTextEditWhenKeydown: true,
// 开启该特性后需要给你的输入框绑定keydown事件并禁止冒泡
enableAutoEnterTextEditWhenKeydown: false,
// 设置富文本节点编辑框和节点大小一致,形成伪原地编辑的效果
// 需要注意的是,只有当节点内只有文本、且形状是矩形才会有比较好的效果
richTextEditFakeInPlace: false
richTextEditFakeInPlace: false,
// 自定义对剪贴板文本的处理。当按ctrl+v粘贴时会读取用户剪贴板中的文本和图片默认只会判断文本是否是普通文本和simple-mind-map格式的节点数据如果你想处理其他思维导图的数据比如processon、zhixi等那么可以传递一个函数接受当前剪贴板中的文本为参数返回处理后的数据可以返回两种类型
/*
1.返回一个纯文本,那么会直接以该文本创建一个子节点
2.返回一个节点对象,格式如下:
{
// 代表是simple-mind-map格式的数据
simpleMindMap: true,
// 节点数据同simple-mind-map节点数据格式
data: {
data: {
text: ''
},
children: []
}
}
*/
// 如果你的处理逻辑存在异步逻辑也可以返回一个promise
customHandleClipboardText: null,
// 禁止鼠标滚轮缩放你仍旧可以使用api进行缩放
disableMouseWheelZoom: false,
// 错误处理函数
errorHandler: (code, error) => {
console.error(code, error)
}
}

View File

@@ -12,11 +12,12 @@ import {
simpleDeepClone,
walk,
bfsWalk,
loadImage
loadImage,
isUndef
} from '../../utils'
import { shapeList } from './node/Shape'
import { lineStyleProps } from '../../themes/default'
import { CONSTANTS } from '../../constants/constant'
import { CONSTANTS, ERROR_TYPES } from '../../constants/constant'
// 布局列表
const layouts = {
@@ -108,10 +109,6 @@ class Render {
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
}
})
// 粘贴事件
this.mindMap.on('paste', data => {
this.onPaste(data)
})
// let timer = null
// this.mindMap.on('view_data_change', () => {
// clearTimeout(timer)
@@ -273,8 +270,7 @@ class Render {
this.copy = this.copy.bind(this)
this.mindMap.keyCommand.addShortcut('Control+c', this.copy)
this.mindMap.keyCommand.addShortcut('Control+v', () => {
// 隐藏输入框可能会失去焦点,所以要重新聚焦
this.textEdit.focusHiddenInput()
this.onPaste()
})
this.cut = this.cut.bind(this)
this.mindMap.keyCommand.addShortcut('Control+x', this.cut)
@@ -449,7 +445,12 @@ class Render {
}
// 插入同级节点,多个节点只会操作第一个节点
insertNode(openEdit = true, appointNodes = [], appointData = null) {
insertNode(
openEdit = true,
appointNodes = [],
appointData = null,
appointChildren = []
) {
appointNodes = this.formatAppointNodes(appointNodes)
if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
return
@@ -485,14 +486,19 @@ class Render {
resetRichText: isRichText,
...(appointData || {})
},
children: []
children: [...appointChildren]
})
this.mindMap.render()
}
}
// 插入子节点
insertChildNode(openEdit = true, appointNodes = [], appointData = null) {
insertChildNode(
openEdit = true,
appointNodes = [],
appointData = null,
appointChildren = []
) {
appointNodes = this.formatAppointNodes(appointNodes)
if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
return
@@ -523,7 +529,7 @@ class Render {
resetRichText: isRichText,
...(appointData || {})
},
children: []
children: [...appointChildren]
})
// 插入子节点时自动展开子节点
node.nodeData.data.expand = true
@@ -591,15 +597,29 @@ class Render {
// 复制节点
copy() {
this.beingCopyData = this.copyNode()
this.setCoptyDataToClipboard(this.beingCopyData)
}
// 剪切节点
cut() {
this.mindMap.execCommand('CUT_NODE', copyData => {
this.beingCopyData = copyData
this.setCoptyDataToClipboard(copyData)
})
}
// 将粘贴或剪切的数据设置到用户剪切板中
setCoptyDataToClipboard(data) {
if (navigator.clipboard) {
navigator.clipboard.writeText(
JSON.stringify({
simpleMindMap: true,
data
})
)
}
}
// 粘贴节点
paste() {
if (this.beingCopyData) {
@@ -608,7 +628,29 @@ class Render {
}
// 粘贴事件
async onPaste({ text, img }) {
async onPaste() {
const { errorHandler } = this.mindMap.opt
// 读取剪贴板的文字和图片
let text = null
let img = null
if (navigator.clipboard) {
try {
text = await navigator.clipboard.readText()
const items = await navigator.clipboard.read()
if (items && items.length > 0) {
for (const clipboardItem of items) {
for (const type of clipboardItem.types) {
if (/^image\//.test(type)) {
img = await clipboardItem.getType(type)
break
}
}
}
}
} catch (error) {
errorHandler(ERROR_TYPES.READ_CLIPBOARD_ERROR, error)
}
}
// 检查剪切板数据是否有变化
// 通过图片大小来判断图片是否发生变化,可能是不准确的,但是目前没有其他好方法
const imgSize = img ? img.size : 0
@@ -626,9 +668,51 @@ class Render {
if (this.currentBeingPasteType === CONSTANTS.PASTE_TYPE.CLIP_BOARD) {
// 存在文本,则创建子节点
if (text) {
this.mindMap.execCommand('INSERT_CHILD_NODE', false, [], {
text
})
// 判断粘贴的是否是simple-mind-map的数据
let smmData = null
let useDefault = true
// 用户自定义处理
if (this.mindMap.opt.customHandleClipboardText) {
try {
const res = await this.mindMap.opt.customHandleClipboardText(text)
if (!isUndef(res)) {
useDefault = false
if (typeof res === 'object' && res.simpleMindMap) {
smmData = res.data
} else {
text = String(res)
}
}
} catch (error) {
errorHandler(ERROR_TYPES.CUSTOM_HANDLE_CLIPBOARD_TEXT_ERROR, error)
}
}
// 默认处理
if (useDefault) {
try {
const parsedData = JSON.parse(text)
if (parsedData && parsedData.simpleMindMap) {
smmData = parsedData.data
}
} catch (error) {
errorHandler(ERROR_TYPES.PARSE_PASTE_DATA_ERROR, error)
}
}
if (smmData) {
this.mindMap.execCommand(
'INSERT_CHILD_NODE',
false,
[],
{
...smmData.data
},
[...smmData.children]
)
} else {
this.mindMap.execCommand('INSERT_CHILD_NODE', false, [], {
text
})
}
}
// 存在图片,则添加到当前激活节点
if (img) {
@@ -645,7 +729,7 @@ class Render {
})
}
} catch (error) {
console.log(error)
errorHandler(ERROR_TYPES.LOAD_CLIPBOARD_IMAGE_ERROR, error)
}
}
} else {
@@ -1027,7 +1111,8 @@ class Render {
}
// 设置节点图片
setNodeImage(node, { url, title, width, height, custom = false }) {
setNodeImage(node, data) {
const { url, title, width, height, custom = false } = data || { url: '', title: '', width: 0, height: 0, custom: false }
this.setNodeDataRender(node, {
image: url,
imageTitle: title || '',

View File

@@ -1,4 +1,5 @@
import { getStrWithBrFromHtml, checkNodeOuter, isMobile } from '../../utils'
import { getStrWithBrFromHtml, checkNodeOuter } from '../../utils'
import { ERROR_TYPES } from '../../constants/constant'
// 节点文字编辑类
export default class TextEdit {
@@ -10,16 +11,11 @@ export default class TextEdit {
this.currentNode = null
// 文本编辑框
this.textEditNode = null
// 隐藏的文本输入框
this.hiddenInputEl = null
// 节点激活时默认聚焦到隐藏输入框
this.enableFocus = true
// 文本编辑框是否显示
this.showTextEdit = false
// 如果编辑过程中缩放画布了,那么缓存当前编辑的内容
this.cacheEditingText = ''
this.bindEvent()
this.createHiddenInput()
}
// 事件
@@ -51,10 +47,6 @@ 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) {
@@ -63,74 +55,29 @@ export default class TextEdit {
this.show(this.renderer.activeNodeList[0])
})
this.mindMap.on('scale', this.onScale)
}
// 创建一个隐藏的文本输入框
createHiddenInput() {
const { enableCreateHiddenInput, enableAutoEnterTextEditWhenKeydown } =
this.mindMap.opt
if (this.hiddenInputEl || isMobile() || !enableCreateHiddenInput) return
this.hiddenInputEl = document.createElement('input')
this.hiddenInputEl.type = 'text'
this.hiddenInputEl.style.cssText = `
position: fixed;
left: -99999px;
top: -99999px;
`
// 监听按键事件
if (enableAutoEnterTextEditWhenKeydown) {
this.hiddenInputEl.addEventListener('keydown', e => {
// // 监听按键事件,判断是否自动进入文本编辑模式
if (this.mindMap.opt.enableAutoEnterTextEditWhenKeydown) {
window.addEventListener('keydown', e => {
const activeNodeList = this.mindMap.renderer.activeNodeList
if (activeNodeList.length <= 0 || activeNodeList.length > 1) return
const node = activeNodeList[0]
// 当正在输入中文或英文或数字时,如果没有按下组合键,那么自动进入文本编辑模式
const keyCode = e.keyCode
if (
node &&
(keyCode === 229 ||
(keyCode >= 65 && keyCode <= 90) ||
(keyCode >= 48 && keyCode <= 57)) &&
!this.mindMap.keyCommand.hasCombinationKey(e)
) {
if (node && this.checkIsAutoEnterTextEditKey(e)) {
this.show(node)
}
})
}
// 监听粘贴事件
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.enableFocus) this.hiddenInputEl.focus()
}
// 关闭默认聚焦
stopFocusOnNodeActive() {
this.enableFocus = false
}
// 开启默认聚焦
openFocusOnNodeActive() {
this.enableFocus = true
// 判断是否是自动进入文本编模式的按钮
checkIsAutoEnterTextEditKey(e) {
const keyCode = e.keyCode
return (
(keyCode === 229 ||
(keyCode >= 65 && keyCode <= 90) ||
(keyCode >= 48 && keyCode <= 57)) &&
!this.mindMap.keyCommand.hasCombinationKey(e)
)
}
// 注册临时快捷键
@@ -158,6 +105,7 @@ export default class TextEdit {
isShow = await beforeTextEdit(node, isInserting)
} catch (error) {
isShow = false
this.mindMap.opt.errorHandler(ERROR_TYPES.BEFORE_TEXT_EDIT_ERROR, error)
}
if (!isShow) return
}
@@ -188,6 +136,7 @@ export default class TextEdit {
// 显示文本编辑框
showEditTextBox(node, rect, isInserting) {
if (this.showTextEdit) return
this.mindMap.emit('before_show_text_edit')
this.registerTmpShortcut()
if (!this.textEditNode) {
@@ -203,6 +152,11 @@ export default class TextEdit {
this.textEditNode.addEventListener('mousedown', e => {
e.stopPropagation()
})
this.textEditNode.addEventListener('keydown', e => {
if (this.checkIsAutoEnterTextEditKey(e)) {
e.stopPropagation()
}
})
const targetNode =
this.mindMap.opt.customInnerElsAppendTo || document.body
targetNode.appendChild(this.textEditNode)

View File

@@ -1,10 +1,11 @@
import Style from './Style'
import Shape from './Shape'
import { G, Rect, ForeignObject, SVG } from '@svgdotjs/svg.js'
import { G, ForeignObject, SVG } from '@svgdotjs/svg.js'
import nodeGeneralizationMethods from './nodeGeneralization'
import nodeExpandBtnMethods from './nodeExpandBtn'
import nodeCommandWrapsMethods from './nodeCommandWraps'
import nodeCreateContentsMethods from './nodeCreateContents'
import nodeExpandBtnPlaceholderRectMethods from './nodeExpandBtnPlaceholderRect'
import { CONSTANTS } from '../../../constants/constant'
// 节点类
@@ -107,6 +108,10 @@ class Node {
Object.keys(nodeExpandBtnMethods).forEach(item => {
this[item] = nodeExpandBtnMethods[item].bind(this)
})
// 展开收起按钮占位元素相关方法
Object.keys(nodeExpandBtnPlaceholderRectMethods).forEach(item => {
this[item] = nodeExpandBtnPlaceholderRectMethods[item].bind(this)
})
// 命令的相关方法
Object.keys(nodeCommandWrapsMethods).forEach(item => {
this[item] = nodeCommandWrapsMethods[item].bind(this)
@@ -252,9 +257,11 @@ class Node {
this.shapeInstance.getShapePadding(_width, _height, paddingX, paddingY)
this.shapePadding.paddingX = shapePaddingX
this.shapePadding.paddingY = shapePaddingY
// 边框宽度,因为边框是以中线向两端发散,所以边框会超出节点
const borderWidth = this.getBorderWidth()
return {
width: _width + paddingX * 2 + shapePaddingX * 2,
height: _height + paddingY * 2 + margin + shapePaddingY * 2
width: _width + paddingX * 2 + shapePaddingX * 2 + borderWidth,
height: _height + paddingY * 2 + margin + shapePaddingY * 2 + borderWidth
}
}
@@ -264,10 +271,12 @@ class Node {
this.group.clear()
let { width, height, textContentItemMargin } = this
let { paddingY } = this.getPaddingVale()
paddingY += this.shapePadding.paddingY
const halfBorderWidth = this.getBorderWidth() / 2
paddingY += this.shapePadding.paddingY + halfBorderWidth
// 节点形状
this.shapeNode = this.shapeInstance.createShape()
this.shapeNode.addClass('smm-node-shape')
this.shapeNode.translate(halfBorderWidth, halfBorderWidth)
this.group.add(this.shapeNode)
this.updateNodeShape()
// 渲染一个隐藏的矩形区域,用来触发展开收起按钮的显示
@@ -358,27 +367,6 @@ class Node {
this.group.add(textContentNested)
}
// 渲染展开收起按钮的隐藏占位元素
renderExpandBtnPlaceholderRect() {
if (!this.mindMap.opt.alwaysShowExpandBtn) {
let { width, height } = this
if (!this._unVisibleRectRegionNode) {
this._unVisibleRectRegionNode = new Rect()
this._unVisibleRectRegionNode.fill({
color: 'transparent'
})
}
this.group.add(this._unVisibleRectRegionNode)
this.renderer.layout.renderExpandBtnRect(
this._unVisibleRectRegionNode,
this.expandBtnSize,
width,
height,
this
)
}
}
// 给节点绑定事件
bindGroupEvent() {
// 单击事件,选中节点
@@ -588,10 +576,7 @@ class Node {
this.needLayout = false
this.layout()
}
if (this.needRerenderExpandBtnPlaceholderRect) {
this.needRerenderExpandBtnPlaceholderRect = false
this.renderExpandBtnPlaceholderRect()
}
this.updateExpandBtnPlaceholderRect()
this.update()
}
// 子节点
@@ -846,6 +831,11 @@ class Node {
) // 父级
}
// 获取节点非节点状态的边框大小
getBorderWidth() {
return this.style.merge('borderWidth', false, false) || 0
}
// 获取数据
getData(key) {
return key ? this.nodeData.data[key] || '' : this.nodeData.data

View File

@@ -62,7 +62,10 @@ export default class Shape {
// 创建形状节点
createShape() {
const shape = this.node.getShape()
const borderWidth = this.node.getBorderWidth()
let { width, height } = this.node
width -= borderWidth
height -= borderWidth
let node = null
// 矩形
if (shape === CONSTANTS.SHAPE.RECTANGLE) {

View File

@@ -1,4 +1,10 @@
import { measureText, resizeImgSize, removeHtmlStyle, addHtmlStyle, checkIsRichText } from '../../../utils'
import {
measureText,
resizeImgSize,
removeHtmlStyle,
addHtmlStyle,
checkIsRichText
} from '../../../utils'
import { Image, SVG, A, G, Rect, Text, ForeignObject } from '@svgdotjs/svg.js'
import iconsSvg from '../../../svg/icons'
import { CONSTANTS, commonCaches } from '../../../constants/constant'
@@ -54,7 +60,10 @@ function createIconNode() {
}
let iconSize = this.mindMap.themeConfig.iconSize
return _data.icon.map(item => {
let src = iconsSvg.getNodeIconListIcon(item, this.mindMap.opt.iconList || [])
let src = iconsSvg.getNodeIconListIcon(
item,
this.mindMap.opt.iconList || []
)
let node = null
// svg图标
if (/^<svg/.test(src)) {
@@ -108,7 +117,10 @@ function createRichTextNode() {
this.nodeData.data.text = text
}
let html = `<div>${this.nodeData.data.text}</div>`
let div = document.createElement('div')
if (!commonCaches.measureRichtextNodeTextSizeEl) {
commonCaches.measureRichtextNodeTextSizeEl = document.createElement('div')
}
let div = commonCaches.measureRichtextNodeTextSizeEl
div.innerHTML = html
div.style.cssText = `position: fixed; left: -999999px;`
let el = div.children[0]
@@ -117,12 +129,18 @@ function createRichTextNode() {
el.style.maxWidth = this.mindMap.opt.textAutoWrapWidth + 'px'
this.mindMap.el.appendChild(div)
let { width, height } = el.getBoundingClientRect()
width = Math.ceil(width) + 1// 修复getBoundingClientRect方法对实际宽度是小数的元素获取到的值是整数导致宽度不够文本发生换行的问题
// 如果文本为空,那么需要计算一个默认高度
if (height <= 0) {
div.innerHTML = '<p>abc123我和你</p>'
let elTmp = div.children[0]
elTmp.classList.add('smm-richtext-node-wrap')
height = elTmp.getBoundingClientRect().height
}
width = Math.ceil(width) + 1 // 修复getBoundingClientRect方法对实际宽度是小数的元素获取到的值是整数导致宽度不够文本发生换行的问题
height = Math.ceil(height)
g.attr('data-width', width)
g.attr('data-height', height)
html = div.innerHTML
this.mindMap.el.removeChild(div)
let foreignObject = new ForeignObject()
foreignObject.width(width)
foreignObject.height(height)
@@ -274,9 +292,10 @@ function createNoteNode() {
box-shadow: 0 2px 5px rgb(0 0 0 / 10%);
display: none;
background-color: #fff;
z-index: ${ this.mindMap.opt.nodeNoteTooltipZIndex }
z-index: ${this.mindMap.opt.nodeNoteTooltipZIndex}
`
const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body
const targetNode =
this.mindMap.opt.customInnerElsAppendTo || document.body
targetNode.appendChild(this.noteEl)
}
this.noteEl.innerText = this.nodeData.data.note
@@ -310,7 +329,7 @@ function createNoteNode() {
}
// 测量自定义节点内容元素的宽高
function measureCustomNodeContentSize (content) {
function measureCustomNodeContentSize(content) {
if (!commonCaches.measureCustomNodeContentSizeEl) {
commonCaches.measureCustomNodeContentSizeEl = document.createElement('div')
commonCaches.measureCustomNodeContentSizeEl.style.cssText = `
@@ -330,19 +349,19 @@ function measureCustomNodeContentSize (content) {
}
// 是否使用的是自定义节点内容
function isUseCustomNodeContent() {
function isUseCustomNodeContent() {
return !!this._customNodeContent
}
export default {
createImgNode,
getImgShowSize,
createIconNode,
createRichTextNode,
createTextNode,
createHyperlinkNode,
createTagNode,
createNoteNode,
measureCustomNodeContentSize,
isUseCustomNodeContent
}
createImgNode,
getImgShowSize,
createIconNode,
createRichTextNode,
createTextNode,
createHyperlinkNode,
createTagNode,
createNoteNode,
measureCustomNodeContentSize,
isUseCustomNodeContent
}

View File

@@ -0,0 +1,66 @@
import { Rect } from '@svgdotjs/svg.js'
// 渲染展开收起按钮的隐藏占位元素
function renderExpandBtnPlaceholderRect() {
// 根节点或没有子节点不需要渲染
if (
!this.nodeData.children ||
this.nodeData.children.length <= 0 ||
this.isRoot
) {
return
}
// 默认显示展开按钮的情况下也不需要渲染
if (!this.mindMap.opt.alwaysShowExpandBtn) {
let { width, height } = this
if (!this._unVisibleRectRegionNode) {
this._unVisibleRectRegionNode = new Rect()
this._unVisibleRectRegionNode.fill({
color: 'transparent'
})
}
this.group.add(this._unVisibleRectRegionNode)
this.renderer.layout.renderExpandBtnRect(
this._unVisibleRectRegionNode,
this.expandBtnSize,
width,
height,
this
)
}
}
// 删除展开收起按钮的隐藏占位元素
function clearExpandBtnPlaceholderRect() {
if (!this._unVisibleRectRegionNode) {
return
}
this._unVisibleRectRegionNode.remove()
this._unVisibleRectRegionNode = null
}
// 更新展开收起按钮的隐藏占位元素
function updateExpandBtnPlaceholderRect() {
// 布局改变需要重新渲染
if (this.needRerenderExpandBtnPlaceholderRect) {
this.needRerenderExpandBtnPlaceholderRect = false
this.renderExpandBtnPlaceholderRect()
}
// 没有子节点到有子节点需要渲染
if (this.nodeData.children && this.nodeData.children.length > 0) {
if (!this._unVisibleRectRegionNode) {
this.renderExpandBtnPlaceholderRect()
}
} else {
// 有子节点到没子节点,需要删除
if (this._unVisibleRectRegionNode) {
this.clearExpandBtnPlaceholderRect()
}
}
}
export default {
renderExpandBtnPlaceholderRect,
clearExpandBtnPlaceholderRect,
updateExpandBtnPlaceholderRect
}

View File

@@ -65,7 +65,8 @@ class View {
mousewheelAction,
mouseScaleCenterUseMousePosition,
mousewheelMoveStep,
mousewheelZoomActionReverse
mousewheelZoomActionReverse,
disableMouseWheelZoom
} = this.mindMap.opt
// 是否自定义鼠标滚轮事件
if (
@@ -76,8 +77,10 @@ class View {
}
// 鼠标滚轮事件控制缩放
if (mousewheelAction === CONSTANTS.MOUSE_WHEEL_ACTION.ZOOM) {
let cx = mouseScaleCenterUseMousePosition ? e.clientX : undefined
let cy = mouseScaleCenterUseMousePosition ? e.clientY : undefined
if (disableMouseWheelZoom) return
const { x: clientX, y: clientY } = this.mindMap.toPos(e.clientX, e.clientY)
let cx = mouseScaleCenterUseMousePosition ? clientX : undefined
let cy = mouseScaleCenterUseMousePosition ? clientY : undefined
switch (dir) {
// 鼠标滚轮,向上和向左,都是缩小
case CONSTANTS.DIR.UP:

View File

@@ -28,7 +28,7 @@ const handleList = node => {
}
// 将markdown转换成节点树
export const transformMarkdownTo = async md => {
export const transformMarkdownTo = md => {
const tree = fromMarkdown(md)
let root = {
children: []

View File

@@ -64,13 +64,13 @@ const transformXmind = async (content, files) => {
}
// 图片
if (node.image && /\.(jpg|jpeg|png|gif|webp)$/.test(node.image.src)) {
// 处理异步逻辑
let resolve = null
let promise = new Promise(_resolve => {
resolve = _resolve
})
waitLoadImageList.push(promise)
try {
// 处理异步逻辑
let resolve = null
let promise = new Promise(_resolve => {
resolve = _resolve
})
waitLoadImageList.push(promise)
// 读取图片
let imageType = /\.([^.]+)$/.exec(node.image.src)[1]
let imageBase64 =

View File

@@ -1,4 +1,9 @@
import { imgToDataUrl, downloadFile, readBlob, removeHTMLEntities } 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'
@@ -8,7 +13,6 @@ class Export {
// 构造函数
constructor(opt) {
this.mindMap = opt.mindMap
this.exportPadding = this.mindMap.opt.exportPadding
}
// 导出
@@ -49,7 +53,7 @@ class Export {
}
// svg转png
svgToPng(svgSrc, transparent) {
svgToPng(svgSrc, transparent, rotateWhenWidthLongerThenHeight = false) {
return new Promise((resolve, reject) => {
const img = new Image()
// 跨域图片需要添加这个属性,否则画布被污染了无法导出图片
@@ -57,12 +61,24 @@ class Export {
img.onload = async () => {
try {
let canvas = document.createElement('canvas')
canvas.width = img.width + this.exportPadding * 2
canvas.height = img.height + this.exportPadding * 2
// 如果宽比高长那么旋转90度
let needRotate =
rotateWhenWidthLongerThenHeight && img.width / img.height > 1
if (needRotate) {
canvas.width = img.height
canvas.height = img.width
} else {
canvas.width = img.width
canvas.height = img.height
}
let ctx = canvas.getContext('2d')
if (needRotate) {
ctx.rotate(0.5 * Math.PI)
ctx.translate(0, -img.height)
}
// 绘制背景
if (!transparent) {
await this.drawBackgroundToCanvas(ctx, canvas.width, canvas.height)
await this.drawBackgroundToCanvas(ctx, img.width, img.height)
}
// 图片绘制到canvas里
ctx.drawImage(
@@ -71,8 +87,8 @@ class Export {
0,
img.width,
img.height,
this.exportPadding,
this.exportPadding,
0,
0,
img.width,
img.height
)
@@ -96,7 +112,7 @@ class Export {
backgroundImage,
backgroundRepeat = 'no-repeat',
backgroundPosition = 'center center',
backgroundSize = 'cover',
backgroundSize = 'cover'
} = this.mindMap.themeConfig
// 背景颜色
ctx.save()
@@ -107,18 +123,25 @@ class Export {
// 背景图片
if (backgroundImage && backgroundImage !== 'none') {
ctx.save()
drawBackgroundImageToCanvas(ctx, width, height, backgroundImage, {
backgroundRepeat,
backgroundPosition,
backgroundSize
}, (err) => {
if (err) {
reject(err)
} else {
resolve()
drawBackgroundImageToCanvas(
ctx,
width,
height,
backgroundImage,
{
backgroundRepeat,
backgroundPosition,
backgroundSize
},
err => {
if (err) {
reject(err)
} else {
resolve()
}
ctx.restore()
}
ctx.restore()
})
)
} else {
resolve()
}
@@ -152,13 +175,17 @@ class Export {
* 方法1.把svg的图片都转化成data:url格式再转换
* 方法2.把svg的图片提取出来再挨个绘制到canvas里最后一起转换
*/
async png(name, transparent = false) {
async png(name, transparent = false, rotateWhenWidthLongerThenHeight) {
let { node, str } = await this.getSvgData()
str = removeHTMLEntities(str)
// 如果开启了富文本则使用htmltocanvas转换为图片
if (this.mindMap.richText) {
let res = await this.mindMap.richText.handleExportPng(node.node)
let imgDataUrl = await this.svgToPng(res, transparent)
let res = await this.mindMap.richText.handleExportPng(node.node)
let imgDataUrl = await this.svgToPng(
res,
transparent,
rotateWhenWidthLongerThenHeight
)
return imgDataUrl
}
// 转换成blob数据
@@ -168,17 +195,21 @@ class Export {
// 转换成data:url数据
let svgUrl = await readBlob(blob)
// 绘制到canvas上
let res = await this.svgToPng(svgUrl, transparent)
let res = await this.svgToPng(
svgUrl,
transparent,
rotateWhenWidthLongerThenHeight
)
return res
}
// 导出为pdf
async pdf(name) {
async pdf(name, useMultiPageExport) {
if (!this.mindMap.doExportPDF) {
throw new Error('请注册ExportPDF插件')
}
let img = await this.png()
this.mindMap.doExportPDF.pdf(name, img)
let img = await this.png('', false, true)
this.mindMap.doExportPDF.pdf(name, img, useMultiPageExport)
}
// 导出为xmind

View File

@@ -8,7 +8,16 @@ class ExportPDF {
}
// 导出为pdf
pdf(name, img) {
pdf(name, img, useMultiPageExport = false) {
if (useMultiPageExport) {
this.multiPageExport(name, img)
} else {
this.onePageExport(name, img)
}
}
// 单页导出
onePageExport(name, img) {
let pdf = new JsPDF('', 'pt', 'a4')
let a4Width = 595
let a4Height = 841
@@ -37,6 +46,52 @@ class ExportPDF {
}
image.src = img
}
// 多页导出
multiPageExport(name, img) {
let image = new Image()
const a4Width = 592.28
const a4Height = 841.89
image.onload = () => {
let imageWidth = image.width
let imageHeight = image.height
// 一页pdf显示高度
let pageHeight = (imageWidth / a4Width) * a4Height
// 未生成pdf的高度
let leftHeight = imageHeight
// 偏移
let position = 0
// a4纸的尺寸[595.28,841.89]图片在pdf中图片的宽高
let imgWidth = a4Width
let imgHeight = (a4Width / imageWidth) * imageHeight
let pdf = new JsPDF('', 'pt', 'a4')
// 有两个高度需要区分一个是图片的实际高度和生成pdf的页面高度(841.89)
// 当内容未超过pdf一页显示的范围无需分页
if (leftHeight < pageHeight) {
pdf.addImage(
img,
'PNG',
(a4Width - imgWidth) / 2,
(a4Height - imgHeight) / 2,
imgWidth,
imgHeight
)
} else {
// 分页
while (leftHeight > 0) {
pdf.addImage(img, 'PNG', 0, position, imgWidth, imgHeight)
leftHeight -= pageHeight
position -= a4Height
// 避免添加空白页
if (leftHeight > 0) {
pdf.addPage()
}
}
}
pdf.save(name)
}
image.src = img
}
}
ExportPDF.instanceName = 'doExportPDF'

View File

@@ -198,6 +198,11 @@ class RichText {
this.textEditNode.addEventListener('mousedown', e => {
e.stopPropagation()
})
this.textEditNode.addEventListener('keydown', e => {
if (this.mindMap.renderer.textEdit.checkIsAutoEnterTextEditKey(e)) {
e.stopPropagation()
}
})
const targetNode = customInnerElsAppendTo || document.body
targetNode.appendChild(this.textEditNode)
}

View File

@@ -78,7 +78,7 @@ class TouchEvent {
return
}
const viewBefore = this.touchStartScaleView
const scale = viewBefore.scale * (distance / viewBefore.distance)
let scale = viewBefore.scale * (distance / viewBefore.distance)
if (Math.abs(distance - viewBefore.distance) <= 10) {
scale = viewBefore.scale
}

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 2479351 */
src: url('iconfont.woff2?t=1690537337895') format('woff2'),
url('iconfont.woff?t=1690537337895') format('woff'),
url('iconfont.ttf?t=1690537337895') format('truetype');
src: url('iconfont.woff2?t=1691822758372') format('woff2'),
url('iconfont.woff?t=1691822758372') format('woff'),
url('iconfont.ttf?t=1691822758372') format('truetype');
}
.iconfont {
@@ -13,6 +13,10 @@
-moz-osx-font-smoothing: grayscale;
}
.icontouming:before {
content: "\e60c";
}
.iconlieri:before {
content: "\e60b";
}

View File

@@ -17,7 +17,9 @@ import {
shapeList as shapeListZh,
sidebarTriggerList as sidebarTriggerListZh,
backgroundSizeList as backgroundSizeListZh,
downTypeList as downTypeListZh
downTypeList as downTypeListZh,
shapeListMap as shapeListMapZh,
lineStyleMap as lineStyleMapZh
} from './zh'
import {
fontFamilyList as fontFamilyListEn,
@@ -48,6 +50,11 @@ const lineStyleList = {
en: lineStyleListEn
}
const lineStyleMap = {
zh: lineStyleMapZh,
en: lineStyleMapZh
}
const rootLineKeepSameInCurveList = {
zh: rootLineKeepSameInCurveListZh,
en: rootLineKeepSameInCurveListEn
@@ -78,6 +85,11 @@ const shapeList = {
en: shapeListEn
}
const shapeListMap = {
zh: shapeListMapZh,
en: shapeListMapZh
}
const sidebarTriggerList = {
zh: sidebarTriggerListZh,
en: sidebarTriggerListEn
@@ -100,12 +112,14 @@ export {
fontFamilyList,
borderDasharrayList,
lineStyleList,
lineStyleMap,
rootLineKeepSameInCurveList,
backgroundRepeatList,
backgroundPositionList,
backgroundSizeList,
shortcutKeyList,
shapeList,
shapeListMap,
sidebarTriggerList,
downTypeList
}

View File

@@ -97,7 +97,8 @@ export const colorList = [
'#0C797D',
'#0062B1',
'#653294',
'#AB149E'
'#AB149E',
'transparent'
]
// 边框宽度
@@ -141,6 +142,12 @@ export const borderRadiusList = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// 线宽
export const lineWidthList = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
export const lineStyleMap = {
straight: `<svg width="60" height="26"><path d="M18,14L30,14L30,5L42,5" fill="none" stroke="#000" stroke-width="2"></path><path d="M18,14L30,14L30,23L42,23" fill="none" stroke="#000" stroke-width="2"></path></svg>`,
curve: `<svg width="60" height="26"><path d="M18,14L30,14A12,-9 0 0 1 42,5" fill="none" stroke="#000" stroke-width="2"></path><path d="M18,14L30,14A12,9 0 0 0 42,23" fill="none" stroke="#000" stroke-width="2"></path></svg>`,
direct: `<svg width="60" height="26"><path d="M18,14L30,14L42,5" fill="none" stroke="#000" stroke-width="2"></path><path d="M18,14L30,14L42,23" fill="none" stroke="#000" stroke-width="2"></path></svg>`
}
// 连线风格
export const lineStyleList = [
{
@@ -378,6 +385,22 @@ export const shortcutKeyList = [
}
]
export const shapeListMap = {
rectangle: 'M 4 12 L 4 3 L 56 3 L 56 21 L 4 21 L 4 12 Z',
diamond: 'M 4 12 L 30 3 L 56 12 L 30 21 L 4 12 Z',
parallelogram: 'M 10 3 L 56 3 L 50 21 L 4 21 L 10 3 Z',
roundedRectangle:
'M 13 3 L 47 3 A 9 9 0, 0 1 47 21 L 13 21 A 9 9 0, 0 1 13 3 Z',
octagonalRectangle:
'M 4 12 L 4 9 L 10 3 L 50 3 L 56 9 L 56 15 L 50 21 L 10 21 L 4 15 L 4 12 Z',
outerTriangularRectangle:
'M 4 12 L 10 3 L 50 3 L 56 12 L 50 21 L 10 21 L 4 12 Z',
innerTriangularRectangle:
'M 10 12 L 4 3 L 56 3 L 50 12 L 56 21 L 4 21 L 10 12 Z',
ellipse: 'M 4 12 A 26 9 0, 1, 0 30 3 A 26 9 0, 0, 0 4 12 Z',
circle: 'M 21 12 A 9 9 0, 1, 0 30 3 A 9 9 0, 0, 0 21 12 Z'
}
// 形状列表
export const shapeList = [
{
@@ -508,4 +531,4 @@ export const downTypeList = [
icon: 'iconxmind',
desc: 'XMind格式'
}
]
]

View File

@@ -77,7 +77,10 @@ export default {
level5: 'Level5',
level6: 'Level6',
zenMode: 'Zen mode',
fitCanvas: 'Fit canvas'
fitCanvas: 'Fit canvas',
removeImage: 'Remove image',
removeHyperlink: 'Remove hyperlink',
removeNote: 'Remove note'
},
count: {
words: 'Words',
@@ -105,7 +108,8 @@ export default {
notifyTitle: 'Info',
notifyMessage: 'If the download is not triggered, check whether it is blocked by the browser',
paddingX: 'Padding x',
paddingY: 'Padding y'
paddingY: 'Padding y',
useMultiPageExport: 'Export multi page'
},
fullscreen: {
fullscreenShow: 'Full screen show',
@@ -142,7 +146,8 @@ export default {
addTip: 'Press Enter to add'
},
outline: {
title: 'Outline'
title: 'Outline',
nodeDefaultText: 'Branch node'
},
scale: {
zoomIn: 'Zoom in',
@@ -167,6 +172,7 @@ export default {
italic: 'Italic',
textDecoration: 'Text decoration',
underline: 'Underline',
none: 'None',
lineThrough: 'Line through',
overline: 'Overline',
border: 'Border',

View File

@@ -77,7 +77,10 @@ export default {
level5: '五级主题',
level6: '六级主题',
zenMode: '禅模式',
fitCanvas: '适应画布'
fitCanvas: '适应画布',
removeImage: '移除图片',
removeHyperlink: '移除超链接',
removeNote: '移除备注'
},
count: {
words: '字数',
@@ -105,7 +108,8 @@ export default {
notifyTitle: '消息',
notifyMessage: '如果没有触发下载,请检查是否被浏览器拦截了',
paddingX: '水平内边距',
paddingY: '垂直内边距'
paddingY: '垂直内边距',
useMultiPageExport: '是否多页导出'
},
fullscreen: {
fullscreenShow: '全屏查看',
@@ -142,7 +146,8 @@ export default {
addTip: '请按回车键添加'
},
outline: {
title: '大纲'
title: '大纲',
nodeDefaultText: '分支节点'
},
scale: {
zoomIn: '放大',
@@ -166,6 +171,7 @@ export default {
addFontWeight: '加粗',
italic: '斜体',
textDecoration: '划线',
none: '无',
underline: '下划线',
lineThrough: '中划线',
overline: '上划线',

View File

@@ -1,5 +1,67 @@
# Changelog
## 0.6.15-fix.1
新增:
> 1.Export PDF supports pagination export based on image size.
>
> 2.Exporting PDF supports automatic direction adjustment based on aspect ratio.
>
> 3.Optimize the placeholder elements of the expand and collapse buttons: 1. Nodes without child nodes do not render this element; 2. Dynamically update the element based on the existence of child nodes.
>
> 4.Add a configuration that prohibits mouse wheel scaling.
>
> 5.Supports passing error handling functions.
修复:
> 1.Fix the issue of displaying exceptions when node text is empty.
>
> 2.Change the paddingX and paddingY of exported SVG graphics to single sided padding.
>
> 3.Fixed an issue where the mouse is not centered when zooming when the canvas is not 0 from the top left corner of the browser window.
>
> 4.Fix the issue of overlapping node borders.
Demo
> 1.The bottom right corner supports jumping to related links.
>
> 2.Adjust the position of the mini map to solve the problem of being blocked by side buttons.
>
> 3.Fix the issue where the prompt in the upper right corner of the open local file cannot be closed.
>
> 4.Editing the outline separately is no longer linked to the canvas, optimizing the editing experience under large data volume.
>
> 5.The sidebar involves graphical options to increase visualization effects.
## 0.6.14
New:
> 1.Remove and create hidden input boxes, and copy and paste them through navigator. clipboard; Support cross browser pasting of mind map node data; Support custom processing of text data in the clipboard.
Demo:
> 1.Fix the issue of enabling input to automatically enter text editing mode and conflicting with other input boxes.
>
> 2.Fix the issue of not being able to delete node images in the node image pop-up window.
>
> 3.Fixed an issue where the text decoration line style of nodes cannot be removed in the node style sidebar.
>
> 4.The color selector supports selecting transparent colors.
>
> 5.Fix the issue of importing mind map data without updating the sidebar data when the basic style sidebar is open.
>
> 6.Fixed the issue of not focusing when modifying the text of one node in the outline and then clicking on other nodes.
>
> 7.Fixed an issue where the node and word count statistics in the bottom left corner were not updated after exiting Zen mode.
>
> 8.Support deleting hyperlinks and notes of nodes from the right-click menu.
>
> 9.Support pasting node data of Zhixi Mind Map.
## 0.6.13
Fix:

View File

@@ -1,6 +1,47 @@
<template>
<div>
<h1>Changelog</h1>
<h2>0.6.15-fix.1</h2>
<p>新增</p>
<blockquote>
<p>1.Export PDF supports pagination export based on image size.</p>
<p>2.Exporting PDF supports automatic direction adjustment based on aspect ratio.</p>
<p>3.Optimize the placeholder elements of the expand and collapse buttons: 1. Nodes without child nodes do not render this element; 2. Dynamically update the element based on the existence of child nodes.</p>
<p>4.Add a configuration that prohibits mouse wheel scaling.</p>
<p>5.Supports passing error handling functions.</p>
</blockquote>
<p>修复</p>
<blockquote>
<p>1.Fix the issue of displaying exceptions when node text is empty.</p>
<p>2.Change the paddingX and paddingY of exported SVG graphics to single sided padding.</p>
<p>3.Fixed an issue where the mouse is not centered when zooming when the canvas is not 0 from the top left corner of the browser window.</p>
<p>4.Fix the issue of overlapping node borders.</p>
</blockquote>
<p>Demo</p>
<blockquote>
<p>1.The bottom right corner supports jumping to related links.</p>
<p>2.Adjust the position of the mini map to solve the problem of being blocked by side buttons.</p>
<p>3.Fix the issue where the prompt in the upper right corner of the open local file cannot be closed.</p>
<p>4.Editing the outline separately is no longer linked to the canvas, optimizing the editing experience under large data volume.</p>
<p>5.The sidebar involves graphical options to increase visualization effects.</p>
</blockquote>
<h2>0.6.14</h2>
<p>New:</p>
<blockquote>
<p>1.Remove and create hidden input boxes, and copy and paste them through navigator. clipboard; Support cross browser pasting of mind map node data; Support custom processing of text data in the clipboard.</p>
</blockquote>
<p>Demo:</p>
<blockquote>
<p>1.Fix the issue of enabling input to automatically enter text editing mode and conflicting with other input boxes.</p>
<p>2.Fix the issue of not being able to delete node images in the node image pop-up window.</p>
<p>3.Fixed an issue where the text decoration line style of nodes cannot be removed in the node style sidebar.</p>
<p>4.The color selector supports selecting transparent colors.</p>
<p>5.Fix the issue of importing mind map data without updating the sidebar data when the basic style sidebar is open.</p>
<p>6.Fixed the issue of not focusing when modifying the text of one node in the outline and then clicking on other nodes.</p>
<p>7.Fixed an issue where the node and word count statistics in the bottom left corner were not updated after exiting Zen mode.</p>
<p>8.Support deleting hyperlinks and notes of nodes from the right-click menu.</p>
<p>9.Support pasting node data of Zhixi Mind Map.</p>
</blockquote>
<h2>0.6.13</h2>
<p>Fix:</p>
<blockquote>

View File

@@ -73,9 +73,12 @@ const mindMap = new MindMap({
| mouseScaleCenterUseMousePositionv0.6.4-fix.1+ | Boolean | true | Is the mouse zoom centered around the current position of the mouse, otherwise centered around the canvas | |
| customInnerElsAppendTov0.6.12+ | null/HTMLElement | null | Specify the location where some internal elements (node text editing element, node note display element, associated line text editing element, node image adjustment button element) are added, and default to document.body | |
| nodeDragPlaceholderMaxSizev0.6.12+ | Number | 20 | When dragging an element, the maximum height of the block indicating the new position of the element | |
| enableCreateHiddenInputv0.6.13+ | Boolean | true | Is it allowed to create a hidden input box that will be focused when the node is activated for pasting data and automatically entering the text editing state | |
| enableAutoEnterTextEditWhenKeydownv0.6.13+ | Boolean | true | Does it automatically enter text editing mode when pressing the Chinese, English, or numeric buttons when there is an activation node? This configuration takes effect when enableCreateHiddenInput is set to true | |
| enableCreateHiddenInputv0.6.13+v0.6.14+ remove this feature | Boolean | true | Is it allowed to create a hidden input box that will be focused when the node is activated for pasting data and automatically entering the text editing state | |
| enableAutoEnterTextEditWhenKeydownv0.6.13+ | Boolean | true | Does it automatically enter text editing mode when pressing the Chinese, English, or numeric buttons when there is an activation node?| |
| richTextEditFakeInPlacev0.6.13+ | Boolean | false | Set the rich text node edit box to match the size of the node, creating a pseudo in place editing effect. It should be noted that only when there is only text within the node and the shape is rectangular, can the effect be better | |
| customHandleClipboardTextv0.6.14+ | Function | null | Customize the processing of clipboard text. When pressing ctrl+v to paste, it will read the text and images from the user's clipboard. By default, it will only determine whether the text is regular text and node data in simple mind map format. If you want to process data from other mind maps, such as process, zhixi, etc., you can pass a function that takes the text from the current clipboard as a parameter and returns the processed data, which can be of two types: 1.If a pure text is returned, a child node will be directly created with that text; 2.Returns a node object in the following format: { simpleMindMap: true, data: { data: { text: '' }, children: [] } }, The representative is data in simple bind map format, and the node data is in the same format as the simple bind map node data. If your processing logic has asynchronous logic, you can also return a promise | |
| errorHandlerv0.6.15+ | Function | | Custom error handling functions currently only throw some asynchronous logic errors. Can pass a function that takes two parameters, the first being the wrong type and the second being the wrong object | |
| disableMouseWheelZoomv0.6.15+ | Boolean | false | Prohibit mouse wheel scaling, you can still use the API for scaling | |
### Watermark config
@@ -326,8 +329,8 @@ redo. All commands are as follows:
| SELECT_ALL | Select all | |
| BACK | Go back a specified number of steps | step (the number of steps to go back, default is 1) |
| FORWARD | Go forward a specified number of steps | step (the number of steps to go forward, default is 1) |
| INSERT_NODE | Insert a sibling node, the active node or appoint node will be the operation node. If there are multiple active nodes, only the first one will be effective | openEditv0.4.6+, Whether to activate the newly inserted node and enter editing mode, default is `true` 、 appointNodesv0.4.7+, Optional, appoint node, Specifying multiple nodes can pass an array、 appointDataOptional, Specify the data for the newly created node, Such as {text: 'xxx', ...}, Detailed structure can be referred to [exampleData.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js) |
| INSERT_CHILD_NODE | Insert a child node, the active node or appoint node will be the operation node | openEditv0.4.6+, Whether to activate the newly inserted node and enter editing mode, default is `true`)、 appointNodesv0.4.7+, Optional, appoint node, Specifying multiple nodes can pass an array、 appointDataOptional, Specify the data for the newly created node, Such as {text: 'xxx', ...}, Detailed structure can be referred to [exampleData.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js) |
| INSERT_NODE | Insert a sibling node, the active node or appoint node will be the operation node. If there are multiple active nodes, only the first one will be effective | openEditv0.4.6+, Whether to activate the newly inserted node and enter editing mode, default is `true` 、 appointNodesv0.4.7+, Optional, appoint node, Specifying multiple nodes can pass an array、 appointDataOptional, Specify the data for the newly created node, Such as {text: 'xxx', ...}, Detailed structure can be referred to [exampleData.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js) 、 appointChildrenv0.6.14+, Optional, Specify the child nodes of the newly created node, array type |
| INSERT_CHILD_NODE | Insert a child node, the active node or appoint node will be the operation node | openEditv0.4.6+, Whether to activate the newly inserted node and enter editing mode, default is `true`)、 appointNodesv0.4.7+, Optional, appoint node, Specifying multiple nodes can pass an array、 appointDataOptional, Specify the data for the newly created node, Such as {text: 'xxx', ...}, Detailed structure can be referred to [exampleData.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js) 、 appointChildrenv0.6.14+, Optional, Specify the child nodes of the newly created node, array type |
| UP_NODE | Move node up, the active node will be the operation node. If there are multiple active nodes, only the first one will be effective. Using this command on the root node or the first node in the list will be invalid | |
| DOWN_NODE | Move node down, the active node will be the operation node. If there are multiple active nodes, only the first one will be effective. Using this command on the root node or the last node in the list will be invalid | |
| REMOVE_NODE | Remove node, the active node or appoint node will be the operation node | appointNodesv0.4.7+, Optional, appoint node, Specifying multiple nodes can pass an array |

View File

@@ -372,7 +372,7 @@
<td></td>
</tr>
<tr>
<td>enableCreateHiddenInputv0.6.13+</td>
<td>enableCreateHiddenInputv0.6.13+v0.6.14+ remove this feature</td>
<td>Boolean</td>
<td>true</td>
<td>Is it allowed to create a hidden input box that will be focused when the node is activated for pasting data and automatically entering the text editing state</td>
@@ -382,7 +382,7 @@
<td>enableAutoEnterTextEditWhenKeydownv0.6.13+</td>
<td>Boolean</td>
<td>true</td>
<td>Does it automatically enter text editing mode when pressing the Chinese, English, or numeric buttons when there is an activation node? This configuration takes effect when enableCreateHiddenInput is set to true</td>
<td>Does it automatically enter text editing mode when pressing the Chinese, English, or numeric buttons when there is an activation node?</td>
<td></td>
</tr>
<tr>
@@ -392,6 +392,27 @@
<td>Set the rich text node edit box to match the size of the node, creating a pseudo in place editing effect. It should be noted that only when there is only text within the node and the shape is rectangular, can the effect be better</td>
<td></td>
</tr>
<tr>
<td>customHandleClipboardTextv0.6.14+</td>
<td>Function</td>
<td>null</td>
<td>Customize the processing of clipboard text. When pressing ctrl+v to paste, it will read the text and images from the user's clipboard. By default, it will only determine whether the text is regular text and node data in simple mind map format. If you want to process data from other mind maps, such as process, zhixi, etc., you can pass a function that takes the text from the current clipboard as a parameter and returns the processed data, which can be of two types: 1.If a pure text is returned, a child node will be directly created with that text; 2.Returns a node object in the following format: { simpleMindMap: true, data: { data: { text: '' }, children: [] } }, The representative is data in simple bind map format, and the node data is in the same format as the simple bind map node data. If your processing logic has asynchronous logic, you can also return a promise</td>
<td></td>
</tr>
<tr>
<td>errorHandlerv0.6.15+</td>
<td>Function</td>
<td></td>
<td>Custom error handling functions currently only throw some asynchronous logic errors. Can pass a function that takes two parameters, the first being the wrong type and the second being the wrong object</td>
<td></td>
</tr>
<tr>
<td>disableMouseWheelZoomv0.6.15+</td>
<td>Boolean</td>
<td>false</td>
<td>Prohibit mouse wheel scaling, you can still use the API for scaling</td>
<td></td>
</tr>
</tbody>
</table>
<h3>Watermark config</h3>
@@ -828,12 +849,12 @@ redo. All commands are as follows:</p>
<tr>
<td>INSERT_NODE</td>
<td>Insert a sibling node, the active node or appoint node will be the operation node. If there are multiple active nodes, only the first one will be effective</td>
<td>openEditv0.4.6+, Whether to activate the newly inserted node and enter editing mode, default is <code>true</code> appointNodesv0.4.7+, Optional, appoint node, Specifying multiple nodes can pass an array appointDataOptional, Specify the data for the newly created node, Such as {text: 'xxx', ...}, Detailed structure can be referred to <a href="https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js">exampleData.js</a> </td>
<td>openEditv0.4.6+, Whether to activate the newly inserted node and enter editing mode, default is <code>true</code> 、 appointNodesv0.4.7+, Optional, appoint node, Specifying multiple nodes can pass an arrayappointDataOptional, Specify the data for the newly created node, Such as {text: 'xxx', ...}, Detailed structure can be referred to <a href="https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js">exampleData.js</a> )、 appointChildrenv0.6.14+, Optional, Specify the child nodes of the newly created node, array type</td>
</tr>
<tr>
<td>INSERT_CHILD_NODE</td>
<td>Insert a child node, the active node or appoint node will be the operation node</td>
<td>openEditv0.4.6+, Whether to activate the newly inserted node and enter editing mode, default is <code>true</code> appointNodesv0.4.7+, Optional, appoint node, Specifying multiple nodes can pass an array appointDataOptional, Specify the data for the newly created node, Such as {text: 'xxx', ...}, Detailed structure can be referred to <a href="https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js">exampleData.js</a> </td>
<td>openEditv0.4.6+, Whether to activate the newly inserted node and enter editing mode, default is <code>true</code>)、 appointNodesv0.4.7+, Optional, appoint node, Specifying multiple nodes can pass an arrayappointDataOptional, Specify the data for the newly created node, Such as {text: 'xxx', ...}, Detailed structure can be referred to <a href="https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js">exampleData.js</a> )、 appointChildrenv0.6.14+, Optional, Specify the child nodes of the newly created node, array type</td>
</tr>
<tr>
<td>UP_NODE</td>

View File

@@ -38,12 +38,14 @@ a.download = 'xxx'
a.click()
```
### png(name, transparent = false)
### png(name, transparent = false, rotateWhenWidthLongerThenHeight)
- `name`: Name, optional
- `transparent`: v0.5.7+, Specify whether the background of the exported image is transparent
- `rotateWhenWidthLongerThenHeight`: v0.6.15+Boolean, false, Automatically rotate 90 degrees when the image has a width to height ratio
Exports as `png`.
### svg(name, plusCssText)
@@ -66,11 +68,13 @@ svg(
Exports as `svg`.
### pdf(name)
### pdf(name, useMultiPageExport)
> v0.2.1+
`name`File name
- `name`File name
- `useMultiPageExport`: v0.6.15+, Boolean, false, Whether to export multiple pages, default to single page
Export as `pdf`. Unlike other export methods, this method does not return data and directly triggers the download.

View File

@@ -27,7 +27,7 @@ a.href = <span class="hljs-string">&#x27;xxx.png&#x27;</span><span class="hljs-c
a.download = <span class="hljs-string">&#x27;xxx&#x27;</span>
a.click()
</code></pre>
<h3>png(name, transparent = false)</h3>
<h3>png(name, transparent = false, rotateWhenWidthLongerThenHeight)</h3>
<ul>
<li>
<p><code>name</code>: Name, optional</p>
@@ -35,6 +35,9 @@ a.click()
<li>
<p><code>transparent</code>: v0.5.7+, Specify whether the background of the exported image is transparent</p>
</li>
<li>
<p><code>rotateWhenWidthLongerThenHeight</code>: v0.6.15+Boolean, false, Automatically rotate 90 degrees when the image has a width to height ratio</p>
</li>
</ul>
<p>Exports as <code>png</code>.</p>
<h3>svg(name, plusCssText)</h3>
@@ -57,11 +60,18 @@ a.click()
)
</code></pre>
<p>Exports as <code>svg</code>.</p>
<h3>pdf(name)</h3>
<h3>pdf(name, useMultiPageExport)</h3>
<blockquote>
<p>v0.2.1+</p>
</blockquote>
<ul>
<li>
<p><code>name</code>File name</p>
</li>
<li>
<p><code>useMultiPageExport</code>: v0.6.15+, Boolean, false, Whether to export multiple pages, default to single page</p>
</li>
</ul>
<p>Export as <code>pdf</code>. Unlike other export methods, this method does not return data and directly triggers the download.</p>
<blockquote>
<p>After v0.6.0, an additional ExportPDF plugin needs to be registered</p>

View File

@@ -18,6 +18,8 @@ The principle of this plugin is to use [Quill](https://github.com/quilljs/quill)
`V0.6.13+` version uses [dom-to-image-more](https://github.com/1904labs/dom-to-image-more) Replaced 'html2canvas' to address the issue of ineffective color export for nodes.
> The compatibility of dom to image more is relatively poor, and exported images are empty on many browsers, so you can replace them with html2canvas according to your own needs.
## Register
```js

View File

@@ -18,6 +18,9 @@
<p>The version of <code>v0.5.7+</code> directly uses <code>html2canvas</code> to convert the entire <code>svg</code>, which is no longer an issue with speed. However, there is currently a bug where the color of the node does not take effect after export.</p>
</blockquote>
<p><code>V0.6.13+</code> version uses <a href="https://github.com/1904labs/dom-to-image-more">dom-to-image-more</a> Replaced 'html2canvas' to address the issue of ineffective color export for nodes.</p>
<blockquote>
<p>The compatibility of dom to image more is relatively poor, and exported images are empty on many browsers, so you can replace them with html2canvas according to your own needs.</p>
</blockquote>
<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> RichText <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;simple-mind-map/src/plugins/RichText.js&#x27;</span>

View File

@@ -1,5 +1,67 @@
# Changelog
## 0.6.15-fix.1
新增:
> 1.导出pdf支持根据图片大小分页导出。
>
> 2.导出pdf支持根据长宽比自动调整方向。
>
> 3.优化展开收起按钮的占位元素1.没有子节点的节点不渲染该元素2.根据是否存在子节点动态更新该元素。
>
> 4.新增禁止鼠标滚轮缩放的配置。
>
> 5.支持传递错误处理函数。
修复:
> 1.修复节点文本为空时显示异常问题。
>
> 2.导出svg的图形的paddingX和paddingY改为单侧padding。
>
> 3.修复画布距浏览器窗口左上角不为0时鼠标缩放时不以鼠标为中心的问题。
>
> 4.修复节点边框会重合的问题。
Demo
> 1.右下角支持跳转相关链接。
>
> 2.调整小地图位置,解决被侧边按钮遮挡的问题。
>
> 3.修复打开本地文件右上角的提示无法关闭的问题。
>
> 4.单独编辑大纲不再和画布联动,优化大数据量下的编辑体验。
>
> 5.侧边栏涉及图形的选项增加可视化效果。
## 0.6.14
新增:
> 1.去除创建隐藏输入框通过navigator.clipboard实现复制粘贴支持跨浏览器粘贴思维导图节点数据支持自定义处理剪贴板中的文本数据。
Demo
> 1.修复开启输入自动进入文本编辑模式和其他输入框冲突的问题。
>
> 2.修复无法在节点图片弹窗里删除节点图片的问题。
>
> 3.修复在节点样式侧边栏里无法去除节点的文本修饰线样式的问题。
>
> 4.颜色选择器支持选择透明颜色。
>
> 5.修复基础样式侧边栏打开状态下,导入思维导图数据后侧边栏数据未更新的问题。
>
> 6.修复大纲修改一个节点的文本后再点击其他节点时不聚焦的问题。
>
> 7.修复退出禅模式后左下角的节点和字数统计未更新的问题。
>
> 8.支持从右键菜单删除节点的超链接、备注。
>
> 9.支持粘贴知犀思维导图的节点数据。
## 0.6.13
修复:

View File

@@ -1,6 +1,47 @@
<template>
<div>
<h1>Changelog</h1>
<h2>0.6.15-fix.1</h2>
<p>新增</p>
<blockquote>
<p>1.导出pdf支持根据图片大小分页导出</p>
<p>2.导出pdf支持根据长宽比自动调整方向</p>
<p>3.优化展开收起按钮的占位元素1.没有子节点的节点不渲染该元素2.根据是否存在子节点动态更新该元素</p>
<p>4.新增禁止鼠标滚轮缩放的配置</p>
<p>5.支持传递错误处理函数</p>
</blockquote>
<p>修复</p>
<blockquote>
<p>1.修复节点文本为空时显示异常问题</p>
<p>2.导出svg的图形的paddingX和paddingY改为单侧padding</p>
<p>3.修复画布距浏览器窗口左上角不为0时鼠标缩放时不以鼠标为中心的问题</p>
<p>4.修复节点边框会重合的问题</p>
</blockquote>
<p>Demo</p>
<blockquote>
<p>1.右下角支持跳转相关链接</p>
<p>2.调整小地图位置解决被侧边按钮遮挡的问题</p>
<p>3.修复打开本地文件右上角的提示无法关闭的问题</p>
<p>4.单独编辑大纲不再和画布联动优化大数据量下的编辑体验</p>
<p>5.侧边栏涉及图形的选项增加可视化效果</p>
</blockquote>
<h2>0.6.14</h2>
<p>新增</p>
<blockquote>
<p>1.去除创建隐藏输入框通过navigator.clipboard实现复制粘贴支持跨浏览器粘贴思维导图节点数据支持自定义处理剪贴板中的文本数据</p>
</blockquote>
<p>Demo</p>
<blockquote>
<p>1.修复开启输入自动进入文本编辑模式和其他输入框冲突的问题</p>
<p>2.修复无法在节点图片弹窗里删除节点图片的问题</p>
<p>3.修复在节点样式侧边栏里无法去除节点的文本修饰线样式的问题</p>
<p>4.颜色选择器支持选择透明颜色</p>
<p>5.修复基础样式侧边栏打开状态下,导入思维导图数据后侧边栏数据未更新的问题</p>
<p>6.修复大纲修改一个节点的文本后再点击其他节点时不聚焦的问题</p>
<p>7.修复退出禅模式后左下角的节点和字数统计未更新的问题</p>
<p>8.支持从右键菜单删除节点的超链接备注</p>
<p>9.支持粘贴知犀思维导图的节点数据</p>
</blockquote>
<h2>0.6.13</h2>
<p>修复</p>
<blockquote>

View File

@@ -73,9 +73,12 @@ const mindMap = new MindMap({
| mouseScaleCenterUseMousePositionv0.6.4-fix.1+ | Boolean | true | 鼠标缩放是否以鼠标当前位置为中心点,否则以画布中心点 | |
| customInnerElsAppendTov0.6.12+ | null/HTMLElement | null | 指定内部一些元素节点文本编辑元素、节点备注显示元素、关联线文本编辑元素、节点图片调整按钮元素添加到的位置默认添加到document.body下 | |
| nodeDragPlaceholderMaxSizev0.6.12+ | Number | 20 | 拖拽元素时,指示元素新位置的块的最大高度 | |
| enableCreateHiddenInputv0.6.13+ | Boolean | true | 是否允许创建一个隐藏的输入框,该输入框会在节点激活时聚焦,用于粘贴数据和自动进入文本编辑状态 | |
| enableAutoEnterTextEditWhenKeydownv0.6.13+ | Boolean | true | 是否在存在一个激活节点时,当按下中文、英文、数字按键时自动进入文本编辑模式该配置在enableCreateHiddenInput设为true时生效 | |
| enableCreateHiddenInputv0.6.13+v0.6.14+版本已去除该特性) | Boolean | true | 是否允许创建一个隐藏的输入框,该输入框会在节点激活时聚焦,用于粘贴数据和自动进入文本编辑状态 | |
| enableAutoEnterTextEditWhenKeydownv0.6.13+ | Boolean | true | 是否在存在一个激活节点时,当按下中文、英文、数字按键时自动进入文本编辑模式 | |
| richTextEditFakeInPlacev0.6.13+ | Boolean | false | 设置富文本节点编辑框和节点大小一致,形成伪原地编辑的效果,需要注意的是,只有当节点内只有文本、且形状是矩形才会有比较好的效果 | |
| customHandleClipboardTextv0.6.14+ | Function | null | 自定义对剪贴板文本的处理。当按ctrl+v粘贴时会读取用户剪贴板中的文本和图片默认只会判断文本是否是普通文本和simple-mind-map格式的节点数据如果你想处理其他思维导图的数据比如processon、zhixi等那么可以传递一个函数接受当前剪贴板中的文本为参数返回处理后的数据可以返回两种类型1.返回一个纯文本那么会直接以该文本创建一个子节点2.返回一个节点对象,格式如下:{ simpleMindMap: true, data: { data: { text: '' }, children: [] } }代表是simple-mind-map格式的数据节点数据同simple-mind-map节点数据格式如果你的处理逻辑存在异步逻辑也可以返回一个promise | |
| errorHandlerv0.6.15+ | Function | | 自定义错误处理函数,目前只会抛出一些异步逻辑出错的情况。可以传递一个函数,会接收两个参数,第一个为错误的类型,第二个为错误对象 | |
| disableMouseWheelZoomv0.6.15+ | Boolean | false | 禁止鼠标滚轮缩放你仍旧可以使用api进行缩放 | |
### 水印配置
@@ -318,8 +321,8 @@ mindMap.updateConfig({
| SELECT_ALL | 全选 | |
| BACK | 回退指定的步数 | step要回退的步数默认为1 |
| FORWARD | 前进指定的步数 | step要前进的步数默认为1 |
| INSERT_NODE | 插入同级节点,操作节点为当前激活的节点或指定节点,如果有多个激活节点,只会对第一个有效 | openEditv0.4.6+,是否激活新插入的节点并进入编辑模式,默认为`true`)、 appointNodesv0.4.7+,可选,指定节点,指定多个节点可以传一个数组)、 appointData可选指定新创建节点的数据比如{text: 'xxx', ...},详细结构可以参考[exampleData.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js) |
| INSERT_CHILD_NODE | 插入子节点,操作节点为当前激活的节点或指定节点 | openEditv0.4.6+,是否激活新插入的节点并进入编辑模式,默认为`true`)、 appointNodesv0.4.7+,可选,指定节点,指定多个节点可以传一个数组)、 appointData可选指定新创建节点的数据比如{text: 'xxx', ...},详细结构可以参考[exampleData.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js) |
| INSERT_NODE | 插入同级节点,操作节点为当前激活的节点或指定节点,如果有多个激活节点,只会对第一个有效 | openEditv0.4.6+,是否激活新插入的节点并进入编辑模式,默认为`true`)、 appointNodesv0.4.7+,可选,指定节点,指定多个节点可以传一个数组)、 appointData可选指定新创建节点的数据比如{text: 'xxx', ...},详细结构可以参考[exampleData.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js)、 appointChildrenv0.6.14+,可选,指定新创建节点的子节点,数组类型) |
| INSERT_CHILD_NODE | 插入子节点,操作节点为当前激活的节点或指定节点 | openEditv0.4.6+,是否激活新插入的节点并进入编辑模式,默认为`true`)、 appointNodesv0.4.7+,可选,指定节点,指定多个节点可以传一个数组)、 appointData可选指定新创建节点的数据比如{text: 'xxx', ...},详细结构可以参考[exampleData.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js)、 appointChildrenv0.6.14+,可选,指定新创建节点的子节点,数组类型) |
| UP_NODE | 上移节点,操作节点为当前激活的节点,如果有多个激活节点,只会对第一个有效,对根节点或在列表里的第一个节点使用无效 | |
| DOWN_NODE | 操作节点为当前激活的节点,如果有多个激活节点,只会对第一个有效,对根节点或在列表里的最后一个节点使用无效 | |
| REMOVE_NODE | 删除节点,操作节点为当前激活的节点或指定节点 | appointNodesv0.4.7+,可选,指定节点,指定多个节点可以传一个数组) |

View File

@@ -372,7 +372,7 @@
<td></td>
</tr>
<tr>
<td>enableCreateHiddenInputv0.6.13+</td>
<td>enableCreateHiddenInputv0.6.13+v0.6.14+版本已去除该特性</td>
<td>Boolean</td>
<td>true</td>
<td>是否允许创建一个隐藏的输入框该输入框会在节点激活时聚焦用于粘贴数据和自动进入文本编辑状态</td>
@@ -382,7 +382,7 @@
<td>enableAutoEnterTextEditWhenKeydownv0.6.13+</td>
<td>Boolean</td>
<td>true</td>
<td>是否在存在一个激活节点时当按下中文英文数字按键时自动进入文本编辑模式该配置在enableCreateHiddenInput设为true时生效</td>
<td>是否在存在一个激活节点时当按下中文英文数字按键时自动进入文本编辑模式</td>
<td></td>
</tr>
<tr>
@@ -392,6 +392,27 @@
<td>设置富文本节点编辑框和节点大小一致形成伪原地编辑的效果需要注意的是只有当节点内只有文本且形状是矩形才会有比较好的效果</td>
<td></td>
</tr>
<tr>
<td>customHandleClipboardTextv0.6.14+</td>
<td>Function</td>
<td>null</td>
<td>自定义对剪贴板文本的处理当按ctrl+v粘贴时会读取用户剪贴板中的文本和图片默认只会判断文本是否是普通文本和simple-mind-map格式的节点数据如果你想处理其他思维导图的数据比如processonzhixi等那么可以传递一个函数接受当前剪贴板中的文本为参数返回处理后的数据可以返回两种类型1.返回一个纯文本那么会直接以该文本创建一个子节点2.返回一个节点对象格式如下{ simpleMindMap: true, data: { data: { text: '' }, children: [] } }代表是simple-mind-map格式的数据节点数据同simple-mind-map节点数据格式如果你的处理逻辑存在异步逻辑也可以返回一个promise</td>
<td></td>
</tr>
<tr>
<td>errorHandlerv0.6.15+</td>
<td>Function</td>
<td></td>
<td>自定义错误处理函数目前只会抛出一些异步逻辑出错的情况可以传递一个函数会接收两个参数第一个为错误的类型第二个为错误对象</td>
<td></td>
</tr>
<tr>
<td>disableMouseWheelZoomv0.6.15+</td>
<td>Boolean</td>
<td>false</td>
<td>禁止鼠标滚轮缩放你仍旧可以使用api进行缩放</td>
<td></td>
</tr>
</tbody>
</table>
<h3>水印配置</h3>
@@ -818,12 +839,12 @@ mindMap.setTheme(<span class="hljs-string">&#x27;主题名称&#x27;</span>)
<tr>
<td>INSERT_NODE</td>
<td>插入同级节点操作节点为当前激活的节点或指定节点如果有多个激活节点只会对第一个有效</td>
<td>openEditv0.4.6+是否激活新插入的节点并进入编辑模式默认为<code>true</code> appointNodesv0.4.7+可选指定节点指定多个节点可以传一个数组 appointData可选指定新创建节点的数据比如{text: 'xxx', ...}详细结构可以参考<a href="https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js">exampleData.js</a></td>
<td>openEditv0.4.6+是否激活新插入的节点并进入编辑模式默认为<code>true</code> appointNodesv0.4.7+可选指定节点指定多个节点可以传一个数组 appointData可选指定新创建节点的数据比如{text: 'xxx', ...}详细结构可以参考<a href="https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js">exampleData.js</a> appointChildrenv0.6.14+可选指定新创建节点的子节点数组类型</td>
</tr>
<tr>
<td>INSERT_CHILD_NODE</td>
<td>插入子节点操作节点为当前激活的节点或指定节点</td>
<td>openEditv0.4.6+是否激活新插入的节点并进入编辑模式默认为<code>true</code> appointNodesv0.4.7+可选指定节点指定多个节点可以传一个数组 appointData可选指定新创建节点的数据比如{text: 'xxx', ...}详细结构可以参考<a href="https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js">exampleData.js</a></td>
<td>openEditv0.4.6+是否激活新插入的节点并进入编辑模式默认为<code>true</code> appointNodesv0.4.7+可选指定节点指定多个节点可以传一个数组 appointData可选指定新创建节点的数据比如{text: 'xxx', ...}详细结构可以参考<a href="https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js">exampleData.js</a> appointChildrenv0.6.14+可选指定新创建节点的子节点数组类型</td>
</tr>
<tr>
<td>UP_NODE</td>

View File

@@ -38,12 +38,14 @@ a.download = 'xxx'
a.click()
```
### png(name, transparent = false)
### png(name, transparent = false, rotateWhenWidthLongerThenHeight)
- `name`:名称,可不传
- `transparent`v0.5.7+,指定导出图片的背景是否是透明的
- `rotateWhenWidthLongerThenHeight`: v0.6.15+Boolean, false, 是否在图片宽比高长时自动旋转90度
导出为`png`
### svg(name, plusCssText)
@@ -66,11 +68,13 @@ svg(
导出为`svg`
### pdf(name)
### pdf(name, useMultiPageExport)
> v0.2.1+
`name`:文件名称
- `name`:文件名称
- `useMultiPageExport`: v0.6.15+Boolean, false, 是否多页导出,默认为单页
导出为`pdf`,和其他导出方法不一样,这个方法不会返回数据,会直接触发下载。

View File

@@ -27,7 +27,7 @@ a.href = <span class="hljs-string">&#x27;xxx.png&#x27;</span><span class="hljs-c
a.download = <span class="hljs-string">&#x27;xxx&#x27;</span>
a.click()
</code></pre>
<h3>png(name, transparent = false)</h3>
<h3>png(name, transparent = false, rotateWhenWidthLongerThenHeight)</h3>
<ul>
<li>
<p><code>name</code>名称可不传</p>
@@ -35,6 +35,9 @@ a.click()
<li>
<p><code>transparent</code>v0.5.7+指定导出图片的背景是否是透明的</p>
</li>
<li>
<p><code>rotateWhenWidthLongerThenHeight</code>: v0.6.15+Boolean, false, 是否在图片宽比高长时自动旋转90度</p>
</li>
</ul>
<p>导出为<code>png</code></p>
<h3>svg(name, plusCssText)</h3>
@@ -57,11 +60,18 @@ a.click()
)
</code></pre>
<p>导出为<code>svg</code></p>
<h3>pdf(name)</h3>
<h3>pdf(name, useMultiPageExport)</h3>
<blockquote>
<p>v0.2.1+</p>
</blockquote>
<ul>
<li>
<p><code>name</code>文件名称</p>
</li>
<li>
<p><code>useMultiPageExport</code>: v0.6.15+Boolean, false, 是否多页导出默认为单页</p>
</li>
</ul>
<p>导出为<code>pdf</code>和其他导出方法不一样这个方法不会返回数据会直接触发下载</p>
<blockquote>
<p>v0.6.0版本以后需要额外注册一个ExportPDF插件</p>

View File

@@ -18,6 +18,8 @@
`v0.6.13+`版本使用[dom-to-image-more](https://github.com/1904labs/dom-to-image-more)替换了`html2canvas`,解决了节点的颜色导出后不生效的问题。
> dom-to-image-more兼容性比较差在很多浏览器上导出图片都是空的所以可以根据你自己的需求替换成html2canvas。
## 注册
```js

View File

@@ -18,6 +18,9 @@
<p><code>v0.5.7+</code>的版本直接使用<code>html2canvas</code>转换整个<code>svg</code>速度不再是问题但是目前存在一个<code>bug</code>就是节点的颜色导出后不生效</p>
</blockquote>
<p><code>v0.6.13+</code>版本使用<a href="https://github.com/1904labs/dom-to-image-more">dom-to-image-more</a>替换了<code>html2canvas</code>解决了节点的颜色导出后不生效的问题</p>
<blockquote>
<p>dom-to-image-more兼容性比较差在很多浏览器上导出图片都是空的所以可以根据你自己的需求替换成html2canvas</p>
</blockquote>
<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> RichText <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;simple-mind-map/src/plugins/RichText.js&#x27;</span>

View File

@@ -137,6 +137,12 @@
:label="item"
:value="item"
>
<span
v-if="item > 0"
class="borderLine"
:class="{ isDark: isDark }"
:style="{ height: item + 'px' }"
></span>
</el-option>
</el-select>
</div>
@@ -160,6 +166,12 @@
:key="item.value"
:label="item.name"
:value="item.value"
class="lineStyleOption"
:class="{
isDark: isDark,
isSelected: style.lineStyle === item.value
}"
v-html="lineStyleMap[item.value]"
>
</el-option>
</el-select>
@@ -227,6 +239,12 @@
:label="item"
:value="item"
>
<span
v-if="item > 0"
class="borderLine"
:class="{ isDark: isDark }"
:style="{ height: item + 'px' }"
></span>
</el-option>
</el-select>
</div>
@@ -271,13 +289,21 @@
:label="item"
:value="item"
>
<span
v-if="item > 0"
class="borderLine"
:class="{ isDark: isDark }"
:style="{ height: item + 'px' }"
></span>
</el-option>
</el-select>
</div>
</div>
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('baseStyle.associativeLineActiveColor') }}</span>
<span class="name">{{
$t('baseStyle.associativeLineActiveColor')
}}</span>
<span
class="block"
v-popover:popover5
@@ -295,7 +321,9 @@
</el-popover>
</div>
<div class="rowItem">
<span class="name">{{ $t('baseStyle.associativeLineActiveWidth') }}</span>
<span class="name">{{
$t('baseStyle.associativeLineActiveWidth')
}}</span>
<el-select
size="mini"
style="width: 80px"
@@ -313,6 +341,12 @@
:label="item"
:value="item"
>
<span
v-if="item > 0"
class="borderLine"
:class="{ isDark: isDark }"
:style="{ height: item + 'px' }"
></span>
</el-option>
</el-select>
</div>
@@ -382,11 +416,15 @@
<div class="title noTop">{{ $t('baseStyle.nodeBorderType') }}</div>
<div class="row">
<div class="rowItem">
<el-checkbox v-model="style.nodeUseLineStyle" @change="
<el-checkbox
v-model="style.nodeUseLineStyle"
@change="
value => {
update('nodeUseLineStyle', value)
}
">{{ $t('baseStyle.nodeUseLineStyle') }}</el-checkbox>
"
>{{ $t('baseStyle.nodeUseLineStyle') }}</el-checkbox
>
</div>
</div>
<!-- 内边距 -->
@@ -520,7 +558,11 @@
<div class="row">
<!-- 是否显示水印 -->
<div class="rowItem">
<el-checkbox v-model="watermarkConfig.show" @change="watermarkShowChange">{{ $t('baseStyle.showWatermark') }}</el-checkbox>
<el-checkbox
v-model="watermarkConfig.show"
@change="watermarkShowChange"
>{{ $t('baseStyle.showWatermark') }}</el-checkbox
>
</div>
</div>
<template v-if="watermarkConfig.show">
@@ -528,7 +570,12 @@
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('baseStyle.watermarkText') }}</span>
<el-input v-model="watermarkConfig.text" size="small" @change="updateWatermarkConfig"></el-input>
<el-input
v-model="watermarkConfig.text"
size="small"
@change="updateWatermarkConfig"
@keydown.native.stop
></el-input>
</div>
</div>
<!-- 水印文字颜色 -->
@@ -543,10 +590,12 @@
<el-popover ref="popover3" placement="bottom" trigger="click">
<Color
:color="watermarkConfig.textStyle.color"
@change="(value) => {
watermarkConfig.textStyle.color = value
updateWatermarkConfig()
}"
@change="
value => {
watermarkConfig.textStyle.color = value
updateWatermarkConfig()
}
"
></Color>
</el-popover>
</div>
@@ -555,35 +604,72 @@
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('baseStyle.watermarkTextOpacity') }}</span>
<el-slider v-model="watermarkConfig.textStyle.opacity" style="width: 170px" :min="0" :max="1" :step="0.1" @change="updateWatermarkConfig"></el-slider>
<el-slider
v-model="watermarkConfig.textStyle.opacity"
style="width: 170px"
:min="0"
:max="1"
:step="0.1"
@change="updateWatermarkConfig"
></el-slider>
</div>
</div>
<!-- 水印文字字号 -->
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('baseStyle.watermarkTextFontSize') }}</span>
<el-input-number v-model="watermarkConfig.textStyle.fontSize" size="small" :min="0" :max="50" :step="1" @change="updateWatermarkConfig"></el-input-number>
<span class="name">{{
$t('baseStyle.watermarkTextFontSize')
}}</span>
<el-input-number
v-model="watermarkConfig.textStyle.fontSize"
size="small"
:min="0"
:max="50"
:step="1"
@change="updateWatermarkConfig"
@keydown.native.stop
></el-input-number>
</div>
</div>
<!-- 旋转角度 -->
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('baseStyle.watermarkAngle') }}</span>
<el-input-number v-model="watermarkConfig.angle" size="small" :min="0" :max="90" :step="10" @change="updateWatermarkConfig"></el-input-number>
<el-input-number
v-model="watermarkConfig.angle"
size="small"
:min="0"
:max="90"
:step="10"
@change="updateWatermarkConfig"
@keydown.native.stop
></el-input-number>
</div>
</div>
<!-- 水印行间距 -->
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('baseStyle.watermarkLineSpacing') }}</span>
<el-input-number v-model="watermarkConfig.lineSpacing" size="small" :step="10" @change="updateWatermarkConfig"></el-input-number>
<el-input-number
v-model="watermarkConfig.lineSpacing"
size="small"
:step="10"
@change="updateWatermarkConfig"
@keydown.native.stop
></el-input-number>
</div>
</div>
<!-- 水印文字间距 -->
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('baseStyle.watermarkTextSpacing') }}</span>
<el-input-number v-model="watermarkConfig.textSpacing" size="small" :step="10" @change="updateWatermarkConfig"></el-input-number>
<el-input-number
v-model="watermarkConfig.textSpacing"
size="small"
:step="10"
@change="updateWatermarkConfig"
@keydown.native.stop
></el-input-number>
</div>
</div>
</template>
@@ -592,17 +678,25 @@
<!-- 配置开启自由拖拽 -->
<div class="row">
<div class="rowItem">
<el-checkbox v-model="config.enableFreeDrag" @change="
<el-checkbox
v-model="config.enableFreeDrag"
@change="
value => {
updateOtherConfig('enableFreeDrag', value)
}
">{{ $t('baseStyle.enableFreeDrag') }}</el-checkbox>
"
>{{ $t('baseStyle.enableFreeDrag') }}</el-checkbox
>
</div>
</div>
<!-- 配置是否启用富文本编辑 -->
<div class="row">
<div class="rowItem">
<el-checkbox v-model="enableNodeRichText" @change="enableNodeRichTextChange">{{ $t('baseStyle.isEnableNodeRichText') }}</el-checkbox>
<el-checkbox
v-model="enableNodeRichText"
@change="enableNodeRichTextChange"
>{{ $t('baseStyle.isEnableNodeRichText') }}</el-checkbox
>
</div>
</div>
<!-- 配置鼠标滚轮行为 -->
@@ -620,15 +714,23 @@
}
"
>
<el-option :label="$t('baseStyle.zoomView') " value="zoom"></el-option>
<el-option :label="$t('baseStyle.moveViewUpDown') " value="move"></el-option>
<el-option
:label="$t('baseStyle.zoomView')"
value="zoom"
></el-option>
<el-option
:label="$t('baseStyle.moveViewUpDown')"
value="move"
></el-option>
</el-select>
</div>
</div>
<!-- 配置鼠标缩放行为 -->
<div class="row" v-if="config.mousewheelAction === 'zoom'">
<div class="rowItem">
<span class="name">{{ $t('baseStyle.mousewheelZoomActionReverse') }}</span>
<span class="name">{{
$t('baseStyle.mousewheelZoomActionReverse')
}}</span>
<el-select
size="mini"
style="width: 120px"
@@ -640,8 +742,14 @@
}
"
>
<el-option :label="$t('baseStyle.mousewheelZoomActionReverse1') " :value="false"></el-option>
<el-option :label="$t('baseStyle.mousewheelZoomActionReverse2') " :value="true"></el-option>
<el-option
:label="$t('baseStyle.mousewheelZoomActionReverse1')"
:value="false"
></el-option>
<el-option
:label="$t('baseStyle.mousewheelZoomActionReverse2')"
:value="true"
></el-option>
</el-select>
</div>
</div>
@@ -652,7 +760,17 @@
<script>
import Sidebar from './Sidebar'
import Color from './Color'
import { lineWidthList, lineStyleList, backgroundRepeatList, backgroundPositionList, backgroundSizeList, fontFamilyList, fontSizeList, rootLineKeepSameInCurveList } from '@/config'
import {
lineWidthList,
lineStyleList,
backgroundRepeatList,
backgroundPositionList,
backgroundSizeList,
fontFamilyList,
fontSizeList,
rootLineKeepSameInCurveList,
lineStyleMap
} from '@/config'
import ImgUpload from '@/components/ImgUpload'
import { storeConfig } from '@/api'
import { mapState, mapMutations } from 'vuex'
@@ -740,13 +858,18 @@ export default {
return lineStyleList[this.$i18n.locale] || lineStyleList.zh
},
rootLineKeepSameInCurveList() {
return rootLineKeepSameInCurveList[this.$i18n.locale] || rootLineKeepSameInCurveList.zh
return (
rootLineKeepSameInCurveList[this.$i18n.locale] ||
rootLineKeepSameInCurveList.zh
)
},
backgroundRepeatList() {
return backgroundRepeatList[this.$i18n.locale] || backgroundRepeatList.zh
},
backgroundPositionList() {
return backgroundPositionList[this.$i18n.locale] || backgroundPositionList.zh
return (
backgroundPositionList[this.$i18n.locale] || backgroundPositionList.zh
)
},
backgroundSizeList() {
return backgroundSizeList[this.$i18n.locale] || backgroundSizeList.zh
@@ -754,6 +877,9 @@ export default {
fontFamilyList() {
return fontFamilyList[this.$i18n.locale] || fontFamilyList.zh
},
lineStyleMap() {
return lineStyleMap[this.$i18n.locale] || lineStyleMap.zh
}
},
watch: {
activeSidebar(val) {
@@ -767,14 +893,25 @@ export default {
}
}
},
created () {
created() {
this.enableNodeRichText = this.localConfig.openNodeRichText
this.mousewheelAction = this.localConfig.mousewheelAction
this.mousewheelZoomActionReverse = this.localConfig.mousewheelZoomActionReverse
this.$bus.$on('setData', this.onSetData)
},
beforeDestroy() {
this.$bus.$off('setData', this.onSetData)
},
methods: {
...mapMutations(['setLocalConfig']),
onSetData() {
if (this.activeSidebar !== 'baseStyle') return
setTimeout(() => {
this.initStyle()
}, 0)
},
/**
* @Author: 王林
* @Date: 2021-05-05 14:02:12
@@ -817,7 +954,11 @@ export default {
// 初始化其他配置
initConfig() {
;['enableFreeDrag', 'mousewheelAction', 'mousewheelZoomActionReverse'].forEach(key => {
;[
'enableFreeDrag',
'mousewheelAction',
'mousewheelZoomActionReverse'
].forEach(key => {
this.config[key] = this.mindMap.getConfig(key)
})
},
@@ -825,7 +966,7 @@ export default {
// 初始化水印配置
initWatermark() {
let config = this.mindMap.getConfig('watermarkConfig')
;['text', 'lineSpacing', 'textSpacing', 'angle'].forEach((key) => {
;['text', 'lineSpacing', 'textSpacing', 'angle'].forEach(key => {
this.watermarkConfig[key] = config[key]
})
this.watermarkConfig.show = !!config.text
@@ -883,16 +1024,18 @@ export default {
updateWatermarkConfig() {
clearTimeout(this.updateWatermarkTimer)
this.updateWatermarkTimer = setTimeout(() => {
let {show, ...config} = this.watermarkConfig
let { show, ...config } = this.watermarkConfig
this.mindMap.watermark.updateWatermark({
...config
})
this.data.config = this.data.config || {}
this.data.config.watermarkConfig = this.mindMap.getConfig('watermarkConfig')
this.data.config.watermarkConfig = this.mindMap.getConfig(
'watermarkConfig'
)
storeConfig({
config: this.data.config
})
}, 300);
}, 300)
},
/**
@@ -918,7 +1061,8 @@ export default {
// 切换显示水印与否
watermarkShowChange(value) {
if (value) {
let text = this.watermarkConfig.text || this.$t('baseStyle.watermarkDefaultText')
let text =
this.watermarkConfig.text || this.$t('baseStyle.watermarkDefaultText')
this.watermarkConfig.text = text
} else {
this.watermarkConfig.text = ''
@@ -940,7 +1084,7 @@ export default {
mousewheelAction: e
})
this.mindMap.updateConfig
},
}
}
}
</script>
@@ -958,7 +1102,7 @@ export default {
.row {
.rowItem {
.name {
color: hsla(0,0%,100%,.6);
color: hsla(0, 0%, 100%, 0.6);
}
}
}
@@ -1048,4 +1192,47 @@ export default {
}
}
}
.borderLine {
display: inline-block;
width: 100%;
background-color: #000;
&.isDark {
background-color: #fff;
}
}
</style>
<style lang="less">
.el-select-dropdown__item.selected {
.borderLine {
background-color: #409eff;
}
}
.lineStyleOption {
&.isDark {
svg {
path {
stroke: #fff;
}
}
}
&.isSelected {
svg {
path {
stroke: #409eff;
}
}
}
svg {
margin-top: 4px;
path {
stroke: #000;
}
}
}
</style>

View File

@@ -2,9 +2,10 @@
<div class="colorContainer" :class="{ isDark: isDark }">
<div class="colorList">
<span
class="colorItem"
class="colorItem iconfont"
v-for="item in colorList"
:style="{ backgroundColor: item }"
:class="{ icontouming: item === 'transparent' }"
:key="item"
@click="clickColorItem(item)"
></span>
@@ -87,9 +88,13 @@ export default {
.colorList {
width: 240px;
display: flex;
flex-wrap: wrap;
.colorItem {
display: inline-block;
display: flex;
justify-content: center;
align-items: center;
width: 15px;
height: 15px;
margin-right: 5px;

View File

@@ -70,6 +70,12 @@
{{ $t('contextmenu.pasteNode') }}
<span class="desc">Ctrl + V</span>
</div>
<div class="item" @click="exec('REMOVE_HYPERLINK')" v-if="hasHyperlink">
{{ $t('contextmenu.removeHyperlink') }}
</div>
<div class="item" @click="exec('REMOVE_NOTE')" v-if="hasNote">
{{ $t('contextmenu.removeNote') }}
</div>
</template>
<template v-if="type === 'svg'">
<div class="item" @click="exec('RETURN_CENTER')">
@@ -180,6 +186,12 @@ export default {
},
isGeneralization() {
return this.node.isGeneralization
},
hasHyperlink() {
return !!this.node.getData('hyperlink')
},
hasNote() {
return !!this.node.getData('note')
}
},
created() {
@@ -302,6 +314,12 @@ export default {
case 'FIT_CANVAS':
this.mindMap.view.fit()
break
case 'REMOVE_HYPERLINK':
this.node.setHyperlink('', '')
break
case 'REMOVE_NOTE':
this.node.setNote('')
break
default:
this.$bus.$emit('execCommand', key, ...args)
break

View File

@@ -23,7 +23,11 @@ import { mapState } from 'vuex'
let countEl = document.createElement('div')
export default {
name: 'Count',
props: {},
props: {
mindMap: {
type: Object
}
},
data() {
return {
textStr: '',
@@ -36,6 +40,9 @@ export default {
},
created() {
this.$bus.$on('data_change', this.onDataChange)
if (this.mindMap) {
this.onDataChange(this.mindMap.getData())
}
},
beforeDestroy() {
this.$bus.$off('data_change', this.onDataChange)

View File

@@ -1,7 +1,7 @@
<template>
<div class="editContainer">
<div class="mindMapContainer" ref="mindMapContainer"></div>
<Count v-if="!isZenMode"></Count>
<Count :mindMap="mindMap" v-if="!isZenMode"></Count>
<Navigator :mindMap="mindMap"></Navigator>
<NavigatorToolbar :mindMap="mindMap" v-if="!isZenMode"></NavigatorToolbar>
<OutlineSidebar :mindMap="mindMap"></OutlineSidebar>
@@ -70,6 +70,7 @@ import NodeIconSidebar from './NodeIconSidebar.vue'
import NodeIconToolbar from './NodeIconToolbar.vue'
import OutlineEdit from './OutlineEdit.vue'
import { showLoading, hideLoading } from '@/utils/loading'
import handleClipboardText from '@/utils/handleClipboardText'
// 注册插件
MindMap.usePlugin(MiniMap)
@@ -275,7 +276,9 @@ export default {
...(config || {}),
iconList: icon,
useLeftKeySelectionRightKeyDrag: this.useLeftKeySelectionRightKeyDrag,
customInnerElsAppendTo: null
customInnerElsAppendTo: null,
enableAutoEnterTextEditWhenKeydown: true,
customHandleClipboardText: handleClipboardText,
// isUseCustomNodeContent: true,
// 示例1组件里用到了router、store、i18n等实例化vue组件时需要用到的东西
// customCreateNodeContent: (node) => {
@@ -300,7 +303,24 @@ export default {
// })
// comp.$mount(el)
// return comp.$el
// }
// },
// 示例3普通元素
// customCreateNodeContent: (node) => {
// let el = document.createElement('div')
// el.style.cssText = `
// width: 203px;
// height: 78px;
// opacity: 0.8;
// background-image: linear-gradient(0deg, rgba(53,130,172,0.06) 0%, rgba(24,75,116,0.06) 100%);
// box-shadow: inset 0 1px 15px 0 rgba(119,196,255,0.40);
// border-radius: 2px;
// display: flex;
// justify-content: center;
// align-items: center;
// `
// el.innerHTML = node.nodeData.data.text
// return el
// },
})
if (this.openNodeRichText) this.addRichTextPlugin()
this.mindMap.keyCommand.addShortcut('Control+s', () => {

View File

@@ -16,6 +16,7 @@
style="width: 300px"
v-model="fileName"
size="mini"
@keydown.native.stop
></el-input>
<el-checkbox
v-show="['smm', 'json'].includes(exportType)"
@@ -34,6 +35,7 @@
v-model="paddingX"
size="mini"
@change="onPaddingChange"
@keydown.native.stop
></el-input>
<span class="name" style="margin-left: 10px;">{{
$t('export.paddingY')
@@ -43,6 +45,7 @@
v-model="paddingY"
size="mini"
@change="onPaddingChange"
@keydown.native.stop
></el-input>
<el-checkbox
v-show="['png'].includes(exportType)"
@@ -50,6 +53,12 @@
style="margin-left: 12px"
>{{ $t('export.isTransparent') }}</el-checkbox
>
<el-checkbox
v-show="['pdf'].includes(exportType)"
v-model="useMultiPageExport"
style="margin-left: 12px"
>{{ $t('export.useMultiPageExport') }}</el-checkbox
>
</div>
<div class="downloadTypeList">
<div
@@ -98,7 +107,8 @@ export default {
loading: false,
loadingText: '',
paddingX: 10,
paddingY: 10
paddingY: 10,
useMultiPageExport: false
}
},
computed: {
@@ -172,6 +182,8 @@ export default {
this.fileName,
this.isTransparent
)
} else if (this.exportType === 'pdf') {
this.$bus.$emit('export', this.exportType, true, this.fileName, this.useMultiPageExport)
} else {
this.$bus.$emit('export', this.exportType, true, this.fileName)
}

View File

@@ -127,7 +127,7 @@ export default {
height: 220px;
background-color: #fff;
bottom: 80px;
right: 20px;
right: 70px;
box-shadow: 0 0 16px #989898;
border-radius: 4px;
border: 1px solid #eee;

View File

@@ -72,9 +72,16 @@
></div>
</div>
<div class="item">
<a href="https://github.com/wanglin2/mind-map" target="_blank">
<span class="iconfont icongithub"></span>
</a>
<el-dropdown @command="handleCommand">
<div class="btn iconfont iconbangzhu"></div>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="github">Github</el-dropdown-item>
<el-dropdown-item command="helpDoc">使用文档</el-dropdown-item>
<el-dropdown-item command="devDoc">开发文档</el-dropdown-item>
<el-dropdown-item command="site">官方网站</el-dropdown-item>
<el-dropdown-item command="issue">意见反馈</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</template>
@@ -116,7 +123,7 @@ export default {
computed: {
...mapState(['isDark'])
},
created () {
created() {
this.lang = getLang()
},
methods: {
@@ -143,6 +150,33 @@ export default {
toggleDark() {
this.setIsDark(!this.isDark)
},
handleCommand(command) {
let url = ''
switch (command) {
case 'github':
url = 'https://github.com/wanglin2/mind-map'
break
case 'helpDoc':
url = 'https://wanglin2.github.io/mind-map/#/help/zh/'
break
case 'devDoc':
url = 'https://wanglin2.github.io/mind-map/#/doc/zh/introduction/'
break
case 'site':
url = 'https://wanglin2.github.io/mind-map/#/index'
break
case 'issue':
url = 'https://github.com/wanglin2/mind-map/issues/new'
break
default:
break
}
const a = document.createElement('a')
a.href = url
a.target = '_blank'
a.click()
}
}
}

View File

@@ -12,11 +12,17 @@
size="mini"
placeholder="http://xxxx.com/"
@keyup.native.stop
@keydown.native.stop
></el-input>
</div>
<div class="item">
<span class="name">{{ $t('nodeHyperlink.name') }}</span>
<el-input v-model="linkTitle" size="mini" @keyup.native.stop></el-input>
<el-input
v-model="linkTitle"
size="mini"
@keyup.native.stop
@keydown.native.stop
></el-input>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="cancel">{{ $t('dialog.cancel') }}</el-button>

View File

@@ -18,12 +18,13 @@
v-model="imgUrl"
size="mini"
placeholder="http://xxx.com/xx.jpg"
@keydown.native.stop
></el-input>
</div>
<div class="title">可选</div>
<div class="inputBox">
<span class="label">{{ $t('nodeImage.imgTitle') }}</span>
<el-input v-model="imgTitle" size="mini"></el-input>
<el-input v-model="imgTitle" size="mini" @keydown.native.stop></el-input>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="cancel">{{ $t('dialog.cancel') }}</el-button>
@@ -100,7 +101,14 @@ export default {
async confirm() {
try {
if (!this.img && !this.imgUrl) return
// 删除图片
if (!this.img && !this.imgUrl) {
this.cancel()
this.activeNodes.forEach(node => {
node.setImage(null)
})
return
}
let res = null
let img = ''
if (this.img) {

View File

@@ -12,7 +12,7 @@
v-model="note"
>
</el-input> -->
<div class="noteEditor" ref="noteEditor" @keyup.stop></div>
<div class="noteEditor" ref="noteEditor" @keyup.stop @keydown.stop></div>
<!-- <div class="tip">换行请使用Enter+Shift</div> -->
<span slot="footer" class="dialog-footer">
<el-button @click="cancel">{{ $t('dialog.cancel') }}</el-button>

View File

@@ -9,6 +9,7 @@
v-model="tag"
@keyup.native.enter="add"
@keyup.native.stop
@keydown.native.stop
:disabled="tagArr.length >= max"
:placeholder="$t('nodeTag.addTip')"
>

View File

@@ -69,12 +69,13 @@ export default {
}
},
computed: {
...mapState(['isDark', 'isOutlineEdit'])
...mapState(['isDark'])
},
created() {
window.addEventListener('keydown', this.onKeyDown)
this.$bus.$on('data_change', this.handleDataChange)
this.$bus.$on('node_tree_render_end', this.handleNodeTreeRenderEnd)
this.$bus.$on('hide_text_edit', this.handleHideTextEdit)
},
mounted() {
this.refresh()
@@ -83,8 +84,16 @@ export default {
window.removeEventListener('keydown', this.onKeyDown)
this.$bus.$off('data_change', this.handleDataChange)
this.$bus.$off('node_tree_render_end', this.handleNodeTreeRenderEnd)
this.$bus.$off('hide_text_edit', this.handleHideTextEdit)
},
methods: {
handleHideTextEdit() {
if (this.notHandleDataChange) {
this.notHandleDataChange = false
this.refresh()
}
},
handleDataChange() {
// 在大纲里操作节点时不要响应该事件,否则会重新刷新树
if (this.notHandleDataChange) {
@@ -191,6 +200,7 @@ export default {
const text = richText ? e.target.innerHTML : e.target.innerText
const targetNode = this.mindMap.renderer.findNodeByUid(node.data.uid)
if (!targetNode) return
this.notHandleDataChange = true
if (richText) {
targetNode.setText(textToNodeRichTextWithWrap(text), true, true)
} else {
@@ -258,9 +268,7 @@ export default {
this.notHandleDataChange = true
const targetNode = this.mindMap.renderer.findNodeByUid(data.uid)
if (targetNode && targetNode.nodeData.data.isActive) return
this.mindMap.renderer.textEdit.stopFocusOnNodeActive()
this.mindMap.execCommand('GO_TARGET_NODE', data.uid, () => {
this.mindMap.renderer.textEdit.openFocusOnNodeActive()
this.notHandleDataChange = false
})
},

View File

@@ -10,7 +10,38 @@
</div>
<div class="outlineEditBox" ref="outlineEditBox">
<div class="outlineEdit">
<Outline :mindMap="mindMap" @scrollTo="onScrollTo"></Outline>
<el-tree
ref="tree"
class="outlineTree"
node-key="uid"
draggable
default-expand-all
:class="{ isDark: isDark }"
:data="data"
:props="defaultProps"
:highlight-current="true"
:expand-on-click-node="false"
:allow-drag="checkAllowDrag"
@node-drop="onNodeDrop"
@current-change="onCurrentChange"
>
<span
class="customNode"
slot-scope="{ node, data }"
:data-id="data.uid"
>
<span
class="nodeEdit"
contenteditable="true"
:key="getKey()"
@blur="onBlur($event, node)"
@keydown.stop="onNodeInputKeydown($event, node)"
@keyup.stop
@paste="onPaste($event, node)"
v-html="node.label"
></span>
</span>
</el-tree>
</div>
</div>
</div>
@@ -18,39 +49,190 @@
<script>
import { mapState, mapMutations } from 'vuex'
import Outline from './Outline.vue'
import {
nodeRichTextToTextWithWrap,
textToNodeRichTextWithWrap,
getTextFromHtml,
createUid,
simpleDeepClone
} from 'simple-mind-map/src/utils'
import { storeData } from '@/api'
// 大纲侧边栏
export default {
name: 'OutlineEdit',
components: {
Outline
},
props: {
mindMap: {
type: Object
}
},
data() {
return {
data: [],
defaultProps: {
label: 'label'
},
currentData: null
}
},
computed: {
...mapState(['isOutlineEdit', 'isDark'])
},
watch: {
isOutlineEdit(val) {
if (val) {
this.refresh()
this.$nextTick(() => {
document.body.appendChild(this.$refs.outlineEditContainer)
})
}
}
},
created() {
window.addEventListener('keydown', this.onKeyDown)
},
beforeDestroy() {
window.removeEventListener('keydown', this.onKeyDown)
},
methods: {
...mapMutations(['setIsOutlineEdit']),
onClose() {
this.setIsOutlineEdit(false)
// 刷新树数据
refresh() {
let data = this.mindMap.getData()
data.root = true // 标记根节点
let walk = root => {
const text = (root.data.richText
? nodeRichTextToTextWithWrap(root.data.text)
: root.data.text
).replaceAll(/\n/g, '<br>')
root.textCache = text // 保存一份修改前的数据,用于对比是否修改了
root.label = text
root.uid = root.data.uid
if (root.children && root.children.length > 0) {
root.children.forEach(item => {
walk(item)
})
}
}
walk(data)
this.data = [data]
},
onScrollTo(y) {
// 根节点不允许拖拽
checkAllowDrag(node) {
return !node.data.root
},
// 拖拽结束事件
onNodeDrop() {
this.save()
},
// 当前选中的树节点变化事件
onCurrentChange(data) {
this.currentData = data
},
// 失去焦点更新节点文本
onBlur(e, node) {
// 节点数据没有修改
if (node.data.textCache === e.target.innerHTML) {
return
}
const richText = node.data.data.richText
const text = richText ? e.target.innerHTML : e.target.innerText
node.data.data.text = richText ? textToNodeRichTextWithWrap(text) : text
if (richText) node.data.data.resetRichText = true
node.data.textCache = e.target.innerHTML
this.save()
},
// 节点输入区域按键事件
onNodeInputKeydown(e, node) {
const richText = !!node.data.data.richText
const uid = createUid()
const text = this.$t('outline.nodeDefaultText')
const data = {
textCache: text,
uid,
label: text,
data: {
text: richText ? textToNodeRichTextWithWrap(text) : text,
uid,
richText
},
children: []
}
if (richText) {
data.data.resetRichText = true
}
if (e.keyCode === 13 && !e.shiftKey) {
e.preventDefault()
this.$refs.tree.insertAfter(data, node)
}
if (e.keyCode === 9) {
e.preventDefault()
this.$refs.tree.append(data, node)
}
this.save()
this.$nextTick(() => {
this.$refs.tree.setCurrentKey(uid)
const el = document.querySelector(
`.customNode[data-id="${uid}"] .nodeEdit`
)
if (el) {
let selection = window.getSelection()
let range = document.createRange()
range.selectNodeContents(el)
selection.removeAllRanges()
selection.addRange(range)
let offsetTop = el.offsetTop
this.scrollTo(offsetTop)
}
})
},
// 删除节点
onKeyDown(e) {
if (!this.isOutlineEdit) return
if ([46, 8].includes(e.keyCode) && this.currentData) {
e.stopPropagation()
this.$refs.tree.remove(this.currentData)
this.currentData = null
this.save()
}
},
// 拦截粘贴事件
onPaste(e) {
e.preventDefault()
const selection = window.getSelection()
if (!selection.rangeCount) return
selection.deleteFromDocument()
let text = (e.clipboardData || window.clipboardData).getData('text')
// 去除格式
text = getTextFromHtml(text)
// 去除换行
text = text.replaceAll(/\n/g, '')
const node = document.createTextNode(text)
selection.getRangeAt(0).insertNode(node)
selection.collapseToEnd()
},
// 生成唯一的key
getKey() {
return Math.random()
},
// 关闭
onClose() {
this.setIsOutlineEdit(false)
this.$bus.$emit('setData', this.getData())
},
// 滚动
scrollTo(y) {
let container = this.$refs.outlineEditBox
let height = container.offsetHeight
let top = container.scrollTop
@@ -58,6 +240,28 @@ export default {
if (y > top + height) {
container.scrollTo(0, y - height / 2)
}
},
// 获取思维导图数据
getData() {
let newNode = {}
let node = this.data[0]
let walk = (root, newRoot) => {
newRoot.data = root.data
newRoot.children = []
;(root.children || []).forEach(child => {
const newChild = {}
newRoot.children.push(newChild)
walk(child, newChild)
})
}
walk(node, newNode)
return simpleDeepClone(newNode)
},
// 保存
save() {
storeData(this.getData())
}
}
}
@@ -115,4 +319,79 @@ export default {
}
}
}
.customNode {
width: 100%;
color: rgba(0, 0, 0, 0.85);
font-weight: bold;
.nodeEdit {
outline: none;
white-space: normal;
padding-right: 20px;
}
}
.outlineTree {
&.isDark {
background-color: #262a2e;
.customNode {
color: #fff;
}
&.el-tree--highlight-current {
/deep/ .el-tree-node.is-current > .el-tree-node__content {
background-color: hsla(0, 0%, 100%, 0.05) !important;
}
}
/deep/ .el-tree-node__content:hover,
.el-upload-list__item:hover {
background-color: hsla(0, 0%, 100%, 0.02) !important;
}
/deep/ .el-tree-node__content {
.el-tree-node__expand-icon {
color: #fff;
&.is-leaf {
&::after {
background-color: #fff;
}
}
}
}
}
/deep/ .el-tree-node > .el-tree-node__children {
overflow: inherit;
}
/deep/ .el-tree-node__content {
height: auto;
margin: 5px 0;
.el-tree-node__expand-icon {
color: #262a2e;
&.is-leaf {
color: transparent;
position: relative;
&::after {
background-color: #262a2e;
position: absolute;
content: '';
width: 5px;
height: 5px;
border-radius: 50%;
left: 10px;
top: 50%;
transform: translateY(-50%);
}
}
}
}
}
</style>

View File

@@ -14,6 +14,7 @@
size="small"
v-model="searchText"
@keyup.native.enter.stop="onSearchNext"
@keydown.native.stop
>
<i slot="prefix" class="el-input__icon el-icon-search"></i>
<el-button
@@ -35,6 +36,7 @@
size="small"
v-model="replaceText"
style="margin: 12px 0;"
@keydown.native.stop
>
<i slot="prefix" class="el-input__icon el-icon-edit"></i>
<el-button size="small" slot="append" @click="hideReplaceInput">{{

View File

@@ -1,6 +1,10 @@
<template>
<Sidebar ref="sidebar" :title="$t('style.title')">
<div class="styleBox" :class="{ isDark: isDark }" v-if="activeNodes.length > 0">
<div
class="styleBox"
:class="{ isDark: isDark }"
v-if="activeNodes.length > 0"
>
<el-tabs class="tab" v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane :label="$t('style.normal')" name="normal"></el-tab-pane>
<el-tab-pane :label="$t('style.active')" name="active"></el-tab-pane>
@@ -142,6 +146,9 @@
v-model="style.textDecoration"
@change="update('textDecoration')"
>
<el-radio-button label="none">{{
$t('style.none')
}}</el-radio-button>
<el-radio-button label="underline">{{
$t('style.underline')
}}</el-radio-button>
@@ -193,6 +200,17 @@
:label="item.name"
:value="item.value"
>
<svg width="120" height="34">
<line
x1="10"
y1="17"
x2="110"
y2="17"
stroke-width="2"
:stroke="style.borderDasharray === item.value ? '#409eff' : isDark ? '#fff' : '#000'"
:stroke-dasharray="item.value"
></line>
</svg>
</el-option>
</el-select>
</div>
@@ -214,6 +232,12 @@
:label="item"
:value="item"
>
<span
v-if="item > 0"
class="borderLine"
:class="{ isDark: isDark }"
:style="{ height: item + 'px' }"
></span>
</el-option>
</el-select>
</div>
@@ -277,6 +301,14 @@
:label="item.name"
:value="item.value"
>
<svg width="60" height="26" style="margin-top: 5px">
<path
:d="shapeListMap[item.value]"
fill="none"
:stroke="style.shape === item.value ? '#409eff' : isDark ? '#fff' : '#000'"
stroke-width="2"
></path>
</svg>
</el-option>
</el-select>
</div>
@@ -317,6 +349,17 @@
:label="item.name"
:value="item.value"
>
<svg width="120" height="34">
<line
x1="10"
y1="17"
x2="110"
y2="17"
stroke-width="2"
:stroke="style.lineDasharray === item.value ? '#409eff' : isDark ? '#fff' : '#000'"
:stroke-dasharray="item.value"
></line>
</svg>
</el-option>
</el-select>
</div>
@@ -338,6 +381,12 @@
:label="item"
:value="item"
>
<span
v-if="item > 0"
class="borderLine"
:class="{ isDark: isDark }"
:style="{ height: item + 'px' }"
></span>
</el-option>
</el-select>
</div>
@@ -385,7 +434,8 @@ import {
borderDasharrayList,
borderRadiusList,
lineHeightList,
shapeList
shapeList,
shapeListMap
} from '@/config'
import { supportActiveStyle } from 'simple-mind-map/src/themes/default'
import { mapState } from 'vuex'
@@ -404,10 +454,8 @@ export default {
data() {
return {
supportActiveStyle,
fontSizeList,
borderWidthList,
borderRadiusList,
lineHeightList,
activeNodes: [],
@@ -445,7 +493,10 @@ export default {
},
shapeList() {
return shapeList[this.$i18n.locale] || shapeList.zh
}
},
shapeListMap() {
return shapeListMap[this.$i18n.locale] || shapeListMap.zh
},
},
watch: {
activeSidebar(val) {
@@ -632,7 +683,7 @@ export default {
.row {
.rowItem {
.name {
color: hsla(0,0%,100%,.6);
color: hsla(0, 0%, 100%, 0.6);
}
}
@@ -761,4 +812,21 @@ export default {
}
}
}
.borderLine {
display: inline-block;
width: 100%;
background-color: #000;
&.isDark {
background-color: #fff;
}
}
</style>
<style lang="less">
.el-select-dropdown__item.selected {
.borderLine {
background-color: #409eff;
}
}
</style>

View File

@@ -354,7 +354,7 @@ export default {
title: '提示',
message: `当前正在编辑你本机的【${file.name}】文件`,
duration: 0,
showClose: false
showClose: true
})
}
fileReader.readAsText(file)

View File

@@ -42,11 +42,11 @@ export default {
dataList: [
{
icon: 'iconstar',
value: 'Github star数量450+'
value: 'Github star数量600+'
},
{
icon: 'iconfork',
value: 'Github fork数量100+'
value: 'Github fork数量150+'
},
{
icon: 'iconxiazai',
@@ -54,7 +54,7 @@ export default {
},
{
icon: 'iconteamwork',
value: '代码贡献者6+'
value: '代码贡献者8+'
}
],
functionList: [

View File

@@ -2,9 +2,9 @@
<div class="block5Container">
<div class="blockContent">
<div class="infoBox">
<div class="infoTitle">街角小林出品</div>
<div class="infoTitle">理想青年实验室</div>
<div class="infoDesc">
90六年+前端开发工程师热爱前端写作开源
专注前端写作开源
</div>
<div class="linkBtnList">
<div class="linkBtn">
@@ -52,7 +52,12 @@
<div class="linkTitle">更多作品</div>
<div class="linkList">
<div class="linkItem" v-for="item in linkList" :key="item.name">
<a :href="item.url" target="_blank">{{ item.name }}</a>
<el-tooltip
:content="item.desc"
placement="top"
>
<a :href="item.url" target="_blank">{{ item.name }}</a>
</el-tooltip>
</div>
</div>
</div>
@@ -67,51 +72,63 @@ export default {
linkList: [
{
name: 'CodeRun',
url: 'https://github.com/wanglin2/code-run'
url: 'https://github.com/wanglin2/code-run',
desc: '一个代码在线编辑预览工具类似codepen、jsbin、jsfiddle等'
},
{
name: 'TinyWhiteboard',
url: 'https://github.com/wanglin2/tiny_whiteboard'
url: 'https://github.com/wanglin2/tiny_whiteboard',
desc: '一个在线小白板类似excalidraw'
},
{
name: 'Mark.js',
url: 'https://github.com/wanglin2/markjs'
url: 'https://github.com/wanglin2/markjs',
desc: '一个插件化的多边形标注库'
},
{
name: 'WebMapEngine',
url: 'https://github.com/wanglin2/web_map_demo'
url: 'https://github.com/wanglin2/web_map_demo',
desc: '一个简单的2D地图加载器'
},
{
name: 'SimpleNoviceGuide',
url: 'https://github.com/wanglin2/simple-novice-guide'
url: 'https://github.com/wanglin2/simple-novice-guide',
desc: ' 一个简洁的新手引导库'
},
{
name: 'CanvasEditor',
url: 'https://github.com/wanglin2/canvas-editor-demo'
url: 'https://github.com/wanglin2/canvas-editor-demo',
desc: '一个用Canvas做的富文本编辑器Demo'
},
{
name: 'JsonTreeView',
url: 'https://github.com/wanglin2/json-tree-view'
url: 'https://github.com/wanglin2/json-tree-view',
desc: '一个简洁的json格式化插件'
},
{
name: 'SimpleFlowChart',
url: 'https://github.com/wanglin2/simple-flow-chart'
url: 'https://github.com/wanglin2/simple-flow-chart',
desc: '一个用flex布局做的流程设计器'
},
{
name: 'VideoTimeLine',
url: 'https://github.com/wanglin2/VideoTimeLine'
url: 'https://github.com/wanglin2/VideoTimeLine',
desc: '一个基于Vue2的视频时间轴组件'
},
{
name: 'MarkdownEditor',
url: 'https://github.com/wanglin2/markdown_editor_sync_scroll_demo'
url: 'https://github.com/wanglin2/markdown_editor_sync_scroll_demo',
desc: '一个能精确同步滚动的Markdown编辑器'
},
{
name: 'AssociationLine',
url: 'https://github.com/wanglin2/AssociationLineDemo'
url: 'https://github.com/wanglin2/AssociationLineDemo',
desc: '关联线探究,如何连接流程图的两个节点'
},
{
name: 'HandPaintedStyle',
url: 'https://github.com/wanglin2/handPaintedStyle'
url: 'https://github.com/wanglin2/handPaintedStyle',
desc: '手绘风格图形的简单实现'
}
]
}

View File

@@ -0,0 +1,70 @@
import { imgToDataUrl } from 'simple-mind-map/src/utils/index'
// 处理知犀
const handleZHIXI = async text => {
text = text.replace('￿', '')
try {
// 只处理一项
const node = JSON.parse(text)[0]
const newNode = {}
const waitLoadImageList = []
const walk = async (root, newRoot) => {
newRoot.data = {
text: root.data.text,
hyperlink: root.data.hyperlink,
hyperlinkTitle: root.data.hyperlinkTitle,
note: root.data.note
}
// 图片
if (root.data.image) {
let resolve = null
let promise = new Promise(_resolve => {
resolve = _resolve
})
waitLoadImageList.push(promise)
try {
newRoot.data.image = await imgToDataUrl(root.data.image)
newRoot.data.imageSize = root.data.imageSize
resolve()
} catch (error) {
resolve()
}
}
// 子节点
newRoot.children = []
if (root.children && root.children.length > 0) {
root.children.forEach(item => {
// 概要
if (item.data.type === 'generalize') {
newRoot.data.generalization = {
text: item.data.text
}
return
}
let newChild = {}
newRoot.children.push(newChild)
walk(item, newChild)
})
}
}
walk(node, newNode)
await Promise.all(waitLoadImageList)
return {
simpleMindMap: true,
data: newNode
}
} catch (error) {
return ''
}
}
const handleClipboardText = async text => {
// 处理知犀数据
if (text.includes('￿')) {
const res = await handleZHIXI(text)
return res
}
return ''
}
export default handleClipboardText