@openui5/sap.m
Version:
OpenUI5 UI Library sap.m
1,629 lines (1,343 loc) • 50.7 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2026 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
sap.ui.define([
"sap/base/i18n/Localization",
'sap/ui/core/Control',
'sap/ui/core/EnabledPropagator',
'sap/ui/core/InvisibleText',
"sap/ui/core/Lib",
'sap/ui/core/library',
'sap/ui/core/ResizeHandler',
'sap/base/Log',
'./library',
'./SliderTooltipContainer',
'./SliderTooltip',
'./SliderUtilities',
'./SliderRenderer',
'./ResponsiveScale',
"sap/ui/thirdparty/jquery",
"sap/ui/events/KeyCodes"
],
function(
Localization,
Control,
EnabledPropagator,
InvisibleText,
Library,
coreLibrary,
ResizeHandler,
log,
library,
SliderTooltipContainer,
SliderTooltip,
SliderUtilities,
SliderRenderer,
ResponsiveScale,
jQuery,
KeyCodes
) {
"use strict";
// shortcut for sap.m.touch
var touch = library.touch;
/**
* Constructor for a new <code>Slider</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
* <h3>Overview</h3>
*
* A {@link sap.m.Slider} control represents a numerical range and a handle.
* The purpose of the control is to enable visual selection of a value in a continuous numerical range by moving an adjustable handle.
*
* <b>Notes:</b>
* <ul><li>Only horizontal sliders are possible. </li>
* <li>The handle can be moved in steps of predefined size. This is done with the <code>step</code> property. </li>
* <li>Setting the property <code>showAdvancedTooltip</code> shows an input field above the handle</li>
* <li>Setting the property <code>inputsAsTooltips</code> enables the user to enter a specific value in the handle's tooltip. </li>
* </ul>
*
* <h3>Structure</h3>
*
* The most important properties of the Slider are:
* <ul>
* <li> min - The minimum value of the slider range </li>
* <li> max - The maximum value of the slider range </li>
* <li> value - The current value of the slider</li>
* <li> progress - Determines if a progress bar will be shown on the slider range</li>
* <li> step - Determines the increments in which the slider will move</li>
* </ul>
* These properties determine the visualization of the tooltips:
* <ul>
* <li> <code>showAdvancedTooltip</code> - Determines if a tooltip should be displayed above the handle</li>
* <li> <code>inputsAsTooltips</code> - Determines if the tooltip displayed above the slider's handle should include an input field</li>
* </ul>
*
* <h3>Usage</h3>
*
* The most common usecase is to select values on a continuous numerical scale (e.g. temperature, volume, etc. ).
*
* <h3>Responsive Behavior</h3>
*
* The <code>sap.m.Slider</code> control adjusts to the size of its parent container by recalculating and resizing the width of the control.
* You can move the slider handle in several different ways:
* <ul>
* <li> Drag and drop to the desired value</li>
* <li> Click/tap on the range bar to move the handle to that location </li>
* <li> Enter the desired value in the input field (if available) </li>
* <li> Keyboard (Arrow keys, "+" and "-") </li>
* </ul>
*
* @extends sap.ui.core.Control
* @implements sap.ui.core.IFormContent, sap.ui.core.ISemanticFormContent
*
* @borrows sap.ui.core.ISemanticFormContent.getFormFormattedValue as #getFormFormattedValue
* @borrows sap.ui.core.ISemanticFormContent.getFormValueProperty as #getFormValueProperty
* @borrows sap.ui.core.ISemanticFormContent.getFormObservingProperties as #getFormObservingProperties
* @borrows sap.ui.core.ISemanticFormContent.getFormRenderAsControl as #getFormRenderAsControl
*
* @author SAP SE
* @version 1.146.0
*
* @constructor
* @public
* @alias sap.m.Slider
* @see {@link fiori:https://experience.sap.com/fiori-design-web/slider/ Slider}
*/
var Slider = Control.extend("sap.m.Slider", /** @lends sap.m.Slider.prototype */ {
metadata: {
interfaces: [
"sap.ui.core.IFormContent",
"sap.ui.core.ISemanticFormContent",
"sap.m.IToolbarInteractiveControl"
],
library: "sap.m",
properties: {
/**
* Defines the width of the control.
*/
width: { type: "sap.ui.core.CSSSize", group: "Appearance", defaultValue: "100%" },
/**
* Indicates whether the user can change the value.
*/
enabled: { type: "boolean", group: "Behavior", defaultValue: true },
/**
* The name property to be used in the HTML code for the slider (e.g. for HTML forms that send data to the server via submit).
*/
name: { type: "string", group: "Misc", defaultValue: "" },
/**
* The minimum value.
*/
min: { type: "float", group: "Data", defaultValue: 0 },
/**
* The maximum value.
*/
max: { type: "float", group: "Data", defaultValue: 100 },
/**
* Define the amount of units to change the slider when adjusting by drag and drop.
*
* Defines the size of the slider's selection intervals. (e.g. min = 0, max = 10, step = 5 would result in possible selection of the values 0, 5, 10).
*
* The step must be positive, if a negative number is provided, the default value will be used instead.
* If the width of the slider converted to pixels is less than the range (max - min), the value will be rounded to multiples of the step size.
*/
step: { type: "float", group: "Data", defaultValue: 1 },
/**
* Indicate whether a progress bar indicator is shown.
*/
progress: { type: "boolean", group: "Misc", defaultValue: true },
/**
* Define the value.
*
* If the value is lower/higher than the allowed minimum/maximum, the value of the properties <code>min</code>/<code>max</code> are used instead.
*/
value: { type: "float", group: "Data", defaultValue: 0 },
/**
* Indicate whether the handle tooltip is shown.
* @since 1.31
*
*/
showHandleTooltip: { type: "boolean", group: "Appearance", defaultValue: true},
/**
* Indicate whether the handle's advanced tooltip is shown. <b>Note:</b> Setting this option to <code>true</code>
* will ignore the value set in <code>showHandleTooltip</code>. This will cause only the advanced tooltip to be shown.
* @since 1.42
*
*/
showAdvancedTooltip: { type: "boolean", group: "Appearance", defaultValue: false},
/**
* Indicates whether input fields should be used as tooltips for the handles. <b>Note:</b> Setting this option to <code>true</code>
* will only work if <code>showAdvancedTooltip</code> is set to <code>true</code>.
* **Note:** To comply with the accessibility standard, it is recommended to set the <code>inputsAsTooltips</code> property to true.
* @since 1.42
*/
inputsAsTooltips: {type: "boolean", group: "Appearance", defaultValue: false},
/**
* Enables tickmarks visualisation
*
* @since 1.44
*/
enableTickmarks: {type: "boolean", group: "Appearance", defaultValue: false},
/**
* Indicates whether a handle is pressed
*
* @private
*/
handlePressed: {type: "boolean", defaultValue: false, group: "Appearance", visibility: "hidden" }
},
defaultAggregation: "scale",
aggregations: {
/**
* A Container popup that renders SliderTooltips
* The actual type should be sap.m.SliderTooltipContainer
*
* @since 1.54
*/
_tooltipContainer: { type: "sap.ui.core.Control", multiple: false, visibility: "hidden" },
/**
* Scale for visualisation of tickmarks and labels
*
* @since 1.46
*/
scale: { type: "sap.m.IScale", multiple: false, singularName: "scale" },
/**
* Default scale for visualisation of tickmarks, if scale aggregation is not provided
*
* @since 1.56
*/
_defaultScale: { type: "sap.m.ResponsiveScale", multiple: false, visibility: "hidden" },
/**
* Multiple Aggregation for Tooltips
*
* @since 1.56
*/
_defaultTooltips: { type: "sap.m.SliderTooltipBase", multiple: true, visibility: "hidden" },
/**
* Aggregation for user-defined tooltips.
* <b>Note:</b> In case of Slider, only the first tooltip of the aggregation is used. In the RangeSlider case - the first two.
* If no custom tooltips are provided, the default are used
*
* @since 1.56
*/
customTooltips: { type: "sap.m.SliderTooltipBase", multiple: true },
/**
* Invisible text for handles and progress announcement
*
* @since 1.54
*/
_handlesLabels: { type: "sap.ui.core.InvisibleText", multiple: true, visibility: "hidden" }
},
associations: {
/**
* Association to controls / IDs which label this control (see WAI-ARIA attribute <code>aria-labelledby</code>).
* @since 1.27.0
*/
ariaLabelledBy: { type: "sap.ui.core.Control", multiple: true, singularName: "ariaLabelledBy" }
},
events: {
/**
* This event is triggered after the end user finishes interacting, if there is any change.
*/
change: {
parameters: {
/**
* The current value of the slider after a change.
*/
value: { type: "float" }
}
},
/**
* This event is triggered during the dragging period, each time the slider value changes.
*/
liveChange: {
parameters: {
/**
* The current value of the slider after a live change.
*/
value: { type: "float" }
}
}
},
designtime: "sap/m/designtime/Slider.designtime"
},
renderer: SliderRenderer
});
EnabledPropagator.apply(Slider.prototype, [true]);
/* =========================================================== */
/* Private methods and properties */
/* =========================================================== */
/* ----------------------------------------------------------- */
/* Private methods */
/* ----------------------------------------------------------- */
/**
* Returns the scale control that is used when tickmarks are enabled
*
* @private
* @returns {sap.m.IScale|undefined} The scale that is used or undefined,
* if tickmarks are not enabled
*/
Slider.prototype._getUsedScale = function () {
if (!this.getEnableTickmarks()) {
return;
}
return this.getAggregation('scale') || this.getAggregation('_defaultScale');
};
Slider.prototype._syncScaleUsage = function () {
var bEnabledTickmarks = this.getEnableTickmarks(),
oUserDefinedScale = this.getAggregation('scale'),
oDefaultScale = this.getAggregation("_defaultScale");
// if the default scale was set, but later on the user adds a scale
// or set the enableTickmarks property to false, we should destroy the default one
if ((oDefaultScale && !bEnabledTickmarks) || (oUserDefinedScale && oDefaultScale)) {
this.destroyAggregation("_defaultScale", true);
}
// if there is no scale set, fall back to the default scale
if (bEnabledTickmarks && !oUserDefinedScale && !oDefaultScale) {
this.setAggregation("_defaultScale", new ResponsiveScale(), true);
}
};
Slider.prototype._showTooltipsIfNeeded = function () {
if (this.getShowAdvancedTooltip()) {
this.getAggregation("_tooltipContainer").show(this);
this.updateAdvancedTooltipDom(this.getValue());
}
};
/**
* Convert <code>fValue</code> for RTL-Mode.
*
* @param {float} fValue input value
* @private
* @returns {float} output value
*/
Slider.prototype._convertValueToRtlMode = function(fValue) {
return this.getMax() - fValue + this.getMin();
};
/**
* Recalculate some styles.
*
* @private
*/
Slider.prototype._recalculateStyles = function() {
var $Slider = this.$();
this._fSliderWidth = $Slider.width();
this._fSliderPaddingLeft = parseFloat($Slider.css("padding-left"));
this._fSliderOffsetLeft = $Slider.offset().left;
this._fHandleWidth = this.$("handle").width();
};
/**
* Checks whether the minimum is lower than or equal to the maximum and
* whether the step is bigger than slider range.
*
* @private
* @returns {boolean} Whether the properties are correctly set
*/
Slider.prototype._validateProperties = function() {
var fMin = this.getMin(),
fMax = this.getMax(),
fStep = this.getStep(),
bMinbiggerThanMax = false,
bError = false;
// if the minimum is lower than or equal to the maximum, log a warning
if (fMin >= fMax) {
bMinbiggerThanMax = true;
bError = true;
log.warning("Warning: " + "Property wrong min: " + fMin + " >= max: " + fMax + " on ", this);
}
// if the step is negative or 0, set to 1 and log a warning
if (fStep <= 0) {
log.warning("Warning: " + "The step could not be negative on ", this);
}
// the step can't be bigger than slider range, log a warning
if (fStep > (fMax - fMin) && !bMinbiggerThanMax) {
bError = true;
log.warning("Warning: " + "Property wrong step: " + fStep + " > max: " + fMax + " - " + "min: " + fMin + " on ", this);
}
return bError;
};
/**
* Calculate percentage.
*
* @param {float} fValue The value
* @private
* @returns {float} percent The corresponding percentage
*/
Slider.prototype._getPercentOfValue = function(fValue) {
return SliderUtilities.getPercentOfValue(fValue, this.getMin(), this.getMax());
};
/**
* Get the value on certain position
*
* @param {float} fPercent The percent value
* @returns {number} The position value
* @private
*/
Slider.prototype._getValueOfPercent = function(fPercent) {
var fMin = this.getMin(),
fValue = (fPercent * (this.getMax() - fMin) / 100) + fMin,
sNewValueFixedPoint = this.toFixed(fValue, this.getDecimalPrecisionOfNumber(this.getStep()));
return Number(sNewValueFixedPoint);
};
/**
* Required by the {@link sap.m.IToolbarInteractiveControl} interface.
* Determines if the Control is interactive.
*
* @returns {boolean} If it is an interactive Control
*
* @private
* @ui5-restricted sap.m.OverflowToolbar, sap.m.Toolbar
*/
Slider.prototype._getToolbarInteractive = function () {
return true;
};
/**
* Checks whether the given step is of the proper type.
*
* @param {int} iStep The step size
* @private
* @returns {int} The validated step size
*/
Slider.prototype._validateStep = function(iStep) {
if (typeof iStep === "undefined") {
return 1; // default n = 1
}
if (typeof iStep !== "number") {
log.warning('Warning: "iStep" needs to be a number', this);
return 0;
}
if ((Math.floor(iStep) === iStep) && isFinite(iStep)) {
return iStep;
}
log.warning('Warning: "iStep" needs to be a finite interger', this);
return 0;
};
/**
* Handles resize of Slider.
*
* @private
*/
Slider.prototype._handleSliderResize = function (oEvent) {
var oScale = this._getUsedScale();
if (this.getEnableTickmarks() && oScale && oScale.handleResize) {
oScale.handleResize(oEvent);
}
if (this.getShowAdvancedTooltip()) {
this._handleTooltipContainerResponsiveness();
}
};
Slider.prototype._handleTooltipContainerResponsiveness = function () {
this.getAggregation("_tooltipContainer").setWidth(this.$().width() + "px");
};
Slider.prototype.getDecimalPrecisionOfNumber = function(fValue) {
// the value is an integer
if (Math.floor(fValue) === fValue) {
return 0;
}
var sValue = fValue.toString(),
iIndexOfDot = sValue.indexOf("."),
iIndexOfENotation = sValue.indexOf("e-"),
bIndexOfENotationFound = iIndexOfENotation !== -1, // the "e-" is found in the value
bIndexOfDotFound = iIndexOfDot !== -1; // the "." is found in the value
// note: numbers such as 0.0000005 are represented using the e-notation
// (for example, 0.0000005 becomes 5e-7)
if (bIndexOfENotationFound) {
// get the e-notation exponent e.g.: in the number 5e-7, the exponent is 7
var iENotationExponent = +sValue.slice(iIndexOfENotation + 2);
// If both, the e-notation and the dot character are found in the string representation of the number,
// it means that the number has a format similar to e.g.: 1.15e-7.
// In this case, the precision is calculated by adding the number of digits between the dot character
// and the e character, e.g.: the number 1.15e-7 has a precision of 9
if (bIndexOfDotFound) {
return iENotationExponent + sValue.slice(iIndexOfDot + 1, iIndexOfENotation).length;
}
return iENotationExponent;
}
if (bIndexOfDotFound) {
return sValue.length - iIndexOfDot - 1;
}
return 0;
};
/**
* Formats the <code>fNumber</code> using the fixed-point notation.
*
* <b>Note:</b> The number of digits to appear after the decimal point of the value
* should be between 0 and 20 to avoid a RangeError when calling the <code>Number.toFixed()</code> method.
*
* @param {float} fNumber The number to format.
* @param {int} [iDigits] The number of digits to appear after the decimal point.
* @returns {string} A string representation of <code>fNumber</code> that does not use exponential notation.
* @private
*/
Slider.prototype.toFixed = function(fNumber, iDigits) {
if (iDigits === undefined) {
iDigits = this.getDecimalPrecisionOfNumber(fNumber);
}
if (iDigits > 20) {
iDigits = 20;
} else if (iDigits < 0) {
iDigits = 0;
}
// note: .toFixed() does not return a string when the number is negative
return fNumber.toFixed(iDigits) + "";
};
Slider.prototype.setDomValue = function(sNewValue) {
var oDomRef = this.getDomRef(),
sScaleLabel = this._formatValueByCustomElement(sNewValue),
oTooltipContainer = this.getAggregation("_tooltipContainer");
if (!oDomRef) {
return;
}
// note: round negative percentages to 0
var sPerValue = Math.max(this._getPercentOfValue(+sNewValue), 0) + "%",
oHandleDomRef = this.getDomRef("handle");
if (this.getName()) {
this.getDomRef("input").setAttribute("value", sScaleLabel);
}
if (this.getProgress()) {
// update the progress indicator
this.getDomRef("progress").style.width = sPerValue;
}
// update the position of the handle
oHandleDomRef.style[Localization.getRTL() ? "right" : "left"] = sPerValue;
// update the position of the advanced tooltip
if (this.getShowAdvancedTooltip() && oTooltipContainer.getDomRef()) {
this.updateAdvancedTooltipDom(sNewValue);
}
if (this.getShowHandleTooltip() && !this.getShowAdvancedTooltip()) {
// update the tooltip
oHandleDomRef.title = sScaleLabel;
}
this._updateHandleAriaAttributeValues(oHandleDomRef, sNewValue, sScaleLabel);
};
/**
* Updates the aria-valuenow and aria-valuetext.
*
* @param {object} oHandleDomRef The DOM reference of the slider handle
* @param {string} sValue The current value
* @param {string} sScaleLabel The label of the tickmark label
* @private
*/
Slider.prototype._updateHandleAriaAttributeValues = function (oHandleDomRef, sValue, sScaleLabel) {
// update the ARIA attribute value
if (this._isElementsFormatterNotNumerical(sValue)) {
oHandleDomRef.setAttribute("aria-valuenow", sValue);
oHandleDomRef.setAttribute("aria-valuetext", sScaleLabel);
} else {
oHandleDomRef.setAttribute("aria-valuenow", sScaleLabel);
oHandleDomRef.removeAttribute("aria-valuetext");
}
};
/**
* Format the value from the Scale or Tooltip callback.
*
* As the scale might want to display something else, but not numbers, we need to ensure that the format
* would be populated to the relevant parts of the Slider:
* - Handle tooltips
* - Accessibility values
* - Advanced tooltips are not taken into consideration as they need to implement their own formatting function.
* That way we'd keep components as loose as possible.
*
* @param {float} fValue The value to be formatted
* @param {string} sPriority Default priority is:
* 1) Float value from the Slider,
* 2) Scale formatter,
* 3) Tooltips formatter so #3 always overwrites the rest.
* Priorities could be changed, so some formatter to prevail the others. Possible values are:
* - 'slider' (uses default value from the Slider),
* - 'scale' (use the Scale formatter),
* - null/undefined (use the Tooltips' formatter)
*
* @returns {string} The formatted value
* @private
*/
Slider.prototype._formatValueByCustomElement = function (fValue, sPriority) {
var oScale = this._getUsedScale(),
oTooltip = this.getUsedTooltips()[0],
sFormattedValue = "" + fValue;
if (sPriority === 'slider') {
return sFormattedValue;
}
// If there's a labelling for the scale, use it
if (this.getEnableTickmarks() && oScale && oScale.getLabel) {
sFormattedValue = "" + oScale.getLabel(fValue, this);
}
if (sPriority === 'scale') {
return sFormattedValue;
}
// If there's a labelling for the tooltips, use it and overwrite previous
if (this.getShowAdvancedTooltip() && oTooltip && oTooltip.getLabel) {
sFormattedValue = "" + oTooltip.getLabel(fValue, this);
}
return sFormattedValue;
};
/**
* Checks whether the scale has a numerical label defined for a certain value
* of the slider.
*
* @param {float} fValue
* @returns {boolean} Returns true, when the scale has a not numerical label defined.
* @private
*/
Slider.prototype._isElementsFormatterNotNumerical = function (fValue) {
var vValue = this._formatValueByCustomElement(fValue);
return isNaN(vValue);
};
/**
* Updates value of the advanced tooltip.
*
* @param {string} sNewValue The new value
* @protected
*/
Slider.prototype.updateAdvancedTooltipDom = function (sNewValue) {
var aTooltips = this.getUsedTooltips();
this.updateTooltipsPositionAndState(aTooltips[0], parseFloat(sNewValue));
};
/**
* Gets the tooltips that should be shown.
* Returns custom tooltips if provided. Otherwise - default tooltips
*
* @protected
* @returns {sap.m.SliderTooltipBase[]} SliderTooltipBase instances.
*/
Slider.prototype.getUsedTooltips = function () {
var aCustomTooltips = this.getCustomTooltips(),
aDefaultTooltips = this.getAggregation("_defaultTooltips") || [];
return aCustomTooltips.length ? aCustomTooltips : aDefaultTooltips;
};
/**
* Updates values of Slider and repositions tooltips.
*
* @param {string} oTooltip Tooltip to be changed
* @param {float} fValue New value of the Slider
* @private
* @ui5-restricted sap.m.SliderTooltipBase
*/
Slider.prototype.updateTooltipsPositionAndState = function (oTooltip, fValue) {
var oTooltipsContainer = this.getAggregation("_tooltipContainer");
oTooltip.setValue(fValue);
oTooltipsContainer.repositionTooltips(this.getMin(), this.getMax());
};
/**
* Gets the closest handle to a <code>touchstart</code> event.
*
* @returns {object} The nearest handle DOM reference.
*/
Slider.prototype.getClosestHandleDomRef = function() {
// there is only one handle, it is always the nearest
return this.getDomRef("handle");
};
/**
* Increase the value of the slider by the given <code>fIncrement</code>.
*
* @param {int} fIncrement The increment size
* @private
*/
Slider.prototype._increaseValueBy = function(fIncrement) {
var fValue,
fNewValue;
if (this.getEnabled()) {
fValue = this.getValue();
this.setValue(fValue + (fIncrement || 1));
fNewValue = this.getValue();
if (fValue < fNewValue) {
this._fireChangeAndLiveChange({ value: fNewValue });
}
}
};
/**
* Decrease the value of the slider by the given <code>fDecrement</code>.
*
* @param {int} fDecrement The decrement size
* @private
*/
Slider.prototype._decreaseValueBy = function(fDecrement) {
var fValue,
fNewValue;
if (this.getEnabled()) {
fValue = this.getValue();
this.setValue(fValue - (fDecrement || 1));
fNewValue = this.getValue();
if (fValue > fNewValue) {
this._fireChangeAndLiveChange({ value: fNewValue });
}
}
};
Slider.prototype._getLongStep = function() {
var fMin = this.getMin(),
fMax = this.getMax(),
fStep = this.getStep(),
fLongStep = (fMax - fMin) / 10,
iStepsFromMinToMax = (fMax - fMin) / fStep;
return iStepsFromMinToMax > 10 ? fLongStep : fStep;
};
Slider.prototype._fireChangeAndLiveChange = function(oParam) {
this.fireChange(oParam);
this.fireLiveChange(oParam);
};
/**
* Handles change of Tooltip's inputs.
*
* @param {jQuery.Event} oEvent
* @protected
*/
Slider.prototype.handleTooltipChange = function (oEvent) {
var fNewValue = parseFloat(oEvent.getParameter("value"));
this.setValue(fNewValue);
this._fireChangeAndLiveChange({ value: fNewValue });
};
/**
* Register the ResizeHandler
*
* @private
*/
Slider.prototype._registerResizeHandler = function () {
if (!this._parentResizeHandler) {
this._parentResizeHandler = ResizeHandler.register(this, this._handleSliderResize.bind(this));
}
};
/**
* Deregister the ResizeHandler
*
* @private
*/
Slider.prototype._deregisterResizeHandler = function () {
if (this._parentResizeHandler) {
ResizeHandler.deregister(this._parentResizeHandler);
this._parentResizeHandler = null;
}
};
/* =========================================================== */
/* Lifecycle methods */
/* =========================================================== */
Slider.prototype.init = function () {
var oSliderLabel;
// used to track the id of touch points
this._iActiveTouchId = -1;
this._bSetValueFirstCall = true;
this._fValueBeforeFocus = 0;
// resize handler of the slider
this._parentResizeHandler = null;
this._oResourceBundle = Library.getResourceBundleFor("sap.m");
// a reference to the SliderTooltipContainer
this._oTooltipContainer = null;
oSliderLabel = new InvisibleText({
text: this._oResourceBundle.getText("SLIDER_HANDLE")
});
this.addAggregation("_handlesLabels", oSliderLabel);
};
Slider.prototype.exit = function () {
if (this._oResourceBundle) {
this._oResourceBundle = null;
}
if (this.getAggregation("_defaultTooltips")) {
this.destroyAggregation("_defaultTooltips");
}
this._deregisterResizeHandler();
};
Slider.prototype.onBeforeRendering = function () {
var bError = this._validateProperties();
// update the value only if there aren't errors
if (!bError) {
this.setValue(this.getValue());
// this is the current % value of the progress bar
// note: round negative percentages to 0
this._sProgressValue = Math.max(this._getPercentOfValue(this.getValue()), 0) + "%";
}
if (this.getShowAdvancedTooltip()) {
this.initAndSyncTooltips(["leftTooltip"]);
}
this._deregisterResizeHandler();
// set the correct scale aggregation, if needed
this._syncScaleUsage();
};
/**
* Forwards properties to a given control
* @param {Array} [aProperties] Array of properties to forward
* @param {sap.ui.core.Element} [oControl] Control to which should be forward
* @protected
*/
Slider.prototype.forwardProperties = function (aProperties, oControl) {
aProperties.forEach(function (sProperty) {
oControl.setProperty(sProperty, this.getProperty(sProperty));
}, this);
};
/**
* Forwards properties to default tooltips
*
* @param {number} [iTooltipCount] Count of the tooltips
* @protected
*/
Slider.prototype.forwardPropertiesToDefaultTooltips = function (iTooltipCount) {
var aDefaultTooltips = this.getAggregation("_defaultTooltips") || [];
for (var index = 0; index < iTooltipCount; index++) {
this.forwardProperties(["min", "max", "step"], aDefaultTooltips[index]);
aDefaultTooltips[index].setWidth(this._getMaxTooltipWidth() + "px");
aDefaultTooltips[index].setEditable(this.getInputsAsTooltips());
}
};
/**
* Creates custom tooltips, if needed, and forwards properties to them
*
* @param {number} [iTooltipCount] Count of the tooltips
* @protected
*/
Slider.prototype.associateCustomTooltips = function (iTooltipCount) {
// destroy all default tooltips
this.destroyAggregation("_defaultTooltips", true);
// prevent invalidation of the children before rendering
this._oTooltipContainer.removeAllAssociation("associatedTooltips", true);
for (var index = 0; index < iTooltipCount; index++) {
this._oTooltipContainer.addAssociation("associatedTooltips", this.getCustomTooltips()[index], true);
}
};
/**
* Creates default tooltips, if needed, and forwards properties to them
*
* @param {Array} [aTooltipIds] Array of strings for ID generation
* @protected
*/
Slider.prototype.assignDefaultTooltips = function (aTooltipIds) {
var aDefaultTooltips = this.getAggregation("_defaultTooltips") || [];
// skip init tooltips if they are already there
if (aDefaultTooltips.length === 0) {
// clear the assoctiated tooltips from the container
this._oTooltipContainer.removeAllAssociation("associatedTooltips", true);
aTooltipIds.forEach(function (sId) {
this.initDefaultTooltip(sId);
}, this);
}
// forward properties to the default tooltips
this.forwardProperties(["enabled"], this._oTooltipContainer);
this.forwardPropertiesToDefaultTooltips(aTooltipIds.length);
};
/**
* Assigns tooltips and forwards properties to them
*
* @param {Array}[aTooltipIds] Array of strings for ID generation
* @protected
*/
Slider.prototype.initAndSyncTooltips = function (aTooltipIds) {
var aCustomTooltips = this.getCustomTooltips(),
iCustomTooltipsCount = aCustomTooltips.length,
iMaxCustomTooltipCount = aTooltipIds.length;
this.initTooltipContainer();
// validates the count of passed tooltips and takes the needed count or fallbacks to default tooltips
if (iCustomTooltipsCount < iMaxCustomTooltipCount) {
this.assignDefaultTooltips(aTooltipIds);
} else {
// log a warning if tooltips are more than one for Slider or more than two for RangeSlider
if (iCustomTooltipsCount > iMaxCustomTooltipCount) {
log.warning("Warning: More than " + iMaxCustomTooltipCount + " Custom Tooltips are provided. Only the first will be used.");
}
// we use the first 2 tooltips of the aggregation
this.associateCustomTooltips(iMaxCustomTooltipCount);
}
};
/**
* Creates a default SliderTooltip instance and adds it as an aggregation
*
* @param {string}[sId] The tooltip ID
* @protected
*/
Slider.prototype.initDefaultTooltip = function (sId) {
var oTooltip = new SliderTooltip(this.getId() + "-" + sId, {
change: this.handleTooltipChange.bind(this)
});
this.getAggregation("_tooltipContainer").addAssociation("associatedTooltips", oTooltip, true);
this.addAggregation("_defaultTooltips", oTooltip, true);
};
/**
* Creates a SliderTooltipContainer
*
* @protected
*/
Slider.prototype.initTooltipContainer = function () {
if (!this._oTooltipContainer) {
this._oTooltipContainer = new SliderTooltipContainer();
this.setAggregation("_tooltipContainer", this._oTooltipContainer, true);
}
};
Slider.prototype._getMaxTooltipWidth = function () {
var aAbsRange = [Math.abs(this.getMin()), Math.abs(this.getMax())],
iRangeIndex = aAbsRange[0] > aAbsRange[1] ? 0 : 1;
return ((aAbsRange[iRangeIndex].toString()).length + this.getDecimalPrecisionOfNumber(this.getStep()) + 1) * SliderUtilities.CONSTANTS.CHARACTER_WIDTH_PX;
};
Slider.prototype.onAfterRendering = function () {
if (this.getShowAdvancedTooltip()) {
this._recalculateStyles();
this._handleTooltipContainerResponsiveness();
}
this._handleSliderResize({control: this});
this._registerResizeHandler();
};
/* =========================================================== */
/* Event handlers */
/* =========================================================== */
/**
* Handles the <code>touchstart</code> event.
*
* @param {jQuery.Event} oEvent The event object.
*/
Slider.prototype.ontouchstart = function(oEvent) {
var fMin = this.getMin(),
oTouch = oEvent.targetTouches[0],
fNewValue,
CSS_CLASS = this.getRenderer().CSS_CLASS,
sEventNamespace = "." + CSS_CLASS;
// mark the event for components that needs to know if the event was handled
oEvent.setMarked();
// Should be prevent as in Safari while dragging the handle everything else gets selection.
// As part of the Slider, Inputs in the tooltips should be excluded
if (oEvent.target.className.indexOf("sapMInput") === -1) {
oEvent.preventDefault();
}
this.focus();
// only process single touches
if (touch.countContained(oEvent.touches, this.getId()) > 1 ||
!this.getEnabled() ||
// detect which mouse button caused the event and only process the standard click
// (this is usually the left button, oEvent.button === 0 for standard click)
// note: if the current event is a touch event oEvent.button property will be not defined
oEvent.button ||
// process the event if the target is not a composite control e.g.: a tooltip
(oEvent.srcControl !== this)) {
return;
}
// track the id of the first active touch point
this._iActiveTouchId = oTouch.identifier;
// registers event listeners
jQuery(document).on("touchend" + sEventNamespace + " touchcancel" + sEventNamespace + " mouseup" + sEventNamespace, this._ontouchend.bind(this))
.on(oEvent.originalEvent.type === "touchstart" ? "touchmove" + sEventNamespace : "touchmove" + sEventNamespace + " mousemove" + sEventNamespace, this._ontouchmove.bind(this));
var oNearestHandleDomRef = this.getClosestHandleDomRef();
if (oTouch.target !== oNearestHandleDomRef) {
// set the focus to the nearest slider handle
setTimeout(oNearestHandleDomRef["focus"].bind(oNearestHandleDomRef), 0);
}
// recalculate some styles,
// those values may change when the device orientation changes
this._recalculateStyles();
this._fDiffX = this._fSliderPaddingLeft;
this._fInitialValue = this.getValue();
// add active state
this.$("inner").addClass(CSS_CLASS + "Pressed");
this.setProperty("handlePressed", true);
if (oTouch.target === this.getDomRef("handle")) {
this._fDiffX = (oTouch.pageX - jQuery(oNearestHandleDomRef).offset().left) + this._fSliderPaddingLeft - (this._fHandleWidth / 2);
} else {
fNewValue = (((oTouch.pageX - this._fSliderPaddingLeft - this._fSliderOffsetLeft) / this._fSliderWidth) * (this.getMax() - fMin)) + fMin;
if (Localization.getRTL()) {
fNewValue = this._convertValueToRtlMode(fNewValue);
}
// update the value
this.setValue(fNewValue);
// new validated value
fNewValue = this.getValue();
if (this._fInitialValue !== fNewValue) {
this.fireLiveChange({ value: fNewValue });
}
}
};
/**
* Handles the <code>touchmove</code> event.
*
* @param {jQuery.Event} oEvent The event object.
* @private
*/
Slider.prototype._ontouchmove = function(oEvent) {
// mark the event for components that needs to know if the event was handled
oEvent.setMarked();
// note: prevent native document scrolling
oEvent.preventDefault();
// suppress the emulated mouse event from touch interfaces
if (oEvent.isMarked("delayedMouseEvent") ||
!this.getEnabled() ||
// detect which mouse button caused the event and only process the standard click
// (this is usually the left button, oEvent.button === 0 for standard click)
// note: if the current event is a touch event oEvent.button property will be not defined
oEvent.button) {
return;
}
var fMin = this.getMin(),
fValue = this.getValue(),
oTouch = touch.find(oEvent.changedTouches, this._iActiveTouchId), // find the active touch point
iPageX = oTouch ? oTouch.pageX : oEvent.pageX,
fNewValue = (((iPageX - this._fDiffX - this._fSliderOffsetLeft) / this._fSliderWidth) * (this.getMax() - fMin)) + fMin;
// RTL mirror
if (Localization.getRTL()) {
fNewValue = this._convertValueToRtlMode(fNewValue);
}
this.setValue(fNewValue);
// validated value
fNewValue = this.getValue();
if (fValue !== fNewValue) {
this.fireLiveChange({ value: fNewValue });
}
};
/**
* Handles the <code>touchend</code> event.
*
* @param {jQuery.Event} oEvent The event object.
* @private
*/
Slider.prototype._ontouchend = function(oEvent) {
var CSS_CLASS = this.getRenderer().CSS_CLASS,
sEventNamespace = "." + CSS_CLASS;
// mark the event for components that needs to know if the event was handled
oEvent.setMarked();
// suppress the emulated mouse event from touch interfaces
if (oEvent.isMarked("delayedMouseEvent") ||
!this.getEnabled() ||
// detect which mouse button caused the event and only process the standard click
// (this is usually the left button, oEvent.button === 0 for standard click)
// note: if the current event is a touch event oEvent.button property will be not defined
oEvent.button) {
return;
}
// removes the registered event listeners
jQuery(document).off(sEventNamespace);
var fValue = this.getValue();
// remove the active state
this.setProperty("handlePressed", false);
if (this._fInitialValue !== fValue) {
this.fireChange({ value: fValue });
}
};
/**
* Handles the <code>focusin</code> event.
*
* @param {jQuery.Event} oEvent The event object.
*/
Slider.prototype.onfocusin = function(oEvent) {
this._fValueBeforeFocus = this.getValue();
if (this.getShowAdvancedTooltip()) {
this.getAggregation("_tooltipContainer").show(this);
this._setAriaControls();
this.updateAdvancedTooltipDom(this.getValue());
}
};
/**
* Adds aria-controls attribute, when the tooltips are rendered.
*
* @private
*/
Slider.prototype._setAriaControls = function () {
var oTooltip = this.getUsedTooltips()[0],
oHandle = this.getFocusDomRef();
if (this.getInputsAsTooltips() && oTooltip && oTooltip.getDomRef()) {
oHandle.setAttribute("aria-controls", oTooltip.getId());
}
};
/**
* Handles the <code>focusout</code> event.
*
* @param {jQuery.Event} oEvent The event object.
*/
Slider.prototype.onfocusout = function(oEvent) {
if (!this.getShowAdvancedTooltip()) {
return;
}
var bSliderFocused = jQuery.contains(this.getDomRef(), oEvent.relatedTarget),
bTooltipFocused = jQuery.contains(this.getAggregation("_tooltipContainer").getDomRef(), oEvent.relatedTarget);
if (bSliderFocused || bTooltipFocused) {
return;
}
this.getAggregation("_tooltipContainer").hide();
};
Slider.prototype.onmouseover = function(oEvent) {
var bTooltipFocused, oTooltipContainer;
if (this.getShowAdvancedTooltip()) {
this.getAggregation("_tooltipContainer").show(this);
oTooltipContainer = this.getAggregation("_tooltipContainer");
bTooltipFocused = jQuery.contains(oTooltipContainer.getDomRef(), document.activeElement);
this._setAriaControls();
// do not update Tooltip's value if it is already focused
if (bTooltipFocused) {
return;
}
this.updateAdvancedTooltipDom(this.getValue());
}
};
Slider.prototype.onmouseout = function (oEvent) {
if (!this.getShowAdvancedTooltip()) {
return;
}
var oTooltipContianerRef = this.getAggregation("_tooltipContainer").getDomRef(),
oSliderRef = this.getDomRef(),
bHandleFocused = jQuery.contains(oSliderRef, document.activeElement),
bTooltipFocused = jQuery.contains(oTooltipContianerRef, document.activeElement);
if (!oTooltipContianerRef || bHandleFocused || bTooltipFocused) {
return;
}
if (jQuery.contains(this.getDomRef(), oEvent.toElement) || (oSliderRef === oEvent.toElement)) {
return;
}
if (jQuery.contains(this.getAggregation("_tooltipContainer").getDomRef(), oEvent.toElement)) {
return;
}
this.getAggregation("_tooltipContainer").hide();
};
/* ----------------------------------------------------------- */
/* Keyboard handling */
/* ----------------------------------------------------------- */
/**
* Slider should focus its inputs of they are advanced and editable on F2.
*
* @param {jQuery.Event} oEvent The event object.
*/
Slider.prototype.onkeydown = function (oEvent) {
var aTooltips = this.getUsedTooltips();
if (oEvent.keyCode === SliderUtilities.CONSTANTS.F2_KEYCODE && aTooltips[0] && this.getInputsAsTooltips()) {
aTooltips[0].focus();
}
if (oEvent.keyCode === KeyCodes.SPACE) {
oEvent.preventDefault();
}
};
/**
* Handles the <code>sapincrease</code> event when right arrow or up arrow is pressed.
*
* @param {jQuery.Event} oEvent The event object.
*/
Slider.prototype.onsapincrease = function(oEvent) {
var fValue,
fNewValue;
// note: prevent document scrolling when arrow keys are pressed
oEvent.preventDefault();
// mark the event for components that needs to know if the event was handled
oEvent.setMarked();
if (this.getEnabled()) {
fValue = this.getValue();
this.stepUp(1);
fNewValue = this.getValue();
if (fValue < fNewValue) {
this._fireChangeAndLiveChange({ value: fNewValue });
}
}
this._showTooltipsIfNeeded();
};
/**
* Handles the <code>sapincreasemodifiers</code> event when Ctrl + right arrow or up arrow are pressed.
*
* @param {jQuery.Event} oEvent The event object.
*/
Slider.prototype.onsapincreasemodifiers = function(oEvent) {
if (oEvent.altKey) {
return;
}
// note: prevent document scrolling when arrow keys are pressed
oEvent.preventDefault();
oEvent.stopPropagation();
// mark the event for components that needs to know if the event was handled
oEvent.setMarked();
this._increaseValueBy(this._getLongStep());
this._showTooltipsIfNeeded();
};
/**
* Handles the <code>sapdecrease</code> event when left arrow or down arrow are pressed.
*
* @param {jQuery.Event} oEvent The event object.
*/
Slider.prototype.onsapdecrease = function(oEvent) {
var fValue,
fNewValue;
// note: prevent document scrolling when arrow keys are pressed
oEvent.preventDefault();
// mark the event for components that needs to know if the event was handled
oEvent.setMarked();
if (this.getEnabled()) {
fValue = this.getValue();
this.stepDown(1);
fNewValue = this.getValue();
if (fValue > fNewValue) {
this._fireChangeAndLiveChange({ value: fNewValue });
}
}
this._showTooltipsIfNeeded();
};
/**
* Handles the <code>sapdecreasemodifiers</code> event when Ctrl + left or Ctrl + down keys are pressed.
*
* @param {jQuery.Event} oEvent The event object.
*/
Slider.prototype.onsapdecreasemodifiers = function(oEvent) {
if (oEvent.altKey) {
return;
}
// note: prevent document scrolling when arrow keys are pressed
oEvent.preventDefault();
oEvent.stopPropagation();
// mark the event for components that needs to know if the event was handled
oEvent.setMarked();
this._decreaseValueBy(this._getLongStep());
this._showTooltipsIfNeeded();
};
/**
* Handles the <code>onsapplus</code> event when "+" is pressed.
*
* @param {jQuery.Event} oEvent The event object.
*/
Slider.prototype.onsapplus = function(oEvent) {
var fValue,
fNewValue;
// mark the event for components that needs to know if the event was handled
oEvent.setMarked();
if (this.getEnabled()) {
fValue = this.getValue();
this.stepUp(1);
fNewValue = this.getValue();
if (fValue < fNewValue) {
this._fireChangeAndLiveChange({ value: fNewValue });
}
}
this._showTooltipsIfNeeded();
};
/**
* Handles the <code>sapminus</code> event when "-" is pressed.
*
* @param {jQuery.Event} oEvent The event object.
*/
Slider.prototype.onsapminus = function(oEvent) {
var fValue,
fNewValue;
// mark the event for components that needs to know if the event was handled
oEvent.setMarked();
if (this.getEnabled()) {
fValue = this.getValue();
this.stepDown(1);
fNewValue = this.getValue();
if (fValue > fNewValue) {
this._fireChangeAndLiveChange({ value: fNewValue });
}
}
this._showTooltipsIfNeeded();
};
/**
* Handles the <code>sapescape</code> event when escape key is pressed.
*
*/
Slider.prototype.onsapescape = function() {
// reset the slider back to the value
// which it had when it got the focus
this.setValue(this._fValueBeforeFocus);
};
/**
* Handles the <code>sappageup</code> event when page up is pressed.
*
* @param {jQuery.Event} oEvent The event object.
*/
Slider.prototype.onsappageup = Slider.prototype.onsapincreasemodifiers;
/**
* Handles the <code>sappagedown</code> event when when page down is pressed.
*
* @param {jQuery.Event} oEvent The event object.
*/
Slider.prototype.onsappagedown = Slider.prototype.onsapdecreasemodifiers;
/**
* Handles the <code>saphome</code> event when home key is pressed.
*
* @param {jQuery.Event} oEvent The event object.
*/
Slider.prototype.onsaphome = function(oEvent) {
// mark the event for components that needs to know if the event was handled
oEvent.setMarked();
var fMin = this.getMin();
// note: prevent document scrolling when Home key is pressed
oEvent.preventDefault();
if (this.getEnabled() && this.getValue() > fMin) {
this.setValue(fMin);
this._fireChangeAndLiveChange({ value: fMin });
}
this._showTooltipsIfNeeded();
};
/**
* Handles the <code>sapend</code> event when the End key pressed.
*
* @param {jQuery.Event} oEvent The event object.
*/
Slider.prototype.onsapend = function(oEvent) {
// mark the event for components that needs to know if the event was handled
oEvent.setMarked();
var fMax = this.getMax();
// note: prevent document scrolling when End key is pressed
oEvent.preventDefault();
if (this.getEnabled() && this.getValue() < fMax) {
this.setValue(fMax);
this._fireChangeAndLiveChange({ value: fMax });
}
this._showTooltipsIfNeeded();
};
/* =========================================================== */
/* API method */
/* =========================================================== */
/* ----------------------------------------------------------- */
/* Public methods */
/* ----------------------------------------------------------- */
Slider.prototype.getFocusDomRef = function() {
return this.getDomRef("handle");
};
/**
* Increments the value by multiplying the <code>step</code> with the given parameter.
*
* @param {int} [iStep=1] The number of steps the slider goes up.
* @returns {this} <code>this</code> to allow method chaining.
* @public
*/
Slider.prototype.stepUp = function(iStep) {
return this.setValue(this.getValue() + (this._validateStep(iStep) * this.getStep()), { snapValue: false });
};
/**
* Decrements the value by multiplying the step the <code>step</code> with the given parameter.
*
* @param {int} [iStep=1] The number of steps the slider goes down.
* @returns {this} <code>this</code> to allow method chaining.
* @public
*/
Slider.prototype.stepDown = function(iStep) {
return this.setValue(this.getValue() - (this._validateStep(iStep) * this.getStep()), { snapValue: false });
};
/**
* Sets the property <code>value</code>.
*
* Default value is <code>0</code>.
*
* @param {float} fNewValue new value for property <code>value</code>.
* @param {{snapValue: boolean}|object} mOptions The options object
* @returns {this} <code>this</code> to allow method chaining.
* @public
*/
Slider.prototype.setValue = function(fNewValue, mOptions) {
// note: sometimes the setValue() method is call before the step, max and min
// properties are set, in this case the value should not be adjusted
if (this._bSetValueFirstCall) {
this._bSetValueFirstCall = false;
return this.setProperty("value", fNewValue, true);
}
var fMin = this.getMin(),
fMax = this.getMax(),
fStep = this.getStep(),
fValue = this.getValue(),
sNewValueFixedPoint,
bSnapValue = true,
fModStepVal;
if (mOptions) {
bSnapValue = !!mOptions.snapValue;
}
// validate the new value before arithmetic calculations
if (typeof fNewValue !== "number" || !isFinite(fNewValue)) {
return this;
}
fModStepVal = Math.abs((fNewValue - fMin) % fStep);
if (bSnapValue && (fModStepVal !== 0) /* division with remainder */) {
// snap the new value to the nearest step
fNewValue = fModStepVal * 2 >= fStep ? fNewValue + fStep - fModStepVal : fNewValue - fModStepVal;
}
// constrain the new value between the minimum and maximum
if (fNewValue < fMin) {
fNewValue = fMin;
} else if (fNewValue > fMax) {
fNewValue = fMax;
}
sNewValueFixedPoint =