src/orgchart.js
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 = {
'rootNodeTitle': null,
'nodeTitle': null,
'firstLine': null,
'secondLine': null,
'nodeId': 'id',
'toggleSiblingsResp': false,
'depth': 999,
'chartClass': '',
'exportButton': false,
'exportFilename': 'OrgChart',
'parentNodeSymbol': 'fa-users',
'draggable': false,
'direction': 't2b',
'pan': false,
'zoom': false,
'showAvatars': false,
'colors': null,
'borderColors': null,
'colored': false,
'onNodeSelect': null,
'onShowAllClick': null
},
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));
}
if (opts.colored) {
chart.classList.add('colored');
}
this.addZoomControls(chartContainer);
chartContainer.appendChild(chart);
}
get name() {
return this._name;
}
toggleColors() {
this.chart.classList.toggle('colored');
}
addZoomControls(appendTo) {
let zoomContainerEl = document.createElement('div');
zoomContainerEl.classList.add('zoom-container');
let zoomInEl = document.createElement('div');
zoomInEl.classList.add('zoom-in');
zoomInEl.innerHTML = '<i class="fa fa-search-plus" aria-hidden="true"></i>';
zoomInEl.addEventListener('click', () => this._setChartScale(this.chart, true));
zoomContainerEl.appendChild(zoomInEl);
let zoomOutEl = document.createElement('div');
zoomOutEl.classList.add('zoom-out');
zoomOutEl.innerHTML = '<i class="fa fa-search-minus" aria-hidden="true"></i>';
zoomOutEl.addEventListener('click', () => this._setChartScale(this.chart, false));
zoomContainerEl.appendChild(zoomOutEl);
appendTo.appendChild(zoomContainerEl);
}
_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('* > .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);
}
}
// define node click event handler
_clickNode(event) {
if (this.options.onNodeSelect) {
this.options.onNodeSelect(JSON.parse(event.currentTarget.dataset.source));
}
}
// 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"> </td><td class="leftLine"> </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-minus-square');
arrow.classList.toggle('fa-plus-square');
}
// 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('.bottomEdge').classList.remove('fa-plus-square');
node.querySelector('.bottomEdge').classList.add('fa-minus-square');
}
}, 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);
});
}
}
}
_addShowAllBottom(node) {
const showAllEl = document.createElement('div');
showAllEl.classList.add('show-all', 'bottom');
showAllEl.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
node.querySelector('.toggleBtn').click();
});
const iconGroupEl = document.createElement('i');
iconGroupEl.classList.add('fa', 'fa-group');
showAllEl.appendChild(iconGroupEl);
const iconDoubleEl = document.createElement('i');
iconDoubleEl.classList.add('fa', 'fa-angle-double-down');
showAllEl.appendChild(iconDoubleEl);
node.appendChild(showAllEl);
}
// 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) => {
let bottomEdgeEl = desc.querySelector('.bottomEdge-click');
if (bottomEdgeEl) {
bottomEdgeEl.classList.remove('fa-minus-square');
bottomEdgeEl.classList.add('fa-plus-square');
this._addShowAllBottom(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');
this._addShowAllBottom(node);
}
// 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);
// Remove bottom arrows from clicked element
Array.from(node.querySelectorAll('.show-all.bottom')).forEach(el => el.remove());
}
} 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 = ' ';
temp[2].insertBefore(rightLine, temp[2].children[1]);
leftLine.setAttribute('class', 'leftLine topLine');
leftLine.innerHTML = ' ';
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-click')) {
this._clickBottomEdge(event);
} else if (classList.contains('toggleBtn-click')) {
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"> </td><td class="leftLine"> </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 = ` `;
temp.insertBefore(leftline, temp.children[1]);
rightline.setAttribute('class', 'rightLine topLine');
rightline.innerHTML = ` `;
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 ' + (opts.colors[nodeData.color] || '') + isHidden);
if (opts.draggable) {
nodeDiv.setAttribute('draggable', true);
}
if (nodeData.parentId) {
nodeDiv.setAttribute('data-parent', nodeData.parentId);
}
if (nodeData.root) {
nodeDiv.classList.add('rootNode');
}
if (nodeData.hasSubordinates) {
const showAllEl = document.createElement('div');
showAllEl.classList.add('show-all');
if (opts.onShowAllClick) {
showAllEl.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
opts.onShowAllClick();
});
}
const iconGroupEl = document.createElement('i');
iconGroupEl.classList.add('fa', 'fa-group');
showAllEl.appendChild(iconGroupEl);
const iconDoubleEl = document.createElement('i');
iconDoubleEl.classList.add('fa', 'fa-angle-double-up');
showAllEl.appendChild(iconDoubleEl);
nodeDiv.prepend(showAllEl);
}
const titleEl = document.createElement('div');
titleEl.classList.add('title');
titleEl.innerHTML = nodeData.root ? nodeData[opts.rootNodeTitle] : nodeData[opts.nodeTitle];
nodeDiv.appendChild(titleEl);
if (!nodeData.root && opts.showAvatars) {
nodeDiv.classList.add('withAvatar');
const avatar = document.createElement('div');
avatar.classList.add('avatar');
if (nodeData.avatarUrl && nodeData.avatarUrl.length > 0) {
avatar.setAttribute('style', `background-image: url(${nodeData.avatarUrl});`);
} else {
const name = nodeData[opts.firstLine];
const firstLetter = name[0];
let secondLetter = '';
const secondLetterIndex = name.indexOf(' ');
if (secondLetterIndex > -1) {
secondLetter = name[secondLetterIndex + 1];
}
avatar.innerHTML = firstLetter + secondLetter;
}
nodeDiv.appendChild(avatar);
}
if (!nodeData.root && opts.firstLine) {
const contentEl = document.createElement('div');
contentEl.classList.add('content');
contentEl.innerHTML = nodeData[opts.firstLine];
if (opts.secondLine && nodeData[opts.secondLine] && nodeData[opts.secondLine].length > 0) {
const contentEl2 = document.createElement('div');
contentEl2.classList.add('contentSecondLine');
contentEl2.innerHTML = nodeData[opts.secondLine].split(' : ').pop();
contentEl.appendChild(contentEl2);
}
if (nodeData.border_color) {
contentEl.classList.add('with-border-legend');
const borderColor = nodeData.border_color ? `border-${opts.borderColors[nodeData.border_color]}` : '';
const borderLegendEl = document.createElement('div');
borderLegendEl.classList.add('border-legend', borderColor);
borderLegendEl.innerHTML = borderColor.split('-').pop();
contentEl.appendChild(borderLegendEl);
}
nodeDiv.appendChild(contentEl);
}
// append toggle button
let flags = nodeData.relationship || '';
if (opts.verticalDepth && (level + 2) > opts.verticalDepth || Number(flags.substr(2, 1))) {
if ((level + 1) >= opts.verticalDepth && Number(flags.substr(2, 1))) {
let toggleBtn = document.createElement('i'),
icon = level + 1 >= opts.depth ? 'plus' : 'minus';
toggleBtn.setAttribute('class', 'toggleBtn toggleBtn-click fa fa-' + icon + '-square');
nodeDiv.appendChild(toggleBtn);
} else {
if (Number(flags.substr(2, 1)) && !nodeData.root) {
let toggleBtn = document.createElement('i'),
icon = level + 1 >= opts.depth ? 'plus' : 'minus';
toggleBtn.setAttribute('class', 'toggleBtn edge bottomEdge bottomEdge-click fa fa-' + icon + '-square');
nodeDiv.appendChild(toggleBtn);
}
}
}
nodeDiv.addEventListener('click', that._dispatchClickEvent.bind(that));
if (opts.draggable) {
nodeDiv.addEventListener('dragstart', that._onDragStart.bind(that));
nodeDiv.addEventListener('dragover', that._onDragOver.bind(that));
nodeDiv.addEventListener('dragend', that._onDragEnd.bind(that));
nodeDiv.addEventListener('drop', that._onDrop.bind(that));
}
// allow user to append dom modification after finishing node create of orgchart
if (opts.createNode) {
opts.createNode(nodeDiv, nodeData);
}
resolve(nodeDiv);
});
}
buildHierarchy(appendTo, nodeData, level, callback) {
// Construct the node
let that = this,
opts = this.options,
nodeWrapper,
childNodes = nodeData.children,
isVerticalNode = opts.verticalDepth && (level + 1) >= opts.verticalDepth;
if (Object.keys(nodeData).length > 1) { // if nodeData has nested structure
nodeWrapper = isVerticalNode ? appendTo : document.createElement('table');
if (!isVerticalNode) {
appendTo.appendChild(nodeWrapper);
}
this._createNode(nodeData, level)
.then(function (nodeDiv) {
if (isVerticalNode) {
nodeWrapper.insertBefore(nodeDiv, nodeWrapper.firstChild);
} else {
let tr = document.createElement('tr');
tr.innerHTML = `
<td ${childNodes ? `colspan="${childNodes.length * 2}"` : ''}>
</td>
`;
tr.children[0].appendChild(nodeDiv);
nodeWrapper.insertBefore(tr, nodeWrapper.children[0] ? nodeWrapper.children[0] : null);
}
if (callback) {
callback();
}
})
.catch(function (err) {
console.error('Failed to creat node', err);
});
}
// Construct the inferior nodes and connectiong lines
if (childNodes) {
if (Object.keys(nodeData).length === 1) { // if nodeData is just an array
nodeWrapper = appendTo;
}
let isHidden,
isVerticalLayer = opts.verticalDepth && (level + 2) >= opts.verticalDepth,
inEdit = that.chart.dataset.inEdit;
if (inEdit) {
isHidden = inEdit === 'addSiblings' ? '' : ' hidden';
} else {
isHidden = level + 1 >= opts.depth ? ' hidden' : '';
}
// draw the line close to parent node
if (!isVerticalLayer) {
let tr = document.createElement('tr');
tr.setAttribute('class', 'lines' + isHidden);
tr.innerHTML = `
<td colspan="${ childNodes.length * 2 }">
<div class="downLine"></div>
</td>
`;
nodeWrapper.appendChild(tr);
}
// draw the lines close to children nodes
let lineLayer = document.createElement('tr');
lineLayer.setAttribute('class', 'lines' + isHidden);
lineLayer.innerHTML = `
<td class="rightLine"> </td>
${childNodes.slice(1).map(() => `
<td class="leftLine topLine"> </td>
<td class="rightLine topLine"> </td>
`).join('')}
<td class="leftLine"> </td>
`;
let nodeLayer;
if (isVerticalLayer) {
nodeLayer = document.createElement('ul');
if (isHidden) {
nodeLayer.classList.add(isHidden.trim());
}
if (level + 2 === opts.verticalDepth) {
let tr = document.createElement('tr');
tr.setAttribute('class', 'verticalNodes' + isHidden);
tr.innerHTML = `<td></td>`;
tr.firstChild.appendChild(nodeLayer);
nodeWrapper.appendChild(tr);
} else {
nodeWrapper.appendChild(nodeLayer);
}
} else {
nodeLayer = document.createElement('tr');
nodeLayer.setAttribute('class', 'nodes' + isHidden);
nodeWrapper.appendChild(lineLayer);
nodeWrapper.appendChild(nodeLayer);
}
// recurse through children nodes
childNodes.forEach((child) => {
let nodeCell;
if (isVerticalLayer) {
nodeCell = document.createElement('li');
} else {
nodeCell = document.createElement('td');
nodeCell.setAttribute('colspan', 2);
}
nodeLayer.appendChild(nodeCell);
that.buildHierarchy(nodeCell, child, level + 1, callback);
});
}
}
_clickChart(event) {
let closestNode = this._closest(event.target, function (el) {
return el.classList && el.classList.contains('node');
});
if (!closestNode && this.chart.querySelector('.node.focused')) {
this.chart.querySelector('.node.focused').classList.remove('focused');
}
}
_clickExportButton() {
let opts = this.options,
chartContainer = this.chartContainer,
mask = chartContainer.querySelector(':scope > .mask'),
sourceChart = chartContainer.querySelector('.orgchart:not(.hidden)'),
flag = opts.direction === 'l2r' || opts.direction === 'r2l';
if (!mask) {
mask = document.createElement('div');
mask.setAttribute('class', 'mask');
mask.innerHTML = `<i class="fa fa-circle-o-notch fa-spin spinner"></i>`;
chartContainer.appendChild(mask);
} else {
mask.classList.remove('hidden');
}
chartContainer.classList.add('canvasContainer');
window.html2canvas(sourceChart, {
'width': flag ? sourceChart.clientHeight : sourceChart.clientWidth,
'height': flag ? sourceChart.clientWidth : sourceChart.clientHeight,
'onclone': function (cloneDoc) {
let canvasContainer = cloneDoc.querySelector('.canvasContainer');
canvasContainer.style.overflow = 'visible';
canvasContainer.querySelector('.orgchart:not(.hidden)').transform = '';
}
})
.then((canvas) => {
let downloadBtn = chartContainer.querySelector('.oc-download-btn');
chartContainer.querySelector('.mask').classList.add('hidden');
downloadBtn.setAttribute('href', canvas.toDataURL());
downloadBtn.click();
})
.catch((err) => {
console.error('Failed to export the curent orgchart!', err);
})
.finally(() => {
chartContainer.classList.remove('canvasContainer');
});
}
_loopChart(chart) {
let subObj = { 'id': chart.querySelector('.node').id };
if (chart.children[3]) {
Array.from(chart.children[3].children).forEach((el) => {
if (!subObj.children) { subObj.children = []; }
subObj.children.push(this._loopChart(el.firstChild));
});
}
return subObj;
}
getHierarchy() {
if (!this.chart.querySelector('.node').id) {
return 'Error: Nodes of orghcart to be exported must have id attribute!';
}
return this._loopChart(this.chart.querySelector('table'));
}
_onPanStart(event) {
let chart = event.currentTarget;
if (this._closest(event.target, (el) => el.classList && el.classList.contains('node')) ||
(event.touches && event.touches.length > 1)) {
chart.dataset.panning = false;
return;
}
chart.style.cursor = 'move';
chart.dataset.panning = true;
let lastX = 0,
lastY = 0,
lastTf = window.getComputedStyle(chart).transform;
if (lastTf !== 'none') {
let temp = lastTf.split(',');
if (!lastTf.includes('3d')) {
lastX = Number.parseInt(temp[4], 10);
lastY = Number.parseInt(temp[5], 10);
} else {
lastX = Number.parseInt(temp[12], 10);
lastY = Number.parseInt(temp[13], 10);
}
}
let startX = 0,
startY = 0;
if (!event.targetTouches) { // pan on desktop
startX = event.pageX - lastX;
startY = event.pageY - lastY;
} else if (event.targetTouches.length === 1) { // pan on mobile device
startX = event.targetTouches[0].pageX - lastX;
startY = event.targetTouches[0].pageY - lastY;
} else if (event.targetTouches.length > 1) {
return;
}
chart.dataset.panStart = JSON.stringify({ 'startX': startX, 'startY': startY });
chart.addEventListener('mousemove', this._onPanning.bind(this));
chart.addEventListener('touchmove', this._onPanning.bind(this));
}
_onPanning(event) {
let chart = event.currentTarget;
if (chart.dataset.panning === 'false') {
return;
}
let newX = 0,
newY = 0,
panStart = JSON.parse(chart.dataset.panStart),
startX = panStart.startX,
startY = panStart.startY;
if (!event.targetTouches) { // pand on desktop
newX = event.pageX - startX;
newY = event.pageY - startY;
} else if (event.targetTouches.length === 1) { // pan on mobile device
newX = event.targetTouches[0].pageX - startX;
newY = event.targetTouches[0].pageY - startY;
} else if (event.targetTouches.length > 1) {
return;
}
let lastTf = window.getComputedStyle(chart).transform;
if (lastTf === 'none') {
if (!lastTf.includes('3d')) {
chart.style.transform = 'matrix(1, 0, 0, 1, ' + newX + ', ' + newY + ')';
} else {
chart.style.transform = 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, ' + newX + ', ' + newY + ', 0, 1)';
}
} else {
let matrix = lastTf.split(',');
if (!lastTf.includes('3d')) {
matrix[4] = newX;
matrix[5] = newY + ')';
} else {
matrix[12] = newX;
matrix[13] = newY;
}
chart.style.transform = matrix.join(',');
}
}
_onPanEnd(event) {
let chart = this.chart;
if (chart.dataset.panning === 'true') {
chart.dataset.panning = false;
chart.style.cursor = 'default';
document.body.removeEventListener('mousemove', this._onPanning);
document.body.removeEventListener('touchmove', this._onPanning);
}
}
_setChartScale(chart, zoomIn) {
const scales = [0.25, 0.33, 0.5, 0.67, 0.75, 0.8, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2, 2.5, 3];
let scale = scales[7];
if (chart.style.transform.length === 0) {
scale = zoomIn ? scales[8] : scales[6];
const newOffsetX = (-1) * (this.chart.clientWidth - this.chart.clientWidth * scale) / 2;
const newOffsetY = (-1) * (this.chart.clientHeight - this.chart.clientHeight * scale) / 2;
chart.style.transform = `matrix(${scale}, 0, 0, ${scale}, ${newOffsetX}, ${newOffsetY})`;
} else {
const matrix = JSON.parse(chart.style.transform.replace(/^\w+\(/, "[").replace(/\)$/, "]"));
const scaleIndex = scales.indexOf(matrix[0]);
scale = zoomIn ? scales[scaleIndex + 1] : scales[scaleIndex - 1];
const ofX = parseFloat(matrix[4] + (this.chart.clientWidth - this.chart.clientWidth * scales[scaleIndex]) / 2);
const ofY = parseFloat(matrix[5] + (this.chart.clientHeight - this.chart.clientHeight * scales[scaleIndex]) / 2);
const newOffsetX = parseFloat((this.chart.clientWidth - this.chart.clientWidth * scale) / 2);
const newOffsetY = parseFloat((this.chart.clientHeight - this.chart.clientHeight * scale) / 2);
let offsetX = ofX - newOffsetX;
let offsetY = ofY - newOffsetY;
chart.style.transform = `matrix(${scale}, ${matrix[1]}, ${matrix[2]}, ${scale}, ${offsetX}, ${offsetY})`;
}
}
_onWheeling(event) {
event.preventDefault();
let newScale = event.deltaY > 0 ? 0.8 : 1.2;
this._setChartScale(this.chart, newScale);
}
_getPinchDist(event) {
return Math.sqrt((event.touches[0].clientX - event.touches[1].clientX) *
(event.touches[0].clientX - event.touches[1].clientX) +
(event.touches[0].clientY - event.touches[1].clientY) *
(event.touches[0].clientY - event.touches[1].clientY));
}
_onTouchStart(event) {
let chart = this.chart;
if (event.touches && event.touches.length === 2) {
let dist = this._getPinchDist(event);
chart.dataset.pinching = true;
chart.dataset.pinchDistStart = dist;
}
}
_onTouchMove(event) {
let chart = this.chart;
if (chart.dataset.pinching) {
let dist = this._getPinchDist(event);
chart.dataset.pinchDistEnd = dist;
}
}
_onTouchEnd(event) {
let chart = this.chart;
if (chart.dataset.pinching) {
chart.dataset.pinching = false;
let diff = chart.dataset.pinchDistEnd - chart.dataset.pinchDistStart;
if (diff > 0) {
this._setChartScale(chart, 1);
} else if (diff < 0) {
this._setChartScale(chart, -1);
}
}
}
}