diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js index 063ef268d9b..1e7aa1e0639 100644 --- a/addons/web/static/src/js/views/form/form_controller.js +++ b/addons/web/static/src/js/views/form/form_controller.js @@ -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); } }, /** diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index e8302e87889..ae80649cd61 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -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'); diff --git a/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js b/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js index 9b94022c349..bb21df5ccfb 100644 --- a/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js +++ b/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js @@ -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'); diff --git a/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js b/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js index ab72d7d5764..d6fb446359f 100644 --- a/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js +++ b/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js @@ -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'); diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index a57f7009e8d..f01f78a2912 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -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 () { diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index 79ef2a7919d..6bc5d34387e 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -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: ` +
+ + + +
`, + 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