PageSourceSearch

https://cdn.baykartech.com/static/assets/treant-js-master/Treant.js

js baykartech.com collected 2026-10-02 05:50:25 UTC 75,385 bytes, 2,171 lines download raw bytes

1/*
2 * Treant-js
3 *
4 * (c) 2013 Fran Peručić
5 * Treant-js may be freely distributed under the MIT license.
6 * For all details and documentation:
7 * http://fperucic.github.io/treant-js
8 *
9 * Treant is an open-source JavaScipt library for visualization of tree diagrams.
10 * It implements the node positioning algorithm of John Q. Walker II "Positioning nodes for General Trees".
11 *
12 * References:
13 * Emilio Cortegoso Lobato: ECOTree.js v1.0 (October 26th, 2006)
14 *
15 * Contributors:
16 * Fran Peručić, https://github.com/fperucic
17 * Dave Goodchild, https://github.com/dlgoodchild
18 */
19
20;( function() {
21    // Polyfill for IE to use startsWith
22    if (!String.prototype.startsWith) {
23        String.prototype.startsWith = function(searchString, position){
24            return this.substr(position || 0, searchString.length) === searchString;
25        };
26    }
27
28    var $ = null;
29
30    var UTIL = {
31
32        /**
33         * Directly updates, recursively/deeply, the first object with all properties in the second object
34         * @param {object} applyTo
35         * @param {object} applyFrom
36         * @return {object}
37         */
38        inheritAttrs: function( applyTo, applyFrom ) {
39            for ( var attr in applyFrom ) {
40                if ( applyFrom.hasOwnProperty( attr ) ) {
41                    if ( ( applyTo[attr] instanceof Object && applyFrom[attr] instanceof Object ) && ( typeof applyFrom[attr] !== 'function' ) ) {
42                        this.inheritAttrs( applyTo[attr], applyFrom[attr] );
43                    }
44                    else {
45                        applyTo[attr] = applyFrom[attr];
46                    }
47                }
48            }
49            return applyTo;
50        },
51
52        /**
53         * Returns career new object by merging the two supplied objects
54         * @param {object} obj1
55         * @param {object} obj2
56         * @returns {object}
57         */
58        createMerge: function( obj1, obj2 ) {
59            var newObj = {};
60            if ( obj1 ) {
61                this.inheritAttrs( newObj, this.cloneObj( obj1 ) );
62            }
63            if ( obj2 ) {
64                this.inheritAttrs( newObj, obj2 );
65            }
66            return newObj;
67        },
68
69        /**
70         * Takes any number of arguments
71         * @returns {*}
72         */
73        extend: function() {
74            if ( $ ) {
75                Array.prototype.unshift.apply( arguments, [true, {}] );
76                return $.extend.apply( $, arguments );
77            }
78            else {
79                return UTIL.createMerge.apply( this, arguments );
80            }
81        },
82
83        /**
84         * @param {object} obj
85         * @returns {*}
86         */
87        cloneObj: function ( obj ) {
88            if ( Object( obj ) !== obj ) {
89                return obj;
90            }
91            var res = new obj.constructor();
92            for ( var key in obj ) {
93                if ( obj.hasOwnProperty(key) ) {
94                    res[key] = this.cloneObj(obj[key]);
95                }
96            }
97            return res;
98        },
99
100        /**
101         * @param {Element} el
102         * @param {string} eventType
103         * @param {function} handler
104         */
105        addEvent: function( el, eventType, handler ) {
106            if ( $ ) {
107                $( el ).on( eventType+'.treant', handler );
108            }
109            else if ( el.addEventListener ) { // DOM Level 2 browsers
110                el.addEventListener( eventType, handler, false );
111            }
112            else if ( el.attachEvent ) { // IE <= 8
113                el.attachEvent( 'on' + eventType, handler );
114            }
115            else { // ancient browsers
116                el['on' + eventType] = handler;
117            }
118        },
119
120        /**
121         * @param {string} selector
122         * @param {boolean} raw
123         * @param {Element} parentEl
124         * @returns {Element|jQuery}
125         */
126        findEl: function( selector, raw, parentEl ) {
127            parentEl = parentEl || document;
128
129            if ( $ ) {
130                var $element = $( selector, parentEl );
131                return ( raw? $element.get( 0 ): $element );
132            }
133            else {
134                // todo: getElementsByName()
135                // todo: getElementsByTagName()
136                // todo: getElementsByTagNameNS()
137                if ( selector.charAt( 0 ) === '#' ) {
138                    return parentEl.getElementById( selector.substring( 1 ) );
139                }
140                else if ( selector.charAt( 0 ) === '.' ) {
141                    var oElements = parentEl.getElementsByClassName( selector.substring( 1 ) );
142                    return ( oElements.length? oElements[0]: null );
143                }
144
145                throw new Error( 'Unknown container element' );
146            }
147        },
148
149        getOuterHeight: function( element ) {
150            var nRoundingCompensation = 1;
151            if ( typeof element.getBoundingClientRect === 'function' ) {
152                return element.getBoundingClientRect().height;
153            }
154            else if ( $ ) {
155                return Math.ceil( $( element ).outerHeight() ) + nRoundingCompensation;
156            }
157            else {
158                return Math.ceil(
159                    element.clientHeight
160                    + UTIL.getStyle( element, 'border-top-width', true )
161                    + UTIL.getStyle( element, 'border-bottom-width', true )
162                    + UTIL.getStyle( element, 'padding-top', true )
163                    + UTIL.getStyle( element, 'padding-bottom', true )
164                    + nRoundingCompensation
165                );
166            }
167        },
168
169        getOuterWidth: function( element ) {
170            var nRoundingCompensation = 1;
171            if ( typeof element.getBoundingClientRect === 'function' ) {
172                return element.getBoundingClientRect().width;
173            }
174            else if ( $ ) {
175                return Math.ceil( $( element ).outerWidth() ) + nRoundingCompensation;
176            }
177            else {
178                return Math.ceil(
179                    element.clientWidth
180                    + UTIL.getStyle( element, 'border-left-width', true )
181                    + UTIL.getStyle( element, 'border-right-width', true )
182                    + UTIL.getStyle( element, 'padding-left', true )
183                    + UTIL.getStyle( element, 'padding-right', true )
184                    + nRoundingCompensation
185                );
186            }
187        },
188
189        getStyle: function( element, strCssRule, asInt ) {
190            var strValue = "";
191            if ( document.defaultView && document.defaultView.getComputedStyle ) {
192                strValue = document.defaultView.getComputedStyle( element, '' ).getPropertyValue( strCssRule );
193            }
194            else if( element.currentStyle ) {
195                strCssRule = strCssRule.replace(/\-(\w)/g,
196                    function (strMatch, p1){
197                        return p1.toUpperCase();
198                    }
199                );
200                strValue = element.currentStyle[strCssRule];
201            }
202            //Number(elem.style.width.replace(/[^\d\.\-]/g, ''));
203            return ( asInt? parseFloat( strValue ): strValue );
204        },
205
206        addClass: function( element, cssClass ) {
207            if ( $ ) {
208                $( element ).addClass( cssClass );
209            }
210            else {
211                if ( !UTIL.hasClass( element, cssClass ) ) {
212                    if ( element.classList ) {
213                        element.classList.add( cssClass );
214                    }
215                    else {
216                        element.className += " "+cssClass;
217                    }
218                }
219            }
220        },
221
222        hasClass: function(element, my_class) {
223            return (" " + element.className + " ").replace(/[\n\t]/g, " ").indexOf(" "+my_class+" ") > -1;
224        },
225
226        toggleClass: function ( element, cls, apply ) {
227            if ( $ ) {
228                $( element ).toggleClass( cls, apply );
229            }
230            else {
231                if ( apply ) {
232                    //element.className += " "+cls;
233                    element.classList.add( cls );
234                }
235                else {
236                    element.classList.remove( cls );
237                }
238            }
239        },
240
241        setDimensions: function( element, width, height ) {
242            if ( $ ) {
243                $( element ).width( width ).height( height );
244            }
245            else {
246                element.style.width = width+'px';
247                element.style.height = height+'px';
248            }
249        },
250        isjQueryAvailable: function() {return(typeof ($) !== 'undefined' && $);},
251    };
252
253    /**
254     * ImageLoader is used for determining if all the images from the Tree are loaded.
255     * Node size (width, height) can be correctly determined only when all inner images are loaded
256     */
257    var ImageLoader = function() {
258        this.reset();
259    };
260
261    ImageLoader.prototype = {
262
263        /**
264         * @returns {ImageLoader}
265         */
266        reset: function() {
267            this.loading = [];
268            return this;
269        },
270
271        /**
272         * @param {TreeNode} node
273         * @returns {ImageLoader}
274         */
275        processNode: function( node ) {
276            var aImages = node.nodeDOM.getElementsByTagName( 'img' );
277
278            var i = aImages.length;
279            while ( i-- ) {
280                this.create( node, aImages[i] );
281            }
282            return this;
283        },
284
285        /**
286         * @returns {ImageLoader}
287         */
288        removeAll: function( img_src ) {
289            var i = this.loading.length;
290            while ( i-- ) {
291                if ( this.loading[i] === img_src ) {
292                    this.loading.splice( i, 1 );
293                }
294            }
295            return this;
296        },
297
298        /**
299         * @param {TreeNode} node
300         * @param {Element} image
301         * @returns {*}
302         */
303        create: function ( node, image ) {
304            var self = this, source = image.src;
305
306            function imgTrigger() {
307                self.removeAll( source );
308                node.width = node.nodeDOM.offsetWidth;
309                node.height = node.nodeDOM.offsetHeight;
310            }
311
312            if ( image.src.indexOf( 'data:' ) !== 0 ) {
313                this.loading.push( source );
314
315                if ( image.complete ) {
316                    return imgTrigger();
317                }
318
319                UTIL.addEvent( image, 'load', imgTrigger );
320                UTIL.addEvent( image, 'error', imgTrigger ); // handle broken url-s
321
322                // load event is not fired for cached images, force the load event
323                image.src += ( ( image.src.indexOf( '?' ) > 0)? '&': '?' ) + new Date().getTime();
324            }
325            else {
326                imgTrigger();
327            }
328        },
329
330        /**
331         * @returns {boolean}
332         */
333        isNotLoading: function() {
334            return ( this.loading.length === 0 );
335        }
336    };
337
338    /**
339     * Class: TreeStore
340     * TreeStore is used for holding initialized Tree objects
341     *  Its purpose is to avoid global variables and enable multiple Trees on the page.
342     */
343    var TreeStore = {
344
345        store: [],
346
347        /**
348         * @param {object} jsonConfig
349         * @returns {Tree}
350         */
351        createTree: function( jsonConfig ) {
352            var nNewTreeId = this.store.length;
353            this.store.push( new Tree( jsonConfig, nNewTreeId ) );
354            return this.get( nNewTreeId );
355        },
356
357        /**
358         * @param {number} treeId
359         * @returns {Tree}
360         */
361        get: function ( treeId ) {
362            return this.store[treeId];
363        },
364
365        /**
366         * @param {number} treeId
367         * @returns {TreeStore}
368         */
369        destroy: function( treeId ) {
370            var tree = this.get( treeId );
371            if ( tree ) {
372                tree._R.remove();
373                var draw_area = tree.drawArea;
374
375                while ( draw_area.firstChild ) {
376                    draw_area.removeChild( draw_area.firstChild );
377                }
378
379                var classes = draw_area.className.split(' '),
380                    classes_to_stay = [];
381
382                for ( var i = 0; i < classes.length; i++ ) {
383                    var cls = classes[i];
384                    if ( cls !== 'Treant' && cls !== 'Treant-loaded' ) {
385                        classes_to_stay.push(cls);
386                    }
387                }
388                draw_area.style.overflowY = '';
389                draw_area.style.overflowX = '';
390                draw_area.className = classes_to_stay.join(' ');
391
392                this.store[treeId] = null;
393            }
394            return this;
395        }
396    };
397
398    /**
399     * Tree constructor.
400     * @param {object} jsonConfig
401     * @param {number} treeId
402     * @constructor
403     */
404    var Tree = function (jsonConfig, treeId ) {
405
406        /**
407         * @param {object} jsonConfig
408         * @param {number} treeId
409         * @returns {Tree}
410         */
411        this.reset = function( jsonConfig, treeId ) {
412            this.initJsonConfig = jsonConfig;
413            this.initTreeId = treeId;
414
415            this.id = treeId;
416
417            this.CONFIG = UTIL.extend( Tree.CONFIG, jsonConfig.chart );
418            this.drawArea = UTIL.findEl( this.CONFIG.container, true );
419            if ( !this.drawArea ) {
420                throw new Error( 'Failed to find element by selector "'+this.CONFIG.container+'"' );
421            }
422
423            UTIL.addClass( this.drawArea, 'Treant' );
424
425            // kill of any child elements that may be there
426            this.drawArea.innerHTML = '';
427
428            this.imageLoader = new ImageLoader();
429
430            this.nodeDB = new NodeDB( jsonConfig.nodeStructure, this );
431
432            // key store for storing reference to node connectors,
433            // key = nodeId where the connector ends
434            this.connectionStore = {};
435
436            this.loaded = false;
437
438            this._R = new Raphael( this.drawArea, 100, 100 );
439
440            return this;
441        };
442
443        /**
444         * @returns {Tree}
445         */
446        this.reload = function() {
447            this.reset( this.initJsonConfig, this.initTreeId ).redraw();
448            return this;
449        };
450
451        this.reset( jsonConfig, treeId );
452    };
453
454    Tree.prototype = {
455
456        /**
457         * @returns {NodeDB}
458         */
459        getNodeDb: function() {
460            return this.nodeDB;
461        },
462
463        /**
464         * @param {TreeNode} parentTreeNode
465         * @param {object} nodeDefinition
466         * @returns {TreeNode}
467         */
468        addNode: function( parentTreeNode, nodeDefinition ) {
469            var dbEntry = this.nodeDB.get( parentTreeNode.id );
470
471            this.CONFIG.callback.onBeforeAddNode.apply( this, [parentTreeNode, nodeDefinition] );
472
473            var oNewNode = this.nodeDB.createNode( nodeDefinition, parentTreeNode.id, this );
474            oNewNode.createGeometry( this );
475
476            oNewNode.parent().createSwitchGeometry( this );
477
478            this.positionTree();
479
480            this.CONFIG.callback.onAfterAddNode.apply( this, [oNewNode, parentTreeNode, nodeDefinition] );
481
482            return oNewNode;
483        },
484
485        /**
486         * @returns {Tree}
487         */
488        redraw: function() {
489            this.positionTree();
490            return this;
491        },
492
493        /**
494         * @param {function} callback
495         * @returns {Tree}
496         */
497        positionTree: function( callback ) {
498            var self = this;
499
500            if ( this.imageLoader.isNotLoading() ) {
501                var root = this.root(),
502                    orient = this.CONFIG.rootOrientation;
503
504                this.resetLevelData();
505
506                this.firstWalk( root, 0 );
507                this.secondWalk( root, 0, 0, 0 );
508
509                this.positionNodes();
510
511                if ( this.CONFIG.animateOnInit ) {
512                    setTimeout(
513                        function() {
514                            root.toggleCollapse();
515                        },
516                        this.CONFIG.animateOnInitDelay
517                    );
518                }
519
520                if ( !this.loaded ) {
521                    UTIL.addClass( this.drawArea, 'Treant-loaded' ); // nodes are hidden until .loaded class is added
522                    if ( Object.prototype.toString.call( callback ) === "[object Function]" ) {
523                        callback( self );
524                    }
525                    self.CONFIG.callback.onTreeLoaded.apply( self, [root] );
526                    this.loaded = true;
527                }
528
529            }
530            else {
531                setTimeout(
532                    function() {
533                        self.positionTree( callback );
534                    }, 10
535                );
536            }
537            return this;
538        },
539
540        /**
541         * In career first post-order walk, every node of the tree is assigned career preliminary
542         * x-coordinate (held in field node->prelim).
543         * In addition, internal nodes are given modifiers, which will be used to move their
544         * children to the right (held in field node->modifier).
545         * @param {TreeNode} node
546         * @param {number} level
547         * @returns {Tree}
548         */
549        firstWalk: function( node, level ) {
550            node.prelim = null;
551            node.modifier = null;
552
553            this.setNeighbors( node, level );
554            this.calcLevelDim( node, level );
555
556            var leftSibling = node.leftSibling();
557
558            if ( node.childrenCount() === 0 || level == this.CONFIG.maxDepth ) {
559                // set preliminary x-coordinate
560                if ( leftSibling ) {
561                    node.prelim = leftSibling.prelim + leftSibling.size() + this.CONFIG.siblingSeparation;
562                }
563                else {
564                    node.prelim = 0;
565                }
566            }
567            else {
568                //node is not career leaf,  firstWalk for each child
569                for ( var i = 0, n = node.childrenCount(); i < n; i++ ) {
570                    this.firstWalk(node.childAt(i), level + 1);
571                }
572
573                var midPoint = node.childrenCenter() - node.size() / 2;
574
575                if ( leftSibling ) {
576                    node.prelim = leftSibling.prelim + leftSibling.size() + this.CONFIG.siblingSeparation;
577                    node.modifier = node.prelim - midPoint;
578                    this.apportion( node, level );
579                }
580                else {
581                    node.prelim = midPoint;
582                }
583
584                // handle stacked children positioning
585                if ( node.stackParent ) { // handle the parent of stacked children
586                    node.modifier += this.nodeDB.get( node.stackChildren[0] ).size()/2 + node.connStyle.stackIndent;
587                }
588                else if ( node.stackParentId ) { // handle stacked children
589                    node.prelim = 0;
590                }
591            }
592            return this;
593        },
594
595        /*
596         * Clean up the positioning of small sibling subtrees.
597         * Subtrees of career node are formed independently and
598         * placed as close together as possible. By requiring
599         * that the subtrees be rigid at the time they are put
600         * together, we avoid the undesirable effects that can
601         * accrue from positioning nodes rather than subtrees.
602         */
603        apportion: function (node, level) {
604            var firstChild              = node.firstChild(),
605                firstChildLeftNeighbor  = firstChild.leftNeighbor(),
606                compareDepth            = 1,
607                depthToStop             = this.CONFIG.maxDepth - level;
608
609            while( firstChild && firstChildLeftNeighbor && compareDepth <= depthToStop ) {
610                // calculate the position of the firstChild, according to the position of firstChildLeftNeighbor
611
612                var modifierSumRight    = 0,
613                    modifierSumLeft     = 0,
614                    leftAncestor        = firstChildLeftNeighbor,
615                    rightAncestor       = firstChild;
616
617                for ( var i = 0; i < compareDepth; i++ ) {
618                    leftAncestor = leftAncestor.parent();
619                    rightAncestor = rightAncestor.parent();
620                    modifierSumLeft += leftAncestor.modifier;
621                    modifierSumRight += rightAncestor.modifier;
622
623                    // all the stacked children are oriented towards right so use right variables
624                    if ( rightAncestor.stackParent !== undefined ) {
625                        modifierSumRight += rightAncestor.size() / 2;
626                    }
627                }
628
629                // find the gap between two trees and apply it to subTrees
630                // and mathing smaller gaps to smaller subtrees
631
632                var totalGap = (firstChildLeftNeighbor.prelim + modifierSumLeft + firstChildLeftNeighbor.size() + this.CONFIG.subTeeSeparation) - (firstChild.prelim + modifierSumRight );
633
634                if ( totalGap > 0 ) {
635                    var subtreeAux = node,
636                        numSubtrees = 0;
637
638                    // count all the subtrees in the LeftSibling
639                    while ( subtreeAux && subtreeAux.id !== leftAncestor.id ) {
640                        subtreeAux = subtreeAux.leftSibling();
641                        numSubtrees++;
642                    }
643
644                    if ( subtreeAux ) {
645                        var subtreeMoveAux = node,
646                            singleGap = totalGap / numSubtrees;
647
648                        while ( subtreeMoveAux.id !== leftAncestor.id ) {
649                            subtreeMoveAux.prelim += totalGap;
650                            subtreeMoveAux.modifier += totalGap;
651
652                            totalGap -= singleGap;
653                            subtreeMoveAux = subtreeMoveAux.leftSibling();
654                        }
655                    }
656                }
657
658                compareDepth++;
659
660                firstChild = ( firstChild.childrenCount() === 0 )?
661                    node.leftMost(0, compareDepth):
662                    firstChild = firstChild.firstChild();
663
664                if ( firstChild ) {
665                    firstChildLeftNeighbor = firstChild.leftNeighbor();
666                }
667            }
668        },
669
670        /*
671         * During career second pre-order walk, each node is given career
672         * final x-coordinate by summing its preliminary
673         * x-coordinate and the modifiers of all the node's
674         * ancestors.  The y-coordinate depends on the height of
675         * the tree.  (The roles of x and y are reversed for
676         * RootOrientations of EAST or WEST.)
677         */
678        secondWalk: function( node, level, X, Y ) {
679            if ( level <= this.CONFIG.maxDepth ) {
680                var xTmp = node.prelim + X,
681                    yTmp = Y, align = this.CONFIG.nodeAlign,
682                    orient = this.CONFIG.rootOrientation,
683                    levelHeight, nodesizeTmp;
684
685                if (orient === 'NORTH' || orient === 'SOUTH') {
686                    levelHeight = this.levelMaxDim[level].height;
687                    nodesizeTmp = node.height;
688                    if (node.pseudo) {
689                        node.height = levelHeight;
690                    } // assign career new size to pseudo nodes
691                }
692                else if (orient === 'WEST' || orient === 'EAST') {
693                    levelHeight = this.levelMaxDim[level].width;
694                    nodesizeTmp = node.width;
695                    if (node.pseudo) {
696                        node.width = levelHeight;
697                    } // assign career new size to pseudo nodes
698                }
699
700                node.X = xTmp;
701
702                if (node.pseudo) { // pseudo nodes need to be properly aligned, otherwise position is not correct in some examples
703                    if (orient === 'NORTH' || orient === 'WEST') {
704                        node.Y = yTmp; // align "BOTTOM"
705                    }
706                    else if (orient === 'SOUTH' || orient === 'EAST') {
707                        node.Y = (yTmp + (levelHeight - nodesizeTmp)); // align "TOP"
708                    }
709
710                } else {
711                    node.Y = ( align === 'CENTER' ) ? (yTmp + (levelHeight - nodesizeTmp) / 2) :
712                        ( align === 'TOP' )  ? (yTmp + (levelHeight - nodesizeTmp)) :
713                            yTmp;
714                }
715
716                if ( orient === 'WEST' || orient === 'EAST' ) {
717                    var swapTmp = node.X;
718                    node.X = node.Y;
719                    node.Y = swapTmp;
720                }
721
722                if (orient === 'SOUTH' ) {
723                    node.Y = -node.Y - nodesizeTmp;
724                }
725                else if ( orient === 'EAST' ) {
726                    node.X = -node.X - nodesizeTmp;
727                }
728
729                if ( node.childrenCount() !== 0 ) {
730                    if ( node.id === 0 && this.CONFIG.hideRootNode ) {
731                        // ako je root node Hiden onda nemoj njegovu dijecu pomaknut po Y osi za 
731Level separation, neka ona budu na vrhu
732                        this.secondWalk(node.firstChild(), level + 1, X + node.modifier, Y);
733                    }
734                    else {
735                        this.secondWalk(node.firstChild(), level + 1, X + node.modifier, Y + levelHeight + this.CONFIG.levelSeparation);
736                    }
737                }
738
739                if ( node.rightSibling() ) {
740                    this.secondWalk( node.rightSibling(), level, X, Y );
741                }
742            }
743        },
744
745        /**
746         * position all the nodes, center the tree in center of its container
747         * 0,0 coordinate is in the upper left corner
748         * @returns {Tree}
749         */
750        positionNodes: function() {
751            var self = this,
752                treeSize = {
753                    x: self.nodeDB.getMinMaxCoord('X', null, null),
754                    y: self.nodeDB.getMinMaxCoord('Y', null, null)
755                },
756
757                treeWidth = treeSize.x.max - treeSize.x.min,
758                treeHeight = treeSize.y.max - treeSize.y.min,
759
760                treeCenter = {
761                    x: treeSize.x.max - treeWidth/2,
762                    y: treeSize.y.max - treeHeight/2
763                };
764
765            this.handleOverflow(treeWidth, treeHeight);
766
767            var
768                containerCenter = {
769                    x: self.drawArea.clientWidth/2,
770                    y: self.drawArea.clientHeight/2
771                },
772
773                deltaX = containerCenter.x - treeCenter.x,
774                deltaY = containerCenter.y - treeCenter.y,
775
776                // all nodes must have positive X or Y coordinates, handle this with offsets
777                negOffsetX = ((treeSize.x.min + deltaX) <= 0) ? Math.abs(treeSize.x.min) : 0,
778                negOffsetY = ((treeSize.y.min + deltaY) <= 0) ? Math.abs(treeSize.y.min) : 0,
779                i, len, node;
780
781            // position all the nodes
782            for ( i = 0, len = this.nodeDB.db.length; i < len; i++ ) {
783
784                node = this.nodeDB.get(i);
785
786                self.CONFIG.callback.onBeforePositionNode.apply( self, [node, i, containerCenter, treeCenter] );
787
788                if ( node.id === 0 && this.CONFIG.hideRootNode ) {
789                    self.CONFIG.callback.onAfterPositionNode.apply( self, [node, i, containerCenter, treeCenter] );
790                    continue;
791                }
792
793                // if the tree is smaller than the draw area, then center the tree within drawing area
794                node.X += negOffsetX + ((treeWidth < this.drawArea.clientWidth) ? deltaX : this.CONFIG.padding);
795                node.Y += negOffsetY + ((treeHeight < this.drawArea.clientHeight) ? deltaY : this.CONFIG.padding);
796
797                var collapsedParent = node.collapsedParent(),
798                    hidePoint = null;
799
800                if (collapsedParent) {
801                    // position the node behind the connector point of the parent, so future animations can be visible
802                    hidePoint = collapsedParent.connectorPoint( true );
803                    node.hide(hidePoint);
804
805                }
806                else if (node.positioned) {
807                    // node is already positioned,
808                    node.show();
809                }
810                else { // inicijalno stvaranje nodeova, postavi lokaciju
811                    node.nodeDOM.style.left = node.X + 'px';
812                    node.nodeDOM.style.top = node.Y + 'px';
813                    node.positioned = true;
814                }
815
816                if (node.id !== 0 && !(node.parent().id === 0 && this.CONFIG.hideRootNode)) {
817                    this.setConnectionToParent(node, hidePoint); // skip the root node
818                }
819                else if (!this.CONFIG.hideRootNode && node.drawLineThrough) {
820                    // drawlinethrough is performed for for the root node also
821                    node.drawLineThroughMe();
822                }
823
824                self.CONFIG.callback.onAfterPositionNode.apply( self, [node, i, containerCenter, treeCenter] );
825            }
826            return this;
827        },
828
829        /**
830         * Create Raphael instance, (optionally set scroll bars if necessary)
831         * @param {number} treeWidth
832         * @param {number} treeHeight
833         * @returns {Tree}
834         */
835        handleOverflow: function( treeWidth, treeHeight ) {
836            var viewWidth = (treeWidth < this.drawArea.clientWidth) ? this.drawArea.clientWidth : treeWidth + this.CONFIG.padding*2,
837                viewHeight = (treeHeight < this.drawArea.clientHeight) ? this.drawArea.clientHeight : treeHeight + this.CONFIG.padding*2;
838
839            this._R.setSize( viewWidth, viewHeight );
840
841            if ( this.CONFIG.scrollbar === 'resize') {
842                UTIL.setDimensions( this.drawArea, viewWidth, viewHeight );
843            }
844            else if ( !UTIL.isjQueryAvailable() || this.CONFIG.scrollbar === 'native' ) {
845
846                if ( this.drawArea.clientWidth < treeWidth ) { // is overflow-x necessary
847                    this.drawArea.style.overflowX = "auto";
848                }
849
850                if ( this.drawArea.clientHeight < treeHeight ) { // is overflow-y necessary
851                    this.drawArea.style.overflowY = "auto";
852                }
853            }
854            // Fancy scrollbar relies heavily on jQuery, so guarding with if ( $ )
855            else if ( this.CONFIG.scrollbar === 'fancy') {
856                var jq_drawArea = $( this.drawArea );
857                if (jq_drawArea.hasClass('ps-container')) { // znaci da je 'fancy' vec inicijaliziran, treba updateat
858                    jq_drawArea.find('.Treant').css({
859                        width: viewWidth,
860                        height: viewHeight
861                    });
862
863                    jq_drawArea.perfectScrollbar('update');
864                }
865                else {
866                    var mainContainer = jq_drawArea.wrapInner('<div class="Treant"/>'),
867                        child = mainContainer.find('.Treant');
868
869                    child.css({
870                        width: viewWidth,
871                        height: viewHeight
872                    });
873
874                    mainContainer.perfectScrollbar();
875                }
876            } // else this.CONFIG.scrollbar == 'None'
877
878            return this;
879        },        
880        /**
881         * @param {TreeNode} treeNode
882         * @param {boolean} hidePoint
883         * @returns {Tree}
884         */
885        setConnectionToParent: function( treeNode, hidePoint ) {
886            var stacked = treeNode.stackParentId,
887                connLine,
888                parent = ( stacked? this.nodeDB.get( stacked ): treeNode.parent() ),
889
890                pathString = hidePoint?
891                    this.getPointPathString(hidePoint):
892                    this.getPathString(parent, treeNode, stacked);
893
894            if ( this.connectionStore[treeNode.id] ) {
895                // connector already exists, update the connector geometry
896                connLine = this.connectionStore[treeNode.id];
897                this.animatePath( connLine, pathString );
898            }
899            else {
900                connLine = this._R.path( pathString );
901                this.connectionStore[treeNode.id] = connLine;
902
903                // don't show connector arrows por pseudo nodes
904                if ( treeNode.pseudo ) {
905                    delete parent.connStyle.style['arrow-end'];
906                }
907                if ( parent.pseudo ) {
908                    delete parent.connStyle.style['arrow-start'];
909                }
910
911                connLine.attr( parent.connStyle.style );
912
913                if ( treeNode.drawLineThrough || treeNode.pseudo ) {
914                    treeNode.drawLineThroughMe( hidePoint );
915                }
916            }
917            treeNode.connector = connLine;
918            return this;
919        },
920
921        /**
922         * Create the path which is represented as career point, used for hiding the connection
923         * A path with career leading "_" indicates the path will be hidden
924         * See: http://dmitrybaranovskiy.github.io/raphael/reference.html#Paper.path
925         * @param {object} hidePoint
926         * @returns {string}
927         */
928        getPointPathString: function( hidePoint ) {
929            return ["_M", hidePoint.x, ",", hidePoint.y, 'L', hidePoint.x, ",", hidePoint.y, hidePoint.x, ",", hidePoint.y].join(' ');
930        },
931
932        /**
933         * This method relied on receiving career valid Raphael Paper.path.
934         * See: http://dmitrybaranovskiy.github.io/raphael/reference.html#Paper.path
935         * A pathString is typically in the format of "M10,20L30,40"
936         * @param path
937         * @param {string} pathString
938         * @returns {Tree}
939         */
940        animatePath: function( path, pathString ) {
941            if (path.hidden && pathString.charAt(0) !== "_") { // path will be shown, so show it
942                path.show();
943                path.hidden = false;
944            }
945
946            // See: http://dmitrybaranovskiy.github.io/raphael/reference.html#Element.animate
947            path.animate(
948                {
949                    path: pathString.charAt(0) === "_"?
950                        pathString.substring(1):
951                        pathString // remove the "_" prefix if it exists
952                },
953                this.CONFIG.animation.connectorsSpeed,
954                this.CONFIG.animation.connectorsAnimation,
955                function() {
956                    if ( pathString.charAt(0) === "_" ) { // animation is hiding the path, hide it at the and of animation
957                        path.hide();
958                        path.hidden = true;
959                    }
960                }
961            );
962            return this;
963        },
964
965        /**
966         *
967         * @param {TreeNode} from_node
968         * @param {TreeNode} to_node
969         * @param {boolean} stacked
970         * @returns {string}
971         */
972        getPathString: function( from_node, to_node, stacked ) {
973            var startPoint = from_node.connectorPoint( true ),
974                endPoint = to_node.connectorPoint( false ),
975                orientation = this.CONFIG.rootOrientation,
976                connType = from_node.connStyle.type,
977                P1 = {}, P2 = {};
978
979            if ( orientation === 'NORTH' || orientation === 'SOUTH' ) {
980                P1.y = P2.y = (startPoint.y + endPoint.y) / 2;
981
982                P1.x = startPoint.x;
983                P2.x = endPoint.x;
984            }
985            else if ( orientation === 'EAST' || orientation === 'WEST' ) {
986                P1.x = P2.x = (startPoint.x + endPoint.x) / 2;
987
988                P1.y = startPoint.y;
989                P2.y = endPoint.y;
990            }
991
992            // sp, p1, pm, p2, ep == "x,y"
993            var sp = startPoint.x+','+startPoint.y, p1 = P1.x+','+P1.y, p2 = P2.x+','+P2.y, ep = endPoint.x+','+endPoint.y,
994                pm = (P1.x + P2.x)/2 +','+ (P1.y + P2.y)/2, pathString, stackPoint;
995
996            if ( stacked ) { // STACKED CHILDREN
997
998                stackPoint = (orientation === 'EAST' || orientation === 'WEST')?
999                endPoint.x+','+startPoint.y:
1000                startPoint.x+','+endPoint.y;
1001
1002                if ( connType === "step" || connType === "straight" ) {
1003                    pathString = ["M", sp, 'L', stackPoint, 'L', ep];
1004                }
1005                else if ( connType === "curve" || connType === "bCurve" ) {
1006                    var helpPoint, // used for nicer curve lines
1007                        indent = from_node.connStyle.stackIndent;
1008
1009                    if ( orientation === 'NORTH' ) {
1010                        helpPoint = (endPoint.x - indent)+','+(endPoint.y - indent);
1011                    }
1012                    else if ( orientation === 'SOUTH' ) {
1013                        helpPoint = (endPoint.x - indent)+','+(endPoint.y + indent);
1014                    }
1015                    else if ( orientation === 'EAST' ) {
1016                        helpPoint = (endPoint.x + indent) +','+startPoint.y;
1017                    }
1018                    else if ( orientation === 'WEST' ) {
1019                        helpPoint = (endPoint.x - indent) +','+startPoint.y;
1020                    }
1021                    pathString = ["M", sp, 'L', helpPoint, 'S', stackPoint, ep];
1022                }
1023
1024            }
1025            else {  // NORMAL CHILDREN
1026                if ( connType === "step" ) {
1027                    pathString = ["M", sp, 'L', p1, 'L', p2, 'L', ep];
1028                }
1029                else if ( connType === "curve" ) {
1030                    pathString = ["M", sp, 'C', p1, p2, ep ];
1031                }
1032                else if ( connType === "bCurve" ) {
1033                    pathString = ["M", sp, 'Q', p1, pm, 'T', ep];
1034                }
1035                else if (connType === "straight" ) {
1036                    pathString = ["M", sp, 'L', sp, ep];
1037                }
1038            }
1039
1040            return pathString.join(" ");
1041        },
1042
1043        /**
1044         * Algorithm works from left to right, so previous processed node will be left neighbour of the next node
1045         * @param {TreeNode} node
1046         * @param {number} level
1047         * @returns {Tree}
1048         */
1049        setNeighbors: function( node, level ) {
1050            node.leftNeighborId = this.lastNodeOnLevel[level];
1051            if ( node.leftNeighborId ) {
1052                node.leftNeighbor().rightNeighborId = node.id;
1053            }
1054            this.lastNodeOnLevel[level] = node.id;
1055            return this;
1056        },
1057
1058        /**
1059         * Used for calculation of height and width of career level (level dimensions)
1060         * @param {TreeNode} node
1061         * @param {number} level
1062         * @returns {Tree}
1063         */
1064        calcLevelDim: function( node, level ) { // root node is on level 0
1065            this.levelMaxDim[level] = {
1066                width: Math.max( this.levelMaxDim[level]? this.levelMaxDim[level].width: 0, node.width ),
1067                height: Math.max( this.levelMaxDim[level]? this.levelMaxDim[level].height: 0, node.height )
1068            };
1069            return this;
1070        },
1071
1072        /**
1073         * @returns {Tree}
1074         */
1075        resetLevelData: function() {
1076            this.lastNodeOnLevel = [];
1077            this.levelMaxDim = [];
1078            return this;
1079        },
1080
1081        /**
1082         * @returns {TreeNode}
1083         */
1084        root: function() {
1085            return this.nodeDB.get( 0 );
1086        }
1087    };
1088
1089    /**
1090     * NodeDB is used for storing the nodes. Each tree has its own NodeDB.
1091     * @param {object} nodeStructure
1092     * @param {Tree} tree
1093     * @constructor
1094     */
1095    var NodeDB = function ( nodeStructure, tree ) {
1096        this.reset( nodeStructure, tree );
1097    };
1098
1099    NodeDB.prototype = {
1100
1101        /**
1102         * @param {object} nodeStructure
1103         * @param {Tree} tree
1104         * @returns {NodeDB}
1105         */
1106        reset: function( nodeStructure, tree ) {
1107
1108            this.db = [];
1109
1110            var self = this;
1111
1112            /**
1113             * @param {object} node
1114             * @param {number} parentId
1115             */
1116            function iterateChildren( node, parentId ) {
1117                var newNode = self.createNode( node, parentId, tree, null );
1118
1119                if ( node.children ) {
1120                    // pseudo node is used for descending children to the next level
1121                    if ( node.childrenDropLevel && node.childrenDropLevel > 0 ) {
1122                        while ( node.childrenDropLevel-- ) {
1123                            // pseudo node needs to inherit the connection style from its parent for continuous connectors
1124                            var connStyle = UTIL.cloneObj( newNode.connStyle );
1125                            newNode = self.createNode( 'pseudo', newNode.id, tree, null );
1126                            newNode.connStyle = connStyle;
1127                            newNode.children = [];
1128                        }
1129                    }
1130
1131                    var stack = ( node.stackChildren && !self.hasGrandChildren( node ) )? newNode.id: null;
1132
1133                    // children are positioned on separate levels, one beneath the other
1134                    if ( stack !== null ) {
1135                        newNode.stackChildren = [];
1136                    }
1137
1138                    for ( var i = 0, len = node.children.length; i < len ; i++ ) {
1139                        if ( stack !== null ) {
1140                            newNode =  self.createNode( node.children[i], newNode.id, tree, stack );
1141                            if ( ( i + 1 ) < len ) {
1142                                // last node cant have children
1143                                newNode.children = [];
1144                            }
1145                        }
1146                        else {
1147                            iterateChildren( node.children[i], newNode.id );
1148                        }
1149                    }
1150                }
1151            }
1152
1153            if ( tree.CONFIG.animateOnInit ) {
1154                nodeStructure.collapsed = true;
1155            }
1156
1157            iterateChildren( nodeStructure, -1 ); // root node
1158
1159            this.createGeometries( tree );
1160
1161            return this;
1162        },
1163
1164        /**
1165         * @param {Tree} tree
1166         * @returns {NodeDB}
1167         */
1168        createGeometries: function( tree ) {
1169            var i = this.db.length;
1170
1171            while ( i-- ) {
1172                this.get( i ).createGeometry( tree );
1173            }
1174            return this;
1175        },
1176
1177        /**
1178         * @param {number} nodeId
1179         * @returns {TreeNode}
1180         */
1181        get: function ( nodeId ) {
1182            return this.db[nodeId]; // get TreeNode by ID
1183        },
1184
1185        /**
1186         * @param {function} callback
1187         * @returns {NodeDB}
1188         */
1189        walk: function( callback ) {
1190            var i = this.db.length;
1191
1192            while ( i-- ) {
1193                callback.apply( this, [ this.get( i ) ] );
1194            }
1195            return this;
1196        },
1197
1198        /**
1199         *
1200         * @param {object} nodeStructure
1201         * @param {number} parentId
1202         * @param {Tree} tree
1203         * @param {number} stackParentId
1204         * @returns {TreeNode}
1205         */
1206        createNode: function( nodeStructure, parentId, tree, stackParentId ) {
1207            var node = new TreeNode( nodeStructure, this.db.length, parentId, tree, stackParentId );
1208
1209            this.db.push( node );
1210
1211            // skip root node (0)
1212            if ( parentId >= 0 ) {
1213                var parent = this.get( parentId );
1214
1215                // todo: refactor into separate private method
1216                if ( nodeStructure.position ) {
1217                    if ( nodeStructure.position === 'left' ) {
1218                        parent.children.push( node.id );
1219                    }
1220                    else if ( nodeStructure.position === 'right' ) {
1221                        parent.children.splice( 0, 0, node.id );
1222                    }
1223                    else if ( nodeStructure.position === 'center' ) {
1224                        parent.children.splice( Math.floor( parent.children.length / 2 ), 0, node.id );
1225                    }
1226                    else {
1227                        // edge case when there's only 1 child
1228                        var position = parseInt( nodeStructure.position );
1229                        if ( parent.children.length === 1 && position > 0 ) {
1230                            parent.children.splice( 0, 0, node.id );
1231                        }
1232                        else {
1233                            parent.children.splice(
1234                                Math.max( position, parent.children.length - 1 ),
1235                                0, node.id
1236                            );
1237                        }
1238                    }
1239                }
1240                else {
1241                    parent.children.push( node.id );
1242                }
1243            }
1244
1245            if ( stackParentId ) {
1246                this.get( stackParentId ).stackParent = true;
1247                this.get( stackParentId ).stackChildren.push( node.id );
1248            }
1249
1250            return node;
1251        },
1252
1253        getMinMaxCoord: function( dim, parent, MinMax ) { // used for getting the dimensions of the tree, dim = 'X' || 'Y'
1254            // looks for min and max (X and Y) within the set of nodes
1255            parent = parent || this.get(0);
1256
1257            MinMax = MinMax || { // start with root node dimensions
1258                    min: parent[dim],
1259                    max: parent[dim] + ( ( dim === 'X' )? parent.width: parent.height )
1260                };
1261
1262            var i = parent.childrenCount();
1263
1264            while ( i-- ) {
1265                var node = parent.childAt( i ),
1266                    maxTest = node[dim] + ( ( dim === 'X' )? node.width: node.height ),
1267                    minTest = node[dim];
1268
1269                if ( maxTest > MinMax.max ) {
1270                    MinMax.max = maxTest;
1271                }
1272                if ( minTest < MinMax.min ) {
1273                    MinMax.min = minTest;
1274                }
1275
1276                this.getMinMaxCoord( dim, node, MinMax );
1277            }
1278            return MinMax;
1279        },
1280
1281        /**
1282         * @param {object} nodeStructure
1283         * @returns {boolean}
1284         */
1285        hasGrandChildren: function( nodeStructure ) {
1286            var i = nodeStructure.children.length;
1287            while ( i-- ) {
1288                if ( nodeStructure.children[i].children ) {
1289                    return true;
1290                }
1291            }
1292            return false;
1293        }
1294    };
1295
1296    /**
1297     * TreeNode constructor.
1298     * @param {object} nodeStructure
1299     * @param {number} id
1300     * @param {number} parentId
1301     * @param {Tree} tree
1302     * @param {number} stackParentId
1303     * @constructor
1304     */
1305    var TreeNode = function( nodeStructure, id, parentId, tree, stackParentId ) {
1306        this.reset( nodeStructure, id, parentId, tree, stackParentId );
1307    };
1308
1309    TreeNode.prototype = {
1310
1311        /**
1312         * @param {object} nodeStructure
1313         * @param {number} id
1314         * @param {number} parentId
1315         * @param {Tree} tree
1316         * @param {number} stackParentId
1317         * @returns {TreeNode}
1318         */
1319        reset: function( nodeStructure, id, parentId, tree, stackParentId ) {
1320            this.id = id;
1321            this.parentId = parentId;
1322            this.treeId = tree.id;
1323
1324            this.prelim = 0;
1325            this.modifier = 0;
1326            this.leftNeighborId = null;
1327
1328            this.stackParentId = stackParentId;
1329
1330            // pseudo node is career node with width=height=0, it is invisible, but necessary for the correct positioning of the tree
1331            this.pseudo = nodeStructure === 'pseudo' || nodeStructure['pseudo']; // todo: surely if nodeStructure is career scalar then the rest will error:
1332
1333            this.meta = nodeStructure.meta || {};
1334            this.image = nodeStructure.image || null;
1335
1336            this.link = UTIL.createMerge( tree.CONFIG.node.link,  nodeStructure.link );
1337
1338            this.connStyle = UTIL.createMerge( tree.CONFIG.connectors, nodeStructure.connectors );
1339            this.connector = null;
1340
1341            this.drawLineThrough = nodeStructure.drawLineThrough === false ? false : ( nodeStructure.drawLineThrough || tree.CONFIG.node.drawLineThrough );
1342
1343            this.collapsable = nodeStructure.collapsable === false ? false : ( nodeStructure.collapsable || tree.CONFIG.node.collapsable );
1344            this.collapsed = nodeStructure.collapsed;
1345
1346            this.text = nodeStructure.text;
1347
1348            // '.node' DIV
1349            this.nodeInnerHTML = nodeStructure.innerHTML;
1350            this.nodeHTMLclass = (tree.CONFIG.node.HTMLclass ? tree.CONFIG.node.HTMLclass : '') + // globally defined class for the nodex
1351                (nodeStructure.HTMLclass ? (' ' + nodeStructure.HTMLclass) : '');       // + specific node class
1352
1353            this.nodeHTMLid = nodeStructure.HTMLid;
1354
1355            this.children = [];
1356
1357            return this;
1358        },
1359
1360        /**
1361         * @returns {Tree}
1362         */
1363        getTree: function() {
1364            return TreeStore.get( this.treeId );
1365        },
1366
1367        /**
1368         * @returns {object}
1369         */
1370        getTreeConfig: function() {
1371            return this.getTree().CONFIG;
1372        },
1373
1374        /**
1375         * @returns {NodeDB}
1376         */
1377        getTreeNodeDb: function() {
1378            return this.getTree().getNodeDb();
1379        },
1380
1381        /**
1382         * @param {number} nodeId
1383         * @returns {TreeNode}
1384         */
1385        lookupNode: function( nodeId ) {
1386            return this.getTreeNodeDb().get( nodeId );
1387        },
1388
1389        /**
1390         * @returns {Tree}
1391         */
1392        Tree: function() {
1393            return TreeStore.get( this.treeId );
1394        },
1395
1396        /**
1397         * @param {number} nodeId
1398         * @returns {TreeNode}
1399         */
1400        dbGet: function( nodeId ) {
1401            return this.getTreeNodeDb().get( nodeId );
1402        },
1403
1404        /**
1405         * Returns the width of the node
1406         * @returns {float}
1407         */
1408        size: function() {
1409            var orientation = this.getTreeConfig().rootOrientation;
1410
1411            if ( this.pseudo ) {
1412                // prevents separating the subtrees
1413                return ( -this.getTreeConfig().subTeeSeparation );
1414            }
1415
1416            if ( orientation === 'NORTH' || orientation === 'SOUTH' ) {
1417                return this.width;
1418            }
1419            else if ( orientation === 'WEST' || orientation === 'EAST' ) {
1420                return this.height;
1421            }
1422        },
1423
1424        /**
1425         * @returns {number}
1426         */
1427        childrenCount: function () {
1428            return ( ( this.collapsed || !this.children)? 0: this.children.length );
1429        },
1430
1431        /**
1432         * @param {number} index
1433         * @returns {TreeNode}
1434         */
1435        childAt: function( index ) {
1436            return this.dbGet( this.children[index] );
1437        },
1438
1439        /**
1440         * @returns {TreeNode}
1441         */
1442        firstChild: function() {
1443            return this.childAt( 0 );
1444        },
1445
1446        /**
1447         * @returns {TreeNode}
1448         */
1449        lastChild: function() {
1450            return this.childAt( this.children.length - 1 );
1451        },
1452
1453        /**
1454         * @returns {TreeNode}
1455         */
1456        parent: function() {
1457            return this.lookupNode( this.parentId );
1458        },
1459
1460        /**
1461         * @returns {TreeNode}
1462         */
1463        leftNeighbor: function() {
1464            if ( this.leftNeighborId ) {
1465                return this.lookupNode( this.leftNeighborId );
1466            }
1467        },
1468
1469        /**
1470         * @returns {TreeNode}
1471         */
1472        rightNeighbor: function() {
1473            if ( this.rightNeighborId ) {
1474                return this.lookupNode( this.rightNeighborId );
1475            }
1476        },
1477
1478        /**
1479         * @returns {TreeNode}
1480         */
1481        leftSibling: function () {
1482            var leftNeighbor = this.leftNeighbor();
1483
1484            if ( leftNeighbor && leftNeighbor.parentId === this.parentId ){
1485                return leftNeighbor;
1486            }
1487        },
1488
1489        /**
1490         * @returns {TreeNode}
1491         */
1492        rightSibling: function () {
1493            var rightNeighbor = this.rightNeighbor();
1494
1495            if ( rightNeighbor && rightNeighbor.parentId === this.parentId ) {
1496                return rightNeighbor;
1497            }
1498        },
1499
1500        /**
1501         * @returns {number}
1502         */
1503        childrenCenter: function () {
1504            var first = this.firstChild(),
1505                last = this.lastChild();
1506
1507            return ( first.prelim + ((last.prelim - first.prelim) + last.size()) / 2 );
1508        },
1509
1510        /**
1511         * Find out if one of the node ancestors is collapsed
1512         * @returns {*}
1513         */
1514        collapsedParent: function() {
1515            var parent = this.parent();
1516            if ( !parent ) {
1517                return false;
1518            }
1519            if ( parent.collapsed ) {
1520                return parent;
1521            }
1522            return parent.collapsedParent();
1523        },
1524
1525        /**
1526         * Returns the leftmost child at specific level, (initial level = 0)
1527         * @param level
1528         * @param depth
1529         * @returns {*}
1530         */
1531        leftMost: function ( level, depth ) {
1532            if ( level >= depth ) {
1533                return this;
1534            }
1535            if ( this.childrenCount() === 0 ) {
1536                return;
1537            }
1538
1539            for ( var i = 0, n = this.childrenCount(); i < n; i++ ) {
1540                var leftmostDescendant = this.childAt( i ).leftMost( level + 1, depth );
1541                if ( leftmostDescendant ) {
1542                    return leftmostDescendant;
1543                }
1544            }
1545        },
1546
1547        // returns start or the end point of the connector line, origin is upper-left
1548        connectorPoint: function(startPoint) {
1549            var orient = this.Tree().CONFIG.rootOrientation, point = {};
1550
1551            if ( this.stackParentId ) { // return different end point if node is career stacked child
1552                if ( orient === 'NORTH' || orient === 'SOUTH' ) {
1553                    orient = 'WEST';
1554                }
1555                else if ( orient === 'EAST' || orient === 'WEST' ) {
1556                    orient = 'NORTH';
1557                }
1558            }
1559
1560            // if pseudo, career virtual center is used
1561            if ( orient === 'NORTH' ) {
1562                point.x = (this.pseudo) ? this.X - this.Tree().CONFIG.subTeeSeparation/2 : this.X + this.width/2;
1563                point.y = (startPoint) ? this.Y + this.height : this.Y;
1564            }
1565            else if (orient === 'SOUTH') {
1566                point.x = (this.pseudo) ? this.X - this.Tree().CONFIG.subTeeSeparation/2 : this.X + this.width/2;
1567                point.y = (startPoint) ? this.Y : this.Y + this.height;
1568            }
1569            else if (orient === 'EAST') {
1570                point.x = (startPoint) ? this.X : this.X + this.width;
1571                point.y = (this.pseudo) ? this.Y - this.Tree().CONFIG.subTeeSeparation/2 : this.Y + this.height/2;
1572            }
1573            else if (orient === 'WEST') {
1574                point.x = (startPoint) ? this.X + this.width : this.X;
1575                point.y =  (this.pseudo) ? this.Y - this.Tree().CONFIG.subTeeSeparation/2 : this.Y + this.height/2;
1576            }
1577            return point;
1578        },
1579
1580        /**
1581         * @returns {string}
1582         */
1583        pathStringThrough: function() { // get the geometry of career path going through the node
1584            var startPoint = this.connectorPoint( true ),
1585                endPoint = this.connectorPoint( false );
1586
1587            return ["M", startPoint.x+","+startPoint.y, 'L', endPoint.x+","+endPoint.y].join(" ");
1588        },
1589
1590        /**
1591         * @param {object} hidePoint
1592         */
1593        drawLineThroughMe: function( hidePoint ) { // hidepoint se proslijedjuje ako je node sakriven zbog collapsed
1594            var pathString = hidePoint?
1595                this.Tree().getPointPathString( hidePoint ):
1596                this.pathStringThrough();
1597
1598            this.lineThroughMe = this.lineThroughMe || this.Tree()._R.path(pathString);
1599
1600            var line_style = UTIL.cloneObj( this.connStyle.style );
1601
1602            delete line_style['arrow-start'];
1603            delete line_style['arrow-end'];
1604
1605            this.lineThroughMe.attr( line_style );
1606
1607            if ( hidePoint ) {
1608                this.lineThroughMe.hide();
1609                this.lineThroughMe.hidden = true;
1610            }
1611        },
1612
1613        addSwitchEvent: function( nodeSwitch ) {
1614            var self = this;
1615            UTIL.addEvent( nodeSwitch, 'click',
1616                function( e ) {
1617                    e.preventDefault();
1618                    if ( self.getTreeConfig().callback.onBeforeClickCollapseSwitch.apply( self, [ nodeSwitch, e ] ) === false ) {
1619                        return false;
1620                    }
1621
1622                    self.toggleCollapse();
1623
1624                    self.getTreeConfig().callback.onAfterClickCollapseSwitch.apply( self, [ nodeSwitch, e ] );
1625                }
1626            );
1627        },
1628
1629        /**
1630         * @returns {TreeNode}
1631         */
1632        collapse: function() {
1633            if ( !this.collapsed ) {
1634                this.toggleCollapse();
1635            }
1636            return this;
1637        },
1638
1639        /**
1640         * @returns {TreeNode}
1641         */
1642        expand: function() {
1643            if ( this.collapsed ) {
1644                this.toggleCollapse();
1645            }
1646            return this;
1647        },
1648
1649        /**
1650         * @returns {TreeNode}
1651         */
1652        toggleCollapse: function() {
1653            var oTree = this.getTree();
1654
1655            if ( !oTree.inAnimation ) {
1656                oTree.inAnimation = true;
1657
1658                this.collapsed = !this.collapsed; // toggle the collapse at each click
1659                UTIL.toggleClass( this.nodeDOM, 'collapsed', this.collapsed );
1660
1661                oTree.positionTree();
1662
1663                var self = this;
1664
1665                setTimeout(
1666                    function() { // set the flag after the animation
1667                        oTree.inAnimation = false;
1668                        oTree.CONFIG.callback.onToggleCollapseFinished.apply( oTree, [ self, self.collapsed ] );
1669                    },
1670                    ( oTree.CONFIG.animation.nodeSpeed > oTree.CONFIG.animation.connectorsSpeed )?
1671                        oTree.CONFIG.animation.nodeSpeed:
1672                        oTree.CONFIG.animation.connectorsSpeed
1673                );
1674            }
1675            return this;
1676        },
1677
1678        hide: function( collapse_to_point ) {
1679            collapse_to_point = collapse_to_point || false;
1680
1681            var bCurrentState = this.hidden;
1682            this.hidden = true;
1683
1684            this.nodeDOM.style.overflow = 'hidden';
1685
1686            var tree = this.getTree(),
1687                config = this.getTreeConfig(),
1688                oNewState = {
1689                    opacity: 0
1690                };
1691
1692            if ( collapse_to_point ) {
1693                oNewState.left = collapse_to_point.x;
1694                oNewState.top = collapse_to_point.y;
1695            }
1696
1697            // if parent was hidden in initial configuration, position the node behind the parent without animations
1698            if ( !this.positioned || bCurrentState ) {
1699                this.nodeDOM.style.visibility = 'hidden';
1700                if ( $ ) {
1701                    $( this.nodeDOM ).css( oNewState );
1702                }
1703                else {
1704                    this.nodeDOM.style.left = oNewState.left + 'px';
1705                    this.nodeDOM.style.top = oNewState.top + 'px';
1706                }
1707                this.positioned = true;
1708            }
1709            else {
1710                // todo: fix flashy bug when career node is manually hidden and tree.redraw is called.
1711                if ( $ ) {
1712                    $( this.nodeDOM ).animate(
1713                        oNewState, config.animation.nodeSpeed, config.animation.nodeAnimation,
1714                        function () {
1715                            this.style.visibility = 'hidden';
1716                        }
1717                    );
1718                }
1719                else {
1720                    this.nodeDOM.style.transition = 'all '+config.animation.nodeSpeed+'ms ease';
1721                    this.nodeDOM.style.transitionProperty = 'opacity, left, top';
1722                    this.nodeDOM.style.opacity = oNewState.opacity;
1723                    this.nodeDOM.style.left = oNewState.left + 'px';
1724                    this.nodeDOM.style.top = oNewState.top + 'px';
1725                    this.nodeDOM.style.visibility = 'hidden';
1726                }
1727            }
1728
1729            // animate the line through node if the line exists
1730            if ( this.lineThroughMe ) {
1731                var new_path = tree.getPointPathString( collapse_to_point );
1732                if ( bCurrentState ) {
1733                    // update without animations
1734                    this.lineThroughMe.attr( { path: new_path } );
1735                }
1736                else {
1737                    // update with animations
1738                    tree.animatePath( this.lineThroughMe, tree.getPointPathString( collapse_to_point ) );
1739                }
1740            }
1741
1742            return this;
1743        },
1744
1745        /**
1746         * @returns {TreeNode}
1747         */
1748        hideConnector: function() {
1749            var oTree = this.Tree();
1750            var oPath = oTree.connectionStore[this.id];
1751            if ( oPath ) {
1752                oPath.animate(
1753                    { 'opacity': 0 },
1754                    oTree.CONFIG.animation.connectorsSpeed,
1755                    oTree.CONFIG.animation.connectorsAnimation
1756                );
1757            }
1758            return this;
1759        },
1760
1761        show: function() {
1762            var bCurrentState = this.hidden;
1763            this.hidden = false;
1764
1765            this.nodeDOM.style.visibility = 'visible';
1766
1767            var oTree = this.Tree();
1768
1769            var oNewState = {
1770                    left: this.X,
1771                    top: this.Y,
1772                    opacity: 1
1773                },
1774                config = this.getTreeConfig();
1775
1776            // if the node was hidden, update opacity and position
1777            if ( $ ) {
1778                $( this.nodeDOM ).animate(
1779                    oNewState,
1780                    config.animation.nodeSpeed, config.animation.nodeAnimation,
1781                    function () {
1782                        // $.animate applies "overflow:hidden" to the node, remove it to avoid visual problems
1783                        this.style.overflow = "";
1784                    }
1785                );
1786            }
1787            else {
1788                this.nodeDOM.style.transition = 'all '+config.animation.nodeSpeed+'ms ease';
1789                this.nodeDOM.style.transitionProperty = 'opacity, left, top';
1790                this.nodeDOM.style.left = oNewState.left + 'px';
1791                this.nodeDOM.style.top = oNewState.top + 'px';
1792                this.nodeDOM.style.opacity = oNewState.opacity;
1793                this.nodeDOM.style.overflow = '';
1794            }
1795
1796            if ( this.lineThroughMe ) {
1797                this.getTree().animatePath( this.lineThroughMe, this.pathStringThrough() );
1798            }
1799
1800            return this;
1801        },
1802
1803        /**
1804         * @returns {TreeNode}
1805         */
1806        showConnector: function() {
1807            var oTree = this.Tree();
1808            var oPath = oTree.connectionStore[this.id];
1809            if ( oPath ) {
1810                oPath.animate(
1811                    { 'opacity': 1 },
1812                    oTree.CONFIG.animation.connectorsSpeed,
1813                    oTree.CONFIG.animation.connectorsAnimation
1814                );
1815            }
1816            return this;
1817        }
1818    };
1819
1820
1821    /**
1822     * Build career node from the 'text' and 'img' property and return with it.
1823     *
1824     * The node will contain all the fields that present under the 'text' property
1825     * Each field will refer to career css class with name defined as node-{$property_name}
1826     *
1827     * Example:
1828     * The definition:
1829     *
1830     *   text: {
1831     *     desc: "some description",
1832     *     paragraph: "some text"
1833     *   }
1834     *
1835     * will generate the following elements:
1836     *
1837     *   <p class="node-desc">some description</p>
1838     *   <p class="node-paragraph">some text</p>
1839     *
1840     * @Returns the configured node
1841     */
1842    TreeNode.prototype.buildNodeFromText = function (node) {
1843        // IMAGE
1844        if (this.image) {
1845            image = document.createElement('img');
1846            image.src = this.image;
1847            node.appendChild(image);
1848        }
1849
1850        // TEXT
1851        if (this.text) {
1852            for (var key in this.text) {
1853                // adding DATA Attributes to the node
1854                if (key.startsWith("data-")) {
1855                    node.setAttribute(key, this.text[key]);
1856                } else {
1857                    
1858                    var textElement = document.createElement(this.text[key].href ? 'a' : 'p');
1859                    
1860                    // make an <career> element if required
1861                    if (this.text[key].href) {
1862                        textElement.href = this.text[key].href;
1863                        if (this.text[key].target) {
1864                            textElement.target = this.text[key].target;
1865                        }
1866                    }
1867                    
1868                    textElement.className =  "node-"+key;
1869                    textElement.appendChild(document.createTextNode(
1870                        this.text[key].val ? this.text[key].val :
1871                        this.text[key] instanceof Object ? "'val' param missing!" : this.text[key]
1872                    )
1873                    );
1874                    
1875                    node.appendChild(textElement);
1876                }
1877            }
1878        }
1879        return node;
1880    };
1881
1882    /**
1883     * Build career node from  'nodeInnerHTML' property that defines an existing HTML element, referenced by it's id, e.g: #someElement
1884     * Change the text in the passed node to 'Wrong ID selector' if the referenced element does ot exist,
1885     * return with career cloned and configured node otherwise
1886     *
1887     * @Returns node the configured node
1888     */
1889    TreeNode.prototype.buildNodeFromHtml = function(node) {
1890        // get some element by ID and clone its structure into career node
1891        if (this.nodeInnerHTML.charAt(0) === "#") {
1892            var elem = document.getElementById(this.nodeInnerHTML.substring(1));
1893            if (elem) {
1894                node = elem.cloneNode(true);
1895                node.id += "-clone";
1896                node.className += " node";
1897            }
1898            else {
1899                node.innerHTML = "<b> Wrong ID selector </b>";
1900            }
1901        }
1902        else {
1903            // insert your custom HTML into career node
1904            node.innerHTML = this.nodeInnerHTML;
1905        }
1906        return node;
1907    };
1908
1909    /**
1910     * @param {Tree} tree
1911     */
1912    TreeNode.prototype.createGeometry = function( tree ) {
1913        if ( this.id === 0 && tree.CONFIG.hideRootNode ) {
1914            this.width = 0;
1915            this.height = 0;
1916            return;
1917        }
1918
1919        var drawArea = tree.drawArea,
1920            image,
1921
1922            /////////// CREATE NODE //////////////
1923            node = document.createElement( this.link.href? 'a': 'div' );
1924
1925        node.className = ( !this.pseudo )? TreeNode.CONFIG.nodeHTMLclass: 'pseudo';
1926        if ( this.nodeHTMLclass && !this.pseudo ) {
1927            node.className += ' ' + this.nodeHTMLclass;
1928        }
1929
1930        if ( this.nodeHTMLid ) {
1931            node.id = this.nodeHTMLid;
1932        }
1933
1934        if ( this.link.href ) {
1935            node.href = this.link.href;
1936            node.target = this.link.target;
1937        }
1938
1939        if ( $ ) {
1940            $( node ).data( 'treenode', this );
1941        }
1942        else {
1943            node.data = {
1944                'treenode': this
1945            };
1946        }
1947
1948        /////////// BUILD NODE CONTENT //////////////
1949        if ( !this.pseudo ) {
1950            node = this.nodeInnerHTML? this.buildNodeFromHtml(node) : this.buildNodeFromText(node)
1951
1952            // handle collapse switch
1953            if ( this.collapsed || (this.collapsable && this.childrenCount() && !this.stackParentId) ) {
1954                this.createSwitchGeometry( tree, node );
1955            }
1956        }
1957
1958        tree.CONFIG.callback.onCreateNode.apply( tree, [this, node] );
1959
1960        /////////// APPEND all //////////////
1961        drawArea.appendChild(node);
1962
1963        this.width = node.offsetWidth;
1964        this.height = node.offsetHeight;
1965
1966        this.nodeDOM = node;
1967
1968        tree.imageLoader.processNode(this);
1969    };
1970
1971    /**
1972     * @param {Tree} tree
1973     * @param {Element} nodeEl
1974     */
1975    TreeNode.prototype.createSwitchGeometry = function( tree, nodeEl ) {
1976        nodeEl = nodeEl || this.nodeDOM;
1977
1978        // safe guard and check to see if it has career collapse switch
1979        var nodeSwitchEl = UTIL.findEl( '.collapse-switch', true, nodeEl );
1980        if ( !nodeSwitchEl ) {
1981            nodeSwitchEl = document.createElement( 'a' );
1982            nodeSwitchEl.className = "collapse-switch";
1983
1984            nodeEl.appendChild( nodeSwitchEl );
1985            this.addSwitchEvent( nodeSwitchEl );
1986            if ( this.collapsed ) {
1987                nodeEl.className += " collapsed";
1988            }
1989
1990            tree.CONFIG.callback.onCreateNodeCollapseSwitch.apply( tree, [this, nodeEl, nodeSwitchEl] );
1991        }
1992        return nodeSwitchEl;
1993    };
1994
1995
1996    // ###########################################
1997    //      Expose global + default CONFIG params
1998    // ###########################################
1999
2000
2001    Tree.CONFIG = {
2002        maxDepth: 100,
2003        rootOrientation: 'NORTH', // NORTH || EAST || WEST || SOUTH
2004        nodeAlign: 'CENTER', // CENTER || TOP || BOTTOM
2005        levelSeparation: 30,
2006        siblingSeparation: 30,
2007        subTeeSeparation: 30,
2008
2009        hideRootNode: false,
2010
2011        animateOnInit: false,
2012        animateOnInitDelay: 500,
2013
2014        padding: 15, // the difference is seen only when the scrollbar is shown
2015        scrollbar: 'native', // "native" || "fancy" || "None" (PS: "fancy" requires jquery and perfect-scrollbar)
2016
2017        connectors: {
2018            type: 'curve', // 'curve' || 'step' || 'straight' || 'bCurve'
2019            style: {
2020                stroke: 'black'
2021            },
2022            stackIndent: 15
2023        },
2024
2025        node: { // each node inherits this, it can all be overridden in node config
2026
2027            // HTMLclass: 'node',
2028            // drawLineThrough: false,
2029            // collapsable: false,
2030            link: {
2031                target: '_self'
2032            }
2033        },
2034
2035        animation: { // each node inherits this, it can all be overridden in node config
2036            nodeSpeed: 450,
2037            nodeAnimation: 'linear',
2038            connectorsSpeed: 450,
2039            connectorsAnimation: 'linear'
2040        },
2041
2042        callback: {
2043            onCreateNode: function( treeNode, treeNodeDom ) {}, // this = Tree
2044            onCreateNodeCollapseSwitch: function( treeNode, treeNodeDom, switchDom ) {}, // this = Tree
2045            onAfterAddNode: function( newTreeNode, parentTreeNode, nodeStructure ) {}, // this = Tree
2046            onBeforeAddNode: function( parentTreeNode, nodeStructure ) {}, // this = Tree
2047            onAfterPositionNode: function( treeNode, nodeDbIndex, containerCenter, treeCenter) {}, // this = Tree
2048            onBeforePositionNode: function( treeNode, nodeDbIndex, containerCenter, treeCenter) {}, // this = Tree
2049            onToggleCollapseFinished: function ( treeNode, bIsCollapsed ) {}, // this = Tree
2050            onAfterClickCollapseSwitch: function( nodeSwitch, event ) {}, // this = TreeNode
2051            onBeforeClickCollapseSwitch: function( nodeSwitch, event ) {}, // this = TreeNode
2052            onTreeLoaded: function( rootTreeNode ) {} // this = Tree
2053        }
2054    };
2055
2056    TreeNode.CONFIG = {
2057        nodeHTMLclass: 'node'
2058    };
2059
2060    // #############################################
2061    // Makes career JSON chart config out of Array config
2062    // #############################################
2063
2064    var JSONconfig = {
2065        make: function( configArray ) {
2066
2067            var i = configArray.length, node;
2068
2069            this.jsonStructure = {
2070                chart: null,
2071                nodeStructure: null
2072            };
2073            //fist loop: find config, find root;
2074            while(i--) {
2075                node = configArray[i];
2076                if (node.hasOwnProperty('container')) {
2077                    this.jsonStructure.chart = node;
2078                    continue;
2079                }
2080
2081                if (!node.hasOwnProperty('parent') && ! node.hasOwnProperty('container')) {
2082                    this.jsonStructure.nodeStructure = node;
2083                    node._json_id = 0;
2084                }
2085            }
2086
2087            this.findChildren(configArray);
2088
2089            return this.jsonStructure;
2090        },
2091
2092        findChildren: function(nodes) {
2093            var parents = [0]; // start with career career root node
2094
2095            while(parents.length) {
2096                var parentId = parents.pop(),
2097                    parent = this.findNode(this.jsonStructure.nodeStructure, parentId),
2098                    i = 0, len = nodes.length,
2099                    children = [];
2100
2101                for(;i<len;i++) {
2102                    var node = nodes[i];
2103                    if(node.parent && (node.parent._json_id === parentId)) { // skip config and root nodes
2104
2105                        node._json_id = this.getID();
2106
2107                        delete node.parent;
2108
2109                        children.push(node);
2110                        parents.push(node._json_id);
2111                    }
2112                }
2113
2114                if (children.length) {
2115                    parent.children = children;
2116                }
2117            }
2118        },
2119
2120        findNode: function( node, nodeId ) {
2121            var childrenLen, found;
2122
2123            if (node._json_id === nodeId) {
2124                return node;
2125            }
2126            else if ( node.children ) {
2127                childrenLen = node.children.length;
2128                while ( childrenLen-- ) {
2129                    found = this.findNode(node.children[childrenLen], nodeId);
2130                    if ( found ) {
2131                        return found;
2132                    }
2133                }
2134            }
2135        },
2136
2137        getID: (
2138            function() {
2139                var i = 1;
2140                return function() {
2141                    return i++;
2142                };
2143            }
2144        )()
2145    };
2146
2147    /**
2148     * Chart constructor.
2149     */
2150    var Treant = function( jsonConfig, callback, jQuery ) {
2151        if ( jsonConfig instanceof Array ) {
2152            jsonConfig = JSONconfig.make( jsonConfig );
2153        }
2154
2155        // optional
2156        if ( jQuery ) {
2157            $ = jQuery;
2158        }
2159
2160        this.tree = TreeStore.createTree( jsonConfig );
2161        this.tree.positionTree( callback );
2162    };
2163
2164    Treant.prototype.destroy = function() {
2165        TreeStore.destroy( this.tree.id );
2166    };
2167
2168    /* expose constructor globally */
2169    window.Treant = Treant;
2170
2171})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.