UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

856 lines (715 loc) 22.3 kB
/* ************************************************************************ 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) * Andreas Ecker (ecker) * Martin Wittemann (martinwittemann) * Jonathan Weiß (jonathan_rass) ************************************************************************ */ /** * A *spinner* is a control that allows you to adjust a numerical value, * typically within an allowed range. An obvious example would be to specify the * month of a year as a number in the range 1 - 12. * * To do so, a spinner encompasses a field to display the current value (a * textfield) and controls such as up and down buttons to change that value. The * current value can also be changed by editing the display field directly, or * using mouse wheel and cursor keys. * * An optional {@link #numberFormat} property allows you to control the format of * how a value can be entered and will be displayed. * * A brief, but non-trivial example: * * <pre class='javascript'> * var s = new qx.ui.form.Spinner(); * s.set({ * maximum: 3000, * minimum: -3000 * }); * var nf = new qx.util.format.NumberFormat(); * nf.setMaximumFractionDigits(2); * s.setNumberFormat(nf); * </pre> * * A spinner instance without any further properties specified in the * constructor or a subsequent *set* command will appear with default * values and behaviour. * * @childControl textfield {qx.ui.form.TextField} holds the current value of the spinner * @childControl upbutton {qx.ui.form.Button} button to increase the value * @childControl downbutton {qx.ui.form.Button} button to decrease the value * */ qx.Class.define("qx.ui.form.Spinner", { extend : qx.ui.core.Widget, implement : [ qx.ui.form.INumberForm, qx.ui.form.IRange, qx.ui.form.IForm ], include : [ qx.ui.core.MContentPadding, qx.ui.form.MForm ], /* ***************************************************************************** CONSTRUCTOR ***************************************************************************** */ /** * @param min {Number} Minimum value * @param value {Number} Current value * @param max {Number} Maximum value */ construct : function(min, value, max) { this.base(arguments); // MAIN LAYOUT var layout = new qx.ui.layout.Grid(); layout.setColumnFlex(0, 1); layout.setRowFlex(0,1); layout.setRowFlex(1,1); this._setLayout(layout); // EVENTS this.addListener("keydown", this._onKeyDown, this); this.addListener("keyup", this._onKeyUp, this); this.addListener("roll", this._onRoll, this); if (qx.core.Environment.get("qx.dynlocale")) { qx.locale.Manager.getInstance().addListener("changeLocale", this._onChangeLocale, this); } // CREATE CONTROLS var textField = this._createChildControl("textfield"); this._createChildControl("upbutton"); this._createChildControl("downbutton"); // INITIALIZATION if (min != null) { this.setMinimum(min); } if (max != null) { this.setMaximum(max); } if (value !== undefined) { this.setValue(value); } else { this.initValue(); } // forward the focusin and focusout events to the textfield. The textfield // is not focusable so the events need to be forwarded manually. this.addListener("focusin", function(e) { textField.fireNonBubblingEvent("focusin", qx.event.type.Focus); }, this); this.addListener("focusout", function(e) { textField.fireNonBubblingEvent("focusout", qx.event.type.Focus); }, this); }, /* ***************************************************************************** PROPERTIES ***************************************************************************** */ properties: { // overridden appearance: { refine : true, init : "spinner" }, // overridden focusable : { refine : true, init : true }, /** The amount to increment on each event (keypress or pointerdown) */ singleStep: { check : "Number", init : 1 }, /** The amount to increment on each pageup/pagedown keypress */ pageStep: { check : "Number", init : 10 }, /** minimal value of the Range object */ minimum: { check : "Number", apply : "_applyMinimum", init : 0, event: "changeMinimum" }, /** The value of the spinner. */ value: { check : "this._checkValue(value)", nullable : true, apply : "_applyValue", init : 0, event : "changeValue" }, /** maximal value of the Range object */ maximum: { check : "Number", apply : "_applyMaximum", init : 100, event: "changeMaximum" }, /** whether the value should wrap around */ wrap: { check : "Boolean", init : false, apply : "_applyWrap" }, /** Controls whether the textfield of the spinner is editable or not */ editable : { check : "Boolean", init : true, apply : "_applyEditable" }, /** Controls the display of the number in the textfield */ numberFormat : { check : "qx.util.format.NumberFormat", apply : "_applyNumberFormat", nullable : true }, // overridden allowShrinkY : { refine : true, init : false } }, /* ***************************************************************************** MEMBERS ***************************************************************************** */ members : { /** Saved last value in case invalid text is entered */ __lastValidValue : null, /** Whether the page-up button has been pressed */ __pageUpMode : false, /** Whether the page-down button has been pressed */ __pageDownMode : false, /* --------------------------------------------------------------------------- WIDGET INTERNALS --------------------------------------------------------------------------- */ // overridden _createChildControlImpl : function(id, hash) { var control; switch(id) { case "textfield": control = new qx.ui.form.TextField(); control.setFilter(this._getFilterRegExp()); control.addState("inner"); control.setWidth(40); control.setFocusable(false); control.addListener("changeValue", this._onTextChange, this); this._add(control, {column: 0, row: 0, rowSpan: 2}); break; case "upbutton": control = new qx.ui.form.RepeatButton(); control.addState("inner"); control.setFocusable(false); control.addListener("execute", this._countUp, this); this._add(control, {column: 1, row: 0}); break; case "downbutton": control = new qx.ui.form.RepeatButton(); control.addState("inner"); control.setFocusable(false); control.addListener("execute", this._countDown, this); this._add(control, {column:1, row: 1}); break; } return control || this.base(arguments, id); }, /** * Returns the regular expression used as the text field's filter * * @return {RegExp} The filter RegExp. */ _getFilterRegExp : function() { var decimalSeparator, groupSeparator, locale; if (this.getNumberFormat() !== null) { locale = this.getNumberFormat().getLocale(); } else { locale = qx.locale.Manager.getInstance().getLocale(); } decimalSeparator = qx.locale.Number.getDecimalSeparator(locale); groupSeparator = qx.locale.Number.getGroupSeparator(locale); var prefix = ""; var postfix = ""; if (this.getNumberFormat() !== null) { prefix = this.getNumberFormat().getPrefix() || ""; postfix = this.getNumberFormat().getPostfix() || ""; } var filterRegExp = new RegExp("[0-9" + qx.lang.String.escapeRegexpChars(decimalSeparator) + qx.lang.String.escapeRegexpChars(groupSeparator) + qx.lang.String.escapeRegexpChars(prefix) + qx.lang.String.escapeRegexpChars(postfix) + "\-]" ); return filterRegExp; }, // overridden /** * @lint ignoreReferenceField(_forwardStates) */ _forwardStates : { focused : true, invalid : true }, // overridden tabFocus : function() { var field = this.getChildControl("textfield"); field.getFocusElement().focus(); field.selectAllText(); }, /* --------------------------------------------------------------------------- APPLY METHODS --------------------------------------------------------------------------- */ /** * Apply routine for the minimum property. * * It sets the value of the spinner to the maximum of the current spinner * value and the given min property value. * * @param value {Number} The new value of the min property * @param old {Number} The old value of the min property */ _applyMinimum : function(value, old) { if (this.getMaximum() < value) { this.setMaximum(value); } if (this.getValue() < value) { this.setValue(value); } else { this._updateButtons(); } }, /** * Apply routine for the maximum property. * * It sets the value of the spinner to the minimum of the current spinner * value and the given max property value. * * @param value {Number} The new value of the max property * @param old {Number} The old value of the max property */ _applyMaximum : function(value, old) { if (this.getMinimum() > value) { this.setMinimum(value); } if (this.getValue() > value) { this.setValue(value); } else { this._updateButtons(); } }, // overridden _applyEnabled : function(value, old) { this.base(arguments, value, old); this._updateButtons(); }, /** * Check whether the value being applied is allowed. * * If you override this to change the allowed type, you will also * want to override {@link #_applyValue}, {@link #_applyMinimum}, * {@link #_applyMaximum}, {@link #_countUp}, {@link #_countDown}, and * {@link #_onTextChange} methods as those cater specifically to numeric * values. * * @param value {var} * The value being set * @return {Boolean} * <i>true</i> if the value is allowed; * <i>false> otherwise. */ _checkValue : function(value) { return typeof value === "number" && value >= this.getMinimum() && value <= this.getMaximum(); }, /** * Apply routine for the value property. * * It disables / enables the buttons and handles the wrap around. * * @param value {Number} The new value of the spinner * @param old {Number} The former value of the spinner */ _applyValue: function(value, old) { var textField = this.getChildControl("textfield"); this._updateButtons(); // save the last valid value of the spinner this.__lastValidValue = value; // write the value of the spinner to the textfield if (value !== null) { if (this.getNumberFormat()) { textField.setValue(this.getNumberFormat().format(value)); } else { textField.setValue(value + ""); } } else { textField.setValue(""); } }, /** * Apply routine for the editable property.<br/> * It sets the textfield of the spinner to not read only. * * @param value {Boolean} The new value of the editable property * @param old {Boolean} The former value of the editable property */ _applyEditable : function(value, old) { var textField = this.getChildControl("textfield"); if (textField) { textField.setReadOnly(!value); } }, /** * Apply routine for the wrap property.<br/> * Enables all buttons if the wrapping is enabled. * * @param value {Boolean} The new value of the wrap property * @param old {Boolean} The former value of the wrap property */ _applyWrap : function(value, old) { this._updateButtons(); }, /** * Apply routine for the numberFormat property.<br/> * When setting a number format, the display of the * value in the text-field will be changed immediately. * * @param value {Boolean} The new value of the numberFormat property * @param old {Boolean} The former value of the numberFormat property */ _applyNumberFormat : function(value, old) { var textField = this.getChildControl("textfield"); textField.setFilter(this._getFilterRegExp()); if (old) { old.removeListener("changeNumberFormat", this._onChangeNumberFormat, this); } var numberFormat = this.getNumberFormat(); if (numberFormat !== null) { numberFormat.addListener("changeNumberFormat", this._onChangeNumberFormat, this); } this._applyValue(this.__lastValidValue, undefined); }, /** * Returns the element, to which the content padding should be applied. * * @return {qx.ui.core.Widget} The content padding target. */ _getContentPaddingTarget : function() { return this.getChildControl("textfield"); }, /** * Checks the min and max values, disables / enables the * buttons and handles the wrap around. */ _updateButtons : function() { var upButton = this.getChildControl("upbutton"); var downButton = this.getChildControl("downbutton"); var value = this.getValue(); if (!this.getEnabled()) { // If Spinner is disabled -> disable buttons upButton.setEnabled(false); downButton.setEnabled(false); } else { if (this.getWrap()) { // If wraped -> always enable buttons upButton.setEnabled(true); downButton.setEnabled(true); } else { // check max value if (value !== null && value < this.getMaximum()) { upButton.setEnabled(true); } else { upButton.setEnabled(false); } // check min value if (value !== null && value > this.getMinimum()) { downButton.setEnabled(true); } else { downButton.setEnabled(false); } } } }, /* --------------------------------------------------------------------------- KEY EVENT-HANDLING --------------------------------------------------------------------------- */ /** * Callback for "keyDown" event.<br/> * Controls the interval mode ("single" or "page") * and the interval increase by detecting "Up"/"Down" * and "PageUp"/"PageDown" keys.<br/> * The corresponding button will be pressed. * * @param e {qx.event.type.KeySequence} keyDown event */ _onKeyDown: function(e) { switch(e.getKeyIdentifier()) { case "PageUp": // mark that the spinner is in page mode and process further this.__pageUpMode = true; case "Up": this.getChildControl("upbutton").press(); break; case "PageDown": // mark that the spinner is in page mode and process further this.__pageDownMode = true; case "Down": this.getChildControl("downbutton").press(); break; default: // Do not stop unused events return; } e.stopPropagation(); e.preventDefault(); }, /** * Callback for "keyUp" event.<br/> * Detecting "Up"/"Down" and "PageUp"/"PageDown" keys.<br/> * Releases the button and disabled the page mode, if necessary. * * @param e {qx.event.type.KeySequence} keyUp event */ _onKeyUp: function(e) { switch(e.getKeyIdentifier()) { case "PageUp": this.getChildControl("upbutton").release(); this.__pageUpMode = false; break; case "Up": this.getChildControl("upbutton").release(); break; case "PageDown": this.getChildControl("downbutton").release(); this.__pageDownMode = false; break; case "Down": this.getChildControl("downbutton").release(); break; } }, /* --------------------------------------------------------------------------- OTHER EVENT HANDLERS --------------------------------------------------------------------------- */ /** * Callback method for the "roll" event.<br/> * Increments or decrements the value of the spinner. * * @param e {qx.event.type.Roll} roll event */ _onRoll: function(e) { // only wheel if (e.getPointerType() != "wheel") { return; } var delta = e.getDelta().y; if (delta < 0) { this._countUp(); } else if (delta > 0) { this._countDown(); } e.stop(); }, /** * Callback method for the "change" event of the textfield. * * @param e {qx.event.type.Event} text change event or blur event */ _onTextChange : function(e) { var textField = this.getChildControl("textfield"); var value; // if a number format is set if (this.getNumberFormat()) { // try to parse the current number using the number format try { value = this.getNumberFormat().parse(textField.getValue()); } catch(ex) { // otherwise, process further } } if (value === undefined) { // try to parse the number as a float value = parseFloat(textField.getValue()); } // if the result is a number if (!isNaN(value)) { // Fix value if invalid if (value > this.getMaximum()) { value = this.getMaximum(); } else if (value < this.getMinimum()) { value = this.getMinimum(); } // If value is the same than before, call directly _applyValue() if (value === this.__lastValidValue) { this._applyValue(this.__lastValidValue); } else { this.setValue(value); } } else { // otherwise, reset the last valid value this._applyValue(this.__lastValidValue, undefined); } }, /** * Callback method for the locale Manager's "changeLocale" event. * * @param ev {qx.event.type.Event} locale change event */ _onChangeLocale : function(ev) { if (this.getNumberFormat() !== null) { this.setNumberFormat(this.getNumberFormat()); var textfield = this.getChildControl("textfield"); textfield.setFilter(this._getFilterRegExp()); textfield.setValue(this.getNumberFormat().format(this.getValue())); } }, /** * Callback method for the number format's "changeNumberFormat" event. * * @param ev {qx.event.type.Event} number format change event */ _onChangeNumberFormat : function(ev) { var textfield = this.getChildControl("textfield"); textfield.setFilter(this._getFilterRegExp()); textfield.setValue(this.getNumberFormat().format(this.getValue())); }, /* --------------------------------------------------------------------------- INTERVAL HANDLING --------------------------------------------------------------------------- */ /** * Checks if the spinner is in page mode and counts either the single * or page Step up. * */ _countUp: function() { if (this.__pageUpMode) { var newValue = this.getValue() + this.getPageStep(); } else { var newValue = this.getValue() + this.getSingleStep(); } // handle the case where wrapping is enabled if (this.getWrap()) { if (newValue > this.getMaximum()) { var diff = this.getMaximum() - newValue; newValue = this.getMinimum() - diff - 1; } } this.gotoValue(newValue); }, /** * Checks if the spinner is in page mode and counts either the single * or page Step down. * */ _countDown: function() { if (this.__pageDownMode) { var newValue = this.getValue() - this.getPageStep(); } else { var newValue = this.getValue() - this.getSingleStep(); } // handle the case where wrapping is enabled if (this.getWrap()) { if (newValue < this.getMinimum()) { var diff = this.getMinimum() + newValue; newValue = this.getMaximum() + diff + 1; } } this.gotoValue(newValue); }, /** * Normalizes the incoming value to be in the valid range and * applies it to the {@link #value} afterwards. * * @param value {Number} Any number * @return {Number} The normalized number */ gotoValue : function(value) { return this.setValue(Math.min(this.getMaximum(), Math.max(this.getMinimum(), value))); }, // overridden focus : function() { this.base(arguments); this.getChildControl("textfield").getFocusElement().focus(); } }, destruct : function() { var nf = this.getNumberFormat(); if (nf) { nf.removeListener("changeNumberFormat", this._onChangeNumberFormat, this); } if (qx.core.Environment.get("qx.dynlocale")) { qx.locale.Manager.getInstance().removeListener("changeLocale", this._onChangeLocale, this); } } });