From 910897fc97d87b08f01627094ec8c159f5267628 Mon Sep 17 00:00:00 2001 From: Samuel Degueldre Date: Mon, 28 Aug 2023 15:42:31 +0200 Subject: [PATCH] [REF] web, *: remove tempusdominus date(time) picker lib As tempusdominus relies on jQuery, we have written our own date(time) picker in owl. This commit converts the remaining uses of the tempusdominus date(time) picker and replaces them by our own. closes odoo/odoo#133349 Signed-off-by: Julien Mougenot (jum) --- addons/purchase/controllers/portal.py | 2 +- .../static/src/js/purchase_datetimepicker.js | 32 +- addons/purchase/views/portal_templates.xml | 14 +- addons/survey/static/src/js/survey_form.js | 137 +- .../src/scss/survey_templates_form.scss | 6 - addons/survey/views/survey_templates.xml | 24 +- addons/web/__manifest__.py | 5 +- addons/web/static/lib/tempusdominus/LICENSE | 21 - .../static/lib/tempusdominus/tempusdominus.js | 2811 ----------------- .../lib/tempusdominus/tempusdominus.scss | 342 -- .../static/src/core/datetime/datetime_hook.js | 456 +-- .../core/datetime/datetimepicker_service.js | 470 +++ .../static/src/core/network/rpc_service.js | 4 +- .../static/src/core/popover/popover_hook.js | 53 +- .../src/legacy/js/public/public_root.js | 2 - .../src/public/datetime_picker_widget.js | 36 + .../core/datetime/datetime_hook_tests.js | 4 +- .../core/datetime/datetime_input_tests.js | 4 +- .../tests/core/domain_selector_tests.js | 2 + .../web/static/tests/core/dropdown_tests.js | 2 + addons/web/static/tests/search/helpers.js | 2 + addons/web/static/tests/webclient/helpers.js | 2 + addons/web_editor/__manifest__.py | 5 - .../static/src/js/editor/snippets.editor.js | 1 + .../static/src/js/editor/snippets.options.js | 93 +- .../static/src/scss/website.edit_mode.scss | 2 +- .../static/src/snippets/s_website_form/000.js | 116 +- .../static/src/xml/website_form_editor.xml | 12 +- .../tours/website_form_editor_frontend.js | 7 +- .../static/src/js/crm_partner_assign.js | 29 +- .../website_crm_partner_assign_templates.xml | 12 +- addons/website_slides/__manifest__.py | 3 - debian/copyright | 4 - 33 files changed, 729 insertions(+), 3986 deletions(-) delete mode 100644 addons/web/static/lib/tempusdominus/LICENSE delete mode 100644 addons/web/static/lib/tempusdominus/tempusdominus.js delete mode 100644 addons/web/static/lib/tempusdominus/tempusdominus.scss create mode 100644 addons/web/static/src/core/datetime/datetimepicker_service.js create mode 100644 addons/web/static/src/public/datetime_picker_widget.js diff --git a/addons/purchase/controllers/portal.py b/addons/purchase/controllers/portal.py index 9726f729f31..0371266727f 100644 --- a/addons/purchase/controllers/portal.py +++ b/addons/purchase/controllers/portal.py @@ -167,7 +167,7 @@ class CustomerPortal(portal.CustomerPortal): return request.render("purchase.portal_my_purchase_order_update_date", values) return request.render("purchase.portal_my_purchase_order", values) - @http.route(['/my/purchase//update'], type='http', methods=['POST'], auth="public", website=True) + @http.route(['/my/purchase//update'], type='json', auth="public", website=True) def portal_my_purchase_order_update_dates(self, order_id=None, access_token=None, **kw): """User update scheduled date on purchase order line. """ diff --git a/addons/purchase/static/src/js/purchase_datetimepicker.js b/addons/purchase/static/src/js/purchase_datetimepicker.js index de1be92f6df..25baf927a5b 100644 --- a/addons/purchase/static/src/js/purchase_datetimepicker.js +++ b/addons/purchase/static/src/js/purchase_datetimepicker.js @@ -1,6 +1,26 @@ -$(function () { - $('input.o-purchase-datetimepicker').datetimepicker(); - $('input.o-purchase-datetimepicker').on("hide.datetimepicker", function () { - $(this).parents('form').submit(); - }); -}) +/** @odoo-module */ +import PublicWidget from "@web/legacy/js/public/public_widget"; + +export const PurchaseDatePicker = PublicWidget.Widget.extend({ + selector: ".o-purchase-datetimepicker", + start() { + this.call("datetime_picker", "create", { + target: this.el, + onChange: (newDate) => { + const { accessToken, orderId, lineId } = this.el.dataset; + this._rpc({ + route: `/my/purchase/${orderId}/update?access_token=${accessToken}`, + params: { + [lineId]: newDate.toISODate(), + }, + }); + }, + pickerProps: { + type: "date", + value: luxon.DateTime.fromISO(this.el.dataset.value), + }, + }).enable(); + }, +}); + +PublicWidget.registry.PurchaseDatePicker = PurchaseDatePicker; diff --git a/addons/purchase/views/portal_templates.xml b/addons/purchase/views/portal_templates.xml index 23cbc00500a..6091f0b4135 100644 --- a/addons/purchase/views/portal_templates.xml +++ b/addons/purchase/views/portal_templates.xml @@ -262,7 +262,6 @@ Products Quantity Scheduled Date - Update Here Unit Price Taxes @@ -296,19 +295,20 @@ - -
-
- +
-
maxDate) || + (minDate.isValid && date < minDate) + ) { errors[questionId] = validationErrorMsg; } } @@ -691,11 +696,15 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa params[this.name] = this.value; break; case 'date': - params = self._prepareSubmitDates(params, this.name, this.value, false); - break; - case 'datetime': - params = self._prepareSubmitDates(params, this.name, this.value, true); + case 'datetime':{ + const [parse, serialize] = + $(this).data("questionType") === "date" + ? [parseDate, serializeDate] + : [parseDateTime, serializeDateTime]; + const date = parse(this.value); + params[this.name] = date ? serialize(date) : ""; break; + } case 'simple_choice_radio': case 'multiple_choice': params = self._prepareSubmitChoices(params, $(this), $(this).data('name')); @@ -706,19 +715,6 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa } }); }, - - /** - * Prepare date answer before submitting form. - * Convert date value from client current timezone to UTC Date to correspond to the server format. - * return params = { 'dateQuestionId' : '2019-05-23', 'datetimeQuestionId' : '2019-05-23 14:05:12' } - */ - _prepareSubmitDates: function (params, questionId, value, isDateTime) { - var momentDate = isDateTime ? parseDateTime(value, null, {timezone: true}) : parseDate(value); - var formattedDate = momentDate ? momentDate.toJSON() : ''; - params[questionId] = formattedDate; - return params; - }, - /** * Prepare choice answer before submitting form. * If the answer is not the 'comment selection' (=Other), calls the _prepareSubmitAnswer method to add the answer to the params @@ -919,77 +915,6 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa } }, - /** - * Initialize datetimepicker in correct format and with constraints - */ - _initDateTimePicker: function ($dateGroup) { - var disabledDates = []; - var questionType = $dateGroup.find('input').data('questionType'); - var minDateData = $dateGroup.data('mindate'); - var maxDateData = $dateGroup.data('maxdate'); - - var datetimepickerFormat = questionType === 'datetime' ? time.getLangDatetimeFormat() : time.getLangDateFormat(); - - var minDate = minDateData - ? this._formatDateTime(minDateData, datetimepickerFormat) - : moment({ y: 1000 }); - - var maxDate = maxDateData - ? this._formatDateTime(maxDateData, datetimepickerFormat) - : moment().add(200, "y"); - - if (questionType === 'date') { - // Include min and max date in selectable values - maxDate = moment(maxDate).add(1, "d"); - minDate = moment(minDate).subtract(1, "d"); - disabledDates = [minDate, maxDate]; - } - - $dateGroup.datetimepicker({ - format: datetimepickerFormat, - minDate: minDate, - maxDate: maxDate, - disabledDates: disabledDates, - useCurrent: false, - viewDate: moment(new Date()).hours(minDate.hours()).minutes(minDate.minutes()).seconds(minDate.seconds()).milliseconds(minDate.milliseconds()), - calendarWeeks: true, - icons: { - time: 'fa fa-clock-o', - date: 'fa fa-calendar', - next: 'oi oi-chevron-right', - previous: 'oi oi-chevron-left', - up: 'oi oi-chevron-up', - down: 'oi oi-chevron-down', - }, - locale: Settings.defaultLocale, - allowInputToggle: true, - }); - $dateGroup.on('error.datetimepicker', (err) => { - if (err.date) { - if (err.date < minDate) { - this.call("dialog", "add", AlertDialog, { - body: - _t("The date you selected is lower than the minimum date: ") + - minDate.format(datetimepickerFormat), - }); - } - - if (err.date > maxDate) { - this.call("dialog", "add", AlertDialog, { - body: - _t("The date you selected is greater than the maximum date: ") + - maxDate.format(datetimepickerFormat), - }); - } - } - return false; - }); - }, - - _formatDateTime: function (datetimeValue, format) { - return moment(formatDateTime(moment(datetimeValue), null, {timezone: true}), format); - }, - _initResultWidget: function () { var $result = this.$('.o_survey_result'); if ($result.length) { diff --git a/addons/survey/static/src/scss/survey_templates_form.scss b/addons/survey/static/src/scss/survey_templates_form.scss index 57754f4ca33..f468123419d 100644 --- a/addons/survey/static/src/scss/survey_templates_form.scss +++ b/addons/survey/static/src/scss/survey_templates_form.scss @@ -114,12 +114,6 @@ _::-webkit-full-page-media, _:future, :root .o_survey_wrap { } } - .o_survey_form_date [data-toggle="datetimepicker"] { - right: 0; - bottom: 5px; - top: auto; - } - .o_survey_choice_btn { transition: background-color 0.3s ease; flex: 1 0 300px; diff --git a/addons/survey/views/survey_templates.xml b/addons/survey/views/survey_templates.xml index 416393c6903..56957489ec7 100644 --- a/addons/survey/views/survey_templates.xml +++ b/addons/survey/views/survey_templates.xml @@ -371,28 +371,28 @@ diff --git a/addons/web/__manifest__.py b/addons/web/__manifest__.py index c0f9ef64849..533a21923e2 100644 --- a/addons/web/__manifest__.py +++ b/addons/web/__manifest__.py @@ -182,8 +182,6 @@ This module provides the core of the Odoo Web Client. ('include', 'web._assets_bootstrap_frontend'), - 'web/static/src/legacy/scss/tempusdominus_overridden.scss', - 'web/static/lib/tempusdominus/tempusdominus.scss', 'web/static/lib/jquery.ui/jquery-ui.css', 'web/static/src/libs/fontawesome/css/font-awesome.css', 'web/static/lib/odoo_ui_icons/*', @@ -234,7 +232,6 @@ This module provides the core of the Odoo Web Client. 'web/static/lib/bootstrap/js/dist/scrollspy.js', 'web/static/lib/bootstrap/js/dist/tab.js', 'web/static/lib/bootstrap/js/dist/toast.js', - 'web/static/lib/tempusdominus/tempusdominus.js', 'web/static/lib/select2/select2.js', 'web/static/lib/clipboard/clipboard.js', 'web/static/lib/jSignature/jSignatureCustom.js', @@ -268,6 +265,7 @@ This module provides the core of the Odoo Web Client. ('remove', 'web/static/src/core/file_viewer/file_viewer.dark.scss'), ('remove', 'web/static/src/core/emoji_picker/emoji_data.js'), 'web/static/src/public/error_notifications.js', + 'web/static/src/public/datetime_picker_widget.js', 'web/static/src/libs/pdfjs.js', 'web/static/src/legacy/utils.js', @@ -512,7 +510,6 @@ This module provides the core of the Odoo Web Client. ('remove', 'web/static/tests/webclient/**/helpers.js'), # Legacy - 'web/static/lib/tempusdominus/tempusdominus.js', 'web/static/tests/legacy/**/*.js', ('remove', 'web/static/tests/legacy/helpers/**/*.js'), ('remove', 'web/static/tests/legacy/legacy_setup.js'), diff --git a/addons/web/static/lib/tempusdominus/LICENSE b/addons/web/static/lib/tempusdominus/LICENSE deleted file mode 100644 index d7ea8a7ec0d..00000000000 --- a/addons/web/static/lib/tempusdominus/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2016 Tempus Dominus - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/addons/web/static/lib/tempusdominus/tempusdominus.js b/addons/web/static/lib/tempusdominus/tempusdominus.js deleted file mode 100644 index 821f2c1d188..00000000000 --- a/addons/web/static/lib/tempusdominus/tempusdominus.js +++ /dev/null @@ -1,2811 +0,0 @@ -/*@preserve - * Tempus Dominus Bootstrap4 v5.0.1 (https://tempusdominus.github.io/bootstrap-4/) - * Copyright 2016-2018 Jonathan Peterson - * Licensed under MIT (https://github.com/tempusdominus/bootstrap-3/blob/master/LICENSE) - */ - -if (typeof jQuery === 'undefined') { - throw new Error('Tempus Dominus Bootstrap4\'s requires jQuery. jQuery must be included before Tempus Dominus Bootstrap4\'s JavaScript.'); -} - -+function ($) { - var version = $.fn.jquery.split(' ')[0].split('.'); - if ((version[0] < 2 && version[1] < 9) || (version[0] === 1 && version[1] === 9 && version[2] < 1) || (version[0] >= 4)) { - throw new Error('Tempus Dominus Bootstrap4\'s requires at least jQuery v3.0.0 but less than v4.0.0'); - } -}(jQuery); - - -if (typeof moment === 'undefined') { - throw new Error('Tempus Dominus Bootstrap4\'s requires moment.js. Moment.js must be included before Tempus Dominus Bootstrap4\'s JavaScript.'); -} - -var version = moment.version.split('.') -if ((version[0] <= 2 && version[1] < 17) || (version[0] >= 3)) { - throw new Error('Tempus Dominus Bootstrap4\'s requires at least moment.js v2.17.0 but less than v3.0.0'); -} - -+function () { - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -// ReSharper disable once InconsistentNaming -var DateTimePicker = function ($, moment) { - // ReSharper disable InconsistentNaming - var NAME = 'datetimepicker', - DATA_KEY = '' + NAME, - EVENT_KEY = '.' + DATA_KEY, - DATA_API_KEY = '.data-api', - Selector = { - // /!\ ODOO PATCH: ensure the datetimepickers can be toggled only after - // the Odoo lazy loader finished loading all lazy scripts. Another - // solution could have been to temporarily removing the data-toggle - // attributes during lazyloading but that would not have been stable as - // custom code could search for data-toggle elements while the lazy - // loading is being performed. Without this, clicking too soon on a - // datetimepicker would not use the right format and UI options. - DATA_TOGGLE: 'body:not(.o_lazy_js_waiting) [data-toggle="' + DATA_KEY + '"]' - }, - ClassName = { - INPUT: NAME + '-input' - }, - Event = { - CHANGE: 'change' + EVENT_KEY, - BLUR: 'blur' + EVENT_KEY, - KEYUP: 'keyup' + EVENT_KEY, - KEYDOWN: 'keydown' + EVENT_KEY, - FOCUS: 'focus' + EVENT_KEY, - CLICK_DATA_API: 'click' + EVENT_KEY + DATA_API_KEY, - //emitted - UPDATE: 'update' + EVENT_KEY, - ERROR: 'error' + EVENT_KEY, - HIDE: 'hide' + EVENT_KEY, - SHOW: 'show' + EVENT_KEY - }, - DatePickerModes = [{ - CLASS_NAME: 'days', - NAV_FUNCTION: 'M', - NAV_STEP: 1 - }, { - CLASS_NAME: 'months', - NAV_FUNCTION: 'y', - NAV_STEP: 1 - }, { - CLASS_NAME: 'years', - NAV_FUNCTION: 'y', - NAV_STEP: 10 - }, { - CLASS_NAME: 'decades', - NAV_FUNCTION: 'y', - NAV_STEP: 100 - }], - KeyMap = { - 'up': 38, - 38: 'up', - 'down': 40, - 40: 'down', - 'left': 37, - 37: 'left', - 'right': 39, - 39: 'right', - 'tab': 9, - 9: 'tab', - 'escape': 27, - 27: 'escape', - 'enter': 13, - 13: 'enter', - 'pageUp': 33, - 33: 'pageUp', - 'pageDown': 34, - 34: 'pageDown', - 'shift': 16, - 16: 'shift', - 'control': 17, - 17: 'control', - 'space': 32, - 32: 'space', - 't': 84, - 84: 't', - 'delete': 46, - 46: 'delete' - }, - ViewModes = ['times', 'days', 'months', 'years', 'decades'], - keyState = {}, - keyPressHandled = {}; - - var MinViewModeNumber = 0, - Default = { - timeZone: '', - format: false, - dayViewHeaderFormat: 'MMMM YYYY', - extraFormats: false, - stepping: 1, - minDate: false, - maxDate: false, - useCurrent: true, - collapse: true, - locale: moment.locale(), - defaultDate: false, - disabledDates: false, - enabledDates: false, - icons: { - time: 'fa fa-clock-o', - date: 'fa fa-calendar', - up: 'oi oi-arrow-up', - down: 'oi oi-arrow-down', - previous: 'oi oi-chevron-left', - next: 'oi oi-chevron-right', - today: 'fa fa-calendar-check-o', - clear: 'fa fa-trash', - close: 'fa fa-times' - }, - tooltips: { - today: 'Go to today', - clear: 'Clear selection', - close: 'Close the picker', - selectMonth: 'Select Month', - prevMonth: 'Previous Month', - nextMonth: 'Next Month', - selectYear: 'Select Year', - prevYear: 'Previous Year', - nextYear: 'Next Year', - selectDecade: 'Select Decade', - prevDecade: 'Previous Decade', - nextDecade: 'Next Decade', - prevCentury: 'Previous Century', - nextCentury: 'Next Century', - pickHour: 'Pick Hour', - incrementHour: 'Increment Hour', - decrementHour: 'Decrement Hour', - pickMinute: 'Pick Minute', - incrementMinute: 'Increment Minute', - decrementMinute: 'Decrement Minute', - pickSecond: 'Pick Second', - incrementSecond: 'Increment Second', - decrementSecond: 'Decrement Second', - togglePeriod: 'Toggle Period', - selectTime: 'Select Time', - selectDate: 'Select Date' - }, - useStrict: false, - sideBySide: false, - daysOfWeekDisabled: false, - calendarWeeks: false, - viewMode: 'days', - toolbarPlacement: 'default', - buttons: { - showToday: false, - showClear: false, - showClose: false - }, - widgetPositioning: { - horizontal: 'auto', - vertical: 'auto' - }, - widgetParent: null, - ignoreReadonly: false, - keepOpen: false, - focusOnShow: true, - inline: false, - keepInvalid: false, - keyBinds: { - up: function up() { - if (!this.widget) { - return false; - } - var d = this._dates[0] || this.getMoment(); - if (this.widget.find('.datepicker').is(':visible')) { - this.date(d.clone().subtract(7, 'd')); - } else { - this.date(d.clone().add(this.stepping(), 'm')); - } - return true; - }, - down: function down() { - if (!this.widget) { - this.show(); - return false; - } - var d = this._dates[0] || this.getMoment(); - if (this.widget.find('.datepicker').is(':visible')) { - this.date(d.clone().add(7, 'd')); - } else { - this.date(d.clone().subtract(this.stepping(), 'm')); - } - return true; - }, - 'control up': function controlUp() { - if (!this.widget) { - return false; - } - var d = this._dates[0] || this.getMoment(); - if (this.widget.find('.datepicker').is(':visible')) { - this.date(d.clone().subtract(1, 'y')); - } else { - this.date(d.clone().add(1, 'h')); - } - return true; - }, - 'control down': function controlDown() { - if (!this.widget) { - return false; - } - var d = this._dates[0] || this.getMoment(); - if (this.widget.find('.datepicker').is(':visible')) { - this.date(d.clone().add(1, 'y')); - } else { - this.date(d.clone().subtract(1, 'h')); - } - return true; - }, - left: function left() { - if (!this.widget) { - return false; - } - var d = this._dates[0] || this.getMoment(); - if (this.widget.find('.datepicker').is(':visible')) { - this.date(d.clone().subtract(1, 'd')); - } - return true; - }, - right: function right() { - if (!this.widget) { - return false; - } - var d = this._dates[0] || this.getMoment(); - if (this.widget.find('.datepicker').is(':visible')) { - this.date(d.clone().add(1, 'd')); - } - return true; - }, - pageUp: function pageUp() { - if (!this.widget) { - return false; - } - var d = this._dates[0] || this.getMoment(); - if (this.widget.find('.datepicker').is(':visible')) { - this.date(d.clone().subtract(1, 'M')); - } - return true; - }, - pageDown: function pageDown() { - if (!this.widget) { - return false; - } - var d = this._dates[0] || this.getMoment(); - if (this.widget.find('.datepicker').is(':visible')) { - this.date(d.clone().add(1, 'M')); - } - return true; - }, - enter: function enter() { - if (!this.widget) { - return false; - } - this.hide(); - return true; - }, - escape: function escape() { - if (!this.widget) { - return false; - } - this.hide(); - return true; - }, - 'control space': function controlSpace() { - if (!this.widget) { - return false; - } - if (this.widget.find('.timepicker').is(':visible')) { - this.widget.find('.btn[data-action="togglePeriod"]').click(); - } - return true; - }, - t: function t() { - if (!this.widget) { - return false; - } - this.date(this.getMoment()); - return true; - }, - 'delete': function _delete() { - if (!this.widget) { - return false; - } - this.clear(); - return true; - } - }, - debug: false, - allowInputToggle: false, - disabledTimeIntervals: false, - disabledHours: false, - enabledHours: false, - viewDate: false, - allowMultidate: false, - multidateSeparator: ',' - }; - - // ReSharper restore InconsistentNaming - - // ReSharper disable once DeclarationHides - // ReSharper disable once InconsistentNaming - - var DateTimePicker = function () { - /** @namespace eData.dateOptions */ - /** @namespace moment.tz */ - - function DateTimePicker(element, options) { - _classCallCheck(this, DateTimePicker); - - this._options = this._getOptions(options); - this._element = element; - this._dates = []; - this._datesFormatted = []; - this._viewDate = null; - this.unset = true; - this.component = false; - this.widget = false; - this.use24Hours = null; - this.actualFormat = null; - this.parseFormats = null; - this.currentViewMode = null; - - this._int(); - } - - /** - * @return {string} - */ - - - //private - - DateTimePicker.prototype._int = function _int() { - var targetInput = this._element.data('target-input'); - if (this._element.is('input')) { - this.input = this._element; - } else if (targetInput !== undefined) { - if (targetInput === 'nearest') { - this.input = this._element.find('input'); - } else { - this.input = $(targetInput); - } - } - - this._dates = []; - this._dates[0] = this.getMoment(); - this._viewDate = this.getMoment().clone(); - - $.extend(true, this._options, this._dataToOptions()); - - this.options(this._options); - - this._initFormatting(); - - if (this.input !== undefined && this.input.is('input') && this.input.val().trim().length !== 0) { - this._setValue(this._parseInputDate(this.input.val().trim()), 0); - } else if (this._options.defaultDate && this.input !== undefined && this.input.attr('placeholder') === undefined) { - this._setValue(this._options.defaultDate, 0); - } - if (this._options.inline) { - this.show(); - } - }; - - DateTimePicker.prototype._update = function _update() { - if (!this.widget) { - return; - } - this._fillDate(); - this._fillTime(); - }; - - DateTimePicker.prototype._setValue = function _setValue(targetMoment, index) { - var oldDate = this.unset ? null : this._dates[index]; - var outpValue = ''; - // case of calling setValue(null or false) - if (!targetMoment) { - if (!this._options.allowMultidate || this._dates.length === 1) { - this.unset = true; - this._dates = []; - this._datesFormatted = []; - } else { - outpValue = this._element.data('date') + ','; - outpValue = outpValue.replace(oldDate.format(this.actualFormat) + ',', '').replace(',,', '').replace(/,\s*$/, ''); - this._dates.splice(index, 1); - this._datesFormatted.splice(index, 1); - } - if (this.input !== undefined) { - this.input.val(outpValue); - this.input.trigger('input'); - } - this._element.data('date', outpValue); - this._notifyEvent({ - type: DateTimePicker.Event.CHANGE, - date: false, - oldDate: oldDate - }); - this._update(); - return; - } - - targetMoment = targetMoment.clone().locale(this._options.locale); - - if (this._hasTimeZone()) { - targetMoment.tz(this._options.timeZone); - } - - if (this._options.stepping !== 1) { - targetMoment.minutes(Math.round(targetMoment.minutes() / this._options.stepping) * this._options.stepping).seconds(0); - } - - if (this._isValid(targetMoment)) { - this._dates[index] = targetMoment; - this._datesFormatted[index] = targetMoment.format('YYYY-MM-DD'); - this._viewDate = targetMoment.clone(); - if (this._options.allowMultidate && this._dates.length > 1) { - for (var i = 0; i < this._dates.length; i++) { - outpValue += '' + this._dates[i].format(this.actualFormat) + this._options.multidateSeparator; - } - outpValue = outpValue.replace(/,\s*$/, ''); - } else { - outpValue = this._dates[index].format(this.actualFormat); - } - if (this.input !== undefined) { - this.input.val(outpValue); - this.input.trigger('input'); - } - this._element.data('date', outpValue); - - this.unset = false; - this._update(); - this._notifyEvent({ - type: DateTimePicker.Event.CHANGE, - date: this._dates[index].clone(), - oldDate: oldDate - }); - } else { - if (!this._options.keepInvalid) { - if (this.input !== undefined) { - this.input.val('' + (this.unset ? '' : this._dates[index].format(this.actualFormat))); - this.input.trigger('input'); - } - } else { - this._notifyEvent({ - type: DateTimePicker.Event.CHANGE, - date: targetMoment, - oldDate: oldDate - }); - } - this._notifyEvent({ - type: DateTimePicker.Event.ERROR, - date: targetMoment, - oldDate: oldDate - }); - } - }; - - DateTimePicker.prototype._change = function _change(e) { - var val = $(e.target).val().trim(), - parsedDate = val ? this._parseInputDate(val) : null; - this._setValue(parsedDate, 0); // Odoo FIX: if a valid date is replaced by an invalid one, lib will crash, see https://github.com/tempusdominus/bootstrap-4/issues/223 - e.stopImmediatePropagation(); - return false; - }; - - //noinspection JSMethodCanBeStatic - - - DateTimePicker.prototype._getOptions = function _getOptions(options) { - options = $.extend(true, {}, Default, options); - return options; - }; - - DateTimePicker.prototype._hasTimeZone = function _hasTimeZone() { - return moment.tz !== undefined && this._options.timeZone !== undefined && this._options.timeZone !== null && this._options.timeZone !== ''; - }; - - DateTimePicker.prototype._isEnabled = function _isEnabled(granularity) { - if (typeof granularity !== 'string' || granularity.length > 1) { - throw new TypeError('isEnabled expects a single character string parameter'); - } - switch (granularity) { - case 'y': - return this.actualFormat.indexOf('Y') !== -1; - case 'M': - return this.actualFormat.indexOf('M') !== -1; - case 'd': - return this.actualFormat.toLowerCase().indexOf('d') !== -1; - case 'h': - case 'H': - return this.actualFormat.toLowerCase().indexOf('h') !== -1; - case 'm': - return this.actualFormat.indexOf('m') !== -1; - case 's': - return this.actualFormat.indexOf('s') !== -1; - case 'a': - case 'A': - return this.actualFormat.toLowerCase().indexOf('a') !== -1; - default: - return false; - } - }; - - DateTimePicker.prototype._hasTime = function _hasTime() { - return this._isEnabled('h') || this._isEnabled('m') || this._isEnabled('s'); - }; - - DateTimePicker.prototype._hasDate = function _hasDate() { - return this._isEnabled('y') || this._isEnabled('M') || this._isEnabled('d'); - }; - - DateTimePicker.prototype._dataToOptions = function _dataToOptions() { - var eData = this._element.data(); - var dataOptions = {}; - - if (eData.dateOptions && eData.dateOptions instanceof Object) { - dataOptions = $.extend(true, dataOptions, eData.dateOptions); - } - - $.each(this._options, function (key) { - var attributeName = 'date' + key.charAt(0).toUpperCase() + key.slice(1); //todo data api key - if (eData[attributeName] !== undefined) { - dataOptions[key] = eData[attributeName]; - } else { - delete dataOptions[key]; - } - }); - return dataOptions; - }; - - DateTimePicker.prototype._notifyEvent = function _notifyEvent(e) { - // /!\ ODOO FIX: these next conditions have been modified by odoo - // FIXME should write a test about the tricky case this handles - if (e.type === DateTimePicker.Event.CHANGE) { - if (!e.date && !e.oldDate) { - return; - } - // check _isUTC flag to ensure that we are not comparing apples and oranges - var bothUTC = e.date && e.oldDate && e.date._isUTC === e.oldDate._isUTC; - if (bothUTC && e.date.isSame(e.oldDate)) { - return; - } - } - this._element.trigger(e); - }; - - DateTimePicker.prototype._viewUpdate = function _viewUpdate(e) { - if (e === 'y') { - e = 'YYYY'; - } - this._notifyEvent({ - type: DateTimePicker.Event.UPDATE, - change: e, - viewDate: this._viewDate.clone() - }); - }; - - DateTimePicker.prototype._showMode = function _showMode(dir) { - if (!this.widget) { - return; - } - if (dir) { - this.currentViewMode = Math.max(MinViewModeNumber, Math.min(3, this.currentViewMode + dir)); - } - this.widget.find('.datepicker > div').hide().filter('.datepicker-' + DatePickerModes[this.currentViewMode].CLASS_NAME).show(); - }; - - DateTimePicker.prototype._isInDisabledDates = function _isInDisabledDates(testDate) { - return this._options.disabledDates[testDate.format('YYYY-MM-DD')] === true; - }; - - DateTimePicker.prototype._isInEnabledDates = function _isInEnabledDates(testDate) { - return this._options.enabledDates[testDate.format('YYYY-MM-DD')] === true; - }; - - DateTimePicker.prototype._isInDisabledHours = function _isInDisabledHours(testDate) { - return this._options.disabledHours[testDate.format('H')] === true; - }; - - DateTimePicker.prototype._isInEnabledHours = function _isInEnabledHours(testDate) { - return this._options.enabledHours[testDate.format('H')] === true; - }; - - DateTimePicker.prototype._isValid = function _isValid(targetMoment, granularity) { - if (!targetMoment.isValid()) { - return false; - } - if (this._options.disabledDates && granularity === 'd' && this._isInDisabledDates(targetMoment)) { - return false; - } - if (this._options.enabledDates && granularity === 'd' && !this._isInEnabledDates(targetMoment)) { - return false; - } - if (this._options.minDate && targetMoment.isBefore(this._options.minDate, granularity)) { - return false; - } - if (this._options.maxDate && targetMoment.isAfter(this._options.maxDate, granularity)) { - return false; - } - if (this._options.daysOfWeekDisabled && granularity === 'd' && this._options.daysOfWeekDisabled.indexOf(targetMoment.day()) !== -1) { - return false; - } - if (this._options.disabledHours && (granularity === 'h' || granularity === 'm' || granularity === 's') && this._isInDisabledHours(targetMoment)) { - return false; - } - if (this._options.enabledHours && (granularity === 'h' || granularity === 'm' || granularity === 's') && !this._isInEnabledHours(targetMoment)) { - return false; - } - if (this._options.disabledTimeIntervals && (granularity === 'h' || granularity === 'm' || granularity === 's')) { - var found = false; - $.each(this._options.disabledTimeIntervals, function () { - if (targetMoment.isBetween(this[0], this[1])) { - found = true; - return false; - } - }); - if (found) { - return false; - } - } - return true; - }; - - DateTimePicker.prototype._parseInputDate = function _parseInputDate(inputDate) { - if (this._options.parseInputDate === undefined) { - if (!moment.isMoment(inputDate)) { - inputDate = this.getMoment(inputDate); - } - } else { - inputDate = this._options.parseInputDate(inputDate); - } - //inputDate.locale(this.options.locale); - return inputDate; - }; - - DateTimePicker.prototype._keydown = function _keydown(e) { - var handler = null, - index = void 0, - index2 = void 0, - keyBindKeys = void 0, - allModifiersPressed = void 0; - var pressedKeys = [], - pressedModifiers = {}, - currentKey = e.which, - pressed = 'p'; - - keyState[currentKey] = pressed; - - for (index in keyState) { - if (keyState.hasOwnProperty(index) && keyState[index] === pressed) { - pressedKeys.push(index); - if (parseInt(index, 10) !== currentKey) { - pressedModifiers[index] = true; - } - } - } - - for (index in this._options.keyBinds) { - if (this._options.keyBinds.hasOwnProperty(index) && typeof this._options.keyBinds[index] === 'function') { - keyBindKeys = index.split(' '); - if (keyBindKeys.length === pressedKeys.length && KeyMap[currentKey] === keyBindKeys[keyBindKeys.length - 1]) { - allModifiersPressed = true; - for (index2 = keyBindKeys.length - 2; index2 >= 0; index2--) { - if (!(KeyMap[keyBindKeys[index2]] in pressedModifiers)) { - allModifiersPressed = false; - break; - } - } - if (allModifiersPressed) { - handler = this._options.keyBinds[index]; - break; - } - } - } - } - - if (handler) { - if (handler.call(this)) { - e.stopPropagation(); - e.preventDefault(); - } - } - }; - - //noinspection JSMethodCanBeStatic,SpellCheckingInspection - - - DateTimePicker.prototype._keyup = function _keyup(e) { - keyState[e.which] = 'r'; - if (keyPressHandled[e.which]) { - keyPressHandled[e.which] = false; - e.stopPropagation(); - e.preventDefault(); - } - }; - - DateTimePicker.prototype._indexGivenDates = function _indexGivenDates(givenDatesArray) { - // Store given enabledDates and disabledDates as keys. - // This way we can check their existence in O(1) time instead of looping through whole array. - // (for example: options.enabledDates['2014-02-27'] === true) - var givenDatesIndexed = {}, - self = this; - $.each(givenDatesArray, function () { - var dDate = self._parseInputDate(this); - if (dDate.isValid()) { - givenDatesIndexed[dDate.format('YYYY-MM-DD')] = true; - } - }); - return Object.keys(givenDatesIndexed).length ? givenDatesIndexed : false; - }; - - DateTimePicker.prototype._indexGivenHours = function _indexGivenHours(givenHoursArray) { - // Store given enabledHours and disabledHours as keys. - // This way we can check their existence in O(1) time instead of looping through whole array. - // (for example: options.enabledHours['2014-02-27'] === true) - var givenHoursIndexed = {}; - $.each(givenHoursArray, function () { - givenHoursIndexed[this] = true; - }); - return Object.keys(givenHoursIndexed).length ? givenHoursIndexed : false; - }; - - DateTimePicker.prototype._initFormatting = function _initFormatting() { - var format = this._options.format || 'L LT', - self = this; - - this.actualFormat = format.replace(/(\[[^\[]*])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g, function (formatInput) { - return self._dates[0].localeData().longDateFormat(formatInput) || formatInput; //todo taking the first date should be ok - }); - - this.parseFormats = this._options.extraFormats ? this._options.extraFormats.slice() : []; - if (this.parseFormats.indexOf(format) < 0 && this.parseFormats.indexOf(this.actualFormat) < 0) { - this.parseFormats.push(this.actualFormat); - } - - this.use24Hours = this.actualFormat.toLowerCase().indexOf('a') < 1 && this.actualFormat.replace(/\[.*?]/g, '').indexOf('h') < 1; - - if (this._isEnabled('y')) { - MinViewModeNumber = 2; - } - if (this._isEnabled('M')) { - MinViewModeNumber = 1; - } - if (this._isEnabled('d')) { - MinViewModeNumber = 0; - } - - this.currentViewMode = Math.max(MinViewModeNumber, this.currentViewMode); - - if (!this.unset) { - this._setValue(this._dates[0], 0); - } - }; - - DateTimePicker.prototype._getLastPickedDate = function _getLastPickedDate() { - return this._dates[this._getLastPickedDateIndex()] || this.getMoment(); // FIXME changed by Odoo - }; - - DateTimePicker.prototype._getLastPickedDateIndex = function _getLastPickedDateIndex() { - return this._dates.length - 1; - }; - - //public - - - DateTimePicker.prototype.getMoment = function getMoment(d, f) { // ODOO FIX: give optional format - var returnMoment = void 0; - - // ODOO FIX: default to original `parseFormats` attribute - if (!f) { - f = this.parseFormats; - } - - if (d === undefined || d === null) { - returnMoment = moment(); //TODO should this use format? and locale? - } else if (this._hasTimeZone()) { - // There is a string to parse and a default time zone - // parse with the tz function which takes a default time zone if it is not in the format string - returnMoment = moment.tz(d, f, this._options.locale, this._options.useStrict, this._options.timeZone); // ODOO FIX: use format argument - } else { - returnMoment = moment(d, f, this._options.locale, this._options.useStrict); // ODOO FIX: use format argument - } - - if (this._hasTimeZone()) { - returnMoment.tz(this._options.timeZone); - } - - return returnMoment; - }; - - DateTimePicker.prototype.toggle = function toggle() { - return this.widget ? this.hide() : this.show(); - }; - - DateTimePicker.prototype.ignoreReadonly = function ignoreReadonly(_ignoreReadonly) { - if (arguments.length === 0) { - return this._options.ignoreReadonly; - } - if (typeof _ignoreReadonly !== 'boolean') { - throw new TypeError('ignoreReadonly () expects a boolean parameter'); - } - this._options.ignoreReadonly = _ignoreReadonly; - }; - - DateTimePicker.prototype.options = function options(newOptions) { - if (arguments.length === 0) { - return $.extend(true, {}, this._options); - } - - if (!(newOptions instanceof Object)) { - throw new TypeError('options() this.options parameter should be an object'); - } - $.extend(true, this._options, newOptions); - var self = this; - $.each(this._options, function (key, value) { - if (self[key] !== undefined) { - self[key](value); - } - }); - }; - - DateTimePicker.prototype.date = function date(newDate, index) { - index = index || 0; - if (arguments.length === 0) { - if (this.unset) { - return null; - } - if (this._options.allowMultidate) { - return this._dates.join(this._options.multidateSeparator); - } else { - return this._dates[index].clone(); - } - } - - if (newDate !== null && typeof newDate !== 'string' && !moment.isMoment(newDate) && !(newDate instanceof Date)) { - throw new TypeError('date() parameter must be one of [null, string, moment or Date]'); - } - - this._setValue(newDate === null ? null : this._parseInputDate(newDate), index); - }; - - DateTimePicker.prototype.format = function format(newFormat) { - if (arguments.length === 0) { - return this._options.format; - } - - if (typeof newFormat !== 'string' && (typeof newFormat !== 'boolean' || newFormat !== false)) { - throw new TypeError('format() expects a string or boolean:false parameter ' + newFormat); - } - - this._options.format = newFormat; - if (this.actualFormat) { - this._initFormatting(); // reinitialize formatting - } - }; - - DateTimePicker.prototype.timeZone = function timeZone(newZone) { - if (arguments.length === 0) { - return this._options.timeZone; - } - - if (typeof newZone !== 'string') { - throw new TypeError('newZone() expects a string parameter'); - } - - this._options.timeZone = newZone; - }; - - DateTimePicker.prototype.dayViewHeaderFormat = function dayViewHeaderFormat(newFormat) { - if (arguments.length === 0) { - return this._options.dayViewHeaderFormat; - } - - if (typeof newFormat !== 'string') { - throw new TypeError('dayViewHeaderFormat() expects a string parameter'); - } - - this._options.dayViewHeaderFormat = newFormat; - }; - - DateTimePicker.prototype.extraFormats = function extraFormats(formats) { - if (arguments.length === 0) { - return this._options.extraFormats; - } - - if (formats !== false && !(formats instanceof Array)) { - throw new TypeError('extraFormats() expects an array or false parameter'); - } - - this._options.extraFormats = formats; - if (this.parseFormats) { - this._initFormatting(); // reinit formatting - } - }; - - DateTimePicker.prototype.disabledDates = function disabledDates(dates) { - if (arguments.length === 0) { - return this._options.disabledDates ? $.extend({}, this._options.disabledDates) : this._options.disabledDates; - } - - if (!dates) { - this._options.disabledDates = false; - this._update(); - return true; - } - if (!(dates instanceof Array)) { - throw new TypeError('disabledDates() expects an array parameter'); - } - this._options.disabledDates = this._indexGivenDates(dates); - this._options.enabledDates = false; - this._update(); - }; - - DateTimePicker.prototype.enabledDates = function enabledDates(dates) { - if (arguments.length === 0) { - return this._options.enabledDates ? $.extend({}, this._options.enabledDates) : this._options.enabledDates; - } - - if (!dates) { - this._options.enabledDates = false; - this._update(); - return true; - } - if (!(dates instanceof Array)) { - throw new TypeError('enabledDates() expects an array parameter'); - } - this._options.enabledDates = this._indexGivenDates(dates); - this._options.disabledDates = false; - this._update(); - }; - - DateTimePicker.prototype.daysOfWeekDisabled = function daysOfWeekDisabled(_daysOfWeekDisabled) { - if (arguments.length === 0) { - return this._options.daysOfWeekDisabled.splice(0); - } - - if (typeof _daysOfWeekDisabled === 'boolean' && !_daysOfWeekDisabled) { - this._options.daysOfWeekDisabled = false; - this._update(); - return true; - } - - if (!(_daysOfWeekDisabled instanceof Array)) { - throw new TypeError('daysOfWeekDisabled() expects an array parameter'); - } - this._options.daysOfWeekDisabled = _daysOfWeekDisabled.reduce(function (previousValue, currentValue) { - currentValue = parseInt(currentValue, 10); - if (currentValue > 6 || currentValue < 0 || isNaN(currentValue)) { - return previousValue; - } - if (previousValue.indexOf(currentValue) === -1) { - previousValue.push(currentValue); - } - return previousValue; - }, []).sort(); - if (this._options.useCurrent && !this._options.keepInvalid) { - for (var i = 0; i < this._dates.length; i++) { - var tries = 0; - while (!this._isValid(this._dates[i], 'd')) { - this._dates[i].add(1, 'd'); - if (tries === 31) { - throw 'Tried 31 times to find a valid date'; - } - tries++; - } - this._setValue(this._dates[i], i); - } - } - this._update(); - }; - - DateTimePicker.prototype.maxDate = function maxDate(_maxDate) { - if (arguments.length === 0) { - return this._options.maxDate ? this._options.maxDate.clone() : this._options.maxDate; - } - - if (typeof _maxDate === 'boolean' && _maxDate === false) { - this._options.maxDate = false; - this._update(); - return true; - } - - if (typeof _maxDate === 'string') { - if (_maxDate === 'now' || _maxDate === 'moment') { - _maxDate = this.getMoment(); - } - } - - var parsedDate = this._parseInputDate(_maxDate); - - if (!parsedDate.isValid()) { - throw new TypeError('maxDate() Could not parse date parameter: ' + _maxDate); - } - if (this._options.minDate && parsedDate.isBefore(this._options.minDate)) { - throw new TypeError('maxDate() date parameter is before this.options.minDate: ' + parsedDate.format(this.actualFormat)); - } - this._options.maxDate = parsedDate; - for (var i = 0; i < this._dates.length; i++) { - if (this._options.useCurrent && !this._options.keepInvalid && this._dates[i].isAfter(_maxDate)) { - this._setValue(this._options.maxDate, i); - } - } - if (this._viewDate.isAfter(parsedDate)) { - this._viewDate = parsedDate.clone().subtract(this._options.stepping, 'm'); - } - this._update(); - }; - - DateTimePicker.prototype.minDate = function minDate(_minDate) { - if (arguments.length === 0) { - return this._options.minDate ? this._options.minDate.clone() : this._options.minDate; - } - - if (typeof _minDate === 'boolean' && _minDate === false) { - this._options.minDate = false; - this._update(); - return true; - } - - if (typeof _minDate === 'string') { - if (_minDate === 'now' || _minDate === 'moment') { - _minDate = this.getMoment(); - } - } - - var parsedDate = this._parseInputDate(_minDate); - - if (!parsedDate.isValid()) { - throw new TypeError('minDate() Could not parse date parameter: ' + _minDate); - } - if (this._options.maxDate && parsedDate.isAfter(this._options.maxDate)) { - throw new TypeError('minDate() date parameter is after this.options.maxDate: ' + parsedDate.format(this.actualFormat)); - } - this._options.minDate = parsedDate; - for (var i = 0; i < this._dates.length; i++) { - if (this._options.useCurrent && !this._options.keepInvalid && this._dates[i].isBefore(_minDate)) { - this._setValue(this._options.minDate, i); - } - } - if (this._viewDate.isBefore(parsedDate)) { - this._viewDate = parsedDate.clone().add(this._options.stepping, 'm'); - } - this._update(); - }; - - DateTimePicker.prototype.defaultDate = function defaultDate(_defaultDate) { - if (arguments.length === 0) { - return this._options.defaultDate ? this._options.defaultDate.clone() : this._options.defaultDate; - } - if (!_defaultDate) { - this._options.defaultDate = false; - return true; - } - - if (typeof _defaultDate === 'string') { - if (_defaultDate === 'now' || _defaultDate === 'moment') { - _defaultDate = this.getMoment(); - } else { - _defaultDate = this.getMoment(_defaultDate); - } - } - - var parsedDate = this._parseInputDate(_defaultDate); - if (!parsedDate.isValid()) { - throw new TypeError('defaultDate() Could not parse date parameter: ' + _defaultDate); - } - if (!this._isValid(parsedDate)) { - throw new TypeError('defaultDate() date passed is invalid according to component setup validations'); - } - - this._options.defaultDate = parsedDate; - - if (this._options.defaultDate && this._options.inline || this.input !== undefined && this.input.val().trim() === '') { - this._setValue(this._options.defaultDate, 0); - } - }; - - DateTimePicker.prototype.locale = function locale(_locale) { - if (arguments.length === 0) { - return this._options.locale; - } - - if (!moment.localeData(_locale)) { - throw new TypeError('locale() locale ' + _locale + ' is not loaded from moment locales!'); - } - - this._options.locale = _locale; - - for (var i = 0; i < this._dates.length; i++) { - this._dates[i].locale(this._options.locale); - } - this._viewDate.locale(this._options.locale); - - if (this.actualFormat) { - this._initFormatting(); // reinitialize formatting - } - if (this.widget) { - this.hide(); - this.show(); - } - }; - - DateTimePicker.prototype.stepping = function stepping(_stepping) { - if (arguments.length === 0) { - return this._options.stepping; - } - - _stepping = parseInt(_stepping, 10); - if (isNaN(_stepping) || _stepping < 1) { - _stepping = 1; - } - this._options.stepping = _stepping; - }; - - DateTimePicker.prototype.useCurrent = function useCurrent(_useCurrent) { - var useCurrentOptions = ['year', 'month', 'day', 'hour', 'minute']; - if (arguments.length === 0) { - return this._options.useCurrent; - } - - if (typeof _useCurrent !== 'boolean' && typeof _useCurrent !== 'string') { - throw new TypeError('useCurrent() expects a boolean or string parameter'); - } - if (typeof _useCurrent === 'string' && useCurrentOptions.indexOf(_useCurrent.toLowerCase()) === -1) { - throw new TypeError('useCurrent() expects a string parameter of ' + useCurrentOptions.join(', ')); - } - this._options.useCurrent = _useCurrent; - }; - - DateTimePicker.prototype.collapse = function collapse(_collapse) { - if (arguments.length === 0) { - return this._options.collapse; - } - - if (typeof _collapse !== 'boolean') { - throw new TypeError('collapse() expects a boolean parameter'); - } - if (this._options.collapse === _collapse) { - return true; - } - this._options.collapse = _collapse; - if (this.widget) { - this.hide(); - this.show(); - } - }; - - DateTimePicker.prototype.icons = function icons(_icons) { - if (arguments.length === 0) { - return $.extend({}, this._options.icons); - } - - if (!(_icons instanceof Object)) { - throw new TypeError('icons() expects parameter to be an Object'); - } - - $.extend(this._options.icons, _icons); - - if (this.widget) { - this.hide(); - this.show(); - } - }; - - DateTimePicker.prototype.tooltips = function tooltips(_tooltips) { - if (arguments.length === 0) { - return $.extend({}, this._options.tooltips); - } - - if (!(_tooltips instanceof Object)) { - throw new TypeError('tooltips() expects parameter to be an Object'); - } - $.extend(this._options.tooltips, _tooltips); - if (this.widget) { - this.hide(); - this.show(); - } - }; - - DateTimePicker.prototype.useStrict = function useStrict(_useStrict) { - if (arguments.length === 0) { - return this._options.useStrict; - } - - if (typeof _useStrict !== 'boolean') { - throw new TypeError('useStrict() expects a boolean parameter'); - } - this._options.useStrict = _useStrict; - }; - - DateTimePicker.prototype.sideBySide = function sideBySide(_sideBySide) { - if (arguments.length === 0) { - return this._options.sideBySide; - } - - if (typeof _sideBySide !== 'boolean') { - throw new TypeError('sideBySide() expects a boolean parameter'); - } - this._options.sideBySide = _sideBySide; - if (this.widget) { - this.hide(); - this.show(); - } - }; - - DateTimePicker.prototype.viewMode = function viewMode(_viewMode) { - if (arguments.length === 0) { - return this._options.viewMode; - } - - if (typeof _viewMode !== 'string') { - throw new TypeError('viewMode() expects a string parameter'); - } - - if (DateTimePicker.ViewModes.indexOf(_viewMode) === -1) { - throw new TypeError('viewMode() parameter must be one of (' + DateTimePicker.ViewModes.join(', ') + ') value'); - } - - this._options.viewMode = _viewMode; - this.currentViewMode = Math.max(DateTimePicker.ViewModes.indexOf(_viewMode) - 1, DateTimePicker.MinViewModeNumber); - - this._showMode(); - }; - - DateTimePicker.prototype.calendarWeeks = function calendarWeeks(_calendarWeeks) { - if (arguments.length === 0) { - return this._options.calendarWeeks; - } - - if (typeof _calendarWeeks !== 'boolean') { - throw new TypeError('calendarWeeks() expects parameter to be a boolean value'); - } - - this._options.calendarWeeks = _calendarWeeks; - this._update(); - }; - - DateTimePicker.prototype.buttons = function buttons(_buttons) { - if (arguments.length === 0) { - return $.extend({}, this._options.buttons); - } - - if (!(_buttons instanceof Object)) { - throw new TypeError('buttons() expects parameter to be an Object'); - } - - $.extend(this._options.buttons, _buttons); - - if (typeof this._options.buttons.showToday !== 'boolean') { - throw new TypeError('buttons.showToday expects a boolean parameter'); - } - if (typeof this._options.buttons.showClear !== 'boolean') { - throw new TypeError('buttons.showClear expects a boolean parameter'); - } - if (typeof this._options.buttons.showClose !== 'boolean') { - throw new TypeError('buttons.showClose expects a boolean parameter'); - } - - if (this.widget) { - this.hide(); - this.show(); - } - }; - - DateTimePicker.prototype.keepOpen = function keepOpen(_keepOpen) { - if (arguments.length === 0) { - return this._options.keepOpen; - } - - if (typeof _keepOpen !== 'boolean') { - throw new TypeError('keepOpen() expects a boolean parameter'); - } - - this._options.keepOpen = _keepOpen; - }; - - DateTimePicker.prototype.focusOnShow = function focusOnShow(_focusOnShow) { - if (arguments.length === 0) { - return this._options.focusOnShow; - } - - if (typeof _focusOnShow !== 'boolean') { - throw new TypeError('focusOnShow() expects a boolean parameter'); - } - - this._options.focusOnShow = _focusOnShow; - }; - - DateTimePicker.prototype.inline = function inline(_inline) { - if (arguments.length === 0) { - return this._options.inline; - } - - if (typeof _inline !== 'boolean') { - throw new TypeError('inline() expects a boolean parameter'); - } - - this._options.inline = _inline; - }; - - DateTimePicker.prototype.clear = function clear() { - this._setValue(null); //todo - }; - - DateTimePicker.prototype.keyBinds = function keyBinds(_keyBinds) { - if (arguments.length === 0) { - return this._options.keyBinds; - } - - this._options.keyBinds = _keyBinds; - }; - - DateTimePicker.prototype.debug = function debug(_debug) { - if (typeof _debug !== 'boolean') { - throw new TypeError('debug() expects a boolean parameter'); - } - - this._options.debug = _debug; - }; - - DateTimePicker.prototype.allowInputToggle = function allowInputToggle(_allowInputToggle) { - if (arguments.length === 0) { - return this._options.allowInputToggle; - } - - if (typeof _allowInputToggle !== 'boolean') { - throw new TypeError('allowInputToggle() expects a boolean parameter'); - } - - this._options.allowInputToggle = _allowInputToggle; - }; - - DateTimePicker.prototype.keepInvalid = function keepInvalid(_keepInvalid) { - if (arguments.length === 0) { - return this._options.keepInvalid; - } - - if (typeof _keepInvalid !== 'boolean') { - throw new TypeError('keepInvalid() expects a boolean parameter'); - } - this._options.keepInvalid = _keepInvalid; - }; - - DateTimePicker.prototype.datepickerInput = function datepickerInput(_datepickerInput) { - if (arguments.length === 0) { - return this._options.datepickerInput; - } - - if (typeof _datepickerInput !== 'string') { - throw new TypeError('datepickerInput() expects a string parameter'); - } - - this._options.datepickerInput = _datepickerInput; - }; - - DateTimePicker.prototype.parseInputDate = function parseInputDate(_parseInputDate2) { - if (arguments.length === 0) { - return this._options.parseInputDate; - } - - if (typeof _parseInputDate2 !== 'function') { - throw new TypeError('parseInputDate() should be as function'); - } - - this._options.parseInputDate = _parseInputDate2; - }; - - DateTimePicker.prototype.disabledTimeIntervals = function disabledTimeIntervals(_disabledTimeIntervals) { - if (arguments.length === 0) { - return this._options.disabledTimeIntervals ? $.extend({}, this._options.disabledTimeIntervals) : this._options.disabledTimeIntervals; - } - - if (!_disabledTimeIntervals) { - this._options.disabledTimeIntervals = false; - this._update(); - return true; - } - if (!(_disabledTimeIntervals instanceof Array)) { - throw new TypeError('disabledTimeIntervals() expects an array parameter'); - } - this._options.disabledTimeIntervals = _disabledTimeIntervals; - this._update(); - }; - - DateTimePicker.prototype.disabledHours = function disabledHours(hours) { - if (arguments.length === 0) { - return this._options.disabledHours ? $.extend({}, this._options.disabledHours) : this._options.disabledHours; - } - - if (!hours) { - this._options.disabledHours = false; - this._update(); - return true; - } - if (!(hours instanceof Array)) { - throw new TypeError('disabledHours() expects an array parameter'); - } - this._options.disabledHours = this._indexGivenHours(hours); - this._options.enabledHours = false; - if (this._options.useCurrent && !this._options.keepInvalid) { - for (var i = 0; i < this._dates.length; i++) { - var tries = 0; - while (!this._isValid(this._dates[i], 'h')) { - this._dates[i].add(1, 'h'); - if (tries === 24) { - throw 'Tried 24 times to find a valid date'; - } - tries++; - } - this._setValue(this._dates[i], i); - } - } - this._update(); - }; - - DateTimePicker.prototype.enabledHours = function enabledHours(hours) { - if (arguments.length === 0) { - return this._options.enabledHours ? $.extend({}, this._options.enabledHours) : this._options.enabledHours; - } - - if (!hours) { - this._options.enabledHours = false; - this._update(); - return true; - } - if (!(hours instanceof Array)) { - throw new TypeError('enabledHours() expects an array parameter'); - } - this._options.enabledHours = this._indexGivenHours(hours); - this._options.disabledHours = false; - if (this._options.useCurrent && !this._options.keepInvalid) { - for (var i = 0; i < this._dates.length; i++) { - var tries = 0; - while (!this._isValid(this._dates[i], 'h')) { - this._dates[i].add(1, 'h'); - if (tries === 24) { - throw 'Tried 24 times to find a valid date'; - } - tries++; - } - this._setValue(this._dates[i], i); - } - } - this._update(); - }; - - DateTimePicker.prototype.viewDate = function viewDate(newDate) { - if (arguments.length === 0) { - return this._viewDate.clone(); - } - - if (!newDate) { - this._viewDate = (this._dates[0] || this.getMoment()).clone(); - return true; - } - - if (typeof newDate !== 'string' && !moment.isMoment(newDate) && !(newDate instanceof Date)) { - throw new TypeError('viewDate() parameter must be one of [string, moment or Date]'); - } - - this._viewDate = this._parseInputDate(newDate); - this._viewUpdate(); - }; - - DateTimePicker.prototype.allowMultidate = function allowMultidate(_allowMultidate) { - if (typeof _allowMultidate !== 'boolean') { - throw new TypeError('allowMultidate() expects a boolean parameter'); - } - - this._options.allowMultidate = _allowMultidate; - }; - - DateTimePicker.prototype.multidateSeparator = function multidateSeparator(_multidateSeparator) { - if (arguments.length === 0) { - return this._options.multidateSeparator; - } - - if (typeof _multidateSeparator !== 'string' || _multidateSeparator.length > 1) { - throw new TypeError('multidateSeparator expects a single character string parameter'); - } - - this._options.multidateSeparator = _multidateSeparator; - }; - - _createClass(DateTimePicker, null, [{ - key: 'NAME', - get: function get() { - return NAME; - } - - /** - * @return {string} - */ - - }, { - key: 'DATA_KEY', - get: function get() { - return DATA_KEY; - } - - /** - * @return {string} - */ - - }, { - key: 'EVENT_KEY', - get: function get() { - return EVENT_KEY; - } - - /** - * @return {string} - */ - - }, { - key: 'DATA_API_KEY', - get: function get() { - return DATA_API_KEY; - } - }, { - key: 'DatePickerModes', - get: function get() { - return DatePickerModes; - } - }, { - key: 'ViewModes', - get: function get() { - return ViewModes; - } - - /** - * @return {number} - */ - - }, { - key: 'MinViewModeNumber', - get: function get() { - return MinViewModeNumber; - } - }, { - key: 'Event', - get: function get() { - return Event; - } - }, { - key: 'Selector', - get: function get() { - return Selector; - } - }, { - key: 'Default', - get: function get() { - return Default; - }, - set: function set(value) { - Default = value; - } - }, { - key: 'ClassName', - get: function get() { - return ClassName; - } - }]); - - return DateTimePicker; - }(); - - return DateTimePicker; -}(jQuery, moment); - -//noinspection JSUnusedGlobalSymbols -/* global DateTimePicker */ -var TempusDominusBootstrap4 = function ($) { - // eslint-disable-line no-unused-vars - // ReSharper disable once InconsistentNaming - var JQUERY_NO_CONFLICT = $.fn[DateTimePicker.NAME], - verticalModes = ['top', 'bottom', 'auto'], - horizontalModes = ['left', 'right', 'auto'], - toolbarPlacements = ['default', 'top', 'bottom'], - getSelectorFromElement = function getSelectorFromElement($element) { - var selector = $element.data('target'), - $selector = void 0; - - if (!selector) { - selector = $element.attr('href') || ''; - selector = /^#[a-z]/i.test(selector) ? selector : null; - } - $selector = $(selector); - if ($selector.length === 0) { - return $selector; - } - - if (!$selector.data(DateTimePicker.DATA_KEY)) { - $.extend({}, $selector.data(), $(this).data()); - } - - return $selector; - }; - - // ReSharper disable once InconsistentNaming - - var TempusDominusBootstrap4 = function (_DateTimePicker) { - _inherits(TempusDominusBootstrap4, _DateTimePicker); - - function TempusDominusBootstrap4(element, options) { - _classCallCheck(this, TempusDominusBootstrap4); - - var _this = _possibleConstructorReturn(this, _DateTimePicker.call(this, element, options)); - - _this._init(); - return _this; - } - - TempusDominusBootstrap4.prototype._init = function _init() { - if (this._element.hasClass('input-group')) { - var datepickerButton = this._element.find('.datepickerbutton'); - if (datepickerButton.length === 0) { - this.component = this._element.find('[data-toggle="datetimepicker"]'); - } else { - this.component = datepickerButton; - } - } - }; - - TempusDominusBootstrap4.prototype._getDatePickerTemplate = function _getDatePickerTemplate() { - var headTemplate = $('').append($('').append($('').addClass('prev').attr('data-action', 'previous').append($('').addClass(this._options.icons.previous))).append($('').addClass('picker-switch').attr('data-action', 'pickerSwitch').attr('colspan', '' + (this._options.calendarWeeks ? '6' : '5'))).append($('').addClass('next').attr('data-action', 'next').append($('').addClass(this._options.icons.next)))), - contTemplate = $('').append($('').append($('').attr('colspan', '' + (this._options.calendarWeeks ? '8' : '7')))); - - return [$('
').addClass('datepicker-days').append($('').addClass('table table-sm').append(headTemplate).append($(''))), $('
').addClass('datepicker-months').append($('
').addClass('table-condensed').append(headTemplate.clone()).append(contTemplate.clone())), $('
').addClass('datepicker-years').append($('
').addClass('table-condensed').append(headTemplate.clone()).append(contTemplate.clone())), $('
').addClass('datepicker-decades').append($('
').addClass('table-condensed').append(headTemplate.clone()).append(contTemplate.clone()))]; - }; - - TempusDominusBootstrap4.prototype._getTimePickerMainTemplate = function _getTimePickerMainTemplate() { - var topRow = $(''), - middleRow = $(''), - bottomRow = $(''); - - if (this._isEnabled('h')) { - topRow.append($('
').append($('').attr({ - href: '#', - tabindex: '-1', - 'title': this._options.tooltips.incrementHour - }).addClass('btn').attr('data-action', 'incrementHours').append($('').addClass(this._options.icons.up)))); - middleRow.append($('').append($('').addClass('timepicker-hour').attr({ - 'data-time-component': 'hours', - 'title': this._options.tooltips.pickHour - }).attr('data-action', 'showHours'))); - bottomRow.append($('').append($('').attr({ - href: '#', - tabindex: '-1', - 'title': this._options.tooltips.decrementHour - }).addClass('btn').attr('data-action', 'decrementHours').append($('').addClass(this._options.icons.down)))); - } - if (this._isEnabled('m')) { - if (this._isEnabled('h')) { - topRow.append($('').addClass('separator')); - middleRow.append($('').addClass('separator').html(':')); - bottomRow.append($('').addClass('separator')); - } - topRow.append($('').append($('').attr({ - href: '#', - tabindex: '-1', - 'title': this._options.tooltips.incrementMinute - }).addClass('btn').attr('data-action', 'incrementMinutes').append($('').addClass(this._options.icons.up)))); - middleRow.append($('').append($('').addClass('timepicker-minute').attr({ - 'data-time-component': 'minutes', - 'title': this._options.tooltips.pickMinute - }).attr('data-action', 'showMinutes'))); - bottomRow.append($('').append($('').attr({ - href: '#', - tabindex: '-1', - 'title': this._options.tooltips.decrementMinute - }).addClass('btn').attr('data-action', 'decrementMinutes').append($('').addClass(this._options.icons.down)))); - } - if (this._isEnabled('s')) { - if (this._isEnabled('m')) { - topRow.append($('').addClass('separator')); - middleRow.append($('').addClass('separator').html(':')); - bottomRow.append($('').addClass('separator')); - } - topRow.append($('').append($('').attr({ - href: '#', - tabindex: '-1', - 'title': this._options.tooltips.incrementSecond - }).addClass('btn').attr('data-action', 'incrementSeconds').append($('').addClass(this._options.icons.up)))); - middleRow.append($('').append($('').addClass('timepicker-second').attr({ - 'data-time-component': 'seconds', - 'title': this._options.tooltips.pickSecond - }).attr('data-action', 'showSeconds'))); - bottomRow.append($('').append($('').attr({ - href: '#', - tabindex: '-1', - 'title': this._options.tooltips.decrementSecond - }).addClass('btn').attr('data-action', 'decrementSeconds').append($('').addClass(this._options.icons.down)))); - } - - if (!this.use24Hours) { - topRow.append($('').addClass('separator')); - middleRow.append($('').append($('').addClass('separator')); - } - - return $('
').addClass('timepicker-picker').append($('').addClass('table-condensed').append([topRow, middleRow, bottomRow])); - }; - - TempusDominusBootstrap4.prototype._getTimePickerTemplate = function _getTimePickerTemplate() { - var hoursView = $('
').addClass('timepicker-hours').append($('
').addClass('table-condensed')), - minutesView = $('
').addClass('timepicker-minutes').append($('
').addClass('table-condensed')), - secondsView = $('
').addClass('timepicker-seconds').append($('
').addClass('table-condensed')), - ret = [this._getTimePickerMainTemplate()]; - - if (this._isEnabled('h')) { - ret.push(hoursView); - } - if (this._isEnabled('m')) { - ret.push(minutesView); - } - if (this._isEnabled('s')) { - ret.push(secondsView); - } - - return ret; - }; - - TempusDominusBootstrap4.prototype._getToolbar = function _getToolbar() { - var row = []; - if (this._options.buttons.showToday) { - row.push($('
').append($('').attr({ - href: '#', - tabindex: '-1', - 'data-action': 'today', - 'title': this._options.tooltips.today - }).append($('').addClass(this._options.icons.today)))); - } - if (!this._options.sideBySide && this._hasDate() && this._hasTime()) { - var title = void 0, - icon = void 0; - if (this._options.viewMode === 'times') { - title = this._options.tooltips.selectDate; - icon = this._options.icons.date; - } else { - title = this._options.tooltips.selectTime; - icon = this._options.icons.time; - } - row.push($('').append($('').attr({ - href: '#', - tabindex: '-1', - 'data-action': 'togglePicker', - 'title': title - }).append($('').addClass(icon)))); - } - if (this._options.buttons.showClear) { - row.push($('').append($('').attr({ - href: '#', - tabindex: '-1', - 'data-action': 'clear', - 'title': this._options.tooltips.clear - }).append($('').addClass(this._options.icons.clear)))); - } - if (this._options.buttons.showClose) { - row.push($('').append($('').attr({ - href: '#', - tabindex: '-1', - 'data-action': 'close', - 'title': this._options.tooltips.close - }).append($('').addClass(this._options.icons.close)))); - } - return row.length === 0 ? '' : $('').addClass('table-condensed').append($('').append($('').append(row))); - }; - - TempusDominusBootstrap4.prototype._getTemplate = function _getTemplate() { - var template = $('
').addClass('bootstrap-datetimepicker-widget dropdown-menu'), - dateView = $('
').addClass('datepicker').append(this._getDatePickerTemplate()), - timeView = $('
').addClass('timepicker').append(this._getTimePickerTemplate()), - content = $('
    ').addClass('list-unstyled'), - toolbar = $('
  • ').addClass('picker-switch' + (this._options.collapse ? ' accordion-toggle' : '')).append(this._getToolbar()); - - if (this._options.inline) { - template.removeClass('dropdown-menu'); - } - - if (this.use24Hours) { - template.addClass('usetwentyfour'); - } - if (this._isEnabled('s') && !this.use24Hours) { - template.addClass('wider'); - } - - if (this._options.sideBySide && this._hasDate() && this._hasTime()) { - template.addClass('timepicker-sbs'); - if (this._options.toolbarPlacement === 'top') { - template.append(toolbar); - } - template.append($('
    ').addClass('row').append(dateView.addClass('col-md-6')).append(timeView.addClass('col-md-6'))); - if (this._options.toolbarPlacement === 'bottom' || this._options.toolbarPlacement === 'default') { - template.append(toolbar); - } - return template; - } - - if (this._options.toolbarPlacement === 'top') { - content.append(toolbar); - } - if (this._hasDate()) { - content.append($('
  • ').addClass(this._options.collapse && this._hasTime() ? 'collapse' : '').addClass(this._options.collapse && this._hasTime() && this._options.viewMode === 'times' ? '' : 'show').append(dateView)); - } - if (this._options.toolbarPlacement === 'default') { - content.append(toolbar); - } - if (this._hasTime()) { - content.append($('
  • ').addClass(this._options.collapse && this._hasDate() ? 'collapse' : '').addClass(this._options.collapse && this._hasDate() && this._options.viewMode === 'times' ? 'show' : '').append(timeView)); - } - if (this._options.toolbarPlacement === 'bottom') { - content.append(toolbar); - } - return template.append(content); - }; - - TempusDominusBootstrap4.prototype._place = function _place(e) { - var self = e && e.data && e.data.picker || this, - vertical = self._options.widgetPositioning.vertical, - horizontal = self._options.widgetPositioning.horizontal, - parent = void 0; - var position = (self.component && self.component.length ? self.component : self._element).position(), - offset = (self.component && self.component.length ? self.component : self._element).offset(); - if (self._options.widgetParent) { - parent = self._options.widgetParent.append(self.widget); - } else if (self._element.is('input')) { - parent = self._element.after(self.widget).parent(); - } else if (self._options.inline) { - parent = self._element.append(self.widget); - return; - } else { - parent = self._element; - self._element.children().first().after(self.widget); - } - - // /!\ ODOO FIX: the 3 next lines have been *added* by odoo - var parentOffset = parent.offset(); - position.top = offset.top - parentOffset.top; - position.left = offset.left - parentOffset.left; - - // Top and bottom logic - if (vertical === 'auto') { - //noinspection JSValidateTypes - if (offset.top + self.widget.height() * 1.5 >= $(window).height() + $(window).scrollTop() && self.widget.height() + self._element.outerHeight() < offset.top) { - vertical = 'top'; - } else { - vertical = 'bottom'; - } - } - - // Left and right logic - if (horizontal === 'auto') { - if (parent.width() < offset.left + self.widget.outerWidth() / 2 && offset.left + self.widget.outerWidth() > $(window).width()) { - horizontal = 'right'; - } else { - horizontal = 'left'; - } - } - - if (vertical === 'top') { - self.widget.addClass('top').removeClass('bottom'); - } else { - self.widget.addClass('bottom').removeClass('top'); - } - - if (horizontal === 'right') { - self.widget.addClass('float-right'); - } else { - self.widget.removeClass('float-right'); - } - - // find the first parent element that has a relative css positioning - if (parent.css('position') !== 'relative') { - parent = parent.parents().filter(function () { - return $(this).css('position') === 'relative'; - }).first(); - } - - if (parent.length === 0) { - throw new Error('datetimepicker component should be placed within a relative positioned container'); - } - - self.widget.css({ - top: vertical === 'top' ? 'auto' : position.top + self._element.outerHeight() + 'px', - bottom: vertical === 'top' ? parent.outerHeight() - (parent === self._element ? 0 : position.top) + 'px' : 'auto', - left: horizontal === 'left' ? (parent === self._element ? 0 : position.left) + 'px' : 'auto', - right: horizontal === 'left' ? 'auto' : parent.outerWidth() - self._element.outerWidth() - (parent === self._element ? 0 : position.left) + 'px' - }); - }; - - TempusDominusBootstrap4.prototype._fillDow = function _fillDow() { - var row = $('
'), - currentDate = this._viewDate.clone().startOf('w').startOf('d'); - - if (this._options.calendarWeeks === true) { - row.append($(''); - if (this._options.calendarWeeks) { - row.append(''); - } - html.push(row); - } - clsName = ''; - if (currentDate.isBefore(this._viewDate, 'M')) { - clsName += ' old'; - } - if (currentDate.isAfter(this._viewDate, 'M')) { - clsName += ' new'; - } - if (this._options.allowMultidate) { - var index = this._datesFormatted.indexOf(currentDate.format('YYYY-MM-DD')); - if (index !== -1) { - if (currentDate.isSame(this._datesFormatted[index], 'd') && !this.unset) { - clsName += ' active'; - } - } - } else { - if (currentDate.isSame(this._getLastPickedDate(), 'd') && !this.unset) { - clsName += ' active'; - } - } - if (!this._isValid(currentDate, 'd')) { - clsName += ' disabled'; - } - // !! ODOO FIX START !! - if (currentDate.date() === now.date() && currentDate.month() === now.month() && currentDate.year() === now.year()) { - // !! ODOO FIX END !! - clsName += ' today'; - } - if (currentDate.day() === 0 || currentDate.day() === 6) { - clsName += ' weekend'; - } - row.append(''); - currentDate.add(1, 'd'); - } - - daysView.find('tbody').empty().append(html); - - this._updateMonths(); - - this._updateYears(); - - this._updateDecades(); - }; - - TempusDominusBootstrap4.prototype._fillHours = function _fillHours() { - var table = this.widget.find('.timepicker-hours table'), - currentHour = this._viewDate.clone().startOf('d'), - html = []; - var row = $(''); - - if (this._viewDate.hour() > 11 && !this.use24Hours) { - currentHour.hour(12); - } - while (currentHour.isSame(this._viewDate, 'd') && (this.use24Hours || this._viewDate.hour() < 12 && currentHour.hour() < 12 || this._viewDate.hour() > 11)) { - if (currentHour.hour() % 4 === 0) { - row = $(''); - html.push(row); - } - row.append(''); - currentHour.add(1, 'h'); - } - table.empty().append(html); - }; - - TempusDominusBootstrap4.prototype._fillMinutes = function _fillMinutes() { - var table = this.widget.find('.timepicker-minutes table'), - currentMinute = this._viewDate.clone().startOf('h'), - html = [], - step = this._options.stepping === 1 ? 5 : this._options.stepping; - var row = $(''); - - while (this._viewDate.isSame(currentMinute, 'h')) { - if (currentMinute.minute() % (step * 4) === 0) { - row = $(''); - html.push(row); - } - row.append(''); - currentMinute.add(step, 'm'); - } - table.empty().append(html); - }; - - TempusDominusBootstrap4.prototype._fillSeconds = function _fillSeconds() { - var table = this.widget.find('.timepicker-seconds table'), - currentSecond = this._viewDate.clone().startOf('m'), - html = []; - var row = $(''); - - while (this._viewDate.isSame(currentSecond, 'm')) { - if (currentSecond.second() % 20 === 0) { - row = $(''); - html.push(row); - } - row.append(''); - currentSecond.add(5, 's'); - } - - table.empty().append(html); - }; - - TempusDominusBootstrap4.prototype._fillTime = function _fillTime() { - var toggle = void 0, - newDate = void 0; - var timeComponents = this.widget.find('.timepicker span[data-time-component]'); - - if (!this.use24Hours) { - toggle = this.widget.find('.timepicker [data-action=togglePeriod]'); - newDate = this._getLastPickedDate().clone().add(this._getLastPickedDate().hours() >= 12 ? -12 : 12, 'h'); - - toggle.text(this._getLastPickedDate().format('A')); - - if (this._isValid(newDate, 'h')) { - toggle.removeClass('disabled'); - } else { - toggle.addClass('disabled'); - } - } - timeComponents.filter('[data-time-component=hours]').text(this._getLastPickedDate().format('' + (this.use24Hours ? 'HH' : 'hh'))); - timeComponents.filter('[data-time-component=minutes]').text(this._getLastPickedDate().format('mm')); - timeComponents.filter('[data-time-component=seconds]').text(this._getLastPickedDate().format('ss')); - - this._fillHours(); - this._fillMinutes(); - this._fillSeconds(); - }; - - TempusDominusBootstrap4.prototype._doAction = function _doAction(e, action) { - var lastPicked = this._getLastPickedDate(); - if ($(e.currentTarget).is('.disabled')) { - return false; - } - action = action || $(e.currentTarget).data('action'); - switch (action) { - case 'next': - { - var navFnc = DateTimePicker.DatePickerModes[this.currentViewMode].NAV_FUNCTION; - this._viewDate.add(DateTimePicker.DatePickerModes[this.currentViewMode].NAV_STEP, navFnc); - this._fillDate(); - this._viewUpdate(navFnc); - break; - } - case 'previous': - { - var _navFnc = DateTimePicker.DatePickerModes[this.currentViewMode].NAV_FUNCTION; - this._viewDate.subtract(DateTimePicker.DatePickerModes[this.currentViewMode].NAV_STEP, _navFnc); - this._fillDate(); - this._viewUpdate(_navFnc); - break; - } - case 'pickerSwitch': - this._showMode(1); - break; - case 'selectMonth': - { - var month = $(e.target).closest('tbody').find('span').index($(e.target)); - this._viewDate.month(month); - if (this.currentViewMode === DateTimePicker.MinViewModeNumber) { - this._setValue(lastPicked.clone().year(this._viewDate.year()).month(this._viewDate.month()), this._getLastPickedDateIndex()); - if (!this._options.inline) { - this.hide(); - } - } else { - this._showMode(-1); - this._fillDate(); - } - this._viewUpdate('M'); - break; - } - case 'selectYear': - { - var year = parseInt($(e.target).text(), 10) || 0; - this._viewDate.year(year); - if (this.currentViewMode === DateTimePicker.MinViewModeNumber) { - this._setValue(lastPicked.clone().year(this._viewDate.year()), this._getLastPickedDateIndex()); - if (!this._options.inline) { - this.hide(); - } - } else { - this._showMode(-1); - this._fillDate(); - } - this._viewUpdate('YYYY'); - break; - } - case 'selectDecade': - { - var _year = parseInt($(e.target).data('selection'), 10) || 0; - this._viewDate.year(_year); - if (this.currentViewMode === DateTimePicker.MinViewModeNumber) { - this._setValue(lastPicked.clone().year(this._viewDate.year()), this._getLastPickedDateIndex()); - if (!this._options.inline) { - this.hide(); - } - } else { - this._showMode(-1); - this._fillDate(); - } - this._viewUpdate('YYYY'); - break; - } - case 'selectDay': - { - var day = this._viewDate.clone(); - if ($(e.target).is('.old')) { - day.subtract(1, 'M'); - } - if ($(e.target).is('.new')) { - day.add(1, 'M'); - } - this._setValue(day.date(parseInt($(e.target).text(), 10)), this._getLastPickedDateIndex()); - if (!this._hasTime() && !this._options.keepOpen && !this._options.inline) { - this.hide(); - } - break; - } - case 'incrementHours': - { - var newDate = lastPicked.clone().add(1, 'h'); - if (this._isValid(newDate, 'h')) { - this._setValue(newDate, this._getLastPickedDateIndex()); - } - break; - } - case 'incrementMinutes': - { - var _newDate = lastPicked.clone().add(this._options.stepping, 'm'); - if (this._isValid(_newDate, 'm')) { - this._setValue(_newDate, this._getLastPickedDateIndex()); - } - break; - } - case 'incrementSeconds': - { - var _newDate2 = lastPicked.clone().add(1, 's'); - if (this._isValid(_newDate2, 's')) { - this._setValue(_newDate2, this._getLastPickedDateIndex()); - } - break; - } - case 'decrementHours': - { - var _newDate3 = lastPicked.clone().subtract(1, 'h'); - if (this._isValid(_newDate3, 'h')) { - this._setValue(_newDate3, this._getLastPickedDateIndex()); - } - break; - } - case 'decrementMinutes': - { - var _newDate4 = lastPicked.clone().subtract(this._options.stepping, 'm'); - if (this._isValid(_newDate4, 'm')) { - this._setValue(_newDate4, this._getLastPickedDateIndex()); - } - break; - } - case 'decrementSeconds': - { - var _newDate5 = lastPicked.clone().subtract(1, 's'); - if (this._isValid(_newDate5, 's')) { - this._setValue(_newDate5, this._getLastPickedDateIndex()); - } - break; - } - case 'togglePeriod': - { - this._setValue(lastPicked.clone().add(lastPicked.hours() >= 12 ? -12 : 12, 'h'), this._getLastPickedDateIndex()); - break; - } - case 'togglePicker': - { - var $this = $(e.target), - $link = $this.closest('a'), - $parent = $this.closest('ul'), - expanded = $parent.find('.show'), - closed = $parent.find('.collapse:not(.show)'), - $span = $this.is('span') ? $this : $this.find('span'); - var collapseData = void 0; - - if (expanded && expanded.length) { - collapseData = expanded.data('collapse'); - if (collapseData && collapseData.transitioning) { - return true; - } - if (expanded.collapse) { - // if collapse plugin is available through bootstrap.js then use it - expanded.collapse('hide'); - closed.collapse('show'); - } else { - // otherwise just toggle in class on the two views - expanded.removeClass('show'); - closed.addClass('show'); - } - $span.toggleClass(this._options.icons.time + ' ' + this._options.icons.date); - - if ($span.hasClass(this._options.icons.date)) { - $link.attr('title', this._options.tooltips.selectDate); - } else { - $link.attr('title', this._options.tooltips.selectTime); - } - } - } - break; - case 'showPicker': - this.widget.find('.timepicker > div:not(.timepicker-picker)').hide(); - this.widget.find('.timepicker .timepicker-picker').show(); - break; - case 'showHours': - this.widget.find('.timepicker .timepicker-picker').hide(); - this.widget.find('.timepicker .timepicker-hours').show(); - break; - case 'showMinutes': - this.widget.find('.timepicker .timepicker-picker').hide(); - this.widget.find('.timepicker .timepicker-minutes').show(); - break; - case 'showSeconds': - this.widget.find('.timepicker .timepicker-picker').hide(); - this.widget.find('.timepicker .timepicker-seconds').show(); - break; - case 'selectHour': - { - var hour = this.getMoment($(e.target).text(), this.use24Hours ? 'HH' : 'hh').hour(); // ODOO FIX: use moment format to get the proper value (not necessarily latn numbers) - - if (!this.use24Hours) { - if (lastPicked.hours() >= 12) { - if (hour !== 12) { - hour += 12; - } - } else { - if (hour === 12) { - hour = 0; - } - } - } - this._setValue(lastPicked.clone().hours(hour), this._getLastPickedDateIndex()); - if (!this._isEnabled('a') && !this._isEnabled('m') && !this._options.keepOpen && !this._options.inline) { - this.hide(); - } else { - this._doAction(e, 'showPicker'); - } - break; - } - case 'selectMinute': - this._setValue(lastPicked.clone().minutes(this.getMoment($(e.target).text(), 'mm').minute()), this._getLastPickedDateIndex()); // ODOO FIX: use moment format to get the proper value (not necessarily latn numbers) - if (!this._isEnabled('a') && !this._isEnabled('s') && !this._options.keepOpen && !this._options.inline) { - this.hide(); - } else { - this._doAction(e, 'showPicker'); - } - break; - case 'selectSecond': - this._setValue(lastPicked.clone().seconds(this.getMoment($(e.target).text(), 'ss').second()), this._getLastPickedDateIndex()); // ODOO FIX: use moment format to get the proper value (not necessarily latn numbers) - if (!this._isEnabled('a') && !this._options.keepOpen && !this._options.inline) { - this.hide(); - } else { - this._doAction(e, 'showPicker'); - } - break; - case 'clear': - this.clear(); - break; - case 'close': - this.hide(); - break; - case 'today': - { - var todaysDate = this.getMoment(); - if (this._isValid(todaysDate, 'd')) { - this._setValue(todaysDate, this._getLastPickedDateIndex()); - } - break; - } - } - return false; - }; - - //public - - - TempusDominusBootstrap4.prototype.hide = function hide() { - var transitioning = false; - if (!this.widget) { - return; - } - // Ignore event if in the middle of a picker transition - this.widget.find('.collapse').each(function () { - var collapseData = $(this).data('collapse'); - if (collapseData && collapseData.transitioning) { - transitioning = true; - return false; - } - return true; - }); - if (transitioning) { - return; - } - if (this.component && this.component.hasClass('btn')) { - this.component.toggleClass('active'); - } - this.widget.hide(); - - $(window).off('resize', this._place()); - this.widget.off('click', '[data-action]'); - this.widget.off('mousedown', false); - - this.widget.remove(); - this.widget = false; - - this._notifyEvent({ - type: DateTimePicker.Event.HIDE, - date: this._getLastPickedDate().clone() - }); - - if (this.input !== undefined) { - this.input.blur(); - } - - this._viewDate = this._getLastPickedDate().clone(); - }; - - TempusDominusBootstrap4.prototype.show = function show() { - var currentMoment = void 0; - var useCurrentGranularity = { - 'year': function year(m) { - return m.month(0).date(1).hours(0).seconds(0).minutes(0); - }, - 'month': function month(m) { - return m.date(1).hours(0).seconds(0).minutes(0); - }, - 'day': function day(m) { - return m.hours(0).seconds(0).minutes(0); - }, - 'hour': function hour(m) { - return m.seconds(0).minutes(0); - }, - 'minute': function minute(m) { - return m.seconds(0); - } - }; - - if (this.input !== undefined) { - if (this.input.prop('disabled') || !this._options.ignoreReadonly && this.input.prop('readonly') || this.widget) { - return; - } - if (this.input.val() !== undefined && this.input.val().trim().length !== 0) { - this._setValue(this._parseInputDate(this.input.val().trim()), 0); - } else if (this.unset && this._options.useCurrent) { - currentMoment = this.getMoment(); - if (typeof this._options.useCurrent === 'string') { - currentMoment = useCurrentGranularity[this._options.useCurrent](currentMoment); - } - this._setValue(currentMoment, 0); - } - } else if (this.unset && this._options.useCurrent) { - currentMoment = this.getMoment(); - if (typeof this._options.useCurrent === 'string') { - currentMoment = useCurrentGranularity[this._options.useCurrent](currentMoment); - } - this._setValue(currentMoment, 0); - } - - this.widget = this._getTemplate(); - - this._fillDow(); - this._fillMonths(); - - this.widget.find('.timepicker-hours').hide(); - this.widget.find('.timepicker-minutes').hide(); - this.widget.find('.timepicker-seconds').hide(); - - this._update(); - this._showMode(); - - $(window).on('resize', { picker: this }, this._place); - this.widget.on('click', '[data-action]', $.proxy(this._doAction, this)); // this handles clicks on the widget - this.widget.on('mousedown', false); - - if (this.component && this.component.hasClass('btn')) { - this.component.toggleClass('active'); - } - this._place(); - this.widget.show(); - if (this.input !== undefined && this._options.focusOnShow && !this.input.is(':focus')) { - this.input.focus(); - } - - this._notifyEvent({ - type: DateTimePicker.Event.SHOW - }); - }; - - TempusDominusBootstrap4.prototype.destroy = function destroy() { - this.hide(); - //todo doc off? - this._element.removeData(DateTimePicker.DATA_KEY); - this._element.removeData('date'); - }; - - TempusDominusBootstrap4.prototype.disable = function disable() { - this.hide(); - if (this.component && this.component.hasClass('btn')) { - this.component.addClass('disabled'); - } - if (this.input !== undefined) { - this.input.prop('disabled', true); //todo disable this/comp if input is null - } - }; - - TempusDominusBootstrap4.prototype.enable = function enable() { - if (this.component && this.component.hasClass('btn')) { - this.component.removeClass('disabled'); - } - if (this.input !== undefined) { - this.input.prop('disabled', false); //todo enable comp/this if input is null - } - }; - - TempusDominusBootstrap4.prototype.toolbarPlacement = function toolbarPlacement(_toolbarPlacement) { - if (arguments.length === 0) { - return this._options.toolbarPlacement; - } - - if (typeof _toolbarPlacement !== 'string') { - throw new TypeError('toolbarPlacement() expects a string parameter'); - } - if (toolbarPlacements.indexOf(_toolbarPlacement) === -1) { - throw new TypeError('toolbarPlacement() parameter must be one of (' + toolbarPlacements.join(', ') + ') value'); - } - this._options.toolbarPlacement = _toolbarPlacement; - - if (this.widget) { - this.hide(); - this.show(); - } - }; - - TempusDominusBootstrap4.prototype.widgetPositioning = function widgetPositioning(_widgetPositioning) { - if (arguments.length === 0) { - return $.extend({}, this._options.widgetPositioning); - } - - if ({}.toString.call(_widgetPositioning) !== '[object Object]') { - throw new TypeError('widgetPositioning() expects an object variable'); - } - if (_widgetPositioning.horizontal) { - if (typeof _widgetPositioning.horizontal !== 'string') { - throw new TypeError('widgetPositioning() horizontal variable must be a string'); - } - _widgetPositioning.horizontal = _widgetPositioning.horizontal.toLowerCase(); - if (horizontalModes.indexOf(_widgetPositioning.horizontal) === -1) { - throw new TypeError('widgetPositioning() expects horizontal parameter to be one of (' + horizontalModes.join(', ') + ')'); - } - this._options.widgetPositioning.horizontal = _widgetPositioning.horizontal; - } - if (_widgetPositioning.vertical) { - if (typeof _widgetPositioning.vertical !== 'string') { - throw new TypeError('widgetPositioning() vertical variable must be a string'); - } - _widgetPositioning.vertical = _widgetPositioning.vertical.toLowerCase(); - if (verticalModes.indexOf(_widgetPositioning.vertical) === -1) { - throw new TypeError('widgetPositioning() expects vertical parameter to be one of (' + verticalModes.join(', ') + ')'); - } - this._options.widgetPositioning.vertical = _widgetPositioning.vertical; - } - this._update(); - }; - - TempusDominusBootstrap4.prototype.widgetParent = function widgetParent(_widgetParent) { - if (arguments.length === 0) { - return this._options.widgetParent; - } - - if (typeof _widgetParent === 'string') { - _widgetParent = $(_widgetParent); - } - - if (_widgetParent !== null && typeof _widgetParent !== 'string' && !(_widgetParent instanceof $)) { - throw new TypeError('widgetParent() expects a string or a jQuery object parameter'); - } - - this._options.widgetParent = _widgetParent; - if (this.widget) { - this.hide(); - this.show(); - } - }; - - //static - - - TempusDominusBootstrap4._jQueryHandleThis = function _jQueryHandleThis(me, option, argument) { - var data = $(me).data(DateTimePicker.DATA_KEY); - if ((typeof option === 'undefined' ? 'undefined' : _typeof(option)) === 'object') { - $.extend({}, DateTimePicker.Default, option); - } - - if (!data) { - data = new TempusDominusBootstrap4($(me), option); - $(me).data(DateTimePicker.DATA_KEY, data); - } - - if (typeof option === 'string') { - if (data[option] === undefined) { - throw new Error('No method named "' + option + '"'); - } - if (argument === undefined) { - return data[option](); - } else { - return data[option](argument); - } - } - }; - - TempusDominusBootstrap4._jQueryInterface = function _jQueryInterface(option, argument) { - if (this.length === 1) { - return TempusDominusBootstrap4._jQueryHandleThis(this[0], option, argument); - } - return this.each(function () { - TempusDominusBootstrap4._jQueryHandleThis(this, option, argument); - }); - }; - - return TempusDominusBootstrap4; - }(DateTimePicker); - - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - - $(document).on(DateTimePicker.Event.CLICK_DATA_API, DateTimePicker.Selector.DATA_TOGGLE, function () { - var $target = getSelectorFromElement($(this)); - if ($target.length === 0) { - return; - } - TempusDominusBootstrap4._jQueryInterface.call($target, 'toggle'); - }).on(DateTimePicker.Event.CHANGE, '.' + DateTimePicker.ClassName.INPUT, function (event) { - var $target = getSelectorFromElement($(this)); - if ($target.length === 0) { - return; - } - TempusDominusBootstrap4._jQueryInterface.call($target, '_change', event); - }).on(DateTimePicker.Event.BLUR, '.' + DateTimePicker.ClassName.INPUT, function (event) { - var $target = getSelectorFromElement($(this)), - config = $target.data(DateTimePicker.DATA_KEY); - if ($target.length === 0) { - return; - } - // /!\ ODOO FIX: check on 'config' existence added by odoo - if (config && config._options.debug || window.debug) { - return; - } - TempusDominusBootstrap4._jQueryInterface.call($target, 'hide', event); - }).on(DateTimePicker.Event.KEYDOWN, '.' + DateTimePicker.ClassName.INPUT, function (event) { - var $target = getSelectorFromElement($(this)); - if ($target.length === 0) { - return; - } - TempusDominusBootstrap4._jQueryInterface.call($target, '_keydown', event); - }).on(DateTimePicker.Event.KEYUP, '.' + DateTimePicker.ClassName.INPUT, function (event) { - var $target = getSelectorFromElement($(this)); - if ($target.length === 0) { - return; - } - TempusDominusBootstrap4._jQueryInterface.call($target, '_keyup', event); - }).on(DateTimePicker.Event.FOCUS, '.' + DateTimePicker.ClassName.INPUT, function (event) { - var $target = getSelectorFromElement($(this)), - config = $target.data(DateTimePicker.DATA_KEY); - if ($target.length === 0) { - return; - } - // /!\ ODOO FIX: check on 'config' existence added by odoo - if (!(config && config._options.allowInputToggle)) { - return; - } - TempusDominusBootstrap4._jQueryInterface.call($target, 'show', event); - }); - - $.fn[DateTimePicker.NAME] = TempusDominusBootstrap4._jQueryInterface; - $.fn[DateTimePicker.NAME].Constructor = TempusDominusBootstrap4; - $.fn[DateTimePicker.NAME].noConflict = function () { - $.fn[DateTimePicker.NAME] = JQUERY_NO_CONFLICT; - return TempusDominusBootstrap4._jQueryInterface; - }; - - return TempusDominusBootstrap4; -}(jQuery); - -}(); diff --git a/addons/web/static/lib/tempusdominus/tempusdominus.scss b/addons/web/static/lib/tempusdominus/tempusdominus.scss deleted file mode 100644 index b5de3afbdf3..00000000000 --- a/addons/web/static/lib/tempusdominus/tempusdominus.scss +++ /dev/null @@ -1,342 +0,0 @@ -$bs-datetimepicker-timepicker-font-size: var(--bs-datetimepicker-timepicker-font-size, 1.2em) !default; -$bs-datetimepicker-active-bg: var(--bs-datetimepicker-active-bg, #{$blue}) !default; -$bs-datetimepicker-active-color: var(--bs-datetimepicker-active-color, #{$white}) !default; -$bs-datetimepicker-border-radius: var(--bs-datetimepicker-border-radius, #{$border-radius}) !default; -$bs-datetimepicker-btn-hover-bg: var(--bs-datetimepicker-btn-hover-bg, #{$gray-200}) !default; -$bs-datetimepicker-disabled-color: var(--bs-datetimepicker-disabled-color, #{$gray-600}) !default; -$bs-datetimepicker-alternate-color: var(--bs-datetimepicker-alternate-color, #{$gray-600}) !default; -$bs-datetimepicker-secondary-border-color: var(--bs-datetimepicker-secondary-border-color, #ccc) !default; -$bs-datetimepicker-secondary-border-color-rgba: var(--bs-datetimepicker-secondary-border-color-rgba, rgba(0, 0, 0, 0.2)) !default; -$bs-datetimepicker-primary-border-color: var(--bs-datetimepicker-primary-border-color, white) !default; -$bs-datetimepicker-text-shadow: var(--bs-datetimepicker-text-shadow, 0 -1px 0 rgba(0, 0, 0, 0.25)) !default; - -.bootstrap-datetimepicker-widget { - list-style: none; - - &.dropdown-menu { - display: block; - margin: 2px 0; - padding: 4px; - width: 14rem; - - &.timepicker-sbs { - @media (min-width: map-get($grid-breakpoints, 'sm')) { - width: 38em; - } - - @media (min-width: map-get($grid-breakpoints, 'md')) { - width: 38em; - } - - @media (min-width: map-get($grid-breakpoints, 'lg')) { - width: 38em; - } - } - - &:before, &:after { - content: ''; - display: inline-block; - position: absolute; - } - - &.bottom { - &:before { - border-left: 7px solid transparent; - border-right: 7px solid transparent; - border-bottom: 7px solid $bs-datetimepicker-secondary-border-color; - border-bottom-color: $bs-datetimepicker-secondary-border-color-rgba; - top: -7px; - left: 7px; - } - - &:after { - border-left: 6px solid transparent; - border-right: 6px solid transparent; - border-bottom: 6px solid $bs-datetimepicker-primary-border-color; - top: -6px; - left: 8px; - } - } - - &.top { - &:before { - border-left: 7px solid transparent; - border-right: 7px solid transparent; - border-top: 7px solid $bs-datetimepicker-secondary-border-color; - border-top-color: $bs-datetimepicker-secondary-border-color-rgba; - bottom: -7px; - left: 6px; - } - - &:after { - border-left: 6px solid transparent; - border-right: 6px solid transparent; - border-top: 6px solid $bs-datetimepicker-primary-border-color; - bottom: -6px; - left: 7px; - } - } - - &.float-right { - &:before { - left: auto; - right: 6px; - } - - &:after { - left: auto; - right: 7px; - } - } - - &.wider { - width: 16rem; - } - } - - .list-unstyled { - margin: 0; - } - - a[data-action] { - padding: 6px 0; - } - - a[data-action]:active { - box-shadow: none; - } - - .timepicker-hour, .timepicker-minute, .timepicker-second { - width: 54px; - font-weight: bold; - font-size: $bs-datetimepicker-timepicker-font-size; - margin: 0; - } - - button[data-action] { - padding: 6px; - } - - .btn[data-action="incrementHours"]::after { - @extend .sr-only; - content: "Increment Hours"; - } - - .btn[data-action="incrementMinutes"]::after { - @extend .sr-only; - content: "Increment Minutes"; - } - - .btn[data-action="decrementHours"]::after { - @extend .sr-only; - content: "Decrement Hours"; - } - - .btn[data-action="decrementMinutes"]::after { - @extend .sr-only; - content: "Decrement Minutes"; - } - - .btn[data-action="showHours"]::after { - @extend .sr-only; - content: "Show Hours"; - } - - .btn[data-action="showMinutes"]::after { - @extend .sr-only; - content: "Show Minutes"; - } - - .btn[data-action="togglePeriod"]::after { - @extend .sr-only; - content: "Toggle AM/PM"; - } - - .btn[data-action="clear"]::after { - @extend .sr-only; - content: "Clear the picker"; - } - - .btn[data-action="today"]::after { - @extend .sr-only; - content: "Set the date to today"; - } - - .picker-switch { - text-align: center; - - &::after { - @extend .sr-only; - content: "Toggle Date and Time Screens"; - } - - td { - padding: 0; - margin: 0; - height: auto; - width: auto; - line-height: inherit; - - span { - line-height: 2.5; - height: 2.5em; - width: 100%; - } - } - } - - table { - width: 100%; - margin: 0; - - - & td, - & th { - text-align: center; - border-radius: $bs-datetimepicker-border-radius; - } - - & th { - height: 20px; - line-height: 20px; - width: 20px; - - &.picker-switch { - width: 145px; - } - - &.disabled, - &.disabled:hover { - background: none; - color: $bs-datetimepicker-disabled-color; - cursor: not-allowed; - } - - &.prev::after { - @extend .sr-only; - content: "Previous Month"; - } - - &.next::after { - @extend .sr-only; - content: "Next Month"; - } - } - - & thead tr:first-child th { - cursor: pointer; - - &:hover { - background: $bs-datetimepicker-btn-hover-bg; - } - } - - & td { - height: 54px; - line-height: 54px; - width: 54px; - - &.cw { - font-size: .8em; - height: 20px; - line-height: 20px; - color: $bs-datetimepicker-alternate-color; - } - - &.day { - height: 20px; - line-height: 20px; - width: 20px; - } - - &.day:hover, - &.hour:hover, - &.minute:hover, - &.second:hover { - background: $bs-datetimepicker-btn-hover-bg; - cursor: pointer; - } - - &.old, - &.new { - color: $bs-datetimepicker-alternate-color; - } - - &.today { - position: relative; - - &:before { - content: ''; - display: inline-block; - border: solid transparent; - border-width: 0 0 7px 7px; - border-bottom-color: $bs-datetimepicker-active-bg; - border-top-color: $bs-datetimepicker-secondary-border-color-rgba; - position: absolute; - bottom: 4px; - right: 4px; - } - } - - &.active, - &.active:hover { - background-color: $bs-datetimepicker-active-bg; - color: $bs-datetimepicker-active-color; - text-shadow: $bs-datetimepicker-text-shadow; - } - - &.active.today:before { - border-bottom-color: #fff; - } - - &.disabled, - &.disabled:hover { - background: none; - color: $bs-datetimepicker-disabled-color; - cursor: not-allowed; - } - - span { - display: inline-block; - width: 54px; - height: 54px; - line-height: 54px; - margin: 2px 1.5px; - cursor: pointer; - border-radius: $bs-datetimepicker-border-radius; - - &:hover { - background: $bs-datetimepicker-btn-hover-bg; - } - - &.active { - background-color: $bs-datetimepicker-active-bg; - color: $bs-datetimepicker-active-color; - text-shadow: $bs-datetimepicker-text-shadow; - } - - &.old { - color: $bs-datetimepicker-alternate-color; - } - - &.disabled, - &.disabled:hover { - background: none; - color: $bs-datetimepicker-disabled-color; - cursor: not-allowed; - } - } - } - } - - &.usetwentyfour { - td.hour { - height: 27px; - line-height: 27px; - } - } -} - -.input-group [data-toggle="datetimepicker"] { - cursor: pointer; -} diff --git a/addons/web/static/src/core/datetime/datetime_hook.js b/addons/web/static/src/core/datetime/datetime_hook.js index 0eec201353c..50d58ad9e62 100644 --- a/addons/web/static/src/core/datetime/datetime_hook.js +++ b/addons/web/static/src/core/datetime/datetime_hook.js @@ -1,440 +1,30 @@ -/** @odoo-module **/ +/** @odoo-module */ -import { markRaw, onPatched, onWillRender, reactive, useEffect, useEnv, useRef } from "@odoo/owl"; -import { areDatesEqual } from "../l10n/dates"; -import { usePopover } from "../popover/popover_hook"; -import { registry } from "../registry"; -import { ensureArray, zip, zipWith } from "../utils/arrays"; -import { deepCopy, shallowEqual } from "../utils/objects"; -import { DateTimePicker } from "./datetime_picker"; -import { DateTimePickerPopover } from "./datetime_picker_popover"; +import { onPatched, onWillRender, useEffect, useRef } from "@odoo/owl"; +import { usePopover } from "@web/core/popover/popover_hook"; +import { useService } from "@web/core/utils/hooks"; -/** - * @typedef {luxon.DateTime} DateTime - * - * @typedef DateTimePickerHookParams - * @property {string} [format] - * @property {(value: DateTimePickerProps["value"]) => any} [onChange] callback - * invoked every time the hook updates the reactive value, either through the inputs - * or the picker. - * @property {(value: DateTimePickerProps["value"]) => any} [onApply] callback - * invoked once the value is committed: this is either when all inputs received - * a "change" event or when the datetime picker popover has been closed. - * @property {DateTimePickerProps} [pickerProps] - * @property {string | ReturnType} [target] - * - * @typedef {import("./datetime_picker").DateTimePickerProps} DateTimePickerProps - */ - -/** - * @template {HTMLElement} T - * @typedef {{ el: T | null }} OwlRef - */ - -/** @type {typeof shallowEqual} */ -const arePropsEqual = (obj1, obj2) => - shallowEqual(obj1, obj2, (a, b) => areDatesEqual(a, b) || shallowEqual(a, b)); - -const FOCUS_CLASSNAME = "text-primary"; - -/** - * @param {DateTimePickerHookParams} hookParams - */ -export const useDateTimePicker = (hookParams) => { - // Hook methods - - /** - * @param {HTMLElement} el - * @param {string} type - * @param {(ev: Event) => any} listener - */ - const addListener = (el, type, listener) => { - el.addEventListener(type, listener); - cleanups.push(() => el.removeEventListener(type, listener)); - }; - - /** - * Wrapper method on the "onApply" callback to only call it when the - * value has changed, and set other internal variables accordingly. - */ - const apply = () => { - const serializedValue = JSON.stringify(pickerProps.value); - if (areDatesEqual(lastInitialProps?.value, serializedValue)) { - return; - } - - lastInitialProps = null; // Next pickerProps are considered final - inputsChanged = ensureArray(pickerProps.value).map(() => false); - - onApply?.(pickerProps.value); - }; - - const computeBasePickerProps = () => { - const nextInitialProps = markValuesRaw(hookParams.pickerProps); - const serializedProps = deepCopy(nextInitialProps); - - if (lastInitialProps && arePropsEqual(lastInitialProps, serializedProps)) { - return; - } - - lastInitialProps = serializedProps; - inputsChanged = ensureArray(lastInitialProps.value).map(() => false); - - for (const [key, value] of Object.entries(nextInitialProps)) { - if (pickerProps[key] !== value && !areDatesEqual(pickerProps[key], value)) { - pickerProps[key] = value; - } - } - }; - - /** - * Ensures the current focused input (indicated by `pickerProps.focusedDateIndex`) - * is actually focused. - */ - const focusActiveInput = () => { - const inputEl = getInput(pickerProps.focusedDateIndex); - if (!inputEl) { - shouldFocusOnPatched = true; - return; - } - - const { activeElement } = inputEl.ownerDocument; - if (activeElement !== inputEl) { - inputEl.focus(); - } - - setInputFocus(inputEl); - }; - - /** - * @param {number} valueIndex - * @returns {HTMLInputElement | null} - */ - const getInput = (valueIndex) => { - const el = inputRefs[valueIndex]?.el; - if (el && document.body.contains(el)) { - return el; - } - return null; - }; - - /** - * Returns the appropriate root element to attach the popover: - * - if the value is a range: the closest common parent of the two inputs - * - if not: the first input - */ - const getPopoverTarget = () => { - if (targetRef?.el) { - return targetRef.el; - } - if (pickerProps.range) { - let parentElement = getInput(0).parentElement; - const inputEls = inputRefs.map((ref) => ref.el); - while (parentElement && !inputEls.every((inputEl) => parentElement.contains(inputEl))) { - parentElement = parentElement.parentElement; - } - return parentElement || getInput(0); - } else { - return getInput(0); - } - }; - - /** - * @template {object} T - * @param {T} obj - */ - const markValuesRaw = (obj) => { - /** @type {T} */ - const copy = {}; - for (const [key, value] of Object.entries(obj)) { - if (value && typeof value === "object") { - copy[key] = markRaw(value); - } else { - copy[key] = value; - } - } - return copy; - }; - - /** - * Inputs "change" event handler. This will trigger an "onApply" callback if - * one of the following is true: - * - there is only one input; - * - the popover is closed; - * - the other input has also changed. - * - * @param {Event} ev - */ - const onInputChange = (ev) => { - updateValueFromInputs(); - inputsChanged[ev.target === getInput(1) ? 1 : 0] = true; - if (!popover.isOpen || inputsChanged.every(Boolean)) { - saveAndClose(); - } - }; - - /** - * @param {PointerEvent} ev - */ - const onInputClick = ({ target }) => { - openPicker(target === getInput(1) ? 1 : 0); - }; - - /** - * @param {FocusEvent} ev - */ - const onInputFocus = ({ target }) => { - pickerProps.focusedDateIndex = target === getInput(1) ? 1 : 0; - setInputFocus(target); - }; - - /** - * @param {KeyboardEvent} ev - */ - const onInputKeydown = (ev) => { - switch (ev.key) { - case "Enter": - case "Escape": { - return saveAndClose(); - } - case "Tab": { - if (!getInput(0) || !getInput(1) || ev.target !== getInput(ev.shiftKey ? 1 : 0)) { - return saveAndClose(); - } - } - } - }; - - /** - * @param {number} inputIndex Input from which to open the picker - */ - const openPicker = (inputIndex) => { - pickerProps.focusedDateIndex = inputIndex; - - if (!popover.isOpen) { - const popoverTarget = getPopoverTarget(); - if (env.isSmall) { - popoverTarget.scrollIntoView(true); - } - popover.open(popoverTarget, { pickerProps }); - } - - focusActiveInput(); - }; - - /** - * @template {"format" | "parse"} T - * @param {T} operation - * @param {T extends "format" ? DateTime : string} value - * @returns {[T extends "format" ? string : DateTime, null] | [null, Error]} - */ - const safeConvert = (operation, value) => { - const { type } = pickerProps; - const convertFn = registry - .category(operation === "format" ? "formatters" : "parsers") - .get(type); - try { - return [convertFn(value, { format }), null]; - } catch (error) { - if (error?.name === "ConversionError") { - return [null, error]; - } else { - throw error; - } - } - }; - - /** - * Wrapper method to ensure the "onApply" callback is called, either: - * - by closing the popover (if any); - * - or by directly calling "apply", without updating the values. - */ - const saveAndClose = () => { - if (popover.isOpen) { - // apply will be done in the "onClose" callback - popover.close(); - } else { - apply(); - } - }; - - /** - * Updates class names on given inputs according to the currently selected input. - * - * @param {HTMLInputElement | null} input - */ - const setFocusClass = (input) => { - for (const { el } of inputRefs) { - if (el) { - el.classList.toggle(FOCUS_CLASSNAME, popover.isOpen && el === input); - } - } - }; - - /** - * Applies class names to all inputs according to whether they are focused or not. - * - * @param {HTMLInputElement} inputEl - */ - const setInputFocus = (inputEl) => { - inputEl.selectionStart = 0; - inputEl.selectionEnd = inputEl.value.length; - - setFocusClass(inputEl); - - shouldFocusOnPatched = false; - }; - - /** - * Synchronizes the given input with the given value. - * - * @param {HTMLInputElement} el - * @param {DateTime} value - */ - const updateInput = (el, value) => { - const [formattedValue] = safeConvert("format", value); - el.value = formattedValue || ""; - }; - - /** - * @param {DateTimePickerProps["value"]} value - */ - const updateValue = (value) => { - const previousValue = pickerProps.value; - pickerProps.value = value; - - if (areDatesEqual(previousValue, pickerProps.value)) { - return; - } - - if (pickerProps.range) { - // When in range: compare each individual value - const [prevStart, prevEnd] = ensureArray(previousValue); - const [nextStart, nextEnd] = ensureArray(pickerProps.value); - if ( - (pickerProps.focusedDateIndex === 0 && areDatesEqual(prevEnd, nextEnd)) || - (pickerProps.focusedDateIndex === 1 && areDatesEqual(prevStart, nextStart)) - ) { - pickerProps.focusedDateIndex = pickerProps.focusedDateIndex === 1 ? 0 : 1; - } - } - - onChange?.(pickerProps.value); - }; - - const updateValueFromInputs = () => { - const values = zipWith( - inputRefs, - ensureArray(pickerProps.value), - ({ el }, currentValue) => { - if (!el) { - return currentValue; - } - const [parsedValue, error] = safeConvert("parse", el.value); - if (error) { - updateInput(el, currentValue); - return currentValue; - } else { - return parsedValue; - } - } - ); - updateValue(values.length === 2 ? values : values[0]); - }; - - // Hook variables - - const { format, onApply, onChange, target } = hookParams; - const targetRef = typeof target === "string" ? useRef(target) : target; - /** @type {ReturnType>[]} */ +export function useDateTimePicker(hookParams) { + const datetimePicker = useService("datetime_picker"); + if (typeof hookParams.target === "string") { + const target = useRef(hookParams.target); + Object.defineProperty(hookParams, "target", { + get() { + return target.el; + }, + }); + } const inputRefs = [useRef("start-date"), useRef("end-date")]; - const env = useEnv(); - const popover = usePopover(DateTimePickerPopover, { - onClose: () => { - if (!allowOnClose) { - return; - } - updateValueFromInputs(); - apply(); - setFocusClass(null); - }, - }); - - /** @type {DateTimePickerProps} */ - const rawPickerProps = { - ...DateTimePicker.defaultProps, - onSelect: (value) => { - value &&= markRaw(value); - updateValue(value); - if (!pickerProps.range && pickerProps.type === "date") { - saveAndClose(); - } - }, - ...markValuesRaw(hookParams.pickerProps), - }; - const pickerProps = reactive(rawPickerProps, () => { - // Resets the popover position when switching from single date to a range - // or vice-versa - const currentIsRange = pickerProps.range; - if (popover.isOpen && lastIsRange !== currentIsRange) { - allowOnClose = false; - popover.open(getPopoverTarget(), { pickerProps }); - allowOnClose = true; - } - lastIsRange = currentIsRange; - - // Update inputs - for (const [{ el }, value] of zip(inputRefs, ensureArray(pickerProps.value), true)) { - if (el) { - updateInput(el, value); - } - } - - shouldFocusOnPatched = true; - }); - - /** Decides whether the popover 'onClose' callback can be called */ - let allowOnClose = true; - /** @type {boolean[]} */ - let inputsChanged = []; - /** @type {DateTimePickerProps | null} */ - let lastInitialProps = null; - let lastIsRange = pickerProps.range; - let shouldFocusOnPatched = false; - + const { computeBasePickerProps, state, open, focusIfNeeded, enable } = datetimePicker.create( + hookParams, + () => inputRefs.map((ref) => ref?.el), + usePopover + ); onWillRender(computeBasePickerProps); - - const cleanups = []; - useEffect(() => { - let editableInputs = 0; - for (const [{ el }, value] of zip(inputRefs, ensureArray(pickerProps.value), true)) { - if (el && !el.disabled && !el.readOnly) { - addListener(el, "change", onInputChange); - addListener(el, "click", onInputClick); - addListener(el, "focus", onInputFocus); - addListener(el, "keydown", onInputKeydown); - - updateInput(el, value); - editableInputs++; - } - } - if (!editableInputs) { - saveAndClose(); - } - return () => { - while (cleanups.length) { - cleanups.pop()(); - } - }; - }); + useEffect(enable); // Note: this `onPatched` callback must be called after the `useEffect` since // the effect may change input values that will be selected by the patch callback. - onPatched(() => { - if (popover.isOpen && shouldFocusOnPatched) { - focusActiveInput(); - } - }); - - return { state: pickerProps, open: openPicker }; -}; + onPatched(focusIfNeeded); + return { state, open }; +} diff --git a/addons/web/static/src/core/datetime/datetimepicker_service.js b/addons/web/static/src/core/datetime/datetimepicker_service.js new file mode 100644 index 00000000000..b75fd463bdd --- /dev/null +++ b/addons/web/static/src/core/datetime/datetimepicker_service.js @@ -0,0 +1,470 @@ +/** @odoo-module **/ + +import { markRaw, reactive } from "@odoo/owl"; +import { areDatesEqual, formatDate, formatDateTime, parseDate, parseDateTime } from "../l10n/dates"; +import { makePopover } from "../popover/popover_hook"; +import { registry } from "../registry"; +import { ensureArray, zip, zipWith } from "../utils/arrays"; +import { deepCopy, shallowEqual } from "../utils/objects"; +import { DateTimePicker } from "./datetime_picker"; +import { DateTimePickerPopover } from "./datetime_picker_popover"; + +/** + * @typedef {luxon.DateTime} DateTime + * + * @typedef DateTimePickerHookParams + * @property {string} [format] + * @property {(value: DateTimePickerProps["value"]) => any} [onChange] callback + * invoked every time the hook updates the reactive value, either through the inputs + * or the picker. + * @property {(value: DateTimePickerProps["value"]) => any} [onApply] callback + * invoked once the value is committed: this is either when all inputs received + * a "change" event or when the datetime picker popover has been closed. + * @property {DateTimePickerProps} [pickerProps] + * @property {string | ReturnType} [target] + * + * @typedef {import("./datetime_picker").DateTimePickerProps} DateTimePickerProps + */ + +/** + * @template {HTMLElement} T + * @typedef {{ el: T | null }} OwlRef + */ + +/** @type {typeof shallowEqual} */ +const arePropsEqual = (obj1, obj2) => + shallowEqual(obj1, obj2, (a, b) => areDatesEqual(a, b) || shallowEqual(a, b)); + +const FOCUS_CLASSNAME = "text-primary"; + +const formatters = { + date: formatDate, + datetime: formatDateTime, +}; + +const parsers = { + date: parseDate, + datetime: parseDateTime, +}; + +export const datetimePickerService = { + dependencies: ["popover"], + start(env, { popover: popoverService }) { + return { + /** + * @param {DateTimePickerHookParams} hookParams + */ + create: ( + hookParams, + getInputs = () => [hookParams.target, null], + createPopover = (...args) => makePopover(popoverService, ...args) + ) => { + const popover = createPopover(DateTimePickerPopover, { + onClose: () => { + if (!allowOnClose) { + return; + } + updateValueFromInputs(); + apply(); + setFocusClass(null); + }, + }); + // Hook methods + + /** + * Wrapper method on the "onApply" callback to only call it when the + * value has changed, and set other internal variables accordingly. + */ + const apply = () => { + const serializedValue = JSON.stringify(pickerProps.value); + if (areDatesEqual(lastInitialProps?.value, serializedValue)) { + return; + } + + lastInitialProps = null; // Next pickerProps are considered final + inputsChanged = ensureArray(pickerProps.value).map(() => false); + + hookParams.onApply?.(pickerProps.value); + }; + + const computeBasePickerProps = () => { + const nextInitialProps = markValuesRaw(hookParams.pickerProps); + const serializedProps = deepCopy(nextInitialProps); + + if (lastInitialProps && arePropsEqual(lastInitialProps, serializedProps)) { + return; + } + + lastInitialProps = serializedProps; + inputsChanged = ensureArray(lastInitialProps.value).map(() => false); + + for (const [key, value] of Object.entries(nextInitialProps)) { + if (pickerProps[key] !== value && !areDatesEqual(pickerProps[key], value)) { + pickerProps[key] = value; + } + } + }; + + /** + * Ensures the current focused input (indicated by `pickerProps.focusedDateIndex`) + * is actually focused. + */ + const focusActiveInput = () => { + const inputEl = getInput(pickerProps.focusedDateIndex); + if (!inputEl) { + shouldFocus = true; + return; + } + + const { activeElement } = inputEl.ownerDocument; + if (activeElement !== inputEl) { + inputEl.focus(); + } + + setInputFocus(inputEl); + }; + + /** + * @param {number} valueIndex + * @returns {HTMLInputElement | null} + */ + const getInput = (valueIndex) => { + const el = getInputs()[valueIndex]; + if (el && document.body.contains(el)) { + return el; + } + return null; + }; + + /** + * Returns the appropriate root element to attach the popover: + * - if the value is a range: the closest common parent of the two inputs + * - if not: the first input + */ + const getPopoverTarget = () => { + if (hookParams.target) { + return hookParams.target; + } + if (pickerProps.range) { + let parentElement = getInput(0).parentElement; + const inputEls = getInputs(); + while ( + parentElement && + !inputEls.every((inputEl) => parentElement.contains(inputEl)) + ) { + parentElement = parentElement.parentElement; + } + return parentElement || getInput(0); + } else { + return getInput(0); + } + }; + + /** + * @template {object} T + * @param {T} obj + */ + const markValuesRaw = (obj) => { + /** @type {T} */ + const copy = {}; + for (const [key, value] of Object.entries(obj)) { + if (value && typeof value === "object") { + copy[key] = markRaw(value); + } else { + copy[key] = value; + } + } + return copy; + }; + + /** + * Inputs "change" event handler. This will trigger an "onApply" callback if + * one of the following is true: + * - there is only one input; + * - the popover is closed; + * - the other input has also changed. + * + * @param {Event} ev + */ + const onInputChange = (ev) => { + updateValueFromInputs(); + inputsChanged[ev.target === getInput(1) ? 1 : 0] = true; + if (!popover.isOpen || inputsChanged.every(Boolean)) { + saveAndClose(); + } + }; + + /** + * @param {PointerEvent} ev + */ + const onInputClick = ({ target }) => { + openPicker(target === getInput(1) ? 1 : 0); + }; + + /** + * @param {FocusEvent} ev + */ + const onInputFocus = ({ target }) => { + pickerProps.focusedDateIndex = target === getInput(1) ? 1 : 0; + setInputFocus(target); + }; + + /** + * @param {KeyboardEvent} ev + */ + const onInputKeydown = (ev) => { + switch (ev.key) { + case "Enter": + case "Escape": { + return saveAndClose(); + } + case "Tab": { + if ( + !getInput(0) || + !getInput(1) || + ev.target !== getInput(ev.shiftKey ? 1 : 0) + ) { + return saveAndClose(); + } + } + } + }; + + /** + * @param {number} inputIndex Input from which to open the picker + */ + const openPicker = (inputIndex) => { + pickerProps.focusedDateIndex = inputIndex; + + if (!popover.isOpen) { + const popoverTarget = getPopoverTarget(); + if (env.isSmall) { + popoverTarget.scrollIntoView(true); + } + popover.open(popoverTarget, { pickerProps }); + } + + focusActiveInput(); + }; + + /** + * @template {"format" | "parse"} T + * @param {T} operation + * @param {T extends "format" ? DateTime : string} value + * @returns {[T extends "format" ? string : DateTime, null] | [null, Error]} + */ + const safeConvert = (operation, value) => { + const { type } = pickerProps; + const convertFn = (operation === "format" ? formatters : parsers)[type]; + try { + return [convertFn(value, { format: hookParams.format }), null]; + } catch (error) { + if (error?.name === "ConversionError") { + return [null, error]; + } else { + throw error; + } + } + }; + + /** + * Wrapper method to ensure the "onApply" callback is called, either: + * - by closing the popover (if any); + * - or by directly calling "apply", without updating the values. + */ + const saveAndClose = () => { + if (popover.isOpen) { + // apply will be done in the "onClose" callback + popover.close(); + } else { + apply(); + } + }; + + /** + * Updates class names on given inputs according to the currently selected input. + * + * @param {HTMLInputElement | null} input + */ + const setFocusClass = (input) => { + for (const el of getInputs()) { + if (el) { + el.classList.toggle(FOCUS_CLASSNAME, popover.isOpen && el === input); + } + } + }; + + /** + * Applies class names to all inputs according to whether they are focused or not. + * + * @param {HTMLInputElement} inputEl + */ + const setInputFocus = (inputEl) => { + inputEl.selectionStart = 0; + inputEl.selectionEnd = inputEl.value.length; + + setFocusClass(inputEl); + + shouldFocus = false; + }; + + /** + * Synchronizes the given input with the given value. + * + * @param {HTMLInputElement} el + * @param {DateTime} value + */ + const updateInput = (el, value) => { + const [formattedValue] = safeConvert("format", value); + el.value = formattedValue || ""; + }; + + /** + * @param {DateTimePickerProps["value"]} value + */ + const updateValue = (value) => { + const previousValue = pickerProps.value; + pickerProps.value = value; + + if (areDatesEqual(previousValue, pickerProps.value)) { + return; + } + + if (pickerProps.range) { + // When in range: compare each individual value + const [prevStart, prevEnd] = ensureArray(previousValue); + const [nextStart, nextEnd] = ensureArray(pickerProps.value); + if ( + (pickerProps.focusedDateIndex === 0 && + areDatesEqual(prevEnd, nextEnd)) || + (pickerProps.focusedDateIndex === 1 && + areDatesEqual(prevStart, nextStart)) + ) { + pickerProps.focusedDateIndex = + pickerProps.focusedDateIndex === 1 ? 0 : 1; + } + } + + hookParams.onChange?.(pickerProps.value); + }; + + const updateValueFromInputs = () => { + const values = zipWith( + getInputs(), + ensureArray(pickerProps.value), + (el, currentValue) => { + if (!el) { + return currentValue; + } + const [parsedValue, error] = safeConvert("parse", el.value); + if (error) { + updateInput(el, currentValue); + return currentValue; + } else { + return parsedValue; + } + } + ); + updateValue(values.length === 2 ? values : values[0]); + }; + + // Hook variables + + /** @type {DateTimePickerProps} */ + const rawPickerProps = { + ...DateTimePicker.defaultProps, + onSelect: (value) => { + value &&= markRaw(value); + updateValue(value); + if (!pickerProps.range && pickerProps.type === "date") { + saveAndClose(); + } + }, + ...markValuesRaw(hookParams.pickerProps), + }; + const pickerProps = reactive(rawPickerProps, () => { + // Resets the popover position when switching from single date to a range + // or vice-versa + const currentIsRange = pickerProps.range; + if (popover.isOpen && lastIsRange !== currentIsRange) { + allowOnClose = false; + popover.open(getPopoverTarget(), { pickerProps }); + allowOnClose = true; + } + lastIsRange = currentIsRange; + + // Update inputs + for (const [el, value] of zip( + getInputs(), + ensureArray(pickerProps.value), + true + )) { + if (el) { + updateInput(el, value); + } + } + + shouldFocus = true; + }); + + /** Decides whether the popover 'onClose' callback can be called */ + let allowOnClose = true; + /** @type {boolean[]} */ + let inputsChanged = []; + /** @type {DateTimePickerProps | null} */ + let lastInitialProps = null; + let lastIsRange = pickerProps.range; + let shouldFocus = false; + + /** + * @param {HTMLElement} el + * @param {string} type + * @param {(ev: Event) => any} listener + */ + const addListener = (el, type, listener) => { + el.addEventListener(type, listener); + return () => el.removeEventListener(type, listener); + }; + + return { + state: pickerProps, + open: openPicker, + computeBasePickerProps, + focusIfNeeded() { + if (popover.isOpen && shouldFocus) { + focusActiveInput(); + } + }, + enable() { + let editableInputs = 0; + const cleanups = []; + for (const [el, value] of zip( + getInputs(), + ensureArray(pickerProps.value), + true + )) { + if (el && !el.disabled && !el.readOnly) { + cleanups.push(addListener(el, "change", onInputChange)); + cleanups.push(addListener(el, "click", onInputClick)); + cleanups.push(addListener(el, "focus", onInputFocus)); + cleanups.push(addListener(el, "keydown", onInputKeydown)); + + updateInput(el, value); + editableInputs++; + } + } + if (!editableInputs) { + saveAndClose(); + } + return () => cleanups.forEach((cleanup) => cleanup()); + }, + get isOpen() { + return popover.isOpen; + }, + }; + }, + }; + }, +}; + +registry.category("services").add("datetime_picker", datetimePickerService); diff --git a/addons/web/static/src/core/network/rpc_service.js b/addons/web/static/src/core/network/rpc_service.js index 06411012fda..3b534d2e0d6 100644 --- a/addons/web/static/src/core/network/rpc_service.js +++ b/addons/web/static/src/core/network/rpc_service.js @@ -123,9 +123,9 @@ export const rpcService = { /** * @param {string} route * @param {Object} params - * @param {Object} settings + * @param {Object} [settings] * @param {boolean} settings.silent - * @param {XmlHttpRequest} settings.xhr + * @param {XMLHttpRequest} settings.xhr */ return function rpc(route, params = {}, settings) { return jsonrpc(env, rpcId++, route, params, settings); diff --git a/addons/web/static/src/core/popover/popover_hook.js b/addons/web/static/src/core/popover/popover_hook.js index 8e0585e14d3..bc51ba77696 100644 --- a/addons/web/static/src/core/popover/popover_hook.js +++ b/addons/web/static/src/core/popover/popover_hook.js @@ -15,6 +15,28 @@ import { onWillUnmount, status, useComponent } from "@odoo/owl"; * - Whether the popover is currently open. */ +export function makePopover(popoverService, component, options) { + let removeFn = null; + function close() { + removeFn?.(); + } + return { + open(target, props) { + close(); + const newOptions = Object.create(options); + newOptions.onClose = () => { + removeFn = null; + options.onClose?.(); + }; + removeFn = popoverService.add(target, component, props, newOptions); + }, + close, + get isOpen() { + return Boolean(removeFn); + }, + }; +} + /** * Manages a component to be used as a popover. * @@ -23,28 +45,15 @@ import { onWillUnmount, status, useComponent } from "@odoo/owl"; * @returns {PopoverHookReturnType} */ export function usePopover(component, options = {}) { - let removeFn = null; - const popover = useService("popover"); + const popoverService = useService("popover"); const owner = useComponent(); - function close() { - removeFn?.(); - } - onWillUnmount(close); - return { - open(target, props) { - close(); - const newOptions = Object.create(options); - newOptions.onClose = function () { - removeFn = null; - if (status(owner) !== "destroyed") { - options.onClose?.(); - } - }; - removeFn = popover.add(target, component, props, newOptions); - }, - close, - get isOpen() { - return Boolean(removeFn); - }, + const newOptions = Object.create(options); + newOptions.onClose = () => { + if (status(owner) !== "destroyed") { + options.onClose?.(); + } }; + const popover = makePopover(popoverService, component, newOptions); + onWillUnmount(popover.close); + return popover; } diff --git a/addons/web/static/src/legacy/js/public/public_root.js b/addons/web/static/src/legacy/js/public/public_root.js index 2b8971b1e1b..0b137cea7c8 100644 --- a/addons/web/static/src/legacy/js/public/public_root.js +++ b/addons/web/static/src/legacy/js/public/public_root.js @@ -101,8 +101,6 @@ export const PublicRoot = publicWidget.RootWidget.extend({ this.el.scrollTop = +window.location.hash.match(/scrollTop=([0-9]+)/)[1]; } - this.$el.children().on('error.datetimepicker', this._onDateTimePickerError.bind(this)); - return Promise.all(defs); }, diff --git a/addons/web/static/src/public/datetime_picker_widget.js b/addons/web/static/src/public/datetime_picker_widget.js new file mode 100644 index 00000000000..8e348aac7af --- /dev/null +++ b/addons/web/static/src/public/datetime_picker_widget.js @@ -0,0 +1,36 @@ +/** @odoo-module */ + +import { + deserializeDate, + deserializeDateTime, + parseDate, + parseDateTime, +} from "@web/core/l10n/dates"; +import PublicWidget from "@web/legacy/js/public/public_widget"; + +export const DateTimePickerWidget = PublicWidget.Widget.extend({ + selector: "[data-widget='datetime-picker']", + start() { + this._super(...arguments); + const { widgetType, minDate, maxDate } = this.el.dataset; + const type = widgetType || "datetime"; + const { value } = this.el; + const [parse, deserialize] = + type === "date" ? [parseDate, deserializeDate] : [parseDateTime, deserializeDateTime]; + this.disable = this.call("datetime_picker", "create", { + target: this.el, + pickerProps: { + type, + minDate: minDate && deserialize(minDate), + maxDate: maxDate && deserialize(maxDate), + value: parse(value), + }, + }).enable(); + }, + destroy() { + this.disable(); + return this._super(...arguments); + }, +}); + +PublicWidget.registry.DateTimePickerWidget = DateTimePickerWidget; diff --git a/addons/web/static/tests/core/datetime/datetime_hook_tests.js b/addons/web/static/tests/core/datetime/datetime_hook_tests.js index 60ff01fa51c..c2548d2a88c 100644 --- a/addons/web/static/tests/core/datetime/datetime_hook_tests.js +++ b/addons/web/static/tests/core/datetime/datetime_hook_tests.js @@ -4,6 +4,7 @@ import { Component, reactive, useState, xml } from "@odoo/owl"; import { clearRegistryWithCleanup, makeTestEnv } from "@web/../tests/helpers/mock_env"; import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services"; import { editInput, getFixture, mount, nextTick } from "@web/../tests/helpers/utils"; +import { datetimePickerService } from "@web/core/datetime/datetimepicker_service"; import { useDateTimePicker } from "@web/core/datetime/datetime_hook"; import { DateTimeInput } from "@web/core/datetime/datetime_input"; import { hotkeyService } from "@web/core/hotkeys/hotkey_service"; @@ -57,7 +58,8 @@ QUnit.module("Components", ({ beforeEach }) => { }) ) .add("popover", popoverService) - .add("ui", uiService); + .add("ui", uiService) + .add("datetime_picker", datetimePickerService); fixture = getFixture(); }); diff --git a/addons/web/static/tests/core/datetime/datetime_input_tests.js b/addons/web/static/tests/core/datetime/datetime_input_tests.js index 6a1bc58534b..4bf2f5c4ddb 100644 --- a/addons/web/static/tests/core/datetime/datetime_input_tests.js +++ b/addons/web/static/tests/core/datetime/datetime_input_tests.js @@ -24,6 +24,7 @@ import { getTimePickers, zoomOut, } from "./datetime_test_helpers"; +import { datetimePickerService } from "@web/core/datetime/datetimepicker_service"; const { DateTime } = luxon; @@ -74,7 +75,8 @@ QUnit.module("Components", ({ beforeEach }) => { }) ) .add("popover", popoverService) - .add("ui", uiService); + .add("ui", uiService) + .add("datetime_picker", datetimePickerService); fixture = getFixture(); }); diff --git a/addons/web/static/tests/core/domain_selector_tests.js b/addons/web/static/tests/core/domain_selector_tests.js index 5bf4adb7139..71e84cf02b3 100644 --- a/addons/web/static/tests/core/domain_selector_tests.js +++ b/addons/web/static/tests/core/domain_selector_tests.js @@ -31,6 +31,7 @@ import { openModelFieldSelectorPopover } from "./model_field_selector_tests"; import { nameService } from "@web/core/name_service"; import { dialogService } from "@web/core/dialog/dialog_service"; import { browser } from "@web/core/browser/browser"; +import { datetimePickerService } from "@web/core/datetime/datetimepicker_service"; let serverData; let target; @@ -159,6 +160,7 @@ QUnit.module("Components", (hooks) => { registry.category("services").add("field", fieldService); registry.category("services").add("name", nameService); registry.category("services").add("dialog", dialogService); + registry.category("services").add("datetime_picker", datetimePickerService); target = getFixture(); }); diff --git a/addons/web/static/tests/core/dropdown_tests.js b/addons/web/static/tests/core/dropdown_tests.js index f28dd7b7d25..eb072562844 100644 --- a/addons/web/static/tests/core/dropdown_tests.js +++ b/addons/web/static/tests/core/dropdown_tests.js @@ -25,6 +25,7 @@ import { } from "../helpers/utils"; import { makeParent } from "./tooltip/tooltip_service_tests"; import { getPickerCell } from "./datetime/datetime_test_helpers"; +import { datetimePickerService } from "@web/core/datetime/datetimepicker_service"; const serviceRegistry = registry.category("services"); @@ -1204,6 +1205,7 @@ QUnit.module("Components", ({ beforeEach }) => { QUnit.test( "Dropdown with a date picker inside do not close when a click occurs in date picker", async (assert) => { + registry.category("services").add("datetime_picker", datetimePickerService); class MyComponent extends owl.Component {} MyComponent.template = owl.xml` diff --git a/addons/web/static/tests/search/helpers.js b/addons/web/static/tests/search/helpers.js index 473dabec9ae..d43438bd1b0 100644 --- a/addons/web/static/tests/search/helpers.js +++ b/addons/web/static/tests/search/helpers.js @@ -26,6 +26,7 @@ import { viewService } from "@web/views/view_service"; import { actionService } from "@web/webclient/actions/action_service"; import { MainComponentsContainer } from "@web/core/main_components_container"; import { nameService } from "@web/core/name_service"; +import { datetimePickerService } from "@web/core/datetime/datetimepicker_service"; const serviceRegistry = registry.category("services"); const favoriteMenuRegistry = registry.category("favoriteMenu"); @@ -41,6 +42,7 @@ export function setupControlPanelServiceRegistry() { serviceRegistry.add("popover", popoverService); serviceRegistry.add("view", viewService); serviceRegistry.add("command", commandService); + serviceRegistry.add("datetime_picker", datetimePickerService); } export function setupControlPanelFavoriteMenuRegistry() { diff --git a/addons/web/static/tests/webclient/helpers.js b/addons/web/static/tests/webclient/helpers.js index c70c381513a..5ebcc47a6bf 100644 --- a/addons/web/static/tests/webclient/helpers.js +++ b/addons/web/static/tests/webclient/helpers.js @@ -31,6 +31,7 @@ import { overlayService } from "@web/core/overlay/overlay_service"; import { Component, xml } from "@odoo/owl"; import { fieldService } from "@web/core/field_service"; import { nameService } from "@web/core/name_service"; +import { datetimePickerService } from "@web/core/datetime/datetimepicker_service"; const actionRegistry = registry.category("actions"); const serviceRegistry = registry.category("services"); @@ -77,6 +78,7 @@ export function setupWebClientRegistries() { user: () => makeFakeUserService(), view: () => viewService, company: () => fakeCompanyService, + datetime_picker: () => datetimePickerService, }; for (const serviceName in services) { if (!serviceRegistry.contains(serviceName)) { diff --git a/addons/web_editor/__manifest__.py b/addons/web_editor/__manifest__.py index 02b970b5781..1d0857da2b9 100644 --- a/addons/web_editor/__manifest__.py +++ b/addons/web_editor/__manifest__.py @@ -194,11 +194,6 @@ Odoo Web Editor widget. ('include', 'web_editor.assets_media_dialog'), - # TODO: remove when refactoring 'DatetimePickerUserValueWidget' - 'web/static/src/legacy/scss/tempusdominus_overridden.scss', - 'web/static/lib/tempusdominus/tempusdominus.scss', - 'web/static/lib/tempusdominus/tempusdominus.js', - 'web_editor/static/src/scss/web_editor.common.scss', 'web_editor/static/src/scss/web_editor.backend.scss', diff --git a/addons/web_editor/static/src/js/editor/snippets.editor.js b/addons/web_editor/static/src/js/editor/snippets.editor.js index 59c25ccc5b3..3bfbda001a6 100644 --- a/addons/web_editor/static/src/js/editor/snippets.editor.js +++ b/addons/web_editor/static/src/js/editor/snippets.editor.js @@ -1758,6 +1758,7 @@ var SnippetsMenu = Widget.extend({ '.modal .btn-close', '.o_we_crop_widget', '.transfo-container', + '.o_datetime_picker', ].join(', '); this.loadingTimers = {}; diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js index 9f5d6e77e97..544b1669d46 100644 --- a/addons/web_editor/static/src/js/editor/snippets.options.js +++ b/addons/web_editor/static/src/js/editor/snippets.options.js @@ -5,7 +5,6 @@ import { MediaDialogWrapper } from "@web_editor/components/media_dialog/media_di import Dialog from "@web/legacy/js/core/dialog"; import dom from "@web/legacy/js/core/dom"; import rpc from "@web/legacy/js/core/rpc"; -import time from "@web/legacy/js/core/time"; import { throttleForAnimation, debounce } from "@web/core/utils/timing"; import utils from "@web/legacy/js/core/utils"; import Widget from "@web/legacy/js/core/widget"; @@ -34,7 +33,6 @@ import { } from "@web_editor/js/editor/image_processing"; import * as OdooEditorLib from "@web_editor/js/editor/odoo-editor/src/OdooEditor"; import {SIZES, MEDIAS_BREAKPOINTS} from "@web/core/ui/ui_service"; -import { uniqueId } from "@web/core/utils/functions"; import { pick } from "@web/core/utils/objects"; import { _t } from "@web/core/l10n/translation"; import { @@ -1837,11 +1835,9 @@ const VideopickerUserValueWidget = MediapickerUserValueWidget.extend({ const DatetimePickerUserValueWidget = InputUserValueWidget.extend({ events: { // Explicitely not consider all InputUserValueWidget events 'blur input': '_onInputBlur', - 'change.datetimepicker': '_onDateTimePickerChange', - 'error.datetimepicker': '_onDateTimePickerError', 'input input': '_onDateInputInput', }, - defaultFormat: time.getLangDatetimeFormat(), + pickerType: 'datetime', /** * @override @@ -1849,7 +1845,6 @@ const DatetimePickerUserValueWidget = InputUserValueWidget.extend({ init: function () { this._super(...arguments); this._value = DateTime.now().toUnixInteger().toString(); - this.__libInput = 0; }, /** * @override @@ -1857,51 +1852,21 @@ const DatetimePickerUserValueWidget = InputUserValueWidget.extend({ start: async function () { await this._super(...arguments); - const datetimePickerId = uniqueId("datetimepicker"); this.el.classList.add('o_we_large'); this.inputEl.classList.add('datetimepicker-input', 'mx-0', 'text-start'); - this.inputEl.setAttribute('id', datetimePickerId); - this.inputEl.setAttribute('data-target', '#' + datetimePickerId); - const datepickersOptions = { - minDate: moment({ y: 1000 }), - maxDate: moment().add(200, 'y'), - calendarWeeks: true, - defaultDate: moment().format(), - icons: { - close: 'fa fa-check primary', + this.picker = this.call("datetime_picker", "create", { + target: this.inputEl, + onChange: this._onDateTimePickerChange.bind(this), + pickerProps: { + type: this.pickerType, + minDate: DateTime.fromObject({ year: 1000 }), + maxDate: DateTime.now().plus({ year: 200 }), + value: DateTime.fromSeconds(parseInt(this._value)), + rounding: 0, }, - locale: moment.locale(), - format: this.defaultFormat, - sideBySide: true, - buttons: { - showClear: true, - showClose: true, - showToday: true, - }, - widgetParent: 'body', - - // Open the datetimepicker on focus not on click. This allows to - // take care of a bug which is due to the wysiwyg editor: - // sometimes, the datetimepicker loses the focus then get it back - // in the same execution flow. This was making the datepicker close - // for no apparent reason. Now, it only closes then reopens directly - // without it be possible to notice. - allowInputToggle: true, - }; - this.__libInput++; - const $input = $(this.inputEl); - $input.datetimepicker(datepickersOptions); - this.__libInput--; - - // Monkey-patch the library option to add custom classes on the pickers - const libObject = $input.data('datetimepicker'); - const oldFunc = libObject._getTemplate; - libObject._getTemplate = function () { - const $template = oldFunc.call(this, ...arguments); - $template.addClass('o_we_no_overlay o_we_datetimepicker'); - return $template; - }; + }); + this.picker.enable(); }, //-------------------------------------------------------------------------- @@ -1920,23 +1885,21 @@ const DatetimePickerUserValueWidget = InputUserValueWidget.extend({ * @override */ isPreviewed: function () { - return this._super(...arguments) || !!$(this.inputEl).data('datetimepicker').widget; + return this._super(...arguments) || this.picker.isOpen; }, /** * @override */ async setValue() { await this._super(...arguments); - let momentObj = null; + let dateTime = ""; if (this._value) { - momentObj = moment.unix(this._value); - if (!momentObj.isValid()) { - momentObj = moment(); + dateTime = DateTime.fromSeconds(parseInt(this._value)) + if (!dateTime.isValid) { + dateTime = DateTime.now(); } } - this.__libInput++; - $(this.inputEl).datetimepicker('date', momentObj); - this.__libInput--; + this.picker.state.value = dateTime; }, //-------------------------------------------------------------------------- @@ -1947,23 +1910,13 @@ const DatetimePickerUserValueWidget = InputUserValueWidget.extend({ * @private * @param {Event} ev */ - _onDateTimePickerChange: function (ev) { - if (this.__libInput > 0) { - return; - } - if (!ev.date || !ev.date.isValid()) { + _onDateTimePickerChange: function (newDateTime) { + if (!newDateTime || !newDateTime.isValid) { this._value = ''; } else { - this._value = ev.date.unix().toString(); + this._value = newDateTime.toUnixInteger().toString(); } - this._onUserValuePreview(ev); - }, - /** - * Prevents crash manager to throw CORS error. Note that library already - * clears the wrong date format. - */ - _onDateTimePickerError: function (ev) { - ev.stopPropagation(); + this._onUserValuePreview(); }, /** * Handles the clear button of the datepicker. @@ -1980,7 +1933,7 @@ const DatetimePickerUserValueWidget = InputUserValueWidget.extend({ }); const DatePickerUserValueWidget = DatetimePickerUserValueWidget.extend({ - defaultFormat: time.getLangDateFormat(), + pickerType: 'date', }); const ListUserValueWidget = UserValueWidget.extend({ diff --git a/addons/website/static/src/scss/website.edit_mode.scss b/addons/website/static/src/scss/website.edit_mode.scss index 533606409a0..ae614e90a5f 100644 --- a/addons/website/static/src/scss/website.edit_mode.scss +++ b/addons/website/static/src/scss/website.edit_mode.scss @@ -262,7 +262,7 @@ body.editor_enable { } } .s_website_form { - [data-toggle="datetimepicker"], textarea:not(.o_translatable_attribute):not(.o_translatable_text) { + textarea:not(.o_translatable_attribute):not(.o_translatable_text) { pointer-events: none; } } diff --git a/addons/website/static/src/snippets/s_website_form/000.js b/addons/website/static/src/snippets/s_website_form/000.js index 8435a056f86..0a685eeef26 100644 --- a/addons/website/static/src/snippets/s_website_form/000.js +++ b/addons/website/static/src/snippets/s_website_form/000.js @@ -1,6 +1,5 @@ /** @odoo-module **/ - import time from "@web/legacy/js/core/time"; import {ReCaptcha} from "@google_recaptcha/js/recaptcha"; import { session } from "@web/session"; import ajax from "@web/legacy/js/core/ajax"; @@ -10,8 +9,15 @@ import { debounce } from "@web/core/utils/timing"; import { _t } from "@web/core/l10n/translation"; import { renderToElement } from "@web/core/utils/render"; - import { formatDate, formatDateTime } from "@web/core/l10n/dates"; - const { DateTime } = luxon; +import { + formatDate, + formatDateTime, + parseDate, + parseDateTime, + serializeDate, + serializeDateTime, +} from "@web/core/l10n/dates"; +const { DateTime } = luxon; publicWidget.registry.EditModeWebsiteForm = publicWidget.Widget.extend({ selector: '.s_website_form form, form.s_website_form', // !compatibility @@ -97,32 +103,18 @@ this._onFieldInputDebounced = debounce(this._onFieldInput.bind(this), 400); this.$el.on('input.s_website_form', '.s_website_form_field', this._onFieldInputDebounced); - // Initialize datetimepickers - var datepickers_options = { - minDate: moment({y: 1000}), - maxDate: moment({y: 9999, M: 11, d: 31}), - calendarWeeks: true, - icons: { - time: 'fa fa-clock-o', - date: 'fa fa-calendar', - next: 'oi oi-chevron-right', - previous: 'oi oi-chevron-left', - up: 'oi oi-chevron-up', - down: 'oi oi-chevron-down', - }, - locale: moment.locale(), - format: time.getLangDatetimeFormat(), - extraFormats: ['X'], - }; - const $datetimes = this.$el.find('.s_website_form_datetime, .o_website_form_datetime'); // !compatibility - $datetimes.datetimepicker(datepickers_options); - - // Adapt options to date-only pickers - datepickers_options.format = time.getLangDateFormat(); - const $dates = this.$el.find('.s_website_form_date, .o_website_form_date'); // !compatibility - $dates.datetimepicker(datepickers_options); - - this.$allDates = $datetimes.add($dates); + this.$allDates = this.$el.find('.s_website_form_datetime, .o_website_form_datetime, .s_website_form_date, .o_website_form_date'); + for (const field of this.$allDates) { + const input = field.querySelector("input"); + const defaultValue = input.getAttribute("value"); + this.call("datetime_picker", "create", { + target: input, + pickerProps: { + type: field.matches('.s_website_form_date, .o_website_form_date') ? 'date' : 'datetime', + value: defaultValue && DateTime.fromSeconds(parseInt(defaultValue)), + }, + }).enable(); + } this.$allDates.addClass('s_website_form_datepicker_initialized'); // Display form values from tag having data-for attribute @@ -339,20 +331,14 @@ this.$el.find('.s_website_form_field:not(.s_website_form_custom)') .find('.s_website_form_date, .s_website_form_datetime').each(function () { const inputEl = this.querySelector('input'); - - // Datetimepicker('viewDate') will return `new Date()` if the - // input is empty but we want to keep the empty value - if (!inputEl.value) { + const { value } = inputEl; + if (!value) { return; } - var date = $(this).datetimepicker('viewDate').clone().locale('en'); - var format = 'YYYY-MM-DD'; - if ($(this).hasClass('s_website_form_datetime')) { - date = date.utc(); - format = 'YYYY-MM-DD HH:mm:ss'; - } - form_values[inputEl.getAttribute('name')] = date.format(format); + form_values[inputEl.getAttribute("name")] = this.matches(".s_website_form_date") + ? serializeDate(parseDate(value)) + : serializeDateTime(parseDateTime(value)); }); if (this._recaptchaLoaded) { @@ -524,11 +510,13 @@ // consider checking the date inputs are not disabled before // saying they are invalid (see checkValidity used here)) } else if ($(input).hasClass('s_website_form_date') || $(input).hasClass('o_website_form_date')) { // !compatibility - if (!self.is_datetime_valid(input.value, 'date')) { + const date = parseDate(input.value); + if (!date || !date.isValid) { return true; } } else if ($(input).hasClass('s_website_form_datetime') || $(input).hasClass('o_website_form_datetime')) { // !compatibility - if (!self.is_datetime_valid(input.value, 'datetime')) { + const date = parseDateTime(input.value); + if (!date || !date.isValid) { return true; } } else if (input.type === "file" && !self.isFileInputValid(input)) { @@ -567,42 +555,6 @@ return form_valid; }, - is_datetime_valid: function (value, type_of_date) { - if (value === "") { - return true; - } else { - try { - this.parse_date(value, type_of_date); - return true; - } catch { - return false; - } - } - }, - - // This is a stripped down version of format.js parse_value function - parse_date: function (value, type_of_date, value_if_empty) { - var date_pattern = time.getLangDateFormat(), - time_pattern = time.getLangTimeFormat(); - var date_pattern_wo_zero = date_pattern.replace('MM', 'M').replace('DD', 'D'), - time_pattern_wo_zero = time_pattern.replace('HH', 'H').replace('mm', 'm').replace('ss', 's'); - switch (type_of_date) { - case 'datetime': - var datetime = moment(value, [date_pattern + ' ' + time_pattern, date_pattern_wo_zero + ' ' + time_pattern_wo_zero], true); - if (datetime.isValid()) { - return time.datetime_to_str(datetime.toDate()); - } - throw new Error(_t("'%s' is not a correct datetime", value)); - case 'date': - var date = moment(value, [date_pattern, date_pattern_wo_zero], true); - if (date.isValid()) { - return time.date_to_str(date.toDate()); - } - throw new Error(_t("'%s' is not a correct date", value)); - } - return value; - }, - update_status: function (status, message) { if (status !== 'success') { // Restore send button behavior if result is an error this.$el.find('.s_website_form_send, .o_website_form_send') @@ -720,13 +672,7 @@ return value.name === ''; } // Date & Date Time comparison requires formatting the value - if (value.includes(':')) { - const datetimeFormat = time.getLangDatetimeFormat(); - value = moment(value, datetimeFormat)._d.getTime() / 1000; - } else { - const dateFormat = time.getLangDateFormat(); - value = moment(value, dateFormat)._d.getTime() / 1000; - } + value = (value.includes(':') ? parseDateTime(value) : parseDate(value)).toUnixInteger(); comparable = parseInt(comparable); between = parseInt(between) || ''; switch (comparator) { diff --git a/addons/website/static/src/xml/website_form_editor.xml b/addons/website/static/src/xml/website_form_editor.xml index 129a1aa7620..3cc82cafb5a 100644 --- a/addons/website/static/src/xml/website_form_editor.xml +++ b/addons/website/static/src/xml/website_form_editor.xml @@ -176,19 +176,17 @@ - -
+
-
+
@@ -196,19 +194,17 @@ - -
+
-
+
diff --git a/addons/website/static/tests/tours/website_form_editor_frontend.js b/addons/website/static/tests/tours/website_form_editor_frontend.js index 7af3742da3c..8dfdccc743b 100644 --- a/addons/website/static/tests/tours/website_form_editor_frontend.js +++ b/addons/website/static/tests/tours/website_form_editor_frontend.js @@ -86,9 +86,14 @@ registry.category("web_tour.tours").add("website_form_editor_tour_submit", { ":has(.s_website_form_field:has(label:contains('Invoice Scan')):not(.o_has_error))", trigger: "input[name=Products][value='Wiko Stairway']" }, + { + content: "Open datetime picker", + trigger: ".s_website_form_datetime input", + run: "click", + }, { content: "Complete Date field", - trigger: ".s_website_form_datetime [data-toggle='datetimepicker']", + trigger: ".o_date_picker .o_today", }, { content: "Check another product", diff --git a/addons/website_crm_partner_assign/static/src/js/crm_partner_assign.js b/addons/website_crm_partner_assign/static/src/js/crm_partner_assign.js index 0ffa0ab84cf..2353b81fca7 100644 --- a/addons/website_crm_partner_assign/static/src/js/crm_partner_assign.js +++ b/addons/website_crm_partner_assign/static/src/js/crm_partner_assign.js @@ -2,8 +2,7 @@ import { _t } from "@web/core/l10n/translation"; import publicWidget from "@web/legacy/js/public/public_widget"; -import time from "@web/legacy/js/core/time"; -import { parseDate, formatDate } from "@web/core/l10n/dates"; +import { parseDate, formatDate, serializeDate } from "@web/core/l10n/dates"; const { DateTime } = luxon; publicWidget.registry.crmPartnerAssign = publicWidget.Widget.extend({ @@ -18,7 +17,6 @@ publicWidget.registry.crmPartnerAssign = publicWidget.Widget.extend({ 'click .edit_opp_confirm': '_onEditOppConfirm', 'change .edit_opp_form .next_activity': '_onChangeNextActivity', 'change #new-opp-dialog .contact_name': '_onChangeContactName', - 'click div.input-group.date[data-target-input="nearest"]': '_onCalendarInputGroupClick', }, //-------------------------------------------------------------------------- @@ -259,32 +257,11 @@ publicWidget.registry.crmPartnerAssign = publicWidget.Widget.extend({ $('.edit_opp_form .activity_date_deadline').val( formatDate(date) ); } }, - /** - * @private - * @param {Event} ev - */ - _onCalendarInputGroupClick: function (ev) { - const $calendarInputGroup = $(ev.currentTarget); - const calendarOptions = { - format : time.getLangDateFormat(), - icons: { - time: 'fa fa-clock-o', - date: 'fa fa-calendar', - up: 'oi oi-chevron-up', - down: 'oi oi-chevron-down', - }, - }; - $calendarInputGroup.datetimepicker(calendarOptions); - }, - _parse_date: function (value) { - console.log(value); var date = parseDate(value); - if (date.isValid && date.year >= 1900) { - return formatDate(date); - } - else { + if (!date.isValid || date.year < 1900) { return false; } + return serializeDate(date); }, }); diff --git a/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml b/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml index ae42241bf3c..63947253c77 100644 --- a/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml +++ b/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml @@ -759,10 +759,10 @@
-
+
- - + +
@@ -784,10 +784,10 @@
-
+
- - + +
diff --git a/addons/website_slides/__manifest__.py b/addons/website_slides/__manifest__.py index 3398ea19578..bbf71b8a776 100644 --- a/addons/website_slides/__manifest__.py +++ b/addons/website_slides/__manifest__.py @@ -130,8 +130,6 @@ Featuring 'web/static/src/scss/pre_variables.scss', 'web/static/lib/bootstrap/scss/_variables.scss', - 'web/static/src/legacy/scss/tempusdominus_overridden.scss', - 'web/static/lib/tempusdominus/tempusdominus.scss', 'web/static/lib/jquery.ui/jquery-ui.css', 'web/static/src/libs/fontawesome/css/font-awesome.css', 'web/static/lib/odoo_ui_icons/*', @@ -184,7 +182,6 @@ Featuring 'web/static/lib/bootstrap/js/dist/scrollspy.js', 'web/static/lib/bootstrap/js/dist/tab.js', 'web/static/lib/bootstrap/js/dist/toast.js', - 'web/static/lib/tempusdominus/tempusdominus.js', 'web/static/lib/select2/select2.js', 'web/static/lib/clipboard/clipboard.js', 'web/static/lib/jSignature/jSignatureCustom.js', diff --git a/debian/copyright b/debian/copyright index 16252768e1d..d1aead1f199 100644 --- a/debian/copyright +++ b/debian/copyright @@ -318,10 +318,6 @@ Files: addons/web/static/lib/select2/* Copyright: 2014-2015 Igor Vaynberg License: Apache-2.0 -Files: addons/web/static/lib/tempusdominus/* -Copyright: 2016 Tempus Dominus; 2016 Jonathan Peterson -License: MIT - Files: addons/web/static/lib/underscore.string/* Copyright: Esa-Matti Suuronen, Alexandru Marasteanu License: MIT
').addClass('cw').text('#')); - } - - while (currentDate.isBefore(this._viewDate.clone().endOf('w'))) { - row.append($('').addClass('dow').text(currentDate.format('dd'))); - currentDate.add(1, 'd'); - } - this.widget.find('.datepicker-days thead').append(row); - }; - - TempusDominusBootstrap4.prototype._fillMonths = function _fillMonths() { - var spans = [], - monthsShort = this._viewDate.clone().startOf('y').startOf('d'); - while (monthsShort.isSame(this._viewDate, 'y')) { - spans.push($('').attr('data-action', 'selectMonth').addClass('month').text(monthsShort.format('MMM'))); - monthsShort.add(1, 'M'); - } - this.widget.find('.datepicker-months td').empty().append(spans); - }; - - TempusDominusBootstrap4.prototype._updateMonths = function _updateMonths() { - var monthsView = this.widget.find('.datepicker-months'), - monthsViewHeader = monthsView.find('th'), - months = monthsView.find('tbody').find('span'), - self = this; - - monthsViewHeader.eq(0).find('span').attr('title', this._options.tooltips.prevYear); - monthsViewHeader.eq(1).attr('title', this._options.tooltips.selectYear); - monthsViewHeader.eq(2).find('span').attr('title', this._options.tooltips.nextYear); - - monthsView.find('.disabled').removeClass('disabled'); - - if (!this._isValid(this._viewDate.clone().subtract(1, 'y'), 'y')) { - monthsViewHeader.eq(0).addClass('disabled'); - } - - monthsViewHeader.eq(1).text(this._viewDate.year()); - - if (!this._isValid(this._viewDate.clone().add(1, 'y'), 'y')) { - monthsViewHeader.eq(2).addClass('disabled'); - } - - months.removeClass('active'); - if (this._getLastPickedDate().isSame(this._viewDate, 'y') && !this.unset) { - months.eq(this._getLastPickedDate().month()).addClass('active'); - } - - months.each(function (index) { - if (!self._isValid(self._viewDate.clone().month(index), 'M')) { - $(this).addClass('disabled'); - } - }); - }; - - TempusDominusBootstrap4.prototype._getStartEndYear = function _getStartEndYear(factor, year) { - var step = factor / 10, - startYear = Math.floor(year / factor) * factor, - endYear = startYear + step * 9, - focusValue = Math.floor(year / step) * step; - return [startYear, endYear, focusValue]; - }; - - TempusDominusBootstrap4.prototype._updateYears = function _updateYears() { - var yearsView = this.widget.find('.datepicker-years'), - yearsViewHeader = yearsView.find('th'), - yearCaps = this._getStartEndYear(10, this._viewDate.year()), - startYear = this._viewDate.clone().year(yearCaps[0]), - endYear = this._viewDate.clone().year(yearCaps[1]); - var html = ''; - - yearsViewHeader.eq(0).find('span').attr('title', this._options.tooltips.prevDecade); - yearsViewHeader.eq(1).attr('title', this._options.tooltips.selectDecade); - yearsViewHeader.eq(2).find('span').attr('title', this._options.tooltips.nextDecade); - - yearsView.find('.disabled').removeClass('disabled'); - - if (this._options.minDate && this._options.minDate.isAfter(startYear, 'y')) { - yearsViewHeader.eq(0).addClass('disabled'); - } - - yearsViewHeader.eq(1).text(startYear.year() + '-' + endYear.year()); - - if (this._options.maxDate && this._options.maxDate.isBefore(endYear, 'y')) { - yearsViewHeader.eq(2).addClass('disabled'); - } - - html += '' + (startYear.year() - 1) + ''; - while (!startYear.isAfter(endYear, 'y')) { - html += '' + startYear.year() + ''; - startYear.add(1, 'y'); - } - html += '' + startYear.year() + ''; - - yearsView.find('td').html(html); - }; - - TempusDominusBootstrap4.prototype._updateDecades = function _updateDecades() { - var decadesView = this.widget.find('.datepicker-decades'), - decadesViewHeader = decadesView.find('th'), - yearCaps = this._getStartEndYear(100, this._viewDate.year()), - startDecade = this._viewDate.clone().year(yearCaps[0]), - endDecade = this._viewDate.clone().year(yearCaps[1]); - var minDateDecade = false, - maxDateDecade = false, - endDecadeYear = void 0, - html = ''; - - decadesViewHeader.eq(0).find('span').attr('title', this._options.tooltips.prevCentury); - decadesViewHeader.eq(2).find('span').attr('title', this._options.tooltips.nextCentury); - - decadesView.find('.disabled').removeClass('disabled'); - - if (startDecade.year() === 0 || this._options.minDate && this._options.minDate.isAfter(startDecade, 'y')) { - decadesViewHeader.eq(0).addClass('disabled'); - } - - decadesViewHeader.eq(1).text(startDecade.year() + '-' + endDecade.year()); - - if (this._options.maxDate && this._options.maxDate.isBefore(endDecade, 'y')) { - decadesViewHeader.eq(2).addClass('disabled'); - } - - if (startDecade.year() - 10 < 0) { - html += ' '; - } else { - html += '' + (startDecade.year() - 10) + ''; - } - - while (!startDecade.isAfter(endDecade, 'y')) { - endDecadeYear = startDecade.year() + 11; - minDateDecade = this._options.minDate && this._options.minDate.isAfter(startDecade, 'y') && this._options.minDate.year() <= endDecadeYear; - maxDateDecade = this._options.maxDate && this._options.maxDate.isAfter(startDecade, 'y') && this._options.maxDate.year() <= endDecadeYear; - html += '' + startDecade.year() + ''; - startDecade.add(10, 'y'); - } - html += '' + startDecade.year() + ''; - - decadesView.find('td').html(html); - }; - - TempusDominusBootstrap4.prototype._fillDate = function _fillDate() { - var daysView = this.widget.find('.datepicker-days'), - daysViewHeader = daysView.find('th'), - html = []; - var currentDate = void 0, - row = void 0, - clsName = void 0, - i = void 0; - - if (!this._hasDate()) { - return; - } - - daysViewHeader.eq(0).find('span').attr('title', this._options.tooltips.prevMonth); - daysViewHeader.eq(1).attr('title', this._options.tooltips.selectMonth); - daysViewHeader.eq(2).find('span').attr('title', this._options.tooltips.nextMonth); - - daysView.find('.disabled').removeClass('disabled'); - daysViewHeader.eq(1).text(this._viewDate.format(this._options.dayViewHeaderFormat)); - - if (!this._isValid(this._viewDate.clone().subtract(1, 'M'), 'M')) { - daysViewHeader.eq(0).addClass('disabled'); - } - if (!this._isValid(this._viewDate.clone().add(1, 'M'), 'M')) { - daysViewHeader.eq(2).addClass('disabled'); - } - - // !! ODOO FIX START !! - var now = this.getMoment(); - // currentDate = this._viewDate.clone().startOf('M').startOf('w').startOf('d'); - // avoid issue of safari + DST at midnight - currentDate = this._viewDate.clone().startOf('M').startOf('w').add(12, 'hours'); - // !! ODOO FIX END !! - - for (i = 0; i < 42; i++) { - //always display 42 days (should show 6 weeks) - if (currentDate.weekday() === 0) { - row = $('
' + currentDate.week() + '' + currentDate.date() + '
' + currentHour.format(this.use24Hours ? 'HH' : 'hh') + '
' + currentMinute.format('mm') + '
' + currentSecond.format('ss') + '