uccser/cs-field-guide

View on GitHub
csfieldguide/static/interactives/tree-diagram/js/third-party/Treant.js

Summary

Maintainability
F
1 wk
Test Coverage
/*
 * Treant-js
 *
 * Modified by UCCSER:
 * Adds fix for showing arrows on connectors after a pseudo connector.
 * Adds lines 423 and 918 to 924.
 *
 * (c) 2013 Fran Peručić
 * Treant-js may be freely distributed under the MIT license.
 * For all details and documentation:
 * http://fperucic.github.io/treant-js
 *
 * Treant is an open-source JavaScipt library for visualization of tree diagrams.
 * It implements the node positioning algorithm of John Q. Walker II "Positioning nodes for General Trees".
 *
 * References:
 * Emilio Cortegoso Lobato: ECOTree.js v1.0 (October 26th, 2006)
 *
 * Contributors:
 * Fran Peručić, https://github.com/fperucic
 * Dave Goodchild, https://github.com/dlgoodchild
 */

; (function () {
    // Polyfill for IE to use startsWith
    if (!String.prototype.startsWith) {
        String.prototype.startsWith = function (searchString, position) {
            return this.substr(position || 0, searchString.length) === searchString;
        };
    }

    var $ = null;

    var UTIL = {

        /**
         * Directly updates, recursively/deeply, the first object with all properties in the second object
         * @param {object} applyTo
         * @param {object} applyFrom
         * @return {object}
         */
        inheritAttrs: function (applyTo, applyFrom) {
            for (var attr in applyFrom) {
                if (applyFrom.hasOwnProperty(attr)) {
                    if ((applyTo[attr] instanceof Object && applyFrom[attr] instanceof Object) && (typeof applyFrom[attr] !== 'function')) {
                        this.inheritAttrs(applyTo[attr], applyFrom[attr]);
                    }
                    else {
                        applyTo[attr] = applyFrom[attr];
                    }
                }
            }
            return applyTo;
        },

        /**
         * Returns a new object by merging the two supplied objects
         * @param {object} obj1
         * @param {object} obj2
         * @returns {object}
         */
        createMerge: function (obj1, obj2) {
            var newObj = {};
            if (obj1) {
                this.inheritAttrs(newObj, this.cloneObj(obj1));
            }
            if (obj2) {
                this.inheritAttrs(newObj, obj2);
            }
            return newObj;
        },

        /**
         * Takes any number of arguments
         * @returns {*}
         */
        extend: function () {
            if ($) {
                Array.prototype.unshift.apply(arguments, [true, {}]);
                return $.extend.apply($, arguments);
            }
            else {
                return UTIL.createMerge.apply(this, arguments);
            }
        },

        /**
         * @param {object} obj
         * @returns {*}
         */
        cloneObj: function (obj) {
            if (Object(obj) !== obj) {
                return obj;
            }
            var res = new obj.constructor();
            for (var key in obj) {
                if (obj.hasOwnProperty(key)) {
                    res[key] = this.cloneObj(obj[key]);
                }
            }
            return res;
        },

        /**
         * @param {Element} el
         * @param {string} eventType
         * @param {function} handler
         */
        addEvent: function (el, eventType, handler) {
            if ($) {
                $(el).on(eventType + '.treant', handler);
            }
            else if (el.addEventListener) { // DOM Level 2 browsers
                el.addEventListener(eventType, handler, false);
            }
            else if (el.attachEvent) { // IE <= 8
                el.attachEvent('on' + eventType, handler);
            }
            else { // ancient browsers
                el['on' + eventType] = handler;
            }
        },

        /**
         * @param {string} selector
         * @param {boolean} raw
         * @param {Element} parentEl
         * @returns {Element|jQuery}
         */
        findEl: function (selector, raw, parentEl) {
            parentEl = parentEl || document;

            if ($) {
                var $element = $(selector, parentEl);
                return (raw ? $element.get(0) : $element);
            }
            else {
                // todo: getElementsByName()
                // todo: getElementsByTagName()
                // todo: getElementsByTagNameNS()
                if (selector.charAt(0) === '#') {
                    return parentEl.getElementById(selector.substring(1));
                }
                else if (selector.charAt(0) === '.') {
                    var oElements = parentEl.getElementsByClassName(selector.substring(1));
                    return (oElements.length ? oElements[0] : null);
                }

                throw new Error('Unknown container element');
            }
        },

        getOuterHeight: function (element) {
            var nRoundingCompensation = 1;
            if (typeof element.getBoundingClientRect === 'function') {
                return element.getBoundingClientRect().height;
            }
            else if ($) {
                return Math.ceil($(element).outerHeight()) + nRoundingCompensation;
            }
            else {
                return Math.ceil(
                    element.clientHeight
                    + UTIL.getStyle(element, 'border-top-width', true)
                    + UTIL.getStyle(element, 'border-bottom-width', true)
                    + UTIL.getStyle(element, 'padding-top', true)
                    + UTIL.getStyle(element, 'padding-bottom', true)
                    + nRoundingCompensation
                );
            }
        },

        getOuterWidth: function (element) {
            var nRoundingCompensation = 1;
            if (typeof element.getBoundingClientRect === 'function') {
                return element.getBoundingClientRect().width;
            }
            else if ($) {
                return Math.ceil($(element).outerWidth()) + nRoundingCompensation;
            }
            else {
                return Math.ceil(
                    element.clientWidth
                    + UTIL.getStyle(element, 'border-left-width', true)
                    + UTIL.getStyle(element, 'border-right-width', true)
                    + UTIL.getStyle(element, 'padding-left', true)
                    + UTIL.getStyle(element, 'padding-right', true)
                    + nRoundingCompensation
                );
            }
        },

        getStyle: function (element, strCssRule, asInt) {
            var strValue = "";
            if (document.defaultView && document.defaultView.getComputedStyle) {
                strValue = document.defaultView.getComputedStyle(element, '').getPropertyValue(strCssRule);
            }
            else if (element.currentStyle) {
                strCssRule = strCssRule.replace(/\-(\w)/g,
                    function (strMatch, p1) {
                        return p1.toUpperCase();
                    }
                );
                strValue = element.currentStyle[strCssRule];
            }
            //Number(elem.style.width.replace(/[^\d\.\-]/g, ''));
            return (asInt ? parseFloat(strValue) : strValue);
        },

        addClass: function (element, cssClass) {
            if ($) {
                $(element).addClass(cssClass);
            }
            else {
                if (!UTIL.hasClass(element, cssClass)) {
                    if (element.classList) {
                        element.classList.add(cssClass);
                    }
                    else {
                        element.className += " " + cssClass;
                    }
                }
            }
        },

        hasClass: function (element, my_class) {
            return (" " + element.className + " ").replace(/[\n\t]/g, " ").indexOf(" " + my_class + " ") > -1;
        },

        toggleClass: function (element, cls, apply) {
            if ($) {
                $(element).toggleClass(cls, apply);
            }
            else {
                if (apply) {
                    //element.className += " "+cls;
                    element.classList.add(cls);
                }
                else {
                    element.classList.remove(cls);
                }
            }
        },

        setDimensions: function (element, width, height) {
            if ($) {
                $(element).width(width).height(height);
            }
            else {
                element.style.width = width + 'px';
                element.style.height = height + 'px';
            }
        },
        isjQueryAvailable: function () { return (typeof ($) !== 'undefined' && $); }
    };

    /**
     * ImageLoader is used for determining if all the images from the Tree are loaded.
     * Node size (width, height) can be correctly determined only when all inner images are loaded
     */
    var ImageLoader = function () {
        this.reset();
    };

    ImageLoader.prototype = {

        /**
         * @returns {ImageLoader}
         */
        reset: function () {
            this.loading = [];
            return this;
        },

        /**
         * @param {TreeNode} node
         * @returns {ImageLoader}
         */
        processNode: function (node) {
            var aImages = node.nodeDOM.getElementsByTagName('img');

            var i = aImages.length;
            while (i--) {
                this.create(node, aImages[i]);
            }
            return this;
        },

        /**
         * @returns {ImageLoader}
         */
        removeAll: function (img_src) {
            var i = this.loading.length;
            while (i--) {
                if (this.loading[i] === img_src) {
                    this.loading.splice(i, 1);
                }
            }
            return this;
        },

        /**
         * @param {TreeNode} node
         * @param {Element} image
         * @returns {*}
         */
        create: function (node, image) {
            var self = this, source = image.src;

            function imgTrigger() {
                self.removeAll(source);
                node.width = node.nodeDOM.offsetWidth;
                node.height = node.nodeDOM.offsetHeight;
            }

            if (image.src.indexOf('data:') !== 0) {
                this.loading.push(source);

                if (image.complete) {
                    return imgTrigger();
                }

                UTIL.addEvent(image, 'load', imgTrigger);
                UTIL.addEvent(image, 'error', imgTrigger); // handle broken url-s

                // load event is not fired for cached images, force the load event
                image.src += ((image.src.indexOf('?') > 0) ? '&' : '?') + new Date().getTime();
            }
            else {
                imgTrigger();
            }
        },

        /**
         * @returns {boolean}
         */
        isNotLoading: function () {
            return (this.loading.length === 0);
        }
    };

    /**
     * Class: TreeStore
     * TreeStore is used for holding initialized Tree objects
     *  Its purpose is to avoid global variables and enable multiple Trees on the page.
     */
    var TreeStore = {

        store: [],

        /**
         * @param {object} jsonConfig
         * @returns {Tree}
         */
        createTree: function (jsonConfig) {
            var nNewTreeId = this.store.length;
            this.store.push(new Tree(jsonConfig, nNewTreeId));
            return this.get(nNewTreeId);
        },

        /**
         * @param {number} treeId
         * @returns {Tree}
         */
        get: function (treeId) {
            return this.store[treeId];
        },

        /**
         * @param {number} treeId
         * @returns {TreeStore}
         */
        destroy: function (treeId) {
            var tree = this.get(treeId);
            if (tree) {
                tree._R.remove();
                var draw_area = tree.drawArea;

                while (draw_area.firstChild) {
                    draw_area.removeChild(draw_area.firstChild);
                }

                var classes = draw_area.className.split(' '),
                    classes_to_stay = [];

                for (var i = 0; i < classes.length; i++) {
                    var cls = classes[i];
                    if (cls !== 'Treant' && cls !== 'Treant-loaded') {
                        classes_to_stay.push(cls);
                    }
                }
                draw_area.style.overflowY = '';
                draw_area.style.overflowX = '';
                draw_area.className = classes_to_stay.join(' ');

                this.store[treeId] = null;
            }
            return this;
        }
    };

    /**
     * Tree constructor.
     * @param {object} jsonConfig
     * @param {number} treeId
     * @constructor
     */
    var Tree = function (jsonConfig, treeId) {

        /**
         * @param {object} jsonConfig
         * @param {number} treeId
         * @returns {Tree}
         */
        this.reset = function (jsonConfig, treeId) {
            this.initJsonConfig = jsonConfig;
            this.initTreeId = treeId;

            this.id = treeId;

            this.CONFIG = UTIL.extend(Tree.CONFIG, jsonConfig.chart);
            this.STYLE = this.CONFIG['connectors']['style'];
            this.drawArea = UTIL.findEl(this.CONFIG.container, true);
            if (!this.drawArea) {
                throw new Error('Failed to find element by selector "' + this.CONFIG.container + '"');
            }

            UTIL.addClass(this.drawArea, 'Treant');

            // kill of any child elements that may be there
            this.drawArea.innerHTML = '';

            this.imageLoader = new ImageLoader();

            this.nodeDB = new NodeDB(jsonConfig.nodeStructure, this);

            // key store for storing reference to node connectors,
            // key = nodeId where the connector ends
            this.connectionStore = {};

            this.loaded = false;

            this._R = new Raphael(this.drawArea, 100, 100);

            return this;
        };

        /**
         * @returns {Tree}
         */
        this.reload = function () {
            this.reset(this.initJsonConfig, this.initTreeId).redraw();
            return this;
        };

        this.reset(jsonConfig, treeId);
    };

    Tree.prototype = {

        /**
         * @returns {NodeDB}
         */
        getNodeDb: function () {
            return this.nodeDB;
        },

        /**
         * @param {TreeNode} parentTreeNode
         * @param {object} nodeDefinition
         * @returns {TreeNode}
         */
        addNode: function (parentTreeNode, nodeDefinition) {
            this.CONFIG.callback.onBeforeAddNode.apply(this, [parentTreeNode, nodeDefinition]);

            var oNewNode = this.nodeDB.createNode(nodeDefinition, parentTreeNode.id, this);
            oNewNode.createGeometry(this);

            oNewNode.parent().createSwitchGeometry(this);

            this.positionTree();

            this.CONFIG.callback.onAfterAddNode.apply(this, [oNewNode, parentTreeNode, nodeDefinition]);

            return oNewNode;
        },

        /**
         * @returns {Tree}
         */
        redraw: function () {
            this.positionTree();
            return this;
        },

        /**
         * @param {function} callback
         * @returns {Tree}
         */
        positionTree: function (callback) {
            var self = this;

            if (this.imageLoader.isNotLoading()) {
                var root = this.root();

                this.resetLevelData();

                this.firstWalk(root, 0);
                this.secondWalk(root, 0, 0, 0);

                this.positionNodes();

                if (this.CONFIG.animateOnInit) {
                    setTimeout(
                        function () {
                            root.toggleCollapse();
                        },
                        this.CONFIG.animateOnInitDelay
                    );
                }

                if (!this.loaded) {
                    UTIL.addClass(this.drawArea, 'Treant-loaded'); // nodes are hidden until .loaded class is added
                    if (Object.prototype.toString.call(callback) === "[object Function]") {
                        callback(self);
                    }
                    self.CONFIG.callback.onTreeLoaded.apply(self, [root]);
                    this.loaded = true;
                }

            }
            else {
                setTimeout(
                    function () {
                        self.positionTree(callback);
                    }, 10
                );
            }
            return this;
        },

        /**
         * In a first post-order walk, every node of the tree is assigned a preliminary
         * x-coordinate (held in field node->prelim).
         * In addition, internal nodes are given modifiers, which will be used to move their
         * children to the right (held in field node->modifier).
         * @param {TreeNode} node
         * @param {number} level
         * @returns {Tree}
         */
        firstWalk: function (node, level) {
            node.prelim = null;
            node.modifier = null;

            this.setNeighbors(node, level);
            this.calcLevelDim(node, level);

            var leftSibling = node.leftSibling();

            if (node.childrenCount() === 0 || level == this.CONFIG.maxDepth) {
                // set preliminary x-coordinate
                if (leftSibling) {
                    node.prelim = leftSibling.prelim + leftSibling.size() + this.CONFIG.siblingSeparation;
                }
                else {
                    node.prelim = 0;
                }
            }
            else {
                //node is not a leaf,  firstWalk for each child
                for (var i = 0, n = node.childrenCount(); i < n; i++) {
                    this.firstWalk(node.childAt(i), level + 1);
                }

                var midPoint = node.childrenCenter() - node.size() / 2;

                if (leftSibling) {
                    node.prelim = leftSibling.prelim + leftSibling.size() + this.CONFIG.siblingSeparation;
                    node.modifier = node.prelim - midPoint;
                    this.apportion(node, level);
                }
                else {
                    node.prelim = midPoint;
                }

                // handle stacked children positioning
                if (node.stackParent) { // handle the parent of stacked children
                    node.modifier += this.nodeDB.get(node.stackChildren[0]).size() / 2 + node.connStyle.stackIndent;
                }
                else if (node.stackParentId) { // handle stacked children
                    node.prelim = 0;
                }
            }
            return this;
        },

        /*
         * Clean up the positioning of small sibling subtrees.
         * Subtrees of a node are formed independently and
         * placed as close together as possible. By requiring
         * that the subtrees be rigid at the time they are put
         * together, we avoid the undesirable effects that can
         * accrue from positioning nodes rather than subtrees.
         */
        apportion: function (node, level) {
            var firstChild = node.firstChild(),
                firstChildLeftNeighbor = firstChild.leftNeighbor(),
                compareDepth = 1,
                depthToStop = this.CONFIG.maxDepth - level;

            while (firstChild && firstChildLeftNeighbor && compareDepth <= depthToStop) {
                // calculate the position of the firstChild, according to the position of firstChildLeftNeighbor

                var modifierSumRight = 0,
                    modifierSumLeft = 0,
                    leftAncestor = firstChildLeftNeighbor,
                    rightAncestor = firstChild;

                for (var i = 0; i < compareDepth; i++) {
                    leftAncestor = leftAncestor.parent();
                    rightAncestor = rightAncestor.parent();
                    modifierSumLeft += leftAncestor.modifier;
                    modifierSumRight += rightAncestor.modifier;

                    // all the stacked children are oriented towards right so use right variables
                    if (rightAncestor.stackParent !== undefined) {
                        modifierSumRight += rightAncestor.size() / 2;
                    }
                }

                // find the gap between two trees and apply it to subTrees
                // and matching smaller gaps to smaller subtrees

                var totalGap = (firstChildLeftNeighbor.prelim + modifierSumLeft + firstChildLeftNeighbor.size() + this.CONFIG.subTeeSeparation) - (firstChild.prelim + modifierSumRight);

                if (totalGap > 0) {
                    var subtreeAux = node,
                        numSubtrees = 0;

                    // count all the subtrees in the LeftSibling
                    while (subtreeAux && subtreeAux.id !== leftAncestor.id) {
                        subtreeAux = subtreeAux.leftSibling();
                        numSubtrees++;
                    }

                    if (subtreeAux) {
                        var subtreeMoveAux = node,
                            singleGap = totalGap / numSubtrees;

                        while (subtreeMoveAux.id !== leftAncestor.id) {
                            subtreeMoveAux.prelim += totalGap;
                            subtreeMoveAux.modifier += totalGap;

                            totalGap -= singleGap;
                            subtreeMoveAux = subtreeMoveAux.leftSibling();
                        }
                    }
                }

                compareDepth++;

                firstChild = (firstChild.childrenCount() === 0) ?
                    node.leftMost(0, compareDepth) :
                    firstChild = firstChild.firstChild();

                if (firstChild) {
                    firstChildLeftNeighbor = firstChild.leftNeighbor();
                }
            }
        },

        /*
         * During a second pre-order walk, each node is given a
         * final x-coordinate by summing its preliminary
         * x-coordinate and the modifiers of all the node's
         * ancestors.  The y-coordinate depends on the height of
         * the tree.  (The roles of x and y are reversed for
         * RootOrientations of EAST or WEST.)
         */
        secondWalk: function (node, level, X, Y) {
            if (level > this.CONFIG.maxDepth) {
                return;
            }

            var xTmp = node.prelim + X,
                yTmp = Y, align = this.CONFIG.nodeAlign,
                orient = this.CONFIG.rootOrientation,
                levelHeight, nodesizeTmp;

            if (orient === 'NORTH' || orient === 'SOUTH') {
                levelHeight = this.levelMaxDim[level].height;
                nodesizeTmp = node.height;
                if (node.pseudo) {
                    node.height = levelHeight;
                } // assign a new size to pseudo nodes
            }
            else if (orient === 'WEST' || orient === 'EAST') {
                levelHeight = this.levelMaxDim[level].width;
                nodesizeTmp = node.width;
                if (node.pseudo) {
                    node.width = levelHeight;
                } // assign a new size to pseudo nodes
            }

            node.X = xTmp;

            if (node.pseudo) { // pseudo nodes need to be properly aligned, otherwise position is not correct in some examples
                if (orient === 'NORTH' || orient === 'WEST') {
                    node.Y = yTmp; // align "BOTTOM"
                }
                else if (orient === 'SOUTH' || orient === 'EAST') {
                    node.Y = (yTmp + (levelHeight - nodesizeTmp)); // align "TOP"
                }

            } else {
                node.Y = (align === 'CENTER') ? (yTmp + (levelHeight - nodesizeTmp) / 2) :
                    (align === 'TOP') ? (yTmp + (levelHeight - nodesizeTmp)) :
                        yTmp;
            }

            if (orient === 'WEST' || orient === 'EAST') {
                var swapTmp = node.X;
                node.X = node.Y;
                node.Y = swapTmp;
            }

            if (orient === 'SOUTH') {
                node.Y = -node.Y - nodesizeTmp;
            }
            else if (orient === 'EAST') {
                node.X = -node.X - nodesizeTmp;
            }

            if (node.childrenCount() !== 0) {
                if (node.id === 0 && this.CONFIG.hideRootNode) {
                    // ako je root node Hiden onda nemoj njegovu dijecu pomaknut po Y osi za Level separation, neka ona budu na vrhu
                    this.secondWalk(node.firstChild(), level + 1, X + node.modifier, Y);
                }
                else {
                    this.secondWalk(node.firstChild(), level + 1, X + node.modifier, Y + levelHeight + this.CONFIG.levelSeparation);
                }
            }

            if (node.rightSibling()) {
                this.secondWalk(node.rightSibling(), level, X, Y);
            }
        },

        /**
         * position all the nodes, center the tree in center of its container
         * 0,0 coordinate is in the upper left corner
         * @returns {Tree}
         */
        positionNodes: function () {
            var self = this,
                treeSize = {
                    x: self.nodeDB.getMinMaxCoord('X', null, null),
                    y: self.nodeDB.getMinMaxCoord('Y', null, null)
                },

                treeWidth = treeSize.x.max - treeSize.x.min,
                treeHeight = treeSize.y.max - treeSize.y.min,

                treeCenter = {
                    x: treeSize.x.max - treeWidth / 2,
                    y: treeSize.y.max - treeHeight / 2
                };

            this.handleOverflow(treeWidth, treeHeight);

            var
                containerCenter = {
                    x: self.drawArea.clientWidth / 2,
                    y: self.drawArea.clientHeight / 2
                },

                deltaX = containerCenter.x - treeCenter.x,
                deltaY = containerCenter.y - treeCenter.y,

                // all nodes must have positive X or Y coordinates, handle this with offsets
                negOffsetX = ((treeSize.x.min + deltaX) <= 0) ? Math.abs(treeSize.x.min) : 0,
                negOffsetY = ((treeSize.y.min + deltaY) <= 0) ? Math.abs(treeSize.y.min) : 0,
                i, len, node;

            // position all the nodes
            for (i = 0, len = this.nodeDB.db.length; i < len; i++) {

                node = this.nodeDB.get(i);

                self.CONFIG.callback.onBeforePositionNode.apply(self, [node, i, containerCenter, treeCenter]);

                if (node.id === 0 && this.CONFIG.hideRootNode) {
                    self.CONFIG.callback.onAfterPositionNode.apply(self, [node, i, containerCenter, treeCenter]);
                    continue;
                }

                // if the tree is smaller than the draw area, then center the tree within drawing area
                node.X += negOffsetX + ((treeWidth < this.drawArea.clientWidth) ? deltaX : this.CONFIG.padding);
                node.Y += negOffsetY + ((treeHeight < this.drawArea.clientHeight) ? deltaY : this.CONFIG.padding);

                var collapsedParent = node.collapsedParent(),
                    hidePoint = null;

                if (collapsedParent) {
                    // position the node behind the connector point of the parent, so future animations can be visible
                    hidePoint = collapsedParent.connectorPoint(true);
                    node.hide(hidePoint);

                }
                else if (node.positioned) {
                    // node is already positioned,
                    node.show();
                }
                else { // inicijalno stvaranje nodeova, postavi lokaciju
                    node.nodeDOM.style.left = node.X + 'px';
                    node.nodeDOM.style.top = node.Y + 'px';
                    node.positioned = true;
                }

                if (node.id !== 0 && !(node.parent().id === 0 && this.CONFIG.hideRootNode)) {
                    this.setConnectionToParent(node, hidePoint); // skip the root node
                }
                else if (!this.CONFIG.hideRootNode && node.drawLineThrough) {
                    // drawlinethrough is performed for for the root node also
                    node.drawLineThroughMe();
                }

                self.CONFIG.callback.onAfterPositionNode.apply(self, [node, i, containerCenter, treeCenter]);
            }
            return this;
        },

        /**
         * Create Raphael instance, (optionally set scroll bars if necessary)
         * @param {number} treeWidth
         * @param {number} treeHeight
         * @returns {Tree}
         */
        handleOverflow: function (treeWidth, treeHeight) {
            var viewWidth = (treeWidth < this.drawArea.clientWidth) ? this.drawArea.clientWidth : treeWidth + this.CONFIG.padding * 2,
                viewHeight = (treeHeight < this.drawArea.clientHeight) ? this.drawArea.clientHeight : treeHeight + this.CONFIG.padding * 2;

            this._R.setSize(viewWidth, viewHeight);

            if (this.CONFIG.scrollbar === 'resize') {
                UTIL.setDimensions(this.drawArea, viewWidth, viewHeight);
            }
            else if (!UTIL.isjQueryAvailable() || this.CONFIG.scrollbar === 'native') {

                if (this.drawArea.clientWidth < treeWidth) { // is overflow-x necessary
                    this.drawArea.style.overflowX = "auto";
                }

                if (this.drawArea.clientHeight < treeHeight) { // is overflow-y necessary
                    this.drawArea.style.overflowY = "auto";
                }
            }
            // Fancy scrollbar relies heavily on jQuery, so guarding with if ( $ )
            else if (this.CONFIG.scrollbar === 'fancy') {
                var jq_drawArea = $(this.drawArea);
                if (jq_drawArea.hasClass('ps-container')) { // znaci da je 'fancy' vec inicijaliziran, treba updateat
                    jq_drawArea.find('.Treant').css({
                        width: viewWidth,
                        height: viewHeight
                    });

                    jq_drawArea.perfectScrollbar('update');
                }
                else {
                    var mainContainer = jq_drawArea.wrapInner('<div class="Treant"/>'),
                        child = mainContainer.find('.Treant');

                    child.css({
                        width: viewWidth,
                        height: viewHeight
                    });

                    mainContainer.perfectScrollbar();
                }
            } // else this.CONFIG.scrollbar == 'None'

            return this;
        },
        /**
         * @param {TreeNode} treeNode
         * @param {boolean} hidePoint
         * @returns {Tree}
         */
        setConnectionToParent: function (treeNode, hidePoint) {
            var stacked = treeNode.stackParentId,
                connLine,
                parent = (stacked ? this.nodeDB.get(stacked) : treeNode.parent()),

                pathString = hidePoint ?
                    this.getPointPathString(hidePoint) :
                    this.getPathString(parent, treeNode, stacked);

            if (this.connectionStore[treeNode.id]) {
                // connector already exists, update the connector geometry
                connLine = this.connectionStore[treeNode.id];
                this.animatePath(connLine, pathString);
            }
            else {
                connLine = this._R.path(pathString);
                this.connectionStore[treeNode.id] = connLine;

                // don't show connector arrows por pseudo nodes
                if (treeNode.pseudo) {
                    delete parent.connStyle.style['arrow-end'];
                }
                if (parent.pseudo) {
                    delete parent.connStyle.style['arrow-start'];
                }

                connLine.attr(parent.connStyle.style);

                if (treeNode.text) {
                    // If text node, reset to default style.
                    // This fixes bug where connector after a pseudo node
                    // would not show the arrow end.
                    connLine.attr(this.STYLE);
                }

                if (treeNode.drawLineThrough || treeNode.pseudo) {
                    treeNode.drawLineThroughMe(hidePoint);
                }
            }
            treeNode.connector = connLine;
            return this;
        },

        /**
         * Create the path which is represented as a point, used for hiding the connection
         * A path with a leading "_" indicates the path will be hidden
         * See: http://dmitrybaranovskiy.github.io/raphael/reference.html#Paper.path
         * @param {object} hidePoint
         * @returns {string}
         */
        getPointPathString: function (hidePoint) {
            return ["_M", hidePoint.x, ",", hidePoint.y, 'L', hidePoint.x, ",", hidePoint.y, hidePoint.x, ",", hidePoint.y].join(' ');
        },

        /**
         * This method relied on receiving a valid Raphael Paper.path.
         * See: http://dmitrybaranovskiy.github.io/raphael/reference.html#Paper.path
         * A pathString is typically in the format of "M10,20L30,40"
         * @param path
         * @param {string} pathString
         * @returns {Tree}
         */
        animatePath: function (path, pathString) {
            if (path.hidden && pathString.charAt(0) !== "_") { // path will be shown, so show it
                path.show();
                path.hidden = false;
            }

            // See: http://dmitrybaranovskiy.github.io/raphael/reference.html#Element.animate
            path.animate(
                {
                    path: pathString.charAt(0) === "_" ?
                        pathString.substring(1) :
                        pathString // remove the "_" prefix if it exists
                },
                this.CONFIG.animation.connectorsSpeed,
                this.CONFIG.animation.connectorsAnimation,
                function () {
                    if (pathString.charAt(0) === "_") { // animation is hiding the path, hide it at the and of animation
                        path.hide();
                        path.hidden = true;
                    }
                }
            );
            return this;
        },

        /**
         *
         * @param {TreeNode} from_node
         * @param {TreeNode} to_node
         * @param {boolean} stacked
         * @returns {string}
         */
        getPathString: function (from_node, to_node, stacked) {
            var startPoint = from_node.connectorPoint(true),
                endPoint = to_node.connectorPoint(false),
                orientation = this.CONFIG.rootOrientation,
                connType = from_node.connStyle.type,
                P1 = {}, P2 = {};

            if (orientation === 'NORTH' || orientation === 'SOUTH') {
                P1.y = P2.y = (startPoint.y + endPoint.y) / 2;

                P1.x = startPoint.x;
                P2.x = endPoint.x;
            }
            else if (orientation === 'EAST' || orientation === 'WEST') {
                P1.x = P2.x = (startPoint.x + endPoint.x) / 2;

                P1.y = startPoint.y;
                P2.y = endPoint.y;
            }

            // sp, p1, pm, p2, ep == "x,y"
            var sp = startPoint.x + ',' + startPoint.y, p1 = P1.x + ',' + P1.y, p2 = P2.x + ',' + P2.y, ep = endPoint.x + ',' + endPoint.y,
                pm = (P1.x + P2.x) / 2 + ',' + (P1.y + P2.y) / 2, pathString, stackPoint;

            if (stacked) { // STACKED CHILDREN

                stackPoint = (orientation === 'EAST' || orientation === 'WEST') ?
                    endPoint.x + ',' + startPoint.y :
                    startPoint.x + ',' + endPoint.y;

                if (connType === "step" || connType === "straight") {
                    pathString = ["M", sp, 'L', stackPoint, 'L', ep];
                }
                else if (connType === "curve" || connType === "bCurve") {
                    var helpPoint, // used for nicer curve lines
                        indent = from_node.connStyle.stackIndent;

                    if (orientation === 'NORTH') {
                        helpPoint = (endPoint.x - indent) + ',' + (endPoint.y - indent);
                    }
                    else if (orientation === 'SOUTH') {
                        helpPoint = (endPoint.x - indent) + ',' + (endPoint.y + indent);
                    }
                    else if (orientation === 'EAST') {
                        helpPoint = (endPoint.x + indent) + ',' + startPoint.y;
                    }
                    else if (orientation === 'WEST') {
                        helpPoint = (endPoint.x - indent) + ',' + startPoint.y;
                    }
                    pathString = ["M", sp, 'L', helpPoint, 'S', stackPoint, ep];
                }

            }
            else {  // NORMAL CHILDREN
                if (connType === "step") {
                    pathString = ["M", sp, 'L', p1, 'L', p2, 'L', ep];
                }
                else if (connType === "curve") {
                    pathString = ["M", sp, 'C', p1, p2, ep];
                }
                else if (connType === "bCurve") {
                    pathString = ["M", sp, 'Q', p1, pm, 'T', ep];
                }
                else if (connType === "straight") {
                    pathString = ["M", sp, 'L', sp, ep];
                }
            }

            return pathString.join(" ");
        },

        /**
         * Algorithm works from left to right, so previous processed node will be left neighbour of the next node
         * @param {TreeNode} node
         * @param {number} level
         * @returns {Tree}
         */
        setNeighbors: function (node, level) {
            node.leftNeighborId = this.lastNodeOnLevel[level];
            if (node.leftNeighborId) {
                node.leftNeighbor().rightNeighborId = node.id;
            }
            this.lastNodeOnLevel[level] = node.id;
            return this;
        },

        /**
         * Used for calculation of height and width of a level (level dimensions)
         * @param {TreeNode} node
         * @param {number} level
         * @returns {Tree}
         */
        calcLevelDim: function (node, level) { // root node is on level 0
            this.levelMaxDim[level] = {
                width: Math.max(this.levelMaxDim[level] ? this.levelMaxDim[level].width : 0, node.width),
                height: Math.max(this.levelMaxDim[level] ? this.levelMaxDim[level].height : 0, node.height)
            };
            return this;
        },

        /**
         * @returns {Tree}
         */
        resetLevelData: function () {
            this.lastNodeOnLevel = [];
            this.levelMaxDim = [];
            return this;
        },

        /**
         * @returns {TreeNode}
         */
        root: function () {
            return this.nodeDB.get(0);
        }
    };

    /**
     * NodeDB is used for storing the nodes. Each tree has its own NodeDB.
     * @param {object} nodeStructure
     * @param {Tree} tree
     * @constructor
     */
    var NodeDB = function (nodeStructure, tree) {
        this.reset(nodeStructure, tree);
    };

    NodeDB.prototype = {

        /**
         * @param {object} nodeStructure
         * @param {Tree} tree
         * @returns {NodeDB}
         */
        reset: function (nodeStructure, tree) {

            this.db = [];

            var self = this;

            /**
             * @param {object} node
             * @param {number} parentId
             */
            function iterateChildren(node, parentId) {
                var newNode = self.createNode(node, parentId, tree, null);

                if (node.children) {
                    // pseudo node is used for descending children to the next level
                    if (node.childrenDropLevel && node.childrenDropLevel > 0) {
                        while (node.childrenDropLevel--) {
                            // pseudo node needs to inherit the connection style from its parent for continuous connectors
                            var connStyle = UTIL.cloneObj(newNode.connStyle);
                            newNode = self.createNode('pseudo', newNode.id, tree, null);
                            newNode.connStyle = connStyle;
                            newNode.children = [];
                        }
                    }

                    var stack = (node.stackChildren && !self.hasGrandChildren(node)) ? newNode.id : null;

                    // children are positioned on separate levels, one beneath the other
                    if (stack !== null) {
                        newNode.stackChildren = [];
                    }

                    for (var i = 0, len = node.children.length; i < len; i++) {
                        if (stack !== null) {
                            newNode = self.createNode(node.children[i], newNode.id, tree, stack);
                            if ((i + 1) < len) {
                                // last node cant have children
                                newNode.children = [];
                            }
                        }
                        else {
                            iterateChildren(node.children[i], newNode.id);
                        }
                    }
                }
            }

            if (tree.CONFIG.animateOnInit) {
                nodeStructure.collapsed = true;
            }

            iterateChildren(nodeStructure, -1); // root node

            this.createGeometries(tree);

            return this;
        },

        /**
         * @param {Tree} tree
         * @returns {NodeDB}
         */
        createGeometries: function (tree) {
            var i = this.db.length;

            while (i--) {
                this.get(i).createGeometry(tree);
            }
            return this;
        },

        /**
         * @param {number} nodeId
         * @returns {TreeNode}
         */
        get: function (nodeId) {
            return this.db[nodeId]; // get TreeNode by ID
        },

        /**
         * @param {function} callback
         * @returns {NodeDB}
         */
        walk: function (callback) {
            var i = this.db.length;

            while (i--) {
                callback.apply(this, [this.get(i)]);
            }
            return this;
        },

        /**
         *
         * @param {object} nodeStructure
         * @param {number} parentId
         * @param {Tree} tree
         * @param {number} stackParentId
         * @returns {TreeNode}
         */
        createNode: function (nodeStructure, parentId, tree, stackParentId) {
            var node = new TreeNode(nodeStructure, this.db.length, parentId, tree, stackParentId);

            this.db.push(node);

            // skip root node (0)
            if (parentId >= 0) {
                var parent = this.get(parentId);

                // todo: refactor into separate private method
                if (nodeStructure.position) {
                    if (nodeStructure.position === 'left') {
                        parent.children.push(node.id);
                    }
                    else if (nodeStructure.position === 'right') {
                        parent.children.splice(0, 0, node.id);
                    }
                    else if (nodeStructure.position === 'center') {
                        parent.children.splice(Math.floor(parent.children.length / 2), 0, node.id);
                    }
                    else {
                        // edge case when there's only 1 child
                        var position = parseInt(nodeStructure.position);
                        if (parent.children.length === 1 && position > 0) {
                            parent.children.splice(0, 0, node.id);
                        }
                        else {
                            parent.children.splice(
                                Math.max(position, parent.children.length - 1),
                                0, node.id
                            );
                        }
                    }
                }
                else {
                    parent.children.push(node.id);
                }
            }

            if (stackParentId) {
                this.get(stackParentId).stackParent = true;
                this.get(stackParentId).stackChildren.push(node.id);
            }

            return node;
        },

        getMinMaxCoord: function (dim, parent, MinMax) { // used for getting the dimensions of the tree, dim = 'X' || 'Y'
            // looks for min and max (X and Y) within the set of nodes
            parent = parent || this.get(0);

            MinMax = MinMax || { // start with root node dimensions
                min: parent[dim],
                max: parent[dim] + ((dim === 'X') ? parent.width : parent.height)
            };

            var i = parent.childrenCount();

            while (i--) {
                var node = parent.childAt(i),
                    maxTest = node[dim] + ((dim === 'X') ? node.width : node.height),
                    minTest = node[dim];

                if (maxTest > MinMax.max) {
                    MinMax.max = maxTest;
                }
                if (minTest < MinMax.min) {
                    MinMax.min = minTest;
                }

                this.getMinMaxCoord(dim, node, MinMax);
            }
            return MinMax;
        },

        /**
         * @param {object} nodeStructure
         * @returns {boolean}
         */
        hasGrandChildren: function (nodeStructure) {
            var i = nodeStructure.children.length;
            while (i--) {
                if (nodeStructure.children[i].children && nodeStructure.children[i].children.length > 0) {
                    return true;
                }
            }
            return false;
        }
    };

    /**
     * TreeNode constructor.
     * @param {object} nodeStructure
     * @param {number} id
     * @param {number} parentId
     * @param {Tree} tree
     * @param {number} stackParentId
     * @constructor
     */
    var TreeNode = function (nodeStructure, id, parentId, tree, stackParentId) {
        this.reset(nodeStructure, id, parentId, tree, stackParentId);
    };

    TreeNode.prototype = {

        /**
         * @param {object} nodeStructure
         * @param {number} id
         * @param {number} parentId
         * @param {Tree} tree
         * @param {number} stackParentId
         * @returns {TreeNode}
         */
        reset: function (nodeStructure, id, parentId, tree, stackParentId) {
            this.id = id;
            this.parentId = parentId;
            this.treeId = tree.id;

            this.prelim = 0;
            this.modifier = 0;
            this.leftNeighborId = null;

            this.stackParentId = stackParentId;

            // pseudo node is a node with width=height=0, it is invisible, but necessary for the correct positioning of the tree
            this.pseudo = nodeStructure === 'pseudo' || nodeStructure['pseudo']; // todo: surely if nodeStructure is a scalar then the rest will error:

            this.meta = nodeStructure.meta || {};
            this.image = nodeStructure.image || null;

            this.link = UTIL.createMerge(tree.CONFIG.node.link, nodeStructure.link);

            this.connStyle = UTIL.createMerge(tree.CONFIG.connectors, nodeStructure.connectors);
            this.connector = null;

            this.drawLineThrough = nodeStructure.drawLineThrough === false ? false : (nodeStructure.drawLineThrough || tree.CONFIG.node.drawLineThrough);

            this.collapsable = nodeStructure.collapsable === false ? false : (nodeStructure.collapsable || tree.CONFIG.node.collapsable);
            this.collapsed = nodeStructure.collapsed;

            this.text = nodeStructure.text;

            // '.node' DIV
            this.nodeInnerHTML = nodeStructure.innerHTML;
            this.nodeHTMLclass = (tree.CONFIG.node.HTMLclass ? tree.CONFIG.node.HTMLclass : '') + // globally defined class for the nodex
                (nodeStructure.HTMLclass ? (' ' + nodeStructure.HTMLclass) : '');       // + specific node class

            this.nodeHTMLid = nodeStructure.HTMLid;

            this.children = [];

            return this;
        },

        /**
         * @returns {Tree}
         */
        getTree: function () {
            return TreeStore.get(this.treeId);
        },

        /**
         * @returns {object}
         */
        getTreeConfig: function () {
            return this.getTree().CONFIG;
        },

        /**
         * @returns {NodeDB}
         */
        getTreeNodeDb: function () {
            return this.getTree().getNodeDb();
        },

        /**
         * @param {number} nodeId
         * @returns {TreeNode}
         */
        lookupNode: function (nodeId) {
            return this.getTreeNodeDb().get(nodeId);
        },

        /**
         * @returns {Tree}
         */
        Tree: function () {
            return TreeStore.get(this.treeId);
        },

        /**
         * @param {number} nodeId
         * @returns {TreeNode}
         */
        dbGet: function (nodeId) {
            return this.getTreeNodeDb().get(nodeId);
        },

        /**
         * Returns the width of the node
         * @returns {float}
         */
        size: function () {
            var orientation = this.getTreeConfig().rootOrientation;

            if (this.pseudo) {
                // prevents separating the subtrees
                return (-this.getTreeConfig().subTeeSeparation);
            }

            if (orientation === 'NORTH' || orientation === 'SOUTH') {
                return this.width;
            }
            else if (orientation === 'WEST' || orientation === 'EAST') {
                return this.height;
            }
        },

        /**
         * @returns {number}
         */
        childrenCount: function () {
            return ((this.collapsed || !this.children) ? 0 : this.children.length);
        },

        /**
         * @param {number} index
         * @returns {TreeNode}
         */
        childAt: function (index) {
            return this.dbGet(this.children[index]);
        },

        /**
         * @returns {TreeNode}
         */
        firstChild: function () {
            return this.childAt(0);
        },

        /**
         * @returns {TreeNode}
         */
        lastChild: function () {
            return this.childAt(this.children.length - 1);
        },

        /**
         * @returns {TreeNode}
         */
        parent: function () {
            return this.lookupNode(this.parentId);
        },

        /**
         * @returns {TreeNode}
         */
        leftNeighbor: function () {
            if (this.leftNeighborId) {
                return this.lookupNode(this.leftNeighborId);
            }
        },

        /**
         * @returns {TreeNode}
         */
        rightNeighbor: function () {
            if (this.rightNeighborId) {
                return this.lookupNode(this.rightNeighborId);
            }
        },

        /**
         * @returns {TreeNode}
         */
        leftSibling: function () {
            var leftNeighbor = this.leftNeighbor();

            if (leftNeighbor && leftNeighbor.parentId === this.parentId) {
                return leftNeighbor;
            }
        },

        /**
         * @returns {TreeNode}
         */
        rightSibling: function () {
            var rightNeighbor = this.rightNeighbor();

            if (rightNeighbor && rightNeighbor.parentId === this.parentId) {
                return rightNeighbor;
            }
        },

        /**
         * @returns {number}
         */
        childrenCenter: function () {
            var first = this.firstChild(),
                last = this.lastChild();

            return (first.prelim + ((last.prelim - first.prelim) + last.size()) / 2);
        },

        /**
         * Find out if one of the node ancestors is collapsed
         * @returns {*}
         */
        collapsedParent: function () {
            var parent = this.parent();
            if (!parent) {
                return false;
            }
            if (parent.collapsed) {
                return parent;
            }
            return parent.collapsedParent();
        },

        /**
         * Returns the leftmost child at specific level, (initial level = 0)
         * @param level
         * @param depth
         * @returns {*}
         */
        leftMost: function (level, depth) {
            if (level >= depth) {
                return this;
            }
            if (this.childrenCount() === 0) {
                return;
            }

            for (var i = 0, n = this.childrenCount(); i < n; i++) {
                var leftmostDescendant = this.childAt(i).leftMost(level + 1, depth);
                if (leftmostDescendant) {
                    return leftmostDescendant;
                }
            }
        },

        // returns start or the end point of the connector line, origin is upper-left
        connectorPoint: function (startPoint) {
            var orient = this.Tree().CONFIG.rootOrientation, point = {};

            if (this.stackParentId) { // return different end point if node is a stacked child
                if (orient === 'NORTH' || orient === 'SOUTH') {
                    orient = 'WEST';
                }
                else if (orient === 'EAST' || orient === 'WEST') {
                    orient = 'NORTH';
                }
            }

            // if pseudo, a virtual center is used
            if (orient === 'NORTH') {
                point.x = (this.pseudo) ? this.X - this.Tree().CONFIG.subTeeSeparation / 2 : this.X + this.width / 2;
                point.y = (startPoint) ? this.Y + this.height : this.Y;
            }
            else if (orient === 'SOUTH') {
                point.x = (this.pseudo) ? this.X - this.Tree().CONFIG.subTeeSeparation / 2 : this.X + this.width / 2;
                point.y = (startPoint) ? this.Y : this.Y + this.height;
            }
            else if (orient === 'EAST') {
                point.x = (startPoint) ? this.X : this.X + this.width;
                point.y = (this.pseudo) ? this.Y - this.Tree().CONFIG.subTeeSeparation / 2 : this.Y + this.height / 2;
            }
            else if (orient === 'WEST') {
                point.x = (startPoint) ? this.X + this.width : this.X;
                point.y = (this.pseudo) ? this.Y - this.Tree().CONFIG.subTeeSeparation / 2 : this.Y + this.height / 2;
            }
            return point;
        },

        /**
         * @returns {string}
         */
        pathStringThrough: function () { // get the geometry of a path going through the node
            var startPoint = this.connectorPoint(true),
                endPoint = this.connectorPoint(false);

            return ["M", startPoint.x + "," + startPoint.y, 'L', endPoint.x + "," + endPoint.y].join(" ");
        },

        /**
         * @param {object} hidePoint
         */
        drawLineThroughMe: function (hidePoint) { // hidepoint se proslijedjuje ako je node sakriven zbog collapsed
            var pathString = hidePoint ?
                this.Tree().getPointPathString(hidePoint) :
                this.pathStringThrough();

            this.lineThroughMe = this.lineThroughMe || this.Tree()._R.path(pathString);

            var line_style = UTIL.cloneObj(this.connStyle.style);

            delete line_style['arrow-start'];
            delete line_style['arrow-end'];

            this.lineThroughMe.attr(line_style);

            if (hidePoint) {
                this.lineThroughMe.hide();
                this.lineThroughMe.hidden = true;
            }
        },

        addSwitchEvent: function (nodeSwitch) {
            var self = this;
            UTIL.addEvent(nodeSwitch, 'click',
                function (e) {
                    e.preventDefault();
                    if (self.getTreeConfig().callback.onBeforeClickCollapseSwitch.apply(self, [nodeSwitch, e]) === false) {
                        return false;
                    }

                    self.toggleCollapse();

                    self.getTreeConfig().callback.onAfterClickCollapseSwitch.apply(self, [nodeSwitch, e]);
                }
            );
        },

        /**
         * @returns {TreeNode}
         */
        collapse: function () {
            if (!this.collapsed) {
                this.toggleCollapse();
            }
            return this;
        },

        /**
         * @returns {TreeNode}
         */
        expand: function () {
            if (this.collapsed) {
                this.toggleCollapse();
            }
            return this;
        },

        /**
         * @returns {TreeNode}
         */
        toggleCollapse: function () {
            var oTree = this.getTree();

            if (!oTree.inAnimation) {
                oTree.inAnimation = true;

                this.collapsed = !this.collapsed; // toggle the collapse at each click
                UTIL.toggleClass(this.nodeDOM, 'collapsed', this.collapsed);

                oTree.positionTree();

                var self = this;

                setTimeout(
                    function () { // set the flag after the animation
                        oTree.inAnimation = false;
                        oTree.CONFIG.callback.onToggleCollapseFinished.apply(oTree, [self, self.collapsed]);
                    },
                    (oTree.CONFIG.animation.nodeSpeed > oTree.CONFIG.animation.connectorsSpeed) ?
                        oTree.CONFIG.animation.nodeSpeed :
                        oTree.CONFIG.animation.connectorsSpeed
                );
            }
            return this;
        },

        hide: function (collapse_to_point) {
            collapse_to_point = collapse_to_point || false;

            var bCurrentState = this.hidden;
            this.hidden = true;

            this.nodeDOM.style.overflow = 'hidden';

            var tree = this.getTree(),
                config = this.getTreeConfig(),
                oNewState = {
                    opacity: 0
                };

            if (collapse_to_point) {
                oNewState.left = collapse_to_point.x;
                oNewState.top = collapse_to_point.y;
            }

            // if parent was hidden in initial configuration, position the node behind the parent without animations
            if (!this.positioned || bCurrentState) {
                this.nodeDOM.style.visibility = 'hidden';
                if ($) {
                    $(this.nodeDOM).css(oNewState);
                }
                else {
                    this.nodeDOM.style.left = oNewState.left + 'px';
                    this.nodeDOM.style.top = oNewState.top + 'px';
                }
                this.positioned = true;
            }
            else {
                // todo: fix flashy bug when a node is manually hidden and tree.redraw is called.
                if ($) {
                    $(this.nodeDOM).animate(
                        oNewState, config.animation.nodeSpeed, config.animation.nodeAnimation,
                        function () {
                            this.style.visibility = 'hidden';
                        }
                    );
                }
                else {
                    this.nodeDOM.style.transition = 'all ' + config.animation.nodeSpeed + 'ms ease';
                    this.nodeDOM.style.transitionProperty = 'opacity, left, top';
                    this.nodeDOM.style.opacity = oNewState.opacity;
                    this.nodeDOM.style.left = oNewState.left + 'px';
                    this.nodeDOM.style.top = oNewState.top + 'px';
                    this.nodeDOM.style.visibility = 'hidden';
                }
            }

            // animate the line through node if the line exists
            if (this.lineThroughMe) {
                var new_path = tree.getPointPathString(collapse_to_point);
                if (bCurrentState) {
                    // update without animations
                    this.lineThroughMe.attr({ path: new_path });
                }
                else {
                    // update with animations
                    tree.animatePath(this.lineThroughMe, tree.getPointPathString(collapse_to_point));
                }
            }

            return this;
        },

        /**
         * @returns {TreeNode}
         */
        hideConnector: function () {
            var oTree = this.Tree();
            var oPath = oTree.connectionStore[this.id];
            if (oPath) {
                oPath.animate(
                    { 'opacity': 0 },
                    oTree.CONFIG.animation.connectorsSpeed,
                    oTree.CONFIG.animation.connectorsAnimation
                );
            }
            return this;
        },

        show: function () {
            this.hidden = false;

            this.nodeDOM.style.visibility = 'visible';

            var oTree = this.Tree();

            var oNewState = {
                left: this.X,
                top: this.Y,
                opacity: 1
            },
                config = this.getTreeConfig();

            // if the node was hidden, update opacity and position
            if ($) {
                $(this.nodeDOM).animate(
                    oNewState,
                    config.animation.nodeSpeed, config.animation.nodeAnimation,
                    function () {
                        // $.animate applies "overflow:hidden" to the node, remove it to avoid visual problems
                        this.style.overflow = "";
                    }
                );
            }
            else {
                this.nodeDOM.style.transition = 'all ' + config.animation.nodeSpeed + 'ms ease';
                this.nodeDOM.style.transitionProperty = 'opacity, left, top';
                this.nodeDOM.style.left = oNewState.left + 'px';
                this.nodeDOM.style.top = oNewState.top + 'px';
                this.nodeDOM.style.opacity = oNewState.opacity;
                this.nodeDOM.style.overflow = '';
            }

            if (this.lineThroughMe) {
                this.getTree().animatePath(this.lineThroughMe, this.pathStringThrough());
            }

            return this;
        },

        /**
         * @returns {TreeNode}
         */
        showConnector: function () {
            var oTree = this.Tree();
            var oPath = oTree.connectionStore[this.id];
            if (oPath) {
                oPath.animate(
                    { 'opacity': 1 },
                    oTree.CONFIG.animation.connectorsSpeed,
                    oTree.CONFIG.animation.connectorsAnimation
                );
            }
            return this;
        }
    };


    /**
     * Build a node from the 'text' and 'img' property and return with it.
     *
     * The node will contain all the fields that present under the 'text' property
     * Each field will refer to a css class with name defined as node-{$property_name}
     *
     * Example:
     * The definition:
     *
     *   text: {
     *     desc: "some description",
     *     paragraph: "some text"
     *   }
     *
     * will generate the following elements:
     *
     *   <p class="node-desc">some description</p>
     *   <p class="node-paragraph">some text</p>
     *
     * @Returns the configured node
     */
    TreeNode.prototype.buildNodeFromText = function (node) {
        // IMAGE
        if (this.image) {
            image = document.createElement('img');
            image.src = this.image;
            node.appendChild(image);
        }

        // TEXT
        if (this.text) {
            for (var key in this.text) {
                // adding DATA Attributes to the node
                if (key.startsWith("data-")) {
                    node.setAttribute(key, this.text[key]);
                } else {

                    var textElement = document.createElement(this.text[key].href ? 'a' : 'p');

                    // make an <a> element if required
                    if (this.text[key].href) {
                        textElement.href = this.text[key].href;
                        if (this.text[key].target) {
                            textElement.target = this.text[key].target;
                        }
                    }

                    textElement.className = "node-" + key;
                    textElement.appendChild(document.createTextNode(
                        this.text[key].val ? this.text[key].val :
                            this.text[key] instanceof Object ? "'val' param missing!" : this.text[key]
                    )
                    );

                    node.appendChild(textElement);
                }
            }
        }
        return node;
    };

    /**
     * Build a node from  'nodeInnerHTML' property that defines an existing HTML element, referenced by it's id, e.g: #someElement
     * Change the text in the passed node to 'Wrong ID selector' if the referenced element does ot exist,
     * return with a cloned and configured node otherwise
     *
     * @Returns node the configured node
     */
    TreeNode.prototype.buildNodeFromHtml = function (node) {
        // get some element by ID and clone its structure into a node
        if (this.nodeInnerHTML.charAt(0) === "#") {
            var elem = document.getElementById(this.nodeInnerHTML.substring(1));
            if (elem) {
                node = elem.cloneNode(true);
                node.id += "-clone";
                node.className += " node";
            }
            else {
                node.innerHTML = "<b> Wrong ID selector </b>";
            }
        }
        else {
            // insert your custom HTML into a node
            node.innerHTML = this.nodeInnerHTML;
        }
        return node;
    };

    /**
     * @param {Tree} tree
     */
    TreeNode.prototype.createGeometry = function (tree) {
        if (this.id === 0 && tree.CONFIG.hideRootNode) {
            this.width = 0;
            this.height = 0;
            return;
        }

        var drawArea = tree.drawArea,
            image,

            /////////// CREATE NODE //////////////
            node = document.createElement(this.link.href ? 'a' : 'div');

        node.className = (!this.pseudo) ? TreeNode.CONFIG.nodeHTMLclass : 'pseudo';
        if (this.nodeHTMLclass && !this.pseudo) {
            node.className += ' ' + this.nodeHTMLclass;
        }

        if (this.nodeHTMLid) {
            node.id = this.nodeHTMLid;
        }

        if (this.link.href) {
            node.href = this.link.href;
            node.target = this.link.target;
        }

        if ($) {
            $(node).data('treenode', this);
        }
        else {
            node.data = {
                'treenode': this
            };
        }

        /////////// BUILD NODE CONTENT //////////////
        if (!this.pseudo) {
            node = this.nodeInnerHTML ? this.buildNodeFromHtml(node) : this.buildNodeFromText(node);

            // handle collapse switch
            if (this.collapsed || (this.collapsable && this.childrenCount() && !this.stackParentId)) {
                this.createSwitchGeometry(tree, node);
            }
        }

        tree.CONFIG.callback.onCreateNode.apply(tree, [this, node]);

        /////////// APPEND all //////////////
        drawArea.appendChild(node);

        this.width = node.offsetWidth;
        this.height = node.offsetHeight;

        this.nodeDOM = node;

        tree.imageLoader.processNode(this);
    };

    /**
     * @param {Tree} tree
     * @param {Element} nodeEl
     */
    TreeNode.prototype.createSwitchGeometry = function (tree, nodeEl) {
        nodeEl = nodeEl || this.nodeDOM;

        // safe guard and check to see if it has a collapse switch
        var nodeSwitchEl = UTIL.findEl('.collapse-switch', true, nodeEl);
        if (!nodeSwitchEl) {
            nodeSwitchEl = document.createElement('a');
            nodeSwitchEl.className = "collapse-switch";

            nodeEl.appendChild(nodeSwitchEl);
            this.addSwitchEvent(nodeSwitchEl);
            if (this.collapsed) {
                nodeEl.className += " collapsed";
            }

            tree.CONFIG.callback.onCreateNodeCollapseSwitch.apply(tree, [this, nodeEl, nodeSwitchEl]);
        }
        return nodeSwitchEl;
    };


    // ###########################################
    //      Expose global + default CONFIG params
    // ###########################################


    Tree.CONFIG = {
        maxDepth: 100,
        rootOrientation: 'NORTH', // NORTH || EAST || WEST || SOUTH
        nodeAlign: 'CENTER', // CENTER || TOP || BOTTOM
        levelSeparation: 30,
        siblingSeparation: 30,
        subTeeSeparation: 30,

        hideRootNode: false,

        animateOnInit: false,
        animateOnInitDelay: 500,

        padding: 15, // the difference is seen only when the scrollbar is shown
        scrollbar: 'native', // "native" || "fancy" || "None" (PS: "fancy" requires jquery and perfect-scrollbar)

        connectors: {
            type: 'curve', // 'curve' || 'step' || 'straight' || 'bCurve'
            style: {
                stroke: 'black'
            },
            stackIndent: 15
        },

        node: { // each node inherits this, it can all be overridden in node config

            // HTMLclass: 'node',
            // drawLineThrough: false,
            // collapsable: false,
            link: {
                target: '_self'
            }
        },

        animation: { // each node inherits this, it can all be overridden in node config
            nodeSpeed: 450,
            nodeAnimation: 'linear',
            connectorsSpeed: 450,
            connectorsAnimation: 'linear'
        },

        callback: {
            onCreateNode: function (treeNode, treeNodeDom) { }, // this = Tree
            onCreateNodeCollapseSwitch: function (treeNode, treeNodeDom, switchDom) { }, // this = Tree
            onAfterAddNode: function (newTreeNode, parentTreeNode, nodeStructure) { }, // this = Tree
            onBeforeAddNode: function (parentTreeNode, nodeStructure) { }, // this = Tree
            onAfterPositionNode: function (treeNode, nodeDbIndex, containerCenter, treeCenter) { }, // this = Tree
            onBeforePositionNode: function (treeNode, nodeDbIndex, containerCenter, treeCenter) { }, // this = Tree
            onToggleCollapseFinished: function (treeNode, bIsCollapsed) { }, // this = Tree
            onAfterClickCollapseSwitch: function (nodeSwitch, event) { }, // this = TreeNode
            onBeforeClickCollapseSwitch: function (nodeSwitch, event) { }, // this = TreeNode
            onTreeLoaded: function (rootTreeNode) { } // this = Tree
        }
    };

    TreeNode.CONFIG = {
        nodeHTMLclass: 'node'
    };

    // #############################################
    // Makes a JSON chart config out of Array config
    // #############################################

    var JSONconfig = {
        make: function (configArray) {

            var i = configArray.length, node;

            this.jsonStructure = {
                chart: null,
                nodeStructure: null
            };
            //fist loop: find config, find root;
            while (i--) {
                node = configArray[i];
                if (node.hasOwnProperty('container')) {
                    this.jsonStructure.chart = node;
                    continue;
                }

                if (!node.hasOwnProperty('parent') && !node.hasOwnProperty('container')) {
                    this.jsonStructure.nodeStructure = node;
                    node._json_id = 0;
                }
            }

            this.findChildren(configArray);

            return this.jsonStructure;
        },

        findChildren: function (nodes) {
            var parents = [0]; // start with a a root node

            while (parents.length) {
                var parentId = parents.pop(),
                    parent = this.findNode(this.jsonStructure.nodeStructure, parentId),
                    i = 0, len = nodes.length,
                    children = [];

                for (; i < len; i++) {
                    var node = nodes[i];
                    if (node.parent && (node.parent._json_id === parentId)) { // skip config and root nodes

                        node._json_id = this.getID();

                        delete node.parent;

                        children.push(node);
                        parents.push(node._json_id);
                    }
                }

                if (children.length) {
                    parent.children = children;
                }
            }
        },

        findNode: function (node, nodeId) {
            var childrenLen, found;

            if (node._json_id === nodeId) {
                return node;
            }
            else if (node.children) {
                childrenLen = node.children.length;
                while (childrenLen--) {
                    found = this.findNode(node.children[childrenLen], nodeId);
                    if (found) {
                        return found;
                    }
                }
            }
        },

        getID: (
            function () {
                var i = 1;
                return function () {
                    return i++;
                };
            }
        )()
    };

    /**
     * Chart constructor.
     */
    var Treant = function (jsonConfig, callback, jQuery) {
        if (jsonConfig instanceof Array) {
            jsonConfig = JSONconfig.make(jsonConfig);
        }

        // optional
        if (jQuery) {
            $ = jQuery;
        }

        this.tree = TreeStore.createTree(jsonConfig);
        this.tree.positionTree(callback);
    };

    Treant.prototype.destroy = function () {
        TreeStore.destroy(this.tree.id);
    };

    /* expose constructor globally */
    window.Treant = Treant;

})();