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