summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAaron Bohy <aab@odoo.com>2018-09-17 12:12:41 +0200
committerAaron Bohy <aab@odoo.com>2018-09-17 15:23:33 +0200
commitc63630d41622412261401300775c0f720e6acbef (patch)
tree094c2ecc786979b6f02921612864f95f84d54aa4
parent76a557c9ace456bdc538d90992b6aeaacc3c3395 (diff)
[FIX] web: datepicker: correctly use lib
The datepicker lib has been updated to tempus dominus (BS4) recently, but our code hasn't been adapted correctly to the requirements of the new version of the lib. As a consequence, the datepicker didn't close itself when the input was focused out anymore. This required a slight change in the DomainSelector widget as it produced a crash when a focused datepicker widget is removed from the DOM (before the datepicker is destroyed), e.g. by a call to html() on one of its parent. Task 1878254
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js19
-rw-r--r--addons/web/static/src/js/widgets/date_picker.js92
-rw-r--r--addons/web/static/src/js/widgets/domain_selector.js2
-rw-r--r--addons/web/static/src/xml/base.xml8
-rw-r--r--addons/web/static/tests/fields/basic_fields_tests.js82
-rw-r--r--addons/web/static/tests/helpers/test_utils.js12
-rw-r--r--addons/web/static/tests/views/calendar_tests.js8
-rw-r--r--addons/web/static/tests/views/form_tests.js8
-rw-r--r--addons/web/static/tests/widgets/domain_selector_tests.js3
9 files changed, 150 insertions, 84 deletions
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index d3ca9a653fd..0a397a65047 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -526,6 +526,25 @@ var FieldDate = InputField.extend({
},
//--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Asks the datepicker widget to activate the input, instead of doing it
+ * ourself, such that 'input' events triggered by the lib are correctly
+ * intercepted, and don't produce unwanted 'field_changed' events.
+ *
+ * @override
+ */
+ activate: function () {
+ if (this.datewidget) {
+ this.datewidget.focus();
+ return true;
+ }
+ return false;
+ },
+
+ //--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
diff --git a/addons/web/static/src/js/widgets/date_picker.js b/addons/web/static/src/js/widgets/date_picker.js
index 0592292a073..109a5c958c4 100644
--- a/addons/web/static/src/js/widgets/date_picker.js
+++ b/addons/web/static/src/js/widgets/date_picker.js
@@ -13,8 +13,6 @@ var DateWidget = Widget.extend({
type_of_date: "date",
events: {
'change.datetimepicker': 'changeDatetime',
- 'show.datetimepicker': '_onShow',
- 'click input.o_datepicker_input': '_onClick',
'change .o_datepicker_input': 'changeDatetime',
'input input': '_onInput',
},
@@ -51,27 +49,26 @@ var DateWidget = Widget.extend({
keyBinds: null,
allowInputToggle: true,
}, options || {});
+
+ this.__libInput = 0;
},
/**
* @override
*/
start: function () {
this.$input = this.$('input.o_datepicker_input');
- this.$input.focus(function (e) {
- e.stopImmediatePropagation();
- });
- this.__libInput = true;
- this.$input.datetimepicker(this.options);
- this.__libInput = false;
+ this.__libInput++;
+ this.$el.datetimepicker(this.options);
+ this.__libInput--;
this._setReadonly(false);
},
/**
* @override
*/
destroy: function () {
- this.__libInput = true;
- this.$input.datetimepicker('destroy');
- this.__libInput = false;
+ this.__libInput++;
+ this.$el.datetimepicker('destroy');
+ this.__libInput--;
this._super.apply(this, arguments);
},
@@ -106,6 +103,16 @@ var DateWidget = Widget.extend({
}
},
/**
+ * Focuses the datepicker input. This function must be called in order to
+ * prevent 'input' events triggered by the lib to bubble up, and to cause
+ * unwanted effects (like triggering 'field_changed' events)
+ */
+ focus: function () {
+ this.__libInput++;
+ this.$input.focus();
+ this.__libInput--;
+ },
+ /**
* @returns {Moment|false}
*/
getValue: function () {
@@ -132,17 +139,17 @@ var DateWidget = Widget.extend({
* @returns {Moment|false} value
*/
maxDate: function (date) {
- this.__libInput = true;
- this.$input.datetimepicker('maxDate', date || null);
- this.__libInput = false;
+ this.__libInput++;
+ this.$el.datetimepicker('maxDate', date || null);
+ this.__libInput--;
},
/**
* @returns {Moment|false} value
*/
minDate: function (date) {
- this.__libInput = true;
- this.$input.datetimepicker('minDate', date || null);
- this.__libInput = false;
+ this.__libInput++;
+ this.$el.datetimepicker('minDate', date || null);
+ this.__libInput--;
},
/**
* @param {Moment|false} value
@@ -151,9 +158,9 @@ var DateWidget = Widget.extend({
this.set({'value': value});
var formatted_value = value ? this._formatClient(value) : null;
this.$input.val(formatted_value);
- this.__libInput = true;
- this.$input.datetimepicker('date', value || null);
- this.__libInput = false;
+ this.__libInput++;
+ this.$el.datetimepicker('date', value || null);
+ this.__libInput--;
},
//--------------------------------------------------------------------------
@@ -166,16 +173,19 @@ var DateWidget = Widget.extend({
* @private
* @param {Moment} currentDate
*/
- _warnFuture: function(currentDate) {
+ _warnFuture: function (currentDate) {
if (!this.$warning) {
- this.$warning = $('<span class="fa fa-exclamation-triangle o_tz_warning o_datepicker_warning"/>')
- this.$warning.attr('title', _t("This date is on the future. Make sure it is what you expected."))
- this.$input.after(this.$warning)
+ this.$warning = $('<span>', {
+ class: 'fa fa-exclamation-triangle o_tz_warning o_datepicker_warning',
+ });
+ var title = _t("This date is on the future. Make sure it is what you expected.");
+ this.$warning.attr('title', title);
+ this.$input.after(this.$warning);
}
if (currentDate.isAfter(moment())) {
- this.$warning.show()
+ this.$warning.show();
} else {
- this.$warning.hide()
+ this.$warning.hide();
}
},
@@ -218,38 +228,18 @@ var DateWidget = Widget.extend({
//--------------------------------------------------------------------------
/**
- * @private
- */
- _onClick: function () {
- this.__libInput = true;
- this.$input.datetimepicker('toggle');
- this.__libInput = false;
- },
- /**
+ * Prevents 'input' events triggered by the library to bubble up, as they
+ * might have unwanted effects (like triggering 'field_changed' events in
+ * the context of field widgets)
+ *
* @private
* @param {Event} ev
*/
_onInput: function (ev) {
- if (this.__libInput) {
+ if (this.__libInput > 0) {
ev.stopImmediatePropagation();
}
},
- /**
- * set the date of the picker by the current date or the today date
- *
- * @private
- */
- _onShow: function () {
- //when opening datetimepicker the date and time by default should be the one from
- //the input field if any or the current day otherwise
- if (this.$input.val().length !== 0 && this.isValid()) {
- var value = this._parseClient(this.$input.val());
- this.__libInput = true;
- this.$input.datetimepicker('date', value);
- this.__libInput = false;
- this.$input.select();
- }
- },
});
var DateTimeWidget = DateWidget.extend({
diff --git a/addons/web/static/src/js/widgets/domain_selector.js b/addons/web/static/src/js/widgets/domain_selector.js
index 215c37cc128..8e20e1ce18d 100644
--- a/addons/web/static/src/js/widgets/domain_selector.js
+++ b/addons/web/static/src/js/widgets/domain_selector.js
@@ -548,10 +548,10 @@ var DomainSelector = DomainTree.extend({
var oldChildren = this.children.slice();
this._initialize(domain || this.getDomain());
return this._renderChildrenTo($("<div/>")).then((function () {
+ _.each(oldChildren, function (child) { child.destroy(); });
this.renderElement();
this._postRender();
_.each(this.children, (function (child) { child.$el.appendTo(this.$childrenContainer); }).bind(this));
- _.each(oldChildren, function (child) { child.destroy(); });
}).bind(this));
},
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index 117299070ea..c79dc52ad42 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -773,11 +773,13 @@
</ul>
<t t-name="web.datepicker">
<t t-set="placeholder" t-value="widget.getParent().node and widget.getParent().node.attrs.placeholder"/>
- <div class="o_datepicker" aria-atomic="true">
+ <t t-set="datepickerID" t-value="_.uniqueId('datepicker')"/>
+ <div class="o_datepicker" aria-atomic="true" t-att-id="datepickerID" data-target-input="nearest">
<input type="text"
- class="o_datepicker_input o_input"
+ class="o_datepicker_input o_input datetimepicker-input"
t-att-name="widget.name"
- t-att-placeholder="placeholder"/>
+ t-att-placeholder="placeholder"
+ t-attf-data-target="##{datepickerID}"/>
<span class="o_datepicker_button"/>
</div>
</t>
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index 548a9cfec23..8407a64f2a7 100644
--- a/addons/web/static/tests/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -2393,6 +2393,36 @@ QUnit.module('basic_fields', {
QUnit.module('FieldDate');
+ QUnit.test('date field: toggle datepicker', function (assert) {
+ assert.expect(3);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form><field name="foo"/><field name="date"/></form>',
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ },
+ });
+
+ assert.strictEqual($('.bootstrap-datetimepicker-widget:visible').length, 0,
+ "datepicker should be closed initially");
+
+ testUtils.openDatepicker(form.$('.o_datepicker'));
+
+ assert.strictEqual($('.bootstrap-datetimepicker-widget:visible').length, 1,
+ "datepicker should be opened");
+
+ // focus another field
+ form.$('.o_field_widget[name=foo]').click().focus();
+
+ assert.strictEqual($('.bootstrap-datetimepicker-widget:visible').length, 0,
+ "datepicker should close itself when the user clicks outside");
+
+ form.destroy();
+ });
+
QUnit.test('date field is empty if no date is set', function (assert) {
assert.expect(2);
@@ -2419,12 +2449,19 @@ QUnit.module('basic_fields', {
arch:'<form string="Partners"><field name="date"/></form>',
res_id: 4,
});
+
form.$buttons.find('.o_form_button_edit').click();
- form.$('.o_datepicker_input').click();
+
+ // open datepicker and select a date
+ testUtils.openDatepicker(form.$('.o_datepicker'));
assert.strictEqual(form.$('.o_datepicker_input').val(), '', "date field's input should be empty on first click");
$('.day:contains(22)').click();
- form.$('.o_datepicker_input').click(); // Open Datepicker second time
- assert.strictEqual($('.day.active').text(), '22', 'datepicker should be highlight with 22nd day of month');
+
+ // re-open datepicker
+ testUtils.openDatepicker(form.$('.o_datepicker'));
+ assert.strictEqual($('.day.active').text(), '22',
+ "datepicker should be highlight with 22nd day of month");
+
form.destroy();
});
@@ -2461,8 +2498,8 @@ QUnit.module('basic_fields', {
assert.strictEqual(form.$('.o_datepicker_input').val(), '02/03/2017',
'the date should be correct in edit mode');
- // click on the input and select another value
- form.$('.o_datepicker_input').click();
+ // open datepicker and select another value
+ testUtils.openDatepicker(form.$('.o_datepicker'));
assert.ok($('.bootstrap-datetimepicker-widget').length, 'datepicker should be open');
assert.strictEqual($('.day.active').data('day'), '02/03/2017', 'datepicker should be highlight February 3');
$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Month selection
@@ -2545,8 +2582,8 @@ QUnit.module('basic_fields', {
assert.strictEqual(list.$('input.o_datepicker_input').val(), '02/03/2017',
'the date should be correct in edit mode');
- // click on the input and select another value
- list.$('input.o_datepicker_input').click();
+ // open datepicker and select another value
+ testUtils.openDatepicker(list.$('.o_datepicker'));
assert.ok($('.bootstrap-datetimepicker-widget').length, 'datepicker should be open');
$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Month selection
$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Year selection
@@ -2590,13 +2627,15 @@ QUnit.module('basic_fields', {
assert.strictEqual(form.$('.o_datepicker_input').val(), '02/03/2017',
'the date should be correct in edit mode');
- $('.o_datepicker_input').val('').trigger('input').trigger('change');
- assert.ok(!form.$('.o_datepicker_input').val());
+ form.$('.o_datepicker_input').val('').trigger('input').trigger('change').trigger('focusout');
+ assert.strictEqual(form.$('.o_datepicker_input').val(), '',
+ 'should have correctly removed the value');
// save
form.$buttons.find('.o_form_button_save').click();
assert.strictEqual(form.$('.o_field_date').text(), '',
'the selected date should be displayed after saving');
+
form.destroy();
});
@@ -2625,7 +2664,8 @@ QUnit.module('basic_fields', {
assert.strictEqual(form.$('.o_datepicker_input').val(), '02/08/2017',
'the date should be correct');
- form.$('input[name="datetime"]').val('02/08/2017').trigger('input').trigger('change');
+ form.$('input[name="datetime"]').val('02/08/2017')
+ .trigger('input').trigger('change').trigger('focusout');
form.$buttons.find('.o_form_button_save').click();
assert.verifySteps(['read']); // should not have save as nothing changed
@@ -2694,7 +2734,7 @@ QUnit.module('basic_fields', {
});
QUnit.test('datetime field in form view 2', function (assert) {
- assert.expect(1);
+ assert.expect(2);
this.data.partner.fields.datetime.default = "2017-08-02 12:00:05";
this.data.partner.fields.datetime.required = true;
@@ -2704,23 +2744,21 @@ QUnit.module('basic_fields', {
model: 'partner',
data: this.data,
arch:'<form string="Partners"><field name="datetime"/></form>',
- res_id: 1,
translateParameters: { // Avoid issues due to localization formats
date_format: '%m/%d/%Y',
time_format: '%H:%M',
},
});
- testUtils.patch(basicFields.FieldDate, {
- _setValue: function () {
- throw "The time format of the language must be taken into account.";
- },
- });
- form.$buttons.find('.o_form_button_create').click();
+
var expectedDateString = "08/02/2017 12:00"; // 10:00:00 without timezone
assert.strictEqual(form.$('.o_field_date input').val(), expectedDateString,
'the datetime should be correctly displayed in readonly');
- testUtils.unpatch(basicFields.FieldDate);
+ form.$buttons.find('.o_form_button_cancel').click();
+
+ assert.strictEqual($('.modal').length, 0,
+ "there should not be a Warning dialog");
+
form.destroy();
});
@@ -2822,13 +2860,15 @@ QUnit.module('basic_fields', {
assert.strictEqual(form.$('.o_datepicker_input').val(), '02/08/2017 12:00:00',
'the date time should be correct in edit mode');
- $('.o_datepicker_input').val('').trigger('input').trigger('change');
- assert.ok(!form.$('.o_datepicker_input').val());
+ $('.o_datepicker_input').val('').trigger('input').trigger('change').trigger('focusout');
+ assert.strictEqual(form.$('.o_datepicker_input').val(), '',
+ "should have an empty input");
// save
form.$buttons.find('.o_form_button_save').click();
assert.strictEqual(form.$('.o_field_date').text(), '',
'the selected date should be displayed after saving');
+
form.destroy();
});
diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js
index 6ca27233835..2cc51ac6d19 100644
--- a/addons/web/static/tests/helpers/test_utils.js
+++ b/addons/web/static/tests/helpers/test_utils.js
@@ -809,7 +809,7 @@ var patches = {};
* @param {Class|Object} target
* @param {Object} props
*/
-function patch (target, props) {
+function patch(target, props) {
var patchID = _.uniqueId('patch_');
target.__patchID = patchID;
patches[patchID] = {
@@ -887,6 +887,15 @@ function unpatch(target) {
delete target.__patchID;
}
+/**
+ * Opens the datepicker of a given element.
+ *
+ * @param {jQuery} $datepickerEl element to which a datepicker is attached
+ */
+function openDatepicker($datepickerEl) {
+ $datepickerEl.find('.o_datepicker_input').trigger('focus.datetimepicker');
+}
+
// Loading static files cannot be properly simulated when their real content is
// really needed. This is the case for static XML files so we load them here,
// before starting the qunit test suite.
@@ -913,6 +922,7 @@ return $.when(
fieldsViewGet: fieldsViewGet,
intercept: intercept,
observe: observe,
+ openDatepicker: openDatepicker,
patch: patch,
patchDate: patchDate,
removeSrcAttribute: removeSrcAttribute,
diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js
index 54ecdbe07e7..7ed0c3914ee 100644
--- a/addons/web/static/tests/views/calendar_tests.js
+++ b/addons/web/static/tests/views/calendar_tests.js
@@ -649,14 +649,14 @@ QUnit.module('Views', {
"should display the datetime from the date with the timezone");
// use datepicker to enter a date: 12/13/2016 08:00:00
- $('.o_field_widget[name="start"] input').trigger('click');
+ testUtils.openDatepicker($('.o_field_widget[name="start"].o_datepicker'));
$('.bootstrap-datetimepicker-widget .picker-switch a[data-action="togglePicker"]').trigger('click');
$('.bootstrap-datetimepicker-widget .timepicker .timepicker-hour').trigger('click');
$('.bootstrap-datetimepicker-widget .timepicker-hours td.hour:contains(08)').trigger('click');
$('.bootstrap-datetimepicker-widget .picker-switch a[data-action="close"]').trigger('click');
// use datepicker to enter a date: 12/13/2016 10:00:00
- $('.o_field_widget[name="stop"] input').trigger('click');
+ testUtils.openDatepicker($('.o_field_widget[name="stop"].o_datepicker'));
$('.bootstrap-datetimepicker-widget .picker-switch a[data-action="togglePicker"]').trigger('click');
$('.bootstrap-datetimepicker-widget .timepicker .timepicker-hour').trigger('click');
$('.bootstrap-datetimepicker-widget .timepicker-hours td.hour:contains(10)').trigger('click');
@@ -898,14 +898,14 @@ QUnit.module('Views', {
"should display the datetime from the date with the timezone");
// use datepicker to enter a date: 12/13/2016 08:00:00
- $('.o_field_widget[name="start"] input').trigger('click');
+ testUtils.openDatepicker($('.o_field_widget[name="start"].o_datepicker'));
$('.bootstrap-datetimepicker-widget .picker-switch a[data-action="togglePicker"]').trigger('click');
$('.bootstrap-datetimepicker-widget .timepicker .timepicker-hour').trigger('click');
$('.bootstrap-datetimepicker-widget .timepicker-hours td.hour:contains(08)').trigger('click');
$('.bootstrap-datetimepicker-widget .picker-switch a[data-action="close"]').trigger('click');
// use datepicker to enter a date: 12/13/2016 10:00:00
- $('.o_field_widget[name="stop"] input').trigger('click');
+ testUtils.openDatepicker($('.o_field_widget[name="stop"].o_datepicker'));
$('.bootstrap-datetimepicker-widget .picker-switch a[data-action="togglePicker"]').trigger('click');
$('.bootstrap-datetimepicker-widget .timepicker .timepicker-hour').trigger('click');
$('.bootstrap-datetimepicker-widget .timepicker-hours td.hour:contains(10)').trigger('click');
diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js
index 0cfc37daff5..896a9108b6a 100644
--- a/addons/web/static/tests/views/form_tests.js
+++ b/addons/web/static/tests/views/form_tests.js
@@ -2280,8 +2280,12 @@ QUnit.module('Views', {
},
});
- form.$buttons.find('.o_form_button_cancel').click();
- form.$buttons.find('.o_form_button_save').click();
+ // focus the buttons before clicking on them to precisely reproduce what
+ // really happens (mostly because the datepicker lib need that focus
+ // event to properly focusout the input, otherwise it crashes later on
+ // when the 'blur' event is triggered by the re-rendering)
+ form.$buttons.find('.o_form_button_cancel').focus().click();
+ form.$buttons.find('.o_form_button_save').focus().click();
assert.strictEqual(form.$('span:contains(2017)').length, 1,
"should have a span with the year somewhere");
diff --git a/addons/web/static/tests/widgets/domain_selector_tests.js b/addons/web/static/tests/widgets/domain_selector_tests.js
index 20ea5d86297..a1369090f51 100644
--- a/addons/web/static/tests/widgets/domain_selector_tests.js
+++ b/addons/web/static/tests/widgets/domain_selector_tests.js
@@ -167,7 +167,8 @@ QUnit.module('DomainSelector', {
assert.strictEqual($datepicker.length, 1,
"there should be a datepicker");
- var val = $datepicker.find('input').focus().click().val();
+ var val = $datepicker.find('input').val();
+ testUtils.openDatepicker($datepicker);
$('.bootstrap-datetimepicker-widget :not(.today)[data-action="selectDay"]').click();
assert.notEqual(domainSelector.$(".o_datepicker:visible input").val(), val,
"datepicker value should have changed");