//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"); define(["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 } }); });