diff --git a/panel/models/tabulator.ts b/panel/models/tabulator.ts index e996d231fe..6befdceed4 100644 --- a/panel/models/tabulator.ts +++ b/panel/models/tabulator.ts @@ -312,6 +312,7 @@ export class DataTabulatorView extends HTMLBoxView { _updating_page: boolean = false _updating_sort: boolean = false _selection_updating: boolean = false + _last_selected_row: any = null _initializing: boolean _lastVerticalScrollbarTopPosition: number = 0 _lastHorizontalScrollbarLeftPosition: number = 0 @@ -785,7 +786,7 @@ export class DataTabulatorView extends HTMLBoxView { _expand_render(cell: any): string { const index = cell._cell.row.data._index const icon = this.model.expanded.indexOf(index) < 0 ? "►" : "▼" - return `${icon}` + return icon } _update_expand(cell: any): void { @@ -1233,44 +1234,25 @@ export class DataTabulatorView extends HTMLBoxView { const selected = this.model.source.selected const index: number = row._row.data._index - if (this.model.pagination === "remote") { - const includes = this.model.source.selected.indices.indexOf(index) == -1 - const flush = !(e.ctrlKey || e.metaKey || e.shiftKey) - if (e.shiftKey && selected.indices.length) { - const start = selected.indices[selected.indices.length-1] - if (index>start) { - for (let i = start; i<=index; i++) { - indices.push(i) - } - } else { - for (let i = start; i>=index; i--) { - indices.push(i) - } - } - } else { - indices.push(index) - } - this._selection_updating = true - this.model.trigger_event(new SelectionEvent(indices, includes, flush)) - this._selection_updating = false - return - } - if (e.ctrlKey || e.metaKey) { - indices = [...this.model.source.selected.indices] - } else if (e.shiftKey && selected.indices.length) { - const start = selected.indices[selected.indices.length-1] - if (index>start) { - for (let i = start; iindex; i--) { - indices.push(i) - } + indices = [...selected.indices] + } else if (e.shiftKey && this._last_selected_row) { + const rows = row._row.parent.getDisplayRows() + const start_idx = rows.indexOf(this._last_selected_row) + if (start_idx !== -1) { + const end_idx = rows.indexOf(row._row) + const reverse = start_idx > end_idx + const [start, end] = reverse ? [end_idx+1, start_idx+1] : [start_idx, end_idx] + indices = rows.slice(start, end).map((r: any) => r.data._index) + if (reverse) { indices = indices.reverse() } } } - if (indices.indexOf(index) < 0) { + const flush = !(e.ctrlKey || e.metaKey || e.shiftKey) + const includes = indices.includes(index) + const remote = this.model.pagination === "remote" + + // Toggle the index on or off (if remote we let Python do the toggling) + if (!includes || remote) { indices.push(index) } else { indices.splice(indices.indexOf(index), 1) @@ -1282,10 +1264,16 @@ export class DataTabulatorView extends HTMLBoxView { } } const filtered = this._filter_selected(indices) - this.tabulator.deselectRow() - this.tabulator.selectRow(filtered) + if (!remote) { + this.tabulator.deselectRow() + this.tabulator.selectRow(filtered) + } + this._last_selected_row = row._row this._selection_updating = true - selected.indices = filtered + if (!remote) { + selected.indices = filtered + } + this.model.trigger_event(new SelectionEvent(indices, !includes, flush)) this._selection_updating = false } diff --git a/panel/tests/ui/widgets/test_tabulator.py b/panel/tests/ui/widgets/test_tabulator.py index a358044a12..fd9dc06cd0 100644 --- a/panel/tests/ui/widgets/test_tabulator.py +++ b/panel/tests/ui/widgets/test_tabulator.py @@ -3497,6 +3497,42 @@ def contains_filter(df, pattern=None): wait_until(lambda: tbl.selection == [7], page) +@pytest.mark.parametrize('pagination', ['remote', 'local', None]) +def test_range_selection_on_sorted_data_downward(page, pagination): + df = pd.DataFrame({'a': [1, 3, 2, 4, 5, 6, 7, 8, 9], 'b': [6, 5, 6, 7, 7, 7, 7, 7, 7]}) + table = Tabulator(df, disabled=True, pagination=pagination) + + serve_component(page, table) + + page.locator('.tabulator-col-title-holder').nth(2).click() + + page.locator('.tabulator-row').nth(0).click() + + page.keyboard.down('Shift') + + page.locator('.tabulator-row').nth(1).click() + + wait_until(lambda: table.selection == [0, 2], page) + + +@pytest.mark.parametrize('pagination', ['remote', 'local', None]) +def test_range_selection_on_sorted_data_upward(page, pagination): + df = pd.DataFrame({'a': [1, 3, 2, 4, 5, 6, 7, 8, 9], 'b': [6, 5, 6, 7, 7, 7, 7, 7, 7]}) + table = Tabulator(df, disabled=True, pagination=pagination, page_size=3) + + serve_component(page, table) + + page.locator('.tabulator-col-title-holder').nth(2).click() + + page.locator('.tabulator-row').nth(1).click() + + page.keyboard.down('Shift') + + page.locator('.tabulator-row').nth(0).click() + + wait_until(lambda: table.selection == [2, 0], page) + + class Test_RemotePagination: @pytest.fixture(autouse=True) @@ -3516,6 +3552,7 @@ def check_selected(self, page, expected, ui_count=None): ui_count = len(expected) expect(page.locator('.tabulator-selected')).to_have_count(ui_count) + wait_until(lambda: self.widget.selection == expected, page) @contextmanager diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index 92b1750621..153358c905 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -1275,7 +1275,8 @@ def _cleanup(self, root: Model | None = None) -> None: def _process_event(self, event) -> None: if event.event_name == 'selection-change': - self._update_selection(event) + if self.pagination == 'remote': + self._update_selection(event) return event_col = self._renamed_cols.get(event.column, event.column)