diff --git a/CHANGELOG b/CHANGELOG index e19b322907..9065299a38 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Use tempusdominus and Popper for date time picker * Support PYSON comparison of date and datetime * Customize bootstrap default style * Set field's name as input's name attribute diff --git a/Gruntfile.js b/Gruntfile.js index d530bd03fb..567a68a2b5 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -106,7 +106,7 @@ module.exports = function(grunt) { 'bower_components/bootstrap', 'bower_components/bootstrap/less', 'bower_components/bootstrap-rtl-ondemand/less', - 'bower_components/eonasdan-bootstrap-datetimepicker/src/less', + 'bower_components/tempusdominus-bootstrap-3/src/less/', ] }, files: { diff --git a/bower.json b/bower.json index 0ca7f94609..13e2212414 100644 --- a/bower.json +++ b/bower.json @@ -18,14 +18,15 @@ "jquery": "^3", "bootstrap": "^3.3.7", "moment": "^2.10", - "eonasdan-bootstrap-datetimepicker": "^4.17", "gettext.js": "^0.7", "c3": "^0.7", "papaparse": "^5.0", "fullcalendar": "^3.10.2", "mousetrap": "^1.6", "bootstrap-rtl-ondemand": "^3.3.4-ondemand", - "Sortable": "sortablejs#^1.8.4" + "Sortable": "sortablejs#^1.8.4", + "tempusdominus-bootstrap-3": "bootstrap-datetimepicker-v5-alpha#^5.0.0-alpha8", + "popper.js": "https://unpkg.com/popper.js" }, "devDependencies": { "qunit": "^1.18" diff --git a/index.html b/index.html index ef61807acd..3955621373 100644 --- a/index.html +++ b/index.html @@ -13,7 +13,8 @@ this repository contains the full copyright notices and license terms. --> - + + diff --git a/src/sao.js b/src/sao.js index e44f56bbd7..888c671efd 100644 --- a/src/sao.js +++ b/src/sao.js @@ -1146,4 +1146,37 @@ var Sao = {}; jQuery('.modal.in:visible:last').focus() .next('.modal-backdrop.in').removeClass('hidden'); } + + // 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(); + }; }()); diff --git a/src/sao.less b/src/sao.less index c5458ebb2d..df4de73712 100644 --- a/src/sao.less +++ b/src/sao.less @@ -2,7 +2,7 @@ this repository contains the full copyright notices and license terms. */ @import "bootstrap"; @import "bootstrap-rtl"; -@import "bootstrap-datetimepicker-build"; +@import "_tempusdominus-bootstrap-3"; @import "sao-variables"; html[theme="default"] { @import "theme"; @@ -681,6 +681,10 @@ 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 a9078326f4..a44fadcfc4 100644 --- a/src/screen.js +++ b/src/screen.js @@ -636,56 +636,51 @@ Sao.ScreenContainer.BetweenDates, { build_entry: function(placeholder, el) { var entry = jQuery('
', { - 'class': 'input-group input-group-sm' + 'class': 'input-group input-group-sm', + 'data-target-input': 'nearest', }).appendTo(el); + entry.uniqueId(); jQuery('', { 'class': 'input-group-btn' }).append(jQuery('', { - 'class': 'datepickerbutton btn btn-default', + 'class': 'btn btn-default', type: 'button', 'tabindex': -1, 'aria-label': Sao.i18n.gettext("Open the calendar"), 'title': Sao.i18n.gettext("Open the calendar"), + 'data-target': '#' + entry.attr('id'), + 'data-toggle': 'datetimepicker', }).append(Sao.common.ICONFACTORY.get_icon_img('tryton-date') )).appendTo(entry); jQuery('', { - 'class': 'form-control input-sm', + 'class': ('form-control input-sm datetimepicker-input ' + + 'mousetrap'), type: 'text', placeholder: placeholder, + 'data-target': '#' + entry.attr('id'), }).appendTo(entry); entry.datetimepicker({ - 'locale': moment.locale(), + 'locale': Sao.common.moment_format(this.format), 'keyBinds': null, - }); - entry.data('DateTimePicker').format(this.format); - // We must set the overflow of the modal-body - // containing the input to visible to prevent vertical scrollbar - // inherited from the auto overflow-x - // (see http://www.w3.org/TR/css-overflow-3/#overflow-properties) - entry.on('dp.hide', function() { - entry.closest('.modal-body').css('overflow', ''); - }); - entry.on('dp.show', function() { - entry.closest('.modal-body').css('overflow', 'visible'); + 'useCurrent': false, }); var mousetrap = new Mousetrap(el[0]); mousetrap.bind('enter', function(e, combo) { - entry.data('DateTimePicker').date(); + entry.datetimepicker('date'); }); mousetrap.bind('=', function(e, combo) { e.preventDefault(); - entry.data('DateTimePicker').date(moment()); + entry.datetimepicker('date', moment()); }); Sao.common.DATE_OPERATORS.forEach(function(operator) { mousetrap.bind(operator[0], function(e, combo) { e.preventDefault(); - var dp = entry.data('DateTimePicker'); - var date = dp.date(); + var date = entry.datetimepicker('date'); date.add(operator[1]); - dp.date(date); + entry.datetimepicker('date', date); }); }); return entry; diff --git a/src/view/form.js b/src/view/form.js index 6914ec761d..338b336cf7 100644 --- a/src/view/form.js +++ b/src/view/form.js @@ -1566,16 +1566,21 @@ function eval_pyson(value){ this.date = this.labelled = jQuery('', { 'class': ('input-group input-group-sm ' + 'input-icon input-icon-primary'), + 'data-target-input': 'nearest', }).appendTo(this.el); + this.date.uniqueId(); Sao.common.ICONFACTORY.get_icon_img('tryton-date') .appendTo(jQuery('', { - 'class': 'datepickerbutton icon-input icon-primary', + 'class': 'icon-input icon-primary', '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 mousetrap', + 'class': 'form-control input-sm datetimepicker-input mousetrap', + 'data-target': '#' + this.date.attr('id'), 'name': attributes.name, }).appendTo(this.date); this.date.datetimepicker({ @@ -1584,32 +1589,18 @@ function eval_pyson(value){ 'useCurrent': false, }); this.date.css('max-width', this._width); - this.date.on('dp.change', this.focus_out.bind(this)); - // We must set the overflow of the treeview and modal-body - // containing the input to visible to prevent vertical scrollbar - // inherited from the auto overflow-x - // (see http://www.w3.org/TR/css-overflow-3/#overflow-properties) - this.date.on('dp.hide', function() { - this.date.closest('.treeview').css('overflow', ''); - this.date.closest('.modal-body').css('overflow', ''); - this.date.closest('.form-group_').css('overflow', 'auto'); - }.bind(this)); - this.date.on('dp.show', function() { - this.date.closest('.treeview').css('overflow', 'visible'); - this.date.closest('.modal-body').css('overflow', 'visible'); - this.date.closest('.form-group_').css('overflow', 'visible'); - }.bind(this)); + this.date.on('change.datetimepicker', this.focus_out.bind(this)); var mousetrap = new Mousetrap(this.el[0]); mousetrap.bind('enter', function(e, combo) { if (!this.date.find('input').prop('readonly')) { - this.date.data('DateTimePicker').date(); + this.date.datetimepicker('date'); } }.bind(this)); mousetrap.bind('=', function(e, combo) { if (!this.date.find('input').prop('readonly')) { e.preventDefault(); - this.date.data('DateTimePicker').date(moment()); + this.date.datetimepicker('date', moment()); } }.bind(this)); @@ -1619,10 +1610,9 @@ function eval_pyson(value){ return; } e.preventDefault(); - var dp = this.date.data('DateTimePicker'); - var date = dp.date(); + var date = this.date.datetimepicker('date'); date.add(operator[1]); - dp.date(date); + this.date.datetimepicker('date', date); }.bind(this)); }.bind(this)); }, @@ -1630,7 +1620,7 @@ function eval_pyson(value){ return this.field.date_format(this.record); }, get_value: function() { - var value = this.date.data('DateTimePicker').date(); + var value = this.date.datetimepicker('date'); if (value) { value.isDate = true; } @@ -1640,8 +1630,8 @@ function eval_pyson(value){ var record = this.record; var field = this.field; if (record && field) { - this.date.data('DateTimePicker').format( - Sao.common.moment_format(this.get_format())); + this.date.datetimepicker( + 'format', Sao.common.moment_format(this.get_format())); } Sao.View.Form.Date._super.display.call(this); var value; @@ -1650,11 +1640,11 @@ function eval_pyson(value){ } else { value = null; } - this.date.off('dp.change'); + this.date.off('change.datetimepicker'); try { - this.date.data('DateTimePicker').date(value); + this.date.datetimepicker('date', value); } finally { - this.date.on('dp.change', this.focus_out.bind(this)); + this.date.on('change.datetimepicker', this.focus_out.bind(this)); } }, focus: function() { @@ -1686,7 +1676,7 @@ function eval_pyson(value){ return field.date_format(record) + ' ' + field.time_format(record); }, get_value: function() { - var value = this.date.data('DateTimePicker').date(); + var value = this.date.datetimepicker('date'); if (value) { value.isDateTime = true; } @@ -1701,7 +1691,7 @@ function eval_pyson(value){ return this.field.time_format(this.record); }, get_value: function() { - var value = this.date.data('DateTimePicker').date(); + var value = this.date.datetimepicker('date'); if (value) { value.isTime = true; } @@ -4788,64 +4778,57 @@ function eval_pyson(value){ 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': 'datepickerbutton icon-input icon-primary', + 'class': 'icon-input icon-primary', '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('dp.change', + this.date.on('change.datetimepicker', this.parent_widget.focus_out.bind(this.parent_widget)); - // We must set the overflow of the treeview and modal-body - // containing the input to visible to prevent vertical scrollbar - // inherited from the auto overflow-x - // (see http://www.w3.org/TR/css-overflow-3/#overflow-properties) - this.date.on('dp.hide', function() { - this.date.closest('.treeview').css('overflow', ''); - this.date.closest('.modal-body').css('overflow', ''); - }.bind(this)); - this.date.on('dp.show', function() { - this.date.closest('.treeview').css('overflow', 'visible'); - this.date.closest('.modal-body').css('overflow', 'visible'); - }.bind(this)); var mousetrap = new Mousetrap(this.el[0]); mousetrap.bind(['enter', '='], function(e, combo) { if (e.which != Sao.common.RETURN_KEYCODE) { e.preventDefault(); } - this.date.data('DateTimePicker').date(moment()); + this.date.datetimepicker('date', moment()); }.bind(this)); Sao.common.DATE_OPERATORS.forEach(function(operator) { mousetrap.bind(operator[0], function(e, combo) { e.preventDefault(); - var dp = this.date.data('DateTimePicker'); - var date = dp.date(); + var date = this.date.datetimepicker('date'); date.add(operator[1]); - dp.date(date); + this.date.datetimepicker('date', date); }.bind(this)); }.bind(this)); }, get_value: function() { - var value = this.date.data('DateTimePicker').date(); + var value = this.date.datetimepicker('date'); if (value) { value.isDate = true; } return value; }, set_value: function(value) { - this.date.off('dp.change'); + this.date.off('change.datetimepicker'); try { - this.date.data('DateTimePicker').date(value); + this.date.datetimepicker('date', value); } finally { - this.date.on('dp.change', + this.date.on('change.datetimepicker', this.parent_widget.focus_out.bind(this.parent_widget)); } } @@ -4855,7 +4838,7 @@ function eval_pyson(value){ class_: 'dict-datetime', format: '%x %X', get_value: function() { - var value = this.date.data('DateTimePicker').date(); + var value = this.date.datetimepicker('date'); if (value) { value.isDateTime = true; } diff --git a/src/view/tree.js b/src/view/tree.js index 7fa30c8bac..2a8f6c0ef1 100644 --- a/src/view/tree.js +++ b/src/view/tree.js @@ -2559,6 +2559,11 @@ 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); } });