summaryrefslogtreecommitdiff
path: root/lib/CheckBoxTree.js
blob: 41c74ab3bbc7f958672c02e1c5c41d9b64fe66b4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
//dojo.provide("lib.CheckBoxTree");
//dojo.provide("lib.CheckBoxStoreModel");

// THIS WIDGET IS BASED ON DOJO/DIJIT 1.4.0 AND WILL NOT WORK WITH PREVIOUS VERSIONS
//
//	Release date: 02/05/2010
//

//dojo.require("dijit.Tree");
//dojo.require("dijit.form.CheckBox");

require(["dojo/_base/declare", "dijit/tree/TreeStoreModel"], function (declare) {

	return declare( "lib.CheckBoxStoreModel", dijit.tree.TreeStoreModel,
		{
			// checkboxAll: Boolean
			//		If true, every node in the tree will receive a checkbox regardless if the 'checkbox' attribute
			//		is specified in the dojo.data.
			checkboxAll: true,

			// checkboxState: Boolean
			// 		The default state applied to every checkbox unless otherwise specified in the dojo.data.
			//		(see also: checkboxIdent)
			checkboxState: false,

			// checkboxRoot: Boolean
			//		If true, the root node will receive a checkbox eventhough it's not a true entry in the store.
			//		This attribute is independent of the showRoot attribute of the tree itself. If the tree
			//		attribute 'showRoot' is set to false to checkbox for the root will not show either.
			checkboxRoot: false,

			// checkboxStrict: Boolean
			//		If true, a strict parent-child checkbox relation is maintained. For example, if all children
			//		are checked the parent will automatically be checked or if any of the children are unchecked
			//		the parent will be unchecked.
			checkboxStrict: true,

			// checkboxIdent: String
			//		The attribute name (attribute of the dojo.data.item) that specifies that items checkbox initial
			//		state. Example:	{ name:'Egypt', type:'country', checkbox: true }
			//		If a dojo.data.item has no 'checkbox' attribute specified it will depend on the attribute
			//		'checkboxAll' if one will be created automatically and if so what the initial state will be as
			//		specified by 'checkboxState'.
			checkboxIdent: "checkbox",

			updateCheckbox: function(/*dojo.data.Item*/ storeItem, /*Boolean*/ newState ) {
				// summary:
				//		Update the checkbox state (true/false) for the item and the associated parent and
				//		child checkboxes if any.
				// description:
				//		Update a single checkbox state (true/false) for the item and the associated parent
				//		and child checkboxes if any. This function is called from the tree if a user checked
				//		or unchecked a checkbox on the tree. The parent and child tree nodes are updated to
				//		maintain consistency if 'checkboxStrict' is set to true.
				//	storeItem:
				//		The item in the dojo.data.store whos checkbox state needs updating.
				//	newState:
				//		The new state of the checkbox: true or false
				//	example:
				//	| model.updateCheckboxState(item, true);
				//

				this._setCheckboxState( storeItem, newState );
				//if( this.checkboxStrict ) { I don't need all this 1-1 stuff, only parent -> child (fox)
				this._updateChildCheckbox( storeItem, newState );
				//this._updateParentCheckbox( storeItem, newState );
				//}
			},
			setAllChecked: function(checked) {
				var items = this.store._arrayOfAllItems;
				this.setCheckboxState(items, checked);
			},
			setCheckboxState: function(items, checked) {
				for (var i = 0; i < items.length; i++) {
					this._setCheckboxState(items[i], checked);
				}
			},
			getCheckedItems: function() {
				var items = this.store._arrayOfAllItems;
				var result = [];

				for (var i = 0; i < items.length; i++) {
					if (this.store.getValue(items[i], 'checkbox'))
						result.push(items[i]);
				}

				return result;
			},

			getCheckboxState: function(/*dojo.data.Item*/ storeItem) {
				// summary:
				//		Get the current checkbox state from the dojo.data.store.
				// description:
				//		Get the current checkbox state from the dojo.data store. A checkbox can have three
				//		different states: true, false or undefined. Undefined in this context means no
				//		checkbox identifier (checkboxIdent) was found in the dojo.data store. Depending on
				//		the checkbox attributes as specified above the following will take place:
				//		a) 	If the current checkbox state is undefined and the checkbox attribute 'checkboxAll' or
				//			'checkboxRoot' is true one will be created and the default state 'checkboxState' will
				//			be applied.
				//		b)	If the current state is undefined and 'checkboxAll' is false the state undefined remains
				//			unchanged and is returned. This will prevent any tree node from creating a checkbox.
				//
				//	storeItem:
				//		The item in the dojo.data.store whos checkbox state is returned.
				//	example:
				//	| var currState = model.getCheckboxState(item);
				//
				var currState = undefined;

				// Special handling required for the 'fake' root entry (the root is NOT a dojo.data.item).
				// this stuff is only relevant for Forest store -fox
				/*		if ( storeItem == this.root ) {
				 if( typeof(storeItem.checkbox) == "undefined" ) {
				 this.root.checkbox = undefined;		// create a new checbox reference as undefined.
				 if( this.checkboxRoot ) {
				 currState = this.root.checkbox = this.checkboxState;
				 }
				 } else {
				 currState = this.root.checkbox;
				 }
				 } else {	// a valid dojo.store.item
				 currState = this.store.getValue(storeItem, this.checkboxIdent);
				 if( currState == undefined && this.checkboxAll) {
				 this._setCheckboxState( storeItem, this.checkboxState );
				 currState = this.checkboxState;
				 }
				 } */

				currState = this.store.getValue(storeItem, this.checkboxIdent);
				if( currState == undefined && this.checkboxAll) {
					this._setCheckboxState( storeItem, this.checkboxState );
					currState = this.checkboxState;
				}

				return currState;  // the current state of the checkbox (true/false or undefined)
			},

			_setCheckboxState: function(/*dojo.data.Item*/ storeItem, /*Boolean*/ newState ) {
				// summary:
				//		Set/update the checkbox state on the dojo.data store.
				// description:
				//		Set/update the checkbox state on the dojo.data.store. Retreive the current
				//		state of the checkbox and validate if an update is required, this will keep
				//		update events to a minimum. On completion a 'onCheckboxChange' event is
				//		triggered.
				//		If the current state is undefined (ie: no checkbox attribute specified for
				//		this dojo.data.item) the 'checkboxAll' attribute is checked	to see if one
				//		needs to be created. In case of the root the 'checkboxRoot' attribute is checked.
				//		NOTE: the store.setValue function will create the 'checkbox' attribute for the
				//		item if none exists.
				//	storeItem:
				//		The item in the dojo.data.store whos checkbox state is updated.
				//	newState:
				//		The new state of the checkbox: true or false
				//	example:
				//	| model.setCheckboxState(item, true);
				//
				var stateChanged = true;

				if( storeItem != this.root ) {
					var currState = this.store.getValue(storeItem, this.checkboxIdent);
					if( currState != newState && ( currState !== undefined || this.checkboxAll ) ) {
						this.store.setValue(storeItem, this.checkboxIdent, newState);
					} else {
						stateChanged = false;	// No changes to the checkbox
					}
				} else {  // Tree root instance
					if( this.root.checkbox != newState && ( this.root.checkbox !== undefined || this.checkboxRoot ) ) {
						this.root.checkbox = newState;
					} else {
						stateChanged = false;
					}
				}
				if( stateChanged ) {	// In case of any changes trigger the update event.
					this.onCheckboxChange(storeItem);
				}
				return stateChanged;
			},

			_updateChildCheckbox: function(/*dojo.data.Item*/ parentItem, /*Boolean*/ newState ) {
				//	summary:
				//		Set all child checkboxes to true/false depending on the parent checkbox state.
				//	description:
				//		If a parent checkbox changes state, all child and grandchild checkboxes will be
				//		updated to reflect the change. For example, if the parent state is set to true,
				//		all child and grandchild checkboxes will receive that same 'true' state.
				//		If a child checkbox changes state and has multiple parent, all of its parents
				//		need to be re-evaluated.
				//	parentItem:
				//		The parent dojo.data.item whos child/grandchild checkboxes require updating.
				//	newState:
				//		The new state of the checkbox: true or false
				//

				if( this.mayHaveChildren( parentItem )) {
					this.getChildren( parentItem, dojo.hitch( this,
						function( children ) {
							dojo.forEach( children, function(child) {
								if( this._setCheckboxState(child, newState) ) {
									var parents = this._getParentsItem(child);
									if( parents.length > 1 ) {
										this._updateParentCheckbox( child, newState );
									}
								}
								if( this.mayHaveChildren( child )) {
									this._updateChildCheckbox( child, newState );
								}
							}, this );
						}),
						function(err) {
							console.error(this, ": updating child checkboxes: ", err);
						}
					);
				}
			},

			_updateParentCheckbox: function(/*dojo.data.Item*/ storeItem, /*Boolean*/ newState ) {
				//	summary:
				//		Update the parent checkbox state depending on the state of all child checkboxes.
				//	description:
				//		Update the parent checkbox state depending on the state of all child checkboxes.
				//		The parent checkbox automatically changes state if ALL child checkboxes are true
				//		or false. If, as a result, the parent checkbox changes state, we will check if
				//		its parent needs to be updated as well all the way upto the root.
				//	storeItem:
				//		The dojo.data.item whos parent checkboxes require updating.
				//	newState:
				//		The new state of the checkbox: true or false
				//
				var parents = this._getParentsItem(storeItem);
				dojo.forEach( parents, function( parentItem ) {
					if( newState ) { // new state = true (checked)
						this.getChildren( parentItem, dojo.hitch( this,
							function(siblings) {
								var allChecked  = true;
								dojo.some( siblings, function(sibling) {
									siblState = this.getCheckboxState(sibling);
									if( siblState !== undefined && allChecked )
										allChecked = siblState;
									return !(allChecked);
								}, this );
								if( allChecked ) {
									this._setCheckboxState( parentItem, true );
									this._updateParentCheckbox( parentItem, true );
								}
							}),
							function(err) {
								console.error(this, ": updating parent checkboxes: ", err);
							}
						);
					} else { 	// new state = false (unchecked)
						if( this._setCheckboxState( parentItem, false ) ) {
							this._updateParentCheckbox( parentItem, false );
						}
					}
				}, this );
			},

			_getParentsItem: function(/*dojo.data.Item*/ storeItem ) {
				// summary:
				//		Get the parent(s) of a dojo.data item.
				// description:
				//		Get the parent(s) of a dojo.data item. The '_reverseRefMap' entry of the item is
				//		used to identify the parent(s). A child will have a parent reference if the parent
				//		specified the '_reference' attribute.
				//		For example: children:[{_reference:'Mexico'}, {_reference:'Canada'}, ...
				//	storeItem:
				//		The dojo.data.item whos parent(s) will be returned.
				//
				var parents = [];

				if( storeItem != this.root ) {
					var references = storeItem[this.store._reverseRefMap];
					for(itemId in references ) {
						parents.push(this.store._itemsByIdentity[itemId]);
					}
					if (!parents.length) {
						parents.push(this.root);
					}
				}
				return parents; // parent(s) of a dojo.data.item (Array of dojo.data.items)
			},

			validateData: function(/*dojo.data.Item*/ storeItem, /*thisObject*/ scope ) {
				// summary:
				//		Validate/normalize the parent(s) checkbox data in the dojo.data store.
				// description:
				//		Validate/normalize the parent-child checkbox relationship if the attribute
				//		'checkboxStrict' is set to true. This function is called as part of the post
				//		creation of the Tree instance. All parent checkboxes are set to the appropriate
				//		state according to the actual state(s) of their children.
				//		This will potentionally overwrite whatever was specified for the parent in the
				//		dojo.data store. This will garantee the tree is in a consistent state after startup.
				//	storeItem:
				//		The element to start traversing the dojo.data.store, typically model.root
				//	scope:
				//		The scope to use when this method executes.
				//	example:
				//	| this.model.validateData(this.model.root, this.model);
				//
				if( !scope.checkboxStrict ) {
					return;
				}
				scope.getChildren( storeItem, dojo.hitch( scope,
					function(children) {
						var allChecked  = true;
						var childState;
						dojo.forEach( children, function( child ) {
							if( this.mayHaveChildren( child )) {
								this.validateData( child, this );
							}
							childState = this.getCheckboxState( child );
							if( childState !== undefined && allChecked )
								allChecked = childState;
						}, this);

						if ( this._setCheckboxState( storeItem, allChecked) ) {
							this._updateParentCheckbox( storeItem, allChecked);
						}
					}),
					function(err) {
						console.error(this, ": validating checkbox data: ", err);
					}
				);
			},

			onCheckboxChange: function(/*dojo.data.Item*/ storeItem ) {
				// summary:
				//		Callback whenever a checkbox state has changed state, so that
				//		the Tree can update the checkbox.  This callback is generally
				//		triggered by the '_setCheckboxState' function.
				// tags:
				//		callback
			}

		});

});

