429 lines
12 KiB
JavaScript
429 lines
12 KiB
JavaScript
import Quill from 'quill'
|
|
import 'quill/dist/quill.snow.css'
|
|
import './css/quill.css'
|
|
import html2canvas from 'html2canvas'
|
|
import { Image as SvgImage } from '@svgdotjs/svg.js'
|
|
import { walk } from './utils'
|
|
|
|
let extended = false
|
|
|
|
// 扩展quill的字体列表
|
|
let fontFamilyList = [
|
|
'宋体, SimSun, Songti SC',
|
|
'微软雅黑, Microsoft YaHei',
|
|
'楷体, 楷体_GB2312, SimKai, STKaiti',
|
|
'黑体, SimHei, Heiti SC',
|
|
'隶书, SimLi',
|
|
'andale mono',
|
|
'arial, helvetica, sans-serif',
|
|
'arial black, avant garde',
|
|
'comic sans ms',
|
|
'impact, chicago',
|
|
'times new roman',
|
|
'sans-serif',
|
|
'serif'
|
|
]
|
|
|
|
// 扩展quill的字号列表
|
|
let fontSizeList = new Array(100).fill(0).map((_, index) => {
|
|
return index + 'px'
|
|
})
|
|
|
|
// 节点支持富文本编辑功能
|
|
class RichText {
|
|
constructor({ mindMap, pluginOpt }) {
|
|
this.mindMap = mindMap
|
|
this.pluginOpt = pluginOpt
|
|
this.textEditNode = null
|
|
this.showTextEdit = false
|
|
this.quill = null
|
|
this.range = null
|
|
this.lastRange = null
|
|
this.node = null
|
|
this.initOpt()
|
|
this.extendQuill()
|
|
}
|
|
|
|
// 处理选项参数
|
|
initOpt() {
|
|
if (
|
|
this.pluginOpt.fontFamilyList &&
|
|
Array.isArray(this.pluginOpt.fontFamilyList)
|
|
) {
|
|
fontFamilyList = this.pluginOpt.fontFamilyList
|
|
}
|
|
if (
|
|
this.pluginOpt.fontSizeList &&
|
|
Array.isArray(this.pluginOpt.fontSizeList)
|
|
) {
|
|
fontSizeList = this.pluginOpt.fontSizeList
|
|
}
|
|
}
|
|
|
|
// 扩展quill编辑器
|
|
extendQuill() {
|
|
if (extended) {
|
|
return
|
|
}
|
|
extended = true
|
|
|
|
// 扩展quill的字体列表
|
|
const FontAttributor = Quill.import('attributors/class/font')
|
|
FontAttributor.whitelist = fontFamilyList
|
|
Quill.register(FontAttributor, true)
|
|
|
|
const FontStyle = Quill.import('attributors/style/font')
|
|
FontStyle.whitelist = fontFamilyList
|
|
Quill.register(FontStyle, true)
|
|
|
|
// 扩展quill的字号列表
|
|
const SizeAttributor = Quill.import('attributors/class/size')
|
|
SizeAttributor.whitelist = fontSizeList
|
|
Quill.register(SizeAttributor, true)
|
|
|
|
const SizeStyle = Quill.import('attributors/style/size')
|
|
SizeStyle.whitelist = fontSizeList
|
|
Quill.register(SizeStyle, true)
|
|
}
|
|
|
|
// 显示文本编辑控件
|
|
showEditText(node, rect) {
|
|
if (this.showTextEdit) {
|
|
return
|
|
}
|
|
this.node = node
|
|
if (!rect) rect = node._textData.node.node.getBoundingClientRect()
|
|
this.mindMap.emit('before_show_text_edit')
|
|
this.mindMap.renderer.textEdit.registerTmpShortcut()
|
|
if (!this.textEditNode) {
|
|
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);outline: none; word-break: break-all;`
|
|
document.body.appendChild(this.textEditNode)
|
|
}
|
|
// 原始宽高
|
|
let g = node._textData.node
|
|
let originWidth = g.attr('data-width')
|
|
let originHeight = g.attr('data-height')
|
|
console.log(`node`, node, rect, originWidth, originHeight)
|
|
this.textEditNode.style.minWidth = originWidth + 'px'
|
|
this.textEditNode.style.minHeight = originHeight + 'px'
|
|
this.textEditNode.style.left =
|
|
rect.left + (rect.width - originWidth) / 2 + 'px'
|
|
this.textEditNode.style.top =
|
|
rect.top + (rect.height - originHeight) / 2 + 'px'
|
|
this.textEditNode.style.display = 'block'
|
|
this.textEditNode.style.maxWidth = this.mindMap.opt.textAutoWrapWidth + 'px'
|
|
this.textEditNode.style.transform = `scale(${rect.width / originWidth}, ${
|
|
rect.height / originHeight
|
|
})`
|
|
if (!node.nodeData.data.richText) {
|
|
// 还不是富文本的情况
|
|
let text = node.nodeData.data.text.split(/\n/gim).join('<br>')
|
|
let html = `<p>${text}</p>`
|
|
this.textEditNode.innerHTML = html
|
|
} else {
|
|
this.textEditNode.innerHTML = node.nodeData.data.text
|
|
}
|
|
this.initQuillEditor()
|
|
document.querySelector('.ql-editor').style.minHeight = originHeight + 'px'
|
|
this.showTextEdit = true
|
|
this.selectAll()
|
|
if (!node.nodeData.data.richText) {
|
|
// 如果是非富文本的情况,需要手动应用文本样式
|
|
this.setTextStyleIfNotRichText(node)
|
|
}
|
|
}
|
|
|
|
// 如果是非富文本的情况,需要手动应用文本样式
|
|
setTextStyleIfNotRichText(node) {
|
|
let style = {
|
|
font: node.style.merge('fontFamily'),
|
|
color: node.style.merge('color'),
|
|
italic: node.style.merge('fontStyle') === 'italic',
|
|
bold: node.style.merge('fontWeight') === 'bold',
|
|
size: node.style.merge('fontSize') + 'px',
|
|
underline: node.style.merge('textDecoration') === 'underline',
|
|
strike: node.style.merge('textDecoration') === 'line-through'
|
|
}
|
|
this.formatText(style)
|
|
}
|
|
|
|
// 隐藏文本编辑控件,即完成编辑
|
|
hideEditText() {
|
|
if (!this.showTextEdit) {
|
|
return
|
|
}
|
|
let html = this.quill.container.firstChild.innerHTML
|
|
// 去除最后的空行
|
|
html = html.replace(/<p><br><\/p>$/, '')
|
|
this.mindMap.renderer.activeNodeList.forEach(node => {
|
|
this.mindMap.execCommand('SET_NODE_TEXT', node, html, true)
|
|
if (node.isGeneralization) {
|
|
// 概要节点
|
|
node.generalizationBelongNode.updateGeneralization()
|
|
}
|
|
this.mindMap.render()
|
|
})
|
|
this.mindMap.emit(
|
|
'hide_text_edit',
|
|
this.textEditNode,
|
|
this.mindMap.renderer.activeNodeList
|
|
)
|
|
this.textEditNode.style.display = 'none'
|
|
this.showTextEdit = false
|
|
this.mindMap.emit('rich_text_selection_change', false)
|
|
this.node = null
|
|
}
|
|
|
|
// 初始化Quill富文本编辑器
|
|
initQuillEditor() {
|
|
this.quill = new Quill(this.textEditNode, {
|
|
modules: {
|
|
toolbar: false,
|
|
keyboard: {
|
|
bindings: {
|
|
enter: {
|
|
key: 13,
|
|
handler: function () {
|
|
// 覆盖默认的回车键换行
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
theme: 'snow'
|
|
})
|
|
this.quill.on('selection-change', range => {
|
|
this.lastRange = this.range
|
|
this.range = null
|
|
if (range) {
|
|
let bounds = this.quill.getBounds(range.index, range.length)
|
|
let rect = this.textEditNode.getBoundingClientRect()
|
|
let rectInfo = {
|
|
left: bounds.left + rect.left,
|
|
top: bounds.top + rect.top,
|
|
right: bounds.right + rect.left,
|
|
bottom: bounds.bottom + rect.top,
|
|
width: bounds.width
|
|
}
|
|
let formatInfo = this.quill.getFormat(range.index, range.length)
|
|
let hasRange = false
|
|
if (range.length == 0) {
|
|
hasRange = false
|
|
} else {
|
|
this.range = range
|
|
hasRange = true
|
|
}
|
|
this.mindMap.emit(
|
|
'rich_text_selection_change',
|
|
hasRange,
|
|
rectInfo,
|
|
formatInfo
|
|
)
|
|
}
|
|
})
|
|
}
|
|
|
|
// 选中全部
|
|
selectAll() {
|
|
this.quill.setSelection(0, this.quill.getLength())
|
|
}
|
|
|
|
// 格式化当前选中的文本
|
|
formatText(config = {}) {
|
|
if (!this.range && !this.lastRange) return
|
|
this.syncFormatToNodeConfig(config)
|
|
let rangeLost = !this.range
|
|
let range = rangeLost ? this.lastRange : this.range
|
|
this.quill.formatText(range.index, range.length, config)
|
|
if (rangeLost) {
|
|
this.quill.setSelection(this.lastRange.index, this.lastRange.length)
|
|
}
|
|
}
|
|
|
|
// 格式化指定范围的文本
|
|
formatRangeText(range, config = {}) {
|
|
if (!range) return
|
|
this.syncFormatToNodeConfig(config)
|
|
this.quill.formatText(range.index, range.length, config)
|
|
}
|
|
|
|
// 格式化所有文本
|
|
formatAllText(config = {}) {
|
|
this.syncFormatToNodeConfig(config)
|
|
this.quill.formatText(0, this.quill.getLength(), config)
|
|
}
|
|
|
|
// 同步格式化到节点样式配置
|
|
syncFormatToNodeConfig(config) {
|
|
if (!this.node) return
|
|
let data = this.richTextStyleToNormalStyle(config)
|
|
this.mindMap.renderer.setNodeData(this.node, data)
|
|
}
|
|
|
|
// 将普通节点样式对象转换成富文本样式对象
|
|
normalStyleToRichTextStyle(style) {
|
|
let config = {}
|
|
Object.keys(style).forEach(prop => {
|
|
let value = style[prop]
|
|
switch (prop) {
|
|
case 'fontFamily':
|
|
config.font = value
|
|
break
|
|
case 'fontSize':
|
|
config.size = value + 'px'
|
|
break
|
|
case 'fontWeight':
|
|
config.bold = value === 'bold'
|
|
break
|
|
case 'fontStyle':
|
|
config.italic = value === 'italic'
|
|
break
|
|
case 'textDecoration':
|
|
config.underline = value === 'underline'
|
|
config.strike = value === 'line-through'
|
|
case 'color':
|
|
config.color = value
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
})
|
|
return config
|
|
}
|
|
|
|
// 将富文本样式对象转换成普通节点样式对象
|
|
richTextStyleToNormalStyle(config) {
|
|
let data = {}
|
|
Object.keys(config).forEach(prop => {
|
|
let value = config[prop]
|
|
switch (prop) {
|
|
case 'font':
|
|
data.fontFamily = value
|
|
break
|
|
case 'size':
|
|
data.fontSize = parseFloat(value)
|
|
break
|
|
case 'bold':
|
|
data.fontWeight = value ? 'bold' : 'normal'
|
|
break
|
|
case 'italic':
|
|
data.fontStyle = value ? 'italic' : 'normal'
|
|
break
|
|
case 'underline':
|
|
data.textDecoration = value ? 'underline' : 'none'
|
|
break
|
|
case 'strike':
|
|
data.textDecoration = value ? 'line-through' : 'none'
|
|
break
|
|
case 'color':
|
|
data.color = value
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
})
|
|
return data
|
|
}
|
|
|
|
// 将svg中嵌入的dom元素转换成图片
|
|
async _handleSvgDomElements(svg) {
|
|
svg = svg.clone()
|
|
let foreignObjectList = svg.find('foreignObject')
|
|
let task = foreignObjectList.map(async item => {
|
|
let clone = item.first().node.cloneNode(true)
|
|
let div = document.createElement('div')
|
|
div.style.cssText = `position: fixed; left: -999999px;`
|
|
div.appendChild(clone)
|
|
this.mindMap.el.appendChild(div)
|
|
let canvas = await html2canvas(clone, {
|
|
backgroundColor: null
|
|
})
|
|
this.mindMap.el.removeChild(div)
|
|
let imgNode = new SvgImage()
|
|
.load(canvas.toDataURL())
|
|
.size(canvas.width, canvas.height)
|
|
item.replace(imgNode)
|
|
})
|
|
await Promise.all(task)
|
|
return {
|
|
svg: svg,
|
|
svgHTML: svg.svg()
|
|
}
|
|
}
|
|
|
|
// 将svg中嵌入的dom元素转换成图片
|
|
handleSvgDomElements(svg) {
|
|
return new Promise((resolve, reject) => {
|
|
svg = svg.clone()
|
|
let foreignObjectList = svg.find('foreignObject')
|
|
let index = 0
|
|
let len = foreignObjectList.length
|
|
let transform = async () => {
|
|
this.mindMap.emit('transforming-dom-to-images', index, len)
|
|
try {
|
|
let item = foreignObjectList[index++]
|
|
let parent = item.parent()
|
|
let clone = item.first().node.cloneNode(true)
|
|
let div = document.createElement('div')
|
|
div.style.cssText = `position: fixed; left: -999999px;`
|
|
div.appendChild(clone)
|
|
this.mindMap.el.appendChild(div)
|
|
let canvas = await html2canvas(clone, {
|
|
backgroundColor: null
|
|
})
|
|
this.mindMap.el.removeChild(div)
|
|
let imgNode = new SvgImage()
|
|
.load(canvas.toDataURL())
|
|
.size(canvas.width, canvas.height)
|
|
.x((parent ? parent.attr('data-offsetx') : 0) || 0)
|
|
item.replace(imgNode)
|
|
if (index <= len - 1) {
|
|
setTimeout(() => {
|
|
transform()
|
|
}, 0)
|
|
} else {
|
|
resolve({
|
|
svg: svg,
|
|
svgHTML: svg.svg()
|
|
})
|
|
}
|
|
} catch (error) {
|
|
reject(error)
|
|
}
|
|
}
|
|
if (len > 0) transform()
|
|
})
|
|
}
|
|
|
|
// 将所有节点转换成非富文本节点
|
|
transformAllNodesToNormalNode() {
|
|
let div = document.createElement('div')
|
|
walk(
|
|
this.mindMap.renderer.renderTree,
|
|
null,
|
|
node => {
|
|
if (node.data.richText) {
|
|
node.data.richText = false
|
|
div.innerHTML = node.data.text
|
|
node.data.text = div.textContent
|
|
}
|
|
},
|
|
null,
|
|
true,
|
|
0,
|
|
0
|
|
)
|
|
this.mindMap.reRender()
|
|
}
|
|
|
|
// 插件被移除前做的事情
|
|
beforePluginRemove() {
|
|
this.transformAllNodesToNormalNode()
|
|
}
|
|
}
|
|
|
|
RichText.instanceName = 'richText'
|
|
|
|
export default RichText
|