1 // ========================================================================== 2 // Project: SproutCore - JavaScript Application Framework 3 // Copyright: ©2006-2011 Strobe Inc. and contributors. 4 // Portions ©2008-2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 8 /** @class 9 10 Displays a horizontal or vertical scroller. You will not usually need to 11 work with scroller views directly, but you may override this class to 12 implement your own custom scrollers. 13 14 Because the scroller uses the dimensions of its constituent elements to 15 calculate layout, you may need to override the default display metrics. 16 17 You can either create a subclass of ScrollerView with the new values, or 18 provide your own in your theme: 19 20 SC.ScrollerView = SC.ScrollerView.extend({ 21 scrollbarThickness: 14, 22 capLength: 18, 23 capOverlap: 14, 24 buttonOverlap: 11, 25 buttonLength: 41 26 }); 27 28 You can change whether scroll buttons are displayed by setting the 29 `hasButtons` property. 30 31 By default, `SC.ScrollerView` has a persistent gutter. If you would like a 32 gutterless scroller that supports fading, see `SC.OverlayScrollerView`. 33 34 @extends SC.View 35 @since SproutCore 1.0 36 */ 37 SC.ScrollerView = SC.View.extend( 38 /** @scope SC.ScrollerView.prototype */ { 39 40 /** @private 41 @type Array 42 @default ['sc-scroller-view'] 43 @see SC.View#classNames 44 */ 45 classNames: ['sc-scroller-view'], 46 47 /** @private 48 @type Array 49 @default ['thumbPosition', 'thumbLength', 'controlsHidden'] 50 @see SC.View#displayProperties 51 */ 52 displayProperties: ['thumbPosition', 'thumbLength', 'controlsHidden'], 53 54 /** @private 55 The WAI-ARIA role for scroller view. 56 57 @type String 58 @default 'scrollbar' 59 @readOnly 60 */ 61 ariaRole: 'scrollbar', 62 63 64 // .......................................................... 65 // PROPERTIES 66 // 67 68 /** 69 If YES, a click on the track will cause the scrollbar to scroll to that position. 70 Otherwise, a click on the track will cause a page down. 71 72 In either case, alt-clicks will perform the opposite behavior. 73 74 @type Boolean 75 @default NO 76 */ 77 shouldScrollToClick: NO, 78 79 /** 80 The value of the scroller. 81 82 The value represents the position of the scroller's thumb. 83 84 @field 85 @type Number 86 @default null 87 */ 88 value: null, 89 90 /** 91 The displayed value of the scroller. 92 93 This is the value of the scroller constrained within the minimum and maximum values. 94 95 @type Number 96 @observes value 97 */ 98 displayValue: function () { 99 return Math.max(Math.min(this.get("value"), this.get('maximum')), this.get('minimum')); 100 }.property("value", 'minimum', 'maximum').cacheable(), 101 102 /** 103 The portion of the track that the thumb should fill. Usually the 104 proportion will be the ratio of the size of the scroll view's content view 105 to the size of the scroll view. 106 107 Should be specified as a value between 0.0 (minimal size) and 1.0 (fills 108 the slot). Note that if the proportion is 1.0 then the control will be 109 disabled. 110 111 @type Number 112 @default 0.0 113 */ 114 proportion: 0, 115 116 /** 117 The maximum offset value for the scroller. This will be used to calculate 118 the internal height/width of the scroller itself. 119 120 When set less than the height of the scroller, the scroller is disabled. 121 122 @type Number 123 @default 0 124 */ 125 maximum: 0, 126 127 /** 128 The minimum offset value for the scroller. This will be used to calculate 129 the internal height/width of the scroller itself. 130 131 @type Number 132 @default 0 133 */ 134 minimum: 0, 135 136 /** 137 YES to enable scrollbar, NO to disable it. Scrollbars will automatically 138 disable if the maximum scroll width does not exceed their capacity. 139 140 @field 141 @type Boolean 142 @default YES 143 @observes proportion 144 */ 145 isEnabled: function (key, value) { 146 if (value !== undefined) { 147 this._scsv_isEnabled = value; 148 } 149 150 if (this._scsv_isEnabled !== undefined) { 151 return this._scsv_isEnabled; 152 } 153 154 return this.get('proportion') < 1; 155 }.property('proportion').cacheable(), 156 157 /** @private */ 158 _scsv_isEnabled: undefined, 159 160 /** 161 Determine the layout direction. Determines whether the scrollbar should 162 appear horizontal or vertical. This must be set when the view is created. 163 Changing this once the view has been created will have no effect. Possible 164 values: 165 166 - SC.LAYOUT_VERTICAL 167 - SC.LAYOUT_HORIZONTAL 168 169 @type String 170 @default SC.LAYOUT_VERTICAL 171 */ 172 layoutDirection: SC.LAYOUT_VERTICAL, 173 174 /** 175 Whether or not the scroller should display scroll buttons 176 177 @type Boolean 178 @default YES 179 */ 180 hasButtons: YES, 181 182 183 // .......................................................... 184 // DISPLAY METRICS 185 // 186 187 /** 188 The width (if vertical scroller) or height (if horizontal scroller) of the 189 scrollbar. 190 191 @type Number 192 @default 14 193 */ 194 scrollbarThickness: 14, 195 196 /** 197 The width or height of the cap that encloses the track. 198 199 @type Number 200 @default 18 201 */ 202 capLength: 18, 203 204 /** 205 The amount by which the thumb overlaps the cap. 206 207 @type Number 208 @default 14 209 */ 210 capOverlap: 14, 211 212 /** 213 The width or height of the up/down or left/right arrow buttons. If the 214 scroller is not displaying arrows, this is the width or height of the end 215 cap. 216 217 @type Number 218 @defaut 41 219 */ 220 buttonLength: 41, 221 222 /** 223 The amount by which the thumb overlaps the arrow buttons. If the scroller 224 is not displaying arrows, this is the amount by which the thumb overlaps 225 the end cap. 226 227 @type Number 228 @default 9 229 */ 230 buttonOverlap: 9, 231 232 /** 233 The minimium length that the thumb will be, regardless of how much content 234 is in the scroll view. 235 236 @type Number 237 @default 20 238 */ 239 minimumThumbLength: 20, 240 241 // .......................................................... 242 // INTERNAL SUPPORT 243 // 244 245 246 /** @private 247 Generates the HTML that gets displayed to the user. 248 249 The first time render is called, the HTML will be output to the DOM. 250 Successive calls will reposition the thumb based on the value property. 251 252 @param {SC.RenderContext} context the render context 253 @param {Boolean} firstTime YES if this is creating a layer 254 @private 255 */ 256 render: function (context, firstTime) { 257 var ariaOrientation = 'vertical', 258 classNames = {}, 259 parentView = this.get('parentView'), 260 layoutDirection = this.get('layoutDirection'), 261 thumbPosition, thumbLength, thumbElement; 262 263 // We set a class name depending on the layout direction so that we can 264 // style them differently using CSS. 265 switch (layoutDirection) { 266 case SC.LAYOUT_VERTICAL: 267 classNames['sc-vertical'] = YES; 268 break; 269 case SC.LAYOUT_HORIZONTAL: 270 classNames['sc-horizontal'] = YES; 271 ariaOrientation = 'horizontal'; 272 break; 273 } 274 275 // The appearance of the scroller changes if disabled 276 // Whether to hide the thumb and buttons 277 classNames['controls-hidden'] = this.get('controlsHidden'); 278 279 // Change the class names of the DOM element all at once to improve 280 // performance 281 context.setClass(classNames); 282 283 // Calculate the position and size of the thumb 284 thumbLength = this.get('thumbLength'); 285 thumbPosition = this.get('thumbPosition'); 286 287 // If this is the first time, generate the actual HTML 288 if (firstTime) { 289 context.push('<div class="track"></div>', 290 '<div class="cap"></div>'); 291 this.renderButtons(context, this.get('hasButtons')); 292 this.renderThumb(context, layoutDirection, thumbLength, thumbPosition); 293 294 //addressing accessibility 295 context.setAttr('aria-orientation', ariaOrientation); 296 297 //addressing accessibility 298 context.setAttr('aria-valuemax', this.get('maximum')); 299 context.setAttr('aria-valuemin', this.get('minimum')); 300 context.setAttr('aria-valuenow', this.get('value')); 301 context.setAttr('aria-controls', parentView.getPath('contentView.layerId')); 302 } else { 303 // The HTML has already been generated, so all we have to do is 304 // reposition and resize the thumb 305 306 // If we aren't displaying controls don't bother 307 if (this.get('controlsHidden')) return; 308 309 thumbElement = this.$('.thumb'); 310 311 this.adjustThumb(thumbElement, thumbPosition, thumbLength); 312 313 //addressing accessibility 314 context.setAttr('aria-valuenow', this.get('value')); 315 if (this.didChangeFor('render-min', 'minimum')) context.setAttr('aria-valuemin', this.get('minimum')); 316 if (this.didChangeFor('render-max', 'maximum')) context.setAttr('aria-valuemax', this.get('maximum')); 317 } 318 }, 319 320 renderThumb: function (context, layoutDirection, thumbLength, thumbPosition) { 321 var styleString; 322 if (layoutDirection === SC.LAYOUT_HORIZONTAL) styleString = 'width: ' + thumbLength + 'px; left: ' + thumbPosition + 'px;'; 323 else styleString = 'height: ' + thumbLength + 'px; top: ' + thumbPosition + 'px;'; 324 325 context.push('<div class="thumb" style="%@">'.fmt(styleString), 326 '<div class="thumb-center"></div>', 327 '<div class="thumb-top"></div>', 328 '<div class="thumb-bottom"></div></div>'); 329 330 }, 331 332 renderButtons: function (context, hasButtons) { 333 if (hasButtons) { 334 context.push('<div class="button-bottom"></div><div class="button-top"></div>'); 335 } else { 336 context.push('<div class="endcap"></div>'); 337 } 338 }, 339 340 // .......................................................... 341 // THUMB MANAGEMENT 342 // 343 344 /** @private 345 Adjusts the thumb (for backwards-compatibility calls adjustThumbPosition+adjustThumbSize by default) 346 */ 347 adjustThumb: function (thumb, position, length) { 348 this.adjustThumbPosition(thumb, position); 349 this.adjustThumbSize(thumb, length); 350 }, 351 352 /** @private 353 Updates the position of the thumb DOM element. 354 355 @param {Number} position the position of the thumb in pixels 356 */ 357 adjustThumbPosition: function (thumb, thumbPosition) { 358 var transformAttribute = SC.browser.experimentalCSSNameFor('transform'), 359 thumbEl = thumb[0]; 360 361 // Don't touch the DOM if the position hasn't changed. 362 if (this._thumbPosition !== thumbPosition) { 363 // Consider that the parent view may be animating its final position, then we need to also animate 364 // our final position. 365 var parentView = this.get('parentView'), 366 parentIsAnimating = parentView._sc_isAnimating; 367 368 if (SC.platform.supportsCSSTransitions) { 369 var transitionStyle = SC.browser.experimentalStyleNameFor('transition'); 370 371 if (parentIsAnimating) { 372 var duration = parentView._sc_animationDuration, 373 timing = parentView._sc_animationTiming.toString(); 374 375 // Will use translation transform to position thumb. 376 if (SC.platform.supportsCSSTransforms) { 377 thumbEl.style[transitionStyle] = transformAttribute + ' ' + duration + 's ' + timing; 378 379 // Will use top/left style to position thumb. 380 } else { 381 switch (this.get('layoutDirection')) { 382 case SC.LAYOUT_VERTICAL: 383 thumbEl.style[transitionStyle] = 'top ' + duration + 's ' + timing; 384 break; 385 case SC.LAYOUT_HORIZONTAL: 386 thumbEl.style[transitionStyle] = 'left ' + duration + 's ' + timing; 387 break; 388 } 389 } 390 391 // No duration, clear any previous transition. 392 } else { 393 thumbEl.style[transitionStyle] = ''; 394 } 395 } 396 397 398 // Position the thumb. 399 var transformStyle; 400 switch (this.get('layoutDirection')) { 401 case SC.LAYOUT_VERTICAL: 402 403 // Use translation transform to position thumb. 404 if (SC.platform.supportsCSSTransforms) { 405 transformStyle = 'translateX(0px) translateY(' + thumbPosition + 'px)'; 406 407 // TODO: Is this a necessary check? 408 if (SC.platform.supportsCSS3DTransforms) { transformStyle += ' translateZ(0px)'; } 409 410 thumbEl.style[transformAttribute] = transformStyle; 411 412 // Use top style to position thumb. 413 } else { 414 thumbEl.style.top = thumbPosition; 415 } 416 417 break; 418 419 case SC.LAYOUT_HORIZONTAL: 420 // Use translation transform to position thumb. 421 if (SC.platform.supportsCSSTransforms) { 422 423 transformStyle = 'translateX(' + thumbPosition + 'px) translateY(0px)'; 424 425 // TODO: Is this a necessary check? 426 if (SC.platform.supportsCSS3DTransforms) { transformStyle += ' translateZ(0px)'; } 427 428 thumbEl.style[transformAttribute] = transformStyle; 429 430 // Use left style to position thumb. 431 } else { 432 thumbEl.style.left = thumbPosition; 433 } 434 435 break; 436 } 437 } 438 439 // Cache these values to check for changes. 440 this._thumbPosition = thumbPosition; 441 }, 442 443 /** @private */ 444 adjustThumbSize: function (thumb, size) { 445 // Don't touch the DOM if the size hasn't changed 446 if (this._thumbSize === size) return; 447 448 switch (this.get('layoutDirection')) { 449 case SC.LAYOUT_VERTICAL: 450 thumb.css('height', Math.max(size, this.get('minimumThumbLength'))); 451 break; 452 case SC.LAYOUT_HORIZONTAL: 453 thumb.css('width', Math.max(size, this.get('minimumThumbLength'))); 454 break; 455 } 456 457 this._thumbSize = size; 458 }, 459 460 // .......................................................... 461 // SCROLLER DIMENSION COMPUTED PROPERTIES 462 // 463 464 /** @private 465 Returns the total length of the track in which the thumb sits. 466 467 The length of the track is the height or width of the scroller, less the 468 cap length and the button length. This property is used to calculate the 469 position of the thumb relative to the view. 470 471 @property 472 */ 473 trackLength: function () { 474 var scrollerLength = this.get('scrollerLength'); 475 476 // Subtract the size of the top/left cap 477 scrollerLength -= this.get('capLength') - this.get('capOverlap'); 478 479 // Subtract the size of the scroll buttons, or the end cap if they are 480 // not shown. 481 scrollerLength -= this.buttonLength - this.buttonOverlap; 482 483 return scrollerLength; 484 }.property('scrollerLength').cacheable(), 485 486 /** @private 487 Returns the height of the view if this is a vertical scroller or the width 488 of the view if this is a horizontal scroller. This is used when scrolling 489 up and down by page, as well as in various layout calculations. 490 491 @type Number 492 */ 493 scrollerLength: function () { 494 switch (this.get('layoutDirection')) { 495 case SC.LAYOUT_VERTICAL: 496 return this.get('frame').height; 497 case SC.LAYOUT_HORIZONTAL: 498 return this.get('frame').width; 499 } 500 501 return 0; 502 }.property('frame').cacheable(), 503 504 /** @private 505 The total length of the thumb. The size of the thumb is the 506 length of the track times the content proportion. 507 508 @property 509 */ 510 thumbLength: function () { 511 var value = this.get('value'), 512 maximum = this.get('maximum'), 513 minimum = this.get('minimum'), 514 proportion = this.get('proportion'), 515 length; 516 517 // If the value is beyond the minimum or maximums, shrink our thumb length to represent the amount 518 // of over scroll. Do this proportionally for the best effect! 519 if (value < minimum) { 520 proportion -= (minimum - value) / maximum; 521 } else if (value > maximum) { 522 proportion -= (value - maximum) / maximum; 523 } 524 525 length = Math.floor(this.get('trackLength') * proportion); 526 length = isNaN(length) ? 0 : length; 527 528 return Math.max(length, this.get('minimumThumbLength')); 529 }.property('value', 'minimum', 'maximum', 'trackLength', 'proportion').cacheable(), 530 531 /** @private 532 The position of the thumb in the track. 533 534 @type Number 535 @isReadOnly 536 */ 537 thumbPosition: function () { 538 var displayValue = this.get('displayValue'), 539 maximum = this.get('maximum'), 540 trackLength = this.get('trackLength'), 541 thumbLength = this.get('thumbLength'), 542 capLength = this.get('capLength'), 543 capOverlap = this.get('capOverlap'), position; 544 545 position = (displayValue / maximum) * (trackLength - thumbLength); 546 position += capLength - capOverlap; // account for the top/left cap 547 548 return Math.floor(isNaN(position) ? 0 : position); 549 }.property('displayValue', 'maximum', 'trackLength', 'thumbLength').cacheable(), 550 551 /** @private 552 YES if the maximum value exceeds the frame size of the scroller. This 553 will hide the thumb and buttons. 554 555 @type Boolean 556 @isReadOnly 557 */ 558 controlsHidden: function () { 559 return this.get('proportion') >= 1; 560 }.property('proportion').cacheable(), 561 562 // .......................................................... 563 // FADE SUPPORT 564 // Controls how the scroller fades in and out. Override these methods to implement 565 // different fading. 566 // 567 568 /* 569 Implement to support ScrollView's overlay fade procedure. 570 571 @param {Number} duration 572 */ 573 fadeIn: null, 574 575 /* 576 Implement to support ScrollView's overlay fade procedure. 577 578 @param {Number} duration 579 */ 580 fadeOut: null, 581 582 // .......................................................... 583 // MOUSE EVENTS 584 // 585 586 /** @private 587 Returns the value for a position within the scroller's frame. 588 */ 589 valueForPosition: function (pos) { 590 var max = this.get('maximum'), 591 trackLength = this.get('trackLength'), 592 thumbLength = this.get('thumbLength'), 593 capLength = this.get('capLength'), 594 capOverlap = this.get('capOverlap'), value; 595 596 value = pos - (capLength - capOverlap); 597 value = value / (trackLength - thumbLength); 598 value = value * max; 599 return value; 600 }, 601 602 /** @private 603 Handles mouse down events and adjusts the value property depending where 604 the user clicked. 605 606 If the control is disabled, we ignore all mouse input. 607 608 If the user clicks the thumb, we note the position of the mouse event but 609 do not take further action until they begin to drag. 610 611 If the user clicks the track, we adjust the value a page at a time, unless 612 alt is pressed, in which case we scroll to that position. 613 614 If the user clicks the buttons, we adjust the value by a fixed amount, unless 615 alt is pressed, in which case we adjust by a page. 616 617 If the user clicks and holds on either the track or buttons, those actions 618 are repeated until they release the mouse button. 619 620 @param evt {SC.Event} the mousedown event 621 */ 622 mouseDown: function (evt) { 623 // Fast path, reject secondary clicks. 624 if (evt.which !== 1) return false; 625 626 if (!this.get('isEnabledInPane')) return NO; 627 628 // keep note of altIsDown for later. 629 this._altIsDown = evt.altKey; 630 this._shiftIsDown = evt.shiftKey; 631 632 var target = evt.target, 633 thumbPosition = this.get('thumbPosition'), 634 clickLocation, 635 scrollerLength = this.get('scrollerLength'); 636 637 // Determine the subcontrol that was clicked 638 if (target.className.indexOf('thumb') >= 0) { 639 // Convert the mouseDown coordinates to the view's coordinates 640 clickLocation = this.convertFrameFromView({ x: evt.pageX, y: evt.pageY }); 641 642 clickLocation.x -= thumbPosition; 643 clickLocation.y -= thumbPosition; 644 645 // Store the starting state so we know how much to adjust the 646 // thumb when the user drags 647 this._thumbDragging = YES; 648 this._thumbOffset = clickLocation; 649 this._mouseDownLocation = { x: evt.pageX, y: evt.pageY }; 650 this._thumbPositionAtDragStart = this.get('thumbPosition'); 651 this._valueAtDragStart = this.get("value"); 652 } else if (target.className.indexOf('button-top') >= 0) { 653 // User clicked the up/left button 654 // Decrement the value by a fixed amount or page size 655 this.decrementProperty('value', (this._altIsDown ? scrollerLength : 30)); 656 this.makeButtonActive('.button-top'); 657 // start a timer that will continue to fire until mouseUp is called 658 this.startMouseDownTimer('scrollUp'); 659 this._isScrollingUp = YES; 660 } else if (target.className.indexOf('button-bottom') >= 0) { 661 // User clicked the down/right button 662 // Increment the value by a fixed amount 663 this.incrementProperty('value', (this._altIsDown ? scrollerLength : 30)); 664 this.makeButtonActive('.button-bottom'); 665 // start a timer that will continue to fire until mouseUp is called 666 this.startMouseDownTimer('scrollDown'); 667 this._isScrollingDown = YES; 668 } else { 669 // User clicked in the track 670 var scrollToClick = this.get("shouldScrollToClick"); 671 if (evt.altKey) scrollToClick = !scrollToClick; 672 673 var thumbLength = this.get('thumbLength'), 674 frame = this.convertFrameFromView({ x: evt.pageX, y: evt.pageY }), 675 mousePosition; 676 677 switch (this.get('layoutDirection')) { 678 case SC.LAYOUT_VERTICAL: 679 this._mouseDownLocation = mousePosition = frame.y; 680 break; 681 case SC.LAYOUT_HORIZONTAL: 682 this._mouseDownLocation = mousePosition = frame.x; 683 break; 684 } 685 686 if (scrollToClick) { 687 this.set('value', Math.min(this.get('maximum'), Math.max(this.get('minimum'), this.valueForPosition(mousePosition - (thumbLength / 2))))); 688 689 // and start a normal mouse down 690 thumbPosition = this.get('thumbPosition'); 691 692 this._thumbDragging = YES; 693 this._thumbOffset = { x: frame.x - thumbPosition, y: frame.y - thumbPosition }; 694 this._mouseDownLocation = { x: evt.pageX, y: evt.pageY }; 695 this._thumbPositionAtDragStart = thumbPosition; 696 this._valueAtDragStart = this.get("value"); 697 } else { 698 // Move the thumb up or down a page depending on whether the click 699 // was above or below the thumb 700 if (mousePosition < thumbPosition) { 701 this.decrementProperty('value', scrollerLength); 702 this.startMouseDownTimer('page'); 703 } else { 704 this.incrementProperty('value', scrollerLength); 705 this.startMouseDownTimer('page'); 706 } 707 } 708 709 } 710 711 return YES; 712 }, 713 714 /** @private 715 When the user releases the mouse button, remove any active 716 state from the button controls, and cancel any outstanding 717 timers. 718 719 @param evt {SC.Event} the mousedown event 720 */ 721 mouseUp: function (evt) { 722 var active = this._scs_buttonActive, ret = NO, timer; 723 724 // If we have an element that was set as active in mouseDown, 725 // remove its active state 726 if (active) { 727 active.removeClass('active'); 728 ret = YES; 729 } 730 731 // Stop firing repeating events after mouseup 732 timer = this._mouseDownTimer; 733 if (timer) { 734 timer.invalidate(); 735 this._mouseDownTimer = null; 736 } 737 738 this._thumbDragging = NO; 739 this._isScrollingDown = NO; 740 this._isScrollingUp = NO; 741 742 return ret; 743 }, 744 745 /** @private 746 If the user began the drag on the thumb, we calculate the difference 747 between the mouse position at click and where it is now. We then 748 offset the thumb by that amount, within the bounds of the track. 749 750 If the user began scrolling up/down using the buttons, this will track 751 what component they are currently over, changing the scroll direction. 752 753 @param evt {SC.Event} the mousedragged event 754 */ 755 mouseDragged: function (evt) { 756 if (!this.get('isEnabledInPane')) return NO; 757 758 var length, delta, thumbPosition, 759 thumbPositionAtDragStart = this._thumbPositionAtDragStart, 760 isScrollingUp = this._isScrollingUp, 761 isScrollingDown = this._isScrollingDown, 762 active = this._scs_buttonActive; 763 764 // Only move the thumb if the user clicked on the thumb during mouseDown 765 if (this._thumbDragging) { 766 767 switch (this.get('layoutDirection')) { 768 case SC.LAYOUT_VERTICAL: 769 delta = (evt.pageY - this._mouseDownLocation.y); 770 break; 771 case SC.LAYOUT_HORIZONTAL: 772 delta = (evt.pageX - this._mouseDownLocation.x); 773 break; 774 } 775 776 // if we are in alt now, but were not before, update the old thumb position to the new one 777 if (evt.altKey) { 778 if (!this._altIsDown || (this._shiftIsDown !== evt.shiftKey)) { 779 thumbPositionAtDragStart = this._thumbPositionAtDragStart = thumbPositionAtDragStart + delta; 780 delta = 0; 781 this._mouseDownLocation = { x: evt.pageX, y: evt.pageY }; 782 this._valueAtDragStart = this.get("value"); 783 } 784 785 // because I feel like it. Probably almost no one will find this tiny, buried feature. 786 // Too bad. 787 if (evt.shiftKey) delta = -delta; 788 789 this.set('value', Math.min(this.get('maximum'), Math.max(this.get('minimum'), Math.round(this._valueAtDragStart + delta * 2)))); 790 } else { 791 thumbPosition = thumbPositionAtDragStart + delta; 792 length = this.get('trackLength') - this.get('thumbLength'); 793 this.set('value', Math.min(this.get('maximum'), Math.max(this.get('minimum'), Math.round((thumbPosition / length) * this.get('maximum'))))); 794 } 795 796 } else if (isScrollingUp || isScrollingDown) { 797 var nowScrollingUp = NO, nowScrollingDown = NO; 798 799 var topButtonRect = this.$('.button-top')[0].getBoundingClientRect(); 800 801 switch (this.get('layoutDirection')) { 802 case SC.LAYOUT_VERTICAL: 803 if (evt.clientY < topButtonRect.bottom) nowScrollingUp = YES; 804 else nowScrollingDown = YES; 805 break; 806 case SC.LAYOUT_HORIZONTAL: 807 if (evt.clientX < topButtonRect.right) nowScrollingUp = YES; 808 else nowScrollingDown = YES; 809 break; 810 } 811 812 if ((nowScrollingUp || nowScrollingDown) && nowScrollingUp !== isScrollingUp) { 813 // 814 // STOP OLD 815 // 816 817 // If we have an element that was set as active in mouseDown, 818 // remove its active state 819 if (active) { 820 active.removeClass('active'); 821 } 822 823 // Stop firing repeating events after mouseup 824 this._mouseDownTimerAction = nowScrollingUp ? "scrollUp" : "scrollDown"; 825 826 if (nowScrollingUp) { 827 this.makeButtonActive('.button-top'); 828 } else if (nowScrollingDown) { 829 this.makeButtonActive('.button-bottom'); 830 } 831 832 this._isScrollingUp = nowScrollingUp; 833 this._isScrollingDown = nowScrollingDown; 834 } 835 } 836 837 838 this._altIsDown = evt.altKey; 839 this._shiftIsDown = evt.shiftKey; 840 return YES; 841 }, 842 843 /** @private 844 Starts a timer that fires after 300ms. This is called when the user 845 clicks a button or inside the track to move a page at a time. If they 846 continue holding the mouse button down, we want to repeat that action 847 after a small delay. This timer will be invalidated in mouseUp. 848 849 Specify "immediate" as YES if it should not wait. 850 */ 851 startMouseDownTimer: function (action, immediate) { 852 this._mouseDownTimerAction = action; 853 this._mouseDownTimer = SC.Timer.schedule({ 854 target: this, 855 action: this.mouseDownTimerDidFire, 856 interval: immediate ? 0 : 300 857 }); 858 }, 859 860 /** @private 861 Called by the mousedown timer. This method determines the initial 862 user action and repeats it until the timer is invalidated in mouseUp. 863 */ 864 mouseDownTimerDidFire: function () { 865 var scrollerLength = this.get('scrollerLength'), 866 mouseLocation = SC.device.get('mouseLocation'), 867 thumbPosition = this.get('thumbPosition'), 868 thumbLength = this.get('thumbLength'), 869 timerInterval = 50; 870 871 switch (this.get('layoutDirection')) { 872 case SC.LAYOUT_VERTICAL: 873 mouseLocation = this.convertFrameFromView(mouseLocation).y; 874 break; 875 case SC.LAYOUT_HORIZONTAL: 876 mouseLocation = this.convertFrameFromView(mouseLocation).x; 877 break; 878 } 879 880 switch (this._mouseDownTimerAction) { 881 case 'scrollDown': 882 this.incrementProperty('value', this._altIsDown ? scrollerLength : 30); 883 break; 884 case 'scrollUp': 885 this.decrementProperty('value', this._altIsDown ? scrollerLength : 30); 886 break; 887 case 'page': 888 timerInterval = 150; 889 if (mouseLocation < thumbPosition) { 890 this.decrementProperty('value', scrollerLength); 891 } else if (mouseLocation > thumbPosition + thumbLength) { 892 this.incrementProperty('value', scrollerLength); 893 } 894 } 895 896 this._mouseDownTimer = SC.Timer.schedule({ 897 target: this, 898 action: this.mouseDownTimerDidFire, 899 interval: timerInterval 900 }); 901 }, 902 903 /** @private 904 Given a selector, finds the corresponding DOM element and adds 905 the 'active' class name. Also stores the returned element so that 906 the 'active' class name can be removed during mouseup. 907 908 @param {String} the selector to find 909 */ 910 makeButtonActive: function (selector) { 911 this._scs_buttonActive = this.$(selector).addClass('active'); 912 } 913 }); 914 915 /** 916 A fading, transparent-backed scroll bar. Suitable for use as an overlaid scroller. (Note 917 that to achieve the overlay effect, you must still set `verticalOverlay` and 918 `horizontalOverlay` on your `ScrollView`.) 919 920 @class 921 @extends SC.ScrollerView 922 */ 923 SC.OverlayScrollerView = SC.ScrollerView.extend( 924 /** @scope SC.OverlayScrollerView.prototype */{ 925 926 // .......................................................... 927 // FADE SUPPORT 928 // Controls how the scroller fades in and out. Override these methods to implement 929 // different fading. 930 // 931 932 /* 933 Supports ScrollView's overlay fade procedure. 934 */ 935 fadeIn: function () { 936 this.$().toggleClass('fade-in', true); 937 this.$().toggleClass('fade-out', false); 938 }, 939 940 /* 941 Supports ScrollView's overlay fade procedure. 942 */ 943 fadeOut: function () { 944 this.$().toggleClass('fade-in', false); 945 this.$().toggleClass('fade-out', true); 946 }, 947 948 /** 949 @type Array 950 @default ['sc-touch-scroller-view', 'sc-overlay-scroller-view] 951 @see SC.View#classNames 952 */ 953 classNames: ['sc-touch-scroller-view', 'sc-overlay-scroller-view'], 954 955 /** 956 @type Number 957 @default 12 958 */ 959 scrollbarThickness: 12, 960 961 /** 962 @type Number 963 @default 3 964 */ 965 capLength: 3, 966 967 /** 968 @type Number 969 @default 0 970 */ 971 capOverlap: 0, 972 973 /** 974 @type Number 975 @default 3 976 */ 977 buttonLength: 3, 978 979 /** 980 @type Number 981 @default 0 982 */ 983 buttonOverlap: 0, 984 985 /** 986 @type Boolean 987 @default NO 988 */ 989 hasButtons: NO, 990 991 /** @private */ 992 adjustThumb: function (thumb, thumbPosition, thumbLength) { 993 var transformAttribute = SC.browser.experimentalCSSNameFor('transform'), 994 thumbEl = thumb[0], 995 thumbInner = this.$('.thumb-inner'), 996 thumbInnerEl = thumbInner[0]; 997 998 // Don't touch the DOM if the position hasn't changed. 999 if (this._thumbPosition !== thumbPosition) { 1000 // Consider that the parent view may be animating its final position, then we need to also animate 1001 // our final position. 1002 var parentView = this.get('parentView'), 1003 parentIsAnimating = parentView._sc_isAnimating; 1004 1005 if (SC.platform.supportsCSSTransitions) { 1006 var transitionStyle = SC.browser.experimentalStyleNameFor('transition'); 1007 1008 if (parentIsAnimating) { 1009 var duration = parentView._sc_animationDuration, 1010 timing = parentView._sc_animationTiming.toString(); 1011 1012 // Will use translation transform to position thumb. 1013 if (SC.platform.supportsCSSTransforms) { 1014 thumbEl.style[transitionStyle] = transformAttribute + ' ' + duration + 's ' + timing; 1015 1016 if (this._thumbSize !== thumbLength) { 1017 thumbInnerEl.style[transitionStyle] = transformAttribute + ' ' + duration + 's ' + timing; 1018 } 1019 1020 // Will use top/left style to position thumb. 1021 } else { 1022 switch (this.get('layoutDirection')) { 1023 case SC.LAYOUT_VERTICAL: 1024 thumbEl.style[transitionStyle] = 'top ' + duration + 's ' + timing; 1025 1026 if (this._thumbSize !== thumbLength) { 1027 thumbInnerEl.style[transitionStyle] = 'top ' + duration + 's ' + timing; 1028 } 1029 1030 break; 1031 case SC.LAYOUT_HORIZONTAL: 1032 thumbEl.style[transitionStyle] = 'left ' + duration + 's ' + timing; 1033 1034 if (this._thumbSize !== thumbLength) { 1035 thumbInnerEl.style[transitionStyle] = 'left ' + duration + 's ' + timing; 1036 } 1037 1038 break; 1039 } 1040 } 1041 1042 // No duration, clear any previous transition. 1043 } else { 1044 thumbEl.style[transitionStyle] = ''; 1045 thumbInnerEl.style[transitionStyle] = ''; 1046 } 1047 } 1048 1049 1050 // Position the thumb. 1051 var transformStyle; 1052 switch (this.get('layoutDirection')) { 1053 case SC.LAYOUT_VERTICAL: 1054 1055 // Use translation transform to position thumb. 1056 if (SC.platform.supportsCSSTransforms) { 1057 transformStyle = 'translateX(0px) translateY(' + thumbPosition + 'px)'; 1058 1059 // TODO: Is this a necessary check? 1060 if (SC.platform.supportsCSS3DTransforms) { transformStyle += ' translateZ(0px)'; } 1061 1062 thumbEl.style[transformAttribute] = transformStyle; 1063 // thumb.css(transformCSS, 'translate3d(0px,' + thumbPosition + 'px,0px)'); 1064 1065 if (this._thumbSize !== thumbLength) { 1066 transformStyle = 'translateX(0px) translateY(' + Math.round(thumbLength - 1044) + 'px)'; 1067 1068 // TODO: Is this a necessary check? 1069 if (SC.platform.supportsCSS3DTransforms) { transformStyle += ' translateZ(0px)'; } 1070 1071 // thumbInner.css(transformCSS, 'translate3d(0px,' + Math.round(thumbLength - 1044) + 'px,0px)'); 1072 thumbInnerEl.style[transformAttribute] = transformStyle; 1073 } 1074 1075 // Use top style to position thumb. 1076 } else { 1077 thumbEl.style.top = thumbPosition; 1078 1079 if (this._thumbSize !== thumbLength) { 1080 thumbInnerEl.style.top = Math.round(thumbLength - 1044); 1081 } 1082 } 1083 1084 break; 1085 1086 case SC.LAYOUT_HORIZONTAL: 1087 // Use translation transform to position thumb. 1088 if (SC.platform.supportsCSSTransforms) { 1089 1090 transformStyle = 'translateX(' + thumbPosition + 'px) translateY(0px)'; 1091 1092 // TODO: Is this a necessary check? 1093 if (SC.platform.supportsCSS3DTransforms) { transformStyle += ' translateZ(0px)'; } 1094 1095 thumbEl.style[transformAttribute] = transformStyle; 1096 // thumb.css(transformCSS, 'translate3d(0px,' + thumbPosition + 'px,0px)'); 1097 1098 if (this._thumbSize !== thumbLength) { 1099 transformStyle = 'translateX(' + Math.round(thumbLength - 1044) + 'px) translateY(0px)'; 1100 1101 // TODO: Is this a necessary check? 1102 if (SC.platform.supportsCSS3DTransforms) { transformStyle += ' translateZ(0px)'; } 1103 1104 // thumbInner.css(transformCSS, 'translate3d(0px,' + Math.round(thumbLength - 1044) + 'px,0px)'); 1105 thumbInnerEl.style[transformAttribute] = transformStyle; 1106 } 1107 1108 // Use left style to position thumb. 1109 } else { 1110 thumbEl.style.left = thumbPosition; 1111 1112 if (this._thumbSize !== thumbLength) { 1113 thumbInnerEl.style.left = Math.round(thumbLength - 1044); 1114 } 1115 } 1116 1117 break; 1118 } 1119 } 1120 1121 // Cache these values to check for changes. 1122 this._thumbPosition = thumbPosition; 1123 this._thumbSize = thumbLength; 1124 }, 1125 1126 /** @private */ 1127 render: function (context, firstTime) { 1128 var classNames = [], 1129 thumbPosition, thumbLength, thumbElement; 1130 1131 // We set a class name depending on the layout direction so that we can 1132 // style them differently using CSS. 1133 switch (this.get('layoutDirection')) { 1134 case SC.LAYOUT_VERTICAL: 1135 classNames.push('sc-vertical'); 1136 break; 1137 case SC.LAYOUT_HORIZONTAL: 1138 classNames.push('sc-horizontal'); 1139 break; 1140 } 1141 1142 // Whether to hide the thumb and buttons 1143 if (this.get('controlsHidden')) classNames.push('controls-hidden'); 1144 1145 // Change the class names of the DOM element all at once to improve 1146 // performance 1147 context.addClass(classNames); 1148 1149 // Calculate the position and size of the thumb 1150 thumbLength = this.get('thumbLength'); 1151 thumbPosition = this.get('thumbPosition'); 1152 1153 // If this is the first time, generate the actual HTML 1154 if (firstTime) { 1155 context.push('<div class="track"></div>' + 1156 '<div class="cap"></div>'); 1157 this.renderButtons(context, this.get('hasButtons')); 1158 this.renderThumb(context, thumbPosition, thumbLength); 1159 1160 // The HTML has already been generated, so all we have to do is 1161 // reposition and resize the thumb 1162 } else { 1163 1164 // If we aren't displaying controls don't bother 1165 if (this.get('controlsHidden')) return; 1166 1167 thumbElement = this.$('.thumb'); 1168 1169 this.adjustThumb(thumbElement, thumbPosition, thumbLength); 1170 } 1171 }, 1172 1173 /** @private */ 1174 renderThumb: function (context, thumbPosition, thumbLength) { 1175 var transformCSS = SC.browser.experimentalCSSNameFor('transform'), 1176 thumbPositionStyle, thumbSizeStyle; 1177 1178 switch (this.get('layoutDirection')) { 1179 case SC.LAYOUT_VERTICAL: 1180 thumbPositionStyle = transformCSS + ': translate3d(0px,' + thumbPosition + 'px,0px)'; 1181 // where is this magic number from? 1182 thumbSizeStyle = transformCSS + ': translateY(' + (thumbLength - 1044) + 'px)'.fmt(); 1183 break; 1184 case SC.LAYOUT_HORIZONTAL: 1185 thumbPositionStyle = transformCSS + ': translate3d(' + thumbPosition + 'px,0px,0px)'; 1186 thumbSizeStyle = transformCSS + ': translateX(' + (thumbLength - 1044) + 'px)'.fmt(); 1187 break; 1188 } 1189 1190 context.push('<div class="thumb" style="%@;">'.fmt(thumbPositionStyle) + 1191 '<div class="thumb-top"></div>' + 1192 '<div class="thumb-clip">' + 1193 '<div class="thumb-inner" style="%@;">'.fmt(thumbSizeStyle) + 1194 '<div class="thumb-center"></div>' + 1195 '<div class="thumb-bottom"></div></div></div></div>'); 1196 1197 // Cache these values to check for changes. 1198 this._thumbPosition = thumbPosition; 1199 this._thumbSize = thumbLength; 1200 } 1201 }); 1202 1203 1204 /* @private Old inaccurate name retained for backward compatibility. */ 1205 SC.TouchScrollerView = SC.OverlayScrollerView.extend({ 1206 //@if(debug) 1207 init: function () { 1208 SC.warn('Developer Warning: SC.TouchScrollerView has been renamed SC.OverlayScrollerView. SC.TouchScrollerView will be removed entirely in a future version.'); 1209 return sc_super(); 1210 } 1211 //@endif 1212 }); 1213