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 */
12var ScrollBar = cr.ui.define('div');
13
14/**
15 * Mode of the scrollbar. As for now, only vertical scrollbars are supported.
16 * @type {number}
17 */
18ScrollBar.Mode = {
19  VERTICAL: 0,
20  HORIZONTAL: 1
21};
22
23ScrollBar.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 */
43ScrollBar.prototype.__proto__ = HTMLDivElement.prototype;
44
45/**
46 * Initializes the DOM structure of the scrollbar.
47 */
48ScrollBar.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 */
69ScrollBar.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 */
79ScrollBar.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 */
92ScrollBar.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 */
112ScrollBar.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 */
126ScrollBar.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 */
142ScrollBar.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 */
153ScrollBar.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 */
186ScrollBar.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 */
201ScrollBar.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 */
241ScrollBar.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 */
250ScrollBar.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 */
259var MainPanelScrollBar = cr.ui.define('div');
260
261/**
262 * Inherits after ScrollBar.
263 */
264MainPanelScrollBar.prototype.__proto__ = ScrollBar.prototype;
265
266/** @override */
267MainPanelScrollBar.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 */
285MainPanelScrollBar.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 */
296MainPanelScrollBar.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 */
304MainPanelScrollBar.prototype.setBottomMarginForPanel = function(margin) {
305  this.bottomMarginForPanel_ = margin;
306};
307