UNPKG

@openui5/sap.m

Version:

OpenUI5 UI Library sap.m

1,417 lines (1,239 loc) 51.5 kB
/*! * UI development toolkit for HTML5 (OpenUI5) * (c) Copyright 2009-2022 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ // Provides control sap.m.StepInput. sap.ui.define([ "sap/ui/core/Icon", "./Input", "./InputBase", "./InputRenderer", "sap/ui/core/Control", "sap/ui/core/IconPool", 'sap/ui/core/LabelEnablement', 'sap/ui/Device', "sap/ui/core/library", "sap/ui/core/Renderer", "sap/m/library", "./StepInputRenderer", "sap/ui/events/KeyCodes", "sap/base/Log" ], function( Icon, Input, InputBase, InputRenderer, Control, IconPool, LabelEnablement, Device, coreLibrary, Renderer, library, StepInputRenderer, KeyCodes, Log ) { "use strict"; // shortcut for sap.m.InputType var InputType = library.InputType; // shortcut for sap.ui.core.TextAlign var TextAlign = coreLibrary.TextAlign; // shortcut for sap.ui.core.ValueState var ValueState = coreLibrary.ValueState; // shortcut for sap.m.StepInputValidationMode var StepInputValidationMode = library.StepInputValidationMode; // shortcut fro sap.m.StepModes var StepModeType = library.StepInputStepModeType; /** * Constructor for a new <code>StepInput</code>. * * @param {string} [sId] ID for the new control, generated automatically if no ID is given * @param {object} [mSettings] Initial settings for the new control * * @class * Allows the user to change the input values with predefined increments (steps). * * <h3>Overview</h3> * * The <code>StepInput</code> consists of an input field and buttons with icons to increase/decrease the value. * * The user can change the value of the control by pressing the increase/decrease buttons, * by typing a number directly, by using the keyboard up/down and page up/down, * or by using the mouse scroll wheel. Decimal values are supported. * * <h3>Usage</h3> * * The default step is 1 but the app developer can set a different one. * * On desktop, the control supports a larger step, when using the keyboard page up/down keys. * You can set a multiple of the step with the use of the <code>largerStep</code> property. * The default value is 2 (two times the set step). For example, when using the keyboard page up/down keys * the value increases/decreases with a double of the default step. If the set step is 2, the larger step is also 2 * and the current value is 1, using the page up key will increase the value to 5 (1 + 2*2). * * App developers can set a maximum and minimum value for the <code>StepInput</code>. * The increase/decrease button and the up/down keyboard navigation become disabled when * the value reaches the max/min or a new value is entered from the input which is greater/less than the max/min. * * <i>When to use</i> * <ul> * <li>To adjust amounts, quantities, or other values quickly.</li> * <li>To adjust values for a specific step.</li> * </ul> * * <i>When not to use</i> * <ul> * <li>To enter a static number (for example, postal code, phone number, or ID). In this case, * use the regular {@link sap.m.Input} instead.</li> * <li>To display a value that rarely needs to be adjusted and does not pertain to a particular step. * In this case, use the regular {@link sap.m.Input} instead.</li> * <li>To enter dates and times. In this case, use the {@link sap.m.DatePicker}, {@link sap.m.DateRangeSelection}, * {@link sap.m.TimePicker}, or {@link sap.m.DateTimePicker} instead.</li> * </ul> * * @extends sap.ui.core.Control * @implements sap.ui.core.IFormContent * * @author SAP SE * @version 1.60.39 * * @constructor * @public * @since 1.40 * @alias sap.m.StepInput * @see {@link fiori:https://experience.sap.com/fiori-design-web/step-input/ Step Input} * @ui5-metamodel This control/element also will be described in the UI5 (legacy) designtime metamodel */ var StepInput = Control.extend("sap.m.StepInput", /** @lends sap.m.StepInput.prototype */ { metadata: { interfaces: ["sap.ui.core.IFormContent"], library: "sap.m", designtime: "sap/m/designtime/StepInput.designtime", properties: { /** * Sets the minimum possible value of the defined range. */ min: {type: "float", group: "Data"}, /** * Sets the maximum possible value of the defined range. */ max: {type: "float", group: "Data"}, /** * Increases/decreases the value of the input. * <ul><b>Note:</b> <li>The value of the <code>step</code> property should not contain more digits after the decimal point than what is set to the <code>displayValuePrecision</code> property, as it may lead to an increase/decrease that is not visible for the user. For example, if the <code>value</code> is set to 1.22 and the <code>displayValuePrecision</code> is set to one digit after the decimal, the user will see 1.2. In this case, if the <code>value</code> of the <code>step</code> property is set to 1.005 and the user selects <code>increase</code>, the resulting value will increase to 1.2261 but the displayed value will remain as 1.2 as it will be rounded to the first digit after the decimal point.</li> <li>Depending on what is set for the <code>value</code> and the <code>displayValuePrecision</code> properties, it is possible the displayed value to be rounded to a higher number, for example to 3.0 when the actual value is 2.99.</li></ul> */ step: {type: "float", group: "Data", defaultValue: 1}, /** * Defines the calculation mode for the provided <code>step<code> and <code>largerStep</code>. * * If the user increases/decreases the value by <code>largerStep</code>, this calculation will consider * it as well. For example, if the current <code>value</code> is 3, <code>step</code> is 5, * <code>largerStep</code> is 5 and the user chooses PageUp, the calculation logic will consider * the value of 3x5=15 to decide what will be the next <code>value</code>. * * @since 1.54 */ stepMode: {type: "sap.m.StepInputStepModeType", group: "Data", defaultValue: StepModeType.AdditionAndSubtraction}, /** * Increases/decreases the value with a larger value than the set step only when using the PageUp/PageDown keys. * Default value is 2 times larger than the set step. */ largerStep: {type: "float", group: "Data", defaultValue: 2}, /** * Determines the value of the <code>StepInput</code> and can be set initially from the app developer. */ value: {type: "float", group: "Data", defaultValue: 0}, /** * Defines the name of the control for the purposes of form submission. * @since 1.44.15 */ name: { type: "string", group: "Misc", defaultValue: null }, /** * Defines a short hint intended to aid the user with data entry when the control has no value. * @since 1.44.15 */ placeholder: { type: "string", group: "Misc", defaultValue: null }, /** * Indicates that user input is required. This property is only needed for accessibility purposes when a single relationship between * the field and a label (see aggregation <code>labelFor</code> of <code>sap.m.Label</code>) cannot be established * (e.g. one label should label multiple fields). * @since 1.44.15 */ required : {type : "boolean", group : "Misc", defaultValue : false}, /** * Defines the width of the control. */ width: {type: "sap.ui.core.CSSSize", group: "Dimension"}, /** * Accepts the core enumeration ValueState.type that supports <code>None</code>, <code>Error</code>, <code>Warning</code> and <code>Success</code>. */ valueState: {type: "sap.ui.core.ValueState", group: "Data", defaultValue: ValueState.None}, /** * Defines the text that appears in the value state message pop-up. * @since 1.52 */ valueStateText: { type: "string", group: "Misc", defaultValue: null }, /** * Defines whether the control can be modified by the user or not. * <b>Note:</b> A user can tab to the non-editable control, highlight it, and copy the text from it. */ editable: {type: "boolean", group: "Behavior", defaultValue: true}, /** * Indicates whether the user can interact with the control or not. * <b>Note:</b> Disabled controls cannot be focused and they are out of the tab-chain. */ enabled: {type: "boolean", group: "Behavior", defaultValue: true}, /** * Determines the number of digits after the decimal point. * * The value should be between 0 (default) and 20. * In case the value is not valid it will be set to the default value. * @since 1.46 */ displayValuePrecision: {type: "int", group: "Data", defaultValue: 0}, /** * Determines the description text after the input field, for example units of measurement, currencies. * @since 1.54 */ description: {type : "string", group : "Misc", defaultValue : null}, /** * Determines the distribution of space between the input field * and the description text . Default value is 50% (leaving the other * 50% for the description). * * <b>Note:</b> This property takes effect only if the * <code>description</code> property is also set. * @since 1.54 */ fieldWidth: {type : "sap.ui.core.CSSSize", group : "Appearance", defaultValue : '50%'}, /** * Defines the horizontal alignment of the text that is displayed inside the input field. * @since 1.54 */ textAlign: {type: "sap.ui.core.TextAlign", group: "Appearance", defaultValue: TextAlign.End}, /** * Defines when the validation of the typed value will happen. By default this happens on focus out. * @since 1.54 */ validationMode: {type: "sap.m.StepInputValidationMode", group: "Misc", defaultValue: StepInputValidationMode.FocusOut} }, aggregations: { /** * Internal aggregation that contains the <code>Input</code>. */ _input: {type: "sap.ui.core.Control", multiple: false, visibility: "hidden"} }, associations: { /** * Association to controls / IDs that label this control (see WAI-ARIA attribute aria-labelledby). */ ariaLabelledBy: {type: "sap.ui.core.Control", multiple: true, singularName: "ariaLabelledBy"}, /** * Association to controls / IDs which describe this control (see WAI-ARIA attribute aria-describedby). */ ariaDescribedBy: {type: "sap.ui.core.Control", multiple: true, singularName: "ariaDescribedBy"} }, events: { /** * Is fired when one of the following happens: <br> * <ol> * <li>the text in the input has changed and the focus leaves the input field or the enter key * is pressed.</li> * <li>One of the decrement or increment buttons is pressed</li> * </ol> */ change: { parameters: { /** * The new <code>value</code> of the <code>control</code>. */ value: {type: "string"} } } } }, constructor : function (vId, mSettings) { Control.prototype.constructor.apply(this, arguments); if (this.getEditable()) { this._getOrCreateDecrementButton(); this._getOrCreateIncrementButton(); } if (typeof vId !== "string"){ mSettings = vId; } if (mSettings && mSettings.value === undefined){ this.setValue(this._getDefaultValue(undefined, mSettings.max, mSettings.min)); } } }); // get resource translation bundle; var oLibraryResourceBundle = sap.ui.getCore().getLibraryResourceBundle("sap.m"); StepInput.STEP_INPUT_INCREASE_BTN_TOOLTIP = oLibraryResourceBundle.getText("STEP_INPUT_INCREASE_BTN"); StepInput.STEP_INPUT_DECREASE_BTN_TOOLTIP = oLibraryResourceBundle.getText("STEP_INPUT_DECREASE_BTN"); StepInput.INITIAL_WAIT_TIMEOUT = 500; StepInput.ACCELLERATION = 0.8; StepInput.MIN_WAIT_TIMEOUT = 50; StepInput.INITIAL_SPEED = 120; //milliseconds StepInput._TOLERANCE = 10; // pixels /** * Map between StepInput properties and their corresponding aria attributes. */ var mNameToAria = { "min": "aria-valuemin", "max": "aria-valuemax", "value": "aria-valuenow" }; /** * Property names which when set are directly forwarded to inner input <code>setProperty</code> method * @type {Array.<string>} */ var aForwardableProps = ["enabled", "editable", "name", "placeholder", "required", "valueStateText", "description", "fieldWidth", "textAlign"]; /****************************************** NUMERIC INPUT CONTROL ****************************************************/ var NumericInputRenderer = Renderer.extend(InputRenderer); NumericInputRenderer.writeInnerAttributes = function(oRm, oControl) { var oStepInput = oControl.getParent(); // inside the Input this function also sets explicitly textAlign to "End" if the type // of the Input is Numeric (our case) // so we have to overwrite it by leaving only the text direction // and the textAlign will be controlled by textAlign property of the StepInput oRm.writeAttribute("type", oControl.getType().toLowerCase()); if (sap.ui.getCore().getConfiguration().getRTL()) { oRm.writeAttribute("dir", "ltr"); } oRm.writeAccessibilityState(oStepInput); }; //Accessibility behavior of the Input needs to be extended /** * Overwrites the accessibility state using the <code>getAccessibilityState</code> method of the <code>InputBaseRenderer</code>. * * @param {NumericInput} oNumericInput The numeric input instance * @returns {Array} mAccAttributes */ NumericInputRenderer.getAccessibilityState = function(oNumericInput) { var mAccAttributes = InputRenderer.getAccessibilityState(oNumericInput), oStepInput = oNumericInput.getParent(), fMin = oStepInput.getMin(), fMax = oStepInput.getMax(), fNow = oStepInput.getValue(), aAriaLabelledByRefs = oStepInput.getAriaLabelledBy(), // If we don't check this manually, we won't have the labels, which were referencing SI, // in aria-labelledby (which normally comes out of the box). This is because writeAccessibilityState // is called for NumericInput, while any labels will be for the parent StepInput. aReferencingLabels = LabelEnablement.getReferencingLabels(oStepInput), sLabeledBy = aAriaLabelledByRefs.concat(aReferencingLabels).join(" "), sDescribedBy = oStepInput.getAriaDescribedBy().join(" "); mAccAttributes["role"] = "spinbutton"; mAccAttributes["valuenow"] = fNow; if (typeof fMin === "number") { mAccAttributes["valuemin"] = fMin; } if (typeof fMax === "number") { mAccAttributes["valuemax"] = fMax; } if (sDescribedBy){ mAccAttributes["describedby"] = sDescribedBy; } if (sLabeledBy){ mAccAttributes["labelledby"] = sLabeledBy; } return mAccAttributes; }; var NumericInput = Input.extend("sap.m.internal.NumericInput", { constructor: function(sId, mSettings) { return Input.apply(this, arguments); }, renderer: NumericInputRenderer }); NumericInput.prototype.onBeforeRendering = function() { InputBase.prototype.onBeforeRendering.call(this); this._deregisterEvents(); }; /** * Initializes the control. */ StepInput.prototype.init = function () { this._iRealPrecision = 0; this._attachChange(); this._bPaste = false; //needed to indicate when a paste is made this._onmousewheel = this._onmousewheel.bind(this); }; /** * Called before the control is rendered. */ StepInput.prototype.onBeforeRendering = function () { var fMin = this.getMin(), fMax = this.getMax(), vValue = this.getValue(); this._iRealPrecision = this._getRealValuePrecision(); this._getInput().setValue(this._getFormatedValue(vValue)); this._disableButtons(vValue, fMax, fMin); }; StepInput.prototype.onAfterRendering = function () { var $domRef = this.$(); $domRef.unbind(Device.browser.firefox ? "DOMMouseScroll" : "mousewheel", this._onmousewheel); $domRef.bind(Device.browser.firefox ? "DOMMouseScroll" : "mousewheel", this._onmousewheel); }; StepInput.prototype.exit = function () { this.$().unbind(Device.browser.firefox ? "DOMMouseScroll" : "mousewheel", this._onmousewheel); }; StepInput.prototype.setProperty = function (sPropertyName, oValue, bSuppressInvalidate) { this._writeAccessibilityState(sPropertyName, oValue); Control.prototype.setProperty.call(this, sPropertyName, oValue, bSuppressInvalidate); if (aForwardableProps.indexOf(sPropertyName) > -1) { this._getInput().setProperty(sPropertyName, this.getProperty(sPropertyName), bSuppressInvalidate); } return this; }; /* * Sets the validation mode. * * @param {sap.m.StepInputValidationMode} sValidationMode The validation mode value * @returns {sap.m.StepInput} Reference to the control instance for chaining */ StepInput.prototype.setValidationMode = function (sValidationMode) { if (this.getValidationMode() !== sValidationMode) { switch (sValidationMode) { case sap.m.StepInputValidationMode.FocusOut: this._detachLiveChange(); break; case sap.m.StepInputValidationMode.LiveChange: this._attachLiveChange(); break; } this.setProperty("validationMode", sValidationMode); } return this; }; /* * Sets the min value. * * @param {float} min The minimum value * @returns {sap.m.StepInput} Reference to the control instance for chaining */ StepInput.prototype.setMin = function (min) { var oResult, vValue = this.getValue(), bSuppressInvalidate = (vValue !== 0 && !vValue); if (min === undefined) { return this.setProperty("min", min, true); } if (!this._validateOptionalNumberProperty("min", min)) { return this; } oResult = this.setProperty("min", min, bSuppressInvalidate); this._disableButtons(vValue, this.getMax(), min); return oResult; }; /* * Sets the max value. * * @param {float} max The max value * @returns {sap.m.StepInput} Reference to the control instance for chaining */ StepInput.prototype.setMax = function (max) { var oResult, vValue = this.getValue(), bSuppressInvalidate = (vValue !== 0 && !vValue); if (max === undefined) { return this.setProperty("max", max, true); } if (!this._validateOptionalNumberProperty("max", max)) { return this; } oResult = this.setProperty("max", max, bSuppressInvalidate); this._disableButtons(this.getValue(), max, this.getMin()); return oResult; }; /** * Verifies if the given value is of a numeric type. * * @param {string} name Property name * @param {variant} value Property value * @returns {boolean} The result of the check. Numbers of type "string" are also valid. * @private */ StepInput.prototype._validateOptionalNumberProperty = function (name, value) { if (this._isNumericLike(value)) { return true; } Log.error("The value of property '" + name + "' must be a number"); return false; }; /* * Sets the <code>displayValuePrecision</code>. * * @param {number} number The value precision * @returns {sap.m.StepInput} Reference to the control instance for chaining */ StepInput.prototype.setDisplayValuePrecision = function (number) { var vValuePrecision, vValue = this.getValue(), bSuppressInvalidate = (vValue !== 0 && !vValue); if (isValidPrecisionValue(number)) { vValuePrecision = parseInt(number, 10); } else { vValuePrecision = 0; Log.warning(this + ": ValuePrecision (" + number + ") is not correct. It should be a number between 0 and 20! Setting the default ValuePrecision:0."); } return this.setProperty("displayValuePrecision", vValuePrecision, bSuppressInvalidate); }; /** * Sets a new tooltip for this object. * @link sap.ui.core.Element#setTooltip * @param {string|sap.ui.core.TooltipBase} sTooltip The value of tooltip */ StepInput.prototype.setTooltip = function (sTooltip) { //We need to call the special logic implemented in InputBase.prototype.setTooltip this._getInput().setTooltip(sTooltip); }; /** * Retrieves the <code>incrementButton</code>. * @returns {sap.ui.core.Icon} the icon that serves as (lightweight) button * @private */ StepInput.prototype._getIncrementButton = function () { var endIcons = this._getInput().getAggregation("_endIcon"); return endIcons ? endIcons[0] : null; //value state icon comes from sap.m.Input constructor and is at index 0 }; /** * Retrieves the <code>decrementButton</code>. * @returns {sap.ui.core.Icon} the icon that serves as (lightweight) button * @private */ StepInput.prototype._getDecrementButton = function () { var beginIcons = this._getInput().getAggregation("_beginIcon"); return beginIcons ? beginIcons[0] : null; }; /** * Creates the <code>incrementButton</code>. * @returns {sap.ui.core.Icon} the icon that serves as (lightweight) button * @private */ StepInput.prototype._createIncrementButton = function () { var that = this; var oIcon = this._getInput().addEndIcon({ src: IconPool.getIconURI("add"), id: this.getId() + "-incrementBtn", noTabStop: true, press: this._handleButtonPress.bind(this, true), tooltip: StepInput.STEP_INPUT_INCREASE_BTN_TOOLTIP }); oIcon.getEnabled = function() { return this.getEnabled() && (this.getValue() < this.getMax()); }.bind(this); oIcon.addEventDelegate({ onAfterRendering: function () { // Set it to -1 so it still won't be part of the tabchain but can be document.activeElement // see _change method, _isButtonFocused call oIcon.$().attr("tabindex", "-1"); that._attachEvents(oIcon, true); } }); return oIcon; }; /** * Creates the <code>decrementButton</code>. * @returns {sap.ui.core.Icon} the icon that serves as (lightweight) button * @private */ StepInput.prototype._createDecrementButton = function() { var that = this; var oIcon = this._getInput().addBeginIcon({ src: IconPool.getIconURI("less"), id: this.getId() + "-decrementBtn", noTabStop: true, press: this._handleButtonPress.bind(this, false), tooltip: StepInput.STEP_INPUT_DECREASE_BTN_TOOLTIP }); oIcon.getEnabled = function() { return this.getEnabled() && (this.getValue() > this.getMin()); }.bind(this); oIcon.addEventDelegate({ onAfterRendering: function () { // Set it to -1 so it still won't be part of the tabchain but can be document.activeElement // see _change method, _isButtonFocused call oIcon.$().attr("tabindex", "-1"); that._attachEvents(oIcon, false); } }); return oIcon; }; /** * Lazily retrieves the <code>Input</code>. * * @returns {sap.m.Input} The underlying input control * @private */ StepInput.prototype._getInput = function () { if (!this.getAggregation("_input")) { var oNumericInput = new NumericInput({ id: this.getId() + "-input", textAlign: this.getTextAlign(), type: InputType.Number, editable: this.getEditable(), enabled: this.getEnabled(), description: this.getDescription(), fieldWidth: this.getFieldWidth(), liveChange: this._inputLiveChangeHandler }); this.setAggregation("_input", oNumericInput); } return this.getAggregation("_input"); }; /** * Handles the button press. * * @param {boolean} isPlusButton Indicates the pressed button either the increment or decrement one * @returns {sap.m.StepInput} Reference to the control instance for chaining * @private */ StepInput.prototype._handleButtonPress = function (isPlusButton) { var oNewValue = this._calculateNewValue(1, isPlusButton), fMin = this.getMin(), fMax = this.getMax(); this._btndown = undefined; this._disableButtons(oNewValue.displayValue, fMax, fMin); this.setValue(oNewValue.value); if (this._sOldValue !== this.getValue()) { this._verifyValue(); this.fireChange({value: this.getValue()}); } // Return the focus on the main element this.$().focus(); return this; }; /** * Handles whether the increment and decrement buttons should be enabled/disabled based on different situations. * * @param {number} value Indicates the value in the input * @param {number} max Indicates the max * @param {number} min Indicates the min * @returns {sap.m.StepInput} Reference to the control instance for chaining */ StepInput.prototype._disableButtons = function (value, max, min) { if (!this._isNumericLike(value)) { return; } var bMaxIsNumber = this._isNumericLike(max), bMinIsNumber = this._isNumericLike(min), oIncrementButton = this._getIncrementButton(), oDecrementButton = this._getDecrementButton(), bEnabled = this.getEnabled(), bReachedMin = bMinIsNumber && min >= value, //min is set and it's bigger or equal to the value bReachedMax = bMaxIsNumber && max <= value, //max is set and it's lower or equal to the value bShouldDisableDecrement = bEnabled ? bReachedMin : true, //if enabled - set the value according to the min value, if not - set disable flag to true bShouldDisableIncrement = bEnabled ? bReachedMax : true; //if enabled - set the value according to the max value, if not - set disable flag to true; oDecrementButton && oDecrementButton.toggleStyleClass("sapMStepInputIconDisabled", bShouldDisableDecrement); oIncrementButton && oIncrementButton.toggleStyleClass("sapMStepInputIconDisabled", bShouldDisableIncrement); return this; }; /** * Sets the <code>valueState</code> if there is a value that is not within a given limit. */ StepInput.prototype._verifyValue = function () { var min = this.getMin(), max = this.getMax(), value = parseFloat(this._getInput().getValue()); if (!this._isNumericLike(value)){ return; } if ((this._isNumericLike(max) && value > max) || (this._isNumericLike(min) && value < min) || (this._areFoldChangeRequirementsFulfilled() && (value % this.getStep() !== 0))) { this.setValueState(ValueState.Error); } else { this.setValueState(ValueState.None); } }; /* * Sets the <code>value</code> by doing some rendering optimizations in case the first rendering was completed. * Otherwise the value is set in onBeforeRendering, where we have all needed parameters for obtaining correct value. * @param {object} oValue The value to be set * */ StepInput.prototype.setValue = function (oValue) { var oResult; if (oValue == undefined) { oValue = 0; } this._sOldValue = this.getValue(); if (!this._validateOptionalNumberProperty("value", oValue)) { return this; } this._getInput().setValue(this._getFormatedValue(oValue)); this._disableButtons(oValue, this.getMax(), this.getMin()); oResult = this.setProperty("value", parseFloat(oValue), true); this._iRealPrecision = this._getRealValuePrecision(); return oResult; }; /** * Formats the <code>vValue</code> accordingly to the <code>displayValuePrecision</code> property. * if vValue is undefined or null, the property <code>value</code> will be used. * * @returns formated value as a String * @private */ StepInput.prototype._getFormatedValue = function (vValue) { var iPrecision = this.getDisplayValuePrecision(), iValueLength, sDigits; if (vValue == undefined) { vValue = this.getValue(); } if (iPrecision <= 0) { // return value without any decimals return parseFloat(vValue).toFixed(0); } sDigits = vValue.toString().split("."); if (sDigits.length === 2) { iValueLength = sDigits[1].length; if (iValueLength > iPrecision) { return parseFloat(vValue).toFixed(iPrecision); } return sDigits[0] + "." + this._padZeroesRight(sDigits[1], iPrecision); } else { return vValue.toString() + "." + this._padZeroesRight("0", iPrecision); } }; /** * Adds zeros to the value according to the given iPrecision. * * @param {string} value The value to which the zeros will be added * @param {int} precision The given precision * @returns {string} value padded with zeroes * @private */ StepInput.prototype._padZeroesRight = function (value, precision) { var sResult = "", iValueLength = value.length; // add zeros for (var i = iValueLength; i < precision; i++) { sResult = sResult + "0"; } sResult = value + sResult; return sResult; }; /** * Handles the <code>onsappageup</code>. * * Increases the value with the larger step. * * @param {jQuery.Event} oEvent Event object */ StepInput.prototype.onsappageup = function (oEvent) { this._applyValue(this._calculateNewValue(this.getLargerStep(), true).displayValue); this._verifyValue(); // prevent document scrolling when page up key is pressed oEvent.preventDefault(); }; /** * Handles the <code>onsappagedown</code> - PageDown key decreases the value with the larger step. * * @param {jQuery.Event} oEvent Event object */ StepInput.prototype.onsappagedown = function (oEvent) { this._applyValue(this._calculateNewValue(this.getLargerStep(), false).displayValue); this._verifyValue(); // prevent document scrolling when page down key is pressed oEvent.preventDefault(); }; /** * Handles the Shift + PageUp key combination and sets the value to maximum. * * @param {jQuery.Event} oEvent Event object */ StepInput.prototype.onsappageupmodifiers = function (oEvent) { if (this._isNumericLike(this.getMax()) && !(oEvent.ctrlKey || oEvent.metaKey || oEvent.altKey) && oEvent.shiftKey) { this._applyValue(this.getMax()); } }; /** * Handles the Shift + PageDown key combination and sets the value to minimum. * * @param {jQuery.Event} oEvent Event object */ StepInput.prototype.onsappagedownmodifiers = function (oEvent) { if (this._isNumericLike(this.getMin()) && !(oEvent.ctrlKey || oEvent.metaKey || oEvent.altKey) && oEvent.shiftKey) { this._applyValue(this.getMin()); } }; /** * Handles the <code>onsapup</code> and increases the value with the default step (1). * * @param {jQuery.Event} oEvent Event object */ StepInput.prototype.onsapup = function (oEvent) { oEvent.preventDefault(); //prevents the value to increase by one (Chrome and Firefox default behavior) this._applyValue(this._calculateNewValue(1, true).displayValue); this._verifyValue(); }; /** * Handles the <code>onsapdown</code> and decreases the value with the default step (1). * * @param {jQuery.Event} oEvent Event object */ StepInput.prototype.onsapdown = function (oEvent) { oEvent.preventDefault(); //prevents the value to decrease by one (Chrome and Firefox default behavior) this._applyValue(this._calculateNewValue(1, false).displayValue); this._verifyValue(); }; StepInput.prototype._onmousewheel = function (oEvent) { var bIsFocused = this.getDomRef().contains(document.activeElement); if (bIsFocused) { oEvent.preventDefault(); var oOriginalEvent = oEvent.originalEvent, bDirectionPositive = oOriginalEvent.detail ? (-oOriginalEvent.detail > 0) : (oOriginalEvent.wheelDelta > 0); this._applyValue(this._calculateNewValue(1, bDirectionPositive).displayValue); this._verifyValue(); } }; /** * Handles the Ctrl + Shift + Up/Down and Shift + Up/Down key combinations and sets the value to maximum/minimum * or increases/decreases the value with the larger step. * * @param {jQuery.Event} oEvent Event object */ StepInput.prototype.onkeydown = function (oEvent) { var bVerifyValue = false; this._bPaste = (oEvent.ctrlKey || oEvent.metaKey) && (oEvent.which === KeyCodes.V); if (oEvent.which === KeyCodes.ARROW_UP && !oEvent.altKey && oEvent.shiftKey && (oEvent.ctrlKey || oEvent.metaKey)) { //ctrl+shift+up this._applyValue(this.getMax()); bVerifyValue = true; } if (oEvent.which === KeyCodes.ARROW_DOWN && !oEvent.altKey && oEvent.shiftKey && (oEvent.ctrlKey || oEvent.metaKey)) { //ctrl+shift+down this._applyValue(this.getMin()); bVerifyValue = true; } if (oEvent.which === KeyCodes.ARROW_UP && !(oEvent.ctrlKey || oEvent.metaKey || oEvent.altKey) && oEvent.shiftKey) { //shift+up oEvent.preventDefault(); //preventing to be added both the minimum step (1) and the larger step this._applyValue(this._calculateNewValue(this.getLargerStep(), true).displayValue); bVerifyValue = true; } if (oEvent.which === KeyCodes.ARROW_DOWN && !(oEvent.ctrlKey || oEvent.metaKey || oEvent.altKey) && oEvent.shiftKey) { //shift+down oEvent.preventDefault(); //preventing to be subtracted both the minimum step (1) and the larger step this._applyValue(this._calculateNewValue(this.getLargerStep(), false).displayValue); bVerifyValue = true; } if (oEvent.which === KeyCodes.ARROW_UP && (oEvent.ctrlKey || oEvent.metaKey)) { // ctrl + up oEvent.preventDefault(); this._applyValue(this._calculateNewValue(1, true).displayValue); bVerifyValue = true; } if (oEvent.which === KeyCodes.ARROW_DOWN && (oEvent.ctrlKey || oEvent.metaKey)) { // ctrl + down oEvent.preventDefault(); this._applyValue(this._calculateNewValue(1, false).displayValue); bVerifyValue = true; } if (oEvent.which === KeyCodes.ARROW_UP && oEvent.altKey) { // alt + up oEvent.preventDefault(); this._applyValue(this._calculateNewValue(1, true).displayValue); bVerifyValue = true; } if (oEvent.which === KeyCodes.ARROW_DOWN && oEvent.altKey) { // alt + down oEvent.preventDefault(); this._applyValue(this._calculateNewValue(1, false).displayValue); bVerifyValue = true; } if (bVerifyValue) { this._verifyValue(); } }; /** * Handles the Esc key and reverts the value in the input field to the previous one. * * @param {jQuery.Event} oEvent Event object */ StepInput.prototype.onsapescape = function (oEvent) { this._getInput().onsapescape(oEvent); }; /** * Attaches the <code>liveChange</code> handler for the input. * @private */ StepInput.prototype._attachLiveChange = function () { this._getInput().attachLiveChange(this._liveChange, this); }; StepInput.prototype._detachLiveChange = function () { this._getInput().detachLiveChange(this._liveChange, this); }; StepInput.prototype._attachChange = function () { this._getInput().attachChange(this._change, this); }; /** * Attaches the <code>liveChange</code> handler for the input. * @private */ StepInput.prototype._liveChange = function () { this._verifyValue(); this._disableButtons(this._getInput().getValue(), this.getMax(), this.getMin()); }; /** * Handles the <code>change</code> event for the input. * @param {Object} oEvent The fired event * @private */ StepInput.prototype._change = function (oEvent) { this._sOldValue = this.getValue(); this.setValue(this._getDefaultValue(this._getInput().getValue(), this.getMax(), this.getMin())); if (this._sOldValue !== this.getValue() && !this._isButtonFocused()) { this._verifyValue(); this.fireChange({value: this.getValue()}); } }; /** * Applies change on the visible value but doesn't force the other checks that come with <code>this.setValue</code>. * Usable for Keyboard Handling when resetting initial value with ESC key. * * @param {float} fNewValue The new value to be applied * @private */ StepInput.prototype._applyValue = function (fNewValue) { if (!this.getEditable() || !this.getEnabled()) { return; } // the property Value is not changing because this is a live change where the final value is not yet confirmed by the user this.getAggregation("_input")._$input.val(this._getFormatedValue(fNewValue)); }; /** * Makes calculations regarding the operation and the number type. * * @param {number} stepMultiplier Holds the step multiplier * @param {boolean} isIncreasing Holds the operation(or direction) whether addition(increasing) or subtraction(decreasing) * @returns {{value, displayValue}} The result of the calculation where: * <ul> * <li>value is the result of the computation where the real stepInput <value> is used</li> * <li>displayValue is the result of the computation where the DOM value (also sap.m.Input.getValue()) is used</li> * </ul> * @private */ StepInput.prototype._calculateNewValue = function (stepMultiplier, isIncreasing) { var fStep = this.getStep(), fMax = this.getMax(), fMin = this.getMin(), fRealValue = this.getValue(), fInputValue = parseFloat(this._getDefaultValue(this._getInput().getValue(), fMax, fMin)), iSign = isIncreasing ? 1 : -1, fMultipliedStep = Math.abs(fStep) * Math.abs(stepMultiplier), fResult = fInputValue + iSign * fMultipliedStep, fDisplayValueResult, fValueResult, iDisplayValuePrecision = this.getDisplayValuePrecision(); if (iDisplayValuePrecision > 0) { fDisplayValueResult = this._sumValues(fInputValue, fMultipliedStep, iSign, iDisplayValuePrecision); } else { fDisplayValueResult = fResult; } if (this._areFoldChangeRequirementsFulfilled()) { fResult = fDisplayValueResult = fValueResult = this._calculateClosestFoldValue(fInputValue, fMultipliedStep, iSign); } else { fValueResult = this._sumValues(fRealValue, fMultipliedStep, iSign, this._iRealPrecision); } // if there is a maxValue set, check if the calculated value is bigger // and if so set the calculated value to the max one if (this._isNumericLike(fMax) && fResult >= fMax){ fValueResult = fMax; fDisplayValueResult = fMax; } // if there is a minValue set, check if the calculated value is less // and if so set the calculated value to the min one if (this._isNumericLike(fMin) && fResult <= fMin){ fValueResult = fMin; fDisplayValueResult = fMin; } return {value: fValueResult, displayValue: fDisplayValueResult}; }; /** * Returns the bigger value precision by comparing * the precision of the value and the precision of the step. * * @returns {int} number of digits after the dot */ StepInput.prototype._getRealValuePrecision = function () { var sDigitsValue = this.getValue().toString().split("."), sDigitsStep = this.getStep().toString().split("."), iDigitsValueL, iDigitsStepL; iDigitsValueL = (!sDigitsValue[1]) ? 0 : sDigitsValue[1].length; iDigitsStepL = (!sDigitsStep[1]) ? 0 : sDigitsStep[1].length; return (iDigitsValueL > iDigitsStepL) ? iDigitsValueL : iDigitsStepL; }; /* * Handles the value state of the control. * * @param {string} valueState The given value state * @returns {sap.m.StepInput} Reference to the control instance for chaining */ StepInput.prototype.setValueState = function (valueState) { var bError = false, bWarning = false; switch (valueState) { case ValueState.Error: bError = true; break; case ValueState.Warning: bWarning = true; break; case ValueState.Success: case ValueState.None: break; default: return this; } this._getInput().setValueState(valueState); setTimeout(function () { this.$().toggleClass("sapMStepInputError", bError).toggleClass("sapMStepInputWarning", bWarning); }.bind(this), 0); this.setProperty("valueState", valueState, true); return this; }; /* * Sets the editable property. * * @param {boolean} editable - Indicates if the value is editable * @returns {sap.m.StepInput} Reference to the control instance for chaining */ StepInput.prototype.setEditable = function (editable) { var oResult = StepInput.prototype.setProperty.call(this, "editable", editable); this._getOrCreateDecrementButton().setVisible(editable); this._getOrCreateIncrementButton().setVisible(editable); return oResult; }; /** * Checks whether there is an existing instance of a decrement button or it has to be created. * * @returns {sap.ui.core.Icon} the icon that serves as (lightweight) button * @private */ StepInput.prototype._getOrCreateDecrementButton = function(){ return this._getDecrementButton() || this._createDecrementButton(); }; /** * Checks whether there is an existing instance of an increment button or it has to be created. * * @returns {sap.ui.core.Icon} the icon that serves as (lightweight) button * @private */ StepInput.prototype._getOrCreateIncrementButton = function(){ return this._getIncrementButton() || this._createIncrementButton(); }; /** * <code>liveChange</code> handler. * @param {sap.ui.base.Event} oEvent Event object * @private */ StepInput.prototype._inputLiveChangeHandler = function (oEvent) { var iValue = this.getParent()._restrictCharsWhenDecimal(oEvent); this.setProperty("value", iValue ? iValue : oEvent.getParameter("newValue"), true); }; /** * Handles the value after the decimal point when user types or pastes. * * @param {sap.ui.base.Event} oEvent Event object * @private */ StepInput.prototype._restrictCharsWhenDecimal = function (oEvent) { var iDecimalMark = oEvent.getParameter("value").indexOf("."), iCharsSet = this.getDisplayValuePrecision(), iValue; if (iDecimalMark > 0 && iCharsSet > 0) { //only for decimals var sEventValue = oEvent.getParameter("value"), sEventValueAfterTheDecimal = sEventValue.split('.')[1], iCharsAfterTheDecimalSign = sEventValueAfterTheDecimal ? sEventValueAfterTheDecimal.length : 0, sEventSourceValue = oEvent.getSource().getProperty("value"), sCharsBeforeTheEventDecimalValue = sEventValue.split('.')[0], sCharsAfterTheEventDecimalValue = sEventSourceValue.substring(sEventSourceValue.indexOf('.') + 1, sEventSourceValue.length); //scenario 1 - user typing after the decimal mark: if (!this._bPaste) { //if the characters after the decimal are more than the displayValuePrecision -> keep the current value after the decimal if (iCharsAfterTheDecimalSign > iCharsSet) { iValue = sCharsBeforeTheEventDecimalValue + "." + sCharsAfterTheEventDecimalValue; this._showWrongValueVisualEffect(); } //scenario 2 - paste - cut the chars with length, bigger than displayValuePrecision } else { if (sEventValue.indexOf(".")){ iValue = sEventValue.split('.')[0] + "." + sEventValueAfterTheDecimal.substring(0, iCharsSet); } this._bPaste = false; } } this._getInput().updateDomValue(iValue); return iValue; }; /** * Triggers the value state "Error" for 1s, and resets the state to the previous one. * * @private */ StepInput.prototype._showWrongValueVisualEffect = function() { var sOldValueState = this.getValueState(), oInput = this._getInput(); if (sOldValueState === ValueState.Error) { return; } oInput.setValueState(ValueState.Error); setTimeout(oInput["setValueState"].bind(oInput, sOldValueState), 1000); }; /** * Returns a default value depending of the given value, min and max properties. * * @param {number} value Indicates the value * @param {number} max Indicates the max * @param {number} min Indicates the min * @returns {number} The default value * @private */ StepInput.prototype._getDefaultValue = function (value, max, min) { if (value !== "" && value !== undefined) { return this._getInput().getValue(); } if (this._isNumericLike(min) && min > 0) { return min; } else if (this._isNumericLike(max) && max < 0) { return max; } else { return 0; } }; /** * Checks whether the value is a number. * * @param {variant} val - Holds the value * @returns {boolean} Whether the value is a number * @private */ StepInput.prototype._isNumericLike = function (val) { return !isNaN(val) && val !== null && val !== ""; }; /** * Determines if a given value is an integer * @param {string|number} val the value to check * @returns {boolean} true if the given value is integer, false otherwise * @private */ StepInput.prototype._isInteger = function(val) { return val === parseInt(val, 10); }; StepInput.prototype._writeAccessibilityState = function (sProp, sValue) { var $input = this._getInput().getDomRef("inner"); if (!$input){ return; } if (sProp && mNameToAria[sProp]) { $input.setAttribute(mNameToAria[sProp], sValue); } }; StepInput.prototype._isButtonFocused = function () { return document.activeElement === this._getIncrementButton().getDomRef() || document.activeElement === this._getDecrementButton().getDomRef(); }; /* * Sums 2 real values by converting them to integers before summing them and restoring the result back to a real value. * This avoids rounding issues. * @param {number} fValue1 the first value to sum * @param {number} fValue2 the second value to sum * @param {int} iSign +1 if the values should be summed, or -1 if the second should be extracted from the first * @param {int} iPrecision the precision the computation should be. Best would be to equal the precision of the * bigger of fValue1 and fValue2. * @returns {number} * @private */ StepInput.prototype._sumValues = function(fValue1, fValue2, iSign, iPrecision) { var iPrecisionMultiplier = Math.pow(10, iPrecision), //For most of the cases fValue1 * iPrecisionMultiplier will produce the integer (ex. 0.11 * 100 = 11), //so toFixed won't change anything. //For some cases fValue1 * iPrecisionMultiplier will produce a floating point number(ex. 0.29 * 100 = 28.999999999999996), //but we still can call toFixed as this floating point number is always as closest as //possible(i.e. no rounding errors could appear) to the real integer we expect. iValue1 = parseInt((fValue1 * iPrecisionMultiplier).toFixed(10), 10), iValue2 = parseInt((fValue2 * iPrecisionMultiplier).toFixed(10), 10); return (iValue1 + (iSign * iValue2)) / iPrecisionMultiplier; }; /** * Determine if the stepMode of type ${@link sap.m.StepInputStepModeType.Multiple} can be applied * depending on the step, larger step, and display value precision. * @returns {boolean} * @private */ StepInput.prototype._areFoldChangeRequirementsFulfilled = function () { return this.getStepMode() === StepModeType.Multiple && this.getDisplayValuePrecision() === 0 && this._isInteger(this.getStep()) && this._isInteger(this.getLargerStep()); }; /** * Calculates next/previous value that is fold by the the provided step * @param {number} fValue the base value * @param {number} step the step to increase the value to * @param {number} iSign direction, where if 1 -> increase, -1-> decrease. * @returns {number} the next/previous value. * @private */ StepInput.prototype._calculateClosestFoldValue = function(fValue, step, iSign) { var fResult = Math.floor(fValue), iLoopCount = step; do { fResult += iSign; iLoopCount--; } while (fResult % step !== 0 && iLoopCount); if (fResult % step !== 0) { Log.error("Wrong next/previous value " + fResult + " for " + fValue + ", step: " + step + " and sign: " + iSign, this); } return fResult; }; /* * displayValuePrecision should be a number between 0 and 20 * @returns {boolean} */ function isValidPrecisionValue(value) { return (typeof (value) === 'number') && !isNaN(value) && value >= 0 && value <= 20; } // speed spin of values functionality /* * Calculates the time which should be waited until _spinValues function is called. */ StepInput.prototype._calcWaitTimeout = function() { this._speed *= StepInput.ACCELLERATION; this._waitTimeout = ((this._waitTimeout - this._speed) < StepInput.MIN_WAIT_TIMEOUT ? StepInput.MIN_WAIT_TIMEOUT : (this._waitTimeout - this._speed)); return this._waitTimeout; }; /* * Called when the increment or decrement button is pressed and held to set new value. * @param {boolean} bIncrementButton - is this the increment button or not so the values should be spin accordingly up or down */ StepInput.prototype._spinValues = function(bIncrementButton) { var that = this; if (this._btndown) { this._spinTimeoutId = setTimeout(function () { if (that._btndown) { ////////////////// just the code for setting a value, not firing an event var oNewValue = that._calculateNewValue(1, bIncrementButton); that.setValue(oNewValue.value); that._verifyValue(); if (!that._getIncrementButton().getEnabled() || !that._getDecrementButton().getEnabled()) { _resetSpinValues.call(that); // fire change event when the buttons get disabled since then no mouseup event is fired that.fireChange({value: that.getValue()}); } that._spinValues(bIncrementButton); } }, that._calcWaitTimeout()); } }; /* * Attaches events to increment and decrement buttons. * @param {object} oBtn - button to which events will be attached * @param {boolean} bIncrementButton - is this the increment button or not so the values should be spin accordingly up or down */ StepInput.prototype._attachEvents = function (oBtn, bIncrementButton) { var that = this; // Desktop events var oEvents = { onmousedown: function (oEvent) { // check if the left mouse button is pre