UNPKG

@openui5/sap.m

Version:

OpenUI5 UI Library sap.m

1,563 lines (1,333 loc) 57.9 kB
/*! * UI development toolkit for HTML5 (OpenUI5) * (c) Copyright 2009-2022 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ // Provides control sap.m.TimePicker. sap.ui.define([ './InputBase', './DateTimeField', './MaskInputRule', './ResponsivePopover', 'sap/ui/core/EnabledPropagator', 'sap/ui/core/IconPool', './TimePickerSliders', './MaskEnabler', 'sap/ui/Device', 'sap/ui/core/format/DateFormat', 'sap/ui/core/Locale', 'sap/m/library', 'sap/ui/core/LocaleData', './TimePickerRenderer', "sap/ui/events/KeyCodes", "sap/base/Log", "sap/ui/core/InvisibleText", "sap/ui/thirdparty/jquery" ], function( InputBase, DateTimeField, MaskInputRule, ResponsivePopover, EnabledPropagator, IconPool, TimePickerSliders, MaskEnabler, Device, DateFormat, Locale, library, LocaleData, TimePickerRenderer, KeyCodes, Log, InvisibleText, jQuery ) { "use strict"; // shortcut for sap.m.PlacementType var PlacementType = library.PlacementType, TimePickerMaskMode = library.TimePickerMaskMode, DEFAULT_STEP = 1; /** * Constructor for a new <code>TimePicker</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 single-field input control that enables the users to fill time related input fields. * * <h3>Overview</h3> * * The <code>TimePicker</code> control enables the users to fill time related input * fields using touch, mouse, or keyboard input. * * <h3>Usage</h3> * * Use this control if you want the user to select a time. If you want the user to * select a duration (1 hour), use the {@link sap.m.Select} control instead. * * The user can enter a date by: * * <ul><li>Using the <code>TimePicker</code> dropdown that opens in a popup</li> * <li>Typing it in directly in the input field</li></ul> * * On app level, there are two options to provide value for the * <code>TimePicker</code> - as a string to the <code>value</code> property or as a * JavaScript Date object to the <code>dateValue</code> property (only one of these * properties should be used at a time): * * <ul><li>Use the <code>value</code> property if you want to bind the * <code>TimePicker</code> to a model using the * <code>sap.ui.model.type.Time</code></li> * <li>Use the <code>value</code> property if the date is provided as a string from * the backend or inside the app (for example, as ABAP type DATS field)</li> * <li>Use the <code>dateValue</code> property if the date is already provided as a * JavaScript Date object or you want to work with a JavaScript Date object. * Use <code>dateValue</code> as a helper property to easily obtain the hours, minutes and seconds * of the chosen time. Although possible to bind it, the recommendation is to not to do it. * When binding is needed, use <code>value</code> property instead</li></ul> * * <h3>Formatting</h3> * * All formatting and parsing of values from and to strings is done using the * {@link sap.ui.core.format.DateFormat}. If a value is entered by typing it into * the input field, it must fit to the used time format and locale. * * Supported format options are pattern-based on Unicode LDML Date Format notation. * See {@link http://unicode.org/reports/tr35/#Date_Field_Symbol_Table} * * A time format must be specified, otherwise the default "HH:mm:ss a" will be * used. For example, if the <code>valueFormat</code> is "HH-mm-ss a", the * <code>displayFormat</code> is "HH:mm:ss a", and the used locale is English, a * valid value string is "10-30-15 AM", which leads to an output of "10:30:15 AM". * * If no placeholder is set to the <code>TimePicker</code>, the used * <code>displayFormat</code> is displayed as a placeholder. If another placeholder * is needed, it must be set. * * <b>Note:</b> If the string does NOT match the <code>displayFormat</code> * (from user input) or the <code>valueFormat</code> (on app level), the * {@link sap.ui.core.format.DateFormat} makes an attempt to parse it based on the * locale settings. For more information, see the respective documentation in the * API Reference. * * <h3>Responsive behavior</h3> * * The <code>TimePicker</code> is responsive and fully adapts to all device types. * For larger screens, such as tablet or desktop, it opens as a popover. For * mobile devices, it opens in full screen. * * @extends sap.m.DateTimeField * * @author SAP SE * @version 1.60.39 * * @constructor * @public * @since 1.32 * @alias sap.m.TimePicker * @see {@link fiori:https://experience.sap.com/fiori-design-web/time-picker/ Time Picker} */ var TimePicker = DateTimeField.extend("sap.m.TimePicker", /** @lends sap.m.TimePicker.prototype */ { metadata : { library : "sap.m", designtime: "sap/m/designtime/TimePicker.designtime", properties : { /** * Defines the locale used to parse string values representing time. * * Determines the locale, used to interpret the string, supplied by the * <code>value</code> property. * * Example: AM in the string "09:04 AM" is locale (language) dependent. * The format comes from the browser language settings if not set explicitly. * Used in combination with 12 hour <code>displayFormat</code> containing 'a', which * stands for day period string. */ localeId: {type : "string", group: "Data"}, /** * Displays the text of the general picker label and is read by screen readers. * It is visible only on phone. */ title: {type: "string", group: "Misc", defaultValue: null}, /** * Sets the minutes slider step. If step is less than 1, it will be automatically converted back to 1. * The minutes slider is populated only by multiples of the step. * @since 1.40 */ minutesStep: {type: "int", group: "Misc", defaultValue: DEFAULT_STEP}, /** * Sets the seconds slider step. If step is less than 1, it will be automatically converted back to 1. * The seconds slider is populated only by multiples of the step. * @since 1.40 */ secondsStep: {type: "int", group: "Misc", defaultValue: DEFAULT_STEP}, /** * Defines a placeholder symbol. Shown at the position where there is no user input yet. */ placeholderSymbol: {type: "string", group: "Misc", defaultValue: "_"}, /** * Mask defined by its characters type (respectively, by its length). * You should consider the following important facts: * 1. The mask characters normally correspond to an existing rule (one rule per unique char). * Characters which don't, are considered immutable characters (for example, the mask '2099', where '9' corresponds to a rule * for digits, has the characters '2' and '0' as immutable). * 2. Adding a rule corresponding to the <code>placeholderSymbol</code> is not recommended and would lead to an unpredictable behavior. * 3. You can use the special escape character '^' called "Caret" prepending a rule character to make it immutable. * Use the double escape '^^' if you want to make use of the escape character as an immutable one. */ mask: {type: "string", group: "Misc", defaultValue: null}, /** * Defines whether the mask is enabled. When disabled, there are no restrictions and * validation for the user and no placeholders are displayed. * * <b>Note:</b> A disabled mask does not reset any validation rules that are already * set. You can update the <code>mask</code> property and add new <code>rules</code> * while it is disabled. When <code>maskMode</code> is set to <code>On</code> again, * the <code>rules</code> and the updated <code>mask</code> will be applied. * * @since 1.54 */ maskMode: {type: "sap.m.TimePickerMaskMode", group: "Misc", defaultValue: TimePickerMaskMode.On}, /** * 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. * @since 1.54 */ support2400: {type: "boolean", group: "Misc", defaultValue: false} }, aggregations: { /** A list of validation rules (one rule per mask character). */ rules: {type: "sap.m.MaskInputRule", multiple: true, singularName: "rule"}, /** * Internal aggregation that contains the inner _picker pop-up. */ _picker: { type: "sap.m.ResponsivePopover", multiple: false, visibility: "hidden" } } }}); /** * Determines the format, displayed in the input field and the picker sliders. * * The default value is the browser's medium time format locale setting * {@link sap.ui.core.LocaleData#getTimePattern}. * If data binding with type {@link sap.ui.model.type.Time} or {@link sap.ui.model.odata.type.Time} * is used for the <code>value</code> property, the <code>displayFormat</code> property * is ignored as the information is provided from the binding itself. * * @returns {string} the value of property <code>displayFormat</code> * @public * @name sap.m.TimePicker#getDisplayFormat * @function */ /** * Determines the format of the value property. * * The default value is the browser's medium time format locale setting * {@link sap.ui.core.LocaleData#getTimePattern}. * If data binding with type {@link sap.ui.model.type.Time} or {@link sap.ui.model.odata.type.Time} * is used for the <code>value</code> property, the <code>valueFormat</code> property * is ignored as the information is provided from the binding itself. * * @returns {string} the value of property <code>valueFormat</code> * @public * @name sap.m.TimePicker#getValueFormat * @function */ /** * Holds a reference to a JavaScript Date Object. The <code>value</code> (string) * property will be set according to it. Alternatively, if the <code>value</code> * and <code>valueFormat</code> pair properties are supplied instead, * the <code>dateValue</code> will be instantiated according to the parsed * <code>value</code>. * * @returns {object} the value of property <code>dateValue</code> * @public * @name sap.m.TimePicker#getDateValue * @function */ IconPool.insertFontFaceStyle(); EnabledPropagator.call(TimePicker.prototype, true); MaskEnabler.call(TimePicker.prototype); var TimeFormatStyles = { Short: "short", Medium: "medium", Long: "long" }, TimeParts = { Hour: "hour", Minute: "minute", Second: "second" }, PLACEHOLDER_SYMBOL = '-'; /** * Initializes the control. * * @public */ TimePicker.prototype.init = function() { DateTimeField.prototype.init.apply(this, arguments); MaskEnabler.init.apply(this, arguments); this.setDisplayFormat(getDefaultDisplayFormat()); this._oResourceBundle = sap.ui.getCore().getLibraryResourceBundle("sap.m"); // marks if the value is valid or not this._bValid = false; /* stores the type of the used locale (e.g. 'medium', 'long') for the display see @https://openui5.hana.ondemand.com/docs/api/symbols/sap.ui.core.LocaleData.html#getTimePattern */ this._sUsedDisplayPattern = null; /* stores the type of the used locale (e.g. 'medium', 'long') for inputting see @https://openui5.hana.ondemand.com/docs/api/symbols/sap.ui.core.LocaleData.html#getTimePattern */ this._sUsedValuePattern = null; this._oDisplayFormat = null; this._sValueFormat = null; this._oPopoverKeydownEventDelegate = null; this._rPlaceholderRegEx = new RegExp(PLACEHOLDER_SYMBOL, 'g'); this._sLastChangeValue = null; var oIcon = this.addEndIcon({ id: this.getId() + "-icon", src: this.getIconSrc(), noTabStop: true, title: "" }); // idicates whether the picker is still open this._bShouldClosePicker = false; oIcon.addEventDelegate({ onmousedown: function (oEvent) { // as the popup closes automatically on blur - we need to remember its state this._bShouldClosePicker = this.isOpen(); } }, this); oIcon.attachPress(function () { this.toggleOpen(this._bShouldClosePicker); }, this); }; TimePicker.prototype.onBeforeRendering = function() { DateTimeField.prototype.onBeforeRendering.apply(this, arguments); var oValueHelpIcon = this._getValueHelpIcon(); if (oValueHelpIcon) { oValueHelpIcon.setProperty("visible", this.getEditable(), true); } }; /** * Called from parent if the control is destroyed. * * @private */ TimePicker.prototype.exit = function () { if (this._oTimeSemanticMaskHelper) { this._oTimeSemanticMaskHelper.destroy(); } MaskEnabler.exit.apply(this, arguments); this._removePickerEvents(); this._oResourceBundle = null; this._bValid = false; this._sUsedDisplayPattern = null; this._oDisplayFormat = null; this._oPopoverKeydownEventDelegate = null; this._sUsedValuePattern = null; this._sValueFormat = null; this._sLastChangeValue = null; }; TimePicker.prototype.getIconSrc = function () { return IconPool.getIconURI("time-entry-request"); }; TimePicker.prototype.isOpen = function () { return this._getPicker() && this._getPicker().isOpen(); }; TimePicker.prototype.toggleOpen = function (bOpened) { this[bOpened ? "_closePicker" : "_openPicker"](); }; /** * Handles the focusin event. * * @param {jQuery.Event} oEvent Event object */ TimePicker.prototype.onfocusin = function (oEvent) { var oPicker = this._getPicker(); var bIconClicked = this._isIconClicked(oEvent); MaskEnabler.onfocusin.apply(this, arguments); if (oPicker && oPicker.isOpen() && !bIconClicked) { this._closePicker(); } }; TimePicker.prototype._isIconClicked = function (oEvent) { return jQuery(oEvent.target).hasClass("sapUiIcon") || jQuery(oEvent.target).hasClass("sapMInputBaseIconContainer"); }; /** * Called before the picker appears. * * @override * @public */ TimePicker.prototype.onBeforeOpen = function() { /* Set the timevalues of the picker here to prevent user from seeing it */ var oSliders = this._getSliders(), oDateValue = this.getDateValue(), sInputValue = this._$input.val(), sFormat = this.getValueFormat(), iIndexOfHH = sFormat.indexOf("HH"), iIndexOfH = sFormat.indexOf("H"); oSliders.setValue(sInputValue); if (this._shouldSetInitialFocusedDateValue()) { oDateValue = this.getInitialFocusedDateValue(); } oSliders._setTimeValues(oDateValue, TimePickerSliders._isHoursValue24(sInputValue, iIndexOfHH, iIndexOfH)); oSliders.collapseAll(); /* Mark input as active */ this.$().addClass(InputBase.ICON_PRESSED_CSS_CLASS); }; /** * Called after the picker appears. * * @override * @public */ TimePicker.prototype.onAfterOpen = function() { var oSliders = this._getSliders(); if (oSliders) { oSliders.openFirstSlider(); //WAI-ARIA region this._handleAriaOnExpandCollapse(); } }; /** * Called after the picker closes. * * @override * @public */ TimePicker.prototype.onAfterClose = function() { this.$().removeClass(InputBase.ICON_PRESSED_CSS_CLASS); //WAI-ARIA region this._handleAriaOnExpandCollapse(); }; TimePicker.prototype._getValueHelpIcon = function () { var oValueHelpIcon = this.getAggregation("_endIcon"); return oValueHelpIcon && oValueHelpIcon[0]; }; /** * Handles input's change event by synchronizing <code>value</code>, * and <code>dateValue</code> properties with the input field. * * @param {string} sValue The string value to be synchronized with, if the input value is used * @private * @returns {boolean} true if <code>change</code> event was called, false otherwise. */ TimePicker.prototype._handleInputChange = function (sValue) { var oDate, sThatValue, bThatValue2400, sFormat = this.getValueFormat(), iIndexOfHH = sFormat.indexOf("HH"), iIndexOfH = sFormat.indexOf("H"); sValue = sValue || this._$input.val(); sThatValue = sValue; bThatValue2400 = TimePickerSliders._isHoursValue24(sThatValue, iIndexOfHH, iIndexOfH); this._bValid = true; if (sValue !== "") { //keep the oDate not changed by the 24 hrs oDate = this._parseValue( TimePickerSliders._isHoursValue24(sValue, iIndexOfHH, iIndexOfH) ? TimePickerSliders._replace24HoursWithZero(sValue, iIndexOfHH, iIndexOfH) : sValue, true); if (!oDate) { this._bValid = false; } else { // check if Formatter changed the value (it corrects some wrong inputs or known patterns) sValue = this._formatValue(oDate); } } sThatValue = this.getSupport2400() && bThatValue2400 ? "24:" + sValue.replace(/[0-9]/g, "0").slice(0, -3) : sValue; //instead on key stroke zeroes could be added after entering '24' this.updateDomValue(sThatValue); if (oDate) { // get the value in valueFormat sThatValue = sValue = this._formatValue(oDate, true); } this.setProperty("value", sThatValue, true); // no rerendering this._lastValue = sValue; if (this._bValid) { this.setProperty("dateValue", oDate, true); // no rerendering } this.fireChangeEvent(sThatValue, {valid: this._bValid}); return true; }; /** * Handles the input change event. * * @override * @param {jQuery.Event} oEvent Event object * @returns {boolean} true if <code>change</code> event was called, false otherwise. */ TimePicker.prototype.onChange = function(oEvent) { // don't call InputBase onChange because this calls setValue what would trigger a new formatting var sValueParam = oEvent ? oEvent.value : null; // check the control is editable or not if (this.getEditable() && this.getEnabled()) { return this._handleInputChange(sValueParam); } return false; }; /** * Sets the minutes slider step. * @param {int} step The step used to generate values for the minutes slider * @returns {*} this * @public */ TimePicker.prototype.setMinutesStep = function(step) { var oSliders = this._getSliders(); step = Math.max(DEFAULT_STEP, step || DEFAULT_STEP); if (oSliders) { oSliders.setMinutesStep(step); } return this.setProperty("minutesStep", step, true); }; /** * Sets the seconds slider step. * @param {int} step The step used to generate values for the seconds slider * @returns {sap.m.TimePicker} <code>this</code> to allow method chaining * @public */ TimePicker.prototype.setSecondsStep = function(step) { var oSliders = this._getSliders(); step = Math.max(DEFAULT_STEP, step || DEFAULT_STEP); if (oSliders) { oSliders.setSecondsStep(step); } return this.setProperty("secondsStep", step, true); }; /* * Sets the title label inside the picker. * * @param {string} title A title * @returns {sap.m.TimePicker} <code>this</code> to allow method chaining */ TimePicker.prototype.setTitle = function(title) { var oSliders = this._getSliders(); if (oSliders) { oSliders.setLabelText(title); } this.setProperty("title", title, true); return this; }; TimePicker.prototype._handleDateValidation = function (oDate) { if (!oDate) { this._bValid = false; Log.warning("Value can not be converted to a valid date", this); } else { this._bValid = true; this.setProperty("dateValue", oDate, true); // no rerendering var sValue = this._formatValue(oDate); if (this.isActive()) { this.updateDomValue(sValue); } else { this.setProperty("value", sValue, true); // no rerendering this._lastValue = sValue; this._sLastChangeValue = sValue; } } }; /** * Sets <code>support2400</code> of the control. * * Allows the control to use 24-hour format. * Recommended usage is to not use it with am/pm format. * * @param {boolean} bSupport2400 * @returns {sap.m.TimePicker} this instance, used for chaining * @public */ TimePicker.prototype.setSupport2400 = function (bSupport2400) { var oSliders = this._getSliders(); this.setProperty("support2400", bSupport2400, true); // no rerendering if (oSliders) { oSliders.setSupport2400(bSupport2400); } this._initMask(); return this; }; TimePicker.prototype.setDisplayFormat = function (sDisplayFormat) { var oSliders = this._getSliders(); this.setProperty("displayFormat", sDisplayFormat, true); // no rerendering this._initMask(); if (oSliders) { oSliders.setDisplayFormat(sDisplayFormat); } var oDateValue = this.getDateValue(); if (!oDateValue) { return this; } var sOutputValue = this._formatValue(oDateValue); this.updateDomValue(sOutputValue); this._lastValue = sOutputValue; return this; }; /** * Sets the current <code>value</code> of the control. * * Sets to whatever string was given if it cannot be parsed based on the * current <code>valueFormat</code>. Recommended usage is when <code>dateValue</code> * is not set as they are mutually exclusive. * * @override * @param {string} sValue New value * @returns {sap.m.TimePicker} this instance, used for chaining * @public */ TimePicker.prototype.setValue = function(sValue) { var oDate, sOutputValue, sFormat = this.getValueFormat(), iIndexOfHH = sFormat.indexOf("HH"), iIndexOfH = sFormat.indexOf("H"), oSliders = this._getSliders(); sValue = this.validateProperty("value", sValue); this._initMask(); MaskEnabler.setValue.call(this, sValue); this._sLastChangeValue = sValue; this._bValid = true; // convert to date object if (sValue) { //date object have to be consistent, so if value is 2400, set oDate to 00 oDate = this._parseValue(TimePickerSliders._isHoursValue24(sValue, iIndexOfHH, iIndexOfH) ? TimePickerSliders._replace24HoursWithZero(sValue, iIndexOfHH, iIndexOfH) : sValue); if (!oDate) { this._bValid = false; Log.warning("Value can not be converted to a valid date", this); } } if (this._bValid) { this.setProperty("dateValue", oDate, true); // no rerendering } // convert to output if (oDate && !this.getSupport2400()) { sOutputValue = this._formatValue(oDate); } else { sOutputValue = sValue; } if (oSliders) { oSliders.setValue(sValue); } // do not call InputBase.setValue because the displayed value and the output value might have different pattern this.updateDomValue(sOutputValue); this._lastValue = sOutputValue; return this; }; TimePicker.prototype.setDateValue = function(sValue) { this._initMask(); return DateTimeField.prototype.setDateValue.apply(this, arguments); }; /** * Sets tooltip of the control. * * @public * @override * @param {string|sap.ui.core.TooltipBase} vTooltip * @returns {sap.m.TimePicker} A reference to <code>this</code> instance to allow method chaining. */ TimePicker.prototype.setTooltip = function(vTooltip) { /* * We need this override the default setter from <code>sap.m.Input</code> because the super class method * doesn't respect the custom role id of the TimePicker which we add in 'aria-describedby' internally. */ var oDomRef = this.getDomRef(), sTooltip; this._refreshTooltipBaseDelegate(vTooltip); this.setAggregation("tooltip", vTooltip, true); if (!oDomRef) { return this; } sTooltip = this.getTooltip_AsString(); if (sTooltip) { oDomRef.setAttribute("title", sTooltip); } else { oDomRef.removeAttribute("title"); } this._handleTooltipHiddenTextLifecycle(); return this; }; /** * Handles the addition/removal of the hidden span element (used as an hidden aria description) and its correct * reference with the TP inner input. This method requires <code>TimePicker</code> to be rendered in the DOM. * @private * @returns {void} */ TimePicker.prototype._handleTooltipHiddenTextLifecycle = function () { var oRenderer, sDescribedByReferences, sAnnouncement, sHiddenTextIdPattern, bCreateHiddenText, oHiddenAriaTooltipElement; if (!sap.ui.getCore().getConfiguration().getAccessibility()) { return; } oRenderer = this.getRenderer(); sDescribedByReferences = oRenderer.getAriaDescribedBy(this); sAnnouncement = oRenderer.getDescribedByAnnouncement(this); sHiddenTextIdPattern = this.getId() + "-describedby"; bCreateHiddenText = sDescribedByReferences.indexOf(sHiddenTextIdPattern) > -1; oHiddenAriaTooltipElement = this.getDomRef("describedby"); if (bCreateHiddenText) { oHiddenAriaTooltipElement = document.createElement("span"); oHiddenAriaTooltipElement.id = sHiddenTextIdPattern; oHiddenAriaTooltipElement.setAttribute("aria-hidden", "true"); oHiddenAriaTooltipElement.className = "sapUiInvisibleText"; oHiddenAriaTooltipElement.textContent = sAnnouncement; this.getDomRef().appendChild(oHiddenAriaTooltipElement); } else { this.getDomRef().removeChild(oHiddenAriaTooltipElement); } this._$input.attr("aria-describedby", sDescribedByReferences); }; /** * Sets the locale of the control. * * Used for parsing and formatting the time values in languages different than English. * Necessary for translation and auto-complete of the day periods, such as AM and PM. * * @param {string} sLocaleId A locale identifier like 'en_US' * @returns {sap.m.TimePicker} this instance, used for chaining * @public */ TimePicker.prototype.setLocaleId = function(sLocaleId) { var sCurrentValue = this.getValue(), oSliders = this._getSliders(); this.setProperty("localeId", sLocaleId, true); this._initMask(); this._oDisplayFormat = null; this._sValueFormat = null; if (sCurrentValue) { this.setValue(sCurrentValue); } if (oSliders) { oSliders.setLocaleId(sLocaleId); } return this; }; TimePicker.prototype._getDefaultDisplayStyle = function () { return TimeFormatStyles.Medium; }; TimePicker.prototype._getDefaultValueStyle = function () { return TimeFormatStyles.Medium; }; // if the user has set localeId, create Locale from it, if not get the locate from the FormatSettings TimePicker.prototype._getLocale = function () { var sLocaleId = this.getLocaleId(); return sLocaleId ? new Locale(sLocaleId) : sap.ui.getCore().getConfiguration().getFormatSettings().getFormatLocale(); }; TimePicker.prototype._getFormatterInstance = function(oFormat, sPattern, bRelative, sCalendarType, bDisplayFormat) { var oLocale = this._getLocale(); if (sPattern === TimeFormatStyles.Short || sPattern === TimeFormatStyles.Medium || sPattern === TimeFormatStyles.Long) { oFormat = DateFormat.getTimeInstance({style: sPattern, strictParsing: true, relative: bRelative}, oLocale); } else { oFormat = DateFormat.getTimeInstance({pattern: sPattern, strictParsing: true, relative: bRelative}, oLocale); } if (bDisplayFormat) { this._sUsedDisplayPattern = sPattern; this._oDisplayFormat = oFormat; } else { this._sUsedValuePattern = sPattern; this._sValueFormat = oFormat; } return oFormat; }; /** * Obtains time pattern * @returns {*} the time pattern * @private */ TimePicker.prototype._getFormat = function () { var sFormat = this._getDisplayFormatPattern(); if (!sFormat) { sFormat = TimeFormatStyles.Medium; } if (Object.keys(TimeFormatStyles).indexOf(sFormat) !== -1) { sFormat = getDefaultDisplayFormat(); } return sFormat; }; /** * Handles the pageup event. * * Increases time by one hour. * * @param {jQuery.Event} oEvent Event object */ TimePicker.prototype.onsappageup = function(oEvent) { //increase by one hour this._increaseTime(1, TimeParts.Hour); oEvent.preventDefault(); //do not move cursor }; /** * Handles the shift + pageup and ctrl + shift + pageup events. * * Increases time by one minute or second. * * @param {jQuery.Event} oEvent Event object */ TimePicker.prototype.onsappageupmodifiers = function(oEvent) { if (!(oEvent.ctrlKey || oEvent.metaKey || oEvent.altKey) && oEvent.shiftKey) { //shift // increase by one minute this._increaseTime(1, TimeParts.Minute); } if (!oEvent.altKey && oEvent.shiftKey && (oEvent.ctrlKey || oEvent.metaKey)) { //ctrl+shift // increase by one second this._increaseTime(1, TimeParts.Second); } oEvent.preventDefault(); // do not move cursor }; /** * Handles the pagedown event. * * Decreases time by one hour. * * @param {jQuery.Event} oEvent Event object */ TimePicker.prototype.onsappagedown = function(oEvent) { //decrease by one hour this._increaseTime(-1, TimeParts.Hour); oEvent.preventDefault(); // do not move cursor }; /** * Handles the shift + pagedown and ctrl + shift + pagedown events. * * Decreases time by one minute or second. * * @param {jQuery.Event} oEvent Event object * @private */ TimePicker.prototype.onsappagedownmodifiers = function(oEvent) { if (!(oEvent.ctrlKey || oEvent.metaKey || oEvent.altKey) && oEvent.shiftKey) { //shift // decrease by one minute this._increaseTime(-1, TimeParts.Minute); } if (!oEvent.altKey && oEvent.shiftKey && (oEvent.ctrlKey || oEvent.metaKey)) { //ctrl+shift // decrease by one second this._increaseTime(-1, TimeParts.Second); } oEvent.preventDefault(); // do not move cursor }; /** * Handles the keydown event. * * Opens or closes the picker if specific key combinations are pressed. * * @param {jQuery.Event} oEvent Event object * @private */ TimePicker.prototype.onkeydown = function(oEvent) { var oKC = KeyCodes, iKC = oEvent.which || oEvent.keyCode, bAlt = oEvent.altKey, bPickerOpened; // Popover should be opened when F4, ALT+UP or ALT+DOWN is pressed if (iKC === oKC.F4 || (bAlt && (iKC === oKC.ARROW_UP || iKC === oKC.ARROW_DOWN))) { bPickerOpened = this._getPicker() && this._getPicker().isOpen(); if (!bPickerOpened) { this._openPicker(); } else { this._closePicker(); } oEvent.preventDefault(); //ie expands the address bar on F4 } else { MaskEnabler.onkeydown.call(this, oEvent); } }; /** * Gets the picker aggregation. * * @returns {sap.m.ResponsivePopover|undefined} The picker aggregation * @private */ TimePicker.prototype._getPicker = function() { return this.getAggregation("_picker"); }; /** * Detaches the picker from the keyboard events. * * @private */ TimePicker.prototype._removePickerEvents = function() { var oPopover, oPicker = this._getPicker(); if (oPicker) { oPopover = oPicker.getAggregation("_popup"); if (typeof this._oPopoverKeydownEventDelegate === 'function') { oPopover.removeEventDelegate(this._oPopoverKeydownEventDelegate); } } }; /** * Opens the picker. * * Creates the picker if necessary. * * @returns {sap.m.ResponsivePopover} The picker part as a control, used for chaining * @private */ TimePicker.prototype._openPicker = function () { var oPicker = this._getPicker(), oSliders; if (!oPicker) { oPicker = this._createPicker(this._getDisplayFormatPattern()); } oPicker.open(); oSliders = this._getSliders(); setTimeout(oSliders._updateSlidersValues.bind(oSliders), 0); return oPicker; }; /** * Closes the TimePicker popover. * * @returns {sap.m.ResponsivePopover|undefined} The picker part as a control, used for chaining * @private */ TimePicker.prototype._closePicker = function () { var oPicker = this._getPicker(); if (oPicker) { oPicker.close(); } else { Log.warning("There is no picker to close."); } return oPicker; }; /** * Creates the picker. * * Uses {@link sap.m.ResponsivePopover} control for a picker. * * @param {string} sFormat Time format used for creating the sliders inside the picker * @returns {sap.m.TimePicker} the sap.m.TimePicker * @private */ TimePicker.prototype._createPicker = function(sFormat) { var that = this, oPopover, oPicker, oResourceBundle, sOKButtonText, sCancelButtonText, sTitle, oIcon = this.getAggregation("_endIcon")[0], sLocaleId = this._getLocale().getLanguage(); oResourceBundle = sap.ui.getCore().getLibraryResourceBundle("sap.m"); sOKButtonText = oResourceBundle.getText("TIMEPICKER_SET"); sCancelButtonText = oResourceBundle.getText("TIMEPICKER_CANCEL"); sTitle = this.getTitle(); oPicker = new ResponsivePopover(that.getId() + "-RP", { showCloseButton: false, showHeader: false, horizontalScrolling: false, verticalScrolling: false, placement: PlacementType.VerticalPreferedBottom, beginButton: new sap.m.Button({ text: sOKButtonText, press: jQuery.proxy(this._handleOkPress, this) }), endButton: new sap.m.Button({ text: sCancelButtonText, press: jQuery.proxy(this._handleCancelPress, this) }), content: [ new TimePickerSliders(this.getId() + "-sliders", { support2400: this.getSupport2400(), displayFormat: sFormat, valueFormat: this.getValueFormat(), labelText: sTitle ? sTitle : "", localeId: sLocaleId, minutesStep: this.getMinutesStep(), secondsStep: this.getSecondsStep() })._setShouldOpenSliderAfterRendering(true) ], contentHeight: TimePicker._PICKER_CONTENT_HEIGHT, ariaLabelledBy: InvisibleText.getStaticId("sap.m", "TIMEPICKER_SET_TIME") }); oPopover = oPicker.getAggregation("_popup"); // hide arrow in case of popover as dialog does not have an arrow if (oPopover.setShowArrow) { oPopover.setShowArrow(false); } oPopover.oPopup.setAutoCloseAreas([oIcon]); oPicker.addStyleClass(this.getRenderer().CSS_CLASS + "DropDown") .attachBeforeOpen(this.onBeforeOpen, this) .attachAfterOpen(this.onAfterOpen, this) .attachAfterClose(this.onAfterClose, this); oPicker.open = function() { return this.openBy(that); }; if (Device.system.desktop) { this._oPopoverKeydownEventDelegate = { onkeydown: function(oEvent) { var oKC = KeyCodes, iKC = oEvent.which || oEvent.keyCode, bAlt = oEvent.altKey; // Popover should be closed when ESCAPE key or ATL+F4 is pressed if ((bAlt && (iKC === oKC.ARROW_UP || iKC === oKC.ARROW_DOWN)) || iKC === oKC.F4) { this._handleOkPress(oEvent); //focus the input this.focus(); oEvent.preventDefault(); } } }; oPopover.addEventDelegate(this._oPopoverKeydownEventDelegate, this); //override popover callback - the best place to update content layout oPopover._afterAdjustPositionAndArrowHook = function() { that._getSliders()._onOrientationChanged(); }; } // define a parent-child relationship between the control's and the _picker pop-up this.setAggregation("_picker", oPicker, true); return oPicker; }; /** * Gets all attached sliders to this TimePicker instance. * @private * @returns {sap.m.TimePickerSliders|null} returns the content of the picker (The sliders control). */ TimePicker.prototype._getSliders = function () { var oPicker = this._getPicker(); if (!oPicker) { return null; } return oPicker.getContent()[0]; }; /** * Handles the press event of the OK button. * * @param {jQuery.Event} oEvent Event object * @private */ TimePicker.prototype._handleOkPress = function(oEvent) { var oDate = this._getSliders().getTimeValues(), sValue = this._formatValue(oDate); //if 24 is selected for hours, it should also go to the input after pressing the OK button if (this.getSupport2400()) { sValue = this._getSliders().getValue(); } this.updateDomValue(sValue); this._handleInputChange(); this._closePicker(); }; /** * Handles the press event of the Cancel button. * * @param {jQuery.Event} oEvent Event object * @private */ TimePicker.prototype._handleCancelPress = function(oEvent) { this._closePicker(); }; TimePicker.prototype._getLocaleBasedPattern = function (sPlaceholder) { return LocaleData.getInstance( sap.ui.getCore().getConfiguration().getFormatSettings().getFormatLocale() ).getTimePattern(sPlaceholder); }; /** * Parses a given string to a date object, based on either the <code>displayFormat</code> * or the <code>valueFormat</code>. * * @param {string} sValue Value to be parsed * @param {boolean} bDisplayFormat Defines whether the string being parsed is in <code>displayFormat</code> or in <code>valueFormat</code> * @returns {Object} A date object * @private */ TimePicker.prototype._parseValue = function(sValue, bDisplayFormat) { //because of the leading space in formats without a leading zero if (bDisplayFormat) { sValue = this._oTimeSemanticMaskHelper.stripValueOfLeadingSpaces(sValue); //if the user input is not full and there are placeholder symbols left, they need to be removed in order //the value to be parsed to a valid fallback format sValue = sValue.replace(this._rPlaceholderRegEx,''); } // convert to date object return DateTimeField.prototype._parseValue.call(this, sValue, bDisplayFormat); }; /** * Converts the time to the output format. * * If bValueFormat is set, it converts it to the <code>valueFormat</code>. * * @param {Date} oDate A JavaScript date object * @param {boolean} bValueFormat Defines whether the result is in <code>valueFormat</code> or <code>displayFormat</code> * @returns {string} Formatted value * @private */ TimePicker.prototype._formatValue = function(oDate, bValueFormat) { var sValue = DateTimeField.prototype._formatValue.apply(this, arguments), sFormat = this.getValueFormat(), iIndexOfHH = sFormat.indexOf("HH"), iIndexOfH = sFormat.indexOf("H"); if (oDate) { // in display format the formatter returns strings without the leading space // that we use in the mask - "9:15" instead of " 9:15" // that's because the mask is fixed length // this._oTimeSemanticMaskHelper will always exist if we have displayformat and localeId set // and they both have default values, but check just in case if (!bValueFormat && this._oTimeSemanticMaskHelper) { sValue = this._oTimeSemanticMaskHelper.formatValueWithLeadingTrailingSpaces(sValue); } } //2400 scenario - be sure that the correct value will be set in all cases - when binding, //setting the value by sliders or only via setValue if (this.getSupport2400() && TimePickerSliders._isHoursValue24(this.getValue(), iIndexOfHH, iIndexOfH) && TimePickerSliders._replaceZeroHoursWith24(sValue, iIndexOfHH, iIndexOfH) === this.getValue()) { sValue = this.getValue(); } return sValue; }; /** * Handles the correct value for ARIA expanded attribute on the TimePicker's input field. * * @private */ TimePicker.prototype._handleAriaOnExpandCollapse = function () { this.getFocusDomRef().setAttribute("aria-expanded", this._getPicker().isOpen()); }; /** * Changes the time value in the input field. * * @param {number} iNumber Number to be added to the existing value * @param {string} sUnit Unit to be changed - minute, hour or second * @private */ TimePicker.prototype._increaseTime = function(iNumber, sUnit) { var oOldDate = this.getDateValue(), oDate, iMsOffset; if (oOldDate && this.getEditable() && this.getEnabled()) { // use a new date object to have a real updated property oDate = new Date(oOldDate.getTime()); switch (sUnit) { case TimeParts.Hour: oDate.setHours(oDate.getHours() + iNumber); iMsOffset = 60 * 60 * 1000; break; case TimeParts.Minute: oDate.setMinutes(oDate.getMinutes() + iNumber); iMsOffset = 60 * 1000; break; case TimeParts.Second: iMsOffset = 1000; oDate.setSeconds(oDate.getSeconds() + iNumber); } // forward moving back from daylight saving doesn't introduce any issues // (because it will get into cycle), 3:00:00 + 00:00:01 === 3:00:01 // but decreasing time when moving into daylignt saving has an issue (3:00:00 - 0:00:01 === 3:59:59) if (iNumber < 0 && oDate.getTime() - oOldDate.getTime() !== iNumber * iMsOffset) { //hour stays the same // so decrease it with the milliseconds offset // and let the hours adjust automatically oDate = new Date(oOldDate.getTime() + iNumber * iMsOffset); } this.setDateValue(oDate); this.fireChangeEvent(this.getValue(), {valid: true}); } }; /** * Sets the mask of the input based on the display format. * * @private */ TimePicker.prototype._initMask = function() { if (this._oTimeSemanticMaskHelper) { this._oTimeSemanticMaskHelper.destroy(); } this._oTimeSemanticMaskHelper = new TimeSemanticMaskHelper(this); }; /** * Returns if the mask is enabled. If value is not valid we should set initialFocusedDateValue * * @returns {boolean} * @private */ TimePicker.prototype._isMaskEnabled = function () { return this.getMaskMode() === TimePickerMaskMode.On; }; TimePicker.prototype._shouldSetInitialFocusedDateValue = function () { if (!this._isValidValue()) { return true; } return !this.getValue() && !!this.getInitialFocusedDateValue(); }; /** * @private */ TimePicker.prototype._isValidValue = function () { return this._bValid; }; /** * Fires the change event for the listeners * * @protected * @param {String} sValue value of the input. * @param {Object} [oParams] extra event parameters. */ TimePicker.prototype.fireChangeEvent = function(sValue, oParams) { if (sValue) { sValue = sValue.trim(); } if (sValue !== this._sLastChangeValue) { this._sLastChangeValue = sValue; InputBase.prototype.fireChangeEvent.call(this, sValue, oParams); } }; var TimeSemanticMaskHelper = function(oTimePicker) { var sDisplayFormat = oTimePicker._getDisplayFormatPattern(), sMask, sAllowedHourChars, oLocale = oTimePicker._getLocale(), i; if (oTimePicker._checkStyle(sDisplayFormat)) { sMask = LocaleData.getInstance(oLocale).getTimePattern(sDisplayFormat); } else { sDisplayFormat = sDisplayFormat.replace(/'/g, ""); // single quotes (like 'ч') are irrelevant for DateFormat, so they are for the mask sMask = sDisplayFormat; } this._oTimePicker = oTimePicker; this.aOriginalAmPmValues = LocaleData.getInstance(oLocale).getDayPeriods("abbreviated"); this.aAmPmValues = this.aOriginalAmPmValues.slice(0); this.iAmPmValueMaxLength = Math.max(this.aAmPmValues[0].length, this.aAmPmValues[1].length); for (i = 0; i < this.aAmPmValues.length; i++) { while (this.aAmPmValues[i].length < this.iAmPmValueMaxLength) { this.aAmPmValues[i] += " "; } } this.b24H = sDisplayFormat.indexOf("H") !== -1; this.bLeadingZero = sDisplayFormat.indexOf("HH") !== -1 || sDisplayFormat.indexOf("hh") !== -1; this.sLeadingChar = this.bLeadingZero ? "0" : " "; this.sAlternativeLeadingChar = this.bLeadingZero ? " " : "0"; this.sLeadingRegexChar = this.bLeadingZero ? "0" : "\\s"; oTimePicker.setPlaceholderSymbol(PLACEHOLDER_SYMBOL); //set hours allowed chars in the mask sMask = sMask.replace(/hh/ig, "h").replace(/h/ig, "h9"); if (this.b24H) { sAllowedHourChars = "[" + this.sLeadingRegexChar + "012]"; } else { sAllowedHourChars = "[" + this.sLeadingRegexChar + "1]"; } this._maskRuleHours = new MaskInputRule({ maskFormatSymbol: "h", regex: sAllowedHourChars }); oTimePicker.addRule(this._maskRuleHours); this.iHourNumber1Index = sMask.indexOf("h9"); this.iHourNumber2Index = this.iHourNumber1Index !== -1 ? this.iHourNumber1Index + 1 : -1; //set minutes and seconds allowed chars in the mask this.iMinuteNumber1Index = sMask.indexOf("mm"); sMask = sMask.replace(/mm/g, "59"); this.iSecondNumber1Index = sMask.indexOf("ss"); sMask = sMask.replace(/ss/g, "59"); this._maskRuleMinSec = new MaskInputRule({ maskFormatSymbol: "5", regex: "[0-5]" }); oTimePicker.addRule(this._maskRuleMinSec); this.aAllowedHours = genValidHourValues.call(this, this.b24H, this.sLeadingChar); this.aAllowedMinutesAndSeconds = genValidMinutesAndSecondsValues.call(this); this.iAmPmChar1Index = sMask.indexOf("a"); this.iAfterAmPmValueIndex = -1; if (this.iAmPmChar1Index !== -1) { this.iAfterAmPmValueIndex = this.iAmPmChar1Index + this.iAmPmValueMaxLength; var iCorrectionIndexes = this.iAmPmValueMaxLength - "a".length; this.shiftIndexes(iCorrectionIndexes); //We start from capital A. Capital letters are not used to this point, so there should be enough of them var currentDefinitionSymbolCharCode = 65; var sAmPmRegex = ""; var currentAllowedChars = ""; var currentDefinitionSymbol = ""; for (i = 0; i < this.iAmPmValueMaxLength; i++) { currentAllowedChars = "["; if (this.aAmPmValues[0][i]) { currentAllowedChars += this.aAmPmValues[0][i]; } else { currentAllowedChars += "\\s"; } if (this.aAmPmValues[1][i] !== this.aAmPmValues[0][i]) { if (this.aAmPmValues[1][i]) { currentAllowedChars += this.aAmPmValues[1][i]; } else { currentAllowedChars += "\\s"; } } currentAllowedChars += "]"; currentDefinitionSymbol = String.fromCharCode(currentDefinitionSymbolCharCode++); sAmPmRegex += currentDefinitionSymbol; this._maskRuleChars = new MaskInputRule({ maskFormatSymbol: currentDefinitionSymbol, regex: currentAllowedChars }); oTimePicker.addRule(this._maskRuleChars); } sMask = sMask.replace(/a/g, sAmPmRegex); } oTimePicker.setMask(sMask); function genValues(iStart, iEnd, sLeadingChar) { var aResultValues = [], sCurrent, i; for (i = iStart; i <= iEnd; i++) { sCurrent = i.toString(); if (i < 10) { sCurrent = sLeadingChar + sCurrent; } aResultValues.push(sCurrent); } return aResultValues; } //not too expensive to generate all values that are valid hour values function genValidHourValues(b24H, sLeadingChar) { var iStart = b24H ? 0 : 1, b2400 = this._oTimePicker.getSupport2400() ? 24 : 23,//if getSupport2400, the user could type 24 in the input iEnd = b24H ? b2400 : 12; return genValues(iStart, iEnd, sLeadingChar); } function genValidMinutesAndSecondsValues() { return genValues(0, 59, "0"); } }; TimeSemanticMaskHelper.prototype.replaceChar = function(sChar, iPlacePosition, sCurrentInputValue) { var iAmPmInsideValueIndex = iPlacePosition - this.iAmPmChar1Index, sCurrentAmPmBeforeValue, sAmPreceedingValue, sPmPreceedingValue, bSameAmAndPmPreceedingValue, sAmRemainingValue, sPmRemainingValue, i; // we type the first hour number, but it doesn't match the mask, // but it would have if we prefill the leading character if (iPlacePosition === this.iHourNumber1Index && this.sAlternativeLeadingChar === sChar) { if (this.aAllowedHours.indexOf(this.sLeadingChar + sChar) !== -1) { return this.sLeadingChar + sChar; } else { return this.sLeadingChar; } } else if (iPlacePosition === this.iHourNumber1Index && !this._oTimePicker._isCharAllowed(sChar, iPlacePosition) && this.aAllowedHours.indexOf(this.sLeadingChar + sChar) !== -1) { return this.sLeadingChar + sChar; } else if (iPlacePosition === this.iHourNumber2Index //the second hour number && this.aAllowedHours.indexOf(sCurrentInputValue[this.iHourNumber1Index] + sChar) === -1) { //allow it only if the whole hour string is a valid hour return ""; //which is invalid and won't pass the test } else if ((iPlacePosition === this.iMinuteNumber1Index || iPlacePosition === this.iSecondNumber1Index) && !this._oTimePicker._isCharAllowed(sChar, iPlacePosition) && this.aAllowedMinutesAndSeconds.indexOf("0" + sChar) !== -1) { //the 1st minute number return "0" + sChar; } else if (iAmPmInsideValueIndex >= 0 && iPlacePosition < this.iAfterAmPmValueIndex) { sCurrentAmPmBeforeValue = sCurrentInputValue.slice(this.iAmPmChar1Index, iPlacePosition); sAmPreceedingValue = this.aAmPmValues[0].slice(0, iAmPmInsideValueIndex); sPmPreceedingValue = this.aAmPmValues[1].slice(0, iAmPmInsideValueIndex); sAmRemainingValue = this.aAmPmValues[0].slice(iAmPmInsideValueIndex, this.iAfterAmPmValueIndex); sPmRemainingValue = this.aAmPmValues[1].slice(iAmPmInsideValueIndex, this.iAfterAmPmValueIndex); bSameAmAndPmPreceedingValue = (sAmPreceedingValue === sPmPreceedingValue); var sMatchValue = ""; for (i = iAmPmInsideValueIndex; i < this.iAmPmValueMaxLength; i++) { if (this.aAmPmValues[0][i] === this.aAmPmValues[1][i]) { sMatchValue += this.aAmPmValues[0][i]; } else { break; } } // we reached the end, so values are all the same to the end // OR there is some string that is the same, but to some point if (i === this.iAmPmValueMaxLength || i !== iAmPmInsideValueIndex) { return sMatchValue; } else { //no match at all - ther