Feat:新增开启节点文本编辑实时更新节点大小和位置的实例化选项

This commit is contained in:
街角小林 2024-08-29 15:33:38 +08:00
parent 4e327c3a48
commit 570bbb1b16
5 changed files with 106 additions and 15 deletions

View File

@ -238,6 +238,10 @@ export const defaultOpt = {
padding: 100, // 超出画布四周指定范围内依旧渲染节点 padding: 100, // 超出画布四周指定范围内依旧渲染节点
removeNodeWhenOutCanvas: true // 节点移除画布可视区域后从画布删除 removeNodeWhenOutCanvas: true // 节点移除画布可视区域后从画布删除
}, },
// 如果节点文本为空,那么为了避免空白节点高度塌陷,会用该字段指定的文本测量一个高度
emptyTextMeasureHeightText: 'abc123我和你',
// 是否在进行节点文本编辑时实时更新节点大小和节点位置,开启后当节点数量比较多时可能会造成卡顿
openRealtimeRenderOnNodeTextEdit: false,
// 【Select插件】 // 【Select插件】
// 多选节点时鼠标移动到边缘时的画布移动偏移量 // 多选节点时鼠标移动到边缘时的画布移动偏移量

View File

@ -147,6 +147,19 @@ class Render {
}) })
// 性能模式 // 性能模式
this.performanceMode() this.performanceMode()
// 实时渲染当节点文本编辑时
if (this.mindMap.opt.openRealtimeRenderOnNodeTextEdit) {
this.mindMap.on('node_text_edit_change', ({ node, text }) => {
node._textData = node.createTextNode(text)
const { width, height } = node.getNodeRect()
node.width = width
node.height = height
node.layout()
this.mindMap.render(() => {
this.textEdit.updateTextEditNode()
})
})
}
} }
// 性能模式,懒加载节点 // 性能模式,懒加载节点

View File

