@openui5/sap.m
Version:
OpenUI5 UI Library sap.m
1,201 lines (981 loc) • 49.6 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2009-2023 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
sap.ui.define([
"sap/ui/core/InvisibleText",
"sap/base/Log",
"./Slider",
"./SliderUtilities",
"./RangeSliderRenderer",
"sap/ui/thirdparty/jquery",
"sap/ui/events/KeyCodes",
"sap/ui/core/Configuration"
],
function(
InvisibleText,
log,
Slider,
SliderUtilities,
RangeSliderRenderer,
jQuery,
KeyCodes,
Configuration
) {
"use strict";
/**
* Constructor for a new <code>RangeSlider</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
* Represents a numerical interval and two handles to select a sub-range within it.
* <h3>Overview</h3>
* The purpose of the control is to enable visual selection of sub-ranges within a given interval.
* <h4>Notes:<h4>
* <ul>
* <li>The RangeSlider extends the functionality of the {@link sap.m.Slider Slider}</li>
* <li>The right and left handle can be moved individually and their positions could therefore switch.</li>
* <li>The entire range can be moved along the interval.</li>
* <li>The right and left handle can select the same value</li>
* </ul>
*
* <h3>Usage</h3>
* The most common usecase is to select and move sub-ranges on a continuous numerical scale.
*
* <h3>Responsive Behavior</h3>
* You can move the currently selected range by clicking on it and dragging it along the interval.
* @extends sap.m.Slider
*
* @author SAP SE
* @version 1.117.4
*
* @constructor
* @public
* @since 1.38
* @alias sap.m.RangeSlider
* @see {@link fiori:https://experience.sap.com/fiori-design-web/range-slider/ Range Slider}
*/
var RangeSlider = Slider.extend("sap.m.RangeSlider", /** @lends sap.m.RangeSlider.prototype */ {
metadata: {
library: "sap.m",
properties: {
/**
* Current second value of the slider. (Position of the second handle.)
*
* <b>Note:</b> If the value is not in the valid range (between <code>min</code> and <code>max</code>) it will be changed to be in the valid range.
* If it is smaller than <code>value</code> it will be set to the same value.
*/
value2: {type: "float", group: "Data", defaultValue: 100},
/**
* Determines the currently selected range on the slider.
*
* If the value is lower/higher than the allowed minimum/maximum, a warning message will be output to the console.
*/
range: {type: "float[]", group: "Data", defaultValue: [0,100]},
/**
* Stores positioning info regarding progress element DOM
*
* @private
*/
progressBarSize: { type: "object", visibility: "hidden" },
/**
* Identifies if start handle is pressed
*
* @private
*/
startHandlePressed: { type: "boolean", visibility: "hidden" },
/**
* Identifies if end handle is pressed
*
* @private
*/
endHandlePressed: { type: "boolean", visibility: "hidden" }
},
designtime: "sap/m/designtime/RangeSlider.designtime"
},
renderer: RangeSliderRenderer
});
RangeSlider.prototype.init = function () {
var oStartLabel, oEndLabel, oRangeLabel;
Slider.prototype.init.call(this, arguments);
// Do not execute "_adjustRangeValue" before all initial setters are finished.
// As max, min, range, value and value2 are dependent on each other,
// we should be sure that at the first run they are set properly and then to be validated.
this._bInitialRangeChecks = true;
// the initial focus range which should be used
this._aInitialFocusRange = this.getRange();
this._oResourceBundle = sap.ui.getCore().getLibraryResourceBundle('sap.m');
this._ariaUpdateDelay = [];
oStartLabel = new InvisibleText({
text: this._oResourceBundle.getText("RANGE_SLIDER_LEFT_HANDLE")
});
oEndLabel = new InvisibleText({
text: this._oResourceBundle.getText("RANGE_SLIDER_RIGHT_HANDLE")
});
oRangeLabel = new InvisibleText({
text: this._oResourceBundle.getText("RANGE_SLIDER_RANGE_HANDLE")
});
// clear Slider's tooltip
this.destroyAggregation("_handlesLabels", true);
this.addAggregation("_handlesLabels", oStartLabel);
this.addAggregation("_handlesLabels", oEndLabel);
this.addAggregation("_handlesLabels", oRangeLabel);
this._mHandleTooltip = {
start: {
handle: null, // Handle is provided by the renderer, available onAfterRendering
tooltip: null,
label: oStartLabel
},
end: {
handle: null, // Handle is provided by the renderer, available onAfterRendering
tooltip: null,
label: oEndLabel
}
};
};
RangeSlider.prototype.exit = function () {
Slider.prototype.exit.apply(this, arguments);
this._oResourceBundle = null;
this._aInitialFocusRange = null;
this._liveChangeLastValue = null;
this._mHandleTooltip.start.handle = null;
this._mHandleTooltip.start.tooltip = null;
this._mHandleTooltip.start.label = null;
this._mHandleTooltip.end.handle = null;
this._mHandleTooltip.end.tooltip = null;
this._mHandleTooltip.end.label = null;
this._ariaUpdateDelay = null;
};
RangeSlider.prototype.onBeforeRendering = function () {
this._bRTL = Configuration.getRTL();
var aRange = this.getRange();
if (this.getShowAdvancedTooltip()) {
this.initAndSyncTooltips(["leftTooltip", "rightTooltip"]);
this._storeTooltipsMetadata();
}
this._recalculateRange();
// At this point it's certain that all setters are executed and values of
// min, max, value, value2 and range are set properly and are not using the Default values.
// It's important however to keep the slider values within the boundaries defined by min and max.
// Executing once again the range setter would adjust values accordingly. It should not matter if we do:
// this.setRange(aRange) OR this.setValue(fValue) && this.setValue2(fValue2).
// Note: this.getRange() is intended to have the same value as [this.getValue(), this.getValue2()]
this._bInitialRangeChecks = false;
// We need the decimal precision in order to be able to set the correct values.
// It is well known that JavaScript has issues with handling floating point values.
// E.g. 0.0001 + 0.0002 = 0.00029999999999999998
this._iDecimalPrecision = this.getDecimalPrecisionOfNumber(this.getStep());
this.setRange(aRange);
this._validateProperties();
// set the correct scale aggregation, if needed
this._syncScaleUsage();
};
RangeSlider.prototype.onAfterRendering = function () {
Slider.prototype.onAfterRendering.apply(this, arguments);
var aRange = this.getRange();
this._mHandleTooltip.start.handle = this.getDomRef("handle1");
this._mHandleTooltip.end.handle = this.getDomRef("handle2");
this._recalculateStyles();
// Setting the handles to the Start and the End points of the provided or the default range
this._updateHandle(this._mHandleTooltip.start.handle, aRange[0]);
this._updateHandle(this._mHandleTooltip.end.handle, aRange[1]);
//Swap tooltips so when range is with reversed values e.g. [12, 1]
//to have properly updated tooltips
if (this.getShowAdvancedTooltip() && (aRange[0] > aRange[1])) {
this._swapTooltips(aRange);
}
};
RangeSlider.prototype._storeTooltipsMetadata = function () {
var aTooltips = this.getUsedTooltips();
// Attach tooltips
if (!this._mHandleTooltip.start.tooltip) {
this._mHandleTooltip.start.tooltip = aTooltips[0];
}
if (!this._mHandleTooltip.end.tooltip) {
this._mHandleTooltip.end.tooltip = aTooltips[1];
}
};
/**
* Recalculates the progress range and updates the progress indicator styles.
* @private
*/
RangeSlider.prototype._recalculateRange = function () {
var aHandlesLeftOffset,
fValue = this.getValue(),
fValue2 = this.getValue2(),
sStart, sEnd;
aHandlesLeftOffset = [
this._getPercentOfValue(this._bRTL ? fValue2 : fValue),
this._getPercentOfValue(this._bRTL ? fValue : fValue2)
];
sStart = Math.min.apply(Math, aHandlesLeftOffset) + "%";
sEnd = (100 - Math.max.apply(Math, aHandlesLeftOffset)) + "%";
this.setProperty("progressBarSize", {
left: this._bRTL ? sEnd : sStart,
right: this._bRTL ? sStart : sEnd
});
};
/**
* Gets the closest to the oEvent x coordinate handle dom element.
* @param {jQuery.Event} oEvent The event object
* @returns {HTMLElement} The handle, from which the event comes from.
* @private
* @override
*/
RangeSlider.prototype.getClosestHandleDomRef = function (oEvent) {
var oHandle1 = this._mHandleTooltip.start.handle,
oHandle2 = this._mHandleTooltip.end.handle,
fPageXCalc = Math.abs(oEvent.pageX - oHandle1.offsetLeft - this._fSliderPaddingLeft - this._fSliderOffsetLeft),
fClientXCalc = Math.abs(oEvent.clientX - oHandle2.offsetLeft - this._fSliderPaddingLeft - this._fSliderOffsetLeft);
return fPageXCalc > fClientXCalc ? oHandle2 : oHandle1;
};
RangeSlider.prototype._getIndexOfHandle = function (oHandle) {
if (oHandle && oHandle.getAttribute && oHandle.getAttribute("data-range-val") === "start") {
return 0;
} else if (oHandle && oHandle.getAttribute && oHandle.getAttribute("data-range-val") === "end") {
return 1;
} else {
return -1;
}
};
/**
* Gets a handle corresponding to a tooltip
* @param {sap.m.SliderTooltipBase} oTooltip Slider/Range slider tooltip
* @returns {HTMLElement} The handle, from which the tooltip is responsible.
* @private
* @ui5-restricted sap.m.SliderTooltipContainer
*/
RangeSlider.prototype._getHandleForTooltip = function (oTooltip) {
var oHandle = oTooltip === this._mHandleTooltip.start.tooltip ?
this._mHandleTooltip.start.handle : this._mHandleTooltip.end.handle;
return oHandle;
};
/**
* Updates the handle with the given new value and recalculates the progress indicator
* @param {HTMLElement} oHandle The handle that should be updated
* @param {float} fValue The new value of the handle
* @private
*/
RangeSlider.prototype._updateHandle = function (oHandle, fValue) {
var oTooltip = (this._mHandleTooltip.start.handle === oHandle) ?
this._mHandleTooltip.start.tooltip : this._mHandleTooltip.end.tooltip,
aRange = this.getRange(),
iIndex = this._getIndexOfHandle(oHandle),
fPercentVal = this._getPercentOfValue(fValue);
aRange[iIndex] = fValue;
this._updateRangePropertyDependencies(aRange);
this._updateHandleDom(oHandle, aRange, iIndex, fValue, fPercentVal);
if (this.getShowAdvancedTooltip()) {
this._updateTooltipContent(oTooltip, fValue);
this._adjustTooltipsContainer();
}
};
RangeSlider.prototype._updateHandleDom = function (oHandle, aRange, iIndex, sValue, fPercentVal) {
var bMergedRanges,
sCssClass = this.getRenderer().CSS_CLASS,
oFormInput = this.getDomRef("input");
if (this.getName()) {
oFormInput.setAttribute(oHandle.getAttribute("data-range-val"), this.toFixed(aRange[iIndex], this._iDecimalPrecision));
oFormInput.setAttribute("value", this.getValue());
}
if (this._bRTL) {
oHandle.style.right = fPercentVal + "%";
} else {
oHandle.style.left = fPercentVal + "%";
}
if (this.getShowHandleTooltip() && !this.getShowAdvancedTooltip()) {
oHandle.title = this._formatValueByCustomElement(sValue);
}
bMergedRanges = aRange[0] === aRange[1];
this.$("handle1").toggleClass(sCssClass + "HandleOverlap", bMergedRanges);
this.$("handle2").toggleClass(sCssClass + "HandleOverlap", bMergedRanges);
// ARIA updates. Delay the update to prevent multiple updates- for example holding the arrow key.
// We need only the latest state
clearTimeout(this._ariaUpdateDelay[iIndex]);
this._ariaUpdateDelay[iIndex] = setTimeout(this["_updateHandleAria"].bind(this, oHandle, sValue), 100);
};
RangeSlider.prototype._updateHandleAria = function (oHandle, sValue) {
var aRange = this.getRange(),
oProgressHandle = this.getDomRef("progress"),
fNormalizedValue = this.toFixed(sValue, this._iDecimalPrecision),
sScaleLabel = this._formatValueByCustomElement(fNormalizedValue),
firstValue = this.getValue(),
secondValue = this.getValue2(),
iValueNow = Math.abs(secondValue - firstValue);
aRange[0] = this.toFixed(aRange[0], this._iDecimalPrecision);
aRange[1] = this.toFixed(aRange[1], this._iDecimalPrecision);
this._updateHandlesAriaLabels();
this._updateHandleAriaAttributeValues(oHandle, sValue, sScaleLabel);
if (oProgressHandle) {
oProgressHandle.setAttribute("aria-valuetext",
this._oResourceBundle.getText('RANGE_SLIDER_RANGE_ANNOUNCEMENT', aRange.map(this._formatValueByCustomElement, this)));
oProgressHandle.setAttribute("aria-valuenow", iValueNow);
}
};
/**
* Updates handles' ARIA Labels.
* When handles are swapped, the corresponding labels should be swapped too. So the state would be always acurate
*
* @private
*/
RangeSlider.prototype._updateHandlesAriaLabels = function () {
var aRange = this.getRange(),
oTempLabel = this._mHandleTooltip.start.label;
if ((aRange[0] > aRange[1] && !this._mHandleTooltip.bAriaHandlesSwapped) ||
(aRange[0] < aRange[1] && this._mHandleTooltip.bAriaHandlesSwapped)) {
this._mHandleTooltip.start.label = this._mHandleTooltip.end.label;
this._mHandleTooltip.end.label = oTempLabel;
if (this._mHandleTooltip.start.handle) {
this._mHandleTooltip.start.handle.setAttribute("aria-labelledby", this._mHandleTooltip.start.label.getId());
}
if (this._mHandleTooltip.end.handle) {
this._mHandleTooltip.end.handle.setAttribute("aria-labelledby", this._mHandleTooltip.end.label.getId());
}
this._mHandleTooltip.bAriaHandlesSwapped = !this._mHandleTooltip.bAriaHandlesSwapped;
}
};
/**
* Adds aria-controls attribute, when the tooltips are rendered.
*
* @private
*/
RangeSlider.prototype._setAriaControls = function () {
if (!this.getShowAdvancedTooltip()) {
return;
}
if (!this._mHandleTooltip.start.handle.getAttribute('aria-controls') && this._mHandleTooltip.start.tooltip) {
this._mHandleTooltip.start.handle.setAttribute('aria-controls', this._mHandleTooltip.start.tooltip.getId());
}
if (!this._mHandleTooltip.end.handle.getAttribute('aria-controls') && this._mHandleTooltip.end.tooltip) {
this._mHandleTooltip.end.handle.setAttribute('aria-controls', this._mHandleTooltip.end.tooltip.getId());
}
};
/**
* Updates the handle's tooltip value
* @param {Object} oTooltip The tooltip object.
* @param {float} fNewValue The new value
* @private
*/
RangeSlider.prototype._updateTooltipContent = function (oTooltip, fNewValue) {
var sNewValue = this.toFixed(fNewValue, this._iDecimalPrecision);
if (oTooltip) {
oTooltip.setValue(parseFloat(sNewValue));
}
};
RangeSlider.prototype._swapTooltips = function (aRange) {
var oTempTooltip = this._mHandleTooltip.start.tooltip;
var bShouldSwapTooltipsToTheRight = aRange[0] >= aRange[1] && !this._mHandleTooltip.bTooltipsSwapped;
var bShouldRevertTooltipsSwapping = (aRange[0] < aRange[1]) && this._mHandleTooltip.bTooltipsSwapped;
if (bShouldSwapTooltipsToTheRight || bShouldRevertTooltipsSwapping) {
this._mHandleTooltip.start.tooltip = this._mHandleTooltip.end.tooltip;
this._mHandleTooltip.end.tooltip = oTempTooltip;
this._updateTooltipContent(this._mHandleTooltip.start.tooltip, aRange[0]);
this._updateTooltipContent(this._mHandleTooltip.end.tooltip, aRange[1]);
// After swapping tooltips, also some accessibility properties should be updated
if (this.getInputsAsTooltips()) {
this._mHandleTooltip.start.handle.setAttribute("aria-controls", this._mHandleTooltip.start.tooltip.getId());
this._mHandleTooltip.end.handle.setAttribute("aria-controls", this._mHandleTooltip.end.tooltip.getId());
}
if (bShouldRevertTooltipsSwapping) {
this._mHandleTooltip.bTooltipsSwapped = false;
}
if (bShouldSwapTooltipsToTheRight) {
this._mHandleTooltip.bTooltipsSwapped = true;
}
}
};
RangeSlider.prototype._adjustTooltipsContainer = function () {
var oTooltipsContainer = this.getAggregation("_tooltipContainer");
if (!oTooltipsContainer.getDomRef()) {
return;
}
oTooltipsContainer.repositionTooltips(this.getMin(), this.getMax());
this._swapTooltips(this.getRange());
};
/**
* Gets the tooltips that should be shown.
* Returns custom tooltips if provided and more than 1 else default tooltips
*
* @protected
* @override
* @returns {sap.m.SliderTooltipBase[]} SliderTooltipBase instances.
*/
RangeSlider.prototype.getUsedTooltips = function () {
var aCustomTooltips = this.getCustomTooltips(),
aDefaultTooltips = this.getAggregation("_defaultTooltips") || [];
return aCustomTooltips.length > 1 ? aCustomTooltips : aDefaultTooltips;
};
/**
* Handles changes in Tooltip Inputs
* @param {jQuery.Event} oEvent The event object
* @private
* @override
*/
RangeSlider.prototype.handleTooltipChange = function (oEvent) {
this.updateTooltipsPositionAndState(oEvent.getSource(), Number(oEvent.getParameter("value")));
};
/**
* Updates values of RangeSlider and repositions tooltips.
*
* @param {string} oTooltip Tooltip to be changed
* @param {float} fValue New value of the RangeSlider
* @private
* @ui5-restricted sap.m.SliderTooltipBase
*/
RangeSlider.prototype.updateTooltipsPositionAndState = function (oTooltip, fValue, bPreventStateUpdate) {
var oHandle, oActiveTooltip,
bTooltipsInitialPositionTouched = this._mHandleTooltip.bTooltipsSwapped;
fValue = this._adjustRangeValue(fValue);
oHandle = this._mHandleTooltip.start.tooltip === oTooltip ?
this._mHandleTooltip.start.handle : this._mHandleTooltip.end.handle;
this._updateHandle(oHandle, fValue);
// When tooltips are swapped, we should put the focus to the corresponding visual representation
if (bTooltipsInitialPositionTouched !== this._mHandleTooltip.bTooltipsSwapped) {
oActiveTooltip = this._mHandleTooltip.start.tooltip !== oTooltip ?
this._mHandleTooltip.start.tooltip : this._mHandleTooltip.end.tooltip;
oActiveTooltip.focus();
}
if (!bPreventStateUpdate) {
this._fireChangeAndLiveChange({ range: this.getRange() });
}
this.updateAdvancedTooltipDom();
};
RangeSlider.prototype._updateDOMAfterSetters = function (fValue, aRange, iHandleIndex) {
var fPercentOfValue, oHandle;
if (this.getDomRef()) {
fPercentOfValue = this._getPercentOfValue(fValue);
oHandle = iHandleIndex === 1 ? this._mHandleTooltip.end : this._mHandleTooltip.start;
this._updateHandleDom(oHandle.handle, aRange, iHandleIndex, fValue, fPercentOfValue);
if (this.getShowAdvancedTooltip()) {
this._updateTooltipContent(oHandle.tooltip, fValue);
}
return true;
}
return false;
};
RangeSlider.prototype.setRange = function (aRange) {
aRange = aRange.map(this._adjustRangeValue, this);
this._updateRangePropertyDependencies(aRange);
return this;
};
RangeSlider.prototype.setStep = function (fStep) {
//Log warning in case fStep is not valid
this._validateProperties();
//Get the new decimal precision
this._iDecimalPrecision = this.getDecimalPrecisionOfNumber(fStep);
// invalidate as setStep is not called regularly
return this.setProperty("step", fStep);
};
RangeSlider.prototype.setValue = function (fValue) {
var aRange = this.getRange();
// validate the new value before arithmetic calculations
if (typeof fValue !== "number" || !isFinite(fValue)) {
return this;
}
fValue = this._adjustRangeValue(fValue);
aRange[0] = fValue;
this._updateRangePropertyDependencies(aRange);
if (this.getShowAdvancedTooltip() && this._mHandleTooltip.start.tooltip) {
this.updateTooltipsPositionAndState(this._mHandleTooltip.start.tooltip, fValue, true);
}
return this;
};
RangeSlider.prototype.setValue2 = function (fValue) {
var aRange = this.getRange();
fValue = this._adjustRangeValue(fValue);
aRange[1] = fValue;
this._updateRangePropertyDependencies(aRange);
if (this.getShowAdvancedTooltip() && this._mHandleTooltip.end.tooltip) {
this.updateTooltipsPositionAndState(this._mHandleTooltip.end.tooltip, fValue, true);
}
return this;
};
RangeSlider.prototype._updateRangePropertyDependencies = function (aRange) {
var aRangeCopy = Array.isArray(aRange) ? aRange.slice() : [],
iDecimal = this._iDecimalPrecision ? this._iDecimalPrecision : 0,
fNewValue = Number(aRangeCopy[0].toFixed(iDecimal)),
fNewValue2 = Number(aRangeCopy[1].toFixed(iDecimal));
if (this.getProperty("value") !== fNewValue) {
this.setProperty("value", fNewValue);
aRangeCopy[0] = fNewValue;
}
if (this.getProperty("value2") !== fNewValue2) {
this.setProperty("value2", fNewValue2);
aRangeCopy[1] = fNewValue2;
}
this.setProperty("range", aRangeCopy);
};
/**
* Calculates the value for the handle of the RangeSlider based on the passed raw pageX event coordinates.
* This will be later transformed to % of the total width of the RangeSlider in order for the handle to be
* updated accordingly.
* @param {float} fValue The new raw value of the handle.
* @returns {float} The new calculated value of the handle.
* @private
*/
RangeSlider.prototype._calculateHandlePosition = function (fValue) {
var fMax = this.getMax(),
fMin = this.getMin(),
fNewValue;
fNewValue = ((fValue - this._fSliderPaddingLeft - this._fSliderOffsetLeft) / this._fSliderWidth) * (fMax - fMin) + fMin;
// RTL mirror
if (this._bRTL) {
fNewValue = this._convertValueToRtlMode(fNewValue);
}
return this._adjustRangeValue(fNewValue);
};
/**
* Checks and adjusts value according to Min and Max properties and checks RTL mode
*
* @param {float} fValue Value to be checked and adjusted
* @returns {float} Adjusted value
* @private
*/
RangeSlider.prototype._adjustRangeValue = function (fValue) {
var fMax = this.getMax(),
fMin = this.getMin(),
fStep = this.getStep(),
fModStepVal;
if (this._bInitialRangeChecks) {
return fValue;
}
fModStepVal = Math.abs((fValue - fMin) % fStep);
if (fModStepVal !== 0 /* division with remainder */) {
// snap the new value to the nearest step
fValue = fModStepVal * 2 >= fStep ? fValue + fStep - fModStepVal : fValue - fModStepVal;
}
if (fValue < fMin) {
log.warning("Warning: " + "Min value (" + fValue + ") not in the range: ["
+ fMin + "," + fMax + "]", this);
fValue = fMin;
} else if (fValue > fMax) {
log.warning("Warning: " + "Max value (" + fValue + ") not in the range: ["
+ fMin + "," + fMax + "]", this);
fValue = fMax;
}
if (!Number.isInteger(fStep)) {
fValue = parseFloat(fValue.toFixed(this._iDecimalPrecision));
}
return fValue;
};
/**
* Handle the touchstart event happening on the range slider.
* @param {jQuery.Event} oEvent The event object.
* @private
* @override
*/
RangeSlider.prototype.ontouchstart = function (oEvent) {
var oTouch = oEvent.targetTouches[0],
CSS_CLASS = this.getRenderer().CSS_CLASS,
sEventNamespace = "." + CSS_CLASS, fMinValue, fMaxValue,
fValue, aHandles, aRange, iHandleIndex, fHandlesDistance,
fTotalNumberOfValues, fPercentOfHandle, fHandleValue, fHandleHalfWidth;
if (!this.getEnabled()) {
return;
}
// mark the event for components that needs to know if the event was handled
oEvent.setMarked();
// we need to recalculate the styles since something may have changed
// the screen size between touches.
this._recalculateStyles();
if (["number", "text"].indexOf(oEvent.target.type) > -1) {
return;
}
fValue = this._calculateHandlePosition(oTouch.pageX);
aRange = this.getRange();
aHandles = [this._mHandleTooltip.start.handle, this._mHandleTooltip.end.handle];
iHandleIndex = this._getIndexOfHandle(oEvent.target);
fHandlesDistance = aHandles.reduce(function (fAccumulation, oHandle) {
return Math.abs(fAccumulation - oHandle.offsetLeft);
}, 0);
fMinValue = Math.min.apply(Math, aRange);
fMaxValue = Math.max.apply(Math, aRange);
// half width of a handle (both are equal)
fHandleHalfWidth = this.$("handle1").outerWidth() / 2;
// total number of possible values
fTotalNumberOfValues = Math.abs(this.getMin()) + Math.abs(this.getMax());
// percents that half a handle takes from the width of the scale
fPercentOfHandle = ((fHandleHalfWidth * 100) / this.$("inner").outerWidth());
// number of values that takes half a handle
fHandleValue = (fPercentOfHandle / 100) * fTotalNumberOfValues;
// if the click is outside the range or distance between handles is below the threshold - update the closest slider handle
if (fValue < fMinValue ||
fValue < fMinValue + fHandleValue ||
fValue > fMaxValue ||
fValue > (fMaxValue - fHandleValue) ||
fHandlesDistance <= SliderUtilities.CONSTANTS.RANGE_MOVEMENT_THRESHOLD) {
aHandles = [this.getClosestHandleDomRef(oTouch)];
this._updateHandle(aHandles[0], fValue);
// _updateHandle would update the range and the check for change event fire would fail in _ontouchend
this.fireLiveChange({range: aRange});
} else if (iHandleIndex !== -1) { // Determine if the press event is on certain handle
aHandles = [this.getDomRef(iHandleIndex === 0 ? "handle1" : "handle2")];
}
// registers event listeners
jQuery(document)
.on("touchend" + sEventNamespace + " touchcancel" + sEventNamespace + " mouseup" + sEventNamespace,
this._ontouchend.bind(this, aHandles))
.on("touchmove" + sEventNamespace + (oEvent.originalEvent.type !== "touchstart" ? " mousemove" + sEventNamespace : ""),
this._ontouchmove.bind(this, fValue, this.getRange(), aHandles));
// adds pressed state
aHandles.forEach(function (oHandle) {
var sProperty = oHandle.getAttribute("data-ui5-handle-position") + "HandlePressed";
if (!this.getProperty(sProperty)) {
this.setProperty(sProperty, true);
}
}, this);
if (aHandles.length === 2) {
setTimeout(function () {
this.getDomRef("progress").focus();
}.bind(this), 0);
}
};
/**
* Handle the touchmove event happening on the slider.
* @param {int} [fInitialPointerPosition] Mouse pointer's initial position
* @param {int[]} [aInitialRange] Initial range array
* @param {HTMLElement[]} [aHandles] The handle that should be updated
* @param {jQuery.Event} oEvent The event object.
* @private
* @override
*/
RangeSlider.prototype._ontouchmove = function (fInitialPointerPosition, aInitialRange, aHandles, oEvent) {
var fOffset, bRangesEquality, bRangeInBoudaries, bRangeOnBoudaries,
iPageX = oEvent.targetTouches ? oEvent.targetTouches[0].pageX : oEvent.pageX,
fMax = this.getMax(),
fMin = this.getMin(),
aRange = [],
aRangeTemp = [];
// note: prevent native document scrolling
oEvent.preventDefault();
// 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;
}
//calculation of the new range based on the mouse position
fOffset = this._calculateHandlePosition(iPageX) - fInitialPointerPosition;
for (var i = 0; i < aInitialRange.length; i++) {
aRange[i] = aInitialRange[i] + fOffset;
}
aRangeTemp = this._getNormalizedRange(this.getRange(), aInitialRange, aHandles);
//check if the current range is equal to the new one
bRangesEquality = aRange.every(function (fValue, iIndex) {return fValue === aRangeTemp[iIndex];});
bRangeInBoudaries = aRange.every(function (fValue) {return (fValue >= fMin && fValue <= fMax );});
bRangeOnBoudaries = aRangeTemp.indexOf(fMin) > -1 || aRangeTemp.indexOf(fMax) > -1;
if (!bRangesEquality) {
//check the need to update the handle depending of number of the selected handles and the handles position
if ((aHandles.length === 1) || bRangeInBoudaries || !bRangeOnBoudaries) {
aHandles.map(function (oHandle) {this._updateHandle(oHandle, aInitialRange[this._getIndexOfHandle(oHandle)] + fOffset);}, this);
}
// adjust container if advanced tooltips have to be shown
this.getShowAdvancedTooltip() && this._adjustTooltipsContainer();
aRangeTemp = this._getNormalizedRange(this.getRange(), aInitialRange, aHandles);
}
this._triggerLiveChange();
this.setRange(aRangeTemp);
};
/**
* Updates values of the advanced tooltips.
*
* @protected
*/
RangeSlider.prototype.updateAdvancedTooltipDom = function () {
this.getAggregation("_tooltipContainer").repositionTooltips(this.getMin(), this.getMax());
};
RangeSlider.prototype._triggerLiveChange = function () {
var bFireLiveChange,
aRange = this.getRange();
this._liveChangeLastValue = this._liveChangeLastValue || [];
bFireLiveChange = aRange.some(function (fValue, index) {
return fValue !== this._liveChangeLastValue[index];
}, this);
if (bFireLiveChange) {
this._liveChangeLastValue = aRange.slice(); //Save a copy, not a reference
this.fireLiveChange({range: aRange});
}
};
/**
* Get the range normalized in the boundaries.
* @param {Array} aRange range value
* @param {Array} aInitialRange last range values
* @param {HTMLElement} [aHandles] The handles of the slider
* @returns {number} The normalized range
* @private
* @override
*/
RangeSlider.prototype._getNormalizedRange = function (aRange, aInitialRange, aHandles) {
var fMax = this.getMax(),
fMin = this.getMin(),
iSelectedRange = Math.abs(aInitialRange[0] - aInitialRange[1]),
aRangeNormalized = [],
i, iOtherElementIndex;
for (i = 0; i < aRange.length; i++) {
aRangeNormalized[i] = (aRange[i] < fMin ? fMin : aRange[i]);
aRangeNormalized[i] = (aRange[i] > fMax ? fMax : aRangeNormalized[i]);
if (aHandles.length === 2) {
if (aRangeNormalized[0] == fMin) {
aRangeNormalized[1] = aRangeNormalized[0] + iSelectedRange;
} else {
iOtherElementIndex = Math.abs(i - 1);
aRangeNormalized[iOtherElementIndex] = (aRangeNormalized[i] <= fMin ? aRangeNormalized[i] + iSelectedRange : aRangeNormalized[iOtherElementIndex]);
aRangeNormalized[iOtherElementIndex] = (aRangeNormalized[i] >= fMax ? aRangeNormalized[i] - iSelectedRange : aRangeNormalized[iOtherElementIndex]);
}
}
}
return aRangeNormalized;
};
/**
* Handle the touchend event happening on the slider.
* @param {HTMLElement} aHandle The handle that should be updated
* @param {jQuery.Event} oEvent The event object.
* @private
* @override
*/
RangeSlider.prototype._ontouchend = function (aHandle, oEvent) {
var aNewRange = this.getRange(),
sCSSClass = this.getRenderer().CSS_CLASS;
// mark the event for components that needs to know if the event was handled
oEvent.setMarked();
this.setProperty("startHandlePressed", false);
this.setProperty("endHandlePressed", false);
jQuery(document).off("." + sCSSClass);
if (this._aInitialFocusRange[0] !== aNewRange[0] || this._aInitialFocusRange[1] !== aNewRange[1]) {
this._aInitialFocusRange = Array.prototype.slice.call(aNewRange);
this.fireChange({ range: aNewRange });
}
if (this.getShowAdvancedTooltip()) {
this._updateTooltipContent(this._mHandleTooltip.start.tooltip, aNewRange[0]);
this._updateTooltipContent(this._mHandleTooltip.end.tooltip, aNewRange[1]);
}
};
/**
* @param {jQuery.Event} oEvent The event object.
* @override
*/
RangeSlider.prototype.onfocusin = function (oEvent) {
var oTooltipsContainer = this.getAggregation("_tooltipContainer");
if (this.getShowAdvancedTooltip()) {
oTooltipsContainer.show(this);
this._adjustTooltipsContainer();
this._setAriaControls();
}
// remember the initial focus range so when esc key is pressed we can return to it
if (document.activeElement !== this.getFocusDomRef()) {
this._aInitialFocusRange = this.getRange();
}
};
RangeSlider.prototype.getFocusDomRef = function() {
/* when invalidation is done and focus is in the slider
the render manager tries to return the focus to the focusDomRef
in this case we want it to be the currently focused element */
if (this.getDomRef().contains(document.activeElement)) {
return document.activeElement;
}
return this.getDomRef("progress");
};
/* ----------------------------------------------------------- */
/* Keyboard handling */
/* ----------------------------------------------------------- */
/**
* Moves handle / entire slider by specified offset
*
* @param {float} fOffset Default value: 1. Increase or decrease value by provided offset
* @param {HTMLElement} oHandle DOM reference to the handle
* @private
*/
RangeSlider.prototype._updateSliderValues = function (fOffset, oHandle) {
var aRange = this.getRange(),
fMax = this.getMax(),
fMin = this.getMin(),
fRangeMax = Math.max.apply(null, aRange),
fRangeMin = Math.min.apply(null, aRange),
iIndex = this._getIndexOfHandle(oHandle),
iOffsetSign = fOffset < 0 ? -1 : 1,
aHandles = iIndex > -1 ? [oHandle] : [this._mHandleTooltip.start.handle, this._mHandleTooltip.end.handle];
// If this is a single handle, both values should be equal
if (aHandles.length === 1) {
fRangeMin = fRangeMax = aRange[iIndex];
}
// Check the boundaries and recalculate the offset if exceeding
if (fRangeMax + fOffset > fMax) {
fOffset = iOffsetSign * (Math.abs(fMax) - Math.abs(fRangeMax));
} else if (fRangeMin + fOffset < fMin) {
fOffset = Math.abs(fRangeMin) - Math.abs(fMin);
fOffset = fOffset < 0 ? fOffset : iOffsetSign * fOffset;
}
aHandles.map(function (oCurHandle) {
this._updateHandle(oCurHandle, aRange[this._getIndexOfHandle(oCurHandle)] + fOffset);
}, this);
};
RangeSlider.prototype.onkeydown = function (oEvent) {
var bFocusableTooltip = this.getInputsAsTooltips(),
bShowAdvancedTooltips = this.getShowAdvancedTooltip(),
bF2Pressed = oEvent.keyCode === SliderUtilities.CONSTANTS.F2_KEYCODE,
bStartTooltipFocused = (oEvent.target === this._mHandleTooltip.start.handle),
bTargetIsHandle = jQuery(oEvent.target).hasClass(SliderUtilities.CONSTANTS.HANDLE_CLASS);
if (bF2Pressed && bShowAdvancedTooltips && bFocusableTooltip && bTargetIsHandle) {
this._mHandleTooltip[bStartTooltipFocused ? "start" : "end"].tooltip.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.
* @override
*/
RangeSlider.prototype.onsapincrease = function (oEvent) {
// 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()) {
this._updateSliderValues(this.getStep(), oEvent.target);
this._fireChangeAndLiveChange({range: this.getRange()});
}
};
/**
* Handles the <code>onsapplus</code> event when "+" is pressed.
*
* @param {jQuery.Event} oEvent The event object.
* @override
*/
RangeSlider.prototype.onsapplus = RangeSlider.prototype.onsapincrease;
/**
* Handles the <code>sapincreasemodifiers</code> event when Ctrl + right arrow or up arrow are pressed.
*
* @param {jQuery.Event} oEvent The event object.
* @override
*/
RangeSlider.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();
if (this.getEnabled()) {
this._updateSliderValues(this._getLongStep(), oEvent.target);
this._fireChangeAndLiveChange({range: this.getRange()});
}
};
/**
* Handles the <code>sappageup</code> event when page up is pressed.
*
* @param {jQuery.Event} oEvent The event object.
* @override
*/
RangeSlider.prototype.onsappageup = RangeSlider.prototype.onsapincreasemodifiers;
/**
* Handles the <code>sapdecrease</code> event when left arrow or down arrow are pressed.
*
* @param {jQuery.Event} oEvent The event object.
* @override
*/
RangeSlider.prototype.onsapdecrease = function (oEvent) {
// 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()) {
this._updateSliderValues(-1 * this.getStep(), oEvent.target);
this._fireChangeAndLiveChange({range: this.getRange()});
}
};
/**
* Handles the <code>sapminus</code> event when "-" is pressed.
*
* @param {jQuery.Event} oEvent The event object.
* @override
*/
RangeSlider.prototype.onsapminus = RangeSlider.prototype.onsapdecrease;
/**
* Handles the <code>sapdecreasemodifiers</code> event when Ctrl + left or Ctrl + down keys are pressed.
*
* @param {jQuery.Event} oEvent The event object.
* @override
*/
RangeSlider.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();
if (this.getEnabled()) {
this._updateSliderValues(-1 * this._getLongStep(), oEvent.target);
this._fireChangeAndLiveChange({range: this.getRange()});
}
};
/**
* Handles the <code>sappagedown</code> event when when page down is pressed.
*
* @param {jQuery.Event} oEvent The event object.
* @override
*/
RangeSlider.prototype.onsappagedown = RangeSlider.prototype.onsapdecreasemodifiers;
/**
* Handles the <code>saphome</code> event when home key is pressed.
*
* @param {jQuery.Event} oEvent The event object.
* @override
*/
RangeSlider.prototype.onsaphome = function (oEvent) {
var iHandleIndex = 0,
fRangeValue, oHandle, fMin;
// mark the event for components that needs to know if the event was handled
oEvent.setMarked();
// note: prevent document scrolling when Home key is pressed
oEvent.preventDefault();
iHandleIndex = this._getIndexOfHandle(oEvent.target);
fRangeValue = this.getRange()[iHandleIndex];
fMin = this.getMin();
if (this.getEnabled() && (fRangeValue !== fMin)) {
oHandle = (iHandleIndex === 1 ? this._mHandleTooltip.end : this._mHandleTooltip.start);
this._updateHandle(oHandle.handle, fMin);
this._fireChangeAndLiveChange({range: this.getRange()});
}
};
/**
* Handles the <code>sapend</code> event when the End key pressed.
*
* @param {jQuery.Event} oEvent The event object.
* @override
*/
RangeSlider.prototype.onsapend = function (oEvent) {
// mark the event for components that needs to know if the event was handled
oEvent.setMarked();
// note: prevent document scrolling when End key is pressed
oEvent.preventDefault();
if (this.getEnabled()) {
this._updateSliderValues(this.getMax(), oEvent.target);
this._fireChangeAndLiveChange({range: this.getRange()});
}
};
/**
* Handles the <code>sapescape</code> event when escape key is pressed.
* @override
*/
RangeSlider.prototype.onsapescape = function () {
// reset the slider back to the value
// which it had when it got the focus
this.setRange(this._aInitialFocusRange);
this._fireChangeAndLiveChange({range: this.getRange()});
};
return RangeSlider;
});