/*global require, exports, window*/
/**
* @module montage/ui/base/abstract-slider.reel
* @requires montage/ui/component
* @requires montage/ui/native-control
* @requires montage/composer/press-composer
*/
var AbstractControl = require("./abstract-control").AbstractControl,
TranslateComposer = require("../../composer/translate-composer").TranslateComposer,
KeyComposer = require("../../composer/key-composer").KeyComposer;
/**
* @class AbstractSlider
* @extends AbstractControl
*/
var AbstractSlider = exports.AbstractSlider = AbstractControl.specialize( /** @lends AbstractSlider# */ {
// Life Cycle
/**
* @private
*/
constructor: {
value: function AbstractSlider() {
if (this.constructor === AbstractSlider) {
throw new Error("AbstractSlider cannot be instantiated.");
}
//this is so that when we read properties from the dom they are not overwritten
this._propertyNamesUsed = {};
this.addOwnPropertyChangeListener("_sliderMagnitude", this);
this.addOwnPropertyChangeListener("_min", this);
this.addOwnPropertyChangeListener("_max", this);
this.addOwnPropertyChangeListener("_value", this);
this.addOwnPropertyChangeListener("_step", this);
this.addOwnPropertyChangeListener("axis", this);
this.axis = "horizontal";
this.defineBinding( "classList.has('montage--disabled')", { "<-": "!enabled" });
this.defineBinding( "classList.has('montage-Slider--active')", { "<-": "active" });
}
},
enterDocument: {
value: function (firstTime) {
if (firstTime) {
this._translateComposer = new TranslateComposer();
this._translateComposer.identifier = "thumb";
this._translateComposer.axis = this.axis;
this._translateComposer.hasMomentum = false;
this.addComposerForElement(this._translateComposer, this._sliderThumbElement);
// check for transform support
if("webkitTransform" in this.element.style) {
this._transform = "webkitTransform";
} else if("MozTransform" in this.element.style) {
this._transform = "MozTransform";
} else if("oTransform" in this.element.style) {
this._transform = "oTransform";
} else {
this._transform = "transform";
}
// read initial values from the input type=range
var used = this._propertyNamesUsed;
if (!used._min) {
this.min = this.element.getAttribute('min') || this._min;
}
if (!used._max) {
this.max = this.element.getAttribute('max') || this._max;
}
if (!used._step) {
this.step = this.element.getAttribute('step') || this._step;
}
if (!used._value) {
this.value = this.element.getAttribute('value') || this._value;
}
delete this._propertyNamesUsed;
this.element.setAttribute("role", "slider");
this.element.tabIndex = "-1";
this._upKeyComposer = KeyComposer.createKey(this, "up", "increase");
this._downKeyComposer = KeyComposer.createKey(this, "down", "decrease");
this._rightKeyComposer = KeyComposer.createKey(this, "right", "increase");
this._leftKeyComposer = KeyComposer.createKey(this, "left", "decrease");
}
}
},
// @todo: Without prepareForActivationEvents, the _translateComposer does not work
prepareForActivationEvents: {
value: function () {
this._translateComposer.addEventListener('translateStart', this, false);
this._translateComposer.addEventListener('translate', this, false);
this._translateComposer.addEventListener('translateEnd', this, false);
// needs to be fixed for pointer handling
this._sliderThumbElement.addEventListener("touchstart", this, false);
document.addEventListener("touchend", this, false);
this._sliderThumbElement.addEventListener("mousedown", this, false);
document.addEventListener("mouseup", this, false);
this._upKeyComposer.addEventListener("keyPress", this, false);
this._downKeyComposer.addEventListener("keyPress", this, false);
this._leftKeyComposer.addEventListener("keyPress", this, false);
this._rightKeyComposer.addEventListener("keyPress", this, false);
}
},
willDraw: {
value: function () {
this._sliderMagnitude = this._calculateSliderMagnitude();
}
},
_previousPercentage: {
value: null
},
draw: {
value: function () {
if (this.axis === "vertical") {
if (this._isUpdatingTranslate) {
this._sliderThumbElement.style[this._transform] =
"translate3d(0," +
(this._previousPercentage - this._valuePercentage) * this._sliderMagnitude * 0.01 +
"px,0)";
this._isUpdatingTranslate = false;
} else {
this._sliderThumbElement.style.top = (100 - this._valuePercentage) + "%";
this._sliderThumbElement.style.left = 0;
this._sliderThumbElement.style[this._transform] = "translate3d(0,0,0)";
this._previousPercentage = this._valuePercentage;
}
} else {
if (this._isUpdatingTranslate) {
this._sliderThumbElement.style[this._transform] =
"translate3d(" +
(this._valuePercentage - this._previousPercentage) * this._sliderMagnitude * 0.01 +
"px,0,0)";
this._isUpdatingTranslate = false;
} else {
this._sliderThumbElement.style.left = this._valuePercentage + "%";
this._sliderThumbElement.style.top = 0;
this._sliderThumbElement.style[this._transform] = "translate3d(0,0,0)";
this._previousPercentage = this._valuePercentage;
}
}
this.element.setAttribute("aria-valuemax", this.max);
this.element.setAttribute("aria-valuemin", this.min);
this.element.setAttribute("aria-valuenow", this.value);
}
},
// Event Handling
acceptsActiveTarget: {
value: true
},
handleTouchstart: {
value: function (e) {
this.active = true;
this.element.focus();
this._isUpdatingTranslate = true;
}
},
handleTouchend: {
value: function (e) {
this.active = false;
}
},
handleMousedown: {
value: function (e) {
this.active = true;
this.element.focus();
// gh-1304
// I did some experimentation based on using -webkit-user-select on the body element. Apart form the obvious
// browser compatibility problems, it made existing text selection pop in and out as the slider is
// interacted with. I'm worried about the possible side effects, but this might be the only solution.
// The problem it solves is more pressing than the potential downside at this point.
e.preventDefault();
this._isUpdatingTranslate = true;
}
},
handleMouseup: {
value: function (e) {
this.active = false;
}
},
_isUpdatingTranslate: {
value: false
},
handleThumbTranslateStart: {
value: function (e) {
if(this.axis === "vertical") {
this._startTranslate = e.translateY;
} else {
this._startTranslate = e.translateX;
}
this._startValue = this.value;
}
},
handleThumbTranslate: {
value: function (event) {
if(this.axis === "vertical") {
this.value = this._startValue + ((this._startTranslate - event.translateY) / this._sliderMagnitude) * (this._max - this._min);
} else {
this.value = this._startValue + ((event.translateX - this._startTranslate) / this._sliderMagnitude) * (this._max - this._min);
}
this._isUpdatingTranslate = true;
}
},
handleThumbTranslateEnd: {
value: function (e) {
this.active = false;
this._isUpdatingTranslate = false;
}
},
_increase: {
value: function () {
var stepBase = (typeof this.min === "number") ? this.min : 0;
var value = this.value - stepBase;
var step = this.step | (this.max-this.min)/100;
if (value % step) {
if (value < 0) {
value -= value % step;
} else {
value += step - (value % step);
}
} else {
value += step;
}
this.value = value + stepBase;
}
},
_decrease: {
value: function () {
var stepBase = (typeof this.min === "number") ? this.min : 0;
var value = this.value - stepBase;
var step = this.step | (this.max-this.min)/100;
if (value % step) {
if (value > 0) {
value -= value % step;
} else {
value -= step + (value % step);
}
} else {
value -= step;
}
this.value = value + stepBase;
}
},
handleKeyPress: {
value: function (event) {
if (!this.enabled) {
return;
}
if(event.identifier === "increase") {
this._increase();
} else if (event.identifier === "decrease") {
this._decrease();
}
}
},
surrenderPointer: {
value: function (pointer, composer) {
// If the user is sliding us then we do not want anyone using
// the pointer
return false;
}
},
// Properties
/**
* This property is true when the slider is being interacted with, either through mouse click or touch event, otherwise false.
* @type {boolean}
* @default false
*/
active: {
value: false
},
_min: {
value: 0
},
_max: {
value: 100
},
_step: {
value: "any"
},
min: {
get: function () {
return this._min;
},
set: function (value) {
if (! isNaN(value = parseFloat(value))) {
if (this._min !== value) {
this._min = value;
}
}
}
},
max: {
get: function () {
return this._max;
},
set: function (value) {
if (! isNaN(value = parseFloat(value))) {
if (this._max !== value) {
this._max = value;
}
}
}
},
step: {
get: function () {
return this._step;
},
set: function (value) {
if (! isNaN(value = parseFloat(value)) && value >= 0) {
if (this._step !== value) {
this._step = value;
}
}
}
},
_value: {
value: 50
},
value: {
get: function () {
return this._value;
},
set: function (value) {
if (! isNaN(value = parseFloat(value))) {
if (value > this._max) {
value = this._max;
} else if (value < this._min) {
value = this._min;
}
if (this._value !== value) {
this._value = value;
}
}
}
},
/**
* Enables or disables the Button from user input. When this property is
* set to `false`, the "disabled" CSS style is applied to the button's DOM
* element during the next draw cycle. When set to `true` the "disabled"
* CSS class is removed from the element's class list.
* @type {boolean}
*/
enabled: {
value: true
},
axis: {
value: null
},
// Machinery
_sliderThumbElement: {
value: null
},
_translateComposer: {
value: null
},
_transform: {
value: null
},
_transition: {
value: null
},
_sliderMagnitude: {
value: null
},
_startTranslate: {
value: null
},
_startValue: {
value: null
},
_valuePercentage: {
value: null
},
_calculateSliderMagnitude: {
value: function () {
var computedStyle = window.getComputedStyle(this._element);
if(this.axis === "vertical") {
return (
this._element.clientHeight -
parseFloat(computedStyle.getPropertyValue("padding-top")) -
parseFloat(computedStyle.getPropertyValue("padding-bottom"))
);
} else {
return (
this._element.clientWidth -
parseFloat(computedStyle.getPropertyValue("padding-left")) -
parseFloat(computedStyle.getPropertyValue("padding-right"))
);
}
}
},
handleAxisChange: {
value: function () {
if (this._translateComposer) {
this._translateComposer.axis = this.axis;
}
if(this.axis === "vertical") {
this.classList.add("montage-Slider--vertical");
this.classList.remove("montage-Slider--horizontal");
} else {
this.classList.remove("montage-Slider--vertical");
this.classList.add("montage-Slider--horizontal");
}
}
},
_propertyRegex: {
value: /_sliderMagnitude|_min|_max|_value|_step/
},
handlePropertyChange: {
value: function (changeValue, key, object) {
if(key.match(this._propertyRegex) !== null) {
if(this._propertyNamesUsed) {
this._propertyNamesUsed[key] = true;
}
//adjust the value
if (this._value <= this._min) {
//first the simple case
this._value = this._min;
} else {
var magnitude = this._value - this._min;
var remainder = magnitude % this._step;
if (remainder) {
//if we have a remainder then we need to adjust the value
// Inspired by http://www.w3.org/html/wg/drafts/html/master/forms.html#range-state-(type=range)
// if we are in the middle of two stepped value then go for the larger one.
var roundup = (remainder >= this._step * 0.5) && ((this._value - remainder) + this._step <= this._max);
if (roundup) {
this._value = (this._value - remainder) + this._step;
} else {
this._value = this._value - remainder;
}
}
}
//otherwise don't adjust the value just check it's within min and max
if (this._value > this._max) {
this._value = this._max;
}
this._valuePercentage = ((this._value - this._min) * 100) / (this._max - this._min);
this.needsDraw = true;
}
}
}
});