@openui5/sap.m
Version:
OpenUI5 UI Library sap.m
1,391 lines (1,228 loc) • 59.2 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2026 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/Control",
"sap/ui/core/IconPool",
"sap/ui/core/Lib",
"sap/ui/core/message/MessageMixin",
"sap/ui/core/format/NumberFormat",
"sap/ui/model/ValidateException",
"sap/ui/Device",
"sap/ui/core/library",
"sap/m/library",
"./NumericInput",
"./StepInputRenderer",
"sap/ui/events/KeyCodes",
"sap/base/Log"
],
function(
Control,
IconPool,
Library,
MessageMixin,
NumberFormat,
ValidateException,
Device,
coreLibrary,
library,
NumericInput,
StepInputRenderer,
KeyCodes,
Log
) {
"use strict";
// 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 for 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>
*
* <b>Note:</b> The control uses a JavaScript number to keep its value, which
* has a certain precision limit.
*
* In general, exponential notation is used:
* <ul>
* <li>if there are more than 21 digits before the decimal point.</li>
* <li>if number starts with "0." followed by more than five zeros.</li>
* </ul>
*
* Exponential notation is not supported by the control and using it may lead to
* unpredictable behavior.
*
* Also, the JavaScript number persists its precision up to 16 digits. If the user enters
* a number with a greater precision, the value will be rounded.
*
* This restriction comes from JavaScript itself and it cannot be worked around in a
* feasible way.
*
* <b>Note:</b> Formatting of decimal numbers is browser dependent, regardless of
* framework number formatting.
*
* @extends sap.ui.core.Control
* @implements sap.ui.core.IFormContent
*
* @author SAP SE
* @version 1.146.0
*
* @constructor
* @public
* @since 1.40
* @alias sap.m.StepInput
* @see {@link fiori:https://experience.sap.com/fiori-design-web/step-input/ Step Input}
*/
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 is managed internally only when validation is triggered by user interaction.
*/
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"}
}
}
},
dnd: { draggable: false, droppable: true }
},
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));
}
},
renderer: StepInputRenderer
});
// get resource translation bundle;
var oLibraryResourceBundle = Library.getResourceBundleFor("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
/**
* 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"];
MessageMixin.call(StepInput.prototype);
/**
* Initializes the control.
*/
StepInput.prototype.init = function () {
this._iRealPrecision = 0;
this._attachChange();
this._bPaste = false; //needed to indicate when a paste is made
this._bNeedsVerification = false; // the control needs verification of the value state
this._bValueStatePreset = true; //If there is a pre-defined value it will be set
this._onmousewheel = this._onmousewheel.bind(this);
window.addEventListener("contextmenu", function(e) {
if (this._btndown === false && e.target.className.indexOf("sapMInputBaseIconContainer") !== -1) {
e.preventDefault();
}
}.bind(this));
};
/**
* Called before the control is rendered.
*/
StepInput.prototype.onBeforeRendering = function () {
var fMin = this._getMin(),
fMax = this._getMax(),
vValue = this._sOriginalValue || this.getValue(),
bEditable = this.getEditable();
this._iRealPrecision = this._getRealValuePrecision();
!this.bLiveChange && this._getInput().setValue(this._getFormattedValue(vValue));
this._getInput().setValueState(this.getValueState());
this._getOrCreateDecrementButton().setVisible(bEditable);
this._getOrCreateIncrementButton().setVisible(bEditable);
this._getInput().setTooltip(this.getTooltip());
this._disableButtons(vValue, fMax, fMin);
this.$().off(Device.browser.firefox ? "DOMMouseScroll" : "mousewheel", this._onmousewheel);
if (this._bNeedsVerification && !this._bValueStatePreset) {
this._verifyValue();
this._bNeedsVerification = false;
}
this.bLiveChange = false;
};
StepInput.prototype.onAfterRendering = function () {
this.$().on(Device.browser.firefox ? "DOMMouseScroll" : "mousewheel", this._onmousewheel);
};
StepInput.prototype.exit = function () {
this.$().off(Device.browser.firefox ? "DOMMouseScroll" : "mousewheel", this._onmousewheel);
this._sOriginalValue = null;
};
StepInput.prototype.setProperty = function (sPropertyName, oValue, bSuppressInvalidate) {
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 {this} Reference to the control instance for chaining
*/
StepInput.prototype.setValidationMode = function (sValidationMode) {
if (this.getValidationMode() !== sValidationMode) {
switch (sValidationMode) {
case StepInputValidationMode.FocusOut:
this._detachLiveChange();
break;
case StepInputValidationMode.LiveChange:
this._attachLiveChange();
break;
}
this.setProperty("validationMode", sValidationMode);
}
return this;
};
/**
* Sets the min value.
*
* @param {float} min The minimum value
* @returns {this} Reference to the control instance for chaining
*/
StepInput.prototype.setMin = function (min) {
if (min !== undefined && !this._validateOptionalNumberProperty("min", min)) {
return this;
}
return this.setProperty("min", min);
};
/**
* Sets the max value.
*
* @param {float} max The max value
* @returns {this} Reference to the control instance for chaining
*/
StepInput.prototype.setMax = function (max) {
if (max !== undefined && !this._validateOptionalNumberProperty("max", max)) {
return this;
}
return this.setProperty("max", max);
};
/**
* 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 {this} Reference to the control instance for chaining
*/
StepInput.prototype.setDisplayValuePrecision = function (number) {
var vValuePrecision,
oInstance;
if (isValidPrecisionValue(number)) {
vValuePrecision = parseInt(number);
} 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.");
}
var oInstance = this.setProperty("displayValuePrecision", vValuePrecision);
this._getNumberFormatter(true);
return oInstance;
};
/**
* 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") || [];
var oIncrementIcon = null;
// sap.m.Input constructor provides some icons on its own.
// Clear icon is always at index 0;
// Value help icon is at index 1;
// Though in this case the Input is controlled by the StepInput's code, the safe approach
// will be to look for the increment button at the last index in the aggregation.
if (endIcons.length) {
oIncrementIcon = endIcons[endIcons.length - 1];
}
return oIncrementIcon;
};
/**
* 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 oIcon = this._getInput().addEndIcon({
src: IconPool.getIconURI("add"),
id: this.getId() + "-incrementBtn",
noTabStop: true,
decorative: !Device.support.touch || Device.system.desktop ? true : false,
press: this._handleButtonPress.bind(this, 1),
useIconTooltip: false,
alt: StepInput.STEP_INPUT_INCREASE_BTN_TOOLTIP
});
oIcon.getEnabled = function () {
return !this._shouldDisableIncrementButton(this._parseNumber(this._getInput().getValue()), this._getMax());
}.bind(this);
oIcon.$().attr("tabindex", "-1");
this._attachEvents(oIcon, true);
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");
}
});
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 oIcon = this._getInput().addBeginIcon({
src: IconPool.getIconURI("less"),
id: this.getId() + "-decrementBtn",
noTabStop: true,
decorative: !Device.support.touch || Device.system.desktop ? true : false,
press: this._handleButtonPress.bind(this, -1),
useIconTooltip: false,
alt: StepInput.STEP_INPUT_DECREASE_BTN_TOOLTIP
});
oIcon.getEnabled = function () {
return !this._shouldDisableDecrementButton(this._parseNumber(this._getInput().getValue()), this._getMin());
}.bind(this);
oIcon.$().attr("tabindex", "-1");
this._attachEvents(oIcon, false);
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");
}
});
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(),
editable: this.getEditable(),
enabled: this.getEnabled(),
description: this.getDescription(),
fieldWidth: this.getFieldWidth(),
liveChange: this._inputLiveChangeHandler.bind(this)
});
this.setAggregation("_input", oNumericInput);
}
return this.getAggregation("_input");
};
/**
* Changes the value of the control and fires the change event.
*
* @param {boolean} bForce If true, will force value change
* @returns {this} Reference to the control instance for chaining
* @private
*/
StepInput.prototype._changeValue = function (bForce) {
this._verifyValue();
this._bValueStatePreset = false;
// Handle the special case where string representation needs formatting
let bShouldFireChange = (this._fTempValue != this._fOldValue && this._isValueWithCorrectPrecision(this._sTempValue)) || bForce;
// Check if we need to format string representation (e.g., ".50" -> "0.50")
if (!bShouldFireChange && this._sTempValue && typeof this._sTempValue === 'string') {
const fParsedInput = this._parseNumber(this._sTempValue);
const sFormattedValue = this._getFormattedValue(this._fTempValue);
// Round the parsed input according to displayValuePrecision so inputs like
// ".4" with displayValuePrecision=0 are treated as 0 after rounding.
const iPrecision = this.getDisplayValuePrecision();
const fParsedRounded = !isNaN(fParsedInput) ? Math.round(fParsedInput * Math.pow(10, iPrecision)) / Math.pow(10, iPrecision) : NaN;
if (!isNaN(fParsedInput) && !isNaN(fParsedRounded) &&
fParsedRounded === this._fTempValue &&
this._isValueWithCorrectPrecision(this._sTempValue) &&
this._sTempValue !== sFormattedValue) {
bShouldFireChange = true;
}
}
if (bShouldFireChange) {
// change the value and fire the event
this.setValue(this._fTempValue);
this._getInput().setValue(this._getFormattedValue());
this.fireChange({value: this._fTempValue});
} else {
// just update the visual value and buttons
this._getInput().setValue(this._sTempValue || this._fTempValue);
this._disableButtons(this._parseNumber(this._getInput().getValue()), this._getMax(), this._getMin());
}
return this;
};
/**
* Handles the press of the increase/decrease buttons.
*
* @param {float} fMultiplier Indicates the direction - increment (positive value)
* or decrement (negative value) and multiplier for modifying the value
* @returns {this} Reference to the control instance for chaining
* @private
*/
StepInput.prototype._handleButtonPress = function (fMultiplier) {
// Focus the input on mobile devices when button is pressed
if (Device.system.phone || Device.system.tablet) {
this.focus();
}
if (!this._bSpinStarted) {
// short click, just a single inc/dec button
this._bDelayedEventFire = false;
this._changeValueWithStep(fMultiplier);
this._btndown = false;
this._changeValue();
} else {
// long click, skip it
this._bSpinStarted = false;
}
this._bNeedsVerification = true;
return this;
};
/**
* Changes the value with requested step multiplier.
*
* @param {float} fMultiplier Indicates the direction - increment (positive value)
* or decrement (negative value), and multiplier for modifying the value
* @returns {this} Reference to the control instance for chaining
* @private
*/
StepInput.prototype._changeValueWithStep = function (fMultiplier) {
var iMultiplier,
fNewValue,
fDelta;
// calculate precision multiplier
if (isNaN(this._iValuePrecision)) {
this._iValuePrecision = this._getNumberPrecision(this.getValue());
}
iMultiplier = Math.pow(10, Math.max(this.getDisplayValuePrecision(), this._iValuePrecision));
if (isNaN(this._fTempValue) || this._fTempValue === undefined) {
this._fTempValue = this.getValue();
}
// check input value to correct requested step if necessary
fDelta = this._checkInputValue();
this._fTempValue += fDelta;
// calculate new value
fNewValue = fMultiplier !== 0 ? this._calculateNewValue(fMultiplier) : this._fTempValue;
// fix value precision
if (fMultiplier === 0) {
fNewValue = Math.round(fNewValue * iMultiplier) / iMultiplier;
}
// save new temp value
if (fMultiplier !== 0 || fDelta !== 0 || this._bDelayedEventFire) {
this._fTempValue = fNewValue;
}
if (this._bDelayedEventFire) {
this._applyValue(fNewValue);
this._disableButtons(this._parseNumber(this._getFormattedValue(fNewValue)), this._getMax(), this._getMin());
this._bNeedsVerification = true;
}
return this;
};
/**
* Handles whether the increment and decrement buttons should be enabled/disabled based on different situations.
*
* @param {number} iValue Indicates the value in the input
* @param {number} iMax Indicates the max
* @param {number} iMin Indicates the min
* @returns {this} Reference to the control instance for chaining
*/
StepInput.prototype._disableButtons = function (iValue, iMax, iMin) {
if (!this._isNumericLike(iValue)) {
return;
}
var oIncrementButton = this._getIncrementButton(),
oDecrementButton = this._getDecrementButton(),
bShouldDisableDecrement = this._shouldDisableDecrementButton(iValue, iMin),
bShouldDisableIncrement = this._shouldDisableIncrementButton(iValue, iMax);
oDecrementButton && oDecrementButton.toggleStyleClass("sapMStepInputIconDisabled", bShouldDisableDecrement);
oIncrementButton && oIncrementButton.toggleStyleClass("sapMStepInputIconDisabled", bShouldDisableIncrement);
return this;
};
StepInput.prototype._shouldDisableDecrementButton = function (iValue, iMin) {
var bMinIsNumber = this._isNumericLike(iMin),
bEnabled = this.getEnabled(),
bReachedMin = bMinIsNumber && iMin >= iValue; // min is set and it's bigger or equal to the value
return bEnabled ? bReachedMin : true; // if enabled - set the value according to the min value, if not - set disable flag to true
};
StepInput.prototype._shouldDisableIncrementButton = function (iValue, iMax) {
var bMaxIsNumber = this._isNumericLike(iMax),
bEnabled = this.getEnabled(),
bReachedMax = bMaxIsNumber && iMax <= iValue; // max is set and it's lower or equal to the value
return bEnabled ? bReachedMax : true; // if enabled - set the value according to the max value, if not - set disable flag to true;
};
/**
* 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(),
sValue = this._getInput().getValue(),
value = this._parseNumber(this._getInput().getValue()),
oCoreMessageBundle = Library.getResourceBundleFor("sap.ui.core"),
oBinding = this.getBinding("value"),
oBindingValueState = this.getBinding("valueState"),
oBindingType = oBinding && oBinding.getType && oBinding.getType(),
sBindingConstraintMax = oBindingType && oBindingType.oConstraints && oBindingType.oConstraints.maximum,
sBindingConstraintMin = oBindingType && oBindingType.oConstraints && oBindingType.oConstraints.minimum,
sMessage,
aViolatedConstraints = [],
bHasValidationErrorListeners = false,
oEventProvider;
if (oBindingValueState && oBindingValueState.oValue && this._bValueStatePreset) {
return;
}
if (!this._isNumericLike(value)) {
return;
}
oEventProvider = this;
do {
bHasValidationErrorListeners = oEventProvider.hasListeners("validationError");
oEventProvider = oEventProvider.getEventingParent();
} while (oEventProvider && !bHasValidationErrorListeners);
if (this._isMoreThanMax(value)) {
if (bHasValidationErrorListeners && sBindingConstraintMax) {
return;
}
sMessage = this.getValueStateText() ? this.getValueStateText() : oCoreMessageBundle.getText("EnterNumberMax", [max]);
aViolatedConstraints.push("maximum");
} else if (this._isLessThanMin(value)) {
if (bHasValidationErrorListeners && sBindingConstraintMin) {
return;
}
sMessage = this.getValueStateText() ? this.getValueStateText() : oCoreMessageBundle.getText("EnterNumberMin", [min]);
aViolatedConstraints.push("minimum");
} else if (this._areFoldChangeRequirementsFulfilled() && (value % this.getStep() !== 0)) {
sMessage = this.getValueStateText() ? this.getValueStateText() : oCoreMessageBundle.getText("Float.Invalid");
} else if (!this._isValueWithCorrectPrecision(sValue)) {
aViolatedConstraints.push("precision");
sMessage = oCoreMessageBundle.getText("EnterNumberWithPrecision", [this.getDisplayValuePrecision()]);
}
if (sMessage) {
// there is error message
// first set valueState and valueStateText
this.setProperty("valueState", ValueState.Error, true);
this._getInput().setValueState(ValueState.Error);
this._getInput().setValueStateText(sMessage);
// then, if there are listeners, fire an exception
if (bHasValidationErrorListeners) {
this.fireValidationError({
element: this,
exception: new ValidateException(sMessage, aViolatedConstraints),
id: this.getId(),
message: sMessage,
property: "value"
});
}
} else {
// no errors
this.setProperty("valueState", ValueState.None, true);
this._getInput().setValueState(ValueState.None);
}
};
/**
* Returns the precision of a number.
* @param {float} fNumber The number whose precision is to be obtained
* @returns {int} the precision of the number passed as parameter
*
*/
StepInput.prototype._getNumberPrecision = function(fNumber) {
var aNumberParts = !isNaN(fNumber) && fNumber !== null ? fNumber.toString().split('.') : [];
return aNumberParts.length > 1 ? aNumberParts[1].length : 0;
};
StepInput.prototype.setValueState = function(sValueState) {
this._bValueStatePreset = true;
this.setProperty("valueState", sValueState);
this._getInput().setValueState(sValueState);
return this;
};
/*
* 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;
this._iValuePrecision = this._getNumberPrecision(oValue);
if (isNaN(oValue) || oValue === null) {
oValue = this._getDefaultValue(undefined, this._getMax(), this._getMin());
} else {
oValue = Number(oValue);
}
if (!this._validateOptionalNumberProperty("value", oValue)) {
return this;
}
this._sOriginalValue = oValue;
this._getInput().setValue(oValue);
this._disableButtons(this._parseNumber(this._getInput().getValue()), this._getMax(), this._getMin());
if (oValue !== this._fOldValue) {
// save current value (for ESC restoring)
this._fOldValue = oValue;
oResult = this.setProperty("value", oValue);
} else {
oResult = this;
}
this._iRealPrecision = this._getRealValuePrecision();
this._fTempValue = oValue;
this._bValueStatePreset = false;
return oResult;
};
StepInput.prototype._getNumberFormatter = function(bReset) {
if (!this._formatter || bReset) {
this._formatter = NumberFormat.getFloatInstance({ decimals: this.getDisplayValuePrecision() });
}
return this._formatter;
};
/**
* 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 formatted value as a String
* @private
*/
StepInput.prototype._getFormattedValue = function (vValue) {
var iPrecision = this.getDisplayValuePrecision(),
iValueLength,
sDigits;
if (vValue == undefined) {
vValue = this.getValue();
}
if (Device.system.desktop) {
return this._getNumberFormatter().format(vValue);
}
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;
};
/**
* Checks the current value of the input and sets the control value according to it
*
* @private
*/
StepInput.prototype._checkInputValue = function () {
var sInputValue = this._getInput().getValue(),
fDelta = 0;
// check for empty input value, and if so - return the last saved value
if (sInputValue === "") {
sInputValue = this._getDefaultValue(sInputValue, this._getMax(), this._getMin()).toString();
}
// fix the entered value if the precision is 0; and filter 'e/E' meanwhile
if (this.getDisplayValuePrecision() === 0) {
sInputValue = Math.round(this._parseNumber(sInputValue.toLowerCase().split('e')[0])).toString();
}
// calculates delta (difference) between input value and real control value
if (this._getFormattedValue(this._fTempValue) !== this._getFormattedValue(sInputValue)) {
fDelta = this._parseNumber(sInputValue) - this._fTempValue;
}
return fDelta;
};
/**
* Handles the <code>onsappageup</code>.
*
* Increases the value with the larger step.
*
* @param {jQuery.Event} oEvent Event object
*/
StepInput.prototype.onsappageup = function (oEvent) {
// prevent document scrolling when page up key is pressed
oEvent.preventDefault();
if (this.getEditable()) {
this._bDelayedEventFire = true;
this._changeValueWithStep(this.getLargerStep());
}
};
/**
* 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) {
// prevent document scrolling when page down key is pressed
oEvent.preventDefault();
if (this.getEditable()) {
this._bDelayedEventFire = true;
this._changeValueWithStep(-this.getLargerStep());
}
};
/**
* 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.getEditable() && this._isNumericLike(this._getMax()) && !(oEvent.ctrlKey || oEvent.metaKey || oEvent.altKey) && oEvent.shiftKey) {
this._bDelayedEventFire = true;
this._fTempValue = this._parseNumber(this._getInput().getValue());
this._changeValueWithStep(this._getMax() - this._fTempValue);
}
};
/**
* 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.getEditable() && this._isNumericLike(this._getMin()) && !(oEvent.ctrlKey || oEvent.metaKey || oEvent.altKey) && oEvent.shiftKey) {
this._bDelayedEventFire = true;
this._fTempValue = this._parseNumber(this._getInput().getValue());
this._changeValueWithStep(-(this._fTempValue - 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)
if (this.getEditable()) {
this._bDelayedEventFire = true;
this._changeValueWithStep(1);
oEvent.setMarked();
}
};
/**
* 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)
if (this.getEditable()) {
this._bDelayedEventFire = true;
this._changeValueWithStep(-1);
oEvent.setMarked();
}
};
StepInput.prototype._onmousewheel = function (oEvent) {
var bIsFocused = this.getDomRef().contains(document.activeElement);
if (bIsFocused && this.getEditable() && this.getEnabled()) {
oEvent.preventDefault();
var oOriginalEvent = oEvent.originalEvent,
bDirectionPositive = oOriginalEvent.detail ? (-oOriginalEvent.detail > 0) : (oOriginalEvent.wheelDelta > 0);
this._bDelayedEventFire = true;
this._changeValueWithStep((bDirectionPositive ? 1 : -1));
}
};
/**
* 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 fStep,
fMax,
fMin;
if (!this.getEditable()) {
return;
}
if (oEvent.which === KeyCodes.ENTER && this._fTempValue !== this.getValue()) {
oEvent.preventDefault();
this._changeValue();
return;
}
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
fMax = this._getMax();
this._fTempValue = this._parseNumber(this._getInput().getValue());
fStep = (fMax !== undefined) ? fMax - this._fTempValue : 0;
} else if (oEvent.which === KeyCodes.ARROW_DOWN && !oEvent.altKey && oEvent.shiftKey && (oEvent.ctrlKey || oEvent.metaKey)) { //ctrl+shift+down
fMin = this._getMin();
this._fTempValue = this._parseNumber(this._getInput().getValue());
fStep = (fMin !== undefined) ? -(this._fTempValue - fMin) : 0;
} else if (oEvent.which === KeyCodes.ARROW_UP && !(oEvent.ctrlKey || oEvent.metaKey || oEvent.altKey) && oEvent.shiftKey) { //shift+up
fStep = this.getLargerStep();
} else if (oEvent.which === KeyCodes.ARROW_DOWN && !(oEvent.ctrlKey || oEvent.metaKey || oEvent.altKey) && oEvent.shiftKey) { //shift+down
fStep = -this.getLargerStep();
} else if (oEvent.which === KeyCodes.ARROW_UP && (oEvent.ctrlKey || oEvent.metaKey)) { // ctrl + up
fStep = 1;
} else if (oEvent.which === KeyCodes.ARROW_DOWN && (oEvent.ctrlKey || oEvent.metaKey)) { // ctrl + down
fStep = -1;
} else if (oEvent.which === KeyCodes.ARROW_UP && oEvent.altKey) { // alt + up
fStep = 1;
} else if (oEvent.which === KeyCodes.ARROW_DOWN && oEvent.altKey) { // alt + down
fStep = -1;
}
// do change if there is any step set
if (fStep !== undefined) {
oEvent.preventDefault();
if (fStep !== 0) {
this._bDelayedEventFire = true;
this._changeValueWithStep(fStep);
}
}
};
/**
* 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) {
if (this._fOldValue !== this._fTempValue) {
this._applyValue(this._fOldValue);
this._bNeedsVerification = true;
}
};
/**
* Attaches the <code>liveChange</code> handler for the input.
* @private
*/
StepInput.prototype._attachLiveChange = function () {
this._getInput().attachLiveChange(this._liveChange, this);
};
/**
* Detaches the <code>liveChange</code> handler for the input.
* @private
*/
StepInput.prototype._detachLiveChange = function () {
this._getInput().detachLiveChange(this._liveChange, this);
};
/**
* Attaches the <code>change</code> handler for the input.
* @private
*/
StepInput.prototype._attachChange = function () {
this._getInput().attachChange(this._change, this);
};
/**
* Attaches the <code>liveChange</code> handler for the input.
* @private
*/
StepInput.prototype._liveChange = function (oEvent) {
this._disableButtons(this._parseNumber(this._getInput().getValue()), this._getMax(), this._getMin());
this._verifyValue();
this._bValueStatePreset = false;
};
/**
* Handles the <code>change</code> event for the input.
* @param {Object} oEvent The fired event
* @private
*/
StepInput.prototype._change = function (oEvent) {
var fOldValue;
var oNewValue = this._getInput().getValue();
var bIsNotInValidRange = this._isLessThanMin(oNewValue) || this._isMoreThanMax(oNewValue);
if (!this._isButtonFocused() ) {
if (!this._btndown || bIsNotInValidRange) {
fOldValue = this._parseNumber(this._getFormattedValue());
if (this._fOldValue === undefined) {
this._fOldValue = fOldValue;
}
this._sTempValue = oNewValue;
this._bDelayedEventFire = false;
this._changeValueWithStep(0);
this._changeValue();
this._bNeedsVerification = true;
} else {
this._fTempValue = this._parseNumber(this._getInput().getValue());
}
}
};
StepInput.prototype._isMoreThanMax = function(iValue) {
return this._isNumericLike(this._getMax()) && this._getMax() < iValue;
};
StepInput.prototype._isLessThanMin = function(iValue) {
return this._isNumericLike(this._getMin()) && this._getMin() > iValue;
};
/**
* 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) {
// the property Value is not changing because this is a live change where the final value is not yet confirmed by the user
this._getInput().setValue(this._getFormattedValue(fNewValue));
};
/**
* Makes calculations regarding the operation and the number type.
*
* @param {float} fStepMultiplier Holds the step multiplier
* @param {boolean} bIsIncreasing Holds the operation(or direction) whether addition(increasing) or subtraction(decreasing)
* @returns {{value: number, displayValue: number}} 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 (fStepMultiplier, bIsIncreasing) {
if (bIsIncreasing === undefined ) {
bIsIncreasing = fStepMultiplier < 0 ? false : true;
}
var fStep = this.getStep(),
fMax = this._getMax(),
fMin = this._getMin(),
fInputValue = parseFloat(this._getDefaultValue(this._getInput().getValue(), fMax, fMin)),
iSign = bIsIncreasing ? 1 : -1,
fMultipliedStep = Math.abs(fStep) * Math.abs(fStepMultiplier),
fResult = fInputValue + iSign * fMultipliedStep,
fValueResult;
if (this._areFoldChangeRequirementsFulfilled()) {
fResult = fValueResult = this._calculateClosestFoldValue(fInputValue, fMultipliedStep, iSign);
} else {
fValueResult = this._sumValues(this._fTempValue, 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;
}
// 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;
}
return fValueResult;
};
/**
* 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("."),
sDigitsLargerStep = this.getLargerStep().toString().split("."),
iDigitsValueL,
iDigitsStepL,
iDigitsLargerStepL;
iDigitsValueL = (!sDigitsValue[1]) ? 0 : sDigitsValue[1].length;
iDigitsStepL = (!sDigitsStep[1]) ? 0 : sDigitsStep[1].length;
iDigitsLargerStepL = (!sDigitsLargerStep[1]) ? 0 : sDigitsLargerStep[1].length;
return Math.max(iDigitsValueL, iDigitsStepL, iDigitsLargerStepL);
};
/**
* 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) {
this.bLiveChange = true;
this._getInput().setProperty("value", oEvent.getParameter("value"), true);
};
StepInput.prototype._isValueWithCorrectPrecision = function (sValue) {
const sDecimalSeparator = Device.system.desktop ? this._getNumberFormatter().oFormatOptions.decimalSeparator : ".",
iCharsSet = this.getDisplayValuePrecision();
// If the value starts with the decimal separator, normalize it to "0.xxx" for the precision check
if (sValue && sValue.indexOf(sDecimalSeparator) === 0) {
sValue = `0${sValue}`;
}
const iDecimalMark = sValue?.indexOf(sDecimalSeparator);
if (iDecimalMark >= 0 && iCharsSet >= 0) { // only for decimals
const sEventValueAfterTheDecimal = sValue?.split(sDecimalSeparator)[1],
iCharsAfterTheDecimalSign = sEventValueAfterTheDecimal ? sEventValueAfterTheDecimal.length : 0;
// if the characters after the decimal are more or less than the displayValuePrecision -> invalid
if (iCharsAfterTheDecimalSign !== iCharsSet) {
return false;
}
}
return true;
};
/**
* 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._parseNumber(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 !== "";
};