@openui5/sap.m
Version:
OpenUI5 UI Library sap.m
1,422 lines (1,240 loc) • 44.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/Control',
'./TimePickerSliderRenderer',
'sap/ui/core/IconPool',
'sap/ui/Device',
"sap/ui/events/KeyCodes",
"sap/m/Button",
"sap/ui/thirdparty/jquery",
"sap/ui/core/Configuration"
],
function(Control, TimePickerSliderRenderer, IconPool, Device, KeyCodes, Button, jQuery, Configuration) {
"use strict";
/**
* Constructor for a new <code>TimePickerSlider</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
* A picker list control used inside a {@link sap.m.TimePicker} to choose a value.
* @extends sap.ui.core.Control
*
* @author SAP SE
* @version 1.117.4
*
* @constructor
* @private
* @since 1.32
* @alias sap.m.TimePickerSlider
*/
var TimePickerSlider = Control.extend("sap.m.TimePickerSlider", /** @lends sap.m.TimePickerSlider.prototype */ {
metadata: {
library: "sap.m",
properties: {
/**
* The key of the currently selected value of the slider.
*/
selectedValue: {type: "string", defaultValue: null},
/**
* Indicates whether the slider supports cyclic scrolling.
*/
isCyclic: {type: "boolean", defaultValue: true},
/**
* Defines the descriptive text for the slider, placed as a label above it.
*/
label: {type: "string", defaultValue: null},
/**
* Indicates whether the slider is currently expanded.
*/
isExpanded: {type: "boolean", defaultValue: false}
},
aggregations: {
/**
* Aggregation that contains the items of the slider.
*/
items: {type: "sap.m.VisibleItem", multiple: true, singularName: "item"},
/**
* Aggregation that contains the up arrow.
*/
_arrowUp: {type: "sap.m.Button", multiple: false, visibility: "hidden" },
/**
* Aggregation that contains the down arrow.
*/
_arrowDown: {type: "sap.m.Button", multiple: false, visibility: "hidden" }
},
events: {
/**
* Fires when the slider is expanded.
*/
expanded: {},
/**
* Fires when the slider is collapsed.
* @since 1.54
*/
collapsed: {}
}
},
renderer: TimePickerSliderRenderer
});
var sAnimationMode = Configuration.getAnimationMode();
var bUseAnimations = sAnimationMode !== Configuration.AnimationMode.none && sAnimationMode !== Configuration.AnimationMode.minimal;
var SCROLL_ANIMATION_DURATION = bUseAnimations ? 200 : 0;
var MIN_ITEMS = 50;
var LABEL_HEIGHT = 32;
var ARROW_HEIGHT = 32;
/**
* Initializes the control.
*
* @public
*/
TimePickerSlider.prototype.init = function() {
this._bIsDrag = null;
this._selectionOffset = 0;
this._mousedown = false;
this._dragSession = null;
this._iSelectedItemIndex = -1;
this._animatingSnap = false;
this._iSelectedIndex = -1;
this._animating = false;
this._intervalId = null;
this._maxScrollTop = null;
this._minScrollTop = null;
this._marginTop = null;
this._marginBottom = null;
this._bOneTimeValueSelectionAnimation = false;
this._bEnabled = true;
if (Device.system.desktop) {
this._fnHandleTypeValues = fnTimedKeydownHelper.call(this);
}
this._initArrows();
};
TimePickerSlider.prototype.exit = function() {
this._detachEvents();
var $Slider = this._getSliderContainerDomRef();
if ($Slider) {
$Slider.stop();
}
if (this._intervalId) {
clearInterval(this._intervalId);
this._intervalId = null;
}
};
/**
* Called before the control is rendered.
*/
TimePickerSlider.prototype.onBeforeRendering = function () {
this._detachEvents();
};
/**
* Called after the control is rendered.
*/
TimePickerSlider.prototype.onAfterRendering = function () {
if (Device.system.phone) { //the layout still 'moves' at this point - dialog and its content, so wait a little
setTimeout(this._afterExpandCollapse.bind(this), 0);
} else {
this._afterExpandCollapse();
}
this._attachEvents();
};
/**
* Handles the themeChanged event.
*
* Does a re-rendering of the control.
* @param {jQuery.Event} oEvent Event object
*/
TimePickerSlider.prototype.onThemeChanged = function(oEvent) {
this.invalidate();
};
/**
* Handles the tap event.
*
* Expands or selects the taped element.
* @param {jQuery.Event} oEvent Event object
*/
TimePickerSlider.prototype.fireTap = function(oEvent) {
var oScrElement,
sItemText,
sItemKey;
//expand column with a click
if (!this.getIsExpanded()) {
if (Device.system.desktop) {
this.focus();
} else {
this.setIsExpanded(true);
}
} else { //or select an element from the list
oScrElement = oEvent.srcElement || oEvent.originalTarget;
if (oScrElement && oScrElement.tagName.toLowerCase() === "li") {
sItemText = jQuery(oScrElement).text();
sItemKey = fnFindKeyByText.call(this, sItemText);
this._iClickedIndex =
Array.prototype.slice.call(oScrElement.parentElement.children).indexOf(oScrElement);
this._bOneTimeValueSelectionAnimation = true;
this.setSelectedValue(sItemKey);
this._fireSelectedValueChange(sItemKey);
} else { //if no selection is happening, return the selected style which was removed ontouchstart
this._addSelectionStyle();
this.focus();
}
}
};
/**
* Sets the currently selected value with an item key.
*
* @override
* @param {string} sValue The key of the new selected value
* @returns {sap.ui.base.ManagedObject}
* @public
*/
TimePickerSlider.prototype.setSelectedValue = function(sValue) {
var iIndexOfValue = findIndexInArray(this._getVisibleItems(), function(oElement) {
return oElement.getKey() === sValue;
}),
that = this,
iIndex,
$Slider,
iItemHeightInPx,
iContentRepeats;
if (iIndexOfValue === -1) {
return this;
}
//scroll
if (this.getDomRef()) {
$Slider = this._getSliderContainerDomRef();
iItemHeightInPx = this._getItemHeightInPx();
iContentRepeats = this._getContentRepeat();
//list items' values are repeated, so find the one nearest to the middle of the list
if (iIndexOfValue * iItemHeightInPx >= this._selectionOffset) {
iIndex = this._getVisibleItems().length * Math.floor(iContentRepeats / 2) + iIndexOfValue;
} else {
iIndex = this._getVisibleItems().length * Math.ceil(iContentRepeats / 2) + iIndexOfValue;
}
if (this._bOneTimeValueSelectionAnimation) {
$Slider.scrollTop((iIndex - this._iClickedIndex + this._iSelectedItemIndex) * iItemHeightInPx - this._selectionOffset);
this._animatingSnap = true;
$Slider.animate({scrollTop: iIndex * iItemHeightInPx - this._selectionOffset}, SCROLL_ANIMATION_DURATION, 'linear', function () {
$Slider.clearQueue();
that._animatingSnap = false;
that._bOneTimeValueSelectionAnimation = false;
});
} else {
$Slider.scrollTop(iIndex * iItemHeightInPx - this._selectionOffset);
}
this._removeSelectionStyle();
this._iSelectedItemIndex = iIndex; //because we repeated content / values
this._addSelectionStyle();
}
return this.setProperty("selectedValue", sValue, true); // no rerendering
};
/**
* Sets the <code>isExpanded</code> property of the slider.
*
* @override
* @param {boolean} bValue True or false
* @param {boolean} suppressEvent Whether to suppress event firing
* @returns {this} this instance, used for chaining
* @public
*/
TimePickerSlider.prototype.setIsExpanded = function(bValue, suppressEvent) {
this.setProperty("isExpanded", bValue, true);
if (!this.getDomRef()) {
return this;
}
var $This = this.$();
if (bValue) {
$This.addClass("sapMTPSliderExpanded");
if (Device.system.phone) {
setTimeout(function() {
this._updateDynamicLayout(bValue);
if (!suppressEvent) {
this.fireExpanded({ctrl: this});
}
}.bind(this), 0);
} else {
this._updateDynamicLayout(bValue);
if (!suppressEvent) {
this.fireExpanded({ctrl: this});
}
}
} else {
this._stopAnimation(); //stop any schedule(interval) for animation
//stop snap animation also
if (this._animatingSnap === true) {
this._animatingSnap = false;
this._getSliderContainerDomRef().stop(true);
//Be careful not to invoke this method twice (the first time is on animate finish callback).
//If this is the first animation, the _iSelectedIndex will remain its initial value, so no need
//to notify the scroller about any snap completion
if (this._animatingTargetIndex !== null && this._animatingTargetIndex !== undefined) {
this._scrollerSnapped(this._animatingTargetIndex);
this._animatingTargetIndex = null;
} else if (this._iSelectedIndex !== -1) {
this._scrollerSnapped(this._iSelectedIndex);
}
}
$This.removeClass("sapMTPSliderExpanded");
this._updateMargins(bValue);
if (Device.system.phone) {
setTimeout(this._afterExpandCollapse.bind(this), 0);
} else {
this._afterExpandCollapse();
}
if (!suppressEvent) {
this.fireCollapsed({ctrl: this});
}
}
return this;
};
/**
* Handles the focusin event.
*
* Expands the focused slider.
* @param {jQuery.Event} oEvent Event object
*/
TimePickerSlider.prototype.onfocusin = function(oEvent) {
if (Device.system.desktop && !this.getIsExpanded()) {
this.setIsExpanded(true);
}
};
/**
* Handles the focusout event.
*
* Make sure the blurred slider is collapsed on desktop.
* @param {jQuery.Event} oEvent Event object
*/
TimePickerSlider.prototype.onfocusout = function(oEvent) {
var sFocusedElementId = oEvent.relatedTarget ? oEvent.relatedTarget.id : null,
aArrowsIds = [this.getAggregation("_arrowUp").getId(), this.getAggregation("_arrowDown").getId()];
// Do not close, if any of the arrows is clicked
if (sFocusedElementId && aArrowsIds.indexOf(sFocusedElementId) !== -1) {
return;
}
if (Device.system.desktop && this.getIsExpanded()) {
this.setIsExpanded(false);
}
};
TimePickerSlider.prototype._onmousewheel = function(oEvent) {
var oOriginalEvent,
bDirectionPositive,
wheelData;
// prevent the default behavior
oEvent.preventDefault();
oEvent.stopPropagation();
if (!this.getIsExpanded() || this._intervalId) {
return false;
}
oOriginalEvent = oEvent.originalEvent;
bDirectionPositive = oOriginalEvent.detail ? (-oOriginalEvent.detail > 0) : (oOriginalEvent.wheelDelta > 0);
wheelData = oOriginalEvent.detail ? (-oOriginalEvent.detail / 3) : (oOriginalEvent.wheelDelta / 120);
if (!wheelData) {
return false;
}
this._handleWheelScroll(bDirectionPositive, wheelData);
};
TimePickerSlider.prototype._handleWheelScroll = function(bDirectionPositive, wheelData) {
var fnRound = bDirectionPositive ? Math.ceil : Math.floor,
iResultOffset;
if (!this._aWheelDeltas) {
this._aWheelDeltas = [];
}
this._aWheelDeltas.push(wheelData);
if (!this._bWheelScrolling) {
this._bWheelScrolling = true;
this._intervalId = setInterval(function () {
if (!this._aWheelDeltas.length) {
clearInterval(this._intervalId);
this._intervalId = null;
this._bWheelScrolling = false;
} else {
iResultOffset = this._aWheelDeltas[0]; //simplification, we could still use the array in some cases
this._aWheelDeltas = [];
iResultOffset = fnRound(iResultOffset);
if (iResultOffset) { // !== 0, actually move
this._offsetSlider(iResultOffset);
}
}
}.bind(this), 150);
}
return false;
};
/**
* Handles the pageup event.
*
* Selects the first item value.
* @param {jQuery.Event} oEvent Event object
*/
TimePickerSlider.prototype.onsappageup = function(oEvent) {
if (this.getIsExpanded()) {
var iFirstItem = this._getVisibleItems()[0],
sValue = iFirstItem.getKey();
this.setSelectedValue(sValue);
this._fireSelectedValueChange(sValue);
}
};
/**
* Handles the pagedown event.
*
* Selects the last item value.
* @param {jQuery.Event} oEvent Event object
*/
TimePickerSlider.prototype.onsappagedown = function(oEvent) {
if (this.getIsExpanded()) {
var iLastItem = this._getVisibleItems()[this._getVisibleItems().length - 1],
sValue = iLastItem.getKey();
this.setSelectedValue(sValue);
this._fireSelectedValueChange(sValue);
}
};
/**
* Handles the arrowup event.
*
* Selects the previous item value.
* @param {jQuery.Event} oEvent Event object
*/
TimePickerSlider.prototype.onsapup = function(oEvent) {
if (this.getIsExpanded()) {
this._offsetValue(-1);
}
};
/**
* Handles the arrowdown event.
*
* Selects the next item value.
* @param {jQuery.Event} oEvent Event object
*/
TimePickerSlider.prototype.onsapdown = function(oEvent) {
if (this.getIsExpanded()) {
this._offsetValue(1);
}
};
/**
* Handles the keydown event.
*
* @param {jQuery.Event} oEvent Event object
*/
TimePickerSlider.prototype.onkeydown = function(oEvent) {
var iKC = oEvent.which || oEvent.keyCode,
oKCs = KeyCodes;
if (iKC >= oKCs.NUMPAD_0 && iKC <= oKCs.NUMPAD_9){
iKC = this._convertNumPadToNumKeyCode(iKC);
}
//we only recieve uppercase codes here, which is nice
if ((iKC >= oKCs.A && iKC <= oKCs.Z)
|| (iKC >= oKCs.DIGIT_0 && iKC <= oKCs.DIGIT_9)) {
this._fnHandleTypeValues(oEvent.timeStamp, iKC);
}
};
/**
* Finds the slider's container in the DOM.
*
* @returns {object} Slider container's jQuery object
* @private
*/
TimePickerSlider.prototype._getSliderContainerDomRef = function() {
return this.$().find(".sapMTimePickerSlider");
};
/**
* Calculates how many times the slider content should be repeated so that it fills the space.
*
* The method is called only when isCyclic property is set to true.
* @returns {number} Content repetitions needed
* @private
*/
TimePickerSlider.prototype._getContentRepeat = function() {
//how many times the content is repeated?
//we target to get at least MIN_ITEMS items in the list,
//so repeat the content as many times as it is needed to get that number
//but repeat the content at least 3 times to ensure cyclic visibility
var iContentRepeat;
if (this.getIsCyclic()) {
iContentRepeat = Math.ceil(MIN_ITEMS / this._getVisibleItems().length);
iContentRepeat = Math.max(iContentRepeat, 3);
} else {
iContentRepeat = 1;
}
return iContentRepeat;
};
/**
* Gets the CSS height of a list item.
*
* @returns {number} CSS height in pixels
* @private
*/
TimePickerSlider.prototype._getItemHeightInPx = function() {
return this.$("content").find("li:not(.TPSliderItemHidden)")[0].getBoundingClientRect().height;
};
/**
* Calculates the center of the column and places the border frame.
* @private
*/
TimePickerSlider.prototype._updateSelectionFrameLayout = function() {
var $Frame,
iFrameTopPosition,
topPadding,
iItemHeight,
oSliderOffset = this.$().offset(),
iSliderOffsetTop = oSliderOffset ? oSliderOffset.top : 0,
oContainerOffset = this.$().parents(".sapMTimePickerContainer").offset(),
iContainerOffsetTop = oContainerOffset ? oContainerOffset.top : 0;
if (this.getDomRef()) {
iItemHeight = this._getItemHeightInPx();
$Frame = this.$().find(".sapMTPPickerSelectionFrame");
//the frame is absolutly positioned in the middle of its container
//its height is the same as the list items' height
//so the top of the middle === container.height/2 - item.height/2 + label.height/2
//corrected with the top of the container
//the label height is added to the calculation in order to display the same amount of items above and below the selected one
topPadding = iSliderOffsetTop - iContainerOffsetTop;
iFrameTopPosition = (this.$().height() - iItemHeight + LABEL_HEIGHT) / 2 + topPadding;
$Frame.css("top", iFrameTopPosition);
if (Device.system.phone) {
setTimeout(this._afterExpandCollapse.bind(this), 0);
} else {
this._afterExpandCollapse();
}
}
};
/**
* Updates the visibility of the slider's items based on the step and the selected value.
* @param {int} iNewValue The new selected value of the slider
* @param {int} iStep The precision step used for the slider
* @private
*/
TimePickerSlider.prototype._updateStepAndValue = function(iNewValue, iStep) {
var iVisibleItemsCount = 0,
$SliderContainer,
i;
for (i = 0; i < this.getItems().length; i++) {
if (i % iStep !== 0 && i !== iNewValue) {
this.getItems()[i].setVisible(false);
} else {
this.getItems()[i].setVisible(true);
iVisibleItemsCount++;
}
}
if (iVisibleItemsCount > 2 && iVisibleItemsCount < 13 && this.getDomRef()) {
$SliderContainer = this.$().find(".sapMTimePickerSlider");
$SliderContainer.className = ""; //remove all classes
jQuery($SliderContainer).addClass("sapMTimePickerSlider SliderValues" + iVisibleItemsCount.toString());
}
this.setIsCyclic(iVisibleItemsCount > 2);
this.setSelectedValue(iNewValue.toString());
};
/**
* Updates the margins of a slider.
* Covers the cases where the slider is constrained to show an exact number of items.
* @param {boolean} bIsExpand If we update margins due to expand
* @private
*/
TimePickerSlider.prototype._updateMargins = function(bIsExpand) {
var iItemHeight = this._getItemHeightInPx(),
$ConstrainedSlider,
iVisibleItems,
iVisibleItemsTop,
iVisibleItemsBottom,
iFocusedItemTopPosition,
iArrowHeight,
iMarginTop,
iMarginBottom;
if (this.getDomRef()) {
iItemHeight = this._getItemHeightInPx();
//add margins only if the slider is constrained to show an exact number of items
$ConstrainedSlider = this.$()
.find(".SliderValues3,.SliderValues4,.SliderValues5,.SliderValues6,.SliderValues7,.SliderValues8,.SliderValues9,.SliderValues10,.SliderValues11,.SliderValues12");
if (!$ConstrainedSlider.length) {
return;
}
if (bIsExpand) {
iVisibleItems = this._getVisibleItems().length;
iVisibleItemsTop = iItemHeight * Math.floor(iVisibleItems / 2);
iVisibleItemsBottom = iItemHeight * Math.ceil(iVisibleItems / 2);
// arrow height if the same as label height
// there are arrows only in compact mode
iArrowHeight = this.$().parents().hasClass('sapUiSizeCompact') ? ARROW_HEIGHT : 0;
iFocusedItemTopPosition = (this.$().height() - iItemHeight + LABEL_HEIGHT) / 2;
iMarginTop = iFocusedItemTopPosition - iVisibleItemsTop - LABEL_HEIGHT - iArrowHeight;
iMarginBottom = this.$().height() - iFocusedItemTopPosition - iVisibleItemsBottom - iArrowHeight;
// add a margin only if there are less items than the maximum visible amount
iMarginTop = Math.max(iMarginTop, 0);
iMarginBottom = Math.max(iMarginBottom, 0);
} else {
iMarginTop = 0;
iMarginBottom = 0;
}
$ConstrainedSlider.css("margin-top", iMarginTop);
$ConstrainedSlider.css("margin-bottom", iMarginBottom);
}
};
/**
* Updates the parts of the layout that depend on the slider's height.
* We call this method when the height changes - like at expand/collapse.
* @param {boolean} bIsExpand If we update due to expand
* @private
*/
TimePickerSlider.prototype._updateDynamicLayout = function (bIsExpand) {
this._updateMargins(bIsExpand);
this._updateSelectionFrameLayout();
};
/**
* Calculates the top offset of the border frame relative to its container.
* @private
* @returns {number} Top offset of the border frame
*/
TimePickerSlider.prototype._getSelectionFrameTopOffset = function() {
var $Frame = this._getSliderContainerDomRef().find(".sapMTPPickerSelectionFrame"),
oFrameOffset = $Frame.offset();
return oFrameOffset.top;
};
/**
* Animates slider scrolling.
*
* @private
* @param {number} iSpeed Animating speed
*/
TimePickerSlider.prototype._animateScroll = function(iSpeed) {
var $Container = this._getSliderContainerDomRef(),
iPreviousScrollTop = $Container.scrollTop(),
frameFrequencyMs = 25, //milliseconds - 40 frames per second; 1000ms / 40frames === 25
$ContainerHeight = $Container.height(),
$ContentHeight = this.$("content").height(),
//increase the distance that the slider can be dragged before reaching one end of the list
//because we do not do updates of list offset while dragging,
//we have to keep that distance long at least while animating
iDragMarginBuffer = 200,
iDragMargin = $ContainerHeight + iDragMarginBuffer,
iContentRepeat = this._getContentRepeat(),
bCycle = this.getIsCyclic(),
fDecelerationCoefficient = 0.9,
fStopSpeed = 0.05,
that = this;
this._intervalId = setInterval(function() {
that._animating = true;
//calculate the new scroll offset by subtracting the distance
iPreviousScrollTop = iPreviousScrollTop - iSpeed * frameFrequencyMs;
if (bCycle) {
iPreviousScrollTop = that._getUpdatedCycleScrollTop($ContainerHeight, $ContentHeight, iPreviousScrollTop, iDragMargin, iContentRepeat);
} else {
if (iPreviousScrollTop > that._maxScrollTop) {
iPreviousScrollTop = that._maxScrollTop;
iSpeed = 0;
}
if (iPreviousScrollTop < that._minScrollTop) {
iPreviousScrollTop = that._minScrollTop;
iSpeed = 0;
}
}
$Container.scrollTop(iPreviousScrollTop);
iSpeed *= fDecelerationCoefficient;
if (Math.abs(iSpeed) < fStopSpeed) { // px/milliseconds
//snapping
var iItemHeight = that._getItemHeightInPx();
var iOffset = that._selectionOffset ? (that._selectionOffset % iItemHeight) : 0;
var iSnapScrollTop = Math.round((iPreviousScrollTop + iOffset) / iItemHeight) * iItemHeight - iOffset;
clearInterval(that._intervalId);
that._intervalId = null;
that._animating = null; //not animating
that._iSelectedIndex = Math.round((iPreviousScrollTop + that._selectionOffset) / iItemHeight);
that._animatingSnap = true;
$Container.animate({ scrollTop: iSnapScrollTop}, SCROLL_ANIMATION_DURATION, 'linear', function() {
$Container.clearQueue();
that._animatingSnap = false;
//make sure the DOM is still visible
if ($Container.css("visibility") === "visible") {
that._scrollerSnapped(that._iSelectedIndex);
}
});
}
}, frameFrequencyMs);
};
/**
* Stops the scrolling animation.
*
* @private
*/
TimePickerSlider.prototype._stopAnimation = function() {
if (this._animating) {
clearInterval(this._intervalId);
this._intervalId = null;
this._animating = null;
}
};
/**
* Starts scroll session.
*
* @param {number} iPageY The starting y-coordinate of the target
* @private
*/
TimePickerSlider.prototype._startDrag = function(iPageY) {
//start collecting touch coordinates
if (!this._dragSession) {
this._dragSession = {};
this._dragSession.positions = [];
}
this._dragSession.pageY = iPageY;
this._dragSession.startTop = this._getSliderContainerDomRef().scrollTop();
};
/**
* Performs vertical scroll.
*
* @param {number} iPageY The current y-coordinate of the target to scroll to
* @param {Date} dTimeStamp Timestamp of the event
* @private
*/
TimePickerSlider.prototype._doDrag = function(iPageY, dTimeStamp) {
if (this._dragSession) {
//calculate the distance between the start of the drag and the current touch
this._dragSession.offsetY = iPageY - this._dragSession.pageY;
this._dragSession.positions.push({pageY: iPageY, timeStamp: dTimeStamp});
//to calculate speed we only need the last touch positions
if (this._dragSession.positions.length > 20) {
this._dragSession.positions.splice(0, 10);
}
if (this._bIsDrag) {
//while dragging update the list position inside its container
this._getSliderContainerDomRef().scrollTop(this._dragSession.startTop - this._dragSession.offsetY);
}
}
};
/**
* Finishes scroll session.
*
* @param {number} iPageY The last y-coordinate of the target to scroll to
* @param {Date} dTimeStamp Timestamp of the event
* @private
*/
TimePickerSlider.prototype._endDrag = function(iPageY, dTimeStamp) {
if (this._dragSession) {
var iOffsetTime, iOffsetY;
//get only the offset calculated including the touches in the last 100ms
for (var i = this._dragSession.positions.length - 1; i >= 0; i--) {
iOffsetTime = dTimeStamp - this._dragSession.positions[i].timeStamp;
iOffsetY = iPageY - this._dragSession.positions[i].pageY;
if (iOffsetTime > 100) {
break;
}
}
var fSpeed = (iOffsetY / iOffsetTime); // px/ms
if (this._animating) {
clearInterval(this._intervalId);
this._intervalId = null;
this._animating = null;
}
this._dragSession = null;
this._animateScroll(fSpeed);
}
};
/**
* Calculates the slider's selection y-offset and margins and selects the corresponding list value.
*
* @private
*/
TimePickerSlider.prototype._afterExpandCollapse = function () {
var sSelectedValue = this.getSelectedValue(),
oSelectionFrameTopOffset = this._getSelectionFrameTopOffset(),
$Slider = this._getSliderContainerDomRef(),
oSliderOffset = $Slider.offset(),
iSliderHeight,
$List,
iListContainerHeight,
iItemHeightInPx;
//calculate the offset from the top of the list container to the selection frame
this._selectionOffset = oSelectionFrameTopOffset - oSliderOffset.top;
if (!this.getIsCyclic()) {
$List = this.$("content");
iItemHeightInPx = this._getItemHeightInPx();
iListContainerHeight = this.$().height();
//if we do not cycle the items, we fill the remaining space with margins
if (this.getIsExpanded()) {
this._minScrollTop = 0;
//top margin is as wide as the selection offset
this._marginTop = oSelectionFrameTopOffset - oSliderOffset.top;
this._maxScrollTop = iItemHeightInPx * (this._getVisibleItems().length - 1);
iSliderHeight = $Slider.height();
//bottom margin allows the bottom of the last item when scrolled down
//to be aligned with the selection frame - one item offset
this._marginBottom = iSliderHeight - this._marginTop - iItemHeightInPx;
if (this._marginBottom < 0) { //android native
this._marginBottom = iListContainerHeight - this._marginTop - iItemHeightInPx;
}
//update top,bottom margins
$List.css("margin-top", this._marginTop);
//bottom margin leaves
$List.css("margin-bottom", this._marginBottom);
} else {
this._marginBottom = iListContainerHeight - iItemHeightInPx;
$List.css("margin-top", 0);
$List.css("margin-bottom", this._marginBottom);
//increase the bottom margin so the list can scroll to its last value
}
this._selectionOffset = 0;
}
if (!this.getIsExpanded()) {
this._selectionOffset = 0;
}
//WAI-ARIA region
this.$().attr('aria-expanded', this.getIsExpanded());
this.setSelectedValue(sSelectedValue);
};
/**
* Handles the cycle effect of the slider's list items.
*
* @param {number} iContainerHeight Height of the slider container
* @param {number} iContentHeight Height of the slider content
* @param {number} iTop Current top position
* @param {number} fDragMargin Remaining scroll limit
* @param {number} iContentRepeatNumber Content repetition counter
* @returns {number} Newly calculated top position
* @private
*/
TimePickerSlider.prototype._getUpdatedCycleScrollTop = function(iContainerHeight, iContentHeight, iTop, fDragMargin, iContentRepeatNumber) {
var fContentHeight = iContentHeight - iTop - iContainerHeight;
while (fContentHeight < fDragMargin) {
iTop = iTop - iContentHeight / iContentRepeatNumber;
fContentHeight = iContentHeight - iTop - iContainerHeight;
}
//they are not exclusive, we depend on a content long enough
while (iTop < fDragMargin) {
iTop = iTop + iContentHeight / iContentRepeatNumber;
}
return iTop;
};
/**
* Calculates the index of the snapped element and selects it.
*
* @param {number} iCurrentItem Index of the selected item
* @private
*/
TimePickerSlider.prototype._scrollerSnapped = function(iCurrentItem) {
var iSelectedItemIndex = iCurrentItem,
iItemsCount = this._getVisibleItems().length,
sNewValue;
while (iSelectedItemIndex >= iItemsCount) {
iSelectedItemIndex = iSelectedItemIndex - iItemsCount;
}
if (!this.getIsCyclic()) {
iSelectedItemIndex = iCurrentItem;
}
sNewValue = this._getVisibleItems()[iSelectedItemIndex].getKey();
this.setSelectedValue(sNewValue);
this._fireSelectedValueChange(sNewValue);
};
/**
* Updates the scrolltop value to be on the center of the slider.
*
* @private
*/
TimePickerSlider.prototype._updateScroll = function() {
var sSelectedValue = this.getSelectedValue();
if (sSelectedValue !== this._getVisibleItems()[0].getKey()
&& this._getSliderContainerDomRef().scrollTop() + (this._selectionOffset ? this._selectionOffset : 0) === 0) {
this.setSelectedValue(sSelectedValue);
this._fireSelectedValueChange(sSelectedValue);
}
};
/**
* Adds CSS class to the selected slider item.
*
* @private
*/
TimePickerSlider.prototype._addSelectionStyle = function() {
var $aItems = this.$("content").find("li:not(.TPSliderItemHidden)"),
sSelectedItemText = $aItems.eq(this._iSelectedItemIndex).text(),
oDescriptionElement,
sAriaLabel;
if (!sSelectedItemText) {
return;
}
sAriaLabel = sSelectedItemText;
if (sAriaLabel && sAriaLabel.length > 1 && sAriaLabel.indexOf('0') === 0) {
//If the label contains digits (hours, minutes, seconds), we must remove any leading zeros because they
//are invalid in the context of what will be read out by the screen readers.
//Values like AM/PM are not changed.
sAriaLabel = sAriaLabel.substring(1);
}
$aItems.eq(this._iSelectedItemIndex).addClass("sapMTimePickerItemSelected");
//WAI-ARIA region
oDescriptionElement = this.getDomRef("valDescription");
if (oDescriptionElement.innerHTML !== sAriaLabel) {
oDescriptionElement.innerHTML = sAriaLabel;
}
};
/**
* Removes CSS class to the selected slider item.
*
* @private
*/
TimePickerSlider.prototype._removeSelectionStyle = function() {
var $aItems = this.$("content").find("li:not(.TPSliderItemHidden)");
$aItems.eq(this._iSelectedItemIndex).removeClass("sapMTimePickerItemSelected");
};
/**
* Attaches all needed events to the slider.
*
* @private
*/
TimePickerSlider.prototype._attachEvents = function () {
var oElement = this._getSliderContainerDomRef()[0];
if (Device.system.combi) { // we need both mouse and touch events
//Attach touch events
oElement.addEventListener("touchstart", jQuery.proxy(onTouchStart, this), false);
oElement.addEventListener("touchmove", jQuery.proxy(onTouchMove, this), false);
document.addEventListener("touchend", jQuery.proxy(onTouchEnd, this), false);
//Attach mouse events
oElement.addEventListener("mousedown", jQuery.proxy(onTouchStart, this), false);
document.addEventListener("mousemove", jQuery.proxy(onTouchMove, this), false);
document.addEventListener("mouseup", jQuery.proxy(onTouchEnd, this), false);
} else {
if (Device.system.phone || Device.system.tablet) {
//Attach touch events
oElement.addEventListener("touchstart", jQuery.proxy(onTouchStart, this), false);
oElement.addEventListener("touchmove", jQuery.proxy(onTouchMove, this), false);
document.addEventListener("touchend", jQuery.proxy(onTouchEnd, this), false);
} else {
//Attach mouse events
oElement.addEventListener("mousedown", jQuery.proxy(onTouchStart, this), false);
document.addEventListener("mousemove", jQuery.proxy(onTouchMove, this), false);
document.addEventListener("mouseup", jQuery.proxy(onTouchEnd, this), false);
}
}
};
/**
* Detaches all attached events to the slider.
*
* @private
*/
TimePickerSlider.prototype._detachEvents = function () {
var oElement = this._getSliderContainerDomRef()[0];
if ( oElement == null ) {
return;
}
if (Device.system.combi) {
//Detach touch events
oElement.removeEventListener("touchstart", jQuery.proxy(onTouchStart, this), false);
oElement.removeEventListener("touchmove", jQuery.proxy(onTouchMove, this), false);
document.removeEventListener("touchend", jQuery.proxy(onTouchEnd, this), false);
//Detach mouse events
oElement.removeEventListener("mousedown", jQuery.proxy(onTouchStart, this), false);
document.removeEventListener("mousemove", jQuery.proxy(onTouchMove, this), false);
document.removeEventListener("mouseup", jQuery.proxy(onTouchEnd, this), false);
} else {
if (Device.system.phone || Device.system.tablet) {
//Detach touch events
oElement.removeEventListener("touchstart", jQuery.proxy(onTouchStart, this), false);
oElement.removeEventListener("touchmove", jQuery.proxy(onTouchMove, this), false);
document.removeEventListener("touchend", jQuery.proxy(onTouchEnd, this), false);
} else {
//Detach mouse events
oElement.removeEventListener("mousedown", jQuery.proxy(onTouchStart, this), false);
document.removeEventListener("mousemove", jQuery.proxy(onTouchMove, this), false);
document.removeEventListener("mouseup", jQuery.proxy(onTouchEnd, this), false);
}
}
};
/**
* Helper function which enables selecting a slider item with an index offset.
*
* @param {number} iIndexOffset The index offset to be scrolled to
* @private
*/
TimePickerSlider.prototype._offsetValue = function(iIndexOffset) {
var $Slider = this._getSliderContainerDomRef(),
iScrollTop,
iItemHeight = this._getItemHeightInPx(),
iSnapScrollTop,
iSelIndex,
bCycle = this.getIsCyclic(),
oThat = this;
this._stopAnimation(); //stop any schedule(interval) for animation
//stop snap animation also
if (this._animatingSnap === true) {
this._animatingSnap = false;
this._getSliderContainerDomRef().stop(true);
//Be careful not to invoke this method twice (the first time is on animate finish callback).
//If this is the first animation, the _iSelectedIndex will remain its initial value, so no need
//to notify the scroller about any snap completion
if (this._animatingTargetIndex !== null) {
this._scrollerSnapped(this._animatingTargetIndex);
this._animatingTargetIndex = null;
} else if (this._iSelectedIndex !== -1) {
this._scrollerSnapped(this._iSelectedIndex);
}
}
iSelIndex = this._iSelectedItemIndex + iIndexOffset;
iScrollTop = $Slider.scrollTop();
iSnapScrollTop = iScrollTop + iIndexOffset * iItemHeight;
if (!bCycle) {
if (iSelIndex < 0 || iSelIndex >= this._getVisibleItems().length) {
return;
}
if (iSnapScrollTop > this._maxScrollTop) {
iSnapScrollTop = this._maxScrollTop;
}
if (iSnapScrollTop < this._minScrollTop) {
iSnapScrollTop = this._minScrollTop;
}
}
this._animatingSnap = true;
this._animatingTargetIndex = iSelIndex;
$Slider.animate({ scrollTop: iSnapScrollTop}, SCROLL_ANIMATION_DURATION, 'linear', function() {
$Slider.clearQueue();
oThat._animatingSnap = false;
oThat._animatingTargetIndex = null;
//make sure the DOM is still visible
if ($Slider.css("visibility") === "visible") {
oThat._scrollerSnapped(iSelIndex);
}
});
};
/**
* Repositions the slider to match the current item plus or minus the given integer offset.
*
* @param {number} iOffsetNumberOfItems The number of items to be added or removed to the current item's index
* @private
*/
TimePickerSlider.prototype._offsetSlider = function(iOffsetNumberOfItems) {
var iScrollTop = this._getSliderContainerDomRef().scrollTop(),
that = this,
$ContainerHeight = that._getSliderContainerDomRef().height(),
$ContentHeight = that.$("content").height(),
iDragMarginBuffer = 200,
iDragMargin = $ContainerHeight + iDragMarginBuffer,
iContentRepeat = that._getContentRepeat(),
bCycle = that.getIsCyclic(),
iItemHeight = that._getItemHeightInPx();
//calculate the new scroll offset by subtracting the distance
iScrollTop = iScrollTop - iOffsetNumberOfItems * iItemHeight;
if (bCycle) {
iScrollTop = that._getUpdatedCycleScrollTop($ContainerHeight, $ContentHeight, iScrollTop, iDragMargin, iContentRepeat);
} else {
if (iScrollTop > that._maxScrollTop) {
iScrollTop = that._maxScrollTop;
}
if (iScrollTop < that._minScrollTop) {
iScrollTop = that._minScrollTop;
}
}
that._getSliderContainerDomRef().scrollTop(iScrollTop);
that._iSelectedIndex = Math.round((iScrollTop + that._selectionOffset) / iItemHeight);
that._scrollerSnapped(that._iSelectedIndex);
};
TimePickerSlider.prototype._initArrows = function() {
var that = this, oArrowUp, oArrowDown,
oRB = sap.ui.getCore().getLibraryResourceBundle("sap.m");
oArrowUp = new Button({
icon: IconPool.getIconURI("slim-arrow-up"),
press: function (oEvent) {
that._offsetValue(-1);
},
tooltip: oRB.getText("TIMEPICKER_TOOLTIP_UP"),
type: 'Transparent'
});
oArrowUp.addEventDelegate({
onAfterRendering: function () {
oArrowUp.$().attr("tabindex", -1);
}
});
this.setAggregation("_arrowUp", oArrowUp);
oArrowDown = new Button({
icon: IconPool.getIconURI("slim-arrow-down"),
press: function (oEvent) {
that._offsetValue(1);
},
tooltip: oRB.getText("TIMEPICKER_TOOLTIP_DOWN"),
type: 'Transparent'
});
oArrowDown.addStyleClass("sapMTimePickerItemArrowDown");
oArrowDown.addEventDelegate({
onAfterRendering: function () {
oArrowDown.$().attr("tabindex", -1);
}
});
this.setAggregation("_arrowDown", oArrowDown);
};
TimePickerSlider.prototype._convertNumPadToNumKeyCode = function (iKeyCode) {
var oKCs = KeyCodes;
// Translate keypad code to number row code
// The difference between NUM pad numbers and numbers in keycode is 48
if (iKeyCode >= oKCs.NUMPAD_0 && iKeyCode <= oKCs.NUMPAD_9){
iKeyCode -= 48;
}
return iKeyCode;
};
/**
* Finds the index of an element, satisfying provided predicate.
*
* @param {array} aArray The array to be predicted
* @param {function} fnPredicate Testing function
* @returns {number} The index in the array, if an element in the array satisfies the provided testing function
* @private
*/
function findIndexInArray(aArray, fnPredicate) {
if (aArray == null) {
throw new TypeError('findIndex called with null or undefined array');
}
if (typeof fnPredicate !== 'function') {
throw new TypeError('predicate must be a function');
}
var iLength = aArray.length;
var fnThisArg = arguments[1];
var vValue;
for (var iIndex = 0; iIndex < iLength; iIndex++) {
vValue = aArray[iIndex];
if (fnPredicate.call(fnThisArg, vValue, iIndex, aArray)) {
return iIndex;
}
}
return -1;
}
/**
* Default onTouchStart handler.
* @param {jQuery.Event} oEvent Event object
*/
var onTouchStart = function (oEvent) {
var iPageY = oEvent.touches && oEvent.touches.length ? oEvent.touches[0].pageY : oEvent.pageY;
this._bIsDrag = false;
if (!this.getIsExpanded()) {
return;
}
this._stopAnimation();
this._startDrag(iPageY);
if (!Device.system.desktop) {
oEvent.preventDefault();
}
this._mousedown = true;
};
/**
* Default onTouchMove handler.
* @param {jQuery.Event} oEvent Event object
*/
var onTouchMove = function (oEvent) {
var iPageY = oEvent.touches && oEvent.touches.length ? oEvent.touches[0].pageY : oEvent.pageY;
if (!this._mousedown || !this.getIsExpanded()) {
return;
}
//galaxy s5 android 5.0 fires touchmove every time - so see if it's far enough to call it a drag
if (!this._bIsDrag && this._dragSession && this._dragSession.positions.length) {
//there is a touch at least 5px away vertically from the initial touch
var bFarEnough = this._dragSession.positions.some(function(pos) {
return Math.abs(pos.pageY - iPageY) > 5;
});
if (bFarEnough) {
this._bIsDrag = true;
}
}
this._doDrag(iPageY, oEvent.timeStamp);
this._mousedown = true;
};
/**
* Default onTouchEnd handler.
* @param {jQuery.Event} oEvent Event object
*/
var onTouchEnd = function (oEvent) {
var iPageY = oEvent.changedTouches && oEvent.changedTouches.length ? oEvent.changedTouches[0].pageY : oEvent.pageY;
if (this._bIsDrag === false) {
this.fireTap(oEvent);
this._dragSession = null;
}
this._bIsDrag = true;
if (!this.getIsExpanded()) {
this._dragSession = null;
return;
}
this._endDrag(iPageY, oEvent.timeStamp);
this._mousedown = false;
};
var fnFindKeyByText = function(sText) {
var aItems = this._getVisibleItems();
var index = findIndexInArray(aItems, function(el) {
return el.getText() === sText;
});
return aItems[index] ? aItems[index].getKey() : '';
};
/*
* Returns a function that remembers consecutive keydown events and adjust the slider values
* if they together match an item key.
*/
var fnTimedKeydownHelper = function() {
var iLastTimeStamp = -1,
iLastTimeoutId = -1,
iWaitTimeout = 1000,
sCurrentKeyPrefix = "",
fnTimedKeydown = function(iTimeStamp, iKeyCode) {
var aMatchingItems;
//the previous call was more than a second ago or this is the first call
if (iLastTimeStamp + iWaitTimeout < iTimeStamp) {
sCurrentKeyPrefix = "";
} else {
if (iLastTimeoutId !== -1) {
clearTimeout(iLastTimeoutId);
iLastTimeoutId = -1;
}
}
sCurrentKeyPrefix += String.fromCharCode(iKeyCode).toLowerCase();
aMatchingItems = this._getVisibleItems().filter(function(item) {
return item.getKey().indexOf(sCurrentKeyPrefix) === 0; //starts with the current prefix
});
if (aMatchingItems.length > 1) {
iLastTimeoutId = setTimeout(function() {
this.setSelectedValue(sCurrentKeyPrefix);
sCurrentKeyPrefix = "";
iLastTimeoutId = -1;
}.bind(this), iWaitTimeout);
} else if (aMatchingItems.length === 1) {
this.setSelectedValue(aMatchingItems[0].getKey());
sCurrentKeyPrefix = "";
} else {
sCurrentKeyPrefix = "";
}
iLastTimeStamp = iTimeStamp;
};
return fnTimedKeydown;
};
/**
* Gets only the visible items.
* @returns {sap.m.VisibleItem[]} the visible sap.m.TimePickerSlider items
* @private
*/
TimePickerSlider.prototype._getVisibleItems = function() {
return this.getItems().filter(function(item) {
return item.getVisible();
});
};
/**
* Setter for enabling/disabling the sliders when 2400.
*
* @private
*/
TimePickerSlider.prototype._setEnabled = function (bEnabled) {
this._bEnabled = bEnabled;
if (bEnabled) {
this.$().removeClass("sapMTPDisabled");
this.$().attr("tabindex", 0);
} else {
this.$().addClass("sapMTPDisabled");
this.$().attr("tabindex", -1);
}
return this;
};
/**
* Getter for enabling/disabling the sliders when 2400.
* @private
*/
TimePickerSlider.prototype._getEnabled = function (bEnabled) {
return this._bEnabled;
};
/**
* Informs the <code>TimePickerSliders</code> for a change value.
* @param {string} sValue selected value
* @private
*/
TimePickerSlider.prototype._fireSelectedValueChange = function (sValue) {
this.fireEvent("_selectedValueChange", { value: sValue });
};
return TimePickerSlider;
});