@qooxdoo/framework
Version:
The JS Framework for Coders
1,127 lines (912 loc) • 29.9 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2004-2008 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:
* Sebastian Werner (wpbasti)
* Fabian Jakobs (fjakobs)
************************************************************************ */
/**
* The Slider widget provides a vertical or horizontal slider.
*
* The Slider is the classic widget for controlling a bounded value.
* It lets the user move a slider handle along a horizontal or vertical
* groove and translates the handle's position into an integer value
* within the defined range.
*
* The Slider has very few of its own functions.
* The most useful functions are slideTo() to set the slider directly to some
* value; setSingleStep(), setPageStep() to set the steps; and setMinimum()
* and setMaximum() to define the range of the slider.
*
* A slider accepts focus on Tab and provides both a mouse wheel and
* a keyboard interface. The keyboard interface is the following:
*
* * Left/Right move a horizontal slider by one single step.
* * Up/Down move a vertical slider by one single step.
* * PageUp moves up one page.
* * PageDown moves down one page.
* * Home moves to the start (minimum).
* * End moves to the end (maximum).
*
* Here are the main properties of the class:
*
* # <code>value</code>: The bounded integer that {@link qx.ui.form.INumberForm}
* maintains.
* # <code>minimum</code>: The lowest possible value.
* # <code>maximum</code>: The highest possible value.
* # <code>singleStep</code>: The smaller of two natural steps that an abstract
* sliders provides and typically corresponds to the user pressing an arrow key.
* # <code>pageStep</code>: The larger of two natural steps that an abstract
* slider provides and typically corresponds to the user pressing PageUp or
* PageDown.
*
* @childControl knob {qx.ui.core.Widget} knob to set the value of the slider
*/
qx.Class.define("qx.ui.form.Slider",
{
extend : qx.ui.core.Widget,
implement : [
qx.ui.form.IForm,
qx.ui.form.INumberForm,
qx.ui.form.IRange
],
include : [qx.ui.form.MForm],
/*
*****************************************************************************
CONSTRUCTOR
*****************************************************************************
*/
/**
* @param orientation {String?"horizontal"} Configure the
* {@link #orientation} property
*/
construct : function(orientation)
{
this.base(arguments);
// Force canvas layout
this._setLayout(new qx.ui.layout.Canvas());
// Add listeners
this.addListener("keypress", this._onKeyPress);
this.addListener("roll", this._onRoll);
this.addListener("pointerdown", this._onPointerDown);
this.addListener("pointerup", this._onPointerUp);
this.addListener("losecapture", this._onPointerUp);
this.addListener("resize", this._onUpdate);
// Stop events
this.addListener("contextmenu", this._onStopEvent);
this.addListener("tap", this._onStopEvent);
this.addListener("dbltap", this._onStopEvent);
// Initialize orientation
if (orientation != null) {
this.setOrientation(orientation);
} else {
this.initOrientation();
}
},
/*
*****************************************************************************
EVENTS
*****************************************************************************
*/
events : {
/**
* Change event for the value.
*/
changeValue: 'qx.event.type.Data',
/** Fired as soon as the slide animation ended. */
slideAnimationEnd: 'qx.event.type.Event'
},
/*
*****************************************************************************
PROPERTIES
*****************************************************************************
*/
properties :
{
// overridden
appearance :
{
refine : true,
init : "slider"
},
// overridden
focusable :
{
refine : true,
init : true
},
/** Whether the slider is horizontal or vertical. */
orientation :
{
check : [ "horizontal", "vertical" ],
init : "horizontal",
apply : "_applyOrientation"
},
/**
* The current slider value.
*
* Strictly validates according to {@link #minimum} and {@link #maximum}.
* Do not apply any value correction to the incoming value. If you depend
* on this, please use {@link #slideTo} instead.
*/
value :
{
check : "typeof value==='number'&&value>=this.getMinimum()&&value<=this.getMaximum()",
init : 0,
apply : "_applyValue",
nullable: true
},
/**
* The minimum slider value (may be negative). This value must be smaller
* than {@link #maximum}.
*/
minimum :
{
check : "Integer",
init : 0,
apply : "_applyMinimum",
event: "changeMinimum"
},
/**
* The maximum slider value (may be negative). This value must be larger
* than {@link #minimum}.
*/
maximum :
{
check : "Integer",
init : 100,
apply : "_applyMaximum",
event : "changeMaximum"
},
/**
* The amount to increment on each event. Typically corresponds
* to the user pressing an arrow key.
*/
singleStep :
{
check : "Integer",
init : 1
},
/**
* The amount to increment on each event. Typically corresponds
* to the user pressing <code>PageUp</code> or <code>PageDown</code>.
*/
pageStep :
{
check : "Integer",
init : 10
},
/**
* Factor to apply to the width/height of the knob in relation
* to the dimension of the underlying area.
*/
knobFactor :
{
check : "Number",
apply : "_applyKnobFactor",
nullable : true
}
},
/*
*****************************************************************************
MEMBERS
*****************************************************************************
*/
members :
{
__sliderLocation : null,
__knobLocation : null,
__knobSize : null,
__dragMode : null,
__dragOffset : null,
__trackingMode : null,
__trackingDirection : null,
__trackingEnd : null,
__timer : null,
// event delay stuff during drag
__dragTimer: null,
__lastValueEvent: null,
__dragValue: null,
__scrollAnimationframe : null,
// overridden
/**
* @lint ignoreReferenceField(_forwardStates)
*/
_forwardStates : {
invalid : true
},
// overridden
renderLayout : function(left, top, width, height) {
this.base(arguments, left, top, width, height);
// make sure the layout engine does not override the knob position
this._updateKnobPosition();
},
// overridden
_createChildControlImpl : function(id, hash)
{
var control;
switch(id)
{
case "knob":
control = new qx.ui.core.Widget();
control.addListener("resize", this._onUpdate, this);
control.addListener("pointerover", this._onPointerOver);
control.addListener("pointerout", this._onPointerOut);
this._add(control);
break;
}
return control || this.base(arguments, id);
},
/*
---------------------------------------------------------------------------
EVENT HANDLER
---------------------------------------------------------------------------
*/
/**
* Event handler for pointerover events at the knob child control.
*
* Adds the 'hovered' state
*
* @param e {qx.event.type.Pointer} Incoming pointer event
*/
_onPointerOver : function(e) {
this.addState("hovered");
},
/**
* Event handler for pointerout events at the knob child control.
*
* Removes the 'hovered' state
*
* @param e {qx.event.type.Pointer} Incoming pointer event
*/
_onPointerOut : function(e) {
this.removeState("hovered");
},
/**
* Listener of roll event
*
* @param e {qx.event.type.Roll} Incoming event object
*/
_onRoll : function(e)
{
// only wheel
if (e.getPointerType() != "wheel") {
return;
}
var axis = this.getOrientation() === "horizontal" ? "x" : "y";
var delta = e.getDelta()[axis];
var direction = delta > 0 ? 1 : delta < 0 ? -1 : 0;
this.slideBy(direction * this.getSingleStep());
e.stop();
},
/**
* Event handler for keypress events.
*
* Adds support for arrow keys, page up, page down, home and end keys.
*
* @param e {qx.event.type.KeySequence} Incoming keypress event
*/
_onKeyPress : function(e)
{
var isHorizontal = this.getOrientation() === "horizontal";
var backward = isHorizontal ? "Left" : "Up";
var forward = isHorizontal ? "Right" : "Down";
switch(e.getKeyIdentifier())
{
case forward:
this.slideForward();
break;
case backward:
this.slideBack();
break;
case "PageDown":
this.slidePageForward(100);
break;
case "PageUp":
this.slidePageBack(100);
break;
case "Home":
this.slideToBegin(200);
break;
case "End":
this.slideToEnd(200);
break;
default:
return;
}
// Stop processed events
e.stop();
},
/**
* Listener of pointerdown event. Initializes drag or tracking mode.
*
* @param e {qx.event.type.Pointer} 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;
}
var isHorizontal = this.__isHorizontal;
var knob = this.getChildControl("knob");
var locationProperty = isHorizontal ? "left" : "top";
var cursorLocation = isHorizontal ? e.getDocumentLeft() : e.getDocumentTop();
var decorator = this.getDecorator();
decorator = qx.theme.manager.Decoration.getInstance().resolve(decorator);
if (isHorizontal) {
var decoratorPadding = decorator ? decorator.getInsets().left : 0;
var padding = (this.getPaddingLeft() || 0) + decoratorPadding;
} else {
var decoratorPadding = decorator ? decorator.getInsets().top : 0;
var padding = (this.getPaddingTop() || 0) + decoratorPadding;
}
var sliderLocation = this.__sliderLocation = qx.bom.element.Location.get(this.getContentElement().getDomElement())[locationProperty];
sliderLocation += padding;
var knobLocation = this.__knobLocation = qx.bom.element.Location.get(knob.getContentElement().getDomElement())[locationProperty];
if (e.getTarget() === knob)
{
// Switch into drag mode
this.__dragMode = true;
if (!this.__dragTimer){
// create a timer to fire delayed dragging events if dragging stops.
this.__dragTimer = new qx.event.Timer(100);
this.__dragTimer.addListener("interval", this._fireValue, this);
}
this.__dragTimer.start();
// Compute dragOffset (includes both: inner position of the widget and
// cursor position on knob)
this.__dragOffset = cursorLocation + sliderLocation - knobLocation;
// add state
knob.addState("pressed");
}
else
{
// Switch into tracking mode
this.__trackingMode = true;
// Detect tracking direction
this.__trackingDirection = cursorLocation <= knobLocation ? -1 : 1;
// Compute end value
this.__computeTrackingEnd(e);
// Directly call interval method once
this._onInterval();
// Initialize timer (when needed)
if (!this.__timer)
{
this.__timer = new qx.event.Timer(100);
this.__timer.addListener("interval", this._onInterval, this);
}
// Start timer
this.__timer.start();
}
// Register move listener
this.addListener("pointermove", this._onPointerMove);
// Activate capturing
this.capture();
// Stop event
e.stopPropagation();
},
/**
* Listener of pointerup event. Used for cleanup of previously
* initialized modes.
*
* @param e {qx.event.type.Pointer} Incoming event object
*/
_onPointerUp : function(e)
{
if (this.__dragMode)
{
// Release capture mode
this.releaseCapture();
// Cleanup status flags
delete this.__dragMode;
// as we come out of drag mode, make
// sure content gets synced
this.__dragTimer.stop();
this._fireValue();
delete this.__dragOffset;
// remove state
this.getChildControl("knob").removeState("pressed");
// it's necessary to check whether the cursor is over the knob widget to be able to
// to decide whether to remove the 'hovered' state.
if (e.getType() === "pointerup")
{
var deltaSlider;
var deltaPosition;
var positionSlider;
if (this.__isHorizontal)
{
deltaSlider = e.getDocumentLeft() - (this._valueToPosition(this.getValue()) + this.__sliderLocation);
positionSlider = qx.bom.element.Location.get(this.getContentElement().getDomElement())["top"];
deltaPosition = e.getDocumentTop() - (positionSlider + this.getChildControl("knob").getBounds().top);
}
else
{
deltaSlider = e.getDocumentTop() - (this._valueToPosition(this.getValue()) + this.__sliderLocation);
positionSlider = qx.bom.element.Location.get(this.getContentElement().getDomElement())["left"];
deltaPosition = e.getDocumentLeft() - (positionSlider + this.getChildControl("knob").getBounds().left);
}
if (deltaPosition < 0 || deltaPosition > this.__knobSize ||
deltaSlider < 0 || deltaSlider > this.__knobSize) {
this.getChildControl("knob").removeState("hovered");
}
}
}
else if (this.__trackingMode)
{
// Stop timer interval
this.__timer.stop();
// Release capture mode
this.releaseCapture();
// Cleanup status flags
delete this.__trackingMode;
delete this.__trackingDirection;
delete this.__trackingEnd;
}
// Remove move listener again
this.removeListener("pointermove", this._onPointerMove);
// Stop event
if (e.getType() === "pointerup") {
e.stopPropagation();
}
},
/**
* Listener of pointermove event for the knob. Only used in drag mode.
*
* @param e {qx.event.type.Pointer} Incoming event object
*/
_onPointerMove : function(e)
{
if (this.__dragMode)
{
var dragStop = this.__isHorizontal ?
e.getDocumentLeft() : e.getDocumentTop();
var position = dragStop - this.__dragOffset;
this.slideTo(this._positionToValue(position));
}
else if (this.__trackingMode)
{
// Update tracking end on pointermove
this.__computeTrackingEnd(e);
}
// Stop event
e.stopPropagation();
},
/**
* Listener of interval event by the internal timer. Only used
* in tracking sequences.
*
* @param e {qx.event.type.Event} Incoming event object
*/
_onInterval : function(e)
{
// Compute new value
var value = this.getValue() + (this.__trackingDirection * this.getPageStep());
// Limit value
if (value < this.getMinimum()) {
value = this.getMinimum();
} else if (value > this.getMaximum()) {
value = this.getMaximum();
}
// Stop at tracking position (where the pointer is pressed down)
var slideBack = this.__trackingDirection == -1;
if ((slideBack && value <= this.__trackingEnd) || (!slideBack && value >= this.__trackingEnd)) {
value = this.__trackingEnd;
}
// Finally slide to the desired position
this.slideTo(value);
},
/**
* Listener of resize event for both the slider itself and the knob.
*
* @param e {qx.event.type.Data} Incoming event object
*/
_onUpdate : function(e)
{
// Update sliding space
var availSize = this.getInnerSize();
var knobSize = this.getChildControl("knob").getBounds();
var sizeProperty = this.__isHorizontal ? "width" : "height";
// Sync knob size
this._updateKnobSize();
// Store knob size
this.__slidingSpace = availSize[sizeProperty] - knobSize[sizeProperty];
this.__knobSize = knobSize[sizeProperty];
// Update knob position (sliding space must be updated first)
this._updateKnobPosition();
},
/*
---------------------------------------------------------------------------
UTILS
---------------------------------------------------------------------------
*/
/** @type {Boolean} Whether the slider is laid out horizontally */
__isHorizontal : false,
/**
* @type {Integer} Available space for knob to slide on, computed on resize of
* the widget
*/
__slidingSpace : 0,
/**
* Computes the value where the tracking should end depending on
* the current pointer position.
*
* @param e {qx.event.type.Pointer} Incoming pointer event
*/
__computeTrackingEnd : function(e)
{
var isHorizontal = this.__isHorizontal;
var cursorLocation = isHorizontal ? e.getDocumentLeft() : e.getDocumentTop();
var sliderLocation = this.__sliderLocation;
var knobLocation = this.__knobLocation;
var knobSize = this.__knobSize;
// Compute relative position
var position = cursorLocation - sliderLocation;
if (cursorLocation >= knobLocation) {
position -= knobSize;
}
// Compute stop value
var value = this._positionToValue(position);
var min = this.getMinimum();
var max = this.getMaximum();
if (value < min) {
value = min;
} else if (value > max) {
value = max;
} else {
var old = this.getValue();
var step = this.getPageStep();
var method = this.__trackingDirection < 0 ? "floor" : "ceil";
// Fix to page step
value = old + (Math[method]((value - old) / step) * step);
}
// Store value when undefined, otherwise only when it follows the
// current direction e.g. goes up or down
if (this.__trackingEnd == null || (this.__trackingDirection == -1 && value <= this.__trackingEnd) || (this.__trackingDirection == 1 && value >= this.__trackingEnd)) {
this.__trackingEnd = value;
}
},
/**
* Converts the given position to a value.
*
* Does not respect single or page step.
*
* @param position {Integer} Position to use
* @return {Integer} Resulting value (rounded)
*/
_positionToValue : function(position)
{
// Reading available space
var avail = this.__slidingSpace;
// Protect undefined value (before initial resize) and division by zero
if (avail == null || avail == 0) {
return 0;
}
// Compute and limit percent
var percent = position / avail;
if (percent < 0) {
percent = 0;
} else if (percent > 1) {
percent = 1;
}
// Compute range
var range = this.getMaximum() - this.getMinimum();
// Compute value
return this.getMinimum() + Math.round(range * percent);
},
/**
* Converts the given value to a position to place
* the knob to.
*
* @param value {Integer} Value to use
* @return {Integer} Computed position (rounded)
*/
_valueToPosition : function(value)
{
// Reading available space
var avail = this.__slidingSpace;
if (avail == null) {
return 0;
}
// Computing range
var range = this.getMaximum() - this.getMinimum();
// Protect division by zero
if (range == 0) {
return 0;
}
// Translating value to distance from minimum
var value = value - this.getMinimum();
// Compute and limit percent
var percent = value / range;
if (percent < 0) {
percent = 0;
} else if (percent > 1) {
percent = 1;
}
// Compute position from available space and percent
return Math.round(avail * percent);
},
/**
* Updates the knob position following the currently configured
* value. Useful on reflows where the dimensions of the slider
* itself have been modified.
*
*/
_updateKnobPosition : function() {
this._setKnobPosition(this._valueToPosition(this.getValue()));
},
/**
* Moves the knob to the given position.
*
* @param position {Integer} Any valid position (needs to be
* greater or equal than zero)
*/
_setKnobPosition : function(position)
{
// Use the DOM Element to prevent unnecessary layout recalculations
var knob = this.getChildControl("knob");
var dec = this.getDecorator();
dec = qx.theme.manager.Decoration.getInstance().resolve(dec);
var content = knob.getContentElement();
if (this.__isHorizontal) {
if (dec && dec.getPadding()) {
position += dec.getPadding().left;
}
position += this.getPaddingLeft() || 0;
content.setStyle("left", position+"px", true);
} else {
if (dec && dec.getPadding()) {
position += dec.getPadding().top;
}
position += this.getPaddingTop() || 0;
content.setStyle("top", position+"px", true);
}
},
/**
* Reconfigures the size of the knob depending on
* the optionally defined {@link #knobFactor}.
*
*/
_updateKnobSize : function()
{
// Compute knob size
var knobFactor = this.getKnobFactor();
if (knobFactor == null) {
return;
}
// Ignore when not rendered yet
var avail = this.getInnerSize();
if (avail == null) {
return;
}
// Read size property
if (this.__isHorizontal) {
this.getChildControl("knob").setWidth(Math.round(knobFactor * avail.width));
} else {
this.getChildControl("knob").setHeight(Math.round(knobFactor * avail.height));
}
},
/*
---------------------------------------------------------------------------
SLIDE METHODS
---------------------------------------------------------------------------
*/
/**
* Slides backward to the minimum value
* @param duration {Number} The time in milliseconds the slide to should take.
*/
slideToBegin : function(duration) {
this.slideTo(this.getMinimum(), duration);
},
/**
* Slides forward to the maximum value
* @param duration {Number} The time in milliseconds the slide to should take.
*/
slideToEnd : function(duration) {
this.slideTo(this.getMaximum(), duration);
},
/**
* Slides forward (right or bottom depending on orientation)
*
*/
slideForward : function() {
this.slideBy(this.getSingleStep());
},
/**
* Slides backward (to left or top depending on orientation)
*
*/
slideBack : function() {
this.slideBy(-this.getSingleStep());
},
/**
* Slides a page forward (to right or bottom depending on orientation)
* @param duration {Number} The time in milliseconds the slide to should take.
*/
slidePageForward : function(duration) {
this.slideBy(this.getPageStep(), duration);
},
/**
* Slides a page backward (to left or top depending on orientation)
* @param duration {Number} The time in milliseconds the slide to should take.
*/
slidePageBack : function(duration) {
this.slideBy(-this.getPageStep(), duration);
},
/**
* Slides by the given offset.
*
* This method works with the value, not with the coordinate.
*
* @param offset {Integer} Offset to scroll by
* @param duration {Number} The time in milliseconds the slide to should take.
*/
slideBy : function(offset, duration) {
this.slideTo(this.getValue() + offset, duration);
},
/**
* Slides to the given value
*
* This method works with the value, not with the coordinate.
*
* @param value {Integer} Scroll to a value between the defined
* minimum and maximum.
* @param duration {Number} The time in milliseconds the slide to should take.
*/
slideTo : function(value, duration)
{
this.stopSlideAnimation();
if (duration) {
this.__animateTo(value, duration);
} else {
this.updatePosition(value);
}
},
/**
* Updates the position property considering the minimum and maximum values.
* @param value {Number} The new position.
*/
updatePosition : function(value) {
this.setValue(this.__normalizeValue(value));
},
/**
* In case a slide animation is currently running, it will be stopped.
* If not, the method does nothing.
*/
stopSlideAnimation : function() {
if (this.__scrollAnimationframe) {
this.__scrollAnimationframe.cancelSequence();
this.__scrollAnimationframe = null;
}
},
/**
* Internal helper to normalize the given value concerning the minimum
* and maximum value.
* @param value {Number} The value to normalize.
* @return {Number} The normalized value.
*/
__normalizeValue : function(value) {
// Bring into allowed range or fix to single step grid
if (value < this.getMinimum()) {
value = this.getMinimum();
} else if (value > this.getMaximum()) {
value = this.getMaximum();
} else {
value = this.getMinimum() + Math.round((value - this.getMinimum()) / this.getSingleStep()) * this.getSingleStep();
}
return value;
},
/**
* Animation helper which takes care of the animated slide.
* @param to {Number} The target value.
* @param duration {Number} The time in milliseconds the slide to should take.
*/
__animateTo : function(to, duration) {
to = this.__normalizeValue(to);
var from = this.getValue();
this.__scrollAnimationframe = new qx.bom.AnimationFrame();
this.__scrollAnimationframe.on("frame", function(timePassed) {
this.setValue(parseInt(timePassed/duration * (to - from) + from));
}, this);
this.__scrollAnimationframe.on("end", function() {
this.setValue(to);
this.__scrollAnimationframe = null;
this.fireEvent("slideAnimationEnd");
}, this);
this.__scrollAnimationframe.startSequence(duration);
},
/*
---------------------------------------------------------------------------
PROPERTY APPLY ROUTINES
---------------------------------------------------------------------------
*/
// property apply
_applyOrientation : function(value, old)
{
var knob = this.getChildControl("knob");
// Update private flag for faster access
this.__isHorizontal = value === "horizontal";
// Toggle states and knob layout
if (this.__isHorizontal)
{
this.removeState("vertical");
knob.removeState("vertical");
this.addState("horizontal");
knob.addState("horizontal");
knob.setLayoutProperties({top:0, right:null, bottom:0});
}
else
{
this.removeState("horizontal");
knob.removeState("horizontal");
this.addState("vertical");
knob.addState("vertical");
knob.setLayoutProperties({right:0, bottom:null, left:0});
}
// Sync knob position
this._updateKnobPosition();
},
// property apply
_applyKnobFactor : function(value, old)
{
if (value != null)
{
this._updateKnobSize();
}
else
{
if (this.__isHorizontal) {
this.getChildControl("knob").resetWidth();
} else {
this.getChildControl("knob").resetHeight();
}
}
},
// property apply
_applyValue : function(value, old) {
if (value != null) {
this._updateKnobPosition();
if (this.__dragMode) {
this.__dragValue = [value,old];
} else {
this.fireEvent("changeValue", qx.event.type.Data, [value,old]);
}
} else {
this.resetValue();
}
},
/**
* Helper for applyValue which fires the changeValue event.
*/
_fireValue: function(){
if (!this.__dragValue){
return;
}
var tmp = this.__dragValue;
this.__dragValue = null;
this.fireEvent("changeValue", qx.event.type.Data, tmp);
},
// property apply
_applyMinimum : function(value, old)
{
if (this.getValue() < value) {
this.setValue(value);
}
this._updateKnobPosition();
},
// property apply
_applyMaximum : function(value, old)
{
if (this.getValue() > value) {
this.setValue(value);
}
this._updateKnobPosition();
}
}
});