UNPKG

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
/* * 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