UNPKG

orgchart.js

Version:

organization chart plugin based on ES6

1,367 lines (1,246 loc) 63.5 kB
export default class OrgChart { constructor(options) { this._name = 'OrgChart'; Promise.prototype.finally = function (callback) { let P = this.constructor; return this.then( value => P.resolve(callback()).then(() => value), reason => P.resolve(callback()).then(() => { throw reason; }) ); }; let that = this, defaultOptions = { 'nodeTitle': 'name', 'nodeId': 'id', 'toggleSiblingsResp': false, 'depth': 999, 'chartClass': '', 'exportButton': false, 'exportFilename': 'OrgChart', 'parentNodeSymbol': 'fa-users', 'draggable': false, 'direction': 't2b', 'pan': false, 'zoom': false }, opts = Object.assign(defaultOptions, options), data = opts.data, chart = document.createElement('div'), chartContainer = document.querySelector(opts.chartContainer); this.options = opts; delete this.options.data; this.chart = chart; this.chartContainer = chartContainer; chart.dataset.options = JSON.stringify(opts); chart.setAttribute('class', 'orgchart' + (opts.chartClass !== '' ? ' ' + opts.chartClass : '') + (opts.direction !== 't2b' ? ' ' + opts.direction : '')); if (typeof data === 'object') { // local json datasource this.buildHierarchy(chart, opts.ajaxURL ? data : this._attachRel(data, '00'), 0); } else if (typeof data === 'string' && data.startsWith('#')) { // ul datasource this.buildHierarchy(chart, this._buildJsonDS(document.querySelector(data).children[0]), 0); } else { // ajax datasource let spinner = document.createElement('i'); spinner.setAttribute('class', 'fa fa-circle-o-notch fa-spin spinner'); chart.appendChild(spinner); this._getJSON(data) .then(function (resp) { that.buildHierarchy(chart, opts.ajaxURL ? resp : that._attachRel(resp, '00'), 0); }) .catch(function (err) { console.error('failed to fetch datasource for orgchart', err); }) .finally(function () { let spinner = chart.querySelector('.spinner'); spinner.parentNode.removeChild(spinner); }); } chart.addEventListener('click', this._clickChart.bind(this)); // append the export button to the chart-container if (opts.exportButton && !chartContainer.querySelector('.oc-export-btn')) { let exportBtn = document.createElement('button'), downloadBtn = document.createElement('a'); exportBtn.setAttribute('class', 'oc-export-btn' + (opts.chartClass !== '' ? ' ' + opts.chartClass : '')); exportBtn.innerHTML = 'Export'; exportBtn.addEventListener('click', this._clickExportButton.bind(this)); downloadBtn.setAttribute('class', 'oc-download-btn' + (opts.chartClass !== '' ? ' ' + opts.chartClass : '')); downloadBtn.setAttribute('download', opts.exportFilename + '.png'); chartContainer.appendChild(exportBtn); chartContainer.appendChild(downloadBtn); } if (opts.pan) { chartContainer.style.overflow = 'hidden'; chart.addEventListener('mousedown', this._onPanStart.bind(this)); chart.addEventListener('touchstart', this._onPanStart.bind(this)); document.body.addEventListener('mouseup', this._onPanEnd.bind(this)); document.body.addEventListener('touchend', this._onPanEnd.bind(this)); } if (opts.zoom) { chartContainer.addEventListener('wheel', this._onWheeling.bind(this)); chartContainer.addEventListener('touchstart', this._onTouchStart.bind(this)); document.body.addEventListener('touchmove', this._onTouchMove.bind(this)); document.body.addEventListener('touchend', this._onTouchEnd.bind(this)); } chartContainer.appendChild(chart); } get name() { return this._name; } _closest(el, fn) { return el && ((fn(el) && el !== this.chart) ? el : this._closest(el.parentNode, fn)); } _siblings(el, expr) { return Array.from(el.parentNode.children).filter((child) => { if (child !== el) { if (expr) { return el.matches(expr); } return true; } return false; }); } _prevAll(el, expr) { let sibs = [], prevSib = el.previousElementSibling; while (prevSib) { if (!expr || prevSib.matches(expr)) { sibs.push(prevSib); } prevSib = prevSib.previousElementSibling; } return sibs; } _nextAll(el, expr) { let sibs = []; let nextSib = el.nextElementSibling; while (nextSib) { if (!expr || nextSib.matches(expr)) { sibs.push(nextSib); } nextSib = nextSib.nextElementSibling; } return sibs; } _isVisible(el) { return el.offsetParent !== null; } _addClass(elements, classNames) { elements.forEach((el) => { if (classNames.indexOf(' ') > 0) { classNames.split(' ').forEach((className) => el.classList.add(className)); } else { el.classList.add(classNames); } }); } _removeClass(elements, classNames) { elements.forEach((el) => { if (classNames.indexOf(' ') > 0) { classNames.split(' ').forEach((className) => el.classList.remove(className)); } else { el.classList.remove(classNames); } }); } _css(elements, prop, val) { elements.forEach((el) => { el.style[prop] = val; }); } _removeAttr(elements, attr) { elements.forEach((el) => { el.removeAttribute(attr); }); } _one(el, type, listener, self) { let one = function (event) { try { listener.call(self, event); } finally { el.removeEventListener(type, one); } }; el.addEventListener(type, one); } _getDescElements(ancestors, selector) { let results = []; ancestors.forEach((el) => results.push(...el.querySelectorAll(selector))); return results; } _getJSON(url) { return new Promise(function (resolve, reject) { let xhr = new XMLHttpRequest(); function handler() { if (this.readyState !== 4) { return; } if (this.status === 200) { resolve(JSON.parse(this.response)); } else { reject(new Error(this.statusText)); } } xhr.open('GET', url); xhr.onreadystatechange = handler; xhr.responseType = 'json'; // xhr.setRequestHeader('Accept', 'application/json'); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.send(); }); } _buildJsonDS(li) { let subObj = { 'name': li.firstChild.textContent.trim(), 'relationship': (li.parentNode.parentNode.nodeName === 'LI' ? '1' : '0') + (li.parentNode.children.length > 1 ? 1 : 0) + (li.children.length ? 1 : 0) }; if (li.id) { subObj.id = li.id; } if (li.querySelector('ul')) { Array.from(li.querySelector('ul').children).forEach((el) => { if (!subObj.children) { subObj.children = []; } subObj.children.push(this._buildJsonDS(el)); }); } return subObj; } _attachRel(data, flags) { data.relationship = flags + (data.children && data.children.length > 0 ? 1 : 0); if (data.children) { for (let item of data.children) { this._attachRel(item, '1' + (data.children.length > 1 ? 1 : 0)); } } return data; } _repaint(node) { if (node) { node.style.offsetWidth = node.offsetWidth; } } // whether the cursor is hovering over the node _isInAction(node) { return node.querySelector(':scope > .edge').className.indexOf('fa-') > -1; } // detect the exist/display state of related node _getNodeState(node, relation) { let criteria, state = { 'exist': false, 'visible': false }; if (relation === 'parent') { criteria = this._closest(node, (el) => el.classList && el.classList.contains('nodes')); if (criteria) { state.exist = true; } if (state.exist && this._isVisible(criteria.parentNode.children[0])) { state.visible = true; } } else if (relation === 'children') { criteria = this._closest(node, (el) => el.nodeName === 'TR').nextElementSibling; if (criteria) { state.exist = true; } if (state.exist && this._isVisible(criteria)) { state.visible = true; } } else if (relation === 'siblings') { criteria = this._siblings(this._closest(node, (el) => el.nodeName === 'TABLE').parentNode); if (criteria.length) { state.exist = true; } if (state.exist && criteria.some((el) => this._isVisible(el))) { state.visible = true; } } return state; } // find the related nodes getRelatedNodes(node, relation) { if (relation === 'parent') { return this._closest(node, (el) => el.classList.contains('nodes')) .parentNode.children[0].querySelector('.node'); } else if (relation === 'children') { return Array.from(this._closest(node, (el) => el.nodeName === 'TABLE').lastChild.children) .map((el) => el.querySelector('.node')); } else if (relation === 'siblings') { return this._siblings(this._closest(node, (el) => el.nodeName === 'TABLE').parentNode) .map((el) => el.querySelector('.node')); } return []; } _switchHorizontalArrow(node) { let opts = this.options, leftEdge = node.querySelector('.leftEdge'), rightEdge = node.querySelector('.rightEdge'), temp = this._closest(node, (el) => el.nodeName === 'TABLE').parentNode; if (opts.toggleSiblingsResp && (typeof opts.ajaxURL === 'undefined' || this._closest(node, (el) => el.classList.contains('.nodes')).dataset.siblingsLoaded)) { let prevSib = temp.previousElementSibling, nextSib = temp.nextElementSibling; if (prevSib) { if (prevSib.classList.contains('hidden')) { leftEdge.classList.add('fa-chevron-left'); leftEdge.classList.remove('fa-chevron-right'); } else { leftEdge.classList.add('fa-chevron-right'); leftEdge.classList.remove('fa-chevron-left'); } } if (nextSib) { if (nextSib.classList.contains('hidden')) { rightEdge.classList.add('fa-chevron-right'); rightEdge.classList.remove('fa-chevron-left'); } else { rightEdge.classList.add('fa-chevron-left'); rightEdge.classList.remove('fa-chevron-right'); } } } else { let sibs = this._siblings(temp), sibsVisible = sibs.length ? !sibs.some((el) => el.classList.contains('hidden')) : false; leftEdge.classList.toggle('fa-chevron-right', sibsVisible); leftEdge.classList.toggle('fa-chevron-left', !sibsVisible); rightEdge.classList.toggle('fa-chevron-left', sibsVisible); rightEdge.classList.toggle('fa-chevron-right', !sibsVisible); } } _hoverNode(event) { let node = event.target, flag = false, topEdge = node.querySelector(':scope > .topEdge'), bottomEdge = node.querySelector(':scope > .bottomEdge'), leftEdge = node.querySelector(':scope > .leftEdge'); if (event.type === 'mouseenter') { if (topEdge) { flag = this._getNodeState(node, 'parent').visible; topEdge.classList.toggle('fa-chevron-up', !flag); topEdge.classList.toggle('fa-chevron-down', flag); } if (bottomEdge) { flag = this._getNodeState(node, 'children').visible; bottomEdge.classList.toggle('fa-chevron-down', !flag); bottomEdge.classList.toggle('fa-chevron-up', flag); } if (leftEdge) { this._switchHorizontalArrow(node); } } else { Array.from(node.querySelectorAll(':scope > .edge')).forEach((el) => { el.classList.remove('fa-chevron-up', 'fa-chevron-down', 'fa-chevron-right', 'fa-chevron-left'); }); } } // define node click event handler _clickNode(event) { let clickedNode = event.currentTarget, focusedNode = this.chart.querySelector('.focused'); if (focusedNode) { focusedNode.classList.remove('focused'); } clickedNode.classList.add('focused'); } // build the parent node of specific node _buildParentNode(currentRoot, nodeData, callback) { let that = this, table = document.createElement('table'); nodeData.relationship = nodeData.relationship || '001'; this._createNode(nodeData, 0) .then(function (nodeDiv) { let chart = that.chart; nodeDiv.classList.remove('slide-up'); nodeDiv.classList.add('slide-down'); let parentTr = document.createElement('tr'), superiorLine = document.createElement('tr'), inferiorLine = document.createElement('tr'), childrenTr = document.createElement('tr'); parentTr.setAttribute('class', 'hidden'); parentTr.innerHTML = `<td colspan="2"></td>`; table.appendChild(parentTr); superiorLine.setAttribute('class', 'lines hidden'); superiorLine.innerHTML = `<td colspan="2"><div class="downLine"></div></td>`; table.appendChild(superiorLine); inferiorLine.setAttribute('class', 'lines hidden'); inferiorLine.innerHTML = `<td class="rightLine">&nbsp;</td><td class="leftLine">&nbsp;</td>`; table.appendChild(inferiorLine); childrenTr.setAttribute('class', 'nodes'); childrenTr.innerHTML = `<td colspan="2"></td>`; table.appendChild(childrenTr); table.querySelector('td').appendChild(nodeDiv); chart.insertBefore(table, chart.children[0]); table.children[3].children[0].appendChild(chart.lastChild); callback(); }) .catch(function (err) { console.error('Failed to create parent node', err); }); } _switchVerticalArrow(arrow) { arrow.classList.toggle('fa-chevron-up'); arrow.classList.toggle('fa-chevron-down'); } // show the parent node of the specified node showParent(node) { // just show only one superior level let temp = this._prevAll(this._closest(node, (el) => el.classList.contains('nodes'))); this._removeClass(temp, 'hidden'); // just show only one line this._addClass(Array(temp[0].children).slice(1, -1), 'hidden'); // show parent node with animation let parent = temp[2].querySelector('.node'); this._one(parent, 'transitionend', function () { parent.classList.remove('slide'); if (this._isInAction(node)) { this._switchVerticalArrow(node.querySelector(':scope > .topEdge')); } }, this); this._repaint(parent); parent.classList.add('slide'); parent.classList.remove('slide-down'); } // show the sibling nodes of the specified node showSiblings(node, direction) { // firstly, show the sibling td tags let siblings = [], temp = this._closest(node, (el) => el.nodeName === 'TABLE').parentNode; if (direction) { siblings = direction === 'left' ? this._prevAll(temp) : this._nextAll(temp); } else { siblings = this._siblings(temp); } this._removeClass(siblings, 'hidden'); // secondly, show the lines let upperLevel = this._prevAll(this._closest(node, (el) => el.classList.contains('nodes'))); temp = Array.from(upperLevel[0].querySelectorAll(':scope > .hidden')); if (direction) { this._removeClass(temp.slice(0, siblings.length * 2), 'hidden'); } else { this._removeClass(temp, 'hidden'); } // thirdly, do some cleaning stuff if (!this._getNodeState(node, 'parent').visible) { this._removeClass(upperLevel, 'hidden'); let parent = upperLevel[2].querySelector('.node'); this._one(parent, 'transitionend', function (event) { event.target.classList.remove('slide'); }, this); this._repaint(parent); parent.classList.add('slide'); parent.classList.remove('slide-down'); } // lastly, show the sibling nodes with animation siblings.forEach((sib) => { Array.from(sib.querySelectorAll('.node')).forEach((node) => { if (this._isVisible(node)) { node.classList.add('slide'); node.classList.remove('slide-left', 'slide-right'); } }); }); this._one(siblings[0].querySelector('.slide'), 'transitionend', function () { siblings.forEach((sib) => { this._removeClass(Array.from(sib.querySelectorAll('.slide')), 'slide'); }); if (this._isInAction(node)) { this._switchHorizontalArrow(node); node.querySelector('.topEdge').classList.remove('fa-chevron-up'); node.querySelector('.topEdge').classList.add('fa-chevron-down'); } }, this); } // hide the sibling nodes of the specified node hideSiblings(node, direction) { let nodeContainer = this._closest(node, (el) => el.nodeName === 'TABLE').parentNode, siblings = this._siblings(nodeContainer); siblings.forEach((sib) => { if (sib.querySelector('.spinner')) { this.chart.dataset.inAjax = false; } }); if (!direction || (direction && direction === 'left')) { let preSibs = this._prevAll(nodeContainer); preSibs.forEach((sib) => { Array.from(sib.querySelectorAll('.node')).forEach((node) => { if (this._isVisible(node)) { node.classList.add('slide', 'slide-right'); } }); }); } if (!direction || (direction && direction !== 'left')) { let nextSibs = this._nextAll(nodeContainer); nextSibs.forEach((sib) => { Array.from(sib.querySelectorAll('.node')).forEach((node) => { if (this._isVisible(node)) { node.classList.add('slide', 'slide-left'); } }); }); } let animatedNodes = []; this._siblings(nodeContainer).forEach((sib) => { Array.prototype.push.apply(animatedNodes, Array.from(sib.querySelectorAll('.slide'))); }); let lines = []; for (let node of animatedNodes) { let temp = this._closest(node, function (el) { return el.classList.contains('nodes'); }).previousElementSibling; lines.push(temp); lines.push(temp.previousElementSibling); } lines = [...new Set(lines)]; lines.forEach(function (line) { line.style.visibility = 'hidden'; }); this._one(animatedNodes[0], 'transitionend', function (event) { lines.forEach(function (line) { line.removeAttribute('style'); }); let sibs = []; if (direction) { if (direction === 'left') { sibs = this._prevAll(nodeContainer, ':not(.hidden)'); } else { sibs = this._nextAll(nodeContainer, ':not(.hidden)'); } } else { sibs = this._siblings(nodeContainer); } let temp = Array.from(this._closest(nodeContainer, function (el) { return el.classList.contains('nodes'); }).previousElementSibling.querySelectorAll(':scope > :not(.hidden)')); let someLines = temp.slice(1, direction ? sibs.length * 2 + 1 : -1); this._addClass(someLines, 'hidden'); this._removeClass(animatedNodes, 'slide'); sibs.forEach((sib) => { Array.from(sib.querySelectorAll('.node')).slice(1).forEach((node) => { if (this._isVisible(node)) { node.classList.remove('slide-left', 'slide-right'); node.classList.add('slide-up'); } }); }); sibs.forEach((sib) => { this._addClass(Array.from(sib.querySelectorAll('.lines')), 'hidden'); this._addClass(Array.from(sib.querySelectorAll('.nodes')), 'hidden'); this._addClass(Array.from(sib.querySelectorAll('.verticalNodes')), 'hidden'); }); this._addClass(sibs, 'hidden'); if (this._isInAction(node)) { this._switchHorizontalArrow(node); } }, this); } // recursively hide the ancestor node and sibling nodes of the specified node hideParent(node) { let temp = Array.from(this._closest(node, function (el) { return el.classList.contains('nodes'); }).parentNode.children).slice(0, 3); if (temp[0].querySelector('.spinner')) { this.chart.dataset.inAjax = false; } // hide the sibling nodes if (this._getNodeState(node, 'siblings').visible) { this.hideSiblings(node); } // hide the lines let lines = temp.slice(1); this._css(lines, 'visibility', 'hidden'); // hide the superior nodes with transition let parent = temp[0].querySelector('.node'), grandfatherVisible = this._getNodeState(parent, 'parent').visible; if (parent && this._isVisible(parent)) { parent.classList.add('slide', 'slide-down'); this._one(parent, 'transitionend', function () { parent.classList.remove('slide'); this._removeAttr(lines, 'style'); this._addClass(temp, 'hidden'); }, this); } // if the current node has the parent node, hide it recursively if (parent && grandfatherVisible) { this.hideParent(parent); } } // exposed method addParent(currentRoot, data) { let that = this; this._buildParentNode(currentRoot, data, function () { if (!currentRoot.querySelector(':scope > .topEdge')) { let topEdge = document.createElement('i'); topEdge.setAttribute('class', 'edge verticalEdge topEdge fa'); currentRoot.appendChild(topEdge); } that.showParent(currentRoot); }); } // start up loading status for requesting new nodes _startLoading(arrow, node) { let opts = this.options, chart = this.chart; if (typeof chart.dataset.inAjax !== 'undefined' && chart.dataset.inAjax === 'true') { return false; } arrow.classList.add('hidden'); let spinner = document.createElement('i'); spinner.setAttribute('class', 'fa fa-circle-o-notch fa-spin spinner'); node.appendChild(spinner); this._addClass(Array.from(node.querySelectorAll(':scope > *:not(.spinner)')), 'hazy'); chart.dataset.inAjax = true; let exportBtn = this.chartContainer.querySelector('.oc-export-btn' + (opts.chartClass !== '' ? '.' + opts.chartClass : '')); if (exportBtn) { exportBtn.disabled = true; } return true; } // terminate loading status for requesting new nodes _endLoading(arrow, node) { let opts = this.options; arrow.classList.remove('hidden'); node.querySelector(':scope > .spinner').remove(); this._removeClass(Array.from(node.querySelectorAll(':scope > .hazy')), 'hazy'); this.chart.dataset.inAjax = false; let exportBtn = this.chartContainer.querySelector('.oc-export-btn' + (opts.chartClass !== '' ? '.' + opts.chartClass : '')); if (exportBtn) { exportBtn.disabled = false; } } // define click event handler for the top edge _clickTopEdge(event) { event.stopPropagation(); let that = this, topEdge = event.target, node = topEdge.parentNode, parentState = this._getNodeState(node, 'parent'), opts = this.options; if (parentState.exist) { let temp = this._closest(node, function (el) { return el.classList.contains('nodes'); }); let parent = temp.parentNode.firstChild.querySelector('.node'); if (parent.classList.contains('slide')) { return; } // hide the ancestor nodes and sibling nodes of the specified node if (parentState.visible) { this.hideParent(node); this._one(parent, 'transitionend', function () { if (this._isInAction(node)) { this._switchVerticalArrow(topEdge); this._switchHorizontalArrow(node); } }, this); } else { // show the ancestors and siblings this.showParent(node); } } else { // load the new parent node of the specified node by ajax request let nodeId = topEdge.parentNode.id; // start up loading status if (this._startLoading(topEdge, node)) { // load new nodes this._getJSON(typeof opts.ajaxURL.parent === 'function' ? opts.ajaxURL.parent(node.dataset.source) : opts.ajaxURL.parent + nodeId) .then(function (resp) { if (that.chart.dataset.inAjax === 'true') { if (Object.keys(resp).length) { that.addParent(node, resp); } } }) .catch(function (err) { console.error('Failed to get parent node data.', err); }) .finally(function () { that._endLoading(topEdge, node); }); } } } // recursively hide the descendant nodes of the specified node hideChildren(node) { let that = this, temp = this._nextAll(node.parentNode.parentNode), lastItem = temp[temp.length - 1], lines = []; if (lastItem.querySelector('.spinner')) { this.chart.dataset.inAjax = false; } let descendants = Array.from(lastItem.querySelectorAll('.node')).filter((el) => that._isVisible(el)), isVerticalDesc = lastItem.classList.contains('verticalNodes'); if (!isVerticalDesc) { descendants.forEach((desc) => { Array.prototype.push.apply(lines, that._prevAll(that._closest(desc, (el) => el.classList.contains('nodes')), '.lines')); }); lines = [...new Set(lines)]; this._css(lines, 'visibility', 'hidden'); } this._one(descendants[0], 'transitionend', function (event) { this._removeClass(descendants, 'slide'); if (isVerticalDesc) { that._addClass(temp, 'hidden'); } else { lines.forEach((el) => { el.removeAttribute('style'); el.classList.add('hidden'); el.parentNode.lastChild.classList.add('hidden'); }); this._addClass(Array.from(lastItem.querySelectorAll('.verticalNodes')), 'hidden'); } if (this._isInAction(node)) { this._switchVerticalArrow(node.querySelector('.bottomEdge')); } }, this); this._addClass(descendants, 'slide slide-up'); } // show the children nodes of the specified node showChildren(node) { let that = this, temp = this._nextAll(node.parentNode.parentNode), descendants = []; this._removeClass(temp, 'hidden'); if (temp.some((el) => el.classList.contains('verticalNodes'))) { temp.forEach((el) => { Array.prototype.push.apply(descendants, Array.from(el.querySelectorAll('.node')).filter((el) => { return that._isVisible(el); })); }); } else { Array.from(temp[2].children).forEach((el) => { Array.prototype.push.apply(descendants, Array.from(el.querySelector('tr').querySelectorAll('.node')).filter((el) => { return that._isVisible(el); })); }); } // the two following statements are used to enforce browser to repaint this._repaint(descendants[0]); this._one(descendants[0], 'transitionend', (event) => { this._removeClass(descendants, 'slide'); if (this._isInAction(node)) { this._switchVerticalArrow(node.querySelector('.bottomEdge')); } }, this); this._addClass(descendants, 'slide'); this._removeClass(descendants, 'slide-up'); } // build the child nodes of specific node _buildChildNode(appendTo, nodeData, callback) { let data = nodeData.children || nodeData.siblings; appendTo.querySelector('td').setAttribute('colSpan', data.length * 2); this.buildHierarchy(appendTo, { 'children': data }, 0, callback); } // exposed method addChildren(node, data) { let that = this, opts = this.options, count = 0; this.chart.dataset.inEdit = 'addChildren'; this._buildChildNode.call(this, this._closest(node, (el) => el.nodeName === 'TABLE'), data, function () { if (++count === data.children.length) { if (!node.querySelector('.bottomEdge')) { let bottomEdge = document.createElement('i'); bottomEdge.setAttribute('class', 'edge verticalEdge bottomEdge fa'); node.appendChild(bottomEdge); } if (!node.querySelector('.symbol')) { let symbol = document.createElement('i'); symbol.setAttribute('class', 'fa ' + opts.parentNodeSymbol + ' symbol'); node.querySelector(':scope > .title').appendChild(symbol); } that.showChildren(node); that.chart.dataset.inEdit = ''; } }); } // bind click event handler for the bottom edge _clickBottomEdge(event) { event.stopPropagation(); let that = this, opts = this.options, bottomEdge = event.target, node = bottomEdge.parentNode, childrenState = this._getNodeState(node, 'children'); if (childrenState.exist) { let temp = this._closest(node, function (el) { return el.nodeName === 'TR'; }).parentNode.lastChild; if (Array.from(temp.querySelectorAll('.node')).some((node) => { return this._isVisible(node) && node.classList.contains('slide'); })) { return; } // hide the descendant nodes of the specified node if (childrenState.visible) { this.hideChildren(node); } else { // show the descendants this.showChildren(node); } } else { // load the new children nodes of the specified node by ajax request let nodeId = bottomEdge.parentNode.id; if (this._startLoading(bottomEdge, node)) { this._getJSON(typeof opts.ajaxURL.children === 'function' ? opts.ajaxURL.children(node.dataset.source) : opts.ajaxURL.children + nodeId) .then(function (resp) { if (that.chart.dataset.inAjax === 'true') { if (resp.children.length) { that.addChildren(node, resp); } } }) .catch(function (err) { console.error('Failed to get children nodes data', err); }) .finally(function () { that._endLoading(bottomEdge, node); }); } } } // subsequent processing of build sibling nodes _complementLine(oneSibling, siblingCount, existingSibligCount) { let temp = oneSibling.parentNode.parentNode.children; temp[0].children[0].setAttribute('colspan', siblingCount * 2); temp[1].children[0].setAttribute('colspan', siblingCount * 2); for (let i = 0; i < existingSibligCount; i++) { let rightLine = document.createElement('td'), leftLine = document.createElement('td'); rightLine.setAttribute('class', 'rightLine topLine'); rightLine.innerHTML = '&nbsp;'; temp[2].insertBefore(rightLine, temp[2].children[1]); leftLine.setAttribute('class', 'leftLine topLine'); leftLine.innerHTML = '&nbsp;'; temp[2].insertBefore(leftLine, temp[2].children[1]); } } // build the sibling nodes of specific node _buildSiblingNode(nodeChart, nodeData, callback) { let that = this, newSiblingCount = nodeData.siblings ? nodeData.siblings.length : nodeData.children.length, existingSibligCount = nodeChart.parentNode.nodeName === 'TD' ? this._closest(nodeChart, (el) => { return el.nodeName === 'TR'; }).children.length : 1, siblingCount = existingSibligCount + newSiblingCount, insertPostion = (siblingCount > 1) ? Math.floor(siblingCount / 2 - 1) : 0; // just build the sibling nodes for the specific node if (nodeChart.parentNode.nodeName === 'TD') { let temp = this._prevAll(nodeChart.parentNode.parentNode); temp[0].remove(); temp[1].remove(); let childCount = 0; that._buildChildNode.call(that, that._closest(nodeChart.parentNode, (el) => el.nodeName === 'TABLE'), nodeData, () => { if (++childCount === newSiblingCount) { let siblingTds = Array.from(that._closest(nodeChart.parentNode, (el) => el.nodeName === 'TABLE') .lastChild.children); if (existingSibligCount > 1) { let temp = nodeChart.parentNode.parentNode; Array.from(temp.children).forEach((el) => { siblingTds[0].parentNode.insertBefore(el, siblingTds[0]); }); temp.remove(); that._complementLine(siblingTds[0], siblingCount, existingSibligCount); that._addClass(siblingTds, 'hidden'); siblingTds.forEach((el) => { that._addClass(el.querySelectorAll('.node'), 'slide-left'); }); } else { let temp = nodeChart.parentNode.parentNode; siblingTds[insertPostion].parentNode.insertBefore(nodeChart.parentNode, siblingTds[insertPostion + 1]); temp.remove(); that._complementLine(siblingTds[insertPostion], siblingCount, 1); that._addClass(siblingTds, 'hidden'); that._addClass(that._getDescElements(siblingTds.slice(0, insertPostion + 1), '.node'), 'slide-right'); that._addClass(that._getDescElements(siblingTds.slice(insertPostion + 1), '.node'), 'slide-left'); } callback(); } }); } else { // build the sibling nodes and parent node for the specific ndoe let nodeCount = 0; that.buildHierarchy.call(that, that.chart, nodeData, 0, () => { if (++nodeCount === siblingCount) { let temp = nodeChart.nextElementSibling.children[3] .children[insertPostion], td = document.createElement('td'); td.setAttribute('colspan', 2); td.appendChild(nodeChart); temp.parentNode.insertBefore(td, temp.nextElementSibling); that._complementLine(temp, siblingCount, 1); let temp2 = that._closest(nodeChart, (el) => el.classList && el.classList.contains('nodes')) .parentNode.children[0]; temp2.classList.add('hidden'); that._addClass(Array.from(temp2.querySelectorAll('.node')), 'slide-down'); let temp3 = this._siblings(nodeChart.parentNode); that._addClass(temp3, 'hidden'); that._addClass(that._getDescElements(temp3.slice(0, insertPostion), '.node'), 'slide-right'); that._addClass(that._getDescElements(temp3.slice(insertPostion), '.node'), 'slide-left'); callback(); } }); } } addSiblings(node, data) { let that = this; this.chart.dataset.inEdit = 'addSiblings'; this._buildSiblingNode.call(this, this._closest(node, (el) => el.nodeName === 'TABLE'), data, () => { that._closest(node, (el) => el.classList && el.classList.contains('nodes')) .dataset.siblingsLoaded = true; if (!node.querySelector('.leftEdge')) { let rightEdge = document.createElement('i'), leftEdge = document.createElement('i'); rightEdge.setAttribute('class', 'edge horizontalEdge rightEdge fa'); node.appendChild(rightEdge); leftEdge.setAttribute('class', 'edge horizontalEdge leftEdge fa'); node.appendChild(leftEdge); } that.showSiblings(node); that.chart.dataset.inEdit = ''; }); } removeNodes(node) { let parent = this._closest(node, el => el.nodeName === 'TABLE').parentNode, sibs = this._siblings(parent.parentNode); if (parent.nodeName === 'TD') { if (this._getNodeState(node, 'siblings').exist) { sibs[2].querySelector('.topLine').nextElementSibling.remove(); sibs[2].querySelector('.topLine').remove(); sibs[0].children[0].setAttribute('colspan', sibs[2].children.length); sibs[1].children[0].setAttribute('colspan', sibs[2].children.length); parent.remove(); } else { sibs[0].children[0].removeAttribute('colspan'); sibs[0].querySelector('.bottomEdge').remove(); this._siblings(sibs[0]).forEach(el => el.remove()); } } else { Array.from(parent.parentNode.children).forEach(el => el.remove()); } } // bind click event handler for the left and right edges _clickHorizontalEdge(event) { event.stopPropagation(); let that = this, opts = this.options, hEdge = event.target, node = hEdge.parentNode, siblingsState = this._getNodeState(node, 'siblings'); if (siblingsState.exist) { let temp = this._closest(node, function (el) { return el.nodeName === 'TABLE'; }).parentNode, siblings = this._siblings(temp); if (siblings.some((el) => { let node = el.querySelector('.node'); return this._isVisible(node) && node.classList.contains('slide'); })) { return; } if (opts.toggleSiblingsResp) { let prevSib = this._closest(node, (el) => el.nodeName === 'TABLE').parentNode.previousElementSibling, nextSib = this._closest(node, (el) => el.nodeName === 'TABLE').parentNode.nextElementSibling; if (hEdge.classList.contains('leftEdge')) { if (prevSib.classList.contains('hidden')) { this.showSiblings(node, 'left'); } else { this.hideSiblings(node, 'left'); } } else { if (nextSib.classList.contains('hidden')) { this.showSiblings(node, 'right'); } else { this.hideSiblings(node, 'right'); } } } else { if (siblingsState.visible) { this.hideSiblings(node); } else { this.showSiblings(node); } } } else { // load the new sibling nodes of the specified node by ajax request let nodeId = hEdge.parentNode.id, url = (this._getNodeState(node, 'parent').exist) ? (typeof opts.ajaxURL.siblings === 'function' ? opts.ajaxURL.siblings(JSON.parse(node.dataset.source)) : opts.ajaxURL.siblings + nodeId) : (typeof opts.ajaxURL.families === 'function' ? opts.ajaxURL.families(JSON.parse(node.dataset.source)) : opts.ajaxURL.families + nodeId); if (this._startLoading(hEdge, node)) { this._getJSON(url) .then(function (resp) { if (that.chart.dataset.inAjax === 'true') { if (resp.siblings || resp.children) { that.addSiblings(node, resp); } } }) .catch(function (err) { console.error('Failed to get sibling nodes data', err); }) .finally(function () { that._endLoading(hEdge, node); }); } } } // event handler for toggle buttons in Hybrid(horizontal + vertical) OrgChart _clickToggleButton(event) { let that = this, toggleBtn = event.target, descWrapper = toggleBtn.parentNode.nextElementSibling, descendants = Array.from(descWrapper.querySelectorAll('.node')), children = Array.from(descWrapper.children).map(item => item.querySelector('.node')); if (children.some((item) => item.classList.contains('slide'))) { return; } toggleBtn.classList.toggle('fa-plus-square'); toggleBtn.classList.toggle('fa-minus-square'); if (descendants[0].classList.contains('slide-up')) { descWrapper.classList.remove('hidden'); this._repaint(children[0]); this._addClass(children, 'slide'); this._removeClass(children, 'slide-up'); this._one(children[0], 'transitionend', () => { that._removeClass(children, 'slide'); }); } else { this._addClass(descendants, 'slide slide-up'); this._one(descendants[0], 'transitionend', () => { that._removeClass(descendants, 'slide'); descendants.forEach(desc => { let ul = that._closest(desc, function (el) { return el.nodeName === 'UL'; }); ul.classList.add('hidden'); }); }); descendants.forEach(desc => { let subTBs = Array.from(desc.querySelectorAll('.toggleBtn')); that._removeClass(subTBs, 'fa-minus-square'); that._addClass(subTBs, 'fa-plus-square'); }); } } _dispatchClickEvent(event) { let classList = event.target.classList; if (classList.contains('topEdge')) { this._clickTopEdge(event); } else if (classList.contains('rightEdge') || classList.contains('leftEdge')) { this._clickHorizontalEdge(event); } else if (classList.contains('bottomEdge')) { this._clickBottomEdge(event); } else if (classList.contains('toggleBtn')) { this._clickToggleButton(event); } else { this._clickNode(event); } } _onDragStart(event) { let nodeDiv = event.target, opts = this.options, isFirefox = /firefox/.test(window.navigator.userAgent.toLowerCase()); if (isFirefox) { event.dataTransfer.setData('text/html', 'hack for firefox'); } // if users enable zoom or direction options if (this.chart.style.transform) { let ghostNode, nodeCover; if (!document.querySelector('.ghost-node')) { ghostNode = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); ghostNode.classList.add('ghost-node'); nodeCover = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); ghostNode.appendChild(nodeCover); this.chart.appendChild(ghostNode); } else { ghostNode = this.chart.querySelector(':scope > .ghost-node'); nodeCover = ghostNode.children[0]; } let transValues = this.chart.style.transform.split(','), scale = Math.abs(window.parseFloat((opts.direction === 't2b' || opts.direction === 'b2t') ? transValues[0].slice(transValues[0].indexOf('(') + 1) : transValues[1])); ghostNode.setAttribute('width', nodeDiv.offsetWidth); ghostNode.setAttribute('height', nodeDiv.offsetHeight); nodeCover.setAttribute('x', 5 * scale); nodeCover.setAttribute('y', 5 * scale); nodeCover.setAttribute('width', 120 * scale); nodeCover.setAttribute('height', 40 * scale); nodeCover.setAttribute('rx', 4 * scale); nodeCover.setAttribute('ry', 4 * scale); nodeCover.setAttribute('stroke-width', 1 * scale); let xOffset = event.offsetX * scale, yOffset = event.offsetY * scale; if (opts.direction === 'l2r') { xOffset = event.offsetY * scale; yOffset = event.offsetX * scale; } else if (opts.direction === 'r2l') { xOffset = nodeDiv.offsetWidth - event.offsetY * scale; yOffset = event.offsetX * scale; } else if (opts.direction === 'b2t') { xOffset = nodeDiv.offsetWidth - event.offsetX * scale; yOffset = nodeDiv.offsetHeight - event.offsetY * scale; } if (isFirefox) { // hack for old version of Firefox(< 48.0) let ghostNodeWrapper = document.createElement('img'); ghostNodeWrapper.src = 'data:image/svg+xml;utf8,' + (new XMLSerializer()).serializeToString(ghostNode); event.dataTransfer.setDragImage(ghostNodeWrapper, xOffset, yOffset); nodeCover.setAttribute('fill', 'rgb(255, 255, 255)'); nodeCover.setAttribute('stroke', 'rgb(191, 0, 0)'); } else { event.dataTransfer.setDragImage(ghostNode, xOffset, yOffset); } } let dragged = event.target, dragZone = this._closest(dragged, (el) => { return el.classList && el.classList.contains('nodes'); }).parentNode.children[0].querySelector('.node'), dragHier = Array.from(this._closest(dragged, (el) => { return el.nodeName === 'TABLE'; }).querySelectorAll('.node')); this.dragged = dragged; Array.from(this.chart.querySelectorAll('.node')).forEach(function (node) { if (!dragHier.includes(node)) { if (opts.dropCriteria) { if (opts.dropCriteria(dragged, dragZone, node)) { node.classList.add('allowedDrop'); } } else { node.classList.add('allowedDrop'); } } }); } _onDragOver(event) { event.preventDefault(); let dropZone = event.currentTarget; if (!dropZone.classList.contains('allowedDrop')) { event.dataTransfer.dropEffect = 'none'; } } _onDragEnd(event) { Array.from(this.chart.querySelectorAll('.allowedDrop')).forEach(function (el) { el.classList.remove('allowedDrop'); }); } _onDrop(event) { let dropZone = event.currentTarget, chart = this.chart, dragged = this.dragged, dragZone = this._closest(dragged, function (el) { return el.classList && el.classList.contains('nodes'); }).parentNode.children[0].children[0]; this._removeClass(Array.from(chart.querySelectorAll('.allowedDrop')), 'allowedDrop'); // firstly, deal with the hierarchy of drop zone if (!dropZone.parentNode.parentNode.nextElementSibling) { // if the drop zone is a leaf node let bottomEdge = document.createElement('i'); bottomEdge.setAttribute('class', 'edge verticalEdge bottomEdge fa'); dropZone.appendChild(bottomEdge); dropZone.parentNode.setAttribute('colspan', 2); let table = this._closest(dropZone, function (el) { return el.nodeName === 'TABLE'; }), upperTr = document.createElement('tr'), lowerTr = document.createElement('tr'), nodeTr = document.createElement('tr'); upperTr.setAttribute('class', 'lines'); upperTr.innerHTML = `<td colspan="2"><div class="downLine"></div></td>`; table.appendChild(upperTr); lowerTr.setAttribute('class', 'lines'); lowerTr.innerHTML = `<td class="rightLine">&nbsp;</td><td class="leftLine">&nbsp;</td>`; table.appendChild(lowerTr); nodeTr.setAttribute('class', 'nodes'); table.appendChild(nodeTr); Array.from(dragged.querySelectorAll('.horizontalEdge')).forEach((hEdge) => { dragged.removeChild(hEdge); }); let draggedTd = this._closest(dragged, (el) => el.nodeName === 'TABLE').parentNode; nodeTr.appendChild(draggedTd); } else { let dropColspan = window.parseInt(dropZone.parentNode.colSpan) + 2; dropZone.parentNode.setAttribute('colspan', dropColspan); dropZone.parentNode.parentNode.nextElementSibling.children[0].setAttribute('colspan', dropColspan); if (!dragged.querySelector('.horizontalEdge')) { let rightEdge = document.createElement('i'), leftEdge = document.createElement('i'); rightEdge.setAttribute('class', 'edge horizontalEdge rightEdge fa'); dragged.appendChild(rightEdge); leftEdge.setAttribute('class', 'edge horizontalEdge leftEdge fa'); dragged.appendChild(leftEdge); } let temp = dropZone.parentNode.parentNode.nextElementSibling.nextElementSibling, leftline = document.createElement('td'), rightline = document.createElement('td'); leftline.setAttribute('class', 'leftLine topLine'); leftline.innerHTML = `&nbsp;`; temp.insertBefore(leftline, temp.children[1]); rightline.setAttribute('class', 'rightLine topLine'); rightline.innerHTML = `&nbsp;`; temp.insertBefore(rightline, temp.children[2]); temp.nextElementSibling.appendChild(this._closest(dragged, function (el) { return el.nodeName === 'TABLE'; }).parentNode); let dropSibs = this._siblings(this._closest(dragged, function (el) { return el.nodeName === 'TABLE'; }).parentNode).map((el) => el.querySelector('.node')); if (dropSibs.length === 1) { let rightEdge = document.createElement('i'), leftEdge = document.createElement('i'); rightEdge.setAttribute('class', 'edge horizontalEdge rightEdge fa'); dropSibs[0].appendChild(rightEdge); leftEdge.setAttribute('class', 'edge horizontalEdge leftEdge fa'); dropSibs[0].appendChild(leftEdge); } } // secondly, deal with the hierarchy of dragged node let dragColSpan = window.parseInt(dragZone.colSpan); if (dragColSpan > 2) { dragZone.setAttribute('colspan', dragColSpan - 2); dragZone.parentNode.nextElementSibling.children[0].setAttribute('colspan', dragColSpan - 2); let temp = dragZone.parentNode.nextElementSibling.nextElementSibling; temp.children[1].remove(); temp.children[1].remove(); let dragSibs = Array.from(dragZone.parentNode.parentNode.children[3].children).map(function (td) { return td.querySelector('.node'); }); if (dragSibs.length === 1) { dragSibs[0].querySelector('.leftEdge').remove(); dragSibs[0].querySelector('.rightEdge').remove(); } } else { dragZone.removeAttribute('colspan'); dragZone.querySelector('.node').removeChild(dragZone.querySelector('.bottomEdge')); Array.from(dragZone.parentNode.parentNode.children).slice(1).forEach((tr) => tr.remove()); } let customE = new CustomEvent('nodedropped.orgchart', { 'detail': { 'draggedNode': dragged, 'dragZone': dragZone.children[0], 'dropZone': dropZone }}); chart.dispatchEvent(customE); } // create node _createNode(nodeData, level) { let that = this, opts = this.options; return new Promise(function (resolve, reject) { if (nodeData.children) { for (let child of nodeData.children) { child.parentId = nodeData.id; } } // construct the content of node let nodeDiv = document.createElement('div'); delete nodeData.children; nodeDiv.dataset.source = JSON.stringify(nodeData); if (nodeData[opts.nodeId]) { nodeDiv.id = nodeData[opts.nodeId]; } let inEdit = that.chart.dataset.inEdit, isHidden; if (inEdit) { isHidden = inEdit === 'addChildren' ? ' slide-up' : ''; } else { isHidden = level >= opts.depth ? ' slide-up' : ''; } nodeDiv.setAttribute('class', 'node ' + (nodeData.className || '') + isHidden); if (opts.draggable) { nodeDiv.setAttribute('draggable', true); } if (nodeData.parentId) { nodeDiv.setAttribute('data-parent', nod