1 // ========================================================================== 2 // Project: SproutCore - JavaScript Application Framework 3 // Copyright: ©2014 7x7 Software Inc. All rights reserved. 4 // Portions ©2008-2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 sc_require('views/scroller'); 8 9 10 SC.SCROLL = { 11 12 /** 13 The rate of deceleration in pixels per square millisecond after scrolling from a drag gesture. 14 15 @static 16 @type Number 17 @default 3.0 18 */ 19 DRAG_SCROLL_DECELERATION: 3.0, 20 21 /** 22 The number of pixels a gesture needs to move before it should be considered a scroll gesture. 23 24 @static 25 @type Number 26 @default 5 27 */ 28 SCROLL_GESTURE_THRESHOLD: 5, 29 30 /** 31 The number of pixels a gesture needs to move in only a single direction, before it should be 32 considered as a locked scrolling direction (i.e. no gestures in the other direction will scroll 33 in that direction). 34 35 @static 36 @type Number 37 @default 50 38 */ 39 SCROLL_LOCK_GESTURE_THRESHOLD: 50, 40 41 /** 42 The number of pixels a gesture needs to expand or contract before it should be considered a scale gesture. 43 44 @static 45 @type Number 46 @default 3 47 */ 48 SCALE_GESTURE_THRESHOLD: 3 49 50 }; 51 52 53 /** @class 54 Implements a complete scroll view. SproutCore implements its own JS-based scrolling in order 55 to unify scrolling behavior across platforms, and to enable progressive rendering (via the 56 clipping frame) during scroll on all devices. 57 58 Important Properties 59 ----- 60 61 ScrollView positions its contentView according to three properties: `verticalScrollOffset`, 62 `horizontalScrollOffset`, and `scale`. These properties are bindable and observable, but you 63 should not override them. 64 65 Gutter vs. Overlaid Scrollers 66 ----- 67 68 Scroll views use swappable scroll-bar views with various behavior (see `verticalScrollerView` 69 and `horizontalScrollerView`). `SC.ScrollerView` is a gutter-based scroller which persists and 70 takes up fourteen pixels. (By default, no scroller is shown for content that is too small to 71 scroll; see `autohidesHorizontalScroller` and `autohidesVerticalScroller`.) `SC.OverlayScrollerView` 72 is a gutterless view which fades when not scrolling. If you would like your view to always have 73 OS X-style fading overlaid scrollers, you can use the following: 74 75 SC.ScrollView.extend({ 76 horizontalOverlay: true, 77 verticalOverlay: true 78 }); 79 80 @extends SC.View 81 @since SproutCore 1.0 82 */ 83 SC.ScrollView = SC.View.extend({ 84 /** @scope SC.ScrollView.prototype */ 85 86 // --------------------------------------------------------------------------------------------- 87 // Properties 88 // 89 90 /** @private Flag used to determine whether to animate the adjustment. */ 91 _sc_animationDuration: null, 92 93 /** @private The animation timing to use. */ 94 _sc_animationTiming: null, 95 96 /** @private The cached height of the container. */ 97 _sc_containerHeight: 0, 98 99 /** @private The cached offset of the container. */ 100 _sc_containerOffset: null, 101 102 /** @private The cached width of the container. */ 103 _sc_containerWidth: 0, 104 105 /** @private The cached height of the content. */ 106 _sc_contentHeight: 0, 107 108 /** @private Flag used to react accordingly when the content's height changes. */ 109 _sc_contentHeightDidChange: false, 110 111 /** @private The cached scale of the content. */ 112 _sc_contentScale: undefined, 113 114 /** @private Flag used to react accordingly when the content's scale changes. */ 115 _sc_contentScaleDidChange: false, 116 117 /** @private The cached width of the content. */ 118 _sc_contentWidth: 0, 119 120 /** @private Flag used to react accordingly when the content's width changes. */ 121 _sc_contentWidthDidChange: false, 122 123 /** @private The anchor horizontal offset of the touch gesture. */ 124 _sc_gestureAnchorHOffset: null, 125 126 /** @private The anchor position of the initial touch gesture. */ 127 _sc_gestureAnchorTotalD: null, 128 129 /** @private The anchor position of the initial touch gesture. */ 130 _sc_gestureAnchorTotalX: null, 131 132 /** @private The anchor position of the initial touch gesture. */ 133 _sc_gestureAnchorTotalY: null, 134 135 /** @private The anchor vertical offset of the touch gesture. */ 136 _sc_gestureAnchorVOffset: null, 137 138 /** @private The anchor position of the last touch gesture. */ 139 _sc_gestureAnchorX: null, 140 141 /** @private The anchor position of the last touch gesture. */ 142 _sc_gestureAnchorY: null, 143 144 /** @private The anchor distance from center of the last touch gesture. */ 145 _sc_gestureAnchorD: null, 146 147 /** @private The original scale before a touch gesture. */ 148 _sc_gestureAnchorScale: null, 149 150 /** @private The timer used to fade out this scroller. */ 151 _sc_horizontalFadeOutTimer: null, 152 153 /** @private The actual horizontal scroll offset. */ 154 _sc_horizontalScrollOffset: null, 155 156 /** @private The percentage offset scrolled horizontally. Used to maintain the horizontal position when the content size changes. */ 157 _sc_horizontalPct: null, 158 159 /** @private Flag is true when scaling. Used in capturing touches. */ 160 _sc_isTouchScaling: false, 161 162 /** @private Flag is true when scrolling horizontally. Used in capturing touches. */ 163 _sc_isTouchScrollingH: false, 164 165 /** @private Flag is true when scrolling is locked to horizontal. */ 166 _sc_isTouchScrollingHOnly: false, 167 168 /** @private Flag is true when scrolling vertically. Used in capturing touches. */ 169 _sc_isTouchScrollingV: false, 170 171 /** @private Flag is true when scrolling is locked to vertical. */ 172 _sc_isTouchScrollingVOnly: false, 173 174 /** @private The minimum delay before applying a fade transition. */ 175 _sc_minimumFadeOutDelay: function () { 176 // The fade out delay is never less than 100ms (so that the current run loop can complete) and is never less than the fade in duration (so that it can fade fully in). 177 return Math.max(Math.max(this.get('fadeOutDelay') || 0, 0.1), this.get('fadeInDuration') || 0) * 1000; 178 }.property('fadeOutDelay').cacheable(), 179 180 /** @private The amount of slip while over dragging (drag past bounds). 1.0 or 100% would slip completely, and 0.0 or 0% would not slip at all. */ 181 _sc_overDragSlip: 0.5, 182 183 /** @private Timer used to pass a touch through to its content if we don't start scrolling in that time. */ 184 _sc_passTouchToContentTimer: null, 185 186 /** @private The actual scale. */ 187 _sc_scale: 1, 188 189 /** @private Flag used to indicate when we should resize the content width manually. */ 190 // _sc_shouldResizeContentWidth: false, 191 192 /** @private Flag used to indicate when we should resize the content height manually. */ 193 // _sc_shouldResizeContentHeight: false, 194 195 /** @private The offset center x of a multi-touch gesture. */ 196 _sc_touchCenterX: null, 197 198 /** @private The offset center y of a multi-touch gesture. */ 199 _sc_touchCenterY: null, 200 201 /** @private The timer used to fade out this scroller. */ 202 _sc_verticalFadeOutTimer: null, 203 204 /** @private The actual vertical scroll offset. */ 205 _sc_verticalScrollOffset: null, 206 207 /** @private The percentage offset scrolled vertically. Used to maintain the vertical position when the content size changes. */ 208 _sc_verticalPct: null, 209 210 /** @see SC.View.prototype.acceptsMultitouch 211 212 @type Boolean 213 @default true 214 */ 215 acceptsMultitouch: true, 216 217 /** @private Animation curves. Kept private b/c it will likely become a computed property. */ 218 animationCurveDecelerate: SC.easingCurve(0.35,0.34,0.84,1), // 'cubic-bezier(.35,.34,.84,1)', // http://cubic-bezier.com 219 220 /** @private Animation curves. Kept private b/c it will likely become a computed property. */ 221 animationCurveReverse: SC.easingCurve(0.45,-0.47,0.73,1.3), // 'cubic-bezier(0.45,-0.47,0.73,1.3)', 222 223 /** @private Animation curves. Kept private b/c it will likely become a computed property. */ 224 animationCurveSnap: SC.easingCurve(0.28,0.36,0.52,1), // 'cubic-bezier(.28,.36,.52,1)', 225 226 /** 227 If true, the horizontal scroller will automatically hide if the contentView is smaller than the 228 visible area. The `hasHorizontalScroller` property must be set to true in order for this property 229 to have any effect. 230 231 @type Boolean 232 @default true 233 */ 234 autohidesHorizontalScroller: true, 235 236 /** 237 If true, the vertical scroller will automatically hide if the contentView is smaller than the 238 visible area. The `hasVerticalScroller` property must be set to true in order for this property 239 to have any effect. 240 241 @type Boolean 242 @default true 243 */ 244 autohidesVerticalScroller: true, 245 246 /** 247 Determines whether scaling is allowed. 248 249 @type Boolean 250 @default false 251 */ 252 canScale: false, 253 254 /** 255 Returns true if the view has both a horizontal scroller and the scroller is visible. 256 257 @field 258 @type Boolean 259 @readonly 260 */ 261 canScrollHorizontal: function () { 262 return !!(this.get('hasHorizontalScroller') && // This property isn't bindable. 263 this.get('horizontalScrollerView') && // This property isn't bindable. 264 this.get('isHorizontalScrollerVisible')); 265 }.property('isHorizontalScrollerVisible').cacheable(), 266 267 /** 268 Returns true if the view has both a vertical scroller and the scroller is visible. 269 270 @field 271 @type Boolean 272 @readonly 273 */ 274 canScrollVertical: function () { 275 return !!(this.get('hasVerticalScroller') && // This property isn't bindable. 276 this.get('verticalScrollerView') && // This property isn't bindable. 277 this.get('isVerticalScrollerVisible')); 278 }.property('isVerticalScrollerVisible').cacheable(), 279 280 /** 281 @type Array 282 @default ['sc-scroll-view'] 283 @see SC.View#classNames 284 */ 285 classNames: ['sc-scroll-view'], 286 287 /** 288 The container view that will wrap your content view. You can replace this property with your own 289 custom class if you prefer. 290 291 @type SC.ContainerView 292 @default SC.ContainerView 293 */ 294 containerView: SC.ContainerView, 295 296 /** 297 The content view you want the scroll view to manage. 298 299 @type SC.View 300 @default null 301 */ 302 contentView: null, 303 304 /** 305 The scroll deceleration rate. 306 307 @type Number 308 @default SC.SCROLL.DRAG_SCROLL_DECELERATION 309 */ 310 decelerationRate: SC.SCROLL.DRAG_SCROLL_DECELERATION, 311 312 /** @private 313 Whether to delay touches from passing through to the content. By default, if the touch moves enough to 314 trigger a scroll within 150ms, this view will retain control of the touch, and content views will not 315 have a chance to handle it. This is generally the behavior you want. 316 317 If you set this to NO, the touch will not trigger a scroll until you pass control back to this view via 318 `touch.restoreLastTouchResponder`, for example when the touch has dragged by a certain amount. You should 319 use this option only if you know what you're doing. 320 321 @type Boolean 322 @default true 323 */ 324 delaysContentTouches: true, 325 326 /** 327 Determines how long (in seconds) scrollbars wait before fading out. 328 329 @property Number 330 @default 0.4 331 */ 332 fadeOutDelay: 0.4, 333 334 /** 335 True if the view should maintain a horizontal scroller. This property must be set when the 336 view is created. 337 338 @type Boolean 339 @default true 340 */ 341 hasHorizontalScroller: true, 342 343 /** 344 True if the view should maintain a vertical scroller. This property must be set when the 345 view is created. 346 347 @type Boolean 348 @default true 349 */ 350 hasVerticalScroller: true, 351 352 /** 353 The horizontal alignment for non-filling content inside of the ScrollView. Possible values: 354 355 - SC.ALIGN_LEFT 356 - SC.ALIGN_RIGHT 357 - SC.ALIGN_CENTER 358 359 @type String 360 @default SC.ALIGN_CENTER 361 */ 362 horizontalAlign: SC.ALIGN_CENTER, 363 364 /** 365 Determines whether the horizontal scroller should fade out while in overlay mode. Has no effect 366 if `horizontalOverlay` is set to false. 367 368 @property Boolean 369 @default true 370 */ 371 horizontalFade: true, 372 373 /** 374 Amount to scroll one horizontal line. 375 376 Used by the default implementation of scrollLeftLine() and 377 scrollRightLine(). 378 379 @type Number 380 @default 20 381 */ 382 horizontalLineScroll: 20, 383 384 /** 385 Use this to overlay the horizontal scroller. 386 387 This ensures that the content container will not resize to accommodate the horizontal scroller, 388 hence overlaying the scroller on top of the container. 389 390 @field 391 @type Boolean 392 @default true 393 */ 394 horizontalOverlay: false, 395 396 /** 397 Amount to scroll one horizontal page. 398 399 Used by the default implementation of scrollLeftPage() and scrollRightPage(). 400 401 @field 402 @type Number 403 @default value of frame.width 404 @observes frame 405 */ 406 horizontalPageScroll: function () { 407 return this.get('frame').width; 408 }.property('frame'), 409 410 /** 411 The current horizontal scroll offset. Changing this value will update both the position of the 412 contentView and the horizontal scroller, if there is one. 413 414 @field 415 @type Number 416 @default 0 417 */ 418 horizontalScrollOffset: function (key, value) { 419 var containerWidth = this._sc_containerWidth, 420 contentWidth = this._sc_contentWidth, 421 min = this.get('minimumHorizontalScrollOffset'), 422 max = this.get('maximumHorizontalScrollOffset'); 423 424 /* jshint eqnull:true */ 425 if (value != null) { 426 // When touch scrolling, we allow scroll to pass the limits by a small amount. 427 if (!this._sc_isTouchScrollingH) { 428 // Constrain to the set limits. 429 value = Math.max(min, Math.min(max, value)); 430 } 431 432 // Record the relative percentage offset for maintaining position while scaling. 433 if (contentWidth > 0) { 434 this._sc_horizontalPct = (value + (containerWidth / 2)) / contentWidth; 435 } 436 437 // Use the cached value. 438 } else { 439 value = this._sc_horizontalScrollOffset; 440 441 // Default value. 442 if (value == null) { 443 var horizontalAlign = this.get('initialHorizontalAlign'); 444 445 value = this._sc_alignedHorizontalOffset(horizontalAlign, containerWidth, contentWidth); 446 } 447 } 448 449 // Update the actual value. 450 this._sc_horizontalScrollOffset = value; 451 452 return value; 453 }.property().cacheable(), 454 455 /** 456 Use to control the positioning of the horizontal scroller. If you do not set 'horizontalOverlay' to 457 true, then the content view will be automatically sized to meet the left edge of the vertical 458 scroller, wherever it may be. 459 460 This allows you to easily, for example, have “one pixel higher and one pixel lower” scroll bars 461 that blend into their parent views. 462 463 If you do set 'horizontalOverlay' to true, then the scroller view will “float on top” of the content view. 464 465 Example: { left: -1, bottom: 0, right: -1 } 466 467 @type Object 468 @default null 469 */ 470 horizontalScrollerLayout: null, 471 472 /** 473 The horizontal scroller view class. This will be replaced with a view instance when the 474 ScrollView is created unless `hasHorizontalScroller` is false. 475 476 If `horizontalOverlay` is `true`, the default view used will be an SC.OverlayScrollerView, 477 otherwise SC.ScrollerView will be used. 478 479 @type SC.View 480 @default SC.ScrollerView | SC.OverlayScrollerView 481 */ 482 horizontalScrollerView: null, 483 484 /** 485 Your content view's initial horizontal alignment, if wider than the container. This allows you to e.g. 486 center the content view when zoomed out, but begin with it zoomed in and left-aligned. If not specified, 487 defaults to value of `horizontalAlign`. May be: 488 489 - SC.ALIGN_LEFT 490 - SC.ALIGN_RIGHT 491 - SC.ALIGN_CENTER 492 493 @type String 494 @default SC.ALIGN_LEFT 495 */ 496 initialHorizontalAlign: SC.outlet('horizontalAlign'), 497 498 /** 499 Your content view's initial vertical alignment, if taller than the container. This allows you to e.g. 500 center the content view when zoomed out, but begin with it zoomed in and top-aligned. If not specified, 501 defaults to the value of `verticalAlign`. May be: 502 503 - SC.ALIGN_TOP 504 - SC.ALIGN_BOTTOM 505 - SC.ALIGN_MIDDLE 506 507 @type String 508 @default SC.ALIGN_TOP 509 */ 510 initialVerticalAlign: SC.outlet('verticalAlign'), 511 512 /** 513 True if the horizontal scroller should be visible. You can change this property value anytime to 514 show or hide the horizontal scroller. If you do not want to use a horizontal scroller at all, you 515 should instead set `hasHorizontalScroller` to false to avoid creating a scroller view in the first 516 place. 517 518 @type Boolean 519 @default true 520 */ 521 isHorizontalScrollerVisible: true, 522 523 /** 524 Walk like a duck. 525 526 @type Boolean 527 @default true 528 @readOnly 529 */ 530 isScrollable: true, 531 532 /** 533 True if the vertical scroller should be visible. You can change this property value anytime to 534 show or hide the vertical scroller. If you do not want to use a vertical scroller at all, you 535 should instead set `hasVerticalScroller` to false to avoid creating a scroller view in the first 536 place. 537 538 @type Boolean 539 @default true 540 */ 541 isVerticalScrollerVisible: true, 542 543 /** 544 The maximum horizontal scroll offset allowed given the current contentView size and the size of 545 the scroll view. If horizontal scrolling is disabled, this will always return 0. 546 547 @field 548 @type Number 549 @default 0 550 */ 551 maximumHorizontalScrollOffset: function () { 552 return Math.max(this._sc_contentWidth - this._sc_containerWidth, 0); 553 }.property('_sc_containerWidth', '_sc_contentWidth').cacheable(), 554 555 /** 556 The maximum scale. 557 558 @type Number 559 @default 2.0 560 */ 561 maximumScale: 2.0, 562 563 /** 564 The maximum vertical scroll offset allowed given the current contentView size and the size of 565 the scroll view. If vertical scrolling is disabled, this will always return 0 (or whatever 566 alignment dictates). 567 568 @field 569 @type Number 570 @default 0 571 */ 572 maximumVerticalScrollOffset: function () { 573 return Math.max(this._sc_contentHeight - this._sc_containerHeight, 0); 574 }.property('_sc_containerHeight', '_sc_contentHeight').cacheable(), 575 576 /** 577 The minimum horizontal scroll offset allowed given the current contentView size and the size of 578 the scroll view. If horizontal scrolling is disabled, this will always return 0 (or whatever alignment dictates). 579 580 @field 581 @type Number 582 @default 0 583 */ 584 minimumHorizontalScrollOffset: function () { 585 return Math.min(this._sc_contentWidth - this._sc_containerWidth, 0); 586 }.property('_sc_containerWidth', '_sc_contentWidth').cacheable(), 587 588 /** 589 The minimum scale. 590 591 @type Number 592 @default 0.25 593 */ 594 minimumScale: 0.25, 595 596 /** 597 The minimum vertical scroll offset allowed given the current contentView size and the size of 598 the scroll view. If vertical scrolling is disabled, this will always return 0 (or whatever alignment dictates). 599 600 @field 601 @type Number 602 @default 0 603 */ 604 minimumVerticalScrollOffset: function () { 605 return Math.min(this._sc_contentHeight - this._sc_containerHeight, 0); 606 }.property('_sc_containerHeight', '_sc_contentHeight').cacheable(), 607 608 /** 609 The current scale. Setting this will adjust the scale of the contentView. 610 611 If the contentView implements the SC.Scalable protocol, it will instead pass the scale to the contentView's 612 `applyScale` method instead. 613 614 Note that on platforms that allow bounce, setting scale outside of the minimum/maximumScale bounds will 615 result in a bounce. It is up to the developer to alert this view when the action is over and it should 616 bounce back. 617 618 @field 619 @type Number 620 @default 1.0 621 */ 622 scale: function (key, value) { 623 /* jshint eqnull:true */ 624 if (value != null) { 625 if (!this.get('canScale')) { 626 value = 1; 627 } else { 628 var min = this.get('minimumScale'), 629 max = this.get('maximumScale'); 630 631 // When touch scaling, we allow scaling to pass the limits by a small amount. 632 if (this._sc_isTouchScaling) { 633 min = min - (min * 0.1); 634 max = max + (max * 0.1); 635 if ((value < min || value > max)) { 636 value = Math.min(Math.max(min, value), max); 637 } 638 639 // Constrain to the set limits. 640 } else { 641 if ((value < min || value > max)) { 642 value = Math.min(Math.max(min, value), max); 643 } 644 } 645 } 646 } else { 647 value = this._sc_scale; 648 } 649 650 // Update the actual value. 651 this._sc_scale = value; 652 653 return value; 654 }.property('canScale', 'minimumScale', 'maximumScale').cacheable(), 655 656 /** 657 This determines how much a gesture must pinch or spread apart (in pixels) before it is registered as a scale action. 658 659 You can change this value for all instances of SC.ScrollView in your application by overriding 660 `SC.SCROLL.SCALE_GESTURE_THRESHOLD` at launch time. 661 662 @type Number 663 @default SC.SCROLL.SCALE_GESTURE_THRESHOLD 664 */ 665 scaleGestureThreshold: SC.SCROLL.SCALE_GESTURE_THRESHOLD, 666 667 /** 668 This determines how far (in pixels) a gesture must move before it is registered as a scroll. 669 670 You can change this value for all instances of SC.ScrollView in your application by overriding 671 `SC.SCROLL.SCROLL_THRESHOLD` at launch time. 672 673 @type Number 674 @default SC.SCROLL.SCROLL_GESTURE_THRESHOLD 675 */ 676 scrollGestureThreshold: SC.SCROLL.SCROLL_GESTURE_THRESHOLD, 677 678 /** 679 Once a vertical or horizontal scroll has been triggered, this determines how far (in pixels) the gesture 680 must move on the other axis to trigger a two-axis scroll. If your scroll view's content is omnidirectional 681 (e.g. a map) you should set this value to 0. 682 683 You can change this value for all instances of SC.ScrollView in your application by overriding 684 `SC.SCROLL.SCROLL_LOCK_GESTURE_THRESHOLD` at launch time. 685 686 @type Number 687 @default SC.SCROLL.SCROLL_LOCK_GESTURE_THRESHOLD 688 */ 689 scrollLockGestureThreshold: SC.SCROLL.SCROLL_LOCK_GESTURE_THRESHOLD, 690 691 /** @private 692 Once a vertical or horizontal scroll has been triggered, this determines how far (in pixels) the gesture 693 must move on the other axis to trigger a two-axis scroll. If your scroll view's content is omnidirectional 694 (e.g. a map) you should set this value to 0. 695 696 You can change this value for all instances of SC.ScrollView in your application by overriding 697 `SC.SCROLL.TOUCH.DEFAULT_SECONDARY_SCROLL_THRESHOLD` at launch time. 698 699 @type Number 700 @default SC.SCROLL.TOUCH.DEFAULT_SECONDARY_SCROLL_THRESHOLD 701 */ 702 // scrollSecondaryGestureThreshold: SC.SCROLL.TOUCH.DEFAULT_SECONDARY_SCROLL_THRESHOLD, 703 704 /** 705 The vertical alignment for non-filling content inside of the ScrollView. Possible values: 706 707 - SC.ALIGN_TOP 708 - SC.ALIGN_BOTTOM 709 - SC.ALIGN_MIDDLE 710 711 @type String 712 @default SC.ALIGN_TOP 713 */ 714 verticalAlign: SC.ALIGN_TOP, 715 716 /** 717 Determines whether the vertical scroller should fade out while in overlay mode. Has no effect if 718 `verticalOverlay` is set to false. 719 720 @property Boolean 721 @default true 722 */ 723 verticalFade: true, 724 725 /** 726 Amount to scroll one vertical line. 727 728 Used by the default implementation of scrollDownLine() and scrollUpLine(). 729 730 @type Number 731 @default 20 732 */ 733 verticalLineScroll: 20, 734 735 /** 736 Use this to overlay the vertical scroller. 737 738 This ensures that the content container will not resize to accommodate the vertical scroller, 739 hence overlaying the scroller on top of the container. 740 741 @field 742 @type Boolean 743 @default true 744 */ 745 verticalOverlay: false, 746 747 /** 748 Amount to scroll one vertical page. 749 750 Used by the default implementation of scrollUpPage() and scrollDownPage(). 751 752 @field 753 @type Number 754 @default value of frame.height 755 @observes frame 756 */ 757 verticalPageScroll: function () { 758 return this.get('frame').height; 759 }.property('frame'), 760 761 /** 762 The current vertical scroll offset. Changing this value will update both the position of the 763 contentView and the vertical scroller, if there is one. 764 765 @field 766 @type Number 767 @default 0 768 */ 769 verticalScrollOffset: function (key, value) { 770 var containerHeight = this._sc_containerHeight, 771 contentHeight = this._sc_contentHeight, 772 min = this.get('minimumVerticalScrollOffset'), 773 max = this.get('maximumVerticalScrollOffset'); 774 775 /* jshint eqnull:true */ 776 if (value != null) { 777 778 // When touch scrolling, we allow scroll to pass the limits by a small amount. 779 if (!this._sc_isTouchScrollingV) { 780 // Constrain to the set limits. 781 value = Math.max(min, Math.min(max, value)); 782 } 783 784 // Record the relative percentage offset for maintaining position while scaling. 785 if (contentHeight > 0) { 786 this._sc_verticalPct = (value + (containerHeight / 2)) / contentHeight; 787 } 788 789 // Use the cached value. 790 } else { 791 value = this._sc_verticalScrollOffset; 792 793 // Default value. 794 if (value == null) { 795 var verticalAlign = this.get('initialVerticalAlign'); 796 797 value = this._sc_alignedVerticalOffset(verticalAlign, containerHeight, contentHeight); 798 } 799 } 800 801 // Update the actual value. 802 this._sc_verticalScrollOffset = value; 803 804 return value; 805 }.property().cacheable(), 806 807 /** 808 Use to control the positioning of the vertical scroller. If you do not set 'verticalOverlay' to 809 true, then the content view will be automatically sized to meet the left edge of the vertical 810 scroller, wherever it may be. 811 812 This allows you to easily, for example, have “one pixel higher and one pixel lower” scroll bars 813 that blend into their parent views. 814 815 If you do set 'verticalOverlay' to true, then the scroller view will “float on top” of the content view. 816 817 Example: { top: -1, bottom: -1, right: 0 } 818 819 @type Object 820 @default null 821 */ 822 verticalScrollerLayout: null, 823 824 /** 825 The vertical scroller view class. This will be replaced with a view instance when the 826 ScrollView is created unless `hasVerticalScroller` is false. 827 828 If `verticalOverlay` is `true`, the default view used will be an SC.OverlayScrollerView, 829 otherwise SC.ScrollerView will be used. 830 831 @type SC.View 832 @default SC.ScrollerView | SC.OverlayScrollerView 833 */ 834 verticalScrollerView: null, 835 836 // --------------------------------------------------------------------------------------------- 837 // Methods 838 // 839 840 /** @private Aligns the content horizontally. */ 841 _sc_alignedHorizontalOffset: function (horizontalAlign, containerWidth, contentWidth) { 842 switch (horizontalAlign) { 843 case SC.ALIGN_RIGHT: 844 return 0 - (containerWidth - contentWidth); 845 case SC.ALIGN_CENTER: 846 return 0 - ((containerWidth - contentWidth) / 2); 847 default: // SC.ALIGN_LEFT 848 return 0; 849 } 850 }, 851 852 /** @private Aligns the content vertically. */ 853 _sc_alignedVerticalOffset: function (verticalAlign, containerHeight, contentHeight) { 854 switch (verticalAlign) { 855 case SC.ALIGN_BOTTOM: 856 return 0 - (containerHeight - contentHeight); 857 case SC.ALIGN_MIDDLE: 858 return 0 - ((containerHeight - contentHeight) / 2); 859 default: // SC.ALIGN_TOP 860 return 0; 861 } 862 }, 863 864 /** @private Manually animates the content view. */ 865 _sc_animateContentView: function (contentAdjustMap) { 866 var easingCurve = this._sc_animationTiming, 867 totalDuration = this._sc_animationDuration * 1000, 868 start = new Date(), 869 contentView = this.get('contentView'), 870 contentViewLayout = contentView.get('layout'), 871 leftStart = contentViewLayout.left, 872 leftDelta = contentAdjustMap.left - leftStart, 873 scaleStart = contentViewLayout.scale == null ? 1 : contentViewLayout.scale, 874 scaleDelta = contentAdjustMap.scale - scaleStart, 875 topStart = contentViewLayout.top, 876 topDelta = contentAdjustMap.top - topStart, 877 self = this; 878 879 function animationFrame() { 880 if (self._sc_isAnimating) { 881 var duration = new Date() - start, 882 percent = Math.min(duration / totalDuration, 1); // Capped at 100% 883 884 SC.run(function () { 885 var currentLeft = leftStart + leftDelta * easingCurve.value(percent), 886 currentScale = scaleStart + scaleDelta * easingCurve.value(percent), 887 currentTop = topStart + topDelta * easingCurve.value(percent); 888 889 contentAdjustMap.left = currentLeft; 890 contentAdjustMap.top = currentTop; 891 contentAdjustMap.scale = currentScale; 892 893 contentView.adjust(contentAdjustMap); 894 }); 895 896 // Keep animating as long as we haven't hit 100%. 897 if (percent < 1) { 898 window.requestAnimationFrame(animationFrame); 899 } else { 900 // Clear out the animation flags. 901 self._sc_isAnimating = false; 902 self._sc_animationDuration = null; 903 self._sc_animationTiming = null; 904 } 905 } 906 } 907 908 // Start the animation. 909 self._sc_isAnimating = true; 910 animationFrame(); 911 }, 912 913 /* @private Cancels any content view animation if it exists. */ 914 _sc_cancelAnimation: function () { 915 if (this._sc_isAnimating) { 916 var contentView = this.get('contentView'); 917 918 // UNUSED. Animate using SC.View.prototype.animate. Cancelling the animation in place proved problematic. 919 // if (contentView.get('viewState') === SC.CoreView.ATTACHED_SHOWN_ANIMATING) { 920 // // Stop the animation in place. 921 // contentView.cancelAnimation(SC.LayoutState.CURRENT); 922 923 var curLayout = contentView.get('layout'); 924 925 // Update offsets to match actual placement. 926 this.set('horizontalScrollOffset', -curLayout.left); 927 this.set('verticalScrollOffset', -curLayout.top); 928 this.set('scale', curLayout.scale); 929 930 // Clear out the animation flags. 931 this._sc_isAnimating = false; 932 this._sc_animationDuration = null; 933 this._sc_animationTiming = null; 934 } 935 936 }, 937 938 /** @private Reposition our content view if necessary according to aligment. */ 939 _sc_containerViewFrameDidChange: function () { 940 // Run the content view size change code (updates min & max offsets, sets content alignment if necessary, shows scrollers if necessary) 941 var containerFrame = this.getPath('containerView.frame'), 942 contentView = this.get('contentView'), 943 lastMaximumHorizontalScrollOffset = this.get('maximumHorizontalScrollOffset'), 944 lastMaximumVerticalScrollOffset = this.get('maximumVerticalScrollOffset'), 945 lastMinimumHorizontalScrollOffset = this.get('minimumHorizontalScrollOffset'), 946 lastMinimumVerticalScrollOffset = this.get('minimumVerticalScrollOffset'); 947 948 // Cache the current height and width of the container view, so we can only watch for size changes. 949 // This will update the maximum scroll offsets when they are requested. 950 this.set('_sc_containerHeight', containerFrame.height); 951 this.set('_sc_containerWidth', containerFrame.width); 952 953 if (contentView) { 954 // var didAdjust = false; 955 956 // if (this._sc_shouldResizeContentHeight) { 957 // contentView.adjust('height', containerFrame.height); 958 // didAdjust = true; 959 // } 960 961 // if (this._sc_shouldResizeContentWidth) { 962 // contentView.adjust('width', containerFrame.width); 963 // didAdjust = true; 964 // } 965 966 // Update the scrollers regardless. 967 // if (!didAdjust) { 968 this._sc_contentViewSizeDidChange(lastMinimumHorizontalScrollOffset, lastMaximumHorizontalScrollOffset, lastMinimumVerticalScrollOffset, lastMaximumVerticalScrollOffset); 969 // } 970 } 971 972 }, 973 974 /** @private Whenever the contentView of the container changes, set up new observers and clean up old observers. */ 975 _sc_contentViewDidChange: function () { 976 var newView = this.get('contentView'), // Our content view. 977 containerView = this.get('containerView'), 978 frameChangeFunc = this._sc_contentViewFrameDidChange; 979 980 // Clean up observers on the previous content view. 981 this._sc_removeContentViewObservers(); 982 983 // Reset caches. 984 // this._sc_shouldResizeContentWidth = false; 985 // this._sc_shouldResizeContentHeight = false; 986 this._sc_contentHeight = 0; 987 this._sc_contentWidth = 0; 988 this._sc_contentScale = undefined; 989 990 // Assign the content view to our container view. This ensures that it is instantiated. 991 containerView.set('contentView', newView); 992 newView = this.contentView = containerView.get('contentView'); // Actual content view. 993 994 if (newView) { 995 /* jshint eqnull:true */ 996 997 // Be wary of content views that replace their layers. 998 // newView.addObserver('layer', this, layerChangeFunc); 999 1000 if (!newView.useStaticLayout) { 1001 // When a view wants an accelerated layer and isn't a fixed size, we convert it to a fixed 1002 // size and resize it when our container resizes. 1003 // if (newView.get('wantsAcceleratedLayer') && !newView.get('isFixedSize')) { 1004 // var contentViewLayout = newView.get('layout'); 1005 1006 // // Fix the width. 1007 // if (contentViewLayout.width == null) { 1008 // this._sc_shouldResizeContentWidth = true; // Flag to indicate that when the container's width changes, we should update the content's width. 1009 1010 // newView.adjust({ 1011 // right: null, 1012 // width: this._sc_containerWidth 1013 // }); 1014 // } 1015 1016 // // Fix the height. 1017 // if (contentViewLayout.height == null) { 1018 // this._sc_shouldResizeContentHeight = true; // Flag to indicate that when the container's height changes, we should update the content's height. 1019 1020 // newView.adjust({ 1021 // bottom: null, 1022 // height: this._sc_containerHeight 1023 // }); 1024 // } 1025 // } 1026 } 1027 1028 // TODO: Can we remove this if a calculated property exists? 1029 newView.addObserver('frame', this, frameChangeFunc); 1030 1031 // Initialize once. 1032 this._sc_contentViewFrameDidChange(); 1033 } 1034 1035 // Cache the current content view so that we can properly clean up when it changes. 1036 this._sc_contentView = newView; 1037 }, 1038 1039 /** @private */ 1040 // _sc_contentViewLayerDidChange: function () { 1041 // ??? 1042 // }, 1043 1044 /** @private Check frame changes for size changes. */ 1045 _sc_contentViewFrameDidChange: function () { 1046 var lastHeight = this._sc_contentHeight, 1047 lastScale = this._sc_contentScale, 1048 lastWidth = this._sc_contentWidth, 1049 newFrame = this.getPath('contentView.borderFrame'), 1050 lastMaximumHorizontalScrollOffset = this.get('maximumHorizontalScrollOffset'), 1051 lastMaximumVerticalScrollOffset = this.get('maximumVerticalScrollOffset'), 1052 lastMinimumHorizontalScrollOffset = this.get('minimumHorizontalScrollOffset'), 1053 lastMinimumVerticalScrollOffset = this.get('minimumVerticalScrollOffset'); 1054 1055 if (newFrame) { 1056 // Determine whether the scale has changed. 1057 if (lastScale !== newFrame.scale) { 1058 this._sc_contentScaleDidChange = true; 1059 this.set('_sc_contentScale', newFrame.scale); 1060 } 1061 1062 if (lastWidth !== newFrame.width) { 1063 this._sc_contentWidthDidChange = true; 1064 this.set('_sc_contentWidth', newFrame.width); 1065 } 1066 1067 if (lastHeight !== newFrame.height) { 1068 this._sc_contentHeightDidChange = true; 1069 this.set('_sc_contentHeight', newFrame.height); 1070 } 1071 1072 // If any of the size values changed, update. 1073 if (this._sc_contentScaleDidChange || this._sc_contentWidthDidChange || this._sc_contentHeightDidChange) { 1074 this._sc_contentViewSizeDidChange(lastMinimumHorizontalScrollOffset, lastMaximumHorizontalScrollOffset, lastMinimumVerticalScrollOffset, lastMaximumVerticalScrollOffset); 1075 } 1076 } 1077 }, 1078 1079 /** @private When the content view's size changes, we need to update our scroll offset properties. */ 1080 _sc_contentViewSizeDidChange: function (lastMinimumHorizontalScrollOffset, lastMaximumHorizontalScrollOffset, lastMinimumVerticalScrollOffset, lastMaximumVerticalScrollOffset) { 1081 var maximumHorizontalScrollOffset = this.get('maximumHorizontalScrollOffset'), 1082 maximumVerticalScrollOffset = this.get('maximumVerticalScrollOffset'), 1083 containerHeight, containerWidth, 1084 contentHeight, contentWidth; 1085 1086 containerHeight = this._sc_containerHeight; 1087 containerWidth = this._sc_containerWidth; 1088 contentHeight = this._sc_contentHeight; 1089 contentWidth = this._sc_contentWidth; 1090 1091 var value; 1092 if (contentWidth) { 1093 if (maximumHorizontalScrollOffset === 0) { 1094 // Align horizontally. 1095 value = this._sc_alignedHorizontalOffset(this.get('horizontalAlign'), containerWidth, contentWidth); 1096 this.set('horizontalScrollOffset', value); // Note: Trigger for _sc_scrollOffsetHorizontalDidChange 1097 1098 } else { 1099 /* jshint eqnull:true */ 1100 // If the horizontal position has never been set, use the initial alignment. 1101 if (this._sc_horizontalPct == null) { 1102 this._sc_horizontalScrollOffset = null; 1103 this.notifyPropertyChange('horizontalScrollOffset'); 1104 1105 // If the scale of the content view changes, we want to maintain relative position so that zooming remains centered. 1106 } else if (this._sc_contentScaleDidChange) { 1107 if (this._sc_touchCenterX != null) { 1108 value = (this._sc_horizontalPct * contentWidth) - this._sc_touchCenterX; 1109 } else { 1110 value = (this._sc_horizontalPct * contentWidth) - (containerWidth / 2); 1111 } 1112 this.set('horizontalScrollOffset', value); // Note: Trigger for _sc_scrollOffsetHorizontalDidChange 1113 1114 // Live scale gesture. Update the anchor so that the scroll deltas are calculated correctly. 1115 if (this._sc_gestureAnchorHOffset != null) { 1116 this._sc_gestureAnchorHOffset = value; 1117 } 1118 } else if (this.get('canScrollHorizontal')) { 1119 // Take alignment into account. 1120 var horizontalAlign = this.get('horizontalAlign'), 1121 horizontalScrollOffset = this._sc_horizontalScrollOffset, 1122 minimumHorizontalScrollOffset = this.get('minimumHorizontalScrollOffset'); 1123 1124 switch (horizontalAlign) { 1125 case SC.ALIGN_CENTER: 1126 // Switched to scrolling horizontally, stick to center OR was scrolled at center and size changed. 1127 if ((lastMinimumHorizontalScrollOffset < 0 && minimumHorizontalScrollOffset === 0) || 1128 (horizontalScrollOffset === lastMaximumHorizontalScrollOffset / 2)) { 1129 this.set('horizontalScrollOffset', maximumHorizontalScrollOffset / 2); 1130 } 1131 1132 break; 1133 case SC.ALIGN_RIGHT: 1134 // Switched to scrolling horizontally, stick to right side OR was scrolled to right and size changed. 1135 if ((lastMinimumHorizontalScrollOffset < 0 && minimumHorizontalScrollOffset === 0) || 1136 (horizontalScrollOffset === lastMaximumHorizontalScrollOffset)) { 1137 this.set('horizontalScrollOffset', maximumHorizontalScrollOffset); 1138 } 1139 1140 break; 1141 } 1142 1143 // Was at right side and size shrunk. 1144 if (horizontalScrollOffset > maximumHorizontalScrollOffset) { 1145 this.set('horizontalScrollOffset', maximumHorizontalScrollOffset); 1146 } 1147 } 1148 } 1149 } 1150 1151 if (contentHeight) { 1152 if (maximumVerticalScrollOffset === 0) { 1153 // Align vertically. 1154 value = this._sc_alignedVerticalOffset(this.get('verticalAlign'), containerHeight, contentHeight); 1155 this.set('verticalScrollOffset', value); // Note: Trigger for _sc_scrollOffsetHorizontalDidChange 1156 1157 } else { 1158 /* jshint eqnull:true */ 1159 // If the vertical position has never been set, use the initial alignment. 1160 if (this._sc_verticalPct == null) { 1161 this._sc_verticalScrollOffset = null; 1162 this.notifyPropertyChange('verticalScrollOffset'); 1163 1164 // If the scale of the content view changes, we want to maintain relative position so that zooming remains centered. 1165 } else if (this._sc_contentScaleDidChange) { 1166 if (this._sc_touchCenterY != null) { 1167 value = (this._sc_verticalPct * contentHeight) - this._sc_touchCenterY; 1168 } else { 1169 value = (this._sc_verticalPct * contentHeight) - (containerHeight / 2); 1170 } 1171 this.set('verticalScrollOffset', value); // Note: Trigger for _sc_scrollOffsetVerticalDidChange 1172 1173 // Live scale gesture. Update the anchor so that the scroll deltas are calculated correctly. 1174 if (this._sc_gestureAnchorVOffset != null) { 1175 this._sc_gestureAnchorVOffset = value; 1176 } 1177 } else if (this.get('canScrollVertical')) { 1178 var verticalAlign = this.get('verticalAlign'), 1179 verticalScrollOffset = this._sc_verticalScrollOffset, 1180 minimumVerticalScrollOffset = this.get('minimumVerticalScrollOffset'); 1181 1182 switch (verticalAlign) { 1183 case SC.ALIGN_MIDDLE: 1184 // Switched to scrolling vertically, stick to middle OR was scrolled at middle and size changed. 1185 if ((lastMinimumVerticalScrollOffset < 0 && minimumVerticalScrollOffset === 0) || 1186 (verticalScrollOffset === lastMaximumVerticalScrollOffset / 2)) { 1187 this.set('verticalScrollOffset', maximumVerticalScrollOffset / 2); 1188 } 1189 1190 break; 1191 case SC.ALIGN_BOTTOM: 1192 // Switched to scrolling vertically, stick to bottom side OR was scrolled to bottom and size changed. 1193 if ((lastMinimumVerticalScrollOffset < 0 && minimumVerticalScrollOffset === 0) || 1194 (verticalScrollOffset === lastMaximumVerticalScrollOffset)) { 1195 this.set('verticalScrollOffset', maximumVerticalScrollOffset); 1196 } 1197 1198 break; 1199 } 1200 1201 // Was at bottom side and size shrunk. 1202 if (verticalScrollOffset > maximumVerticalScrollOffset) { 1203 this.set('verticalScrollOffset', maximumVerticalScrollOffset); 1204 } 1205 } 1206 } 1207 } 1208 1209 // Reset our flags. 1210 this._sc_contentScaleDidChange = false; 1211 this._sc_contentHeightDidChange = false; 1212 this._sc_contentWidthDidChange = false; 1213 1214 // TODO: Updating scrollers may change the container size, which will cause this to run again. Can we bring 1215 // this into a single call? 1216 this._sc_updateScrollers(); 1217 }, 1218 1219 /** @private Fade in the horizontal scroller. Each scroller fades in/out independently. */ 1220 _sc_fadeInHorizontalScroller: function () { 1221 var canScrollHorizontal = this.get('canScrollHorizontal'), 1222 horizontalScroller = this.get('horizontalScrollerView'), 1223 delay; 1224 1225 if (canScrollHorizontal && horizontalScroller && horizontalScroller.get('fadeIn')) { 1226 if (this._sc_horizontalFadeOutTimer) { 1227 // Reschedule the current timer (avoid creating a new instance). 1228 this._sc_horizontalFadeOutTimer.startTime = null; 1229 this._sc_horizontalFadeOutTimer.schedule(); 1230 } else { 1231 // Fade in. 1232 horizontalScroller.fadeIn(); 1233 1234 // Wait the minimum time before fading out again. 1235 delay = this.get('_sc_minimumFadeOutDelay'); 1236 this._sc_horizontalFadeOutTimer = this.invokeLater(this._sc_fadeOutHorizontalScroller, delay); 1237 } 1238 } 1239 }, 1240 1241 /** @private Fade in the vertical scroller. Each scroller fades in/out independently. */ 1242 _sc_fadeInVerticalScroller: function () { 1243 var canScrollVertical = this.get('canScrollVertical'), 1244 verticalScroller = this.get('verticalScrollerView'), 1245 delay; 1246 1247 if (canScrollVertical && verticalScroller && verticalScroller.get('fadeIn')) { 1248 if (this._sc_verticalFadeOutTimer) { 1249 // Reschedule the current timer (avoid creating a new instance). 1250 this._sc_verticalFadeOutTimer.startTime = null; 1251 this._sc_verticalFadeOutTimer.schedule(); 1252 1253 } else { 1254 // Fade in. 1255 verticalScroller.fadeIn(); 1256 1257 // Wait the minimum time before fading out again. 1258 delay = this.get('_sc_minimumFadeOutDelay'); 1259 this._sc_verticalFadeOutTimer = this.invokeLater(this._sc_fadeOutVerticalScroller, delay); 1260 } 1261 } 1262 }, 1263 1264 /** @private Fade out the horizontal scroller. */ 1265 _sc_fadeOutHorizontalScroller: function () { 1266 var horizontalScroller = this.get('horizontalScrollerView'); 1267 1268 if (horizontalScroller && horizontalScroller.get('fadeOut')) { 1269 // Fade out. 1270 horizontalScroller.fadeOut(); 1271 } 1272 1273 this._sc_horizontalFadeOutTimer = null; 1274 }, 1275 1276 /** @private Fade out the vertical scroller. */ 1277 _sc_fadeOutVerticalScroller: function () { 1278 var verticalScroller = this.get('verticalScrollerView'); 1279 1280 if (verticalScroller && verticalScroller.get('fadeOut')) { 1281 // Fade out. 1282 verticalScroller.fadeOut(); 1283 } 1284 1285 this._sc_verticalFadeOutTimer = null; 1286 }, 1287 1288 /** @private Adjust the content alignment horizontally on change. */ 1289 _sc_horizontalAlignDidChange: function () { 1290 var maximumHorizontalScrollOffset = this.get('maximumHorizontalScrollOffset'); 1291 1292 // Align horizontally (Unless content width is zero). 1293 if (maximumHorizontalScrollOffset === 0 && this._sc_contentWidth) { 1294 var horizontalAlign = this.get('horizontalAlign'), 1295 value; 1296 1297 value = this._sc_alignedHorizontalOffset(horizontalAlign, this._sc_containerWidth, this._sc_contentWidth); 1298 this.set('horizontalScrollOffset', value); 1299 } 1300 }, 1301 1302 /** @private 1303 Calculates the maximum offset given content and container sizes, and the 1304 alignment. 1305 */ 1306 _sc_maximumScrollOffset: function (contentSize, containerSize, align, canScroll) { 1307 // If we can't scroll, we pretend our content size is no larger than the container. 1308 if (canScroll === NO) contentSize = Math.min(contentSize, containerSize); 1309 1310 // if our content size is larger than or the same size as the container, it's quite 1311 // simple to calculate the answer. Otherwise, we need to do some fancy-pants 1312 // alignment logic (read: simple math) 1313 if (contentSize >= containerSize) return contentSize - containerSize; 1314 1315 // alignment, yeah 1316 if (align === SC.ALIGN_LEFT || align === SC.ALIGN_TOP) { 1317 // if we left-align something, and it is smaller than the view, does that not mean 1318 // that it's maximum (and minimum) offset is 0, because it should be positioned at 0? 1319 return 0; 1320 } else if (align === SC.ALIGN_MIDDLE || align === SC.ALIGN_CENTER) { 1321 // middle align means the difference divided by two, because we want equal parts on each side. 1322 return 0 - Math.round((containerSize - contentSize) / 2); 1323 } else { 1324 // right align means the entire difference, because we want all that space on the left 1325 return 0 - (containerSize - contentSize); 1326 } 1327 }, 1328 1329 /** @private 1330 Calculates the minimum offset given content and container sizes, and the 1331 alignment. 1332 */ 1333 _sc_minimumScrollOffset: function (contentSize, containerSize, align, canScroll) { 1334 // If we can't scroll, we pretend our content size is no larger than the container. 1335 if (canScroll === NO) contentSize = Math.min(contentSize, containerSize); 1336 1337 // if the content is larger than the container, we have no need to change the minimum 1338 // away from the natural 0 position. 1339 if (contentSize > containerSize) return 0; 1340 1341 // alignment, yeah 1342 if (align === SC.ALIGN_LEFT || align === SC.ALIGN_TOP) { 1343 // if we left-align something, and it is smaller than the view, does that not mean 1344 // that it's maximum (and minimum) offset is 0, because it should be positioned at 0? 1345 return 0; 1346 } else if (align === SC.ALIGN_MIDDLE || align === SC.ALIGN_CENTER) { 1347 // middle align means the difference divided by two, because we want equal parts on each side. 1348 return 0 - Math.round((containerSize - contentSize) / 2); 1349 } else { 1350 // right align means the entire difference, because we want all that space on the left 1351 return 0 - (containerSize - contentSize); 1352 } 1353 }, 1354 1355 /** @private Registers/deregisters view with SC.Drag for autoscrolling. */ 1356 _sc_registerAutoscroll: function () { 1357 if (this.get('isVisibleInWindow') && this.get('isEnabledInPane')) { 1358 SC.Drag.addScrollableView(this); 1359 } else { 1360 SC.Drag.removeScrollableView(this); 1361 } 1362 }, 1363 1364 /** @private Reposition the content view to match the current scroll offsets and scale. */ 1365 _sc_repositionContentView: function () { 1366 var contentView = this.get('contentView'); 1367 1368 if (contentView) { 1369 this.invokeOnce(this._sc_repositionContentViewUnfiltered); 1370 } 1371 }, 1372 1373 /** @private Reposition the content view to match the current scroll offsets and scale. */ 1374 _sc_repositionContentViewUnfiltered: function () { 1375 var containerView = this.get('containerView'), 1376 contentView = this.get('contentView'), 1377 horizontalScrollOffset = this.get('horizontalScrollOffset'), 1378 verticalScrollOffset = this.get('verticalScrollOffset'), 1379 scale = this.get('scale'); 1380 1381 // If the content is statically laid out, use margins in the container layer to move it. 1382 // TODO: Remove static layout support. 1383 if (contentView.useStaticLayout) { 1384 var containerViewLayer = containerView.get('layer'); 1385 1386 // Set the margins on the layer. 1387 containerViewLayer.style.marginLeft = -horizontalScrollOffset + 'px'; 1388 containerViewLayer.style.marginTop = -verticalScrollOffset + 'px'; 1389 1390 // Otherwise call adjust on the content. 1391 } else { 1392 // Constrain the offsets to full (actual) pixels to prevent blurry text et cetera. Note that this assumes 1393 // that the scroll view itself is living at a scale of 1, and may give blurry subpixel results if scaled. 1394 var horizontalAlign = this.get('horizontalAlign'), 1395 verticalAlign = this.get('verticalAlign'), 1396 left, top; 1397 1398 left = -horizontalScrollOffset; 1399 1400 // Round according to the alignment to avoid jitter at the edges. For example, we don't want 0.55 rounding up to 1 when left aligned. This also prevents implied percentage values (i.e. 0.0 > value > 1.0 == %)! 1401 switch (horizontalAlign) { 1402 case SC.ALIGN_CENTER: 1403 left = Math.round(left); 1404 break; 1405 case SC.ALIGN_RIGHT: 1406 left = Math.ceil(left); 1407 break; 1408 default: // SC.ALIGN_LEFT 1409 left = Math.floor(left); 1410 } 1411 1412 top = -verticalScrollOffset; 1413 1414 switch (verticalAlign) { 1415 case SC.ALIGN_MIDDLE: 1416 top = Math.round(top); 1417 break; 1418 case SC.ALIGN_BOTTOM: 1419 top = Math.ceil(top); 1420 break; 1421 default: // SC.ALIGN_TOP 1422 top = Math.floor(top); 1423 } 1424 1425 // Cancel any active animation in place. 1426 // this._sc_cancelAnimation(); 1427 1428 var contentAdjustMap = SC.ScrollView._SC_CONTENT_ADJUST_MAP; // Shared object used to avoid continually initializing/destroying objects. 1429 1430 // Create the content adjust map once. Note: This is a shared object, all properties must be overwritten each time. 1431 if (!contentAdjustMap) { 1432 contentAdjustMap = SC.ScrollView._SC_CONTENT_ADJUST_MAP = { 1433 // Ensure that scale transforms occur from the top-left corner (per our math). 1434 transformOriginX: 0, 1435 transformOriginY: 0 1436 }; 1437 } 1438 1439 contentAdjustMap.left = left; 1440 contentAdjustMap.top = top; 1441 contentAdjustMap.scale = scale; 1442 1443 if (this._sc_animationDuration) { 1444 // UNUSED. Animate using SC.View.prototype.animate. Cancelling the animation in place proved problematic. 1445 // contentView.animate({ left: left, top: top, scale: scale }, { 1446 // duration: this._sc_animationDuration, 1447 // timing: this._sc_animationTiming 1448 // }); 1449 1450 // // Run the animation immediately (don't wait for next Run Loop). 1451 // // Note: The next run loop will be queued none-the-less, so we may want to avoid that entirely in the future. 1452 // contentView._animate(); 1453 this._sc_animateContentView(contentAdjustMap); 1454 1455 } else { 1456 contentView.adjust(contentAdjustMap); 1457 } 1458 } 1459 }, 1460 1461 /** @private Re-position the scrollers and content depending on the need to scroll or not. */ 1462 _sc_repositionScrollers: function () { 1463 this.invokeOnce(this._sc_repositionScrollersUnfiltered); 1464 }, 1465 1466 /** @private Re-position the scrollers and content depending on the need to scroll or not. */ 1467 _sc_repositionScrollersUnfiltered: function () { 1468 var hasHorizontalScroller = this.get('hasHorizontalScroller'), 1469 hasVerticalScroller = this.get('hasVerticalScroller'), 1470 canScrollHorizontal = this.get('canScrollHorizontal'), 1471 canScrollVertical = this.get('canScrollVertical'), 1472 containerLayoutMap = SC.ScrollView._SC_CONTAINER_LAYOUT_MAP, // Shared object used to avoid continually initializing/destroying objects. 1473 horizontalScrollerView = this.get('horizontalScrollerView'), 1474 horizontalScrollerHeight = horizontalScrollerView && canScrollHorizontal ? horizontalScrollerView.get('scrollbarThickness') : 0, 1475 verticalScrollerView = this.get('verticalScrollerView'), 1476 verticalScrollerWidth = verticalScrollerView && canScrollVertical ? verticalScrollerView.get('scrollbarThickness') : 0, 1477 layout; // The new layout to be applied to each scroller. 1478 1479 // Create the container layout map once. Note: This is a shared object, all properties must be overwritten each time. 1480 if (!containerLayoutMap) { containerLayoutMap = SC.ScrollView._SC_CONTAINER_LAYOUT_MAP = {}; } 1481 1482 // Set the standard. 1483 containerLayoutMap.bottom = 0; 1484 containerLayoutMap.right = 0; 1485 1486 // Adjust the horizontal scroller. 1487 if (hasHorizontalScroller) { 1488 var horizontalOverlay = this.get('horizontalOverlay'), 1489 horizontalScrollerLayout = this.get('horizontalScrollerLayout'); 1490 1491 // Adjust the scroller view accordingly. Allow for a custom default scroller layout to be set. 1492 if (horizontalScrollerLayout) { 1493 layout = { 1494 left: horizontalScrollerLayout.left, 1495 bottom: horizontalScrollerLayout.bottom, 1496 right: horizontalScrollerLayout.right + verticalScrollerWidth, 1497 height: horizontalScrollerHeight 1498 }; 1499 } else { 1500 layout = { 1501 left: 0, 1502 bottom: 0, 1503 right: verticalScrollerWidth, 1504 height: horizontalScrollerHeight 1505 }; 1506 } 1507 horizontalScrollerView.set('layout', layout); 1508 1509 // Adjust the content view accordingly. 1510 if (canScrollHorizontal && !horizontalOverlay) { 1511 containerLayoutMap.bottom = horizontalScrollerHeight; 1512 } 1513 1514 // Set the visibility of the scroller immediately. 1515 horizontalScrollerView.set('isVisible', canScrollHorizontal); 1516 this._sc_fadeInHorizontalScroller(); 1517 } 1518 1519 // Adjust the vertical scroller. 1520 if (hasVerticalScroller) { 1521 var verticalOverlay = this.get('verticalOverlay'), 1522 verticalScrollerLayout = this.get('verticalScrollerLayout'); 1523 1524 // Adjust the scroller view accordingly. Allow for a custom default scroller layout to be set. 1525 if (verticalScrollerLayout) { 1526 layout = { 1527 top: verticalScrollerLayout.top, 1528 right: verticalScrollerLayout.right, 1529 bottom: verticalScrollerLayout.bottom + horizontalScrollerHeight, 1530 width: verticalScrollerWidth 1531 }; 1532 } else { 1533 layout = { 1534 top: 0, 1535 right: 0, 1536 bottom: horizontalScrollerHeight, 1537 width: verticalScrollerWidth 1538 }; 1539 } 1540 verticalScrollerView.set('layout', layout); 1541 1542 // Prepare to adjust the content view accordingly. 1543 if (canScrollVertical && !verticalOverlay) { 1544 containerLayoutMap.right = verticalScrollerWidth; 1545 } 1546 1547 // Set the visibility of the scroller immediately. 1548 verticalScrollerView.set('isVisible', canScrollVertical); 1549 this._sc_fadeInVerticalScroller(); 1550 } 1551 1552 // Adjust the container view accordingly (i.e. to make space for scrollers or take space back). 1553 var containerView = this.get('containerView'); 1554 containerView.adjust(containerLayoutMap); 1555 }, 1556 1557 /** @private Clean up observers on the content view. */ 1558 _sc_removeContentViewObservers: function () { 1559 var oldView = this._sc_contentView, 1560 frameChangeFunc = this._sc_contentViewFrameDidChange; 1561 // layerChangeFunc = this._sc_contentViewLayerDidChange; 1562 1563 if (oldView) { 1564 oldView.removeObserver('frame', this, frameChangeFunc); 1565 // oldView.removeObserver('layer', this, layerChangeFunc); 1566 1567 // this._sc_shouldResizeContentWidth = false; 1568 // this._sc_shouldResizeContentHeight = false; 1569 } 1570 }, 1571 1572 /** @private Whenever the scale changes, update the scrollers and adjust the location of the content view. */ 1573 _sc_scaleDidChange: function () { 1574 var contentView = this.get('contentView'), 1575 scale = this.get('scale'); 1576 1577 // If the content is statically laid out, use margins in the container layer to move it. 1578 // TODO: Remove static layout support. 1579 if (contentView) { 1580 if (contentView.useStaticLayout) { 1581 //@if(debug) 1582 // If the scale is not 1 then assume the developer is trying to scale static content. 1583 if (scale !== 1) { 1584 SC.warn("Developer Warning: SC.ScrollView's `scale` feature does not support statically laid out content views."); 1585 } 1586 //@endif 1587 1588 // Reposition the content view to apply the scale. 1589 } else { 1590 // Apply this change. 1591 this._sc_repositionContentView(); 1592 } 1593 } 1594 }, 1595 1596 /** @private Whenever the scroll offsets change, update the scrollers and adjust the location of the content view. */ 1597 _sc_scrollOffsetHorizontalDidChange: function () { 1598 this._sc_repositionContentView(); 1599 this.invokeLast(this._sc_fadeInHorizontalScroller); 1600 }, 1601 1602 /** @private Whenever the scroll offsets change, update the scrollers and adjust the location of the content view. */ 1603 _sc_scrollOffsetVerticalDidChange: function () { 1604 this._sc_repositionContentView(); 1605 this.invokeLast(this._sc_fadeInVerticalScroller); 1606 }, 1607 1608 /** @private Update the scrollers. */ 1609 _sc_updateScrollers: function () { 1610 var horizontalScrollerView = this.get('horizontalScrollerView'), 1611 verticalScrollerView = this.get('verticalScrollerView'), 1612 minimumHorizontalScrollOffset = this.get('minimumHorizontalScrollOffset'), 1613 minimumVerticalScrollOffset = this.get('minimumVerticalScrollOffset'), 1614 maximumHorizontalScrollOffset = this.get('maximumHorizontalScrollOffset'), 1615 maximumVerticalScrollOffset = this.get('maximumVerticalScrollOffset'), 1616 containerHeight, containerWidth, 1617 contentHeight, contentWidth; 1618 1619 containerHeight = this._sc_containerHeight; 1620 containerWidth = this._sc_containerWidth; 1621 contentHeight = this._sc_contentHeight; 1622 contentWidth = this._sc_contentWidth; 1623 1624 // Update the minimum and maximum scrollable distance on the scrollers as well as their visibility. 1625 var proportion; 1626 if (horizontalScrollerView) { 1627 horizontalScrollerView.set('minimum', minimumHorizontalScrollOffset); 1628 horizontalScrollerView.set('maximum', maximumHorizontalScrollOffset); 1629 1630 if (this.get('autohidesHorizontalScroller')) { 1631 this.setIfChanged('isHorizontalScrollerVisible', contentWidth > containerWidth); 1632 } 1633 1634 // Constrain the proportion to 100%. 1635 proportion = Math.min(containerWidth / contentWidth, 1.0); 1636 horizontalScrollerView.setIfChanged('proportion', proportion); 1637 } 1638 1639 if (verticalScrollerView) { 1640 verticalScrollerView.set('minimum', minimumVerticalScrollOffset); 1641 verticalScrollerView.set('maximum', maximumVerticalScrollOffset); 1642 1643 if (this.get('autohidesVerticalScroller')) { 1644 this.setIfChanged('isVerticalScrollerVisible', contentHeight > containerHeight); 1645 } 1646 1647 // Constrain the proportion to 100%. 1648 proportion = Math.min(containerHeight / contentHeight, 1.0); 1649 verticalScrollerView.setIfChanged('proportion', proportion); 1650 } 1651 }, 1652 1653 /** @private Adjust the content alignment vertically on change. */ 1654 _sc_verticalAlignDidChange: function () { 1655 var maximumVerticalScrollOffset = this.get('maximumVerticalScrollOffset'); 1656 1657 // Align vertically (Unless content height is zero). 1658 if (maximumVerticalScrollOffset === 0 && this._sc_contentHeight) { 1659 var verticalAlign = this.get('verticalAlign'), 1660 value; 1661 1662 value = this._sc_alignedVerticalOffset(verticalAlign, this._sc_containerHeight, this._sc_contentHeight); 1663 this.set('verticalScrollOffset', value); 1664 } 1665 }, 1666 1667 /** @private Instantiate the container view and the scrollers as needed. */ 1668 createChildViews: function () { 1669 var childViews = []; 1670 1671 // Set up the container view. 1672 var containerView = this.get('containerView'); 1673 1674 //@if(debug) 1675 // Provide some debug-mode only developer support to prevent problems. 1676 if (!containerView) { 1677 throw new Error("Developer Error: SC.ScrollView must have a containerView class set before it is instantiated."); 1678 } 1679 //@endif 1680 1681 containerView = this.containerView = this.createChildView(containerView, { 1682 contentView: this.contentView // Initialize the view with the currently set container view. 1683 }); 1684 this.contentView = containerView.get('contentView'); // Replace our content view with the instantiated version. 1685 childViews.push(containerView); 1686 1687 // Set up the scrollers. 1688 var scrollerView; 1689 1690 // Create a horizontal scroller view if needed. 1691 if (!this.get('hasHorizontalScroller')) { 1692 // Remove the class entirely. 1693 this.horizontalScrollerView = null; 1694 } else { 1695 scrollerView = this.get('horizontalScrollerView'); 1696 1697 // Use a default scroller view. 1698 /* jshint eqnull:true */ 1699 if (scrollerView == null) { 1700 scrollerView = this.get('horizontalOverlay') ? SC.OverlayScrollerView : SC.ScrollerView; 1701 } 1702 1703 // Replace the class property with an instance. 1704 scrollerView = this.horizontalScrollerView = this.createChildView(scrollerView, { 1705 isVisible: !this.get('autohidesHorizontalScroller'), 1706 layoutDirection: SC.LAYOUT_HORIZONTAL, 1707 value: this.get('horizontalScrollOffset'), 1708 valueBinding: '.parentView.horizontalScrollOffset', // Bind the value of the scroller to our horizontal offset. 1709 minimum: this.get('minimumHorizontalScrollOffset'), 1710 maximum: this.get('maximumHorizontalScrollOffset') 1711 }); 1712 1713 // Add the scroller view to the child views array. 1714 childViews.push(scrollerView); 1715 } 1716 1717 // Create a vertical scroller view if needed. 1718 if (!this.get('hasVerticalScroller')) { 1719 // Remove the class entirely. 1720 this.verticalScrollerView = null; 1721 } else { 1722 scrollerView = this.get('verticalScrollerView'); 1723 1724 // Use a default scroller view. 1725 /* jshint eqnull:true */ 1726 if (scrollerView == null) { 1727 scrollerView = this.get('verticalOverlay') ? SC.OverlayScrollerView : SC.ScrollerView; 1728 } 1729 1730 // Replace the class property with an instance. 1731 scrollerView = this.verticalScrollerView = this.createChildView(scrollerView, { 1732 isVisible: !this.get('autohidesVerticalScroller'), 1733 layoutDirection: SC.LAYOUT_VERTICAL, 1734 value: this.get('verticalScrollOffset'), 1735 valueBinding: '.parentView.verticalScrollOffset', // Bind the value of the scroller to our vertical offset. 1736 minimum: this.get('minimumVerticalScrollOffset'), 1737 maximum: this.get('maximumVerticalScrollOffset') 1738 }); 1739 1740 // Add the scroller view to the child views array. 1741 childViews.push(scrollerView); 1742 } 1743 1744 // Set the childViews array. 1745 this.childViews = childViews; 1746 }, 1747 1748 /** @private */ 1749 destroy: function() { 1750 // Clean up. 1751 this._sc_removeContentViewObservers(); 1752 this.removeObserver('contentView', this, this._sc_contentViewDidChange); 1753 1754 this.removeObserver('horizontalAlign', this, this._sc_horizontalAlignDidChange); 1755 this.removeObserver('verticalAlign', this, this._sc_verticalAlignDidChange); 1756 1757 sc_super(); 1758 }, 1759 1760 /** @private SC.View */ 1761 didCreateLayer: function () { 1762 // Observe the scroll offsets for changes and initialize once. 1763 this.addObserver('horizontalScrollOffset', this, this._sc_scrollOffsetHorizontalDidChange); 1764 this.addObserver('verticalScrollOffset', this, this._sc_scrollOffsetVerticalDidChange); 1765 this._sc_scrollOffsetHorizontalDidChange(); 1766 this._sc_scrollOffsetVerticalDidChange(); 1767 1768 // Observe the scroller visibility properties for changes and initialize once. 1769 this.addObserver('isHorizontalScrollerVisible', this, this._sc_repositionScrollers); 1770 this.addObserver('isVerticalScrollerVisible', this, this._sc_repositionScrollers); 1771 this._sc_repositionScrollers(); 1772 1773 // Observe the scale for changes and initialize once. 1774 this.addObserver('scale', this, this._sc_scaleDidChange); 1775 this._sc_scaleDidChange(); 1776 1777 // Observe our container view frame for changes and initialize once. 1778 var containerView = this.get('containerView'); 1779 containerView.addObserver('frame', this, this._sc_containerViewFrameDidChange); 1780 this._sc_containerViewFrameDidChange(); 1781 1782 // Observe for changes in enablement and visibility for registering with SC.Drag auto-scrolling and initialize once. 1783 this.addObserver('isVisibleInWindow', this, this._sc_registerAutoscroll); 1784 this.addObserver('isEnabledInPane', this, this._sc_registerAutoscroll); 1785 this._sc_registerAutoscroll(); 1786 }, 1787 1788 /** SC.Object.prototype */ 1789 init: function () { 1790 sc_super(); 1791 1792 // Observe the content view for changes and initialize once. 1793 this.addObserver('contentView', this, this._sc_contentViewDidChange); 1794 this._sc_contentViewDidChange(); 1795 1796 // Observe the alignment properties for changes. No need to initialize, the default alignment property 1797 // will be used. 1798 this.addObserver('horizontalAlign', this, this._sc_horizontalAlignDidChange); 1799 this.addObserver('verticalAlign', this, this._sc_verticalAlignDidChange); 1800 }, 1801 1802 /** 1803 Scrolls the receiver in the horizontal and vertical directions by the amount specified, if 1804 allowed. The actual scroll amount will be constrained by the current scroll minimums and 1805 maximums. (If you wish to scroll outside of those bounds, you should call `scrollTo` directly.) 1806 1807 If you only want to scroll in one direction, pass null or 0 for the other direction. 1808 1809 @param {Number} x change in the x direction (or hash) 1810 @param {Number} y change in the y direction 1811 @returns {SC.ScrollView} receiver 1812 */ 1813 scrollBy: function (x, y) { 1814 // Normalize (deprecated). 1815 if (y === undefined && SC.typeOf(x) === SC.T_HASH) { 1816 //@if(debug) 1817 // Add some developer support. It's faster to pass the arguments individually so that we don't need to do this normalization and the 1818 // developer isn't creating an extra Object needlessly. 1819 SC.warn("Developer Warning: Passing an object to SC.ScrollView.scrollBy is deprecated. Please pass both the x and y arguments."); 1820 //@endif 1821 1822 y = x.y; 1823 x = x.x; 1824 } 1825 1826 // If null, undefined, or 0, pass null; otherwise just add current offset. 1827 x = (x) ? this.get('horizontalScrollOffset') + x : null; 1828 y = (y) ? this.get('verticalScrollOffset') + y : null; 1829 1830 // Constrain within min and max. (Calls to scrollBy are generally convenience calls that should not have to 1831 // worry about exceeding bounds and making the followup call. Features that want to allow overscroll should call 1832 // scrollTo directly.) 1833 if (x !== null) { 1834 x = Math.min(Math.max(this.get('minimumHorizontalScrollOffset'), x), this.get('maximumHorizontalScrollOffset')); 1835 } 1836 1837 if (y !== null) { 1838 y = Math.min(Math.max(this.get('minimumVerticalScrollOffset'), y), this.get('maximumVerticalScrollOffset')); 1839 } 1840 1841 return this.scrollTo(x, y); 1842 }, 1843 1844 /** 1845 Scrolls the receiver down one or more lines if allowed. If number of 1846 lines is not specified, scrolls one line. 1847 1848 @param {Number} lines number of lines 1849 @returns {SC.ScrollView} receiver 1850 */ 1851 scrollDownLine: function (lines) { 1852 if (lines === undefined) lines = 1; 1853 return this.scrollBy(null, this.get('verticalLineScroll') * lines); 1854 }, 1855 1856 /** 1857 Scrolls the receiver down one or more page if allowed. If number of 1858 pages is not specified, scrolls one page. The page size is determined by 1859 the verticalPageScroll value. By default this is the size of the current 1860 scrollable area. 1861 1862 @param {Number} pages number of lines 1863 @returns {SC.ScrollView} receiver 1864 */ 1865 scrollDownPage: function (pages) { 1866 if (pages === undefined) pages = 1; 1867 return this.scrollBy(null, this.get('verticalPageScroll') * pages); 1868 }, 1869 1870 /** 1871 Scrolls the receiver left one or more lines if allowed. If number of 1872 lines is not specified, scrolls one line. 1873 1874 @param {Number} lines number of lines 1875 @returns {SC.ScrollView} receiver 1876 */ 1877 scrollLeftLine: function (lines) { 1878 if (lines === undefined) lines = 1; 1879 return this.scrollTo(0 - this.get('horizontalLineScroll') * lines, null); 1880 }, 1881 1882 /** 1883 Scrolls the receiver left one or more page if allowed. If number of 1884 pages is not specified, scrolls one page. The page size is determined by 1885 the verticalPageScroll value. By default this is the size of the current 1886 scrollable area. 1887 1888 @param {Number} pages number of lines 1889 @returns {SC.ScrollView} receiver 1890 */ 1891 scrollLeftPage: function (pages) { 1892 if (pages === undefined) pages = 1; 1893 return this.scrollBy(0 - (this.get('horizontalPageScroll') * pages), null); 1894 }, 1895 1896 /** 1897 Scrolls the receiver right one or more lines if allowed. If number of 1898 lines is not specified, scrolls one line. 1899 1900 @param {Number} lines number of lines 1901 @returns {SC.ScrollView} receiver 1902 */ 1903 scrollRightLine: function (lines) { 1904 if (lines === undefined) lines = 1; 1905 return this.scrollTo(this.get('horizontalLineScroll') * lines, null); 1906 }, 1907 1908 /** 1909 Scrolls the receiver right one or more page if allowed. If number of 1910 pages is not specified, scrolls one page. The page size is determined by 1911 the verticalPageScroll value. By default this is the size of the current 1912 scrollable area. 1913 1914 @param {Number} pages number of lines 1915 @returns {SC.ScrollView} receiver 1916 */ 1917 scrollRightPage: function (pages) { 1918 if (pages === undefined) pages = 1; 1919 return this.scrollBy(this.get('horizontalPageScroll') * pages, null); 1920 }, 1921 1922 /** 1923 Scrolls to the specified x,y coordinates. This should be the offset into the contentView that 1924 you want to appear at the top-left corner of the scroll view. 1925 1926 This method will contain the actual scroll based on whether the view can scroll in the named 1927 direction and the maximum distance it can scroll. 1928 1929 If you only want to scroll in one direction, pass null for the other direction. 1930 1931 @param {Number} x the x scroll location 1932 @param {Number} y the y scroll location 1933 @returns {SC.ScrollView} receiver 1934 */ 1935 scrollTo: function (x, y) { 1936 // Normalize (deprecated). 1937 if (y === undefined && SC.typeOf(x) === SC.T_HASH) { 1938 //@if(debug) 1939 // Add some developer support. It's faster to pass the arguments individually so that we don't need to do this normalization and the 1940 // developer isn't creating an extra Object needlessly. 1941 SC.warn("Developer Warning: Passing an object to SC.ScrollView.scrollTo is deprecated. Please pass both the x and y arguments."); 1942 //@endif 1943 1944 y = x.y; 1945 x = x.x; 1946 } 1947 1948 if (!SC.none(x)) { 1949 this.set('horizontalScrollOffset', x); 1950 } 1951 1952 if (!SC.none(y)) { 1953 this.set('verticalScrollOffset', y); 1954 } 1955 1956 return this; 1957 }, 1958 1959 /** 1960 Scroll to the supplied rectangle. 1961 1962 If the rectangle is bigger than the viewport, the top-left 1963 will be preferred. 1964 1965 (Note that if your content is scaled, the rectangle must be 1966 relative to the contentView's scale, not the ScrollView's.) 1967 1968 @param {Rect} rect Rectangle to which to scroll. 1969 @returns {Boolean} YES if scroll position was changed. 1970 */ 1971 scrollToRect: function (rect) { 1972 // find current visible frame. 1973 var vo = SC.cloneRect(this.get('containerView').get('frame')), 1974 origX = this.get('horizontalScrollOffset'), 1975 origY = this.get('verticalScrollOffset'), 1976 scale = this.get('scale'); 1977 1978 vo.x = origX; 1979 vo.y = origY; 1980 1981 // Scale rect. 1982 if (scale !== 1) { 1983 rect = SC.cloneRect(rect); 1984 rect.x *= scale; 1985 rect.y *= scale; 1986 rect.height *= scale; 1987 rect.width *= scale; 1988 } 1989 1990 // if bottom edge is not visible, shift origin 1991 vo.y += Math.max(0, SC.maxY(rect) - SC.maxY(vo)); 1992 vo.x += Math.max(0, SC.maxX(rect) - SC.maxX(vo)); 1993 1994 // if top edge is not visible, shift origin 1995 vo.y -= Math.max(0, SC.minY(vo) - SC.minY(rect)); 1996 vo.x -= Math.max(0, SC.minX(vo) - SC.minX(rect)); 1997 1998 // scroll to that origin. 1999 if ((origX !== vo.x) || (origY !== vo.y)) { 2000 this.scrollTo(vo.x, vo.y); 2001 return YES; 2002 } else { 2003 return NO; 2004 } 2005 }, 2006 2007 /** 2008 Scrolls the receiver up one or more lines if allowed. If number of 2009 lines is not specified, scrolls one line. 2010 2011 @param {Number} lines number of lines 2012 @returns {SC.ScrollView} receiver 2013 */ 2014 scrollUpLine: function (lines) { 2015 if (lines === undefined) lines = 1; 2016 return this.scrollBy(null, 0 - this.get('verticalLineScroll') * lines); 2017 }, 2018 2019 /** 2020 Scrolls the receiver up one or more page if allowed. If number of 2021 pages is not specified, scrolls one page. The page size is determined by 2022 the verticalPageScroll value. By default this is the size of the current 2023 scrollable area. 2024 2025 @param {Number} pages number of lines 2026 @returns {SC.ScrollView} receiver 2027 */ 2028 scrollUpPage: function (pages) { 2029 if (pages === undefined) pages = 1; 2030 return this.scrollBy(null, 0 - (this.get('verticalPageScroll') * pages)); 2031 }, 2032 2033 /** 2034 Scroll the view to make the view's frame visible. For this to make sense, 2035 the view should be a subview of the contentView. Otherwise the results 2036 will be undefined. 2037 2038 @param {SC.View} view view to scroll or null to scroll receiver visible 2039 @returns {Boolean} YES if scroll position was changed 2040 */ 2041 scrollToVisible: function (view) { 2042 2043 // if no view is passed, do default 2044 if (arguments.length === 0) return sc_super(); 2045 2046 var contentView = this.get('contentView'); 2047 if (!contentView) return NO; // nothing to do if no contentView. 2048 2049 // get the frame for the view - should work even for views with static 2050 // layout, assuming it has been added to the screen. 2051 var viewFrame = view.get('borderFrame'); 2052 if (!viewFrame) return NO; // nothing to do 2053 2054 // convert view's frame to an offset from the contentView origin. This 2055 // will become the new scroll offset after some adjustment. 2056 viewFrame = contentView.convertFrameFromView(viewFrame, view.get('parentView')); 2057 2058 return this.scrollToRect(viewFrame); 2059 }, 2060 2061 /** @private SC.View */ 2062 willDestroyLayer: function () { 2063 // Clean up. 2064 this.removeObserver('horizontalScrollOffset', this, this._sc_scrollOffsetHorizontalDidChange); 2065 this.removeObserver('verticalScrollOffset', this, this._sc_scrollOffsetVerticalDidChange); 2066 this.removeObserver('isHorizontalScrollerVisible', this, this._sc_repositionScrollers); 2067 this.removeObserver('isVerticalScrollerVisible', this, this._sc_repositionScrollers); 2068 2069 this.removeObserver('scale', this, this._sc_scaleDidChange); 2070 2071 var containerView = this.get('containerView'); 2072 containerView.removeObserver('frame', this, this._sc_containerViewFrameDidChange); 2073 2074 // Be sure to remove this view as a scrollable view for SC.Drag. 2075 this.removeObserver('isVisibleInWindow', this, this._sc_registerAutoscroll); 2076 this.removeObserver('isEnabledInPane', this, this._sc_registerAutoscroll); 2077 SC.Drag.removeScrollableView(this); 2078 }, 2079 2080 // --------------------------------------------------------------------------------------------- 2081 // Interaction 2082 // 2083 2084 /** @private 2085 This method gives our descendent views a chance to capture the touch via captureTouch, and subsequently to handle the 2086 touch, via touchStart. If no view elects to do so, control is returned to the scroll view for standard scrolling. 2087 */ 2088 _sc_beginTouchesInContent: function (touch) { 2089 // Clean up. 2090 this._sc_passTouchToContentTimer = null; 2091 2092 // If the touch is not a scroll or scale, see if any of our descendent views want to handle the touch. If not, 2093 // we keep our existing respondership and all is well. 2094 if (!touch.captureTouch(this, true)) { 2095 touch.makeTouchResponder(touch.targetView, true, this); 2096 } 2097 }, 2098 2099 /** @private */ 2100 _sc_touchEnded: function (touch, wasCancelled) { 2101 // When the last touch ends, we stop touch scrolling. 2102 var hasTouch = this.get('hasTouch'); 2103 if (hasTouch) { 2104 // Update the average distance to center of the touch to include the new touch. This is used to recognize pinch/zoom movement of the touch. 2105 var avgTouch = touch.averagedTouchesForView(this); 2106 2107 this._sc_gestureAnchorD = this._sc_gestureAnchorTotalD = avgTouch.d; 2108 this._sc_gestureAnchorX = this._sc_gestureAnchorTotalX = avgTouch.x; 2109 this._sc_gestureAnchorY = this._sc_gestureAnchorTotalY = avgTouch.y; 2110 2111 if (this._sc_containerOffset) { 2112 this._sc_touchCenterX = avgTouch.x - this._sc_containerOffset.x; 2113 this._sc_touchCenterY = avgTouch.y - this._sc_containerOffset.y; 2114 } 2115 2116 } else { 2117 2118 // If we were scrolling, continue scrolling at present velocity with deceleration. 2119 if (this._sc_isTouchScrollingV || this._sc_isTouchScrollingH || this._sc_isTouchScaling) { 2120 var decelerationRate = this.get('decelerationRate'), 2121 containerHeight = this._sc_containerHeight, 2122 containerWidth = this._sc_containerWidth, 2123 durationH = 0, 2124 durationV = 0, 2125 c2x, c2y; 2126 2127 if (this._sc_isTouchScrollingH) { 2128 var horizontalScrollOffset = this.get('horizontalScrollOffset'), 2129 maximumHorizontalScrollOffset = this.get('maximumHorizontalScrollOffset'), 2130 minimumHorizontalScrollOffset = this.get('minimumHorizontalScrollOffset'), 2131 horizontalVelocity = this._sc_touchVelocityH; 2132 2133 // Past the maximum. 2134 if (horizontalScrollOffset > maximumHorizontalScrollOffset) { 2135 this.set('horizontalScrollOffset', maximumHorizontalScrollOffset); 2136 2137 // Moving away from maximum. Change direction. 2138 if (horizontalVelocity < 0.2) { 2139 this._sc_animationTiming = this.get('animationCurveReverse'); 2140 2141 // Stopped or moving back towards maximum. Maintain direction, snap at the end. 2142 } else { 2143 this._sc_animationTiming = this.get('animationCurveSnap'); 2144 } 2145 2146 // 0.8 seconds for a full screen animation (most will be 50% or less of screen) 2147 durationH = 0.8 * (horizontalScrollOffset - maximumHorizontalScrollOffset) / containerWidth; 2148 2149 // Bounce back from min. 2150 } else if (horizontalScrollOffset < minimumHorizontalScrollOffset) { 2151 this.set('horizontalScrollOffset', minimumHorizontalScrollOffset); 2152 2153 // Moving away from minimum. Change direction. 2154 if (horizontalVelocity > 0.2) { 2155 this._sc_animationTiming = this.get('animationCurveReverse'); 2156 2157 // Stopped or moving back towards minimum. Maintain direction, snap at the end. 2158 } else { 2159 this._sc_animationTiming = this.get('animationCurveSnap'); 2160 } 2161 2162 // 0.8 seconds for a full screen animation (most will be 50% or less of screen) 2163 durationH = 0.8 * (minimumHorizontalScrollOffset - horizontalScrollOffset) / containerWidth; 2164 2165 // Slide. 2166 } else { 2167 // Set the final position we should slide to as we decelerate based on last velocity. 2168 horizontalScrollOffset -= (Math.abs(horizontalVelocity) * horizontalVelocity * 1000) / (2 * decelerationRate); 2169 2170 // Constrain within bounds. 2171 if (horizontalScrollOffset > maximumHorizontalScrollOffset) { 2172 // Generate an animation curve that bounces past the end point. 2173 c2x = (horizontalScrollOffset - maximumHorizontalScrollOffset) / containerWidth; 2174 c2y = 2 * c2x; 2175 this._sc_animationTiming = SC.easingCurve(0.0,0.5,c2x.toFixed(1),c2y.toFixed(1)); // 'cubic-bezier(0.0,0.5,%@,%@)'.fmt(c2x.toFixed(1), c2y.toFixed(1)); 2176 2177 horizontalScrollOffset = maximumHorizontalScrollOffset; 2178 2179 } else if (horizontalScrollOffset < minimumHorizontalScrollOffset) { 2180 // Generate an animation curve that bounces past the end point. 2181 c2x = (minimumHorizontalScrollOffset - horizontalScrollOffset) / containerWidth; 2182 c2y = 2 * c2x; 2183 this._sc_animationTiming = SC.easingCurve(0.0,0.5,c2x.toFixed(1),c2y.toFixed(1)); // 'cubic-bezier(0.0,0.5,%@,%@)'.fmt(c2x.toFixed(1), c2y.toFixed(1)); 2184 2185 horizontalScrollOffset = minimumHorizontalScrollOffset; 2186 2187 } else { 2188 this._sc_animationTiming = this.get('animationCurveDecelerate'); 2189 } 2190 2191 this.set('horizontalScrollOffset', horizontalScrollOffset); 2192 2193 durationH = Math.abs(horizontalVelocity / decelerationRate); 2194 } 2195 } 2196 2197 if (this._sc_isTouchScrollingV) { 2198 var verticalScrollOffset = this.get('verticalScrollOffset'), 2199 maximumVerticalScrollOffset = this.get('maximumVerticalScrollOffset'), 2200 minimumVerticalScrollOffset = this.get('minimumVerticalScrollOffset'), 2201 verticalVelocity = this._sc_touchVelocityV; 2202 2203 // Past the maximum. 2204 if (verticalScrollOffset > maximumVerticalScrollOffset) { 2205 this.set('verticalScrollOffset', maximumVerticalScrollOffset); 2206 2207 // Moving away from maximum. Change direction. 2208 if (verticalVelocity < 0.2) { 2209 this._sc_animationTiming = this.get('animationCurveReverse'); 2210 2211 // Stopped or moving back towards maximum. Maintain direction, snap at the end. 2212 } else { 2213 this._sc_animationTiming = this.get('animationCurveSnap'); 2214 } 2215 2216 // 0.8 seconds for a full screen animation (most will be 50% or less of screen) 2217 durationV = 0.8 * (verticalScrollOffset - maximumVerticalScrollOffset) / containerHeight; 2218 2219 // Bounce back from min. 2220 } else if (verticalScrollOffset < minimumVerticalScrollOffset) { 2221 this.set('verticalScrollOffset', minimumVerticalScrollOffset); 2222 2223 // Moving away from minimum. Change direction. 2224 if (verticalVelocity > 0.2) { 2225 this._sc_animationTiming = this.get('animationCurveReverse'); 2226 2227 // Stopped or moving back towards minimum. Maintain direction, snap at the end. 2228 } else { 2229 this._sc_animationTiming = this.get('animationCurveSnap'); 2230 } 2231 2232 // 0.8 seconds for a full screen animation (most will be 50% or less of screen) 2233 durationV = 0.8 * (minimumVerticalScrollOffset - verticalScrollOffset) / containerHeight; 2234 2235 // Slide. 2236 } else { 2237 // Set the final position we should slide to as we decelerate based on last velocity. 2238 verticalScrollOffset -= (Math.abs(verticalVelocity) * verticalVelocity * 1000) / (2 * decelerationRate); 2239 2240 // Constrain within bounds. 2241 if (verticalScrollOffset > maximumVerticalScrollOffset) { 2242 // Generate an animation curve that bounces past the end point. 2243 c2x = (verticalScrollOffset - maximumVerticalScrollOffset) / containerHeight; 2244 c2y = 2 * c2x; 2245 this._sc_animationTiming = SC.easingCurve(0.0, 0.5,c2x.toFixed(1), c2y.toFixed(1)); 'cubic-bezier(0.0,0.5,%@,%@)'.fmt(c2x.toFixed(1), c2y.toFixed(1)); 2246 2247 verticalScrollOffset = maximumVerticalScrollOffset; 2248 2249 } else if (verticalScrollOffset < minimumVerticalScrollOffset) { 2250 // Generate an animation curve that bounces past the end point. 2251 c2x = (minimumVerticalScrollOffset - verticalScrollOffset) / containerHeight; 2252 c2y = 2 * c2x; 2253 this._sc_animationTiming = SC.easingCurve(0.0, 0.5,c2x.toFixed(1), c2y.toFixed(1)); 'cubic-bezier(0.0,0.5,%@,%@)'.fmt(c2x.toFixed(1), c2y.toFixed(1)); 2254 2255 verticalScrollOffset = minimumVerticalScrollOffset; 2256 2257 } else { 2258 this._sc_animationTiming = this.get('animationCurveDecelerate'); 2259 } 2260 2261 this.set('verticalScrollOffset', verticalScrollOffset); 2262 2263 durationV = Math.abs(verticalVelocity / decelerationRate); 2264 } 2265 } 2266 2267 var scale = this.get('scale'), 2268 maximumScale = this.get('maximumScale'), 2269 minimumScale = this.get('minimumScale'), 2270 durationS = 0; 2271 2272 // Bounce back from max. 2273 if (scale > maximumScale) { 2274 this.set('scale', maximumScale); 2275 durationS = 0.25; 2276 2277 // Bounce back from min. 2278 } else if (scale < minimumScale) { 2279 this.set('scale', minimumScale); 2280 durationS = 0.25; 2281 2282 // Slide. 2283 } else { 2284 2285 } 2286 2287 // Determine how long the deceleration should take (we can't animate left/top separately, so use the largest duration for both). 2288 // This variable also acts as a flag so that when the content view is repositioned, it will be animated. 2289 this._sc_animationDuration = Math.max(Math.max(durationH, durationV), durationS); 2290 2291 // Clear up all caches from touchesDragged. 2292 this._sc_touchVelocityH = null; 2293 this._sc_touchVelocityV = null; 2294 2295 // Pass the initial touch on to the content view if it hasn't tried yet (i.e. a tap) and the touch wasn't cancelled. 2296 } else if (this._sc_passTouchToContentTimer) { 2297 // Clean up. 2298 this._sc_passTouchToContentTimer.invalidate(); 2299 this._sc_passTouchToContentTimer = null; 2300 2301 if (!wasCancelled) { 2302 // If the content has handled the touch, then immediately end it. 2303 if (touch.makeTouchResponder(touch.targetView, true, this)) { 2304 touch.end(); 2305 } 2306 } 2307 } 2308 2309 // Clean up all caches from touchStart & touchesDragged 2310 this._sc_gestureAnchorX = this._sc_gestureAnchorY = this._sc_gestureAnchorD = null; 2311 this._sc_gestureAnchorTotalX = this._sc_gestureAnchorTotalY = this._sc_gestureAnchorTotalD = null; 2312 this._sc_gestureAnchorScale = null; 2313 this._sc_gestureAnchorHOffset = null; 2314 this._sc_gestureAnchorVOffset = null; 2315 this._sc_containerOffset = null; 2316 this._sc_touchCenterX = null; 2317 this._sc_touchCenterY = null; 2318 } 2319 2320 // Force recalculation of scrolling and scaling. 2321 this._sc_isTouchScrollingH = false; 2322 this._sc_isTouchScrollingHOnly = false; 2323 this._sc_isTouchScrollingV = false; 2324 this._sc_isTouchScrollingVOnly = false; 2325 this._sc_isTouchScaling = false; 2326 2327 // TODO: What happens when isEnabledInPane goes false while interacting? Statechart would help solve this. 2328 return true; 2329 }, 2330 2331 /** @private @see SC.RootResponder.prototype.captureTouch */ 2332 captureTouch: function (touch) { 2333 // Capture the touch and begin determination of actual scroll or not. 2334 if (this.get('delaysContentTouches')) { 2335 return true; 2336 2337 // Otherwise, suggest ourselves as a reasonable fallback responder. If none of our children capture 2338 // the touch or handle touchStart, we'll get another crack at it in touchStart. 2339 } else { 2340 touch.stackCandidateTouchResponder(this); 2341 2342 return false; 2343 } 2344 }, 2345 2346 /** @private */ 2347 mouseWheel: function (evt) { 2348 var handled = false, 2349 contentView = this.get('contentView'); 2350 2351 // Ignore it if not enabled. 2352 if (contentView && this.get('isEnabledInPane')) { 2353 2354 var horizontalScrollOffset = this.get('horizontalScrollOffset'), 2355 minimumHorizontalScrollOffset = this.get('minimumHorizontalScrollOffset'), 2356 minimumVerticalScrollOffset = this.get('minimumVerticalScrollOffset'), 2357 maximumHorizontalScrollOffset = this.get('maximumHorizontalScrollOffset'), 2358 maximumVerticalScrollOffset = this.get('maximumVerticalScrollOffset'), 2359 verticalScrollOffset = this.get('verticalScrollOffset'), 2360 wheelDeltaX = evt.wheelDeltaX, 2361 wheelDeltaY = evt.wheelDeltaY; 2362 2363 // If we can't scroll in one direction, limit that direction. 2364 if (!this.get('canScrollHorizontal')) { // Don't allow inverted scrolling for now. 2365 wheelDeltaX = 0; 2366 } 2367 2368 if (!this.get('canScrollVertical')) { // Don't allow inverted scrolling for now. 2369 wheelDeltaY = 0; 2370 } 2371 2372 // Only attempt to scroll if we are allowed to scroll in the direction and have room to scroll 2373 // in the direction. Otherwise, ignore the event so that an outer ScrollView may capture it. 2374 handled = ((wheelDeltaX < 0 && horizontalScrollOffset > minimumHorizontalScrollOffset) || 2375 (wheelDeltaX > 0 && horizontalScrollOffset < maximumHorizontalScrollOffset)) || 2376 ((wheelDeltaY < 0 && verticalScrollOffset > minimumVerticalScrollOffset) || 2377 (wheelDeltaY > 0 && verticalScrollOffset < maximumVerticalScrollOffset)); 2378 2379 if (handled) { 2380 this.scrollBy(wheelDeltaX, wheelDeltaY); 2381 } 2382 } 2383 2384 return handled; 2385 }, 2386 2387 /** @private */ 2388 touchesDragged: function (evt, touchesForView) { 2389 var avgTouch = evt.averagedTouchesForView(this), 2390 canScale = this.get('canScale'), 2391 canScrollHorizontal = this.get('canScrollHorizontal'), 2392 canScrollVertical = this.get('canScrollVertical'), 2393 scrollThreshold = this.get('scrollGestureThreshold'), 2394 scaleThreshold = this.get('scaleGestureThreshold'), 2395 scrollLockThreshold = this.get('scrollLockGestureThreshold'), 2396 horizontalScrollOffset, 2397 verticalScrollOffset; 2398 2399 2400 // Determine if we've moved enough to claim horizontal or vertical scrolling. 2401 if (!(this._sc_isTouchScrollingH && this._sc_isTouchScrollingV) && 2402 !this._sc_isTouchScrollingHOnly && !this._sc_isTouchScrollingVOnly) { 2403 2404 if (canScrollHorizontal) { 2405 var totalAbsDeltaX = Math.abs(this._sc_gestureAnchorTotalX - avgTouch.x); 2406 2407 if (!this._sc_isTouchScrollingH) { 2408 this._sc_isTouchScrollingH = totalAbsDeltaX >= scrollThreshold; 2409 2410 // Determine if we've moved enough to lock scrolling to only this direction. 2411 } else { 2412 this._sc_isTouchScrollingHOnly = totalAbsDeltaX >= scrollLockThreshold; 2413 } 2414 } 2415 2416 if (canScrollVertical) { 2417 var totalAbsDeltaY = Math.abs(this._sc_gestureAnchorTotalY - avgTouch.y); 2418 2419 if (!this._sc_isTouchScrollingV) { 2420 this._sc_isTouchScrollingV = totalAbsDeltaY >= scrollThreshold; 2421 2422 // Determine if we've moved enough to lock scrolling to only this direction. 2423 } else { 2424 this._sc_isTouchScrollingVOnly = totalAbsDeltaY >= scrollLockThreshold; 2425 } 2426 } 2427 } 2428 2429 var touchDeltaX = this._sc_gestureAnchorX - avgTouch.x, 2430 absDeltaX = Math.abs(touchDeltaX); 2431 2432 // Adjust scroll. 2433 if (canScrollHorizontal && absDeltaX >= 1 && !this._sc_isTouchScrollingVOnly) { 2434 // Record the last velocity. 2435 this._sc_touchVelocityH = avgTouch.velocityX; 2436 2437 var minimumHorizontalScrollOffset = this.get('minimumHorizontalScrollOffset'), 2438 maximumHorizontalScrollOffset = this.get('maximumHorizontalScrollOffset'); 2439 2440 horizontalScrollOffset = this._sc_gestureAnchorHOffset + touchDeltaX; 2441 2442 // Reset the anchor. Note: Do this before degrading the offset. 2443 this._sc_gestureAnchorX = avgTouch.x; 2444 this._sc_gestureAnchorHOffset = horizontalScrollOffset; 2445 2446 // Degrade the offset as we pass maximum. 2447 if (horizontalScrollOffset > maximumHorizontalScrollOffset) { 2448 horizontalScrollOffset = horizontalScrollOffset - this._sc_overDragSlip * (horizontalScrollOffset - maximumHorizontalScrollOffset); 2449 2450 // Degrade the offset as we pass minimum. 2451 } else if (horizontalScrollOffset < minimumHorizontalScrollOffset) { 2452 horizontalScrollOffset = horizontalScrollOffset + this._sc_overDragSlip * (minimumHorizontalScrollOffset - horizontalScrollOffset); 2453 } 2454 2455 // Update the scroll offset. 2456 this.set('horizontalScrollOffset', horizontalScrollOffset); 2457 } 2458 2459 var touchDeltaY = this._sc_gestureAnchorY - avgTouch.y, 2460 absDeltaY = Math.abs(touchDeltaY); 2461 2462 if (canScrollVertical && absDeltaY > 0 && !this._sc_isTouchScrollingHOnly) { 2463 // Record the last velocity. 2464 this._sc_touchVelocityV = avgTouch.velocityY; 2465 2466 var minimumVerticalScrollOffset = this.get('minimumVerticalScrollOffset'), 2467 maximumVerticalScrollOffset = this.get('maximumVerticalScrollOffset'); 2468 2469 verticalScrollOffset = this._sc_gestureAnchorVOffset + touchDeltaY; 2470 2471 // Reset the anchor. Note: Do this before degrading the offset. 2472 this._sc_gestureAnchorY = avgTouch.y; 2473 this._sc_gestureAnchorVOffset = verticalScrollOffset; 2474 2475 // Degrade the offset as we pass maximum. 2476 if (verticalScrollOffset > maximumVerticalScrollOffset) { 2477 verticalScrollOffset = verticalScrollOffset - this._sc_overDragSlip * (verticalScrollOffset - maximumVerticalScrollOffset); 2478 2479 // Degrade the offset as we pass minimum. 2480 } else if (verticalScrollOffset < minimumVerticalScrollOffset) { 2481 verticalScrollOffset = verticalScrollOffset + this._sc_overDragSlip * (minimumVerticalScrollOffset - verticalScrollOffset); 2482 } 2483 2484 // Update the scroll offset. 2485 this.set('verticalScrollOffset', verticalScrollOffset); 2486 } 2487 2488 // Adjust scale. 2489 if (canScale) { 2490 2491 // Determine if we've moved enough to claim scaling. 2492 if (!this._sc_isTouchScaling) { 2493 var totalAbsDeltaD = Math.abs(this._sc_gestureAnchorTotalD - avgTouch.d); 2494 this._sc_isTouchScaling = !!avgTouch.d && totalAbsDeltaD > scaleThreshold; 2495 } 2496 2497 var touchDeltaD = this._sc_gestureAnchorD - avgTouch.d, 2498 absDeltaD = Math.abs(touchDeltaD); 2499 if (absDeltaD > 0) { 2500 // The percentage difference in touch distance. 2501 var scalePercentChange = avgTouch.d / this._sc_gestureAnchorD, 2502 scale = this._sc_gestureAnchorScale * scalePercentChange; 2503 2504 // Adjust the center of the zoom to the center of the gesture. 2505 horizontalScrollOffset = this._sc_horizontalScrollOffset; 2506 verticalScrollOffset = this._sc_verticalScrollOffset; 2507 2508 // Cache the current offset of the container view in the document. Calculated each time touch scaling begins. 2509 if (!this._sc_containerOffset) { 2510 var el = this.getPath('containerView.layer'); 2511 2512 this._sc_containerOffset = SC.offset(el); 2513 this._sc_touchCenterX = avgTouch.x - this._sc_containerOffset.x; 2514 this._sc_touchCenterY = avgTouch.y - this._sc_containerOffset.y; 2515 } 2516 2517 // Compute the relative center of the scale gesture. 2518 this._sc_horizontalPct = (horizontalScrollOffset + this._sc_touchCenterX) / this._sc_contentWidth; 2519 this._sc_verticalPct = (verticalScrollOffset + this._sc_touchCenterY) / this._sc_contentHeight; 2520 2521 this.set('scale', scale); 2522 2523 // Reset the anchor. 2524 this._sc_gestureAnchorD = avgTouch.d; 2525 this._sc_gestureAnchorScale = scale; 2526 } 2527 } 2528 2529 // No longer pass the initial touch on to the content view if it was still about to. 2530 if (this._sc_passTouchToContentTimer && (this._sc_isTouchScrollingV || this._sc_isTouchScrollingH || this._sc_isTouchScaling)) { 2531 this._sc_passTouchToContentTimer.invalidate(); 2532 this._sc_passTouchToContentTimer = null; 2533 } 2534 2535 // Note: If the content view has already accepted the initial touch, it will be sent a touchCancelled event. 2536 }, 2537 2538 /** @private 2539 If we're in hand-holding mode and our content claims the touch, we will receive a touchCancelled 2540 event at its completion. We still need to do most of our touch-ending wrap up, for example to finish 2541 bouncing back from a previous gesture. 2542 */ 2543 touchCancelled: function (touch) { 2544 return this._sc_touchEnded(touch, true); 2545 }, 2546 2547 /** @private 2548 If we are the touch's responder at its completion, we'll get a touchEnd event. If this is the 2549 gesture's last touch, we wrap up in spectacular fashion. 2550 */ 2551 touchEnd: function (touch) { 2552 return this._sc_touchEnded(touch, false); 2553 }, 2554 2555 // /** @private */ 2556 touchStart: function (touch) { 2557 var handled = false, 2558 contentView = this.get('contentView'); 2559 2560 if (contentView && this.get('isEnabledInPane')) { 2561 var hasTouch = this.get('hasTouch'); 2562 2563 // Additional touches can be used for pinching gestures. 2564 if (hasTouch) { 2565 2566 // If a new touch has appeared, force scrolling to recalculate. 2567 this._sc_isTouchScrollingV = this._sc_isTouchScrollingH = false; 2568 this._sc_isTouchScrollingHOnly = this._sc_isTouchScrollingHOnly = false; 2569 2570 // No longer pass the initial touch on to the content view if it was still about to. 2571 if (this._sc_passTouchToContentTimer) { 2572 this._sc_passTouchToContentTimer.invalidate(); 2573 this._sc_passTouchToContentTimer = null; 2574 } 2575 2576 // The first touch is used to set up initial state. 2577 } else { 2578 // Cancel any active animation in place. 2579 this._sc_cancelAnimation(); 2580 2581 // If we have captured the touch and are not yet scrolling, we may want to delay a moment to test for 2582 // scrolling and if not scrolling, we will pass the touch through to the content. 2583 // If configured to do so, delay 150ms to verify that the user is not scrolling before passing touches through to the content. 2584 if (this.get('delaysContentTouches')) { 2585 this._sc_passTouchToContentTimer = this.invokeLater(this._sc_beginTouchesInContent, 150, touch); 2586 } // Else do nothing. 2587 } 2588 2589 // Update the average distance to center of the touch, which is used to recognize pinch/zoom movement of the touch. 2590 var avgTouch = touch.averagedTouchesForView(this, true); 2591 2592 /* A note on these variables: 2593 2594 _sc_gestureAnchorX: the last x position (so that we don't update horizontal scroll if the change since last is 0) 2595 _sc_gestureAnchorTotalX: the initial x position (so that we can determine whether to take total control of touches and possibly lock the position) 2596 */ 2597 this._sc_gestureAnchorX = this._sc_gestureAnchorTotalX = avgTouch.x; 2598 this._sc_gestureAnchorY = this._sc_gestureAnchorTotalY = avgTouch.y; 2599 this._sc_gestureAnchorD = this._sc_gestureAnchorTotalD = avgTouch.d; 2600 this._sc_gestureAnchorScale = this.get('scale'); 2601 this._sc_gestureAnchorHOffset = this.get('horizontalScrollOffset'); 2602 this._sc_gestureAnchorVOffset = this.get('verticalScrollOffset'); 2603 2604 handled = true; 2605 } 2606 2607 return handled; 2608 } 2609 2610 }); 2611 2612 2613 SC.ScrollView.mixin( 2614 /** @scope SC.ScrollView */ { 2615 2616 /** @private Shared object used to avoid continually initializing/destroying objects. */ 2617 _SC_CONTAINER_LAYOUT_MAP: null, 2618 2619 /** @private Shared object used to avoid continually initializing/destroying objects. */ 2620 _SC_CONTENT_ADJUST_MAP: null 2621 2622 }); 2623