orgchart
Version:
Simple and direct organization chart(tree-like hierarchy) plugin based on pure DOM and jQuery.
1,209 lines (1,191 loc) • 73 kB
JavaScript
/*
* jQuery OrgChart Plugin
* https://github.com/dabeng/OrgChart
*
* Copyright 2016, dabeng
* https://github.com/dabeng
*
* Licensed under the MIT license:
* http://www.opensource.org/licenses/MIT
*/
'use strict';
(function (factory) {
if (typeof module === 'object' && typeof module.exports === 'object') {
factory(require('jquery'), window, document);
} else {
factory(jQuery, window, document);
}
}(function ($, window, document, undefined) {
var OrgChart = function (elem, opts) {
this.$chartContainer = $(elem);
this.opts = opts;
this.defaultOptions = {
'icons': {
'theme': 'oci',
'parentNode': 'oci-menu',
'expandToUp': 'oci-chevron-up',
'collapseToDown': 'oci-chevron-down',
'collapseToLeft': 'oci-chevron-left',
'expandToRight': 'oci-chevron-right',
'backToCompact': 'oci-corner-top-left',
'backToLoose': 'oci-corner-bottom-right',
'collapsed': 'oci-plus-square',
'expanded': 'oci-minus-square',
'spinner': 'oci-spinner'
},
'nodeTitle': 'name',
'nodeId': 'id',
'toggleSiblingsResp': false,
'visibleLevel': 999,
'chartClass': '',
'exportButton': false,
'exportButtonName': 'Export',
'exportFilename': 'OrgChart',
'exportFileextension': 'png',
'draggable': false,
'direction': 't2b',
'pan': false,
'zoom': false,
'zoominLimit': 7,
'zoomoutLimit': 0.5
};
};
//
OrgChart.prototype = {
//
init: function (opts) {
var that = this;
this.options = $.extend({}, this.defaultOptions, this.opts, opts);
// build the org-chart
var $chartContainer = this.$chartContainer;
if (this.$chart) {
this.$chart.remove();
}
var data = this.options.data;
var $chart = this.$chart = $('<div>', {
'data': { 'options': this.options },
'class': 'orgchart' + (this.options.chartClass !== '' ? ' ' + this.options.chartClass : '') + (this.options.direction !== 't2b' ? ' ' + this.options.direction : ''),
'click': function(event) {
if (!$(event.target).closest('.node').length) {
$chart.find('.node.focused').removeClass('focused');
}
}
});
if (typeof MutationObserver !== 'undefined') {
this.triggerInitEvent();
}
var $root = Array.isArray(data) ? $chart.append($('<ul class="nodes"></ul>')).find('.nodes')
: $chart.append($('<ul class="nodes"><li class="hierarchy"></li></ul>')).find('.hierarchy');
if (data instanceof $) { // ul datasource
this.buildHierarchy($root, this.buildJsonDS(data.children()), 0, this.options);
} else { // local json datasource
if (data.relationship) {
this.buildHierarchy($root, data);
} else {
this.buildHierarchy($root, Array.isArray(data) ? data : this.attachRel(data, '00'));
}
}
$chartContainer.append($chart);
// append the export button
if (this.options.exportButton && !$('.oc-export-btn').length) {
this.attachExportButton();
}
if (this.options.pan) {
this.bindPan();
}
if (this.options.zoom) {
this.bindZoom();
}
return this;
},
handleCompactNodes: function () {
// caculate the compact nodes' level which is used to add different styles
this.$chart.find('.node.compact')
.each((index, node) => {
$(node).addClass($(node).parents('.compact').length % 2 === 0 ? 'even' : 'odd');
}); // the following code snippets is used to add direction arrows for the most top compact node, however the styles is not adjusted correctly
// .filter((index, node) => !$(node).parent().is('.compact'))
// .each((index, node) => {
// $(node).append(`<i class="edge verticalEdge topEdge ${this.options.icons.theme}"></i>`);
// if (this.getSiblings($(node)).length) {
// $(node).append(`<i class="edge horizontalEdge rightEdge ${this.options.icons.theme}"></i><i class="edge horizontalEdge leftEdge ${this.options.icons.theme}"></i>`);
// }
// });
},
//
triggerInitEvent: function () {
var that = this;
var mo = new MutationObserver(function (mutations) {
mo.disconnect();
initTime:
for (var i = 0; i < mutations.length; i++) {
for (var j = 0; j < mutations[i].addedNodes.length; j++) {
if (mutations[i].addedNodes[j].classList.contains('orgchart')) {
that.handleCompactNodes();
if (that.options.initCompleted && typeof that.options.initCompleted === 'function') {
that.options.initCompleted(that.$chart);
}
var initEvent = $.Event('init.orgchart');
that.$chart.trigger(initEvent);
break initTime;
}
}
}
});
mo.observe(this.$chartContainer[0], { childList: true });
},
triggerShowEvent: function ($target, rel) {
var initEvent = $.Event('show-' + rel + '.orgchart');
$target.trigger(initEvent);
},
triggerHideEvent: function ($target, rel) {
var initEvent = $.Event('hide-' + rel + '.orgchart');
$target.trigger(initEvent);
},
// add export button for orgchart
attachExportButton: function () {
var that = this;
var $exportBtn = $('<button>', {
'class': 'oc-export-btn',
'text': this.options.exportButtonName,
'click': function(e) {
e.preventDefault();
that.export();
}
});
this.$chartContainer.after($exportBtn);
},
setOptions: function (opts, val) {
if (typeof opts === 'string') {
if (opts === 'pan') {
if (val) {
this.bindPan();
} else {
this.unbindPan();
}
}
if (opts === 'zoom') {
if (val) {
this.bindZoom();
} else {
this.unbindZoom();
}
}
}
if (typeof opts === 'object') {
if (opts.data) {
this.init(opts);
} else {
if (typeof opts.pan !== 'undefined') {
if (opts.pan) {
this.bindPan();
} else {
this.unbindPan();
}
}
if (typeof opts.zoom !== 'undefined') {
if (opts.zoom) {
this.bindZoom();
} else {
this.unbindZoom();
}
}
}
}
return this;
},
//
panStartHandler: function (e) {
var $chart = $(e.delegateTarget);
if ($(e.target).closest('.node').length || (e.touches && e.touches.length > 1)) {
$chart.data('panning', false);
return;
} else {
$chart.css('cursor', 'move').data('panning', true);
}
var lastX = 0;
var lastY = 0;
var lastTf = $chart.css('transform');
if (lastTf !== 'none') {
var temp = lastTf.split(',');
if (lastTf.indexOf('3d') === -1) {
lastX = parseInt(temp[4]);
lastY = parseInt(temp[5]);
} else {
lastX = parseInt(temp[12]);
lastY = parseInt(temp[13]);
}
}
var startX = 0;
var startY = 0;
if (!e.targetTouches) { // pand on desktop
startX = e.pageX - lastX;
startY = e.pageY - lastY;
} else if (e.targetTouches.length === 1) { // pan on mobile device
startX = e.targetTouches[0].pageX - lastX;
startY = e.targetTouches[0].pageY - lastY;
} else if (e.targetTouches.length > 1) {
return;
}
$chart.on('mousemove touchmove',function(e) {
if (!$chart.data('panning')) {
return;
}
var newX = 0;
var newY = 0;
if (!e.targetTouches) { // pand on desktop
newX = e.pageX - startX;
newY = e.pageY - startY;
} else if (e.targetTouches.length === 1) { // pan on mobile device
newX = e.targetTouches[0].pageX - startX;
newY = e.targetTouches[0].pageY - startY;
} else if (e.targetTouches.length > 1) {
return;
}
var lastTf = $chart.css('transform');
if (lastTf === 'none') {
if (lastTf.indexOf('3d') === -1) {
$chart.css('transform', 'matrix(1, 0, 0, 1, ' + newX + ', ' + newY + ')');
} else {
$chart.css('transform', 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, ' + newX + ', ' + newY + ', 0, 1)');
}
} else {
var matrix = lastTf.split(',');
if (lastTf.indexOf('3d') === -1) {
matrix[4] = ' ' + newX;
matrix[5] = ' ' + newY + ')';
} else {
matrix[12] = ' ' + newX;
matrix[13] = ' ' + newY;
}
$chart.css('transform', matrix.join(','));
}
});
},
//
panEndHandler: function (e) {
if (e.data.chart.data('panning')) {
e.data.chart.data('panning', false).css('cursor', 'default').off('mousemove');
}
},
//
bindPan: function () {
this.$chartContainer.css('overflow', 'hidden');
this.$chart.on('mousedown touchstart', this.panStartHandler);
$(document).on('mouseup touchend', { 'chart': this.$chart }, this.panEndHandler);
},
//
unbindPan: function () {
this.$chartContainer.css('overflow', 'auto');
this.$chart.off('mousedown touchstart', this.panStartHandler);
$(document).off('mouseup touchend', this.panEndHandler);
},
//
zoomWheelHandler: function (e) {
var oc = e.data.oc;
e.preventDefault();
var newScale = 1 + (e.originalEvent.deltaY > 0 ? -0.2 : 0.2);
oc.setChartScale(oc.$chart, newScale);
},
//
zoomStartHandler: function (e) {
if(e.touches && e.touches.length === 2) {
var oc = e.data.oc;
oc.$chart.data('pinching', true);
var dist = oc.getPinchDist(e);
oc.$chart.data('pinchDistStart', dist);
}
},
zoomingHandler: function (e) {
var oc = e.data.oc;
if(oc.$chart.data('pinching')) {
var dist = oc.getPinchDist(e);
oc.$chart.data('pinchDistEnd', dist);
}
},
zoomEndHandler: function (e) {
var oc = e.data.oc;
if(oc.$chart.data('pinching')) {
oc.$chart.data('pinching', false);
var diff = oc.$chart.data('pinchDistEnd') - oc.$chart.data('pinchDistStart');
if (diff > 0) {
oc.setChartScale(oc.$chart, 1.2);
} else if (diff < 0) {
oc.setChartScale(oc.$chart, 0.8);
}
}
},
//
bindZoom: function () {
this.$chartContainer.on('wheel', { 'oc': this }, this.zoomWheelHandler);
this.$chartContainer.on('touchstart', { 'oc': this }, this.zoomStartHandler);
$(document).on('touchmove', { 'oc': this }, this.zoomingHandler);
$(document).on('touchend', { 'oc': this }, this.zoomEndHandler);
},
unbindZoom: function () {
this.$chartContainer.off('wheel', this.zoomWheelHandler);
this.$chartContainer.off('touchstart', this.zoomStartHandler);
$(document).off('touchmove', this.zoomingHandler);
$(document).off('touchend', this.zoomEndHandler);
},
//
getPinchDist: function (e) {
return Math.sqrt((e.touches[0].clientX - e.touches[1].clientX) * (e.touches[0].clientX - e.touches[1].clientX) +
(e.touches[0].clientY - e.touches[1].clientY) * (e.touches[0].clientY - e.touches[1].clientY));
},
//
setChartScale: function ($chart, newScale) {
var opts = $chart.data('options');
var lastTf = $chart.css('transform');
var matrix = '';
var targetScale = 1;
if (lastTf === 'none') {
$chart.css('transform', 'scale(' + newScale + ',' + newScale + ')');
} else {
matrix = lastTf.split(',');
if (lastTf.indexOf('3d') === -1) {
targetScale = Math.abs(window.parseFloat(matrix[3]) * newScale);
if (targetScale > opts.zoomoutLimit && targetScale < opts.zoominLimit) {
$chart.css('transform', lastTf + ' scale(' + newScale + ',' + newScale + ')');
}
} else {
targetScale = Math.abs(window.parseFloat(matrix[1]) * newScale);
if (targetScale > opts.zoomoutLimit && targetScale < opts.zoominLimit) {
$chart.css('transform', lastTf + ' scale3d(' + newScale + ',' + newScale + ', 1)');
}
}
}
},
//
buildJsonDS: function ($li) {
var that = this;
var subObj = {
'name': $li.contents().eq(0).text().trim(),
'relationship': ($li.parent().parent().is('li') ? '1': '0') + ($li.siblings('li').length ? 1: 0) + ($li.children('ul').length ? 1 : 0)
};
$.each($li.data(), function(key, value) {
subObj[key] = value;
});
$li.children('ul').children().each(function() {
if (!subObj.children) { subObj.children = []; }
subObj.children.push(that.buildJsonDS($(this)));
});
return subObj;
},
// process datasource and add necessary information
attachRel: function (data, flags) {
var that = this;
data.relationship = flags + (data.children && data.children.length > 0 ? 1 : 0);
if (this.options?.compact?.constructor === Function && this.options.compact(data)) {
data.compact = true;
}
if (data.children) {
data.children.forEach(function(item) {
if (data.hybrid || data.vertical) { // identify all the descendant nodes except the root node of hybrid structure
item.vertical = true;
} else if (data.compact && item.children) { // identify all the compact ancestor nodes
item.compact = true;
} else if (data.compact && !item.children) { // identify all the compact descendant nodes
item.associatedCompact = true;
}
that.attachRel(item, '1' + (data.children.length > 1 ? 1 : 0));
});
}
return data;
},
//
loopChart: function ($chart, includeNodeData) {
includeNodeData = (includeNodeData !== null && includeNodeData !== undefined) ? includeNodeData : false;
var that = this;
var $node = $chart.find('.node:first');
var subObj = { 'id': $node[0].id };
if (includeNodeData) {
$.each($node.data('nodeData'), function (key, value) {
subObj[key] = value;
});
}
$node.siblings('.nodes').children().each(function() {
if (!subObj.children) { subObj.children = []; }
subObj.children.push(that.loopChart($(this), includeNodeData));
});
return subObj;
},
//
getHierarchy: function (includeNodeData) {
includeNodeData = (includeNodeData !== null && includeNodeData !== undefined) ? includeNodeData : false;
if (typeof this.$chart === 'undefined') {
return 'Error: orgchart does not exist'
} else {
if (!this.$chart.find('.node').length) {
return 'Error: nodes do not exist'
} else {
var valid = true;
this.$chart.find('.node').each(function () {
if (!this.id) {
valid = false;
return false;
}
});
if (!valid) {
return 'Error: All nodes of orghcart to be exported must have data-id attribute!';
}
}
}
return this.loopChart(this.$chart, includeNodeData);
},
// detect the exist/display state of related node
getNodeState: function ($node, relation) {
var $target = {};
var isVerticalNode = !!$node.closest('vertical').length;
var relation = relation || 'self';
if (relation === 'parent') {
if (isVerticalNode) {
$target = $node.closest('ul').parents('ul');
if (!$target.length) {
$target = $node.closest('.nodes');
if (!$target.length) {
$target = $node.closest('.vertical').siblings(':first');
}
}
} else {
$target = $node.closest('.nodes').siblings('.node');
}
if ($target.length) {
if ($target.is('.hidden') || (!$target.is('.hidden') && $target.closest('.nodes').is('.hidden')) || (!$target.is('.hidden') && $target.closest('.vertical').is('.hidden'))) {
return { 'exist': true, 'visible': false };
}
return { 'exist': true, 'visible': true };
}
} else if (relation === 'children') {
$target = isVerticalNode ? $node.parent().children('ul') : $node.siblings('.nodes');
if ($target.length) {
if (!$target.is('.hidden')) {
return { 'exist': true, 'visible': true };
}
return { 'exist': true, 'visible': false };
}
} else if (relation === 'siblings') {
$target = isVerticalNode ? $node.closest('ul') : $node.parent().siblings();
if ($target.length && (!isVerticalNode || $target.children('li').length > 1)) {
if (!$target.is('.hidden') && !$target.parent().is('.hidden') && (!isVerticalNode || !$target.closest('.vertical').is('.hidden'))) {
return { 'exist': true, 'visible': true };
}
return { 'exist': true, 'visible': false };
}
} else {
$target = $node;
if ($target.length) {
if (!(($target.closest('.nodes').length && $target.closest('.nodes').is('.hidden')) ||
($target.closest('.hierarchy').length && $target.closest('.hierarchy').is('.hidden')) ||
($target.closest('.vertical').length && ($target.closest('.nodes').is('.hidden') || $target.closest('.vertical').is('.hidden')))
)) {
return { 'exist': true, 'visible': true };
}
return { 'exist': true, 'visible': false };
}
}
return { 'exist': false, 'visible': false };
},
getParent: function ($node) {
return this.getRelatedNodes($node, 'parent');
},
getChildren: function ($node) {
return this.getRelatedNodes($node, 'children');
},
getSiblings: function ($node) {
return this.getRelatedNodes($node, 'siblings');
},
// find the related nodes
getRelatedNodes: function ($node, relation) {
if (!$node || !($node instanceof $) || !$node.is('.node')) {
return $();
}
if (relation === 'parent') {
return $node.closest('.nodes').siblings('.node');
} else if (relation === 'children') {
return $node.siblings('.nodes').children('.hierarchy').find('.node:first');
} else if (relation === 'siblings') {
return $node.closest('.hierarchy').siblings().find('.node:first');
} else {
return $();
}
},
hideParentEnd: function (event) {
$(event.target).removeClass('sliding');
event.data.parent.addClass('hidden');
},
// recursively hide the ancestor node and sibling nodes of the specified node
hideParent: function ($node) {
var $parent = $node.closest('.nodes').siblings('.node');
if ($parent.find('.spinner').length) {
$node.closest('.orgchart').data('inAjax', false);
}
// hide the sibling nodes
if (this.getNodeState($node, 'siblings').visible) {
this.hideSiblings($node);
}
// hide the lines
$node.parent().addClass('isAncestorsCollapsed');
// hide the superior nodes with transition
if (this.getNodeState($parent).visible) {
$parent.addClass('sliding slide-down').one('transitionend', { 'parent': $parent }, this.hideParentEnd);
}
// if the current node has the parent node, hide it recursively
if (this.getNodeState($parent, 'parent').visible) {
this.hideParent($parent);
}
},
showParentEnd: function (event) {
var $node = event.data.node;
$(event.target).removeClass('sliding');
if (this.isInAction($node)) {
this.switchVerticalArrow($node.children('.topEdge'));
}
},
// show the parent node of the specified node
showParent: function ($node) {
// just show only one superior level
var $parent = $node.closest('.nodes').siblings('.node').removeClass('hidden');
// just show only one line
$node.closest('.hierarchy').removeClass('isAncestorsCollapsed');
// show parent node with animation
this.repaint($parent[0]);
$parent.addClass('sliding').removeClass('slide-down').one('transitionend', { 'node': $node }, this.showParentEnd.bind(this));
},
stopAjax: function ($nodeLevel) {
if ($nodeLevel.find('.spinner').length) {
$nodeLevel.closest('.orgchart').data('inAjax', false);
}
},
isVisibleNode: function (index, elem) {
return this.getNodeState($(elem)).visible;
},
isCompactDescendant: function (index, elem) {
return $(elem).parent().is('.node.compact');
},
// do some necessary cleanup tasks when hide animation is finished
hideChildrenEnd: function (event) {
var $node = event.data.node;
event.data.animatedNodes.removeClass('sliding');
event.data.animatedNodes.closest('.nodes').addClass('hidden');
if (this.isInAction($node)) {
this.switchVerticalArrow($node.children('.bottomEdge'));
}
},
// recursively hide the descendant nodes of the specified node
hideChildren: function ($node) {
$node.closest('.hierarchy').addClass('isChildrenCollapsed');
var $lowerLevel = $node.siblings('.nodes');
this.stopAjax($lowerLevel);
var $animatedNodes = $lowerLevel.find('.node').filter(this.isVisibleNode.bind(this)).not(this.isCompactDescendant.bind(this));
var isVerticalDesc = $lowerLevel.is('.vertical');
if (!isVerticalDesc) {
$animatedNodes.closest('.hierarchy').addClass('isCollapsedDescendant');
}
if ($lowerLevel.is('.vertical') || $lowerLevel.find('.vertical').length) {
$animatedNodes.find(this.options.icons.expanded).removeClass(this.options.icons.expanded).addClass(this.options.icons.collapsed);
}
this.repaint($animatedNodes.get(0));
$animatedNodes.addClass('sliding slide-up').eq(0).one('transitionend', { 'animatedNodes': $animatedNodes, 'lowerLevel': $lowerLevel, 'node': $node }, this.hideChildrenEnd.bind(this));
},
//
showChildrenEnd: function (event) {
var $node = event.data.node;
event.data.animatedNodes.removeClass('sliding');
if (this.isInAction($node)) {
this.switchVerticalArrow($node.children('.bottomEdge'));
}
},
// show the children nodes of the specified node
showChildren: function ($node) {
var that = this;
$node.closest('.hierarchy').removeClass('isChildrenCollapsed');
var $levels = $node.siblings('.nodes');
var isVerticalDesc = $levels.is('.vertical');
var $animatedNodes = isVerticalDesc
? $levels.removeClass('hidden').find('.node').filter(this.isVisibleNode.bind(this))
: $levels.removeClass('hidden').children('.hierarchy').find('.node:first').filter(this.isVisibleNode.bind(this));
if (!isVerticalDesc) {
$animatedNodes.filter(':not(:only-child)').closest('.hierarchy').addClass('isChildrenCollapsed');
$animatedNodes.closest('.hierarchy').removeClass('isCollapsedDescendant');
}
// the two following statements are used to enforce browser to repaint
this.repaint($animatedNodes.get(0));
$animatedNodes.addClass('sliding').removeClass('slide-up').eq(0).one('transitionend', { 'node': $node, 'animatedNodes': $animatedNodes }, this.showChildrenEnd.bind(this));
},
//
hideSiblingsEnd: function (event) {
var that = this;
var $node = event.data.node;
var $nodeContainer = event.data.nodeContainer;
var direction = event.data.direction;
var $siblings = direction ? (direction === 'left' ? $nodeContainer.prevAll(':not(.hidden)') : $nodeContainer.nextAll(':not(.hidden)')) : $nodeContainer.siblings();
event.data.animatedNodes.removeClass('sliding');
$siblings.find('.node:gt(0)').filter(this.isVisibleNode.bind(this))
.removeClass('slide-left slide-right')
.addClass(function() {
if (that.options.compact) {
return '';
} else {
return 'slide-up';
}
});
$siblings.find('.nodes, .vertical').addClass('hidden')
.end().addClass('hidden');
if (this.isInAction($node)) {
this.switchHorizontalArrow($node);
}
},
// hide the sibling nodes of the specified node
hideSiblings: function ($node, direction) {
var that = this;
var $nodeContainer = $node.closest('.hierarchy').addClass('isSiblingsCollapsed');
if ($nodeContainer.siblings().find('.spinner').length) {
$node.closest('.orgchart').data('inAjax', false);
}
if (direction) {
if (direction === 'left') {
$nodeContainer.addClass('left-sibs')
.prevAll('.isSiblingsCollapsed').removeClass('isSiblingsCollapsed left-sibs').end()
.prevAll().addClass('isCollapsedSibling isChildrenCollapsed')
.find('.node').filter(this.isVisibleNode.bind(this)).addClass('sliding slide-right');
} else {
$nodeContainer.addClass('right-sibs')
.nextAll('.isSiblingsCollapsed').removeClass('isSiblingsCollapsed right-sibs').end()
.nextAll().addClass('isCollapsedSibling isChildrenCollapsed')
.find('.node').filter(this.isVisibleNode.bind(this)).addClass('sliding slide-left');
}
} else {
$nodeContainer.prevAll().find('.node').filter(this.isVisibleNode.bind(this)).addClass('sliding slide-right');
$nodeContainer.nextAll().find('.node').filter(this.isVisibleNode.bind(this)).addClass('sliding slide-left');
$nodeContainer.siblings().addClass('isCollapsedSibling isChildrenCollapsed');
}
var $animatedNodes = $nodeContainer.siblings().find('.sliding');
$animatedNodes.eq(0).one('transitionend', { 'node': $node, 'nodeContainer': $nodeContainer, 'direction': direction, 'animatedNodes': $animatedNodes }, this.hideSiblingsEnd.bind(this));
},
//
showSiblingsEnd: function (event) {
var $node = event.data.node;
event.data.visibleNodes.removeClass('sliding');
if (this.isInAction($node)) {
this.switchHorizontalArrow($node);
$node.children('.topEdge').removeClass(this.options.icons.expandToUp).addClass(this.options.icons.collapseToDown);
}
},
//
showRelatedParentEnd: function(event) {
$(event.target).removeClass('sliding');
},
// show the sibling nodes of the specified node
showSiblings: function ($node, direction) {
var that = this;
// firstly, show the sibling nodes
var $siblings = $();
var $nodeContainer = $node.closest('.hierarchy');
if (direction) {
if (direction === 'left') {
$siblings = $nodeContainer.prevAll().removeClass('hidden');
} else {
$siblings = $nodeContainer.nextAll().removeClass('hidden');
}
} else {
$siblings = $node.closest('.hierarchy').siblings().removeClass('hidden');
}
// secondly, show the lines
var $upperLevel = $node.closest('.nodes').siblings('.node');
if (direction) {
$nodeContainer.removeClass(direction + '-sibs');
if (!$nodeContainer.is('[class*=-sibs]')) {
$nodeContainer.removeClass('isSiblingsCollapsed');
}
$siblings.removeClass('isCollapsedSibling ' + direction + '-sibs');
} else {
$node.closest('.hierarchy').removeClass('isSiblingsCollapsed');
$siblings.removeClass('isCollapsedSibling');
}
// thirdly, show parent node if it is collapsed
if (!this.getNodeState($node, 'parent').visible) {
$node.closest('.hierarchy').removeClass('isAncestorsCollapsed');
$upperLevel.removeClass('hidden');
this.repaint($upperLevel[0]);
$upperLevel.addClass('sliding').removeClass('slide-down').one('transitionend', this.showRelatedParentEnd);
}
// lastly, show the sibling nodes with animation
var $visibleNodes = $siblings.find('.node').filter(this.isVisibleNode.bind(this));
this.repaint($visibleNodes.get(0));
$visibleNodes.addClass('sliding').removeClass('slide-left slide-right');
$visibleNodes.eq(0).one('transitionend', { 'node': $node, 'visibleNodes': $visibleNodes }, this.showSiblingsEnd.bind(this));
},
// start up loading status for requesting new nodes
startLoading: function ($edge) {
var $chart = this.$chart;
if (typeof $chart.data('inAjax') !== 'undefined' && $chart.data('inAjax') === true) {
return false;
}
$edge.addClass('hidden');
$edge.parent().append(`<i class="${this.options.icons.theme} ${this.options.icons.spinner} spinner"></i>`)
.children().not('.spinner').css('opacity', 0.2);
$chart.data('inAjax', true);
$('.oc-export-btn').prop('disabled', true);
return true;
},
// terminate loading status for requesting new nodes
endLoading: function ($edge) {
var $node = $edge.parent();
$edge.removeClass('hidden');
$node.find('.spinner').remove();
$node.children().removeAttr('style');
this.$chart.data('inAjax', false);
$('.oc-export-btn').prop('disabled', false);
},
// whether the cursor is hovering over the node
isInAction: function ($node) {
// TODO: 展开/折叠的按钮不止4个箭头,还有toggleBtn
return [
this.options.icons.expandToUp,
this.options.icons.collapseToDown,
this.options.icons.collapseToLeft,
this.options.icons.expandToRight
].some((icon) => $node.children('.edge').attr('class').indexOf(icon) > -1);
},
//
switchVerticalArrow: function ($arrow) {
$arrow.toggleClass(`${this.options.icons.expandToUp} ${this.options.icons.collapseToDown}`);
},
//
switchHorizontalArrow: function ($node) {
var opts = this.options;
if (opts.toggleSiblingsResp && (typeof opts.ajaxURL === 'undefined' || $node.closest('.nodes').data('siblingsLoaded'))) {
var $prevSib = $node.parent().prev();
if ($prevSib.length) {
if ($prevSib.is('.hidden')) {
$node.children('.leftEdge').addClass(opts.icons.collapseToLeft).removeClass(opts.icons.expandToRight);
} else {
$node.children('.leftEdge').addClass(opts.icons.expandToRight).removeClass(opts.icons.collapseToLeft);
}
}
var $nextSib = $node.parent().next();
if ($nextSib.length) {
if ($nextSib.is('.hidden')) {
$node.children('.rightEdge').addClass(opts.icons.expandToRight).removeClass(opts.icons.collapseToLeft);
} else {
$node.children('.rightEdge').addClass(opts.icons.collapseToLeft).removeClass(opts.icons.expandToRight);
}
}
} else {
var $sibs = $node.parent().siblings();
var sibsVisible = $sibs.length ? !$sibs.is('.hidden') : false;
$node.children('.leftEdge').toggleClass(opts.icons.expandToRight, sibsVisible).toggleClass(opts.icons.collapseToLeft, !sibsVisible);
$node.children('.rightEdge').toggleClass(opts.icons.collapseToLeft, sibsVisible).toggleClass(opts.icons.expandToRight, !sibsVisible);
}
},
//
repaint: function (node) {
if (node) {
node.style.offsetWidth = node.offsetWidth;
}
},
// determines how to show arrow buttons
nodeEnterLeaveHandler: function (event) {
var $node = $(event.delegateTarget);
var flag = false;
if ($node.closest('.nodes.vertical').length) {
var $toggleBtn = $node.children('.toggleBtn');
if (event.type === 'mouseenter') {
if ($node.children('.toggleBtn').length) {
flag = this.getNodeState($node, 'children').visible;
$toggleBtn.toggleClass(this.options.icons.collapsed, !flag).toggleClass(this.options.icons.expanded, flag);
}
} else {
$toggleBtn.removeClass(`${this.options.icons.collapsed} ${this.options.icons.expanded}`);
}
} else {
var $topEdge = $node.children('.topEdge');
var $rightEdge = $node.children('.rightEdge');
var $bottomEdge = $node.children('.bottomEdge');
var $leftEdge = $node.children('.leftEdge');
if (event.type === 'mouseenter') {
if ($topEdge.length) {
flag = this.getNodeState($node, 'parent').visible;
$topEdge.toggleClass(this.options.icons.expandToUp, !flag).toggleClass(this.options.icons.collapseToDown, flag);
}
if ($bottomEdge.length) {
flag = this.getNodeState($node, 'children').visible;
$bottomEdge.toggleClass(this.options.icons.collapseToDown, !flag).toggleClass(this.options.icons.expandToUp, flag);
}
if ($leftEdge.length) {
this.switchHorizontalArrow($node);
}
} else {
$node.children('.edge').removeClass(`${this.options.icons.expandToUp} ${this.options.icons.collapseToDown} ${this.options.icons.collapseToLeft} ${this.options.icons.expandToRight}`);
}
}
},
//
nodeClickHandler: function (event) {
this.$chart.find('.focused').removeClass('focused');
$(event.delegateTarget).addClass('focused');
},
addAncestors: function (data, parentId) {
var $root = this.$chart.children('.nodes').children('.hierarchy');
this.buildHierarchy($root, data);
$root.children().slice(0, 2)
.wrapAll('<li class="hierarchy"></li>').parent()
.appendTo($('#' + parentId).siblings('.nodes'));
},
addDescendants:function (data, $parent) {
var that = this;
var $descendants = $('<ul class="nodes"></ul>');
$parent.after($descendants);
$.each(data, function (i) {
$descendants.append($('<li class="hierarchy"></li>'));
that.buildHierarchy($descendants.children().eq(i), this);
});
},
//
HideFirstParentEnd: function (event) {
var $topEdge = event.data.topEdge;
var $node = $topEdge.parent();
if (this.isInAction($node)) {
this.switchVerticalArrow($topEdge);
this.switchHorizontalArrow($node);
}
},
// actions on clinking top edge of a node
topEdgeClickHandler: function (event) {
var that = this;
var $topEdge = $(event.target);
var $node = $(event.delegateTarget);
var parentState = this.getNodeState($node, 'parent');
if (parentState.exist) {
var $parent = $node.closest('.nodes').siblings('.node');
if ($parent.is('.sliding')) { return; }
// hide the ancestor nodes and sibling nodes of the specified node
if (parentState.visible) {
this.hideParent($node);
$parent.one('transitionend', { 'topEdge': $topEdge }, this.HideFirstParentEnd.bind(this));
this.triggerHideEvent($node, 'parent');
} else { // show the ancestors and siblings
this.showParent($node);
this.triggerShowEvent($node, 'parent');
}
}
},
// actions on clinking bottom edge of a node
bottomEdgeClickHandler: function (event) {
var $bottomEdge = $(event.target);
var $node = $(event.delegateTarget);
var childrenState = this.getNodeState($node, 'children');
if (childrenState.exist) {
var $children = $node.siblings('.nodes').children().children('.node');
if ($children.is('.sliding')) { return; }
// hide the descendant nodes of the specified node
if (childrenState.visible) {
this.hideChildren($node);
this.triggerHideEvent($node, 'children');
} else { // show the descendants
this.showChildren($node);
this.triggerShowEvent($node, 'children');
}
}
},
// actions on clicking horizontal edges
hEdgeClickHandler: function (event) {
var $hEdge = $(event.target);
var $node = $(event.delegateTarget);
var opts = this.options;
var siblingsState = this.getNodeState($node, 'siblings');
if (siblingsState.exist) {
var $siblings = $node.closest('.hierarchy').siblings();
if ($siblings.find('.sliding').length) { return; }
if (opts.toggleSiblingsResp) {
var $prevSib = $node.closest('.hierarchy').prev();
var $nextSib = $node.closest('.hierarchy').next();
if ($hEdge.is('.leftEdge')) {
if ($prevSib.is('.hidden')) {
this.showSiblings($node, 'left');
this.triggerShowEvent($node,'siblings');
} else {
this.hideSiblings($node, 'left');
this.triggerHideEvent($node, 'siblings');
}
} else {
if ($nextSib.is('.hidden')) {
this.showSiblings($node, 'right');
this.triggerShowEvent($node,'siblings');
} else {
this.hideSiblings($node, 'right');
this.triggerHideEvent($node, 'siblings');
}
}
} else {
if (siblingsState.visible) {
this.hideSiblings($node);
this.triggerHideEvent($node, 'siblings');
} else {
this.showSiblings($node);
this.triggerShowEvent($node, 'siblings');
}
}
}
},
// show the compact node's children in the compact mode
backToCompactHandler: function (event) {
$(event.delegateTarget).removeClass('looseMode')
.find('.looseMode').removeClass('looseMode')
.children('.backToCompactSymbol').addClass('hidden').end()
.children('.backToLooseSymbol').removeClass('hidden');
$(event.delegateTarget).children('.backToCompactSymbol').addClass('hidden').end()
.children('.backToLooseSymbol').removeClass('hidden');
},
// show the compact node's children in the loose mode
backToLooseHandler: function (event) {
$(event.delegateTarget)
.addClass('looseMode')
.children('.backToLooseSymbol').addClass('hidden').end()
.children('.backToCompactSymbol').removeClass('hidden');
},
//
expandVNodesEnd: function (event) {
event.data.vNodes.removeClass('sliding');
},
//
collapseVNodesEnd: function (event) {
event.data.vNodes.removeClass('sliding').closest('ul').addClass('hidden');
},
// event handler for toggle buttons in Hybrid(horizontal + vertical) OrgChart
toggleVNodes: function (event) {
var $toggleBtn = $(event.target);
var $descWrapper = $toggleBtn.parent().next();
var $descendants = $descWrapper.find('.node');
var $children = $descWrapper.children().children('.node');
if ($children.is('.sliding')) { return; }
$toggleBtn.toggleClass(`${this.options.icons.collapsed} ${this.options.icons.expanded}`);
if ($descendants.eq(0).is('.slide-up')) {
$descWrapper.removeClass('hidden');
this.repaint($children.get(0));
$children.addClass('sliding').removeClass('slide-up').eq(0).one('transitionend', { 'vNodes': $children }, this.expandVNodesEnd);
} else {
$descendants.addClass('sliding slide-up').eq(0).one('transitionend', { 'vNodes': $descendants }, this.collapseVNodesEnd);
$descendants.find('.toggleBtn').removeClass(`${this.options.icons.collapsed} ${this.options.icons.expanded}`);
}
},
//
createGhostNode: function (event) {
var $nodeDiv = $(event.target);
var opts = this.options;
var origEvent = event.originalEvent;
var isFirefox = /firefox/.test(window.navigator.userAgent.toLowerCase());
var ghostNode, nodeCover;
if (!document.querySelector('.ghost-node')) {
ghostNode = document.createElementNS("http://www.w3.org/2000/svg", "svg");
if (!ghostNode.classList) return;
ghostNode.classList.add('ghost-node');
nodeCover = document.createElementNS('http://www.w3.org/2000/svg','rect');
ghostNode.appendChild(nodeCover);
$nodeDiv.closest('.orgchart').append(ghostNode);
} else {
ghostNode = $nodeDiv.closest('.orgchart').children('.ghost-node').get(0);
nodeCover = $(ghostNode).children().get(0);
}
var transValues = $nodeDiv.closest('.orgchart').css('transform').split(',');
var isHorizontal = opts.direction === 't2b' || opts.direction === 'b2t';
var scale = Math.abs(window.parseFloat(isHorizontal ? transValues[0].slice(transValues[0].indexOf('(') + 1) : transValues[1]));
ghostNode.setAttribute('width', isHorizontal ? $nodeDiv.outerWidth(false) : $nodeDiv.outerHeight(false));
ghostNode.setAttribute('height', isHorizontal ? $nodeDiv.outerHeight(false) : $nodeDiv.outerWidth(false));
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);
var xOffset = origEvent.offsetX * scale;
var yOffset = origEvent.offsetY * scale;
if (opts.direction === 'l2r') {
xOffset = origEvent.offsetY * scale;
yOffset = origEvent.offsetX * scale;
} else if (opts.direction === 'r2l') {
xOffset = $nodeDiv.outerWidth(false) - origEvent.offsetY * scale;
yOffset = origEvent.offsetX * scale;
} else if (opts.direction === 'b2t') {
xOffset = $nodeDiv.outerWidth(false) - origEvent.offsetX * scale;
yOffset = $nodeDiv.outerHeight(false) - origEvent.offsetY * scale;
}
if (isFirefox) { // hack for old version of Firefox(< 48.0)
nodeCover.setAttribute('fill', 'rgb(255, 255, 255)');
nodeCover.setAttribute('stroke', 'rgb(191, 0, 0)');
var ghostNodeWrapper = document.createElement('img');
ghostNodeWrapper.src = 'data:image/svg+xml;utf8,' + (new XMLSerializer()).serializeToString(ghostNode);
origEvent.dataTransfer.setDragImage(ghostNodeWrapper, xOffset, yOffset);
} else {
// IE/Edge do not support this, so only use it if we can
if (origEvent.dataTransfer.setDragImage)
origEvent.dataTransfer.setDragImage(ghostNode, xOffset, yOffset);
}
},
// get the level amount of a hierachy
getUpperLevel: function ($node) {
if (!$node.is('.node')) {
return 0;
}
return $node.parents('.hierarchy').length;
},
// get the level amount of a hierachy
getLowerLevel: function ($node) {
if (!$node.is('.node')) {
return 0;
}
return $node.closest('.hierarchy').find('.nodes').length + 1;
},
// get nodes in level order traversal
getLevelOrderNodes: function ($root) {
if(!$root) return [];
var queue = [];
var output = [];
queue.push($root);
while(queue.length) {
var row = [];
for(var i = 0; i < queue.length; i++) {
var cur = queue.shift();
var children = this.getChildren(cur);
if(children.length) {
queue.push(children.toArray().flat());
}
row.push($(cur));
}
output.push(row);
}
return output;
},
//
filterAllowedDropNodes: function ($dragged) {
var opts = this.options;
// what is being dragged? a node, or something within a node?
var draggingNode = $dragged.closest('[draggable]').hasClass('node');
var $dragZone = $dragged.closest('.nodes').siblings('.node'); // parent node
var $dragHier = $dragged.closest('.hierarchy').find('.node'); // this node, and its children
this.$chart.data('dragged', $dragged)
.find('.node').each(function (index, node) {
if (!draggingNode || $dragHier.index(node) === -1) {
if (opts.dropCriteria) {
if (opts.dropCriteria($dragged, $dragZone, $(node))) {
$(node).addClass('allowedDrop');
}
} else {
$(node).addClass('allowedDrop');
}
}
});
},
//
dragstartHandler: function (event) {
event.originalEvent.dataTransfer.setData('text/html', 'hack for firefox');
// if users enable zoom or direction options
if (this.$chart.css('transform') !== 'none') {
this.createGhostNode(event);
}
this.filterAllowedDropNodes($(event.target));
},
//
dragoverHandler: function (event) {
if (!$(event.delegateTarget).is('.allowedDrop')) {
event.originalEvent.dataTransfer.dropEffect = 'none';
} else {
// default action for drag-and-drop of div is not to drop, so preventing default action for nodes which have allowedDrop class
//to fix drag and drop on IE and Edge
event.preventDefault();
}
},
//
dragendHandler: function (event) {
this.$chart.find('.allowedDrop').removeClass('allowedDrop');
},
// when user drops the node, it will be removed from original parent node and be added to new parent node
dropHandler: async function (event) {
var that = this;
var $dropZone = $(event.delegateTarget);
var $dragged = this.$chart.data('dragged');
// Pass on drops which are not nodes (since they are not our doing)
if (!$dragged.hasClass('node')) {
this.$chart.triggerHandler({ 'type': 'otherdropped.orgchart', 'draggedItem': $dragged, 'dropZone': $dropZone });
return;
}
if (!$dropZone.hasClass('allowedDrop')) {
// We are trying to drop a node into a node which isn't allowed
// IE/Edge have a habit of allowing this, so we need our own double-check
return;
}
var $dragZone = $dragged.closest('.nodes').siblings('.node');
var dropEvent = $.Event('nodedrop.orgchart');
this.$chart.trigger(dropEvent, { 'draggedNode': $dragged, 'dragZone': $dragZone, 'dropZone': $dropZone });
if (dropEvent.isDefaultPrevented()) {
return;
}
// special process for hybrid chart
var datasource = this.$chart.data('options').data;
var digger = new JSONDigger(datasource, this.$chart.data('options').nodeId, 'children');
const hybridNode = digger.findOneNode({ 'hybrid': true });
if (this.$chart.data('options').verticalLevel > 1 || hybridNode) {
var draggedNode = digger.findNodeById($dragged.data('nodeData').id);
var copy = Object.assign({}, draggedNode);
digger.removeNode(draggedNode.id);
var dropNode = digger.findNodeById($dropZone.data('nodeData').id);
if (dropNode.children) {
dropNode.children.push(copy);
} else {
dropNode.children = [copy];
}
that.init({ 'data': datasource });
} else {
// The folowing code snippets are used to process horizontal chart
// firstly, deal with the hierarchy of drop zone
if (!$dropZone.siblings('.nodes').length) { // if the drop zone is a leaf node
$dropZone.append(`<i class="edge verticalEdge bottomEdge ${this.options.icons.theme}"></i>`)
.after('<ul class="nodes"></ul>')
.siblings('.nodes').append($dragged.find('.horizontalEdge').remove().end().closest('.hierarchy'));
if ($dropZone.children('.title').length) {
$dropZone.children('.title').prepend(`<i class="${this.options.icons.theme} ${this.$chart.data('options').icons.parentNode} parentNodeSymbol"></i>`);
}
} else {
var horizontalEdges = `<i class="edge horizontalEdge rightEdge ${this.options.icons.theme}"></i><i class="edge horizontalEdge leftEdge ${this.options.icons.theme}"></i>`;
if (!$dragged.find('.horizontalEdge').length) {
$dragged.append(horizontalEdges);
}
$dropZone.siblings('.nodes').append($dragged.closest('.hierarchy'));
var $dropSibs = $dragged.closest('.hierarchy').siblings().find('.node:first');
if ($dropSibs.length === 1) {
$dropSibs.append(horizontalEdges);
}
}
// secondly, deal with the hierarchy of dragged node
if ($dragZone.siblings('.nodes').children('.hierarchy').length === 1) { // if there is only one sibling node left
$dragZone.siblings('.nodes').children('.hierarchy').find('.node:first')
.find('.horizontalEdge').remove();
} else if ($dragZone.siblings('.nodes').children('.hierarchy').length === 0) {
$dragZone.find('.bottomEdge, .parentNodeSymbol').remove()
.end().siblings('.nodes').remove();
}
}
},
//
touchstartHandler: function (event) {
if (this.touchHandled)
return;
if (event.touches && event.touches.length > 1)
return;
this.touchHandled = true;
this.touchMoved = false; // this is so we can work out later if this was a 'press' or a 'drag' touch
event.preventDefault();
},
//
touchmoveHandler: function (event) {
if (!this.touchHandled)
return;
if (event.touches && event.touches.length > 1)
return;
event.preventDefault();
if (!this.touchMoved) {
// we do not bother with createGhostNode (dragstart does) since the touch event does not have a dataTransfer property
this.filterAllowedDropNodes($(event.currentTarget)); // will also set 'this.$chart.data('dragged')' for us
// create an image which can be used to illustrate the drag (our own