/*global require, exports, console, MontageElement */
/**
@module "montage/ui/native/input-range.reel"
*/
var Control = require("ui/control").Control,
TranslateComposer = require("../../composer/translate-composer").TranslateComposer,
KeyComposer = require("../../composer/key-composer").KeyComposer,
Map = require("collections/map"),
WeakMap = require("collections/weak-map"),
MONTAGE_SLIDER_THUMB_CLASS = "montage-Slider--thumb";
/**
* Wraps the a <input type="range"> element with binding support for the element's standard attributes.
@class module:"montage/ui/native/input-range.reel".InputRange
@extends module:montage/ui/text-input.TextInput
*/
var Slider = exports.Slider = Control.specialize({
/**
* @private
*/
constructor: {
value: function Slider() {
Control.constructor.call(this); // super
//this is so that when we read properties from the dom they are not overwritten
this._propertyNamesUsed = {};
this._values = [50];
this._isThumbElementTranslating = new WeakMap();
this._percentageValues = [];
this._previousPercentageValues = [];
this.addOwnPropertyChangeListener("_sliderMagnitude", this);
this.addOwnPropertyChangeListener("_min", this);
this.addOwnPropertyChangeListener("_max", this);
this.addOwnPropertyChangeListener("_value", this);
this.addOwnPropertyChangeListener("values", this);
this.addOwnPropertyChangeListener("_step", this);
this.addOwnPropertyChangeListener("axis", this);
//this._values.addRangeChangeListener(this, "values");
// this._values.addRangeChangeListener(this);
this.addRangeAtPathChangeListener("_values",this);
}
},
handleValuesRangeChange: {
value: function (plus, minus, index) {
this.needsDraw = true;
}
},
handleRangeChange: {
value: function (plus, minus, index) {
this.handlePropertyChange(plus[0], "values", this);
//this.needsDraw = true;
}
},
/**
* @private
*/
_orientation: {value: "horizontal"},
/**
* Slider's orientation is horizontal by default,
* can be overridden by setting this property,
* isn't auto-responsive due to inability to react to parent container dimension changes.
*
* @property {String} orientation
*/
orientation: {
get: function () {
return this._orientation;
},
set: function (orientation) {
if (this._orientation !== orientation) {
this._orientation = orientation;
this.needsDraw = true;
}
}
},
thumbElement: {
get: function() {
return this.thumbElements[0];
}
},
_spacer: {
value: void 0,
},
thumbWrappers: {
value: void 0,
},
thumbElements: {
value: void 0,
},
trackElements: {
value: void 0,
},
_values: {
value: void 0,
},
values: {
get: function() {
return this._values || (this._values = [50]);
},
set: function (values) {
if (this._values !== values) {
this._values = values;
this.needsDraw = true;
}
}
},
_percentageValues: {
value: void 0,
},
_previousPercentageValues: {
value: void 0,
},
_translateComposers: {
value: void 0,
},
_spacerMarginEnd: {
value: void 0,
},
enterDocument: {
value: function (firstTime) {
this.super(firstTime);
if (firstTime) {
if(this.hasStandardElement) {
this.element.addEventListener('input', this);
this.element.addEventListener('change', this);
// 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;
}
else {
var isHorizontal = (this.orientation === "horizontal");
this.thumbWrappers= [];
this.thumbElements = [];
this.trackElements = [];
var ownerDocument = this._element.ownerDocument,
fragment = ownerDocument.createDocumentFragment(),
spacer = this._spacer = this._element.firstElementChild,
dimensionLength = this._dimensionLength,
i=0, iThumbElement, offset = 0, iDimension = 0, iThumbWrapper, iTrackElement, iThumbElementWithClass;
while((iThumbElement = spacer.firstElementChild)) {
//If iThumbElement has no childElement, it got to be the thumb
if(!iThumbElement.firstElementChild) {
//If it doesn't have the required flag MONTAGE-SLIDER-THUMB-CLASS
//We'll gladly fix it, but that's as far as we can go
if(!iThumbElement.classList.contains(MONTAGE_SLIDER_THUMB_CLASS)) {
iThumbElement.classList.add(MONTAGE_SLIDER_THUMB_CLASS);
}
iThumbElementWithClass = iThumbElement;
}
else {
iThumbElementWithClass = iThumbElement.getElementsByClassName(MONTAGE_SLIDER_THUMB_CLASS)[0];
if(!iThumbElementWithClass) {
throw new Error("Slider couldn't identify a thumb element with "+MONTAGE_SLIDER_THUMB_CLASS+" class");
}
}
iThumbWrapper = ownerDocument.createElement("div");
iThumbWrapper.className = "montage-Slider--thumbWrapper";
iTrackElement = ownerDocument.createElement("div");
iTrackElement.className = "montage-Slider--track";
iTrackElement.setAttribute("data-montage-index",i);
iDimension = isHorizontal ? iThumbElementWithClass.offsetWidth : iThumbElementWithClass.offsetHeight;
//If the thumb has no size, or if it's horizontak and occupy the whole width, we're stepping in
if(iDimension === 0 || (isHorizontal && iDimension === spacer.offsetWidth)) {
iThumbElementWithClass.classList.add("montage-Slider-thumb--default");
}
iThumbElement.parentNode.removeChild(iThumbElement);
iThumbWrapper.appendChild(iThumbElement);
fragment.appendChild(iTrackElement);
fragment.appendChild(iThumbWrapper);
this.trackElements.push(iTrackElement);
this.thumbWrappers.push(iThumbWrapper);
this.thumbElements.push(iThumbElementWithClass);
i++;
}
//Last track element:
iTrackElement = ownerDocument.createElement("div");
iTrackElement.className = "montage-Slider--track";
fragment.appendChild(iTrackElement);
this.trackElements.push(iTrackElement);
spacer.appendChild(fragment);
i = 0;
while((iThumbWrapper = this.thumbWrappers[i])) {
iThumbElement = this.thumbElements[i];
iDimension = isHorizontal ? iThumbElement.offsetWidth : iThumbElement.offsetHeight;
// //If the thumb has no size, or if it's horizontak and occupy the whole width, we're stepping in
// if(iDimension === 0 || (isHorizontal && iDimension === spacer.offsetWidth)) {
// iThumbElement.classList.add("montage-Slider-thumb--default");
// }
/* marginLeft / marginTop must be the width of all previous thumbs */
if (isHorizontal) {
iThumbWrapper.style.marginLeft = offset + "px";
} else {
iThumbWrapper.style.marginTop = offset + "px";
}
offset += iDimension;
i++;
}
// this._thumbWidth = offset;
if(isHorizontal) {
spacer.style.marginRight = (this._spacerMarginEnd = offset) + "px";
}
else {
spacer.style.marginBottom = (this._spacerMarginEnd = offset) + "px";
}
}
// check for transform support. This should really be central and soon not needed anymore
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";
}
this.element.setAttribute("role", "slider");
//Loosk like this should be 0. And when there are multiple thumbs then it should be -1 and each thumb should have a tabIndex of 0
this.element.tabIndex = "-1";
}
}
},
// @todo: Without prepareForActivationEvents, the _translateComposer does not work
prepareForActivationEvents: {
value: function () {
this.super();
if(!this.hasStandardElement) {
var thumbWrappers = this.thumbWrappers;
if(thumbWrappers && thumbWrappers.length > 0) {
if(!this._startTranslateValues) {
this._startTranslateValues = new Array(thumbWrappers.length);
}
if(!this._startValues) {
this._startValues = new Array(thumbWrappers.length);
}
this._translateComposers = new Map();
for(var i=0, iThumbElement, iTranslateComposer;(iThumbElement = thumbWrappers[i]);i++) {
//Setting up our TranslateComposer
iTranslateComposer = new TranslateComposer();
this._translateComposers.set(iTranslateComposer,i);
iTranslateComposer.identifier = "thumb-"+i;
iTranslateComposer.axis = this.orientation;
iTranslateComposer.hasMomentum = false;
this.addComposerForElement(iTranslateComposer, iThumbElement.firstChild);
iTranslateComposer.addEventListener('translateStart', this, false);
iTranslateComposer.addEventListener('translate', this, false);
iTranslateComposer.addEventListener('translateEnd', this, false);
}
//We're missing pageUp and pageDown that would bring slider to max/min
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");
this._upKeyComposer.addEventListener("keyPress", this, false);
this._downKeyComposer.addEventListener("keyPress", this, false);
this._leftKeyComposer.addEventListener("keyPress", this, false);
this._rightKeyComposer.addEventListener("keyPress", this, false);
}
}
}
},
_previousPercentage: {
value: null
},
_dimensionLength : {
get: function() {
var dimensionLength,
computedStyle = window.getComputedStyle(this._element);
if (this.orientation === this._VERTICAL) {
dimensionLength = this._spacer.offsetHeight -
parseFloat(computedStyle.getPropertyValue("padding-top")) -
parseFloat(computedStyle.getPropertyValue("padding-bottom"));
} else {
dimensionLength = this._spacer.offsetWidth -
parseFloat(computedStyle.getPropertyValue("padding-left")) -
parseFloat(computedStyle.getPropertyValue("padding-right"));
}
return dimensionLength;
}
},
_VERTICAL: {
value: "vertical"
},
_PERCENT_UNIT: {
value: "%"
},
_PIXEL_UNIT: {
value: "px"
},
_TRANSLATE_RESET: {
value: "translate3d(0,0,0)"
},
_TRANSLATE_VERTICAL_PREFIX: {
value: "translate3d(0,"
},
_TRANSLATE_VERTICAL_SUFFIX: {
value: "px,0)"
},
_TRANSLATE_HORIZONTAL_PREFIX: {
value: "translate3d("
},
_TRANSLATE_HORIZONTAL_SUFFIX: {
value: "px,0,0)"
},
_drawThumbElement: {
value: function (thumbElementWrapper, thumbElement, index, isVertical, sliderMagnitude, length, cumulatedThumbSize, thumbElementOffsetSize) {
var percent = this._percentageValueAt(index), position, positionString, trackElement = this.trackElements[index];
if(isVertical) {
if (this._isThumbElementTranslating.get(thumbElementWrapper)) {
position = (this._percentageValueAt(index) - this._previousPercentageValues[index]) * sliderMagnitude * 0.01;
positionString = this._TRANSLATE_VERTICAL_PREFIX;
positionString += position;
positionString += this._TRANSLATE_VERTICAL_SUFFIX;
thumbElementWrapper.style[this._transform] = positionString;
} else {
thumbElementWrapper.style.top = percent + this._PERCENT_UNIT;
delete thumbElementWrapper.style.left;
thumbElementWrapper.style[this._transform] = this._TRANSLATE_RESET;
this._previousPercentageValues[index] = this._percentageValueAt(index);
}
var trackElemenTopPercent = index === 0 ? 0 : this._percentageValueAt(index-1),
height = index ? percent-this._percentageValueAt(index-1) : percent;
trackElement.style.top = trackElemenTopPercent+this._PERCENT_UNIT;
trackElement.style.marginTop = cumulatedThumbSize+this._PIXEL_UNIT;
trackElement.style.height = height+this._PERCENT_UNIT;
//Last track part if at the end
if((index+1) === length) {
trackElement = this.trackElements[index+1];
//We need the size of user-land element.
trackElement.style.top = percent+this._PERCENT_UNIT;
trackElement.style.marginTop = (cumulatedThumbSize + thumbElementOffsetSize)+this._PIXEL_UNIT;
trackElement.style.height = 100-percent+this._PERCENT_UNIT;
}
}
else {
if (this._isThumbElementTranslating.get(thumbElementWrapper)) {
position = (this._percentageValueAt(index) - this._previousPercentageValues[index]) * sliderMagnitude * 0.01;
positionString = this._TRANSLATE_HORIZONTAL_PREFIX;
positionString += position;
positionString += this._TRANSLATE_HORIZONTAL_SUFFIX;
thumbElementWrapper.style[this._transform] = positionString;
} else {
thumbElementWrapper.style.left = percent+this._PERCENT_UNIT;
delete thumbElementWrapper.style.top;
thumbElementWrapper.style[this._transform] = this._TRANSLATE_RESET;
this._previousPercentageValues[index] = this._percentageValueAt(index);
}
var trackElementLeftPercent = index === 0 ? 0 : this._percentageValueAt(index-1),
width = index ? percent-this._percentageValueAt(index-1) : percent;
trackElement.style.left = trackElementLeftPercent+this._PERCENT_UNIT;
trackElement.style.marginLeft = cumulatedThumbSize+this._PIXEL_UNIT;
trackElement.style.width = width+this._PERCENT_UNIT;
//Last track part if at the end
if((index+1) === length) {
trackElement = this.trackElements[index+1];
//We need the size of user-land element.
trackElement.style.left = percent+this._PERCENT_UNIT;
trackElement.style.marginLeft = (cumulatedThumbSize + thumbElementOffsetSize)+this._PIXEL_UNIT;
trackElement.style.width = 100-percent+this._PERCENT_UNIT;
}
}
}
},
draw: {
value: function () {
var value = this.value;
if(!this.hasStandardElement) {
//console.log("this._values is ", this._values);
var isVertical = (this.orientation === this._VERTICAL),
sliderMagnitude = isVertical ? this._spacer.offsetHeight: this._spacer.offsetWidth;
for(var i=0, iThumbElementWrapper, iThumbElement, iThumbElementOffsetSize, countI = this.thumbWrappers.length, cumulatedThumbSize = 0;(iThumbElementWrapper = this.thumbWrappers[i]);i++) {
iThumbElement = this.thumbElements[i];
iThumbElementOffsetSize = isVertical ? iThumbElement.offsetHeight : iThumbElement.offsetWidth;
this._drawThumbElement(iThumbElementWrapper,iThumbElement, i,isVertical,sliderMagnitude, countI, cumulatedThumbSize, iThumbElementOffsetSize);
cumulatedThumbSize += iThumbElementOffsetSize;
}
this.element.setAttribute("aria-valuemax", this.max);
this.element.setAttribute("aria-valuemin", this.min);
this.element.setAttribute("aria-valuenow", value);
this.element.setAttribute("aria-orientation", this.orientation);
}
else {
if (value !== this.element.value) {
this.element.value = (value === null || value === undefined ? '' : value);
}
this.element.setAttribute("max", this.max);
this.element.setAttribute("min", this.min);
}
}
},
// Event Handlers
acceptsActiveTarget: {
value: true
},
_isThumbElementTranslating: {
value: void 0
},
_startTranslateValues: {
value: void 0
},
_startValues: {
value: void 0
},
handleTranslateStart: {
value: function (e) {
this.active = true;
var index = this._translateComposers.get(e.target);
this._currentThumbIndex = index;
if(this.orientation === this._VERTICAL) {
this._startTranslateValues[index] = e.translateY;
} else {
this._startTranslateValues[index]= e.translateX;
}
this._startValues[index] = this.values[index]||0;
}
},
handleTranslate: {
value: function (event) {
var index = this._translateComposers.get(event.target),
sliderMagnitude = this._dimensionLength,
translate;
//sliderMagnitude = this._calculateSliderMagnitude();
this._currentThumbIndex = index;
if(this.orientation === this._VERTICAL) {
//this.value = this._startValues[index] + ((this._startTranslateValues[index] - event.translateY) / this._sliderMagnitude) * (this._max - this._min);
translate = event.translateY;
} else {
translate = event.translateX;
}
// var max = this.values[index+1] || this._max,
// min = this.values[index-1] || this._min,
// diff = event.translateX - this._startTranslateValues[index];
//
// if(diff < min) diff = min;
// else if(diff > max) diff = max;
var max = this._max,
min = this._min,
value;
value = this._startValues[index] + ((translate - this._startTranslateValues[index]) / sliderMagnitude) * (max - min);
max = this.values[index+1];
max = (typeof max === "number" ? max : this._max);
min = this.values[index-1];
min = (typeof min === "number" ? min : this._min);
if(value <= min) {
value = min;
} else if(value > max) {
value = max;
}
this.value = value;
// console.log("this._startValues[index] is ",this._startValues[index], "event.translateX is ",event.translateX," this._startTranslateValues[index] is ",this._startTranslateValues[index],", sliderMagnitude is ",sliderMagnitude);
// console.log("this.value = ",this.value, "min is ",min," max is ",max);
this._isThumbElementTranslating.set(event.target.element, true);
}
},
handleTranslateEnd: {
value: function (e) {
//TODO: active should only be false when none of the thumbs are being interacted with.
this.active = false;
this._isThumbElementTranslating.set(e.target.element, 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 !this.active;
//return false;
}
},
// Properties
_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: {
get: function () {
return this.values[this._currentThumbIndex];
},
set: function (value) {
this.values.set(this._currentThumbIndex, value);
}
},
/*
Should introduce a validate method
*/
value: {
get: function () {
if (this._value > this._max) {
return this._max;
} else if (this._value < this._min) {
return this._min;
}
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) {
Object.getOwnPropertyDescriptor(Control.prototype, "value").set.call(this,value);
}
}
}
},
/* this should be renamed orientation */
axis: {
value: null
},
// Machinery
_currentThumbIndex: {
value: 0
},
_translateComposer: {
value: null
},
_transform: {
value: null
},
_transition: {
value: null
},
_sliderMagnitude: {
value: null
},
_startTranslate: {
value: null
},
_startValue: {
value: null
},
_percentageValue: {
value: null
},
/* Axis should be renamed orientation and a setter should be put in place for
backward compatibility
*/
handleAxisChange: {
value: function () {
//TODO: this should handle all the thumb's translate composer
if (this._translateComposer) {
this._translateComposer.axis = this.orientation;
}
if(this.orientation === this._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|_values|values|_step/
},
handlePropertyChange: {
value: function (changeValue, key, object) {
if(key.match(this._propertyRegex) !== null) {
if(this._propertyNamesUsed) {
this._propertyNamesUsed[key] = true;
}
var MIN = this._min,
MAX = this._max,
RANGE = this._max - this._min,
valueOverriden = false;
for(var i=0, values = this.values, countI = values.length, value, min, max;i<countI;i++) {
value = values[i];
max = values[i+1] || MAX;
min = values[i-1] || MIN;
//adjust the value
if (value <= min) {
//first the simple case
value = min;
} else {
var magnitude = value - 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) && ((value - remainder) + this._step <= max);
if (roundup) {
value = (value - remainder) + this._step;
} else {
value = value - remainder;
}
}
}
//otherwise don't adjust the value just check it's within min and max
if (value > max) {
value = max;
}
values[i] = value;
if(i === this._currentThumbIndex) {
this._value = value;
}
if(valueOverriden) {
values[i] = value;
}
this._percentageValues[i] = ((value - MIN) * 100) / RANGE;
}
this.needsDraw = true;
}
}
},
_percentageValueAt: {
value: function (index) {
var value = this._percentageValues[index];
return value > 100 ? 100 : (value < 0 ? 0 : value);
}
},
handleInput: {
enumerable: false,
value: function() {
if (this.converter) {
if (this.converter.allowPartialConversion === true && this.updateOnInput === true) {
this.takeValueFromElement();
}
} else {
this.takeValueFromElement();
}
}
},
/**
Description TODO
@function
@param {Event Handler} event TODO
*/
handleChange: {
enumerable: false,
value: function(event) {
this.takeValueFromElement();
//this.dispatchActionEvent();
// this.hasFocus = false;
}
}
});
Slider.addAttributes( /** @lends module:"montage/ui/native/input-range.reel".InputRange# */ {
/**
The maximum value displayed but the input control.
@type {number}
@default null
*/
max: {
dataType: 'number',
get: function () {
return this._max;
},
set: function (value) {
if (! isNaN(value = parseFloat(value))) {
if (this._max !== value) {
this._max = value;
}
}
}
},
/**
The minimum value displayed but the input control.
@type {number}
@default null
*/
min: {
dataType: 'number',
get: function () {
return this._min;
},
set: function (value) {
if (! isNaN(value = parseFloat(value))) {
if (this._min !== value) {
this._min = value;
}
}
}
}
});
if (window.MontageElement) {
MontageElement.define("montage-slider", Slider);
}