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