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