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('views/collection');
  9 sc_require('views/list_item');
 10 sc_require('mixins/collection_row_delegate');
 11 
 12 /** @class
 13 
 14   A list view renders vertical or horizontal lists of items.  It is a specialized
 15   form of collection view that is simpler than a table view, but more refined than
 16   a generic collection.
 17 
 18   You can use a list view just like any collection view, except that often you
 19   provide the rowSize, which will be either the height of each row when laying
 20   out rows vertically (the default) or the widht of each row when laying out
 21   the rows horizontally.
 22 
 23   ## Variable Row Heights
 24 
 25   Normally you set the row height or width through the rowSize property, but
 26   you can also support custom row sizes by assigning the `customRowSizeIndexes`
 27   property to an index set of all custom sized rows.
 28 
 29   ## Using ListView with Very Large Data Sets
 30 
 31   ListView implements incremental rendering, which means it will only render
 32   HTML for the items that are currently visible on the screen.  This means you
 33   can use it to efficiently render lists with 100K+ items or more very efficiently.
 34 
 35   If you need to work with very large lists of items however, be aware that
 36   calculating variable row sizes can become very expensive since the list
 37   view will essentially have to iterate over every item in the collection to
 38   determine each the total height or width.
 39 
 40   Therefore, to work with very large lists, you should consider using a design
 41   that allows your row sizes to remain uniform.  This will allow the list view
 42   to much more efficiently render content.
 43 
 44   Alternatively, to support differently sized and incrementally rendered item
 45   views, you may want to consider overriding the `offsetForRowAtContentIndex()`
 46   and `rowSizeForContentIndex()` methods to perform some specialized faster
 47   calculations that do not require inspecting every item in the collection.
 48 
 49   Note: row sizes and offsets are cached so once they are calculated
 50   the list view will be able to display very quickly.
 51 
 52   ## Dragging and Dropping
 53 
 54   When the list view is configured to accept drops onto its items, it
 55   will set the `isDropTarget` property on the target item accordingly.  This
 56   allows you to modify the appearance of the drop target list item accordingly
 57   (@see SC.ListItemView#isDropTarget).
 58 
 59   @extends SC.CollectionView
 60   @extends SC.CollectionRowDelegate
 61   @since SproutCore 1.0
 62 */
 63 // (Can we also have an 'estimate row heights' property that will simply
 64 // cheat for very long data sets to make rendering more efficient?)
 65 SC.ListView = SC.CollectionView.extend(SC.CollectionRowDelegate,
 66 /** @scope SC.ListView.prototype */ {
 67 
 68   /** @private */
 69   _sc_customRowSizeIndexes: null,
 70 
 71   /** @private */
 72   _sc_insertionPointView: null,
 73 
 74   /** @private */
 75   _sc_lastDropOnView: null,
 76 
 77   /** @private */
 78   _sc_layout: null,
 79 
 80   /** @private */
 81   _sc_sizeCache: null,
 82 
 83   /** @private */
 84   _sc_offsetCache: null,
 85 
 86   /** @private */
 87   _sc_rowDelegate: null,
 88 
 89   /** @private */
 90   _sc_rowSize: null,
 91 
 92   /**
 93     @type Array
 94     @default ['sc-list-view']
 95     @see SC.View#classNames
 96   */
 97   classNames: ['sc-list-view'],
 98 
 99   /**
100     @type Boolean
101     @default true
102   */
103   acceptsFirstResponder: true,
104 
105   /** @private SC.CollectionView.prototype */
106   exampleView: SC.ListItemView,
107 
108   /**
109     Determines the layout direction of the rows of items, either vertically or
110     horizontally. Possible values:
111 
112       - SC.LAYOUT_HORIZONTAL
113       - SC.LAYOUT_VERTICAL
114 
115     @type String
116     @default SC.LAYOUT_VERTICAL
117   */
118   layoutDirection: SC.LAYOUT_VERTICAL,
119 
120   /**
121     If set to true, the default theme will show alternating rows
122     for the views this ListView created through exampleView property.
123 
124     @type Boolean
125     @default false
126   */
127   showAlternatingRows: false,
128 
129   // ..........................................................
130   // METHODS
131   //
132 
133   /** @private */
134   init: function () {
135     sc_super();
136 
137     this._sc_rowDelegateDidChange();
138   },
139 
140   /** @private SC.CollectionView.prototype.destroy. */
141   destroy: function () {
142     sc_super();
143 
144     // All manipulations made to objects we use must be reversed!
145     var del = this._sc_rowDelegate;
146     if (del) {
147       del.removeObserver('_sc_totalRowSize', this, this._sc_rowSizeDidChange);
148       del.removeObserver('customRowSizeIndexes', this, this._sc_customRowSizeIndexesDidChange);
149 
150       this._sc_rowDelegate = null;
151     }
152 
153     var customRowSizeIndexes = this._sc_customRowSizeIndexes;
154     if (customRowSizeIndexes) {
155       customRowSizeIndexes.removeObserver('[]', this, this._sc_customRowSizeIndexesContentDidChange);
156 
157       this._sc_customRowSizeIndexes = null;
158     }
159   },
160 
161   /** @private */
162   render: function (context, firstTime) {
163     context.setClass('alternating', this.get('showAlternatingRows'));
164 
165     return sc_super();
166   },
167 
168   // ..........................................................
169   // COLLECTION ROW DELEGATE SUPPORT
170   //
171 
172   /**
173     @field
174     @type Object
175     @observes 'delegate'
176     @observes 'content'
177   */
178   rowDelegate: function () {
179     var del = this.delegate,
180       content = this.get('content');
181 
182     return this.delegateFor('isCollectionRowDelegate', del, content);
183   }.property('delegate', 'content').cacheable(),
184 
185   /** @private - Whenever the rowDelegate changes, begin observing important properties */
186   _sc_rowDelegateDidChange: function () {
187     var last = this._sc_rowDelegate,
188       del  = this.get('rowDelegate'),
189       func = this._sc_rowSizeDidChange,
190       func2 = this._sc_customRowSizeIndexesDidChange;
191 
192     if (last === del) return this; // nothing to do
193     this._sc_rowDelegate = del;
194 
195     // last may be null on a new object
196     if (last) {
197       last.removeObserver('_sc_totalRowSize', this, func);
198       last.removeObserver('customRowSizeIndexes', this, func2);
199     }
200 
201     //@if(debug)
202     if (!del) {
203       throw new Error("%@ - Developer Error: SC.ListView must always have a rowDelegate.".fmt(this));
204     }
205     //@endif
206 
207     // Add the new observers.
208     del.addObserver('_sc_totalRowSize', this, func);
209     del.addObserver('customRowSizeIndexes', this, func2);
210 
211     // Trigger once to initialize.
212     this._sc_rowSizeDidChange()._sc_customRowSizeIndexesDidChange();
213 
214     return this;
215   }.observes('rowDelegate'),
216 
217   /** @private - Called whenever the _sc_totalRowSize changes. If the property actually changed then invalidate all row sizes. */
218   _sc_rowSizeDidChange: function () {
219     var del = this.get('rowDelegate'),
220       totalRowSize = del.get('_sc_totalRowSize'),
221       indexes;
222 
223     if (totalRowSize === this._sc_rowSize) return this; // nothing to do
224     this._sc_rowSize = totalRowSize;
225 
226     indexes = SC.IndexSet.create(0, this.get('length'));
227     this.rowSizeDidChangeForIndexes(indexes);
228 
229     return this;
230   },
231 
232   /** @private - Called whenever the customRowSizeIndexes changes. If the property actually changed then invalidate affected row sizes. */
233   _sc_customRowSizeIndexesDidChange: function () {
234     var del   = this.get('rowDelegate'),
235       indexes = del.get('customRowSizeIndexes'),
236       last    = this._sc_customRowSizeIndexes,
237       func    = this._sc_customRowSizeIndexesContentDidChange;
238 
239     // nothing to do
240     if ((indexes === last) || (last && last.isEqual(indexes))) return this;
241 
242     // if we were observing the last index set, then remove observer
243     if (last && this._sc_isObservingCustomRowSizeIndexes) {
244       last.removeObserver('[]', this, func);
245     }
246 
247     // only observe new index set if it exists and it is not frozen.
248     this._sc_isObservingCustomRowSizeIndexes = indexes;
249     if (indexes && !indexes.get('isFrozen')) {
250       indexes.addObserver('[]', this, func);
251     }
252 
253     // Trigger once to initialize.
254     this._sc_customRowSizeIndexesContentDidChange();
255 
256     return this;
257   },
258 
259   /** @private - Called whenever the customRowSizeIndexes set is modified. */
260   _sc_customRowSizeIndexesContentDidChange: function () {
261     var del     = this.get('rowDelegate'),
262       indexes = del.get('customRowSizeIndexes'),
263       last    = this._sc_customRowSizeIndexes,
264       changed;
265 
266     // compute the set to invalidate.  the union of cur and last set
267     if (indexes && last) {
268       changed = indexes.copy().add(last);
269     } else {
270       changed = indexes || last;
271     }
272 
273     this._sc_customRowSizeIndexes = indexes ? indexes.frozenCopy() : null;
274 
275     // invalidate
276     this.rowSizeDidChangeForIndexes(changed);
277 
278     return this;
279   },
280 
281 
282   // ..........................................................
283   // ROW PROPERTIES
284   //
285 
286   /**
287     Returns the top or left offset for the specified content index.  This will take
288     into account any custom row sizes and group views.
289 
290     @param {Number} idx the content index
291     @returns {Number} the row offset
292   */
293   rowOffsetForContentIndex: function (idx) {
294     if (idx === 0) return 0; // Fast path!
295 
296     var del = this.get('rowDelegate'),
297       totalRowSize = del.get('_sc_totalRowSize'),
298       rowSpacing = del.get('rowSpacing'),
299       ret, custom, cache, delta, max;
300 
301     ret = idx * totalRowSize;
302 
303 		if (rowSpacing) {
304       ret += idx * rowSpacing;
305     }
306 
307     if (del.customRowSizeIndexes && (custom = del.get('customRowSizeIndexes'))) {
308 
309       // prefill the cache with custom rows.
310       cache = this._sc_offsetCache;
311       if (!cache) {
312         cache = [];
313         delta = max = 0;
314         custom.forEach(function (idx) {
315           delta += this.rowSizeForContentIndex(idx) - totalRowSize;
316           cache[idx + 1] = delta;
317           max = idx;
318         }, this);
319         this._sc_max = max + 1;
320 
321         // moved down so that the cache is not marked as initialized until it actually is
322         this._sc_offsetCache = cache;
323       }
324 
325       // now just get the delta for the last custom row before the current
326       // idx.
327       delta = cache[idx];
328       if (delta === undefined) {
329         delta = cache[idx] = cache[idx - 1];
330         if (delta === undefined) {
331           max = this._sc_max;
332           if (idx < max) max = custom.indexBefore(idx) + 1;
333           delta = cache[idx] = cache[max] || 0;
334         }
335       }
336 
337       ret += delta;
338     }
339 
340     return ret;
341   },
342 
343   /** @deprecated Version 1.11. Please use the `rowSizeForContentIndex()` function instead.
344     Not implemented by default.
345 
346     @field
347     @param {Number} idx content index
348     @returns {Number} the row height
349   */
350   rowHeightForContentIndex: null,
351 
352   /**
353     Returns the row size for the specified content index.  This will take
354     into account custom row sizes and group rows.
355 
356     @param {Number} idx content index
357     @returns {Number} the row height
358   */
359   rowSizeForContentIndex: function (idx) {
360     var del = this.get('rowDelegate'),
361         ret, cache, content, indexes;
362 
363     if (this.rowHeightForContentIndex) {
364       //@if(debug)
365       SC.warn('Developer Warning: The rowHeightForContentIndex() method of SC.ListView has been renamed rowSizeForContentIndex().');
366       //@endif
367       return this.rowHeightForContentIndex(idx);
368     }
369 
370     if (del.customRowSizeIndexes && (indexes = del.get('customRowSizeIndexes'))) {
371       cache = this._sc_sizeCache;
372       if (!cache) {
373         cache = [];
374         content = this.get('content');
375         indexes.forEach(function (idx) {
376           cache[idx] = del.contentIndexRowSize(this, content, idx);
377         }, this);
378 
379         // moved down so that the cache is not marked as initialized until it actually is.
380         this._sc_sizeCache = cache;
381       }
382 
383       ret = cache[idx];
384       if (ret === undefined) ret = del.get('_sc_totalRowSize');
385     } else {
386       ret = del.get('_sc_totalRowSize');
387     }
388 
389     return ret;
390   },
391 
392   /** @deprecated Version 1.11. Please use the `rowSizeDidChangeForIndexes()` function instead.
393     Call this method whenever a row height has changed in one or more indexes.
394     This will invalidate the row height cache and reload the content indexes.
395     Pass either an index set or a single index number.
396 
397     This method is called automatically whenever you change the rowSize, rowPadding
398     or customRowSizeIndexes properties on the collectionRowDelegate.
399 
400     @param {SC.IndexSet|Number} indexes
401     @returns {SC.ListView} receiver
402   */
403   rowHeightDidChangeForIndexes: function (indexes) {
404     //@if(debug)
405     SC.warn('Developer Warning: The rowHeightDidChangeForIndexes() function of SC.ListView has been renamed to rowSizeDidChangeForIndexes().');
406     //@endif
407     return this.rowSizeDidChangeForIndexes(indexes);
408   },
409 
410   /**
411     Call this method whenever a row size has changed in one or more indexes.
412     This will invalidate the row size cache and reload the content indexes.
413     Pass either an index set or a single index number.
414 
415     This method is called automatically whenever you change the rowSize, rowPadding
416     or customRowSizeIndexes properties on the collectionRowDelegate.
417 
418     @param {SC.IndexSet|Number} indexes
419     @returns {SC.ListView} receiver
420   */
421   rowSizeDidChangeForIndexes: function (indexes) {
422     var len = this.get('length');
423 
424     // clear any cached offsets
425     this._sc_sizeCache = this._sc_offsetCache = null;
426 
427     // find the smallest index changed; invalidate everything past it
428     if (indexes && indexes.isIndexSet) indexes = indexes.get('min');
429     this.reload(SC.IndexSet.create(indexes, len - indexes));
430 
431     // If the row height changes, our entire layout needs to change.
432     this.invokeOnce('adjustLayout');
433 
434     return this;
435   },
436 
437   // ..........................................................
438   // SUBCLASS IMPLEMENTATIONS
439   //
440 
441   /**
442     The layout for a ListView is computed from the total number of rows
443     along with any custom row heights.
444   */
445   computeLayout: function () {
446     // default layout
447     var ret = this._sc_layout,
448       layoutDirection = this.get('layoutDirection'),
449       del = this.get('rowDelegate'),
450       rowSpacing = del.get('rowSpacing');
451 
452     // Initialize lazily.
453     if (!ret) ret = this._sc_layout = {};
454 
455     // Support both vertical and horizontal lists.
456     if (layoutDirection === SC.LAYOUT_HORIZONTAL) {
457       // Don't include the row spacing after the last item in the width.
458       ret.width = Math.max(this.rowOffsetForContentIndex(this.get('length')) - rowSpacing, 0);
459     } else {
460       // Don't include the row spacing after the last item in the height.
461       ret.height = Math.max(this.rowOffsetForContentIndex(this.get('length')) - rowSpacing, 0);
462     }
463     return ret;
464   },
465 
466   /**
467     Computes the layout for a specific content index by combining the current
468     row heights.
469 
470     @param {Number} contentIndex
471     @returns {Hash} layout hash for the index provided
472   */
473   layoutForContentIndex: function (contentIndex) {
474     var del = this.get('rowDelegate'),
475       layoutDirection = this.get('layoutDirection'),
476       offset, size;
477 
478     offset = this.rowOffsetForContentIndex(contentIndex);
479     size = this.rowSizeForContentIndex(contentIndex) - del.get('rowPadding') * 2;
480 
481     // Support both vertical and horizontal lists.
482     if (layoutDirection === SC.LAYOUT_HORIZONTAL) {
483       return {
484         left: offset,
485         width: size,
486         top: 0,
487         bottom: 0
488       };
489     } else {
490       return {
491         top: offset,
492         height: size,
493         left: 0,
494         right: 0
495       };
496     }
497   },
498 
499   /**
500     Override to return an IndexSet with the indexes that are at least
501     partially visible in the passed rectangle.  This method is used by the
502     default implementation of computeNowShowing() to determine the new
503     nowShowing range after a scroll.
504 
505     Override this method to implement incremental rendering.
506 
507     The default simply returns the current content length.
508 
509     @param {Rect} rect the visible rect or a point
510     @returns {SC.IndexSet} now showing indexes
511   */
512   contentIndexesInRect: function (rect) {
513     var del = this.get('rowDelegate'),
514       totalRowSize = del.get('_sc_totalRowSize'),
515       rowSpacing = del.get('rowSpacing'),
516       totalRowSizeWithSpacing = totalRowSize + rowSpacing,
517       layoutDirection = this.get('layoutDirection'),
518       len = this.get('length'),
519       offset, start, end,
520       firstEdge, lastEdge,
521       size;
522 
523     // Support both vertical and horizontal lists.
524     if (layoutDirection === SC.LAYOUT_HORIZONTAL) {
525       firstEdge = SC.minX(rect);
526       lastEdge = SC.maxX(rect);
527       size = rect.width || 0;
528     } else {
529       firstEdge = SC.minY(rect);
530       lastEdge = SC.maxY(rect);
531       size = rect.height || 0;
532     }
533 
534     // estimate the starting row and then get actual offsets until we are
535     // right.
536     start = (firstEdge - (firstEdge % totalRowSizeWithSpacing)) / totalRowSizeWithSpacing;
537     offset = this.rowOffsetForContentIndex(start);
538 
539     // go backwards until offset of row is before first edge
540     while (start > 0 && offset > firstEdge) {
541       start--;
542       offset -= (this.rowSizeForContentIndex(start) + rowSpacing);
543     }
544 
545     // go forwards until offset plus size of row is after first edge
546     offset += this.rowSizeForContentIndex(start);
547     while (start < len && offset <= firstEdge) {
548       start++;
549       offset += this.rowSizeForContentIndex(start) + rowSpacing;
550     }
551     if (start < 0) start = 0;
552     if (start >= len) start = len;
553 
554 
555     // estimate the final row and then get the actual offsets until we are
556     // right. - look at the offset of the _following_ row
557     end = start + ((size - (size % totalRowSizeWithSpacing)) / totalRowSizeWithSpacing);
558     if (end > len) end = len;
559     offset = this.rowOffsetForContentIndex(end);
560 
561     // walk backwards until offset of row is before or at last edge
562     while (end >= start && offset >= lastEdge) {
563       end--;
564       offset -= (this.rowSizeForContentIndex(end) + rowSpacing);
565     }
566 
567     // go forwards until offset plus size of row is after last edge
568     offset += this.rowSizeForContentIndex(end) + rowSpacing;
569     while (end < len && offset < lastEdge) {
570       end++;
571       offset += this.rowSizeForContentIndex(end) + rowSpacing;
572     }
573 
574     end++; // end should be after start
575 
576     if (end < start) end = start;
577     if (end > len) end = len;
578 
579     // convert to IndexSet and return
580     return SC.IndexSet.create(start, end - start);
581   },
582 
583 
584   // ..........................................................
585   // DRAG AND DROP SUPPORT
586   //
587 
588   /**
589     Default view class used to draw an insertion point, which uses CSS
590     styling to show a horizontal line.
591 
592     This view's position (top & left) will be automatically adjusted to the
593     point of insertion.
594 
595     @field
596     @type SC.View
597   */
598   insertionPointView: SC.View.extend({
599     classNames: 'sc-list-insertion-point',
600 
601     layout: function (key, value) {
602       var layoutDirection = this.get('layoutDirection');
603 
604       key = layoutDirection === SC.LAYOUT_HORIZONTAL ? 'width' : 'height';
605 
606       // Getter – create layout hash.
607       if (value === undefined) {
608         value = {};
609       }
610 
611       // Either way, add the narrow dimension to the layout if needed.
612       if (SC.none(value[key])) value[key] = 2;
613 
614       return value;
615     }.property('layoutDirection').cacheable(),
616 
617     /**
618       The direction of layout of the SC.ListView.
619       This property will be set by the list view when this view is created.
620       */
621     layoutDirection: SC.LAYOUT_VERTICAL,
622 
623     /** @private */
624     render: function (context, firstTime) {
625       if (firstTime) context.push('<div class="anchor"></div>');
626     }
627   }),
628 
629   /**
630     Default implementation will show an insertion point
631     @see SC.CollectionView#showInsertionPoint
632   */
633   showInsertionPoint: function (itemView, dropOperation) {
634     // FAST PATH: If we're dropping on the item view itself... (Note: support for this
635     // should be built into CollectionView's calling method and not the unrelated method
636     // for showing an insertion point.)
637     if (dropOperation & SC.DROP_ON) {
638       if (itemView && itemView !== this._sc_lastDropOnView) {
639         this.hideInsertionPoint();
640 
641         // If the drag is supposed to drop onto an item, notify the item that it
642         // is the current target of the drop.
643         itemView.set('isDropTarget', YES);
644 
645         // Track the item so that we can clear isDropTarget when the drag changes;
646         // versus having to clear it from all items.
647         this._sc_lastDropOnView = itemView;
648       }
649       return;
650     }
651 
652     // Otherwise, we're actually inserting.
653 
654     // TODO: CollectionView's notes on showInsertionPoint specify that if no itemView
655     // is passed, this should try to get the last itemView. (Note that ListView's
656     // itemViewForContentIndex creates a new view on demand, so make sure that we
657     // have content items before getting the last view.) This is a change in established
658     // behavior however, so proceed carefully.
659 
660     // If there was an item that was the target of the drop previously, be
661     // sure to clear it.
662     if (this._sc_lastDropOnView) {
663       this._sc_lastDropOnView.set('isDropTarget', NO);
664       this._sc_lastDropOnView = null;
665     }
666 
667     var len = this.get('length'),
668       index, level, indent;
669 
670     // Get values from itemView, if present.
671     if (itemView) {
672       index = itemView.get('contentIndex');
673       level = itemView.get('outlineLevel');
674       indent = itemView.get('outlineIndent');
675     }
676 
677     // Set defaults.
678     index = index || 0;
679     if (SC.none(level)) level = -1;
680     indent = indent || 0;
681 
682     // Show item indented if we are inserting at the end and the last item
683     // is a group item.  This is a special case that should really be
684     // converted into a more general protocol.
685     if ((index >= len) && index > 0) {
686       var previousItem = this.itemViewForContentIndex(len - 1);
687       if (previousItem.get('isGroupView')) {
688         level = 1;
689         indent = previousItem.get('outlineIndent');
690       }
691     }
692 
693     // Get insertion point.
694     var insertionPoint = this._sc_insertionPointView,
695       layoutDirection = this.get('layoutDirection');
696 
697     if (!insertionPoint) {
698       insertionPoint = this._sc_insertionPointView = this.get('insertionPointView').create({
699         layoutDirection: layoutDirection
700       });
701     }
702 
703     // Calculate where it should go.
704     var itemViewLayout = itemView ? itemView.get('layout') : { top: 0, left: 0 },
705       top, left;
706 
707     // Support both vertical and horizontal lists.
708     if (layoutDirection === SC.LAYOUT_HORIZONTAL) {
709       left = itemViewLayout.left;
710       if (dropOperation & SC.DROP_AFTER) { left += itemViewLayout.width; }
711       top = ((level + 1) * indent) + 12;
712     } else {
713       top = itemViewLayout.top;
714       if (dropOperation & SC.DROP_AFTER) { top += itemViewLayout.height; }
715       left = ((level + 1) * indent) + 12;
716     }
717 
718     // Put it there.
719     insertionPoint.adjust({ top: top, left: left });
720 
721     this.appendChild(insertionPoint);
722   },
723 
724   /** @see SC.CollectionView#hideInsertionPoint */
725   hideInsertionPoint: function () {
726     // If there was an item that was the target of the drop previously, be
727     // sure to clear it.
728     if (this._sc_lastDropOnView) {
729       this._sc_lastDropOnView.set('isDropTarget', NO);
730       this._sc_lastDropOnView = null;
731     }
732 
733     var view = this._sc_insertionPointView;
734     if (view) view.removeFromParent().destroy();
735     this._sc_insertionPointView = null;
736   },
737 
738   /**
739     Compute the insertion index for the passed location.  The location is
740     a point, relative to the top/left corner of the receiver view.  The return
741     value is an index plus a dropOperation, which is computed as such:
742 
743       - if outlining is not used and you are within 5px of an edge, DROP_BEFORE
744         the item after the edge.
745       - if outlining is used and you are within 5px of an edge and the previous
746         item has a different outline level then the next item, then DROP_AFTER
747         the previous item if you are closer to that outline level.
748       - if dropOperation = SC.DROP_ON and you are over the middle of a row, then
749         use DROP_ON.
750 
751     @see SC.CollectionView.insertionIndexForLocation
752   */
753   insertionIndexForLocation: function (loc, dropOperation) {
754     var locRect = { x: loc.x, y: loc.y, width: 1, height: 1 },
755       indexes = this.contentIndexesInRect(locRect),
756       index   = indexes.get('min'),
757       len     = this.get('length'),
758       min, max, diff, clevel, cindent, plevel, pindent, itemView;
759 
760     // if there are no indexes in the rect, then we need to either insert
761     // before the top item or after the last item.  Figure that out by
762     // computing both.
763     if (SC.none(index) || index < 0) {
764       if ((len === 0) || (loc.y <= this.rowOffsetForContentIndex(0))) index = 0;
765       else if (loc.y >= this.rowOffsetForContentIndex(len)) index = len;
766     }
767 
768     // figure the range of the row the location must be within.
769     min = this.rowOffsetForContentIndex(index);
770     max = min + this.rowSizeForContentIndex(index);
771 
772     // now we know which index we are in.  if dropOperation is DROP_ON, figure
773     // if we can drop on or not.
774     if (dropOperation === SC.DROP_ON) {
775       // editable size - reduce height by a bit to handle dropping
776       if (this.get('isEditable')) diff = Math.min(Math.floor((max - min) * 0.2), 5);
777       else diff = 0;
778 
779       // if we're inside the range, then DROP_ON
780       if (loc.y >= (min + diff) || loc.y <= (max + diff)) {
781         return [index, SC.DROP_ON];
782       }
783     }
784 
785     // finally, let's decide if we want to actually insert before/after.  Only
786     // matters if we are using outlining.
787     if (index > 0) {
788 
789       itemView = this.itemViewForContentIndex(index - 1);
790       pindent  = (itemView ? itemView.get('outlineIndent') : 0) || 0;
791       plevel   = itemView ? itemView.get('outlineLevel') : 0;
792 
793       if (index < len) {
794         itemView = this.itemViewForContentIndex(index);
795         clevel   = itemView ? itemView.get('outlineLevel') : 0;
796         cindent  = (itemView ? itemView.get('outlineIndent') : 0) || 0;
797         cindent  *= clevel;
798       } else {
799         clevel = itemView.get('isGroupView') ? 1 : 0; // special case...
800         cindent = pindent * clevel;
801       }
802 
803       pindent *= plevel;
804 
805       // if indent levels are different, then try to figure out which level
806       // it should be on.
807       if ((clevel !== plevel) && (cindent !== pindent)) {
808 
809         // use most inner indent as boundary
810         if (pindent > cindent) {
811           index--;
812           dropOperation = SC.DROP_AFTER;
813         }
814       }
815     }
816 
817     // we do not support dropping before a group item.  If dropping before
818     // a group item, always try to instead drop after the previous item.  If
819     // the previous item is also a group then, well, dropping is just not
820     // allowed.  Note also that dropping at 0, first item must not be group
821     // and dropping at length, last item must not be a group
822     //
823     if (dropOperation === SC.DROP_BEFORE) {
824       itemView = (index < len) ? this.itemViewForContentIndex(index) : null;
825       if (!itemView || itemView.get('isGroupView')) {
826         if (index > 0) {
827           itemView = this.itemViewForContentIndex(index - 1);
828 
829           // don't allow a drop if the previous item is a group view and we're
830           // insert before the end.  For the end, allow the drop if the
831           // previous item is a group view but OPEN.
832           if (!itemView.get('isGroupView') || (itemView.get('disclosureState') === SC.BRANCH_OPEN)) {
833             index = index - 1;
834             dropOperation = SC.DROP_AFTER;
835           } else index = -1;
836 
837         } else index = -1;
838       }
839 
840       if (index < 0) dropOperation = SC.DRAG_NONE;
841     }
842 
843     // return whatever we came up with
844     return [index, dropOperation];
845   }
846 
847 });
848