require(["dojo/_base/declare", "dojo/dom-construct", "dijit/Tree"], function (declare, domConstruct) {

	return declare("lib._CheckBoxTreeNode", dijit._TreeNode,
		{
			// _checkbox: [protected] dojo.doc.element
			//		Local reference to the dojo.doc.element of type 'checkbox'
			_checkbox: null,

			_createCheckbox: function () {
				// summary:
				//		Create a checkbox on the CheckBoxTreeNode
				// description:
				//		Create a checkbox on the CheckBoxTreeNode. The checkbox is ONLY created if a
				//		valid reference was found in the dojo.data store or the attribute 'checkboxAll'
				//		is set to true. If the current state is 'undefined' no reference was found and
				//		'checkboxAll' is set to false.
				//		Note: the attribute 'checkboxAll' is validated by the getCheckboxState function
				//		therefore no need to do that here. (see getCheckboxState for details).
				//
				var currState = this.tree.model.getCheckboxState(this.item);
				if (currState !== undefined) {
					this._checkbox = new dijit.form.CheckBox();
					//this._checkbox = dojo.doc.createElement('input');
					this._checkbox.type = 'checkbox';
					this._checkbox.attr('checked', currState);
					domConstruct.place(this._checkbox.domNode, this.expandoNode, 'after');
				}
			},

			postCreate: function () {
				// summary:
				//		Handle the creation of the checkbox after the CheckBoxTreeNode has been instanciated.
				// description:
				//		Handle the creation of the checkbox after the CheckBoxTreeNode has been instanciated.
				this._createCheckbox();
				this.inherited(arguments);
			}

		});
});

