[FIX] web: no quick edit on mouse text selection

Do not trigger quick edit when we are selection text with mouse:

- click and holding to select text
- multiple clicks selecting text

Since click on an editable field now change the edit mode, we could no
longer select the text.

With this change, quick edit is delayed by a given delay and if the edit
mode is not enabled if:

- there is a subsequent click within the delay
- there is a text selection when the click event is handled

closes #68023

X-original-commit: 2939ea4dd4736e659cc3e4df2226a3b08ac8ec48
This commit is contained in:
Nicolas Lempereur
2021-03-22 16:59:38 +00:00
parent 0de75b228a
commit 0e004e1b7c
6 changed files with 112 additions and 5 deletions
@@ -20,6 +20,10 @@ var FormController = BasicController.extend({
form_dialog_discarded: '_onFormDialogDiscarded',
quick_edit: '_onQuickEdit',
}),
/**
* Time between multiple clicks (used to detect double click text selection)
*/
multiClickTime: 350,
/**
* @override
*
@@ -35,6 +39,9 @@ var FormController = BasicController.extend({
this.defaultButtons = params.defaultButtons;
this.hasActionMenus = params.hasActionMenus;
this.toolbarActions = params.toolbarActions || {};
// Quick edit is delayed by `multiClickTime` time. If a subsequent click
// happens within this time, the quick edit is aborted.
this.quickEditTimeout = undefined;
},
/**
* Called each time the form view is attached into the DOM
@@ -691,9 +698,14 @@ var FormController = BasicController.extend({
*/
_onQuickEdit: async function (ev) {
ev.stopPropagation();
if (this.activeActions.edit) {
await this._setEditMode();
this.renderer.quickEdit(ev.data);
clearTimeout(this.quickEditTimeout);
if (this.activeActions.edit && !window.getSelection().toString()) {
this.quickEditTimeout = setTimeout(async () => {
if (!this.isDestroyed()) {
await this._setEditMode();
this.renderer.quickEdit(ev.data);
}
}, this.multiClickTime);
}
},
/**
@@ -6,6 +6,7 @@ var basicFields = require('web.basic_fields');
var concurrency = require('web.concurrency');
var config = require('web.config');
var core = require('web.core');
var FormController = require('web.FormController');
var FormView = require('web.FormView');
var KanbanView = require('web.KanbanView');
var ListView = require('web.ListView');
@@ -150,7 +151,14 @@ QUnit.module('basic_fields', {
}]
},
};
}
testUtils.mock.patch(FormController, {
'multiClickTime': 0,
});
},
afterEach() {
testUtils.mock.unpatch(FormController);
},
}, function () {
QUnit.module('DebouncedField');
@@ -2,6 +2,7 @@ odoo.define('web.field_many_to_one_tests', function (require) {
"use strict";
var BasicModel = require('web.BasicModel');
var FormController = require('web.FormController');
var FormView = require('web.FormView');
var ListView = require('web.ListView');
var relationalFields = require('web.relational_fields');
@@ -151,6 +152,13 @@ QUnit.module('fields', {}, function () {
}]
},
};
testUtils.mock.patch(FormController, {
'multiClickTime': 0,
});
},
afterEach: function () {
testUtils.mock.unpatch(FormController);
},
}, function () {
QUnit.module('FieldMany2One');
@@ -5,6 +5,7 @@ var AbstractField = require('web.AbstractField');
var AbstractStorageService = require('web.AbstractStorageService');
const ControlPanel = require('web.ControlPanel');
const fieldRegistry = require('web.field_registry');
var FormController = require('web.FormController');
var FormView = require('web.FormView');
var KanbanRecord = require('web.KanbanRecord');
var ListRenderer = require('web.ListRenderer');
@@ -163,7 +164,14 @@ QUnit.module('fields', {}, function () {
}]
},
};
}
testUtils.mock.patch(FormController, {
'multiClickTime': 0,
});
},
afterEach: function () {
testUtils.mock.unpatch(FormController);
},
}, function () {
QUnit.module('FieldOne2Many');
@@ -2,6 +2,7 @@ odoo.define('web.relational_fields_tests', function (require) {
"use strict";
var AbstractStorageService = require('web.AbstractStorageService');
var FormController = require('web.FormController');
var FormView = require('web.FormView');
var ListView = require('web.ListView');
var RamStorage = require('web.RamStorage');
@@ -169,6 +170,13 @@ QUnit.module('relational_fields', {
onchanges: {},
},
};
testUtils.mock.patch(FormController, {
'multiClickTime': 0,
});
},
afterEach: function () {
testUtils.mock.unpatch(FormController);
},
}, function () {
@@ -8,6 +8,7 @@ var concurrency = require('web.concurrency');
var core = require('web.core');
var fieldRegistry = require('web.field_registry');
const fieldRegistryOwl = require('web.field_registry_owl');
const FormController = require('web.FormController');
const FormRenderer = require('web.FormRenderer');
var FormView = require('web.FormView');
var mixins = require('web.mixins');
@@ -151,6 +152,13 @@ QUnit.module('Views', {
type: 'ir.actions.act_window',
views: [[false, 'kanban'], [false, 'form']],
}];
testUtils.mock.patch(FormController, {
'multiClickTime': 0,
});
},
afterEach: function () {
testUtils.mock.unpatch(FormController);
},
}, function () {
@@ -10674,6 +10682,7 @@ QUnit.module('Views', {
assert.containsNone(document.body, '.modal');
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
await testUtils.nextTick(); // wait for quick edit
assert.containsOnce(form, '.o_form_view.o_form_editable',
'should switch into edit mode');
@@ -11098,6 +11107,60 @@ QUnit.module('Views', {
form.destroy();
});
QUnit.test('Quick Edition: selecting text of quick editable field', async function (assert) {
assert.expect(5);
const MULTI_CLICK_TIME = 50;
testUtils.mock.patch(FormController, {
'multiClickTime': MULTI_CLICK_TIME,
});
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<group>
<field name="display_name"/>
</group>
</form>`,
res_id: 1,
});
assert.containsOnce(form, '.o_form_view.o_form_readonly');
// text selected by holding and dragging doesn't start quick edit
window.getSelection().removeAllRanges();
const range = document.createRange();
await range.selectNode(form.$('.o_field_widget[name="display_name"]')[0]);
window.getSelection().addRange(range);
await testUtils.dom.click(form.$('.o_field_widget[name="display_name"]'));
await concurrency.delay(MULTI_CLICK_TIME);
assert.containsOnce(form, '.o_form_view.o_form_readonly');
// double click selecting text doesn't start quick edit
window.getSelection().removeAllRanges();
await testUtils.dom.click(form.$('.o_field_widget[name="display_name"]'));
range.selectNode(form.$('.o_field_widget[name="display_name"]')[0]);
window.getSelection().addRange(range);
await testUtils.dom.click(form.$('.o_field_widget[name="display_name"]'));
await concurrency.delay(MULTI_CLICK_TIME);
assert.containsOnce(form, '.o_form_view.o_form_readonly');
// quick edit happens after timeout
window.getSelection().removeAllRanges();
await testUtils.dom.click(form.$('.o_field_widget[name="display_name"]'));
await testUtils.nextTick();
assert.containsOnce(form, '.o_form_view.o_form_readonly');
await concurrency.delay(MULTI_CLICK_TIME);
assert.containsOnce(form, '.o_form_view.o_form_editable');
// FormController unpatch done in afterEach
form.destroy();
});
QUnit.test("attach callbacks with long processing in __renderView", async function (assert) {
/**
* The main use case of this test is discuss, in which the FormRenderer