2021-07-04 22:53:36 +08:00

636 lines
18 KiB
JavaScript

import Style from './Style'
import {
resizeImgSize,
copyRenderTree,
imgToDataUrl
} from './utils'
import {
Image,
SVG,
Circle
} from '@svgdotjs/svg.js'
import btnsSvg from './svg/btns'
import iconsSvg from './svg/icons';
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-04-06 11:26:00
* @Desc: 节点类
*/
class Node {
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-04-06 11:26:17
* @Desc: 构造函数
*/
constructor(opt = {}) {
// 节点数据
this.nodeData = this.handleData(opt.data || {})
// id
this.uid = opt.uid
// 控制实例
this.mindMap = opt.mindMap
// 渲染实例
this.renderer = opt.renderer
// 渲染器
this.draw = opt.draw || null
// 主题配置
this.themeConfig = this.mindMap.themeConfig
// 样式实例
this.style = new Style(this, this.themeConfig)
// 是否是根节点
this.isRoot = opt.isRoot === undefined ? false : opt.isRoot
// 节点层级
this.layerIndex = opt.layerIndex === undefined ? 0 : opt.layerIndex
// 节点宽
this.width = opt.width || 0
// 节点高
this.height = opt.height || 0
// 节点文字内容部分高
this._textContentHeight = 0
// left
this.left = opt.left || 0
// top
this.top = opt.top || 0
// 父节点
this.parent = opt.parent || null
// 子节点
this.children = opt.children || []
// 节点内容的容器
this.group = null
// 节点内容是否发生了变化,是的话会重新计算和渲染
this.changed = true
// 文本节点
this.textNode = null
// icon间距
this._textContentItemMargin = 2
// 图片和文字节点的间距
this._blockContentMargin = 5
// 展开收缩按钮尺寸
this._expandBtnSize = 20
// 计算节点尺寸
this.refreshSize()
}
/**
* @Author: 王林
* @Date: 2021-06-20 10:12:31
* @Desc: 处理数据
*/
handleData(data) {
data.data.expand = data.data.expand === false ? false : true
data.data.isActive = data.data.isActive === true ? true : false
return data;
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-04-06 15:55:04
* @Desc: 添加子节点
*/
addChildren(node) {
this.children.push(node)
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-04-09 09:46:23
* @Desc: 刷新节点的宽高
*/
refreshSize() {
if (!this.changed) {
return;
}
let {
width,
height
} = this.getNodeRect()
this.width = width
this.height = height
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-04-06 14:52:17
* @Desc: 计算节点尺寸信息
*/
getNodeRect() {
// 宽高
let imgContentWidth = 0
let imgContentHeight = 0
let textContentWidth = 0
let textContentHeight = 0
// 存在图片
let imgObj = this.createImgNode()
if (imgObj) {
imgContentWidth = imgObj.width
imgContentHeight = imgObj.height
}
// 图标
let iconObjs = this.createIconNode()
if (iconObjs.length > 0) {
textContentWidth += iconObjs.reduce((sum, cur) => {
textContentHeight = Math.max(textContentHeight, cur.height)
return sum += cur.width + this._textContentItemMargin
}, 0)
}
// 文字
let textObj = this.createTextNode()
if (textObj) {
textContentWidth += textObj.width
textContentHeight = Math.max(textContentHeight, textObj.height)
}
// 超链接
let hyperlinkObj = this.createHyperlinkNode()
if (hyperlinkObj) {
textContentWidth += hyperlinkObj.width
textContentHeight = Math.max(textContentHeight, hyperlinkObj.height)
hyperlinkObj.node.remove()
}
// 标签
let tagObjs = this.createTagNode()
if (tagObjs.length > 0) {
textContentWidth += tagObjs.reduce((sum, cur) => {
textContentHeight = Math.max(textContentHeight, cur.height)
cur.node.remove()
return sum += cur.width + this._textContentItemMargin
}, 0)
}
// 备注
let noteObj = this.createNoteNode()
if (noteObj) {
textContentWidth += noteObj.width
textContentHeight = Math.max(textContentHeight, noteObj.height)
noteObj.node.remove()
}
// 文字内容部分的高度
this._textContentHeight = textContentHeight
// 间距
let margin = imgContentHeight > 0 && textContentHeight > 0 ? this._blockContentMargin : 0
let { paddingX, paddingY } = this.getPaddingVale()
return {
width: Math.max(imgContentWidth, textContentWidth) + paddingX * 2,
height: imgContentHeight + textContentHeight + paddingY * 2 + margin
}
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-04-09 14:06:17
* @Desc: 创建图片节点
*/
createImgNode() {
let img = this.nodeData.data.image
if (!img) {
return
}
let imgSize = this.getImgShowSize()
let node = new Image().load(img).size(...imgSize)
if (this.nodeData.data.imageTitle) {
node.attr('title', this.nodeData.data.imageTitle)
}
return {
node,
width: imgSize[0],
height: imgSize[1]
}
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-04-09 10:12:51
* @Desc: 获取图片显示宽高
*/
getImgShowSize() {
return resizeImgSize(this.nodeData.data.imageSize.width, this.nodeData.data.imageSize.height, this.themeConfig.imgMaxWidth, this.themeConfig.imgMaxHeight)
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-04-09 14:10:48
* @Desc: 创建icon节点
*/
createIconNode() {
let _data = this.nodeData.data
if (!_data.icon || _data.icon.length <= 0) {
return [];
}
let iconSize = this.themeConfig.iconSize
return _data.icon.map((item) => {
return {
node: SVG(iconsSvg.getNodeIconListIcon(item)).size(iconSize, iconSize),
width: iconSize,
height: iconSize
};
});
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-04-09 14:08:56
* @Desc: 创建文本节点
*/
createTextNode() {
let node = this.draw.text(this.nodeData.data.text || '')
this.style.text(node)
let {
width,
height
} = node.bbox()
let cloneNode = node.clone()
node.remove()
return {
node: cloneNode,
width,
height
}
}
/**
* @Author: 王林
* @Date: 2021-06-20 15:28:54
* @Desc: 创建超链接节点
*/
createHyperlinkNode() {
let { hyperlink, hyperlinkTitle } = this.nodeData.data
if (!hyperlink) {
return
}
let iconSize = this.themeConfig.iconSize
let node = this.draw.link(hyperlink).target('_blank')
node.node.addEventListener('click', (e) => {
e.stopPropagation()
})
if (hyperlinkTitle) {
node.attr('title', hyperlinkTitle)
}
node.rect(iconSize, iconSize).fill({ color: 'transparent' })
let iconNode = SVG(iconsSvg.hyperlink).size(iconSize, iconSize)
this.style.iconNode(iconNode)
node.add(iconNode)
return {
node: node,
width: iconSize,
height: iconSize
}
}
/**
* @Author: 王林
* @Date: 2021-06-20 19:49:15
* @Desc: 创建标签节点
*/
createTagNode() {
let tagData = this.nodeData.data.tag
if (!tagData || tagData.length <= 0) {
return [];
}
let nodes = []
tagData.slice(0, this.mindMap.opt.maxTag).forEach((item, index) => {
let tag = this.draw.group()
let text = this.draw.text(item).x(8).cy(10)
this.style.tagText(text, index)
let {
width,
height
} = text.bbox()
let cloneText = text.clone()
text.remove()
let rect = this.draw.rect(width + 16, 20)
this.style.tagRect(rect, index)
tag.add(rect).add(cloneText)
nodes.push({
node: tag,
width: width + 16,
height: 20
})
})
return nodes;
}
/**
* @Author: 王林
* @Date: 2021-06-20 21:19:36
* @Desc: 创建备注节点
*/
createNoteNode() {
if (!this.nodeData.data.note) {
return null;
}
let node = this.draw.group().attr('cursor', 'pointer')
let iconSize = this.themeConfig.iconSize
node.add(this.draw.rect(iconSize, iconSize).fill({ color: 'transparent' }))
let iconNode = SVG(iconsSvg.note).size(iconSize, iconSize)
this.style.iconNode(iconNode)
node.add(iconNode)
let el = document.createElement('div')
el.style.cssText = `
position: absolute;
padding: 10px;
border-radius: 5px;
box-shadow: 0 2px 5px rgb(0 0 0 / 10%);
display: none;
background-color: #fff;
`
el.innerText = this.nodeData.data.note
document.body.appendChild(el)
node.on('mouseover', () => {
let { left, top } = node.node.getBoundingClientRect()
el.style.left = left + 'px'
el.style.top = top + iconSize + 'px'
el.style.display = 'block'
})
node.on('mouseout', () => {
el.style.display = 'none'
})
return {
node,
width: iconSize,
height: iconSize
};
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-04-09 11:10:11
* @Desc: 创建内容节点
*/
createNode() {
let {
left,
top,
width,
height,
_textContentHeight,
_textContentItemMargin
} = this
let { paddingY } = this.getPaddingVale()
// 创建组
this.group = this.draw.group()
// 节点矩形
this.style.rect(this.group.rect(width, height).x(left).y(top))
// 图片节点
let imgObj = this.createImgNode()
let imgHeight = 0
if (imgObj) {
imgHeight = imgObj.height
this.group.add(imgObj.node)
imgObj.node.cx(left + width / 2).y(top + paddingY)
}
// 内容节点
let textContentNested = this.draw.group()
let textContentOffsetX = 0
// icon
let iconObjs = this.createIconNode()
let iconNested = this.draw.group()
if (iconObjs && iconObjs.length > 0) {
let iconLeft = 0
iconObjs.forEach((item) => {
item.node.x(textContentOffsetX + iconLeft).y((_textContentHeight - item.height) / 2)
iconNested.add(item.node)
iconLeft += item.width + _textContentItemMargin
})
textContentNested.add(iconNested)
textContentOffsetX += iconLeft
}
// 文字
let textObj = this.createTextNode()
if (textObj) {
textObj.node.x(textContentOffsetX).y(0)
this.textNode = textObj
textContentNested.add(textObj.node)
textContentOffsetX += textObj.width + _textContentItemMargin
}
// 超链接
let hyperlinkObj = this.createHyperlinkNode()
if (hyperlinkObj) {
hyperlinkObj.node.translate(textContentOffsetX, (_textContentHeight - hyperlinkObj.height) / 2)
textContentNested.add(hyperlinkObj.node)
textContentOffsetX += hyperlinkObj.width + _textContentItemMargin
}
// 标签
let tagObjs = this.createTagNode()
let tagNested = this.draw.group()
if (tagObjs && tagObjs.length > 0) {
let tagLeft = 0
tagObjs.forEach((item) => {
item.node.x(textContentOffsetX + tagLeft).y((_textContentHeight - item.height) / 2)
tagNested.add(item.node)
tagLeft += item.width + _textContentItemMargin
})
textContentNested.add(tagNested)
textContentOffsetX += tagLeft
}
// 备注
let noteObj = this.createNoteNode()
if (noteObj) {
noteObj.node.translate(textContentOffsetX, (_textContentHeight - noteObj.height) / 2)
textContentNested.add(noteObj.node)
textContentOffsetX += noteObj.width
}
// 文字内容整体
textContentNested.translate(
left + width / 2 - textContentNested.bbox().width / 2,
top + imgHeight + paddingY + (imgHeight > 0 && _textContentHeight > 0 ? this._blockContentMargin : 0)
)
this.group.add(textContentNested)
// 单击事件,选中节点
this.group.click((e) => {
e.stopPropagation()
if (this.nodeData.data.isActive) {
return;
}
this.mindMap.emit('before_node_active', this, this.renderer.activeNodeList)
this.renderer.clearActive()
this.mindMap.execCommand('UPDATE_NODE_DATA', this, {
isActive: !this.nodeData.data.isActive
})
this.renderNode()
this.renderer.activeNodeList.push(this)
this.mindMap.emit('node_active', this, this.renderer.activeNodeList)
})
// 双击事件
this.group.dblclick(() => {
this.mindMap.emit('node_dblclick', this)
})
}
/**
* @Author: 王林
* @Date: 2021-07-04 20:20:09
* @Desc: 渲染节点到画布
*/
renderNode() {
if (this.group) {
this.group.remove()
}
this.createNode()
this.draw.add(this.group)
}
/**
* @Author: 王林
* @Date: 2021-07-04 22:47:01
* @Desc: 更新整体位置
*/
updatePos() {
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-04-07 13:55:58
* @Desc: 渲染
*/
render() {
// 连线
this.renderLine()
// 按钮
this.renderExpandBtn()
// 节点
if (this.changed) {
this.renderNode()
} else {
this.updatePos()
}
this.changed = false
// 子节点
if (this.children && this.children.length && this.nodeData.data.expand !== false) {
this.children.forEach((child) => {
child.render()
})
}
}
/**
* @Author: 王林
* @Date: 2021-04-10 22:01:53
* @Desc: 连线
*/
renderLine() {
if (this.nodeData.data.expand === false) {
return;
}
let lines = this.renderer.layout.renderLine(this)
lines.forEach((line) => {
this.style.line(line)
})
}
/**
* @Author: 王林
* @Date: 2021-04-11 19:47:01
* @Desc: 展开收缩按钮
*/
renderExpandBtn() {
if (this.children.length <= 0 || this.isRoot) {
return;
}
let g = this.draw.group()
let iconSvg
if (this.nodeData.data.expand === false) {
iconSvg = btnsSvg.open
} else {
iconSvg = btnsSvg.close
}
let node = SVG(iconSvg).size(this._expandBtnSize, this._expandBtnSize)
let fillNode = new Circle().size(this._expandBtnSize)
this.renderer.layout.renderExpandBtn(this, [node, fillNode])
node.dx(0).dy(-10)
fillNode.dx(0).dy(-10)
this.style.iconBtn(node, fillNode)
g.mouseover(() => {
g.css({
cursor: 'pointer'
})
})
g.mouseout(() => {
g.css({
cursor: 'auto'
})
})
g.click(() => {
// 展开收缩
this.mindMap.execCommand('UPDATE_NODE_DATA', this, {
expand: !this.mindMap.nodeData.data.expand
}, children)
this.mindMap.emit('expand_btn_click', this)
})
g.add(fillNode)
g.add(node)
}
/**
* @Author: 王林
* @Date: 2021-06-20 22:51:57
* @Desc: 获取padding值
*/
getPaddingVale() {
return {
paddingX: this.getStyle('paddingX', true, this.nodeData.data.isActive),
paddingY: this.getStyle('paddingY', true, this.nodeData.data.isActive)
};
}
/**
* @Author: 王林
* @Date: 2021-05-04 21:48:49
* @Desc: 获取某个样式
*/
getStyle(prop, root, isActive) {
let v = this.style.merge(prop, root, isActive)
return v === undefined ? '' : v
}
/**
* @Author: 王林
* @Date: 2021-05-04 22:18:07
* @Desc: 修改某个样式
*/
setStyle(prop, value, isActive) {
if (isActive) {
this.mindMap.execCommand('UPDATE_NODE_DATA', this, {
activeStyle: {
...(this.nodeData.data.activeStyle || {}),
[prop]: value
}
})
} else {
this.mindMap.execCommand('UPDATE_NODE_DATA', this, {
[prop]: value
})
}
this.renderNode()
}
/**
* @Author: 王林
* @Date: 2021-06-22 22:04:02
* @Desc: 获取数据
*/
getData(key) {
return key ? this.nodeData.data[key] || '' : this.nodeData.data;
}
/**
* @Author: 王林
* @Date: 2021-06-22 22:12:01
* @Desc: 设置数据
*/
setData(data = {}) {
this.mindMap.execCommand('UPDATE_NODE_DATA', this, data)
}
}
export default Node