/* * Treant-js * * (c) 2013 Fran Peručić * Treant-js may be freely distributed under the MIT license. * For all details and documentation: * http://fperucic.github.io/treant-js * * Treant is an open-source JavaScipt library for visualization of tree diagrams. * It implements the node positioning algorithm of John Q. Walker II "Positioning nodes for General Trees". * * References: * Emilio Cortegoso Lobato: ECOTree.js v1.0 (October 26th, 2006) * * Contributors: * Fran Peručić, https://github.com/fperucic * Dave Goodchild, https://github.com/dlgoodchild */ ( function() { // Polyfill for IE to use startsWith if (!String.prototype.startsWith) { String.prototype.startsWith = function(searchString, position){ return this.substr(position || 0, searchString.length) === searchString; }; } var $ = null; var UTIL = { /** * Directly updates, recursively/deeply, the first object with all properties in the second object * @param {object} applyTo * @param {object} applyFrom * @return {object} */ inheritAttrs: function( applyTo, applyFrom ) { for ( var attr in applyFrom ) { if ( applyFrom.hasOwnProperty( attr ) ) { if ( ( applyTo[attr] instanceof Object && applyFrom[attr] instanceof Object ) && ( typeof applyFrom[attr] !== 'function' ) ) { this.inheritAttrs( applyTo[attr], applyFrom[attr] ); } else { applyTo[attr] = applyFrom[attr]; } } } return applyTo; }, /** * Returns a new object by merging the two supplied objects * @param {object} obj1 * @param {object} obj2 * @returns {object} */ createMerge: function( obj1, obj2 ) { var newObj = {}; if ( obj1 ) { this.inheritAttrs( newObj, this.cloneObj( obj1 ) ); } if ( obj2 ) { this.inheritAttrs( newObj, obj2 ); } return newObj; }, /** * Takes any number of arguments * @returns {*} */ extend: function() { if ( $ ) { Array.prototype.unshift.apply( arguments, [true, {}] ); return $.extend.apply( $, arguments ); } else { return UTIL.createMerge.apply( this, arguments ); } }, /** * @param {object} obj * @returns {*} */ cloneObj: function ( obj ) { if ( Object( obj ) !== obj ) { return obj; } var res = new obj.constructor(); for ( var key in obj ) { if ( obj.hasOwnProperty(key) ) { res[key] = this.cloneObj(obj[key]); } } return res; }, /** * @param {Element} el * @param {string} eventType * @param {function} handler */ addEvent: function( el, eventType, handler ) { if ( $ ) { $( el ).on( eventType+'.treant', handler ); } else if ( el.addEventListener ) { // DOM Level 2 browsers el.addEventListener( eventType, handler, false ); } else if ( el.attachEvent ) { // IE <= 8 el.attachEvent( 'on' + eventType, handler ); } else { // ancient browsers el['on' + eventType] = handler; } }, /** * @param {string} selector * @param {boolean} raw * @param {Element} parentEl * @returns {Element|jQuery} */ findEl: function( selector, raw, parentEl ) { parentEl = parentEl || document; if ( $ ) { var $element = $( selector, parentEl ); return ( raw? $element.get( 0 ): $element ); } else { // todo: getElementsByName() // todo: getElementsByTagName() // todo: getElementsByTagNameNS() if ( selector.charAt( 0 ) === '#' ) { return parentEl.getElementById( selector.substring( 1 ) ); } else if ( selector.charAt( 0 ) === '.' ) { var oElements = parentEl.getElementsByClassName( selector.substring( 1 ) ); return ( oElements.length? oElements[0]: null ); } throw new Error( 'Unknown container element' ); } }, getOuterHeight: function( element ) { var nRoundingCompensation = 1; if ( typeof element.getBoundingClientRect === 'function' ) { return element.getBoundingClientRect().height; } else if ( $ ) { return Math.ceil( $( element ).outerHeight() ) + nRoundingCompensation; } else { return Math.ceil( element.clientHeight + UTIL.getStyle( element, 'border-top-width', true ) + UTIL.getStyle( element, 'border-bottom-width', true ) + UTIL.getStyle( element, 'padding-top', true ) + UTIL.getStyle( element, 'padding-bottom', true ) + nRoundingCompensation, ); } }, getOuterWidth: function( element ) { var nRoundingCompensation = 1; if ( typeof element.getBoundingClientRect === 'function' ) { return element.getBoundingClientRect().width; } else if ( $ ) { return Math.ceil( $( element ).outerWidth() ) + nRoundingCompensation; } else { return Math.ceil( element.clientWidth + UTIL.getStyle( element, 'border-left-width', true ) + UTIL.getStyle( element, 'border-right-width', true ) + UTIL.getStyle( element, 'padding-left', true ) + UTIL.getStyle( element, 'padding-right', true ) + nRoundingCompensation, ); } }, getStyle: function( element, strCssRule, asInt ) { var strValue = ""; if ( document.defaultView && document.defaultView.getComputedStyle ) { strValue = document.defaultView.getComputedStyle( element, '' ).getPropertyValue( strCssRule ); } else if( element.currentStyle ) { strCssRule = strCssRule.replace(/\-(\w)/g, function (strMatch, p1){ return p1.toUpperCase(); }, ); strValue = element.currentStyle[strCssRule]; } //Number(elem.style.width.replace(/[^\d\.\-]/g, '')); return ( asInt? parseFloat( strValue ): strValue ); }, addClass: function( element, cssClass ) { if ( $ ) { $( element ).addClass( cssClass ); } else { if ( !UTIL.hasClass( element, cssClass ) ) { if ( element.classList ) { element.classList.add( cssClass ); } else { element.className += " "+cssClass; } } } }, hasClass: function(element, my_class) { return (" " + element.className + " ").replace(/[\n\t]/g, " ").indexOf(" "+my_class+" ") > -1; }, toggleClass: function ( element, cls, apply ) { if ( $ ) { $( element ).toggleClass( cls, apply ); } else { if ( apply ) { //element.className += " "+cls; element.classList.add( cls ); } else { element.classList.remove( cls ); } } }, setDimensions: function( element, width, height ) { if ( $ ) { $( element ).width( width ).height( height ); } else { element.style.width = width+'px'; element.style.height = height+'px'; } }, isjQueryAvailable: function() {return(typeof ($) !== 'undefined' && $);}, }; /** * ImageLoader is used for determining if all the images from the Tree are loaded. * Node size (width, height) can be correctly determined only when all inner images are loaded */ var ImageLoader = function() { this.reset(); }; ImageLoader.prototype = { /** * @returns {ImageLoader} */ reset: function() { this.loading = []; return this; }, /** * @param {TreeNode} node * @returns {ImageLoader} */ processNode: function( node ) { var aImages = node.nodeDOM.getElementsByTagName( 'img' ); var i = aImages.length; while ( i-- ) { this.create( node, aImages[i] ); } return this; }, /** * @returns {ImageLoader} */ removeAll: function( img_src ) { var i = this.loading.length; while ( i-- ) { if ( this.loading[i] === img_src ) { this.loading.splice( i, 1 ); } } return this; }, /** * @param {TreeNode} node * @param {Element} image * @returns {*} */ create: function ( node, image ) { var self = this, source = image.src; function imgTrigger() { self.removeAll( source ); node.width = node.nodeDOM.offsetWidth; node.height = node.nodeDOM.offsetHeight; } if ( image.src.indexOf( 'data:' ) !== 0 ) { this.loading.push( source ); if ( image.complete ) { return imgTrigger(); } UTIL.addEvent( image, 'load', imgTrigger ); UTIL.addEvent( image, 'error', imgTrigger ); // handle broken url-s // load event is not fired for cached images, force the load event image.src += ( ( image.src.indexOf( '?' ) > 0)? '&': '?' ) + new Date().getTime(); } else { imgTrigger(); } }, /** * @returns {boolean} */ isNotLoading: function() { return ( this.loading.length === 0 ); }, }; /** * Class: TreeStore * TreeStore is used for holding initialized Tree objects * Its purpose is to avoid global variables and enable multiple Trees on the page. */ var TreeStore = { store: [], /** * @param {object} jsonConfig * @returns {Tree} */ createTree: function( jsonConfig ) { var nNewTreeId = this.store.length; this.store.push( new Tree( jsonConfig, nNewTreeId ) ); return this.get( nNewTreeId ); }, /** * @param {number} treeId * @returns {Tree} */ get: function ( treeId ) { return this.store[treeId]; }, /** * @param {number} treeId * @returns {TreeStore} */ destroy: function( treeId ) { var tree = this.get( treeId ); if ( tree ) { tree._R.remove(); var draw_area = tree.drawArea; while ( draw_area.firstChild ) { draw_area.removeChild( draw_area.firstChild ); } var classes = draw_area.className.split(' '), classes_to_stay = []; for ( var i = 0; i < classes.length; i++ ) { var cls = classes[i]; if ( cls !== 'Treant' && cls !== 'Treant-loaded' ) { classes_to_stay.push(cls); } } draw_area.style.overflowY = ''; draw_area.style.overflowX = ''; draw_area.className = classes_to_stay.join(' '); this.store[treeId] = null; } return this; }, }; /** * Tree constructor. * @param {object} jsonConfig * @param {number} treeId * @constructor */ var Tree = function (jsonConfig, treeId ) { /** * @param {object} jsonConfig * @param {number} treeId * @returns {Tree} */ this.reset = function( jsonConfig, treeId ) { this.initJsonConfig = jsonConfig; this.initTreeId = treeId; this.id = treeId; this.CONFIG = UTIL.extend( Tree.CONFIG, jsonConfig.chart ); this.drawArea = UTIL.findEl( this.CONFIG.container, true ); if ( !this.drawArea ) { throw new Error( 'Failed to find element by selector "'+this.CONFIG.container+'"' ); } UTIL.addClass( this.drawArea, 'Treant' ); // kill of any child elements that may be there this.drawArea.innerHTML = ''; this.imageLoader = new ImageLoader(); this.nodeDB = new NodeDB( jsonConfig.nodeStructure, this ); // key store for storing reference to node connectors, // key = nodeId where the connector ends this.connectionStore = {}; this.loaded = false; this._R = new Raphael( this.drawArea, 100, 100 ); return this; }; /** * @returns {Tree} */ this.reload = function() { this.reset( this.initJsonConfig, this.initTreeId ).redraw(); return this; }; this.reset( jsonConfig, treeId ); }; Tree.prototype = { /** * @returns {NodeDB} */ getNodeDb: function() { return this.nodeDB; }, /** * @param {TreeNode} parentTreeNode * @param {object} nodeDefinition * @returns {TreeNode} */ addNode: function( parentTreeNode, nodeDefinition ) { var dbEntry = this.nodeDB.get( parentTreeNode.id ); this.CONFIG.callback.onBeforeAddNode.apply( this, [parentTreeNode, nodeDefinition] ); var oNewNode = this.nodeDB.createNode( nodeDefinition, parentTreeNode.id, this ); oNewNode.createGeometry( this ); oNewNode.parent().createSwitchGeometry( this ); this.positionTree(); this.CONFIG.callback.onAfterAddNode.apply( this, [oNewNode, parentTreeNode, nodeDefinition] ); return oNewNode; }, /** * @returns {Tree} */ redraw: function() { this.positionTree(); return this; }, /** * @param {function} callback * @returns {Tree} */ positionTree: function( callback ) { var self = this; if ( this.imageLoader.isNotLoading() ) { var root = this.root(), orient = this.CONFIG.rootOrientation; this.resetLevelData(); this.firstWalk( root, 0 ); this.secondWalk( root, 0, 0, 0 ); this.positionNodes(); if ( this.CONFIG.animateOnInit ) { setTimeout( function() { root.toggleCollapse(); }, this.CONFIG.animateOnInitDelay, ); } if ( !this.loaded ) { UTIL.addClass( this.drawArea, 'Treant-loaded' ); // nodes are hidden until .loaded class is added if ( Object.prototype.toString.call( callback ) === "[object Function]" ) { callback( self ); } self.CONFIG.callback.onTreeLoaded.apply( self, [root] ); this.loaded = true; } } else { setTimeout( function() { self.positionTree( callback ); }, 10, ); } return this; }, /** * In a first post-order walk, every node of the tree is assigned a preliminary * x-coordinate (held in field node->prelim). * In addition, internal nodes are given modifiers, which will be used to move their * children to the right (held in field node->modifier). * @param {TreeNode} node * @param {number} level * @returns {Tree} */ firstWalk: function( node, level ) { node.prelim = null; node.modifier = null; this.setNeighbors( node, level ); this.calcLevelDim( node, level ); var leftSibling = node.leftSibling(); if ( node.childrenCount() === 0 || level == this.CONFIG.maxDepth ) { // set preliminary x-coordinate if ( leftSibling ) { node.prelim = leftSibling.prelim + leftSibling.size() + this.CONFIG.siblingSeparation; } else { node.prelim = 0; } } else { //node is not a leaf, firstWalk for each child for ( var i = 0, n = node.childrenCount(); i < n; i++ ) { this.firstWalk(node.childAt(i), level + 1); } var midPoint = node.childrenCenter() - node.size() / 2; if ( leftSibling ) { node.prelim = leftSibling.prelim + leftSibling.size() + this.CONFIG.siblingSeparation; node.modifier = node.prelim - midPoint; this.apportion( node, level ); } else { node.prelim = midPoint; } // handle stacked children positioning if ( node.stackParent ) { // handle the parent of stacked children node.modifier += this.nodeDB.get( node.stackChildren[0] ).size()/2 + node.connStyle.stackIndent; } else if ( node.stackParentId ) { // handle stacked children node.prelim = 0; } } return this; }, /* * Clean up the positioning of small sibling subtrees. * Subtrees of a node are formed independently and * placed as close together as possible. By requiring * that the subtrees be rigid at the time they are put * together, we avoid the undesirable effects that can * accrue from positioning nodes rather than subtrees. */ apportion: function (node, level) { var firstChild = node.firstChild(), firstChildLeftNeighbor = firstChild.leftNeighbor(), compareDepth = 1, depthToStop = this.CONFIG.maxDepth - level; while( firstChild && firstChildLeftNeighbor && compareDepth <= depthToStop ) { // calculate the position of the firstChild, according to the position of firstChildLeftNeighbor var modifierSumRight = 0, modifierSumLeft = 0, leftAncestor = firstChildLeftNeighbor, rightAncestor = firstChild; for ( var i = 0; i < compareDepth; i++ ) { leftAncestor = leftAncestor.parent(); rightAncestor = rightAncestor.parent(); modifierSumLeft += leftAncestor.modifier; modifierSumRight += rightAncestor.modifier; // all the stacked children are oriented towards right so use right variables if ( rightAncestor.stackParent !== undefined ) { modifierSumRight += rightAncestor.size() / 2; } } // find the gap between two trees and apply it to subTrees // and mathing smaller gaps to smaller subtrees var totalGap = (firstChildLeftNeighbor.prelim + modifierSumLeft + firstChildLeftNeighbor.size() + this.CONFIG.subTeeSeparation) - (firstChild.prelim + modifierSumRight ); if ( totalGap > 0 ) { var subtreeAux = node, numSubtrees = 0; // count all the subtrees in the LeftSibling while ( subtreeAux && subtreeAux.id !== leftAncestor.id ) { subtreeAux = subtreeAux.leftSibling(); numSubtrees++; } if ( subtreeAux ) { var subtreeMoveAux = node, singleGap = totalGap / numSubtrees; while ( subtreeMoveAux.id !== leftAncestor.id ) { subtreeMoveAux.prelim += totalGap; subtreeMoveAux.modifier += totalGap; totalGap -= singleGap; subtreeMoveAux = subtreeMoveAux.leftSibling(); } } } compareDepth++; firstChild = ( firstChild.childrenCount() === 0 )? node.leftMost(0, compareDepth): firstChild = firstChild.firstChild(); if ( firstChild ) { firstChildLeftNeighbor = firstChild.leftNeighbor(); } } }, /* * During a second pre-order walk, each node is given a * final x-coordinate by summing its preliminary * x-coordinate and the modifiers of all the node's * ancestors. The y-coordinate depends on the height of * the tree. (The roles of x and y are reversed for * RootOrientations of EAST or WEST.) */ secondWalk: function( node, level, X, Y ) { if ( level <= this.CONFIG.maxDepth ) { var xTmp = node.prelim + X, yTmp = Y, align = this.CONFIG.nodeAlign, orient = this.CONFIG.rootOrientation, levelHeight, nodesizeTmp; if (orient === 'NORTH' || orient === 'SOUTH') { levelHeight = this.levelMaxDim[level].height; nodesizeTmp = node.height; if (node.pseudo) { node.height = levelHeight; } // assign a new size to pseudo nodes } else if (orient === 'WEST' || orient === 'EAST') { levelHeight = this.levelMaxDim[level].width; nodesizeTmp = node.width; if (node.pseudo) { node.width = levelHeight; } // assign a new size to pseudo nodes } node.X = xTmp; if (node.pseudo) { // pseudo nodes need to be properly aligned, otherwise position is not correct in some examples if (orient === 'NORTH' || orient === 'WEST') { node.Y = yTmp; // align "BOTTOM" } else if (orient === 'SOUTH' || orient === 'EAST') { node.Y = (yTmp + (levelHeight - nodesizeTmp)); // align "TOP" } } else { node.Y = ( align === 'CENTER' ) ? (yTmp + (levelHeight - nodesizeTmp) / 2) : ( align === 'TOP' ) ? (yTmp + (levelHeight - nodesizeTmp)) : yTmp; } if ( orient === 'WEST' || orient === 'EAST' ) { var swapTmp = node.X; node.X = node.Y; node.Y = swapTmp; } if (orient === 'SOUTH' ) { node.Y = -node.Y - nodesizeTmp; } else if ( orient === 'EAST' ) { node.X = -node.X - nodesizeTmp; } if ( node.childrenCount() !== 0 ) { if ( node.id === 0 && this.CONFIG.hideRootNode ) { // ako je root node Hiden onda nemoj njegovu dijecu pomaknut po Y osi za Level separation, neka ona budu na vrhu this.secondWalk(node.firstChild(), level + 1, X + node.modifier, Y); } else { this.secondWalk(node.firstChild(), level + 1, X + node.modifier, Y + levelHeight + this.CONFIG.levelSeparation); } } if ( node.rightSibling() ) { this.secondWalk( node.rightSibling(), level, X, Y ); } } }, /** * position all the nodes, center the tree in center of its container * 0,0 coordinate is in the upper left corner * @returns {Tree} */ positionNodes: function() { var self = this, treeSize = { x: self.nodeDB.getMinMaxCoord('X', null, null), y: self.nodeDB.getMinMaxCoord('Y', null, null), }, treeWidth = treeSize.x.max - treeSize.x.min, treeHeight = treeSize.y.max - treeSize.y.min, treeCenter = { x: treeSize.x.max - treeWidth/2, y: treeSize.y.max - treeHeight/2, }; this.handleOverflow(treeWidth, treeHeight); var containerCenter = { x: self.drawArea.clientWidth/2, y: self.drawArea.clientHeight/2, }, deltaX = containerCenter.x - treeCenter.x, deltaY = containerCenter.y - treeCenter.y, // all nodes must have positive X or Y coordinates, handle this with offsets negOffsetX = ((treeSize.x.min + deltaX) <= 0) ? Math.abs(treeSize.x.min) : 0, negOffsetY = ((treeSize.y.min + deltaY) <= 0) ? Math.abs(treeSize.y.min) : 0, i, len, node; // position all the nodes for ( i = 0, len = this.nodeDB.db.length; i < len; i++ ) { node = this.nodeDB.get(i); self.CONFIG.callback.onBeforePositionNode.apply( self, [node, i, containerCenter, treeCenter] ); if ( node.id === 0 && this.CONFIG.hideRootNode ) { self.CONFIG.callback.onAfterPositionNode.apply( self, [node, i, containerCenter, treeCenter] ); continue; } // if the tree is smaller than the draw area, then center the tree within drawing area node.X += negOffsetX + ((treeWidth < this.drawArea.clientWidth) ? deltaX : this.CONFIG.padding); node.Y += negOffsetY + ((treeHeight < this.drawArea.clientHeight) ? deltaY : this.CONFIG.padding); var collapsedParent = node.collapsedParent(), hidePoint = null; if (collapsedParent) { // position the node behind the connector point of the parent, so future animations can be visible hidePoint = collapsedParent.connectorPoint( true ); node.hide(hidePoint); } else if (node.positioned) { // node is already positioned, node.show(); } else { // inicijalno stvaranje nodeova, postavi lokaciju node.nodeDOM.style.left = node.X + 'px'; node.nodeDOM.style.top = node.Y + 'px'; node.positioned = true; } if (node.id !== 0 && !(node.parent().id === 0 && this.CONFIG.hideRootNode)) { this.setConnectionToParent(node, hidePoint); // skip the root node } else if (!this.CONFIG.hideRootNode && node.drawLineThrough) { // drawlinethrough is performed for for the root node also node.drawLineThroughMe(); } self.CONFIG.callback.onAfterPositionNode.apply( self, [node, i, containerCenter, treeCenter] ); } return this; }, /** * Create Raphael instance, (optionally set scroll bars if necessary) * @param {number} treeWidth * @param {number} treeHeight * @returns {Tree} */ handleOverflow: function( treeWidth, treeHeight ) { var viewWidth = (treeWidth < this.drawArea.clientWidth) ? this.drawArea.clientWidth : treeWidth + this.CONFIG.padding*2, viewHeight = (treeHeight < this.drawArea.clientHeight) ? this.drawArea.clientHeight : treeHeight + this.CONFIG.padding*2; this._R.setSize( viewWidth, viewHeight ); if ( this.CONFIG.scrollbar === 'resize') { UTIL.setDimensions( this.drawArea, viewWidth, viewHeight ); } else if ( !UTIL.isjQueryAvailable() || this.CONFIG.scrollbar === 'native' ) { if ( this.drawArea.clientWidth < treeWidth ) { // is overflow-x necessary this.drawArea.style.overflowX = "auto"; } if ( this.drawArea.clientHeight < treeHeight ) { // is overflow-y necessary this.drawArea.style.overflowY = "auto"; } } // Fancy scrollbar relies heavily on jQuery, so guarding with if ( $ ) else if ( this.CONFIG.scrollbar === 'fancy') { var jq_drawArea = $( this.drawArea ); if (jq_drawArea.hasClass('ps-container')) { // znaci da je 'fancy' vec inicijaliziran, treba updateat jq_drawArea.find('.Treant').css({ width: viewWidth, height: viewHeight, }); jq_drawArea.perfectScrollbar('update'); } else { var mainContainer = jq_drawArea.wrapInner('
'), child = mainContainer.find('.Treant'); child.css({ width: viewWidth, height: viewHeight, }); mainContainer.perfectScrollbar(); } } // else this.CONFIG.scrollbar == 'None' return this; }, /** * @param {TreeNode} treeNode * @param {boolean} hidePoint * @returns {Tree} */ setConnectionToParent: function( treeNode, hidePoint ) { var stacked = treeNode.stackParentId, connLine, parent = ( stacked? this.nodeDB.get( stacked ): treeNode.parent() ), pathString = hidePoint? this.getPointPathString(hidePoint): this.getPathString(parent, treeNode, stacked); if ( this.connectionStore[treeNode.id] ) { // connector already exists, update the connector geometry connLine = this.connectionStore[treeNode.id]; this.animatePath( connLine, pathString ); } else { connLine = this._R.path( pathString ); this.connectionStore[treeNode.id] = connLine; // don't show connector arrows por pseudo nodes if ( treeNode.pseudo ) { delete parent.connStyle.style['arrow-end']; } if ( parent.pseudo ) { delete parent.connStyle.style['arrow-start']; } connLine.attr( parent.connStyle.style ); if ( treeNode.drawLineThrough || treeNode.pseudo ) { treeNode.drawLineThroughMe( hidePoint ); } } treeNode.connector = connLine; return this; }, /** * Create the path which is represented as a point, used for hiding the connection * A path with a leading "_" indicates the path will be hidden * See: http://dmitrybaranovskiy.github.io/raphael/reference.html#Paper.path * @param {object} hidePoint * @returns {string} */ getPointPathString: function( hidePoint ) { return ["_M", hidePoint.x, ",", hidePoint.y, 'L', hidePoint.x, ",", hidePoint.y, hidePoint.x, ",", hidePoint.y].join(' '); }, /** * This method relied on receiving a valid Raphael Paper.path. * See: http://dmitrybaranovskiy.github.io/raphael/reference.html#Paper.path * A pathString is typically in the format of "M10,20L30,40" * @param path * @param {string} pathString * @returns {Tree} */ animatePath: function( path, pathString ) { if (path.hidden && pathString.charAt(0) !== "_") { // path will be shown, so show it path.show(); path.hidden = false; } // See: http://dmitrybaranovskiy.github.io/raphael/reference.html#Element.animate path.animate( { path: pathString.charAt(0) === "_"? pathString.substring(1): pathString, // remove the "_" prefix if it exists }, this.CONFIG.animation.connectorsSpeed, this.CONFIG.animation.connectorsAnimation, function() { if ( pathString.charAt(0) === "_" ) { // animation is hiding the path, hide it at the and of animation path.hide(); path.hidden = true; } }, ); return this; }, /** * * @param {TreeNode} from_node * @param {TreeNode} to_node * @param {boolean} stacked * @returns {string} */ getPathString: function( from_node, to_node, stacked ) { var startPoint = from_node.connectorPoint( true ), endPoint = to_node.connectorPoint( false ), orientation = this.CONFIG.rootOrientation, connType = from_node.connStyle.type, P1 = {}, P2 = {}; if ( orientation === 'NORTH' || orientation === 'SOUTH' ) { P1.y = P2.y = (startPoint.y + endPoint.y) / 2; P1.x = startPoint.x; P2.x = endPoint.x; } else if ( orientation === 'EAST' || orientation === 'WEST' ) { P1.x = P2.x = (startPoint.x + endPoint.x) / 2; P1.y = startPoint.y; P2.y = endPoint.y; } // sp, p1, pm, p2, ep == "x,y" var sp = startPoint.x+','+startPoint.y, p1 = P1.x+','+P1.y, p2 = P2.x+','+P2.y, ep = endPoint.x+','+endPoint.y, pm = (P1.x + P2.x)/2 +','+ (P1.y + P2.y)/2, pathString, stackPoint; if ( stacked ) { // STACKED CHILDREN stackPoint = (orientation === 'EAST' || orientation === 'WEST')? endPoint.x+','+startPoint.y: startPoint.x+','+endPoint.y; if ( connType === "step" || connType === "straight" ) { pathString = ["M", sp, 'L', stackPoint, 'L', ep]; } else if ( connType === "curve" || connType === "bCurve" ) { var helpPoint, // used for nicer curve lines indent = from_node.connStyle.stackIndent; if ( orientation === 'NORTH' ) { helpPoint = (endPoint.x - indent)+','+(endPoint.y - indent); } else if ( orientation === 'SOUTH' ) { helpPoint = (endPoint.x - indent)+','+(endPoint.y + indent); } else if ( orientation === 'EAST' ) { helpPoint = (endPoint.x + indent) +','+startPoint.y; } else if ( orientation === 'WEST' ) { helpPoint = (endPoint.x - indent) +','+startPoint.y; } pathString = ["M", sp, 'L', helpPoint, 'S', stackPoint, ep]; } } else { // NORMAL CHILDREN if ( connType === "step" ) { pathString = ["M", sp, 'L', p1, 'L', p2, 'L', ep]; } else if ( connType === "curve" ) { pathString = ["M", sp, 'C', p1, p2, ep ]; } else if ( connType === "bCurve" ) { pathString = ["M", sp, 'Q', p1, pm, 'T', ep]; } else if (connType === "straight" ) { pathString = ["M", sp, 'L', sp, ep]; } } return pathString.join(" "); }, /** * Algorithm works from left to right, so previous processed node will be left neighbour of the next node * @param {TreeNode} node * @param {number} level * @returns {Tree} */ setNeighbors: function( node, level ) { node.leftNeighborId = this.lastNodeOnLevel[level]; if ( node.leftNeighborId ) { node.leftNeighbor().rightNeighborId = node.id; } this.lastNodeOnLevel[level] = node.id; return this; }, /** * Used for calculation of height and width of a level (level dimensions) * @param {TreeNode} node * @param {number} level * @returns {Tree} */ calcLevelDim: function( node, level ) { // root node is on level 0 this.levelMaxDim[level] = { width: Math.max( this.levelMaxDim[level]? this.levelMaxDim[level].width: 0, node.width ), height: Math.max( this.levelMaxDim[level]? this.levelMaxDim[level].height: 0, node.height ), }; return this; }, /** * @returns {Tree} */ resetLevelData: function() { this.lastNodeOnLevel = []; this.levelMaxDim = []; return this; }, /** * @returns {TreeNode} */ root: function() { return this.nodeDB.get( 0 ); }, }; /** * NodeDB is used for storing the nodes. Each tree has its own NodeDB. * @param {object} nodeStructure * @param {Tree} tree * @constructor */ var NodeDB = function ( nodeStructure, tree ) { this.reset( nodeStructure, tree ); }; NodeDB.prototype = { /** * @param {object} nodeStructure * @param {Tree} tree * @returns {NodeDB} */ reset: function( nodeStructure, tree ) { this.db = []; var self = this; /** * @param {object} node * @param {number} parentId */ function iterateChildren( node, parentId ) { var newNode = self.createNode( node, parentId, tree, null ); if ( node.children ) { // pseudo node is used for descending children to the next level if ( node.childrenDropLevel && node.childrenDropLevel > 0 ) { while ( node.childrenDropLevel-- ) { // pseudo node needs to inherit the connection style from its parent for continuous connectors var connStyle = UTIL.cloneObj( newNode.connStyle ); newNode = self.createNode( 'pseudo', newNode.id, tree, null ); newNode.connStyle = connStyle; newNode.children = []; } } var stack = ( node.stackChildren && !self.hasGrandChildren( node ) )? newNode.id: null; // children are positioned on separate levels, one beneath the other if ( stack !== null ) { newNode.stackChildren = []; } for ( var i = 0, len = node.children.length; i < len ; i++ ) { if ( stack !== null ) { newNode = self.createNode( node.children[i], newNode.id, tree, stack ); if ( ( i + 1 ) < len ) { // last node cant have children newNode.children = []; } } else { iterateChildren( node.children[i], newNode.id ); } } } } if ( tree.CONFIG.animateOnInit ) { nodeStructure.collapsed = true; } iterateChildren( nodeStructure, -1 ); // root node this.createGeometries( tree ); return this; }, /** * @param {Tree} tree * @returns {NodeDB} */ createGeometries: function( tree ) { var i = this.db.length; while ( i-- ) { this.get( i ).createGeometry( tree ); } return this; }, /** * @param {number} nodeId * @returns {TreeNode} */ get: function ( nodeId ) { return this.db[nodeId]; // get TreeNode by ID }, /** * @param {function} callback * @returns {NodeDB} */ walk: function( callback ) { var i = this.db.length; while ( i-- ) { callback.apply( this, [ this.get( i ) ] ); } return this; }, /** * * @param {object} nodeStructure * @param {number} parentId * @param {Tree} tree * @param {number} stackParentId * @returns {TreeNode} */ createNode: function( nodeStructure, parentId, tree, stackParentId ) { var node = new TreeNode( nodeStructure, this.db.length, parentId, tree, stackParentId ); this.db.push( node ); // skip root node (0) if ( parentId >= 0 ) { var parent = this.get( parentId ); // todo: refactor into separate private method if ( nodeStructure.position ) { if ( nodeStructure.position === 'left' ) { parent.children.push( node.id ); } else if ( nodeStructure.position === 'right' ) { parent.children.splice( 0, 0, node.id ); } else if ( nodeStructure.position === 'center' ) { parent.children.splice( Math.floor( parent.children.length / 2 ), 0, node.id ); } else { // edge case when there's only 1 child var position = parseInt( nodeStructure.position ); if ( parent.children.length === 1 && position > 0 ) { parent.children.splice( 0, 0, node.id ); } else { parent.children.splice( Math.max( position, parent.children.length - 1 ), 0, node.id, ); } } } else { parent.children.push( node.id ); } } if ( stackParentId ) { this.get( stackParentId ).stackParent = true; this.get( stackParentId ).stackChildren.push( node.id ); } return node; }, getMinMaxCoord: function( dim, parent, MinMax ) { // used for getting the dimensions of the tree, dim = 'X' || 'Y' // looks for min and max (X and Y) within the set of nodes parent = parent || this.get(0); MinMax = MinMax || { // start with root node dimensions min: parent[dim], max: parent[dim] + ( ( dim === 'X' )? parent.width: parent.height ), }; var i = parent.childrenCount(); while ( i-- ) { var node = parent.childAt( i ), maxTest = node[dim] + ( ( dim === 'X' )? node.width: node.height ), minTest = node[dim]; if ( maxTest > MinMax.max ) { MinMax.max = maxTest; } if ( minTest < MinMax.min ) { MinMax.min = minTest; } this.getMinMaxCoord( dim, node, MinMax ); } return MinMax; }, /** * @param {object} nodeStructure * @returns {boolean} */ hasGrandChildren: function( nodeStructure ) { var i = nodeStructure.children.length; while ( i-- ) { if ( nodeStructure.children[i].children ) { return true; } } return false; }, }; /** * TreeNode constructor. * @param {object} nodeStructure * @param {number} id * @param {number} parentId * @param {Tree} tree * @param {number} stackParentId * @constructor */ var TreeNode = function( nodeStructure, id, parentId, tree, stackParentId ) { this.reset( nodeStructure, id, parentId, tree, stackParentId ); }; TreeNode.prototype = { /** * @param {object} nodeStructure * @param {number} id * @param {number} parentId * @param {Tree} tree * @param {number} stackParentId * @returns {TreeNode} */ reset: function( nodeStructure, id, parentId, tree, stackParentId ) { this.id = id; this.parentId = parentId; this.treeId = tree.id; this.prelim = 0; this.modifier = 0; this.leftNeighborId = null; this.stackParentId = stackParentId; // pseudo node is a node with width=height=0, it is invisible, but necessary for the correct positioning of the tree this.pseudo = nodeStructure === 'pseudo' || nodeStructure['pseudo']; // todo: surely if nodeStructure is a scalar then the rest will error: this.meta = nodeStructure.meta || {}; this.image = nodeStructure.image || null; this.link = UTIL.createMerge( tree.CONFIG.node.link, nodeStructure.link ); this.connStyle = UTIL.createMerge( tree.CONFIG.connectors, nodeStructure.connectors ); this.connector = null; this.drawLineThrough = nodeStructure.drawLineThrough === false ? false : ( nodeStructure.drawLineThrough || tree.CONFIG.node.drawLineThrough ); this.collapsable = nodeStructure.collapsable === false ? false : ( nodeStructure.collapsable || tree.CONFIG.node.collapsable ); this.collapsed = nodeStructure.collapsed; this.text = nodeStructure.text; // '.node' DIV this.nodeInnerHTML = nodeStructure.innerHTML; this.nodeHTMLclass = (tree.CONFIG.node.HTMLclass ? tree.CONFIG.node.HTMLclass : '') + // globally defined class for the nodex (nodeStructure.HTMLclass ? (' ' + nodeStructure.HTMLclass) : ''); // + specific node class this.nodeHTMLid = nodeStructure.HTMLid; this.children = []; return this; }, /** * @returns {Tree} */ getTree: function() { return TreeStore.get( this.treeId ); }, /** * @returns {object} */ getTreeConfig: function() { return this.getTree().CONFIG; }, /** * @returns {NodeDB} */ getTreeNodeDb: function() { return this.getTree().getNodeDb(); }, /** * @param {number} nodeId * @returns {TreeNode} */ lookupNode: function( nodeId ) { return this.getTreeNodeDb().get( nodeId ); }, /** * @returns {Tree} */ Tree: function() { return TreeStore.get( this.treeId ); }, /** * @param {number} nodeId * @returns {TreeNode} */ dbGet: function( nodeId ) { return this.getTreeNodeDb().get( nodeId ); }, /** * Returns the width of the node * @returns {float} */ size: function() { var orientation = this.getTreeConfig().rootOrientation; if ( this.pseudo ) { // prevents separating the subtrees return ( -this.getTreeConfig().subTeeSeparation ); } if ( orientation === 'NORTH' || orientation === 'SOUTH' ) { return this.width; } else if ( orientation === 'WEST' || orientation === 'EAST' ) { return this.height; } }, /** * @returns {number} */ childrenCount: function () { return ( ( this.collapsed || !this.children)? 0: this.children.length ); }, /** * @param {number} index * @returns {TreeNode} */ childAt: function( index ) { return this.dbGet( this.children[index] ); }, /** * @returns {TreeNode} */ firstChild: function() { return this.childAt( 0 ); }, /** * @returns {TreeNode} */ lastChild: function() { return this.childAt( this.children.length - 1 ); }, /** * @returns {TreeNode} */ parent: function() { return this.lookupNode( this.parentId ); }, /** * @returns {TreeNode} */ leftNeighbor: function() { if ( this.leftNeighborId ) { return this.lookupNode( this.leftNeighborId ); } }, /** * @returns {TreeNode} */ rightNeighbor: function() { if ( this.rightNeighborId ) { return this.lookupNode( this.rightNeighborId ); } }, /** * @returns {TreeNode} */ leftSibling: function () { var leftNeighbor = this.leftNeighbor(); if ( leftNeighbor && leftNeighbor.parentId === this.parentId ){ return leftNeighbor; } }, /** * @returns {TreeNode} */ rightSibling: function () { var rightNeighbor = this.rightNeighbor(); if ( rightNeighbor && rightNeighbor.parentId === this.parentId ) { return rightNeighbor; } }, /** * @returns {number} */ childrenCenter: function () { var first = this.firstChild(), last = this.lastChild(); return ( first.prelim + ((last.prelim - first.prelim) + last.size()) / 2 ); }, /** * Find out if one of the node ancestors is collapsed * @returns {*} */ collapsedParent: function() { var parent = this.parent(); if ( !parent ) { return false; } if ( parent.collapsed ) { return parent; } return parent.collapsedParent(); }, /** * Returns the leftmost child at specific level, (initial level = 0) * @param level * @param depth * @returns {*} */ leftMost: function ( level, depth ) { if ( level >= depth ) { return this; } if ( this.childrenCount() === 0 ) { return; } for ( var i = 0, n = this.childrenCount(); i < n; i++ ) { var leftmostDescendant = this.childAt( i ).leftMost( level + 1, depth ); if ( leftmostDescendant ) { return leftmostDescendant; } } }, // returns start or the end point of the connector line, origin is upper-left connectorPoint: function(startPoint) { var orient = this.Tree().CONFIG.rootOrientation, point = {}; if ( this.stackParentId ) { // return different end point if node is a stacked child if ( orient === 'NORTH' || orient === 'SOUTH' ) { orient = 'WEST'; } else if ( orient === 'EAST' || orient === 'WEST' ) { orient = 'NORTH'; } } // if pseudo, a virtual center is used if ( orient === 'NORTH' ) { point.x = (this.pseudo) ? this.X - this.Tree().CONFIG.subTeeSeparation/2 : this.X + this.width/2; point.y = (startPoint) ? this.Y + this.height : this.Y; } else if (orient === 'SOUTH') { point.x = (this.pseudo) ? this.X - this.Tree().CONFIG.subTeeSeparation/2 : this.X + this.width/2; point.y = (startPoint) ? this.Y : this.Y + this.height; } else if (orient === 'EAST') { point.x = (startPoint) ? this.X : this.X + this.width; point.y = (this.pseudo) ? this.Y - this.Tree().CONFIG.subTeeSeparation/2 : this.Y + this.height/2; } else if (orient === 'WEST') { point.x = (startPoint) ? this.X + this.width : this.X; point.y = (this.pseudo) ? this.Y - this.Tree().CONFIG.subTeeSeparation/2 : this.Y + this.height/2; } return point; }, /** * @returns {string} */ pathStringThrough: function() { // get the geometry of a path going through the node var startPoint = this.connectorPoint( true ), endPoint = this.connectorPoint( false ); return ["M", startPoint.x+","+startPoint.y, 'L', endPoint.x+","+endPoint.y].join(" "); }, /** * @param {object} hidePoint */ drawLineThroughMe: function( hidePoint ) { // hidepoint se proslijedjuje ako je node sakriven zbog collapsed var pathString = hidePoint? this.Tree().getPointPathString( hidePoint ): this.pathStringThrough(); this.lineThroughMe = this.lineThroughMe || this.Tree()._R.path(pathString); var line_style = UTIL.cloneObj( this.connStyle.style ); delete line_style['arrow-start']; delete line_style['arrow-end']; this.lineThroughMe.attr( line_style ); if ( hidePoint ) { this.lineThroughMe.hide(); this.lineThroughMe.hidden = true; } }, addSwitchEvent: function( nodeSwitch ) { var self = this; UTIL.addEvent( nodeSwitch, 'click', function( e ) { e.preventDefault(); if ( self.getTreeConfig().callback.onBeforeClickCollapseSwitch.apply( self, [ nodeSwitch, e ] ) === false ) { return false; } self.toggleCollapse(); self.getTreeConfig().callback.onAfterClickCollapseSwitch.apply( self, [ nodeSwitch, e ] ); }, ); }, /** * @returns {TreeNode} */ collapse: function() { if ( !this.collapsed ) { this.toggleCollapse(); } return this; }, /** * @returns {TreeNode} */ expand: function() { if ( this.collapsed ) { this.toggleCollapse(); } return this; }, /** * @returns {TreeNode} */ toggleCollapse: function() { var oTree = this.getTree(); if ( !oTree.inAnimation ) { oTree.inAnimation = true; this.collapsed = !this.collapsed; // toggle the collapse at each click UTIL.toggleClass( this.nodeDOM, 'collapsed', this.collapsed ); oTree.positionTree(); var self = this; setTimeout( function() { // set the flag after the animation oTree.inAnimation = false; oTree.CONFIG.callback.onToggleCollapseFinished.apply( oTree, [ self, self.collapsed ] ); }, ( oTree.CONFIG.animation.nodeSpeed > oTree.CONFIG.animation.connectorsSpeed )? oTree.CONFIG.animation.nodeSpeed: oTree.CONFIG.animation.connectorsSpeed, ); } return this; }, hide: function( collapse_to_point ) { collapse_to_point = collapse_to_point || false; var bCurrentState = this.hidden; this.hidden = true; this.nodeDOM.style.overflow = 'hidden'; var tree = this.getTree(), config = this.getTreeConfig(), oNewState = { opacity: 0, }; if ( collapse_to_point ) { oNewState.left = collapse_to_point.x; oNewState.top = collapse_to_point.y; } // if parent was hidden in initial configuration, position the node behind the parent without animations if ( !this.positioned || bCurrentState ) { this.nodeDOM.style.visibility = 'hidden'; if ( $ ) { $( this.nodeDOM ).css( oNewState ); } else { this.nodeDOM.style.left = oNewState.left + 'px'; this.nodeDOM.style.top = oNewState.top + 'px'; } this.positioned = true; } else { // todo: fix flashy bug when a node is manually hidden and tree.redraw is called. if ( $ ) { $( this.nodeDOM ).animate( oNewState, config.animation.nodeSpeed, config.animation.nodeAnimation, function () { this.style.visibility = 'hidden'; }, ); } else { this.nodeDOM.style.transition = 'all '+config.animation.nodeSpeed+'ms ease'; this.nodeDOM.style.transitionProperty = 'opacity, left, top'; this.nodeDOM.style.opacity = oNewState.opacity; this.nodeDOM.style.left = oNewState.left + 'px'; this.nodeDOM.style.top = oNewState.top + 'px'; this.nodeDOM.style.visibility = 'hidden'; } } // animate the line through node if the line exists if ( this.lineThroughMe ) { var new_path = tree.getPointPathString( collapse_to_point ); if ( bCurrentState ) { // update without animations this.lineThroughMe.attr( { path: new_path } ); } else { // update with animations tree.animatePath( this.lineThroughMe, tree.getPointPathString( collapse_to_point ) ); } } return this; }, /** * @returns {TreeNode} */ hideConnector: function() { var oTree = this.Tree(); var oPath = oTree.connectionStore[this.id]; if ( oPath ) { oPath.animate( { 'opacity': 0 }, oTree.CONFIG.animation.connectorsSpeed, oTree.CONFIG.animation.connectorsAnimation, ); } return this; }, show: function() { var bCurrentState = this.hidden; this.hidden = false; this.nodeDOM.style.visibility = 'visible'; var oTree = this.Tree(); var oNewState = { left: this.X, top: this.Y, opacity: 1, }, config = this.getTreeConfig(); // if the node was hidden, update opacity and position if ( $ ) { $( this.nodeDOM ).animate( oNewState, config.animation.nodeSpeed, config.animation.nodeAnimation, function () { // $.animate applies "overflow:hidden" to the node, remove it to avoid visual problems this.style.overflow = ""; }, ); } else { this.nodeDOM.style.transition = 'all '+config.animation.nodeSpeed+'ms ease'; this.nodeDOM.style.transitionProperty = 'opacity, left, top'; this.nodeDOM.style.left = oNewState.left + 'px'; this.nodeDOM.style.top = oNewState.top + 'px'; this.nodeDOM.style.opacity = oNewState.opacity; this.nodeDOM.style.overflow = ''; } if ( this.lineThroughMe ) { this.getTree().animatePath( this.lineThroughMe, this.pathStringThrough() ); } return this; }, /** * @returns {TreeNode} */ showConnector: function() { var oTree = this.Tree(); var oPath = oTree.connectionStore[this.id]; if ( oPath ) { oPath.animate( { 'opacity': 1 }, oTree.CONFIG.animation.connectorsSpeed, oTree.CONFIG.animation.connectorsAnimation, ); } return this; }, }; /** * Build a node from the 'text' and 'img' property and return with it. * * The node will contain all the fields that present under the 'text' property * Each field will refer to a css class with name defined as node-{$property_name} * * Example: * The definition: * * text: { * desc: "some description", * paragraph: "some text" * } * * will generate the following elements: * *

some description

*

some text

* * @Returns the configured node */ TreeNode.prototype.buildNodeFromText = function (node) { // IMAGE if (this.image) { image = document.createElement('img'); image.src = this.image; node.appendChild(image); } // TEXT if (this.text) { for (var key in this.text) { // adding DATA Attributes to the node if (key.startsWith("data-")) { node.setAttribute(key, this.text[key]); } else { var textElement = document.createElement(this.text[key].href ? 'a' : 'p'); // make an element if required if (this.text[key].href) { textElement.href = this.text[key].href; if (this.text[key].target) { textElement.target = this.text[key].target; } } textElement.className = "node-"+key; textElement.appendChild(document.createTextNode( this.text[key].val ? this.text[key].val : this.text[key] instanceof Object ? "'val' param missing!" : this.text[key], ), ); node.appendChild(textElement); } } } return node; }; /** * Build a node from 'nodeInnerHTML' property that defines an existing HTML element, referenced by it's id, e.g: #someElement * Change the text in the passed node to 'Wrong ID selector' if the referenced element does ot exist, * return with a cloned and configured node otherwise * * @Returns node the configured node */ TreeNode.prototype.buildNodeFromHtml = function(node) { // get some element by ID and clone its structure into a node if (this.nodeInnerHTML.charAt(0) === "#") { var elem = document.getElementById(this.nodeInnerHTML.substring(1)); if (elem) { node = elem.cloneNode(true); node.id += "-clone"; node.className += " node"; } else { node.innerHTML = " Wrong ID selector "; } } else { // insert your custom HTML into a node node.innerHTML = this.nodeInnerHTML; } return node; }; /** * @param {Tree} tree */ TreeNode.prototype.createGeometry = function( tree ) { if ( this.id === 0 && tree.CONFIG.hideRootNode ) { this.width = 0; this.height = 0; return; } var drawArea = tree.drawArea, image, /////////// CREATE NODE ////////////// node = document.createElement( this.link.href? 'a': 'div' ); node.className = ( !this.pseudo )? TreeNode.CONFIG.nodeHTMLclass: 'pseudo'; if ( this.nodeHTMLclass && !this.pseudo ) { node.className += ' ' + this.nodeHTMLclass; } if ( this.nodeHTMLid ) { node.id = this.nodeHTMLid; } if ( this.link.href ) { node.href = this.link.href; node.target = this.link.target; } if ( $ ) { $( node ).data( 'treenode', this ); } else { node.data = { 'treenode': this, }; } /////////// BUILD NODE CONTENT ////////////// if ( !this.pseudo ) { node = this.nodeInnerHTML? this.buildNodeFromHtml(node) : this.buildNodeFromText(node) // handle collapse switch if ( this.collapsed || (this.collapsable && this.childrenCount() && !this.stackParentId) ) { this.createSwitchGeometry( tree, node ); } } tree.CONFIG.callback.onCreateNode.apply( tree, [this, node] ); /////////// APPEND all ////////////// drawArea.appendChild(node); this.width = node.offsetWidth; this.height = node.offsetHeight; this.nodeDOM = node; tree.imageLoader.processNode(this); }; /** * @param {Tree} tree * @param {Element} nodeEl */ TreeNode.prototype.createSwitchGeometry = function( tree, nodeEl ) { nodeEl = nodeEl || this.nodeDOM; // safe guard and check to see if it has a collapse switch var nodeSwitchEl = UTIL.findEl( '.collapse-switch', true, nodeEl ); if ( !nodeSwitchEl ) { nodeSwitchEl = document.createElement( 'a' ); nodeSwitchEl.className = "collapse-switch"; nodeEl.appendChild( nodeSwitchEl ); this.addSwitchEvent( nodeSwitchEl ); if ( this.collapsed ) { nodeEl.className += " collapsed"; } tree.CONFIG.callback.onCreateNodeCollapseSwitch.apply( tree, [this, nodeEl, nodeSwitchEl] ); } return nodeSwitchEl; }; // ########################################### // Expose global + default CONFIG params // ########################################### Tree.CONFIG = { maxDepth: 100, rootOrientation: 'NORTH', // NORTH || EAST || WEST || SOUTH nodeAlign: 'CENTER', // CENTER || TOP || BOTTOM levelSeparation: 30, siblingSeparation: 30, subTeeSeparation: 30, hideRootNode: false, animateOnInit: false, animateOnInitDelay: 500, padding: 15, // the difference is seen only when the scrollbar is shown scrollbar: 'native', // "native" || "fancy" || "None" (PS: "fancy" requires jquery and perfect-scrollbar) connectors: { type: 'curve', // 'curve' || 'step' || 'straight' || 'bCurve' style: { stroke: 'black', }, stackIndent: 15, }, node: { // each node inherits this, it can all be overridden in node config // HTMLclass: 'node', // drawLineThrough: false, // collapsable: false, link: { target: '_self', }, }, animation: { // each node inherits this, it can all be overridden in node config nodeSpeed: 450, nodeAnimation: 'linear', connectorsSpeed: 450, connectorsAnimation: 'linear', }, callback: { onCreateNode: function( treeNode, treeNodeDom ) {}, // this = Tree onCreateNodeCollapseSwitch: function( treeNode, treeNodeDom, switchDom ) {}, // this = Tree onAfterAddNode: function( newTreeNode, parentTreeNode, nodeStructure ) {}, // this = Tree onBeforeAddNode: function( parentTreeNode, nodeStructure ) {}, // this = Tree onAfterPositionNode: function( treeNode, nodeDbIndex, containerCenter, treeCenter) {}, // this = Tree onBeforePositionNode: function( treeNode, nodeDbIndex, containerCenter, treeCenter) {}, // this = Tree onToggleCollapseFinished: function ( treeNode, bIsCollapsed ) {}, // this = Tree onAfterClickCollapseSwitch: function( nodeSwitch, event ) {}, // this = TreeNode onBeforeClickCollapseSwitch: function( nodeSwitch, event ) {}, // this = TreeNode onTreeLoaded: function( rootTreeNode ) {}, // this = Tree }, }; TreeNode.CONFIG = { nodeHTMLclass: 'node', }; // ############################################# // Makes a JSON chart config out of Array config // ############################################# var JSONconfig = { make: function( configArray ) { var i = configArray.length, node; this.jsonStructure = { chart: null, nodeStructure: null, }; //fist loop: find config, find root; while(i--) { node = configArray[i]; if (node.hasOwnProperty('container')) { this.jsonStructure.chart = node; continue; } if (!node.hasOwnProperty('parent') && ! node.hasOwnProperty('container')) { this.jsonStructure.nodeStructure = node; node._json_id = 0; } } this.findChildren(configArray); return this.jsonStructure; }, findChildren: function(nodes) { var parents = [0]; // start with a a root node while(parents.length) { var parentId = parents.pop(), parent = this.findNode(this.jsonStructure.nodeStructure, parentId), i = 0, len = nodes.length, children = []; for(;i