UNPKG

vue-chart-tree

Version:

一个特殊布局模式结构的`vue Tree`组件

329 lines (324 loc) 13.9 kB
import Vue from 'vue' import { dataOpened, treeNodeClassName, connectLineClassName, treeParentClassName, treeChildrenClassName } from './const' /** * 初始化树状图 tree * @param {HTMLElement} rootTreeNode 树状图的跟 treeNode 节点 */ export function resetTree(rootTreeNode) { Vue.nextTick(() => { _resetTree(rootTreeNode) }) } /** * 初始化树状图 tree * @param {HTMLElement} rootTreeNode 树状图的跟 treeNode 节点 */ function _resetTree (rootTreeNode) { const allLevelParentEles = getAllLevelParentEles(rootTreeNode) allLevelParentEles.forEach(item => { // 如果当前层只有一个元素(或是只有一个展开可见的元素),说明不需要对齐,也就不需要专门计算位置了 if (item.length > 1) { computedParentElesPosition(item) } }) // 必须等到 computedParentElesPosition 全部计算完毕,所有的元素放置在了正确的位置上, // 才能继续计算伸缩节点和竖向连接线的信息 allLevelParentEles.forEach(item => { item.forEach((v, index) => { // 如果最后一个节点存在下一个兄弟节点(这个节点就是伸缩节点),说明肯定有子节点,需要正常计算; // 否则就是叶子节点,肯定没有伸缩节点和子节点,就不需要计算 if (index !== item.length - 1 || v.nextElementSibling) { // 计算伸缩节点的位置 computedShrinkPosition(v) // 计算当前节点下直接子节点竖向连接线的位置和高度 resetChildenVline(v) } }) }) } /** * 更新树状图 * 因局部节点状态发生变化,更新受到影响的所有节点的位置和height信息 * @param {HTMLElement} treeNode 发生变化的节点所在的 treeNode 节点 */ export function updatePartTree (treeNode) { Vue.nextTick(() => { _updatePartTree(treeNode) }) } /** * 更新树状图 * 因局部节点状态发生变化,更新受到影响的所有节点的位置和height信息 * @param {HTMLElement} treeNode 发生变化的节点所在的 treeNode 节点 */ function _updatePartTree(treeNode) { if (!isOpened(treeNode)) { _resetTree(treeNode) } // 更新当前层的位置 updateParentElesPosition(treeNode) // 更新伸缩节点位置 updateShrinkPosition(treeNode) // 更新竖向连接线位置和height updateConnectVLine(treeNode) } /** * 获取 treeNode 下所有的第一次被展开的 .treeParentClassName 元素 * 每一层的 .treeParentClassName 元素合成一个数组,所有层的数组再合成一个数组 * @param {HTMLElement} treeNode treeNode 节点 * @returns {HTMLElement[][]} */ function getAllLevelParentEles (treeNode) { const parentEles = [] // 每一层的 .treeParentClassName 元素合成一个数组 let currentLevelParentEles = [] let currentTreeNode let cacheCurrentTreeNode = null let currentChildrenEle = null let nextLevelTreeNodes = [treeNode] while (currentTreeNode = nextLevelTreeNodes.pop()) { // 已经被展开过的节点,就无需重复计算位置了 while (currentTreeNode && !isOpened(currentTreeNode)) { cacheCurrentTreeNode = currentTreeNode currentLevelParentEles.push(getEleByClassName(currentTreeNode, treeParentClassName)) // 继续查找当前节点的下级 treeNode 节点 currentChildrenEle = getEleByClassName(currentTreeNode, treeChildrenClassName) currentTreeNode = getLowTreeNode(currentTreeNode) // 如果子元素 display: none, 即视觉不可见,那么无需关心其子元素 if (currentChildrenEle && currentChildrenEle.style.display !== 'none') { // 当前子元素的兄弟节点也需要进行同样操作 // 第0个是竖向连接线节点,第 1 个是当前treeNode节点,第2个及其后续才是当前 treeNode 的兄弟treeNode节点 nextLevelTreeNodes = nextLevelTreeNodes.concat(getSiblings(currentTreeNode).slice(2)) setDataOpened(cacheCurrentTreeNode) } else { break } } parentEles.push(currentLevelParentEles) currentLevelParentEles = [] } return parentEles } /** * 对齐当前层的 .treeParentClassName 元素 * @param {HTMLElement[]} parentEles 位于同一层的需要进行居中对齐的 .treeParentClassName 元素集合 */ function computedParentElesPosition (parentEles) { const heights = parentEles.map(ele => ele.offsetHeight) // 找出最大高度 const maxHeight = Math.max.apply(null, heights) const halfMaxHeight = maxHeight / 2 parentEles.forEach((ele, index) => { if (heights[index] < maxHeight) { ele.style.marginTop = halfMaxHeight - heights[index] / 2 + 'px' } else { ele.style.marginTop = '0' } }) } /** * 重置伸缩节点的位置 * @param {HTMLElement} parentEle 和需要重置位置的伸缩节点拥有同一个直接父节点的 .treeParentClassName 节点 */ function computedShrinkPosition (parentEle) { // 当前 treeParentClassName 节点的下一个兄弟节点就是控制伸缩的节点 const stretchNodeEle = parentEle.nextElementSibling if (stretchNodeEle) { stretchNodeEle.style.transform = `translateY(${parentEle.offsetTop + parentEle.offsetHeight / 2 - stretchNodeEle.offsetHeight / 2}px)` } } /** * 计算当前节点下所有直接子节点竖向连接线的位置和高度 * @param {HTMLElement} parentEle 和需要计算位置和高度的竖向连接线隶属于的 .treeChildrenClassName 节点拥有同一个直接父节点的 .treeParentClassName 节点 */ export function resetChildenVline (parentEle) { // 当前节点的下下个兄弟节点,就是当前节点的子节点所在的容器 const accountsChildrenEle = parentEle.nextElementSibling.nextElementSibling const treeNodeEle = getEleByClassName(accountsChildrenEle, treeNodeClassName) const treeNodes = getSiblings(treeNodeEle) // 说明只有0个或者1个子节点,不需要竖向连接线,也就不需要计算竖向连接线的位置和高度了 if (treeNodes.length <= 1) return computedConnectVLine(treeNodeEle, treeNodes[treeNodes.length - 1], getEleByClassName(accountsChildrenEle, connectLineClassName)) } /** * 计算竖向连接线的高度及其位置 * @param {HTMLElement} firstTreeNodeEle 竖向连接线连接的子节点集合中的第一个子节点所在的 treeNode 节点 * @param {HTMLElement} lastTreeNodeEle 竖向连接线连接的子节点集合中的最后一个子节点所在的 treeNode 节点 * @param {HTMLElement} connectLineEle 竖向连接线节点 */ function computedConnectVLine (firstTreeNodeEle, lastTreeNodeEle, connectLineEle) { // 第一个子节点 const firstNodeEle = getEleByClassName(firstTreeNodeEle, treeParentClassName) // 最后一个子节点 const lastNodeEle = getEleByClassName(lastTreeNodeEle, treeParentClassName) const firstNodeEleRect = firstNodeEle.getBoundingClientRect() const lastNodeEleRect = lastNodeEle.getBoundingClientRect() // 根据第一个子节点和最后一个子节点的位置和尺寸关系,计算出 connectLineEle 的位置和高度 connectLineEle.style.transform = `translateY(${firstNodeEleRect.height / 2 + firstNodeEle.offsetTop}px)` connectLineEle.style.height = lastNodeEleRect.bottom - firstNodeEleRect.top - firstNodeEleRect.height / 2 - lastNodeEleRect.height / 2 + 'px' } /** * 更新当前层所有 .treeParentClassName 元素的位置 * @param {HTMLElement|null} treeNode 状态发生了变化的 treeNode 节点 */ function updateParentElesPosition (treeNode) { const eles = [] let prevTreeNode = treeNode // 获取其所有层级下被展开子节点 treeNode let accountsChildrenEle = getEleByClassName(prevTreeNode, treeChildrenClassName) // 只获取被展开的子节点(没被展开的肯定也就不需要关心位置了) while (accountsChildrenEle && accountsChildrenEle.style.display !== 'none') { eles.push(getEleByClassName(accountsChildrenEle, treeNodeClassName, treeParentClassName)) accountsChildrenEle = getEleByClassName(accountsChildrenEle, treeNodeClassName, treeChildrenClassName) } // 获取同层上级父节点 treeNode while (prevTreeNode && isFirstTreeNode(prevTreeNode)) { eles.push(getEleByClassName(prevTreeNode, treeParentClassName)) prevTreeNode = getHighTreeNode(prevTreeNode) } if (prevTreeNode) { eles.push(getEleByClassName(prevTreeNode, treeParentClassName)) } computedParentElesPosition(eles) } /** * 更新当前 treeNode 所能影响到的所有伸缩节点的位置 * @param {HTMLElement|null} treeNode 状态发生了变化的 treeNode 节点 */ function updateShrinkPosition (treeNode) { let currentTreeNode = treeNode const parentEles = [] // 父元素 while (currentTreeNode) { parentEles.push(getEleByClassName(currentTreeNode, treeParentClassName)) currentTreeNode = getHighTreeNode(currentTreeNode) } // 同层子元素 currentTreeNode = getLowTreeNode(treeNode) while (currentTreeNode) { parentEles.push(getEleByClassName(currentTreeNode, treeParentClassName)) currentTreeNode = getLowTreeNode(currentTreeNode) } parentEles.forEach(computedShrinkPosition) } /** * 更新当前 treeNode 所能影响到的所有竖向连接线的高度和位置 * @param {HTMLElement|null} treeNode treeNode元素 */ function updateConnectVLine (treeNode) { let currentTreeNode = treeNode const siblingsTreeNodes = [] // currentTreeNode存在,并且不是最顶层的 treeNode(最顶层的 treeNode 肯定没有父级了,也就不会被父级的竖向连接线所连接,所以不需要考虑对父级竖向连接线的影响) while (currentTreeNode && !isRootTreeNode(currentTreeNode)) { pushSiblingsTreeNodes(currentTreeNode, siblingsTreeNodes) currentTreeNode = getHighTreeNode(currentTreeNode) } // 继续查找同层子 treeNode currentTreeNode = getLowTreeNode(treeNode) while (currentTreeNode) { pushSiblingsTreeNodes(currentTreeNode, siblingsTreeNodes) currentTreeNode = getLowTreeNode(currentTreeNode) } siblingsTreeNodes.forEach(item => computedConnectVLine(item.firstTreeNodeEle, item.lastTreeNodeEle, item.connectLineEle)) } /** * 更新 siblingsTreeNodes * @param {HTMLElement} currentTreeNode * @param {any[]} siblingsTreeNodes */ function pushSiblingsTreeNodes (currentTreeNode, siblingsTreeNodes) { const siblings = getSiblings(currentTreeNode) if (siblings.length > 1) { // 如果兄弟节点数量大于 1,那么说明这个 treeNode 字节点肯定存在同为 treeNode的兄弟节点,也肯定存在竖向连接线节点 // 其父节点下的第一个节点肯定是竖向连接线节点 // 0 是 竖向连接线, 1 才是第一个 treeNode siblingsTreeNodes.push({ firstTreeNodeEle: siblings[1], lastTreeNodeEle: siblings[siblings.length - 1], connectLineEle: siblings[0] }) } } /** * 获取 rootEle 元素下的第一个 className 元素 * @param rootEle rootEle节点元素 * @param {string[]} classNames 类名 * @returns {HTMLElement|null} */ function getEleByClassName (rootEle, ...classNames) { let ele = rootEle for (let index = 0; index < classNames.length; index++) { if (!ele) return null ele = ele.querySelector('.' + classNames[index]) } return ele } /** * 获取所有兄弟节点(按照子节点在父节点中的前后顺序,包括自身) * @param {HTMLElement|null} ele 节点元素 * @returns {HTMLElement[]} */ function getSiblings (ele) { if (!ele) return [] const eles = [] let rootEle = ele // 移动到第一个兄弟节点 while (rootEle.previousElementSibling) { rootEle = rootEle.previousElementSibling } while (rootEle) { eles.push(rootEle) rootEle = rootEle.nextElementSibling } return eles } /** * 根据当前 treeNode 元素查找上一层的 treeNode(treeNode 指的是 className 为 treeNodeClassName,即 <accounts-tree-node /> 组件的顶层DOM节点) * @param {HTMLElement} treeNode 当前 treeNode 节点 */ function getHighTreeNode (treeNode) { const highTreeNode = treeNode.parentElement && treeNode.parentElement.parentElement if (highTreeNode && highTreeNode.className.indexOf(treeNodeClassName) !== -1) { return highTreeNode } return null } /** * 根据当前 treeNode 元素,查找第一个子 treeNode(并且 display 不能是 none) * @param {HTMLElement} treeNode */ function getLowTreeNode (treeNode) { const lowChildren = getEleByClassName(treeNode, treeChildrenClassName) if (lowChildren && lowChildren.style.display !== 'none') { return getEleByClassName(lowChildren, treeNodeClassName) } return null } /** * 传入的参数节点是否是其父节点下第一个 treeNode 节点 * @param treeNode treeNode节点 */ function isFirstTreeNode (treeNode) { const prevTreeNode = treeNode.previousElementSibling // 是其父节点的第一个子节点,并且没有上一个兄弟节点(这种情况下,treeNode就是顶层节点), // 或者存在兄弟 treeNode 节点,并且是第一个 treeNode 节点(这种情况下,treeNode节点的第一个节点就是竖向连接线节点 .connectLineClassName) // 都认为是第一个 treeNode 节点 return !prevTreeNode || (prevTreeNode && prevTreeNode.className.indexOf(connectLineClassName) !== -1) } /** * 传入的节点是否是根 treeNode 节点 * @param {HTMLElement} treeNode treeNode */ function isRootTreeNode (treeNode) { // 根节点肯定不是其他 treeNode 的子节点 return treeNode.parentElement.className.indexOf(treeChildrenClassName) === -1 } function isOpened (treeNode) { return treeNode.getAttribute('data-' + dataOpened.key) === dataOpened.opened } function setDataOpened (treeNode) { treeNode.setAttribute('data-' + dataOpened.key, dataOpened.opened) }