/** * @file Arboreal.js * @author Vasiliy Altunin <skyr@altunin.online> * @copyright 2017 Vasiliy Altunin * @version 0.0.1 * @license MIT * @description Javascript tree traversal and manipulation library (Forked from Nenad V. Nikolić, originally by Andrea Fiore) * @module arboreal */ (function () { /** * @private * @param {type} array * @param {type} item * @returns {Number|arboreal_L8.indexOf.i} */ function indexOf(array, item) { for (var i = 0, j = array.length; i < j; i++) { if (array[i] === item) { return i; } } return -1; } /** * @private * @param {type} array * @param {type} item * @returns {Boolean} */ function include(array, item) { return indexOf(array, item) > -1; } /** * Traverse down tree iterator * @private * @param {type} context * @param {function} iterator Iterator function to be called when traverse node * @param {function} iteratorAfter Optional iterator function to be called AFTER node was traversed */ function _traverseDown(context, iterator, iteratorAfter) { var doContinue = true; (function walkDown(node) { var i, newContext; if (!doContinue) { return; } if (iterator(node) === false) { //break the traversal loop if the iterator returns a falsy value doContinue = false; } else { for (i = 0; i < node.children.length; i++) { newContext = node.children[i]; walkDown(newContext); if (iteratorAfter !== undefined) { iteratorAfter.call(newContext, newContext); } } } })(context); } /** * Traverse up tree iterator * @private * @param {type} context * @param {function} iterator Iterator function to be called when traverse node */ function _traverseUp(context, iterator) { var i, node; while (context) { if (iterator(context) === false) { return; } for (i = 0; i < context.children.length; i++) { node = context.children[i]; if (iterator(node) === false) { return; } } context = context.parent; } } /** * Bubble up tree iterator * @private * @param {type} context * @param {function} iterator Iterator function to be called when traverse node * */ function _bubbleUp(context, iterator) { while (context) { if (iterator(context) === false) { return; } context = context.parent; } } /** * Traverse node and calls callback * @private * @param {type} context * @param {type} iterator Iterator function to be called when traverse node * @param {type} callback Iterator callback * @param {type} iteratorAfter Optional iterator function to be called AFTER node was traversed */ function _traverse(context, iterator, callback, iteratorAfter) { var visited = [], callIterator = function (node) { var id = node.id, returned; if (!include(visited, id)) { returned = iterator.call(node, node); visited.push(id); if (returned === false) { return returned; } } }; // ,i, node; callback(context, callIterator, iteratorAfter); } /** * Removes node * @private * @param {node} node * @returns {Arboreal} */ function _removeChild(node) { var parent = node.parent, child, i; for (i = 0; i < parent.children.length; i++) { child = parent.children[i]; if (child === node) { return parent.children.splice(i, 1).shift(); } } } /** * Build Id for node by concat parent node id with separator and parent children length * @private * @param {Node} parent Parent node * @param {Char} Separator for numbers in Id * @returns {String} Id string */ function _nodeId(parent, separator) { separator = separator || '/'; if (parent) { return [parent.id, parent.children.length].join(separator); } else { return '0'; } } /** * Creates Arboreal root tree object or child node object * @param {Arboreal=} parent Parent node * @param {Object=} data Data to store in node * @param {String=} id - node id * @param {Char=} separator - separator for id numbers * @memberOf module:arboreal * @example * * var tree = new Arboreal(null,{category: 'JavaScript'}); * console.log(tree.toString(true)); * * Result: * * 0 {"category":"JavaScript"} * @returns {Arboreal} Arboreal object */ function Arboreal(parent, data, id, separator) { this.depth = parent ? parent.depth + 1 : 0; this.data = data || {}; this.parent = parent || null; if (separator === undefined) { if ((parent !== undefined) && (parent !== null)) { this.separator = this.parent.separator; } else { this.separator = '/'; } } else { this.separator = separator; } this.id = id || _nodeId(parent, this.separator); this.children = []; } /** * Parses Object and creates tree from it * @param {Object} object Object to parse * @param {String} childrenAttr - name of object attr to use as children attr * @param {Arboreal} parent - Parent node, if null new tree created * @memberOf module:arboreal * @example * var wikipediaJsCategory = { * category: 'JavaScript', * subcategories: [ * {category: 'Ajax (programming)'}, * {category: 'JavaScript engines'}, * {category: 'JavaScript programming languages family', * subcategories: [{ * category: 'JavaScript dialect engines' * }] * }, * {category: 'JavaScript based calendar components'}, * {category: 'JavaScript based HTML editors'} * ] * }; * tree = Arboreal.parse(wikipediaJsCategory, 'subcategories'); * * Result: * * 0 {"category":"JavaScript"} * |- 0/0 {"category":"Ajax (programming)"} * |- 0/1 {"category":"JavaScript engines"} * |- 0/2 {"category":"JavaScript programming languages family"} * |- 0/2/0 {"category":"JavaScript dialect engines"} * |- 0/3 {"category":"JavaScript based calendar components"} * |- 0/4 {"category":"JavaScript based HTML editors"} * @returns {Arboreal} Arboreal root object or parent object */ Arboreal.parse = function (object, childrenAttr, parent) { var root, getNodeData = function (node) { var attr, nodeData = {}; for (attr in node) { if (attr !== childrenAttr) { nodeData[attr] = node[attr]; } } return nodeData; }; (function walkDown(node, parent) { var newNode, i; if (!parent) { newNode = root = new Arboreal(null, getNodeData(node)); } else { newNode = new Arboreal(parent, getNodeData(node)); parent.children.push(newNode); } if (childrenAttr in node) { for (i = 0; i < node[childrenAttr].length; i++) { walkDown(node[childrenAttr][i], newNode); } } })(object, parent); return root || parent; }; /** * Appends child node to tree * @param {Object} data Data to store in node * @param {String=} id - Node id * @memberOf module:arboreal * @example * var tree = new Arboreal(null,{category: 'JavaScript'}); * tree.appendChild({category: 'Ajax (programming)'}); * * Result * * 0 {"category":"JavaScript"} * |- 0/0 {"category":"Ajax (programming)"} * @returns {Arboreal} Arboreal root object or parent object */ Arboreal.prototype.appendChild = function (data, id) { var child = new Arboreal(this, data, id); this.children.push(child); return this; }; /** * Parse data and add it to `this` node * @param {Object} data Object to be parsed * @param {String} childrenAttr Attr to use as children attr * @memberOf module:arboreal * @example * * var wikipediaJsCategory = { * category: 'JavaScript', * subcategories: [ * {category: 'Ajax (programming)'}, * {category: 'JavaScript engines'}, * {category: 'JavaScript programming languages family', * subcategories: [{ * category: 'JavaScript dialect engines' * }] * }, * {category: 'JavaScript based calendar components'}, * {category: 'JavaScript based HTML editors'} * ] * }; * * tree = Arboreal.parse(wikipediaJsCategory, 'subcategories'); * * var subCategory = { * category: 'JavaScript ajax query', * subcategories: [ * {category: 'JSON'}, * {category: 'jQuery'} * ]}; * * tree.children[0].appendChildren(subCategory, 'subcategories'); * * console.log(tree.toString(true)); * * Result: * * 0 {"category":"JavaScript"} * |- 0/0 {"category":"Ajax (programming)"} * |- 0/0/0 {"category":"JavaScript ajax query"} * |- 0/0/0/0 {"category":"JSON"} * |- 0/0/0/1 {"category":"jQuery"} * |- 0/1 {"category":"JavaScript engines"} * |- 0/2 {"category":"JavaScript programming languages family"} * |- 0/2/0 {"category":"JavaScript dialect engines"} * |- 0/3 {"category":"JavaScript based calendar components"} * |- 0/4 {"category":"JavaScript based HTML editors"} * @returns {Arboreal} Arboreal root object or parent object */ Arboreal.prototype.appendChildren = function (data, childrenAttr) { return Arboreal.parse(data, childrenAttr, this); }; /** * Remove child node from `this` node * @param {String= | Node=} arg Node id or Arboreal * @memberOf module:arboreal * @example * * var wikipediaJsCategory = { * category: 'JavaScript', * subcategories: [ * {category: 'Ajax (programming)'}, * {category: 'JavaScript engines'}, * {category: 'JavaScript programming languages family', * subcategories: [{ * category: 'JavaScript dialect engines' * }] * }, * {category: 'JavaScript based calendar components'}, * {category: 'JavaScript based HTML editors'} * ] * }; * * tree.removeChild(2); * * console.log(tree.toString(true)); * * Result: * * 0 {"category":"JavaScript"} * |- 0/0 {"category":"Ajax (programming)"} * |- 0/1 {"category":"JavaScript engines"} * |- 0/3 {"category":"JavaScript based calendar components"} * |- 0/4 {"category":"JavaScript based HTML editors"} * @returns {Arboreal} Arboreal */ Arboreal.prototype.removeChild = function (arg) { if (typeof arg === 'number' && this.children[arg]) { return this.children.splice(arg, 1).shift(); } if (arg instanceof Arboreal) { return _removeChild(arg); } throw new Error("Invalid argument " + arg); }; /** * Removes `this` node * @memberOf module:arboreal * @example * var wikipediaJsCategory = { * category: 'JavaScript', * subcategories: [ * {category: 'Ajax (programming)'}, * {category: 'JavaScript engines'}, * {category: 'JavaScript programming languages family', * subcategories: [{ * category: 'JavaScript dialect engines' * }] * }, * {category: 'JavaScript based calendar components'}, * {category: 'JavaScript based HTML editors'} * ] * }; * * tree.children[0].remove(); * * console.log(tree.toString(true)); * * Result: * * 0 {"category":"JavaScript"} * |- 0/1 {"category":"JavaScript engines"} * |- 0/2 {"category":"JavaScript programming languages family"} * |- 0/2/0 {"category":"JavaScript dialect engines"} * |- 0/3 {"category":"JavaScript based calendar components"} * |- 0/4 {"category":"JavaScript based HTML editors"} * @returns {Arboreal} */ Arboreal.prototype.remove = function () { return _removeChild(this); }; /** * Traverse tree down * @param {function} iterator Iterator function * @memberOf module:arboreal * @example * //Traverse and creating HTML list discribed in example * //https://vasiliyaltunin.github.io/arboreal.js/examples * * var wikipediaJsCategory = { * category: 'JavaScript', * subcategories: [ * {category: 'Ajax (programming)'}, * {category: 'JavaScript engines'}, * {category: 'JavaScript programming languages family', * subcategories: [{ * category: 'JavaScript dialect engines' * }] * }, * {category: 'JavaScript based calendar components'}, * {category: 'JavaScript based HTML editors'} * ] * }; * * function iterator (node) { * var depth = "", i; * for (i = 1; i <= node.depth; i++) depth += ">>"; * console.log([depth, node.data.category].join(" ")); * } * * tree.traverseDown(iterator); * * Result: * * JavaScript * >> Ajax (programming) * >> JavaScript engines * >> JavaScript programming languages family * >>>> JavaScript dialect engines * >> JavaScript based calendar components * >> JavaScript based HTML editors */ Arboreal.prototype.traverseDown = function (iterator, iteratorAfter) { _traverse(this, iterator, _traverseDown, iteratorAfter); }; /** * Traverse tree up * @param {function} iterator Iterator function * @memberOf module:arboreal * @example * var wikipediaJsCategory = { * category: 'JavaScript', * subcategories: [ * {category: 'Ajax (programming)'}, * {category: 'JavaScript engines'}, * {category: 'JavaScript programming languages family', * subcategories: [{ * category: 'JavaScript dialect engines' * }] * }, * {category: 'JavaScript based calendar components'}, * {category: 'JavaScript based HTML editors'} * ] * }; * * function iterator (node) { * var depth = "", i; * for (i = 1; i <= node.depth; i++) depth += ">>"; * console.log([depth, node.data.category].join(" ")); * } * * tree.children[2].traverseUp(iterator); * * Result: * * >> JavaScript programming languages family * >>>> JavaScript dialect engines * JavaScript * >> Ajax (programming) * >> JavaScript engines * >> JavaScript based calendar components * >> JavaScript based HTML editors */ Arboreal.prototype.traverseUp = function (iterator) { _traverse(this, iterator, _traverseUp); }; /** * Traverse tree bubble up * @param {function} iterator Iterator function * @memberOf module:arboreal * @example * var wikipediaJsCategory = { * category: 'JavaScript', * subcategories: [ * {category: 'Ajax (programming)'}, * {category: 'JavaScript engines'}, * {category: 'JavaScript programming languages family', * subcategories: [{ * category: 'JavaScript dialect engines' * }] * }, * {category: 'JavaScript based calendar components'}, * {category: 'JavaScript based HTML editors'} * ] * }; * * function iterator (node) { * var depth = "", i; * for (i = 1; i <= node.depth; i++) depth += ">>"; * console.log([depth, node.data.category].join(" ")); * } * * tree.children[2].children[0].bubbleUp(iterator); * * Result: * * >>>> JavaScript dialect engines * >> JavaScript programming languages family * JavaScript */ Arboreal.prototype.bubbleUp = function (iterator) { _traverse(this, iterator, _bubbleUp); }; /** * Retrns string representation of a tree * @param {boolean=} isValue If true than data value printed * @memberOf module:arboreal * @example * var wikipediaJsCategory = { * category: 'JavaScript', * subcategories: [ * {category: 'Ajax (programming)'}, * {category: 'JavaScript engines'}, * {category: 'JavaScript programming languages family', * subcategories: [{ * category: 'JavaScript dialect engines' * }] * }, * {category: 'JavaScript based calendar components'}, * {category: 'JavaScript based HTML editors'} * ] * }; * * console.log(tree.toString()); * * Result: * * 0 * |- 0/0 * |- 0/1 * |- 0/2 * |- 0/2/0 * |- 0/3 * |- 0/4 * * If True passed as arg result changed: * * 0 {"category":"JavaScript"} * |- 0/0 {"category":"Ajax (programming)"} * |- 0/1 {"category":"JavaScript engines"} * |- 0/2 {"category":"JavaScript programming languages family"} * |- 0/2/0 {"category":"JavaScript dialect engines"} * |- 0/3 {"category":"JavaScript based calendar components"} * |- 0/4 {"category":"JavaScript based HTML editors"} * @returns {String} */ Arboreal.prototype.toString = function (isValue) { var lines = []; isValue = isValue || false; this.traverseDown(function (node) { var separator = '|- ', indentation = '', i; var value = ""; if (isValue) { value = JSON.stringify(node.data); } if (node.depth === 0) { lines.push(node.id + " " + value); return; } for (i = 0; i < node.depth; i++) { indentation += ' '; } lines.push(indentation + separator + node.id + indentation + value); }); return lines.join("\n"); }; /** * Finds node in tree by using iterator * @param {function} finder Iterator * @memberOf module:arboreal * @example * var wikipediaJsCategory = { * category: 'JavaScript', * subcategories: [ * {category: 'Ajax (programming)'}, * {category: 'JavaScript engines'}, * {category: 'JavaScript programming languages family', * subcategories: [{ * category: 'JavaScript dialect engines' * }] * }, * {category: 'JavaScript based calendar components'}, * {category: 'JavaScript based HTML editors'} * ] * }; * * var result = tree.find(function (node) { * return (/calendar/).test(node.data.category) * }).data.category; * console.log(result); * * Result: * * JavaScript based calendar components * @returns {Arboreal} First match */ Arboreal.prototype.find = function (finder) { var match = null, iterator = (typeof finder === 'function') ? finder : function (node) { if (node.id === finder) { match = node; return false; } }; this.traverseDown(function (node) { if (iterator.call(this, node)) { match = node; return false; } }); return match; }; /** * Returns Arboreal for givent id path * @param {String} path Id string * @param {Char=} separator Separator to use * @memberOf module:arboreal * @example * var wikipediaJsCategory = { * category: 'JavaScript', * subcategories: [ * {category: 'Ajax (programming)'}, * {category: 'JavaScript engines'}, * {category: 'JavaScript programming languages family', * subcategories: [{ * category: 'JavaScript dialect engines' * }] * }, * {category: 'JavaScript based calendar components'}, * {category: 'JavaScript based HTML editors'} * ] * }; * * var result = tree.path("/2/0").data.category; * console.log(result); * * Result: * * JavaScript dialect engines * @returns {Arboreal} Node finded by path */ Arboreal.prototype.path = function (path, separator) { separator = separator || '/'; //allow path to begin with if (path[0] === separator) { path = path.substring(1); } var indexes = path.split(separator), index = null, context = this, i; for (i = 0; i < indexes.length; i++) { index = parseInt(indexes[i], 10); context = (context.children.length && context.children.length > index) ? context.children[index] : null; } return context; }; /** * Converts tree to array of objects * @private * @returns {array} Array pepresentation of tree */ Arboreal.prototype.toArray = function () { var nodeList = []; this.traverseDown(function (node) { nodeList.push(node); }); return nodeList; }; /** * Returns root node for `this` * @private * @returns {Arboreal} */ Arboreal.prototype.root = function () { var node = this; if (!node.parent) { return this; } while (node.parent) { node = node.parent; } return node; }; /** * Checks is root * @private * @returns {Arboreal} */ Arboreal.prototype.isRoot = function () { return !this.parent; }; /** * @private */ Object.defineProperty(Arboreal.prototype, 'length', { get: function () { return this.toArray().length; } }); if (typeof module !== 'undefined' && module.exports) { module.exports = Arboreal; } else { this.Arboreal = Arboreal; } }(this));