From edd296da29eb35ee62e1dcbf826f3d5f2ca0bbed Mon Sep 17 00:00:00 2001 From: "Dardenne Florent (dafl)" Date: Fri, 26 Aug 2022 15:21:23 +0000 Subject: [PATCH] [IMP] web: list: select range of records with shift Before, in the list view, the user could not select a record range easily. He had to check/uncheck manually each checkbox in the range. Now, the user can select a range of records in list view by hitting its first record and and its last record while holding SHIFT for the second. It's also possible to do it with the keyboard: - pressing to select a record and to select a range of records. - pressing to extend to current checkbox task-2039894 Part-of: odoo/odoo#99050 --- .../static/src/views/list/list_renderer.js | 85 ++++++++- .../static/src/views/list/list_renderer.xml | 2 +- .../web/static/tests/views/list_view_tests.js | 161 +++++++++++++++++- 3 files changed, 240 insertions(+), 8 deletions(-) diff --git a/addons/web/static/src/views/list/list_renderer.js b/addons/web/static/src/views/list/list_renderer.js index 0fe4fa41f1b..2616fcb90e3 100644 --- a/addons/web/static/src/views/list/list_renderer.js +++ b/addons/web/static/src/views/list/list_renderer.js @@ -174,6 +174,20 @@ export class ListRenderer extends Component { this.columnWidths = null; this.freezeColumnWidths(); }); + + useExternalListener(window, "keydown", (ev) => { + this.shiftKeyMode = ev.shiftKey; + }); + useExternalListener(window, "keyup", (ev) => { + this.shiftKeyMode = ev.shiftKey; + const hotkey = getActiveHotkey(ev); + if (hotkey === "shift") { + this.shiftKeyedRecord = undefined; + } + }); + useExternalListener(window, "blur", (ev) => { + this.shiftKeyMode = false; + }); } getActiveColumns(list) { @@ -1059,6 +1073,38 @@ export class ListRenderer extends Component { return null; } + expandCheckboxes(record, direction) { + const { records } = this.props.list; + const recordIndex = records.indexOf(record); + const shiftKeyedRecordIndex = records.indexOf(this.shiftKeyedRecord); + let nextRecord; + let isExpanding; + switch (direction) { + case "up": + if (recordIndex <= 0) { + return false; + } + nextRecord = records[recordIndex - 1]; + isExpanding = shiftKeyedRecordIndex > recordIndex - 1; + break; + case "down": + if (recordIndex === records.length - 1) { + return false; + } + nextRecord = records[recordIndex + 1]; + isExpanding = shiftKeyedRecordIndex < recordIndex + 1; + break; + } + + if (isExpanding) { + nextRecord.toggleSelection(this.shiftKeyedRecord.selected); + } else { + record.toggleSelection(false); + } + + return true; + } + applyCellKeydownMultiEditMode(hotkey, cell, group, record) { const { list } = this.props; const row = cell.parentElement; @@ -1387,6 +1433,25 @@ export class ListRenderer extends Component { toFocus = buttons[index - 1] || currentButton; } break; + case "shift+arrowdown": { + if (this.expandCheckboxes(record, "down")) { + toFocus = this.findFocusFutureCell(cell, cellIsInGroupRow, "down"); + } + break; + } + case "shift+arrowup": { + if (this.expandCheckboxes(record, "up")) { + toFocus = this.findFocusFutureCell(cell, cellIsInGroupRow, "up"); + } + break; + } + case "shift+space": + this.toggleRecordSelection(record); + toFocus = getElementToFocus(cell); + break; + case "shift": + this.shiftKeyedRecord = record; + break; case "enter": if (!group && !record) { return false; @@ -1518,10 +1583,28 @@ export class ListRenderer extends Component { } toggleRecordSelection(record) { - record.toggleSelection(); + if (this.shiftKeyMode && this.lastCheckedRecord) { + this.toggleRecordShiftSelection(record); + } else { + record.toggleSelection(); + } + this.lastCheckedRecord = record; this.props.list.selectDomain(false); } + toggleRecordShiftSelection(record) { + const { records } = this.props.list; + const recordIndex = records.indexOf(record); + const lastCheckedRecordIndex = records.indexOf(this.lastCheckedRecord); + const start = Math.min(recordIndex, lastCheckedRecordIndex); + const end = Math.max(recordIndex, lastCheckedRecordIndex); + const { selected } = record; + + for (let i = start; i <= end; i++) { + records[i].toggleSelection(!selected); + } + } + async toggleOptionalField(fieldName) { this.optionalActiveFields[fieldName] = !this.optionalActiveFields[fieldName]; this.state.columns = this.getActiveColumns(this.props.list); diff --git a/addons/web/static/src/views/list/list_renderer.xml b/addons/web/static/src/views/list/list_renderer.xml index a6a9edbbd66..d74ce34a9eb 100644 --- a/addons/web/static/src/views/list/list_renderer.xml +++ b/addons/web/static/src/views/list/list_renderer.xml @@ -217,7 +217,7 @@ t-on-touchend="() => this.onRowTouchEnd(record)" t-on-touchmove="() => this.onRowTouchMove(record)" > - + diff --git a/addons/web/static/tests/views/list_view_tests.js b/addons/web/static/tests/views/list_view_tests.js index 6cf46d1da7f..144ebc51b5b 100644 --- a/addons/web/static/tests/views/list_view_tests.js +++ b/addons/web/static/tests/views/list_view_tests.js @@ -259,6 +259,158 @@ QUnit.module("Views", (hooks) => { assert.isNotVisible(target.querySelector(".o_list_button_discard")); }); + QUnit.test("select record range with shift click", async function (assert) { + await makeView({ + type: "list", + resModel: "foo", + serverData, + arch: '', + }); + await click(target.querySelectorAll(".o_data_row .o_list_record_selector input")[0]); + assert.containsOnce(target.querySelector(".o_cp_buttons"), ".o_list_selection_box"); + assert.containsNone(target.querySelector(".o_list_selection_box"), ".o_list_select_domain"); + assert.strictEqual( + target.querySelector(".o_list_selection_box").textContent.trim(), + "1 selected" + ); + assert.strictEqual( + document.querySelectorAll(".o_data_row .o_list_record_selector input:checked").length, + 1 + ); + + // shift click the 4th record to have 0-1-2-3 toggled + await triggerEvents( + target.querySelectorAll(".o_data_row .o_list_record_selector input")[3], + null, + [["keydown", { key: "Shift", shiftKey: true }], "click"] + ); + + assert.strictEqual( + target.querySelector(".o_list_selection_box").textContent.trim(), + "4 selected" + ); + assert.strictEqual( + document.querySelectorAll(".o_data_row .o_list_record_selector input:checked").length, + 4 + ); + + // shift click the 3rd record to untoggle 2-3 + await triggerEvents( + target.querySelectorAll(".o_data_row .o_list_record_selector input")[2], + null, + [["keydown", { key: "Shift", shiftKey: true }], "click"] + ); + assert.strictEqual( + target.querySelector(".o_list_selection_box").textContent.trim(), + "2 selected" + ); + assert.strictEqual( + document.querySelectorAll(".o_data_row .o_list_record_selector input:checked").length, + 2 + ); + + // shift click the 1st record to untoggle 0-1 + await triggerEvents( + target.querySelectorAll(".o_data_row .o_list_record_selector input")[0], + null, + [["keydown", { key: "Shift", shiftKey: true }], "click"] + ); + assert.strictEqual( + document.querySelectorAll(".o_data_row .o_list_record_selector input:checked").length, + 0 + ); + }); + + QUnit.test("select record range with shift+space", async function (assert) { + await makeView({ + type: "list", + resModel: "foo", + serverData, + arch: '', + }); + + // Go to the first checkbox and check it + triggerHotkey("ArrowDown"); + triggerHotkey("ArrowDown"); + await nextTick(); + let checkbox = target.querySelector( + ".o_data_row:nth-child(1) .o_list_record_selector input" + ); + assert.strictEqual(document.activeElement, checkbox); + await click(checkbox); + assert.ok(checkbox.checked); + + // Go to the fourth checkbox and shift+space + triggerHotkey("ArrowDown"); + triggerHotkey("ArrowDown"); + triggerHotkey("ArrowDown"); + await nextTick(); + checkbox = target.querySelector(".o_data_row:nth-child(4) .o_list_record_selector input"); + assert.strictEqual(document.activeElement, checkbox); + assert.ok(!checkbox.checked); + await triggerEvents(document.activeElement, null, [ + ["keydown", { key: "Shift", shiftKey: true }], + ["keydown", { key: " ", shiftKey: true }], + ]); + // focus is on the input and not in the td cell + assert.strictEqual(document.activeElement.tagName, "INPUT"); + + // Check that all checkbox is checked + for (let i = 1; i < 5; i++) { + checkbox = target.querySelector( + `.o_data_row:nth-child(${i}) .o_list_record_selector input` + ); + assert.ok(checkbox.checked); + } + }); + + QUnit.test("expand range of checkbox with shift+arrow", async function (assert) { + await makeView({ + type: "list", + resModel: "foo", + serverData, + arch: '', + }); + + // Go to the first checkbox and check it + triggerHotkey("ArrowDown"); + triggerHotkey("ArrowDown"); + await nextTick(); + let checkbox = target.querySelector( + ".o_data_row:nth-child(1) .o_list_record_selector input" + ); + assert.strictEqual(document.activeElement, checkbox); + await click(checkbox); + assert.ok(checkbox.checked); + + // expand the checkbox with arrowdown + await triggerEvent(document.activeElement, null, "keydown", { + key: "Shift", + shiftKey: true, + }); + triggerHotkey("shift+ArrowDown"); + triggerHotkey("shift+ArrowDown"); + triggerHotkey("shift+ArrowDown"); + triggerHotkey("shift+ArrowUp"); + await nextTick(); + await triggerEvent(document.activeElement, null, "keyup", { + key: "Shift", + shiftKey: false, + }); + + checkbox = target.querySelector(".o_data_row:nth-child(3) .o_list_record_selector input"); + assert.strictEqual(document.activeElement, checkbox); + assert.ok(checkbox.checked); + + // Check that the three checkbox are checked + for (let i = 1; i < 4; i++) { + checkbox = target.querySelector( + `.o_data_row:nth-child(${i}) .o_list_record_selector input` + ); + assert.ok(checkbox.checked); + } + }); + QUnit.test("list with class", async function (assert) { await makeView({ type: "list", @@ -1993,8 +2145,7 @@ QUnit.module("Views", (hooks) => { type: "list", resModel: "foo", serverData, - arch: - '', + arch: '', mockRPC: async (route, args) => { if (args.method === "write") { assert.step(`write ${args.args[0]}`); @@ -2035,8 +2186,7 @@ QUnit.module("Views", (hooks) => { type: "list", resModel: "foo", serverData, - arch: - '', + arch: '', groupBy: ["bar"], mockRPC: async (route, args) => { if (args.method === "write") { @@ -3048,8 +3198,7 @@ QUnit.module("Views", (hooks) => { resModel: "foo", serverData, groupBy: ["m2o"], - arch: - '', + arch: '', }); const groupHeaders = target.querySelectorAll(".o_group_header"); assert.strictEqual(