@perspective-dev/viewer-datagrid 4.4.0 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/cdn/perspective-viewer-datagrid.js +4 -22
  2. package/dist/cdn/perspective-viewer-datagrid.js.map +4 -4
  3. package/dist/css/perspective-viewer-datagrid-toolbar.css +1 -1
  4. package/dist/css/perspective-viewer-datagrid.css +1 -1
  5. package/dist/esm/color_utils.d.ts +22 -0
  6. package/dist/esm/custom_elements/datagrid.d.ts +16 -21
  7. package/dist/esm/data_listener/format_cell.d.ts +1 -1
  8. package/dist/esm/data_listener/formatter_cache.d.ts +1 -1
  9. package/dist/esm/data_listener/index.d.ts +3 -2
  10. package/dist/esm/event_handlers/click/edit_click.d.ts +3 -2
  11. package/dist/esm/event_handlers/click.d.ts +4 -6
  12. package/dist/esm/event_handlers/dispatch_click.d.ts +3 -2
  13. package/dist/esm/event_handlers/expand_collapse.d.ts +1 -1
  14. package/dist/esm/event_handlers/focus.d.ts +4 -5
  15. package/dist/esm/event_handlers/header_click.d.ts +5 -3
  16. package/dist/esm/event_handlers/keydown/edit_keydown.d.ts +3 -4
  17. package/dist/esm/event_handlers/select_region.d.ts +3 -1
  18. package/dist/esm/event_handlers/sort.d.ts +8 -7
  19. package/dist/esm/model/create.d.ts +1 -1
  20. package/dist/esm/model/meta_columns.d.ts +1 -0
  21. package/dist/esm/perspective-viewer-datagrid.js +3 -3
  22. package/dist/esm/perspective-viewer-datagrid.js.map +4 -4
  23. package/dist/esm/plugin/activate.d.ts +1 -1
  24. package/dist/esm/plugin/column_config_schema.d.ts +31 -0
  25. package/dist/esm/style_handlers/body.d.ts +3 -3
  26. package/dist/esm/style_handlers/column_header.d.ts +4 -3
  27. package/dist/esm/style_handlers/consolidated.d.ts +3 -47
  28. package/dist/esm/style_handlers/editable.d.ts +3 -2
  29. package/dist/esm/style_handlers/focus.d.ts +4 -4
  30. package/dist/esm/style_handlers/group_header.d.ts +1 -1
  31. package/dist/esm/style_handlers/table_cell/boolean.d.ts +1 -1
  32. package/dist/esm/style_handlers/table_cell/cell_flash.d.ts +1 -1
  33. package/dist/esm/style_handlers/table_cell/datetime.d.ts +1 -1
  34. package/dist/esm/style_handlers/table_cell/numeric.d.ts +1 -1
  35. package/dist/esm/style_handlers/table_cell/row_header.d.ts +1 -1
  36. package/dist/esm/style_handlers/table_cell/string.d.ts +1 -1
  37. package/dist/esm/style_handlers/types.d.ts +0 -4
  38. package/dist/esm/types.d.ts +10 -17
  39. package/package.json +2 -4
  40. package/src/css/regular_table.css +87 -31
  41. package/src/css/row-hover.css +20 -7
  42. package/src/css/toolbar.css +11 -0
  43. package/src/ts/color_utils.ts +181 -16
  44. package/src/ts/custom_elements/datagrid.ts +70 -56
  45. package/src/ts/custom_elements/toolbar.ts +4 -5
  46. package/src/ts/data_listener/format_cell.ts +28 -9
  47. package/src/ts/data_listener/format_tree_header.ts +2 -2
  48. package/src/ts/data_listener/formatter_cache.ts +9 -96
  49. package/src/ts/data_listener/index.ts +13 -11
  50. package/src/ts/event_handlers/click/edit_click.ts +10 -6
  51. package/src/ts/event_handlers/click.ts +39 -68
  52. package/src/ts/event_handlers/dispatch_click.ts +27 -25
  53. package/src/ts/event_handlers/expand_collapse.ts +11 -8
  54. package/src/ts/event_handlers/focus.ts +38 -35
  55. package/src/ts/event_handlers/header_click.ts +107 -62
  56. package/src/ts/event_handlers/keydown/edit_keydown.ts +60 -54
  57. package/src/ts/event_handlers/select_region.ts +153 -131
  58. package/src/ts/event_handlers/sort.ts +20 -25
  59. package/src/ts/get_cell_config.ts +10 -3
  60. package/src/ts/model/column_overrides.ts +16 -9
  61. package/src/ts/model/create.ts +68 -55
  62. package/src/ts/{event_handlers/deselect_all.ts → model/meta_columns.ts} +33 -14
  63. package/src/ts/model/toolbar.ts +33 -8
  64. package/src/ts/plugin/activate.ts +122 -92
  65. package/src/ts/plugin/column_config_schema.ts +187 -0
  66. package/src/ts/plugin/draw.ts +1 -0
  67. package/src/ts/plugin/restore.ts +6 -2
  68. package/src/ts/plugin/save.ts +2 -5
  69. package/src/ts/style_handlers/body.ts +48 -51
  70. package/src/ts/style_handlers/column_header.ts +22 -21
  71. package/src/ts/style_handlers/consolidated.ts +23 -123
  72. package/src/ts/style_handlers/editable.ts +16 -10
  73. package/src/ts/style_handlers/focus.ts +7 -5
  74. package/src/ts/style_handlers/group_header.ts +13 -6
  75. package/src/ts/style_handlers/table_cell/boolean.ts +3 -3
  76. package/src/ts/style_handlers/table_cell/cell_flash.ts +11 -11
  77. package/src/ts/style_handlers/table_cell/datetime.ts +3 -3
  78. package/src/ts/style_handlers/table_cell/numeric.ts +24 -25
  79. package/src/ts/style_handlers/table_cell/row_header.ts +2 -2
  80. package/src/ts/style_handlers/table_cell/string.ts +20 -18
  81. package/src/ts/style_handlers/types.ts +0 -10
  82. package/src/ts/types.ts +28 -20
  83. package/dist/esm/event_handlers/deselect_all.d.ts +0 -5
  84. package/dist/esm/event_handlers/row_select_click.d.ts +0 -4
  85. package/dist/esm/plugin/column_style_controls.d.ts +0 -28
  86. package/src/ts/event_handlers/row_select_click.ts +0 -92
  87. package/src/ts/plugin/column_style_controls.ts +0 -76
