@qooxdoo/framework
Version:
The JS Framework for Coders
648 lines (548 loc) • 18.2 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2013 1&1 Internet AG, Germany, http://www.1und1.de
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
Authors:
* Martin Wittemann (wittemann)
* Daniel Wagner (danielwagner)
************************************************************************ */
/**
* The Slider control is used to select a numerical value from a given range.
* It supports custom minimum/maximum values, step sizes and offsets (which limit
* the knob's range).
*
* <h2>Markup</h2>
* The Slider contains a single button element (the knob), which will be
* created if it's not already present.
*
* <h2>CSS Classes</h2>
* <table>
* <thead>
* <tr>
* <td>Class Name</td>
* <td>Applied to</td>
* <td>Description</td>
* </tr>
* </thead>
* <tbody>
* <tr>
* <td><code>qx-slider</code></td>
* <td>Container element</td>
* <td>Identifies the Slider widget</td>
* </tr>
* <tr>
* <td><code>qx-slider-knob</code></td>
* <td>Slider knob (button)</td>
* <td>Identifies and styles the Slider's draggable knob</td>
* </tr>
* </tbody>
* </table>
*
* <h2 class="widget-markup">Generated DOM Structure</h2>
*
* @require(qx.module.event.Pointer)
* @require(qx.module.Transform)
* @require(qx.module.Template)
* @require(qx.module.util.Type)
*
*
* @group (Widget)
*/
qx.Bootstrap.define("qx.ui.website.Slider",
{
extend : qx.ui.website.Widget,
statics : {
/**
* *step*
*
* The steps can be either a number or an array of predefined steps. In the
* case of a number, it defines the amount of each step. In the case of an
* array, the values of the array will be used as step values.
*
* Default value: <pre>1</pre>
*
*
* *minimum*
*
* The minimum value of the slider. This will only be used if no explicit
* steps are given.
*
* Default value: <pre>0 </pre>
*
*
* *maximum*
*
* The maximum value of the slider. This will only be used if no explicit
* steps are given.
*
* Default value: <pre>100</pre>
*
*
* *offset*
*
* The amount of pixel the slider should be position away from its left and
* right border.
*
* Default value: <pre>0 </pre>
*/
_config : {
minimum : 0,
maximum : 100,
offset : 0,
step : 1
},
/**
* *knobContent*
*
* The content of the knob element.
*
* Default value: <pre>{{value}}</pre>
*/
_templates : {
knobContent : "{{value}}"
},
/**
* Factory method which converts the current collection into a collection of
* slider widgets.
*
* @param value {Number?} The initial value of each slider widget
* @param step {Number|Array?} The step config value to configure the step
* width or the steps as array of numbers.
* @return {qx.ui.website.Slider} A new Slider collection.
* @attach {qxWeb}
*/
slider : function(value, step) {
var slider = new qx.ui.website.Slider(this);
slider.init();
if (typeof step !== "undefined") {
slider.setConfig("step", step);
}
if (typeof value !== "undefined") {
slider.setValue(value);
} else {
slider.setValue(slider.getConfig("minimum"));
}
return slider;
}
},
construct : function(selector, context) {
this.base(arguments, selector, context);
},
events :
{
/** Fired at each value change */
"changeValue" : "Number",
/** Fired with each pointer move event */
"changePosition" : "Number"
},
members :
{
__dragMode : null,
_value : 0,
init : function() {
if (!this.base(arguments)) {
return false;
}
var cssPrefix = this.getCssPrefix();
if (!this.getValue()) {
var step = this.getConfig("step");
var defaultVal= qxWeb.type.get(step) == "Array" ? step[0] : this.getConfig("minimum");
this._value = defaultVal;
}
this.on("pointerup", this._onSliderPointerUp, this)
.on("focus", this._onSliderFocus, this)
.setStyle("touch-action", "pan-y");
qxWeb(document).on("pointerup", this._onDocPointerUp, this);
qxWeb(window).on("resize", this._onWindowResize, this);
if (this.getChildren("." + cssPrefix + "-knob").length === 0) {
this.append(qx.ui.website.Widget.create("<button>")
.addClass(cssPrefix + "-knob"));
}
this.getChildren("." + cssPrefix + "-knob")
.setAttributes({
"draggable": "false",
"unselectable": "true"
})
.setHtml(this._getKnobContent())
.on("pointerdown", this._onPointerDown, this)
.on("dragstart", this._onDragStart, this)
.on("focus", this._onKnobFocus, this)
.on("blur", this._onKnobBlur, this);
this.render();
return true;
},
/**
* Returns the current value of the slider
*
* @return {Integer} slider value
*/
getValue : function() {
return this._value;
},
/**
* Sets the current value of the slider.
*
* @param value {Integer} new value of the slider
*
* @return {qx.ui.website.Slider} The collection for chaining
*/
setValue : function(value)
{
if (qxWeb.type.get(value) != "Number") {
throw Error("Please provide a Number value for 'value'!");
}
var step = this.getConfig("step");
if (qxWeb.type.get(step) != "Array") {
var min = this.getConfig("minimum");
var max = this.getConfig("maximum");
if (value < min) {
value = min;
}
if (value > max) {
value = max;
}
if (qxWeb.type.get(step) == "Number") {
value = Math.round(value / step) * step;
}
}
this._value = value;
if (qxWeb.type.get(step) != "Array" || step.indexOf(value) != -1) {
this.__valueToPosition(value);
this.getChildren("." + this.getCssPrefix() + "-knob")
.setHtml(this._getKnobContent());
this.emit("changeValue", value);
}
return this;
},
render : function() {
var step = this.getConfig("step");
if (qxWeb.type.get(step) == "Array") {
this._getPixels();
if (step.indexOf(this.getValue()) == -1) {
this.setValue(step[0]);
} else {
this.setValue(this.getValue());
}
} else if (qxWeb.type.get(step) == "Number") {
this.setValue(Math.round(this.getValue() / step) * step);
} else {
this.setValue(this.getValue());
}
this.getChildren("." + this.getCssPrefix() + "-knob")
.setHtml(this._getKnobContent());
return this;
},
/**
* Returns the content that should be displayed in the knob
* @return {String} knob content
*/
_getKnobContent : function() {
return qxWeb.template.render(
this.getTemplate("knobContent"), {value: this.getValue()}
);
},
/**
* Returns half of the slider knob's width, used for positioning
* @return {Integer} half knob width
*/
_getHalfKnobWidth : function() {
var knobWidth = this.getChildren("." + this.getCssPrefix() + "-knob").getWidth();
return Math.round(parseFloat(knobWidth / 2));
},
/**
* Returns the boundaries (in pixels) of the slider's range of motion
* @return {Map} a map with the keys <code>min</code> and <code>max</code>
*/
_getDragBoundaries : function()
{
var paddingLeft = Math.ceil(parseFloat(this.getStyle("paddingLeft")) || 0);
var paddingRight = Math.ceil(parseFloat(this.getStyle("paddingRight")) || 0);
var offset = this.getConfig("offset");
return {
min : this.getOffset().left + offset + paddingLeft,
max : this.getOffset().left + this.getWidth() - offset - paddingRight
};
},
/**
* Creates a lookup table to get the pixel values for each slider step
* and computes the "breakpoint" between two steps in pixel.
*
* @return {Integer[]} list of pixel values
*/
_getPixels : function()
{
var step = this.getConfig("step");
if (qxWeb.type.get(step) != "Array") {
return [];
}
var dragBoundaries = this._getDragBoundaries();
var pixel = [];
// First pixel value is fixed
pixel.push(dragBoundaries.min);
var lastIndex = step.length-1;
var paddingLeft = Math.ceil(parseFloat(this.getStyle("paddingLeft")) || 0);
var paddingRight = Math.ceil(parseFloat(this.getStyle("paddingRight")) || 0);
//The width really used by the slider (drag area)
var usedWidth = this.getWidth() - (this.getConfig("offset") * 2) - paddingLeft - paddingRight;
//The width of a single slider step
var stepWidth = usedWidth/(step[lastIndex] - step[0]);
var stepCount = 0;
for(var i=1, j=step.length-1; i<j; i++){
stepCount = step[i] - step[0];
pixel.push(Math.round(stepCount*stepWidth) + dragBoundaries.min);
}
// Last pixel value is fixed
pixel.push(dragBoundaries.max);
return pixel;
},
/**
* Returns the nearest existing slider value according to he position of the knob element.
* @param position {Integer} The current knob position in pixels
* @return {Integer} The next position to snap to
*/
_getNearestValue : function(position) {
var pixels = this._getPixels();
if (pixels.length === 0) {
var dragBoundaries = this._getDragBoundaries();
var availableWidth = dragBoundaries.max - dragBoundaries.min;
var relativePosition = position - dragBoundaries.min;
var fraction = relativePosition / availableWidth;
var min = this.getConfig("minimum");
var max = this.getConfig("maximum");
var result = (max - min) * fraction + min;
if (result < min) {
result = min;
}
if (result > max) {
result = max;
}
var step = this.getConfig("step");
if (qxWeb.type.get(step) == "Number") {
result = Math.round(result / step) * step;
}
return result;
}
var currentIndex = 0, before = 0, after = 0;
for (var i=0, j=pixels.length; i<j; i++) {
if (position >= pixels[i]) {
currentIndex = i;
before = pixels[i];
after = pixels[i+1] || before;
} else {
break;
}
}
currentIndex = Math.abs(position - before) <= Math.abs(position - after) ? currentIndex : currentIndex + 1;
return this.getConfig("step")[currentIndex];
},
/**
* Reads the pointer's position and sets slider value to the nearest step.
*
* @param e {qx.event.Emitter} Incoming event object
*/
_onSliderPointerUp : function(e) {
if ((e.getDocumentLeft() === 0 && e.getDocumentTop() === 0) ||
!this.getEnabled()) {
return;
}
this.setValue(this._getNearestValue(e.getDocumentLeft()));
},
/**
* Listener for the pointerdown event. Initializes drag or tracking mode.
*
* @param e {qx.event.Emitter} Incoming event object
*/
_onPointerDown : function(e) {
// this can happen if the user releases the button while dragging outside
// of the browser viewport
if (this.__dragMode) {
return;
}
this.__dragMode = true;
qxWeb(document.documentElement).on("pointermove", this._onPointerMove, this)
.setStyle("cursor", "pointer");
e.stopPropagation();
},
/**
* Listener for the pointerup event. Used for cleanup of previously
* initialized modes.
*
* @param e {qx.event.Emitter} Incoming event object
*/
_onDocPointerUp : function(e) {
if (this.__dragMode === true) {
// Cleanup status flags
delete this.__dragMode;
this.__valueToPosition(this.getValue());
qxWeb(document.documentElement).off("pointermove", this._onPointerMove, this)
.setStyle("cursor", "auto");
e.stopPropagation();
}
},
/**
* Listener for the pointermove event for the knob. Only used in drag mode.
*
* @param e {qx.event.Emitter} Incoming event object
*/
_onPointerMove : function(e) {
e.preventDefault();
if (this.__dragMode) {
var dragPosition = e.getDocumentLeft();
var dragBoundaries = this._getDragBoundaries();
var paddingLeft = Math.ceil(parseFloat(this.getStyle("paddingLeft")) || 0);
var positionKnob = dragPosition - this.getOffset().left - this._getHalfKnobWidth() - paddingLeft;
if (dragPosition >= dragBoundaries.min && dragPosition <= dragBoundaries.max) {
this.setValue(this._getNearestValue(dragPosition));
if (positionKnob > 0) {
this._setKnobPosition(positionKnob);
this.emit("changePosition", positionKnob);
}
}
e.stopPropagation();
}
},
/**
* Prevents drag event propagation
* @param e {Event} e drag start event
*/
_onDragStart : function(e) {
e.stopPropagation();
e.preventDefault();
},
/**
* Delegates the Slider's focus to the knob
* @param e {Event} focus event
*/
_onSliderFocus : function(e) {
this.getChildren("." + this.getCssPrefix() + "-knob").focus();
},
/**
* Attaches the event listener for keyboard support to the knob on focus
* @param e {Event} focus event
*/
_onKnobFocus : function(e) {
this.getChildren("." + this.getCssPrefix() + "-knob")
.on("keydown", this._onKeyDown, this);
},
/**
* Removes the event listener for keyboard support from the knob on blur
* @param e {Event} blur event
*/
_onKnobBlur : function(e) {
this.getChildren("." + this.getCssPrefix() + "-knob")
.off("keydown", this._onKeyDown, this);
},
/**
* Moves the knob if the left or right arrow key is pressed
* @param e {Event} keydown event
*/
_onKeyDown : function(e) {
var newValue;
var currentValue = this.getValue();
var step = this.getConfig("step");
var stepType = qxWeb.type.get(step);
var key = e.getKeyIdentifier();
var idx;
if (key == "Right") {
if (stepType === "Array") {
idx = step.indexOf(currentValue);
if (idx !== undefined) {
newValue = step[idx + 1] || currentValue;
}
} else if (stepType === "Number") {
newValue = currentValue + step;
}
else {
newValue = currentValue + 1;
}
}
else if (key == "Left") {
if (stepType === "Array") {
idx = step.indexOf(currentValue);
if (idx !== undefined) {
newValue = step[idx - 1] || currentValue;
}
} else if (stepType === "Number") {
newValue = currentValue - step;
}
else {
newValue = currentValue - 1;
}
} else {
return;
}
this.setValue(newValue);
},
/**
* Applies the horizontal position
* @param x {Integer} the position to move to
*/
_setKnobPosition : function(x) {
var knob = this.getChildren("." + this.getCssPrefix() + "-knob");
if (qxWeb.env.get("css.transform")) {
knob.translate([x + "px", 0, 0]);
} else {
knob.setStyle("left", x + "px");
}
},
/**
* Listener for window resize events. This listener method resets the
* calculated values which are used to position the slider knob.
*/
_onWindowResize : function() {
if (qxWeb.type.get(this.getConfig("step")) == "Array") {
this._getPixels();
}
this.__valueToPosition(this._value);
},
/**
* Positions the slider knob to the given value and fires the "changePosition"
* event with the current position as integer.
*
* @param value {Integer} slider step value
*/
__valueToPosition : function(value)
{
var pixels = this._getPixels();
var paddingLeft = Math.ceil(parseFloat(this.getStyle("paddingLeft")) || 0);
var valueToPixel;
if (pixels.length > 0) {
// Get the pixel value of the current step value
valueToPixel = pixels[this.getConfig("step").indexOf(value)] - paddingLeft;
} else {
var dragBoundaries = this._getDragBoundaries();
var availableWidth = dragBoundaries.max - dragBoundaries.min;
var range = this.getConfig("maximum") - this.getConfig("minimum");
var fraction = (value - this.getConfig("minimum")) / range;
valueToPixel = (availableWidth * fraction) + dragBoundaries.min - paddingLeft;
}
// relative position is necessary here
var position = valueToPixel - this.getOffset().left - this._getHalfKnobWidth();
this._setKnobPosition(position);
this.emit("changePosition", position);
},
dispose : function()
{
qxWeb(document).off("pointerup", this._onDocPointerUp, this);
qxWeb(window).off("resize", this._onWindowResize, this);
this.off("pointerup", this._onSliderPointerUp, this)
.off("focus", this._onSliderFocus, this);
this.getChildren("." + this.getCssPrefix() + "-knob")
.off("pointerdown", this._onPointerDown, this)
.off("dragstart", this._onDragStart, this)
.off("focus", this._onKnobFocus, this)
.off("blur", this._onKnobBlur, this)
.off("keydown", this._onKeyDown, this);
this.setHtml("");
return this.base(arguments);
}
},
// Make the slider widget available as a qxWeb module
defer : function(statics) {
qxWeb.$attach({slider : statics.slider});
}
});