@gradio/dataframe 0.14.0 → 0.16.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 (44) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/Dataframe.stories.svelte +283 -7
  3. package/Index.svelte +22 -3
  4. package/dist/Index.svelte +18 -4
  5. package/dist/Index.svelte.d.ts +16 -0
  6. package/dist/shared/EditableCell.svelte +49 -7
  7. package/dist/shared/EditableCell.svelte.d.ts +1 -1
  8. package/dist/shared/Table.svelte +692 -411
  9. package/dist/shared/Table.svelte.d.ts +4 -0
  10. package/dist/shared/Toolbar.svelte +122 -30
  11. package/dist/shared/Toolbar.svelte.d.ts +4 -0
  12. package/dist/shared/VirtualTable.svelte +70 -26
  13. package/dist/shared/VirtualTable.svelte.d.ts +1 -0
  14. package/dist/shared/icons/FilterIcon.svelte +11 -0
  15. package/dist/shared/icons/FilterIcon.svelte.d.ts +16 -0
  16. package/dist/shared/icons/SortIcon.svelte +90 -0
  17. package/dist/shared/icons/SortIcon.svelte.d.ts +20 -0
  18. package/dist/shared/selection_utils.d.ts +30 -0
  19. package/dist/shared/selection_utils.js +139 -0
  20. package/dist/shared/types.d.ts +18 -0
  21. package/dist/shared/types.js +2 -0
  22. package/dist/shared/utils/menu_utils.d.ts +42 -0
  23. package/dist/shared/utils/menu_utils.js +58 -0
  24. package/dist/shared/utils/sort_utils.d.ts +7 -0
  25. package/dist/shared/utils/sort_utils.js +39 -0
  26. package/dist/shared/utils/table_utils.d.ts +12 -0
  27. package/dist/shared/utils/table_utils.js +148 -0
  28. package/package.json +8 -8
  29. package/shared/EditableCell.svelte +55 -7
  30. package/shared/Table.svelte +762 -478
  31. package/shared/Toolbar.svelte +125 -30
  32. package/shared/VirtualTable.svelte +73 -26
  33. package/shared/icons/FilterIcon.svelte +12 -0
  34. package/shared/icons/SortIcon.svelte +95 -0
  35. package/shared/selection_utils.ts +230 -0
  36. package/shared/types.ts +29 -0
  37. package/shared/utils/menu_utils.ts +115 -0
  38. package/shared/utils/sort_utils.test.ts +71 -0
  39. package/shared/utils/sort_utils.ts +55 -0
  40. package/shared/utils/table_utils.test.ts +114 -0
  41. package/shared/utils/table_utils.ts +206 -0
  42. package/dist/shared/table_utils.d.ts +0 -6
  43. package/dist/shared/table_utils.js +0 -27
  44. package/shared/table_utils.ts +0 -38
@@ -1,6 +1,5 @@
1
1
  <script lang="ts">
2
2
  import { afterUpdate, createEventDispatcher, tick, onMount } from "svelte";
3
- import { dsvFormat } from "d3-dsv";
4
3
  import { dequal } from "dequal/lite";
5
4
  import { Upload } from "@gradio/upload";
6
5
 
@@ -17,7 +16,28 @@
17
16
  } from "./utils";
18
17
  import CellMenu from "./CellMenu.svelte";
19
18
  import Toolbar from "./Toolbar.svelte";
20
- import { copy_table_data } from "./table_utils";
19
+ import SortIcon from "./icons/SortIcon.svelte";
20
+ import type { CellCoordinate, EditingState } from "./types";
21
+ import {
22
+ is_cell_selected,
23
+ handle_selection,
24
+ handle_delete_key,
25
+ should_show_cell_menu,
26
+ get_next_cell_coordinates,
27
+ get_range_selection,
28
+ move_cursor,
29
+ get_current_indices,
30
+ handle_click_outside as handle_click_outside_util,
31
+ select_column,
32
+ select_row,
33
+ calculate_selection_positions
34
+ } from "./selection_utils";
35
+ import {
36
+ copy_table_data,
37
+ get_max,
38
+ handle_file_upload,
39
+ sort_table_data
40
+ } from "./utils/table_utils";
21
41
 
22
42
  export let datatype: Datatype | Datatype[];
23
43
  export let label: string | null = null;
@@ -46,52 +66,86 @@
46
66
  export let show_fullscreen_button = false;
47
67
  export let show_copy_button = false;
48
68
  export let value_is_output = false;
69
+ export let max_chars: number | undefined = undefined;
70
+ export let show_search: "none" | "search" | "filter" = "none";
71
+ export let pinned_columns = 0;
72
+
73
+ let actual_pinned_columns = 0;
74
+ $: actual_pinned_columns =
75
+ pinned_columns && data?.[0]?.length
76
+ ? Math.min(pinned_columns, data[0].length)
77
+ : 0;
78
+
79
+ let selected_cells: CellCoordinate[] = [];
80
+ $: selected_cells = [...selected_cells];
81
+ let selected: CellCoordinate | false = false;
82
+ $: selected =
83
+ selected_cells.length > 0
84
+ ? selected_cells[selected_cells.length - 1]
85
+ : false;
49
86
 
50
- let selected: false | [number, number] = false;
51
- let clicked_cell: { row: number; col: number } | undefined = undefined;
52
87
  export let display_value: string[][] | null = null;
53
88
  export let styling: string[][] | null = null;
54
89
  let t_rect: DOMRectReadOnly;
90
+ let els: Record<
91
+ string,
92
+ { cell: null | HTMLTableCellElement; input: null | HTMLInputElement }
93
+ > = {};
94
+ let data_binding: Record<string, (typeof data)[0][0]> = {};
55
95
 
56
96
  const dispatch = createEventDispatcher<{
57
97
  change: DataframeValue;
58
98
  input: undefined;
59
99
  select: SelectData;
100
+ search: string | null;
60
101
  }>();
61
102
 
62
- let editing: false | [number, number] = false;
103
+ let editing: EditingState = false;
104
+ let clear_on_focus = false;
105
+ let header_edit: number | false = false;
106
+ let selected_header: number | false = false;
107
+ let active_cell_menu: {
108
+ row: number;
109
+ col: number;
110
+ x: number;
111
+ y: number;
112
+ } | null = null;
113
+ let active_header_menu: {
114
+ col: number;
115
+ x: number;
116
+ y: number;
117
+ } | null = null;
118
+ let is_fullscreen = false;
119
+ let dragging = false;
120
+ let copy_flash = false;
121
+
122
+ let color_accent_copied: string;
123
+ onMount(() => {
124
+ const color = getComputedStyle(document.documentElement)
125
+ .getPropertyValue("--color-accent")
126
+ .trim();
127
+ color_accent_copied = color + "40"; // 80 is 50% opacity in hex
128
+ document.documentElement.style.setProperty(
129
+ "--color-accent-copied",
130
+ color_accent_copied
131
+ );
132
+ });
63
133
 
64
134
  const get_data_at = (row: number, col: number): string | number =>
65
135
  data?.[row]?.[col]?.value;
66
136
 
67
- let last_selected: [number, number] | null = null;
68
-
69
- $: {
70
- if (selected !== false && !dequal(selected, last_selected)) {
71
- const [row, col] = selected;
72
- if (!isNaN(row) && !isNaN(col) && data[row]) {
73
- dispatch("select", {
74
- index: [row, col],
75
- value: get_data_at(row, col),
76
- row_value: data[row].map((d) => d.value)
77
- });
78
- last_selected = selected;
79
- }
80
- }
81
- }
82
-
83
- let els: Record<
84
- string,
85
- { cell: null | HTMLTableCellElement; input: null | HTMLInputElement }
86
- > = {};
87
-
88
- let data_binding: Record<string, (typeof data)[0][0]> = {};
89
-
90
137
  function make_id(): string {
91
138
  return Math.random().toString(36).substring(2, 15);
92
139
  }
