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 sc_require('mixins/collection_view_delegate') ;
  9 
 10 /**
 11   Special drag operation passed to delegate if the collection view proposes
 12   to perform a reorder event.
 13 
 14   @static
 15   @constant
 16 */
 17 SC.DRAG_REORDER = 0x0010;
 18 
 19 /**
 20   @class
 21 
 22   This class renders a collection of views based on the items array set
 23   as its content.  You will not use this class directly as it does not
 24   order the views in any manner.  Instead you will want to subclass
 25   SC.CollectionView or use one of its existing subclasses in SproutCore
 26   such as SC.ListView, which renders items in a vertical list or SC.GridView,
 27   which renders items in a grid.
 28 
 29   To use a CollectionView subclass, just create the view and set the 'content'
 30   property to an array of objects.  The collection view will create instances of
 31   the given exampleView class for each item in the array.  You can also bind to
 32   the selection property if you want to monitor the current selection.
 33 
 34   # Extreme Performance
 35 
 36   SC.CollectionView does not just naively render one view per item and
 37   instead is aggressively optimized to allow for collections of
 38   hundreds of thousands of items to perform as fast as only a few items.  In
 39   order to achieve this, first it only creates views and elements for the items
 40   currently visible.  Therefore, when overriding SC.CollectionView, it is
 41   critically important to implement `contentIndexesInRect` which should return
 42   only the indexes of those items that should appear within the visible rect.
 43   By returning only the indexes that are visible, SC.CollectionView can represent
 44   enormous collections with only a few views and elements.
 45 
 46   The second optimization, is that SC.CollectionView will pool and reuse the
 47   few views and elements that it does need to create.  Creating and destroying
 48   views incrementally hurts performance, so by reusing the same views over and
 49   over, the view can much more quickly alter the set of visible views.  As well,
 50   inserting and removing elements from the DOM takes more time than simply
 51   modifying the contents of the same elements over and over, which allows us to
 52   leave the DOM tree untouched.
 53 
 54   @extends SC.View
 55   @extends SC.CollectionViewDelegate
 56   @extends SC.CollectionContent
 57   @since SproutCore 0.9
 58 */
 59 SC.CollectionView = SC.View.extend(SC.ActionSupport, SC.CollectionViewDelegate, SC.CollectionContent,
 60 /** @scope SC.CollectionView.prototype */ {
 61 
 62   /** @private */
 63   _content: null,
 64 
 65   /** @private */
 66   _cv_actionTimer: null,
 67 
 68   /** @private */
 69   _cv_contentRangeObserver: null,
 70 
 71   /** @private */
 72   _cv_selection: null,
 73 
 74   /** @private */
 75   _pools: null,
 76 
 77   /** @private Timer used to track time immediately after a mouse up event. */
 78   _sc_clearMouseJustDownTimer: null,
 79 
 80   /** @private Flag used to track when the mouse is pressed. */
 81   _sc_isMouseDown: false,
 82 
 83   /** @private Flag used to track when mouse was just down so that mousewheel events firing as the finger is lifted don't shoot the slider over. */
 84   _sc_isMouseJustDown: false,
 85 
 86   /** @private */
 87   _sc_itemViews: null,
 88 
 89 
 90   /** @private */
 91   _TMP_DIFF1: SC.IndexSet.create(),
 92 
 93   /** @private */
 94   _TMP_DIFF2: SC.IndexSet.create(),
 95 
 96   /**
 97     @type Array
 98     @default ['sc-collection-view']
 99     @see SC.View#classNames
100   */
101   classNames: ['sc-collection-view'],
102 
103   /**
104     @type Array
105     @default ['isActive']
106   */
107   displayProperties: ['isActive'],
108 
109   /**
110     @type String
111     @default 'collectionRenderDelegate'
112   */
113   renderDelegateName: 'collectionRenderDelegate',
114 
115   /**
116     @type Number
117     @default 200
118   */
119   ACTION_DELAY: 200,
120 
121   // ......................................
122   // PROPERTIES
123   //
124 
125   /**
126     If `YES`, uses the experimental fast `CollectionView` path.
127 
128     *Note* The performance improvements in the experimental code have been
129     integrated directly into SC.CollectionView.  If you have set this property
130     to true, you should set it to false and refer to the class documentation
131     explaining how to modify the performance boost behavior if necessary.
132 
133     Generally, no modifications should be necessary and you should see an
134     immediate performance improvement in all collections, especially on
135     mobile devices.
136 
137     @type Boolean
138     @deprecated Version 1.10
139     @default NO
140   */
141   useFastPath: NO,
142 
143   /**
144     An array of content objects
145 
146     This array should contain the content objects you want the collection view
147     to display.  An item view (based on the `exampleView` view class) will be
148     created for each content object, in the order the content objects appear
149     in this array.
150 
151     If you make the collection editable, the collection view will also modify
152     this array using the observable array methods of `SC.Array`.
153 
154     Usually you will want to bind this property to a controller property
155     that actually contains the array of objects you to display.
156 
157     @type SC.Array
158     @default null
159   */
160   content: null,
161 
162   /** @private */
163   contentBindingDefault: SC.Binding.multiple(),
164 
165   /**
166     The current length of the content.
167 
168     @readonly
169     @type Number
170     @default 0
171   */
172   length: 0,
173 
174   /**
175     The set of indexes that are currently tracked by the collection view.
176     This property is used to determine the range of items the collection view
177     should monitor for changes.
178 
179     The default implementation of this property returns an index set covering
180     the entire range of the content.  It changes automatically whenever the
181     length changes.
182 
183     Note that the returned index set for this property will always be frozen.
184     To change the nowShowing index set, you must create a new index set and
185     apply it.
186 
187     @field
188     @type SC.IndexSet
189     @observes length
190     @observes clippingFrame
191   */
192   nowShowing: function() {
193     // If there is an in-scroll clipping frame, use it.
194     var clippingFrame = this.get('clippingFrame');
195 
196     return this.computeNowShowing(clippingFrame);
197   }.property('length', 'clippingFrame').cacheable(),
198 
199   /**
200     Indexes of selected content objects.  This `SC.SelectionSet` is modified
201     automatically by the collection view when the user changes the selection
202     on the collection.
203 
204     Any item views representing content objects in this set will have their
205     isSelected property set to `YES` automatically.
206 
207     @type SC.SelectionSet
208     @default null
209   */
210   selection: null,
211 
212   /**
213     Allow user to select content using the mouse and keyboard.
214 
215     Set this property to `NO` to disallow the user from selecting items. If you
216     have items in your `selectedIndexes` property, they will still be reflected
217     visually.
218 
219     @type Boolean
220     @default YES
221   */
222   isSelectable: YES,
223 
224   /** @private */
225   isSelectableBindingDefault: SC.Binding.bool(),
226 
227   /**
228     Enable or disable the view.
229 
230     The collection view will set the `isEnabled` property of its item views to
231     reflect the same view of this property.  Whenever `isEnabled` is false,
232     the collection view will also be not selectable or editable, regardless of
233     the settings for `isEditable` & `isSelectable`.
234 
235     @type Boolean
236     @default YES
237   */
238   isEnabled: YES,
239 
240   /** @private */
241   isEnabledBindingDefault: SC.Binding.bool(),
242 
243   /**
244     Allow user to edit content views.
245 
246     Whenever `isEditable` is false, the user will not be able to reorder, add,
247     or delete items regardless of the `canReorderContent` and `canDeleteContent`
248     and `isDropTarget` properties.
249 
250     @type Boolean
251     @default YES
252   */
253   isEditable: YES,
254 
255   /** @private */
256   isEditableBindingDefault: SC.Binding.bool(),
257 
258   /**
259     Allow user to reorder items using drag and drop.
260 
261     If true, the user can use drag and drop to reorder items in the list.
262     If you also accept drops, this will allow the user to drop items into
263     specific points in the list.  Otherwise items will be added to the end.
264 
265     When canReorderContent is true, item views will have the `isReorderable`
266     property set to true (if the `isEditable` is true on the collection).
267 
268     @type Boolean
269     @default NO
270   */
271   canReorderContent: NO,
272 
273   /** @private */
274   canReorderContentBindingDefault: SC.Binding.bool(),
275 
276   /**
277     Allow the user to delete items using the delete key
278 
279     If true the user will be allowed to delete selected items using the delete
280     key.  Otherwise deletes will not be permitted.
281 
282     When canDeleteContent is true, item views will have the `isDeletable`
283     property set to true (if the `isEditable` is true on the collection).
284 
285     @type Boolean
286     @default NO
287   */
288   canDeleteContent: NO,
289 
290   /** @private */
291   canDeleteContentBindingDefault: SC.Binding.bool(),
292 
293   /**
294     Allow user to edit the content by double clicking on it or hitting return.
295     This will only work if isEditable is `YES` and the item view implements
296     the `beginEditing()` method.
297 
298     When canEditContent is true, item views will have the `isEditable`
299     property set to true (if the `isEditable` is true on the collection).
300 
301     @type Boolean
302   */
303   canEditContent: NO,
304 
305   /** @private */
306   canEditContentBindingDefault: SC.Binding.bool(),
307 
308   /**
309     Accept drops for data other than reordering.
310 
311     Setting this property to return true when the view is instantiated will
312     cause it to be registered as a drop target, activating the other drop
313     machinery.
314 
315     @type Boolean
316     @default NO
317   */
318   isDropTarget: NO,
319 
320   /**
321     Use toggle selection instead of normal click behavior.
322 
323     If set to true, then selection will use a toggle instead of the normal
324     click behavior.  Command modifiers will be ignored and instead clicking
325     once will select an item and clicking on it again will deselect it.
326 
327     @type Boolean
328     @default NO
329   */
330   useToggleSelection: NO,
331 
332   /**
333     Trigger the action method on a single click.
334 
335     Normally, clicking on an item view in a collection will select the content
336     object and double clicking will trigger the action method on the
337     collection view.
338 
339     If you set this property to `YES`, then clicking on a view will both select
340     it (if `isSelected` is true) and trigger the action method.
341 
342     Use this if you are using the collection view as a menu of items.
343 
344     @type Boolean
345     @default NO
346   */
347   actOnSelect: NO,
348 
349 
350   /**
351     Select an item immediately on mouse down
352 
353     Normally as soon as you begin a click the item will be selected.
354 
355     In some UI scenarios, you might want to prevent selection until
356     the mouse is released, so you can perform, for instance, a drag operation
357     without actually selecting the target item.
358 
359     @type Boolean
360     @default YES
361   */
362   selectOnMouseDown: YES,
363 
364   /**
365     The view class to use when creating new item views.
366 
367     The collection view will automatically create an instance of the view
368     class you set here for each item in its content array.  You should provide
369     your own subclass for this property to display the type of content you
370     want.
371 
372     The view you set here should understand the following properties, which
373     it can use to alter its display:
374 
375     - `content` -- The content object from the content array your view should
376       display.
377     - `isEnabled` -- False if the view should appear disabled.
378     - `isSelected` -- True if the view should appear selected.
379     - `contentIndex` -- The current index of the view's content.
380     - `isEditable` -- True if the view should appear editable by clicking on it
381       or hitting the Return key.
382     - `isReorderable` -- True if the view should appear reorderable by dragging
383       it.
384     - `isDeletable` -- True if the view should appear deletable, by clicking on
385       a delete button within it or hitting the Delete key.
386 
387     # Working with View and Element Pooling
388 
389     As noted in the SC.CollectionView description above, by default the few
390     instances that are needed of the exampleView class will be created and then
391     reused.  Reusing an exampleView means that the content, isSelected, isEnabled,
392     isEditable, isReorderable, isDeletable and contentIndex properties will be
393     updated as an existing view is pulled from the pool to be displayed.
394 
395     If your custom exampleView class has trouble being reused, you may want to
396     implement the `sleepInPool` and `awakeFromPool` methods in your exampleView.
397     These two methods will be called on the view, one before it is pooled,
398     sleepInPool, and the other before it is unpooled, awakeFromPool.  For
399     example, if your item views have images and there is a delay for new
400     images to appear, you may want to use sleepInPool to ensure the previous
401     image is unloaded so it doesn't appear momentarily while the new image loads.
402 
403     Also, if the rendered output of your exampleView does not update properly you
404     can disable reuse of the layer by setting `isLayerReusable` to false.  This
405     will reduce the performance of your collection though and it is recommended
406     that you instead look at ways to properly update the existing layer as the
407     content changes.
408 
409     Finally, if you really don't want view or element reuse at all, you may
410     disable them both by setting `isReusable` to false in your exampleView class.
411     Your collection will still benefit greatly from incremental rendering, but
412     it will perform slightly less well than with optimal re-use.
413 
414     # Event handling
415 
416     In general you do not want your child views to actually respond to mouse
417     and keyboard events themselves.  It is better to let the collection view
418     do that.
419 
420     If you do implement your own event handlers such as mouseDown or mouseUp,
421     you should be sure to actually call the same method on the collection view
422     to give it the chance to perform its own selection housekeeping.
423 
424     @type SC.View
425     @default SC.View
426   */
427   exampleView: SC.View,
428 
429   /**
430     If set, this key will be used to get the example view for a given
431     content object.  The exampleView property will be ignored.
432 
433     @type String
434     @default null
435   */
436   contentExampleViewKey: null,
437 
438   /**
439     The view class to use when creating new group item views.
440 
441     The collection view will automatically create an instance of the view
442     class you set here for each item in its content array.  You should provide
443     your own subclass for this property to display the type of content you
444     want.
445 
446     If you leave this set to null then the regular example view will be used
447     with the isGroupView property set to YES on the item view.
448 
449     @type SC.View
450     @default null
451   */
452   groupExampleView: null,
453 
454   /**
455     If set, this key will be used to get the example view for a given
456     content object.  The `groupExampleView` property will be ignored.
457 
458     @type String
459     @default null
460   */
461   contentGroupExampleViewKey: null,
462 
463   /**
464     Invoked when the user double clicks on an item (or single clicks of
465     actOnSelect is true)
466 
467     Set this to the name of the action you want to send down the
468     responder chain when the user double clicks on an item (or single clicks
469     if `actOnSelect` is true).  You can optionally specify a specific target as
470     well using the target property.
471 
472     If you do not specify an action, then the collection view will also try to
473     invoke the action named on the target item view.
474 
475     @type String
476     @default null
477     @see SC.ActionSupport
478   */
479   action: null,
480 
481   /**
482     Optional target to send the action to when the user double clicks.
483 
484     If you set the action property to the name of an action, you can
485     optionally specify the target object you want the action to be sent to.
486     This can be either an actual object or a property path that will resolve
487     to an object at the time that the action is invoked.
488 
489     @type String|Object
490     @default null
491   */
492   target: null,
493 
494   /**
495     Invoked when the user single clicks on the right icon of an item.
496 
497     Set this to the name of the action you want to send down the
498     responder chain when the user single clicks on the right icon of an item
499     You can optionally specify a specific target as
500     well using the rightIconTarget property.
501 
502     @type String
503     @default null
504   */
505   rightIconAction: null,
506 
507   /**
508     Optional target to send the action to when the user clicks on the right icon
509     of an item.
510 
511     If you set the rightIconAction property to the name of an action, you can
512     optionally specify the target object you want the action to be sent to.
513     This can be either an actual object or a property path that will resolve
514     to an object at the time that the action is invoked.
515 
516     @type String|Object
517     @default null
518   */
519   rightIconTarget: null,
520 
521   /**
522     Property on content items to use for display.
523 
524     Built-in item views such as the `LabelView`s and `ImageView`s will use the
525     value of this property as a key on the content object to determine the
526     value they should display.
527 
528     For example, if you set `contentValueKey` to 'name' and set the
529     exampleView to an `SC.LabelView`, then the label views created by the
530     collection view will display the value of the content.name.
531 
532     If you are writing your own custom item view for a collection, you can
533     get this behavior automatically by including the SC.Control mixin on your
534     view.  You can also ignore this property if you like.  The collection view
535     itself does not use this property to impact rendering.
536 
537     @type String
538     @default null
539   */
540   contentValueKey: null,
541 
542   /**
543     Enables keyboard-based navigate, deletion, etc. if set to true.
544 
545     @type Boolean
546     @default NO
547   */
548   acceptsFirstResponder: NO,
549 
550   /**
551     Changing this property value by default will cause the `CollectionView` to
552     add/remove an 'active' class name to the root element.
553 
554     @type Boolean
555     @default NO
556   */
557   isActive: NO,
558 
559   /** @deprecated Version 1.11.0.  SC.ScrollView observes the frame (height/width) of the collection.
560 
561     @type Number
562     @default 0
563   */
564   calculatedHeight: 0,
565 
566   /** @deprecated Version 1.11.0.  SC.ScrollView observes the frame (height/width) of the collection.
567 
568     @type Number
569     @default 0
570   */
571   calculatedWidth: 0,
572 
573 
574   // ..........................................................
575   // SUBCLASS METHODS
576   //
577 
578   /**
579     Adjusts the layout of the view according to the computed layout.  Call
580     this method to apply the computed layout to the view.
581   */
582   adjustLayout: function () {
583     var layout = this.computeLayout();
584     if (layout) { this.adjust(layout); }
585   },
586 
587   /**
588     Override to return the computed layout dimensions of the collection view.
589     You can omit any dimensions you don't care about setting in your
590     computed value.
591 
592     This layout is automatically applied whenever the content changes.
593 
594     If you don't care about computing the layout at all, you can return null.
595 
596     @returns {Hash} layout properties
597   */
598   computeLayout: function() {
599     return null;
600   },
601 
602   /**
603     Override to compute the layout of the itemView for the content at the
604     specified index.  This layout will be applied to the view just before it
605     is rendered.
606 
607     @param {Number} contentIndex the index of content being rendered by
608       itemView
609     @returns {Hash} a view layout
610   */
611   layoutForContentIndex: function(contentIndex) {
612     return null;
613   },
614 
615   /**
616     This computed property returns an index set selecting all content indexes.
617     It will recompute anytime the length of the collection view changes.
618 
619     This is used by the default `contentIndexesInRect()` implementation.
620 
621     @field
622     @type SC.IndexSet
623     @observes length
624   */
625   allContentIndexes: function() {
626     return SC.IndexSet.create(0, this.get('length')).freeze();
627   }.property('length').cacheable(),
628 
629   /**
630     Override to return an IndexSet with the indexes that are at least
631     partially visible in the passed rectangle.  This method is used by the
632     default implementation of `computeNowShowing()` to determine the new
633     `nowShowing` range after a scroll.
634 
635     Override this method to implement incremental rendering.
636 
637     @param {Rect} rect the visible rect
638     @returns {SC.IndexSet} now showing indexes
639   */
640   contentIndexesInRect: function(rect) {
641     return null; // select all
642   },
643 
644   /**
645     Compute the nowShowing index set.  The default implementation simply
646     returns the full range.  Override to implement incremental rendering.
647 
648     You should not normally call this method yourself.  Instead get the
649     nowShowing property.
650 
651     @returns {SC.IndexSet} new now showing range
652   */
653   computeNowShowing: function (clippingFrame) {
654     var r = this.contentIndexesInRect(clippingFrame);
655     if (!r) r = this.get('allContentIndexes'); // default show all
656 
657     // make sure the index set doesn't contain any indexes greater than the
658     // actual content.
659     else {
660       var len = this.get('length'),
661           max = r.get('max');
662       if (max > len) r = r.copy().remove(len, max-len).freeze();
663     }
664 
665     return r;
666   },
667 
668   /**
669     Override to show the insertion point during a drag.
670 
671     Called during a drag to show the insertion point.  Passed value is the
672     item view that you should display the insertion point before.  If the
673     passed value is `null`, then you should show the insertion point *AFTER* that
674     last item view returned by the itemViews property.
675 
676     Once this method is called, you are guaranteed to also receive a call to
677     `hideInsertionPoint()` at some point in the future.
678 
679     The default implementation of this method does nothing.
680 
681     @param itemView {SC.ClassicView} view the insertion point should appear directly before. If null, show insertion point at end.
682     @param dropOperation {Number} the drop operation.  will be SC.DROP_BEFORE, SC.DROP_AFTER, or SC.DROP_ON
683 
684     @returns {void}
685   */
686   showInsertionPoint: function(itemView, dropOperation) {},
687 
688   /**
689     Override to hide the insertion point when a drag ends.
690 
691     Called during a drag to hide the insertion point.  This will be called
692     when the user exits the view, cancels the drag or completes the drag.  It
693     will not be called when the insertion point changes during a drag.
694 
695     You should expect to receive one or more calls to
696     `showInsertionPointBefore()` during a drag followed by at least one call to
697     this method at the end.  Your method should not raise an error if it is
698     called more than once.
699 
700     @returns {void}
701   */
702   hideInsertionPoint: function() {},
703 
704 
705   // ..........................................................
706   // DELEGATE SUPPORT
707   //
708 
709 
710   /**
711     Delegate used to implement fine-grained control over collection view
712     behaviors.
713 
714     You can assign a delegate object to this property that will be consulted
715     for various decisions regarding drag and drop, selection behavior, and
716     even rendering.  The object you place here must implement some or all of
717     the `SC.CollectionViewDelegate` mixin.
718 
719     If you do not supply a delegate but the content object you set implements
720     the `SC.CollectionViewDelegate` mixin, then the content will be
721     automatically set as the delegate.  Usually you will work with a
722     `CollectionView` in this way rather than setting a delegate explicitly.
723 
724     @type SC.CollectionViewDelegate
725     @default null
726   */
727   delegate: null,
728 
729   /**
730     The delegate responsible for handling selection changes.  This property
731     will be either the delegate, content, or the collection view itself,
732     whichever implements the `SC.CollectionViewDelegate` mixin.
733 
734     @field
735     @type Object
736   */
737   selectionDelegate: function() {
738     var del = this.get('delegate'), content = this.get('content');
739     return this.delegateFor('isCollectionViewDelegate', del, content);
740   }.property('delegate', 'content').cacheable(),
741 
742   /**
743     The delegate responsible for providing additional display information
744     about the content.  If you bind a collection view to a controller, this
745     the content will usually also be the content delegate, though you
746     could implement your own delegate if you prefer.
747 
748     @field
749     @type Object
750   */
751   contentDelegate: function() {
752     var del = this.get('delegate'), content = this.get('content');
753     return this.delegateFor('isCollectionContent', del, content);
754   }.property('delegate', 'content').cacheable(),
755 
756 
757   // ..........................................................
758   // CONTENT CHANGES
759   //
760 
761   /**
762     Called whenever the content array or an item in the content array or a
763     property on an item in the content array changes.  Reloads the appropriate
764     item view when the content array itself changes or calls
765     `contentPropertyDidChange()` if a property changes.
766 
767     Normally you will not call this method directly though you may override
768     it if you need to change the way changes to observed ranges are handled.
769 
770     @param {SC.Array} content the content array generating the change
771     @param {Object} object the changed object
772     @param {String} key the changed property or '[]' or an array change
773     @param {SC.IndexSet} indexes affected indexes or null for all items
774     @returns {void}
775   */
776   contentRangeDidChange: function(content, object, key, indexes) {
777     if (!object && (key === '[]')) {
778       this.notifyPropertyChange('_contentGroupIndexes');
779       this.reload(indexes); // note: if indexes == null, reloads all
780     } else {
781       this.contentPropertyDidChange(object, key, indexes);
782     }
783   },
784 
785   /**
786     Called whenever a property on an item in the content array changes.  This
787     is only called if you have set `observesContentProperties` to `YES`.
788 
789     Override this property if you want to do some custom work whenever a
790     property on a content object changes.
791 
792     The default implementation does nothing.
793 
794     @param {Object} target the object that changed
795     @param {String} key the property that changed value
796     @param {SC.IndexSet} indexes the indexes in the content array affected
797     @returns {void}
798   */
799   contentPropertyDidChange: function(target, key, indexes) {},
800 
801   /**
802     Called whenever the view needs to updates its `contentRangeObserver` to
803     reflect the current nowShowing index set.  You will not usually call this
804     method yourself but you may override it if you need to provide some
805     custom range observer behavior.
806 
807     Note that if you do implement this method, you are expected to maintain
808     the range observer object yourself.  If a range observer has not been
809     created yet, this method should create it.  If an observer already exists
810     this method should update it.
811 
812     When you create a new range observer, the observer must eventually call
813     `contentRangeDidChange()` for the collection view to function properly.
814 
815     If you override this method you probably also need to override
816     `destroyRangeObserver()` to cleanup any existing range observer.
817 
818     @returns {void}
819   */
820   updateContentRangeObserver: function() {
821     var nowShowing = this.get('nowShowing'),
822         observer   = this._cv_contentRangeObserver,
823         content    = this.get('content');
824 
825     if (!content) return ; // nothing to do
826 
827     if (observer) {
828       content.updateRangeObserver(observer, nowShowing);
829     } else {
830       var func = this.contentRangeDidChange;
831       observer = content.addRangeObserver(nowShowing, this, func, null);
832 
833       // Cache the range observer so we can clean it up later.
834       this._cv_contentRangeObserver = observer ;
835     }
836 
837   },
838 
839   /**
840     Called whever the view needs to invalidate the current content range
841     observer.  This is called whenever the content array changes.  You will
842     not usually call this method yourself but you may override it if you
843     provide your own range observer behavior.
844 
845     Note that if you override this method you should probably also override
846     `updateRangeObserver()` to create or update a range observer as needed.
847 
848     @returns {void}
849   */
850   removeContentRangeObserver: function() {
851     var content  = this.get('content'),
852         observer = this._cv_contentRangeObserver ;
853 
854     if (observer) {
855       if (content) content.removeRangeObserver(observer);
856       this._cv_contentRangeObserver = null ;
857     }
858   },
859 
860   /**
861     Called whenever the content length changes.  This will invalidate the
862     length property of the view itself causing the `nowShowing` to recompute
863     which will in turn update the UI accordingly.
864 
865     @returns {void}
866   */
867   contentLengthDidChange: function() {
868     var content = this.get('content');
869     this.set('length', content ? content.get('length') : 0);
870     this.invokeOnce(this.adjustLayout);
871   },
872 
873   /** @private
874     Whenever content property changes to a new value:
875 
876       - remove any old observers
877       - setup new observers (maybe wait until end of runloop to do this?)
878       - recalc height/reload content
879       - set content as delegate if delegate was old content
880       - reset selection
881 
882     Whenever content array mutates:
883 
884       - possibly stop observing property changes on objects, observe new objs
885       - reload effected item views
886       - update layout for receiver
887   */
888   _cv_contentDidChange: function() {
889     var content = this.get('content'),
890         lfunc   = this.contentLengthDidChange ;
891 
892     if (content === this._content) return; // nothing to do
893 
894     // cleanup old content
895     this.removeContentRangeObserver();
896     if (this._content) {
897       this._content.removeObserver('length', this, lfunc);
898     }
899 
900     // Destroy all pooled views.
901     if (this._pools) {
902       for (var key in this._pools) {
903         this._pools[key].invoke('destroy');
904       }
905 
906       this._pools = null;
907     }
908 
909     // cache
910     this._content = content;
911 
912     // add new observers - range observer will be added lazily
913     if (content) {
914       content.addObserver('length', this, lfunc);
915     }
916 
917     // notify all items changed
918     this.contentLengthDidChange();
919     this.contentRangeDidChange(content, null, '[]', null);
920   }.observes('content'),
921 
922   // ..........................................................
923   // ITEM VIEWS
924   //
925 
926   /** @private
927     The indexes that need to be reloaded.  Must be one of YES, NO, or an
928     SC.IndexSet.
929   */
930   _invalidIndexes: NO,
931 
932   /** @private
933     We need to reload if isEnabled, isEditable, canEditContent, canReorderContent or
934     canDeleteContent change.
935   */
936   _isEnabledDidChange: function () {
937     // Reload the nowShowing indexes.
938     this.reload();
939   }.observes('isEnabled', 'isEditable', 'canEditContent', 'canReorderContent', 'canDeleteContent'),
940 
941   /**
942     Regenerates the item views for the content items at the specified indexes.
943     If you pass null instead of an index set, regenerates all item views.
944 
945     This method is called automatically whenever the content array changes in
946     an observable way, but you can call its yourself also if you need to
947     refresh the collection view for some reason.
948 
949     Note that if the length of the content is shorter than the child views
950     and you call this method, then the child views will be removed no matter
951     what the index.
952 
953     @param {SC.IndexSet} indexes
954     @returns {SC.CollectionView} receiver
955   */
956   reload: function(indexes) {
957     var invalid = this._invalidIndexes,
958       length;
959 
960     if (indexes && invalid !== YES) {
961       if (invalid) invalid.add(indexes);
962       else invalid = this._invalidIndexes = indexes.clone();
963 
964       // If the last item in the list changes, we need to reload the previous last
965       // item also so that the isLast attribute updates appropriately.
966       length = this.get('length');
967       if (length > 1 && invalid.max === length) {
968         invalid.add(length - 2);
969     }
970     } else {
971       this._invalidIndexes = YES ; // force a total reload
972     }
973 
974     if (this.get('isVisibleInWindow')) this.invokeOnce(this.reloadIfNeeded);
975 
976     return this ;
977   },
978 
979   /**
980     Invoked once per runloop to actually reload any needed item views.
981     You can call this method at any time to actually force the reload to
982     happen immediately if any item views need to be reloaded.
983 
984     @returns {SC.CollectionView} receiver
985   */
986   reloadIfNeeded: function() {
987     var invalid = this._invalidIndexes;
988     if (!invalid || !this.get('isVisibleInWindow')) return this ; // delay
989     this._invalidIndexes = NO ;
990 
991     var len, existing,
992         nowShowing = this.get('nowShowing'),
993       itemViews = this._sc_itemViews || [],
994       idx;
995 
996     // if the set is defined but it contains the entire nowShowing range, just
997     // replace
998     if (invalid.isIndexSet && invalid.contains(nowShowing)) invalid = YES ;
999 
1000     // if an index set, just update indexes
1001     if (invalid.isIndexSet) {
1002 
1003       // Go through the invalid indexes and determine if the matching views
1004       // should be redrawn (exists and still showing), should be created (
1005       // doesn't exist and now showing) or should be destroyed (exists and no
1006       // longer showing).
1007       invalid.forEach(function(idx) {
1008         // Get the existing item view, if there is one.
1009         existing = itemViews[idx];
1010         if (existing) {
1011           // Exists so remove it (may send to pool).
1012           this._removeItemView(existing, idx);
1013         }
1014 
1015         // Create it (may fetch from pool).
1016         if (nowShowing.contains(idx)) {
1017           this.itemViewForContentIndex(idx, YES);
1018           }
1019       },this);
1020 
1021     // if set is NOT defined, replace entire content with nowShowing
1022     } else {
1023 
1024       // Process the removals.
1025       for (idx = 0, len = itemViews.length;  idx < len; idx++) {
1026         // Get the existing item view, if there is one.
1027         existing = itemViews ? itemViews[idx] : null;
1028         if (existing) {
1029           this._removeItemView(existing, idx);
1030           }
1031         }
1032 
1033       // Only after the children are removed should we create the new views.
1034       // We do this in order to maximize the chance of re-use should the view
1035       // be marked as such.
1036       nowShowing.forEach(function(idx) {
1037         this.itemViewForContentIndex(idx, YES);
1038       }, this);
1039     }
1040 
1041     return this ;
1042   },
1043 
1044   /** @private Use a shared object so that we are not creating objects for every item view configuration. */
1045   _TMP_ATTRS: {},
1046 
1047   /** @private
1048     The item view classes, cached here for performance. Note that if these ever change, they may
1049     also need to be updated in the isGroupView code block in _reconfigureItemView below.
1050   */
1051   _COLLECTION_CLASS_NAMES: ['sc-collection-item', 'sc-item'],
1052 
1053   /** @private
1054     The group view classes, cached here for performance. Note that if these ever change, they may
1055     also need to be updated in the isGroupView code block in _reconfigureItemView below.
1056   */
1057   _GROUP_COLLECTION_CLASS_NAMES: ['sc-collection-item', 'sc-group-item'],
1058 
1059   /**
1060     Returns the item view for the content object at the specified index. Call
1061     this method instead of accessing child views directly whenever you need
1062     to get the view associated with a content index.
1063 
1064     Although this method take two parameters, you should almost always call
1065     it with just the content index.  The other two parameters are used
1066     internally by the CollectionView.
1067 
1068     If you need to change the way the collection view manages item views
1069     you can override this method as well.  If you just want to change the
1070     default options used when creating item views, override createItemView()
1071     instead.
1072 
1073     Note that if you override this method, then be sure to implement this
1074     method so that it uses a cache to return the same item view for a given
1075     index unless "force" is YES.  In that case, generate a new item view and
1076     replace the old item view in your cache with the new item view.
1077 
1078     @param {Number} idx the content index
1079     @param {Boolean} rebuild internal use only
1080     @returns {SC.View} instantiated view
1081   */
1082   itemViewForContentIndex: function(idx, rebuild) {
1083     var ret,
1084       views;
1085 
1086     // Gatekeep! Since this method is often called directly by loops that may
1087     // suffer from bounds issues, we should validate the idx and return nothing
1088     // rather than returning an invalid item view.
1089     if (SC.none(idx) || idx < 0 || idx >= this.get('length')) {
1090       //@if(debug)
1091       // Developer support
1092       SC.warn("Developer Warning: %@ - itemViewForContentIndex(%@): The index, %@, is not within the range of the content.".fmt(this, idx, idx));
1093       //@endif
1094 
1095       return null; // FAST PATH!!
1096     }
1097 
1098 
1099     // Initialize internal views cache.
1100     views = this._sc_itemViews;
1101     if (!views) { views = this._sc_itemViews = []; }
1102 
1103     // Use an existing view for this index if we have it and aren't rebuilding all.
1104     ret = views[idx];
1105     if (ret) {
1106       if (rebuild) {
1107         ret.destroy();
1108         ret = null;
1109     } else {
1110         return ret;
1111     }
1112     }
1113 
1114     var attrs,
1115       containerView = this.get('containerView') || this,
1116       exampleView,
1117       pool,
1118       prototype;
1119 
1120     // Set up the attributes for the view.
1121     attrs = this._attrsForContentIndex(idx);
1122 
1123     // If the view is reusable and there is an appropriate view inside the
1124     // pool, simply reuse it to avoid having to create a new view.
1125     exampleView = this._exampleViewForContentIndex(idx);
1126     prototype = exampleView.prototype;
1127     if (SC.none(prototype.isReusable) || prototype.isReusable) {
1128       pool = this._poolForExampleView(exampleView);
1129 
1130       // Is there a view we can re-use?
1131       if (pool.length > 0) {
1132         ret = pool.shift();
1133 
1134         // Reconfigure the view.
1135         this._reconfigureItemView(ret, attrs);
1136 
1137         // Awake the view.
1138         if (ret.awakeFromPool) { ret.awakeFromPool(this); }
1139 
1140         // Recreate the layer if it was destroyed.
1141         if (!ret.get('_isRendered')) {
1142           ret.invokeOnce(ret._doRender);
1143         }
1144         }
1145       }
1146 
1147     // If we weren't able to re-use a view, then create a new one.
1148     if (!ret) {
1149       ret = this.createItemView(exampleView, idx, attrs);
1150       containerView.insertBefore(ret, null);   // Equivalent to 'append()', but avoids one more function call
1151       }
1152 
1153     views[idx] = ret;
1154     return ret ;
1155   },
1156 
1157   /**
1158     Convenience method for getting the item view of a content object.
1159 
1160     @param {Object} object
1161   */
1162   itemViewForContentObject: function(object) {
1163     var content = this.get('content');
1164     if (!content) return null;
1165     var contentIndex = content.indexOf(object);
1166     if (contentIndex === -1) return null;
1167     return this.itemViewForContentIndex(contentIndex);
1168   },
1169 
1170   /** @private */
1171   _TMP_LAYERID: [],
1172 
1173   /**
1174     Primitive to instantiate an item view.  You will be passed the class
1175     and a content index.  You can override this method to perform any other
1176     one time setup.
1177 
1178     Note that item views may be created somewhat frequently so keep this fast.
1179 
1180     *IMPORTANT:* The attrs hash passed is reused each time this method is
1181     called.   If you add properties to this hash be sure to delete them before
1182     returning from this method.
1183 
1184     @param {Class} exampleClass example view class
1185     @param {Number} idx the content index
1186     @param {Hash} attrs expected attributes
1187     @returns {SC.View} item view instance
1188   */
1189   createItemView: function(exampleClass, idx, attrs) {
1190     return exampleClass.create(attrs);
1191   },
1192 
1193   /**
1194     Generates a layerId for the passed index and item.  Usually the default
1195     implementation is suitable.
1196 
1197     @param {Number} idx the content index
1198     @returns {String} layer id, must be suitable for use in HTML id attribute
1199   */
1200   layerIdFor: function(idx) {
1201     var ret = this._TMP_LAYERID;
1202     ret[0] = this.get('layerId');
1203     ret[1] = idx;
1204     return ret.join('-');
1205   },
1206 
1207   /**
1208     Extracts the content index from the passed layerId.  If the layer id does
1209     not belong to the receiver or if no value could be extracted, returns NO.
1210 
1211     @param {String} id the layer id
1212   */
1213   contentIndexForLayerId: function(id) {
1214     if (!id || !(id = id.toString())) return null ; // nothing to do
1215 
1216     var base = this.get('layerId') + '-';
1217 
1218     // no match
1219     if ((id.length <= base.length) || (id.indexOf(base) !== 0)) return null ;
1220     var ret = Number(id.slice(id.lastIndexOf('-')+1));
1221     return isNaN(ret) ? null : ret ;
1222   },
1223 
1224 
1225   /**
1226     Find the first content item view for the passed event.
1227 
1228     This method will go up the view chain, starting with the view that was the
1229     target of the passed event, looking for a child item.  This will become
1230     the view that is selected by the mouse event.
1231 
1232     This method only works for mouseDown & mouseUp events.  mouseMoved events
1233     do not have a target.
1234 
1235     @param {SC.Event} evt An event
1236     @returns {SC.View} the item view or null
1237   */
1238   itemViewForEvent: function(evt) {
1239     var responder = this.getPath('pane.rootResponder') ;
1240     if (!responder) return null ; // fast path
1241 
1242     var element = evt.target,
1243         layer   = this.get('layer'),
1244         contentIndex = null,
1245         id;
1246 
1247     // walk up the element hierarchy until we find this or an element with an
1248     // id matching the base guid (i.e. a collection item)
1249     while (element && element !== document && element !== layer) {
1250       id = element ? SC.$(element).attr('id') : null ;
1251       if (id && (contentIndex = this.contentIndexForLayerId(id)) !== null) {
1252           break;
1253       }
1254       element = element.parentNode ;
1255     }
1256 
1257     // no matching element found?
1258     if (contentIndex===null || (element === layer)) {
1259       element = layer = null; // avoid memory leaks
1260       return null;
1261     }
1262 
1263     // okay, found the DOM node for the view, go ahead and create it
1264     // first, find the contentIndex
1265     if (contentIndex >= this.get('length')) {
1266       throw new Error("layout for item view %@ was found when item view does not exist (%@)".fmt(id, this));
1267     }
1268 
1269     return this.itemViewForContentIndex(contentIndex);
1270   },
1271 
1272   // ..........................................................
1273   // DISCLOSURE SUPPORT
1274   //
1275 
1276   /**
1277     Expands any items in the passed selection array that have a disclosure
1278     state.
1279 
1280     @param {SC.IndexSet} indexes the indexes to expand
1281     @returns {SC.CollectionView} receiver
1282   */
1283   expand: function(indexes) {
1284     if (!indexes) return this; // nothing to do
1285     var del     = this.get('contentDelegate'),
1286         content = this.get('content');
1287 
1288     indexes.forEach(function(i) {
1289       var state = del.contentIndexDisclosureState(this, content, i);
1290       if (state === SC.BRANCH_CLOSED) del.contentIndexExpand(this,content,i);
1291     }, this);
1292     return this;
1293   },
1294 
1295   /**
1296     Collapses any items in the passed selection array that have a disclosure
1297     state.
1298 
1299     @param {SC.IndexSet} indexes the indexes to expand
1300     @returns {SC.CollectionView} receiver
1301   */
1302   collapse: function(indexes) {
1303     if (!indexes) return this; // nothing to do
1304     var del     = this.get('contentDelegate'),
1305         content = this.get('content');
1306 
1307     indexes.forEach(function(i) {
1308       var state = del.contentIndexDisclosureState(this, content, i);
1309       if (state === SC.BRANCH_OPEN) del.contentIndexCollapse(this,content,i);
1310     }, this);
1311     return this;
1312   },
1313 
1314   // ..........................................................
1315   // SELECTION SUPPORT
1316   //
1317 
1318   /** @private
1319     Called whenever the selection object is changed to a new value.  Begins
1320     observing the selection for changes.
1321   */
1322   _cv_selectionDidChange: function() {
1323     var sel  = this.get('selection'),
1324         last = this._cv_selection,
1325         func = this._cv_selectionContentDidChange;
1326 
1327     if (sel === last) return; // nothing to do
1328     if (last) last.removeObserver('[]', this, func);
1329     if (sel) sel.addObserver('[]', this, func);
1330 
1331     this._cv_selection = sel ;
1332     this._cv_selectionContentDidChange();
1333   }.observes('selection'),
1334 
1335   /** @private
1336     Called whenever the selection object or its content changes.  This will
1337     repaint any items that changed their selection state.
1338   */
1339   _cv_selectionContentDidChange: function() {
1340     var sel  = this.get('selection'),
1341         last = this._cv_selindexes, // clone of last known indexes
1342         content = this.get('content'),
1343         diff ;
1344 
1345     // save new last
1346     this._cv_selindexes = sel ? sel.frozenCopy() : null;
1347 
1348     // determine which indexes are now invalid
1349     if (last) last = last.indexSetForSource(content);
1350     if (sel) sel = sel.indexSetForSource(content);
1351 
1352     if (sel && last) diff = sel.without(last).add(last.without(sel));
1353     else diff = sel || last;
1354 
1355     if (diff && diff.get('length')>0) this.reloadSelectionIndexes(diff);
1356   },
1357 
1358   /** @private
1359     Contains the current item views that need their selection to be repainted.
1360     This may be either NO, YES, or an IndexSet.
1361   */
1362   _invalidSelection: NO,
1363 
1364   /**
1365     Called whenever the selection changes.  The passed index set will contain
1366     any affected indexes including those indexes that were previously
1367     selected and now should be deselected.
1368 
1369     Pass null to reload the selection state for all items.
1370 
1371     @param {SC.IndexSet} indexes affected indexes
1372     @returns {SC.CollectionView} receiver
1373   */
1374   reloadSelectionIndexes: function(indexes) {
1375     var invalid = this._invalidSelection ;
1376     if (indexes && (invalid !== YES)) {
1377       if (invalid) { invalid.add(indexes) ; }
1378       else { invalid = this._invalidSelection = indexes.copy(); }
1379 
1380     } else this._invalidSelection = YES ; // force a total reload
1381 
1382     if (this.get('isVisibleInWindow')) {
1383       this.invokeOnce(this.reloadSelectionIndexesIfNeeded);
1384     }
1385 
1386     return this ;
1387   },
1388 
1389   /**
1390     Reloads the selection state if needed on any dirty indexes.  Normally this
1391     will run once at the end of the runloop, but you can force the item views
1392     to reload their selection immediately by calling this method.
1393 
1394     You can also override this method if needed to change the way the
1395     selection is reloaded on item views.  The default behavior will simply
1396     find any item views in the nowShowing range that are affected and
1397     modify them.
1398 
1399     @returns {SC.CollectionView} receiver
1400   */
1401   reloadSelectionIndexesIfNeeded: function() {
1402     var invalid = this._invalidSelection;
1403     if (!invalid || !this.get('isVisibleInWindow')) return this ;
1404 
1405     var nowShowing = this.get('nowShowing'),
1406         reload     = this._invalidIndexes,
1407         content    = this.get('content'),
1408         sel        = this.get('selection');
1409 
1410     this._invalidSelection = NO; // reset invalid
1411 
1412     // fast path.  if we are going to reload everything anyway, just forget
1413     // about it.  Also if we don't have a nowShowing, nothing to do.
1414     if (reload === YES || !nowShowing) return this ;
1415 
1416     // if invalid is YES instead of index set, just reload everything
1417     if (invalid === YES) invalid = nowShowing;
1418 
1419     // if we will reload some items anyway, don't bother
1420     if (reload && reload.isIndexSet) invalid = invalid.without(reload);
1421 
1422     // iterate through each item and set the isSelected state.
1423     invalid.forEach(function(idx) {
1424       if (!nowShowing.contains(idx)) return; // not showing
1425       var view = this.itemViewForContentIndex(idx, NO);
1426       if (view) view.set('isSelected', sel ? sel.contains(content, idx) : NO);
1427     },this);
1428 
1429     return this ;
1430   },
1431 
1432   /**
1433     Selection primitive.  Selects the passed IndexSet of items, optionally
1434     extending the current selection.  If extend is NO or not passed then this
1435     will replace the selection with the passed value.  Otherwise the indexes
1436     will be added to the current selection.
1437 
1438     @param {Number|SC.IndexSet} indexes index or indexes to select
1439     @param extend {Boolean} optionally extend the selection
1440     @returns {SC.CollectionView} receiver
1441   */
1442   select: function(indexes, extend) {
1443     var content = this.get('content'),
1444         del     = this.get('selectionDelegate'),
1445         groupIndexes = this.get('_contentGroupIndexes'),
1446         sel;
1447 
1448     if (!this.get('isSelectable') || !this.get('isEnabledInPane')) return this;
1449 
1450     // normalize
1451     if (SC.typeOf(indexes) === SC.T_NUMBER) {
1452       indexes = SC.IndexSet.create(indexes, 1);
1453     }
1454 
1455     // if we are passed an empty index set or null, clear the selection.
1456     if (indexes && indexes.get('length')>0) {
1457 
1458       // first remove any group indexes - these can never be selected
1459       if (groupIndexes && groupIndexes.get('length')>0) {
1460         indexes = indexes.copy().remove(groupIndexes);
1461       }
1462 
1463       // give the delegate a chance to alter the items
1464       indexes = del.collectionViewShouldSelectIndexes(this, indexes, extend);
1465       if (!indexes || indexes.get('length')===0) return this; // nothing to do
1466 
1467     } else indexes = null;
1468 
1469     // build the selection object, merging if needed
1470     if (extend && (sel = this.get('selection'))) sel = sel.copy();
1471     else sel = SC.SelectionSet.create();
1472 
1473     if (indexes && indexes.get('length')>0) {
1474 
1475       // when selecting only one item, always select by content
1476       if (indexes.get('length') === 1 && !this.get('allowDuplicateItems')) {
1477         sel.addObject(content.objectAt(indexes.get('firstObject')));
1478 
1479       // otherwise select an index range
1480       } else sel.add(content, indexes);
1481 
1482     }
1483 
1484     // give delegate one last chance
1485     sel = del.collectionViewSelectionForProposedSelection(this, sel);
1486     if (!sel) sel = SC.SelectionSet.create(); // empty
1487 
1488     // if we're not extending the selection, clear the selection anchor
1489     this._selectionAnchor = null ;
1490     this.set('selection', sel.freeze()) ;
1491     return this;
1492   },
1493 
1494   /**
1495     Primitive to remove the indexes from the selection.
1496 
1497     @param {Number|SC.IndexSet} indexes index or indexes to deselect
1498     @returns {SC.CollectionView} receiver
1499   */
1500   deselect: function(indexes) {
1501     var sel     = this.get('selection'),
1502         content = this.get('content'),
1503         del     = this.get('selectionDelegate');
1504 
1505     if (!this.get('isSelectable') || !this.get('isEnabledInPane')) return this;
1506     if (!sel || sel.get('length')===0) return this; // nothing to do
1507 
1508     // normalize
1509     if (SC.typeOf(indexes) === SC.T_NUMBER) {
1510       indexes = SC.IndexSet.create(indexes, 1);
1511     }
1512 
1513     // give the delegate a chance to alter the items
1514     indexes = del.collectionViewShouldDeselectIndexes(this, indexes) ;
1515     if (!indexes || indexes.get('length')===0) return this; // nothing to do
1516 
1517     // now merge change - note we expect sel && indexes to not be null
1518     sel = sel.copy().remove(content, indexes);
1519     sel = del.collectionViewSelectionForProposedSelection(this, sel);
1520     if (!sel) sel = SC.SelectionSet.create(); // empty
1521 
1522     this.set('selection', sel.freeze()) ;
1523     return this ;
1524   },
1525 
1526   /** @private
1527    Finds the next selectable item, up to content length, by asking the
1528    delegate. If a non-selectable item is found, the index is skipped. If
1529    no item is found, selection index is returned unmodified.
1530 
1531    Return value will always be in the range of the bottom of the current
1532    selection index and the proposed index.
1533 
1534    @param {Number} proposedIndex the desired index to select
1535    @param {Number} bottom optional bottom of selection use as fallback
1536    @returns {Number} next selectable index.
1537   */
1538   _findNextSelectableItemFromIndex: function(proposedIndex, bottom) {
1539     var lim     = this.get('length'),
1540         range   = SC.IndexSet.create(),
1541         del     = this.get('selectionDelegate'),
1542         groupIndexes = this.get('_contentGroupIndexes'),
1543         ret, sel ;
1544 
1545     // fast path
1546     if (!groupIndexes && (del.collectionViewShouldSelectIndexes === this.collectionViewShouldSelectIndexes)) {
1547       return proposedIndex;
1548     }
1549 
1550     // loop forwards looking for an index that is allowed by delegate
1551     // we could alternatively just pass the whole range but this might be
1552     // slow for the delegate
1553     while (proposedIndex < lim) {
1554       if (!groupIndexes || !groupIndexes.contains(proposedIndex)) {
1555         range.add(proposedIndex);
1556         ret = del.collectionViewShouldSelectIndexes(this, range);
1557         if (ret && ret.get('length') >= 1) return proposedIndex ;
1558         range.remove(proposedIndex);
1559       }
1560       proposedIndex++;
1561     }
1562 
1563     // if nothing was found, return top of selection
1564     if (bottom === undefined) {
1565       sel = this.get('selection');
1566       bottom = sel ? sel.get('max') : -1 ;
1567     }
1568     return bottom ;
1569   },
1570 
1571   /** @private
1572    Finds the previous selectable item, up to the first item, by asking the
1573    delegate. If a non-selectable item is found, the index is skipped. If
1574    no item is found, selection index is returned unmodified.
1575 
1576    @param {Integer} proposedIndex the desired index to select
1577    @returns {Integer} the previous selectable index. This will always be in the range of the top of the current selection index and the proposed index.
1578   */
1579   _findPreviousSelectableItemFromIndex: function(proposedIndex, top) {
1580     var range   = SC.IndexSet.create(),
1581         del     = this.get('selectionDelegate'),
1582         groupIndexes = this.get('_contentGroupIndexes'),
1583         ret ;
1584 
1585     if (SC.none(proposedIndex)) proposedIndex = -1;
1586 
1587     // fast path
1588     if (!groupIndexes && (del.collectionViewShouldSelectIndexes === this.collectionViewShouldSelectIndexes)) {
1589       return proposedIndex;
1590     }
1591 
1592     // loop backwards looking for an index that is allowed by delegate
1593     // we could alternatively just pass the whole range but this might be
1594     // slow for the delegate
1595     while (proposedIndex >= 0) {
1596       if (!groupIndexes || !groupIndexes.contains(proposedIndex)) {
1597         range.add(proposedIndex);
1598         ret = del.collectionViewShouldSelectIndexes(this, range);
1599         if (ret && ret.get('length') >= 1) return proposedIndex ;
1600         range.remove(proposedIndex);
1601       }
1602       proposedIndex--;
1603     }
1604 
1605     // if nothing was found, return top of selection
1606     if (top === undefined) {
1607       var sel = this.get('selection');
1608       top = sel ? sel.get('min') : -1 ;
1609     }
1610     if (SC.none(top)) top = -1;
1611     return top ;
1612   },
1613 
1614   /**
1615     Select one or more items before the current selection, optionally
1616     extending the current selection.  Also scrolls the selected item into
1617     view.
1618 
1619     Selection does not wrap around.
1620 
1621     @param {Boolean} [extend] If true, the selection will be extended
1622       instead of replaced. Defaults to false.
1623     @param {Integer} [numberOfItems] The number of previous to be
1624       selected.  Defaults to 1
1625     @returns {SC.CollectionView} receiver
1626   */
1627   selectPreviousItem: function(extend, numberOfItems) {
1628     if (SC.none(numberOfItems)) numberOfItems = 1;
1629     if (SC.none(extend)) extend = false;
1630 
1631     var sel     = this.get('selection'),
1632         content = this.get('content');
1633     if (sel) sel = sel.indexSetForSource(content);
1634 
1635     var selTop    = sel ? sel.get('min') : -1,
1636         selBottom     = sel ? sel.get('max')-1 : -1,
1637         anchor        = this._selectionAnchor;
1638     if (SC.none(anchor)) anchor = selTop;
1639 
1640     // if extending, then we need to do some fun stuff to build the array
1641     if (extend) {
1642 
1643       // If the selBottom is after the anchor, then reduce the selection
1644       if (selBottom > anchor) {
1645         selBottom = selBottom - numberOfItems ;
1646 
1647       // otherwise, select the previous item from the top
1648       } else {
1649         selTop = this._findPreviousSelectableItemFromIndex(selTop - numberOfItems);
1650       }
1651 
1652       // Ensure we are not out of bounds
1653       if (SC.none(selTop) || (selTop < 0)) selTop = 0 ;
1654       if (!content.objectAt(selTop)) selTop = sel ? sel.get('min') : -1;
1655       if (selBottom < selTop) selBottom = selTop ;
1656 
1657     // if not extending, just select the item previous to the selTop
1658     } else {
1659       selTop = this._findPreviousSelectableItemFromIndex(selTop - numberOfItems);
1660       if (SC.none(selTop) || (selTop < 0)) selTop = 0 ;
1661       if (!content.objectAt(selTop)) selTop = sel ? sel.get('min') : -1;
1662       selBottom = selTop ;
1663       anchor = null ;
1664     }
1665 
1666     var scrollToIndex = selTop ;
1667 
1668     // now build new selection
1669     sel = SC.IndexSet.create(selTop, selBottom+1-selTop);
1670 
1671     // ensure that the item is visible and set the selection
1672     this.scrollToContentIndex(scrollToIndex) ;
1673     this.select(sel) ;
1674     this._selectionAnchor = anchor ;
1675     return this ;
1676   },
1677 
1678   /**
1679     Select one or more items following the current selection, optionally
1680     extending the current selection.  Also scrolls to selected item.
1681 
1682     Selection does not wrap around.
1683 
1684     @param {Boolean} [extend] If true, the selection will be extended
1685       instead of replaced. Defaults to false.
1686     @param {Integer} [numberOfItems] The number of items to be
1687       selected. Defaults to 1.
1688     @returns {SC.CollectionView} receiver
1689   */
1690   selectNextItem: function(extend, numberOfItems) {
1691     if (SC.none(numberOfItems)) numberOfItems = 1 ;
1692     if (SC.none(extend)) extend = false ;
1693 
1694     var sel     = this.get('selection'),
1695         content = this.get('content');
1696     if (sel) sel = sel.indexSetForSource(content);
1697 
1698     var selTop    = sel ? sel.get('min') : -1,
1699         selBottom = sel ? sel.get('max')-1 : -1,
1700         anchor    = this._selectionAnchor,
1701         lim       = this.get('length');
1702 
1703     if (SC.none(anchor)) anchor = selTop;
1704 
1705     // if extending, then we need to do some fun stuff to build the array
1706     if (extend) {
1707 
1708       // If the selTop is before the anchor, then reduce the selection
1709       if (selTop < anchor) {
1710         selTop = selTop + numberOfItems ;
1711 
1712       // otherwise, select the next item after the bottom
1713       } else {
1714         selBottom = this._findNextSelectableItemFromIndex(selBottom + numberOfItems, selBottom);
1715       }
1716 
1717       // Ensure we are not out of bounds
1718       if (selBottom >= lim) selBottom = lim-1;
1719 
1720       // we also need to check that the item exists
1721       if (!content.objectAt(selBottom)) selBottom = sel ? sel.get('max') - 1 : -1;
1722 
1723       // and if top has eclipsed bottom, handle that too.
1724       if (selTop > selBottom) selTop = selBottom ;
1725 
1726     // if not extending, just select the item next to the selBottom
1727     } else {
1728       selBottom = this._findNextSelectableItemFromIndex(selBottom + numberOfItems, selBottom);
1729 
1730       if (selBottom >= lim) selBottom = lim-1;
1731       if (!content.objectAt(selBottom)) selBottom = sel ? sel.get('max') - 1 : -1;
1732       selTop = selBottom ;
1733       anchor = null ;
1734     }
1735 
1736     var scrollToIndex = selBottom ;
1737 
1738     // now build new selection
1739     sel = SC.IndexSet.create(selTop, selBottom-selTop+1);
1740 
1741     // ensure that the item is visible and set the selection
1742     this.scrollToContentIndex(scrollToIndex) ;
1743     this.select(sel) ;
1744     this._selectionAnchor = anchor ;
1745     return this ;
1746   },
1747 
1748   /**
1749     Deletes the selected content if canDeleteContent is YES.  This will invoke
1750     delegate methods to provide fine-grained control.  Returns YES if the
1751     deletion was possible, even if none actually occurred.
1752 
1753     @returns {Boolean} YES if deletion is possible.
1754   */
1755   deleteSelection: function() {
1756     // perform some basic checks...
1757     if (!this.get('isEditable') || !this.get('canDeleteContent')) return NO;
1758 
1759     var sel     = this.get('selection'),
1760         content = this.get('content'),
1761         del     = this.get('selectionDelegate'),
1762         indexes = sel&&content ? sel.indexSetForSource(content) : null;
1763 
1764     if (!content || !indexes || indexes.get('length') === 0) return NO ;
1765 
1766     // let the delegate decide what to actually delete.  If this returns an
1767     // empty index set or null, just do nothing.
1768     indexes = del.collectionViewShouldDeleteIndexes(this, indexes);
1769     if (!indexes || indexes.get('length') === 0) return NO ;
1770 
1771     // now have the delegate (or us) perform the deletion. The default
1772     // delegate implementation just uses standard SC.Array methods to do the
1773     // right thing.
1774     del.collectionViewDeleteContent(this, this.get('content'), indexes);
1775 
1776     return YES ;
1777   },
1778 
1779   // ..........................................................
1780   // SCROLLING
1781   //
1782 
1783   /**
1784     Scroll the rootElement (if needed) to ensure that the item is visible.
1785 
1786     @param {Number} contentIndex The index of the item to scroll to
1787     @returns {SC.CollectionView} receiver
1788   */
1789   scrollToContentIndex: function(contentIndex) {
1790     var itemView = this.itemViewForContentIndex(contentIndex) ;
1791     if (itemView) this.scrollToItemView(itemView) ;
1792     return this;
1793   },
1794 
1795   /**
1796     Scroll to the passed item view.  If the item view is not visible on screen
1797     this method will not work.
1798 
1799     @param {SC.View} view The item view to scroll to
1800     @returns {SC.CollectionView} receiver
1801   */
1802   scrollToItemView: function(view) {
1803     if (view) view.scrollToVisible();
1804     return this ;
1805   },
1806 
1807   // ..........................................................
1808   // KEYBOARD EVENTS
1809   //
1810 
1811   /** @private */
1812   keyDown: function(evt) {
1813     var ret = this.interpretKeyEvents(evt) ;
1814     return !ret ? NO : ret ;
1815   },
1816 
1817   /** @private */
1818   keyUp: function() { return true; },
1819 
1820   /** @private
1821     Handle space key event.  Do action
1822   */
1823   insertText: function(chr, evt) {
1824     if (chr === ' ') {
1825       var sel = this.get('selection');
1826       if (sel && sel.get('length')>0) {
1827         this.invokeLater(this._cv_action, 0, null, evt);
1828       }
1829       return YES ;
1830     } else return NO ;
1831   },
1832 
1833   /** @private
1834     Handle select all keyboard event.
1835   */
1836   selectAll: function(evt) {
1837     var content = this.get('content'),
1838         del = this.delegateFor('allowsMultipleSelection', this.get('delegate'), content);
1839 
1840     if (del && del.get('allowsMultipleSelection')) {
1841       var sel = content ? SC.IndexSet.create(0, content.get('length')) : null;
1842     this.select(sel, NO) ;
1843     }
1844     return YES ;
1845   },
1846 
1847   /** @private
1848     Remove selection of any selected items.
1849   */
1850   deselectAll: function() {
1851     var content = this.get('content'),
1852         del = this.delegateFor('allowsEmptySelection', this.get('delegate'), content);
1853 
1854     if (del && del.get('allowsEmptySelection')) {
1855       var sel = content ? SC.IndexSet.create(0, content.get('length')) : null;
1856     this.deselect(sel, NO) ;
1857     }
1858     return YES ;
1859   },
1860 
1861   /** @private
1862     Handle delete keyboard event.
1863   */
1864   deleteBackward: function(evt) {
1865     return this.deleteSelection() ;
1866   },
1867 
1868   /** @private
1869     Handle delete keyboard event.
1870   */
1871   deleteForward: function(evt) {
1872     return this.deleteSelection() ;
1873   },
1874 
1875   /** @private
1876     Selects the same item on the next row or moves down one if itemsPerRow = 1
1877   */
1878   moveDown: function(sender, evt) {
1879     this.selectNextItem(false, this.get('itemsPerRow') || 1) ;
1880     this._cv_performSelectAction(null, evt, this.ACTION_DELAY);
1881     return true ;
1882   },
1883 
1884   /** @private
1885     Selects the same item on the next row or moves up one if itemsPerRow = 1
1886   */
1887   moveUp: function(sender, evt) {
1888     this.selectPreviousItem(false, this.get('itemsPerRow') || 1) ;
1889     this._cv_performSelectAction(null, evt, this.ACTION_DELAY);
1890     return true ;
1891   },
1892 
1893   /** @private
1894     Selects the previous item if itemsPerRow > 1.  Otherwise does nothing.
1895     If item is expandable, will collapse.
1896   */
1897   moveLeft: function(evt) {
1898     // If the control key is down, this may be a browser shortcut and
1899     // we should not handle the arrow key.
1900     if (evt.ctrlKey || evt.metaKey) return NO;
1901 
1902     if ((this.get('itemsPerRow') || 1) > 1) {
1903       this.selectPreviousItem(false, 1);
1904       this._cv_performSelectAction(null, evt, this.ACTION_DELAY);
1905 
1906     } else {
1907       var sel     = this.get('selection'),
1908           content = this.get('content'),
1909           indexes = sel ? sel.indexSetForSource(content) : null;
1910 
1911       // Collapse the element if it is expanded.  However, if there is exactly
1912       // one item selected and the item is already collapsed or is a leaf
1913       // node, then select the (expanded) parent element instead as a
1914       // convenience to the user.
1915       if ( indexes ) {
1916         var del,     // We'll load it lazily
1917             selectParent = false,
1918             index;
1919 
1920         if ( indexes.get('length') === 1 ) {
1921           index = indexes.get('firstObject');
1922           del = this.get('contentDelegate');
1923           var state = del.contentIndexDisclosureState(this, content, index);
1924           if (state !== SC.BRANCH_OPEN) selectParent = true;
1925         }
1926 
1927         if ( selectParent ) {
1928           // TODO:  PERFORMANCE:  It would be great to have a function like
1929           //        SC.CollectionView.selectParentItem() or something similar
1930           //        for performance reasons.  But since we don't currently
1931           //        have such a function, let's just iterate through the
1932           //        previous items until we find the first one with a outline
1933           //        level of one less than the selected item.
1934           var desiredOutlineLevel = del.contentIndexOutlineLevel(this, content, index) - 1;
1935           if ( desiredOutlineLevel >= 0 ) {
1936             var parentIndex = -1;
1937             while ( parentIndex < 0 ) {
1938               var previousItemIndex = this._findPreviousSelectableItemFromIndex(index - 1);
1939               if (previousItemIndex < 0 ) return false;    // Sanity-check.
1940               index = previousItemIndex;
1941               var outlineLevel = del.contentIndexOutlineLevel(this, content, index);
1942               if ( outlineLevel === desiredOutlineLevel ) {
1943                 parentIndex = previousItemIndex;
1944               }
1945             }
1946 
1947             // If we found the parent, select it now.
1948             if ( parentIndex !== -1 ) {
1949               this.select(index);
1950             }
1951           }
1952         }
1953         else {
1954           this.collapse(indexes);
1955         }
1956       }
1957     }
1958 
1959     return true ;
1960   },
1961 
1962   /** @private
1963     Selects the next item if itemsPerRow > 1.  Otherwise does nothing.
1964   */
1965   moveRight: function(evt) {
1966     // If the control key is down, this may be a browser shortcut and
1967     // we should not handle the arrow key.
1968     if (evt.ctrlKey || evt.metaKey) return NO;
1969 
1970     if ((this.get('itemsPerRow') || 1) > 1) {
1971       this.selectNextItem(false, 1) ;
1972       this._cv_performSelectAction(null, evt, this.ACTION_DELAY);
1973     } else {
1974       var sel     = this.get('selection'),
1975           content = this.get('content'),
1976           indexes = sel ? sel.indexSetForSource(content) : null;
1977       if (indexes) this.expand(indexes);
1978     }
1979 
1980     return true ;
1981   },
1982 
1983   /** @private */
1984   moveDownAndModifySelection: function(sender, evt) {
1985     var content = this.get('content'),
1986         del = this.delegateFor('allowsMultipleSelection', this.get('delegate'), content);
1987 
1988     if (del && del.get('allowsMultipleSelection')) {
1989     this.selectNextItem(true, this.get('itemsPerRow') || 1) ;
1990     this._cv_performSelectAction(null, evt, this.ACTION_DELAY);
1991     }
1992     return true ;
1993   },
1994 
1995   /** @private */
1996   moveUpAndModifySelection: function(sender, evt) {
1997     var content = this.get('content'),
1998         del = this.delegateFor('allowsMultipleSelection', this.get('delegate'), content);
1999 
2000     if (del && del.get('allowsMultipleSelection')) {
2001     this.selectPreviousItem(true, this.get('itemsPerRow') || 1) ;
2002     this._cv_performSelectAction(null, evt, this.ACTION_DELAY);
2003     }
2004     return true ;
2005   },
2006 
2007   /** @private
2008     Selects the previous item if itemsPerRow > 1.  Otherwise does nothing.
2009   */
2010   moveLeftAndModifySelection: function(sender, evt) {
2011     if ((this.get('itemsPerRow') || 1) > 1) {
2012       this.selectPreviousItem(true, 1) ;
2013       this._cv_performSelectAction(null, evt, this.ACTION_DELAY);
2014     }
2015     return true ;
2016   },
2017 
2018   /** @private
2019     Selects the next item if itemsPerRow > 1.  Otherwise does nothing.
2020   */
2021   moveRightAndModifySelection: function(sender, evt) {
2022     if ((this.get('itemsPerRow') || 1) > 1) {
2023       this.selectNextItem(true, 1) ;
2024       this._cv_performSelectAction(null, evt, this.ACTION_DELAY);
2025     }
2026     return true ;
2027   },
2028 
2029   /** @private
2030     if content value is editable and we have one item selected, then edit.
2031     otherwise, invoke action.
2032   */
2033   insertNewline: function(sender, evt) {
2034     var wantsEdit = this.get('isEditable') && this.get('canEditContent'),
2035       canEdit = false,
2036         sel, content, set, idx, itemView;
2037 
2038     // Make sure we have a single item selected and the item view supports beginEditing
2039     if (wantsEdit) {
2040       sel     = this.get('selection') ;
2041       content = this.get('content');
2042 
2043       if (sel && sel.get('length') === 1) {
2044         set = sel.indexSetForSource(content);
2045         idx = set ? set.get('min') : -1;
2046 
2047     // next find itemView and ensure it supports editing
2048       itemView = this.itemViewForContentIndex(idx);
2049       canEdit = itemView && SC.typeOf(itemView.beginEditing)===SC.T_FUNCTION;
2050     }
2051     }
2052 
2053     // ok, we can edit..
2054     if (canEdit) {
2055       this.scrollToContentIndex(idx);
2056       itemView.beginEditing();
2057 
2058     // invoke action
2059     } else {
2060       this.invokeLater(this._cv_action, 0, itemView, null) ;
2061     }
2062 
2063     return YES ; // always handle
2064   },
2065 
2066   insertTab: function(evt) {
2067     var view = this.get('nextValidKeyView');
2068     if (view) view.becomeFirstResponder();
2069     else evt.allowDefault();
2070     return YES ; // handled
2071   },
2072 
2073   insertBacktab: function(evt) {
2074     var view = this.get('previousValidKeyView');
2075     if (view) view.becomeFirstResponder();
2076     else evt.allowDefault();
2077     return YES ; // handled
2078   },
2079 
2080   // ..........................................................
2081   // MOUSE EVENTS
2082   //
2083 
2084   doubleClick: function (ev) {
2085     var isEnabledInPane = this.get('isEnabledInPane'),
2086         handled = false;
2087 
2088     if (isEnabledInPane) {
2089       var action = this.get('action');
2090 
2091       if (action) {
2092         var itemView = this.itemViewForEvent(ev);
2093 
2094         this._cv_performSelectAction(itemView, ev, 0, ev.clickCount);
2095         handled = true;
2096       }
2097     }
2098 
2099     return handled;
2100   },
2101 
2102   /** @private
2103     Handles mouse down events on the collection view or on any of its
2104     children.
2105 
2106     The default implementation of this method can handle a wide variety
2107     of user behaviors depending on how you have configured the various
2108     options for the collection view.
2109 
2110     @param ev {Event} the mouse down event
2111     @returns {Boolean} Usually YES.
2112   */
2113   mouseDown: function(ev) {
2114     var isEnabledInPane = this.get('isEnabledInPane'),
2115         isSelectable = this.get('isSelectable'),
2116         handled = false;
2117 
2118     // If enabled and selectable, handle the event.
2119     if (isEnabledInPane && isSelectable) {
2120       var content = this.get('content');
2121 
2122       handled = true;
2123 
2124       if (content) {
2125         var itemView = this.itemViewForEvent(ev),
2126             allowsMultipleSel = content.get('allowsMultipleSelection'),
2127             didSelect = false,
2128             sel, isSelected,
2129             contentIndex;
2130 
2131         // Ensure that the view is first responder if possible.
2132         this.becomeFirstResponder();
2133 
2134         // Determine the content index of the item view.
2135         contentIndex = itemView ? itemView.get('contentIndex') : -1;
2136 
2137         // Toggle the selection if useToggleSelection is true.
2138         if (this.get('useToggleSelection')) {
2139 
2140           if (this.get('selectOnMouseDown') && itemView) {
2141             // Determine if item is selected. If so, then go on.
2142             sel = this.get('selection');
2143 
2144             isSelected = sel && sel.contains(content, contentIndex, 1);
2145             if (isSelected) {
2146               this.deselect(contentIndex);
2147             } else if (!allowsMultipleSel) {
2148               this.select(contentIndex, false);
2149               didSelect = true;
2150             } else {
2151               this.select(contentIndex, true);
2152               didSelect = true;
2153             }
2154           }
2155 
2156         // Normal selection behavior.
2157         } else {
2158           var info, anchor, modifierKeyPressed;
2159 
2160           // Received a mouseDown on the view, but not on one of the item views.
2161           if (!itemView) {
2162             // Deselect all.
2163             if (this.get('allowDeselectAll')) this.select(null, false);
2164 
2165           } else {
2166             // Collect some basic setup info.
2167             sel = this.get('selection');
2168             if (sel) sel = sel.indexSetForSource(content);
2169 
2170             info = this.mouseDownInfo = {
2171               event:        ev,
2172               itemView:     itemView,
2173               contentIndex: contentIndex,
2174               at:           Date.now()
2175             };
2176 
2177             isSelected = sel ? sel.contains(contentIndex) : NO;
2178             info.modifierKeyPressed = modifierKeyPressed = ev.ctrlKey || ev.metaKey;
2179 
2180 
2181             // holding down a modifier key while clicking a selected item should
2182             // deselect that item...deselect and bail.
2183             if (modifierKeyPressed && isSelected) {
2184               info.shouldDeselect = contentIndex >= 0;
2185 
2186             // if the shiftKey was pressed, then we want to extend the selection
2187             // from the last selected item
2188             } else if (ev.shiftKey && sel && sel.get('length') > 0 && allowsMultipleSel) {
2189               sel = this._findSelectionExtendedByShift(sel, contentIndex);
2190               anchor = this._selectionAnchor;
2191               this.select(sel) ;
2192               didSelect = true;
2193               this._selectionAnchor = anchor; //save the anchor
2194 
2195             // If no modifier key was pressed, then clicking on the selected item
2196             // should clear the selection and reselect only the clicked on item.
2197             } else if (!modifierKeyPressed && isSelected) {
2198               info.shouldReselect = contentIndex >= 0;
2199 
2200             // Otherwise, if selecting on mouse down,  simply select the clicked on
2201             // item, adding it to the current selection if a modifier key was pressed.
2202             } else {
2203 
2204               if ((ev.shiftKey || modifierKeyPressed) && !allowsMultipleSel) {
2205                 this.select(null, false);
2206                 didSelect = true;
2207               }
2208 
2209               if (this.get("selectOnMouseDown")) {
2210                 this.select(contentIndex, modifierKeyPressed);
2211                 didSelect = true;
2212               } else {
2213                 info.shouldSelect = contentIndex >= 0;
2214               }
2215             }
2216 
2217             // saved for extend by shift ops.
2218             info.previousContentIndex = contentIndex;
2219           }
2220         }
2221 
2222         // Trigger select action if select occurred.
2223         if (didSelect && this.get('actOnSelect')) {
2224           this._cv_performSelectAction(itemView, ev);
2225         }
2226       }
2227     }
2228 
2229     if (handled) {
2230       // Track that mouse is down.
2231       this._sc_isMouseDown = true;
2232     }
2233 
2234     return handled;
2235   },
2236 
2237   /** @private */
2238   mouseUp: function(ev) {
2239     var isEnabledInPane = this.get('isEnabledInPane'),
2240         isSelectable = this.get('isSelectable');
2241 
2242     // If enabled and selectable, handle the event.
2243     if (isEnabledInPane && isSelectable) {
2244       var content = this.get('content');
2245 
2246       if (content) {
2247         var itemView = this.itemViewForEvent(ev),
2248             info = this.mouseDownInfo,
2249             didSelect = false,
2250             sel, isSelected,
2251             contentIndex;
2252 
2253         // Determine the content index of the item view.
2254         contentIndex = itemView ? itemView.get('contentIndex') : -1;
2255 
2256         // Toggle the selection if useToggleSelection is true.
2257         if (this.get('useToggleSelection')) {
2258           // If the toggle wasn't done on mouse down, handle it now.
2259           if (!this.get('selectOnMouseDown') && itemView) {
2260             var allowsMultipleSel = content.get('allowsMultipleSelection');
2261 
2262             // determine if item is selected. If so, then go on.
2263             sel = this.get('selection') ;
2264             isSelected = sel && sel.contains(content, contentIndex, 1);
2265 
2266             if (isSelected) {
2267               this.deselect(contentIndex) ;
2268             } else if (!allowsMultipleSel) {
2269               this.select(contentIndex, false);
2270               didSelect = true;
2271             } else {
2272               this.select(contentIndex, true);
2273               didSelect = true;
2274             }
2275           }
2276 
2277         } else if (info) {
2278           var idx = info.contentIndex;
2279 
2280           // This will be set if the user simply clicked on an unselected item and selectOnMouseDown was NO.
2281           if (info.shouldSelect) {
2282             this.select(idx, info.modifierKeyPressed);
2283             didSelect = true;
2284           }
2285 
2286           // This is true if the user clicked on a selected item with a modifier key pressed.
2287           if (info.shouldDeselect) this.deselect(idx);
2288 
2289           // This is true if the user clicked on a selected item without a modifier-key pressed.
2290           // When this happens we try to begin editing on the content.  If that is not allowed, then
2291           // simply clear the selection and reselect the clicked on item.
2292           if (info.shouldReselect) {
2293 
2294             // - contentValueIsEditable is true
2295             var canEdit = this.get('isEditable') && this.get('canEditContent') ;
2296 
2297             // - the user clicked on an item that was already selected
2298             //   ^ this is the only way shouldReset is set to YES
2299 
2300             // - is the only item selected
2301             if (canEdit) {
2302               sel = this.get('selection') ;
2303               canEdit = sel && (sel.get('length') === 1);
2304             }
2305 
2306             // - the item view responds to contentHitTest() and returns YES.
2307             // - the item view responds to beginEditing and returns YES.
2308             if (canEdit) {
2309               itemView = this.itemViewForContentIndex(idx) ;
2310               canEdit = itemView && (!itemView.contentHitTest || itemView.contentHitTest(ev)) ;
2311               canEdit = (canEdit && itemView.beginEditing) ? itemView.beginEditing() : NO ;
2312             }
2313 
2314             // if cannot edit, schedule a reselect (but give doubleClick a chance)
2315             if (!canEdit) {
2316               if (this._cv_reselectTimer) this._cv_reselectTimer.invalidate() ;
2317               this._cv_reselectTimer = this.invokeLater(this.select, 300, idx, false) ;
2318             }
2319           }
2320 
2321           // Clean up.
2322           this._cleanupMouseDown();
2323         }
2324 
2325         // Trigger select action if select occurred.
2326         if (didSelect && this.get('actOnSelect')) {
2327           this._cv_performSelectAction(itemView, ev);
2328         }
2329       }
2330 
2331       // To avoid annoying jitter from Magic Mouse (which sends mousewheel events while trying
2332       // to lift your finger after a drag), capture mousewheel events for a small period of time.
2333       this._sc_isMouseJustDown = true;
2334       this._sc_clearMouseJustDownTimer = this.invokeLater(this._sc_clearMouseJustDown, 250);
2335     }
2336 
2337     // Track that mouse is up no matter what (e.g. mouse went down and then view was disabled before mouse up).
2338     this._sc_isMouseDown = false;
2339 
2340     return false;  // Bubble event to allow doubleClick to be called.
2341   },
2342 
2343   /** @private */
2344   _cleanupMouseDown: function() {
2345 
2346     // delete items explicitly to avoid leaks on IE
2347     var info = this.mouseDownInfo, key;
2348     if (info) {
2349       for (key in info) {
2350         if (!info.hasOwnProperty(key)) continue;
2351         delete info[key];
2352       }
2353     }
2354     this.mouseDownInfo = null;
2355   },
2356 
2357   /** @private */
2358   mouseMoved: function(ev) {
2359     var view = this.itemViewForEvent(ev),
2360         last = this._lastHoveredItem ;
2361 
2362     // handle hover events.
2363     if (view !== last) {
2364       if (last && last.mouseExited) last.mouseExited(ev);
2365       if (view && view.mouseEntered) view.mouseEntered(ev);
2366     }
2367     this._lastHoveredItem = view ;
2368 
2369     if (view && view.mouseMoved) view.mouseMoved(ev);
2370     return YES;
2371   },
2372 
2373   /** @private */
2374   mouseExited: function(ev) {
2375     var view = this._lastHoveredItem ;
2376     this._lastHoveredItem = null ;
2377     if (view && view.mouseExited) view.mouseExited(ev) ;
2378     return YES ;
2379   },
2380 
2381   /** @private We capture mouseWheel events while the mouse is pressed, this is to prevent jitter from slight mouse wheels while pressing
2382     and lifting the finger (especially a problem with the Magic Mouse) */
2383   mouseWheel: function (evt) {
2384     // Capture mouse wheel events when mouse is pressed or immediately after a mouse up (to avoid
2385     // excessive Magic Mouse wheel events while the person lifts their finger).
2386     return this._sc_isMouseDown || this._sc_isMouseJustDown;
2387   },
2388 
2389   // ..........................................................
2390   // TOUCH EVENTS
2391   //
2392 
2393   /** @private */
2394   touchStart: function(touch, evt) {
2395     var itemView = this.itemViewForEvent(touch),
2396         contentIndex = itemView ? itemView.get('contentIndex') : -1;
2397 
2398     if (!this.get('isEnabledInPane')) return contentIndex > -1;
2399 
2400     // become first responder if possible.
2401     this.becomeFirstResponder() ;
2402 
2403     this._touchSelectedView = itemView;
2404 
2405     if (!this.get('useToggleSelection')) {
2406       // We're faking the selection visually here
2407       // Only track this if we added a selection so we can remove it later
2408       if (itemView && !itemView.get('isSelected')) {
2409         itemView.set('isSelected', YES);
2410       }
2411     }
2412 
2413     return YES;
2414   },
2415 
2416   /** @private */
2417   touchesDragged: function(evt, touches) {
2418     touches.forEach(function(touch){
2419       if (
2420         Math.abs(touch.pageX - touch.startX) > 5 ||
2421         Math.abs(touch.pageY - touch.startY) > 5
2422       ) {
2423         // This calls touchCancelled
2424         touch.makeTouchResponder(touch.nextTouchResponder);
2425       }
2426     }, this);
2427 
2428   },
2429 
2430   /** @private */
2431   touchEnd: function(touch) {
2432     /*
2433       TODO [CC] We should be using itemViewForEvent here, but because
2434             ListItemView re-renders itself once isSelected is called
2435             in touchStart, the elements attached to this event are
2436             getting orphaned and this event is basically a complete
2437             fail when using touch events.
2438     */
2439     // var itemView = this.itemViewForEvent(touch),
2440     var content = this.get('content'),
2441         itemView = this._touchSelectedView,
2442         contentIndex = itemView ? itemView.get('contentIndex') : -1,
2443         isSelected = NO, sel, shouldSelect;
2444 
2445     if (!this.get('isEnabledInPane')) return contentIndex > -1;
2446 
2447     if (contentIndex > -1) {
2448       if (this.get('useToggleSelection')) {
2449         sel = this.get('selection');
2450         isSelected = sel && sel.contains(content, contentIndex, 1);
2451         shouldSelect = !isSelected;
2452       }
2453       else
2454         shouldSelect = true;
2455 
2456       if (shouldSelect) {
2457         this.select(contentIndex, NO);
2458 
2459         // If actOnSelect is implemented, the action will be fired.
2460         this._cv_performSelectAction(itemView, touch, 0);
2461       } else {
2462         this.deselect(contentIndex);
2463       }
2464     }
2465 
2466     this._touchSelectedView = null;
2467   },
2468 
2469   /** @private */
2470   touchCancelled: function(evt) {
2471     // Remove fake selection
2472     if (this._touchSelectedView) {
2473       this._touchSelectedView.set('isSelected', NO);
2474       this._touchSelectedView = null;
2475     }
2476   },
2477 
2478   /** @private */
2479   _findSelectionExtendedByShift: function(sel, contentIndex) {
2480 
2481     // fast path.  if we don't have a selection, just select index
2482     if (!sel || sel.get('length')===0) {
2483       return SC.IndexSet.create(contentIndex);
2484     }
2485 
2486     // if we do have a selection, then figure out how to extend it.
2487     var min     = sel.get('min'),
2488         max     = sel.get('max')-1,
2489         anchor  = this._selectionAnchor ;
2490     if (SC.none(anchor)) anchor = -1;
2491 
2492     // clicked before the current selection set... extend it's beginning...
2493     if (contentIndex < min) {
2494       min = contentIndex;
2495       if (anchor<0) this._selectionAnchor = anchor = max; //anchor at end
2496 
2497     // clicked after the current selection set... extend it's ending...
2498     } else if (contentIndex > max) {
2499       max = contentIndex;
2500       if (anchor<0) this._selectionAnchor = anchor = min; // anchor at start
2501 
2502     // clicked inside the selection set... need to determine where the last
2503     // selection was and use that as an anchor.
2504     } else if (contentIndex >= min && contentIndex <= max) {
2505       if (anchor<0) this._selectionAnchor = anchor = min; //anchor at start
2506 
2507       if (contentIndex === anchor) min = max = contentIndex ;
2508       else if (contentIndex > anchor) {
2509         min = anchor;
2510         max = contentIndex ;
2511       } else if (contentIndex < anchor) {
2512         min = contentIndex;
2513         max = anchor ;
2514       }
2515     }
2516 
2517     return SC.IndexSet.create(min, max - min + 1);
2518   },
2519 
2520   // ......................................
2521   // DRAG AND DROP SUPPORT
2522   //
2523 
2524   /**
2525     When reordering its content, the collection view will store its reorder
2526     data using this special data type.  The data type is unique to each
2527     collection view instance.  You can use this data type to detect reorders
2528     if necessary.
2529 
2530     @field
2531     @type String
2532   */
2533   reorderDataType: function() {
2534     return 'SC.CollectionView.Reorder.'+SC.guidFor(this) ;
2535   }.property().cacheable(),
2536 
2537   /**
2538     This property is set to the IndexSet of content objects that are the
2539     subject of a drag whenever a drag is initiated on the collection view.
2540     You can consult this property when implementing your collection view
2541     delegate  methods, but otherwise you should not use this property in your
2542     code.
2543 
2544     @type SC.IndexSet
2545     @default null
2546   */
2547   dragContent: null,
2548 
2549   /**
2550     This property is set to the proposed insertion index during a call to
2551     collectionViewValidateDragOperation().  Your delegate implementations can
2552     change the value of this property to enforce a drop some in some other
2553     location.
2554 
2555     @type Number
2556     @default null
2557   */
2558   proposedInsertionIndex: null,
2559 
2560   /**
2561     This property is set to the proposed drop operation during a call to
2562     collectionViewValidateDragOperation().  Your delegate implementations can
2563     change the value of this property to enforce a different type of drop
2564     operation.
2565 
2566     @type Number
2567     @default null
2568   */
2569   proposedDropOperation: null,
2570 
2571   /** @private
2572     mouseDragged event handler.  Initiates a drag if the following conditions
2573     are met:
2574 
2575     - collectionViewShouldBeginDrag() returns YES *OR*
2576     - the above method is not implemented and canReorderContent is true.
2577     - the dragDataTypes property returns a non-empty array
2578     - a mouse down event was saved by the mouseDown method.
2579   */
2580   mouseDragged: function (evt) {
2581     var del     = this.get('selectionDelegate'),
2582         content = this.get('content'),
2583         sel     = this.get('selection'),
2584         info    = this.mouseDownInfo,
2585         groupIndexes = this.get('_contentGroupIndexes'),
2586         dragContent, dragDataTypes, dragView;
2587 
2588     // if the mouse down event was cleared, there is nothing to do; return.
2589     if (!info || info.contentIndex<0) return YES ;
2590 
2591     // Don't do anything unless the user has been dragging for 123msec
2592     if ((Date.now() - info.at) < 123) return YES ;
2593 
2594     // OK, they must be serious, decide if a drag will be allowed.
2595     if (this.get('isEditable') && del.collectionViewShouldBeginDrag(this)) {
2596 
2597       // First, get the selection to drag.  Drag an array of selected
2598       // items appearing in this collection, in the order of the
2599       // collection.
2600       //
2601       // Compute the dragContent - the indexes we will be dragging.
2602       // if we don't select on mouse down, then the selection has not been
2603       // updated to whatever the user clicked.  Instead use
2604       // mouse down content.
2605       if (!this.get("selectOnMouseDown")) {
2606         dragContent = SC.IndexSet.create(info.contentIndex);
2607       } else dragContent = sel ? sel.indexSetForSource(content) : null;
2608 
2609       // remove any group indexes.  groups cannot be dragged.
2610       if (dragContent && groupIndexes && groupIndexes.get('length')>0) {
2611         dragContent = dragContent.copy().remove(groupIndexes);
2612         if (dragContent.get('length')===0) dragContent = null;
2613         else dragContent.freeze();
2614       }
2615 
2616       if (!dragContent) return YES; // nothing to drag
2617       else dragContent = dragContent.frozenCopy(); // so it doesn't change
2618 
2619       dragContent = { content: content, indexes: dragContent };
2620       this.set('dragContent', dragContent) ;
2621 
2622       // Get the set of data types supported by the delegate.  If this returns
2623       // a null or empty array and reordering content is not also supported
2624       // then do not start the drag.
2625       dragDataTypes = this.get('dragDataTypes');
2626       if (dragDataTypes && dragDataTypes.get('length') > 0) {
2627 
2628         // Build the drag view to use for the ghost drag.  This
2629         // should essentially contain any visible drag items.
2630         dragView = del.collectionViewDragViewFor(this, dragContent.indexes);
2631         if (!dragView) dragView = this._cv_dragViewFor(dragContent.indexes);
2632 
2633         // Initiate the drag
2634         SC.Drag.start({
2635           event: info.event,
2636           source: this,
2637           dragView: dragView,
2638           ghost: NO,
2639           ghostActsLikeCursor: del.ghostActsLikeCursor,
2640           slideBack: YES,
2641           dataSource: this
2642         });
2643 
2644         // Also use this opportunity to clean up since mouseUp won't
2645         // get called.
2646         this._cleanupMouseDown() ;
2647         this._lastInsertionIndex = null ;
2648 
2649       // Drag was not allowed by the delegate, so bail.
2650       } else this.set('dragContent', null) ;
2651 
2652       return YES ;
2653     }
2654   },
2655 
2656   /** @private
2657     Compute a default drag view by grabbing the raw layers and inserting them
2658     into a drag view.
2659   */
2660   _cv_dragViewFor: function(dragContent) {
2661     // find only the indexes that are in both dragContent and nowShowing.
2662     var indexes = this.get('nowShowing').without(dragContent),
2663         dragLayer = this.get('layer').cloneNode(false),
2664         view = SC.View.create({ layer: dragLayer, parentView: this }),
2665         height=0, layout;
2666 
2667     indexes = this.get('nowShowing').without(indexes);
2668 
2669     // cleanup weird stuff that might make the drag look out of place
2670     SC.$(dragLayer).css('backgroundColor', 'transparent')
2671       .css('border', 'none')
2672       .css('top', 0).css('left', 0);
2673 
2674     indexes.forEach(function(i) {
2675       var itemView = this.itemViewForContentIndex(i),
2676           isSelected, itemViewLayer, layer;
2677 
2678       if (itemView) {
2679       // render item view without isSelected state.
2680         isSelected = itemView.get('isSelected');
2681         itemView.set('isSelected', NO);
2682         itemView.updateLayerIfNeeded();
2683         itemViewLayer = itemView.get('layer');
2684 
2685         if (itemViewLayer) {
2686           layer = itemViewLayer.cloneNode(true);
2687 
2688           // photocopy any canvas elements
2689           var itemViewCanvasses = itemView.$().find('canvas');
2690           if (itemViewCanvasses) {
2691             var layerCanvasses = $(layer).find('canvas'),
2692                 len = itemViewCanvasses.length,
2693                 itemViewCanvas, layerCanvas;
2694             for (i = 0; i < len; i++) {
2695               itemViewCanvas = itemViewCanvasses[i];
2696               layerCanvas = layerCanvasses[i];
2697               layerCanvas.height = itemViewCanvas.height;
2698               layerCanvas.width = itemViewCanvas.width;
2699               layerCanvas.getContext('2d').drawImage(itemViewCanvas, 0, 0);
2700             }
2701           }
2702         }
2703 
2704         // reset item view
2705         itemView.set('isSelected', isSelected);
2706         itemView.updateLayerIfNeeded();
2707       }
2708 
2709       if (layer) {
2710         dragLayer.appendChild(layer);
2711         layout = itemView.get('layout');
2712         if(layout.height+layout.top>height){
2713           height = layout.height+layout.top;
2714         }
2715       }
2716 
2717       layer = null;
2718     }, this);
2719 
2720     // we don't want to show the scrollbars, resize the dragview
2721     view.set('layout', {height:height});
2722 
2723     dragLayer = null;
2724     return view ;
2725   },
2726 
2727 
2728   /**
2729     Implements the drag data source protocol for the collection view.  This
2730     property will consult the collection view delegate if one is provided. It
2731     will also do the right thing if you have set canReorderContent to YES.
2732 
2733     @field
2734     @type Array
2735   */
2736   dragDataTypes: function() {
2737     // consult delegate.
2738     var del = this.get('selectionDelegate'),
2739         ret = del.collectionViewDragDataTypes(this),
2740         key ;
2741 
2742     if (this.get('canReorderContent')) {
2743       ret = ret ? ret.copy() : [];
2744       key = this.get('reorderDataType');
2745       if (ret.indexOf(key) < 0) ret.push(key);
2746     }
2747 
2748     return ret ? ret : [];
2749   }.property(),
2750 
2751   /**
2752     Implements the drag data source protocol method. The implementation of
2753     this method will consult the collection view delegate if one has been
2754     provided.  It also respects the canReorderContent method.
2755   */
2756   dragDataForType: function(drag, dataType) {
2757 
2758     // if this is a reorder, then return drag content.
2759     if (this.get('canReorderContent')) {
2760       if (dataType === this.get('reorderDataType')) {
2761         return this.get('dragContent') ;
2762       }
2763     }
2764 
2765     // otherwise, just pass along to the delegate
2766     var del = this.get('selectionDelegate');
2767     return del.collectionViewDragDataForType(this, drag, dataType);
2768   },
2769 
2770   /**
2771     Implements the SC.DropTargetProtocol interface.  The default implementation will
2772     consult the collection view delegate, if you implement those methods.
2773 
2774     This method is called once when the drag enters the view area.  It's
2775     return value will be stored on the drag object as allowedDragOperations,
2776     possibly further constrained by the drag source.
2777 
2778     @param {SC.Drag} drag the drag object
2779     @param {SC.Event} evt the event triggering this change, if available
2780     @returns {Number} logical OR'd mask of allowed drag operations.
2781   */
2782   computeDragOperations: function(drag, evt) {
2783     // the proposed drag operation is DRAG_REORDER only if we can reorder
2784     // content and the drag contains reorder content.
2785     var op  = SC.DRAG_NONE,
2786         del = this.get('selectionDelegate');
2787 
2788     if (this.get('canReorderContent')) {
2789       if (drag.get('dataTypes').indexOf(this.get('reorderDataType')) >= 0) {
2790         op = SC.DRAG_REORDER ;
2791       }
2792     }
2793 
2794     // Now pass this onto the delegate.
2795     op = del.collectionViewComputeDragOperations(this, drag, op);
2796     if (op & SC.DRAG_REORDER) op = SC.DRAG_MOVE ;
2797 
2798     return op ;
2799   },
2800 
2801   /** @private
2802     Determines the allowed drop operation insertion point, operation type,
2803     and the drag operation to be performed.  Used by dragUpdated() and
2804     performDragOperation().
2805 
2806     @param {SC.Drag} drag the drag object
2807     @param {SC.Event} evt source of this request, if available
2808     @param {Number} dragOp allowed drag operation mask
2809     Returns three params: [drop index, drop operation, allowed drag ops]
2810   */
2811   _computeDropOperationState: function(drag, evt, dragOp) {
2812     // get the insertion index for this location.  This can be computed
2813     // by a subclass using whatever method.  This method is not expected to
2814     // do any data validation, just to map the location to an insertion
2815     // index.
2816     var loc    = this.convertFrameFromView(drag.get('location'), null),
2817         dropOp = SC.DROP_BEFORE,
2818         del    = this.get('selectionDelegate'),
2819         canReorder = this.get('canReorderContent'),
2820         objects, content, isPreviousInDrag, isNextInDrag, len, tmp;
2821 
2822     // STEP 1: Try with a DROP_ON option -- send straight to delegate if
2823     // supported by view.
2824 
2825     // get the computed insertion index and possibly drop operation.
2826     // prefer to drop ON.
2827     var idx = this.insertionIndexForLocation(loc, SC.DROP_ON) ;
2828     if (SC.typeOf(idx) === SC.T_ARRAY) {
2829       dropOp = idx[1] ; // order matters here
2830       idx = idx[0] ;
2831     }
2832 
2833     // if the return drop operation is DROP_ON, then just check it with the
2834     // delegate method.  If the delegate method does not support dropping on,
2835     // then it will return DRAG_NONE, in which case we will try again with
2836     // drop before.
2837     if (dropOp === SC.DROP_ON) {
2838 
2839       // Now save the insertion index and the dropOp.  This may be changed by
2840       // the collection delegate.
2841       this.set('proposedInsertionIndex', idx) ;
2842       this.set('proposedDropOperation', dropOp) ;
2843       tmp = del.collectionViewValidateDragOperation(this, drag, dragOp, idx, dropOp) ;
2844       idx = this.get('proposedInsertionIndex') ;
2845       dropOp = this.get('proposedDropOperation') ;
2846       this._dropInsertionIndex = this._dropOperation = null ;
2847 
2848       // The delegate is OK with a drop on also, so just return.
2849       if (tmp !== SC.DRAG_NONE) return [idx, dropOp, tmp] ;
2850 
2851       // The delegate is NOT OK with a drop on, try to get the insertion
2852       // index again, but this time prefer SC.DROP_BEFORE, then let the
2853       // rest of the method run...
2854       else {
2855         dropOp = SC.DROP_BEFORE ;
2856         idx = this.insertionIndexForLocation(loc, SC.DROP_BEFORE) ;
2857         if (SC.typeOf(idx) === SC.T_ARRAY) {
2858           dropOp = idx[1] ; // order matters here
2859           idx = idx[0] ;
2860         }
2861       }
2862     }
2863 
2864     // if this is a reorder drag, set the proposed op to SC.DRAG_REORDER and
2865     // validate the insertion point.  This only works if the insertion point
2866     // is DROP_BEFORE or DROP_AFTER.  DROP_ON is not handled by reordering
2867     // content.
2868     if ((idx >= 0) && canReorder && (dropOp !== SC.DROP_ON)) {
2869 
2870       objects = drag.dataForType(this.get('reorderDataType')) ;
2871       if (objects) {
2872         content = this.get('content') ;
2873 
2874         // if the insertion index is in between two items in the drag itself,
2875         // then this is not allowed.  Either use the last insertion index or
2876         // find the first index that is not in between selections.  Stop when
2877         // we get to the beginning.
2878         if (dropOp === SC.DROP_BEFORE) {
2879           isPreviousInDrag = objects.indexes.contains(idx-1);
2880           isNextInDrag     = objects.indexes.contains(idx);
2881         } else {
2882           isPreviousInDrag = objects.indexes.contains(idx);
2883           isNextInDrag     = objects.indexes.contains(idx-1);
2884         }
2885 
2886         if (isPreviousInDrag && isNextInDrag) {
2887           if (SC.none(this._lastInsertionIndex)) {
2888             if (dropOp === SC.DROP_BEFORE) {
2889               while ((idx >= 0) && objects.indexes.contains(idx)) idx--;
2890             } else {
2891               len = content ? content.get('length') : 0;
2892               while ((idx < len) && objects.indexes.contains(idx)) idx++;
2893             }
2894           } else idx = this._lastInsertionIndex ;
2895         }
2896 
2897         // If we found a valid insertion point to reorder at, then set the op
2898         // to custom DRAG_REORDER.
2899         if (idx >= 0) dragOp = SC.DRAG_REORDER ;
2900       }
2901     }
2902 
2903     // Now save the insertion index and the dropOp.  This may be changed by
2904     // the collection delegate.
2905     this.set('proposedInsertionIndex', idx) ;
2906     this.set('proposedDropOperation', dropOp) ;
2907     dragOp = del.collectionViewValidateDragOperation(this, drag, dragOp, idx, dropOp) ;
2908     idx = this.get('proposedInsertionIndex') ;
2909     dropOp = this.get('proposedDropOperation') ;
2910     this._dropInsertionIndex = this._dropOperation = null ;
2911 
2912     // return generated state
2913     return [idx, dropOp, dragOp] ;
2914   },
2915 
2916   /**
2917     Implements the SC.DropTargetProtocol interface.  The default implementation will
2918     determine the drop location and then consult the collection view delegate
2919     if you implement those methods.  Otherwise it will handle reordering
2920     content on its own.
2921 
2922     @param {SC.Drag} drag The drag that was updated
2923     @param {SC.Event} evt The event for the drag
2924   */
2925   dragUpdated: function(drag, evt) {
2926     var op     = drag.get('allowedDragOperations'),
2927         state  = this._computeDropOperationState(drag, evt, op),
2928         idx    = state[0], dropOp = state[1], dragOp = state[2];
2929 
2930     // if the insertion index or dropOp have changed, update the insertion
2931     // point
2932     if (dragOp !== SC.DRAG_NONE) {
2933       if ((this._lastInsertionIndex !== idx) || (this._lastDropOperation !== dropOp)) {
2934         var itemView = this.itemViewForContentIndex(idx) ;
2935         this.showInsertionPoint(itemView, dropOp) ;
2936       }
2937 
2938       this._lastInsertionIndex = idx ;
2939       this._lastDropOperation = dropOp ;
2940     } else {
2941       this.hideInsertionPoint() ;
2942       this._lastInsertionIndex = this._lastDropOperation = null ;
2943     }
2944 
2945     // Normalize drag operation to the standard kinds accepted by the drag
2946     // system.
2947     return (dragOp & SC.DRAG_REORDER) ? SC.DRAG_MOVE : dragOp;
2948   },
2949 
2950   /**
2951     Implements the SC.DropTargetProtocol protocol.  Hides any visible insertion
2952     point and clears some cached values.
2953   */
2954   dragEnded: function () {
2955     this.hideInsertionPoint() ;
2956     this._lastInsertionIndex = this._lastDropOperation = null ;
2957   },
2958 
2959   /**
2960     Implements the SC.DropTargetProtocol protocol.
2961 
2962     @returns {Boolean} YES
2963   */
2964   acceptDragOperation: function(drag, op) {
2965     return YES;
2966   },
2967 
2968   /**
2969     Implements the SC.DropTargetProtocol protocol. Consults the collection view
2970     delegate to actually perform the operation unless the operation is
2971     reordering content.
2972 
2973     @param {SC.Drag} drag The drag to perform the operation on
2974     @param {Number} op The drag operation to perform
2975     @return {Number} The operation performed
2976   */
2977   performDragOperation: function(drag, op) {
2978     // Get the correct insertion point, drop operation, etc.
2979     var state = this._computeDropOperationState(drag, null, op),
2980         idx   = state[0], dropOp = state[1], dragOp = state[2],
2981         del   = this.get('selectionDelegate'),
2982         performed, objects, data, content, shift, indexes;
2983 
2984     // The dragOp is the kinds of ops allowed.  The drag operation must
2985     // be included in that set.
2986     if (dragOp & SC.DRAG_REORDER) {
2987       op = (op & SC.DRAG_MOVE) ? SC.DRAG_REORDER : SC.DRAG_NONE ;
2988     } else op = op & dragOp ;
2989 
2990     // If no allowed drag operation could be found, just return.
2991     if (op === SC.DRAG_NONE) return op;
2992 
2993     // Some operation is allowed through, give the delegate a chance to
2994     // handle it.
2995     performed = del.collectionViewPerformDragOperation(this, drag, op, idx, dropOp) ;
2996 
2997     // If the delegate did not handle the drag (i.e. returned SC.DRAG_NONE),
2998     // and the op type is REORDER, then do the reorder here.
2999     if ((performed === SC.DRAG_NONE) && (op & SC.DRAG_REORDER)) {
3000 
3001       data = drag.dataForType(this.get('reorderDataType')) ;
3002       if (!data) return SC.DRAG_NONE ;
3003 
3004       content = this.get('content') ;
3005 
3006       // check for special case - inserting BEFORE ourself...
3007       // in this case just pretend the move happened since it's a no-op
3008       // anyway
3009       indexes = data.indexes;
3010       if (indexes.get('length')===1) {
3011         if (((dropOp === SC.DROP_BEFORE) || (dropOp === SC.DROP_AFTER)) &&
3012             (indexes.get('min')===idx)) return SC.DRAG_MOVE;
3013       }
3014 
3015       content.beginPropertyChanges(); // suspend notifications
3016 
3017       // get each object, then remove it from the content. they will be
3018       // added again later.
3019       objects = [];
3020       shift = 0;
3021       data.indexes.forEach(function(i) {
3022         objects.push(content.objectAt(i - shift));
3023         content.removeAt(i-shift);
3024         shift++;
3025         if (i < idx) idx--;
3026       }, this);
3027 
3028       // now insert objects into new insertion location
3029       if (dropOp === SC.DROP_AFTER) idx++;
3030       content.replace(idx, 0, objects, dropOp);
3031       this.select(SC.IndexSet.create(idx, objects.length));
3032       content.endPropertyChanges(); // restart notifications
3033 
3034       // make the op into its actual value
3035       op = SC.DRAG_MOVE ;
3036     }
3037 
3038     return op;
3039   },
3040 
3041   /**
3042     Default delegate method implementation, returns YES if canReorderContent
3043     is also true.
3044 
3045     @param {SC.View} view
3046   */
3047   collectionViewShouldBeginDrag: function(view) {
3048     return this.get('canReorderContent');
3049   },
3050 
3051 
3052   // ..........................................................
3053   // INSERTION POINT
3054   //
3055 
3056 
3057   /**
3058     Get the preferred insertion point for the given location, including
3059     an insertion preference of before, after or on the named index.
3060 
3061     You can implement this method in a subclass if you like to perform a
3062     more efficient check.  The default implementation will loop through the
3063     item views looking for the first view to "switch sides" in the orientation
3064     you specify.
3065 
3066     This method should return an array with two values.  The first value is
3067     the insertion point index and the second value is the drop operation,
3068     which should be one of SC.DROP_BEFORE, SC.DROP_AFTER, or SC.DROP_ON.
3069 
3070     The preferred drop operation passed in should be used as a hint as to
3071     the type of operation the view would prefer to receive. If the
3072     dropOperation is SC.DROP_ON, then you should return a DROP_ON mode if
3073     possible.  Otherwise, you should never return DROP_ON.
3074 
3075     For compatibility, you can also return just the insertion index.  If you
3076     do this, then the collection view will assume the drop operation is
3077     SC.DROP_BEFORE.
3078 
3079     If an insertion is NOT allowed, you should return -1 as the insertion
3080     point.  In this case, the drop operation will be ignored.
3081 
3082     @param {Point} loc the mouse location.
3083     @param {DropOp} dropOperation the preferred drop operation.
3084     @returns {Array} format: [index, op]
3085   */
3086   insertionIndexForLocation: function(loc, dropOperation) {
3087     return -1;
3088   },
3089 
3090   // ..........................................................
3091   // INTERNAL SUPPORT
3092   //
3093 
3094   /** @private Clears the mouse just down flag. */
3095   _sc_clearMouseJustDown: function () {
3096     this._sc_isMouseJustDown = false;
3097   },
3098 
3099   /** @private - when we are about to become visible, reload if needed. */
3100   willShowInDocument: function () {
3101     if (this._invalidIndexes) this.invokeOnce(this.reloadIfNeeded);
3102     if (this._invalidSelection) {
3103       this.invokeOnce(this.reloadSelectionIndexesIfNeeded);
3104     }
3105   },
3106 
3107   /** @private - when we are added, reload if needed. */
3108   didAppendToDocument: function () {
3109     if (this._invalidIndexes) this.invokeOnce(this.reloadIfNeeded);
3110     if (this._invalidSelection) {
3111       this.invokeOnce(this.reloadSelectionIndexesIfNeeded);
3112     }
3113   },
3114 
3115   /**
3116     Default delegate method implementation, returns YES if isSelectable
3117     is also true.
3118   */
3119   collectionViewShouldSelectItem: function(view, item) {
3120     return this.get('isSelectable') ;
3121   },
3122 
3123   /** @private
3124 
3125     Whenever the nowShowing range changes, update the range observer on the
3126     content item and instruct the view to reload any indexes that are not in
3127     the previous nowShowing range.
3128 
3129   */
3130   _cv_nowShowingDidChange: function() {
3131     var nowShowing  = this.get('nowShowing'),
3132         last        = this._sccv_lastNowShowing,
3133         diff, diff1, diff2;
3134 
3135     // find the differences between the two
3136     // NOTE: reuse a TMP IndexSet object to avoid creating lots of objects
3137     // during scrolling
3138     if (last !== nowShowing) {
3139       if (last && nowShowing) {
3140         diff1 = this._TMP_DIFF1.add(last).remove(nowShowing);
3141         diff2 = this._TMP_DIFF2.add(nowShowing).remove(last);
3142         diff = diff1.add(diff2);
3143       } else diff = last || nowShowing ;
3144     }
3145 
3146     // if nowShowing has actually changed, then update
3147     if (diff && diff.get('length') > 0) {
3148       this._sccv_lastNowShowing = nowShowing ? nowShowing.frozenCopy() : null;
3149       this.updateContentRangeObserver();
3150       this.reload(diff);
3151     }
3152 
3153     // cleanup tmp objects
3154     if (diff1) diff1.clear();
3155     if (diff2) diff2.clear();
3156 
3157   }.observes('nowShowing'),
3158 
3159   /** @private */
3160   init: function() {
3161      sc_super();
3162 
3163     //@if (debug)
3164     if (this.useFastPath) {
3165       // Deprecation warning for those that were using SC.CollectionFastPath.
3166       SC.warn("Developer Warning: SC.CollectionView `useFastPath` has been deprecated.  The performance improvements have been integrated directly into SC.CollectionView as the default behavior.  Please disable the useFastPath property and refer to the SC.CollectionView documentation for more information.");
3167     }
3168     //@endif
3169 
3170     //@if (debug)
3171     if (this.willReload || this.didReload) {
3172       // Deprecation warning for willReload and didReload.  These don't seem to serve any purpose.
3173       SC.warn("Developer Warning: SC.CollectionView no longer calls willReload and didReload on its subclasses because it includes item view and layer pooling in itself by default.");
3174     }
3175     //@endif
3176 
3177      if (this.get('canReorderContent')) this._cv_canReorderContentDidChange();
3178      this._sccv_lastNowShowing = this.get('nowShowing').clone();
3179 
3180      if (this.content) this._cv_contentDidChange();
3181      if (this.selection) this._cv_selectionDidChange();
3182 
3183      // Set our initial layout. It's important that our computed layout exist on instantiation so that containing views
3184      // understand in which way the collection will grow (e.g. if we compute height, then the container won't adjust height).
3185      this.adjustLayout();
3186   },
3187 
3188   /** @private SC.View.prototype.destroy. */
3189   destroy: function () {
3190     sc_super();
3191 
3192     // All manipulations made to objects we use must be reversed!
3193     var content = this._content;
3194     if (content) {
3195       content.removeObserver('length', this, this.contentLengthDidChange);
3196 
3197       this._content = null;
3198     }
3199 
3200     var sel = this._cv_selection;
3201     if (sel) {
3202       sel.removeObserver('[]', this, this._cv_selectionContentDidChange);
3203 
3204       this._cv_selection = null;
3205     }
3206 
3207     var contentRangeObserver = this._cv_contentRangeObserver;
3208     if (contentRangeObserver) {
3209       if (content) content.removeRangeObserver(contentRangeObserver);
3210 
3211       this._cv_contentRangeObserver = null;
3212     }
3213   },
3214 
3215   /** @private
3216     Become a drop target whenever reordering content is enabled.
3217   */
3218   _cv_canReorderContentDidChange: function() {
3219     if (this.get('canReorderContent')) {
3220       if (!this.get('isDropTarget')) this.set('isDropTarget', YES);
3221       SC.Drag.addDropTarget(this);
3222     }
3223   }.observes('canReorderContent'),
3224 
3225   /** @private
3226     Fires an action after a selection if enabled.
3227 
3228     if actOnSelect is YES, then try to invoke the action, passing the
3229     current selection (saved as a separate array so that a change in sel
3230     in the meantime will not be lost)
3231   */
3232   _cv_performSelectAction: function(view, ev, delay, clickCount) {
3233     var sel;
3234     if (delay === undefined) delay = 0 ;
3235     if (clickCount === undefined) clickCount = 1;
3236     if ((clickCount>1) || this.get('actOnSelect')) {
3237       if (this._cv_reselectTimer) this._cv_reselectTimer.invalidate() ;
3238       sel = this.get('selection');
3239       sel = sel ? sel.toArray() : [];
3240       if (this._cv_actionTimer) this._cv_actionTimer.invalidate();
3241       this._cv_actionTimer = this.invokeLater(this._cv_action, delay, view, ev, sel) ;
3242     }
3243   },
3244 
3245   /** @private
3246     Perform the action.  Supports legacy behavior as well as newer style
3247     action dispatch.
3248   */
3249   _cv_action: function(view, evt, context) {
3250     var action = this.get('action');
3251 
3252     this._cv_actionTimer = null;
3253     if (action) {
3254 
3255       // Legacy support for action functions.
3256       if (action && (SC.typeOf(action) === SC.T_FUNCTION)) {
3257         return this.action(view, evt);
3258 
3259       // Use SC.ActionSupport.
3260       } else {
3261         return this.fireAction(context);
3262       }
3263 
3264     // if no action is specified, then trigger the support action,
3265     // if supported.
3266     } else if (!view) {
3267       return ; // nothing to do
3268 
3269     // if the target view has its own internal action handler,
3270     // trigger that.
3271     } else if (SC.typeOf(view._action) === SC.T_FUNCTION) {
3272       return view._action(evt) ;
3273 
3274     // otherwise call the action method to support older styles.
3275     } else if (SC.typeOf(view.action) === SC.T_FUNCTION) {
3276       return view.action(evt) ;
3277     }
3278   },
3279 
3280   /** @private */
3281   _attrsForContentIndex: function (idx) {
3282     var attrs = this._TMP_ATTRS, // NOTE: This is a shared object so every property of it must be set for each use.
3283       del = this.get('contentDelegate'),
3284       items = this.get('content'),
3285       isGroupView = this._contentIndexIsGroup(idx),
3286       isEditable = this.get('isEditable') && this.get('canEditContent'),
3287       isReorderable = this.get('isEditable') && this.get('canReorderContent'),
3288       isDeletable = this.get('isEditable') && this.get('canDeleteContent'),
3289       isEnabled = del.contentIndexIsEnabled(this, items, idx),
3290       isSelected = del.contentIndexIsSelected(this, items, idx),
3291       outlineLevel = del.contentIndexOutlineLevel(this, items, idx),
3292       disclosureState = del.contentIndexDisclosureState(this, items, idx);
3293 
3294     attrs.contentIndex = idx;
3295     attrs.content = items.objectAt(idx);
3296     attrs.disclosureState = disclosureState;
3297     attrs.isEnabled = isEnabled;
3298     attrs.isEditable = isEditable;
3299     attrs.isReorderable = isReorderable;
3300     attrs.isDeletable = isDeletable;
3301     attrs.isSelected = isSelected;
3302     attrs.isGroupView = isGroupView;
3303     attrs.layerId = this.layerIdFor(idx);
3304     attrs.owner = attrs.displayDelegate = this;
3305     attrs.page = this.page;
3306     attrs.outlineLevel = outlineLevel;
3307     attrs.isLast = idx === items.get('length') - 1;
3308 
3309     if (isGroupView) attrs.classNames = this._GROUP_COLLECTION_CLASS_NAMES;
3310     else attrs.classNames = this._COLLECTION_CLASS_NAMES;
3311 
3312     // Layout may be calculated by the collection view beforehand. If so,
3313     // assign it to the attributes. If the collection view doesn't calculate
3314     // layout or defers calculating layout, then we shouldn't force a layout
3315     // on the child view.
3316     var layout = this.layoutForContentIndex(idx);
3317     if (layout) { attrs.layout = layout; }
3318     else { delete attrs.layout; }
3319 
3320     return attrs;
3321   },
3322 
3323   /** @private
3324     A cache of the `contentGroupIndexes` value returned by the delegate.  This
3325     is frequently accessed and usually involves creating an `SC.IndexSet`
3326     object, so it's worthwhile to cache.
3327   */
3328   _contentGroupIndexes: function () {
3329     return this.get('contentDelegate').contentGroupIndexes(this, this.get('content'));
3330   }.property('contentDelegate').cacheable(),
3331 
3332   /** @private
3333     Rather than calling contentIndexIsGroup on the delegate each time, first
3334     check if there are even any contentGroupIndexes.
3335   */
3336   _contentIndexIsGroup: function (idx) {
3337     var groupIndexes = this.get('_contentGroupIndexes');
3338 
3339     // If there are groupIndexes and the given index is within them, check
3340     // with the delegate.
3341     if (groupIndexes && groupIndexes.contains(idx)) {
3342       var del = this.get('contentDelegate'),
3343         items = this.get('content');
3344 
3345       return del.contentIndexIsGroup(this, items, idx);
3346     } else {
3347       return false;
3348   }
3349   },
3350 
3351   /** @private
3352     Determines the example view for a content index.
3353   */
3354   _exampleViewForContentIndex: function (idx) {
3355     var key,
3356       ExampleView,
3357       items = this.get('content'),
3358       item = items.objectAt(idx);
3359 
3360     if (this._contentIndexIsGroup(idx)) {
3361       // so, if it is indeed a group view, we go that route to get the example view
3362       key = this.get('contentGroupExampleViewKey');
3363       if (key && item) ExampleView = item.get(key);
3364       if (!ExampleView) ExampleView = this.get('groupExampleView') || this.get('exampleView');
3365     } else {
3366       // otherwise, we go through the normal example view
3367       key = this.get('contentExampleViewKey');
3368       if (key && item) ExampleView = item.get(key);
3369       if (!ExampleView) ExampleView = this.get('exampleView');
3370     }
3371 
3372     return ExampleView;
3373   },
3374 
3375   /** @private
3376     Returns the pool for a given example view.
3377 
3378     The pool is calculated based on the guid for the example view class.
3379 
3380     @param {SC.View} exampleView
3381   */
3382   _poolForExampleView: function (exampleView) {
3383     var poolKey = SC.guidFor(exampleView);
3384     if (!this._pools) { this._pools = {}; }
3385     if (!this._pools[poolKey]) this._pools[poolKey] = [];
3386     return this._pools[poolKey];
3387   },
3388 
3389   /** @private
3390     Override to compute the hidden layout of the itemView for the content at the
3391     specified idnex.  This layout will be applied when it is moved to the
3392     pool for reuse and should be completely outside the visible portion
3393     of the collection.
3394 
3395     By default this layout is determined using the normal layout for the item.
3396     If the regular layout has a height, the pooled layout will be one height
3397     off the top (for top positioned) or off the bottom (for bottom positioned)
3398     and if the regular layout has a width, the pooled layout will be one
3399     width off the left (for left positioned) or off the right (for right
3400     positioned).
3401 
3402     @param Number contentIndex the index of the item in the content
3403     @returns Object a view layout
3404   */
3405   _poolLayoutForContentIndex: function (contentIndex) {
3406     var layout = this.layoutForContentIndex(contentIndex);
3407 
3408     if (layout && layout.height) {
3409       // Handle both top aligned and bottom aligned layouts.
3410       if (layout.top) { layout.top = -layout.height; }
3411       else { layout.bottom = -layout.height; }
3412     } else if (layout && layout.width) {
3413       // Handle both left aligned and right aligned layouts.
3414       if (layout.left) { layout.left = -layout.width; }
3415       else { layout.right = -layout.width; }
3416     }
3417 
3418     return layout;
3419   },
3420 
3421   /** @private
3422     Configures an existing item view with new attributes.
3423 
3424     @param {SC.View} itemView
3425     @param {Hash} attrs
3426   */
3427   _reconfigureItemView: function (itemView, attrs) {
3428     itemView.beginPropertyChanges();
3429 
3430     // Update the view with the new properties.
3431     itemView.set('content', attrs.content);
3432     itemView.set('contentIndex', attrs.contentIndex);
3433     itemView.set('isEnabled', attrs.isEnabled);
3434     itemView.set('isEditable', attrs.isEditable);
3435     itemView.set('isReorderable', attrs.isReorderable);
3436     itemView.set('isDeletable', attrs.isDeletable);
3437     itemView.set('isSelected', attrs.isSelected);
3438     itemView.set('layerId', attrs.layerId);
3439     itemView.set('outlineLevel', attrs.outlineLevel);
3440     itemView.set('disclosureState', attrs.disclosureState);
3441     itemView.set('isLast', attrs.isLast);
3442 
3443     // Don't assign null/undefined layouts.
3444     if (attrs.layout) { itemView.set('layout', attrs.layout); }
3445 
3446     // If the view's isGroupView property is changing, the associated CSS classes need to
3447     // be updated.
3448     var isCurrentlyGroupView = itemView.get('isGroupView'),
3449         shouldBeGroupView = attrs.isGroupView;
3450     if (isCurrentlyGroupView !== shouldBeGroupView) {
3451       itemView.set('isGroupView', shouldBeGroupView);
3452       var classNames = itemView.get('classNames'),
3453           elem = itemView.$();
3454       // Going from group view to item view...
3455       if (isCurrentlyGroupView && !shouldBeGroupView) {
3456         classNames.pushObject('sc-item');
3457         classNames.removeObject('sc-group-item');
3458         elem.setClass({ 'sc-item': YES, 'sc-group-item': NO });
3459       }
3460       // Going from item view to group view...
3461       else {
3462         classNames.removeObject('sc-item');
3463         classNames.pushObject('sc-group-item');
3464         elem.setClass({ 'sc-item': NO, 'sc-group-item': YES });
3465       }
3466     }
3467     // Wrap up.
3468     itemView.endPropertyChanges();
3469   },
3470 
3471   /** @private
3472     Removes the item view, pooling it for re-use if possible.
3473   */
3474   _removeItemView: function (itemView, idx) {
3475     var exampleView,
3476       items = this.get('content'),
3477       canPoolView, canPoolLayer,
3478       poolLayout,
3479       pool,
3480       prototype,
3481       wasPooled = false;
3482 
3483     // Don't pool views whose content has changed, because if the example
3484     // view used is different than the new content, we would pool the wrong
3485     // type of view.
3486     if (items && itemView.get('content') === items.objectAt(idx)) {
3487 
3488       exampleView = this._exampleViewForContentIndex(idx);
3489       prototype = exampleView.prototype;
3490       canPoolView = SC.none(prototype.isReusable) || prototype.isReusable;
3491       if (canPoolView) {
3492         // If the exampleView is reusable, send the view to its pool.
3493         pool = this._poolForExampleView(exampleView);
3494 
3495         //@if (debug)
3496         // Add a bit of developer support if they are migrating over from SC.CollectionFastPath
3497         if (itemView.hibernateInPool) {
3498           SC.error("Developer Error: Item views that want to do clean up before being pooled should implement sleepInPool not hibernateInPool.  This will be temporarily fixed up for development mode only, but must be changed before production.");
3499           itemView.sleepInPool = itemView.hibernateInPool;
3500         }
3501         //@endif
3502 
3503         // Give the view a chance to do some clean up before sleeping.
3504         if (itemView.sleepInPool) { itemView.sleepInPool(this); }
3505 
3506         pool.push(itemView);
3507 
3508         // If the exampleView's layer isn't reusable, destroy it.
3509         poolLayout = this._poolLayoutForContentIndex(idx);
3510         canPoolLayer = poolLayout && (SC.none(prototype.isLayerReusable) || prototype.isLayerReusable);
3511         if (canPoolLayer) {
3512           // If the layer is sticking around, be sure to move it out of view.
3513           itemView.set('layout', poolLayout);
3514         } else {
3515           // We can't pool layers that are prohibited or that cannot be moved out of view (i.e. no poolLayout)
3516           itemView.destroyLayer();
3517         }
3518 
3519         // Ensure that the id of views in the pool don't clash with ids that
3520         // are used outside of it.
3521         itemView.set('layerId', SC.generateGuid(null, 'pool-'));
3522 
3523         wasPooled = true;
3524       }
3525     }
3526 
3527     if (!wasPooled) {
3528       itemView.destroy();
3529     }
3530 
3531     // Remove the cached view (can still exist in the pool)
3532     delete this._sc_itemViews[idx];
3533   }
3534 
3535 });
3536