From 765e828c19bb78d4ff471113ca74fd5ae489b0a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Sun, 21 Feb 2021 19:38:25 +0100 Subject: [PATCH] Remove tempusdominus dependency Instead we use native input of type date, datetime-local and time to pop-up a calendar. Such input can not be directly used because we can not control the format, so we synchronize the value with a text input using the proper format. The text input is also parsed using the right format. issue9922 review339181002 --- CHANGELOG | 1 + Gruntfile.js | 3 +- bower.json | 1 - src/common.js | 20 ++-- src/sao.js | 51 ---------- src/sao.less | 30 +++++- src/screen.js | 163 +++++++++++++++++++------------ src/view/form.js | 248 +++++++++++++++++++++++++---------------------- src/view/tree.js | 5 - 9 files changed, 267 insertions(+), 255 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index cdb1206069..c4ebbfbb51 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Remove tempusdominus dependency * Handle device cookie * Add breadcrumb as title of window form * Manage help for each selection diff --git a/Gruntfile.js b/Gruntfile.js index 37667a6fba..33d2442e4d 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -8,6 +8,7 @@ module.exports = function(grunt) { 'src/rpc.js', 'src/pyson.js', 'src/session.js', + 'src/common.js', 'src/model.js', 'src/tab.js', 'src/screen.js', @@ -18,7 +19,6 @@ module.exports = function(grunt) { 'src/view/calendar.js', 'src/view/list_form.js', 'src/action.js', - 'src/common.js', 'src/window.js', 'src/wizard.js', 'src/board.js', @@ -32,7 +32,6 @@ module.exports = function(grunt) { 'bower_components/bootstrap', 'bower_components/bootstrap/less', 'bower_components/bootstrap-rtl-ondemand/less', - 'bower_components/tempusdominus-bootstrap-3/src/less/' ]; // Project configuration. diff --git a/bower.json b/bower.json index 13e2212414..85ef7d286e 100644 --- a/bower.json +++ b/bower.json @@ -25,7 +25,6 @@ "mousetrap": "^1.6", "bootstrap-rtl-ondemand": "^3.3.4-ondemand", "Sortable": "sortablejs#^1.8.4", - "tempusdominus-bootstrap-3": "bootstrap-datetimepicker-v5-alpha#^5.0.0-alpha8", "popper.js": "https://unpkg.com/popper.js" }, "devDependencies": { diff --git a/src/common.js b/src/common.js index b05e128909..cd7d8e7cfc 100644 --- a/src/common.js +++ b/src/common.js @@ -280,17 +280,15 @@ return date; }; - Sao.common.format_datetime = function(date_format, time_format, date) { + Sao.common.format_datetime = function(datetime_format, date) { if (!date) { return ''; } - return date.format( - Sao.common.moment_format(date_format + ' ' + time_format)); + return date.format(Sao.common.moment_format(datetime_format)); }; - Sao.common.parse_datetime = function(date_format, time_format, value) { - var date = moment(value, - Sao.common.moment_format(date_format + ' ' + time_format)); + Sao.common.parse_datetime = function(datetime_format, value) { + var date = moment(value, Sao.common.moment_format(datetime_format)); if (date.isValid()) { date = Sao.DateTime(date.year(), date.month(), date.date(), date.hour(), date.minute(), date.second(), @@ -1969,9 +1967,8 @@ 'reference': convert_selection, 'datetime': function() { var result = Sao.common.parse_datetime( - Sao.common.date_format(context.date_format), - this.time_format(field), - value); + Sao.common.date_format(context.date_format) + ' ' + + this.time_format(field), value); return result; }.bind(this), 'date': function() { @@ -2093,9 +2090,8 @@ value); } return Sao.common.format_datetime( - Sao.common.date_format(context.date_format), - this.time_format(field), - value); + Sao.common.date_format(context.date_format) + ' ' + + this.time_format(field), value); }.bind(this), 'date': function() { return Sao.common.format_date( diff --git a/src/sao.js b/src/sao.js index e07d4a641d..26b7bcd8c0 100644 --- a/src/sao.js +++ b/src/sao.js @@ -1171,55 +1171,4 @@ var Sao = {}; .next('.modal-backdrop.in').removeClass('hidden'); } - if ($.fn.datetimepicker) { - // XXX: use Popper to place popup - // cherry picked from tempusdominus-core.js - $.fn.datetimepicker.prototype.constructor.Constructor.prototype._place = function _place(e) { - var self = (e && e.data && e.data.picker) || this; - if (self._options.sideBySide) { - self._element.append(self.widget); - return; - } - if (self._options.widgetParent) { - self._options.widgetParent.append(self.widget); - } else if (self._element.is('input')) { - self._element.after(self.widget).parent(); - } else { - self._element.children().first().after(self.widget); - } - - var reference = self.component[0]; - - if (!reference) { - reference = self._element; - } - - // ReSharper disable once ConstructorCallNotUsed - new Popper(reference, self.widget[0], { - placement: 'bottom-start' - }); - }; - - // XXX: Ensure to always return a date - $.fn.datetimepicker.prototype.constructor.Constructor.prototype._getLastPickedDate = function _getLastPickedDate() { - return this._dates[this._getLastPickedDateIndex()] || this.getMoment(); - }; - - // XXX: Add fallback index - var setValue = $.fn.datetimepicker.prototype.constructor.Constructor.prototype._setValue; - $.fn.datetimepicker.prototype.constructor.Constructor.prototype._setValue = function _setValue(targetMoment, index) { - index = index || 0; - return setValue.call(this, targetMoment, index); - }; - - // XXX: trigger show/hide event without date - var notifyEvent = $.fn.datetimepicker.prototype.constructor.Constructor.prototype._notifyEvent; - $.fn.datetimepicker.prototype.constructor.Constructor.prototype._notifyEvent = function _notifyEvent(e) { - if ((e.type === 'datetimepicker.hide') || (e.type === 'datetimepicker.show')) { - this._element.trigger(e); - } else { - notifyEvent.call(this, e); - } - }; - } }()); diff --git a/src/sao.less b/src/sao.less index fc9fd0ab86..cceba2fb11 100644 --- a/src/sao.less +++ b/src/sao.less @@ -2,7 +2,6 @@ this repository contains the full copyright notices and license terms. */ @import "bootstrap"; @import "bootstrap-rtl"; -@import "_tempusdominus-bootstrap-3"; @import "sao-variables"; html[theme="default"] { @import "theme"; @@ -686,6 +685,31 @@ input.column-boolean { .center-block(); } +.form-date, .form-datetime, .form-time, +.dict-date, .dict-datetime, +.input-date, .input-datetime-local, .input-time, +.editabletree-date, .editabletree-datetime, .editabletree-time { + .icon-input { + overflow-y: hidden; + } + input[type="date"], input[type="datetime-local"], input[type="time"] { + min-width: 0; + opacity: 0; + position: absolute; + width: calc(100% + 2em); + left: -2em; + + &::-webkit-calendar-picker-indicator { + bottom: 0; + left: 0; + position: absolute; + top: 0; + width: 100%; + height: 100%; + } + } +} + .dict-label { text-align: right; text-align: end; @@ -733,10 +757,6 @@ input.column-boolean { } } -.bootstrap-datetimepicker-widget.dropdown-menu { - z-index: 2000; -} - @media screen and (max-width: @screen-xs-max) { .treeview { height: calc(100vh - 370px); diff --git a/src/screen.js b/src/screen.js index 03b41a836d..142a423679 100644 --- a/src/screen.js +++ b/src/screen.js @@ -491,6 +491,7 @@ var input; var entry; + var format, date_format, time_format; switch (field.type) { case 'boolean': entry = input = jQuery('', { - 'class': ('form-control input-sm datetimepicker-input ' + - 'mousetrap'), - type: 'text', - placeholder: placeholder, - 'data-target': '#' + entry.attr('id'), + var date = jQuery('', { + 'type': 'text', + 'class': 'form-control input-sm mousetrap', }).appendTo(entry); - entry.datetimepicker({ - 'locale': Sao.common.moment_format(this.format), - 'keyBinds': null, - 'useCurrent': false, + var input = jQuery('', { + 'type': this._input, + 'role': 'button', + 'tabindex': -1, }); - entry.on('datetimepicker.hide', function() { - Sao.common.set_overflow(entry, 'hide'); - }); - entry.on('datetimepicker.show', function() { - Sao.common.set_overflow(entry, 'show'); - }); - - var mousetrap = new Mousetrap(el[0]); + input.click(function() { + var value = this._parse(this.format, date.val()); + value = this._format(this._input_format, value); + input.val(value); + }.bind(this)); + input.change(function() { + var value = input.val(); + if (value) { + value = this._parse(this._input_format, value); + value = this._format(this.format, value); + date.val(value); + date.focus(); + } + }.bind(this)); + if (input[0].type == this._input) { + var icon = jQuery('
', { + 'class': 'icon-input icon-secondary', + 'aria-label': Sao.i18n.gettext("Open the calendar"), + 'title': Sao.i18n.gettext("Open the calendar"), + }).appendTo(entry); + input.appendTo(icon); + Sao.common.ICONFACTORY.get_icon_img('tryton-date') + .appendTo(icon); + } + var mousetrap = new Mousetrap(date[0]); mousetrap.bind('enter', function(e, combo) { - entry.datetimepicker('date'); + var value = this._parse(this.format, date.val()); + value = this._format(this.format, value); + date.val(value); }); mousetrap.bind('=', function(e, combo) { e.preventDefault(); - entry.datetimepicker('date', moment()); + date.val(this._format(this.format, moment())); }); Sao.common.DATE_OPERATORS.forEach(function(operator) { mousetrap.bind(operator[0], function(e, combo) { e.preventDefault(); - var date = entry.datetimepicker('date'); - date.add(operator[1]); - entry.datetimepicker('date', date); - }); - }); + var value = (this._parse(this.format, date.val()) || + Sao.DateTime()); + value.add(operator[1]); + date.val(this._format(this.format, value)); + }.bind(this)); + }.bind(this)); return entry; }, }); - Sao.ScreenContainer.Numbers = Sao.class_(Sao.ScreenContainer.BetweenDates, { + Sao.ScreenContainer.DateTimes = Sao.class_( + Sao.ScreenContainer.Dates, { + _input: 'datetime-local', + _input_format: '%Y-%m-%dT%H:%M:%S', + _format: Sao.common.format_datetime, + _parse: Sao.common.parse_datetime, + }); + + Sao.ScreenContainer.Times = Sao.class_( + Sao.ScreenContainer.Dates, { + _input: 'time', + _input_format: '%H:%M:%S', + _format: Sao.common.format_time, + _parse: Sao.common.parse_time, + build_entry: function(placeholder, el) { + entry = Sao.ScreenContainer.Time._super.call( + this, placeholder, el); + if (~navigator.userAgent.indexOf("Firefox")) { + // time input on Firefox does not have a pop-up + entry.find('.icon-input').hide(); + } + return entry; + }, + }); + + Sao.ScreenContainer.Numbers = Sao.class_(Sao.ScreenContainer.Between, { init: function(id) { Sao.ScreenContainer.Numbers._super.init.call(this, id); this.from.change(this._from_changed.bind(this)); diff --git a/src/view/form.js b/src/view/form.js index 687d9314c7..dbab0f08c9 100644 --- a/src/view/form.js +++ b/src/view/form.js @@ -1559,55 +1559,68 @@ function eval_pyson(value){ Sao.View.Form.Date = Sao.class_(Sao.View.Form.Widget, { class_: 'form-date', _width: '10em', + _input: 'date', + _input_format: '%Y-%m-%d', + _format: Sao.common.format_date, + _parse: Sao.common.parse_date, + _default_format: '%x', init: function(view, attributes) { Sao.View.Form.Date._super.init.call(this, view, attributes); this.el = jQuery('
', { 'class': this.class_ }); - this.date = this.labelled = jQuery('
', { + var group = this.labelled = jQuery('
', { 'class': ('input-group input-group-sm ' + - 'input-icon input-icon-primary'), - 'data-target-input': 'nearest', + 'input-icon input-icon-secondary'), }).appendTo(this.el); + this.date = this.labelled = jQuery('', { + 'type': 'text', + 'class': 'form-control input-sm mousetrap', + 'name': attributes.name, + }).appendTo(group); this.date.uniqueId(); - Sao.common.ICONFACTORY.get_icon_img('tryton-date') - .appendTo(jQuery('
', { - 'class': 'icon-input icon-primary', + this.input = jQuery('', { + 'type': this._input, + 'role': 'button', + 'tabindex': -1, + }); + this.input.click(function() { + var value = this.get_value(); + value = this._format(this._input_format, value); + this.input.val(value); + }.bind(this)); + this.input.change(function() { + var value = this.input.val(); + if (value) { + value = this._parse(this._input_format, value); + value = this._format(this.get_format(), value); + this.date.val(value); + this.date.focus(); + } + }.bind(this)); + if (this.input[0].type == this._input) { + var icon = jQuery('
', { + 'class': 'icon-input icon-secondary', 'aria-label': Sao.i18n.gettext("Open the calendar"), 'title': Sao.i18n.gettext("Open the calendar"), - 'data-target': '#' + this.date.attr('id'), - 'data-toggle': 'datetimepicker', - }).appendTo(this.date)); - this.input = jQuery('', { - 'type': 'text', - 'class': 'form-control input-sm datetimepicker-input mousetrap', - 'data-target': '#' + this.date.attr('id'), - 'name': attributes.name, - }).appendTo(this.date); - this.date.datetimepicker({ - 'locale': moment.locale(), - 'keyBinds': null, - 'useCurrent': false, - }); + }).appendTo(group); + this.input.appendTo(icon); + Sao.common.ICONFACTORY.get_icon_img('tryton-date') + .appendTo(icon); + } this.date.css('max-width', this._width); - this.date.on('change.datetimepicker', this.focus_out.bind(this)); - this.date.on('datetimepicker.hide', function() { - Sao.common.set_overflow(this.date, 'hide'); - }.bind(this)); - this.date.on('datetimepicker.show', function() { - Sao.common.set_overflow(this.date, 'show'); - }.bind(this)); - var mousetrap = new Mousetrap(this.el[0]); + this.date.change(this.focus_out.bind(this)); + var mousetrap = new Mousetrap(this.date[0]); mousetrap.bind('enter', function(e, combo) { - if (!this.date.find('input').prop('readonly')) { - this.date.datetimepicker('date'); + if (!this.date.prop('readonly')) { + this.focus_out(); } }.bind(this)); mousetrap.bind('=', function(e, combo) { if (!this.date.find('input').prop('readonly')) { e.preventDefault(); - this.date.datetimepicker('date', moment()); + this.date.val(this._format(this.get_format(), moment())); } }.bind(this)); @@ -1617,42 +1630,31 @@ function eval_pyson(value){ return; } e.preventDefault(); - var date = this.date.datetimepicker('date'); + var date = this.get_value() || Sao.DateTime(); date.add(operator[1]); - this.date.datetimepicker('date', date); + this.date.val(this._format(this.get_format(), date)); }.bind(this)); }.bind(this)); }, get_format: function() { - return this.field.date_format(this.record); + if (this.field && this.record) { + return this.field.date_format(this.record); + } else { + return this._default_format; + } }, get_value: function() { - var value = this.date.datetimepicker('date'); - if (value) { - value.isDate = true; - } - return value; + return this._parse(this.get_format(), this.date.val()); }, display: function() { var record = this.record; var field = this.field; - if (record && field) { - this.date.datetimepicker( - 'format', Sao.common.moment_format(this.get_format())); - } Sao.View.Form.Date._super.display.call(this); var value; if (record) { value = field.get_client(record); - } else { - value = null; - } - this.date.off('change.datetimepicker'); - try { - this.date.datetimepicker('date', value); - } finally { - this.date.on('change.datetimepicker', this.focus_out.bind(this)); } + this.date.val(this._format(this.get_format(), value)); }, focus: function() { this.input.focus(); @@ -1677,33 +1679,43 @@ function eval_pyson(value){ Sao.View.Form.DateTime = Sao.class_(Sao.View.Form.Date, { class_: 'form-datetime', _width: '20em', + _input: 'datetime-local', + _input_format: '%Y-%m-%dT%H:%M:%S', + _format: Sao.common.format_datetime, + _parse: Sao.common.parse_datetime, + _default_format: '%x %X', get_format: function() { - var record = this.record; - var field = this.field; - return field.date_format(record) + ' ' + field.time_format(record); - }, - get_value: function() { - var value = this.date.datetimepicker('date'); - if (value) { - value.isDateTime = true; + if (this.field && this.record) { + return (this.field.date_format(this.record) + ' ' + + this.field.time_format(this.record)); + } else { + return this._default_format; } - return value; - } + }, }); Sao.View.Form.Time = Sao.class_(Sao.View.Form.Date, { class_: 'form-time', _width: '10em', - get_format: function() { - return this.field.time_format(this.record); - }, - get_value: function() { - var value = this.date.datetimepicker('date'); - if (value) { - value.isTime = true; + _input: 'time', + _input_format: '%H:%M:%S', + _format: Sao.common.format_time, + _parse: Sao.common.parse_time, + _default_format: '%X', + init: function(view, attributes) { + Sao.View.Form.Time._super.init.call(this, view, attributes); + if (~navigator.userAgent.indexOf("Firefox")) { + // time input on Firefox does not have a pop-up + this.input.parent().hide(); } - return value; - } + }, + get_format: function() { + if (this.field && this.record) { + return this.field.time_format(this.record); + } else { + return this._default_format; + } + }, }); Sao.View.Form.TimeDelta = Sao.class_(Sao.View.Form.Widget, { @@ -4734,76 +4746,78 @@ function eval_pyson(value){ Sao.View.Form.Dict.Date = Sao.class_(Sao.View.Form.Dict.Entry, { class_: 'dict-date', format: '%x', + _input: 'date', + _input_format: '%Y-%m-%d', + _format: Sao.common.format_date, + _parse: Sao.common.parse_date, create_widget: function() { Sao.View.Form.Dict.Date._super.create_widget.call(this); - this.date = this.input.parent(); - this.date.addClass('input-icon input-icon-primary'); - this.date.attr('data-target-input', 'nearest'); - this.date.uniqueId(); - Sao.common.ICONFACTORY.get_icon_img('tryton-date') - .appendTo(jQuery('
', { - 'class': 'icon-input icon-primary', + var group = this.input.parent().find('.input-group-btn'); + this.input_date = jQuery('', { + 'type': this._input, + 'role': 'button', + 'tabindex': -1, + }); + this.input_date.click(function() { + var value = this.get_value(); + value = this._format(this._input_format, value); + this.input_date.val(value); + }.bind(this)); + this.input_date.change(function() { + var value = this.input_date.val(); + if (value) { + value = this._parse(this._input_format, value); + value = this._format(this.format, value); + this.input.val(value); + this.input.focus(); + } + }.bind(this)); + if (this.input_date[0].type == this._input) { + var icon = jQuery('
', { + 'class': 'btn btn-default', 'aria-label': Sao.i18n.gettext("Open the calendar"), 'title': Sao.i18n.gettext("Open the calendar"), - 'data-target': '#' + this.date.attr('id'), - 'data-toggle': 'datetimepicker', - }).prependTo(this.date)); - this.input.addClass('datetimepicker-input'); - this.input.data('target', '#' + this.date.attr('id')); - this.date.datetimepicker({ - 'format': Sao.common.moment_format(this.format), - 'locale': moment.locale(), - 'keyBinds': null, - 'useCurrent': false, - }); - this.date.on('change.datetimepicker', - this.parent_widget.focus_out.bind(this.parent_widget)); + }).prependTo(group); + this.input_date.appendTo(icon); + Sao.common.ICONFACTORY.get_icon_img('tryton-date') + .appendTo(icon); + } var mousetrap = new Mousetrap(this.el[0]); - mousetrap.bind(['enter', '='], function(e, combo) { - if (e.which != Sao.common.RETURN_KEYCODE) { - e.preventDefault(); - } - this.date.datetimepicker('date', moment()); + mousetrap.bind('enter', function(e, combo) { + var value = this._parse(this.format, this.input.val()); + value = this._format(this.format, value); + this.input.val(value); + }.bind(this)); + mousetrap.bind('=', function(e, combo) { + e.preventDefault(); + this.input.val(this._format(this.format, moment())); }.bind(this)); Sao.common.DATE_OPERATORS.forEach(function(operator) { mousetrap.bind(operator[0], function(e, combo) { e.preventDefault(); - var date = this.date.datetimepicker('date'); + var date = this.get_value() || Sao.DateTime(); date.add(operator[1]); - this.date.datetimepicker('date', date); + this.input.val(this._format(this.format, date)); }.bind(this)); }.bind(this)); }, get_value: function() { - var value = this.date.datetimepicker('date'); - if (value) { - value.isDate = true; - } - return value; + return this._parse(this.format, this.input.val()); }, set_value: function(value) { - this.date.off('change.datetimepicker'); - try { - this.date.datetimepicker('date', value); - } finally { - this.date.on('change.datetimepicker', - this.parent_widget.focus_out.bind(this.parent_widget)); - } - } + this.input.val(this._format(this.format, value)); + }, }); Sao.View.Form.Dict.DateTime = Sao.class_(Sao.View.Form.Dict.Date, { class_: 'dict-datetime', format: '%x %X', - get_value: function() { - var value = this.date.datetimepicker('date'); - if (value) { - value.isDateTime = true; - } - return value; - } + _input: 'datetime-local', + _input_format: '%Y-%m-%dT%H:%M:%S', + _format: Sao.common.format_datetime, + _parse: Sao.common.parse_datetime, }); Sao.View.Form.PYSON = Sao.class_(Sao.View.Form.Char, { diff --git a/src/view/tree.js b/src/view/tree.js index 15d36697c3..9870c33765 100644 --- a/src/view/tree.js +++ b/src/view/tree.js @@ -2617,11 +2617,6 @@ init: function(view, attributes) { Sao.View.EditableTree.Date._super.init.call( this, view, attributes); - // XXX: click event does not work in editable - // so we manage it ourselves to toggle the picker. - this.date.find('.icon-input').removeAttr('data-toggle').click(function() { - this.date.datetimepicker('toggle'); - }.bind(this)); Sao.View.EditableTree.editable_mixin(this); } });