Home | History | Annotate | Download | only in js
      1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 'use strict';
      6 
      7 /**
      8  * Creates a new scroll bar element.
      9  * @extends {HTMLDivElement}
     10  * @constructor
     11  */
     12 var ScrollBar = cr.ui.define('div');
     13 
     14 /**
     15  * Mode of the scrollbar. As for now, only vertical scrollbars are supported.
     16  * @type {number}
     17  */
     18 ScrollBar.Mode = {
     19   VERTICAL: 0,
     20   HORIZONTAL: 1
     21 };
     22 
     23 ScrollBar.prototype = {
     24   set mode(value) {
     25     this.mode_ = value;
     26     if (this.mode_ == ScrollBar.Mode.VERTICAL) {
     27       this.classList.remove('scrollbar-horizontal');
     28       this.classList.add('scrollbar-vertical');
     29     } else {
     30       this.classList.remove('scrollbar-vertical');
     31       this.classList.add('scrollbar-horizontal');
     32     }
     33     this.redraw_();
     34   },
     35   get mode() {
     36     return this.mode_;
     37   }
     38 };
     39 
     40 /**
     41  * Inherits after HTMLDivElement.
     42  */
     43 ScrollBar.prototype.__proto__ = HTMLDivElement.prototype;
     44 
     45 /**
     46  * Initializes the DOM structure of the scrollbar.
     47  */
     48 ScrollBar.prototype.decorate = function() {
     49   this.classList.add('scrollbar');
     50   this.button_ = util.createChild(this, 'scrollbar-button', 'div');
     51   this.mode = ScrollBar.Mode.VERTICAL;
     52   this.idleTimerId_ = 0;
     53 
     54   this.button_.addEventListener('mousedown',
     55                                 this.onButtonPressed_.bind(this));
     56   window.addEventListener('mouseup', this.onMouseUp_.bind(this));
     57   window.addEventListener('mousemove', this.onMouseMove_.bind(this));
     58 };
     59 
     60 /**
     61  * Initialize a scrollbar.
     62  *
     63  * @param {Element} parent Parent element, must have a relative or absolute
     64  *     positioning.
     65  * @param {Element=} opt_scrollableArea Element with scrollable contents.
     66  *     If not passed, then call attachToView manually when the scrollable
     67  *     element becomes available.
     68  */
     69 ScrollBar.prototype.initialize = function(parent, opt_scrollableArea) {
     70   parent.appendChild(this);
     71   if (opt_scrollableArea)
     72     this.attachToView(opt_scrollableArea);
     73 };
     74 
     75 /**
     76  * Attaches the scrollbar to a scrollable element and attaches handlers.
     77  * @param {Element} view Scrollable element.
     78  */
     79 ScrollBar.prototype.attachToView = function(view) {
     80   this.view_ = view;
     81   this.view_.addEventListener('scroll', this.onScroll_.bind(this));
     82   this.view_.addEventListener('relayout', this.onRelayout_.bind(this));
     83   this.domObserver_ = new MutationObserver(this.onDomChanged_.bind(this));
     84   this.domObserver_.observe(this.view_, {subtree: true, attributes: true});
     85   this.onRelayout_();
     86 };
     87 
     88 /**
     89  * Scroll handler.
     90  * @private
     91  */
     92 ScrollBar.prototype.onScroll_ = function() {
     93   this.scrollTop_ = this.view_.scrollTop;
     94   this.redraw_();
     95 
     96   // Add class 'scrolling' to scrollbar to make it visible while scrolling.
     97   this.button_.classList.add('scrolling');
     98 
     99   // Set timer to remove class 'scrolling' after scrolling becomes idle.
    100   if (this.idleTimerId_)
    101     clearTimeout(this.idleTimerId_);
    102   this.idleTimerId_ = setTimeout(function() {
    103     this.idleTimerId_ = 0;
    104     this.button_.classList.remove('scrolling');
    105   }.bind(this), 1000);
    106 };
    107 
    108 /**
    109  * Relayout handler.
    110  * @private
    111  */
    112 ScrollBar.prototype.onRelayout_ = function() {
    113   this.scrollHeight_ = this.view_.scrollHeight;
    114   this.clientHeight_ = this.view_.clientHeight;
    115   this.offsetTop_ = this.view_.offsetTop;
    116   this.scrollTop_ = this.view_.scrollTop;
    117   this.redraw_();
    118 };
    119 
    120 /**
    121  * Pressing on the scrollbar's button handler.
    122  *
    123  * @param {Event} event Pressing event.
    124  * @private
    125  */
    126 ScrollBar.prototype.onButtonPressed_ = function(event) {
    127   this.buttonPressed_ = true;
    128   this.buttonPressedEvent_ = event;
    129   this.buttonPressedPosition_ = this.button_.offsetTop - this.view_.offsetTop;
    130   this.button_.classList.add('pressed');
    131 
    132   event.preventDefault();
    133 };
    134 
    135 /**
    136  * Releasing the button handler. Note, that it may not be called when releasing
    137  * outside of the window. Therefore this is also called from onMouseMove_.
    138  *
    139  * @param {Event} event Mouse event.
    140  * @private
    141  */
    142 ScrollBar.prototype.onMouseUp_ = function(event) {
    143   this.buttonPressed_ = false;
    144   this.button_.classList.remove('pressed');
    145 };
    146 
    147 /**
    148  * Mouse move handler. Updates the scroll position.
    149  *
    150  * @param {Event} event Mouse event.
    151  * @private
    152  */
    153 ScrollBar.prototype.onMouseMove_ = function(event) {
    154   if (!this.buttonPressed_)
    155     return;
    156   if (!event.which) {
    157     this.onMouseUp_(event);
    158     return;
    159   }
    160   var clientSize = this.getClientHeight();
    161   var totalSize = this.getTotalHeight();
    162   // TODO(hirono): Fix the geometric calculation.  crbug.com/253779
    163   var buttonSize = Math.max(50, clientSize / totalSize * clientSize);
    164   var buttonPosition = this.buttonPressedPosition_ +
    165       (event.screenY - this.buttonPressedEvent_.screenY);
    166   // Ensures the scrollbar is in the view.
    167   buttonPosition =
    168       Math.max(0, Math.min(buttonPosition, clientSize - buttonSize));
    169   var scrollPosition;
    170   if (clientSize > buttonSize) {
    171     scrollPosition = Math.max(totalSize - clientSize, 0) *
    172         buttonPosition / (clientSize - buttonSize);
    173   } else {
    174     scrollPosition = 0;
    175   }
    176 
    177   this.scrollTop_ = scrollPosition;
    178   this.view_.scrollTop = scrollPosition;
    179   this.redraw_();
    180 };
    181 
    182 /**
    183  * Handles changed in Dom by redrawing the scrollbar. Ignores consecutive calls.
    184  * @private
    185  */
    186 ScrollBar.prototype.onDomChanged_ = function() {
    187   if (this.domChangedTimer_) {
    188     clearTimeout(this.domChangedTimer_);
    189     this.domChangedTimer_ = null;
    190   }
    191   this.domChangedTimer_ = setTimeout(function() {
    192     this.onRelayout_();
    193     this.domChangedTimer_ = null;
    194   }.bind(this), 50);
    195 };
    196 
    197 /**
    198  * Redraws the scrollbar.
    199  * @private
    200  */
    201 ScrollBar.prototype.redraw_ = function() {
    202   if (!this.view_)
    203     return;
    204 
    205   var clientSize = this.getClientHeight();
    206   var clientTop = this.offsetTop_;
    207   var scrollPosition = this.scrollTop_;
    208   var totalSize = this.getTotalHeight();
    209   var hidden = totalSize <= clientSize;
    210 
    211   var buttonSize = Math.max(50, clientSize / totalSize * clientSize);
    212   var buttonPosition;
    213   if (clientSize - buttonSize > 0) {
    214     buttonPosition = scrollPosition / (totalSize - clientSize) *
    215         (clientSize - buttonSize);
    216   } else {
    217     buttonPosition = 0;
    218   }
    219   var buttonTop = buttonPosition + clientTop;
    220 
    221   var time = Date.now();
    222   if (this.hidden != hidden ||
    223       this.lastButtonTop_ != buttonTop ||
    224       this.lastButtonSize_ != buttonSize) {
    225     requestAnimationFrame(function() {
    226       this.hidden = hidden;
    227       this.button_.style.top = buttonTop + 'px';
    228       this.button_.style.height = buttonSize + 'px';
    229     }.bind(this));
    230   }
    231 
    232   this.lastButtonTop_ = buttonTop;
    233   this.lastButtonSize_ = buttonSize;
    234 };
    235 
    236 /**
    237  * Returns the viewport height of the view.
    238  * @return {number} The viewport height of the view in px.
    239  * @protected
    240  */
    241 ScrollBar.prototype.getClientHeight = function() {
    242   return this.clientHeight_;
    243 };
    244 
    245 /**
    246  * Returns the total height of the view.
    247  * @return {number} The total height of the view in px.
    248  * @protected
    249  */
    250 ScrollBar.prototype.getTotalHeight = function() {
    251   return this.scrollHeight_;
    252 };
    253 
    254 /**
    255  * Creates a new scroll bar for elements in the main panel.
    256  * @extends {ScrollBar}
    257  * @constructor
    258  */
    259 var MainPanelScrollBar = cr.ui.define('div');
    260 
    261 /**
    262  * Inherits after ScrollBar.
    263  */
    264 MainPanelScrollBar.prototype.__proto__ = ScrollBar.prototype;
    265 
    266 /** @override */
    267 MainPanelScrollBar.prototype.decorate = function() {
    268   ScrollBar.prototype.decorate.call(this);
    269 
    270   /**
    271    * Margin for the transparent preview panel at the bottom.
    272    * @type {number}
    273    * @private
    274    */
    275   this.bottomMarginForPanel_ = 0;
    276 };
    277 
    278 /**
    279  * GReturns the viewport height of the view, considering the preview panel.
    280  *
    281  * @return {number} The viewport height of the view in px.
    282  * @override
    283  * @protected
    284  */
    285 MainPanelScrollBar.prototype.getClientHeight = function() {
    286   return this.clientHeight_ - this.bottomMarginForPanel_;
    287 };
    288 
    289 /**
    290  * Returns the total height of the view, considering the preview panel.
    291  *
    292  * @return {number} The total height of the view in px.
    293  * @override
    294  * @protected
    295  */
    296 MainPanelScrollBar.prototype.getTotalHeight = function() {
    297   return this.scrollHeight_ - this.bottomMarginForPanel_;
    298 };
    299 
    300 /**
    301  * Sets the bottom margin height of the view for the transparent preview panel.
    302  * @param {number} margin Margin to be set in px.
    303  */
    304 MainPanelScrollBar.prototype.setBottomMarginForPanel = function(margin) {
    305   this.bottomMarginForPanel_ = margin;
    306 };
    307