@ -25,6 +25,8 @@ export default class TextEdit {
// 如果编辑过程中缩放画布了,那么缓存当前编辑的内容 // 如果编辑过程中缩放画布了,那么缓存当前编辑的内容
this.cacheEditingText = '' this.cacheEditingText = ''
this.hasBodyMousedown = false this.hasBodyMousedown = false
this.textNodePaddingX = 5
this.textNodePaddingY = 3
this.bindEvent() this.bindEvent()
} }
@ -214,7 +216,7 @@ export default class TextEdit {
this.registerTmpShortcut() this.registerTmpShortcut()
if (!this.textEditNode) { if (!this.textEditNode) {
this.textEditNode = document.createElement('div') this.textEditNode = document.createElement('div')
this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;background-color:#fff;box-shadow: 0 0 20px rgba(0,0,0,.5);padding: 3px 5px;margin-left: -5px;margin-top: -3px;outline: none; word-break: break-all;` this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;background-color:#fff;box-shadow: 0 0 20px rgba(0,0,0,.5);padding: ${this.textNodePaddingY}px ${this.textNodePaddingX}px;margin-left: -5px;margin-top: -3px;outline: none; word-break: break-all;`
this.textEditNode.setAttribute('contenteditable', true) this.textEditNode.setAttribute('contenteditable', true)
this.textEditNode.addEventListener('keyup', e => { this.textEditNode.addEventListener('keyup', e => {
e.stopPropagation() e.stopPropagation()
@ -240,6 +242,13 @@ export default class TextEdit {
handleInputPasteText(e) handleInputPasteText(e)
} }
}) })
this.textEditNode.addEventListener('input', () => {
this.mindMap.emit('node_text_edit_change', {
node: this.currentNode,
text: this.getEditText(),
richText: false
})
})
const targetNode = const targetNode =
this.mindMap.opt.customInnerElsAppendTo || document.body this.mindMap.opt.customInnerElsAppendTo || document.body
targetNode.appendChild(this.textEditNode) targetNode.appendChild(this.textEditNode)
@ -256,8 +265,10 @@ export default class TextEdit {
node.style.domText(this.textEditNode, scale, isMultiLine) node.style.domText(this.textEditNode, scale, isMultiLine)
this.textEditNode.style.zIndex = nodeTextEditZIndex this.textEditNode.style.zIndex = nodeTextEditZIndex
this.textEditNode.innerHTML = textLines.join('<br>') this.textEditNode.innerHTML = textLines.join('<br>')
this.textEditNode.style.minWidth = rect.width + 10 + 'px' this.textEditNode.style.minWidth =
this.textEditNode.style.minHeight = rect.height + 6 + 'px' rect.width + this.textNodePaddingX * 2 + 'px'
this.textEditNode.style.minHeight =
rect.height + this.textNodePaddingY * 2 + 'px'
this.textEditNode.style.left = rect.left + 'px' this.textEditNode.style.left = rect.left + 'px'
this.textEditNode.style.top = rect.top + 'px' this.textEditNode.style.top = rect.top + 'px'
this.textEditNode.style.display = 'block' this.textEditNode.style.display = 'block'
@ -280,6 +291,24 @@ export default class TextEdit {
this.cacheEditingText = '' this.cacheEditingText = ''
} }
// 更新文本编辑框的大小和位置
updateTextEditNode() {
if (this.mindMap.richText) {
this.mindMap.richText.updateTextEditNode()
return
}
if (!this.showTextEdit || !this.currentNode) {
return
}
const rect = this.currentNode._textData.node.node.getBoundingClientRect()
this.textEditNode.style.minWidth =
rect.width + this.textNodePaddingX * 2 + 'px'
this.textEditNode.style.minHeight =
rect.height + this.textNodePaddingY * 2 + 'px'
this.textEditNode.style.left = rect.left + 'px'
this.textEditNode.style.top = rect.top + 'px'
}
// 删除文本编辑元素 // 删除文本编辑元素
removeTextEditEl() { removeTextEditEl() {
if (this.mindMap.richText) { if (this.mindMap.richText) {

View File

@ -114,8 +114,10 @@ function createIconNode() {
} }
// 创建富文本节点 // 创建富文本节点
function createRichTextNode() { function createRichTextNode(specifyText) {
const { textAutoWrapWidth } = this.mindMap.opt let text =
typeof specifyText === 'string' ? specifyText : this.getData('text')
const { textAutoWrapWidth, emptyTextMeasureHeightText } = this.mindMap.opt
let g = new G() let g = new G()
// 重新设置富文本节点内容 // 重新设置富文本节点内容
let recoverText = false let recoverText = false
@ -129,7 +131,6 @@ function createRichTextNode() {
recoverText = true recoverText = true
} }
} }
let text = this.getData('text')
if (recoverText && !isUndef(text)) { if (recoverText && !isUndef(text)) {
// 判断节点内容是否是富文本 // 判断节点内容是否是富文本
let isRichText = checkIsRichText(text) let isRichText = checkIsRichText(text)
@ -153,7 +154,7 @@ function createRichTextNode() {
text: text text: text
}) })
} }
let html = `<div>${this.getData('text')}</div>` let html = `<div>${text}</div>`
if (!this.mindMap.commonCaches.measureRichtextNodeTextSizeEl) { if (!this.mindMap.commonCaches.measureRichtextNodeTextSizeEl) {
this.mindMap.commonCaches.measureRichtextNodeTextSizeEl = this.mindMap.commonCaches.measureRichtextNodeTextSizeEl =
document.createElement('div') document.createElement('div')
@ -174,7 +175,7 @@ function createRichTextNode() {
let { width, height } = el.getBoundingClientRect() let { width, height } = el.getBoundingClientRect()
// 如果文本为空,那么需要计算一个默认高度 // 如果文本为空,那么需要计算一个默认高度
if (height <= 0) { if (height <= 0) {
div.innerHTML = '<p>abc123我和你</p>' div.innerHTML = `<p>${emptyTextMeasureHeightText}</p>`
let elTmp = div.children[0] let elTmp = div.children[0]
elTmp.classList.add('smm-richtext-node-wrap') elTmp.classList.add('smm-richtext-node-wrap')
height = elTmp.getBoundingClientRect().height height = elTmp.getBoundingClientRect().height
@ -199,10 +200,12 @@ function createRichTextNode() {
} }
// 创建文本节点 // 创建文本节点
function createTextNode() { function createTextNode(specifyText) {
if (this.getData('richText')) { if (this.getData('richText')) {
return this.createRichTextNode() return this.createRichTextNode(specifyText)
} }
const text =
typeof specifyText === 'string' ? specifyText : this.getData('text')
if (this.getData('resetRichText')) { if (this.getData('resetRichText')) {
delete this.nodeData.data.resetRichText delete this.nodeData.data.resetRichText
} }
@ -212,10 +215,11 @@ function createTextNode() {
// 文本超长自动换行 // 文本超长自动换行
let textStyle = this.style.getTextFontStyle() let textStyle = this.style.getTextFontStyle()
let textArr = [] let textArr = []
if (!isUndef(this.getData('text'))) { if (!isUndef(text)) {
textArr = String(this.getData('text')).split(/\n/gim) textArr = String(text).split(/\n/gim)
} }
let maxWidth = this.mindMap.opt.textAutoWrapWidth const { textAutoWrapWidth: maxWidth, emptyTextMeasureHeightText } =
this.mindMap.opt
let isMultiLine = false let isMultiLine = false
textArr.forEach((item, index) => { textArr.forEach((item, index) => {
let arr = item.split('') let arr = item.split('')
@ -247,6 +251,13 @@ function createTextNode() {
g.add(node) g.add(node)
}) })
let { width, height } = g.bbox() let { width, height } = g.bbox()
// 如果文本为空,那么需要计算一个默认高度
if (height <= 0) {
const tmpNode = new Text().text(emptyTextMeasureHeightText)
this.style.text(tmpNode)
const tmpBbox = tmpNode.bbox()
height = tmpBbox.height
}
width = Math.min(Math.ceil(width), maxWidth) width = Math.min(Math.ceil(width), maxWidth)
height = Math.ceil(height) height = Math.ceil(height)
g.attr('data-width', width) g.attr('data-width', width)

View File

@ -57,6 +57,8 @@ class RichText {
this.cacheEditingText = '' this.cacheEditingText = ''
this.lostStyle = false this.lostStyle = false
this.isCompositing = false this.isCompositing = false
this.textNodePaddingX = 6
this.textNodePaddingY = 4
this.initOpt() this.initOpt()
this.extendQuill() this.extendQuill()
this.appendCss() this.appendCss()
@ -71,14 +73,17 @@ class RichText {
// 绑定事件 // 绑定事件
bindEvent() { bindEvent() {
this.onCompositionStart = this.onCompositionStart.bind(this) this.onCompositionStart = this.onCompositionStart.bind(this)
this.onCompositionUpdate = this.onCompositionUpdate.bind(this)
this.onCompositionEnd = this.onCompositionEnd.bind(this) this.onCompositionEnd = this.onCompositionEnd.bind(this)
window.addEventListener('compositionstart', this.onCompositionStart) window.addEventListener('compositionstart', this.onCompositionStart)
window.addEventListener('compositionupdate', this.onCompositionUpdate)
window.addEventListener('compositionend', this.onCompositionEnd) window.addEventListener('compositionend', this.onCompositionEnd)
} }
// 解绑事件 // 解绑事件
unbindEvent() { unbindEvent() {
window.removeEventListener('compositionstart', this.onCompositionStart) window.removeEventListener('compositionstart', this.onCompositionStart)
window.removeEventListener('compositionupdate', this.onCompositionUpdate)
window.removeEventListener('compositionend', this.onCompositionEnd) window.removeEventListener('compositionend', this.onCompositionEnd)
} }
@ -198,8 +203,8 @@ class RichText {
let scaleX = rect.width / originWidth let scaleX = rect.width / originWidth
let scaleY = rect.height / originHeight let scaleY = rect.height / originHeight
// 内边距 // 内边距
let paddingX = 6 let paddingX = this.textNodePaddingX
let paddingY = 4 let paddingY = this.textNodePaddingY
if (richTextEditFakeInPlace) { if (richTextEditFakeInPlace) {
let paddingValue = node.getPaddingVale() let paddingValue = node.getPaddingVale()
paddingX = paddingValue.paddingX paddingX = paddingValue.paddingX
@ -287,6 +292,20 @@ class RichText {
this.cacheEditingText = '' this.cacheEditingText = ''
} }
// 更新文本编辑框的大小和位置
updateTextEditNode() {
if (!this.node) return
const rect = this.node._textData.node.node.getBoundingClientRect()
const g = this.node._textData.node
const originWidth = g.attr('data-width')
const originHeight = g.attr('data-height')
this.textEditNode.style.minWidth =
originWidth + this.textNodePaddingX * 2 + 'px'
this.textEditNode.style.minHeight = originHeight + 'px'
this.textEditNode.style.left = rect.left + 'px'
this.textEditNode.style.top = rect.top + 'px'
}
// 删除文本编辑框元素 // 删除文本编辑框元素
removeTextEditEl() { removeTextEditEl() {
if (!this.textEditNode) return if (!this.textEditNode) return
@ -491,6 +510,11 @@ class RichText {
this.setTextStyleIfNotRichText(this.node) this.setTextStyleIfNotRichText(this.node)
this.lostStyle = false this.lostStyle = false
} }
this.mindMap.emit('node_text_edit_change', {
node: this.node,
text: this.getEditText(),
richText: true
})
}) })
// 拦截粘贴,只允许粘贴纯文本 // 拦截粘贴,只允许粘贴纯文本
// this.quill.clipboard.addMatcher(Node.TEXT_NODE, node => { // this.quill.clipboard.addMatcher(Node.TEXT_NODE, node => {
@ -545,6 +569,16 @@ class RichText {
this.isCompositing = true this.isCompositing = true
} }
// 中文输入中
onCompositionUpdate() {
if (!this.showTextEdit || !this.node) return
this.mindMap.emit('node_text_edit_change', {
node: this.node,
text: this.getEditText(),
richText: true
})
}
// 中文输入结束 // 中文输入结束
onCompositionEnd() { onCompositionEnd() {
if (!this.showTextEdit) { if (!this.showTextEdit) {