treechartjs
Advanced tools
Comparing version 0.0.1 to 0.0.2
1458
dist/index.js
@@ -1,1457 +0,1 @@ | ||
(function(l, r) { if (l.getElementById('livereloadscript')) return; r = l.createElement('script'); r.async = 1; r.src = '//' + (window.location.host || 'localhost').split(':')[0] + ':35729/livereload.js?snipver=1'; r.id = 'livereloadscript'; l.getElementsByTagName('head')[0].appendChild(r) })(window.document); | ||
(function () { | ||
'use strict'; | ||
class FollowScroll { | ||
constructor(option) { | ||
const { scrollContainer, eventContainer, autoScrollTriggerDistance, autoScrollSpeed } = option; | ||
this.scrollContainer = scrollContainer; | ||
this.eventContainer = eventContainer; | ||
this.autoScrollSpeed = autoScrollSpeed; | ||
this.triggerDistance = autoScrollTriggerDistance || 0; | ||
this.targetNode = null; | ||
this.interval = 0; | ||
this.directData = { | ||
left: false, | ||
right: false, | ||
top: false, | ||
bottom: false | ||
}; | ||
this.setEvent(); | ||
} | ||
setEvent() { | ||
const { eventContainer } = this; | ||
this.mouseMoveHandler = e => { | ||
const { button, movementX, movementY } = e; | ||
// 处理Chrome76版本长按不移动也会触发的情况 | ||
if (movementX === 0 && movementY === 0) return | ||
if (button || !this.targetNode) return | ||
this.setDirectData(e); | ||
this.triggerScroll(); | ||
}; | ||
eventContainer.addEventListener('mousemove', this.mouseMoveHandler); | ||
} | ||
setDirectData(event) { | ||
const { movementX, movementY } = event; | ||
const { scrollContainer, targetNode, triggerDistance, directData } = this; | ||
const { scrollLeft, scrollTop, scrollWidth, scrollHeight, clientWidth, clientHeight } = scrollContainer; | ||
const { left: scrollContainerLeft, right: scrollContainerRight, top: scrollContainerTop, bottom: scrollContainerBottom } = scrollContainer.getBoundingClientRect(); | ||
const { left: targetNodeLeft, right: targetNodeRight, top: targetNodeTop, bottom: targetNodeBottom } = targetNode.getBoundingClientRect(); | ||
if (targetNodeLeft - scrollContainerLeft < triggerDistance) directData.left = true; | ||
if (targetNodeTop - scrollContainerTop < triggerDistance) directData.top = true; | ||
if (scrollContainerRight - targetNodeRight < triggerDistance) directData.right = true; | ||
if (scrollContainerBottom - targetNodeBottom < triggerDistance) directData.bottom = true; | ||
if (movementX > 0 || scrollLeft === 0) directData.left = false; | ||
if (movementY > 0 || scrollTop === 0) directData.top = false; | ||
if (movementX < 0 || scrollLeft + clientWidth >= scrollWidth) directData.right = false; | ||
if (movementY < 0 || scrollTop + clientHeight >= scrollHeight) directData.bottom = false; | ||
} | ||
triggerScroll() { | ||
const { directData, scrollContainer, autoScrollSpeed } = this; | ||
let existDirect = false; | ||
for (const key in directData) { | ||
if (directData[key]) { | ||
existDirect = true; | ||
break | ||
} | ||
} | ||
if (!existDirect) return this.stop(true) | ||
if (this.interval) return | ||
this.interval = setInterval(() => { | ||
let { scrollLeft, scrollTop } = scrollContainer; | ||
if (directData.left) scrollLeft -= autoScrollSpeed; | ||
if (directData.right) scrollLeft += autoScrollSpeed; | ||
if (directData.top) scrollTop -= autoScrollSpeed; | ||
if (directData.bottom) scrollTop += autoScrollSpeed; | ||
scrollContainer.scrollLeft = scrollLeft; | ||
scrollContainer.scrollTop = scrollTop; | ||
}, 20); | ||
} | ||
start(targetNode) { | ||
this.targetNode = targetNode; | ||
} | ||
stop(keepTargetNode = false) { | ||
if (!keepTargetNode) this.targetNode = null; | ||
if (this.interval) { | ||
clearInterval(this.interval); | ||
this.interval = 0; | ||
} | ||
} | ||
destroy() { | ||
this.eventContainer.removeEventListener('mousemove', this.mouseMoveHandler); | ||
this.targetNode = this.scrollContainer = this.eventContainer = null; | ||
this.interval = 0; | ||
} | ||
} | ||
const isElement = data => /HTML/.test(Object.prototype.toString.call(data)) && data.nodeType === 1; | ||
const isNumber = data => /Number/.test(Object.prototype.toString.call(data)); | ||
const setNotAllowEffect = node => node.classList.add('show-not-allow'); | ||
// 求数组的交集 | ||
const getArrayIntersection = (...arrays) => { | ||
const arrayCount = arrays.length; | ||
if (arrayCount < 2) return [] | ||
const result = []; | ||
const countMap = {}; | ||
arrays.reduce((a, b) => a.concat(b), []).forEach(item => { | ||
if (countMap[item]) { | ||
countMap[item]++; | ||
countMap[item] === arrayCount && result.push(item); | ||
} else { | ||
countMap[item] = 1; | ||
} | ||
}); | ||
return result | ||
}; | ||
class TreeChart { | ||
/* ======== API ======== */ | ||
getKeyByElement(nodeElement) { | ||
if (!isElement(nodeElement)) return null | ||
return nodeElement.classList.contains('tree-chart-node') ? nodeElement.getAttribute('data-key') : null | ||
} | ||
getNodeElement(key) { | ||
return this.nodesContainer.querySelector(`.tree-chart-item-${key}`) | ||
} | ||
getPreviousKey(key) { | ||
try { | ||
const nodeElement = this.getNodeElement(key); | ||
return this.getKeyByElement(nodeElement.parentElement.previousElementSibling.querySelector('.tree-chart-node')) | ||
} catch (e) { | ||
return null | ||
} | ||
} | ||
getNextKey(key) { | ||
try { | ||
const nodeElement = this.getNodeElement(key); | ||
return this.getKeyByElement(nodeElement.parentElement.nextElementSibling.querySelector('.tree-chart-node')) | ||
} catch (e) { | ||
return null | ||
} | ||
} | ||
getParentKey(key) { | ||
try { | ||
const nodeElement = this.getNodeElement(key); | ||
return this.getKeyByElement(nodeElement.parentElement.parentElement.previousElementSibling) | ||
} catch (e) { | ||
return null | ||
} | ||
} | ||
getChildrenKeys(key) { | ||
const childrenKeys = this.getNodeElement(key).getAttribute('data-children'); | ||
if (!childrenKeys) return [] | ||
return childrenKeys.split(',') | ||
} | ||
existChildren(key) { | ||
const nodeElement = this.getNodeElement(key); | ||
if (!nodeElement) return false | ||
return Boolean(nodeElement.getAttribute('data-children')) | ||
} | ||
insertNode(targetKey, origin, type) { | ||
const targetNode = this.getNodeElement(targetKey); | ||
// 限制不能给根节点添加兄弟元素 | ||
if (/next|previous/.test(type) && targetNode === this.rootNode) return | ||
// 处理origin部分 | ||
// 是否需要新建节点 | ||
const needCreateNode = /Object/.test(Object.prototype.toString.call(origin)); | ||
let originKey = null; | ||
let originNode = null; | ||
let originNodeContainer = null; | ||
if (needCreateNode) { | ||
originKey = this.getKeyField(origin); | ||
originNode = this.createNode(origin); | ||
originNodeContainer = this.createNodeContainer(); | ||
originNodeContainer.appendChild(originNode); | ||
this.setNodeEvent(originNode); | ||
} else { | ||
originKey = origin; | ||
originNode = this.getNodeElement(originKey); | ||
originNodeContainer = originNode.parentElement; | ||
// 修改原先节点父节点的data-children属性 | ||
const originParentKey = this.getParentKey(originKey); | ||
this.removeChildrenKey(originParentKey, originKey); | ||
// 如果移动节点后原位置没有兄弟节点的话移除childrenContainer容器和展开收起按钮 | ||
if (!this.existChildren(originParentKey)) { | ||
this.removeChildrenContainer(originParentKey); | ||
this.allowFold && this.removeFoldButton(originParentKey); | ||
} | ||
} | ||
// 处理target部分 | ||
if (type === 'child') { | ||
// 本身存在子节点的情况,直接插入 | ||
if (this.existChildren(targetKey)) { | ||
this.getChildrenContainer(targetKey).appendChild(originNodeContainer); | ||
// 如果目标节点是折叠状态,插入子节点后自动展开 | ||
this.nodeIsFold(targetKey) && this.toggleFold(targetKey); | ||
} else { | ||
// 没有任何子节点的话创建一个容器 | ||
const newChildrenContainer = this.createChildrenContainer(); | ||
newChildrenContainer.appendChild(originNodeContainer); | ||
targetNode.parentElement.appendChild(newChildrenContainer); | ||
// 没有展开按钮需要新增 | ||
this.createFoldButton(targetNode); | ||
} | ||
this.addChildrenKey(targetKey, originKey); | ||
} else { | ||
const targetParentKey = this.getParentKey(targetKey); | ||
const parentChildrenContainer = this.getChildrenContainer(targetParentKey); | ||
const targetNodeContainer = targetNode.parentElement; | ||
if (type === 'previous') parentChildrenContainer.insertBefore(originNodeContainer, targetNodeContainer); | ||
if (type === 'next') parentChildrenContainer.insertBefore(originNodeContainer, targetNodeContainer.nextElementSibling); | ||
this.addChildrenKey(targetParentKey, originKey); | ||
} | ||
this.reloadLink(); | ||
} | ||
removeNode(targetKey) { | ||
const targetNode = this.getNodeElement(targetKey); | ||
if (!targetNode) return | ||
// 不支持移除root节点 | ||
if (targetNode === this.rootNode) return console.warn('not allow remove root node') | ||
// 修改父节点的data-children | ||
const parentNodeKey = this.getParentKey(targetKey); | ||
this.removeChildrenKey(parentNodeKey, targetKey); | ||
// 移除当前节点 | ||
const targetNodeContainer = targetNode.parentElement; | ||
targetNodeContainer.parentElement.removeChild(targetNodeContainer); | ||
// 如果当前节点是唯一的子节点就移除父节点的子容器 | ||
if (!this.existChildren(parentNodeKey)) { | ||
this.removeChildrenContainer(parentNodeKey); | ||
this.allowFold && this.removeFoldButton(parentNodeKey); | ||
} | ||
this.reloadLink(); | ||
} | ||
nodeIsFold(targetKey) { | ||
return this.getFoldButton(targetKey).classList.contains('is-fold') | ||
} | ||
toggleFold(targetKey) { | ||
const foldButton = this.getFoldButton(targetKey); | ||
if (!foldButton) return | ||
const childNodeContainer = this.getChildrenContainer(targetKey); | ||
if (this.nodeIsFold(targetKey)) { | ||
childNodeContainer.classList.remove('is-hidden'); | ||
foldButton.classList.remove('is-fold'); | ||
} else { | ||
childNodeContainer.classList.add('is-hidden'); | ||
foldButton.classList.add('is-fold'); | ||
} | ||
this.reloadLink(); | ||
} | ||
reRenderNode(targetKey, data) { | ||
const oldNodeKey = targetKey.toString(); | ||
const newNodeKey = this.getKeyField(data); | ||
const node = this.getNodeElement(oldNodeKey); | ||
const childrenKeys = this.getChildrenKeys(targetKey); | ||
// 更新key | ||
if (newNodeKey !== oldNodeKey) { | ||
const { linkContainer } = this; | ||
const parentNodeKey = this.getParentKey(oldNodeKey); | ||
// 替换父节点的children-key | ||
this.replaceChildrenKey(parentNodeKey, oldNodeKey, newNodeKey); | ||
// 替换line的类名 | ||
const oldLineClassName = `line-${parentNodeKey}-${oldNodeKey}`; | ||
const parentLine = linkContainer.querySelector(`.${oldLineClassName}`); | ||
parentLine.classList.remove(oldLineClassName); | ||
parentLine.classList.add(`line-${parentNodeKey}-${newNodeKey}`); | ||
childrenKeys.forEach(childKey => { | ||
const oldChildLineClassName = `line-${oldNodeKey}-${childKey}`; | ||
const childLink = this.linkContainer.querySelector(`.${oldChildLineClassName}`); | ||
childLink.classList.remove(oldChildLineClassName); | ||
childLink.classList.add(`line-${newNodeKey}-${childKey}`); | ||
}); | ||
// 更新position数据 | ||
this.replacePositionNodeKey(oldNodeKey, newNodeKey); | ||
} | ||
// 替换节点 | ||
const newNode = this.createNode(data); | ||
childrenKeys.length && newNode.setAttribute('data-children', childrenKeys.join()); | ||
node.querySelector('.tree-chart-unfold') && this.createFoldButton(newNode); | ||
const nodeContainer = node.parentElement; | ||
nodeContainer.insertBefore(newNode, node); | ||
nodeContainer.removeChild(node); | ||
} | ||
reloadLink() { | ||
this.createLink(); | ||
this.resize(); | ||
} | ||
reRender(data) { | ||
const { nodesContainer } = this; | ||
// 更新nodes | ||
nodesContainer.innerHTML = ''; | ||
const nodeContainer = this.createNodeContainer(); | ||
nodesContainer.appendChild(nodeContainer); | ||
this.createNodes(data, nodeContainer, true); | ||
// 更新links | ||
this.reloadLink(); | ||
} | ||
/* ================ */ | ||
// 需要取消注册的才使用该函数 | ||
registerEvent(eventType, handler, eventTarget = window) { | ||
if (!this.eventList) this.eventList = []; | ||
const handlerFunction = handler.bind(this); | ||
this.eventList.push({ eventTarget, type: eventType, handler: handlerFunction }); | ||
eventTarget.addEventListener(eventType, handlerFunction); | ||
} | ||
unregisterEvent() { | ||
this.eventList.forEach(item => item.eventTarget.removeEventListener(item.type, item.handler)); | ||
} | ||
addChildrenKey(targetKey, newKey) { | ||
const targetNodeElement = this.getNodeElement(targetKey); | ||
const childrenKeys = targetNodeElement.getAttribute('data-children') || ''; | ||
targetNodeElement.setAttribute('data-children', childrenKeys ? `${childrenKeys},${newKey}` : newKey); | ||
} | ||
removeChildrenKey(targetKey, removeKey) { | ||
const childrenKeys = this.getChildrenKeys(targetKey); | ||
if (!childrenKeys.length) return | ||
const index = childrenKeys.indexOf(removeKey); | ||
index > -1 && childrenKeys.splice(index, 1); | ||
const targetNodeElement = this.getNodeElement(targetKey); | ||
if (childrenKeys.length) { | ||
targetNodeElement.setAttribute('data-children', childrenKeys.join()); | ||
} else { | ||
targetNodeElement.removeAttribute('data-children'); | ||
} | ||
} | ||
replaceChildrenKey(targetKey, oldKey, newKey) { | ||
const childrenKeys = this.getChildrenKeys(targetKey); | ||
if (!childrenKeys.length) return | ||
const index = childrenKeys.indexOf(oldKey); | ||
if (index === -1) return | ||
childrenKeys.splice(index, 1, newKey); | ||
this.getNodeElement(targetKey).setAttribute('data-children', childrenKeys.join()); | ||
} | ||
createChildrenContainer(className) { | ||
const container = document.createElement('div'); | ||
container.classList.add('tree-chart-children-container'); | ||
className && container.classList.add(className); | ||
return container | ||
} | ||
getChildrenContainer(targetKey) { | ||
return this.getNodeElement(targetKey).nextElementSibling | ||
} | ||
removeChildrenContainer(targetKey) { | ||
const container = this.getChildrenContainer(targetKey); | ||
container.parentElement.removeChild(container); | ||
} | ||
getKeyField(data) { | ||
return data[this.option.keyField].toString() || null | ||
} | ||
constructor(option) { | ||
this.mergeOption(option); | ||
this.createChartElement(); | ||
this.resize(); | ||
this.setEvent(); | ||
} | ||
mergeOption(data) { | ||
const option = { | ||
keyField: 'id', // 作为唯一ID的字段 | ||
isVertical: true, | ||
distanceX: 40, // item的垂直间距,不小于40 | ||
distanceY: 40, // item的水平间距,不小于40 | ||
allowFold: false, // 是否能折叠 | ||
foldNodeKeys: [], // 初始需要折叠的节点 | ||
draggable: false, // 是否能拖拽item | ||
dragScroll: false, // 是否开启拖拽滚动 | ||
autoScrollTriggerDistance: 50, // 自动触发滚动的距离 | ||
autoScrollSpeed: 6, // 滚动速度 | ||
line: { | ||
type: 'bezier', | ||
smooth: 50 // 光滑程度(0-100,100为直线) | ||
}, | ||
...data | ||
}; | ||
const { | ||
draggable, | ||
allowFold, | ||
dragScroll, | ||
isVertical, | ||
line, | ||
padding, | ||
distanceX, | ||
distanceY | ||
} = option; | ||
if (distanceX < 40) option.distanceX = 40; | ||
if (distanceY < 40) option.distanceY = 40; | ||
this.draggable = draggable; | ||
this.allowFold = allowFold; | ||
this.dragScroll = dragScroll; | ||
this.isVertical = isVertical; | ||
this.lineConfig = line; | ||
// draggable时候最小内边距不能小于30 | ||
const containerPadding = { | ||
top: 30, | ||
right: 30, | ||
bottom: 30, | ||
left: 30, | ||
...padding | ||
}; | ||
for (const key in containerPadding) { | ||
let paddingValue = containerPadding[key]; | ||
if (!isNumber(paddingValue)) paddingValue = 0; | ||
if (draggable && paddingValue < 30) paddingValue = 30; | ||
containerPadding[key] = paddingValue; | ||
} | ||
this.containerPadding = containerPadding; | ||
this.option = option; | ||
this.initHooks(); | ||
} | ||
initHooks() { | ||
const controlHooks = [ | ||
'nodeControl', | ||
'preventDrag' | ||
]; | ||
const eventHooks = [ | ||
'dragStart', | ||
'dragEnd', | ||
'click', | ||
'mouseEnter', | ||
'mouseLeave' | ||
]; | ||
const hookList = ['contentRender', ...controlHooks, ...eventHooks]; | ||
this.hooks = {}; | ||
hookList.forEach(name => { | ||
const handler = this.option[name]; | ||
if (typeof handler === 'function') this.hooks[name] = handler; | ||
}); | ||
} | ||
createChartElement() { | ||
const { isVertical } = this; | ||
const { container, data } = this.option; | ||
container.classList.add('tree-chart'); | ||
isVertical && container.classList.add('is-vertical'); | ||
this.container = container; | ||
this.createNodes(data, this.createNodesContainer()); | ||
this.createLinkContainer(); | ||
this.createLink(); | ||
this.createGhostContainer(); | ||
} | ||
createNodesContainer() { | ||
const { container, containerPadding } = this; | ||
const nodesContainer = this.createNodeContainer(); | ||
nodesContainer.classList.add('is-nodes-container'); | ||
for (const direct in containerPadding) { | ||
if (!/left|top|right|bottom/.test(direct)) continue | ||
const paddingValue = containerPadding[direct]; | ||
nodesContainer.style[`padding${direct.replace(/^./, $ => $.toUpperCase())}`] = `${paddingValue}px`; | ||
} | ||
container.appendChild(nodesContainer); | ||
this.nodesContainer = nodesContainer; | ||
const nodeContainer = this.createNodeContainer(); | ||
nodesContainer.append(nodeContainer); | ||
return nodeContainer | ||
} | ||
createNodeContainer() { | ||
const nodeContainer = document.createElement('div'); | ||
nodeContainer.classList.add('tree-chart-container'); | ||
return nodeContainer | ||
} | ||
// 数据数据结构生成节点 | ||
createNodes(data, parentNodeContainer, reRender) { | ||
const { allowFold, rootNode, option } = this; | ||
const { children } = data; | ||
const { foldNodeKeys } = option; | ||
const existChildren = Array.isArray(children) && children.length > 0; | ||
// 创建节点 | ||
const node = this.createNode(data); | ||
parentNodeContainer.appendChild(node); | ||
// 初始化或重新渲染的时候 | ||
if (!rootNode || reRender) this.rootNode = node; | ||
if (existChildren) { | ||
const childrenContainer = this.createChildrenContainer(); | ||
parentNodeContainer.appendChild(childrenContainer); | ||
if (allowFold) { | ||
const foldButton = this.createFoldButton(node); | ||
// 节点需要初始折叠 | ||
if (foldNodeKeys.includes(this.getKeyField(data))) { | ||
foldButton.classList.add('is-fold'); | ||
childrenContainer.classList.add('is-hidden'); | ||
} | ||
} | ||
const childKeys = []; | ||
children.forEach(childData => { | ||
childKeys.push(this.getKeyField(childData)); | ||
const childNodeContainer = this.createNodeContainer(); | ||
childrenContainer.appendChild(childNodeContainer); | ||
this.createNodes(childData, childNodeContainer); | ||
}); | ||
node.setAttribute('data-children', childKeys.join()); | ||
} | ||
} | ||
createNode(data) { | ||
const { draggable, hooks, option } = this; | ||
const { distanceX, distanceY } = option; | ||
const { contentRender, nodeControl } = hooks; | ||
const node = document.createElement('div'); | ||
const key = this.getKeyField(data); | ||
node.classList.add('tree-chart-node', `tree-chart-item-${key}`); | ||
node.setAttribute('data-key', key); | ||
node.style.marginBottom = `${distanceY}px`; | ||
node.style.marginRight = `${distanceX}px`; | ||
const renderContainer = document.createElement('div'); | ||
renderContainer.classList.add('tree-render-container'); | ||
// 生成用户自定义模板 | ||
if (contentRender) { | ||
const renderResult = contentRender(data); | ||
if (typeof renderResult === 'string') { | ||
renderContainer.innerHTML = renderResult.replace(/>\s+</g, '><'); | ||
} else if (isElement(renderResult)) { | ||
renderContainer.appendChild(renderResult); | ||
} else { | ||
renderContainer.innerText = 'Please check contentRender return type is string or element'; | ||
} | ||
} else { | ||
renderContainer.innerText = 'option.contentRender is required'; | ||
} | ||
node.appendChild(renderContainer); | ||
// 添加节点事件 | ||
this.setNodeEvent(node); | ||
// 拖拽控制 | ||
if (draggable && nodeControl) { | ||
const controlConfig = { | ||
draggable: true, | ||
insertChild: true, | ||
insertPrevious: true, | ||
insertNext: true, | ||
...nodeControl(data) | ||
}; | ||
!controlConfig.draggable && node.classList.add('not-allow-drag'); | ||
!controlConfig.insertChild && node.classList.add('not-allow-insert-child'); | ||
!controlConfig.insertPrevious && node.classList.add('not-allow-insert-previous'); | ||
!controlConfig.insertNext && node.classList.add('not-allow-insert-next'); | ||
} | ||
return node | ||
} | ||
// 设置拖动镜像容器 | ||
createGhostContainer() { | ||
const { container } = this; | ||
const ghostContainer = document.createElement('div'); | ||
ghostContainer.classList.add('tree-chart-ghost-container'); | ||
container.appendChild(ghostContainer); | ||
this.ghostContainer = ghostContainer; | ||
} | ||
// 创建展开收起按钮 | ||
createFoldButton(nodeElement) { | ||
if (nodeElement.querySelector('.tree-chart-unfold')) return | ||
const button = document.createElement('div'); | ||
button.classList.add('tree-chart-unfold'); | ||
button.innerHTML = '<div></div><div></div>'; | ||
nodeElement.appendChild(button); | ||
return button | ||
} | ||
getFoldButton(targetKey) { | ||
const targetNodeElement = this.getNodeElement(targetKey); | ||
return targetNodeElement.querySelector('.tree-chart-unfold') | ||
} | ||
removeFoldButton(targetKey) { | ||
const foldButton = this.getFoldButton(targetKey); | ||
foldButton && foldButton.parentElement.removeChild(foldButton); | ||
} | ||
createLinkContainer() { | ||
const { container } = this; | ||
const linkContainer = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); | ||
linkContainer.classList.add('tree-chart-link-container'); | ||
container.appendChild(linkContainer); | ||
this.linkContainer = linkContainer; | ||
} | ||
// 根据节点间父子关系生成连线信息,同时生成节点位置数据 | ||
createLink() { | ||
const { container, nodesContainer, linkContainer, draggable, isVertical } = this; | ||
const { left: containerLeft, top: containerTop } = container.getBoundingClientRect(); | ||
const { scrollLeft, scrollTop } = container; | ||
linkContainer.innerHTML = ''; | ||
draggable && this.initPositionData(); | ||
nodesContainer.querySelectorAll('.tree-chart-node').forEach(nodeElement => { | ||
const childrenKeys = nodeElement.getAttribute('data-children'); | ||
const { left: nodeLeft, right: nodeRight, top: nodeTop, bottom: nodeBottom } = nodeElement.getBoundingClientRect(); | ||
const nodeKey = this.getKeyByElement(nodeElement); | ||
const childrenNodeContainer = nodeElement.nextElementSibling; | ||
// 忽略收起状态的节点 | ||
if (childrenKeys && !childrenNodeContainer.classList.contains('is-hidden')) { | ||
const from = { | ||
x: nodeLeft - containerLeft + scrollLeft + (isVertical ? nodeElement.offsetWidth / 2 : nodeElement.offsetWidth), | ||
y: nodeTop - containerTop + scrollTop + (isVertical ? nodeElement.offsetHeight : nodeElement.offsetHeight / 2), | ||
key: nodeKey | ||
}; | ||
childrenKeys.split(',').forEach(childNodeKey => { | ||
const childElement = this.getNodeElement(childNodeKey); | ||
const { left: childElementLeft, top: childElementTop } = childElement.getBoundingClientRect(); | ||
const { offsetWidth: childElementWidth, offsetHeight: childElementHeight } = childElement; | ||
const to = { | ||
x: childElementLeft - containerLeft + scrollLeft + (isVertical ? childElementWidth / 2 : 0), | ||
y: childElementTop - containerTop + scrollTop + (isVertical ? 0 : childElement.offsetHeight / 2), | ||
key: childNodeKey | ||
}; | ||
this.drawLine(from, to); | ||
}); | ||
} | ||
draggable && this.addPositionData(nodeKey, { | ||
left: nodeLeft - containerLeft + scrollLeft, | ||
right: nodeRight - containerLeft + scrollLeft, | ||
top: nodeTop - containerTop + scrollTop, | ||
bottom: nodeBottom - containerTop + scrollTop | ||
}); | ||
}); | ||
} | ||
// 生成节点位置信息,方便后面的碰撞检测 | ||
initPositionData() { | ||
this.positionData = { | ||
left: { sortList: [] }, | ||
right: { sortList: [] }, | ||
top: { sortList: [] }, | ||
bottom: { sortList: [] }, | ||
node: {} | ||
}; | ||
} | ||
addPositionData(nodeKey, positionDataItem) { | ||
const { positionData } = this; | ||
// 保存节点位置信息 | ||
// nodeKey->{left,top,right,bottom } | ||
positionData.node[nodeKey] = positionDataItem; | ||
for (const direct in positionDataItem) { | ||
// 获取节点方位数据值 | ||
const positionValue = positionDataItem[direct]; | ||
// 获取数据归类容器,形成position->[...nodeKey]的映射 | ||
const directPositionMap = positionData[direct]; | ||
if (directPositionMap[positionValue]) { | ||
directPositionMap[positionValue].push(nodeKey); | ||
} else { | ||
directPositionMap[positionValue] = [nodeKey]; | ||
} | ||
// 插入排序,并去重 | ||
// sortList->[...position] | ||
const sortList = directPositionMap.sortList; | ||
if (sortList.length) { | ||
for (let index = 0, len = sortList.length; index < len; index++) { | ||
const currentValue = sortList[index]; | ||
if (positionValue === currentValue) break | ||
if (positionValue < currentValue) { | ||
sortList.splice(index, 0, positionValue); | ||
break | ||
} | ||
index === len - 1 && sortList.push(positionValue); | ||
} | ||
} else { | ||
sortList.push(positionValue); | ||
} | ||
} | ||
} | ||
replacePositionNodeKey(oldKey, newKey) { | ||
const { positionData } = this; | ||
// 更新node集合 | ||
positionData.node[newKey] = positionData.node[oldKey]; | ||
delete positionData.node[oldKey]; | ||
void ['left', 'top', 'right', 'bottom'].forEach(direct => { | ||
const directPositionMap = positionData[direct]; | ||
for (const positionValue in directPositionMap) { | ||
if (positionValue === 'sortList') continue | ||
const nodeKeyList = directPositionMap[positionValue]; | ||
const oldKeyIndex = nodeKeyList.indexOf(oldKey); | ||
if (oldKeyIndex > -1) { | ||
nodeKeyList.splice(oldKeyIndex, 1, newKey); | ||
break | ||
} | ||
} | ||
}); | ||
} | ||
setEvent() { | ||
const { allowFold, draggable, dragScroll } = this; | ||
allowFold && this.setFoldEvent(); | ||
this.setClickHook(); | ||
draggable && this.setDragEvent(); | ||
dragScroll && this.setDragScroll(); | ||
} | ||
setFoldEvent() { | ||
this.nodesContainer.addEventListener('click', ({ target }) => { | ||
if (!target.classList.contains('tree-chart-unfold')) return | ||
this.toggleFold(this.getKeyByElement(target.parentElement)); | ||
}); | ||
} | ||
// 两点间连线 | ||
drawLine(from, to, isTemp) { | ||
const { linkContainer, isVertical, allowFold, lineConfig } = this; | ||
const { type: lineType, smooth } = lineConfig; | ||
const lineClassName = `line-${from.key}-${to.key}`; | ||
let link = document.querySelector(`.${lineClassName}`); | ||
if (!link) { | ||
link = document.createElementNS('http://www.w3.org/2000/svg', 'path'); | ||
link.classList.add(lineClassName, `line-from-${from.key}`); | ||
isTemp && link.classList.add('is-temp-line'); | ||
linkContainer.appendChild(link); | ||
} | ||
let path = ''; | ||
let { x: fromX, y: fromY } = from; | ||
const { x: toX, y: toY } = to; | ||
// 需要加上fold按钮的宽度 | ||
if (allowFold && this.existChildren(from.key)) isVertical ? fromY += 5 : fromX += 5; | ||
const centerX = (toX - fromX) / 2; | ||
const centerY = (toY - fromY) / 2; | ||
const M = `${fromX} ${fromY}`; | ||
const T = `${toX} ${toY}`; | ||
if (lineType === 'straight') path = `M${M} T ${T}`; | ||
if (lineType === 'broken') { | ||
let L1 = ''; | ||
let L2 = ''; | ||
if (isVertical) { | ||
L1 = `${fromX} ${fromY + centerY}`; | ||
L2 = `${toX} ${toY - centerY}`; | ||
} else { | ||
L1 = `${fromX + centerX} ${fromY}`; | ||
L2 = `${toX - centerX} ${toY}`; | ||
} | ||
path = `M${M} L${L1} L${L2} L${T}`; | ||
} | ||
if (lineType === 'bezier') { | ||
const smoothScale = smooth / 100; | ||
let Q1 = ''; | ||
if (isVertical) { | ||
Q1 = `${fromX} ${fromY + (centerY - centerY * smoothScale)}`; | ||
} else { | ||
Q1 = `${fromX + (centerX - centerX * smoothScale)} ${fromY}`; | ||
} | ||
const Q2 = `${fromX + centerX} ${fromY + centerY}`; | ||
path = `M${M} Q${Q1} ${Q2} T ${T}`; | ||
} | ||
link.setAttribute('d', path); | ||
} | ||
// 沿着事件触发路径向上找node节点 | ||
getCurrentEventNode(eventTarget) { | ||
const { nodesContainer } = this; | ||
// 忽略展开按钮 | ||
if (eventTarget.classList.contains('tree-chart-unfold')) return null | ||
let searchElement = eventTarget; | ||
while (nodesContainer !== searchElement) { | ||
if (searchElement.classList.contains('tree-chart-node')) return searchElement | ||
searchElement = searchElement.parentElement; | ||
} | ||
return null | ||
} | ||
// 判断是否处于拖动过程 | ||
isDragging() { | ||
const { draggable, dragData } = this; | ||
if (!draggable) return false | ||
const { ghostTranslateX, ghostTranslateY, key } = dragData; | ||
return key && (ghostTranslateX !== 0 || ghostTranslateY !== 0) | ||
} | ||
setClickHook() { | ||
const { nodesContainer, hooks } = this; | ||
const { click: clickHook } = hooks; | ||
if (!clickHook) return | ||
// 用mouseEvent来实现click主要是为了区别dragStart和click的行为 | ||
let mouseDownNode = null; | ||
nodesContainer.addEventListener('mousedown', ({ button, target }) => { | ||
if (button !== 0) return | ||
mouseDownNode = this.getCurrentEventNode(target); | ||
}); | ||
nodesContainer.addEventListener('mouseup', e => { | ||
const { button, target } = e; | ||
if (button !== 0) return | ||
const mouseUpNode = this.getCurrentEventNode(target); | ||
if (mouseUpNode && mouseUpNode === mouseDownNode && !this.isDragging()) { | ||
clickHook({ key: this.getKeyByElement(mouseUpNode), element: mouseUpNode }, e); | ||
} | ||
}); | ||
} | ||
setNodeEvent(node) { | ||
const { mouseEnter: enterHook, mouseLeave: leaveHook } = this.hooks; | ||
const argumentData = { key: this.getKeyByElement(node), element: node }; | ||
const contentElement = node.querySelector('.tree-render-container'); | ||
enterHook && contentElement.addEventListener('mouseenter', e => { | ||
// 忽略拖动被覆盖的情况 | ||
if (this.isDragging()) return | ||
enterHook(argumentData, e); | ||
}); | ||
leaveHook && contentElement.addEventListener('mouseleave', e => { | ||
// 忽略拖动被覆盖的情况 | ||
if (this.isDragging()) return | ||
leaveHook(argumentData, e); | ||
}); | ||
} | ||
createNodeParams(nodeKey) { | ||
return { | ||
key: nodeKey, | ||
parentKey: this.getParentKey(nodeKey), | ||
previousKey: this.getPreviousKey(nodeKey), | ||
nextKey: this.getNextKey(nodeKey) | ||
} | ||
} | ||
initDragData() { | ||
this.dragData = { | ||
key: null, | ||
ghostElement: null, | ||
ghostTranslateX: 0, | ||
ghostTranslateY: 0, | ||
// mousedown事件在节点的触发的偏移位置 | ||
mouseDownOffsetX: 0, | ||
mouseDownOffsetY: 0 | ||
}; | ||
} | ||
// 绑定拖动事件 | ||
setDragEvent() { | ||
const { ghostContainer, container, nodesContainer, hooks, option } = this; | ||
const { autoScrollTriggerDistance, autoScrollSpeed } = option; | ||
const { preventDrag, dragStart, dragEnd } = hooks; | ||
// 是否触发dragStart事件 | ||
let emitDragStart = true; | ||
this.initDragData(); | ||
nodesContainer.addEventListener('mousedown', e => { | ||
const { button, clientX, clientY } = e; | ||
if (button !== 0) return | ||
const dragNode = this.getCurrentEventNode(e.target); | ||
const dragNodeKey = this.getKeyByElement(dragNode); | ||
// 根节点不允许拖动 | ||
if (!dragNode || dragNode === this.rootNode) return | ||
// 用户禁止拖动的节点 | ||
if (dragNode.classList.contains('not-allow-drag')) return | ||
// preventDrag返回true时阻止拖动 | ||
if (preventDrag && preventDrag({ key: dragNodeKey, element: dragNode }, e)) return | ||
// 保存偏移量等 | ||
const { left: nodePositionLeft, top: nodePositionTop } = this.positionData.node[dragNodeKey]; | ||
const ghostElement = dragNode.cloneNode(true); | ||
ghostElement.style.margin = '0px'; | ||
Object.assign(this.dragData, { | ||
key: dragNodeKey, | ||
ghostElement, | ||
mouseDownOffsetX: clientX + container.scrollLeft - nodePositionLeft, | ||
mouseDownOffsetY: clientY + container.scrollTop - nodePositionTop | ||
}); | ||
// 跟随滚动 | ||
this.FollowScroll.start(ghostElement); | ||
}); | ||
nodesContainer.addEventListener('mousemove', e => { | ||
const { button, movementX, movementY, clientX, clientY } = e; | ||
const { ghostElement, mouseDownOffsetX, mouseDownOffsetY, key } = this.dragData; | ||
if (button !== 0 || !key) return | ||
// 处理Chrome76版本长按不移动也会触发的情况 | ||
if (movementX === 0 && movementY === 0) return | ||
// 清除文字选择对拖动的影响 | ||
getSelection && getSelection().removeAllRanges(); | ||
// 光标形状变为move | ||
nodesContainer.classList.add('cursor-move'); | ||
// 添加镜像元素和同步位置 | ||
!ghostContainer.contains(ghostElement) && ghostContainer.appendChild(ghostElement); | ||
const ghostTranslateX = clientX + container.scrollLeft - mouseDownOffsetX; | ||
const ghostTranslateY = clientY + container.scrollTop - mouseDownOffsetY; | ||
ghostElement.style.transform = `translate(${ghostTranslateX}px, ${ghostTranslateY}px)`; | ||
Object.assign(this.dragData, { | ||
ghostTranslateX, | ||
ghostTranslateY | ||
}); | ||
const ghostPosition = this.getGhostPosition(); | ||
this.setDragEffect(ghostPosition); | ||
// 跟随滚动 | ||
if (dragStart && emitDragStart) { | ||
emitDragStart = false; | ||
dragStart({ key, element: this.getNodeElement(key) }); | ||
} | ||
}); | ||
this.registerEvent('mouseup', () => { | ||
emitDragStart = true; | ||
const { dragData, nodesContainer, ghostContainer } = this; | ||
let targetKey = ''; | ||
let type = ''; | ||
const dragKey = dragData.key; | ||
if (!dragKey) return | ||
const targetNode = nodesContainer.querySelector('.collide-node'); | ||
if (targetNode) { | ||
targetKey = this.getKeyByElement(targetNode); | ||
if (targetNode.classList.contains('become-previous')) type = 'previous'; | ||
if (targetNode.classList.contains('become-next')) type = 'next'; | ||
if (targetNode.classList.contains('become-child')) type = 'child'; | ||
} | ||
// 停止拖动,移除拖动效果 | ||
this.FollowScroll.stop(); | ||
nodesContainer.classList.remove('cursor-move'); | ||
ghostContainer.innerHTML = ''; | ||
this.removeDragEffect(); | ||
this.initDragData(); | ||
if (targetNode) { | ||
const from = this.createNodeParams(dragKey); | ||
this.insertNode(targetKey, dragKey, type); | ||
const to = this.createNodeParams(dragKey); | ||
// emit dragEndHook | ||
dragEnd && dragEnd({ from, to, target: targetKey, key: dragKey, type }); | ||
} | ||
}); | ||
// 处理拖拽过程中滚动的情况 | ||
const oldScroll = { | ||
top: container.scrollTop, | ||
left: container.scrollLeft | ||
}; | ||
this.registerEvent('scroll', () => { | ||
const { key, ghostElement, ghostTranslateY: oldTranslateY, ghostTranslateX: oldTranslateX } = this.dragData; | ||
const { left: oldScrollLeft, top: oldScrollTop } = oldScroll; | ||
const { scrollLeft: currentScrollLeft, scrollTop: currentScrollTop } = container; | ||
if (key && ghostElement) { | ||
const ghostTranslateX = oldTranslateX + currentScrollLeft - oldScrollLeft; | ||
const ghostTranslateY = oldTranslateY + currentScrollTop - oldScrollTop; | ||
ghostElement.style.transform = `translate(${ghostTranslateX}px, ${ghostTranslateY}px)`; | ||
Object.assign(this.dragData, { ghostTranslateX, ghostTranslateY }); | ||
this.setDragEffect(this.getGhostPosition()); | ||
} | ||
oldScroll.left = currentScrollLeft; | ||
oldScroll.top = currentScrollTop; | ||
}, container); | ||
this.FollowScroll = new FollowScroll({ | ||
scrollContainer: container, | ||
autoScrollTriggerDistance, | ||
eventContainer: nodesContainer, | ||
autoScrollSpeed | ||
}); | ||
} | ||
getGhostPosition() { | ||
const { ghostTranslateX, ghostTranslateY, ghostElement } = this.dragData; | ||
return { | ||
left: ghostTranslateX, | ||
top: ghostTranslateY, | ||
right: ghostTranslateX + ghostElement.offsetWidth, | ||
bottom: ghostTranslateY + ghostElement.offsetHeight | ||
} | ||
} | ||
removeDragEffect() { | ||
const { linkContainer, nodesContainer } = this; | ||
const tempLink = linkContainer.querySelector('.is-temp-line'); | ||
tempLink && linkContainer.removeChild(tempLink); | ||
const collideNode = nodesContainer.querySelector('.collide-node'); | ||
collideNode && collideNode.classList.remove('collide-node', 'become-previous', 'become-next', 'become-child'); | ||
const tempChildrenContainer = nodesContainer.querySelector('.temp-children-container'); | ||
tempChildrenContainer && tempChildrenContainer.parentElement.removeChild(tempChildrenContainer); | ||
const notAllowEffect = nodesContainer.querySelector('.show-not-allow'); | ||
notAllowEffect && notAllowEffect.classList.remove('show-not-allow'); | ||
} | ||
getCollideType(collideNodeKey, ghostPosition) { | ||
const { rootNode, isVertical, dragData, positionData } = this; | ||
const { key: dragNodeKey } = dragData; | ||
const { top: collideNodeTop, bottom: collideNodeBottom, left: collideNodeLeft, right: collideNodeRight } = positionData.node[collideNodeKey]; | ||
const { left: ghostLeft, right: ghostRight, top: ghostTop, bottom: ghostBottom } = ghostPosition; | ||
const dragNode = this.getNodeElement(dragNodeKey); | ||
const collideNode = this.getNodeElement(collideNodeKey); | ||
// 拖到父节点时只能作为兄弟节点插入 | ||
const coverIsParent = collideNodeKey === this.getParentKey(dragNodeKey); | ||
// 禁止插入到上一个兄弟节点的下面 | ||
const coverIsPrevious = collideNodeKey === this.getPreviousKey(dragNodeKey); | ||
// 禁止插入到下一个兄弟节点的上面 | ||
const coverIsNext = collideNodeKey === this.getNextKey(dragNodeKey); | ||
// 权限数据 | ||
const allowConfig = { | ||
child: !collideNode.classList.contains('not-allow-insert-child') && !coverIsParent, | ||
next: !collideNode.classList.contains('not-allow-insert-next') && !coverIsPrevious, | ||
previous: !collideNode.classList.contains('not-allow-insert-previous') && !coverIsNext | ||
}; | ||
// 如果被覆盖的是根节点的话只允许作为子节点插入 | ||
if (collideNode === rootNode) return allowConfig.child ? 'child' : 'notAllow' | ||
// 不可拖到子节点上 | ||
if (dragNode.parentElement.contains(collideNode)) return 'notAllow' | ||
if (isVertical) { | ||
// 位置偏左或者偏右(50%)则认为是添加兄弟节点 | ||
const offsetValue = (collideNodeRight - collideNodeLeft) * 0.5; | ||
const leftPositionValue = collideNodeLeft + offsetValue; | ||
const rightPositionValue = collideNodeRight - offsetValue; | ||
// 在左边插入 | ||
if (ghostRight <= leftPositionValue && allowConfig.previous) return 'previous' | ||
// 在右边插入 | ||
if (ghostLeft >= rightPositionValue && allowConfig.next) return 'next' | ||
} else { | ||
// 位置偏上或者偏下(50%)则认为是添加兄弟节点 | ||
const offsetValue = (collideNodeBottom - collideNodeTop) * 0.5; | ||
const topPositionValue = collideNodeTop + offsetValue; | ||
const bottomPositionValue = collideNodeBottom - offsetValue; | ||
// 在上方插入 | ||
if (ghostBottom <= topPositionValue && allowConfig.previous) return 'previous' | ||
// 在下方插入 | ||
if (ghostTop >= bottomPositionValue && allowConfig.next) return 'next' | ||
} | ||
// 作为子节点插入 | ||
if (allowConfig.child) return 'child' | ||
// 不满足自定义控制条件的按照child=>next=>previous的权重取一个 | ||
for (const key in allowConfig) { | ||
if (allowConfig[key]) return key | ||
} | ||
return 'notAllow' | ||
} | ||
getCollideLinePosition(collideType, collideNodeKey) { | ||
const { isVertical, positionData, option } = this; | ||
const { distanceX, distanceY } = option; | ||
const linPositionData = {}; | ||
const { top: collideNodeTop, bottom: collideNodeBottom, left: collideNodeLeft, right: collideNodeRight } = positionData.node[collideNodeKey]; | ||
if (isVertical) { | ||
// 插入子节点类型 | ||
if (collideType === 'child') { | ||
linPositionData.from = { | ||
x: (collideNodeLeft + collideNodeRight) / 2, | ||
y: collideNodeBottom, | ||
key: collideNodeKey | ||
}; | ||
// 有子节点并且子节点展开 | ||
if (this.existChildren(collideNodeKey) && !this.nodeIsFold(collideNodeKey)) { | ||
const lastChildrenNodeKey = this.getChildrenKeys(collideNodeKey).pop(); | ||
const { right: lastChildrenNodeRight, top: lastChildrenNodeTop } = positionData.node[lastChildrenNodeKey]; | ||
linPositionData.to = { | ||
x: lastChildrenNodeRight + 20, | ||
y: lastChildrenNodeTop, | ||
key: 'temp' | ||
}; | ||
return linPositionData | ||
} | ||
// 没有子节点和子节点折叠相同处理 | ||
linPositionData.to = { | ||
x: (collideNodeLeft + collideNodeRight) / 2, | ||
y: collideNodeBottom + distanceY, | ||
key: 'temp' | ||
}; | ||
return linPositionData | ||
} | ||
const parentNodeKey = this.getParentKey(collideNodeKey); | ||
const parentPosition = positionData.node[parentNodeKey]; | ||
linPositionData.from = { | ||
x: (parentPosition.left + parentPosition.right) / 2, | ||
y: parentPosition.bottom, | ||
key: parentNodeKey | ||
}; | ||
linPositionData.to = { | ||
x: collideType === 'previous' ? collideNodeLeft - 20 : collideNodeRight + 20, | ||
y: collideNodeTop, | ||
key: 'temp' | ||
}; | ||
return linPositionData | ||
} | ||
// 插入子节点类型 | ||
if (collideType === 'child') { | ||
linPositionData.from = { | ||
x: collideNodeRight, | ||
y: (collideNodeTop + collideNodeBottom) / 2, | ||
key: collideNodeKey | ||
}; | ||
// 有子节点并且子节点展开 | ||
if (this.existChildren(collideNodeKey) && !this.nodeIsFold(collideNodeKey)) { | ||
const lastChildrenNodeKey = this.getChildrenKeys(collideNodeKey).pop(); | ||
const { left: lastChildrenNodeLeft, bottom: lastChildrenNodeBottom } = positionData.node[lastChildrenNodeKey]; | ||
linPositionData.to = { | ||
x: lastChildrenNodeLeft, | ||
y: lastChildrenNodeBottom + 20, | ||
key: 'temp' | ||
}; | ||
return linPositionData | ||
} | ||
// 没有子节点和子节点折叠相同处理 | ||
linPositionData.to = { | ||
x: collideNodeRight + distanceX, | ||
y: (collideNodeTop + collideNodeBottom) / 2, | ||
key: 'temp' | ||
}; | ||
return linPositionData | ||
} | ||
const parentNodeKey = this.getParentKey(collideNodeKey); | ||
const parentPosition = positionData.node[parentNodeKey]; | ||
linPositionData.from = { | ||
x: parentPosition.right, | ||
y: (parentPosition.top + parentPosition.bottom) / 2, | ||
key: parentNodeKey | ||
}; | ||
linPositionData.to = { | ||
x: collideNodeLeft, | ||
y: collideType === 'previous' ? collideNodeTop - 20 : collideNodeBottom + 20, | ||
key: 'temp' | ||
}; | ||
return linPositionData | ||
} | ||
// 生成拖动效果 | ||
createDragEffect(collideNodeKey, ghostPosition) { | ||
const { positionData } = this; | ||
const collideNode = this.getNodeElement(collideNodeKey); | ||
const collideType = this.getCollideType(collideNodeKey, ghostPosition); | ||
if (collideType === 'notAllow') return setNotAllowEffect(collideNode) | ||
collideNode.classList.add(`become-${collideType}`, 'collide-node'); | ||
const { from, to } = this.getCollideLinePosition(collideType, collideNodeKey); | ||
this.drawLine(from, to, true); | ||
// 插入子节点类型:不存在子节点,或者子节点被收起,这时候需要创建临时节点 | ||
if (collideType === 'child' && (!this.existChildren(collideNodeKey) || this.nodeIsFold(collideNodeKey))) { | ||
const { top: collideNodeTop, bottom: collideNodeBottom, left: collideNodeLeft, right: collideNodeRight } = positionData.node[collideNodeKey]; | ||
const chartContent = document.createElement('div'); | ||
chartContent.classList.add('tree-chart-node', 'temp-chart-content'); | ||
chartContent.style.width = `${collideNodeRight - collideNodeLeft}px`; | ||
chartContent.style.height = `${collideNodeBottom - collideNodeTop}px`; | ||
const childrenContainer = this.createChildrenContainer('temp-children-container'); | ||
const chartContainer = this.createNodeContainer(true); | ||
chartContainer.appendChild(chartContent); | ||
childrenContainer.appendChild(chartContainer); | ||
collideNode.parentElement.appendChild(childrenContainer); | ||
} | ||
} | ||
// 获取拖动过程中碰撞的元素 | ||
getCollideNode({ left: ghostLeft, right: ghostRight, top: ghostTop, bottom: ghostBottom }) { | ||
const { key: draggingKey } = this.dragData; | ||
const { | ||
left: positionLeft, | ||
right: positionRight, | ||
top: positionTop, | ||
bottom: positionBottom, | ||
node: nodePosition | ||
} = this.positionData; | ||
// 寻找符合条件最近的位置 | ||
const searchCurrentPosition = (target, sortList, searchLarge) => { | ||
const listLen = sortList.length; | ||
if (searchLarge) { | ||
for (let i = 0; i < listLen; i++) { | ||
if (sortList[i] >= target) return sortList[i] | ||
} | ||
} else { | ||
for (let i = listLen - 1; i > -1; i--) { | ||
if (sortList[i] <= target) return sortList[i] | ||
} | ||
} | ||
return null | ||
}; | ||
const currentLeft = searchCurrentPosition(ghostLeft, positionRight.sortList, true); | ||
const currentTop = searchCurrentPosition(ghostTop, positionBottom.sortList, true); | ||
const currentRight = searchCurrentPosition(ghostRight, positionLeft.sortList); | ||
const currentBottom = searchCurrentPosition(ghostBottom, positionTop.sortList); | ||
// 寻找交叉范围内的节点 | ||
const getRangeList = (markValue, directPositionList, searchLarge) => { | ||
let result = []; | ||
isNumber(markValue) && directPositionList.sortList.forEach(positionValue => { | ||
if (searchLarge ? positionValue >= markValue : positionValue <= markValue) { | ||
result = result.concat(directPositionList[positionValue]); | ||
} | ||
}); | ||
return result | ||
}; | ||
// 根据ghost节点左边界和上边界求交集确定在右下方的元素 | ||
const leftCatchNodes = getRangeList(currentLeft, positionRight, true); | ||
const topCatchNodes = getRangeList(currentTop, positionBottom, true); | ||
// 根据ghost节点右边界和下边界求交集确定在左上方的元素 | ||
const rightCatchNodes = getRangeList(currentRight, positionLeft); | ||
const bottomCatchNodes = getRangeList(currentBottom, positionTop); | ||
// 四个区间求交集确定目标元素 | ||
const collideNodes = getArrayIntersection(leftCatchNodes, topCatchNodes, rightCatchNodes, bottomCatchNodes); | ||
// 忽略自身 | ||
const draggingNodeIndex = collideNodes.indexOf(draggingKey); | ||
draggingNodeIndex > -1 && collideNodes.splice(draggingNodeIndex, 1); | ||
if (!collideNodes.length) return null | ||
if (collideNodes.length === 1) return collideNodes[0] | ||
// 如果存在多个被覆盖的节点需要根据覆盖面积决策,取覆盖面最大的节点 | ||
const maxAreaNode = { nodeKey: '', nodeArea: 0 }; | ||
const getArea = nodeKey => { | ||
const { left: nodeLeft, right: nodeRight, top: nodeTop, bottom: nodeBottom } = nodePosition[nodeKey]; | ||
const x = nodeLeft <= ghostLeft ? nodeRight - ghostLeft : ghostRight - nodeLeft; | ||
const y = nodeTop <= ghostTop ? nodeBottom - ghostTop : ghostBottom - nodeTop; | ||
return x * y | ||
}; | ||
collideNodes.forEach(nodeKey => { | ||
const nodeArea = getArea(nodeKey); | ||
nodeArea > maxAreaNode.nodeArea && Object.assign(maxAreaNode, { nodeKey, nodeArea }); | ||
}); | ||
return maxAreaNode.nodeKey | ||
} | ||
setDragEffect(ghostElementPosition) { | ||
this.removeDragEffect(); | ||
const collideNodeKey = this.getCollideNode(ghostElementPosition); | ||
collideNodeKey && this.createDragEffect(collideNodeKey, ghostElementPosition); | ||
} | ||
resize() { | ||
const { nodesContainer, linkContainer, draggable, ghostContainer, rootNode, isVertical } = this; | ||
const { style: nodesContainerStyle } = nodesContainer; | ||
const { style: linkContainerStyle } = linkContainer; | ||
// 需要先清除旧的尺寸 | ||
nodesContainerStyle.height = 'auto'; | ||
nodesContainerStyle.width = 'auto'; | ||
nodesContainerStyle.minHeight = 'auto'; | ||
nodesContainerStyle.minWidth = 'auto'; | ||
const { clientWidth: nodeContainerWidth, clientHeight: nodeContainerHeight } = nodesContainer; | ||
let width = nodeContainerWidth; | ||
let height = nodeContainerHeight; | ||
// 可拖拽时需要扩展宽度或高度,这样才能插入新节点 | ||
if (draggable) { | ||
const { width: rootNodeWidth, height: rootNodeHeight } = rootNode.getBoundingClientRect(); | ||
if (isVertical) { | ||
height += rootNodeHeight; | ||
} else { | ||
width += rootNodeWidth; | ||
} | ||
} | ||
const newWidth = `${width}px`; | ||
const newHeight = `${height}px`; | ||
nodesContainerStyle[isVertical ? 'minHeight' : 'minWidth'] = '100%'; | ||
nodesContainerStyle.width = linkContainerStyle.width = newWidth; | ||
nodesContainerStyle.height = linkContainerStyle.height = newHeight; | ||
if (draggable) { | ||
const { style: ghostContainerStyle } = ghostContainer; | ||
ghostContainerStyle.width = newWidth; | ||
ghostContainerStyle.height = newHeight; | ||
} | ||
} | ||
setDragScroll() { | ||
const { container, nodesContainer } = this; | ||
let lock = true; | ||
const getEventNode = target => { | ||
if (target.classList.contains('tree-chart-node')) return target | ||
let searchElement = target; | ||
while (nodesContainer !== searchElement) { | ||
if (searchElement.classList.contains('tree-chart-node')) return searchElement | ||
searchElement = searchElement.parentElement; | ||
} | ||
return null | ||
}; | ||
nodesContainer.addEventListener('mousedown', e => { | ||
if (e.button !== 0 || getEventNode(e.target)) return | ||
lock = false; | ||
}); | ||
nodesContainer.addEventListener('mousemove', e => { | ||
e.preventDefault(); | ||
if (e.button !== 0 || lock) return | ||
container.scrollLeft = container.scrollLeft - e.movementX; | ||
container.scrollTop = container.scrollTop - e.movementY; | ||
}); | ||
this.registerEvent('mouseup', e => { | ||
if (e.button !== 0) return | ||
lock = true; | ||
}); | ||
} | ||
destroy() { | ||
this.FollowScroll.destroy(); | ||
this.unregisterEvent(); | ||
this.container.innerHTML = ''; | ||
for (const key in this) { | ||
this[key] = null; | ||
} | ||
} | ||
} | ||
const data = { | ||
name: 1, | ||
id: 1, | ||
children: [ | ||
{ | ||
name: 11, | ||
id: 11, | ||
children: [{ name: 111, id: 111 }] | ||
}, | ||
{ | ||
name: 12, | ||
id: 12, | ||
children: [ | ||
{ name: 121, id: 121 }, | ||
{ | ||
name: 122, | ||
id: 122, | ||
children: [{ name: 1221, id: 1221 }, { name: 1222, id: 1222 }, { name: 1223, id: 1223 }] | ||
} | ||
] | ||
}, | ||
{ | ||
id: 13, | ||
name: 13, | ||
children: [{ | ||
name: 131, | ||
id: 131, | ||
children: [ | ||
{ | ||
name: 1311, | ||
id: 1311, | ||
children: [{ name: 13111, id: 13111 }] | ||
}] | ||
}] | ||
}, | ||
{ | ||
id: 14, | ||
name: 14, | ||
children: [{ name: 141, id: 141 }] | ||
} | ||
] | ||
}; | ||
const chart = new TreeChart({ | ||
data, | ||
container: document.querySelector('#demo'), | ||
// isVertical: false, | ||
distanceX: 80, | ||
distanceY: 80, | ||
draggable: true, | ||
dragScroll: true, | ||
autoScrollTriggerDistance: 20, | ||
allowFold: true, | ||
foldNodeKeys: ['122'], | ||
// line: { | ||
// type: 'broken' | ||
// }, | ||
nodeControl(data) { | ||
return { | ||
draggable: data.id !== 12, | ||
insertChild: data.id !== 12, | ||
insertPrevious: data.id !== 12, | ||
insertNext: data.id !== 12 | ||
} | ||
}, | ||
dragStart() { | ||
console.log('dragstart'); | ||
}, | ||
dragEnd(data) { | ||
console.log('dragend', data); | ||
}, | ||
click() { | ||
console.log('click'); | ||
}, | ||
// mouseEnter(data) { | ||
// console.log('enter', data) | ||
// }, | ||
// mouseLeave(data) { | ||
// console.log('leave', data) | ||
// }, | ||
contentRender(data) { | ||
const container = document.createElement('div'); | ||
const { style: containerStyle } = container; | ||
containerStyle.width = '230px'; | ||
containerStyle.height = '80px'; | ||
containerStyle.border = 'solid 1px'; | ||
containerStyle.lineHeight = '80px'; | ||
container.innerText = data.name; | ||
return container | ||
} | ||
}); | ||
document.querySelector('.re-render').addEventListener('click', () => { | ||
chart.reRender(data); | ||
}); | ||
document.querySelector('.re-render-node').addEventListener('click', () => { | ||
chart.reRenderNode('122', { name: 999, id: 999 }); | ||
}); | ||
document.querySelector('.remove-node').addEventListener('click', () => { | ||
chart.removeNode('11'); | ||
}); | ||
document.querySelector('.move-node').addEventListener('click', () => { | ||
chart.insertNode('12', '111', 'child'); | ||
}); | ||
document.querySelector('.insert-child').addEventListener('click', () => { | ||
chart.insertNode('12', { name: 'insertChild', id: 'insertChild' }, 'child'); | ||
}); | ||
document.querySelector('.insert-previous').addEventListener('click', () => { | ||
chart.insertNode('12', { name: 'insertPrevious', id: 'insertPrevious' }, 'previous'); | ||
}); | ||
document.querySelector('.insert-next').addEventListener('click', () => { | ||
chart.insertNode('12', { name: 'insertNext', id: 'insertNext' }, 'next'); | ||
}); | ||
document.querySelector('.destroy').addEventListener('click', () => { | ||
chart.destroy(); | ||
}); | ||
}()); | ||
function e(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function t(e,t){for(var n=0;n<t.length;n++){var i=t[n];i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}function n(e,n,i){return n&&t(e.prototype,n),i&&t(e,i),e}function i(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function r(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);t&&(i=i.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,i)}return n}function o(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?r(Object(n),!0).forEach((function(t){i(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):r(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}var a=function(){function t(n){e(this,t);var i=n.scrollContainer,r=n.eventContainer,o=n.autoScrollTriggerDistance,a=n.autoScrollSpeed;this.scrollContainer=i,this.eventContainer=r,this.autoScrollSpeed=a,this.triggerDistance=o||0,this.targetNode=null,this.interval=0,this.directData={left:!1,right:!1,top:!1,bottom:!1},this.setEvent()}return n(t,[{key:"setEvent",value:function(){var e=this,t=this.eventContainer;this.mouseMoveHandler=function(t){var n=t.button,i=t.movementX,r=t.movementY;0===i&&0===r||!n&&e.targetNode&&(e.setDirectData(t),e.triggerScroll())},t.addEventListener("mousemove",this.mouseMoveHandler)}},{key:"setDirectData",value:function(e){var t=e.movementX,n=e.movementY,i=this.scrollContainer,r=this.targetNode,o=this.triggerDistance,a=this.directData,s=i.scrollLeft,l=i.scrollTop,c=i.scrollWidth,d=i.scrollHeight,h=i.clientWidth,u=i.clientHeight,v=i.getBoundingClientRect(),g=v.left,f=v.right,y=v.top,p=v.bottom,m=r.getBoundingClientRect(),C=m.left,k=m.right,E=m.top,L=m.bottom;C-g<o&&(a.left=!0),E-y<o&&(a.top=!0),f-k<o&&(a.right=!0),p-L<o&&(a.bottom=!0),(t>0||0===s)&&(a.left=!1),(n>0||0===l)&&(a.top=!1),(t<0||s+h>=c)&&(a.right=!1),(n<0||l+u>=d)&&(a.bottom=!1)}},{key:"triggerScroll",value:function(){var e=this.directData,t=this.scrollContainer,n=this.autoScrollSpeed,i=!1;for(var r in e)if(e[r]){i=!0;break}if(!i)return this.stop(!0);this.interval||(this.interval=setInterval((function(){var i=t.scrollLeft,r=t.scrollTop;e.left&&(i-=n),e.right&&(i+=n),e.top&&(r-=n),e.bottom&&(r+=n),t.scrollLeft=i,t.scrollTop=r}),20))}},{key:"start",value:function(e){this.targetNode=e}},{key:"stop",value:function(){var e=arguments.length>0&&void 0!==arguments[0]&&arguments[0];e||(this.targetNode=null),this.interval&&(clearInterval(this.interval),this.interval=0)}},{key:"destroy",value:function(){this.eventContainer.removeEventListener("mousemove",this.mouseMoveHandler),this.targetNode=this.scrollContainer=this.eventContainer=null,this.interval=0}}]),t}(),s=function(e){return/HTML/.test(Object.prototype.toString.call(e))&&1===e.nodeType},l=function(e){return/Number/.test(Object.prototype.toString.call(e))},c=function(){function t(n){e(this,t),this.mergeOption(n),this.createChartElement(),this.resize(),this.setEvent()}return n(t,[{key:"getKeyByElement",value:function(e){return s(e)&&e.classList.contains("tree-chart-node")?e.getAttribute("data-key"):null}},{key:"getNodeElement",value:function(e){return this.nodesContainer.querySelector(".tree-chart-item-".concat(e))}},{key:"getPreviousKey",value:function(e){try{var t=this.getNodeElement(e);return this.getKeyByElement(t.parentElement.previousElementSibling.querySelector(".tree-chart-node"))}catch(e){return null}}},{key:"getNextKey",value:function(e){try{var t=this.getNodeElement(e);return this.getKeyByElement(t.parentElement.nextElementSibling.querySelector(".tree-chart-node"))}catch(e){return null}}},{key:"getParentKey",value:function(e){try{var t=this.getNodeElement(e);return this.getKeyByElement(t.parentElement.parentElement.previousElementSibling)}catch(e){return null}}},{key:"getChildrenKeys",value:function(e){var t=this.getNodeElement(e).getAttribute("data-children");return t?t.split(","):[]}},{key:"existChildren",value:function(e){var t=this.getNodeElement(e);return!!t&&Boolean(t.getAttribute("data-children"))}},{key:"insertNode",value:function(e,t,n){var i=this.getNodeElement(e);if(!/next|previous/.test(n)||i!==this.rootNode){var r=null,o=null,a=null;if(/Object/.test(Object.prototype.toString.call(t)))r=this.getKeyField(t),o=this.createNode(t),(a=this.createNodeContainer()).appendChild(o),this.setNodeEvent(o);else{r=t,a=(o=this.getNodeElement(r)).parentElement;var s=this.getParentKey(r);this.removeChildrenKey(s,r),this.existChildren(s)||(this.removeChildrenContainer(s),this.allowFold&&this.removeFoldButton(s))}if("child"===n){if(this.existChildren(e))this.getChildrenContainer(e).appendChild(a),this.nodeIsFold(e)&&this.toggleFold(e);else{var l=this.createChildrenContainer();l.appendChild(a),i.parentElement.appendChild(l),this.createFoldButton(i)}this.addChildrenKey(e,r)}else{var c=this.getParentKey(e),d=this.getChildrenContainer(c),h=i.parentElement;"previous"===n&&d.insertBefore(a,h),"next"===n&&d.insertBefore(a,h.nextElementSibling),this.addChildrenKey(c,r)}this.reloadLink()}}},{key:"removeNode",value:function(e){var t=this.getNodeElement(e);if(t){if(t===this.rootNode)return console.warn("not allow remove root node");var n=this.getParentKey(e);this.removeChildrenKey(n,e);var i=t.parentElement;i.parentElement.removeChild(i),this.existChildren(n)||(this.removeChildrenContainer(n),this.allowFold&&this.removeFoldButton(n)),this.reloadLink()}}},{key:"nodeIsFold",value:function(e){return this.getFoldButton(e).classList.contains("is-fold")}},{key:"toggleFold",value:function(e){var t=this.getFoldButton(e);if(t){var n=this.getChildrenContainer(e);this.nodeIsFold(e)?(n.classList.remove("is-hidden"),t.classList.remove("is-fold")):(n.classList.add("is-hidden"),t.classList.add("is-fold")),this.reloadLink()}}},{key:"reRenderNode",value:function(e,t){var n=this,i=e.toString(),r=this.getKeyField(t),o=this.getNodeElement(i),a=this.getChildrenKeys(e);if(r!==i){var s=this.linkContainer,l=this.getParentKey(i);this.replaceChildrenKey(l,i,r);var c="line-".concat(l,"-").concat(i),d=s.querySelector(".".concat(c));d.classList.remove(c),d.classList.add("line-".concat(l,"-").concat(r)),a.forEach((function(e){var t="line-".concat(i,"-").concat(e),o=n.linkContainer.querySelector(".".concat(t));o.classList.remove(t),o.classList.add("line-".concat(r,"-").concat(e))})),this.replacePositionNodeKey(i,r)}var h=this.createNode(t);a.length&&h.setAttribute("data-children",a.join()),o.querySelector(".tree-chart-unfold")&&this.createFoldButton(h);var u=o.parentElement;u.insertBefore(h,o),u.removeChild(o)}},{key:"reloadLink",value:function(){this.createLink(),this.resize()}},{key:"reRender",value:function(e){var t=this.nodesContainer;t.innerHTML="";var n=this.createNodeContainer();t.appendChild(n),this.createNodes(e,n,!0),this.reloadLink()}},{key:"registerEvent",value:function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:window;this.eventList||(this.eventList=[]);var i=t.bind(this);this.eventList.push({eventTarget:n,type:e,handler:i}),n.addEventListener(e,i)}},{key:"unregisterEvent",value:function(){this.eventList.forEach((function(e){return e.eventTarget.removeEventListener(e.type,e.handler)}))}},{key:"addChildrenKey",value:function(e,t){var n=this.getNodeElement(e),i=n.getAttribute("data-children")||"";n.setAttribute("data-children",i?"".concat(i,",").concat(t):t)}},{key:"removeChildrenKey",value:function(e,t){var n=this.getChildrenKeys(e);if(n.length){var i=n.indexOf(t);i>-1&&n.splice(i,1);var r=this.getNodeElement(e);n.length?r.setAttribute("data-children",n.join()):r.removeAttribute("data-children")}}},{key:"replaceChildrenKey",value:function(e,t,n){var i=this.getChildrenKeys(e);if(i.length){var r=i.indexOf(t);-1!==r&&(i.splice(r,1,n),this.getNodeElement(e).setAttribute("data-children",i.join()))}}},{key:"createChildrenContainer",value:function(e){var t=document.createElement("div");return t.classList.add("tree-chart-children-container"),e&&t.classList.add(e),t}},{key:"getChildrenContainer",value:function(e){return this.getNodeElement(e).nextElementSibling}},{key:"removeChildrenContainer",value:function(e){var t=this.getChildrenContainer(e);t.parentElement.removeChild(t)}},{key:"getKeyField",value:function(e){return e[this.option.keyField].toString()||null}}]),n(t,[{key:"mergeOption",value:function(e){var t=o({keyField:"id",isVertical:!0,distanceX:40,distanceY:40,allowFold:!1,foldNodeKeys:[],draggable:!1,dragScroll:!1,autoScrollTriggerDistance:50,autoScrollSpeed:6,line:{type:"bezier",smooth:50}},e),n=t.draggable,i=t.allowFold,r=t.dragScroll,a=t.isVertical,s=t.line,c=t.padding,d=t.distanceX,h=t.distanceY;d<40&&(t.distanceX=40),h<40&&(t.distanceY=40),this.draggable=n,this.allowFold=i,this.dragScroll=r,this.isVertical=a,this.lineConfig=s;var u=o({top:30,right:30,bottom:30,left:30},c);for(var v in u){var g=u[v];l(g)||(g=0),n&&g<30&&(g=30),u[v]=g}this.containerPadding=u,this.option=t,this.initHooks()}},{key:"initHooks",value:function(){var e=this,t=["contentRender"].concat(["nodeControl","preventDrag"],["dragStart","dragEnd","click","mouseEnter","mouseLeave"]);this.hooks={},t.forEach((function(t){var n=e.option[t];"function"==typeof n&&(e.hooks[t]=n)}))}},{key:"createChartElement",value:function(){var e=this.isVertical,t=this.option,n=t.container,i=t.data;n.classList.add("tree-chart"),e&&n.classList.add("is-vertical"),this.container=n,this.createNodes(i,this.createNodesContainer()),this.createLinkContainer(),this.createLink(),this.createGhostContainer()}},{key:"createNodesContainer",value:function(){var e=this.container,t=this.containerPadding,n=this.createNodeContainer();for(var i in n.classList.add("is-nodes-container"),t)if(/left|top|right|bottom/.test(i)){var r=t[i];n.style["padding".concat(i.replace(/^./,(function(e){return e.toUpperCase()})))]="".concat(r,"px")}e.appendChild(n),this.nodesContainer=n;var o=this.createNodeContainer();return n.append(o),o}},{key:"createNodeContainer",value:function(){var e=document.createElement("div");return e.classList.add("tree-chart-container"),e}},{key:"createNodes",value:function(e,t,n){var i=this,r=this.allowFold,o=this.rootNode,a=this.option,s=e.children,l=a.foldNodeKeys,c=Array.isArray(s)&&s.length>0,d=this.createNode(e);if(t.appendChild(d),o&&!n||(this.rootNode=d),c){var h=this.createChildrenContainer();if(t.appendChild(h),r){var u=this.createFoldButton(d);l.includes(this.getKeyField(e))&&(u.classList.add("is-fold"),h.classList.add("is-hidden"))}var v=[];s.forEach((function(e){v.push(i.getKeyField(e));var t=i.createNodeContainer();h.appendChild(t),i.createNodes(e,t)})),d.setAttribute("data-children",v.join())}}},{key:"createNode",value:function(e){var t=this.draggable,n=this.hooks,i=this.option,r=i.distanceX,a=i.distanceY,l=n.contentRender,c=n.nodeControl,d=document.createElement("div"),h=this.getKeyField(e);d.classList.add("tree-chart-node","tree-chart-item-".concat(h)),d.setAttribute("data-key",h),d.style.marginBottom="".concat(a,"px"),d.style.marginRight="".concat(r,"px");var u=document.createElement("div");if(u.classList.add("tree-render-container"),l){var v=l(e);"string"==typeof v?u.innerHTML=v.replace(/>\s+</g,"><"):s(v)?u.appendChild(v):u.innerText="Please check contentRender return type is string or element"}else u.innerText="option.contentRender is required";if(d.appendChild(u),this.setNodeEvent(d),t&&c){var g=o({draggable:!0,insertChild:!0,insertPrevious:!0,insertNext:!0},c(e));!g.draggable&&d.classList.add("not-allow-drag"),!g.insertChild&&d.classList.add("not-allow-insert-child"),!g.insertPrevious&&d.classList.add("not-allow-insert-previous"),!g.insertNext&&d.classList.add("not-allow-insert-next")}return d}},{key:"createGhostContainer",value:function(){var e=this.container,t=document.createElement("div");t.classList.add("tree-chart-ghost-container"),e.appendChild(t),this.ghostContainer=t}},{key:"createFoldButton",value:function(e){if(!e.querySelector(".tree-chart-unfold")){var t=document.createElement("div");return t.classList.add("tree-chart-unfold"),t.innerHTML="<div></div><div></div>",e.appendChild(t),t}}},{key:"getFoldButton",value:function(e){return this.getNodeElement(e).querySelector(".tree-chart-unfold")}},{key:"removeFoldButton",value:function(e){var t=this.getFoldButton(e);t&&t.parentElement.removeChild(t)}},{key:"createLinkContainer",value:function(){var e=this.container,t=document.createElementNS("http://www.w3.org/2000/svg","svg");t.classList.add("tree-chart-link-container"),e.appendChild(t),this.linkContainer=t}},{key:"createLink",value:function(){var e=this,t=this.container,n=this.nodesContainer,i=this.linkContainer,r=this.draggable,o=this.isVertical,a=t.getBoundingClientRect(),s=a.left,l=a.top,c=t.scrollLeft,d=t.scrollTop;i.innerHTML="",r&&this.initPositionData(),n.querySelectorAll(".tree-chart-node").forEach((function(t){var n=t.getAttribute("data-children"),i=t.getBoundingClientRect(),a=i.left,h=i.right,u=i.top,v=i.bottom,g=e.getKeyByElement(t),f=t.nextElementSibling;if(n&&!f.classList.contains("is-hidden")){var y={x:a-s+c+(o?t.offsetWidth/2:t.offsetWidth),y:u-l+d+(o?t.offsetHeight:t.offsetHeight/2),key:g};n.split(",").forEach((function(t){var n=e.getNodeElement(t),i=n.getBoundingClientRect(),r=i.left,a=i.top,h=n.offsetWidth,u=(n.offsetHeight,{x:r-s+c+(o?h/2:0),y:a-l+d+(o?0:n.offsetHeight/2),key:t});e.drawLine(y,u)}))}r&&e.addPositionData(g,{left:a-s+c,right:h-s+c,top:u-l+d,bottom:v-l+d})}))}},{key:"initPositionData",value:function(){this.positionData={left:{sortList:[]},right:{sortList:[]},top:{sortList:[]},bottom:{sortList:[]},node:{}}}},{key:"addPositionData",value:function(e,t){var n=this.positionData;for(var i in n.node[e]=t,t){var r=t[i],o=n[i];o[r]?o[r].push(e):o[r]=[e];var a=o.sortList;if(a.length)for(var s=0,l=a.length;s<l;s++){var c=a[s];if(r===c)break;if(r<c){a.splice(s,0,r);break}s===l-1&&a.push(r)}else a.push(r)}}},{key:"replacePositionNodeKey",value:function(e,t){var n=this.positionData;n.node[t]=n.node[e],delete n.node[e],["left","top","right","bottom"].forEach((function(i){var r=n[i];for(var o in r)if("sortList"!==o){var a=r[o],s=a.indexOf(e);if(s>-1){a.splice(s,1,t);break}}}))}},{key:"setEvent",value:function(){var e=this.allowFold,t=this.draggable,n=this.dragScroll;e&&this.setFoldEvent(),this.setClickHook(),t&&this.setDragEvent(),n&&this.setDragScroll()}},{key:"setFoldEvent",value:function(){var e=this;this.nodesContainer.addEventListener("click",(function(t){var n=t.target;n.classList.contains("tree-chart-unfold")&&e.toggleFold(e.getKeyByElement(n.parentElement))}))}},{key:"drawLine",value:function(e,t,n){var i=this.linkContainer,r=this.isVertical,o=this.allowFold,a=this.lineConfig,s=a.type,l=a.smooth,c="line-".concat(e.key,"-").concat(t.key),d=document.querySelector(".".concat(c));d||((d=document.createElementNS("http://www.w3.org/2000/svg","path")).classList.add(c,"line-from-".concat(e.key)),n&&d.classList.add("is-temp-line"),i.appendChild(d));var h="",u=e.x,v=e.y,g=t.x,f=t.y;o&&this.existChildren(e.key)&&(r?v+=5:u+=5);var y=(g-u)/2,p=(f-v)/2,m="".concat(u," ").concat(v),C="".concat(g," ").concat(f);if("straight"===s&&(h="M".concat(m," T ").concat(C)),"broken"===s){var k="",E="";r?(k="".concat(u," ").concat(v+p),E="".concat(g," ").concat(f-p)):(k="".concat(u+y," ").concat(v),E="".concat(g-y," ").concat(f)),h="M".concat(m," L").concat(k," L").concat(E," L").concat(C)}if("bezier"===s){var L=l/100,b="";b=r?"".concat(u," ").concat(v+(p-p*L)):"".concat(u+(y-y*L)," ").concat(v);var N="".concat(u+y," ").concat(v+p);h="M".concat(m," Q").concat(b," ").concat(N," T ").concat(C)}d.setAttribute("d",h)}},{key:"getCurrentEventNode",value:function(e){var t=this.nodesContainer;if(e.classList.contains("tree-chart-unfold"))return null;for(var n=e;t!==n;){if(n.classList.contains("tree-chart-node"))return n;n=n.parentElement}return null}},{key:"isDragging",value:function(){var e=this.draggable,t=this.dragData;if(!e)return!1;var n=t.ghostTranslateX,i=t.ghostTranslateY;return t.key&&(0!==n||0!==i)}},{key:"setClickHook",value:function(){var e=this,t=this.nodesContainer,n=this.hooks.click;if(n){var i=null;t.addEventListener("mousedown",(function(t){var n=t.button,r=t.target;0===n&&(i=e.getCurrentEventNode(r))})),t.addEventListener("mouseup",(function(t){var r=t.button,o=t.target;if(0===r){var a=e.getCurrentEventNode(o);a&&a===i&&!e.isDragging()&&n({key:e.getKeyByElement(a),element:a},t)}}))}}},{key:"setNodeEvent",value:function(e){var t=this,n=this.hooks,i=n.mouseEnter,r=n.mouseLeave,o={key:this.getKeyByElement(e),element:e},a=e.querySelector(".tree-render-container");i&&a.addEventListener("mouseenter",(function(e){t.isDragging()||i(o,e)})),r&&a.addEventListener("mouseleave",(function(e){t.isDragging()||r(o,e)}))}},{key:"createNodeParams",value:function(e){return{key:e,parentKey:this.getParentKey(e),previousKey:this.getPreviousKey(e),nextKey:this.getNextKey(e)}}},{key:"initDragData",value:function(){this.dragData={key:null,ghostElement:null,ghostTranslateX:0,ghostTranslateY:0,mouseDownOffsetX:0,mouseDownOffsetY:0}}},{key:"setDragEvent",value:function(){var e=this,t=this.ghostContainer,n=this.container,i=this.nodesContainer,r=this.hooks,o=this.option,s=o.autoScrollTriggerDistance,l=o.autoScrollSpeed,c=r.preventDrag,d=r.dragStart,h=r.dragEnd,u=!0;this.initDragData(),i.addEventListener("mousedown",(function(t){var i=t.button,r=t.clientX,o=t.clientY;if(0===i){var a=e.getCurrentEventNode(t.target),s=e.getKeyByElement(a);if(a&&a!==e.rootNode&&!(a.classList.contains("not-allow-drag")||c&&c({key:s,element:a},t))){var l=e.positionData.node[s],d=l.left,h=l.top,u=a.cloneNode(!0);u.style.margin="0px",Object.assign(e.dragData,{key:s,ghostElement:u,mouseDownOffsetX:r+n.scrollLeft-d,mouseDownOffsetY:o+n.scrollTop-h}),e.FollowScroll.start(u)}}})),i.addEventListener("mousemove",(function(r){var o=r.button,a=r.movementX,s=r.movementY,l=r.clientX,c=r.clientY,h=e.dragData,v=h.ghostElement,g=h.mouseDownOffsetX,f=h.mouseDownOffsetY,y=h.key;if(0===o&&y&&(0!==a||0!==s)){getSelection&&getSelection().removeAllRanges(),i.classList.add("cursor-move"),!t.contains(v)&&t.appendChild(v);var p=l+n.scrollLeft-g,m=c+n.scrollTop-f;v.style.transform="translate(".concat(p,"px, ").concat(m,"px)"),Object.assign(e.dragData,{ghostTranslateX:p,ghostTranslateY:m});var C=e.getGhostPosition();e.setDragEffect(C),d&&u&&(u=!1,d({key:y,element:e.getNodeElement(y)}))}})),this.registerEvent("mouseup",(function(){u=!0;var t=e.dragData,n=e.nodesContainer,i=e.ghostContainer,r="",o="",a=t.key;if(a){var s=n.querySelector(".collide-node");if(s&&(r=e.getKeyByElement(s),s.classList.contains("become-previous")&&(o="previous"),s.classList.contains("become-next")&&(o="next"),s.classList.contains("become-child")&&(o="child")),e.FollowScroll.stop(),n.classList.remove("cursor-move"),i.innerHTML="",e.removeDragEffect(),e.initDragData(),s){var l=e.createNodeParams(a);e.insertNode(r,a,o);var c=e.createNodeParams(a);h&&h({from:l,to:c,target:r,key:a,type:o})}}}));var v={top:n.scrollTop,left:n.scrollLeft};this.registerEvent("scroll",(function(){var t=e.dragData,i=t.key,r=t.ghostElement,o=t.ghostTranslateY,a=t.ghostTranslateX,s=v.left,l=v.top,c=n.scrollLeft,d=n.scrollTop;if(i&&r){var h=a+c-s,u=o+d-l;r.style.transform="translate(".concat(h,"px, ").concat(u,"px)"),Object.assign(e.dragData,{ghostTranslateX:h,ghostTranslateY:u}),e.setDragEffect(e.getGhostPosition())}v.left=c,v.top=d}),n),this.FollowScroll=new a({scrollContainer:n,autoScrollTriggerDistance:s,eventContainer:i,autoScrollSpeed:l})}},{key:"getGhostPosition",value:function(){var e=this.dragData,t=e.ghostTranslateX,n=e.ghostTranslateY,i=e.ghostElement;return{left:t,top:n,right:t+i.offsetWidth,bottom:n+i.offsetHeight}}},{key:"removeDragEffect",value:function(){var e=this.linkContainer,t=this.nodesContainer,n=e.querySelector(".is-temp-line");n&&e.removeChild(n);var i=t.querySelector(".collide-node");i&&i.classList.remove("collide-node","become-previous","become-next","become-child");var r=t.querySelector(".temp-children-container");r&&r.parentElement.removeChild(r);var o=t.querySelector(".show-not-allow");o&&o.classList.remove("show-not-allow")}},{key:"getCollideType",value:function(e,t){var n=this.rootNode,i=this.isVertical,r=this.dragData,o=this.positionData,a=r.key,s=o.node[e],l=s.top,c=s.bottom,d=s.left,h=s.right,u=t.left,v=t.right,g=t.top,f=t.bottom,y=this.getNodeElement(a),p=this.getNodeElement(e),m=e===this.getParentKey(a),C=e===this.getPreviousKey(a),k=e===this.getNextKey(a),E={child:!p.classList.contains("not-allow-insert-child")&&!m,next:!p.classList.contains("not-allow-insert-next")&&!C,previous:!p.classList.contains("not-allow-insert-previous")&&!k};if(p===n)return E.child?"child":"notAllow";if(y.parentElement.contains(p))return"notAllow";if(i){var L=.5*(h-d);if(v<=d+L&&E.previous)return"previous";if(u>=h-L&&E.next)return"next"}else{var b=.5*(c-l);if(f<=l+b&&E.previous)return"previous";if(g>=c-b&&E.next)return"next"}if(E.child)return"child";for(var N in E)if(E[N])return N;return"notAllow"}},{key:"getCollideLinePosition",value:function(e,t){var n=this.isVertical,i=this.positionData,r=this.option,o=r.distanceX,a=r.distanceY,s={},l=i.node[t],c=l.top,d=l.bottom,h=l.left,u=l.right;if(n){if("child"===e){if(s.from={x:(h+u)/2,y:d,key:t},this.existChildren(t)&&!this.nodeIsFold(t)){var v=this.getChildrenKeys(t).pop(),g=i.node[v],f=g.right,y=g.top;return s.to={x:f+20,y:y,key:"temp"},s}return s.to={x:(h+u)/2,y:d+a,key:"temp"},s}var p=this.getParentKey(t),m=i.node[p];return s.from={x:(m.left+m.right)/2,y:m.bottom,key:p},s.to={x:"previous"===e?h-20:u+20,y:c,key:"temp"},s}if("child"===e){if(s.from={x:u,y:(c+d)/2,key:t},this.existChildren(t)&&!this.nodeIsFold(t)){var C=this.getChildrenKeys(t).pop(),k=i.node[C],E=k.left,L=k.bottom;return s.to={x:E,y:L+20,key:"temp"},s}return s.to={x:u+o,y:(c+d)/2,key:"temp"},s}var b=this.getParentKey(t),N=i.node[b];return s.from={x:N.right,y:(N.top+N.bottom)/2,key:b},s.to={x:h,y:"previous"===e?c-20:d+20,key:"temp"},s}},{key:"createDragEffect",value:function(e,t){var n=this.positionData,i=this.getNodeElement(e),r=this.getCollideType(e,t);if("notAllow"===r)return i.classList.add("show-not-allow");i.classList.add("become-".concat(r),"collide-node");var o=this.getCollideLinePosition(r,e),a=o.from,s=o.to;if(this.drawLine(a,s,!0),"child"===r&&(!this.existChildren(e)||this.nodeIsFold(e))){var l=n.node[e],c=l.top,d=l.bottom,h=l.left,u=l.right,v=document.createElement("div");v.classList.add("tree-chart-node","temp-chart-content"),v.style.width="".concat(u-h,"px"),v.style.height="".concat(d-c,"px");var g=this.createChildrenContainer("temp-children-container"),f=this.createNodeContainer(!0);f.appendChild(v),g.appendChild(f),i.parentElement.appendChild(g)}}},{key:"getCollideNode",value:function(e){var t=e.left,n=e.right,i=e.top,r=e.bottom,o=this.dragData.key,a=this.positionData,s=a.left,c=a.right,d=a.top,h=a.bottom,u=a.node,v=function(e,t,n){var i=t.length;if(n){for(var r=0;r<i;r++)if(t[r]>=e)return t[r]}else for(var o=i-1;o>-1;o--)if(t[o]<=e)return t[o];return null},g=v(t,c.sortList,!0),f=v(i,h.sortList,!0),y=v(n,s.sortList),p=v(r,d.sortList),m=function(e,t,n){var i=[];return l(e)&&t.sortList.forEach((function(r){(n?r>=e:r<=e)&&(i=i.concat(t[r]))})),i},C=function(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];var i=t.length;if(i<2)return[];var r=[],o={};return t.reduce((function(e,t){return e.concat(t)}),[]).forEach((function(e){o[e]?(o[e]++,o[e]===i&&r.push(e)):o[e]=1})),r}(m(g,c,!0),m(f,h,!0),m(y,s),m(p,d)),k=C.indexOf(o);if(k>-1&&C.splice(k,1),!C.length)return null;if(1===C.length)return C[0];var E={nodeKey:"",nodeArea:0};return C.forEach((function(e){var o=function(e){var o=u[e],a=o.left,s=o.right,l=o.top,c=o.bottom;return(a<=t?s-t:n-a)*(l<=i?c-i:r-l)}(e);o>E.nodeArea&&Object.assign(E,{nodeKey:e,nodeArea:o})})),E.nodeKey}},{key:"setDragEffect",value:function(e){this.removeDragEffect();var t=this.getCollideNode(e);t&&this.createDragEffect(t,e)}},{key:"resize",value:function(){var e=this.nodesContainer,t=this.linkContainer,n=this.draggable,i=this.ghostContainer,r=this.rootNode,o=this.isVertical,a=e.style,s=t.style;a.height="auto",a.width="auto",a.minHeight="auto",a.minWidth="auto";var l=e.clientWidth,c=e.clientHeight;if(n){var d=r.getBoundingClientRect(),h=d.width,u=d.height;o?c+=u:l+=h}var v="".concat(l,"px"),g="".concat(c,"px");if(a[o?"minHeight":"minWidth"]="100%",a.width=s.width=v,a.height=s.height=g,n){var f=i.style;f.width=v,f.height=g}}},{key:"setDragScroll",value:function(){var e=this.container,t=this.nodesContainer,n=!0;t.addEventListener("mousedown",(function(e){0!==e.button||function(e){if(e.classList.contains("tree-chart-node"))return e;for(var n=e;t!==n;){if(n.classList.contains("tree-chart-node"))return n;n=n.parentElement}return null}(e.target)||(n=!1)})),t.addEventListener("mousemove",(function(t){t.preventDefault(),0!==t.button||n||(e.scrollLeft=e.scrollLeft-t.movementX,e.scrollTop=e.scrollTop-t.movementY)})),this.registerEvent("mouseup",(function(e){0===e.button&&(n=!0)}))}},{key:"destroy",value:function(){for(var e in this.FollowScroll.destroy(),this.unregisterEvent(),this.container.innerHTML="",this)this[e]=null}}]),t}();export default c; |
{ | ||
"name": "treechartjs", | ||
"version": "0.0.1", | ||
"version": "0.0.2", | ||
"description": "", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
391733
1690