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