93
140
 
94
- function make_headers(_head: Headers): HeadersWithIDs {
141
+ function make_headers(
142
+ _head: Headers,
143
+ col_count: [number, "fixed" | "dynamic"],
144
+ els: Record<
145
+ string,
146
+ { cell: null | HTMLTableCellElement; input: null | HTMLInputElement }
147
+ >
148
+ ): HeadersWithIDs {
95
149
  let _h = _head || [];
96
150
  if (col_count[1] === "fixed" && _h.length < col_count[0]) {
97
151
  const fill = Array(col_count[0] - _h.length)
@@ -124,8 +178,8 @@
124
178
  const data_row_length = _values.length;
125
179
  return Array(row_count[1] === "fixed" ? row_count[0] : data_row_length)
126
180
  .fill(0)
127
- .map((_, i) =>
128
- Array(
181
+ .map((_, i) => {
182
+ return Array(
129
183
  col_count[1] === "fixed"
130
184
  ? col_count[0]
131
185
  : data_row_length > 0
@@ -139,16 +193,16 @@
139
193
  const obj = { value: _values?.[i]?.[j] ?? "", id };
140
194
  data_binding[id] = obj;
141
195
  return obj;
142
- })
143
- );
196
+ });
197
+ });
144
198
  }
145
199
 
146
- let _headers = make_headers(headers);
200
+ let _headers = make_headers(headers, col_count, els);
147
201
  let old_headers: string[] = headers;
148
202
 
149
203
  $: {
150
204
  if (!dequal(headers, old_headers)) {
151
- _headers = make_headers(headers);
205
+ _headers = make_headers(headers, col_count, els);
152
206
  old_headers = JSON.parse(JSON.stringify(headers));
153
207
  }
154
208
  }
@@ -165,6 +219,8 @@
165
219
  let previous_data = data.map((row) => row.map((cell) => String(cell.value)));
166
220
 
167
221
  async function trigger_change(): Promise<void> {
222
+ // shouldnt trigger if data changed due to search
223
+ if (current_search_query) return;
168
224
  const current_headers = _headers.map((h) => h.value);
169
225
  const current_data = data.map((row) =>
170
226
  row.map((cell) => String(cell.value))
@@ -199,48 +255,9 @@
199
255
  if (direction === "asc") return "ascending";
200
256
  if (direction === "des") return "descending";
201
257
  }
202
-
203
258
  return "none";
204
259
  }
205
260
 
206
- function get_current_indices(id: string): [number, number] {
207
- return data.reduce(
208
- (acc, arr, i) => {
209
- const j = arr.reduce(
210
- (_acc, _data, k) => (id === _data.id ? k : _acc),
211
- -1
212
- );
213
-
214
- return j === -1 ? acc : [i, j];
215
- },
216
- [-1, -1]
217
- );
218
- }
219
-
220
- function move_cursor(
221
- key: "ArrowRight" | "ArrowLeft" | "ArrowDown" | "ArrowUp",
222
- current_coords: [number, number]
223
- ): void {
224
- const dir = {
225
- ArrowRight: [0, 1],
226
- ArrowLeft: [0, -1],
227
- ArrowDown: [1, 0],
228
- ArrowUp: [-1, 0]
229
- }[key];
230
-
231
- const i = current_coords[0] + dir[0];
232
- const j = current_coords[1] + dir[1];
233
-
234
- if (i < 0 && j <= 0) {
235
- selected_header = j;
236
- selected = false;
237
- } else {
238
- const is_data = data[i]?.[j];
239
- selected = is_data ? [i, j] : selected;
240
- }
241
- }
242
-
243
- let clear_on_focus = false;
244
261
  // eslint-disable-next-line complexity
245
262
  async function handle_keydown(event: KeyboardEvent): Promise<void> {
246
263
  if (selected_header !== false && header_edit === false) {
@@ -268,6 +285,50 @@
268
285
  break;
269
286
  }
270
287
  }
288
+
289
+ if (event.key === "Delete" || event.key === "Backspace") {
290
+ if (!editable) return;
291
+
292
+ if (editing) {
293
+ const [row, col] = editing;
294
+ const input_el = els[data[row][col].id].input;
295
+ if (input_el && input_el.selectionStart !== input_el.selectionEnd) {
296
+ return;
297
+ }
298
+ if (
299
+ event.key === "Delete" &&
300
+ input_el?.selectionStart !== input_el?.value.length
301
+ ) {
302
+ return;
303
+ }
304
+ if (event.key === "Backspace" && input_el?.selectionStart !== 0) {
305
+ return;
306
+ }
307
+ }
308
+
309
+ event.preventDefault();
310
+ if (selected_cells.length > 0) {
311
+ data = handle_delete_key(data, selected_cells);
312
+ dispatch("change", {
313
+ data: data.map((row) => row.map((cell) => cell.value)),
314
+ headers: _headers.map((h) => h.value),
315
+ metadata: null
316
+ });
317
+ if (!value_is_output) {
318
+ dispatch("input");
319
+ }
320
+ }
321
+ return;
322
+ }
323
+
324
+ if (event.key === "c" && (event.metaKey || event.ctrlKey)) {
325
+ event.preventDefault();
326
+ if (selected_cells.length > 0) {
327
+ await handle_copy();
328
+ }
329
+ return;
330
+ }
331
+
271
332
  if (!selected) {
272
333
  return;
273
334
  }
@@ -281,7 +342,31 @@
281
342
  case "ArrowUp":
282
343
  if (editing) break;
283
344
  event.preventDefault();
284
- move_cursor(event.key, [i, j]);
345
+ const next_coords = move_cursor(event.key, [i, j], data);
346
+ if (next_coords) {
347
+ if (event.shiftKey) {
348
+ selected_cells = get_range_selection(
349
+ selected_cells.length > 0 ? selected_cells[0] : [i, j],
350
+ next_coords
351
+ );
352
+ editing = false;
353
+ } else {
354
+ selected_cells = [next_coords];
355
+ if (editable) {
356
+ editing = next_coords;
357
+ clear_on_focus = false;
358
+ }
359
+ }
360
+ selected = next_coords;
361
+ } else if (
362
+ next_coords === false &&
363
+ event.key === "ArrowUp" &&
364
+ i === 0
365
+ ) {
366
+ selected_header = j;
367
+ selected = false;
368
+ editing = false;
369
+ }
285
370
  break;
286
371
 
287
372
  case "Escape":
@@ -290,59 +375,45 @@
290
375
  editing = false;
291
376
  break;
292
377
  case "Enter":
293
- if (!editable) break;
294
378
  event.preventDefault();
