// 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 }; });