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)