@@ -10,14 +10,10 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
- import { RegularTableElement } from "regular-table";
14
- import type {
15
- DatagridModel,
16
- PerspectiveViewerElement,
17
- SortRotationOrder,
18
- SortTerm,
19
- } from "../types.js";
20
- import { SortDir } from "@perspective-dev/client";
13
+ import type { RegularTableElement } from "regular-table";
14
+ import type { DatagridModel, SortRotationOrder, SortTerm } from "../types.js";
15
+ import type { SortDir } from "@perspective-dev/client";
16
+ import type { HTMLPerspectiveViewerElement } from "@perspective-dev/viewer";
21
17
 
22
18
  const ROW_SORT_ORDER: SortRotationOrder = {
23
19
  desc: "asc",
@@ -31,22 +27,21 @@ const ROW_COL_SORT_ORDER: SortRotationOrder = {
31
27
  asc: undefined,
32
28
  "desc abs": "asc abs",
33
29
  "asc abs": undefined,
34
- // "col desc": "col asc",
35
- // "col asc": undefined,
36
- // "col desc abs": "col asc abs",
37
- // "col asc abs": undefined,
38
30
  };
39
31
 
40
32
  export async function sortHandler(
41
- this: DatagridModel,
33
+ model: DatagridModel,
42
34
  regularTable: RegularTableElement,
43
- viewer: PerspectiveViewerElement,
35
+ viewer: HTMLPerspectiveViewerElement,
44
36
  event: MouseEvent,
45
37
  target: HTMLElement,
46
38
  ): Promise<void> {
47
39
  const meta = regularTable.getMeta(target);
48
- if (!meta?.column_header) return;
49
- const column_name = meta.column_header[this._config.split_by.length];
40
+ if (!meta?.column_header) {
41
+ return;
42
+ }
43
+
44
+ const column_name = meta.column_header[model._config.split_by.length];
50
45
  const sort_method =
51
46
  event.ctrlKey ||
52
47
  (event as MouseEvent & { metaKey?: boolean }).metaKey ||
@@ -55,22 +50,22 @@ export async function sortHandler(
55
50
  : override_sort;
56
51
 
57
52
  const abs = event.shiftKey;
58
- const sort = sort_method.call(this, `${column_name}`, abs);
53
+ const sort = sort_method(model, `${column_name}`, abs);
59
54
  await viewer.restore({ sort });
60
55
  }
61
56
 
62
57
  export function append_sort(
63
- this: DatagridModel,
58
+ model: DatagridModel,
64
59
  column_name: string,
65
60
  abs: boolean,
66
61
  ): SortTerm[] {
67
62
  const sort: SortTerm[] = [];
68
63
  let found = false;
69
- for (const sort_term of this._config.sort) {
64
+ for (const sort_term of model._config.sort) {
70
65
  const [_column_name, _sort_dir] = sort_term;
71
66
  if (_column_name === column_name) {
72
67
  found = true;
73
- const term = create_sort.call(this, column_name, _sort_dir, abs);
68
+ const term = create_sort(model, column_name, _sort_dir, abs);
74
69
  if (term) {
75
70
  sort.push(term);
76
71
  }
@@ -87,13 +82,13 @@ export function append_sort(
87
82
  }
88
83
 
89
84
  export function override_sort(
90
- this: DatagridModel,
85
+ model: DatagridModel,
91
86
  column_name: string,
92
87
  abs: boolean,
93
88
  ): SortTerm[] {
94
- for (const [_column_name, _sort_dir] of this._config.sort) {
89
+ for (const [_column_name, _sort_dir] of model._config.sort) {
95
90
  if (_column_name === column_name) {
96
- const sort = create_sort.call(this, column_name, _sort_dir, abs);
91
+ const sort = create_sort(model, column_name, _sort_dir, abs);
97
92
  return sort ? [sort] : [];
98
93
  }
99
94
  }
@@ -102,12 +97,12 @@ export function override_sort(
102
97
  }
103
98
 
104
99
  export function create_sort(
105
- this: DatagridModel,
100
+ model: DatagridModel,
106
101
  column_name: string,
107
102
  sort_dir: SortDir | undefined,
108
103
  _abs: boolean,
109
104
  ): SortTerm | undefined {
110
- const is_col_sortable = this._config.split_by.length > 0;
105
+ const is_col_sortable = model._config.split_by.length > 0;
111
106
  const order = is_col_sortable ? ROW_COL_SORT_ORDER : ROW_SORT_ORDER;
112
107
  const inc_sort_dir: SortDir | undefined = sort_dir
113
108
  ? order[sort_dir]
@@ -12,6 +12,7 @@
12
12
 
13
13
  import type { View, ViewConfig, Filter, Scalar } from "@perspective-dev/client";
14
14
  import type { CellConfigResult } from "./types.js";
15
+ import { isMetaColumn } from "./model/meta_columns.js";
15
16
 
16
17
  interface ModelWithViewAndConfig {
17
18
  _view: View;
@@ -42,8 +43,14 @@ export default async function getCellConfig(
42
43
  })
43
44
  .filter((x): x is Filter => x !== undefined);
44
45
 
45
- const column_index = group_by.length > 0 ? col_idx + 1 : col_idx;
46
- const column_paths = Object.keys(r[0])[column_index];
46
+ // Filter out *all* meta columns before indexing into the row's
47
+ // keys the DuckDB virtual server's JSON output now includes
48
+ // per-level `__ROW_PATH_<n>__` columns alongside the
49
+ // `__ROW_PATH__` sidecar, so the previous `+1` skip (which
50
+ // assumed exactly one leading meta column) lands on a meta key
51
+ // when group_by has multiple levels.
52
+ const user_keys = Object.keys(r[0]).filter((k) => !isMetaColumn(k));
53
+ const column_paths = user_keys[col_idx];
47
54
  const result: CellConfigResult = {
48
55
  row: r[0] as Record<string, unknown>,
49
56
  column_names: [],
@@ -60,7 +67,7 @@ export default async function getCellConfig(
60
67
  return pivot_value ? [pivot, "==", pivot_value] : undefined;
61
68
  })
62
69
  .filter((x): x is Filter => x !== undefined)
63
- .filter(([, , value]) => value !== "__ROW_PATH__");
70
+ .filter(([, , value]) => !isMetaColumn(value as string));
64
71
  }
65
72
 
66
73
  const filter = _config.filter.concat(row_filters).concat(column_filters);
@@ -10,11 +10,7 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
- import type {
14
- ColumnOverrides,
15
- DatagridPluginElement,
16
- RegularTable,
17
- } from "../types.js";
13
+ import type { ColumnOverrides, DatagridPluginElement } from "../types.js";
18
14
 
19
15
  interface RegularTableWithOverrides {
20
16
  restoreColumnSizes(overrides: Record<number, number | undefined>): void;
@@ -46,7 +42,10 @@ export function restore_column_size_overrides(
46
42
  this._cached_column_sizes = old_sizes;
47
43
  }
48
44
 
49
- const overrides: Record<number, number | undefined> = {};
45
+ const regular_table = this.regular_table as RegularTableWithOverrides;
46
+ const overrides: Record<number, number | undefined> = {
47
+ ...regular_table.saveColumnSizes(),
48
+ };
50
49
  const { group_by } = this.model!._config;
51
50
  const tree_header_offset = group_by?.length > 0 ? group_by.length + 1 : 0;
52
51
 
@@ -57,15 +56,23 @@ export function restore_column_size_overrides(
57
56
  | undefined;
58
57
  } else {
59
58
  const index = this.model!._column_paths.indexOf(key);
59
+
60
+ // Skip keys that don't resolve to a known column — e.g. on the
61
+ // first draw after `activate`, `_column_paths` has not yet been
62
+ // populated by the data listener, so we leave any existing
63
+ // `regular-table` widths untouched rather than clobbering them
64
+ // with garbage indices.
65
+ if (index === -1) {
66
+ continue;
67
+ }
68
+
60
69
  overrides[index + tree_header_offset] = old_sizes[key] as
61
70
  | number
62
71
  | undefined;
63
72
  }
64
73
  }
65
74
 
66
- (this.regular_table as RegularTableWithOverrides).restoreColumnSizes(
67
- overrides,
68
- );
75
+ regular_table.restoreColumnSizes(overrides);
69
76
  }
70
77
 
71
78
  /**
@@ -10,9 +10,8 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
- import chroma from "chroma-js";
14
13
  import { createDataListener } from "../data_listener/index.js";
15
- import { blend, make_color_record } from "../color_utils.js";
14
+ import { blend, make_color_record, parseColor } from "../color_utils.js";
16
15
  import type {
17
16
  ColumnType,
18
17
  Table,
@@ -26,10 +25,42 @@ import {
26
25
  type Schema,
27
26
  type ElemFactory,
28
27
  type EditMode,
29
- type PerspectiveViewerElement,
30
- get_psp_type,
31
28
  } from "../types.js";
32
- import { CellMetadata } from "regular-table/dist/esm/types.js";
29
+ import type { HTMLPerspectiveViewerElement } from "@perspective-dev/viewer";
30
+
31
+ function arraysChanged<T>(a: T[], b: T[]): boolean {
32
+ if (a.length !== b.length) {
33
+ return true;
34
+ }
35
+
36
+ for (let i = 0; i < a.length; i++) {
37
+ if (a[i] !== b[i]) {
38
+ return true;
39
+ }
40
+ }
41
+
42
+ return false;
43
+ }
44
+
45
+ function nestedArraysChanged<T>(a: T[][], b: T[][]): boolean {
46
+ if (a.length !== b.length) {
47
+ return true;
48
+ }
49
+
50
+ for (let i = 0; i < a.length; i++) {
51
+ if (a[i].length !== b[i].length) {
52
+ return true;
53
+ }
54
+
55
+ for (let j = 0; j < a[i].length; j++) {
56
+ if (a[i][j] !== b[i][j]) {
57
+ return true;
58
+ }
59
+ }
60
+ }
61
+
62
+ return false;
63
+ }
33
64
 
34
65
  function get_rule(regular: HTMLElement, tag: string, def: string): string {
35
66
  const color = window.getComputedStyle(regular).getPropertyValue(tag).trim();
@@ -59,6 +90,7 @@ class ElemFactoryImpl implements ElemFactory {
59
90
  if (!this._elements[this._index]) {
60
91
  this._elements[this._index] = document.createElement(this._name);
61
92
  }
93
+
62
94
  const elem = this._elements[this._index];
63
95
  this._index += 1;
64
96
  return elem;
@@ -70,61 +102,33 @@ export async function createModel(
70
102
  regular: RegularTable,
71
103
  table: Table,
72
104
  view: View,
105
+ theme: string,
73
106
  extend: Partial<DatagridModel> = {},
74
107
  ): Promise<DatagridModel> {
75
108
  const config = (await view.get_config()) as ViewConfig;
76
109
  if (this?.model?._config) {
77
110
  const old = this.model._config;
78
- let group_by_changed = old.group_by.length !== config.group_by.length;
111
+ const group_by_changed = arraysChanged(old.group_by, config.group_by);
79
112
  const type_changed =
80
113
  (old.group_by.length === 0 || config.group_by.length === 0) &&
81
114
  group_by_changed;
82
115
 
83
- if (!group_by_changed) {
84
- for (const lvl in old.group_by) {
85
- group_by_changed ||= config.group_by[lvl] !== old.group_by[lvl];
86
- }
87
- }
88
-
89
- let split_by_changed = old.split_by.length !== config.split_by.length;
90
- if (!split_by_changed) {
91
- for (const lvl in old.split_by) {
92
- split_by_changed ||= config.split_by[lvl] !== old.split_by[lvl];
93
- }
94
- }
95
-
96
- let columns_changed = old.columns.length !== config.columns.length;
97
- if (!columns_changed) {
98
- for (const lvl in old.columns) {
99
- columns_changed ||= config.columns[lvl] !== old.columns[lvl];
100
- }
101
- }
102
-
103
- let filter_changed = old.filter.length !== config.filter.length;
104
- if (!filter_changed) {
105
- for (const lvl in old.filter) {
106
- for (const i in config.filter[lvl]) {
107
- filter_changed ||=
108
- config.filter[lvl][i as unknown as number] !==
109
- old.filter[lvl][i as unknown as number];
110
- }
111
- }
112
- }
116
+ const split_by_changed = arraysChanged(old.split_by, config.split_by);
117
+ const columns_changed = arraysChanged(old.columns, config.columns);
118
+ const filter_changed = nestedArraysChanged(
119
+ old.filter as unknown[][],
120
+ config.filter as unknown[][],
121
+ );
113
122
 
114
- let sort_changed = old.sort.length !== config.sort.length;
115
- if (!sort_changed) {
116
- for (const lvl in old.sort) {
117
- for (const i in config.sort[lvl]) {
118
- sort_changed ||=
119
- config.sort[lvl][i as unknown as number] !==
120
- old.sort[lvl][i as unknown as number];
121
- }
122
- }
123
- }
123
+ const sort_changed = nestedArraysChanged(
124
+ old.sort as unknown[][],
125
+ config.sort as unknown[][],
126
+ );
124
127
 
125
128
  const group_rollup_mode_changed =
126
129
  old.group_rollup_mode !== config.group_rollup_mode;
127
130
 
131
+ const theme_changed = this.model._theme !== theme;
128
132
  this._reset_scroll_top = group_by_changed;
129
133
  this._reset_scroll_left = split_by_changed;
130
134
  this._reset_select =
@@ -139,6 +143,7 @@ export async function createModel(
139
143
  split_by_changed ||
140
144
  group_by_changed ||
141
145
  columns_changed ||
146
+ theme_changed ||
142
147
  type_changed;
143
148
  }
144
149
 
@@ -148,12 +153,12 @@ export async function createModel(
148
153
  view.num_rows(),
149
154
  view.schema(),
150
155
  view.expression_schema(),
151
- (this.parentElement as PerspectiveViewerElement).getEditPort(),
156
+ (this.parentElement as HTMLPerspectiveViewerElement).getEditPort(),
152
157
  ]);
153
158
 
154
- const _plugin_background = chroma(
159
+ const _plugin_background = parseColor(
155
160
  get_rule(regular, "--psp--background-color", "#FFFFFF"),
156
- ).rgb();
161
+ );
157
162
 
158
163
  const _pos_fg_color = make_color_record(
159
164
  get_rule(regular, "--psp-datagrid--pos-cell--color", "#338DCD"),
@@ -187,10 +192,17 @@ export async function createModel(
187
192
  const _column_paths: string[] = [];
188
193
  const _is_editable: boolean[] = [];
189
194
  const _column_types: ColumnType[] = [];
195
+ let _edit_mode: EditMode = this._edit_mode || "READ_ONLY";
190
196
 
191
- const _edit_mode: EditMode = this._edit_mode || "READ_ONLY";
192
- this._edit_button!.dataset.editMode = _edit_mode;
197
+ if (
198
+ _edit_mode === "SELECT_ROW_TREE" &&
199
+ (config.group_by.length === 0 || config.group_rollup_mode === "flat")
200
+ ) {
201
+ _edit_mode = "READ_ONLY";
202
+ this._edit_mode = _edit_mode;
203
+ }
193
204
 
205
+ this._edit_button!.dataset.editMode = _edit_mode;
194
206
  const model: DatagridModel = Object.assign(extend, {
195
207
  _edit_port,
196
208
  _view: view,
@@ -208,6 +220,7 @@ export async function createModel(
208
220
  _neg_bg_color,
209
221
  _column_paths,
210
222
  _column_types,
223
+ _theme: theme,
211
224
  _is_editable,
212
225
  _edit_mode,
213
226
  _selection_state: {
@@ -219,15 +232,15 @@ export async function createModel(
219
232
  }),
220
233
  _series_color_map: new Map<string, string>(),
221
234
  _series_color_seed: new Map<string, number>(),
235
+
222
236
  // get_psp_type,
223
237
  _div_factory: extend._div_factory || new ElemFactoryImpl("div"),
224
238
  }) as DatagridModel;
225
239
 
226
240
  regular.setDataListener(
227
- createDataListener(this.parentElement as PerspectiveViewerElement).bind(
228
- model,
229
- regular,
230
- ) as any,
241
+ createDataListener(
242
+ this.parentElement as HTMLPerspectiveViewerElement,
243
+ ).bind(model, regular) as any,
231
244
  {
232
245
  virtual_mode: (window
233
246
  .getComputedStyle(regular)
@@ -10,19 +10,38 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
- import { RegularTableElement } from "regular-table";
14
- import type { PerspectiveViewerElement } from "../types.js";
13
+ /**
14
+ * Single-source-of-truth predicate for "this column is metadata, hide
15
+ * it from the user-visible grid." Five distinct names show up across
16
+ * the perspective protocol; older datagrid code filtered them by
17
+ * exact-match in three places, which only worked when the wire shape
18
+ * was the JSON-sidecar form (`__ROW_PATH__` array-of-arrays).
19
+ *
20
+ * The DuckDB virtual-server backend additionally surfaces per-level
21
+ * `__ROW_PATH_<n>__` columns directly in `to_columns_string` /
22
+ * `to_json` output (a side effect of keeping them in the frozen Arrow
23
+ * batch so that `to_arrow` consumers — viewer-charts via
24
+ * `with_typed_arrays` — see them inline, matching native
25
+ * `perspective-server`'s `to_arrow` behavior). Without this helper,
26
+ * those per-level columns slip through the legacy exact-match filters
27
+ * and render as user columns to the right of the grid.
28
+ *
29
+ * Match list:
30
+ * - `__ROW_PATH__` — JSON sidecar; used by the tree header.
31
+ * - `__ROW_PATH_<n>__` — per-level columns from the virtual server's
32
+ * inline-arrow shape.
33
+ * - `__ID__` — per-row identity column.
34
+ * - `__GROUPING_ID__` — internal SQL-rollup discriminator. The
35
+ * virtual server strips it server-side, but
36
+ * we cover it defensively in case a future
37
+ * backend leaks it.
38
+ *
39
+ * User columns named with leading/trailing double-underscores (e.g.
40
+ * `__user_col__`) are *not* matched — the regex requires the exact
41
+ * stems above.
42
+ */
43
+ const META_COLUMN_RE = /^__(?:ROW_PATH(?:_\d+)?|ID|GROUPING_ID)__$/;
15
44
 
16
- type SelectedRowsMap = Map<RegularTableElement, unknown[]>;
17
-
18
- export async function deselect_all_listener(
19
- regularTable: RegularTableElement,
20
- _viewer: PerspectiveViewerElement,
21
- selected_rows_map: SelectedRowsMap,
22
- ): Promise<void> {
23
- selected_rows_map.delete(regularTable);
24
- for (const td of regularTable.querySelectorAll("td,th")) {
25
- td.classList.toggle("psp-row-selected", false);
26
- td.classList.toggle("psp-row-subselected", false);
27
- }
45
+ export function isMetaColumn(name: string): boolean {
46
+ return META_COLUMN_RE.test(name);
28
47
  }
@@ -10,7 +10,12 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
- import type { DatagridPluginElement, EditMode } from "../types.js";
13
+ import { HTMLPerspectiveViewerElement } from "@perspective-dev/viewer";
14
+ import type {
15
+ DatagridModel,
16
+ DatagridPluginElement,
17
+ EditMode,
18
+ } from "../types.js";
14
19
 
15
20
  export const EDIT_MODES: readonly EditMode[] = [
16
21
  "READ_ONLY",
@@ -18,34 +23,54 @@ export const EDIT_MODES: readonly EditMode[] = [
18
23
  "SELECT_ROW",
19
24
  "SELECT_COLUMN",
20
25
  "SELECT_REGION",
26
+ "SELECT_ROW_TREE",
21
27
  ] as const;
22
28
 
29
+ function isSelectRowTreeAvailable(model?: DatagridModel): boolean {
30
+ if (!model) {
31
+ return false;
32
+ }
33
+
34
+ return (
35
+ model._config.group_by.length > 0 &&
36
+ model._config.group_rollup_mode !== "flat"
37
+ );
38
+ }
39
+
23
40
  export function toggle_edit_mode(
24
41
  this: DatagridPluginElement,
25
42
  mode?: EditMode,
26
43
  ): void {
27
44
  if (typeof mode === "undefined") {
28
- mode =
29
- EDIT_MODES[
30
- (EDIT_MODES.indexOf(this._edit_mode) + 1) % EDIT_MODES.length
31
- ];
45
+ let idx = EDIT_MODES.indexOf(this._edit_mode);
46
+ do {
47
+ idx = (idx + 1) % EDIT_MODES.length;
48
+ } while (
49
+ EDIT_MODES[idx] === "SELECT_ROW_TREE" &&
50
+ !isSelectRowTreeAvailable(this.model)
51
+ );
52
+
53
+ mode = EDIT_MODES[idx];
32
54
  }
33
55
 
34
- (this.parentElement as any)?.setSelection?.();
56
+ (this.parentElement as HTMLPerspectiveViewerElement)?.setSelection?.();
35
57
  this._edit_mode = mode;
36
58
  if (this.model) {
37
59
  this.model._edit_mode = mode;
60
+ this.model._tree_selection_id = undefined;
38
61
  this.model._selection_state = {
39
62
  selected_areas: [],
40
63
  dirty: true,
41
64
  };
42
65
  }
43
66
 
67
+ (this.parentElement as HTMLPerspectiveViewerElement)?.restore?.({
68
+ plugin_config: { edit_mode: mode },
69
+ });
70
+
44
71
  if (this._edit_button !== undefined) {
45
72
  this._edit_button.dataset.editMode = mode;
46
73
  }
47
-
48
- this.dataset.editMode = mode;
49
74
  }
50
75
 
51
76
  export function toggle_scroll_lock(