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.