Javascript  |  337行  |  10.9 KB

// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

'use strict';

/**
 * @fileoverview A base class for scrollbar-like controls.
 */
base.require('ui');
base.require('base.properties');
base.require('ui.mouse_tracker');

base.requireStylesheet('ui.value_bar');

base.exportTo('ui', function() {

  /**
   * @constructor
   */
  var ValueBar = ui.define('value-bar');

  ValueBar.prototype = {
    __proto__: HTMLDivElement.prototype,

    decorate: function() {
      this.className = 'value-bar';
      this.lowestValueControl_ = this.createLowestValueControl_();
      this.valueRangeControl_ = this.createValueRangeControl_();
      this.highestValueControl_ = this.createHighestValueControl_();
      this.valueSliderControl_ =
          this.createValueSlider_(this.valueRangeControl_);

      this.vertical = true;
      this.exponentBase_ = 1.0;
      this.lowestValue = 0.1;
      this.highestValue = 2.0;
      this.value = 0.5;
    },

    get lowestValue() {
      return this.lowestValue_;
    },

    set lowestValue(newValue) {
      base.setPropertyAndDispatchChange(this, 'lowestValue', newValue);
    },

    get value() {
      return this.value_;
    },

    set value(newValue) {
      if (newValue === this.value)
        return;
      newValue = this.limitValue_(newValue);
      base.setPropertyAndDispatchChange(this, 'value', newValue);
    },

    // A value that changes when you mouseover slider.
    get previewValue() {
      return this.previewValue_;
    },

    set previewValue(newValue) {
      if (newValue === this.previewValue_)
        return;
      newValue = this.limitValue_(newValue);
      base.setPropertyAndDispatchChange(this, 'previewValue', newValue);
    },

    get highestValue() {
      return this.highestValue_;
    },

    set highestValue(newValue) {
      base.setPropertyAndDispatchChange(this, 'highestValue', newValue);
    },

    get vertical() {
      return this.vertical_;
    },

    set vertical(newValue) {
      this.vertical_ = !!newValue;
      delete this.rangeControlOffset_;
      delete this.rangeControlPixelRange_;
      delete this.valueSliderCenterOffset_;
      this.setAttribute('orient', this.vertical_ ? 'vertical' : 'horizontal');
      base.setPropertyAndDispatchChange(this, 'value', this.value);
    },

    get exponentBase() {
      return this.exponentBase_;
    },

    // Controls the amount of non-linearity in the value bar.
    // Higher bases make changes at low value value slower
    // and changes at high values faster.
    set exponentBase(newValue) {
      this.exponentBase_ = newValue;
    },

    // Override to change content.
    updateLowestValueElement: function(element) {
      element.removeAttribute('style');
      var str = event.newValue.toFixed(1) + '';
      element.textContent = str.substr(0, 3);
    },

    updateHighestValueElement: function(element) {
      element.removeAttribute('style');
      var str = event.newValue.toFixed(1) + '';
      element.textContent = str.substr(0, 3);
    },

    get rangeControlOffset() {
      if (!this.rangeControlOffset_) {
        var rect = this.valueRangeControl_.getBoundingClientRect();
        this.rangeControlOffset_ = this.vertical_ ? rect.top : rect.left;
      }
      return this.rangeControlOffset_;
    },

    get valueSlideCenterOffset() {
      var offsetDirection = this.vertical_ ? 'offsetTop' : 'offsetLeft';
      return this.valueSliderCenter_[offsetDirection] + 1;
    },

    get rangeControlPixelRange() {
      if (!this.rangeControlPixelRange_ || this.rangeControlPixelRange_ < 1) {
        var rangeRect = this.valueRangeControl_.getBoundingClientRect();
        this.rangeControlPixelRange_ =
            this.vertical_ ? rangeRect.height - 1 : rangeRect.width - 1;
      }
      return this.rangeControlPixelRange_;
    },

    // The value <--> pixel conversion formulas are all normalized to the
    // range 0-1 to avoid overflow surprises. Three layers of normalization
    // include:
    // 1. pixel range of the valuebar
    // 2. exponent/log of the normalized ranges
    // 3. value range

    // offset zero gives 0, offset rangeControlPixelRange_ gives 1,
    // exponential in between.
    fractionalValue_: function(offset) {
      if (!this.rangeControlPixelRange)
        return 0;
      console.assert(offset >= 0);
      // min offset is zero, so this ratio is (offset - min) / (max - min)
      var fractionOfRange = offset / this.rangeControlPixelRange_;
      if (fractionOfRange > 1)
        fractionOfRange = 1.0;
      if (this.exponentBase === 1)
        return fractionOfRange;
      // The - 1 terms are Math.pow(this.exponentBase_, 0) for the minimum
      // pixel range of zero.
      var numerator = Math.pow(this.exponentBase_, fractionOfRange) - 1;
      return numerator / (this.exponentBase_ - 1);
    },

    // fractionalValue zero gives zero, 1.0 gives rangeControlPixelRange_
    pixelByValue_: function(fractionalValue) {
      console.assert(fractionalValue >= 0 && fractionalValue <= 1);
      if (this.exponentBase_ === 1)
        return this.rangeControlPixelRange * fractionalValue;

      // fractionalValue *(this.exponentBase_^1 - this.exponentBase_^0) +
      //   this.exponentBase_^0
      var expPixel = fractionalValue * (this.exponentBase_ - 1) + 1;
      var fractionalPixel = Math.log(expPixel) / Math.log(this.exponentBase_);
      // (max - min) * fractionalPixel + min for min == 0
      return this.rangeControlPixelRange * fractionalPixel;
    },

    limitValue_: function(newValue) {
      var limitedValue = newValue;
      if (newValue < this.lowestValue)
        limitedValue = this.lowestValue;
      if (newValue > this.highestValue)
        limitedValue = this.highestValue;
      return limitedValue;
    },

    eventToPixelOffset_: function(event) {
      var coord = this.vertical_ ? 'y' : 'x';
      var pixelOffset = event[coord] - this.rangeControlOffset;
      return Math.max(pixelOffset, 1);
    },

    convertPixelOffsetToValue_: function(offset) {
      var rangeInValue = this.highestValue - this.lowestValue;
      return this.fractionalValue_(offset) * (rangeInValue) + this.lowestValue;
    },

    convertValueToPixelOffset: function(value) {
      if (!this.highestValue)
        return 0;
      var rangeInValue = this.highestValue - this.lowestValue;
      var valueInPx =
          this.pixelByValue_((value - this.lowestValue) / rangeInValue);
      return valueInPx;
    },

    setValueOnRangeClick_: function(event) {
      var pixelOffset = this.eventToPixelOffset_(event);
      this.value = this.convertPixelOffsetToValue_(pixelOffset);
    },

    setPreviewValueByEvent_: function(event) {
      var pixelOffset = this.eventToPixelOffset_(event);
      if (event.currentTarget.classList.contains('lowest-value-control'))
        pixelOffset = 0; // There is a 4 pixel error on the bottom of the range.
      this.previewValue = this.convertPixelOffsetToValue_(pixelOffset);
    },
    /**
      @param {Event} event: mouse event relative to slider control
    */
    slideStart_: function(event) {
      this.slideStart_ = event;
    },
    /**
      @param {Event} event: mouse event relative to slider control
    */
    slideValue_: function(event) {
      var pixelOffset = this.eventToPixelOffset_(event);
      this.value =
          this.convertPixelOffsetToValue_(pixelOffset);
    },

    slideEnd_: function(event) {
      this.preview = this.value;
    },

    onValueChange_: function(valueKey) {
      var pixelOffset = this.convertValueToPixelOffset(this[valueKey]);
      pixelOffset = pixelOffset - this.valueSlideCenterOffset;
      if (this.vertical_) {
        this.valueSliderControl_.style.left = 0;
        this.valueSliderControl_.style.top = pixelOffset + 'px';
      } else {
        this.valueSliderControl_.style.left = pixelOffset + 'px';
        this.valueSliderControl_.style.top = 0;
      }
    },

    createValueControl_: function(className) {
      return ui.createDiv({
        className: className + ' value-control',
        parent: this
      });
    },

    createLowestValueControl_: function() {
      var lowestValueControl = this.createValueControl_('lowest-value-control');

      lowestValueControl.addEventListener('click', function() {
        this.value = this.lowestValue;
        base.dispatchSimpleEvent(this, 'lowestValueClick');
      }.bind(this));
      lowestValueControl.addEventListener('mouseover',
          this.setPreviewValueByEvent_.bind(this));

      // Interior element to control the whitespace around the button text
      var lowestValueControlContent =
          ui.createSpan({className: 'lowest-value-control-content'});
      lowestValueControl.appendChild(lowestValueControlContent);

      this.addEventListener('lowestValueChange', function(event) {
        this.updateLowestValueElement(lowestValueControlContent);
      }.bind(this));

      return lowestValueControl;
    },

    createValueRangeControl_: function() {
      var valueRangeControl = this.createValueControl_('value-range-control');
      // As the user moves over our range control, preview the result.
      valueRangeControl.addEventListener('mousemove',
          this.setPreviewValueByEvent_.bind(this));
      // Accept the current value.
      valueRangeControl.addEventListener('click',
          this.setValueOnRangeClick_.bind(this));
      this.addEventListener('valueChange',
          this.onValueChange_.bind(this, 'value'), true);
      return valueRangeControl;
    },

    createHighestValueControl_: function() {
      var highestValueControl =
          this.createValueControl_('highest-value-control');
      highestValueControl.addEventListener('click', function() {
        this.value = this.highestValue;
        base.dispatchSimpleEvent(this, 'highestValueClick');
      }.bind(this));
      var highestValueControlContent =
          ui.createSpan({className: 'highest-value-control-content'});
      highestValueControl.appendChild(highestValueControlContent);
      this.addEventListener('highestValueChange', function(event) {
        this.updateHighestValueElement(highestValueControlContent);
      }.bind(this));
      return highestValueControl;
    },

    createValueSlider_: function(rangeControl) {
      var valueSlider = ui.createDiv({
        className: 'value-slider',
        parent: rangeControl
      });
      ui.createDiv({
        className: 'value-slider-top',
        parent: valueSlider
      });
      this.valueSliderCenter_ = ui.createDiv({
        className: 'value-slider-bottom',
        parent: valueSlider
      });

      this.mouseTracker = new ui.MouseTracker(valueSlider);
      valueSlider.addEventListener('mouse-tracker-start',
          this.slideStart_.bind(this));
      valueSlider.addEventListener('mouse-tracker-move',
          this.slideValue_.bind(this));
      valueSlider.addEventListener('mouse-tracker-end',
          this.slideEnd_.bind(this));
      return valueSlider;
    },

  };

  return {
    ValueBar: ValueBar
  };
});