tree-model
Version:
Manipulate and traverse tree-like structures in javascript.
371 lines (323 loc) • 11.3 kB
JavaScript
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.TreeModel = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
var mergeSort, findInsertIndex;
mergeSort = require('mergesort');
findInsertIndex = require('find-insert-index');
module.exports = (function () {
'use strict';
var walkStrategies;
walkStrategies = {};
function k(result) {
return function () {
return result;
};
}
function TreeModel(config) {
config = config || {};
this.config = config;
this.config.childrenPropertyName = config.childrenPropertyName || 'children';
this.config.modelComparatorFn = config.modelComparatorFn;
}
TreeModel.prototype.parse = function (model) {
var i, childCount, node;
if (!(model instanceof Object)) {
throw new TypeError('Model must be of type object.');
}
node = new Node(this.config, model);
if (model[this.config.childrenPropertyName] instanceof Array) {
if (this.config.modelComparatorFn) {
model[this.config.childrenPropertyName] = mergeSort(
this.config.modelComparatorFn,
model[this.config.childrenPropertyName]);
}
for (i = 0, childCount = model[this.config.childrenPropertyName].length; i < childCount; i++) {
addChildToNode(node, this.parse(model[this.config.childrenPropertyName][i]));
}
}
return node;
};
function addChildToNode(node, child) {
child.parent = node;
node.children.push(child);
return child;
}
function hasComparatorFunction(node) {
return typeof node.config.modelComparatorFn === 'function';
}
function Node(config, model) {
this.config = config;
this.model = model;
this.children = [];
}
Node.prototype.isRoot = function () {
return this.parent === undefined;
};
Node.prototype.hasChildren = function () {
return this.children.length > 0;
};
Node.prototype.addChild = function (child) {
return addChild(this, child);
};
Node.prototype.addChildAtIndex = function (child, index) {
if (hasComparatorFunction(this)) {
throw new Error('Cannot add child at index when using a comparator function.');
}
return addChild(this, child, index);
};
Node.prototype.setIndex = function (index) {
if (hasComparatorFunction(this)) {
throw new Error('Cannot set node index when using a comparator function.');
}
if (this.isRoot()) {
if (index === 0) {
return this;
}
throw new Error('Invalid index.');
}
if (index < 0 || index >= this.parent.children.length) {
throw new Error('Invalid index.');
}
var oldIndex = this.parent.children.indexOf(this);
this.parent.children.splice(index, 0, this.parent.children.splice(oldIndex, 1)[0]);
this.parent.model[this.parent.config.childrenPropertyName]
.splice(index, 0, this.parent.model[this.parent.config.childrenPropertyName].splice(oldIndex, 1)[0]);
return this;
};
function addChild(self, child, insertIndex) {
var index;
if (!(child instanceof Node)) {
throw new TypeError('Child must be of type Node.');
}
child.parent = self;
if (!(self.model[self.config.childrenPropertyName] instanceof Array)) {
self.model[self.config.childrenPropertyName] = [];
}
if (hasComparatorFunction(self)) {
// Find the index to insert the child
index = findInsertIndex(
self.config.modelComparatorFn,
self.model[self.config.childrenPropertyName],
child.model);
// Add to the model children
self.model[self.config.childrenPropertyName].splice(index, 0, child.model);
// Add to the node children
self.children.splice(index, 0, child);
} else {
if (insertIndex === undefined) {
self.model[self.config.childrenPropertyName].push(child.model);
self.children.push(child);
} else {
if (insertIndex < 0 || insertIndex > self.children.length) {
throw new Error('Invalid index.');
}
self.model[self.config.childrenPropertyName].splice(insertIndex, 0, child.model);
self.children.splice(insertIndex, 0, child);
}
}
return child;
}
Node.prototype.getPath = function () {
var path = [];
(function addToPath(node) {
path.unshift(node);
if (!node.isRoot()) {
addToPath(node.parent);
}
})(this);
return path;
};
Node.prototype.getIndex = function () {
if (this.isRoot()) {
return 0;
}
return this.parent.children.indexOf(this);
};
/**
* Parse the arguments of traversal functions. These functions can take one optional
* first argument which is an options object. If present, this object will be stored
* in args.options. The only mandatory argument is the callback function which can
* appear in the first or second position (if an options object is given). This
* function will be saved to args.fn. The last optional argument is the context on
* which the callback function will be called. It will be available in args.ctx.
*
* @returns Parsed arguments.
*/
function parseArgs() {
var args = {};
if (arguments.length === 1) {
if (typeof arguments[0] === 'function') {
args.fn = arguments[0];
} else {
args.options = arguments[0];
}
} else if (arguments.length === 2) {
if (typeof arguments[0] === 'function') {
args.fn = arguments[0];
args.ctx = arguments[1];
} else {
args.options = arguments[0];
args.fn = arguments[1];
}
} else {
args.options = arguments[0];
args.fn = arguments[1];
args.ctx = arguments[2];
}
args.options = args.options || {};
if (!args.options.strategy) {
args.options.strategy = 'pre';
}
if (!walkStrategies[args.options.strategy]) {
throw new Error('Unknown tree walk strategy. Valid strategies are \'pre\' [default], \'post\' and \'breadth\'.');
}
return args;
}
Node.prototype.walk = function () {
var args;
args = parseArgs.apply(this, arguments);
walkStrategies[args.options.strategy].call(this, args.fn, args.ctx);
};
walkStrategies.pre = function depthFirstPreOrder(callback, context) {
var i, childCount, keepGoing;
keepGoing = callback.call(context, this);
for (i = 0, childCount = this.children.length; i < childCount; i++) {
if (keepGoing === false) {
return false;
}
keepGoing = depthFirstPreOrder.call(this.children[i], callback, context);
}
return keepGoing;
};
walkStrategies.post = function depthFirstPostOrder(callback, context) {
var i, childCount, keepGoing;
for (i = 0, childCount = this.children.length; i < childCount; i++) {
keepGoing = depthFirstPostOrder.call(this.children[i], callback, context);
if (keepGoing === false) {
return false;
}
}
keepGoing = callback.call(context, this);
return keepGoing;
};
walkStrategies.breadth = function breadthFirst(callback, context) {
var queue = [this];
(function processQueue() {
var i, childCount, node;
if (queue.length === 0) {
return;
}
node = queue.shift();
for (i = 0, childCount = node.children.length; i < childCount; i++) {
queue.push(node.children[i]);
}
if (callback.call(context, node) !== false) {
processQueue();
}
})();
};
Node.prototype.all = function () {
var args, all = [];
args = parseArgs.apply(this, arguments);
args.fn = args.fn || k(true);
walkStrategies[args.options.strategy].call(this, function (node) {
if (args.fn.call(args.ctx, node)) {
all.push(node);
}
}, args.ctx);
return all;
};
Node.prototype.first = function () {
var args, first;
args = parseArgs.apply(this, arguments);
args.fn = args.fn || k(true);
walkStrategies[args.options.strategy].call(this, function (node) {
if (args.fn.call(args.ctx, node)) {
first = node;
return false;
}
}, args.ctx);
return first;
};
Node.prototype.drop = function () {
var indexOfChild;
if (!this.isRoot()) {
indexOfChild = this.parent.children.indexOf(this);
this.parent.children.splice(indexOfChild, 1);
this.parent.model[this.config.childrenPropertyName].splice(indexOfChild, 1);
this.parent = undefined;
delete this.parent;
}
return this;
};
return TreeModel;
})();
},{"find-insert-index":2,"mergesort":3}],2:[function(require,module,exports){
module.exports = (function () {
'use strict';
/**
* Find the index to insert an element in array keeping the sort order.
*
* @param {function} comparatorFn The comparator function which sorted the array.
* @param {array} arr The sorted array.
* @param {object} el The element to insert.
*/
function findInsertIndex(comparatorFn, arr, el) {
var i, len;
for (i = 0, len = arr.length; i < len; i++) {
if (comparatorFn(arr[i], el) > 0) {
break;
}
}
return i;
}
return findInsertIndex;
})();
},{}],3:[function(require,module,exports){
module.exports = (function () {
'use strict';
/**
* Sort an array using the merge sort algorithm.
*
* @param {function} comparatorFn The comparator function.
* @param {array} arr The array to sort.
* @returns {array} The sorted array.
*/
function mergeSort(comparatorFn, arr) {
var len = arr.length, firstHalf, secondHalf;
if (len >= 2) {
firstHalf = arr.slice(0, len / 2);
secondHalf = arr.slice(len / 2, len);
return merge(comparatorFn, mergeSort(comparatorFn, firstHalf), mergeSort(comparatorFn, secondHalf));
} else {
return arr.slice();
}
}
/**
* The merge part of the merge sort algorithm.
*
* @param {function} comparatorFn The comparator function.
* @param {array} arr1 The first sorted array.
* @param {array} arr2 The second sorted array.
* @returns {array} The merged and sorted array.
*/
function merge(comparatorFn, arr1, arr2) {
var result = [], left1 = arr1.length, left2 = arr2.length;
while (left1 > 0 && left2 > 0) {
if (comparatorFn(arr1[0], arr2[0]) <= 0) {
result.push(arr1.shift());
left1--;
} else {
result.push(arr2.shift());
left2--;
}
}
if (left1 > 0) {
result.push.apply(result, arr1);
} else {
result.push.apply(result, arr2);
}
return result;
}
return mergeSort;
})();
},{}]},{},[1])(1)
});