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