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
This commit is contained in:
Cédric Krier
2021-02-21 19:38:25 +01:00
parent ba5044b150
commit 765e828c19
9 changed files with 267 additions and 255 deletions
+1
View File
@@ -1,3 +1,4 @@
* Remove tempusdominus dependency
* Handle device cookie
* Add breadcrumb as title of window form
* Manage help for each selection
+1 -2
View File
@@ -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.
-1
View File
@@ -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": {
+8 -12
View File
@@ -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(
-51
View File
@@ -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);
}
};
}
}());
+25 -5
View File
@@ -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);
+101 -62
View File
@@ -491,6 +491,7 @@
var input;
var entry;
var format, date_format, time_format;
switch (field.type) {
case 'boolean':
entry = input = jQuery('<select/>', {
@@ -509,25 +510,29 @@
input = entry.el;
break;
case 'date':
case 'datetime':
case 'time':
var format;
var date_format = Sao.common.date_format(
format = Sao.common.date_format(
this.screen.context.date_format);
if (field.type == 'date') {
format = date_format;
} else {
var time_format = new Sao.PYSON.Decoder({}).decode(
field.format);
time_format = Sao.common.moment_format(time_format);
if (field.type == 'time') {
format = time_format;
} else if (field.type == 'datetime') {
format = date_format + ' ' + time_format;
}
}
entry = new Sao.ScreenContainer.Dates(
format, prefix + field.name);
input = entry.el;
break;
case 'datetime':
date_format = Sao.common.date_format(
this.screen.context.date_format);
time_format = new Sao.PYSON.Decoder({}).decode(
field.format);
time_format = Sao.common.moment_format(time_format);
format = date_format + ' ' + time_format;
entry = new Sao.ScreenContainer.DateTimes(
format, prefix + field.name);
format, prefix + field.name);
input = entry.el;
break;
case 'time':
time_format = new Sao.PYSON.Decoder({}).decode(
field.format);
format = Sao.common.moment_format(time_format);
entry = new Sao.ScreenContainer.Times(
format, prefix + field.name);
input = entry.el;
break;
case 'integer':
@@ -631,75 +636,109 @@
this.from.on('dp.change', this._from_changed.bind(this));
},
_get_value: function(entry, value) {
return entry.find('input').val();
return entry.find('input[type=text]').val();
},
_set_value: function(entry, value) {
entry.data('DateTimePicker').date(value);
entry.find('input[type=text]').val(value);
},
});
Sao.ScreenContainer.DateTimes = Sao.class_(
Sao.ScreenContainer.Dates = Sao.class_(
Sao.ScreenContainer.BetweenDates, {
build_entry: function(placeholder, el) {
_input: 'date',
_input_format: '%Y-%m-%d',
_format: Sao.common.format_date,
_parse: Sao.common.parse_date,
build_entry: function(placeholder, el) {
var entry = jQuery('<div/>', {
'class': 'input-group input-group-sm',
'data-target-input': 'nearest',
'class': ('input-group input-group-sm ' +
'input-icon input-icon-secondary ' +
'input-' + this._input),
}).appendTo(el);
entry.uniqueId();
jQuery('<span/>', {
'class': 'input-group-btn'
}).append(jQuery('<button/>', {
'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('<input/>', {
'class': ('form-control input-sm datetimepicker-input ' +
'mousetrap'),
type: 'text',
placeholder: placeholder,
'data-target': '#' + entry.attr('id'),
var date = jQuery('<input/>', {
'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('<input/>', {
'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('<div/>', {
'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));
+131 -117
View File
@@ -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('<div/>', {
'class': this.class_
});
this.date = this.labelled = jQuery('<div/>', {
var group = this.labelled = jQuery('<div/>', {
'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('<input/>', {
'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('<div/>', {
'class': 'icon-input icon-primary',
this.input = jQuery('<input/>', {
'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('<div/>', {
'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('<input/>', {
'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('<div/>', {
'class': 'icon-input icon-primary',
var group = this.input.parent().find('.input-group-btn');
this.input_date = jQuery('<input/>', {
'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('<div/>', {
'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, {
-5
View File
@@ -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);
}
});