Home | History | Annotate | Download | only in ui
      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