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 * @fileoverview A base class for scrollbar-like controls. 9 */ 10 base.require('ui'); 11 base.require('base.properties'); 12 base.require('ui.mouse_tracker'); 13 14 base.requireStylesheet('ui.value_bar'); 15 16 base.exportTo('ui', function() { 17 18 /** 19 * @constructor 20 */ 21 var ValueBar = ui.define('value-bar'); 22 23 ValueBar.prototype = { 24 __proto__: HTMLDivElement.prototype, 25 26 decorate: function() { 27 this.className = 'value-bar'; 28 this.lowestValueControl_ = this.createLowestValueControl_(); 29 this.valueRangeControl_ = this.createValueRangeControl_(); 30 this.highestValueControl_ = this.createHighestValueControl_(); 31 this.valueSliderControl_ = 32 this.createValueSlider_(this.valueRangeControl_); 33 34 this.vertical = true; 35 this.exponentBase_ = 1.0; 36 this.lowestValue = 0.1; 37 this.highestValue = 2.0; 38 this.value = 0.5; 39 }, 40 41 get lowestValue() { 42 return this.lowestValue_; 43 }, 44 45 set lowestValue(newValue) { 46 base.setPropertyAndDispatchChange(this, 'lowestValue', newValue); 47 }, 48 49 get value() { 50 return this.value_; 51 }, 52 53 set value(newValue) { 54 if (newValue === this.value) 55 return; 56 newValue = this.limitValue_(newValue); 57 base.setPropertyAndDispatchChange(this, 'value', newValue); 58 }, 59 60 // A value that changes when you mouseover slider. 61 get previewValue() { 62 return this.previewValue_; 63 }, 64 65 set previewValue(newValue) { 66 if (newValue === this.previewValue_) 67 return; 68 newValue = this.limitValue_(newValue); 69 base.setPropertyAndDispatchChange(this, 'previewValue', newValue); 70 }, 71 72 get highestValue() { 73 return this.highestValue_; 74 }, 75 76 set highestValue(newValue) { 77 base.setPropertyAndDispatchChange(this, 'highestValue', newValue); 78 }, 79 80 get vertical() { 81 return this.vertical_; 82 }, 83 84 set vertical(newValue) { 85 this.vertical_ = !!newValue; 86 delete this.rangeControlOffset_; 87 delete this.rangeControlPixelRange_; 88 delete this.valueSliderCenterOffset_; 89 this.setAttribute('orient', this.vertical_ ? 'vertical' : 'horizontal'); 90 base.setPropertyAndDispatchChange(this, 'value', this.value); 91 }, 92 93 get exponentBase() { 94 return this.exponentBase_; 95 }, 96 97 // Controls the amount of non-linearity in the value bar. 98 // Higher bases make changes at low value value slower 99 // and changes at high values faster. 100 set exponentBase(newValue) { 101 this.exponentBase_ = newValue; 102 }, 103 104 // Override to change content. 105 updateLowestValueElement: function(element) { 106 element.removeAttribute('style'); 107 var str = event.newValue.toFixed(1) + ''; 108 element.textContent = str.substr(0, 3); 109 }, 110 111 updateHighestValueElement: function(element) { 112 element.removeAttribute('style'); 113 var str = event.newValue.toFixed(1) + ''; 114 element.textContent = str.substr(0, 3); 115 }, 116 117 get rangeControlOffset() { 118 if (!this.rangeControlOffset_) { 119 var rect = this.valueRangeControl_.getBoundingClientRect(); 120 this.rangeControlOffset_ = this.vertical_ ? rect.top : rect.left; 121 } 122 return this.rangeControlOffset_; 123 }, 124 125 get valueSlideCenterOffset() { 126 var offsetDirection = this.vertical_ ? 'offsetTop' : 'offsetLeft'; 127 return this.valueSliderCenter_[offsetDirection] + 1; 128 }, 129 130 get rangeControlPixelRange() { 131 if (!this.rangeControlPixelRange_ || this.rangeControlPixelRange_ < 1) { 132 var rangeRect = this.valueRangeControl_.getBoundingClientRect(); 133 this.rangeControlPixelRange_ = 134 this.vertical_ ? rangeRect.height - 1 : rangeRect.width - 1; 135 } 136 return this.rangeControlPixelRange_; 137 }, 138 139 // The value <--> pixel conversion formulas are all normalized to the 140 // range 0-1 to avoid overflow surprises. Three layers of normalization 141 // include: 142 // 1. pixel range of the valuebar 143 // 2. exponent/log of the normalized ranges 144 // 3. value range 145 146 // offset zero gives 0, offset rangeControlPixelRange_ gives 1, 147 // exponential in between. 148 fractionalValue_: function(offset) { 149 if (!this.rangeControlPixelRange) 150 return 0; 151 console.assert(offset >= 0); 152 // min offset is zero, so this ratio is (offset - min) / (max - min) 153 var fractionOfRange = offset / this.rangeControlPixelRange_; 154 if (fractionOfRange > 1) 155 fractionOfRange = 1.0; 156 if (this.exponentBase === 1) 157 return fractionOfRange; 158 // The - 1 terms are Math.pow(this.exponentBase_, 0) for the minimum 159 // pixel range of zero. 160 var numerator = Math.pow(this.exponentBase_, fractionOfRange) - 1; 161 return numerator / (this.exponentBase_ - 1); 162 }, 163 164 // fractionalValue zero gives zero, 1.0 gives rangeControlPixelRange_ 165 pixelByValue_: function(fractionalValue) { 166 console.assert(fractionalValue >= 0 && fractionalValue <= 1); 167 if (this.exponentBase_ === 1) 168 return this.rangeControlPixelRange * fractionalValue; 169 170 // fractionalValue *(this.exponentBase_^1 - this.exponentBase_^0) + 171 // this.exponentBase_^0 172 var expPixel = fractionalValue * (this.exponentBase_ - 1) + 1; 173 var fractionalPixel = Math.log(expPixel) / Math.log(this.exponentBase_); 174 // (max - min) * fractionalPixel + min for min == 0 175 return this.rangeControlPixelRange * fractionalPixel; 176 }, 177 178 limitValue_: function(newValue) { 179 var limitedValue = newValue; 180 if (newValue < this.lowestValue) 181 limitedValue = this.lowestValue; 182 if (newValue > this.highestValue) 183 limitedValue = this.highestValue; 184 return limitedValue; 185 }, 186 187 eventToPixelOffset_: function(event) { 188 var coord = this.vertical_ ? 'y' : 'x'; 189 var pixelOffset = event[coord] - this.rangeControlOffset; 190 return Math.max(pixelOffset, 1); 191 }, 192 193 convertPixelOffsetToValue_: function(offset) { 194 var rangeInValue = this.highestValue - this.lowestValue; 195 return this.fractionalValue_(offset) * (rangeInValue) + this.lowestValue; 196 }, 197 198 convertValueToPixelOffset: function(value) { 199 if (!this.highestValue) 200 return 0; 201 var rangeInValue = this.highestValue - this.lowestValue; 202 var valueInPx = 203 this.pixelByValue_((value - this.lowestValue) / rangeInValue); 204 return valueInPx; 205 }, 206 207 setValueOnRangeClick_: function(event) { 208 var pixelOffset = this.eventToPixelOffset_(event); 209 this.value = this.convertPixelOffsetToValue_(pixelOffset); 210 }, 211 212 setPreviewValueByEvent_: function(event) { 213 var pixelOffset = this.eventToPixelOffset_(event); 214 if (event.currentTarget.classList.contains('lowest-value-control')) 215 pixelOffset = 0; // There is a 4 pixel error on the bottom of the range. 216 this.previewValue = this.convertPixelOffsetToValue_(pixelOffset); 217 }, 218 /** 219 @param {Event} event: mouse event relative to slider control 220 */ 221 slideStart_: function(event) { 222 this.slideStart_ = event; 223 }, 224 /** 225 @param {Event} event: mouse event relative to slider control 226 */ 227 slideValue_: function(event) { 228 var pixelOffset = this.eventToPixelOffset_(event); 229 this.value = 230 this.convertPixelOffsetToValue_(pixelOffset); 231 }, 232 233 slideEnd_: function(event) { 234 this.preview = this.value; 235 }, 236 237 onValueChange_: function(valueKey) { 238 var pixelOffset = this.convertValueToPixelOffset(this[valueKey]); 239 pixelOffset = pixelOffset - this.valueSlideCenterOffset; 240 if (this.vertical_) { 241 this.valueSliderControl_.style.left = 0; 242 this.valueSliderControl_.style.top = pixelOffset + 'px'; 243 } else { 244 this.valueSliderControl_.style.left = pixelOffset + 'px'; 245 this.valueSliderControl_.style.top = 0; 246 } 247 }, 248 249 createValueControl_: function(className) { 250 return ui.createDiv({ 251 className: className + ' value-control', 252 parent: this 253 }); 254 }, 255 256 createLowestValueControl_: function() { 257 var lowestValueControl = this.createValueControl_('lowest-value-control'); 258 259 lowestValueControl.addEventListener('click', function() { 260 this.value = this.lowestValue; 261 base.dispatchSimpleEvent(this, 'lowestValueClick'); 262 }.bind(this)); 263 lowestValueControl.addEventListener('mouseover', 264 this.setPreviewValueByEvent_.bind(this)); 265 266 // Interior element to control the whitespace around the button text 267 var lowestValueControlContent = 268 ui.createSpan({className: 'lowest-value-control-content'}); 269 lowestValueControl.appendChild(lowestValueControlContent); 270 271 this.addEventListener('lowestValueChange', function(event) { 272 this.updateLowestValueElement(lowestValueControlContent); 273 }.bind(this)); 274 275 return lowestValueControl; 276 }, 277 278 createValueRangeControl_: function() { 279 var valueRangeControl = this.createValueControl_('value-range-control'); 280 // As the user moves over our range control, preview the result. 281 valueRangeControl.addEventListener('mousemove', 282 this.setPreviewValueByEvent_.bind(this)); 283 // Accept the current value. 284 valueRangeControl.addEventListener('click', 285 this.setValueOnRangeClick_.bind(this)); 286 this.addEventListener('valueChange', 287 this.onValueChange_.bind(this, 'value'), true); 288 return valueRangeControl; 289 }, 290 291 createHighestValueControl_: function() { 292 var highestValueControl = 293 this.createValueControl_('highest-value-control'); 294 highestValueControl.addEventListener('click', function() { 295 this.value = this.highestValue; 296 base.dispatchSimpleEvent(this, 'highestValueClick'); 297 }.bind(this)); 298 var highestValueControlContent = 299 ui.createSpan({className: 'highest-value-control-content'}); 300 highestValueControl.appendChild(highestValueControlContent); 301 this.addEventListener('highestValueChange', function(event) { 302 this.updateHighestValueElement(highestValueControlContent); 303 }.bind(this)); 304 return highestValueControl; 305 }, 306 307 createValueSlider_: function(rangeControl) { 308 var valueSlider = ui.createDiv({ 309 className: 'value-slider', 310 parent: rangeControl 311 }); 312 ui.createDiv({ 313 className: 'value-slider-top', 314 parent: valueSlider 315 }); 316 this.valueSliderCenter_ = ui.createDiv({ 317 className: 'value-slider-bottom', 318 parent: valueSlider 319 }); 320 321 this.mouseTracker = new ui.MouseTracker(valueSlider); 322 valueSlider.addEventListener('mouse-tracker-start', 323 this.slideStart_.bind(this)); 324 valueSlider.addEventListener('mouse-tracker-move', 325 this.slideValue_.bind(this)); 326 valueSlider.addEventListener('mouse-tracker-end', 327 this.slideEnd_.bind(this)); 328 return valueSlider; 329 }, 330 331 }; 332 333 return { 334 ValueBar: ValueBar 335 }; 336 }); 337