1 // ========================================================================== 2 // Project: SproutCore - JavaScript Application Framework 3 // Copyright: ©2006-2011 Strobe Inc. and contributors. 4 // Portions ©2008-2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 /** 8 @namespace 9 10 Implements common selection management properties for controllers. 11 12 Selection can be managed by any controller in your applications. This 13 mixin provides some common management features you might want such as 14 disabling selection, or restricting empty or multiple selections. 15 16 To use this mixin, simply add it to any controller you want to manage 17 selection and call updateSelectionAfterContentChange() 18 whenever your source content changes. You can also override the properties 19 defined below to configure how the selection management will treat your 20 content. 21 22 This mixin assumes the arrangedObjects property will return an SC.Array of 23 content you want the selection to reflect. 24 25 Add this mixin to any controller you want to manage selection. It is 26 already applied to SC.ArrayController. 27 28 @since SproutCore 1.0 29 */ 30 SC.SelectionSupport = { 31 32 // .......................................................... 33 // PROPERTIES 34 // 35 /** 36 Walk like a duck. 37 38 @type Boolean 39 */ 40 hasSelectionSupport: YES, 41 42 /** 43 If YES, selection is allowed. Default is YES. 44 45 @type Boolean 46 */ 47 allowsSelection: YES, 48 49 /** 50 If YES, multiple selection is allowed. Default is YES. 51 52 @type Boolean 53 */ 54 allowsMultipleSelection: YES, 55 56 /** 57 If YES, allow empty selection Default is YES. 58 59 @type Boolean 60 */ 61 allowsEmptySelection: YES, 62 63 /** 64 Override to return the first selectable object. For example, if you 65 have groups or want to otherwise limit the kinds of objects that can be 66 selected. 67 68 the default implementation returns firstObject property. 69 70 @returns {Object} first selectable object 71 */ 72 firstSelectableObject: function() { 73 return this.get('firstObject'); 74 }.property(), 75 76 /** 77 This is the current selection. You can make this selection and another 78 controller's selection work in concert by binding them together. You 79 generally have a master selection that relays changes TO all the others. 80 81 @property {SC.SelectionSet} 82 */ 83 selection: function(key, value) { 84 var old = this._scsel_selection, 85 oldlen = old ? old.get('length') : 0, 86 empty, 87 arrangedObjects = this.get('arrangedObjects'), 88 len; 89 90 // whenever we have to recompute selection, reapply all the conditions to 91 // the selection. This ensures that changing the conditions immediately 92 // updates the selection. 93 // 94 // Note also if we don't allowSelection, we don't clear the old selection; 95 // we just don't allow it to be changed. 96 if ((value === undefined) || !this.get('allowsSelection')) { value = old; } 97 98 len = (value && value.isEnumerable) ? value.get('length') : 0; 99 100 // if we don't allow multiple selection 101 if ((len > 1) && !this.get('allowsMultipleSelection')) { 102 103 if (oldlen > 1) { 104 value = SC.SelectionSet.create().addObject(old.get('firstObject')).freeze(); 105 len = 1; 106 } else { 107 value = old; 108 len = oldlen; 109 } 110 } 111 112 // if we don't allow empty selection, block that also, unless we 113 // have nothing to select. select first selectable item if necessary. 114 if ((len === 0) && !this.get('allowsEmptySelection') && arrangedObjects && arrangedObjects.get('length') !== 0) { 115 if (oldlen === 0) { 116 value = this.get('firstSelectableObject'); 117 if (value) { value = SC.SelectionSet.create().addObject(value).freeze(); } 118 else { value = SC.SelectionSet.EMPTY; } 119 len = value.get('length'); 120 121 } else { 122 value = old; 123 len = oldlen; 124 } 125 } 126 127 // if value is empty or is not enumerable, then use empty set 128 if (len === 0) { value = SC.SelectionSet.EMPTY; } 129 130 // always use a frozen copy... 131 if(value !== old) value = value.frozenCopy(); 132 this._scsel_selection = value; 133 134 return value; 135 136 }.property('arrangedObjects', 'allowsEmptySelection', 'allowsMultipleSelection', 'allowsSelection').cacheable(), 137 138 /** 139 YES if the receiver currently has a non-zero selection. 140 141 @type Boolean 142 */ 143 hasSelection: function() { 144 var sel = this.get('selection'); 145 return !! sel && (sel.get('length') > 0); 146 }.property('selection').cacheable(), 147 148 // .......................................................... 149 // METHODS 150 // 151 /** 152 Selects the passed objects in your content. If you set "extend" to YES, 153 then this will attempt to extend your selection as well. 154 155 @param {SC.Enumerable} objects objects to select 156 @param {Boolean} extend optionally set to YES to extend selection 157 @returns {Object} receiver 158 */ 159 selectObjects: function(objects, extend) { 160 161 // handle passing an empty array 162 if (!objects || objects.get('length') === 0) { 163 if (!extend) { this.set('selection', SC.SelectionSet.EMPTY); } 164 return this; 165 } 166 167 var sel = this.get('selection'); 168 if (extend && sel) { sel = sel.copy(); } 169 else { sel = SC.SelectionSet.create(); } 170 171 sel.addObjects(objects).freeze(); 172 this.set('selection', sel); 173 return this; 174 }, 175 176 /** 177 Selects a single passed object in your content. If you set "extend" to 178 YES then this will attempt to extend your selection as well. 179 180 @param {Object} object object to select 181 @param {Boolean} extend optionally set to YES to extend selection 182 @returns {Object} receiver 183 */ 184 selectObject: function(object, extend) { 185 if (object === null) { 186 if (!extend) { this.set('selection', null); } 187 return this; 188 189 } else { return this.selectObjects([object], extend); } 190 }, 191 192 /** 193 Deselects the passed objects in your content. 194 195 @param {SC.Enumerable} objects objects to select 196 @returns {Object} receiver 197 */ 198 deselectObjects: function(objects) { 199 200 if (!objects || objects.get('length') === 0) { return this; } // nothing to do 201 var sel = this.get('selection'); 202 if (!sel || sel.get('length') === 0) { return this; } // nothing to do 203 // find index for each and remove it 204 sel = sel.copy().removeObjects(objects).freeze(); 205 this.set('selection', sel.freeze()); 206 return this; 207 }, 208 209 /** 210 Deselects the passed object in your content. 211 212 @param {SC.Object} object single object to select 213 @returns {Object} receiver 214 */ 215 deselectObject: function(object) { 216 if (!object) { return this; } // nothing to do 217 else { return this.deselectObjects([object]); } 218 }, 219 220 /** 221 Call this method whenever your source content changes to ensure the 222 selection always remains up-to-date and valid. 223 224 @returns {Object} 225 */ 226 updateSelectionAfterContentChange: function() { 227 var arrangedObjects = this.get('arrangedObjects'); 228 var selectionSet = this.get('selection'); 229 var allowsEmptySelection = this.get('allowsEmptySelection'); 230 var indexSet; // Selection index set for arranged objects 231 232 // If we don't have any selection, there's nothing to update 233 if (!selectionSet) { return this; } 234 // Remove any selection set objects that are no longer in the content 235 indexSet = selectionSet.indexSetForSource(arrangedObjects); 236 if ((indexSet && (indexSet.get('length') !== selectionSet.get('length'))) || (!indexSet && (selectionSet.get('length') > 0))) { // then the selection content has changed 237 if (arrangedObjects) { 238 // Constrain the current selection set to matches in arrangedObjects. 239 selectionSet = selectionSet.copy().constrain(arrangedObjects).freeze(); 240 } else { 241 // No arrangedObjects, so clear the selection. 242 selectionSet = SC.SelectionSet.EMPTY; 243 } 244 this.set('selection', selectionSet); 245 } 246 247 // Reselect an object if required (if content length > 0) 248 if ((selectionSet.get('length') === 0) && arrangedObjects && (arrangedObjects.get('length') > 0) && !allowsEmptySelection) { 249 this.selectObject(this.get('firstSelectableObject'), NO); 250 } 251 252 return this; 253 } 254 255 }; 256