require(["dojo/_base/declare", "dijit/Tree"], function (declare) {

	return declare( "lib.CheckBoxTree", dijit.Tree,
		{

			onNodeChecked: function(/*dojo.data.Item*/ storeItem, /*treeNode*/ treeNode) {
				// summary:
				//		Callback when a checkbox tree node is checked
				// tags:
				//		callback
			},

			onNodeUnchecked: function(/*dojo.data.Item*/ storeItem, /* treeNode */ treeNode) {
				// summary:
				//		Callback when a checkbox tree node is unchecked
				// tags:
				//		callback
			},

			_onClick: function(/*TreeNode*/ nodeWidget, /*Event*/ e) {
				// summary:
				//		Translates click events into commands for the controller to process
				// description:
				//		the _onClick function is called whenever a 'click' is detected. This
				//		instance of _onClick only handles the click events associated with
				//		the checkbox whos DOM name is INPUT.
				//
				var domElement = e.target;

				// Only handle checkbox clicks here
				if(domElement.type != 'checkbox') {
					return this.inherited( arguments );
				}

				this._publish("execute", { item: nodeWidget.item, node: nodeWidget} );
				// Go tell the model to update the checkbox state

				this.model.updateCheckbox( nodeWidget.item, nodeWidget._checkbox.checked );
				// Generate some additional events
				//this.onClick( nodeWidget.item, nodeWidget, e );
				if(nodeWidget._checkbox.checked) {
					this.onNodeChecked( nodeWidget.item, nodeWidget);
				} else {
					this.onNodeUnchecked( nodeWidget.item, nodeWidget);
				}
				this.focusNode(nodeWidget);
			},

			_onCheckboxChange: function(/*dojo.data.Item*/ storeItem ) {
				// summary:
				//		Processes notification of a change to a checkbox state (triggered by the model).
				// description:
				//		Whenever the model changes the state of a checkbox in the dojo.data.store it will
				//		trigger the 'onCheckboxChange' event allowing the Tree to make the same changes
				//		on the tree Node. There are several conditions why a tree node or checkbox does not
				//		exists:
				//		a) 	The node has not been created yet (the user has not expanded the tree node yet).
				//		b) 	The checkbox may be null if condition (a) exists or no 'checkbox' attribute was
				//			specified for the associated dojo.data.item and the attribute 'checkboxAll' is
				//			set to false.
				// tags:
				//		callback
				var model 	 = this.model,
					identity = model.getIdentity(storeItem),
					nodes 	 = this._itemNodesMap[identity];

				// As of dijit.Tree 1.4 multiple references (parents) are supported, therefore we may have
				// to update multiple nodes which are all associated with the same dojo.data.item.
				if( nodes ) {
					dojo.forEach( nodes, function(node) {
						if( node._checkbox != null ) {
							node._checkbox.attr('checked', this.model.getCheckboxState( storeItem ));
						}
					}, this );
				}
			},

			postCreate: function() {
				// summary:
				//		Handle any specifics related to the tree and model after the instanciation of the Tree.
				// description:
				//		Validate if we have a 'write' store first. Subscribe to the 'onCheckboxChange' event
				//		(triggered by the model) and kickoff the initial checkbox data validation.
				//
				var store = this.model.store;
				if(!store.getFeatures()['dojo.data.api.Write']){
					throw new Error("lib.CheckboxTree: store must support dojo.data.Write");
				}
				this.connect(this.model, "onCheckboxChange", "_onCheckboxChange");
				this.model.validateData( this.model.root, this.model );
				this.inherited(arguments);
			},

			_createTreeNode: function( args ) {
				// summary:
				//		Create a new CheckboxTreeNode instance.
				// description:
				//		Create a new CheckboxTreeNode instance.
				return new lib._CheckBoxTreeNode(args);
			}

		});

});