angular-ivh-treeview
Version:
Treeview for angular with filtering and checkboxes
454 lines (400 loc) • 15.3 kB
JavaScript
/**
* Manager for treeview data stores
*
* Used to assist treeview operations, e.g. selecting or validating a tree-like
* collection.
*
* @package ivh.treeview
* @copyright 2014 iVantage Health Analytics, Inc.
*/
angular.module('ivh.treeview')
.factory('ivhTreeviewMgr', ['ivhTreeviewOptions', 'ivhTreeviewBfs', function(ivhTreeviewOptions, ivhTreeviewBfs) {
'use strict';
var ng = angular
, options = ivhTreeviewOptions()
, exports = {};
// The make* methods and validateParent need to be bound to an options
// object
var makeDeselected = function(node) {
node[this.selectedAttribute] = false;
node[this.indeterminateAttribute] = false;
};
var makeSelected = function(node) {
node[this.selectedAttribute] = true;
node[this.indeterminateAttribute] = false;
};
var validateParent = function(node) {
var children = node[this.childrenAttribute]
, selectedAttr = this.selectedAttribute
, indeterminateAttr = this.indeterminateAttribute
, numSelected = 0
, numIndeterminate = 0;
ng.forEach(children, function(n, ix) {
if(n[selectedAttr]) {
numSelected++;
} else {
if(n[indeterminateAttr]) {
numIndeterminate++;
}
}
});
if(0 === numSelected && 0 === numIndeterminate) {
node[selectedAttr] = false;
node[indeterminateAttr] = false;
} else if(numSelected === children.length) {
node[selectedAttr] = true;
node[indeterminateAttr] = false;
} else {
node[selectedAttr] = false;
node[indeterminateAttr] = true;
}
};
var findNode = function(tree, node, opts, cb) {
var useId = isId(node)
, proceed = true
, idAttr = opts.idAttribute;
// Our return values
var foundNode = null
, foundParents = [];
ivhTreeviewBfs(tree, opts, function(n, p) {
var isNode = proceed && (useId ?
node === n[idAttr] :
node === n);
if(isNode) {
// I've been looking for you all my life
proceed = false;
foundNode = n;
foundParents = p;
}
return proceed;
});
return cb(foundNode, foundParents);
};
var isId = function(val) {
return ng.isString(val) || ng.isNumber(val);
};
/**
* Select (or deselect) a tree node
*
* This method will update the rest of the tree to account for your change.
*
* You may alternatively pass an id as `node`, in which case the tree will
* be searched for your item.
*
* @param {Object|Array} tree The tree data
* @param {Object|String} node The node (or id) to (de)select
* @param {Object} opts [optional] Options to override default options with
* @param {Boolean} isSelected [optional] Whether or not to select `node`, defaults to `true`
* @return {Object} Returns the ivhTreeviewMgr instance for chaining
*/
exports.select = function(tree, node, opts, isSelected) {
if(arguments.length > 2) {
if(typeof opts === 'boolean') {
isSelected = opts;
opts = {};
}
}
opts = ng.extend({}, options, opts);
isSelected = ng.isDefined(isSelected) ? isSelected : true;
var useId = isId(node)
, proceed = true
, idAttr = opts.idAttribute;
ivhTreeviewBfs(tree, opts, function(n, p) {
var isNode = proceed && (useId ?
node === n[idAttr] :
node === n);
if(isNode) {
// I've been looking for you all my life
proceed = false;
var cb = isSelected ?
makeSelected.bind(opts) :
makeDeselected.bind(opts);
if (opts.disableCheckboxSelectionPropagation) {
cb(n);
} else {
ivhTreeviewBfs(n, opts, cb);
ng.forEach(p, validateParent.bind(opts));
}
}
return proceed;
});
return exports;
};
/**
* Select all nodes in a tree
*
* `opts` will default to an empty object, `isSelected` defaults to `true`.
*
* @param {Object|Array} tree The tree data
* @param {Object} opts [optional] Default options overrides
* @param {Boolean} isSelected [optional] Whether or not to select items
* @return {Object} Returns the ivhTreeviewMgr instance for chaining
*/
exports.selectAll = function(tree, opts, isSelected) {
if(arguments.length > 1) {
if(typeof opts === 'boolean') {
isSelected = opts;
opts = {};
}
}
opts = ng.extend({}, options, opts);
isSelected = ng.isDefined(isSelected) ? isSelected : true;
var selectedAttr = opts.selectedAttribute
, indeterminateAttr = opts.indeterminateAttribute;
ivhTreeviewBfs(tree, opts, function(node) {
node[selectedAttr] = isSelected;
node[indeterminateAttr] = false;
});
return exports;
};
/**
* Select or deselect each of the passed items
*
* Eventually it would be nice if this did something more intelligent than
* just calling `select` on each item in the array...
*
* @param {Object|Array} tree The tree data
* @param {Array} nodes The array of nodes or node ids
* @param {Object} opts [optional] Default options overrides
* @param {Boolean} isSelected [optional] Whether or not to select items
* @return {Object} Returns the ivhTreeviewMgr instance for chaining
*/
exports.selectEach = function(tree, nodes, opts, isSelected) {
/**
* @todo Surely we can do something better than this...
*/
ng.forEach(nodes, function(node) {
exports.select(tree, node, opts, isSelected);
});
return exports;
};
/**
* Deselect a tree node
*
* Delegates to `ivhTreeviewMgr.select` with `isSelected` set to `false`.
*
* @param {Object|Array} tree The tree data
* @param {Object|String} node The node (or id) to (de)select
* @param {Object} opts [optional] Options to override default options with
* @return {Object} Returns the ivhTreeviewMgr instance for chaining
*/
exports.deselect = function(tree, node, opts) {
return exports.select(tree, node, opts, false);
};
/**
* Deselect all nodes in a tree
*
* Delegates to `ivhTreeviewMgr.selectAll` with `isSelected` set to `false`.
*
* @param {Object|Array} tree The tree data
* @param {Object} opts [optional] Default options overrides
* @return {Object} Returns the ivhTreeviewMgr instance for chaining
*/
exports.deselectAll = function(tree, opts) {
return exports.selectAll(tree, opts, false);
};
/**
* Deselect each of the passed items
*
* Delegates to `ivhTreeviewMgr.selectEach` with `isSelected` set to
* `false`.
*
* @param {Object|Array} tree The tree data
* @param {Array} nodes The array of nodes or node ids
* @param {Object} opts [optional] Default options overrides
* @return {Object} Returns the ivhTreeviewMgr instance for chaining
*/
exports.deselectEach = function(tree, nodes, opts) {
return exports.selectEach(tree, nodes, opts, false);
};
/**
* Validate tree for parent/child selection consistency
*
* Assumes `bias` as default selected state. The first element with
* `node.select !== bias` will be assumed correct. For example, if `bias` is
* `true` (the default) we'll traverse the tree until we come to an
* unselected node at which point we stop and deselect each of that node's
* children (and their children, etc.).
*
* Indeterminate states will also be resolved.
*
* @param {Object|Array} tree The tree data
* @param {Object} opts [optional] Options to override default options with
* @param {Boolean} bias [optional] Default selected state
* @return {Object} Returns the ivhTreeviewMgr instance for chaining
*/
exports.validate = function(tree, opts, bias) {
if(!tree) {
// Guard against uninitialized trees
return exports;
}
if(arguments.length > 1) {
if(typeof opts === 'boolean') {
bias = opts;
opts = {};
}
}
opts = ng.extend({}, options, opts);
bias = ng.isDefined(bias) ? bias : opts.defaultSelectedState;
var selectedAttr = opts.selectedAttribute
, indeterminateAttr = opts.indeterminateAttribute;
ivhTreeviewBfs(tree, opts, function(node, parents) {
if(ng.isDefined(node[selectedAttr]) && node[selectedAttr] !== bias) {
exports.select(tree, node, opts, !bias);
return false;
} else {
node[selectedAttr] = bias;
node[indeterminateAttr] = false;
}
});
return exports;
};
/**
* Expand/collapse a given tree node
*
* `node` may be either an actual tree node object or a node id.
*
* `opts` may override any of the defaults set by `ivhTreeviewOptions`.
*
* @param {Object|Array} tree The tree data
* @param {Object|String} node The node (or id) to expand/collapse
* @param {Object} opts [optional] Options to override default options with
* @param {Boolean} isExpanded [optional] Whether or not to expand `node`, defaults to `true`
* @return {Object} Returns the ivhTreeviewMgr instance for chaining
*/
exports.expand = function(tree, node, opts, isExpanded) {
if(arguments.length > 2) {
if(typeof opts === 'boolean') {
isExpanded = opts;
opts = {};
}
}
opts = ng.extend({}, options, opts);
isExpanded = ng.isDefined(isExpanded) ? isExpanded : true;
var useId = isId(node)
, expandedAttr = opts.expandedAttribute;
if(!useId) {
// No need to do any searching if we already have the node in hand
node[expandedAttr] = isExpanded;
return exports;
}
return findNode(tree, node, opts, function(n, p) {
n[expandedAttr] = isExpanded;
return exports;
});
};
/**
* Expand/collapse a given tree node and its children
*
* `node` may be either an actual tree node object or a node id. You may
* leave off `node` entirely to expand/collapse the entire tree, however, if
* you specify a value for `opts` or `isExpanded` you must provide a value
* for `node`.
*
* `opts` may override any of the defaults set by `ivhTreeviewOptions`.
*
* @param {Object|Array} tree The tree data
* @param {Object|String} node [optional*] The node (or id) to expand/collapse recursively
* @param {Object} opts [optional] Options to override default options with
* @param {Boolean} isExpanded [optional] Whether or not to expand `node`, defaults to `true`
* @return {Object} Returns the ivhTreeviewMgr instance for chaining
*/
exports.expandRecursive = function(tree, node, opts, isExpanded) {
if(arguments.length > 2) {
if(typeof opts === 'boolean') {
isExpanded = opts;
opts = {};
}
}
node = ng.isDefined(node) ? node : tree;
opts = ng.extend({}, options, opts);
isExpanded = ng.isDefined(isExpanded) ? isExpanded : true;
var useId = isId(node)
, expandedAttr = opts.expandedAttribute
, branch;
// If we have an ID first resolve it to an actual node in the tree
if(useId) {
findNode(tree, node, opts, function(n, p) {
branch = n;
});
} else {
branch = node;
}
if(branch) {
ivhTreeviewBfs(branch, opts, function(n, p) {
n[expandedAttr] = isExpanded;
});
}
return exports;
};
/**
* Collapse a given tree node
*
* Delegates to `exports.expand` with `isExpanded` set to `false`.
*
* @param {Object|Array} tree The tree data
* @param {Object|String} node The node (or id) to collapse
* @param {Object} opts [optional] Options to override default options with
* @return {Object} Returns the ivhTreeviewMgr instance for chaining
*/
exports.collapse = function(tree, node, opts) {
return exports.expand(tree, node, opts, false);
};
/**
* Collapse a given tree node and its children
*
* Delegates to `exports.expandRecursive` with `isExpanded` set to `false`.
*
* @param {Object|Array} tree The tree data
* @param {Object|String} node The node (or id) to expand/collapse recursively
* @param {Object} opts [optional] Options to override default options with
* @return {Object} Returns the ivhTreeviewMgr instance for chaining
*/
exports.collapseRecursive = function(tree, node, opts, isExpanded) {
return exports.expandRecursive(tree, node, opts, false);
};
/**
* Expand[/collapse] all parents of a given node, i.e. "reveal" the node
*
* @param {Object|Array} tree The tree data
* @param {Object|String} node The node (or id) to expand to
* @param {Object} opts [optional] Options to override default options with
* @param {Boolean} isExpanded [optional] Whether or not to expand parent nodes
* @return {Object} Returns the ivhTreeviewMgr instance for chaining
*/
exports.expandTo = function(tree, node, opts, isExpanded) {
if(arguments.length > 2) {
if(typeof opts === 'boolean') {
isExpanded = opts;
opts = {};
}
}
opts = ng.extend({}, options, opts);
isExpanded = ng.isDefined(isExpanded) ? isExpanded : true;
var expandedAttr = opts.expandedAttribute;
var expandCollapseNode = function(n) {
n[expandedAttr] = isExpanded;
};
// Even if wer were given the actual node and not its ID we must still
// traverse the tree to find that node's parents.
return findNode(tree, node, opts, function(n, p) {
ng.forEach(p, expandCollapseNode);
return exports;
});
};
/**
* Collapse all parents of a give node
*
* Delegates to `exports.expandTo` with `isExpanded` set to `false`.
*
* @param {Object|Array} tree The tree data
* @param {Object|String} node The node (or id) to expand to
* @param {Object} opts [optional] Options to override default options with
* @return {Object} Returns the ivhTreeviewMgr instance for chaining
*/
exports.collapseParents = function(tree, node, opts) {
return exports.expandTo(tree, node, opts, false);
};
return exports;
}
]);