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