Merge branch 'main' into main

This commit is contained in:
街角小林 2023-08-10 09:22:56 +08:00 committed by GitHub
commit 7e7b6fae9d
12 changed files with 248 additions and 78 deletions

View File

@ -1,17 +1,17 @@
{ {
"name": "simple-mind-map", "name": "simple-mind-map",
"version": "0.6.11-fix.1", "version": "0.6.12",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"version": "0.6.11-fix.1", "version": "0.6.12",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@svgdotjs/svg.js": "^3.0.16", "@svgdotjs/svg.js": "^3.0.16",
"deepmerge": "^1.5.2", "deepmerge": "^1.5.2",
"dom-to-image-more": "^3.1.6",
"eventemitter3": "^4.0.7", "eventemitter3": "^4.0.7",
"html2canvas": "^1.4.1",
"jspdf": "^2.5.1", "jspdf": "^2.5.1",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"mdast-util-from-markdown": "^1.3.0", "mdast-util-from-markdown": "^1.3.0",
@ -255,6 +255,7 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"optional": true,
"engines": { "engines": {
"node": ">= 0.6.0" "node": ">= 0.6.0"
} }
@ -411,6 +412,7 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"optional": true,
"dependencies": { "dependencies": {
"utrie": "^1.0.2" "utrie": "^1.0.2"
} }
@ -516,6 +518,11 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/dom-to-image-more": {
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/dom-to-image-more/-/dom-to-image-more-3.1.6.tgz",
"integrity": "sha512-VMO0jNme32T06mWtkOC9QXfj+1npoJxkaTFW0DCwBLguwBKMjqwndiDANxDnbZ0kvNEecwxkv0Zmgdr96cGtAA=="
},
"node_modules/dompurify": { "node_modules/dompurify": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.1.tgz", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.1.tgz",
@ -937,6 +944,7 @@
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"optional": true,
"dependencies": { "dependencies": {
"css-line-break": "^2.1.0", "css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3" "text-segmentation": "^1.0.3"
@ -2142,6 +2150,7 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"optional": true,
"dependencies": { "dependencies": {
"utrie": "^1.0.2" "utrie": "^1.0.2"
} }
@ -2206,6 +2215,7 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"optional": true,
"dependencies": { "dependencies": {
"base64-arraybuffer": "^1.0.2" "base64-arraybuffer": "^1.0.2"
} }
@ -2461,7 +2471,8 @@
"base64-arraybuffer": { "base64-arraybuffer": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==" "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"optional": true
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
@ -2576,6 +2587,7 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"optional": true,
"requires": { "requires": {
"utrie": "^1.0.2" "utrie": "^1.0.2"
} }
@ -2648,6 +2660,11 @@
"esutils": "^2.0.2" "esutils": "^2.0.2"
} }
}, },
"dom-to-image-more": {
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/dom-to-image-more/-/dom-to-image-more-3.1.6.tgz",
"integrity": "sha512-VMO0jNme32T06mWtkOC9QXfj+1npoJxkaTFW0DCwBLguwBKMjqwndiDANxDnbZ0kvNEecwxkv0Zmgdr96cGtAA=="
},
"dompurify": { "dompurify": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.1.tgz", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.1.tgz",
@ -2966,6 +2983,7 @@
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"optional": true,
"requires": { "requires": {
"css-line-break": "^2.1.0", "css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3" "text-segmentation": "^1.0.3"
@ -3749,6 +3767,7 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"optional": true,
"requires": { "requires": {
"utrie": "^1.0.2" "utrie": "^1.0.2"
} }
@ -3800,6 +3819,7 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"optional": true,
"requires": { "requires": {
"base64-arraybuffer": "^1.0.2" "base64-arraybuffer": "^1.0.2"
} }

View File

@ -26,8 +26,8 @@
"dependencies": { "dependencies": {
"@svgdotjs/svg.js": "^3.0.16", "@svgdotjs/svg.js": "^3.0.16",
"deepmerge": "^1.5.2", "deepmerge": "^1.5.2",
"dom-to-image-more": "^3.1.6",
"eventemitter3": "^4.0.7", "eventemitter3": "^4.0.7",
"html2canvas": "^1.4.1",
"jspdf": "^2.5.1", "jspdf": "^2.5.1",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"mdast-util-from-markdown": "^1.3.0", "mdast-util-from-markdown": "^1.3.0",

View File

@ -126,5 +126,10 @@ export const defaultOpt = {
// 指定内部一些元素节点文本编辑元素、节点备注显示元素、关联线文本编辑元素、节点图片调整按钮元素添加到的位置默认添加到document.body下 // 指定内部一些元素节点文本编辑元素、节点备注显示元素、关联线文本编辑元素、节点图片调整按钮元素添加到的位置默认添加到document.body下
customInnerElsAppendTo: null, customInnerElsAppendTo: null,
// 拖拽元素时,指示元素新位置的块的最大高度 // 拖拽元素时,指示元素新位置的块的最大高度
nodeDragPlaceholderMaxSize: 20 nodeDragPlaceholderMaxSize: 20,
// 是否允许创建一个隐藏的输入框,该输入框会在节点激活时聚焦,用于粘贴数据和自动进入文本编辑状态
enableCreateHiddenInput: true,
// 是否在存在一个激活节点时,当按下中文、英文、数字按键时自动进入文本编辑模式
// 该配置在enableCreateHiddenInput设为true时生效
enableAutoEnterTextEditWhenKeydown: true
} }

View File

@ -108,6 +108,11 @@ export default class KeyCommand {
return arr return arr
} }
// 判断是否按下了组合键
hasCombinationKey(e) {
return e.ctrlKey || e.metaKey || e.altKey || e.shiftKey
}
// 获取快捷键对应的键值数组 // 获取快捷键对应的键值数组
getKeyCodeArr(key) { getKeyCodeArr(key) {
let keyArr = key.split(/\s*\+\s*/) let keyArr = key.split(/\s*\+\s*/)

View File

@ -1,4 +1,4 @@
import { getStrWithBrFromHtml, checkNodeOuter } from '../../utils' import { getStrWithBrFromHtml, checkNodeOuter, isMobile } from '../../utils'
// 节点文字编辑类 // 节点文字编辑类
export default class TextEdit { export default class TextEdit {
@ -67,7 +67,9 @@ export default class TextEdit {
// 创建一个隐藏的文本输入框 // 创建一个隐藏的文本输入框
createHiddenInput() { createHiddenInput() {
if (this.hiddenInputEl) return const { enableCreateHiddenInput, enableAutoEnterTextEditWhenKeydown } =
this.mindMap.opt
if (this.hiddenInputEl || isMobile() || !enableCreateHiddenInput) return
this.hiddenInputEl = document.createElement('input') this.hiddenInputEl = document.createElement('input')
this.hiddenInputEl.type = 'text' this.hiddenInputEl.type = 'text'
this.hiddenInputEl.style.cssText = ` this.hiddenInputEl.style.cssText = `
@ -75,6 +77,25 @@ export default class TextEdit {
left: -99999px; left: -99999px;
top: -99999px; top: -99999px;
` `
// 监听按键事件
if (enableAutoEnterTextEditWhenKeydown) {
this.hiddenInputEl.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)
) {
this.show(node)
}
})
}
// 监听粘贴事件 // 监听粘贴事件
this.hiddenInputEl.addEventListener('paste', async event => { this.hiddenInputEl.addEventListener('paste', async event => {
event.preventDefault() event.preventDefault()
@ -148,7 +169,7 @@ export default class TextEdit {
this.mindMap.richText.showEditText(node, rect, isInserting) this.mindMap.richText.showEditText(node, rect, isInserting)
return return
} }
this.showEditTextBox(node, rect) this.showEditTextBox(node, rect, isInserting)
} }
// 处理画布缩放 // 处理画布缩放
@ -166,7 +187,7 @@ export default class TextEdit {
} }
// 显示文本编辑框 // 显示文本编辑框
showEditTextBox(node, rect) { showEditTextBox(node, rect, isInserting) {
this.mindMap.emit('before_show_text_edit') this.mindMap.emit('before_show_text_edit')
this.registerTmpShortcut() this.registerTmpShortcut()
if (!this.textEditNode) { if (!this.textEditNode) {
@ -179,10 +200,11 @@ export default class TextEdit {
this.textEditNode.addEventListener('click', e => { this.textEditNode.addEventListener('click', e => {
e.stopPropagation() e.stopPropagation()
}) })
this.textEditNode.addEventListener('mousedown', (e) => { this.textEditNode.addEventListener('mousedown', e => {
e.stopPropagation() e.stopPropagation()
}) })
const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body const targetNode =
this.mindMap.opt.customInnerElsAppendTo || document.body
targetNode.appendChild(this.textEditNode) targetNode.appendChild(this.textEditNode)
} }
let scale = this.mindMap.view.scale let scale = this.mindMap.view.scale
@ -209,12 +231,27 @@ export default class TextEdit {
} }
this.showTextEdit = true this.showTextEdit = true
// 选中文本 // 选中文本
if (!this.cacheEditingText) { // if (!this.cacheEditingText) {
// this.selectNodeText()
// }
if (isInserting) {
this.selectNodeText() this.selectNodeText()
} else {
this.focus()
} }
this.cacheEditingText = '' this.cacheEditingText = ''
} }
// 聚焦
focus() {
let selection = window.getSelection()
let range = document.createRange()
range.selectNodeContents(this.textEditNode)
range.collapse()
selection.removeAllRanges()
selection.addRange(range)
}
// 选中文本 // 选中文本
selectNodeText() { selectNodeText() {
let selection = window.getSelection() let selection = window.getSelection()

View File

@ -204,7 +204,7 @@ class Drag extends Base {
// 检测重叠节点 // 检测重叠节点
checkOverlapNode() { checkOverlapNode() {
if (!this.drawTransform) { if (!this.drawTransform || !this.placeholder) {
return return
} }
const { nodeDragPlaceholderMaxSize } = this.mindMap.opt const { nodeDragPlaceholderMaxSize } = this.mindMap.opt

View File

@ -1,7 +1,12 @@
import Quill from 'quill' import Quill from 'quill'
import 'quill/dist/quill.snow.css' import 'quill/dist/quill.snow.css'
import html2canvas from 'html2canvas' import domtoimage from 'dom-to-image-more'
import { walk, getTextFromHtml, isWhite, getVisibleColorFromTheme } from '../utils' import {
walk,
getTextFromHtml,
isWhite,
getVisibleColorFromTheme
} from '../utils'
import { CONSTANTS } from '../constants/constant' import { CONSTANTS } from '../constants/constant'
let extended = false let extended = false
@ -172,10 +177,11 @@ class RichText {
this.textEditNode.addEventListener('click', e => { this.textEditNode.addEventListener('click', e => {
e.stopPropagation() e.stopPropagation()
}) })
this.textEditNode.addEventListener('mousedown', (e) => { this.textEditNode.addEventListener('mousedown', e => {
e.stopPropagation() e.stopPropagation()
}) })
const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body const targetNode =
this.mindMap.opt.customInnerElsAppendTo || document.body
targetNode.appendChild(this.textEditNode) targetNode.appendChild(this.textEditNode)
} }
// 使用节点的填充色,否则如果节点颜色是白色的话编辑时看不见 // 使用节点的填充色,否则如果节点颜色是白色的话编辑时看不见
@ -185,7 +191,11 @@ class RichText {
this.textEditNode.style.marginTop = `-${paddingY * scaleY}px` this.textEditNode.style.marginTop = `-${paddingY * scaleY}px`
this.textEditNode.style.zIndex = this.mindMap.opt.nodeTextEditZIndex this.textEditNode.style.zIndex = this.mindMap.opt.nodeTextEditZIndex
this.textEditNode.style.backgroundColor = this.textEditNode.style.backgroundColor =
bgColor === 'transparent' ? isWhite(color) ? getVisibleColorFromTheme(this.mindMap.themeConfig) : '#fff' : bgColor bgColor === 'transparent'
? isWhite(color)
? getVisibleColorFromTheme(this.mindMap.themeConfig)
: '#fff'
: bgColor
this.textEditNode.style.minWidth = originWidth + paddingX * 2 + 'px' this.textEditNode.style.minWidth = originWidth + paddingX * 2 + 'px'
this.textEditNode.style.minHeight = originHeight + 'px' this.textEditNode.style.minHeight = originHeight + 'px'
this.textEditNode.style.left = rect.left + 'px' this.textEditNode.style.left = rect.left + 'px'
@ -507,11 +517,16 @@ class RichText {
} }
} }
walk(node) walk(node)
let canvas = await html2canvas(el, {
backgroundColor: null // 如果使用html2canvas
}) // let canvas = await html2canvas(el, {
// backgroundColor: null
// })
// return canvas.toDataURL()
const res = await domtoimage.toPng(el)
this.mindMap.el.removeChild(el) this.mindMap.el.removeChild(el)
return canvas.toDataURL() return res
} }
// 将所有节点转换成非富文本节点 // 将所有节点转换成非富文本节点

View File

@ -58,7 +58,6 @@ class TouchEvent {
let { x: touch2ClientX, y: touch2ClientY } = this.mindMap.toPos(touch2.clientX, touch2.clientY) let { x: touch2ClientX, y: touch2ClientY } = this.mindMap.toPos(touch2.clientX, touch2.clientY)
let cx = (touch1ClientX + touch2ClientX) / 2 let cx = (touch1ClientX + touch2ClientX) / 2
let cy = (touch1ClientY + touch2ClientY) / 2 let cy = (touch1ClientY + touch2ClientY) / 2
// 手势缩放,基于最开始的位置进行缩放(基于前一个位置缩放不是线性关系); 缩放同时支持位置拖动 // 手势缩放,基于最开始的位置进行缩放(基于前一个位置缩放不是线性关系); 缩放同时支持位置拖动
var view = this.mindMap.view; var view = this.mindMap.view;
if(!this.touchScaleViewBefore){ if(!this.touchScaleViewBefore){

View File

@ -455,25 +455,25 @@ export const loadImage = imgFile => {
} }
// 移除字符串中的html实体 // 移除字符串中的html实体
export const removeHTMLEntities = (str) => { export const removeHTMLEntities = str => {
[['&nbsp;', '&#160;']].forEach((item) => { ;[['&nbsp;', '&#160;']].forEach(item => {
str = str.replaceAll(item[0], item[1]) str = str.replaceAll(item[0], item[1])
}) })
return str return str
} }
// 获取一个数据的类型 // 获取一个数据的类型
export const getType = (data) => { export const getType = data => {
return Object.prototype.toString.call(data).slice(7, -1) return Object.prototype.toString.call(data).slice(7, -1)
} }
// 判断一个数据是否是null和undefined和空字符串 // 判断一个数据是否是null和undefined和空字符串
export const isUndef = (data) => { export const isUndef = data => {
return data === null || data === undefined || data === '' return data === null || data === undefined || data === ''
} }
// 移除html字符串中节点的内联样式 // 移除html字符串中节点的内联样式
export const removeHtmlStyle = (html) => { export const removeHtmlStyle = html => {
return html.replaceAll(/(<[^\s]+)\s+style=["'][^'"]+["']\s*(>)/g, '$1$2') return html.replaceAll(/(<[^\s]+)\s+style=["'][^'"]+["']\s*(>)/g, '$1$2')
} }
@ -485,12 +485,12 @@ export const addHtmlStyle = (html, tag, style) => {
// 检查一个字符串是否是富文本字符 // 检查一个字符串是否是富文本字符
let checkIsRichTextEl = null let checkIsRichTextEl = null
export const checkIsRichText = (str) => { export const checkIsRichText = str => {
if (!checkIsRichTextEl) { if (!checkIsRichTextEl) {
checkIsRichTextEl = document.createElement('div') checkIsRichTextEl = document.createElement('div')
} }
checkIsRichTextEl.innerHTML = str checkIsRichTextEl.innerHTML = str
for (let c = checkIsRichTextEl.childNodes, i = c.length; i--;) { for (let c = checkIsRichTextEl.childNodes, i = c.length; i--; ) {
if (c[i].nodeType == 1) return true if (c[i].nodeType == 1) return true
} }
return false return false
@ -503,13 +503,20 @@ export const replaceHtmlText = (html, searchText, replaceText) => {
replaceHtmlTextEl = document.createElement('div') replaceHtmlTextEl = document.createElement('div')
} }
replaceHtmlTextEl.innerHTML = html replaceHtmlTextEl.innerHTML = html
let walk = (root) => { let walk = root => {
let childNodes = root.childNodes let childNodes = root.childNodes
childNodes.forEach((node) => { childNodes.forEach(node => {
if (node.nodeType === 1) {// 元素节点 if (node.nodeType === 1) {
// 元素节点
walk(node) walk(node)
} else if (node.nodeType === 3) {// 文本节点 } else if (node.nodeType === 3) {
root.replaceChild(document.createTextNode(node.nodeValue.replaceAll(searchText, replaceText)), node) // 文本节点
root.replaceChild(
document.createTextNode(
node.nodeValue.replaceAll(searchText, replaceText)
),
node
)
} }
}) })
} }
@ -518,22 +525,39 @@ export const replaceHtmlText = (html, searchText, replaceText) => {
} }
// 判断一个颜色是否是白色 // 判断一个颜色是否是白色
export const isWhite = (color) => { export const isWhite = color => {
color = String(color).replaceAll(/\s+/g, '') color = String(color).replaceAll(/\s+/g, '')
return ['#fff', '#ffffff', '#FFF', '#FFFFFF', 'rgb(255,255,255)'].includes(color) || /rgba\(255,255,255,[^)]+\)/.test(color) return (
['#fff', '#ffffff', '#FFF', '#FFFFFF', 'rgb(255,255,255)'].includes(
color
) || /rgba\(255,255,255,[^)]+\)/.test(color)
)
} }
// 判断一个颜色是否是透明 // 判断一个颜色是否是透明
export const isTransparent = (color) => { export const isTransparent = color => {
color = String(color).replaceAll(/\s+/g, '') color = String(color).replaceAll(/\s+/g, '')
return ['', 'transparent'].includes(color) || /rgba\(\d+,\d+,\d+,0\)/.test(color) return (
['', 'transparent'].includes(color) || /rgba\(\d+,\d+,\d+,0\)/.test(color)
)
} }
// 从当前主题里获取一个非透明非白色的颜色 // 从当前主题里获取一个非透明非白色的颜色
export const getVisibleColorFromTheme = (themeConfig) => { export const getVisibleColorFromTheme = themeConfig => {
let { lineColor, root, second, node } = themeConfig let { lineColor, root, second, node } = themeConfig
let list = [lineColor, root.fillColor, root.color, second.fillColor, second.color, node.fillColor, node.color, root.borderColor, second.borderColor, node.borderColor] let list = [
for(let i = 0; i < list.length; i++) { lineColor,
root.fillColor,
root.color,
second.fillColor,
second.color,
node.fillColor,
node.color,
root.borderColor,
second.borderColor,
node.borderColor
]
for (let i = 0; i < list.length; i++) {
let color = list[i] let color = list[i]
if (!isTransparent(color) && !isWhite(color)) { if (!isTransparent(color) && !isWhite(color)) {
return color return color
@ -543,22 +567,24 @@ export const getVisibleColorFromTheme = (themeConfig) => {
// 将<p><span></span><p>形式的节点富文本内容转换成\n换行的文本 // 将<p><span></span><p>形式的节点富文本内容转换成\n换行的文本
let nodeRichTextToTextWithWrapEl = null let nodeRichTextToTextWithWrapEl = null
export const nodeRichTextToTextWithWrap = (html) => { export const nodeRichTextToTextWithWrap = html => {
if (!nodeRichTextToTextWithWrapEl) { if (!nodeRichTextToTextWithWrapEl) {
nodeRichTextToTextWithWrapEl = document.createElement('div') nodeRichTextToTextWithWrapEl = document.createElement('div')
} }
nodeRichTextToTextWithWrapEl.innerHTML = html nodeRichTextToTextWithWrapEl.innerHTML = html
const childNodes = nodeRichTextToTextWithWrapEl.childNodes const childNodes = nodeRichTextToTextWithWrapEl.childNodes
let res = '' let res = ''
for(let i = 0; i < childNodes.length; i++) { for (let i = 0; i < childNodes.length; i++) {
const node = childNodes[i] const node = childNodes[i]
if (node.nodeType === 1) {// 元素节点 if (node.nodeType === 1) {
// 元素节点
if (node.tagName.toLowerCase() === 'p') { if (node.tagName.toLowerCase() === 'p') {
res += node.textContent + '\n' res += node.textContent + '\n'
} else { } else {
res += node.textContent res += node.textContent
} }
} else if (node.nodeType === 3) {// 文本节点 } else if (node.nodeType === 3) {
// 文本节点
res += node.nodeValue res += node.nodeValue
} }
} }
@ -567,7 +593,7 @@ export const nodeRichTextToTextWithWrap = (html) => {
// 将<br>换行的文本转换成<p><span></span><p>形式的节点富文本内容 // 将<br>换行的文本转换成<p><span></span><p>形式的节点富文本内容
let textToNodeRichTextWithWrapEl = null let textToNodeRichTextWithWrapEl = null
export const textToNodeRichTextWithWrap = (html) => { export const textToNodeRichTextWithWrap = html => {
if (!textToNodeRichTextWithWrapEl) { if (!textToNodeRichTextWithWrapEl) {
textToNodeRichTextWithWrapEl = document.createElement('div') textToNodeRichTextWithWrapEl = document.createElement('div')
} }
@ -575,23 +601,34 @@ export const textToNodeRichTextWithWrap = (html) => {
const childNodes = textToNodeRichTextWithWrapEl.childNodes const childNodes = textToNodeRichTextWithWrapEl.childNodes
let list = [] let list = []
let str = '' let str = ''
for(let i = 0; i < childNodes.length; i++) { for (let i = 0; i < childNodes.length; i++) {
const node = childNodes[i] const node = childNodes[i]
if (node.nodeType === 1) {// 元素节点 if (node.nodeType === 1) {
// 元素节点
if (node.tagName.toLowerCase() === 'br') { if (node.tagName.toLowerCase() === 'br') {
list.push(str) list.push(str)
str = '' str = ''
} else { } else {
str += node.textContent str += node.textContent
} }
} else if (node.nodeType === 3) {// 文本节点 } else if (node.nodeType === 3) {
// 文本节点
str += node.nodeValue str += node.nodeValue
} }
} }
if (str) { if (str) {
list.push(str) list.push(str)
} }
return list.map((item) => { return list
return `<p><span>${item}</span></p>` .map(item => {
}).join('') return `<p><span>${item}</span></p>`
})
.join('')
}
// 判断是否是移动端环境
export const isMobile = () => {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
)
} }

View File

@ -3,6 +3,7 @@
class="contextmenuContainer listBox" class="contextmenuContainer listBox"
v-if="isShow" v-if="isShow"
:style="{ left: left + 'px', top: top + 'px' }" :style="{ left: left + 'px', top: top + 'px' }"
:class="{ isDark: isDark }"
> >
<template v-if="type === 'node'"> <template v-if="type === 'node'">
<div <div
@ -13,7 +14,11 @@
{{ $t('contextmenu.insertSiblingNode') }} {{ $t('contextmenu.insertSiblingNode') }}
<span class="desc">Enter</span> <span class="desc">Enter</span>
</div> </div>
<div class="item" @click="exec('INSERT_CHILD_NODE')" :class="{ disabled: isGeneralization }"> <div
class="item"
@click="exec('INSERT_CHILD_NODE')"
:class="{ disabled: isGeneralization }"
>
{{ $t('contextmenu.insertChildNode') }} {{ $t('contextmenu.insertChildNode') }}
<span class="desc">Tab</span> <span class="desc">Tab</span>
</div> </div>
@ -45,18 +50,23 @@
{{ $t('contextmenu.deleteNode') }} {{ $t('contextmenu.deleteNode') }}
<span class="desc">Delete</span> <span class="desc">Delete</span>
</div> </div>
<div class="item" @click="exec('COPY_NODE')" :class="{ disabled: isGeneralization }"> <div
class="item"
@click="exec('COPY_NODE')"
:class="{ disabled: isGeneralization }"
>
{{ $t('contextmenu.copyNode') }} {{ $t('contextmenu.copyNode') }}
<span class="desc">Ctrl + C</span> <span class="desc">Ctrl + C</span>
</div> </div>
<div class="item" @click="exec('CUT_NODE')" :class="{ disabled: isGeneralization }"> <div
class="item"
@click="exec('CUT_NODE')"
:class="{ disabled: isGeneralization }"
>
{{ $t('contextmenu.cutNode') }} {{ $t('contextmenu.cutNode') }}
<span class="desc">Ctrl + X</span> <span class="desc">Ctrl + X</span>
</div> </div>
<div <div class="item" @click="exec('PASTE_NODE')">
class="item"
@click="exec('PASTE_NODE')"
>
{{ $t('contextmenu.pasteNode') }} {{ $t('contextmenu.pasteNode') }}
<span class="desc">Ctrl + V</span> <span class="desc">Ctrl + V</span>
</div> </div>
@ -74,7 +84,7 @@
</div> </div>
<div class="item"> <div class="item">
{{ $t('contextmenu.expandTo') }} {{ $t('contextmenu.expandTo') }}
<div class="subItems listBox"> <div class="subItems listBox" :class="{ isDark: isDark }">
<div <div
class="item" class="item"
v-for="(item, index) in expandList" v-for="(item, index) in expandList"
@ -130,7 +140,8 @@ export default {
}, },
computed: { computed: {
...mapState({ ...mapState({
isZenMode: state => state.localConfig.isZenMode isZenMode: state => state.localConfig.isZenMode,
isDark: state => state.isDark
}), }),
expandList() { expandList() {
return [ return [
@ -309,6 +320,10 @@ export default {
border-radius: 4px; border-radius: 4px;
padding-top: 16px; padding-top: 16px;
padding-bottom: 16px; padding-bottom: 16px;
&.isDark {
background: #363b3f;
}
} }
.contextmenuContainer { .contextmenuContainer {
position: fixed; position: fixed;
@ -317,6 +332,16 @@ export default {
font-weight: 400; font-weight: 400;
color: #1a1a1a; color: #1a1a1a;
&.isDark {
color: #fff;
.item {
&:hover {
background: hsla(0, 0%, 100%, 0.05);
}
}
}
.item { .item {
position: relative; position: relative;
height: 28px; height: 28px;

View File

@ -61,7 +61,7 @@ export default {
}, },
currentData: null, currentData: null,
notHandleDataChange: false, notHandleDataChange: false,
handleNodeTreeRenderEnd: false, isHandleNodeTreeRenderEnd: false,
beInsertNodeUid: '', beInsertNodeUid: '',
insertType: '', insertType: '',
isInTreArea: false, isInTreArea: false,
@ -106,8 +106,8 @@ export default {
return return
} }
// //
if (this.handleNodeTreeRenderEnd) { if (this.isHandleNodeTreeRenderEnd) {
this.handleNodeTreeRenderEnd = false this.isHandleNodeTreeRenderEnd = false
this.refresh() this.refresh()
this.$nextTick(() => { this.$nextTick(() => {
this.afterCreateNewNode() this.afterCreateNewNode()
@ -236,7 +236,7 @@ export default {
// //
insertNode() { insertNode() {
this.notHandleDataChange = true this.notHandleDataChange = true
this.handleNodeTreeRenderEnd = true this.isHandleNodeTreeRenderEnd = true
this.beInsertNodeUid = createUid() this.beInsertNodeUid = createUid()
this.mindMap.execCommand('INSERT_NODE', false, [], { this.mindMap.execCommand('INSERT_NODE', false, [], {
uid: this.beInsertNodeUid uid: this.beInsertNodeUid
@ -246,7 +246,7 @@ export default {
// //
insertChildNode() { insertChildNode() {
this.notHandleDataChange = true this.notHandleDataChange = true
this.handleNodeTreeRenderEnd = true this.isHandleNodeTreeRenderEnd = true
this.beInsertNodeUid = createUid() this.beInsertNodeUid = createUid()
this.mindMap.execCommand('INSERT_CHILD_NODE', false, [], { this.mindMap.execCommand('INSERT_CHILD_NODE', false, [], {
uid: this.beInsertNodeUid uid: this.beInsertNodeUid

View File

@ -3,6 +3,7 @@
class="richTextToolbar" class="richTextToolbar"
ref="richTextToolbar" ref="richTextToolbar"
:style="style" :style="style"
:class="{ isDark: isDark }"
@click.stop.passive @click.stop.passive
v-show="showRichTextToolbar" v-show="showRichTextToolbar"
> >
@ -44,7 +45,7 @@
<el-tooltip content="字体" placement="top"> <el-tooltip content="字体" placement="top">
<el-popover placement="bottom" trigger="hover"> <el-popover placement="bottom" trigger="hover">
<div class="fontOptionsList"> <div class="fontOptionsList" :class="{ isDark: isDark }">
<div <div
class="fontOptionItem" class="fontOptionItem"
v-for="item in fontFamilyList" v-for="item in fontFamilyList"
@ -64,7 +65,7 @@
<el-tooltip content="字号" placement="top"> <el-tooltip content="字号" placement="top">
<el-popover placement="bottom" trigger="hover"> <el-popover placement="bottom" trigger="hover">
<div class="fontOptionsList"> <div class="fontOptionsList" :class="{ isDark: isDark }">
<div <div
class="fontOptionItem" class="fontOptionItem"
v-for="item in fontSizeList" v-for="item in fontSizeList"
@ -93,7 +94,10 @@
<el-tooltip content="背景颜色" placement="top"> <el-tooltip content="背景颜色" placement="top">
<el-popover placement="bottom" trigger="hover"> <el-popover placement="bottom" trigger="hover">
<Color :color="fontBackgroundColor" @change="changeFontBackgroundColor"></Color> <Color
:color="fontBackgroundColor"
@change="changeFontBackgroundColor"
></Color>
<div class="btn" slot="reference"> <div class="btn" slot="reference">
<span class="icon iconfont iconbeijingyanse"></span> <span class="icon iconfont iconbeijingyanse"></span>
</div> </div>
@ -101,9 +105,7 @@
</el-tooltip> </el-tooltip>
<el-tooltip content="清除样式" placement="top"> <el-tooltip content="清除样式" placement="top">
<div <div class="btn" @click="removeFormat">
class="btn" @click="removeFormat"
>
<span class="icon iconfont iconqingchu"></span> <span class="icon iconfont iconqingchu"></span>
</div> </div>
</el-tooltip> </el-tooltip>
@ -113,6 +115,7 @@
<script> <script>
import { fontFamilyList, fontSizeList } from '@/config' import { fontFamilyList, fontSizeList } from '@/config'
import Color from './Color' import Color from './Color'
import { mapState } from 'vuex'
export default { export default {
name: 'RichTextToolbar', name: 'RichTextToolbar',
@ -138,6 +141,8 @@ export default {
} }
}, },
computed: { computed: {
...mapState(['isDark']),
fontFamilyList() { fontFamilyList() {
return fontFamilyList[this.$i18n.locale] || fontFamilyList.zh return fontFamilyList[this.$i18n.locale] || fontFamilyList.zh
} }
@ -211,7 +216,7 @@ export default {
}, },
changeFontBackgroundColor(background) { changeFontBackgroundColor(background) {
this.formatInfo.background = background this.formatInfo.background = background
this.mindMap.richText.formatText({ this.mindMap.richText.formatText({
background background
}) })
@ -237,6 +242,18 @@ export default {
align-items: center; align-items: center;
transform: translateX(-50%); transform: translateX(-50%);
&.isDark {
background: #363b3f;
.btn {
color: #fff;
&:hover {
background: hsla(0, 0%, 100%, 0.05);
}
}
}
.btn { .btn {
width: 55px; width: 55px;
height: 55px; height: 55px;
@ -266,6 +283,16 @@ export default {
.fontOptionsList { .fontOptionsList {
width: 150px; width: 150px;
&.isDark {
.fontOptionItem {
color: #fff;
&:hover {
background-color: hsla(0, 0%, 100%, 0.05);
}
}
}
.fontOptionItem { .fontOptionItem {
height: 30px; height: 30px;
width: 100%; width: 100%;