mirror of
https://github.com/wanglin2/mind-map.git
synced 2026-02-17 14:04:47 +08:00
Feat:外框支持文本编辑,开发中
This commit is contained in:
@@ -496,6 +496,7 @@ export const defaultOpt = {
|
|||||||
// 【OuterFrame】插件
|
// 【OuterFrame】插件
|
||||||
outerFramePaddingX: 10,
|
outerFramePaddingX: 10,
|
||||||
outerFramePaddingY: 10,
|
outerFramePaddingY: 10,
|
||||||
|
defaultOuterFrameText: '外框',
|
||||||
|
|
||||||
// 【Painter】插件
|
// 【Painter】插件
|
||||||
// 是否只格式刷节点手动设置的样式,不考虑节点通过主题的应用的样式
|
// 是否只格式刷节点手动设置的样式,不考虑节点通过主题的应用的样式
|
||||||
|
|||||||
@@ -1,149 +1,63 @@
|
|||||||
import {
|
import {
|
||||||
formatDataToArray,
|
formatDataToArray,
|
||||||
walk,
|
walk,
|
||||||
getTopAncestorsFomNodeList,
|
|
||||||
getNodeListBoundingRect,
|
getNodeListBoundingRect,
|
||||||
createUid
|
createUid
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
|
import {
|
||||||
// 解析要添加外框的节点实例列表
|
parseAddNodeList,
|
||||||
const parseAddNodeList = list => {
|
getNodeOuterFrameList
|
||||||
// 找出顶层节点
|
} from './outerFrame/outerFrameUtils'
|
||||||
list = getTopAncestorsFomNodeList(list)
|
import outerFrameTextMethods from './outerFrame/outerFrameText'
|
||||||
const cache = {}
|
|
||||||
const uidToParent = {}
|
|
||||||
// 找出列表中节点在兄弟节点中的索引,并和父节点关联起来
|
|
||||||
list.forEach(node => {
|
|
||||||
const parent = node.parent
|
|
||||||
if (parent) {
|
|
||||||
const pUid = parent.uid
|
|
||||||
uidToParent[pUid] = parent
|
|
||||||
const index = node.getIndexInBrothers()
|
|
||||||
const data = {
|
|
||||||
node,
|
|
||||||
index
|
|
||||||
}
|
|
||||||
if (cache[pUid]) {
|
|
||||||
if (
|
|
||||||
!cache[pUid].find(item => {
|
|
||||||
return item.index === data.index
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
cache[pUid].push(data)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cache[pUid] = [data]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const res = []
|
|
||||||
Object.keys(cache).forEach(uid => {
|
|
||||||
const indexList = cache[uid]
|
|
||||||
const parentNode = uidToParent[uid]
|
|
||||||
if (indexList.length > 1) {
|
|
||||||
// 多个节点
|
|
||||||
const rangeList = indexList
|
|
||||||
.map(item => {
|
|
||||||
return item.index
|
|
||||||
})
|
|
||||||
.sort((a, b) => {
|
|
||||||
return a - b
|
|
||||||
})
|
|
||||||
const minIndex = rangeList[0]
|
|
||||||
const maxIndex = rangeList[rangeList.length - 1]
|
|
||||||
let curStart = -1
|
|
||||||
let curEnd = -1
|
|
||||||
for (let i = minIndex; i <= maxIndex; i++) {
|
|
||||||
// 连续索引
|
|
||||||
if (rangeList.includes(i)) {
|
|
||||||
if (curStart === -1) {
|
|
||||||
curStart = i
|
|
||||||
}
|
|
||||||
curEnd = i
|
|
||||||
} else {
|
|
||||||
// 连续断开
|
|
||||||
if (curStart !== -1 && curEnd !== -1) {
|
|
||||||
res.push({
|
|
||||||
node: parentNode,
|
|
||||||
range: [curStart, curEnd]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
curStart = -1
|
|
||||||
curEnd = -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 不要忘了最后一段索引
|
|
||||||
if (curStart !== -1 && curEnd !== -1) {
|
|
||||||
res.push({
|
|
||||||
node: parentNode,
|
|
||||||
range: [curStart, curEnd]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 单个节点
|
|
||||||
res.push({
|
|
||||||
node: parentNode,
|
|
||||||
range: [indexList[0].index, indexList[0].index]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析获取节点的子节点生成的外框列表
|
|
||||||
const getNodeOuterFrameList = node => {
|
|
||||||
const children = node.children
|
|
||||||
if (!children || children.length <= 0) return
|
|
||||||
const res = []
|
|
||||||
const map = {}
|
|
||||||
children.forEach((item, index) => {
|
|
||||||
const outerFrameData = item.getData('outerFrame')
|
|
||||||
if (!outerFrameData) return
|
|
||||||
const groupId = outerFrameData.groupId
|
|
||||||
if (groupId) {
|
|
||||||
if (!map[groupId]) {
|
|
||||||
map[groupId] = []
|
|
||||||
}
|
|
||||||
map[groupId].push({
|
|
||||||
node: item,
|
|
||||||
index
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
res.push({
|
|
||||||
nodeList: [item],
|
|
||||||
range: [index, index]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
Object.keys(map).forEach(id => {
|
|
||||||
const list = map[id]
|
|
||||||
res.push({
|
|
||||||
nodeList: list.map(item => {
|
|
||||||
return item.node
|
|
||||||
}),
|
|
||||||
range: [list[0].index, list[list.length - 1].index]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认外框样式
|
// 默认外框样式
|
||||||
const defaultStyle = {
|
const defaultStyle = {
|
||||||
|
// 外框圆角大小
|
||||||
radius: 5,
|
radius: 5,
|
||||||
|
// 外框边框宽度
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
|
// 外框边框颜色
|
||||||
strokeColor: '#0984e3',
|
strokeColor: '#0984e3',
|
||||||
|
// 外框边框虚线样式
|
||||||
strokeDasharray: '5,5',
|
strokeDasharray: '5,5',
|
||||||
fill: 'rgba(9,132,227,0.05)'
|
// 外框填充颜色
|
||||||
|
fill: 'rgba(9,132,227,0.05)',
|
||||||
|
// 外框文字字号
|
||||||
|
fontSize: 14,
|
||||||
|
// 外框文字字体
|
||||||
|
fontFamily: '微软雅黑, Microsoft YaHei',
|
||||||
|
// 外框文字颜色
|
||||||
|
color: '#fff',
|
||||||
|
// 外框文字行高
|
||||||
|
lineHeight: 1.2,
|
||||||
|
// 外框文字背景
|
||||||
|
textFill: '#0984e3',
|
||||||
|
// 外框文字圆角
|
||||||
|
textFillRadius: 5,
|
||||||
|
// 外框文字矩内边距,左上右下
|
||||||
|
textFillPadding: [5, 5, 5, 5],
|
||||||
|
// 外框文字水平显示位置,相对于外框
|
||||||
|
textAlign: 'left' // left、center、right
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const OUTER_FRAME_TEXT_EDIT_WRAP = 'outer-frame-text-edit-warp'
|
||||||
|
|
||||||
// 外框插件
|
// 外框插件
|
||||||
class OuterFrame {
|
class OuterFrame {
|
||||||
constructor(opt = {}) {
|
constructor(opt = {}) {
|
||||||
this.mindMap = opt.mindMap
|
this.mindMap = opt.mindMap
|
||||||
this.draw = null
|
this.draw = null
|
||||||
this.createDrawContainer()
|
this.createDrawContainer()
|
||||||
|
this.textNodeList = []
|
||||||
this.outerFrameElList = []
|
this.outerFrameElList = []
|
||||||
this.activeOuterFrame = null
|
this.activeOuterFrame = null
|
||||||
|
// 文字相关方法
|
||||||
|
this.textEditNode = null
|
||||||
|
this.showTextEdit = false
|
||||||
|
Object.keys(outerFrameTextMethods).forEach(item => {
|
||||||
|
this[item] = outerFrameTextMethods[item].bind(this)
|
||||||
|
})
|
||||||
|
this.mindMap.addEditNodeClass(OUTER_FRAME_TEXT_EDIT_WRAP)
|
||||||
this.bindEvent()
|
this.bindEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,6 +78,11 @@ class OuterFrame {
|
|||||||
this.clearActiveOuterFrame = this.clearActiveOuterFrame.bind(this)
|
this.clearActiveOuterFrame = this.clearActiveOuterFrame.bind(this)
|
||||||
this.mindMap.on('draw_click', this.clearActiveOuterFrame)
|
this.mindMap.on('draw_click', this.clearActiveOuterFrame)
|
||||||
this.mindMap.on('node_click', this.clearActiveOuterFrame)
|
this.mindMap.on('node_click', this.clearActiveOuterFrame)
|
||||||
|
// 缩放事件
|
||||||
|
this.mindMap.on('scale', this.onScale)
|
||||||
|
// 实例销毁事件
|
||||||
|
this.onBeforeDestroy = this.onBeforeDestroy.bind(this)
|
||||||
|
this.mindMap.on('beforeDestroy', this.onBeforeDestroy)
|
||||||
|
|
||||||
this.addOuterFrame = this.addOuterFrame.bind(this)
|
this.addOuterFrame = this.addOuterFrame.bind(this)
|
||||||
this.mindMap.command.add('ADD_OUTER_FRAME', this.addOuterFrame)
|
this.mindMap.command.add('ADD_OUTER_FRAME', this.addOuterFrame)
|
||||||
@@ -181,6 +100,8 @@ class OuterFrame {
|
|||||||
this.mindMap.off('data_change', this.renderOuterFrames)
|
this.mindMap.off('data_change', this.renderOuterFrames)
|
||||||
this.mindMap.off('draw_click', this.clearActiveOuterFrame)
|
this.mindMap.off('draw_click', this.clearActiveOuterFrame)
|
||||||
this.mindMap.off('node_click', this.clearActiveOuterFrame)
|
this.mindMap.off('node_click', this.clearActiveOuterFrame)
|
||||||
|
this.mindMap.off('scale', this.onScale)
|
||||||
|
this.mindMap.off('beforeDestroy', this.onBeforeDestroy)
|
||||||
this.mindMap.command.remove('ADD_OUTER_FRAME', this.addOuterFrame)
|
this.mindMap.command.remove('ADD_OUTER_FRAME', this.addOuterFrame)
|
||||||
this.mindMap.keyCommand.removeShortcut(
|
this.mindMap.keyCommand.removeShortcut(
|
||||||
'Del|Backspace',
|
'Del|Backspace',
|
||||||
@@ -188,6 +109,12 @@ class OuterFrame {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 实例销毁时清除关联线文字编辑框
|
||||||
|
onBeforeDestroy() {
|
||||||
|
this.hideEditTextBox()
|
||||||
|
this.removeTextEditEl()
|
||||||
|
}
|
||||||
|
|
||||||
// 给节点添加外框数据
|
// 给节点添加外框数据
|
||||||
/*
|
/*
|
||||||
config: {
|
config: {
|
||||||
@@ -279,8 +206,14 @@ class OuterFrame {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取某个节点指定范围的带外框的第一个子节点
|
||||||
|
getNodeRangeFirstNode(node, range) {
|
||||||
|
return node.children[range[0]]
|
||||||
|
}
|
||||||
|
|
||||||
// 渲染外框
|
// 渲染外框
|
||||||
renderOuterFrames() {
|
renderOuterFrames() {
|
||||||
|
this.clearTextNodes()
|
||||||
this.clearOuterFrameElList()
|
this.clearOuterFrameElList()
|
||||||
let tree = this.mindMap.renderer.root
|
let tree = this.mindMap.renderer.root
|
||||||
if (!tree) return
|
if (!tree) return
|
||||||
@@ -317,11 +250,15 @@ class OuterFrame {
|
|||||||
t.scaleY,
|
t.scaleY,
|
||||||
(width + outerFramePaddingX * 2) / t.scaleX,
|
(width + outerFramePaddingX * 2) / t.scaleX,
|
||||||
(height + outerFramePaddingY * 2) / t.scaleY,
|
(height + outerFramePaddingY * 2) / t.scaleY,
|
||||||
nodeList[0].getData('outerFrame') // 使用第一个节点的外框样式
|
this.getStyle(nodeList[0]) // 使用第一个节点的外框样式
|
||||||
)
|
)
|
||||||
|
// 渲染文字,如果有的话
|
||||||
|
const textNode = this.createText(el, cur, range)
|
||||||
|
this.textNodeList.push(textNode)
|
||||||
|
this.renderText(this.getText(nodeList[0]), el, textNode, cur, range)
|
||||||
el.on('click', e => {
|
el.on('click', e => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
this.setActiveOuterFrame(el, cur, range)
|
this.setActiveOuterFrame(el, cur, range, textNode)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -333,33 +270,54 @@ class OuterFrame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 激活外框
|
// 激活外框
|
||||||
setActiveOuterFrame(el, node, range) {
|
setActiveOuterFrame(el, node, range, textNode) {
|
||||||
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
|
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
|
||||||
this.clearActiveOuterFrame()
|
this.clearActiveOuterFrame()
|
||||||
this.activeOuterFrame = {
|
this.activeOuterFrame = {
|
||||||
el,
|
el,
|
||||||
node,
|
node,
|
||||||
range
|
range,
|
||||||
|
textNode
|
||||||
}
|
}
|
||||||
el.stroke({
|
el.stroke({
|
||||||
dasharray: 'none'
|
dasharray: 'none'
|
||||||
})
|
})
|
||||||
|
// 如果没有输入过文字,那么显示默认文字
|
||||||
|
if (!this.getText(this.getNodeRangeFirstNode(node, range))) {
|
||||||
|
this.renderText(
|
||||||
|
this.mindMap.opt.defaultOuterFrameText,
|
||||||
|
el,
|
||||||
|
textNode,
|
||||||
|
node,
|
||||||
|
range
|
||||||
|
)
|
||||||
|
}
|
||||||
this.mindMap.emit('outer_frame_active', el, node, range)
|
this.mindMap.emit('outer_frame_active', el, node, range)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除当前激活的外框
|
// 清除当前激活的外框
|
||||||
clearActiveOuterFrame() {
|
clearActiveOuterFrame() {
|
||||||
if (!this.activeOuterFrame) return
|
if (!this.activeOuterFrame) return
|
||||||
const { el } = this.activeOuterFrame
|
const { el, textNode, node, range } = this.activeOuterFrame
|
||||||
el.stroke({
|
el.stroke({
|
||||||
dasharray: el.cacheStyle.dasharray || defaultStyle.strokeDasharray
|
dasharray: el.cacheStyle.dasharray || defaultStyle.strokeDasharray
|
||||||
})
|
})
|
||||||
|
// 隐藏文本编辑框
|
||||||
|
this.hideEditTextBox()
|
||||||
|
// 如果没有输入过文字,那么隐藏
|
||||||
|
if (!this.getText(this.getNodeRangeFirstNode(node, range))) {
|
||||||
|
textNode.clear()
|
||||||
|
}
|
||||||
this.activeOuterFrame = null
|
this.activeOuterFrame = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取指定外框的样式
|
||||||
|
getStyle(node) {
|
||||||
|
return { ...defaultStyle, ...(node.getData('outerFrame') || {}) }
|
||||||
|
}
|
||||||
|
|
||||||
// 创建外框元素
|
// 创建外框元素
|
||||||
createOuterFrameEl(x, y, width, height, styleConfig = {}) {
|
createOuterFrameEl(x, y, width, height, styleConfig = {}) {
|
||||||
styleConfig = { ...defaultStyle, ...styleConfig }
|
|
||||||
const el = this.draw
|
const el = this.draw
|
||||||
.rect()
|
.rect()
|
||||||
.size(width, height)
|
.size(width, height)
|
||||||
@@ -381,6 +339,13 @@ class OuterFrame {
|
|||||||
return el
|
return el
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清除文本元素
|
||||||
|
clearTextNodes() {
|
||||||
|
this.textNodeList.forEach(item => {
|
||||||
|
item.remove()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 清除外框元素
|
// 清除外框元素
|
||||||
clearOuterFrameElList() {
|
clearOuterFrameElList() {
|
||||||
this.outerFrameElList.forEach(item => {
|
this.outerFrameElList.forEach(item => {
|
||||||
@@ -392,15 +357,18 @@ class OuterFrame {
|
|||||||
|
|
||||||
// 插件被移除前做的事情
|
// 插件被移除前做的事情
|
||||||
beforePluginRemove() {
|
beforePluginRemove() {
|
||||||
|
this.mindMap.deleteEditNodeClass(OUTER_FRAME_TEXT_EDIT_WRAP)
|
||||||
this.unBindEvent()
|
this.unBindEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 插件被卸载前做的事情
|
// 插件被卸载前做的事情
|
||||||
beforePluginDestroy() {
|
beforePluginDestroy() {
|
||||||
|
this.mindMap.deleteEditNodeClass(OUTER_FRAME_TEXT_EDIT_WRAP)
|
||||||
this.unBindEvent()
|
this.unBindEvent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OuterFrame.instanceName = 'outerFrame'
|
OuterFrame.instanceName = 'outerFrame'
|
||||||
|
OuterFrame.defaultStyle = defaultStyle
|
||||||
|
|
||||||
export default OuterFrame
|
export default OuterFrame
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
htmlEscape,
|
htmlEscape,
|
||||||
compareVersion
|
compareVersion
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { CONSTANTS, richTextSupportStyleList } from '../constants/constant'
|
import { richTextSupportStyleList } from '../constants/constant'
|
||||||
import MindMapNode from '../core/render/node/MindMapNode'
|
import MindMapNode from '../core/render/node/MindMapNode'
|
||||||
import { Scope } from 'parchment'
|
import { Scope } from 'parchment'
|
||||||
|
|
||||||
|
|||||||
230
simple-mind-map/src/plugins/outerFrame/outerFrameText.js
Normal file
230
simple-mind-map/src/plugins/outerFrame/outerFrameText.js
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
import { Text, Rect, G } from '@svgdotjs/svg.js'
|
||||||
|
import {
|
||||||
|
getStrWithBrFromHtml,
|
||||||
|
focusInput,
|
||||||
|
selectAllInput
|
||||||
|
} from '../../utils/index'
|
||||||
|
|
||||||
|
const OUTER_FRAME_TEXT_EDIT_WRAP = 'outer-frame-text-edit-warp'
|
||||||
|
|
||||||
|
// 创建文字节点
|
||||||
|
function createText(el, cur, range) {
|
||||||
|
const g = this.draw.group()
|
||||||
|
const setActive = () => {
|
||||||
|
if (!this.activeOuterFrame || this.activeOuterFrame.el !== el) {
|
||||||
|
this.setActiveOuterFrame(el, cur, range, g)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.click(e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setActive()
|
||||||
|
})
|
||||||
|
g.on('dblclick', e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setActive()
|
||||||
|
this.showEditTextBox(g)
|
||||||
|
})
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示文本编辑框
|
||||||
|
function showEditTextBox(g) {
|
||||||
|
this.mindMap.emit('before_show_text_edit')
|
||||||
|
// 注册回车快捷键
|
||||||
|
this.mindMap.keyCommand.addShortcut('Enter', () => {
|
||||||
|
this.hideEditTextBox()
|
||||||
|
})
|
||||||
|
// 输入框元素没有创建过,则先创建
|
||||||
|
if (!this.textEditNode) {
|
||||||
|
this.textEditNode = document.createElement('div')
|
||||||
|
this.textEditNode.className = OUTER_FRAME_TEXT_EDIT_WRAP
|
||||||
|
this.textEditNode.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 0 20px rgba(0,0,0,.5);
|
||||||
|
outline: none;
|
||||||
|
word-break: break-all;
|
||||||
|
`
|
||||||
|
this.textEditNode.setAttribute('contenteditable', true)
|
||||||
|
this.textEditNode.addEventListener('keyup', e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
})
|
||||||
|
this.textEditNode.addEventListener('click', e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
})
|
||||||
|
const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body
|
||||||
|
targetNode.appendChild(this.textEditNode)
|
||||||
|
}
|
||||||
|
const { node, range } = this.activeOuterFrame
|
||||||
|
const style = this.getStyle(this.getNodeRangeFirstNode(node, range))
|
||||||
|
const [pl, pt, pr, pb] = style.textFillPadding
|
||||||
|
let { defaultOuterFrameText, nodeTextEditZIndex } = this.mindMap.opt
|
||||||
|
let scale = this.mindMap.view.scale
|
||||||
|
let text = this.getText(this.getNodeRangeFirstNode(node, range))
|
||||||
|
let textLines = (text || defaultOuterFrameText).split(/\n/gim)
|
||||||
|
this.textEditNode.style.padding = `${pl}px ${pt}px ${pr}px ${pb}px`
|
||||||
|
this.textEditNode.style.fontFamily = style.fontFamily
|
||||||
|
this.textEditNode.style.fontSize = style.fontSize * scale + 'px'
|
||||||
|
this.textEditNode.style.lineHeight =
|
||||||
|
textLines.length > 1 ? style.lineHeight : 'normal'
|
||||||
|
this.textEditNode.style.zIndex = nodeTextEditZIndex
|
||||||
|
this.textEditNode.innerHTML = textLines.join('<br>')
|
||||||
|
this.textEditNode.style.display = 'block'
|
||||||
|
this.updateTextEditBoxPos(g)
|
||||||
|
this.setIsShowTextEdit(true)
|
||||||
|
// 如果是默认文本要全选输入框
|
||||||
|
if (text === '' || text === defaultOuterFrameText) {
|
||||||
|
selectAllInput(this.textEditNode)
|
||||||
|
} else {
|
||||||
|
// 否则聚焦即可
|
||||||
|
focusInput(this.textEditNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置文本编辑框是否处于显示状态
|
||||||
|
function setIsShowTextEdit(val) {
|
||||||
|
this.showTextEdit = val
|
||||||
|
if (val) {
|
||||||
|
this.mindMap.keyCommand.stopCheckInSvg()
|
||||||
|
} else {
|
||||||
|
this.mindMap.keyCommand.recoveryCheckInSvg()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除文本编辑框元素
|
||||||
|
function removeTextEditEl() {
|
||||||
|
if (!this.textEditNode) return
|
||||||
|
const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body
|
||||||
|
targetNode.removeChild(this.textEditNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理画布缩放
|
||||||
|
function onScale() {
|
||||||
|
this.hideEditTextBox()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新文本编辑框位置
|
||||||
|
function updateTextEditBoxPos(g) {
|
||||||
|
let rect = g.node.getBoundingClientRect()
|
||||||
|
if (this.textEditNode) {
|
||||||
|
this.textEditNode.style.minWidth = `${rect.width}px`
|
||||||
|
this.textEditNode.style.minHeight = `${rect.height}px`
|
||||||
|
this.textEditNode.style.left = `${rect.left}px`
|
||||||
|
this.textEditNode.style.top = `${rect.top}px`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏文本编辑框
|
||||||
|
function hideEditTextBox() {
|
||||||
|
if (!this.showTextEdit) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let { el, textNode, node, range } = this.activeOuterFrame
|
||||||
|
let str = getStrWithBrFromHtml(this.textEditNode.innerHTML)
|
||||||
|
// 如果是默认文本,那么不保存
|
||||||
|
let isDefaultText = str === this.mindMap.opt.defaultOuterFrameText
|
||||||
|
str = isDefaultText ? '' : str
|
||||||
|
this.updateActiveOuterFrame({
|
||||||
|
text: str
|
||||||
|
})
|
||||||
|
this.textEditNode.style.display = 'none'
|
||||||
|
this.textEditNode.innerHTML = ''
|
||||||
|
this.setIsShowTextEdit(false)
|
||||||
|
this.renderText(str, el, textNode, node, range)
|
||||||
|
this.mindMap.emit('hide_text_edit')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染文字
|
||||||
|
function renderText(str, rect, textNode, node, range) {
|
||||||
|
if (!str) return
|
||||||
|
// 先清空文字节点原内容
|
||||||
|
textNode.clear()
|
||||||
|
// 创建背景矩形
|
||||||
|
const shape = new Rect()
|
||||||
|
textNode.add(shape)
|
||||||
|
// 获取样式配置
|
||||||
|
const style = this.getStyle(this.getNodeRangeFirstNode(node, range))
|
||||||
|
const [pl, pt, pr, pb] = style.textFillPadding
|
||||||
|
// 创建文本节点
|
||||||
|
let textArr = str.replace(/\n$/g, '').split(/\n/gim)
|
||||||
|
const g = new G()
|
||||||
|
textArr.forEach((item, index) => {
|
||||||
|
// 避免尾部的空行不占宽度,导致文本编辑框定位异常的问题
|
||||||
|
if (item === '') {
|
||||||
|
item = ''
|
||||||
|
}
|
||||||
|
let text = new Text().text(item)
|
||||||
|
text.y(style.fontSize * style.lineHeight * index)
|
||||||
|
this.styleText(text, style)
|
||||||
|
g.add(text)
|
||||||
|
})
|
||||||
|
textNode.add(g)
|
||||||
|
// 计算高度
|
||||||
|
const { width: textWidth, height: textHeight } = textNode.bbox()
|
||||||
|
const totalWidth = textWidth + pl + pr
|
||||||
|
const totalHeight = textHeight + pt + pb
|
||||||
|
shape.size(totalWidth, totalHeight).x(0).dy(0)
|
||||||
|
this.styleTextShape(shape, style)
|
||||||
|
// 设置节点位置
|
||||||
|
let tx = 0
|
||||||
|
switch (style.textAlign) {
|
||||||
|
case 'left':
|
||||||
|
tx = rect.x()
|
||||||
|
break
|
||||||
|
case 'center':
|
||||||
|
tx = rect.x() + rect.width() / 2 - totalWidth / 2
|
||||||
|
break
|
||||||
|
case 'right':
|
||||||
|
tx = rect.x() + rect.width() - totalWidth
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
const ty = rect.y() - totalHeight
|
||||||
|
shape.x(tx)
|
||||||
|
shape.y(ty)
|
||||||
|
g.x(tx + pl)
|
||||||
|
g.y(ty + pt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 给文本背景设置样式
|
||||||
|
function styleTextShape(shape, style) {
|
||||||
|
shape
|
||||||
|
.fill({
|
||||||
|
color: style.textFill
|
||||||
|
})
|
||||||
|
.radius(style.textFillRadius)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 给文本设置样式
|
||||||
|
function styleText(textNode, style) {
|
||||||
|
textNode
|
||||||
|
.fill({
|
||||||
|
color: style.color
|
||||||
|
})
|
||||||
|
.css({
|
||||||
|
'font-family': style.fontFamily,
|
||||||
|
'font-size': style.fontSize + 'px'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取外框文字
|
||||||
|
function getText(node) {
|
||||||
|
const data = node.getData('outerFrame')
|
||||||
|
return data && data.text ? data.text : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getText,
|
||||||
|
createText,
|
||||||
|
styleTextShape,
|
||||||
|
styleText,
|
||||||
|
onScale,
|
||||||
|
showEditTextBox,
|
||||||
|
setIsShowTextEdit,
|
||||||
|
removeTextEditEl,
|
||||||
|
hideEditTextBox,
|
||||||
|
updateTextEditBoxPos,
|
||||||
|
renderText
|
||||||
|
}
|
||||||
122
simple-mind-map/src/plugins/outerFrame/outerFrameUtils.js
Normal file
122
simple-mind-map/src/plugins/outerFrame/outerFrameUtils.js
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import { getTopAncestorsFomNodeList } from '../../utils'
|
||||||
|
|
||||||
|
// 解析要添加外框的节点实例列表
|
||||||
|
export const parseAddNodeList = list => {
|
||||||
|
// 找出顶层节点
|
||||||
|
list = getTopAncestorsFomNodeList(list)
|
||||||
|
const cache = {}
|
||||||
|
const uidToParent = {}
|
||||||
|
// 找出列表中节点在兄弟节点中的索引,并和父节点关联起来
|
||||||
|
list.forEach(node => {
|
||||||
|
const parent = node.parent
|
||||||
|
if (parent) {
|
||||||
|
const pUid = parent.uid
|
||||||
|
uidToParent[pUid] = parent
|
||||||
|
const index = node.getIndexInBrothers()
|
||||||
|
const data = {
|
||||||
|
node,
|
||||||
|
index
|
||||||
|
}
|
||||||
|
if (cache[pUid]) {
|
||||||
|
if (
|
||||||
|
!cache[pUid].find(item => {
|
||||||
|
return item.index === data.index
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
cache[pUid].push(data)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cache[pUid] = [data]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const res = []
|
||||||
|
Object.keys(cache).forEach(uid => {
|
||||||
|
const indexList = cache[uid]
|
||||||
|
const parentNode = uidToParent[uid]
|
||||||
|
if (indexList.length > 1) {
|
||||||
|
// 多个节点
|
||||||
|
const rangeList = indexList
|
||||||
|
.map(item => {
|
||||||
|
return item.index
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
return a - b
|
||||||
|
})
|
||||||
|
const minIndex = rangeList[0]
|
||||||
|
const maxIndex = rangeList[rangeList.length - 1]
|
||||||
|
let curStart = -1
|
||||||
|
let curEnd = -1
|
||||||
|
for (let i = minIndex; i <= maxIndex; i++) {
|
||||||
|
// 连续索引
|
||||||
|
if (rangeList.includes(i)) {
|
||||||
|
if (curStart === -1) {
|
||||||
|
curStart = i
|
||||||
|
}
|
||||||
|
curEnd = i
|
||||||
|
} else {
|
||||||
|
// 连续断开
|
||||||
|
if (curStart !== -1 && curEnd !== -1) {
|
||||||
|
res.push({
|
||||||
|
node: parentNode,
|
||||||
|
range: [curStart, curEnd]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
curStart = -1
|
||||||
|
curEnd = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 不要忘了最后一段索引
|
||||||
|
if (curStart !== -1 && curEnd !== -1) {
|
||||||
|
res.push({
|
||||||
|
node: parentNode,
|
||||||
|
range: [curStart, curEnd]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 单个节点
|
||||||
|
res.push({
|
||||||
|
node: parentNode,
|
||||||
|
range: [indexList[0].index, indexList[0].index]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析获取节点的子节点生成的外框列表
|
||||||
|
export const getNodeOuterFrameList = node => {
|
||||||
|
const children = node.children
|
||||||
|
if (!children || children.length <= 0) return
|
||||||
|
const res = []
|
||||||
|
const map = {}
|
||||||
|
children.forEach((item, index) => {
|
||||||
|
const outerFrameData = item.getData('outerFrame')
|
||||||
|
if (!outerFrameData) return
|
||||||
|
const groupId = outerFrameData.groupId
|
||||||
|
if (groupId) {
|
||||||
|
if (!map[groupId]) {
|
||||||
|
map[groupId] = []
|
||||||
|
}
|
||||||
|
map[groupId].push({
|
||||||
|
node: item,
|
||||||
|
index
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
res.push({
|
||||||
|
nodeList: [item],
|
||||||
|
range: [index, index]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Object.keys(map).forEach(id => {
|
||||||
|
const list = map[id]
|
||||||
|
res.push({
|
||||||
|
nodeList: list.map(item => {
|
||||||
|
return item.node
|
||||||
|
}),
|
||||||
|
range: [list[0].index, list[list.length - 1].index]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return res
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user