'
116 | ].join('\n')
117 | .replace(new RegExp('{{', 'g'), symbolStart)
118 | .replace(new RegExp('}}', 'g'), symbolEnd)
119 | };
120 |
121 | /**
122 | * Update global options
123 | *
124 | * @param {Object} opts options object to override defaults with
125 | */
126 | this.set = function(opts) {
127 | angular.extend(options, opts);
128 | };
129 |
130 | this.$get = function() {
131 | /**
132 | * Get a copy of the global options
133 | *
134 | * @return {Object} The options object
135 | */
136 | return function() {
137 | return angular.copy(options);
138 | };
139 | };
140 | }]);
141 |
--------------------------------------------------------------------------------
/docs/templates-and-skins.md:
--------------------------------------------------------------------------------
1 |
2 | ## Contents
3 |
4 | - [Basic Skins](#basic-skins)
5 | - [Tree Layout](#tree-layout)
6 | - [Global Templates](#global-templates)
7 | - [Inline Templates](#inline-templates)
8 | - [Template Helper Directives](#template-helper-directives)
9 | - [Supported Template Scope Variables](#supported-template-scope-variables)
10 |
11 |
12 | ### Skins
13 |
14 | Custom node templates are most likely overkill for style tweaks. Making the look
15 | and feel of your tree match the rest of your application can often be
16 | accomplished with a bit of css and your own twisties. IVH Treeview ships with
17 | only minimal styling and you are encouraged to apply your own styles.
18 |
19 | #### Tree Layout
20 |
21 | Using the default template your tree will have the following general layout to
22 | aid in styling:
23 |
24 | ```
25 | ul.ivh-treeview
26 | li.ivh-treeview-node[?.ivh-treeview-node-collapsed][?.ivh-treeview-node-leaf]
27 |
28 |
29 |
30 | .ivh-treeview-node-content
31 | .ivh-treeview-twistie-wrapper
32 | .ivh-treeview-twistie
33 | [?.ivh-treeview-twistie-collapsed]
34 | [?.ivh-treeview-twistie-expanded]
35 | [?.ivh-treeview-twistie-leaf]
36 | .ivh-treeview-checkbox-wrapper
37 | .ivh-treeview-checkbox
38 | .ivh-treeview-node-label
39 | ul.ivh-treeview
40 | [... more nodes]
41 |
42 | [... more nodes]
43 | ```
44 |
45 | Where `ivh-treeview-node-collapsed` and the various twistie classnames are
46 | conditionally applied as appropriate.
47 |
48 | The top level `li` for a given node is give the classname
49 | `ivh-treeview-node-leaf` when it is a leaf node.
50 |
51 | ### Global Templates
52 |
53 | Tree node templates can be set globally using the `nodeTpl` options:
54 |
55 | ```
56 | app.config(function(ivhTreeviewOptionsProvider) {
57 | ivhTreeviewOptionsProvider.set({
58 | nodeTpl: ''
59 | });
60 | });
61 | ```
62 |
63 | ### Inline Templates
64 |
65 | Want different node templates for different trees? This can be accomplished
66 | using inline templates. Inline templates can be specified in any of three ways:
67 |
68 | With the `ivh-treeview-node-tpl` attribute:
69 |
70 | ```
71 |
73 | ```
74 |
75 | ***Demo***: [Custom templates: inline](http://jsbin.com/fokunu/edit)
76 |
77 | As a property in the `ivh-treeview-options` object:
78 |
79 | ```
80 |
82 | ```
83 |
84 | Or as transcluded content in the treeview directive itself:
85 |
86 | ```
87 |
88 |
101 |
102 | ```
103 |
104 | ***Demo***: [Custom templates: transcluded](http://jsbin.com/jaqosi/edit)
105 |
106 | Note the use of the ng-template script tag wrapping the rest of the transcluded
107 | content, this wrapper is a mandatory. Also note that this form is intended to
108 | serve as a convenient and declarative way to essentially provide a template
109 | string to your treeview. The template itself does not (currently) have access a
110 | transcluded scope.
111 |
112 |
113 | ### Template Helper Directives
114 |
115 | You have access to a number of helper directives when building your node
116 | templates. These are mostly optional but should make your life a bit easier, not
117 | that all support both element and attribute level usage:
118 |
119 | - `ivh-treeview-toggle` (*attribute*) Clicking this element will expand or
120 | collapse the tree node if it is not a leaf.
121 | - `ivh-treeview-twistie` (*attribute*) Display as either an "expanded" or
122 | "collapsed" twistie as appropriate.
123 | - `ivh-treeview-checkbox` (*attribute*|*element*) A checkbox that is "plugged
124 | in" to the treeview. It will reflect your node's selected state and update
125 | parents and children appropriately out of the box.
126 | - `ivh-treeview-children` (*attribute*|*element*) The recursive step. If you
127 | want your tree to display more than one level of nodes you will need to place
128 | this some where, or have your own way of getting child nodes into the view.
129 |
130 | #### Supported Template Scope Variables
131 |
132 | **`node`**
133 |
134 | A reference to the tree node itself. Note that in general you should use
135 | controller helper methods to access node properties when possible.
136 |
137 | **`depth`**
138 |
139 | The depth of the current node in the tree. The root node will be at depth `0`,
140 | its children will be at depth `1`, etc.
141 |
142 | **`trvw`**
143 |
144 | A reference to the treeview controller with a number of useful properties and
145 | helper functions:
146 |
147 | - `trvw.select(Object node[, Boolean isSelected])`
148 | Set the seleted state of `node` to `isSelected`. The will update parent and
149 | child node selected states appropriately. `isSelected` defaults to `true`.
150 | - `trvw.isSelected(Object node) -> Boolean`
151 | Returns `true` if `node` is selected and `false` otherwise.
152 | - `trvw.toggleSelected(Object node)`
153 | Toggles the selected state of `node`. This will update parent and child note
154 | selected states appropriately.
155 | - `trvw.expand(Object node[, Boolean isExpanded])`
156 | Set the expanded state of `node` to `isExpanded`, i.e. expand or collapse
157 | `node`. `isExpanded` defaults to `true`.
158 | - `trvw.isExpanded(Object node) --> Boolean`
159 | Returns `true` if `node` is expanded and `false` otherwise.
160 | - `trvw.toggleExpanded(Object node)`
161 | Toggle the expanded state of `node`.
162 | - `trvw.isLeaf(Object node) --> Boolean`
163 | Returns `true` if `node` is a leaf node in the tree and `false` otherwise.
164 | - `trvw.label(Object node) --> String`
165 | Returns the label attribute of `node` as determined by the `labelAttribute`
166 | treeview option.
167 | - `trvw.root() --> Array|Object`
168 | Returns the tree root as handed to `ivh-treeview`.
169 | - `trvw.children(Object node) --> Array`
170 | Returns the array of children for `node`. Returns an empty array if `node` has
171 | no children or the `childrenAttribute` property value is not defined.
172 | - `trvw.opts() --> Object`
173 | Returns a merged version of the global and local options.
174 | - `trvw.isVisible(Object node) --> Boolean`
175 | Returns `true` if `node` should be considered visible under the current
176 | **filter** and `false` otherwise. Note that this only relates to treeview
177 | filters and does not take into account whether or not `node` can actually be
178 | seen as a result of expanded/collapsed parents.
179 | - `trvw.useCheckboxes() --> Boolean`
180 | Returns `true` if checkboxes should be used in the template and `false`
181 | otherwise.
182 |
183 |
--------------------------------------------------------------------------------
/dist/ivh-treeview.min.js:
--------------------------------------------------------------------------------
1 | angular.module("ivh.treeview",[]),angular.module("ivh.treeview").constant("ivhTreeviewInterpolateEndSymbol","}}"),angular.module("ivh.treeview").constant("ivhTreeviewInterpolateStartSymbol","{{"),angular.module("ivh.treeview").directive("ivhTreeviewCheckboxHelper",[function(){"use strict";return{restrict:"A",scope:{node:"=ivhTreeviewCheckboxHelper"},require:"^ivhTreeview",link:function(e,t,i,n){var r=e.node,l=n.opts(),o=l.indeterminateAttribute,a=l.selectedAttribute;e.isSelected=r[a],e.trvw=n,e.resolveIndeterminateClick=function(){l.disableCheckboxSelectionPropagation||r[o]&&n.select(r,!0)},e.$watch("node."+a,function(t,i){e.isSelected=t}),l.disableCheckboxSelectionPropagation||e.$watch("node."+o,function(e,i){t.find("input").prop("indeterminate",e)})},template:[''].join("\n")}}]),angular.module("ivh.treeview").directive("ivhTreeviewCheckbox",[function(){"use strict";return{restrict:"AE",require:"^ivhTreeview",template:''}}]),angular.module("ivh.treeview").directive("ivhTreeviewChildren",function(){"use strict";return{restrict:"AE",require:"^ivhTreeviewNode",template:['
14 | * ```
15 | *
16 | * @package ivh.treeview
17 | * @copyright 2014 iVantage Health Analytics, Inc.
18 | */
19 |
20 | angular.module('ivh.treeview').directive('ivhTreeview', ['ivhTreeviewMgr', function(ivhTreeviewMgr) {
21 | 'use strict';
22 | return {
23 | restrict: 'A',
24 | transclude: true,
25 | scope: {
26 | // The tree data store
27 | root: '=ivhTreeview',
28 |
29 | // Specific config options
30 | childrenAttribute: '=ivhTreeviewChildrenAttribute',
31 | defaultSelectedState: '=ivhTreeviewDefaultSelectedState',
32 | disableCheckboxSelectionPropagation: '=ivhTreeviewDisableCheckboxSelectionPropagation',
33 | expandToDepth: '=ivhTreeviewExpandToDepth',
34 | idAttribute: '=ivhTreeviewIdAttribute',
35 | indeterminateAttribute: '=ivhTreeviewIndeterminateAttribute',
36 | expandedAttribute: '=ivhTreeviewExpandedAttribute',
37 | labelAttribute: '=ivhTreeviewLabelAttribute',
38 | nodeTpl: '=ivhTreeviewNodeTpl',
39 | selectedAttribute: '=ivhTreeviewSelectedAttribute',
40 | onCbChange: '&ivhTreeviewOnCbChange',
41 | onToggle: '&ivhTreeviewOnToggle',
42 | twistieCollapsedTpl: '=ivhTreeviewTwistieCollapsedTpl',
43 | twistieExpandedTpl: '=ivhTreeviewTwistieExpandedTpl',
44 | twistieLeafTpl: '=ivhTreeviewTwistieLeafTpl',
45 | useCheckboxes: '=ivhTreeviewUseCheckboxes',
46 | validate: '=ivhTreeviewValidate',
47 | visibleAttribute: '=ivhTreeviewVisibleAttribute',
48 |
49 | // Generic options object
50 | userOptions: '=ivhTreeviewOptions',
51 |
52 | // The filter
53 | filter: '=ivhTreeviewFilter'
54 | },
55 | controllerAs: 'trvw',
56 | controller: ['$scope', '$element', '$attrs', '$transclude', 'ivhTreeviewOptions', 'filterFilter', function($scope, $element, $attrs, $transclude, ivhTreeviewOptions, filterFilter) {
57 | var ng = angular
58 | , trvw = this;
59 |
60 | // Merge any locally set options with those registered with hte
61 | // ivhTreeviewOptions provider
62 | var localOpts = ng.extend({}, ivhTreeviewOptions(), $scope.userOptions);
63 |
64 | // Two-way bound attributes (=) can be copied over directly if they're
65 | // non-empty
66 | ng.forEach([
67 | 'childrenAttribute',
68 | 'defaultSelectedState',
69 | 'disableCheckboxSelectionPropagation',
70 | 'expandToDepth',
71 | 'idAttribute',
72 | 'indeterminateAttribute',
73 | 'expandedAttribute',
74 | 'labelAttribute',
75 | 'nodeTpl',
76 | 'selectedAttribute',
77 | 'twistieCollapsedTpl',
78 | 'twistieExpandedTpl',
79 | 'twistieLeafTpl',
80 | 'useCheckboxes',
81 | 'validate',
82 | 'visibleAttribute'
83 | ], function(attr) {
84 | if(ng.isDefined($scope[attr])) {
85 | localOpts[attr] = $scope[attr];
86 | }
87 | });
88 |
89 | // Attrs with the `&` prefix will yield a defined scope entity even if
90 | // no value is specified. We must check to make sure the attribute string
91 | // is non-empty before copying over the scope value.
92 | var normedAttr = function(attrKey) {
93 | return 'ivhTreeview' +
94 | attrKey.charAt(0).toUpperCase() +
95 | attrKey.slice(1);
96 | };
97 |
98 | ng.forEach([
99 | 'onCbChange',
100 | 'onToggle'
101 | ], function(attr) {
102 | if($attrs[normedAttr(attr)]) {
103 | localOpts[attr] = $scope[attr];
104 | }
105 | });
106 |
107 | // Treat the transcluded content (if there is any) as our node template
108 | var transcludedScope;
109 | $transclude(function(clone, scope) {
110 | var transcludedNodeTpl = '';
111 | angular.forEach(clone, function(c) {
112 | transcludedNodeTpl += (c.innerHTML || '').trim();
113 | });
114 | if(transcludedNodeTpl.length) {
115 | transcludedScope = scope;
116 | localOpts.nodeTpl = transcludedNodeTpl;
117 | }
118 | });
119 |
120 | /**
121 | * Get the merged global and local options
122 | *
123 | * @return {Object} the merged options
124 | */
125 | trvw.opts = function() {
126 | return localOpts;
127 | };
128 |
129 | // If we didn't provide twistie templates we'll be doing a fair bit of
130 | // extra checks for no reason. Let's just inform down stream directives
131 | // whether or not they need to worry about twistie non-global templates.
132 | var userOpts = $scope.userOptions || {};
133 |
134 | /**
135 | * Whether or not we have local twistie templates
136 | *
137 | * @private
138 | */
139 | trvw.hasLocalTwistieTpls = !!(
140 | userOpts.twistieCollapsedTpl ||
141 | userOpts.twistieExpandedTpl ||
142 | userOpts.twistieLeafTpl ||
143 | $scope.twistieCollapsedTpl ||
144 | $scope.twistieExpandedTpl ||
145 | $scope.twistieLeafTpl);
146 |
147 | /**
148 | * Get the child nodes for `node`
149 | *
150 | * Abstracts away the need to know the actual label attribute in
151 | * templates.
152 | *
153 | * @param {Object} node a tree node
154 | * @return {Array} the child nodes
155 | */
156 | trvw.children = function(node) {
157 | var children = node[localOpts.childrenAttribute];
158 | return ng.isArray(children) ? children : [];
159 | };
160 |
161 | /**
162 | * Get the label for `node`
163 | *
164 | * Abstracts away the need to know the actual label attribute in
165 | * templates.
166 | *
167 | * @param {Object} node A tree node
168 | * @return {String} The node label
169 | */
170 | trvw.label = function(node) {
171 | return node[localOpts.labelAttribute];
172 | };
173 |
174 | /**
175 | * Returns `true` if this treeview has a filter
176 | *
177 | * @return {Boolean} Whether on not we have a filter
178 | * @private
179 | */
180 | trvw.hasFilter = function() {
181 | return ng.isDefined($scope.filter);
182 | };
183 |
184 | /**
185 | * Get the treeview filter
186 | *
187 | * @return {String} The filter string
188 | * @private
189 | */
190 | trvw.getFilter = function() {
191 | return $scope.filter || '';
192 | };
193 |
194 | /**
195 | * Returns `true` if current filter should hide `node`, false otherwise
196 | *
197 | * @todo Note that for object and function filters each node gets hit with
198 | * `isVisible` N-times where N is its depth in the tree. We may be able to
199 | * optimize `isVisible` in this case by:
200 | *
201 | * - On first call to `isVisible` in a given digest cycle walk the tree to
202 | * build a flat array of nodes.
203 | * - Run the array of nodes through the filter.
204 | * - Build a map (`id`/$scopeId --> true) for the nodes that survive the
205 | * filter
206 | * - On subsequent calls to `isVisible` just lookup the node id in our
207 | * map.
208 | * - Clean the map with a $timeout (?)
209 | *
210 | * In theory the result of a call to `isVisible` could change during a
211 | * digest cycle as scope variables are updated... I think calls would
212 | * happen bottom up (i.e. from "leaf" to "root") so that might not
213 | * actually be an issue. Need to investigate if this ends up feeling for
214 | * large/deep trees.
215 | *
216 | * @param {Object} node A tree node
217 | * @return {Boolean} Whether or not `node` is filtered out
218 | */
219 | trvw.isVisible = function(node) {
220 | var filter = trvw.getFilter();
221 |
222 | // Quick shortcut
223 | if(!filter || filterFilter([node], filter).length) {
224 | return true;
225 | }
226 |
227 | // If we have an object or function filter we have to check children
228 | // separately
229 | if(typeof filter === 'object' || typeof filter === 'function') {
230 | var children = trvw.children(node);
231 | // If any child is visible then so is this node
232 | for(var ix = children.length; ix--;) {
233 | if(trvw.isVisible(children[ix])) {
234 | return true;
235 | }
236 | }
237 | }
238 |
239 | return false;
240 | };
241 |
242 | /**
243 | * Returns `true` if we should use checkboxes, false otherwise
244 | *
245 | * @return {Boolean} Whether or not to use checkboxes
246 | */
247 | trvw.useCheckboxes = function() {
248 | return localOpts.useCheckboxes;
249 | };
250 |
251 | /**
252 | * Select or deselect `node`
253 | *
254 | * Updates parent and child nodes appropriately, `isSelected` defaults to
255 | * `true`.
256 | *
257 | * @param {Object} node The node to select or deselect
258 | * @param {Boolean} isSelected Defaults to `true`
259 | */
260 | trvw.select = function(node, isSelected) {
261 | ivhTreeviewMgr.select($scope.root, node, localOpts, isSelected);
262 | trvw.onCbChange(node, isSelected);
263 | };
264 |
265 | /**
266 | * Get the selected state of `node`
267 | *
268 | * @param {Object} node The node to get the selected state of
269 | * @return {Boolean} `true` if `node` is selected
270 | */
271 | trvw.isSelected = function(node) {
272 | return node[localOpts.selectedAttribute];
273 | };
274 |
275 | /**
276 | * Toggle the selected state of `node`
277 | *
278 | * Updates parent and child node selected states appropriately.
279 | *
280 | * @param {Object} node The node to update
281 | */
282 | trvw.toggleSelected = function(node) {
283 | var isSelected = !node[localOpts.selectedAttribute];
284 | trvw.select(node, isSelected);
285 | };
286 |
287 | /**
288 | * Expand or collapse a given node
289 | *
290 | * `isExpanded` is optional and defaults to `true`.
291 | *
292 | * @param {Object} node The node to expand/collapse
293 | * @param {Boolean} isExpanded Whether to expand (`true`) or collapse
294 | */
295 | trvw.expand = function(node, isExpanded) {
296 | ivhTreeviewMgr.expand($scope.root, node, localOpts, isExpanded);
297 | };
298 |
299 | /**
300 | * Get the expanded state of a given node
301 | *
302 | * @param {Object} node The node to check the expanded state of
303 | * @return {Boolean}
304 | */
305 | trvw.isExpanded = function(node) {
306 | return node[localOpts.expandedAttribute];
307 | };
308 |
309 | /**
310 | * Toggle the expanded state of a given node
311 | *
312 | * @param {Object} node The node to toggle
313 | */
314 | trvw.toggleExpanded = function(node) {
315 | trvw.expand(node, !trvw.isExpanded(node));
316 | };
317 |
318 | /**
319 | * Whether or not nodes at `depth` should be expanded by default
320 | *
321 | * Use -1 to fully expand the tree by default.
322 | *
323 | * @param {Integer} depth The depth to expand to
324 | * @return {Boolean} Whether or not nodes at `depth` should be expanded
325 | * @private
326 | */
327 | trvw.isInitiallyExpanded = function(depth) {
328 | var expandTo = localOpts.expandToDepth === -1 ?
329 | Infinity : localOpts.expandToDepth;
330 | return depth < expandTo;
331 | };
332 |
333 | /**
334 | * Returns `true` if `node` is a leaf node
335 | *
336 | * @param {Object} node The node to check
337 | * @return {Boolean} `true` if `node` is a leaf
338 | */
339 | trvw.isLeaf = function(node) {
340 | return trvw.children(node).length === 0;
341 | };
342 |
343 | /**
344 | * Get the tree node template
345 | *
346 | * @return {String} The node template
347 | * @private
348 | */
349 | trvw.getNodeTpl = function() {
350 | return localOpts.nodeTpl;
351 | };
352 |
353 | /**
354 | * Get the root of the tree
355 | *
356 | * Mostly a helper for custom templates
357 | *
358 | * @return {Object|Array} The tree root
359 | * @private
360 | */
361 | trvw.root = function() {
362 | return $scope.root;
363 | };
364 |
365 | /**
366 | * Call the registered toggle handler
367 | *
368 | * Handler will get a reference to `node` and the root of the tree.
369 | *
370 | * @param {Object} node Tree node to pass to the handler
371 | * @private
372 | */
373 | trvw.onToggle = function(node) {
374 | if(localOpts.onToggle) {
375 | var locals = {
376 | ivhNode: node,
377 | ivhIsExpanded: trvw.isExpanded(node),
378 | ivhTree: $scope.root
379 | };
380 | localOpts.onToggle(locals);
381 | }
382 | };
383 |
384 | /**
385 | * Call the registered selection change handler
386 | *
387 | * Handler will get a reference to `node`, the new selected state of
388 | * `node, and the root of the tree.
389 | *
390 | * @param {Object} node Tree node to pass to the handler
391 | * @param {Boolean} isSelected Selected state for `node`
392 | * @private
393 | */
394 | trvw.onCbChange = function(node, isSelected) {
395 | if(localOpts.onCbChange) {
396 | var locals = {
397 | ivhNode: node,
398 | ivhIsSelected: isSelected,
399 | ivhTree: $scope.root
400 | };
401 | localOpts.onCbChange(locals);
402 | }
403 | };
404 | }],
405 | link: function(scope, element, attrs) {
406 | var opts = scope.trvw.opts();
407 |
408 | // Allow opt-in validate on startup
409 | if(opts.validate) {
410 | ivhTreeviewMgr.validate(scope.root, opts);
411 | }
412 | },
413 | template: [
414 | '
',
415 | '
',
421 | '
',
422 | '
'
423 | ].join('\n')
424 | };
425 | }]);
426 |
--------------------------------------------------------------------------------
/src/scripts/services/ivh-treeview-mgr.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * Manager for treeview data stores
4 | *
5 | * Used to assist treeview operations, e.g. selecting or validating a tree-like
6 | * collection.
7 | *
8 | * @package ivh.treeview
9 | * @copyright 2014 iVantage Health Analytics, Inc.
10 | */
11 |
12 | angular.module('ivh.treeview')
13 | .factory('ivhTreeviewMgr', ['ivhTreeviewOptions', 'ivhTreeviewBfs', function(ivhTreeviewOptions, ivhTreeviewBfs) {
14 | 'use strict';
15 |
16 | var ng = angular
17 | , options = ivhTreeviewOptions()
18 | , exports = {};
19 |
20 | // The make* methods and validateParent need to be bound to an options
21 | // object
22 | var makeDeselected = function(node) {
23 | node[this.selectedAttribute] = false;
24 | node[this.indeterminateAttribute] = false;
25 | };
26 |
27 | var makeSelected = function(node) {
28 | node[this.selectedAttribute] = true;
29 | node[this.indeterminateAttribute] = false;
30 | };
31 |
32 | var validateParent = function(node) {
33 | var children = node[this.childrenAttribute]
34 | , selectedAttr = this.selectedAttribute
35 | , indeterminateAttr = this.indeterminateAttribute
36 | , numSelected = 0
37 | , numIndeterminate = 0;
38 | ng.forEach(children, function(n, ix) {
39 | if(n[selectedAttr]) {
40 | numSelected++;
41 | } else {
42 | if(n[indeterminateAttr]) {
43 | numIndeterminate++;
44 | }
45 | }
46 | });
47 |
48 | if(0 === numSelected && 0 === numIndeterminate) {
49 | node[selectedAttr] = false;
50 | node[indeterminateAttr] = false;
51 | } else if(numSelected === children.length) {
52 | node[selectedAttr] = true;
53 | node[indeterminateAttr] = false;
54 | } else {
55 | node[selectedAttr] = false;
56 | node[indeterminateAttr] = true;
57 | }
58 | };
59 |
60 | var isId = function(val) {
61 | return ng.isString(val) || ng.isNumber(val);
62 | };
63 |
64 | var findNode = function(tree, node, opts, cb) {
65 | var useId = isId(node)
66 | , proceed = true
67 | , idAttr = opts.idAttribute;
68 |
69 | // Our return values
70 | var foundNode = null
71 | , foundParents = [];
72 |
73 | ivhTreeviewBfs(tree, opts, function(n, p) {
74 | var isNode = proceed && (useId ?
75 | node === n[idAttr] :
76 | node === n);
77 |
78 | if(isNode) {
79 | // I've been looking for you all my life
80 | proceed = false;
81 | foundNode = n;
82 | foundParents = p;
83 | }
84 |
85 | return proceed;
86 | });
87 |
88 | return cb(foundNode, foundParents);
89 | };
90 |
91 | /**
92 | * Select (or deselect) a tree node
93 | *
94 | * This method will update the rest of the tree to account for your change.
95 | *
96 | * You may alternatively pass an id as `node`, in which case the tree will
97 | * be searched for your item.
98 | *
99 | * @param {Object|Array} tree The tree data
100 | * @param {Object|String} node The node (or id) to (de)select
101 | * @param {Object} opts [optional] Options to override default options with
102 | * @param {Boolean} isSelected [optional] Whether or not to select `node`, defaults to `true`
103 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining
104 | */
105 | exports.select = function(tree, node, opts, isSelected) {
106 | if(arguments.length > 2) {
107 | if(typeof opts === 'boolean') {
108 | isSelected = opts;
109 | opts = {};
110 | }
111 | }
112 | opts = ng.extend({}, options, opts);
113 | isSelected = ng.isDefined(isSelected) ? isSelected : true;
114 |
115 | var useId = isId(node)
116 | , proceed = true
117 | , idAttr = opts.idAttribute;
118 |
119 | ivhTreeviewBfs(tree, opts, function(n, p) {
120 | var isNode = proceed && (useId ?
121 | node === n[idAttr] :
122 | node === n);
123 |
124 | if(isNode) {
125 | // I've been looking for you all my life
126 | proceed = false;
127 |
128 | var cb = isSelected ?
129 | makeSelected.bind(opts) :
130 | makeDeselected.bind(opts);
131 |
132 | if (opts.disableCheckboxSelectionPropagation) {
133 | cb(n);
134 | } else {
135 | ivhTreeviewBfs(n, opts, cb);
136 | ng.forEach(p, validateParent.bind(opts));
137 | }
138 | }
139 |
140 | return proceed;
141 | });
142 |
143 | return exports;
144 | };
145 |
146 | /**
147 | * Select all nodes in a tree
148 | *
149 | * `opts` will default to an empty object, `isSelected` defaults to `true`.
150 | *
151 | * @param {Object|Array} tree The tree data
152 | * @param {Object} opts [optional] Default options overrides
153 | * @param {Boolean} isSelected [optional] Whether or not to select items
154 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining
155 | */
156 | exports.selectAll = function(tree, opts, isSelected) {
157 | if(arguments.length > 1) {
158 | if(typeof opts === 'boolean') {
159 | isSelected = opts;
160 | opts = {};
161 | }
162 | }
163 |
164 | opts = ng.extend({}, options, opts);
165 | isSelected = ng.isDefined(isSelected) ? isSelected : true;
166 |
167 | var selectedAttr = opts.selectedAttribute
168 | , indeterminateAttr = opts.indeterminateAttribute;
169 |
170 | ivhTreeviewBfs(tree, opts, function(node) {
171 | node[selectedAttr] = isSelected;
172 | node[indeterminateAttr] = false;
173 | });
174 |
175 | return exports;
176 | };
177 |
178 | /**
179 | * Select or deselect each of the passed items
180 | *
181 | * Eventually it would be nice if this did something more intelligent than
182 | * just calling `select` on each item in the array...
183 | *
184 | * @param {Object|Array} tree The tree data
185 | * @param {Array} nodes The array of nodes or node ids
186 | * @param {Object} opts [optional] Default options overrides
187 | * @param {Boolean} isSelected [optional] Whether or not to select items
188 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining
189 | */
190 | exports.selectEach = function(tree, nodes, opts, isSelected) {
191 | /**
192 | * @todo Surely we can do something better than this...
193 | */
194 | ng.forEach(nodes, function(node) {
195 | exports.select(tree, node, opts, isSelected);
196 | });
197 | return exports;
198 | };
199 |
200 | /**
201 | * Deselect a tree node
202 | *
203 | * Delegates to `ivhTreeviewMgr.select` with `isSelected` set to `false`.
204 | *
205 | * @param {Object|Array} tree The tree data
206 | * @param {Object|String} node The node (or id) to (de)select
207 | * @param {Object} opts [optional] Options to override default options with
208 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining
209 | */
210 | exports.deselect = function(tree, node, opts) {
211 | return exports.select(tree, node, opts, false);
212 | };
213 |
214 | /**
215 | * Deselect all nodes in a tree
216 | *
217 | * Delegates to `ivhTreeviewMgr.selectAll` with `isSelected` set to `false`.
218 | *
219 | * @param {Object|Array} tree The tree data
220 | * @param {Object} opts [optional] Default options overrides
221 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining
222 | */
223 | exports.deselectAll = function(tree, opts) {
224 | return exports.selectAll(tree, opts, false);
225 | };
226 |
227 | /**
228 | * Deselect each of the passed items
229 | *
230 | * Delegates to `ivhTreeviewMgr.selectEach` with `isSelected` set to
231 | * `false`.
232 | *
233 | * @param {Object|Array} tree The tree data
234 | * @param {Array} nodes The array of nodes or node ids
235 | * @param {Object} opts [optional] Default options overrides
236 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining
237 | */
238 | exports.deselectEach = function(tree, nodes, opts) {
239 | return exports.selectEach(tree, nodes, opts, false);
240 | };
241 |
242 | /**
243 | * Validate tree for parent/child selection consistency
244 | *
245 | * Assumes `bias` as default selected state. The first element with
246 | * `node.select !== bias` will be assumed correct. For example, if `bias` is
247 | * `true` (the default) we'll traverse the tree until we come to an
248 | * unselected node at which point we stop and deselect each of that node's
249 | * children (and their children, etc.).
250 | *
251 | * Indeterminate states will also be resolved.
252 | *
253 | * @param {Object|Array} tree The tree data
254 | * @param {Object} opts [optional] Options to override default options with
255 | * @param {Boolean} bias [optional] Default selected state
256 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining
257 | */
258 | exports.validate = function(tree, opts, bias) {
259 | if(!tree) {
260 | // Guard against uninitialized trees
261 | return exports;
262 | }
263 |
264 | if(arguments.length > 1) {
265 | if(typeof opts === 'boolean') {
266 | bias = opts;
267 | opts = {};
268 | }
269 | }
270 | opts = ng.extend({}, options, opts);
271 | bias = ng.isDefined(bias) ? bias : opts.defaultSelectedState;
272 |
273 | var selectedAttr = opts.selectedAttribute
274 | , indeterminateAttr = opts.indeterminateAttribute;
275 |
276 | ivhTreeviewBfs(tree, opts, function(node, parents) {
277 | if(ng.isDefined(node[selectedAttr]) && node[selectedAttr] !== bias) {
278 | exports.select(tree, node, opts, !bias);
279 | return false;
280 | } else {
281 | node[selectedAttr] = bias;
282 | node[indeterminateAttr] = false;
283 | }
284 | });
285 |
286 | return exports;
287 | };
288 |
289 | /**
290 | * Expand/collapse a given tree node
291 | *
292 | * `node` may be either an actual tree node object or a node id.
293 | *
294 | * `opts` may override any of the defaults set by `ivhTreeviewOptions`.
295 | *
296 | * @param {Object|Array} tree The tree data
297 | * @param {Object|String} node The node (or id) to expand/collapse
298 | * @param {Object} opts [optional] Options to override default options with
299 | * @param {Boolean} isExpanded [optional] Whether or not to expand `node`, defaults to `true`
300 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining
301 | */
302 | exports.expand = function(tree, node, opts, isExpanded) {
303 | if(arguments.length > 2) {
304 | if(typeof opts === 'boolean') {
305 | isExpanded = opts;
306 | opts = {};
307 | }
308 | }
309 | opts = ng.extend({}, options, opts);
310 | isExpanded = ng.isDefined(isExpanded) ? isExpanded : true;
311 |
312 | var useId = isId(node)
313 | , expandedAttr = opts.expandedAttribute;
314 |
315 | if(!useId) {
316 | // No need to do any searching if we already have the node in hand
317 | node[expandedAttr] = isExpanded;
318 | return exports;
319 | }
320 |
321 | return findNode(tree, node, opts, function(n, p) {
322 | n[expandedAttr] = isExpanded;
323 | return exports;
324 | });
325 | };
326 |
327 | /**
328 | * Expand/collapse a given tree node and its children
329 | *
330 | * `node` may be either an actual tree node object or a node id. You may
331 | * leave off `node` entirely to expand/collapse the entire tree, however, if
332 | * you specify a value for `opts` or `isExpanded` you must provide a value
333 | * for `node`.
334 | *
335 | * `opts` may override any of the defaults set by `ivhTreeviewOptions`.
336 | *
337 | * @param {Object|Array} tree The tree data
338 | * @param {Object|String} node [optional*] The node (or id) to expand/collapse recursively
339 | * @param {Object} opts [optional] Options to override default options with
340 | * @param {Boolean} isExpanded [optional] Whether or not to expand `node`, defaults to `true`
341 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining
342 | */
343 | exports.expandRecursive = function(tree, node, opts, isExpanded) {
344 | if(arguments.length > 2) {
345 | if(typeof opts === 'boolean') {
346 | isExpanded = opts;
347 | opts = {};
348 | }
349 | }
350 | node = ng.isDefined(node) ? node : tree;
351 | opts = ng.extend({}, options, opts);
352 | isExpanded = ng.isDefined(isExpanded) ? isExpanded : true;
353 |
354 | var useId = isId(node)
355 | , expandedAttr = opts.expandedAttribute
356 | , branch;
357 |
358 | // If we have an ID first resolve it to an actual node in the tree
359 | if(useId) {
360 | findNode(tree, node, opts, function(n, p) {
361 | branch = n;
362 | });
363 | } else {
364 | branch = node;
365 | }
366 |
367 | if(branch) {
368 | ivhTreeviewBfs(branch, opts, function(n, p) {
369 | n[expandedAttr] = isExpanded;
370 | });
371 | }
372 |
373 | return exports;
374 | };
375 |
376 | /**
377 | * Collapse a given tree node
378 | *
379 | * Delegates to `exports.expand` with `isExpanded` set to `false`.
380 | *
381 | * @param {Object|Array} tree The tree data
382 | * @param {Object|String} node The node (or id) to collapse
383 | * @param {Object} opts [optional] Options to override default options with
384 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining
385 | */
386 | exports.collapse = function(tree, node, opts) {
387 | return exports.expand(tree, node, opts, false);
388 | };
389 |
390 | /**
391 | * Collapse a given tree node and its children
392 | *
393 | * Delegates to `exports.expandRecursive` with `isExpanded` set to `false`.
394 | *
395 | * @param {Object|Array} tree The tree data
396 | * @param {Object|String} node The node (or id) to expand/collapse recursively
397 | * @param {Object} opts [optional] Options to override default options with
398 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining
399 | */
400 | exports.collapseRecursive = function(tree, node, opts, isExpanded) {
401 | return exports.expandRecursive(tree, node, opts, false);
402 | };
403 |
404 | /**
405 | * Expand[/collapse] all parents of a given node, i.e. "reveal" the node
406 | *
407 | * @param {Object|Array} tree The tree data
408 | * @param {Object|String} node The node (or id) to expand to
409 | * @param {Object} opts [optional] Options to override default options with
410 | * @param {Boolean} isExpanded [optional] Whether or not to expand parent nodes
411 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining
412 | */
413 | exports.expandTo = function(tree, node, opts, isExpanded) {
414 | if(arguments.length > 2) {
415 | if(typeof opts === 'boolean') {
416 | isExpanded = opts;
417 | opts = {};
418 | }
419 | }
420 | opts = ng.extend({}, options, opts);
421 | isExpanded = ng.isDefined(isExpanded) ? isExpanded : true;
422 |
423 | var expandedAttr = opts.expandedAttribute;
424 |
425 | var expandCollapseNode = function(n) {
426 | n[expandedAttr] = isExpanded;
427 | };
428 |
429 | // Even if wer were given the actual node and not its ID we must still
430 | // traverse the tree to find that node's parents.
431 | return findNode(tree, node, opts, function(n, p) {
432 | ng.forEach(p, expandCollapseNode);
433 | return exports;
434 | });
435 | };
436 |
437 | /**
438 | * Collapse all parents of a give node
439 | *
440 | * Delegates to `exports.expandTo` with `isExpanded` set to `false`.
441 | *
442 | * @param {Object|Array} tree The tree data
443 | * @param {Object|String} node The node (or id) to expand to
444 | * @param {Object} opts [optional] Options to override default options with
445 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining
446 | */
447 | exports.collapseParents = function(tree, node, opts) {
448 | return exports.expandTo(tree, node, opts, false);
449 | };
450 |
451 | return exports;
452 | }
453 | ]);
454 |
--------------------------------------------------------------------------------
/test/spec/services/ivh-treeview-mgr.js:
--------------------------------------------------------------------------------
1 | /*global jQuery, describe, beforeEach, afterEach, it, module, inject, expect */
2 |
3 | describe('Service: ivhTreeviewMgr', function() {
4 | 'use strict';
5 |
6 | beforeEach(module('ivh.treeview'));
7 |
8 | var ivhTreeviewMgr;
9 |
10 | var tree
11 | , nodes
12 | , hatNodes
13 | , bagNodes
14 | , stuff
15 | , hats
16 | , fedora
17 | , flatcap
18 | , bags
19 | , messenger
20 | , backpack;
21 |
22 | beforeEach(inject(function(_ivhTreeviewMgr_) {
23 | ivhTreeviewMgr = _ivhTreeviewMgr_;
24 | }));
25 |
26 | beforeEach(function() {
27 | tree = [{
28 | label: 'Stuff',
29 | id: 'stuff',
30 | children: [{
31 | label: 'Hats',
32 | id: 'hats',
33 | children: [{
34 | label: 'Fedora',
35 | id: 'fedora'
36 | }, {
37 | label: 'Flatcap',
38 | id: 'flatcap'
39 | }]
40 | }, {
41 | label: 'Bags',
42 | id: 'bags',
43 | children: [{
44 | label: 'Messenger',
45 | id: 'messenger'
46 | }, {
47 | label: 'Backpack',
48 | id: 'backpack'
49 | }]
50 | }]
51 | }];
52 |
53 | stuff = tree[0];
54 | hats = stuff.children[0];
55 | bags = stuff.children[1];
56 | fedora = hats.children[0];
57 | flatcap = hats.children[1];
58 | messenger = bags.children[0];
59 | backpack = bags.children[1];
60 |
61 | nodes = [hats, bags, fedora, flatcap, messenger, backpack];
62 | hatNodes = [hats, fedora, flatcap];
63 | bagNodes = [bags, messenger, backpack];
64 | });
65 |
66 | describe('#select', function() {
67 |
68 | it('should select all child nodes', function() {
69 | ivhTreeviewMgr.select(tree, hats);
70 | expect(fedora.selected).toBe(true);
71 | expect(flatcap.selected).toBe(true);
72 | });
73 |
74 | it('should select nodes by id', function() {
75 | ivhTreeviewMgr.select(tree, 'hats');
76 | expect(fedora.selected).toBe(true);
77 | expect(flatcap.selected).toBe(true);
78 | });
79 |
80 | it('should allow numeric ids', function() {
81 | var t = {id: 1, label: 'One'};
82 | ivhTreeviewMgr.select(t, 1);
83 | expect(t.selected).toBe(true);
84 | });
85 |
86 | it('should make parents indeterminate if there are unselected siblings', function() {
87 | ivhTreeviewMgr.select(tree, fedora);
88 | expect(stuff.__ivhTreeviewIndeterminate).toBe(true);
89 | expect(stuff.selected).toBe(false); // Indeterminte nodes are not selected
90 | expect(hats.__ivhTreeviewIndeterminate).toBe(true);
91 | expect(hats.selected).toBe(false); // Indeterminte nodes are not selected
92 | });
93 |
94 | });
95 |
96 | describe('#select (disabled propagation)', function() {
97 |
98 | it('should not affect child nodes', function() {
99 | var options = {
100 | disableCheckboxSelectionPropagation: true
101 | };
102 | ivhTreeviewMgr.select(tree, hats, options);
103 | expect(fedora.selected).toBeFalsy();
104 | expect(flatcap.selected).toBeFalsy();
105 | });
106 |
107 | it('should NOT affect parents (indeterminate state is not used at all)', function() {
108 | var options = {
109 | disableCheckboxSelectionPropagation: true
110 | };
111 | ivhTreeviewMgr.select(tree, fedora, options);
112 |
113 | // Indeterminte state is not handled with disableCheckboxSelectionPropagation option set
114 | expect(stuff.__ivhTreeviewIndeterminate).toBeFalsy();
115 | expect(stuff.selected).toBeFalsy();
116 | expect(hats.__ivhTreeviewIndeterminate).toBeFalsy();
117 | expect(hats.selected).toBeFalsy();
118 | });
119 |
120 | });
121 |
122 | describe('#selectAll', function() {
123 |
124 | it('should select all nodes in a tree', function() {
125 | ivhTreeviewMgr.selectAll(tree);
126 | nodes.forEach(function(n) {
127 | expect(n.selected).toBe(true);
128 | expect(n.__ivhTreeviewIndeterminate).toBe(false);
129 | });
130 | });
131 |
132 | });
133 |
134 | describe('#selectEach', function() {
135 |
136 | it('should select with an array of node references', function() {
137 | ivhTreeviewMgr.selectEach(tree, [flatcap, bags]);
138 | [flatcap, bags, messenger, backpack].forEach(function(n) {
139 | expect(n.selected).toBe(true);
140 | });
141 | [stuff, hats, fedora].forEach(function(n) {
142 | expect(n.selected).not.toBe(true);
143 | });
144 | });
145 |
146 | it('should select with an array of node ids', function() {
147 | ivhTreeviewMgr.selectEach(tree, ['flatcap', 'bags']);
148 | [flatcap, bags, messenger, backpack].forEach(function(n) {
149 | expect(n.selected).toBe(true);
150 | });
151 | [stuff, hats, fedora].forEach(function(n) {
152 | expect(n.selected).not.toBe(true);
153 | });
154 | });
155 |
156 | });
157 |
158 | describe('#deselect', function() {
159 |
160 | beforeEach(function() {
161 | angular.forEach(nodes, function(n) {
162 | n.selected = true;
163 | });
164 | });
165 |
166 | it('should deselect all child nodes', function() {
167 | ivhTreeviewMgr.deselect(tree, hats);
168 | expect(fedora.selected).toBe(false);
169 | expect(flatcap.selected).toBe(false);
170 | });
171 |
172 | it('should make parents indeterminate if there are selected siblings', function() {
173 | ivhTreeviewMgr.deselect(tree, hats);
174 | expect(stuff.__ivhTreeviewIndeterminate).toBe(true);
175 | expect(stuff.selected).toBe(false); // Indeterminte nodes are not selected
176 | });
177 |
178 | });
179 |
180 | describe('#deselect (disabled propagation)', function() {
181 |
182 | beforeEach(function() {
183 | angular.forEach(nodes, function(n) {
184 | n.selected = true;
185 | });
186 | stuff.selected = true;
187 | });
188 |
189 | var options = {
190 | disableCheckboxSelectionPropagation: true
191 | };
192 |
193 | it('should deselect only hats, child nodes remain selected', function() {
194 | ivhTreeviewMgr.deselect(tree, hats, options);
195 | expect(hats.selected).toBe(false);
196 | expect(fedora.selected).toBe(true);
197 | expect(flatcap.selected).toBe(true);
198 | });
199 |
200 | it('should not affect parents state', function() {
201 | ivhTreeviewMgr.deselect(tree, hats, options);
202 | expect(stuff.selected).toBe(true);
203 | });
204 |
205 | });
206 |
207 | describe('#deselectAll', function() {
208 | beforeEach(function() {
209 | nodes.forEach(function(n) {
210 | n.selected = true;
211 | });
212 | });
213 |
214 | it('should deselect all nodes in a tree', function() {
215 | ivhTreeviewMgr.deselectAll(tree);
216 | nodes.forEach(function(n) {
217 | expect(n.selected).toBe(false);
218 | expect(n.__ivhTreeviewIndeterminate).toBe(false);
219 | });
220 | });
221 |
222 | });
223 |
224 | describe('#deselectEach', function() {
225 | beforeEach(function() {
226 | angular.forEach(nodes, function(n) {
227 | n.selected = true;
228 | });
229 | });
230 |
231 | it('should deselect with an array of node references', function() {
232 | ivhTreeviewMgr.deselectEach(tree, [flatcap, bags]);
233 | [stuff, hats, flatcap, bags, messenger, backpack].forEach(function(n) {
234 | expect(n.selected).toBe(false);
235 | });
236 | [fedora].forEach(function(n) {
237 | expect(n.selected).toBe(true);
238 | });
239 | });
240 |
241 | it('should deselect with an array of node ids', function() {
242 | ivhTreeviewMgr.deselectEach(tree, ['flatcap', 'bags']);
243 | [stuff, hats, flatcap, bags, messenger, backpack].forEach(function(n) {
244 | expect(n.selected).toBe(false);
245 | });
246 | [fedora].forEach(function(n) {
247 | expect(n.selected).toBe(true);
248 | });
249 | });
250 |
251 | });
252 |
253 | describe('#validate', function() {
254 |
255 | it('should assume selected state by default', function() {
256 | angular.forEach(nodes, function(n) {
257 | n.selected = true;
258 | });
259 | hats.selected = false;
260 | ivhTreeviewMgr.validate(tree);
261 |
262 | expect(stuff.selected).toBe(false);
263 | expect(stuff.__ivhTreeviewIndeterminate).toBe(true);
264 |
265 | expect(hats.selected).toBe(false);
266 | expect(hats.__ivhTreeviewIndeterminate).toBe(false);
267 |
268 | expect(bags.selected).toBe(true);
269 | expect(bags.__ivhTreeviewIndeterminate).toBe(false);
270 |
271 | expect(fedora.selected).toBe(false);
272 | expect(fedora.__ivhTreeviewIndeterminate).toBe(false);
273 |
274 | expect(flatcap.selected).toBe(false);
275 | expect(flatcap.__ivhTreeviewIndeterminate).toBe(false);
276 |
277 | expect(messenger.selected).toBe(true);
278 | expect(messenger.__ivhTreeviewIndeterminate).toBe(false);
279 |
280 | expect(backpack.selected).toBe(true);
281 | expect(backpack.__ivhTreeviewIndeterminate).toBe(false);
282 | });
283 |
284 | it('should not throw when validating empty/null trees', function() {
285 | var fn = function() {
286 | ivhTreeviewMgr.validate(null);
287 | };
288 | expect(fn).not.toThrow();
289 | });
290 |
291 | });
292 |
293 | describe('#expand', function() {
294 |
295 | it('should be able to expand a single node', function() {
296 | angular.forEach(nodes, function(n) {
297 | n.__ivhTreeviewExpanded = false;
298 | });
299 |
300 | ivhTreeviewMgr.expand(tree, bags);
301 |
302 | angular.forEach(nodes, function(n) {
303 | expect(n.__ivhTreeviewExpanded).toBe(n === bags);
304 | });
305 | });
306 |
307 | it('should be able to expand a single node by id', function() {
308 | angular.forEach(nodes, function(n) {
309 | n.__ivhTreeviewExpanded = false;
310 | });
311 |
312 | ivhTreeviewMgr.expand(tree, 'bags');
313 |
314 | angular.forEach(nodes, function(n) {
315 | expect(n.__ivhTreeviewExpanded).toBe(n === bags);
316 | });
317 | });
318 |
319 | it('should return ivhTreeviewMgr for chaining', function() {
320 | expect(ivhTreeviewMgr.expand(tree, bags)).toBe(ivhTreeviewMgr);
321 | });
322 |
323 | it('should honor local options for the is-expanded attribute', function() {
324 | ivhTreeviewMgr.expand(tree, bags, {expandedAttribute: 'expanded'}, true);
325 | expect(bags.expanded).toBe(true);
326 | expect(bags.__ivhTreeviewExpanded).toBeUndefined();
327 | });
328 |
329 | });
330 |
331 | describe('#expandRecursive', function() {
332 |
333 | it('should be able to expand a node and all its children', function() {
334 | angular.forEach(nodes, function(n) {
335 | n.__ivhTreeviewExpanded = false;
336 | });
337 |
338 | ivhTreeviewMgr.expandRecursive(tree, bags);
339 |
340 | angular.forEach(bagNodes, function(n) {
341 | expect(n.__ivhTreeviewExpanded).toBe(true);
342 | });
343 | angular.forEach(hatNodes, function(n) {
344 | expect(n.__ivhTreeviewExpanded).toBe(false);
345 | });
346 | });
347 |
348 | it('should be able to expand a node and all its children by id', function() {
349 | angular.forEach(nodes, function(n) {
350 | n.__ivhTreeviewExpanded = false;
351 | });
352 |
353 | ivhTreeviewMgr.expandRecursive(tree, 'bags');
354 |
355 | angular.forEach(bagNodes, function(n) {
356 | expect(n.__ivhTreeviewExpanded).toBe(true);
357 | });
358 | angular.forEach(hatNodes, function(n) {
359 | expect(n.__ivhTreeviewExpanded).toBe(false);
360 | });
361 | });
362 |
363 | it('should be able to expand the entire tree', function() {
364 | angular.forEach(nodes, function(n) {
365 | n.__ivhTreeviewExpanded = false;
366 | });
367 |
368 | ivhTreeviewMgr.expandRecursive(tree);
369 |
370 | angular.forEach(nodes, function(n) {
371 | expect(n.__ivhTreeviewExpanded).toBe(true);
372 | });
373 | });
374 |
375 | it('should return ivhTreeviewMgr for chaining', function() {
376 | expect(ivhTreeviewMgr.expandRecursive(tree, hats)).toBe(ivhTreeviewMgr);
377 | });
378 |
379 | });
380 |
381 | describe('#collapse', function() {
382 |
383 | it('should be able to callapse a single node', function() {
384 | angular.forEach(nodes, function(n) {
385 | n.__ivhTreeviewExpanded = true;
386 | });
387 |
388 | ivhTreeviewMgr.collapse(tree, bags);
389 |
390 | angular.forEach(nodes, function(n) {
391 | expect(n.__ivhTreeviewExpanded).toBe(n !== bags);
392 | });
393 | });
394 |
395 | it('should be able to callapse a single node by id', function() {
396 | angular.forEach(nodes, function(n) {
397 | n.__ivhTreeviewExpanded = true;
398 | });
399 |
400 | ivhTreeviewMgr.collapse(tree, 'bags');
401 |
402 | angular.forEach(nodes, function(n) {
403 | expect(n.__ivhTreeviewExpanded).toBe(n !== bags);
404 | });
405 | });
406 |
407 | it('should return ivhTreeviewMgr for chaining', function() {
408 | expect(ivhTreeviewMgr.collapse(tree, bags)).toBe(ivhTreeviewMgr);
409 | });
410 |
411 | });
412 |
413 | describe('#collapseRecursive', function() {
414 |
415 | it('should be able to collapse a node and all its children', function() {
416 | angular.forEach(nodes, function(n) {
417 | n.__ivhTreeviewExpanded = true;
418 | });
419 |
420 | ivhTreeviewMgr.collapseRecursive(tree, bags);
421 |
422 | angular.forEach(bagNodes, function(n) {
423 | expect(n.__ivhTreeviewExpanded).toBe(false);
424 | });
425 | angular.forEach(hatNodes, function(n) {
426 | expect(n.__ivhTreeviewExpanded).toBe(true);
427 | });
428 | });
429 |
430 | it('should be able to collapse a node and all its children by id', function() {
431 | angular.forEach(nodes, function(n) {
432 | n.__ivhTreeviewExpanded = true;
433 | });
434 |
435 | ivhTreeviewMgr.collapseRecursive(tree, 'bags');
436 |
437 | angular.forEach(bagNodes, function(n) {
438 | expect(n.__ivhTreeviewExpanded).toBe(false);
439 | });
440 | angular.forEach(hatNodes, function(n) {
441 | expect(n.__ivhTreeviewExpanded).toBe(true);
442 | });
443 | });
444 |
445 | it('should be able to collapse the entire tree', function() {
446 | angular.forEach(nodes, function(n) {
447 | n.__ivhTreeviewExpanded = true;
448 | });
449 |
450 | ivhTreeviewMgr.collapseRecursive(tree);
451 |
452 | angular.forEach(nodes, function(n) {
453 | expect(n.__ivhTreeviewExpanded).toBe(false);
454 | });
455 | });
456 |
457 | it('should return ivhTreeviewMgr for chaining', function() {
458 | expect(ivhTreeviewMgr.collapseRecursive(tree, hats)).toBe(ivhTreeviewMgr);
459 | });
460 |
461 | });
462 |
463 | describe('#expandTo', function() {
464 |
465 | it('should be able to expand all *parents* of a given node', function() {
466 | angular.forEach(nodes, function(n) {
467 | n.__ivhTreeviewExpanded = false;
468 | });
469 |
470 | ivhTreeviewMgr.expandTo(tree, fedora);
471 |
472 | var parents = [stuff, hats];
473 |
474 | angular.forEach(nodes.concat([stuff]), function(n) {
475 | expect(n.__ivhTreeviewExpanded).toBe(parents.indexOf(n) > -1);
476 | });
477 | });
478 |
479 | it('should be able to expand all *parents* of a given node by id', function() {
480 | angular.forEach(nodes, function(n) {
481 | n.__ivhTreeviewExpanded = false;
482 | });
483 |
484 | ivhTreeviewMgr.expandTo(tree, 'fedora');
485 |
486 | var parents = [stuff, hats];
487 |
488 | angular.forEach(nodes.concat([stuff]), function(n) {
489 | expect(n.__ivhTreeviewExpanded).toBe(parents.indexOf(n) > -1);
490 | });
491 | });
492 |
493 | it('should return ivhTreeviewMgr for chaining', function() {
494 | expect(ivhTreeviewMgr.expandTo(fedora)).toBe(ivhTreeviewMgr);
495 | });
496 |
497 | });
498 |
499 | describe('#collapseParents', function() {
500 |
501 | it('should be able to collapse all *parents* of a given node', function() {
502 | angular.forEach(nodes, function(n) {
503 | n.__ivhTreeviewExpanded = true;
504 | });
505 |
506 | ivhTreeviewMgr.collapseParents(tree, fedora);
507 |
508 | var parents = [stuff, hats];
509 |
510 | angular.forEach(nodes.concat([stuff]), function(n) {
511 | expect(n.__ivhTreeviewExpanded).toBe(parents.indexOf(n) === -1);
512 | });
513 | });
514 |
515 | it('should be able to collapse all *parents* of a given node by id', function() {
516 | angular.forEach(nodes, function(n) {
517 | n.__ivhTreeviewExpanded = true;
518 | });
519 |
520 | ivhTreeviewMgr.collapseParents(tree, 'fedora');
521 |
522 | var parents = [stuff, hats];
523 |
524 | angular.forEach(nodes.concat([stuff]), function(n) {
525 | expect(n.__ivhTreeviewExpanded).toBe(parents.indexOf(n) === -1);
526 | });
527 | });
528 |
529 | it('should return ivhTreeviewMgr for chaining', function() {
530 | expect(ivhTreeviewMgr.collapseParents(fedora)).toBe(ivhTreeviewMgr);
531 | });
532 |
533 | });
534 |
535 | });
536 |
537 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Angular IVH Treeview
2 |
3 | [ ![Build Status][travis-img] ][travis-link]
4 |
5 | > A treeview for AngularJS with filtering, checkbox support, custom templates,
6 | > and more.
7 |
8 | ## Contents
9 |
10 | - [Getting Started](#getting-started)
11 | - [Example Usage](#example-usage)
12 | - [Options](#options)
13 | - [Filtering](#filtering)
14 | - [Expanded by Default](#expanded-by-default)
15 | - [Default Selected State](#default-selected-state)
16 | - [Validate on Startup](#validate-on-startup)
17 | - [Twisties](#twisties)
18 | - [Templates and Skins](#templates-and-skins)
19 | - [Toggle Handlers](#toggle-handlers)
20 | - [Select/Deselect Handlers](#selectdeselect-handlers)
21 | - [All the Options](#all-the-options)
22 | - [Treeview Manager Service](#treeview-manager-service)
23 | - [`ivhTreeviewMgr.select(tree, node[, opts][, isSelected])`](#ivhtreeviewmgrselecttree-node-opts-isselected)
24 | - [`ivhTreeviewMgr.selectAll(tree[, opts][, isSelected])`](#ivhtreeviewmgrselectalltree-opts-isselected)
25 | - [`ivhTreeviewMgr.selectEach(tree, nodes[, opts][, isSelected])`](#ivhtreeviewmgrselecteachtree-nodes-opts-isselected)
26 | - [`ivhTreeviewMgr.deselect(tree, node[, opts])`](#ivhtreeviewmgrdeselecttree-node-opts)
27 | - [`ivhTreeviewMgr.deselectAll(tree[, opts])`](#ivhtreeviewmgrdeselectalltree-opts)
28 | - [`ivhTreeviewMgr.deselectEach(tree, nodes[, opts])`](#ivhtreeviewmgrdeselecteachtree-nodes-opts)
29 | - [`ivhTreeviewMgr.expand(tree, node[, opts][, isExpanded])`](#ivhtreeviewmgrexpandtree-node-opts-isexpanded)
30 | - [`ivhTreeviewMgr.expandRecursive(tree[, node[, opts][,isExpanded]])`](#ivhtreeviewmgrexpandrecursivetree-node-opts-isexpanded)
31 | - [`ivhTreeviewMgr.expandTo(tree, node[, opts][, isExpanded])`](#ivhtreeviewmgrexpandtotree-node-opts-isexpanded)
32 | - [`ivhTreeviewMgr.collapse(tree, node[, opts])`](#ivhtreeviewmgrcollapsetree-node-opts)
33 | - [`ivhTreeviewMgr.collapseRecursive(tree[, node[, opts]])`](#ivhtreeviewmgrcollapserecursivetree-node-opts)
34 | - [`ivhTreeviewMgr.collapseParents(tree, node[, opts])`](#ivhtreeviewmgrcollapseparentstree-node-opts)
35 | - [`ivhTreeviewMgr.validate(tree[, opts][, bias])`](#ivhtreeviewmgrvalidatetree-opts-bias)
36 | - [Dynamic Changes](#dynamic-changes)
37 | - [Tree Traversal](#tree-traversal)
38 | - [`ivhTreeviewBfs(tree[, opts][, cb])`](#ivhtreeviewbfstree-opts-cb)
39 | - [Optimizations and Known Limitations](#optimizations-and-known-limitations)
40 | - [Reporting Issues](#reporting-issues-and-getting-help)
41 | - [Contributing](#contributing)
42 | - [Release History](#release-history)
43 | - [License](#license)
44 |
45 |
46 | ## Getting Started
47 |
48 | IVH Treeview can be installed with bower and npm:
49 |
50 | ```
51 | bower install angular-ivh-treeview
52 | # or
53 | npm install angular-ivh-treeview
54 | ```
55 |
56 | Once installed, include the following files in your app:
57 |
58 | - `dist/ivh-treeview.js`
59 | - `dist/ivh-treeview.css`
60 | - `dist/ivh-treeview-theme-basic.css` (optional minimalist theme)
61 |
62 | And add the `ivh.treeview` module to your main Angular module:
63 |
64 | ```javascript
65 | angular.module('myApp', [
66 | 'ivh.treeview'
67 | // other module dependencies...
68 | ]);
69 | ```
70 |
71 | You're now ready to use the `ivh-treeview` directive, `ivhTreeviewMgr` service,
72 | and `ivhTreeviewBfs` service.
73 |
74 | ## Example Usage
75 |
76 | In your controller...
77 |
78 | ```javascript
79 | app.controller('MyCtrl', function() {
80 | this.bag = [{
81 | label: 'Glasses',
82 | value: 'glasses',
83 | children: [{
84 | label: 'Top Hat',
85 | value: 'top_hat'
86 | },{
87 | label: 'Curly Mustache',
88 | value: 'mustachio'
89 | }]
90 | }];
91 |
92 | this.awesomeCallback = function(node, tree) {
93 | // Do something with node or tree
94 | };
95 |
96 | this.otherAwesomeCallback = function(node, isSelected, tree) {
97 | // Do soemthing with node or tree based on isSelected
98 | }
99 | });
100 | ```
101 |
102 | In your view...
103 |
104 | ```html
105 |
106 |
107 |
108 |
111 |
112 |
113 | ```
114 |
115 | ## Options
116 |
117 | IVH Treeview is pretty configurable. By default it expects your elements to have
118 | `label` and `children` properties for node display text and child nodes
119 | respectively. It'll also make use of a `selected` attribute to manage selected
120 | states. If you would like to pick out nodes by ID rather than reference it'll
121 | also use an `id` attribute. Those attributes can all be changed, for example:
122 |
123 | ```html
124 |
125 |
130 |
131 | ```
132 |
133 | IVH Treeview attaches checkboxes to each item in your tree for a hierarchical
134 | selection model. If you'd rather not have these checkboxes use
135 | `ivh-treeview-use-checkboxes="false"`:
136 |
137 | ```html
138 |
139 |
141 |
142 | ```
143 |
144 | There's also a provider if you'd like to change the global defaults:
145 |
146 | ```javascript
147 | app.config(function(ivhTreeviewOptionsProvider) {
148 | ivhTreeviewOptionsProvider.set({
149 | idAttribute: 'id',
150 | labelAttribute: 'label',
151 | childrenAttribute: 'children',
152 | selectedAttribute: 'selected',
153 | useCheckboxes: true,
154 | disableCheckboxSelectionPropagation: false,
155 | expandToDepth: 0,
156 | indeterminateAttribute: '__ivhTreeviewIndeterminate',
157 | expandedAttribute: '__ivhTreeviewExpanded',
158 | defaultSelectedState: true,
159 | validate: true,
160 | twistieExpandedTpl: '(-)',
161 | twistieCollapsedTpl: '(+)',
162 | twistieLeafTpl: 'o',
163 | nodeTpl: '...'
164 | });
165 | });
166 | ```
167 |
168 | Note that you can also use the `ivhTreeviewOptions` service to inspect global
169 | options at runtime. For an explanation of each option see the comments in the
170 | [source for ivhTreeviewOptions][trvw-opts].
171 |
172 | ```javascript
173 | app.controller('MyCtrl', function(ivhTreeviewOptions) {
174 | var opts = ivhTreeviewOptions();
175 |
176 | // opts.idAttribute === 'id'
177 | // opts.labelAttribute === 'label'
178 | // opts.childrenAttribute === 'children'
179 | // opts.selectedAttribute === 'selected'
180 | // opts.useCheckboxes === true
181 | // opts.disableCheckboxSelectionPropagation === false
182 | // opts.expandToDepth === 0
183 | // opts.indeterminateAttribute === '__ivhTreeviewIndeterminate'
184 | // opts.expandedAttribute === '__ivhTreeviewExpanded'
185 | // opts.defaultSelectedState === true
186 | // opts.validate === true
187 | // opts.twistieExpandedTpl === '(-)'
188 | // opts.twistieCollapsedTpl === '(+)'
189 | // opts.twistieLeafTpl === 'o'
190 | // opts.nodeTpl =(eh)= '...'
191 | });
192 |
193 | ```
194 |
195 |
196 | ### Filtering
197 |
198 | We support filtering through the `ivh-treeview-filter` attribute, this value is
199 | supplied to Angular's `filterFilter` and applied to each node individually.
200 |
201 | IVH Treeview uses `ngHide` to hide filtered out nodes. If you would like to
202 | customize the hide/show behavior of nodes as they are filtered in and out of
203 | view (e.g. with `ngAnimate`) you can target elements with elements with the
204 | `.ivh-treeview-node` class:
205 |
206 | ```css
207 | /* with e.g. keyframe animations */
208 | .ivh-treeview-node.ng-enter {
209 | animation: my-enter-animation 0.5s linear;
210 | }
211 |
212 | .ivh-treeview-node.ng-leave {
213 | animation: my-leave-animation 0.5s linear;
214 | }
215 |
216 | /* or class based animations */
217 | .ivh-treeview-node.ng-hide {
218 | transition: 0.5s linear all;
219 | opacity: 0;
220 | }
221 |
222 | /* alternatively, just strike-through filtered out nodes */
223 | .ivh-treeview-node.ng-hide {
224 | display: block !important;
225 | }
226 |
227 | .ivh-treeview-node.ng-hide .ivh-treeview-node-label {
228 | color: red;
229 | text-decoration: line-through;
230 | }
231 | ```
232 |
233 | ***Demo***: [Filtering](http://jsbin.com/zitiri/edit?html,output)
234 |
235 | ### Expanded by Default
236 |
237 | If you want the tree to start out expanded to a certain depth use the
238 | `ivh-treeview-expand-to-depth` attribute:
239 |
240 | ```html
241 |
242 |
246 |
247 | ```
248 |
249 | You can also use the `ivhTreeviewOptionsProvider` to set a global default.
250 |
251 | If you want the tree *entirely* expanded use a depth of `-1`. Providing a depth
252 | greater than your tree's maximum depth will cause the entire tree to be
253 | initially expanded.
254 |
255 | ***Demo***: [Expand to depth on
256 | load](http://jsbin.com/ruxedo/edit?html,js,output)
257 |
258 | ### Default Selected State
259 |
260 | When using checkboxes you can have a default selected state of `true` or
261 | `false`. The default selected state is used when validating your tree data with
262 | `ivhTreeviewMgr.validate` which will assume this state if none is specified,
263 | i.e. any node without a selected state will assume the default state.
264 | Futhermore, when `ivhTreeviewMgr.validate` finds a node whose selected state
265 | differs from the default it will assign the same state to each of that node's
266 | childred, parent nodes are updated accordingly.
267 |
268 | Use `ivh-treeview-default-selected-state` attribute or `defaultSelectedState`
269 | option to set this property.
270 |
271 | ***Demo***: [Default selected state and validate on
272 | startup](http://jsbin.com/pajeze/2/edit)
273 |
274 | ### Validate on Startup
275 |
276 | `ivh.treeview` will not assume control of your model on startup if you do not
277 | want it to. You can opt out of validation on startup by setting
278 | `ivh-treeview-validate="false"` at the attribute level or by globally setting
279 | the `validate` property in `ivhTreeviewOptionsProvider`.
280 |
281 | ***Demo***: [Default selected state and validate on
282 | startup](http://jsbin.com/pajeze/2/edit)
283 |
284 | ### Twisties
285 |
286 | The basic twisties that ship with this `ivh.treeview` are little more than ASCII
287 | art. You're encouraged to use your own twistie templates. For example, if you've
288 | got bootstrap on your page you might do something like this:
289 |
290 | ```javascript
291 | ivhTreeviewOptionsProvider.set({
292 | twistieCollapsedTpl: '',
293 | twistieExpandedTpl: '',
294 | twistieLeafTpl: '●'
295 | });
296 | ```
297 |
298 | If you need different twistie templates for different treeview elements you can
299 | assign these templates at the attribute level:
300 |
301 | ```html
302 |
305 |
306 | ```
307 |
308 | Alternatively, you can pass them as part of a [full configuration
309 | object](https://github.com/iVantage/angular-ivh-treeview#all-the-options).
310 |
311 |
312 | ***Demo***: [Custom twisties](http://jsbin.com/gizofu/edit?html,js,output)
313 |
314 | ### Templates and Skins
315 |
316 | IVH Treeview allows you to fully customize your tree nodes. See
317 | [docs/templates-and-skins.md](docs/templates-and-skins.md) for demos and
318 | details.
319 |
320 | ### Toggle Handlers
321 |
322 | Want to register a callback for whenever a user expands or collapses a node? Use
323 | the `ivh-treeview-on-toggle` attribute. Your expression will be evaluated with
324 | the following local variables: `ivhNode`, the node that was toggled; `ivhTree`,
325 | the tree it belongs to; `ivhIsExpanded`, whether or not the node is now
326 | expanded.
327 |
328 | ```html
329 |
330 |
333 |
334 | ```
335 |
336 | You may also supply a toggle handler as a function (rather than an angular
337 | expression) using `ivh-treeview-options` or by setting a global `onToggle`
338 | option. In this case the function will be passed a single object with `ivhNode`
339 | and `ivhTree` properties.
340 |
341 | ***Demo***: [Toggle Handler](http://jsbin.com/xegari/edit)
342 |
343 | ### Select/Deselect Handlers
344 |
345 | Want to be notified any time a checkbox changes state as the result of a click?
346 | Use the `ivh-treeview-on-cb-change` attribute. Your expression will be evaluated
347 | whenever a node checkbox changes state with the following local variables:
348 | `ivhNode`, the node whose selected state changed; `ivhIsSelected`, the new
349 | selected state of the node; and `ivhTree`, the tree `ivhNode` belongs to.
350 |
351 | You may also supply a selected handler as a function (rather than an angular
352 | expression) using `ivh-treeview-options` or by setting a global `onCbChange`
353 | option. In this case the function will be passed a single object with `ivhNode`,
354 | `ivhIsSelected`, and `ivhTree` properties.
355 |
356 | Note that programmatic changes to a node's selected state (including selection
357 | change propagation) will not trigger this callback. It is only run for the
358 | actual node clicked on by a user.
359 |
360 | ```html
361 |
362 |
365 |
366 | ```
367 |
368 | ***Demo***: [Select/Deselect Handler](http://jsbin.com/febexe/edit)
369 |
370 |
371 | ## All the Options
372 |
373 | If passing a configuration object is more your style than inlining everything in
374 | the view, that's OK too.
375 |
376 | In your fancy controller...
377 |
378 | ```javascript
379 | this.customOpts = {
380 | useCheckboxes: false,
381 | onToggle: this.awesomeCallback
382 | };
383 | ```
384 |
385 | In your view...
386 |
387 | ```html
388 |
391 |
392 | ```
393 |
394 | Any option that can be set with `ivhTreeviewOptionsProvider` can be overriden
395 | here.
396 |
397 |
398 | ## Treeview Manager Service
399 |
400 | `ivh.treeview` supplies a service, `ivhTreeviewMgr`, for interacting with your
401 | tree data directly.
402 |
403 | #### `ivhTreeviewMgr.select(tree, node[, opts][, isSelected])`
404 |
405 | Select (or deselect) an item in `tree`, `node` can be either a reference to the
406 | actual tree node or its ID.
407 |
408 | We'll use settings registered with `ivhTreeviewOptions` by default, but you can
409 | override any of them with the optional `opts` parameter.
410 |
411 | `isSelected` is also optional and defaults to `true` (i.e. the node will be
412 | selected).
413 |
414 | When an item is selected each of its children are also selected and the
415 | indeterminate state of each of the node's parents is validated.
416 |
417 | ***Demo***: [Programmatic select/deselect](http://jsbin.com/kotohu/edit)
418 |
419 | #### `ivhTreeviewMgr.selectAll(tree[, opts][, isSelected])`
420 |
421 | Like `ivhTreeviewMgr.select` except every node in `tree` is either selected or
422 | deselected.
423 |
424 | ***Demo***: [Programmatic selectAll/deselectAll](http://jsbin.com/buhife/edit)
425 |
426 | #### `ivhTreeviewMgr.selectEach(tree, nodes[, opts][, isSelected])`
427 |
428 | Like `ivhTreeviewMgr.select` except an array of nodes (or node IDs) is used.
429 | Each node in `tree` corresponding to one of the passed `nodes` will be selected
430 | or deselected.
431 |
432 | ***Demo***: [Programmatic selectEach/deselectEach](http://jsbin.com/burigo/edit)
433 |
434 | #### `ivhTreeviewMgr.deselect(tree, node[, opts])`
435 |
436 | A convenience method, delegates to `ivhTreeviewMgr.select` with `isSelected` set
437 | to `false`.
438 |
439 | ***Demo***: [Programmatic select/deselect](http://jsbin.com/kotohu/edit)
440 |
441 | #### `ivhTreeviewMgr.deselectAll(tree[, opts])`
442 |
443 | A convenience method, delegates to `ivhTreeviewMgr.selectAll` with `isSelected`
444 | set to `false`.
445 |
446 | ***Demo***: [Programmatic selectAll/deselectAll](http://jsbin.com/buhife/edit)
447 |
448 | #### `ivhTreeviewMgr.deselectEach(tree, nodes[, opts])`
449 |
450 | A convenience method, delegates to `ivhTreeviewMgr.selectEach` with `isSelected`
451 | set to `false`.
452 |
453 | ***Demo***: [Programmatic selectEach/deselectEach](http://jsbin.com/burigo/edit)
454 |
455 | #### `ivhTreeviewMgr.expand(tree, node[, opts][, isExpanded])`
456 |
457 | Expand (or collapse) a given `node` in `tree`, again `node` may be an actual
458 | object reference or an ID.
459 |
460 | We'll use settings registered with `ivhTreeviewOptions` by default, but you can
461 | override any of them with the optional `opts` parameter.
462 |
463 | By default this method will expand the node in question, you may pass `false` as
464 | the last parameter though to collapse the node. Or, just use
465 | `ivhTreeviewMgr.collapse`.
466 |
467 | ***Demo***: [Programmatic expand/collapse](http://jsbin.com/degofo/edit?html,js,output)
468 |
469 | #### `ivhTreeviewMgr.expandRecursive(tree[, node[, opts][, isExpanded]])`
470 |
471 | Expand (or collapse) `node` and all its child nodes. Note that you may omit the
472 | `node` parameter (i.e. expand/collapse the entire tree) but only when all other
473 | option parameters are also omitted.
474 |
475 | ***Demo***: [Programmatic recursive expand/collapse](http://jsbin.com/wugege/edit)
476 |
477 | #### `ivhTreeviewMgr.expandTo(tree, node[, opts][, isExpanded])`
478 |
479 | Expand (or collapse) all parents of `node`. This may be used to "reveal" a
480 | nested node or to recursively collapse all parents of a node.
481 |
482 | ***Demo***: [Programmatic reveal/hide](http://jsbin.com/musodi/edit)
483 |
484 | #### `ivhTreeviewMgr.collapse(tree, node[, opts])`
485 |
486 | A convenience method, delegates to `ivhTreeviewMgr.expand` with `isExpanded`
487 | set to `false`.
488 |
489 | #### `ivhTreeviewMgr.collapseRecursive(tree[, node[, opts]])`
490 |
491 | A convenience method, delegates to `ivhTreeviewMgr.expandRecursive` with
492 | `isExpanded` set to `false`,
493 |
494 | ***Demo***: [Programmatic recursive expand/collapse](http://jsbin.com/wugege/edit)
495 |
496 | #### `ivhTreeviewMgr.collapseParents(tree, node[, opts])`
497 |
498 | A convenience method, delegates to `ivhTreeviewMgr.expandTo` with `isExpanded`
499 | set to `false`.
500 |
501 | ***Demo***: [Programmatic reveal/hide](http://jsbin.com/musodi/edit)
502 |
503 | #### `ivhTreeviewMgr.validate(tree[, opts][, bias])`
504 |
505 | Validate a `tree` data store, `bias` is a convenient redundancy for
506 | `opts.defaultSelectedState`.
507 |
508 | When validating tree data we look for the first node in each branch which has a
509 | selected state defined that differs from `opts.defaultSelectedState` (or
510 | `bias`). Each of that node's children are updated to match the differing node
511 | and parent indeterminate states are updated.
512 |
513 | ***Demo***: [Programmatic select/deselect](http://jsbin.com/bexedi/edit)
514 |
515 | ## Dynamic Changes
516 |
517 | Adding and removing tree nodes on the fly is supported. Just keep in mind that
518 | added nodes do not automatically inherit selected states (i.e. checkbox states)
519 | from their parent nodes. Similarly, adding new child nodes does not cause parent
520 | nodes to automatically validate their own selected states. You will typically
521 | want to use `ivhTreeviewMgr.validate` or `ivhTreeviewMgr.select` after adding
522 | new nodes to your tree:
523 |
524 | ```javascript
525 | // References to the tree, parent node, and children...
526 | var tree = getTree()
527 | , parent = getParent()
528 | , newNodes = [{label: 'Hello'},{label: 'World'}];
529 |
530 | // Attach new children to parent node
531 | parent.children = newNodes;
532 |
533 | // Force revalidate on tree given parent node's selected status
534 | ivhTreeviewMgr.select(myTree, parent, parent.selected);
535 | ```
536 |
537 | ## Tree Traversal
538 |
539 | The internal tree traversal service is exposed as `ivhTreeviewBfs` (bfs -->
540 | breadth first search).
541 |
542 | #### `ivhTreeviewBfs(tree[, opts][, cb])`
543 |
544 | We perform a breadth first traversal of `tree` applying the function `cb` to
545 | each node as it is reached. `cb` is passed two parameters, the node itself and
546 | an array of parents nodes ordered nearest to farthest. If the `cb` returns
547 | `false` traversal of that branch is stopped.
548 |
549 | Note that even if `false` is returned each of `nodes` siblings will still be
550 | traversed. Essentially none of `nodes` children will be added to traversal
551 | queue. All other branches in `tree` will be traversed as normal.
552 |
553 | In other words returning `false` tells `ivhTreeviewBfs` to go no deeper in the
554 | current branch only.
555 |
556 | ***Demo***: [`ivhTreeviewBfs` in
557 | action](http://jsbin.com/wofunu/1/edit?html,js,output)
558 |
559 |
560 | ## Optimizations and Known Limitations
561 |
562 | ### Performance at Scale
563 |
564 | The default node template assumes a reasonable number of tree nodes. As your
565 | tree grows (3k-10k+ nodes) you will likely notice a significant dip in
566 | performance. This can be mitigated by using a custom template with a few easy
567 | tweaks.
568 |
569 | **Only process visible nodes** by adding an `ng-if` to the
570 | `ivh-treeview-children` element. This small change will result in significant
571 | performance boosts for large trees as now only the visible nodes (i.e. nodes
572 | with all parents expanded) will be processed. This change will likely be added
573 | to the default template in version 1.1.
574 |
575 | **Use Angular's bind-once syntx in a custom template**. The default template
576 | supports angular@1.2.x and so does not leverage the native double-colon syntax
577 | to make one time bindings. By binding once where possible you can trim a large
578 | number of watches from your trees.
579 |
580 | ### Known Issues
581 |
582 | - Creating multiple treeviews within an ngRepeat loops creates an issue where
583 | each treeview accesses the same controller instance after initial load. See
584 | issue #113.
585 | - We use Angular's `filterFilter` for filtering, by default this compares your
586 | filter string with at all object attributes. This directive attaches an
587 | attribute to your tree nodes to track its selected state (e.g. `selected:
588 | false`). If you want your filter to ignore the selection tracking attribute
589 | use an object or function filter. See issue #151.
590 |
591 | ## Reporting Issues and Getting Help
592 |
593 | When reporting an issue please take a moment to reproduce your setup by
594 | modifying our [starter template](http://jsbin.com/wecafa/2/edit). Only make as
595 | many changes as necessary to demonstrate your issue but do comment your added
596 | code.
597 |
598 | Please use Stack Overflow for general questions and help with implementation.
599 |
600 |
601 | ## Contributing
602 |
603 | Please see our consolidated [contribution
604 | guidelines](https://github.com/iVantage/Contribution-Guidelines).
605 |
606 |
607 | ## Release History
608 |
609 | - 2015-11-29 v1.0.2 Allow numeric ids as well as string ids
610 | - 2015-09-23 v1.0.0 Use expressions rather than callbacks for change/toggle
611 | handlers, update default template. See MIGRATING doc for breaking changes.
612 | - 2015-05-06 v0.10.0 Make node templates customizable
613 | - 2015-02-10 v0.9.0 All options are set-able via attributes or config object
614 | - 2015-01-02 v0.8.0 Add ability to expand/collapse nodes programmatically
615 | - 2014-09-21 v0.6.0 Tree accepts nodes added on the fly
616 | - 2014-09-09 v0.3.0 Complete refactor. Directive no longer propagates changes
617 | automatically on programmatic changes, use ivhTreeviewMgr.
618 | - 2014-08-25 v0.2.0 Allow for initial expansion
619 | - 2014-06-20 v0.1.0 Initial release
620 |
621 |
622 | ## License
623 |
624 | [MIT license][license], copyright iVantage Health Analytics, Inc.
625 |
626 | [license]: https://raw.github.com/iVantage/angular-ivh-treeview/master/LICENSE-MIT
627 | [bootstrap]: http://getbootstrap.com/
628 | [travis-img]: https://travis-ci.org/iVantage/angular-ivh-treeview.svg?branch=master
629 | [travis-link]: https://travis-ci.org/iVantage/angular-ivh-treeview
630 | [trvw-opts]: https://github.com/iVantage/angular-ivh-treeview/blob/master/src/scripts/services/ivh-treeview-options.js#L13-L103
631 |
--------------------------------------------------------------------------------
/dist/ivh-treeview.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * The iVantage Treeview module
4 | *
5 | * @package ivh.treeview
6 | */
7 |
8 | angular.module('ivh.treeview', []);
9 |
10 |
11 | /**
12 | * Supports non-default interpolation symbols
13 | *
14 | * @package ivh.treeview
15 | * @copyright 2016 iVantage Health Analytics, Inc.
16 | */
17 |
18 | angular.module('ivh.treeview').constant('ivhTreeviewInterpolateEndSymbol', '}}');
19 |
20 |
21 |
22 | /**
23 | * Supports non-default interpolation symbols
24 | *
25 | * @package ivh.treeview
26 | * @copyright 2016 iVantage Health Analytics, Inc.
27 | */
28 |
29 | angular.module('ivh.treeview').constant('ivhTreeviewInterpolateStartSymbol', '{{');
30 |
31 |
32 |
33 | /**
34 | * Selection management logic for treeviews with checkboxes
35 | *
36 | * @private
37 | * @package ivh.treeview
38 | * @copyright 2014 iVantage Health Analytics, Inc.
39 | */
40 |
41 | angular.module('ivh.treeview').directive('ivhTreeviewCheckboxHelper', [function() {
42 | 'use strict';
43 | return {
44 | restrict: 'A',
45 | scope: {
46 | node: '=ivhTreeviewCheckboxHelper'
47 | },
48 | require: '^ivhTreeview',
49 | link: function(scope, element, attrs, trvw) {
50 | var node = scope.node
51 | , opts = trvw.opts()
52 | , indeterminateAttr = opts.indeterminateAttribute
53 | , selectedAttr = opts.selectedAttribute;
54 |
55 | // Set initial selected state of this checkbox
56 | scope.isSelected = node[selectedAttr];
57 |
58 | // Local access to the parent controller
59 | scope.trvw = trvw;
60 |
61 | // Enforce consistent behavior across browsers by making indeterminate
62 | // checkboxes become checked when clicked/selected using spacebar
63 | scope.resolveIndeterminateClick = function() {
64 |
65 | //intermediate state is not handled when CheckBoxes state propagation is disabled
66 | if (opts.disableCheckboxSelectionPropagation) {
67 | return;
68 | }
69 |
70 | if(node[indeterminateAttr]) {
71 | trvw.select(node, true);
72 | }
73 | };
74 |
75 | // Update the checkbox when the node's selected status changes
76 | scope.$watch('node.' + selectedAttr, function(newVal, oldVal) {
77 | scope.isSelected = newVal;
78 | });
79 |
80 | if (!opts.disableCheckboxSelectionPropagation) {
81 | // Update the checkbox when the node's indeterminate status changes
82 | scope.$watch('node.' + indeterminateAttr, function(newVal, oldVal) {
83 | element.find('input').prop('indeterminate', newVal);
84 | });
85 | }
86 | },
87 | template: [
88 | ''
93 | ].join('\n')
94 | };
95 | }]);
96 |
97 |
98 |
99 | /**
100 | * Wrapper for a checkbox directive
101 | *
102 | * Basically exists so folks creeting custom node templates don't need to attach
103 | * their node to this directive explicitly - i.e. keeps consistent interface
104 | * with the twistie and toggle directives.
105 | *
106 | * @package ivh.treeview
107 | * @copyright 2014 iVantage Health Analytics, Inc.
108 | */
109 |
110 | angular.module('ivh.treeview').directive('ivhTreeviewCheckbox', [function() {
111 | 'use strict';
112 | return {
113 | restrict: 'AE',
114 | require: '^ivhTreeview',
115 | template: ''
116 | };
117 | }]);
118 |
119 |
120 | /**
121 | * The recursive step, output child nodes for the scope node
122 | *
123 | * @package ivh.treeview
124 | * @copyright 2014 iVantage Health Analytics, Inc.
125 | */
126 |
127 | angular.module('ivh.treeview').directive('ivhTreeviewChildren', function() {
128 | 'use strict';
129 | return {
130 | restrict: 'AE',
131 | require: '^ivhTreeviewNode',
132 | template: [
133 | '
',
134 | '
',
140 | '
',
141 | '
'
142 | ].join('\n')
143 | };
144 | });
145 |
146 |
147 | /**
148 | * Treeview tree node directive
149 | *
150 | * @private
151 | * @package ivh.treeview
152 | * @copyright 2014 iVantage Health Analytics, Inc.
153 | */
154 |
155 | angular.module('ivh.treeview').directive('ivhTreeviewNode', ['ivhTreeviewCompiler', function(ivhTreeviewCompiler) {
156 | 'use strict';
157 | return {
158 | restrict: 'A',
159 | scope: {
160 | node: '=ivhTreeviewNode',
161 | depth: '=ivhTreeviewDepth'
162 | },
163 | require: '^ivhTreeview',
164 | compile: function(tElement) {
165 | return ivhTreeviewCompiler
166 | .compile(tElement, function(scope, element, attrs, trvw) {
167 | var node = scope.node;
168 |
169 | var getChildren = scope.getChildren = function() {
170 | return trvw.children(node);
171 | };
172 |
173 | scope.trvw = trvw;
174 | scope.childDepth = scope.depth + 1;
175 |
176 | // Expand/collapse the node as dictated by the expandToDepth property.
177 | // Note that we will respect the expanded state of this node if it has
178 | // been expanded by e.g. `ivhTreeviewMgr.expandTo` but not yet
179 | // rendered.
180 | if(!trvw.isExpanded(node)) {
181 | trvw.expand(node, trvw.isInitiallyExpanded(scope.depth));
182 | }
183 |
184 | /**
185 | * @todo Provide a way to opt out of this
186 | */
187 | scope.$watch(function() {
188 | return getChildren().length > 0;
189 | }, function(newVal) {
190 | if(newVal) {
191 | element.removeClass('ivh-treeview-node-leaf');
192 | } else {
193 | element.addClass('ivh-treeview-node-leaf');
194 | }
195 | });
196 | });
197 | }
198 | };
199 | }]);
200 |
201 |
202 |
203 | /**
204 | * Toggle logic for treeview nodes
205 | *
206 | * Handles expand/collapse on click. Does nothing for leaf nodes.
207 | *
208 | * @private
209 | * @package ivh.treeview
210 | * @copyright 2014 iVantage Health Analytics, Inc.
211 | */
212 |
213 | angular.module('ivh.treeview').directive('ivhTreeviewToggle', [function() {
214 | 'use strict';
215 | return {
216 | restrict: 'A',
217 | require: '^ivhTreeview',
218 | link: function(scope, element, attrs, trvw) {
219 | var node = scope.node;
220 |
221 | element.addClass('ivh-treeview-toggle');
222 |
223 | element.bind('click', function() {
224 | if(!trvw.isLeaf(node)) {
225 | scope.$apply(function() {
226 | trvw.toggleExpanded(node);
227 | trvw.onToggle(node);
228 | });
229 | }
230 | });
231 | }
232 | };
233 | }]);
234 |
235 |
236 | /**
237 | * Treeview twistie directive
238 | *
239 | * @private
240 | * @package ivh.treeview
241 | * @copyright 2014 iVantage Health Analytics, Inc.
242 | */
243 |
244 | angular.module('ivh.treeview').directive('ivhTreeviewTwistie', ['$compile', 'ivhTreeviewOptions', function($compile, ivhTreeviewOptions) {
245 | 'use strict';
246 |
247 | var globalOpts = ivhTreeviewOptions();
248 |
249 | return {
250 | restrict: 'A',
251 | require: '^ivhTreeview',
252 | template: [
253 | '',
254 | '',
255 | globalOpts.twistieCollapsedTpl,
256 | '',
257 | '',
258 | globalOpts.twistieExpandedTpl,
259 | '',
260 | '',
261 | globalOpts.twistieLeafTpl,
262 | '',
263 | ''
264 | ].join('\n'),
265 | link: function(scope, element, attrs, trvw) {
266 |
267 | if(!trvw.hasLocalTwistieTpls) {
268 | return;
269 | }
270 |
271 | var opts = trvw.opts()
272 | , $twistieContainers = element
273 | .children().eq(0) // Template root
274 | .children(); // The twistie spans
275 |
276 | angular.forEach([
277 | // Should be in the same order as elements in template
278 | 'twistieCollapsedTpl',
279 | 'twistieExpandedTpl',
280 | 'twistieLeafTpl'
281 | ], function(tplKey, ix) {
282 | var tpl = opts[tplKey]
283 | , tplGlobal = globalOpts[tplKey];
284 |
285 | // Do nothing if we don't have a new template
286 | if(!tpl || tpl === tplGlobal) {
287 | return;
288 | }
289 |
290 | // Super gross, the template must actually be an html string, we won't
291 | // try too hard to enforce this, just don't shoot yourself in the foot
292 | // too badly and everything will be alright.
293 | if(tpl.substr(0, 1) !== '<' || tpl.substr(-1, 1) !== '>') {
294 | tpl = '' + tpl + '';
295 | }
296 |
297 | var $el = $compile(tpl)(scope)
298 | , $container = $twistieContainers.eq(ix);
299 |
300 | // Clean out global template and append the new one
301 | $container.html('').append($el);
302 | });
303 |
304 | }
305 | };
306 | }]);
307 |
308 |
309 | /**
310 | * The `ivh-treeview` directive
311 | *
312 | * A filterable tree view with checkbox support.
313 | *
314 | * Example:
315 | *
316 | * ```
317 | *