/*
 * jQuery OrgChart Plugin
 * https://github.com/dabeng/OrgChart
 *
 * Demos of jQuery OrgChart Plugin
 * http://dabeng.github.io/OrgChart/
 *
 * Copyright 2016, dabeng
 * http://dabeng.github.io/
 *
 * 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) {
    $.fn.orgchart = function (options) {
        var defaultOptions = {
            'nodeTitle': 'name',
            'nodeId': 'id',
            'toggleSiblingsResp': false,
            'depth': 999,
            'chartClass': '',
            'exportButton': false,
            'exportFilename': 'OrgChart',
            'exportFileextension': 'png',
            'parentNodeSymbol': 'fa-users',
            'draggable': false,
            'direction': 't2b',
            'pan': false,
            'zoom': false,
            'zoominLimit': 7,
            'zoomoutLimit': 0.5
        };

        switch (options) {
            case 'buildHierarchy':
                return buildHierarchy.apply(this, Array.prototype.splice.call(arguments, 1));
            case 'addChildren':
                return addChildren.apply(this, Array.prototype.splice.call(arguments, 1));
            case 'addParent':
                return addParent.apply(this, Array.prototype.splice.call(arguments, 1));
            case 'addSiblings':
                return addSiblings.apply(this, Array.prototype.splice.call(arguments, 1));
            case 'removeNodes':
                return removeNodes.apply(this, Array.prototype.splice.call(arguments, 1));
            case 'getHierarchy':
                return getHierarchy.apply(this, Array.prototype.splice.call(arguments, 1));
            case 'hideParent':
                return hideParent.apply(this, Array.prototype.splice.call(arguments, 1));
            case 'showParent':
                return showParent.apply(this, Array.prototype.splice.call(arguments, 1));
            case 'hideChildren':
                return hideChildren.apply(this, Array.prototype.splice.call(arguments, 1));
            case 'showChildren':
                return showChildren.apply(this, Array.prototype.splice.call(arguments, 1));
            case 'hideSiblings':
                return hideSiblings.apply(this, Array.prototype.splice.call(arguments, 1));
            case 'showSiblings':
                return showSiblings.apply(this, Array.prototype.splice.call(arguments, 1));
            case 'getNodeState':
                return getNodeState.apply(this, Array.prototype.splice.call(arguments, 1));
            case 'getRelatedNodes':
                return getRelatedNodes.apply(this, Array.prototype.splice.call(arguments, 1));
            case 'setChartScale':
                return setChartScale.apply(this, Array.prototype.splice.call(arguments, 1));
            default: // initiation time
                var opts = $.extend(defaultOptions, options);
        }

        // build the org-chart
        var $chartContainer = this;
        var data = opts.data;
        var $chart = $('<div>', {
            'data': { 'options': opts },
            'class': 'orgchart' + (opts.chartClass !== '' ? ' ' + opts.chartClass : '') + (opts.direction !== 't2b' ? ' ' + opts.direction : ''),
            'click': function (event) {
                if (!$(event.target).closest('.node').length) {
                    $chart.find('.node.focused').removeClass('focused');
                }
            }
        });
        if ($.type(data) === 'object') {
            if (data instanceof $) { // ul datasource
                buildHierarchy($chart, buildJsonDS(data.children()), 0, opts);
            } else { // local json datasource
                buildHierarchy($chart, opts.ajaxURL ? data : attachRel(data, '00'), 0, opts);
            }
        } else {
            $.ajax({
                'url': data,
                'dataType': 'json',
                'beforeSend': function () {
                    $chart.append('<i class="fa fa-circle-o-notch fa-spin spinner"></i>');
                }
            })
            .done(function (data, textStatus, jqXHR) {
                buildHierarchy($chart, opts.ajaxURL ? data : attachRel(data, '00'), 0, opts);
            })
            .fail(function (jqXHR, textStatus, errorThrown) {
                console.log(errorThrown);
            })
            .always(function () {
                $chart.children('.spinner').remove();
            });
        }
        $chartContainer.append($chart);

        // append the export button
        if (opts.exportButton && !$chartContainer.find('.oc-export-btn').length) {
            var $exportBtn = $('<button>', {
                'class': 'oc-export-btn' + (opts.chartClass !== '' ? ' ' + opts.chartClass : ''),
                'text': 'Export',
                'click': function (e) {
                    e.preventDefault();
                    if ($(this).children('.spinner').length) {
                        return false;
                    }
                    var $mask = $chartContainer.find('.mask');
                    if (!$mask.length) {
                        $chartContainer.append('<div class="mask"><i class="fa fa-circle-o-notch fa-spin spinner"></i></div>');
                    } else {
                        $mask.removeClass('hidden');
                    }
                    var sourceChart = $chartContainer.addClass('canvasContainer').find('.orgchart:visible').get(0);
                    var flag = opts.direction === 'l2r' || opts.direction === 'r2l';
                    html2canvas(sourceChart, {
                        'width': flag ? sourceChart.clientHeight : sourceChart.clientWidth,
                        'height': flag ? sourceChart.clientWidth : sourceChart.clientHeight,
                        'onclone': function (cloneDoc) {
                            $(cloneDoc).find('.canvasContainer').css('overflow', 'visible')
                              .find('.orgchart:visible:first').css('transform', '');
                        },
                        'onrendered': function (canvas) {
                            $chartContainer.find('.mask').addClass('hidden');
                            if (opts.exportFileextension.toLowerCase() === 'pdf') {
                                var doc = {};
                                var docWidth = Math.floor(canvas.width * 0.2646);
                                var docHeight = Math.floor(canvas.height * 0.2646);
                                if (docWidth > docHeight) {
                                    doc = new jsPDF('l', 'mm', [docWidth, docHeight]);
                                } else {
                                    doc = new jsPDF('p', 'mm', [docHeight, docWidth]);
                                }
                                doc.addImage(canvas.toDataURL(), 'png', 0, 0);
                                doc.save(opts.exportFilename + '.pdf');
                            } else {
                                var isWebkit = 'WebkitAppearance' in document.documentElement.style;
                                var isFf = !!window.sidebar;
                                var isEdge = navigator.appName === 'Microsoft Internet Explorer' || (navigator.appName === "Netscape" && navigator.appVersion.indexOf('Edge') > -1);

                                if ((!isWebkit && !isFf) || isEdge) {
                                    window.navigator.msSaveBlob(canvas.msToBlob(), opts.exportFilename + '.png');
                                }
                                else {
                                    $chartContainer.find('.oc-download-btn').attr('href', canvas.toDataURL())[0].click();
                                }
                            }
                        }
                    })
                    .then(function () {
                        $chartContainer.removeClass('canvasContainer');
                    }, function () {
                        $chartContainer.removeClass('canvasContainer');
                    });
                }
            });
            $chartContainer.append($exportBtn);
            if (opts.exportFileextension.toLowerCase() !== 'pdf') {
                var downloadBtn = '<a class="oc-download-btn' + (opts.chartClass !== '' ? ' ' + opts.chartClass : '') + '"'
                  + ' download="' + opts.exportFilename + '.png"></a>';
                $exportBtn.after(downloadBtn);
            }
        }

        if (opts.pan) {
            $chartContainer.css('overflow', 'hidden');
            $chart.on('mousedown touchstart', function (e) {
                var $this = $(this);
                if ($(e.target).closest('.node').length || (e.touches && e.touches.length > 1)) {
                    $this.data('panning', false);
                    return;
                } else {
                    $this.css('cursor', 'move').data('panning', true);
                }
                var lastX = 0;
                var lastY = 0;
                var lastTf = $this.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 (!$this.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 = $this.css('transform');
                    if (lastTf === 'none') {
                        if (lastTf.indexOf('3d') === -1) {
                            $this.css('transform', 'matrix(1, 0, 0, 1, ' + newX + ', ' + newY + ')');
                        } else {
                            $this.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;
                        }
                        $this.css('transform', matrix.join(','));
                    }
                });
            });
            $(document).on('mouseup touchend', function (e) {
                if ($chart.data('panning')) {
                    $chart.data('panning', false).css('cursor', 'default').off('mousemove');
                }
            });
        }

        if (opts.zoom) {
            $chartContainer.on('wheel', function (event) {
                event.preventDefault();
                var newScale = 1 + (event.originalEvent.deltaY > 0 ? -0.2 : 0.2);
                setChartScale($chart, newScale);
            });

            $chartContainer.on('touchstart', function (e) {
                if (e.touches && e.touches.length === 2) {
                    $chart.data('pinching', true);
                    var dist = getPinchDist(e);
                    $chart.data('pinchDistStart', dist);
                }
            });
            $(document).on('touchmove', function (e) {
                if ($chart.data('pinching')) {
                    var dist = getPinchDist(e);
                    $chart.data('pinchDistEnd', dist);
                }
            })
            .on('touchend', function (e) {
                if ($chart.data('pinching')) {
                    $chart.data('pinching', false);
                    var diff = $chart.data('pinchDistEnd') - $chart.data('pinchDistStart');
                    if (diff > 0) {
                        setChartScale($chart, 1.2);
                    } else if (diff < 0) {
                        setChartScale($chart, 0.8);
                    }
                }
            });
        }

        return $chartContainer;
    };

    function getPinchDist(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));
    }

    function setChartScale($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 = window.parseFloat(matrix[3]) * newScale;
                if (targetScale > opts.zoomoutLimit && targetScale < opts.zoominLimit) {
                    $chart.css('transform', lastTf + ' scale(' + newScale + ',' + newScale + ')');
                }
            } else {
                targetScale = window.parseFloat(matrix[1]) * newScale;
                if (targetScale > opts.zoomoutLimit && targetScale < opts.zoominLimit) {
                    $chart.css('transform', lastTf + ' scale3d(' + newScale + ',' + newScale + ', 1)');
                }
            }
        }
    }

    function buildJsonDS($li) {
        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)
        };
        if ($li[0].id) {
            subObj.id = $li[0].id;
        }
        $li.children('ul').children().each(function () {
            if (!subObj.children) { subObj.children = []; }
            subObj.children.push(buildJsonDS($(this)));
        });
        return subObj;
    }

    function attachRel(data, flags) {
        data.relationship = flags + (data.children && data.children.length > 0 ? 1 : 0);
        if (data.children) {
            data.children.forEach(function (item) {
                attachRel(item, '1' + (data.children.length > 1 ? 1 : 0));
            });
        }
        return data;
    }

    function loopChart($chart) {
        var $tr = $chart.find('tr:first');
        var subObj = { 'id': $tr.find('.node')[0].id };
        $tr.siblings(':last').children().each(function () {
            if (!subObj.children) { subObj.children = []; }
            subObj.children.push(loopChart($(this)));
        });
        return subObj;
    }

    function getHierarchy($chart) {
        var $chart = $chart || $(this).find('.orgchart');
        if (!$chart.find('.node:first')[0].id) {
            return 'Error: Nodes of orghcart to be exported must have id attribute!';
        }
        return loopChart($chart);
    }

    // detect the exist/display state of related node
    function getNodeState($node, relation) {
        var $target = {};
        if (relation === 'parent') {
            $target = $node.closest('.nodes').siblings(':first');
        } else if (relation === 'children') {
            $target = $node.closest('tr').siblings();
        } else if (relation === 'siblings') {
            $target = $node.closest('table').parent().siblings();
        }
        if ($target.length) {
            if ($target.is(':visible')) {
                return { 'exist': true, 'visible': true };
            }
            return { 'exist': true, 'visible': false };
        }
        return { 'exist': false, 'visible': false };
    }

    // find the related nodes
    function getRelatedNodes($node, relation) {
        if (relation === 'parent') {
            return $node.closest('.nodes').parent().children(':first').find('.node');
        } else if (relation === 'children') {
            return $node.closest('table').children(':last').children().find('.node:first');
        } else if (relation === 'siblings') {
            return $node.closest('table').parent().siblings().find('.node:first');
        }
    }

    // recursively hide the ancestor node and sibling nodes of the specified node
    function hideParent($node) {
        var $temp = $node.closest('table').closest('tr').siblings();
        if ($temp.eq(0).find('.spinner').length) {
            $node.closest('.orgchart').data('inAjax', false);
        }
        // hide the sibling nodes
        if (getNodeState($node, 'siblings').visible) {
            hideSiblings($node);
        }
        // hide the lines
        var $lines = $temp.slice(1);
        $lines.css('visibility', 'hidden');
        // hide the superior nodes with transition
        var $parent = $temp.eq(0).find('.node');
        var grandfatherVisible = getNodeState($parent, 'parent').visible;
        if ($parent.length && $parent.is(':visible')) {
            $parent.addClass('slide slide-down').one('transitionend', function () {
                $parent.removeClass('slide');
                $lines.removeAttr('style');
                $temp.addClass('hidden');
            });
        }
        // if the current node has the parent node, hide it recursively
        if ($parent.length && grandfatherVisible) {
            hideParent($parent);
        }
    }

    // show the parent node of the specified node
    function showParent($node) {
        // just show only one superior level
        var $temp = $node.closest('table').closest('tr').siblings().removeClass('hidden');
        // just show only one line
        $temp.eq(2).children().slice(1, -1).addClass('hidden');
        // show parent node with animation
        var parent = $temp.eq(0).find('.node')[0];
        repaint(parent);
        $(parent).addClass('slide').removeClass('slide-down').one('transitionend', function () {
            $(parent).removeClass('slide');
            if (isInAction($node)) {
                switchVerticalArrow($node.children('.topEdge'));
            }
        });
    }

    // recursively hide the descendant nodes of the specified node
    function hideChildren($node) {
        var $temp = $node.closest('tr').siblings();
        if ($temp.last().find('.spinner').length) {
            $node.closest('.orgchart').data('inAjax', false);
        }
        var $visibleNodes = $temp.last().find('.node:visible');
        var isVerticalDesc = $temp.last().is('.verticalNodes') ? true : false;
        if (!isVerticalDesc) {
            var $lines = $visibleNodes.closest('table').closest('tr').prevAll('.lines').css('visibility', 'hidden');
        }
        $visibleNodes.addClass('slide slide-up').eq(0).one('transitionend', function () {
            $visibleNodes.removeClass('slide');
            if (isVerticalDesc) {
                $temp.addClass('hidden');
            } else {
                $lines.removeAttr('style').addClass('hidden').siblings('.nodes').addClass('hidden');
                $temp.last().find('.verticalNodes').addClass('hidden');
            }
            if (isInAction($node)) {
                switchVerticalArrow($node.children('.bottomEdge'));
            }
        });
    }

    // show the children nodes of the specified node
    function showChildren($node) {
        var $levels = $node.closest('tr').siblings();
        var isVerticalDesc = $levels.is('.verticalNodes') ? true : false;
        var $descendants = isVerticalDesc
          ? $levels.removeClass('hidden').find('.node:visible')
          : $levels.removeClass('hidden').eq(2).children().find('.node:first');
        // the two following statements are used to enforce browser to repaint
        repaint($descendants.get(0));
        $descendants.addClass('slide').removeClass('slide-up').eq(0).one('transitionend', function () {
            $descendants.removeClass('slide');
            if (isInAction($node)) {
                switchVerticalArrow($node.children('.bottomEdge'));
            }
        });
    }

    // hide the sibling nodes of the specified node
    function hideSiblings($node, direction) {
        var $nodeContainer = $node.closest('table').parent();
        if ($nodeContainer.siblings().find('.spinner').length) {
            $node.closest('.orgchart').data('inAjax', false);
        }
        if (direction) {
            if (direction === 'left') {
                $nodeContainer.prevAll().find('.node:visible').addClass('slide slide-right');
            } else {
                $nodeContainer.nextAll().find('.node:visible').addClass('slide slide-left');
            }
        } else {
            $nodeContainer.prevAll().find('.node:visible').addClass('slide slide-right');
            $nodeContainer.nextAll().find('.node:visible').addClass('slide slide-left');
        }
        var $animatedNodes = $nodeContainer.siblings().find('.slide');
        var $lines = $animatedNodes.closest('.nodes').prevAll('.lines').css('visibility', 'hidden');
        $animatedNodes.eq(0).one('transitionend', function () {
            $lines.removeAttr('style');
            var $siblings = direction ? (direction === 'left' ? $nodeContainer.prevAll(':not(.hidden)') : $nodeContainer.nextAll(':not(.hidden)')) : $nodeContainer.siblings();
            $nodeContainer.closest('.nodes').prev().children(':not(.hidden)')
              .slice(1, direction ? $siblings.length * 2 + 1 : -1).addClass('hidden');
            $animatedNodes.removeClass('slide');
            $siblings.find('.node:visible:gt(0)').removeClass('slide-left slide-right').addClass('slide-up')
              .end().find('.lines, .nodes, .verticalNodes').addClass('hidden')
              .end().addClass('hidden');

            if (isInAction($node)) {
                switchHorizontalArrow($node);
            }
        });
    }

    // show the sibling nodes of the specified node
    function showSiblings($node, direction) {
        // firstly, show the sibling td tags
        var $siblings = $();
        if (direction) {
            if (direction === 'left') {
                $siblings = $node.closest('table').parent().prevAll().removeClass('hidden');
            } else {
                $siblings = $node.closest('table').parent().nextAll().removeClass('hidden');
            }
        } else {
            $siblings = $node.closest('table').parent().siblings().removeClass('hidden');
        }
        // secondly, show the lines
        var $upperLevel = $node.closest('table').closest('tr').siblings();
        if (direction) {
            $upperLevel.eq(2).children('.hidden').slice(0, $siblings.length * 2).removeClass('hidden');
        } else {
            $upperLevel.eq(2).children('.hidden').removeClass('hidden');
        }
        // thirdly, do some cleaning stuff
        if (!getNodeState($node, 'parent').visible) {
            $upperLevel.removeClass('hidden');
            var parent = $upperLevel.find('.node')[0];
            repaint(parent);
            $(parent).addClass('slide').removeClass('slide-down').one('transitionend', function () {
                $(this).removeClass('slide');
            });
        }
        // lastly, show the sibling nodes with animation
        $siblings.find('.node:visible').addClass('slide').removeClass('slide-left slide-right').eq(-1).one('transitionend', function () {
            $siblings.find('.node:visible').removeClass('slide');
            if (isInAction($node)) {
                switchHorizontalArrow($node);
                $node.children('.topEdge').removeClass('fa-chevron-up').addClass('fa-chevron-down');
            }
        });
    }

    // start up loading status for requesting new nodes
    function startLoading($arrow, $node, options) {
        var $chart = $node.closest('.orgchart');
        if (typeof $chart.data('inAjax') !== 'undefined' && $chart.data('inAjax') === true) {
            return false;
        }

        $arrow.addClass('hidden');
        $node.append('<i class="fa fa-circle-o-notch fa-spin spinner"></i>');
        $node.children().not('.spinner').css('opacity', 0.2);
        $chart.data('inAjax', true);
        $('.oc-export-btn' + (options.chartClass !== '' ? '.' + options.chartClass : '')).prop('disabled', true);
        return true;
    }

    // terminate loading status for requesting new nodes
    function endLoading($arrow, $node, options) {
        var $chart = $node.closest('div.orgchart');
        $arrow.removeClass('hidden');
        $node.find('.spinner').remove();
        $node.children().removeAttr('style');
        $chart.data('inAjax', false);
        $('.oc-export-btn' + (options.chartClass !== '' ? '.' + options.chartClass : '')).prop('disabled', false);
    }

    // whether the cursor is hovering over the node
    function isInAction($node) {
        return $node.children('.edge').attr('class').indexOf('fa-') > -1 ? true : false;
    }

    function switchVerticalArrow($arrow) {
        $arrow.toggleClass('fa-chevron-up').toggleClass('fa-chevron-down');
    }

    function switchHorizontalArrow($node) {
        var opts = $node.closest('.orgchart').data('options');
        if (opts.toggleSiblingsResp && (typeof opts.ajaxURL === 'undefined' || $node.closest('.nodes').data('siblingsLoaded'))) {
            var $prevSib = $node.closest('table').parent().prev();
            if ($prevSib.length) {
                if ($prevSib.is('.hidden')) {
                    $node.children('.leftEdge').addClass('fa-chevron-left').removeClass('fa-chevron-right');
                } else {
                    $node.children('.leftEdge').addClass('fa-chevron-right').removeClass('fa-chevron-left');
                }
            }
            var $nextSib = $node.closest('table').parent().next();
            if ($nextSib.length) {
                if ($nextSib.is('.hidden')) {
                    $node.children('.rightEdge').addClass('fa-chevron-right').removeClass('fa-chevron-left');
                } else {
                    $node.children('.rightEdge').addClass('fa-chevron-left').removeClass('fa-chevron-right');
                }
            }
        } else {
            var $sibs = $node.closest('table').parent().siblings();
            var sibsVisible = $sibs.length ? !$sibs.is('.hidden') : false;
            $node.children('.leftEdge').toggleClass('fa-chevron-right', sibsVisible).toggleClass('fa-chevron-left', !sibsVisible);
            $node.children('.rightEdge').toggleClass('fa-chevron-left', sibsVisible).toggleClass('fa-chevron-right', !sibsVisible);
        }
    }

    function repaint(node) {
        if (node) {
            node.style.offsetWidth = node.offsetWidth;
        }
    }


    var expandedOrgList = [];

    // create node
    function createNode(nodeData, level, opts) {

        $.each(nodeData.children, function (index, child) {
            child.parentId = nodeData.id;
        });
        var dtd = $.Deferred();
        // construct the content of node
        var $nodeDiv = $('<div' + (opts.draggable ? ' draggable="true"' : '') + (nodeData[opts.nodeId] ? ' id="' + nodeData[opts.nodeId] + '"' : '') + (nodeData.parentId ? ' data-parent="' + nodeData.parentId + '"' : '') + '>')
          .addClass('node ' + (nodeData.className || '') + (level >= opts.depth ? ' slide-up' : ''))
          .append('<div class="title">' + nodeData[opts.nodeTitle] + '</div>')
          .append(typeof opts.nodeContent !== 'undefined' ? '<div class="content">' + (nodeData[opts.nodeContent] || '') + '</div>' : '');
        // append 4 direction arrows or expand/collapse buttons
        var flags = nodeData.relationship || '';
        if (opts.verticalDepth && (level + 2) > opts.verticalDepth) {
            if ((level + 1) >= opts.verticalDepth && Number(flags.substr(2, 1))) {
                var icon = level + 1 >= opts.depth ? 'plus' : 'minus';
                $nodeDiv.append('<i class="toggleBtn fa fa-' + icon + '-square"></i>');
            }
        } else {
            if (Number(flags.substr(0, 1))) {
                $nodeDiv.append('<i class="edge verticalEdge topEdge fa"></i>');
            }
            if (Number(flags.substr(1, 1))) {
                $nodeDiv.append('<i class="edge horizontalEdge rightEdge fa"></i>' +
                  '<i class="edge horizontalEdge leftEdge fa"></i>');
            }
            if (Number(flags.substr(2, 1))) {
                $nodeDiv.append('<i class="edge verticalEdge bottomEdge fa"></i>')
                  .children('.title').prepend('<i class="fa ' + opts.parentNodeSymbol + ' symbol"></i>');
            }
        }

        $nodeDiv.on('mouseenter mouseleave', function (event) {
            var $node = $(this), flag = false;
            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 = getNodeState($node, 'parent').visible;
                    $topEdge.toggleClass('fa-chevron-up', !flag).toggleClass('fa-chevron-down', flag);
                }
                if ($bottomEdge.length) {
                    flag = getNodeState($node, 'children').visible;
                    $bottomEdge.toggleClass('fa-chevron-down', !flag).toggleClass('fa-chevron-up', flag);
                }
                if ($leftEdge.length) {
                    switchHorizontalArrow($node);
                }
            } else {
                $node.children('.edge').removeClass('fa-chevron-up fa-chevron-down fa-chevron-right fa-chevron-left');
            }
        });

        // define click event handler
        $nodeDiv.on('click', function (event) {
            $(this).closest('.orgchart').find('.focused').removeClass('focused');
            $(this).addClass('focused');
        });

        // define click event handler for the top edge
        $nodeDiv.on('click', '.topEdge', function (event) {
            event.stopPropagation();
            var $that = $(this);
            var $node = $that.parent();
            var parentState = getNodeState($node, 'parent');
            if (parentState.exist) {
                var $parent = $node.closest('table').closest('tr').siblings(':first').find('.node');
                if ($parent.is('.slide')) { return; }
                // hide the ancestor nodes and sibling nodes of the specified node
                if (parentState.visible) {
                    hideParent($node);
                    $parent.one('transitionend', function () {
                        if (isInAction($node)) {
                            switchVerticalArrow($that);
                            switchHorizontalArrow($node);
                        }
                    });
                } else { // show the ancestors and siblings
                    showParent($node);
                }
            } else {
                // load the new parent node of the specified node by ajax request
                var nodeId = $that.parent()[0].id;
                // start up loading status
                if (startLoading($that, $node, opts)) {
                    // load new nodes 
                    $.ajax({ 'url': $.isFunction(opts.ajaxURL.parent) ? opts.ajaxURL.parent(nodeData) : opts.ajaxURL.parent + nodeId, 'dataType': 'json' })
                    .done(function (data) {
                        if ($node.closest('.orgchart').data('inAjax')) {
                            if (!$.isEmptyObject(data)) {
                                addParent.call($node.closest('.orgchart').parent(), $node, data, opts);
                            }
                        }
                    })
                    .fail(function () { console.log('Failed to get parent node data'); })
                    .always(function () { endLoading($that, $node, opts); });
                }
            }
        });

        //get webapi token info
        function getApiToken() {
            var apiTokenObj = JSON.parse(getCookie('AtmsApiToken'));
            var apiToken = apiTokenObj.access_token;
            var tokenType = apiTokenObj.token_type;
            var api_host = apiTokenObj.api_host;

            return {
                apiToken: apiToken,
                tokenType: tokenType,
                api_host: api_host
            };
        };

        function getParameterByName(name, url) {
            if (!url) url = window.location.href;
            name = name.replace(/[\[\]]/g, "\\$&");
            var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
                results = regex.exec(url);
            if (!results) return null;
            if (!results[2]) return '';
            return decodeURIComponent(results[2].replace(/\+/g, " "));
        };

        // bind click event handler for the bottom edge - jacob0702
        $nodeDiv.on('click', '.bottomEdge', function (event) {
            event.stopPropagation();
            var $that = $(this);
            var $node = $that.parent();
            var childrenState = getNodeState($node, 'children');

            var plevel = $node.find('.title:eq(0)').attr('plevel');

            if (childrenState.exist) {
                var $children = $node.closest('tr').siblings(':last');
                if ($children.find('.node:visible').is('.slide')) {
                    return;
                }

                var parentOrgID = $node.find('#OrgID').text()
                // hide the descendant nodes of the specified node
                if (childrenState.visible) {

                    hideChildren($node);

                    expandedOrgList = jQuery.grep(expandedOrgList, function (value) {
                        return value != parentOrgID;
                    });
                } else { // show the descendants

                    if (expandedOrgList.indexOf(parentOrgID) < 0) {
                        expandedOrgList.push(parentOrgID);
                    }

                    showChildren($node);
                }
            } else { // load the new children nodes of the specified node by ajax request
                var nodeId = $that.parent()[0].id;
                if (startLoading($that, $node, opts)) {

                    var apiTokenObj = getApiToken();
                    var antiForgeryToken = $('input[name="__RequestVerificationToken"]').val();
                    var url = $.isFunction(opts.ajaxURL.children) ? opts.ajaxURL.children(nodeData) : opts.ajaxURL.children + nodeId;

                    $.ajax({
                        'type': "get",
                        'url': url,
                        'dataType': 'json',
                        beforeSend: function (xhr) {
                            console.log("__RequestVerificationToken", antiForgeryToken);
                            xhr.setRequestHeader('Authorization', apiTokenObj.tokenType + ' ' + apiTokenObj.apiToken);
                        }
                    })
                        .done(function (data, textStatus, jqXHR) {

                            var parentOrgID = getParameterByName('parentOrgID', url);
                            if (expandedOrgList.indexOf(parentOrgID) < 0) {
                                expandedOrgList.push(parentOrgID);
                            }

                            if ($node.closest('.orgchart').data('inAjax')) {

                                //modified by jacob on May 30
                                if (data.length == 0) return;

                                data = {
                                    'children': createNodes(data, plevel)
                                };
                                if (data.children.length) {
                                    addChildren($node, data, $.extend({
                                    }, opts, {
                                        depth: 0
                                    }));
                                }
                            }
                        })
                        .fail(function (jqXHR, textStatus, errorThrown) {
                            console.log('Failed to get children nodes data');
                        })
                        .always(function () {
                            endLoading($that, $node, opts);
                        });
                }
            }
        });


        function getCookie(cname) {
            var name = cname + "=";
            var ca = document.cookie.split(';');
            for (var i = 0; i < ca.length; i++) {
                var c = ca[i].trim();
                //if (c.indexOf(name) == 0) return c.substring(name.length, c.length);
                if (c.indexOf(name) == 0) return ATMS_EXTRA.decodeCookieValue(c.substring(name.length, c.length));
            }
            return "";
        }

        //added by jacob on May 30
        var createNodes = function (data, plevel) {
            var plevel = plevel + 1;

            var childrenNode = [];
            if (data !== undefined && data !== null && data.length > 0) {
                data.forEach(function (row) {

                    var newNode = createNewNode(row);

                    newNode.pLevel = plevel;
                    childrenNode.push(newNode);
                });

                return childrenNode;
            }
        };

        var ReplaceObj = {
            //顶部节点
            topNodeName: '{{#topNodeName#}}',
            //事业部名称
            dimensionValue: '{{#dimensionValue#}}',
            dimensionValueID: '{{#dimensionValueID#}}',
            //高亮部分统计
            //hightNodeCount: '{{#dimensionValueCount#}}', //比如 '100'
            //hightNodeName: '{{#attributeName#}}',   //比如'分公司'
            //普通维度统计
            attributeID: '{{#attributeID#}}',
            attributeName: '{{#attributeName#}}',  //维度名字,如用户数,覆盖区域,所属行业等
            attributeValueCount: '{{#attributeValueCount#}}',      //对于维度的统计
            attributeType: '{{#attributeType#}}',             //维度的类型区分值,如1:事业部,2:分公司
            firstAttrName: '{{#firstAttr}}',
            secondAttrName: '{{#secondAttr}}',
            firstAttrTitle: '{{#firstAttrTitle}}',
            secondAttrTitle: '{{#secondAttrTitle}}'
        };

        var filterString = function (str, maxLength) {
            if (str != '' && str != undefined && str != null) {
                var apostrophe = '...';
                var twoChar = 'mm';
                var r = /[^\x00-\xff]/g;
                if (str.replace(r, twoChar).length <= maxLength) {
                    return str;
                }
                var m = Math.floor(maxLength / 2);
                for (var i = m; i < str.length; i++) {
                    if (str.substr(0, i).replace(r, twoChar).length >= maxLength) {
                        return str.substr(0, i) + apostrophe;
                    }
                }
                return str;
            }
        };

        var template = {
            topNodeTemplate: $("#dimension-topNode").html(),  //最顶层节点
            nameNodeQuery: $("#dimension-org-node-name-query").html(),  //机构名称 
            nameNodeEdit: $("#dimension-org-node-name-edit").html(),  //机构名称 
            rowNode: $("#dimension-row-node").html() //普通行
        };

        //创建orgchart节点
        var createNewNode = function (row) {

            var temp = [];
            var relationship = '110';
            if (row.hasChildren !== undefined && row.hasChildren) {
                relationship = '111';
            }

            var templateNameNode = template.nameNodeQuery;
            if (opts.hasEditPermission) {
                templateNameNode = template.nameNodeEdit;
            }

            var node = {
                'id': row.dimensionValueID,
                'name': templateNameNode.replaceToEnd(ReplaceObj.dimensionValue, row.dimensionValue)
                    .replaceToEnd(ReplaceObj.dimensionValueID, row.dimensionValueID)
                    .replaceToEnd(ReplaceObj.dimensionValueID, row.dimensionValueID)
                    .replaceToEnd(ReplaceObj.firstAttrName, row.firstAttrName === null ? ' 未指定 ' : filterString(row.firstAttrName, 10))
                    .replaceToEnd(ReplaceObj.secondAttrName, row.secondAttrName === null ? ' 未指定 ' : filterString(row.secondAttrName, 10))
                    .replaceToEnd(ReplaceObj.firstAttrTitle, row.firstAttrName === null ? ' 未指定 ' : row.firstAttrName)
                    .replaceToEnd(ReplaceObj.secondAttrTitle, row.secondAttrName === null ? ' 未指定 ' : row.secondAttrName),

                'title': (function () {

                    row.orgDtoList.forEach(function (item, index) {

                        temp += template.rowNode.replaceToEnd(ReplaceObj.attributeName, item.attributeName)
                                .replaceToEnd(ReplaceObj.attributeValueCount, item.attributeValueCount)
                                .replaceToEnd(ReplaceObj.dimensionValueID, row.dimensionValueID)
                                .replaceToEnd(ReplaceObj.dimensionValue, row.dimensionValue)
                               .replaceToEnd(ReplaceObj.attributeID, item.attributeID)
                               .replaceToEnd(ReplaceObj.attributeType, item.attributeType);
                    });
                    return temp;

                })(),
                'children': [],
                'relationship': relationship
            };

            //特殊处理,如果没有子机构,只显示本身即可
            if (row.hasChildren === undefined || !row.hasChildren) {
                node.title = '';
            }

            return node;
        };

        // event handler for toggle buttons in Hybrid(horizontal + vertical) OrgChart
        $nodeDiv.on('click', '.toggleBtn', function (event) {
            var $this = $(this);
            var $descWrapper = $this.parent().next();
            var $descendants = $descWrapper.find('.node');
            var $children = $descWrapper.children().children('.node');
            if ($children.is('.slide')) {
                return;
            }
            $this.toggleClass('fa-plus-square fa-minus-square');
            if ($descendants.eq(0).is('.slide-up')) {
                $descWrapper.removeClass('hidden');
                repaint($children.get(0));
                $children.addClass('slide').removeClass('slide-up').eq(0).one('transitionend', function () {
                    $children.removeClass('slide');
                });
            } else {
                $descendants.addClass('slide slide-up').eq(0).one('transitionend', function () {
                    $descendants.removeClass('slide');
                    // $descWrapper.addClass('hidden');
                    $descendants.closest('ul').addClass('hidden');
                });
                $descendants.find('.toggleBtn').removeClass('fa-minus-square').addClass('fa-plus-square');
            }
        });

        // bind click event handler for the left and right edges
        $nodeDiv.on('click', '.leftEdge, .rightEdge', function (event) {
            event.stopPropagation();
            var $that = $(this);
            var $node = $that.parent();
            var siblingsState = getNodeState($node, 'siblings');
            if (siblingsState.exist) {
                var $siblings = $node.closest('table').parent().siblings();
                if ($siblings.find('.node:visible').is('.slide')) {
                    return;
                }
                if (opts.toggleSiblingsResp) {
                    var $prevSib = $node.closest('table').parent().prev();
                    var $nextSib = $node.closest('table').parent().next();
                    if ($that.is('.leftEdge')) {
                        if ($prevSib.is('.hidden')) {
                            showSiblings($node, 'left');
                        } else {
                            hideSiblings($node, 'left');
                        }
                    } else {
                        if ($nextSib.is('.hidden')) {
                            showSiblings($node, 'right');
                        } else {
                            hideSiblings($node, 'right');
                        }
                    }
                } else {
                    if (siblingsState.visible) {
                        hideSiblings($node);
                    } else {
                        showSiblings($node);
                    }
                }
            } else {
                // load the new sibling nodes of the specified node by ajax request
                var nodeId = $that.parent()[0].id;
                var url = (getNodeState($node, 'parent').exist) ?
                  ($.isFunction(opts.ajaxURL.siblings) ? opts.ajaxURL.siblings(nodeData) : opts.ajaxURL.siblings + nodeId) :
                  ($.isFunction(opts.ajaxURL.families) ? opts.ajaxURL.families(nodeData) : opts.ajaxURL.families + nodeId);
                if (startLoading($that, $node, opts)) {
                    $.ajax({
                        'url': url, 'dataType': 'json'
                    })
                    .done(function (data, textStatus, jqXHR) {
                        if ($node.closest('.orgchart').data('inAjax')) {
                            if (data.siblings || data.children) {
                                addSiblings($node, data, opts);
                            }
                        }
                    })
                    .fail(function (jqXHR, textStatus, errorThrown) {
                        console.log('Failed to get sibling nodes data');
                    })
                    .always(function () {
                        endLoading($that, $node, opts);
                    });
                }
            }
        });
        if (opts.draggable) {
            $nodeDiv.on('dragstart', function (event) {
                var origEvent = event.originalEvent;
                var isFirefox = /firefox/.test(window.navigator.userAgent.toLowerCase());
                if (isFirefox) {
                    origEvent.dataTransfer.setData('text/html', 'hack for firefox');
                }
                // if users enable zoom or direction options
                if ($nodeDiv.closest('.orgchart').css('transform') !== 'none') {
                    var 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);
                        $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 scale = Math.abs(window.parseFloat((opts.direction === 't2b' || opts.direction === 'b2t') ? transValues[0].slice(transValues[0].indexOf('(') + 1) : transValues[1]));
                    ghostNode.setAttribute('width', $nodeDiv.outerWidth(false));
                    ghostNode.setAttribute('height', $nodeDiv.outerHeight(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 {
                        origEvent.dataTransfer.setDragImage(ghostNode, xOffset, yOffset);
                    }
                }
                var $dragged = $(this);
                var $dragZone = $dragged.closest('.nodes').siblings().eq(0).find('.node:first');
                var $dragHier = $dragged.closest('table').find('.node');
                $dragged.closest('.orgchart')
                  .data('dragged', $dragged)
                  .find('.node').each(function (index, node) {
                      if ($dragHier.index(node) === -1) {
                          if (opts.dropCriteria) {
                              if (opts.dropCriteria($dragged, $dragZone, $(node))) {
                                  $(node).addClass('allowedDrop');
                              }
                          } else {
                              $(node).addClass('allowedDrop');
                          }
                      }
                  });
            })
                  .on('dragover', function (event) {
                      event.preventDefault();
                      if (!$(this).is('.allowedDrop')) {
                          event.originalEvent.dataTransfer.dropEffect = 'none';
                      }
                  })
            .on('dragend', function (event) {
                $(this).closest('.orgchart').find('.allowedDrop').removeClass('allowedDrop');
            })
            .on('drop', function (event) {
                var $dropZone = $(this);

                var $orgchart = $dropZone.closest('.orgchart');
                var $dragged = $orgchart.data('dragged');
                $orgchart.find('.allowedDrop').removeClass('allowedDrop');

                //jacob - rewrite to meeting our business requirements, it happened when drag and ajax loading data together
                var destOrgID = $dropZone.find('#OrgID').text();
                var srcOrgID = $dragged.find('#OrgID').text();

                //use ajax to request webapi to update database
                var apiTokenObj = getApiToken();
                var webApiUrl = apiTokenObj.api_host + constant.webapi.prefix + '/org/updateHierarchy?srcOrgID=' + srcOrgID + '&destOrgID=' + destOrgID;

                $.ajax({
                    'url': webApiUrl,
                    'dataType': 'json',
                    beforeSend: function (xhr) {
                        console.log("Authorization", apiTokenObj.tokenType + ' ' + apiTokenObj.apiToken);
                        xhr.setRequestHeader("Authorization", apiTokenObj.tokenType + ' ' + apiTokenObj.apiToken);
                    }
                })
                .done(function (data, textStatus, jqXHR) {

                    //trigger the children loading of this node
                    //$dropZone.find('.bottomEdge').click();

                    //we encountered many bugs when using above trigger to load children,
                    //then we changed the following way

                    expandedOrgList.push(destOrgID);
                    opts.initOrgChartData('', expandedOrgList);

                    //var $dragZone = $dragged.closest('.nodes').siblings().eq(0).children();
                    //// firstly, deal with the hierarchy of drop zone
                    //if (!$dropZone.closest('tr').siblings().length) { // if the drop zone is a leaf node
                    //    //remove dragged node
                    //    $('#subsidiary-chart').orgchart('removeNodes', $dragged);
                    //} else {
                    //    var dropColspan = parseInt($dropZone.parent().attr('colspan')) + 2;
                    //    var horizontalEdges = '<i class="edge horizontalEdge rightEdge fa"></i><i class="edge horizontalEdge leftEdge fa"></i>';
                    //    $dropZone.closest('tr').next().addBack().children().attr('colspan', dropColspan);
                    //    if (!$dragged.find('.horizontalEdge').length) {
                    //        $dragged.append(horizontalEdges);
                    //    }
                    //    $dropZone.closest('tr').siblings().eq(1).children(':last').before('<td class="leftLine topLine">&nbsp;</td><td class="rightLine topLine">&nbsp;</td>')
                    //      .end().next().append($dragged.closest('table').parent());
                    //    var $dropSibs = $dragged.closest('table').parent().siblings().find('.node:first');
                    //    if ($dropSibs.length === 1) {
                    //        $dropSibs.append(horizontalEdges);
                    //    }
                    //}

                    //// secondly, deal with the hierarchy of dragged node
                    //var dragColspan = parseInt($dragZone.attr('colspan'));
                    //if (dragColspan > 2) {
                    //    $dragZone.attr('colspan', dragColspan - 2)
                    //      .parent().next().children().attr('colspan', dragColspan - 2)
                    //      .end().next().children().slice(1, 3).remove();
                    //    var $dragSibs = $dragZone.parent().siblings('.nodes').children().find('.node:first');
                    //    if ($dragSibs.length === 1) {
                    //        $dragSibs.find('.horizontalEdge').remove();
                    //    }
                    //} else {
                    //    $dragZone.removeAttr('colspan')
                    //      .find('.bottomEdge').remove()
                    //      .end().end().siblings().remove();
                    //}
                    //$orgchart.triggerHandler({
                    //    'type': 'nodedropped.orgchart', 'draggedNode': $dragged, 'dragZone': $dragZone.children(), 'dropZone': $dropZone
                    //});
                })
                .fail(function (jqXHR, textStatus, errorThrown) {
                    console.log('Failed to update dragged hierarchy');
                })
            });
        }
        // allow user to append dom modification after finishing node create of orgchart
        if (opts.createNode) {
            opts.createNode($nodeDiv, nodeData);
        }
        dtd.resolve($nodeDiv);
        return dtd.promise();
    }
    // recursively build the tree
    function buildHierarchy($appendTo, nodeData, level, opts, callback) {
        var $nodeWrapper;
        // Construct the node
        var $childNodes = nodeData.children;
        var hasChildren = $childNodes ? $childNodes.length : false;
        var isVerticalNode = (opts.verticalDepth && (level + 1) >= opts.verticalDepth) ? true : false;
        if (Object.keys(nodeData).length > 1) { // if nodeData has nested structure
            $nodeWrapper = isVerticalNode ? $appendTo : $('<table>');
            if (!isVerticalNode) {
                $appendTo.append($nodeWrapper);
            }
            $.when(createNode(nodeData, level, opts))
            .done(function ($nodeDiv) {
                if (isVerticalNode) {
                    $nodeWrapper.append($nodeDiv);
                } else {
                    $nodeWrapper.append($nodeDiv.wrap('<tr><td' + (hasChildren ? ' colspan="' + $childNodes.length * 2 + '"' : '') + '></td></tr>').closest('tr'));
                }
                if (callback) {
                    callback();
                }
            })
            .fail(function () {
                console.log('Failed to creat node')
            });
        }
        // Construct the inferior nodes and connectiong lines
        if (hasChildren) {
            if (Object.keys(nodeData).length === 1) { // if nodeData is just an array
                $nodeWrapper = $appendTo;
            }
            var isHidden = (level + 1 >= opts.depth || nodeData.collapsed) ? ' hidden' : '';
            var isVerticalLayer = (opts.verticalDepth && (level + 2) >= opts.verticalDepth) ? true : false;

            // draw the line close to parent node
            if (!isVerticalLayer) {
                $nodeWrapper.append('<tr class="lines' + isHidden + '"><td colspan="' + $childNodes.length * 2 + '"><div class="downLine"></div></td></tr>');
            }
            // draw the lines close to children nodes
            var lineLayer = '<tr class="lines' + isHidden + '"><td class="rightLine">&nbsp;</td>';
            for (var i = 1; i < $childNodes.length; i++) {
                lineLayer += '<td class="leftLine topLine">&nbsp;</td><td class="rightLine topLine">&nbsp;</td>';
            }
            lineLayer += '<td class="leftLine">&nbsp;</td></tr>';
            var $nodeLayer;
            if (isVerticalLayer) {
                $nodeLayer = $('<ul>');
                if (isHidden) {
                    $nodeLayer.addClass(isHidden);
                }
                if (level + 2 === opts.verticalDepth) {
                    $nodeWrapper.append('<tr class="verticalNodes' + isHidden + '"><td></td></tr>')
                      .find('.verticalNodes').children().append($nodeLayer);
                } else {
                    $nodeWrapper.append($nodeLayer);
                }
            } else {
                $nodeLayer = $('<tr class="nodes' + isHidden + '">');
                $nodeWrapper.append(lineLayer).append($nodeLayer);
            }
            // recurse through children nodes
            $.each($childNodes, function () {
                var $nodeCell = isVerticalLayer ? $('<li>') : $('<td colspan="2">');
                $nodeLayer.append($nodeCell);
                buildHierarchy($nodeCell, this, level + 1, opts, callback);
            });
        }
    }

    // build the child nodes of specific node
    function buildChildNode($appendTo, nodeData, opts, callback) {
        var opts = opts || $appendTo.closest('.orgchart').data('options');
        var data = nodeData.children || nodeData.siblings;
        $appendTo.find('td:first').attr('colspan', data.length * 2);
        buildHierarchy($appendTo, { 'children': data }, 0, opts, callback);
    }
    // exposed method
    function addChildren($node, data, opts) {
        var opts = opts || $node.closest('.orgchart').data('options');
        var count = 0;
        buildChildNode.call($node.closest('.orgchart').parent(), $node.closest('table'), data, opts, function () {
            if (++count === data.children.length) {
                if (!$node.children('.bottomEdge').length) {
                    $node.append('<i class="edge verticalEdge bottomEdge fa"></i>');
                }
                if (!$node.find('.symbol').length) {
                    $node.children('.title').prepend('<i class="fa ' + opts.parentNodeSymbol + ' symbol"></i>');
                }
                showChildren($node);
            }
        });
    }

    // build the parent node of specific node
    function buildParentNode($currentRoot, nodeData, opts, callback) {
        var that = this;
        var $table = $('<table>');
        nodeData.relationship = nodeData.relationship || '001';
        $.when(createNode(nodeData, 0, opts || $currentRoot.closest('.orgchart').data('options')))
          .done(function ($nodeDiv) {
              $table.append($nodeDiv.removeClass('slide-up').addClass('slide-down').wrap('<tr class="hidden"><td colspan="2"></td></tr>').closest('tr'));
              $table.append('<tr class="lines hidden"><td colspan="2"><div class="downLine"></div></td></tr>');
              var linesRow = '<td class="rightLine">&nbsp;</td><td class="leftLine">&nbsp;</td>';
              $table.append('<tr class="lines hidden">' + linesRow + '</tr>');
              var $oc = that.children('.orgchart');
              $oc.prepend($table)
                .children('table:first').append('<tr class="nodes"><td colspan="2"></td></tr>')
                .children('tr:last').children().append($oc.children('table').last());
              callback();
          })
          .fail(function () {
              console.log('Failed to create parent node');
          });
    }

    // exposed method
    function addParent($currentRoot, data, opts) {
        buildParentNode.call(this, $currentRoot, data, opts, function () {
            if (!$currentRoot.children('.topEdge').length) {
                $currentRoot.children('.title').after('<i class="edge verticalEdge topEdge fa"></i>');
            }
            showParent($currentRoot);
        });
    }

    // subsequent processing of build sibling nodes
    function complementLine($oneSibling, siblingCount, existingSibligCount) {
        var lines = '';
        for (var i = 0; i < existingSibligCount; i++) {
            lines += '<td class="leftLine topLine">&nbsp;</td><td class="rightLine topLine">&nbsp;</td>';
        }
        $oneSibling.parent().prevAll('tr:gt(0)').children().attr('colspan', siblingCount * 2)
          .end().next().children(':first').after(lines);
    }

    // build the sibling nodes of specific node
    function buildSiblingNode($nodeChart, nodeData, opts, callback) {
        var opts = opts || $nodeChart.closest('.orgchart').data('options');
        var newSiblingCount = nodeData.siblings ? nodeData.siblings.length : nodeData.children.length;
        var existingSibligCount = $nodeChart.parent().is('td') ? $nodeChart.closest('tr').children().length : 1;
        var siblingCount = existingSibligCount + newSiblingCount;
        var insertPostion = (siblingCount > 1) ? Math.floor(siblingCount / 2 - 1) : 0;
        // just build the sibling nodes for the specific node
        if ($nodeChart.parent().is('td')) {
            var $parent = $nodeChart.closest('tr').prevAll('tr:last');
            $nodeChart.closest('tr').prevAll('tr:lt(2)').remove();
            var childCount = 0;
            buildChildNode.call($nodeChart.closest('.orgchart').parent(), $nodeChart.parent().closest('table'), nodeData, opts, function () {
                if (++childCount === newSiblingCount) {
                    var $siblingTds = $nodeChart.parent().closest('table').children('tr:last').children('td');
                    if (existingSibligCount > 1) {
                        complementLine($siblingTds.eq(0).before($nodeChart.closest('td').siblings().addBack().unwrap()), siblingCount, existingSibligCount);
                        $siblingTds.addClass('hidden').find('.node').addClass('slide-left');
                    } else {
                        complementLine($siblingTds.eq(insertPostion).after($nodeChart.closest('td').unwrap()), siblingCount, 1);
                        $siblingTds.not(':eq(' + insertPostion + 1 + ')').addClass('hidden')
                          .slice(0, insertPostion).find('.node').addClass('slide-right')
                          .end().end().slice(insertPostion).find('.node').addClass('slide-left');
                    }
                    callback();
                }
            });
        } else { // build the sibling nodes and parent node for the specific ndoe
            var nodeCount = 0;
            buildHierarchy($nodeChart.closest('.orgchart'), nodeData, 0, opts, function () {
                if (++nodeCount === siblingCount) {
                    complementLine($nodeChart.next().children('tr:last')
                      .children().eq(insertPostion).after($('<td colspan="2">')
                      .append($nodeChart)), siblingCount, 1);
                    $nodeChart.closest('tr').siblings().eq(0).addClass('hidden').find('.node').addClass('slide-down');
                    $nodeChart.parent().siblings().addClass('hidden')
                      .slice(0, insertPostion).find('.node').addClass('slide-right')
                      .end().end().slice(insertPostion).find('.node').addClass('slide-left');
                    callback();
                }
            });
        }
    }

    function addSiblings($node, data, opts) {
        buildSiblingNode.call($node.closest('.orgchart').parent(), $node.closest('table'), data, opts, function () {
            $node.closest('.nodes').data('siblingsLoaded', true);
            if (!$node.children('.leftEdge').length) {
                $node.children('.topEdge').after('<i class="edge horizontalEdge rightEdge fa"></i><i class="edge horizontalEdge leftEdge fa"></i>');
            }
            showSiblings($node);
        });
    }

    function removeNodes($node) {
        var $parent = $node.closest('table').parent();
        var $sibs = $parent.parent().siblings();
        if ($parent.is('td')) {
            if (getNodeState($node, 'siblings').exist) {
                $sibs.eq(2).children('.topLine:lt(2)').remove();
                $sibs.slice(0, 2).children().attr('colspan', $sibs.eq(2).children().length);
                $parent.remove();
            } else {
                $sibs.eq(0).children().removeAttr('colspan')
                  .find('.bottomEdge').remove()
                  .end().end().siblings().remove();
            }
        } else {
            $parent.add($parent.siblings()).remove();
        }
    }

}));