UNPKG

@openui5/sap.m

Version:

OpenUI5 UI Library sap.m

727 lines (643 loc) 22.7 kB
/*! * 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/ui/core/Control", "sap/ui/core/Configuration", "./TimePickerClockRenderer", "sap/ui/Device", "sap/ui/core/ControlBehavior", "sap/ui/thirdparty/jquery" ], function(Control, Configuration, TimePickerClockRenderer, Device, ControlBehavior, jQuery) { "use strict"; var LONG_TOUCH_DURATION = 1000; // duration for long-touch interaction /** * Constructor for a new <code>TimePickerClock</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 control used inside a {@link sap.m.TimePicker} to choose a value using a clock dial. * @extends sap.ui.core.Control * * @author SAP SE * @version 1.146.0 * * @constructor * @private * @since 1.90 * @alias sap.m.TimePickerClock */ var TimePickerClock = Control.extend("sap.m.TimePickerClock", /** @lends sap.m.TimePickerClock.prototype */ { metadata: { library: "sap.m", properties: { /** * If set to <code>true</code>, the clock is interactive. */ enabled : {type : "boolean", group : "Misc", defaultValue : true}, /** * Minimum item value for the outer circle. */ itemMin: {type: "int", group: "Data", defaultValue: -1}, /** * Maximum item value for the outer circle. */ itemMax: {type: "int", group: "Data", defaultValue: -1}, /** * Label of the clock dial - for example, 'Hours', 'Minutes', or 'Seconds'. */ label: {type: "string", group: "Appearance", defaultValue: null}, /** * If set to <code>true</code>, a surrounding circle with markers (dots) will be displayed * (for example, on the 'Minutes' clock-dial, markers represent minutes) */ fractions: {type: "boolean", group: "Appearance", defaultValue: true}, /** * If provided, this will replace the last item displayed. Usually, the last item '24' or '60' is replaced with '0'. * Do not replace the last item if <code>support2400</code> is set to <code>true</code>. */ lastItemReplacement: {type: "int", group: "Data", defaultValue: -1}, /** * Prepend with zero flag. If <code>true</code>, values less than 10 will be prepend with 0. */ prependZero: {type: "boolean", group: "Appearance", defaultValue: false}, /** * The currently selected value of the clock. */ selectedValue: {type: "int", group: "Data", defaultValue: -1}, /** * The currently hovered value of the clock. */ hoveredValue: {type: "int", group: "Data", defaultValue: -1}, /** * The step for displaying of one unit of items. * 1 means 1/60 of the circle. * The default display step is 5 which means minutes and seconds are displayed as "0", "5", "10", etc. * For hours the display step must be set to 1. */ displayStep: {type: "int", group: "Data", defaultValue: 5}, /** * The step for selection of items. * 1 means 1 unit: * - if the clock displays hours - 1 unit = 1 hour * - if the clock displays minutes/seconds - 1 unit = 1 minute/second */ valueStep: {type: "int", group: "Data", defaultValue: 1}, /** * Allows to set a value of 24:00, used to indicate the end of the day. * Works only with HH or H formats. Don't use it together with am/pm. * * When this property is set to <code>true</code>, the clock can display either 24 or 00 as last hour. * The change between 24 and 00 (and vice versa) can be done as follows: * * - on a desktop device: hold down the <code>Ctrl</code> key (this changes 24 to 00 and vice versa), and either * click with mouse on the 00/24 number, or navigate to this value using Arrow keys/PageUp/PageDown and press * <code>Space</code> key (Space key selects the highlighted value and switch to the next available clock). * * - on mobile/touch device: make a long touch on 24/00 value - this action toggles the value to the opposite one. * * - on both device types, if there is a keyboard attached: 24 or 00 can be typed directly. */ support2400: {type: "boolean", group: "Misc", defaultValue: false}, /** * When set to <code>true</code>, the clock will be displayed without the animation. */ skipAnimation: {type: "boolean", group: "Misc", defaultValue: false}, /** * When set to <code>true</code>, the clock will fade in from transparent to full opacity, otherwise it will be hidden. * If the skipAnimation property is set to <code>true</code>, the clock will fade in without the animation, otherwise * the fade in will be animated. */ fadeIn: {type: "boolean", group: "Misc", defaultValue: false}, /** * When set to <code>true</code>, the clock will fade out to transparent */ fadeOut: {type: "boolean", group: "Misc", defaultValue: false} }, events: { /** * Fires when a value of clock is changed. */ change: { parameters: { /** * The new <code>value</code> of the control. */ value: { type: "int" }, /** * The new <code>value</code> of the control, as string, zero-prepended when necessary. */ stringValue: { type: "string" }, /** * <code>true</code> when a value is selected and confirmed * <code>false</code> when a value is only selected but not confirmed */ finalChange: { type: "boolean" } } } } }, renderer: TimePickerClockRenderer }); /** * Initializes the control. * * @public */ TimePickerClock.prototype.init = function() { this._onMouseWheel = this._onMouseWheel.bind(this); this._bFinalChange = false; }; /** * Before rendering. * * @private */ TimePickerClock.prototype.onBeforeRendering = function() { var oDomRef = this.getDomRef(); if (oDomRef) { this._bFocused = oDomRef.contains(document.activeElement); this._detachEvents(); } if (this.getSupport2400() && this._get24HoursVisible() === undefined) { this._save2400State(); } }; /** * After rendering. * * @private */ TimePickerClock.prototype.onAfterRendering = function() { this._attachEvents(); }; /** * Destroy the control. * * @private */ TimePickerClock.prototype.exit = function() { this._detachEvents(); }; /** * Handles the themeChanged event. * * Does a rerendering of the control. * @param {jQuery.Event} oEvent Event object */ TimePickerClock.prototype.onThemeChanged = function(oEvent) { this.invalidate(); }; /** * Value setter. * * @param {int} iValue value to set as selected for the clock * @returns {this} the clock object for chaining */ TimePickerClock.prototype.setSelectedValue = function(iValue) { var iReplacement = this.getLastItemReplacement(), iMaxValue = this.getItemMax(); if (!this.getSupport2400()) { if (iValue === 0) { iValue = iMaxValue; } if (iValue === iMaxValue && iReplacement !== -1) { iValue = iReplacement; } } this.setProperty("selectedValue", iValue); this.fireChange({value: iValue, stringValue: this._getStringValue(iValue), finalChange: this._bFinalChange}); this._bFinalChange = false; return this; }; /** * Value getter. * * @returns {int} selected value of the clock */ TimePickerClock.prototype.getSelectedValue = function() { var iValue = this.getProperty("selectedValue"), iReplacement = this.getLastItemReplacement(); if (this.getSupport2400() && this._get24HoursVisible() && iValue === this.getItemMax() && iReplacement !== -1) { iValue = iReplacement; } return parseInt(iValue); }; /** * Returns value as a string and prepend it with zeroes if necessary. * * @param {int} iValue value to set as selected for the clock * @returns {string} value as a string */ TimePickerClock.prototype._getStringValue = function(iValue) { var sValue = iValue.toString(); if (this.getPrependZero()) { sValue = sValue.padStart(2, "0"); } return sValue; }; /** * Saves the state when a clock has <code>support2400</code> property set. * Sets the flag that says if "24" is visible or not. * * @private */ TimePickerClock.prototype._save2400State = function () { this._set24HoursVisible(this.getSupport2400() && this.getSelectedValue() === 0 ? false : true); }; /** * Sets the flag for "24" visibility and correxponding last item replacement * when a clock has <code>support2400</code> property set. * * @param {boolean} bIsVisible is "24" visible or not * @private */ TimePickerClock.prototype._set24HoursVisible = function (bIsVisible) { if (this.getSupport2400()) { this._is24HoursVisible = bIsVisible; this.setLastItemReplacement(bIsVisible ? 24 : 0); } else { this._is24HoursVisible = false; } }; /** * Gets the flag for "24" visibility (used when a clock has <code>support2400</code> property set). * * @returns {boolean} is "24" visible or not * @private */ TimePickerClock.prototype._get24HoursVisible = function () { return this.getSupport2400() ? this._is24HoursVisible : false; }; /** * Mark/unmark toggled element (24/00) as selected * * @param {boolean} bIsMarked Whether to mark 24/00 element as selected * @returns {this} the clock object for chaining */ TimePickerClock.prototype._markToggleAsSelected = function (bIsMarked) { this._selectToggledElement = bIsMarked; return this; }; /** * Attaches all needed events to the clock. * * @private */ TimePickerClock.prototype._attachEvents = function() { var oElement = this._getClockCoverContainerDomRef(); this.$().on(Device.browser.firefox ? "DOMMouseScroll" : "mousewheel", this._onMouseWheel); document.addEventListener("mouseup", jQuery.proxy(this._onMouseOutUp, this), false); if (oElement) { if (Device.system.combi || Device.system.phone || Device.system.tablet) { // Attach touch events oElement.addEventListener("touchstart", jQuery.proxy(this._onTouchStart, this), false); oElement.addEventListener("touchmove", jQuery.proxy(this._onTouchMove, this), false); oElement.addEventListener("touchend", jQuery.proxy(this._onTouchEnd, this), false); } if (Device.system.desktop || Device.system.combi) { // Attach mouse events oElement.addEventListener("mousedown", jQuery.proxy(this._onTouchStart, this), false); oElement.addEventListener("mousemove", jQuery.proxy(this._onTouchMove, this), false); oElement.addEventListener("mouseup", jQuery.proxy(this._onTouchEnd, this), false); oElement.addEventListener("mouseout", jQuery.proxy(this._onMouseOut, this), false); } } }; /** * Detaches all attached events to the clock. * * @private */ TimePickerClock.prototype._detachEvents = function() { var oElement = this._getClockCoverContainerDomRef(); this.$().off(Device.browser.firefox ? "DOMMouseScroll" : "mousewheel", this._onMouseWheel); document.removeEventListener("mouseup", jQuery.proxy(this._onMouseOutUp, this), false); if (oElement) { if (Device.system.combi || Device.system.phone || Device.system.tablet) { // Detach touch events oElement.removeEventListener("touchstart", jQuery.proxy(this._onTouchStart, this), false); oElement.removeEventListener("touchmove", jQuery.proxy(this._onTouchMove, this), false); oElement.removeEventListener("touchend", jQuery.proxy(this._onTouchEnd, this), false); } if (Device.system.desktop || Device.system.combi) { // Detach mouse events oElement.removeEventListener("mousedown", jQuery.proxy(this._onTouchStart, this), false); oElement.removeEventListener("mousemove", jQuery.proxy(this._onTouchMove, this), false); oElement.removeEventListener("mouseup", jQuery.proxy(this._onTouchEnd, this), false); oElement.removeEventListener("mouseout", jQuery.proxy(this._onMouseOut, this), false); } } }; /** * Finds the clock's cover container in the DOM. * * @returns {object} Slider container's jQuery object * @private */ TimePickerClock.prototype._getClockCoverContainerDomRef = function() { return this.getDomRef("cover"); }; /** * Mouseup handler for whole document. * Prevents selection movement when mouse is down inside the clock, * then moved outside it, released (mouseup) and moved back to the clock. * * @param {jQuery.Event} oEvent Event object * @private */ TimePickerClock.prototype._onMouseOutUp = function(oEvent) { this._mouseOrTouchDown = false; }; /** * Mouseup handler for the clock. * Restores normal state of currently hovered number. * * @param {jQuery.Event} oEvent Event object * @private */ TimePickerClock.prototype._onMouseOut = function(oEvent) { this.setHoveredValue(-1); }; /** * Mousewheel handler. Increases/decreases value of the clock. * * @param {boolean} bIncreaseValue whether to increase or decrease the value * @private */ TimePickerClock.prototype.modifyValue = function(bIncreaseValue) { var iSelectedValue = this.getSelectedValue(), iReplacementValue = this.getLastItemReplacement(), iMin = this.getItemMin(), iMax = this.getItemMax(), iStep = this.getValueStep(), iNewSelectedValue; // fix step in order to change value to the nearest possible if step is > 1 if (iSelectedValue % iStep !== 0) { iNewSelectedValue = bIncreaseValue ? Math.ceil(iSelectedValue / iStep) * iStep : Math.floor(iSelectedValue / iStep) * iStep; iStep = Math.abs(iSelectedValue - iNewSelectedValue); } if (this.getSupport2400() && !this._get24HoursVisible()) { iMin = 0; iMax = 23; iReplacementValue = -1; } if (iSelectedValue === iReplacementValue) { iSelectedValue = iMax; } if (bIncreaseValue) { iSelectedValue += iStep; if (iSelectedValue > iMax) { iSelectedValue = this.getSupport2400() ? iMin : iSelectedValue - iMax; } } else { iSelectedValue -= iStep; if (iSelectedValue < iMin) { iSelectedValue = iMax; } } this.setSelectedValue(iSelectedValue); }; /** * Mousewheel handler. Increases/decreases value of the clock. * * @param {jQuery.Event} oEvent Event object * @private */ TimePickerClock.prototype._onMouseWheel = function(oEvent) { var oOriginalEvent = oEvent.originalEvent, bIncreaseValue = oOriginalEvent.detail ? (-oOriginalEvent.detail > 0) : (oOriginalEvent.wheelDelta > 0); oEvent.preventDefault(); if (!this._mouseOrTouchDown) { this.modifyValue(bIncreaseValue); } }; /** * onTouchStart handler. * * @param {jQuery.Event} oEvent Event object * @private */ TimePickerClock.prototype._onTouchStart = function(oEvent) { this._cancelTouchOut = false; if (!this.getEnabled() || (oEvent.type === "mousedown" && oEvent.button !== 0) || this.getFadeOut()) { return; } this._iMovSelectedValue = this.getSelectedValue(); this._calculateDimensions(); this._x = oEvent.type === "touchstart" ? oEvent.touches[0].pageX : oEvent.pageX; this._y = oEvent.type === "touchstart" ? oEvent.touches[0].pageY : oEvent.pageY; this._calculatePosition(this._x, this._y); if (this.getSupport2400() && oEvent.type === "touchstart" && (this._iSelectedValue === 24 || this._iSelectedValue === 0)) { this._resetLongTouch(); this._startLongTouch(); } this._mouseOrTouchDown = true; }; /** * onTouchMove handler. * * @param {jQuery.Event} oEvent Event object * @private */ TimePickerClock.prototype._onTouchMove = function(oEvent) { if (this.getFadeOut()) { return; } oEvent.preventDefault(); if (this._mouseOrTouchDown) { this._x = oEvent.type === "touchmove" ? oEvent.touches[0].pageX : oEvent.pageX; this._y = oEvent.type === "touchmove" ? oEvent.touches[0].pageY : oEvent.pageY; this._calculatePosition(this._x, this._y); if (this.getEnabled() && this._iSelectedValue !== -1 && this._iSelectedValue !== this._iMovSelectedValue) { this.setSelectedValue(this._iSelectedValue); this._iMovSelectedValue = this._iSelectedValue; if (this.getSupport2400() && oEvent.type === "touchmove" && (this._iSelectedValue === 24 || this._iSelectedValue === 0)) { this._resetLongTouch(); this._startLongTouch(); } } } else if (oEvent.type === "mousemove") { if (!this._dimensionParameters) { this._calculateDimensions(); } this._x = oEvent.pageX; this._y = oEvent.pageY; this._calculatePosition(this._x, this._y); } }; /** * onTouchEnd handler. * * @param {jQuery.Event} oEvent Event object * @private */ TimePickerClock.prototype._onTouchEnd = function(oEvent) { if (!this._mouseOrTouchDown || this.getFadeOut()) { return; } this._mouseOrTouchDown = false; oEvent.preventDefault(); if (!this.getEnabled() || this._iSelectedValue === -1) { return; } if (oEvent.type === "touchend") { this._resetLongTouch(); } if (!this._cancelTouchOut) { this._bFinalChange = true; this.setSelectedValue(this._iSelectedValue); } }; /** * Clears the currently existing long touch period and starts new one if requested. * * @private */ TimePickerClock.prototype._resetLongTouch = function() { if (this._longTouchId) { clearTimeout(this._longTouchId); } }; /** * Starts new long touch period. * * @private */ TimePickerClock.prototype._startLongTouch = function() { this._longTouchId = setTimeout(function() { var iValue = this._iSelectedValue; this._longTouchId = null; if (iValue === 0 || iValue === 24) { this._toggle2400(); } }.bind(this), LONG_TOUCH_DURATION); }; /** * Toggles 24 and 0 values when a clock has <code>support2400</code> property set. * * @param {boolean} bSkipSelection Whether to skip the setting of the toggled value * @returns {this} the clock object for chaining * @private */ TimePickerClock.prototype._toggle2400 = function(bSkipSelection) { var bIs24HoursVisible = this._get24HoursVisible(), iValue = bIs24HoursVisible ? 0 : 24; this._cancelTouchOut = true; this._set24HoursVisible(!bIs24HoursVisible); this.setLastItemReplacement(iValue); if (!bSkipSelection) { this._iMovSelectedValue = iValue; this.setSelectedValue(iValue); } return this; }; /** * Returns the number of items in the clock. * * @private */ TimePickerClock.prototype._getItemsCount = function(bForceRealCount) { return this.getItemMax() - this.getItemMin() + 1; }; /** * Returns the angle step for the clock. * * @private */ TimePickerClock.prototype._getAngleStep = function(bForceRealAngle) { const iItemsCount = this._getItemsCount(); return iItemsCount === 12 ? 6 : 360 / this._getItemsCount(); }; /** * Calculates dimension variables necessary for determining of item selection. * * @private */ TimePickerClock.prototype._calculateDimensions = function() { const oCover = this._getClockCoverContainerDomRef(); if (!oCover) { return; } const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft, scrollTop = window.pageYOffset || document.documentElement.scrollTop, iRadius = Math.round(oCover.offsetHeight / 2), iDotHeight = jQuery('.sapMTPCDot').first().outerHeight(true), iNumberHeight = jQuery('.sapMTPCNumber').first().outerHeight(true), oOffset = oCover.getBoundingClientRect(); this._dimensionParameters = { 'radius': iRadius, 'centerX': iRadius, 'centerY': iRadius, 'dotHeight': iDotHeight, 'numberHeight': iNumberHeight, 'radiusMax': iRadius, 'radiusMin': iRadius - iNumberHeight * 1.6 - 1, 'offsetX': oOffset.left + scrollLeft, 'offsetY': oOffset.top + scrollTop }; }; /** * Calculates item selection based on click/touch position. * * @param {int} iX X position of click/touch returned by the event * @param {int} iY Y position of click/touch returned by the event * @private */ TimePickerClock.prototype._calculatePosition = function(iX, iY) { var iItemMax = this.getItemMax(), iReplacement = this.getLastItemReplacement(), iValueStep = this.getValueStep(), iDx = iX - this._dimensionParameters.offsetX + 1 - this._dimensionParameters.radius, iDy = iY - this._dimensionParameters.offsetY + 1 - this._dimensionParameters.radius, iMod = iDx >= 0 ? 0 : 180, iAngle = (Math.atan(iDy / iDx) * 180 / Math.PI) + 90 + iMod, iAngleStep = this._getAngleStep(true), iRadius = Math.sqrt(iDx * iDx + iDy * iDy), iFinalAngle = Math.round((iAngle === 0 ? 360 : iAngle) / iAngleStep / iValueStep) * iAngleStep * iValueStep, bIsInActiveZone = iRadius <= this._dimensionParameters.radiusMax && iRadius > this._dimensionParameters.radiusMin, iMultiplier = 360 / this._getItemsCount() / iAngleStep, bSupport2400 = this.getSupport2400(), bIs24HoursVisible = this._get24HoursVisible(); if (iFinalAngle === 0) { iFinalAngle = 360; } // selected item calculations if (bIsInActiveZone) { this._iSelectedValue = Math.round((iFinalAngle / iAngleStep / iMultiplier / iValueStep)) * iValueStep; if (this._iSelectedValue === 0 && iReplacement === -1) { this._iSelectedValue = iItemMax; } if (bSupport2400 && !bIs24HoursVisible && this._iSelectedValue === 24) { this._iSelectedValue = 0; } } else { this._iSelectedValue = -1; } if (this._iSelectedValue === iItemMax && iReplacement !== -1) { this._iSelectedValue = iReplacement; } // hover simulation calculations this.setHoveredValue(bIsInActiveZone ? this._iSelectedValue : -1); }; /** * Setter for enabling/disabling the sliders when 2400. * * @private */ TimePickerClock.prototype._setEnabled = function(bEnabled) { this.setEnabled(bEnabled); if (bEnabled) { this.$().removeClass("sapMTPDisabled"); } else { this.$().addClass("sapMTPDisabled"); } return this; }; return TimePickerClock; });