295
-
296
- if (event.shiftKey) {
297
- add_row(i);
298
- await tick();
299
-
300
- selected = [i + 1, j];
301
- } else {
302
- if (dequal(editing, [i, j])) {
303
- const cell_id = data[i][j].id;
304
- const input_el = els[cell_id].input;
305
- if (input_el) {
306
- data[i][j].value = input_el.value;
307
- }
308
- editing = false;
379
+ if (editable) {
380
+ if (event.shiftKey) {
381
+ add_row(i);
309
382
  await tick();
310
- selected = [i, j];
383
+ selected = [i + 1, j];
311
384
  } else {
312
- editing = [i, j];
385
+ if (dequal(editing, [i, j])) {
386
+ const cell_id = data[i][j].id;
387
+ const input_el = els[cell_id].input;
388
+ if (input_el) {
389
+ data[i][j].value = input_el.value;
390
+ }
391
+ editing = false;
392
+ await tick();
393
+ selected = [i, j];
394
+ } else {
395
+ editing = [i, j];
396
+ clear_on_focus = false;
397
+ }
313
398
  }
314
399
  }
315
-
316
- break;
317
- case "Backspace":
318
- if (!editable) break;
319
- if (!editing) {
320
- event.preventDefault();
321
- data[i][j].value = "";
322
- }
323
- break;
324
- case "Delete":
325
- if (!editable) break;
326
- if (!editing) {
327
- event.preventDefault();
328
- data[i][j].value = "";
329
- }
330
400
  break;
331
401
  case "Tab":
332
- let direction = event.shiftKey ? -1 : 1;
333
-
334
- let is_data_x = data[i][j + direction];
335
- let is_data_y =
336
- data?.[i + direction]?.[direction > 0 ? 0 : _headers.length - 1];
337
-
338
- if (is_data_x || is_data_y) {
339
- event.preventDefault();
340
- selected = is_data_x
341
- ? [i, j + direction]
342
- : [i + direction, direction > 0 ? 0 : _headers.length - 1];
343
- }
402
+ event.preventDefault();
344
403
  editing = false;
345
-
404
+ const next_cell = get_next_cell_coordinates(
405
+ [i, j],
406
+ data,
407
+ event.shiftKey
408
+ );
409
+ if (next_cell) {
410
+ selected_cells = [next_cell];
411
+ selected = next_cell;
412
+ if (editable) {
413
+ editing = next_cell;
414
+ clear_on_focus = false;
415
+ }
416
+ }
346
417
  break;
347
418
  default:
348
419
  if (!editable) break;
@@ -360,29 +431,26 @@
360
431
  let sort_direction: SortDirection | undefined;
361
432
  let sort_by: number | undefined;
362
433
 
363
- function handle_sort(col: number): void {
434
+ function handle_sort(col: number, direction: SortDirection): void {
364
435
  if (typeof sort_by !== "number" || sort_by !== col) {
365
- sort_direction = "asc";
436
+ sort_direction = direction;
366
437
  sort_by = col;
367
- } else {
368
- if (sort_direction === "asc") {
369
- sort_direction = "des";
370
- } else if (sort_direction === "des") {
371
- sort_direction = "asc";
438
+ } else if (sort_by === col) {
439
+ if (sort_direction === direction) {
440
+ sort_direction = undefined;
441
+ sort_by = undefined;
442
+ } else {
443
+ sort_direction = direction;
372
444
  }
373
445
  }
374
446
  }
375
447
 
376
- let header_edit: number | false;
377
-
378
- let select_on_focus = false;
379
- let selected_header: number | false = false;
380
448
  async function edit_header(i: number, _select = false): Promise<void> {
381
449
  if (!editable || col_count[1] !== "dynamic" || header_edit === i) return;
382
450
  selected = false;
451
+ selected_cells = [];
383
452
  selected_header = i;
384
453
  header_edit = i;
385
- select_on_focus = _select;
386
454
  }
387
455
 
388
456
  function end_header_edit(event: CustomEvent<KeyboardEvent>): void {
@@ -457,107 +525,14 @@
457
525
  }
458
526
 
459
527
  function handle_click_outside(event: Event): void {
460
- if (
461
- (active_cell_menu &&
462
- !(event.target as HTMLElement).closest(".cell-menu")) ||
463
- (active_header_menu &&
464
- !(event.target as HTMLElement).closest(".cell-menu"))
465
- ) {
528
+ if (handle_click_outside_util(event, parent)) {
529
+ editing = false;
530
+ selected_cells = [];
531
+ header_edit = false;
532
+ selected_header = false;
466
533
  active_cell_menu = null;
467
534
  active_header_menu = null;
468
535
  }
469
-
470
- const [trigger] = event.composedPath() as HTMLElement[];
471
- if (parent.contains(trigger)) {
472
- return;
473
- }
474
-
475
- clicked_cell = undefined;
476
- editing = false;
477
- selected = false;
478
- header_edit = false;
479
- selected_header = false;
480
- active_cell_menu = null;
481
- active_header_menu = null;
482
- }
483
-
484
- function guess_delimitaor(
485
- text: string,
486
- possibleDelimiters: string[]
487
- ): string[] {
488
- return possibleDelimiters.filter(weedOut);
489
-
490
- function weedOut(delimiter: string): boolean {
491
- var cache = -1;
492
- return text.split("\n").every(checkLength);
493
-
494
- function checkLength(line: string): boolean {
495
- if (!line) {
496
- return true;
497
- }
498
-
499
- var length = line.split(delimiter).length;
500
- if (cache < 0) {
501
- cache = length;
502
- }
503
- return cache === length && length > 1;
504
- }
505
- }
506
- }
507
-
508
- function data_uri_to_blob(data_uri: string): Blob {
509
- const byte_str = atob(data_uri.split(",")[1]);
510
- const mime_str = data_uri.split(",")[0].split(":")[1].split(";")[0];
511
-
512
- const ab = new ArrayBuffer(byte_str.length);
513
- const ia = new Uint8Array(ab);
514
-
515
- for (let i = 0; i < byte_str.length; i++) {
516
- ia[i] = byte_str.charCodeAt(i);
517
- }
518
-
519
- return new Blob([ab], { type: mime_str });
520
- }
521
-
522
- function blob_to_string(blob: Blob): void {
523
- const reader = new FileReader();
524
-
525
- function handle_read(e: ProgressEvent<FileReader>): void {
526
- if (!e?.target?.result || typeof e.target.result !== "string") return;
527
-
528
- const [delimiter] = guess_delimitaor(e.target.result, [",", "\t"]);
529
-
530
- const [head, ...rest] = dsvFormat(delimiter).parseRows(e.target.result);
531
-
532
- _headers = make_headers(
533
- col_count[1] === "fixed" ? head.slice(0, col_count[0]) : head
534
- );
535
-
536
- values = rest;
537
- reader.removeEventListener("loadend", handle_read);
538
- }
539
-
540
- reader.addEventListener("loadend", handle_read);
541
-
542
- reader.readAsText(blob);
543
- }
544
-
545
- let dragging = false;
546
-
547
- function get_max(
548
- _d: { value: any; id: string }[][]
549
- ): { value: any; id: string }[] {
550
- if (!_d || _d.length === 0 || !_d[0]) return [];
551
- let max = _d[0].slice();
552
- for (let i = 0; i < _d.length; i++) {
553
- for (let j = 0; j < _d[i].length; j++) {
554
- if (`${max[j].value}`.length < `${_d[i][j].value}`.length) {
555
- max[j] = _d[i][j];
556
- }
557
- }
558
- }
559
-
560
- return max;
561
536
  }
562
537
 
563
538
  $: max = get_max(data);
@@ -568,16 +543,25 @@
568
543
  let table: HTMLTableElement;
569
544
 
570
545
  function set_cell_widths(): void {
571
- const widths = cells.map((el, i) => {
572
- return el?.clientWidth || 0;
573
- });
546
+ const widths = cells.map((el) => el?.clientWidth || 0);
574
547
  if (widths.length === 0) return;
575
- for (let i = 0; i < widths.length; i++) {
576
- parent.style.setProperty(
577
- `--cell-width-${i}`,
578
- `${widths[i] - scrollbar_width / widths.length}px`
579
- );
548
+
549
+ if (show_row_numbers) {
550
+ parent.style.setProperty(`--cell-width-row-number`, `${widths[0]}px`);
580
551
  }
552
+ const data_cells = show_row_numbers ? widths.slice(1) : widths;
553
+ data_cells.forEach((width, i) => {
554
+ if (!column_widths[i]) {
555
+ parent.style.setProperty(
556
+ `--cell-width-${i}`,
557
+ `${width - scrollbar_width / data_cells.length}px`
558
+ );
559
+ }
560
+ });
561
+ }
562
+
563
+ function get_cell_width(index: number): string {
564
+ return column_widths[index] || `var(--cell-width-${index})`;
581
565
  }
582
566
 
583
567
  let table_height: number =
@@ -592,43 +576,18 @@
592
576
  dir?: SortDirection
593
577
  ): void {
594
578
  let id = null;
595
- //Checks if the selected cell is still in the data
596
- if (selected && selected[0] in data && selected[1] in data[selected[0]]) {
597
- id = data[selected[0]][selected[1]].id;
579
+ if (selected && selected[0] in _data && selected[1] in _data[selected[0]]) {
580
+ id = _data[selected[0]][selected[1]].id;
598
581
  }
599
582
  if (typeof col !== "number" || !dir) {
600
583
  return;
601
584
  }
602
- const indices = [...Array(_data.length).keys()];
603
-
604
- if (dir === "asc") {
605
- indices.sort((i, j) =>
606
- _data[i][col].value < _data[j][col].value ? -1 : 1
607
- );
608
- } else if (dir === "des") {
609
- indices.sort((i, j) =>
610
- _data[i][col].value > _data[j][col].value ? -1 : 1
611
- );
612
- } else {
613
- return;
614
- }
615
-
616
- // sort all the data and metadata based on the values in the data
617
- const temp_data = [..._data];
618
- const temp_display_value = _display_value ? [..._display_value] : null;
619
- const temp_styling = _styling ? [..._styling] : null;
620
- indices.forEach((originalIndex, sortedIndex) => {
621
- _data[sortedIndex] = temp_data[originalIndex];
622
- if (_display_value && temp_display_value)
623
- _display_value[sortedIndex] = temp_display_value[originalIndex];
624
- if (_styling && temp_styling)
625
- _styling[sortedIndex] = temp_styling[originalIndex];
626
- });
627
585
 
586
+ sort_table_data(_data, _display_value, _styling, col, dir);
628
587
  data = data;
629
588
 
630
589
  if (id) {
631
- const [i, j] = get_current_indices(id);
590
+ const [i, j] = get_current_indices(id, data);
632
591
  selected = [i, j];
633
592
  }
634
593
  }
@@ -640,19 +599,17 @@
640
599
  let is_visible = false;
641
600
 
642
601
  onMount(() => {
643
- const observer = new IntersectionObserver((entries, observer) => {
602
+ const observer = new IntersectionObserver((entries) => {
644
603
  entries.forEach((entry) => {
645
604
  if (entry.isIntersecting && !is_visible) {
646
605
  set_cell_widths();
647
606
  data = data;
648
607
  }
649
-
650
608
  is_visible = entry.isIntersecting;
651
609
  });
652
610
  });
653
611
 
654
612
  observer.observe(parent);
655
-
656
613
  document.addEventListener("click", handle_click_outside);
657
614
  window.addEventListener("resize", handle_resize);
658
615
  document.addEventListener("fullscreenchange", handle_fullscreen_change);
@@ -668,14 +625,53 @@
668
625
  };
669
626
  });
670
627
 
671
- let highlighted_column: number | null = null;
628
+ function handle_cell_click(
629
+ event: MouseEvent,
630
+ row: number,
631
+ col: number
632
+ ): void {
633
+ if (event.target instanceof HTMLAnchorElement) {
634
+ return;
635
+ }
672
636
 
673
- let active_cell_menu: {
674
- row: number;
675
- col: number;
676
- x: number;
677
- y: number;
678
- } | null = null;
637
+ event.preventDefault();
638
+ event.stopPropagation();
639
+
640
+ if (show_row_numbers && col === -1) return;
641
+
642
+ clear_on_focus = false;
643
+ active_cell_menu = null;
644
+ active_header_menu = null;
645
+ selected_header = false;
646
+ header_edit = false;
647
+
648
+ selected_cells = handle_selection([row, col], selected_cells, event);
649
+ parent.focus();
650
+
651
+ if (editable) {
652
+ if (selected_cells.length === 1) {
653
+ editing = [row, col];
654
+ tick().then(() => {
655
+ const input_el = els[data[row][col].id].input;
656
+ if (input_el) {
657
+ input_el.focus();
658
+ input_el.selectionStart = input_el.selectionEnd =
659
+ input_el.value.length;
660
+ }
661
+ });
662
+ } else {
663
+ editing = false;
664
+ }
665
+ }
666
+
667
+ toggle_cell_button(row, col);
668
+
669
+ dispatch("select", {
670
+ index: [row, col],
671
+ value: get_data_at(row, col),
672
+ row_value: data[row].map((d) => d.value)
673
+ });
674
+ }
679
675
 
680
676
  function toggle_cell_menu(event: MouseEvent, row: number, col: number): void {
681
677
  event.stopPropagation();
@@ -689,12 +685,7 @@
689
685
  const cell = (event.target as HTMLElement).closest("td");
690
686
  if (cell) {
691
687
  const rect = cell.getBoundingClientRect();
692
- active_cell_menu = {
693
- row,
694
- col,
695
- x: rect.right,
696
- y: rect.bottom
697
- };
688
+ active_cell_menu = { row, col, x: rect.right, y: rect.bottom };
698
689
  }
699
690
  }
700
691
  }
@@ -716,6 +707,9 @@
716
707
  function handle_resize(): void {
717
708
  active_cell_menu = null;
718
709
  active_header_menu = null;
710
+ selected_cells = [];
711
+ selected = false;
712
+ editing = false;
719
713
  set_cell_widths();
720
714
  }
721
715
 
@@ -726,33 +720,21 @@
726
720
  } | null = null;
727
721
 
728
722
  function toggle_header_button(col: number): void {
729
- if (active_button?.type === "header" && active_button.col === col) {
730
- active_button = null;
731
- } else {
732
- active_button = { type: "header", col };
733
- }
723
+ active_button =
724
+ active_button?.type === "header" && active_button.col === col
725
+ ? null
726
+ : { type: "header", col };
734
727
  }
735
728
 
736
729
  function toggle_cell_button(row: number, col: number): void {
737
- if (
730
+ active_button =
738
731
  active_button?.type === "cell" &&
739
732
  active_button.row === row &&
740
733
  active_button.col === col
741
- ) {
742
- active_button = null;
743
- } else {
744
- active_button = { type: "cell", row, col };
745
- }
734
+ ? null
735
+ : { type: "cell", row, col };
746
736
  }
747
737
 
748
- let active_header_menu: {
749
- col: number;
750
- x: number;
751
- y: number;
752
- } | null = null;
753
-
754
- let is_fullscreen = false;
755
-
756
738
  function toggle_fullscreen(): void {
757
739
  if (!document.fullscreenElement) {
758
740
  parent.requestFullscreen();
@@ -768,7 +750,11 @@
768
750
  }
769
751
 
770
752
  async function handle_copy(): Promise<void> {
771
- await copy_table_data(data, _headers);
753
+ await copy_table_data(data, selected_cells);
754
+ copy_flash = true;
755
+ setTimeout(() => {
756
+ copy_flash = false;
757
+ }, 800);
772
758
  }
773
759
 
774
760
  function toggle_header_menu(event: MouseEvent, col: number): void {
@@ -779,11 +765,7 @@
779
765
  const header = (event.target as HTMLElement).closest("th");
780
766
  if (header) {
781
767
  const rect = header.getBoundingClientRect();
782
- active_header_menu = {
783
- col,
784
- x: rect.right,
785
- y: rect.bottom
786
- };
768
+ active_header_menu = { col, x: rect.right, y: rect.bottom };
787
769
  }
788
770
  }
789
771
  }
@@ -827,6 +809,94 @@
827
809
  active_cell_menu = null;
828
810
  active_header_menu = null;
829
811
  }
812
+
813
+ let row_order: number[] = [];
814
+
815
+ $: {
816
+ if (
817
+ typeof sort_by === "number" &&
818
+ sort_direction &&
819
+ sort_by >= 0 &&
820
+ sort_by < data[0].length
821
+ ) {
822
+ const indices = [...Array(data.length)].map((_, i) => i);
823
+ const sort_index = sort_by as number;
824
+ indices.sort((a, b) => {
825
+ const row_a = data[a];
826
+ const row_b = data[b];
827
+ if (
828
+ !row_a ||
829
+ !row_b ||
830
+ sort_index >= row_a.length ||
831
+ sort_index >= row_b.length
832
+ )
833
+ return 0;
834
+ const val_a = row_a[sort_index].value;
835
+ const val_b = row_b[sort_index].value;
836
+ const comp = val_a < val_b ? -1 : val_a > val_b ? 1 : 0;
837
+ return sort_direction === "asc" ? comp : -comp;
838
+ });
839
+ row_order = indices;
840
+ } else {
841
+ row_order = [...Array(data.length)].map((_, i) => i);
842
+ }
843
+ }
844
+
845
+ function handle_select_column(col: number): void {
846
+ selected_cells = select_column(data, col);
847
+ selected = selected_cells[0];
848
+ editing = false;
849
+ }
850
+
851
+ function handle_select_row(row: number): void {
852
+ selected_cells = select_row(data, row);
853
+ selected = selected_cells[0];
854
+ editing = false;
855
+ }
856
+
857
+ let coords: CellCoordinate;
858
+ $: if (selected !== false) coords = selected;
859
+
860
+ $: if (selected !== false) {
861
+ const positions = calculate_selection_positions(
862
+ selected,
863
+ data,
864
+ els,
865
+ parent,
866
+ table
867
+ );
868
+ document.documentElement.style.setProperty(
869
+ "--selected-col-pos",
870
+ positions.col_pos
871
+ );
872
+ if (positions.row_pos) {
873
+ document.documentElement.style.setProperty(
874
+ "--selected-row-pos",
875
+ positions.row_pos
876
+ );
877
+ }
878
+ }
879
+
880
+ let current_search_query: string | null = null;
881
+
882
+ function handle_search(search_query: string | null): void {
883
+ current_search_query = search_query;
884
+ dispatch("search", search_query);
885
+ }
886
+
887
+ function commit_filter(): void {
888
+ if (current_search_query && show_search === "filter") {
889
+ dispatch("change", {
890
+ data: data.map((row) => row.map((cell) => cell.value)),
891
+ headers: _headers.map((h) => h.value),
892
+ metadata: null
893
+ });
894
+ if (!value_is_output) {
895
+ dispatch("input");
896
+ }
897
+ current_search_query = null;
898
+ }
899
+ }
830
900
  </script>
831
901
 
832
902
  <svelte:window on:resize={() => set_cell_widths()} />
@@ -844,6 +914,10 @@
844
914
  on:click={toggle_fullscreen}
845
915
  on_copy={handle_copy}
846
916
  {show_copy_button}
917
+ {show_search}
918
+ on:search={(e) => handle_search(e.detail)}
919
+ on_commit_filter={commit_filter}
920
+ {current_search_query}
847
921
  />
848
922
  </div>
849
923
  <div
@@ -851,11 +925,28 @@
851
925
  class="table-wrap"
852
926
  class:dragging
853
927
  class:no-wrap={!wrap}
854
- style="height:{table_height}px"
928
+ style="height:{table_height}px;"
929
+ class:menu-open={active_cell_menu || active_header_menu}
855
930
  on:keydown={(e) => handle_keydown(e)}
856
931
  role="grid"
857
932
  tabindex="0"
858
933
  >
934
+ {#if selected !== false && selected_cells.length === 1}
935
+ <button
936
+ class="selection-button selection-button-column"
937
+ on:click|stopPropagation={() => handle_select_column(coords[1])}
938
+ aria-label="Select column"
939
+ >
940
+ &#8942;
941
+ </button>
942
+ <button
943
+ class="selection-button selection-button-row"
944
+ on:click|stopPropagation={() => handle_select_row(coords[0])}
945
+ aria-label="Select row"
946
+ >
947
+ &#8942;
948
+ </button>
949
+ {/if}
859
950
  <table
860
951
  bind:contentRect={t_rect}
861
952
  bind:this={table}
@@ -867,39 +958,59 @@
867
958
  <thead>
868
959
  <tr>
869
960
  {#if show_row_numbers}
870
- <th class="row-number-header"></th>
961
+ <th
962
+ class="row-number-header frozen-column always-frozen"
963
+ style="left: 0;"
964
+ >
965
+ <div class="cell-wrap">
966
+ <div class="header-content">
967
+ <div class="header-text"></div>
968
+ </div>
969
+ </div>
970
+ </th>
871
971
  {/if}
872
972
  {#each _headers as { value, id }, i (id)}
873
973
  <th
974
+ class:frozen-column={i < actual_pinned_columns}
975
+ class:last-frozen={show_row_numbers
976
+ ? i === actual_pinned_columns - 1
977
+ : i === actual_pinned_columns - 1}
874
978
  class:editing={header_edit === i}
875
979
  aria-sort={get_sort_status(value, sort_by, sort_direction)}
876
- style:width={column_widths.length ? column_widths[i] : undefined}
980
+ style="width: {column_widths.length
981
+ ? column_widths[i]
982
+ : undefined}; left: {i < actual_pinned_columns
983
+ ? i === 0
984
+ ? show_row_numbers
985
+ ? 'var(--cell-width-row-number)'
986
+ : '0'
987
+ : `calc(${show_row_numbers ? 'var(--cell-width-row-number) + ' : ''}${Array(
988
+ i
989
+ )
990
+ .fill(0)
991
+ .map((_, idx) => `var(--cell-width-${idx})`)
992
+ .join(' + ')})`
993
+ : 'auto'};"
877
994
  >
878
995
  <div class="cell-wrap">
879
- <EditableCell
880
- {value}
881
- {latex_delimiters}
882
- {line_breaks}
883
- header
884
- edit={false}
885
- el={null}
886
- {root}
887
- />
888
-
889
- <div
890
- class:sorted={sort_by === i}
891
- class:des={sort_by === i && sort_direction === "des"}
892
- class="sort-button {sort_direction} "
893
- >
894
- <svg
895
- width="1em"
896
- height="1em"
897
- viewBox="0 0 9 7"
898
- fill="none"
899
- xmlns="http://www.w3.org/2000/svg"
900
- >
901
- <path d="M4.49999 0L8.3971 6.75H0.602875L4.49999 0Z" />
902
- </svg>
996
+ <div class="header-content">
997
+ <EditableCell
998
+ {value}
999
+ {latex_delimiters}
1000
+ {line_breaks}
1001
+ header
1002
+ edit={false}
1003
+ el={null}
1004
+ {root}
1005
+ {editable}
1006
+ />
1007
+ <div class="sort-buttons">
1008
+ <SortIcon
1009
+ direction={sort_by === i ? sort_direction : null}
1010
+ on:sort={({ detail }) => handle_sort(i, detail)}
1011
+ {i18n}
1012
+ />
1013
+ </div>
903
1014
  </div>
904
1015
  </div>
905
1016
  </th>
@@ -919,6 +1030,7 @@
919
1030
  edit={false}
920
1031
  el={null}
921
1032
  {root}
1033
+ {editable}
922
1034
  />
923
1035
  </div>
924
1036
  </td>
@@ -934,8 +1046,23 @@
934
1046
  boundedheight={false}
935
1047
  disable_click={true}
936
1048
  {root}
937
- on:load={(e) => blob_to_string(data_uri_to_blob(e.detail.data))}
1049
+ on:load={({ detail }) =>
1050
+ handle_file_upload(
1051
+ detail.data,
1052
+ (head) => {
1053
+ _headers = make_headers(
1054
+ head.map((h) => h ?? ""),
1055
+ col_count,
1056
+ els
1057
+ );
1058
+ return _headers;
1059
+ },
1060
+ (vals) => {
1061
+ values = vals;
1062
+ }
1063
+ )}
938
1064
  bind:dragging
1065
+ aria_label={i18n("dataframe.drop_to_upload")}
939
1066
  >
940
1067
  <VirtualTable
941
1068
  bind:items={data}
@@ -943,19 +1070,44 @@
943
1070
  bind:actual_height={table_height}
944
1071
  bind:table_scrollbar_width={scrollbar_width}
945
1072
  selected={selected_index}
1073
+ disable_scroll={active_cell_menu !== null ||
1074
+ active_header_menu !== null}
946
1075
  >
947
1076
  {#if label && label.length !== 0}
948
1077
  <caption class="sr-only">{label}</caption>
949
1078
  {/if}
950
1079
  <tr slot="thead">
951
1080
  {#if show_row_numbers}
952
- <th class="row-number-header"></th>
1081
+ <th
1082
+ class="row-number-header frozen-column always-frozen"
1083
+ style="left: 0;"
1084
+ >
1085
+ <div class="cell-wrap">
1086
+ <div class="header-content">
1087
+ <div class="header-text"></div>
1088
+ </div>
1089
+ </div>
1090
+ </th>
953
1091
  {/if}
954
1092
  {#each _headers as { value, id }, i (id)}
955
1093
  <th
1094
+ class:frozen-column={i < actual_pinned_columns}
1095
+ class:last-frozen={i === actual_pinned_columns - 1}
956
1096
  class:focus={header_edit === i || selected_header === i}
957
1097
  aria-sort={get_sort_status(value, sort_by, sort_direction)}
958
- style="width: var(--cell-width-{i});"
1098
+ style="width: {get_cell_width(i)}; left: {i <
1099
+ actual_pinned_columns
1100
+ ? i === 0
1101
+ ? show_row_numbers
1102
+ ? 'var(--cell-width-row-number)'
1103
+ : '0'
1104
+ : `calc(${show_row_numbers ? 'var(--cell-width-row-number) + ' : ''}${Array(
1105
+ i
1106
+ )
1107
+ .fill(0)
1108
+ .map((_, idx) => `var(--cell-width-${idx})`)
1109
+ .join(' + ')})`
1110
+ : 'auto'};"
959
1111
  on:click={() => {
960
1112
  toggle_header_button(i);
961
1113
  }}
@@ -963,6 +1115,7 @@
963
1115
  <div class="cell-wrap">
964
1116
  <div class="header-content">
965
1117
  <EditableCell
1118
+ {max_chars}
966
1119
  bind:value={_headers[i].value}
967
1120
  bind:el={els[id].input}
968
1121
  {latex_delimiters}
@@ -972,84 +1125,76 @@
972
1125
  on:dblclick={() => edit_header(i)}
973
1126
  header
974
1127
  {root}
1128
+ {editable}
975
1129
  />
976
- <button
977
- class:sorted={sort_by === i}
978
- class:des={sort_by === i && sort_direction === "des"}
979
- class="sort-button {sort_direction}"
980
- tabindex="0"
981
- on:click={(event) => {
982
- event.stopPropagation();
983
- handle_sort(i);
984
- }}
985
- >
986
- <svg
987
- width="1em"
988
- height="1em"
989
- viewBox="0 0 9 7"
990
- fill="none"
991
- xmlns="http://www.w3.org/2000/svg"
992
- >
993
- <path d="M4.49999 0L8.3971 6.75H0.602875L4.49999 0Z" />
994
- </svg>
995
- </button>
1130
+ <div class="sort-buttons">
1131
+ <SortIcon
1132
+ direction={sort_by === i ? sort_direction : null}
1133
+ on:sort={({ detail }) => handle_sort(i, detail)}
1134
+ {i18n}
1135
+ />
1136
+ </div>
996
1137
  </div>
997
-
998
1138
  {#if editable}
999
1139
  <button
1000
1140
  class="cell-menu-button"
1001
1141
  on:click={(event) => toggle_header_menu(event, i)}
1002
1142
  >
1003
-
1143
+ &#8942;
1004
1144
  </button>
1005
1145
  {/if}
1006
1146
  </div>
1007
1147
  </th>
1008
1148
  {/each}
1009
1149
  </tr>
1010
-
1011
1150
  <tr slot="tbody" let:item let:index class:row_odd={index % 2 === 0}>
1012
1151
  {#if show_row_numbers}
1013
- <td class="row-number" title={`Row ${index + 1}`}>{index + 1}</td>
1152
+ <td
1153
+ class="row-number frozen-column always-frozen"
1154
+ style="left: 0;"
1155
+ tabindex="-1"
1156
+ >
1157
+ {index + 1}
1158
+ </td>
1014
1159
  {/if}
1015
1160
  {#each item as { value, id }, j (id)}
1016
1161
  <td
1017
- tabindex="0"
1162
+ class:frozen-column={j < actual_pinned_columns}
1163
+ class:last-frozen={j === actual_pinned_columns - 1}
1164
+ tabindex={show_row_numbers && j === 0 ? -1 : 0}
1165
+ bind:this={els[id].cell}
1018
1166
  on:touchstart={(event) => {
1019
- event.preventDefault();
1020
- event.stopPropagation();
1021
- clear_on_focus = false;
1022
- clicked_cell = { row: index, col: j };
1023
- selected = [index, j];
1024
- selected_header = false;
1025
- header_edit = false;
1026
- if (editable) {
1027
- editing = [index, j];
1028
- }
1029
- toggle_cell_button(index, j);
1167
+ const touch = event.touches[0];
1168
+ const mouseEvent = new MouseEvent("click", {
1169
+ clientX: touch.clientX,
1170
+ clientY: touch.clientY,
1171
+ bubbles: true,
1172
+ cancelable: true,
1173
+ view: window
1174
+ });
1175
+ handle_cell_click(mouseEvent, index, j);
1030
1176
  }}
1031
1177
  on:mousedown={(event) => {
1032
1178
  event.preventDefault();
1033
1179
  event.stopPropagation();
1034
1180
  }}
1035
- on:click={(event) => {
1036
- event.preventDefault();
1037
- event.stopPropagation();
1038
- clear_on_focus = false;
1039
- active_cell_menu = null;
1040
- active_header_menu = null;
1041
- clicked_cell = { row: index, col: j };
1042
- selected = [index, j];
1043
- selected_header = false;
1044
- header_edit = false;
1045
- if (editable) {
1046
- editing = [index, j];
1047
- }
1048
- toggle_cell_button(index, j);
1049
- }}
1050
- style:width="var(--cell-width-{j})"
1051
- style={styling?.[index]?.[j] || ""}
1052
- class:focus={dequal(selected, [index, j])}
1181
+ on:click={(event) => handle_cell_click(event, index, j)}
1182
+ style="width: {get_cell_width(j)}; left: {j <
1183
+ actual_pinned_columns
1184
+ ? j === 0
1185
+ ? show_row_numbers
1186
+ ? 'var(--cell-width-row-number)'
1187
+ : '0'
1188
+ : `calc(${show_row_numbers ? 'var(--cell-width-row-number) + ' : ''}${Array(
1189
+ j
1190
+ )
1191
+ .fill(0)
1192
+ .map((_, idx) => `var(--cell-width-${idx})`)
1193
+ .join(' + ')})`
1194
+ : 'auto'}; {styling?.[index]?.[j] || ''}"
1195
+ class:flash={copy_flash &&
1196
+ is_cell_selected([index, j], selected_cells)}
1197
+ class={is_cell_selected([index, j], selected_cells)}
1053
1198
  class:menu-active={active_cell_menu &&
1054
1199
  active_cell_menu.row === index &&
1055
1200
  active_cell_menu.col === j}
@@ -1068,15 +1213,25 @@
1068
1213
  clear_on_focus = false;
1069
1214
  parent.focus();
1070
1215
  }}
1216
+ on:focus={() => {
1217
+ const row = index;
1218
+ const col = j;
1219
+ if (
1220
+ !selected_cells.some(([r, c]) => r === row && c === col)
1221
+ ) {
1222
+ selected_cells = [[row, col]];
1223
+ }
1224
+ }}
1071
1225
  {clear_on_focus}
1072
1226
  {root}
1227
+ {max_chars}
1073
1228
  />
1074
- {#if editable}
1229
+ {#if editable && should_show_cell_menu([index, j], selected_cells, editable)}
1075
1230
  <button
1076
1231
  class="cell-menu-button"
1077
1232
  on:click={(event) => toggle_cell_menu(event, index, j)}
1078
1233
  >
1079
-
1234
+ &#8942;
1080
1235
  </button>
1081
1236
  {/if}
1082
1237
  </div>
@@ -1128,15 +1283,6 @@
1128
1283
  {/if}
1129
1284
 
1130
1285
  <style>
1131
- .button-wrap:hover svg {
1132
- color: var(--color-accent);
1133
- }
1134
-
1135
- .button-wrap svg {
1136
- margin-right: var(--size-1);
1137
- margin-left: -5px;
1138
- }
1139
-
1140
1286
  .label p {
1141
1287
  position: relative;
1142
1288
  z-index: var(--layer-4);
@@ -1145,17 +1291,25 @@
1145
1291
  font-size: var(--block-label-text-size);
1146
1292
  }
1147
1293
 
1294
+ .table-container {
1295
+ display: flex;
1296
+ flex-direction: column;
1297
+ gap: var(--size-2);
1298
+ }
1299
+
1148
1300
  .table-wrap {
1149
1301
  position: relative;
1150
1302
  transition: 150ms;
1151
1303
  border: 1px solid var(--border-color-primary);
1152
1304
  border-radius: var(--table-radius);
1305
+ }
1306
+
1307
+ .table-wrap.menu-open {
1153
1308
  overflow: hidden;
1154
1309
  }
1155
1310
 
1156
1311
  .table-wrap:focus-within {
1157
1312
  outline: none;
1158
- background-color: none;
1159
1313
  }
1160
1314
 
1161
1315
  .dragging {
@@ -1177,6 +1331,7 @@
1177
1331
  line-height: var(--line-md);
1178
1332
  font-family: var(--font-mono);
1179
1333
  border-spacing: 0;
1334
+ border-collapse: separate;
1180
1335
  }
1181
1336
 
1182
1337
  div:not(.no-wrap) td {
@@ -1194,8 +1349,7 @@
1194
1349
  thead {
1195
1350
  position: sticky;
1196
1351
  top: 0;
1197
- left: 0;
1198
- z-index: var(--layer-1);
1352
+ z-index: var(--layer-2);
1199
1353
  box-shadow: var(--shadow-drop);
1200
1354
  }
1201
1355
 
@@ -1231,6 +1385,12 @@
1231
1385
  th.focus,
1232
1386
  td.focus {
1233
1387
  --ring-color: var(--color-accent);
1388
+ box-shadow: inset 0 0 0 2px var(--ring-color);
1389
+ z-index: var(--layer-1);
1390
+ }
1391
+
1392
+ th.focus {
1393
+ z-index: var(--layer-2);
1234
1394
  }
1235
1395
 
1236
1396
  tr:last-child td:first-child {
@@ -1245,33 +1405,10 @@
1245
1405
  background: var(--table-even-background-fill);
1246
1406
  }
1247
1407
 
1248
- th svg {
1249
- fill: currentColor;
1250
- font-size: 10px;
1251
- }
1252
-
1253
- .sort-button {
1408
+ .sort-buttons {
1254
1409
  display: flex;
1255
- flex: none;
1256
- justify-content: center;
1257
1410
  align-items: center;
1258
- transition: 150ms;
1259
- cursor: pointer;
1260
- padding: var(--size-2);
1261
- color: var(--body-text-color-subdued);
1262
- line-height: var(--text-sm);
1263
- }
1264
-
1265
- .sort-button:hover {
1266
- color: var(--body-text-color);
1267
- }
1268
-
1269
- .des {
1270
- transform: scaleY(-1);
1271
- }
1272
-
1273
- .sort-button.sorted {
1274
- color: var(--color-accent);
1411
+ flex-shrink: 0;
1275
1412
  }
1276
1413
 
1277
1414
  .editing {
@@ -1280,25 +1417,26 @@
1280
1417
 
1281
1418
  .cell-wrap {
1282
1419
  display: flex;
1283
- align-items: center;
1420
+ align-items: flex-start;
1284
1421
  outline: none;
1285
- height: var(--size-full);
1286
1422
  min-height: var(--size-9);
1287
- overflow: hidden;
1423
+ position: relative;
1424
+ height: auto;
1288
1425
  }
1289
1426
 
1290
1427
  .header-content {
1291
1428
  display: flex;
1292
1429
  align-items: center;
1430
+ justify-content: space-between;
1293
1431
  overflow: hidden;
1294
1432
  flex-grow: 1;
1295
1433
  min-width: 0;
1296
- }
1297
-
1298
- .controls-wrap {
1299
- display: flex;
1300
- justify-content: flex-end;
1301
- padding-top: var(--size-2);
1434
+ white-space: normal;
1435
+ overflow-wrap: break-word;
1436
+ word-break: normal;
1437
+ height: 100%;
1438
+ padding: var(--size-1);
1439
+ gap: var(--size-1);
1302
1440
  }
1303
1441
 
1304
1442
  .row_odd {
@@ -1309,10 +1447,6 @@
1309
1447
  background: var(--background-fill-primary);
1310
1448
  }
1311
1449
 
1312
- table {
1313
- border-collapse: separate;
1314
- }
1315
-
1316
1450
  .cell-menu-button {
1317
1451
  flex-shrink: 0;
1318
1452
  display: none;
@@ -1325,77 +1459,227 @@
1325
1459
  padding: 0;
1326
1460
  margin-right: var(--spacing-sm);
1327
1461
  z-index: var(--layer-1);
1462
+ position: absolute;
1463
+ right: var(--size-1);
1464
+ top: 50%;
1465
+ transform: translateY(-50%);
1328
1466
  }
1329
1467
 
1330
- .cell-menu-button:hover {
1331
- background-color: var(--color-bg-hover);
1468
+ .cell-selected .cell-menu-button {
1469
+ display: flex;
1470
+ align-items: center;
1471
+ justify-content: center;
1332
1472
  }
1333
1473
 
1334
- td.focus .cell-menu-button {
1474
+ .header-row {
1335
1475
  display: flex;
1476
+ justify-content: flex-end;
1336
1477
  align-items: center;
1337
- justify-content: center;
1478
+ gap: var(--size-2);
1479
+ min-height: var(--size-6);
1480
+ flex-wrap: nowrap;
1481
+ width: 100%;
1338
1482
  }
1339
1483
 
1340
- th .header-content {
1341
- white-space: normal;
1342
- overflow-wrap: break-word;
1343
- word-break: break-word;
1484
+ .label {
1485
+ flex: 1 1 auto;
1486
+ margin-right: auto;
1344
1487
  }
1345
1488
 
1346
- .table-container {
1347
- display: flex;
1348
- flex-direction: column;
1349
- gap: var(--size-2);
1489
+ .label p {
1490
+ margin: 0;
1491
+ color: var(--block-label-text-color);
1492
+ font-size: var(--block-label-text-size);
1493
+ line-height: var(--line-sm);
1494
+ }
1495
+
1496
+ .toolbar {
1497
+ flex: 0 0 auto;
1350
1498
  }
1351
1499
 
1352
1500
  .row-number,
1353
1501
  .row-number-header {
1354
- width: var(--size-7);
1355
- min-width: var(--size-7);
1356
1502
  text-align: center;
1357
1503
  background: var(--table-even-background-fill);
1358
- position: sticky;
1359
- left: 0;
1360
1504
  font-size: var(--input-text-size);
1361
1505
  color: var(--body-text-color);
1362
- padding: var(--size-1) var(--size-2);
1506
+ padding: var(--size-1);
1507
+ min-width: var(--size-12);
1508
+ width: var(--size-12);
1363
1509
  overflow: hidden;
1364
1510
  text-overflow: ellipsis;
1365
1511
  white-space: nowrap;
1366
1512
  font-weight: var(--weight-semibold);
1367
1513
  }
1368
1514
 
1369
- .row-number-header {
1370
- z-index: var(--layer-2);
1515
+ .row-number-header .header-content {
1516
+ justify-content: space-between;
1517
+ padding: var(--size-1);
1518
+ height: var(--size-9);
1519
+ display: flex;
1520
+ align-items: center;
1371
1521
  }
1372
1522
 
1373
- .row-number {
1374
- z-index: var(--layer-1);
1523
+ .row-number-header :global(.sort-icons) {
1524
+ margin-right: 0;
1375
1525
  }
1376
1526
 
1377
1527
  :global(tbody > tr:nth-child(odd)) .row-number {
1378
1528
  background: var(--table-odd-background-fill);
1379
1529
  }
1380
1530
 
1381
- .header-row {
1531
+ .cell-selected {
1532
+ --ring-color: var(--color-accent);
1533
+ box-shadow: inset 0 0 0 2px var(--ring-color);
1534
+ z-index: var(--layer-1);
1535
+ position: relative;
1536
+ }
1537
+
1538
+ .cell-selected.no-top {
1539
+ box-shadow:
1540
+ inset 2px 0 0 var(--ring-color),
1541
+ inset -2px 0 0 var(--ring-color),
1542
+ inset 0 -2px 0 var(--ring-color);
1543
+ }
1544
+
1545
+ .cell-selected.no-bottom {
1546
+ box-shadow:
1547
+ inset 2px 0 0 var(--ring-color),
1548
+ inset -2px 0 0 var(--ring-color),
1549
+ inset 0 2px 0 var(--ring-color);
1550
+ }
1551
+
1552
+ .cell-selected.no-left {
1553
+ box-shadow:
1554
+ inset 0 2px 0 var(--ring-color),
1555
+ inset -2px 0 0 var(--ring-color),
1556
+ inset 0 -2px 0 var(--ring-color);
1557
+ }
1558
+
1559
+ .cell-selected.no-right {
1560
+ box-shadow:
1561
+ inset 0 2px 0 var(--ring-color),
1562
+ inset 2px 0 0 var(--ring-color),
1563
+ inset 0 -2px 0 var(--ring-color);
1564
+ }
1565
+
1566
+ .cell-selected.no-top.no-left {
1567
+ box-shadow:
1568
+ inset -2px 0 0 var(--ring-color),
1569
+ inset 0 -2px 0 var(--ring-color);
1570
+ }
1571
+
1572
+ .cell-selected.no-top.no-right {
1573
+ box-shadow:
1574
+ inset 2px 0 0 var(--ring-color),
1575
+ inset 0 -2px 0 var(--ring-color);
1576
+ }
1577
+
1578
+ .cell-selected.no-bottom.no-left {
1579
+ box-shadow:
1580
+ inset -2px 0 0 var(--ring-color),
1581
+ inset 0 2px 0 var(--ring-color);
1582
+ }
1583
+
1584
+ .cell-selected.no-bottom.no-right {
1585
+ box-shadow:
1586
+ inset 2px 0 0 var(--ring-color),
1587
+ inset 0 2px 0 var(--ring-color);
1588
+ }
1589
+
1590
+ .cell-selected.no-top.no-bottom {
1591
+ box-shadow:
1592
+ inset 2px 0 0 var(--ring-color),
1593
+ inset -2px 0 0 var(--ring-color);
1594
+ }
1595
+
1596
+ .cell-selected.no-left.no-right {
1597
+ box-shadow:
1598
+ inset 0 2px 0 var(--ring-color),
1599
+ inset 0 -2px 0 var(--ring-color);
1600
+ }
1601
+
1602
+ .cell-selected.no-top.no-left.no-right {
1603
+ box-shadow: inset 0 -2px 0 var(--ring-color);
1604
+ }
1605
+
1606
+ .cell-selected.no-bottom.no-left.no-right {
1607
+ box-shadow: inset 0 2px 0 var(--ring-color);
1608
+ }
1609
+
1610
+ .cell-selected.no-left.no-top.no-bottom {
1611
+ box-shadow: inset -2px 0 0 var(--ring-color);
1612
+ }
1613
+
1614
+ .cell-selected.no-right.no-top.no-bottom {
1615
+ box-shadow: inset 2px 0 0 var(--ring-color);
1616
+ }
1617
+
1618
+ .cell-selected.no-top.no-bottom.no-left.no-right {
1619
+ box-shadow: none;
1620
+ }
1621
+
1622
+ .selection-button {
1623
+ position: absolute;
1382
1624
  display: flex;
1383
- justify-content: space-between;
1384
1625
  align-items: center;
1385
- gap: var(--size-2);
1386
- height: var(--size-6);
1387
- min-height: var(--size-6);
1626
+ justify-content: center;
1627
+ background: var(--color-accent);
1628
+ color: white;
1629
+ border-radius: var(--radius-sm);
1630
+ z-index: var(--layer-4);
1388
1631
  }
1389
1632
 
1390
- .label {
1391
- flex: 1;
1633
+ .selection-button-column {
1634
+ width: var(--size-3);
1635
+ height: var(--size-5);
1636
+ top: -10px;
1637
+ left: var(--selected-col-pos);
1638
+ transform: rotate(90deg);
1392
1639
  }
1393
1640
 
1394
- .label p {
1395
- position: relative;
1396
- z-index: var(--layer-4);
1397
- margin: 0;
1398
- color: var(--block-label-text-color);
1399
- font-size: var(--block-label-text-size);
1641
+ .selection-button-row {
1642
+ width: var(--size-3);
1643
+ height: var(--size-5);
1644
+ left: -7px;
1645
+ top: calc(var(--selected-row-pos) - var(--size-5) / 2);
1646
+ }
1647
+
1648
+ .table-wrap:not(:focus-within) .selection-button {
1649
+ opacity: 0;
1650
+ pointer-events: none;
1651
+ }
1652
+
1653
+ .flash.cell-selected {
1654
+ animation: flash-color 700ms ease-out;
1655
+ }
1656
+
1657
+ @keyframes flash-color {
1658
+ 0%,
1659
+ 30% {
1660
+ background: var(--color-accent-copied);
1661
+ }
1662
+
1663
+ 100% {
1664
+ background: transparent;
1665
+ }
1666
+ }
1667
+
1668
+ .frozen-column {
1669
+ position: sticky;
1670
+ z-index: var(--layer-2);
1671
+ border-right: 1px solid var(--border-color-primary);
1672
+ }
1673
+
1674
+ tr:nth-child(odd) .frozen-column {
1675
+ background: var(--table-odd-background-fill);
1676
+ }
1677
+
1678
+ tr:nth-child(even) .frozen-column {
1679
+ background: var(--table-even-background-fill);
1680
+ }
1681
+
1682
+ .always-frozen {
1683
+ z-index: var(--layer-3);
1400
1684
  }
1401
1685
  </style>