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 /** @class
  9 
 10   A SelectionSet contains a set of objects that represent the current
 11   selection.  You can select objects by either adding them to the set directly
 12   or indirectly by selecting a range of indexes on a source object.
 13 
 14   @extends SC.Object
 15   @extends SC.Enumerable
 16   @extends SC.Freezable
 17   @extends SC.Copyable
 18   @since SproutCore 1.0
 19 */
 20 SC.SelectionSet = SC.Object.extend(SC.Enumerable, SC.Freezable, SC.Copyable,
 21   /** @scope SC.SelectionSet.prototype */ {
 22 
 23   /**
 24     Walk like a duck.
 25 
 26     @type Boolean
 27   */
 28   isSelectionSet: YES,
 29 
 30   /**
 31     Total number of indexes in the selection set
 32 
 33     @type Number
 34   */
 35   length: function() {
 36     var ret     = 0,
 37         sets    = this._sets,
 38         objects = this._objects;
 39     if (objects) ret += objects.get('length');
 40     if (sets) sets.forEach(function(s) { ret += s.get('length'); });
 41     return ret ;
 42   }.property().cacheable(),
 43 
 44   // ..........................................................
 45   // INDEX-BASED SELECTION
 46   //
 47 
 48   /**
 49     A set of all the source objects used in the selection set.  This
 50     property changes automatically as you add or remove index sets.
 51 
 52     @type SC.Array
 53   */
 54   sources: function() {
 55     var ret  = [],
 56         sets = this._sets,
 57         len  = sets ? sets.length : 0,
 58         idx, set, source;
 59 
 60     for(idx=0;idx<len;idx++) {
 61       set = sets[idx];
 62       if (set && set.get('length')>0 && set.source) ret.push(set.source);
 63     }
 64     return ret ;
 65   }.property().cacheable(),
 66 
 67   /**
 68     Returns the index set for the passed source object or null if no items are
 69     seleted in the source.
 70 
 71     @param {SC.Array} source the source object
 72     @returns {SC.IndexSet} index set or null
 73   */
 74   indexSetForSource: function(source) {
 75     if (!source || !source.isSCArray) return null; // nothing to do
 76 
 77     var cache   = this._indexSetCache,
 78         objects = this._objects,
 79         ret, idx;
 80 
 81     // try to find in cache
 82     if (!cache) cache = this._indexSetCache = {};
 83     ret = cache[SC.guidFor(source)];
 84     if (ret && ret._sourceRevision && (ret._sourceRevision !== source.propertyRevision)) {
 85       ret = null;
 86     }
 87 
 88     // not in cache.  generate from index sets and any saved objects
 89     if (!ret) {
 90       ret = this._indexSetForSource(source, NO);
 91       if (ret && ret.get('length')===0) ret = null;
 92 
 93       if (objects) {
 94         if (ret) ret = ret.copy();
 95         objects.forEach(function(o) {
 96           if ((idx = source.indexOf(o)) >= 0) {
 97             if (!ret) ret = SC.IndexSet.create();
 98             ret.add(idx);
 99           }
100         }, this);
101       }
102 
103       if (ret) {
104         ret = cache[SC.guidFor(source)] = ret.frozenCopy();
105         ret._sourceRevision = source.propertyRevision;
106       }
107     }
108 
109     return ret;
110   },
111 
112   /**
113     @private
114 
115     Internal method gets the index set for the source, ignoring objects
116     that have been added directly.
117   */
118   _indexSetForSource: function(source, canCreate) {
119     if (canCreate === undefined) canCreate = YES;
120 
121     var guid  = SC.guidFor(source),
122         index = this[guid],
123         sets  = this._sets,
124         len   = sets ? sets.length : 0,
125         ret   = null;
126 
127     if (index >= len) index = null;
128     if (SC.none(index)) {
129       if (canCreate && !this.isFrozen) {
130         this.propertyWillChange('sources');
131         if (!sets) sets = this._sets = [];
132         ret = sets[len] = SC.IndexSet.create();
133         ret.source = source ;
134         this[guid] = len;
135         this.propertyDidChange('sources');
136       }
137 
138     } else ret = sets ? sets[index] : null;
139     return ret ;
140   },
141 
142   /**
143     Add the passed index, range of indexSet belonging to the passed source
144     object to the selection set.
145 
146     The first parameter you pass must be the source array you are selecting
147     from.  The following parameters may be one of a start/length pair, a
148     single index, a range object or an IndexSet.  If some or all of the range
149     you are selecting is already in the set, it will not be selected again.
150 
151     You can also pass an SC.SelectionSet to this method and all the selected
152     sets will be added from their instead.
153 
154     @param {SC.Array} source source object or object to add.
155     @param {Number} start index, start of range, range or IndexSet
156     @param {Number} length length if passing start/length pair.
157     @returns {SC.SelectionSet} receiver
158   */
159   add: function(source, start, length) {
160 
161     if (this.isFrozen) throw new Error(SC.FROZEN_ERROR);
162 
163     var sets, len, idx, set, oldlen, newlen, setlen, objects;
164 
165     // normalize
166     if (start === undefined && length === undefined) {
167       if (!source) throw new Error("Must pass params to SC.SelectionSet.add()");
168       if (source.isIndexSet) return this.add(source.source, source);
169       if (source.isSelectionSet) {
170         sets = source._sets;
171         objects = source._objects;
172         len  = sets ? sets.length : 0;
173 
174         this.beginPropertyChanges();
175         for(idx=0;idx<len;idx++) {
176           set = sets[idx];
177           if (set && set.get('length')>0) this.add(set.source, set);
178         }
179         if (objects) this.addObjects(objects);
180         this.endPropertyChanges();
181         return this ;
182 
183       }
184     }
185 
186     set    = this._indexSetForSource(source, YES);
187     oldlen = this.get('length');
188     setlen = set.get('length');
189     newlen = oldlen - setlen;
190 
191     set.add(start, length);
192 
193     this._indexSetCache = null;
194 
195     newlen += set.get('length');
196     if (newlen !== oldlen) {
197       this.propertyDidChange('length');
198       this.enumerableContentDidChange();
199       if (setlen === 0) this.notifyPropertyChange('sources');
200     }
201 
202     return this ;
203   },
204 
205   /**
206     Removes the passed index, range of indexSet belonging to the passed source
207     object from the selection set.
208 
209     The first parameter you pass must be the source array you are selecting
210     from.  The following parameters may be one of a start/length pair, a
211     single index, a range object or an IndexSet.  If some or all of the range
212     you are selecting is already in the set, it will not be selected again.
213 
214     @param {SC.Array} source source object. must not be null
215     @param {Number} start index, start of range, range or IndexSet
216     @param {Number} length length if passing start/length pair.
217     @returns {SC.SelectionSet} receiver
218   */
219   remove: function(source, start, length) {
220 
221     if (this.isFrozen) throw new Error(SC.FROZEN_ERROR);
222 
223     var sets, len, idx, i, set, oldlen, newlen, setlen, objects, object;
224 
225     // normalize
226     if (start === undefined && length === undefined) {
227       if (!source) throw new Error("Must pass params to SC.SelectionSet.remove()");
228       if (source.isIndexSet) return this.remove(source.source, source);
229       if (source.isSelectionSet) {
230         sets = source._sets;
231         objects = source._objects;
232         len  = sets ? sets.length : 0;
233 
234         this.beginPropertyChanges();
235         for(idx=0;idx<len;idx++) {
236           set = sets[idx];
237           if (set && set.get('length')>0) this.remove(set.source, set);
238         }
239         if (objects) this.removeObjects(objects);
240         this.endPropertyChanges();
241         return this ;
242       }
243     }
244 
245     // save starter info
246     set    = this._indexSetForSource(source, YES);
247     oldlen = this.get('length');
248     newlen = oldlen - set.get('length');
249 
250     // if we have objects selected, determine if they are in the index
251     // set and remove them as well.
252     if (set && (objects = this._objects)) {
253 
254       // convert start/length to index set so the iterator below will work...
255       if (length !== undefined) {
256         start = SC.IndexSet.create(start, length);
257         length = undefined;
258       }
259 
260       for (i = objects.get('length') - 1; i >= 0; --i) {
261         object = objects[i];
262         idx = source.indexOf(object);
263         if (start.contains(idx)) {
264           objects.remove(object);
265           newlen--;
266         }
267       }
268     }
269 
270     // remove indexes from source index set
271     set.remove(start, length);
272     setlen = set.get('length');
273     newlen += setlen;
274 
275     // update caches; change enumerable...
276     this._indexSetCache = null;
277     if (newlen !== oldlen) {
278       this.propertyDidChange('length');
279       this.enumerableContentDidChange();
280       if (setlen === 0) this.notifyPropertyChange('sources');
281     }
282 
283     return this ;
284   },
285 
286 
287   /**
288     Returns YES if the selection contains the named index, range of indexes.
289 
290     @param {Object} source source object for range
291     @param {Number} start index, start of range, range object, or indexSet
292     @param {Number} length optional range length
293     @returns {Boolean}
294   */
295   contains: function(source, start, length) {
296     if (start === undefined && length === undefined) {
297       return this.containsObject(source);
298     }
299 
300     var set = this.indexSetForSource(source);
301     if (!set) return NO ;
302     return set.contains(start, length);
303   },
304 
305   /**
306     Returns YES if the index set contains any of the passed indexes.  You
307     can pass a single index, a range or an index set.
308 
309     @param {Object} source source object for range
310     @param {Number} start index, range, or IndexSet
311     @param {Number} length optional range length
312     @returns {Boolean}
313   */
314   intersects: function(source, start, length) {
315     var set = this.indexSetForSource(source, NO);
316     if (!set) return NO ;
317     return set.intersects(start, length);
318   },
319 
320 
321   // ..........................................................
322   // OBJECT-BASED API
323   //
324 
325   _TMP_ARY: [],
326 
327   /**
328     Adds the object to the selection set.  Unlike adding an index set, the
329     selection will actually track the object independent of its location in
330     the array.
331 
332     @param {Object} object
333     @returns {SC.SelectionSet} receiver
334   */
335   addObject: function(object) {
336     var ary = this._TMP_ARY, ret;
337     ary[0] = object;
338 
339     ret = this.addObjects(ary);
340     ary.length = 0;
341 
342     return ret;
343   },
344 
345   /**
346     Adds objects in the passed enumerable to the selection set.  Unlike adding
347     an index set, the seleciton will actually track the object independent of
348     its location the array.
349 
350     @param {SC.Enumerable} objects
351     @returns {SC.SelectionSet} receiver
352   */
353   addObjects: function(objects) {
354     var cur = this._objects,
355         oldlen, newlen;
356     if (!cur) cur = this._objects = SC.CoreSet.create();
357     oldlen = cur.get('length');
358 
359     cur.addEach(objects);
360     newlen = cur.get('length');
361 
362     this._indexSetCache = null;
363     if (newlen !== oldlen) {
364       this.propertyDidChange('length');
365       this.enumerableContentDidChange();
366     }
367     return this;
368   },
369 
370   /**
371     Removes the object from the selection set.  Note that if the selection
372     set also selects a range of indexes that includes this object, it may
373     still be in the selection set.
374 
375     @param {Object} object
376     @returns {SC.SelectionSet} receiver
377   */
378   removeObject: function(object) {
379     var ary = this._TMP_ARY, ret;
380     ary[0] = object;
381 
382     ret = this.removeObjects(ary);
383     ary.length = 0;
384 
385     return ret;
386   },
387 
388   /**
389     Removes the objects from the selection set.  Note that if the selection
390     set also selects a range of indexes that includes this object, it may
391     still be in the selection set.
392 
393     @param {Object} object
394     @returns {SC.SelectionSet} receiver
395   */
396   removeObjects: function(objects) {
397     var cur = this._objects,
398         oldlen, newlen, sets;
399 
400     if (!cur) return this;
401 
402     oldlen = cur.get('length');
403 
404     cur.removeEach(objects);
405     newlen = cur.get('length');
406 
407     // also remove from index sets, if present
408     if (sets = this._sets) {
409       sets.forEach(function(set) {
410         oldlen += set.get('length');
411         set.removeObjects(objects);
412         newlen += set.get('length');
413       }, this);
414     }
415 
416     this._indexSetCache = null;
417     if (newlen !== oldlen) {
418       this.propertyDidChange('length');
419       this.enumerableContentDidChange();
420     }
421     return this;
422   },
423 
424   /**
425     Returns YES if the selection contains the passed object.  This will search
426     selected ranges in all source objects.
427 
428     @param {Object} object the object to search for
429     @returns {Boolean}
430   */
431   containsObject: function(object) {
432     // fast path
433     var objects = this._objects ;
434     if (objects && objects.contains(object)) return YES ;
435 
436     var sets = this._sets,
437         len  = sets ? sets.length : 0,
438         idx, set;
439     for(idx=0;idx<len;idx++) {
440       set = sets[idx];
441       if (set && set.indexOf(object)>=0) return YES;
442     }
443 
444     return NO ;
445   },
446 
447 
448   // ..........................................................
449   // GENERIC HELPER METHODS
450   //
451 
452   /**
453     Constrains the selection set to only objects found in the passed source
454     object.  This will remove any indexes selected in other sources, any
455     indexes beyond the length of the content, and any objects not found in the
456     set.
457 
458     @param {Object} source the source to limit
459     @returns {SC.SelectionSet} receiver
460   */
461   constrain: function (source) {
462     var set, len, max, objects;
463 
464     this.beginPropertyChanges();
465 
466     // remove sources other than this one
467     this.get('sources').forEach(function(cur) {
468       if (cur === source) return; //skip
469       var set = this._indexSetForSource(source, NO);
470       if (set) this.remove(source, set);
471     },this);
472 
473     // remove indexes beyond end of source length
474     set = this._indexSetForSource(source, NO);
475     if (set && ((max=set.get('max'))>(len=source.get('length')))) {
476       this.remove(source, len, max-len);
477     }
478 
479     // remove objects not in source
480     if (objects = this._objects) {
481       var i, cur;
482       for (i = objects.length - 1; i >= 0; i--) {
483         cur = objects[i];
484         if (source.indexOf(cur) < 0) this.removeObject(cur);
485       }
486     }
487 
488     this.endPropertyChanges();
489     return this ;
490   },
491 
492   /**
493     Returns YES if the passed index set or selection set contains the exact
494     same source objects and indexes as  the receiver.  If you pass any object
495     other than an IndexSet or SelectionSet, returns NO.
496 
497     @param {Object} obj another object.
498     @returns {Boolean}
499   */
500   isEqual: function(obj) {
501     var left, right, idx, len, sources, source;
502 
503     // fast paths
504     if (!obj || !obj.isSelectionSet) return NO ;
505     if (obj === this) return YES;
506     if ((this._sets === obj._sets) && (this._objects === obj._objects)) return YES;
507     if (this.get('length') !== obj.get('length')) return NO;
508 
509     // check objects
510     left = this._objects;
511     right = obj._objects;
512     if (left || right) {
513       if ((left ? left.get('length'):0) !== (right ? right.get('length'):0)) {
514         return NO;
515       }
516       if (left && !left.isEqual(right)) return NO ;
517     }
518 
519     // now go through the sets
520     sources = this.get('sources');
521     len     = sources.get('length');
522     for(idx=0;idx<len;idx++) {
523       source = sources.objectAt(idx);
524       left = this._indexSetForSource(source, NO);
525       right = this._indexSetForSource(source, NO);
526       if (!!right !== !!left) return NO ;
527       if (left && !left.isEqual(right)) return NO ;
528     }
529 
530     return YES ;
531   },
532 
533   /**
534     Clears the set.  Removes all IndexSets from the object
535 
536     @returns {SC.SelectionSet}
537   */
538   clear: function() {
539     if (this.isFrozen) throw new Error(SC.FROZEN_ERROR);
540     if (this._sets) this._sets.length = 0 ; // truncate
541     if (this._objects) this._objects = null;
542 
543     this._indexSetCache = null;
544     this.propertyDidChange('length');
545     this.enumerableContentDidChange();
546     this.notifyPropertyChange('sources');
547 
548     return this ;
549   },
550 
551   /**
552    Clones the set into a new set.
553 
554    @returns {SC.SelectionSet}
555   */
556   copy: function() {
557     var ret  = this.constructor.create(),
558         sets = this._sets,
559         len  = sets ? sets.length : 0 ,
560         idx, set;
561 
562     if (sets && len>0) {
563       sets = ret._sets = sets.slice();
564       for(idx=0;idx<len;idx++) {
565         if (!(set = sets[idx])) continue ;
566         set = sets[idx] = set.copy();
567         ret[SC.guidFor(set.source)] = idx;
568       }
569     }
570 
571     if (this._objects) ret._objects = this._objects.copy();
572     return ret ;
573   },
574 
575   /**
576     @private
577 
578     Freezing a SelectionSet also freezes its internal sets.
579   */
580   freeze: function() {
581     if (this.get('isFrozen')) { return this ; }
582     var sets = this._sets,
583         loc  = sets ? sets.length : 0,
584         set ;
585 
586     while(--loc >= 0) {
587       set = sets[loc];
588       if (set) { set.freeze(); }
589     }
590 
591     if (this._objects) { this._objects.freeze(); }
592     this.set('isFrozen', YES);
593     return this;
594     // return sc_super();
595   },
596 
597   // ..........................................................
598   // ITERATORS
599   //
600 
601   /** @private */
602   toString: function() {
603     var sets = this._sets || [];
604     sets = sets.map(function(set) {
605       return set.toString().replace("SC.IndexSet", SC.guidFor(set.source));
606     }, this);
607     if (this._objects) sets.push(this._objects.toString());
608     return "SC.SelectionSet:%@<%@>".fmt(SC.guidFor(this), sets.join(','));
609   },
610 
611   /** @private */
612   firstObject: function() {
613     var sets    = this._sets,
614         objects = this._objects;
615 
616     // if we have sets, get the first one
617     if (sets && sets.get('length')>0) {
618       var set  = sets ? sets[0] : null,
619           src  = set ? set.source : null,
620           idx  = set ? set.firstObject() : -1;
621       if (src && idx>=0) return src.objectAt(idx);
622     }
623 
624     // otherwise if we have objects, get the first one
625     return objects ? objects.firstObject() : undefined;
626 
627   }.property(),
628 
629   /** @private
630     Implement primitive enumerable support.  Returns each object in the
631     selection.
632   */
633   nextObject: function(count, lastObject, context) {
634     var objects, ret;
635 
636     // TODO: Make this more efficient.  Right now it collects all objects
637     // first.
638 
639     if (count === 0) {
640       objects = context.objects = [];
641       this.forEach(function(o) { objects.push(o); }, this);
642       context.max = objects.length;
643     }
644 
645     objects = context.objects ;
646     ret = objects[count];
647 
648     if (count+1 >= context.max) {
649       context.objects = context.max = null;
650     }
651 
652     return ret ;
653   },
654 
655   /**
656     Iterates over the selection, invoking your callback with each __object__.
657     This will actually find the object referenced by each index in the
658     selection, not just the index.
659 
660     The callback must have the following signature:
661 
662         function callback(object, index, source, indexSet) { ... }
663 
664     If you pass a target, it will be used when the callback is called.
665 
666     @param {Function} callback function to invoke.
667     @param {Object} target optional content. otherwise uses window
668     @returns {SC.SelectionSet} receiver
669   */
670   forEach: function(callback, target) {
671     var sets = this._sets,
672         objects = this._objects,
673         len = sets ? sets.length : 0,
674         set, idx;
675 
676     for(idx=0;idx<len;idx++) {
677       set = sets[idx];
678       if (set) set.forEachObject(callback, target);
679     }
680 
681     if (objects) objects.forEach(callback, target);
682     return this ;
683   }
684 
685 });
686 
687 /** @private */
688 SC.SelectionSet.prototype.clone = SC.SelectionSet.prototype.copy;
689 
690 /**
691   Default frozen empty selection set
692 
693   @property {SC.SelectionSet}
694 */
695 SC.SelectionSet.EMPTY = SC.SelectionSet.create().freeze();
696 
697