@gradio/dataframe 0.15.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 (43) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/Dataframe.stories.svelte +168 -2
  3. package/Index.svelte +20 -3
  4. package/dist/Index.svelte +16 -4
  5. package/dist/Index.svelte.d.ts +12 -0
  6. package/dist/shared/EditableCell.svelte +1 -4
  7. package/dist/shared/Table.svelte +423 -181
  8. package/dist/shared/Table.svelte.d.ts +3 -0
  9. package/dist/shared/Toolbar.svelte +122 -30
  10. package/dist/shared/Toolbar.svelte.d.ts +4 -0
  11. package/dist/shared/VirtualTable.svelte +70 -26
  12. package/dist/shared/VirtualTable.svelte.d.ts +1 -0
  13. package/dist/shared/icons/FilterIcon.svelte +11 -0
  14. package/dist/shared/icons/FilterIcon.svelte.d.ts +16 -0
  15. package/dist/shared/icons/SortIcon.svelte +90 -0
  16. package/dist/shared/icons/SortIcon.svelte.d.ts +20 -0
  17. package/dist/shared/selection_utils.d.ts +12 -2
  18. package/dist/shared/selection_utils.js +33 -5
  19. package/dist/shared/types.d.ts +16 -0
  20. package/dist/shared/types.js +1 -0
  21. package/dist/shared/utils/menu_utils.d.ts +42 -0
  22. package/dist/shared/utils/menu_utils.js +58 -0
  23. package/dist/shared/utils/sort_utils.d.ts +7 -0
  24. package/dist/shared/utils/sort_utils.js +39 -0
  25. package/dist/shared/utils/table_utils.d.ts +12 -0
  26. package/dist/shared/utils/table_utils.js +148 -0
  27. package/package.json +8 -8
  28. package/shared/EditableCell.svelte +1 -4
  29. package/shared/Table.svelte +453 -182
  30. package/shared/Toolbar.svelte +125 -30
  31. package/shared/VirtualTable.svelte +73 -26
  32. package/shared/icons/FilterIcon.svelte +12 -0
  33. package/shared/icons/SortIcon.svelte +95 -0
  34. package/shared/selection_utils.ts +51 -9
  35. package/shared/types.ts +27 -0
  36. package/shared/utils/menu_utils.ts +115 -0
  37. package/shared/utils/sort_utils.test.ts +71 -0
  38. package/shared/utils/sort_utils.ts +55 -0
  39. package/shared/utils/table_utils.test.ts +114 -0
  40. package/shared/utils/table_utils.ts +206 -0
  41. package/dist/shared/table_utils.d.ts +0 -12
  42. package/dist/shared/table_utils.js +0 -113
  43. package/shared/table_utils.ts +0 -148
@@ -16,6 +16,7 @@
16
16
  } from "./utils";
17
17
  import CellMenu from "./CellMenu.svelte";
18
18
  import Toolbar from "./Toolbar.svelte";
19
+ import SortIcon from "./icons/SortIcon.svelte";
19
20
  import type { CellCoordinate, EditingState } from "./types";
20
21
  import {
21
22
  is_cell_selected,
@@ -26,9 +27,17 @@
26
27
  get_range_selection,
27
28
  move_cursor,
28
29
  get_current_indices,
29
- handle_click_outside as handle_click_outside_util
30
+ handle_click_outside as handle_click_outside_util,
31
+ select_column,
32
+ select_row,
33
+ calculate_selection_positions
30
34
  } from "./selection_utils";
31
- import { copy_table_data, get_max, handle_file_upload } from "./table_utils";
35
+ import {
36
+ copy_table_data,
37
+ get_max,
38
+ handle_file_upload,
39
+ sort_table_data
40
+ } from "./utils/table_utils";
32
41
 
33
42
  export let datatype: Datatype | Datatype[];
34
43
  export let label: string | null = null;
@@ -58,6 +67,14 @@
58
67
  export let show_copy_button = false;
59
68
  export let value_is_output = false;
60
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;
61
78
 
62
79
  let selected_cells: CellCoordinate[] = [];
63
80
  $: selected_cells = [...selected_cells];
@@ -80,6 +97,7 @@
80
97
  change: DataframeValue;
81
98
  input: undefined;
82
99
  select: SelectData;
100
+ search: string | null;
83
101
  }>();
84
102
 
85
103
  let editing: EditingState = false;
@@ -99,6 +117,19 @@
99
117
  } | null = null;
100
118
  let is_fullscreen = false;
101
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
+ });
102
133
 
103
134
  const get_data_at = (row: number, col: number): string | number =>
104
135
  data?.[row]?.[col]?.value;
@@ -107,7 +138,14 @@
107
138
  return Math.random().toString(36).substring(2, 15);
108
139
  }
109
140
 
110
- 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 {
111
149
  let _h = _head || [];
112
150
  if (col_count[1] === "fixed" && _h.length < col_count[0]) {
113
151
  const fill = Array(col_count[0] - _h.length)
@@ -140,8 +178,8 @@
140
178
  const data_row_length = _values.length;
141
179
  return Array(row_count[1] === "fixed" ? row_count[0] : data_row_length)
142
180
  .fill(0)
143
- .map((_, i) =>
144
- Array(
181
+ .map((_, i) => {
182
+ return Array(
145
183
  col_count[1] === "fixed"
146
184
  ? col_count[0]
147
185
  : data_row_length > 0
@@ -155,16 +193,16 @@
155
193
  const obj = { value: _values?.[i]?.[j] ?? "", id };
156
194
  data_binding[id] = obj;
157
195
  return obj;
158
- })
159
- );
196
+ });
197
+ });
160
198
  }
161
199
 
162
- let _headers = make_headers(headers);
200
+ let _headers = make_headers(headers, col_count, els);
163
201
  let old_headers: string[] = headers;
164
202
 
165
203
  $: {
166
204
  if (!dequal(headers, old_headers)) {
167
- _headers = make_headers(headers);
205
+ _headers = make_headers(headers, col_count, els);
168
206
  old_headers = JSON.parse(JSON.stringify(headers));
169
207
  }
170
208
  }
@@ -181,6 +219,8 @@
181
219
  let previous_data = data.map((row) => row.map((cell) => String(cell.value)));
182
220
 
183
221
  async function trigger_change(): Promise<void> {
222
+ // shouldnt trigger if data changed due to search
223
+ if (current_search_query) return;
184
224
  const current_headers = _headers.map((h) => h.value);
185
225
  const current_data = data.map((row) =>
186
226
  row.map((cell) => String(cell.value))
@@ -312,8 +352,10 @@
312
352
  editing = false;
313
353
  } else {
314
354
  selected_cells = [next_coords];
315
- editing = next_coords;
316
- clear_on_focus = false;
355
+ if (editable) {
356
+ editing = next_coords;
357
+ clear_on_focus = false;
358
+ }
317
359
  }
318
360
  selected = next_coords;
319
361
  } else if (
@@ -333,27 +375,26 @@
333
375
  editing = false;
334
376
  break;
335
377
  case "Enter":
336
- if (!editable) break;
337
378
  event.preventDefault();
338
-
339
- if (event.shiftKey) {
340
- add_row(i);
341
- await tick();
342
-
343
- selected = [i + 1, j];
344
- } else {
345
- if (dequal(editing, [i, j])) {
346
- const cell_id = data[i][j].id;
347
- const input_el = els[cell_id].input;
348
- if (input_el) {
349
- data[i][j].value = input_el.value;
350
- }
351
- editing = false;
379
+ if (editable) {
380
+ if (event.shiftKey) {
381
+ add_row(i);
352
382
  await tick();
353
- selected = [i, j];
383
+ selected = [i + 1, j];
354
384
  } else {
355
- editing = [i, j];
356
- clear_on_focus = false;
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
+ }
357
398
  }
358
399
  }
359
400
  break;
@@ -390,15 +431,16 @@
390
431
  let sort_direction: SortDirection | undefined;
391
432
  let sort_by: number | undefined;
392
433
 
393
- function handle_sort(col: number): void {
434
+ function handle_sort(col: number, direction: SortDirection): void {
394
435
  if (typeof sort_by !== "number" || sort_by !== col) {
395
- sort_direction = "asc";
436
+ sort_direction = direction;
396
437
  sort_by = col;
397
- } else {
398
- if (sort_direction === "asc") {
399
- sort_direction = "des";
400
- } else if (sort_direction === "des") {
401
- 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;
402
444
  }
403
445
  }
404
446
  }
@@ -501,16 +543,25 @@
501
543
  let table: HTMLTableElement;
502
544
 
503
545
  function set_cell_widths(): void {
504
- const widths = cells.map((el, i) => {
505
- return el?.clientWidth || 0;
506
- });
546
+ const widths = cells.map((el) => el?.clientWidth || 0);
507
547
  if (widths.length === 0) return;
508
- for (let i = 0; i < widths.length; i++) {
509
- parent.style.setProperty(
510
- `--cell-width-${i}`,
511
- `${widths[i] - scrollbar_width / widths.length}px`
512
- );
548
+
549
+ if (show_row_numbers) {
550
+ parent.style.setProperty(`--cell-width-row-number`, `${widths[0]}px`);
513
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})`;
514
565
  }
515
566
 
516
567
  let table_height: number =
@@ -525,39 +576,14 @@
525
576
  dir?: SortDirection
526
577
  ): void {
527
578
  let id = null;
528
- //Checks if the selected cell is still in the data
529
- if (selected && selected[0] in data && selected[1] in data[selected[0]]) {
530
- 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;
531
581
  }
532
582
  if (typeof col !== "number" || !dir) {
533
583
  return;
534
584
  }
535
- const indices = [...Array(_data.length).keys()];
536
-
537
- if (dir === "asc") {
538
- indices.sort((i, j) =>
539
- _data[i][col].value < _data[j][col].value ? -1 : 1
540
- );
541
- } else if (dir === "des") {
542
- indices.sort((i, j) =>
543
- _data[i][col].value > _data[j][col].value ? -1 : 1
544
- );
545
- } else {
546
- return;
547
- }
548
-
549
- // sort all the data and metadata based on the values in the data
550
- const temp_data = [..._data];
551
- const temp_display_value = _display_value ? [..._display_value] : null;
552
- const temp_styling = _styling ? [..._styling] : null;
553
- indices.forEach((originalIndex, sortedIndex) => {
554
- _data[sortedIndex] = temp_data[originalIndex];
555
- if (_display_value && temp_display_value)
556
- _display_value[sortedIndex] = temp_display_value[originalIndex];
557
- if (_styling && temp_styling)
558
- _styling[sortedIndex] = temp_styling[originalIndex];
559
- });
560
585
 
586
+ sort_table_data(_data, _display_value, _styling, col, dir);
561
587
  data = data;
562
588
 
563
589
  if (id) {
@@ -604,8 +630,15 @@
604
630
  row: number,
605
631
  col: number
606
632
  ): void {
633
+ if (event.target instanceof HTMLAnchorElement) {
634
+ return;
635
+ }
636
+
607
637
  event.preventDefault();
608
638
  event.stopPropagation();
639
+
640
+ if (show_row_numbers && col === -1) return;
641
+
609
642
  clear_on_focus = false;
610
643
  active_cell_menu = null;
611
644
  active_header_menu = null;
@@ -613,19 +646,22 @@
613
646
  header_edit = false;
614
647
 
615
648
  selected_cells = handle_selection([row, col], selected_cells, event);
649
+ parent.focus();
616
650
 
617
- if (selected_cells.length === 1 && editable) {
618
- editing = [row, col];
619
- tick().then(() => {
620
- const input_el = els[data[row][col].id].input;
621
- if (input_el) {
622
- input_el.focus();
623
- input_el.selectionStart = input_el.selectionEnd =
624
- input_el.value.length;
625
- }
626
- });
627
- } else {
628
- editing = false;
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
+ }
629
665
  }
630
666
 
631
667
  toggle_cell_button(row, col);
@@ -671,6 +707,9 @@
671
707
  function handle_resize(): void {
672
708
  active_cell_menu = null;
673
709
  active_header_menu = null;
710
+ selected_cells = [];
711
+ selected = false;
712
+ editing = false;
674
713
  set_cell_widths();
675
714
  }
676
715
 
@@ -711,7 +750,11 @@
711
750
  }
712
751
 
713
752
  async function handle_copy(): Promise<void> {
714
- await copy_table_data(data, _headers, selected_cells);
753
+ await copy_table_data(data, selected_cells);
754
+ copy_flash = true;
755
+ setTimeout(() => {
756
+ copy_flash = false;
757
+ }, 800);
715
758
  }
716
759
 
717
760
  function toggle_header_menu(event: MouseEvent, col: number): void {
@@ -766,6 +809,94 @@
766
809
  active_cell_menu = null;
767
810
  active_header_menu = null;
768
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
+ }
769
900
  </script>
770
901
 
771
902
  <svelte:window on:resize={() => set_cell_widths()} />
@@ -783,6 +914,10 @@
783
914
  on:click={toggle_fullscreen}
784
915
  on_copy={handle_copy}
785
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}
786
921
  />
787
922
  </div>
788
923
  <div
@@ -790,11 +925,28 @@
790
925
  class="table-wrap"
791
926
  class:dragging
792
927
  class:no-wrap={!wrap}
793
- style="height:{table_height}px"
928
+ style="height:{table_height}px;"
929
+ class:menu-open={active_cell_menu || active_header_menu}
794
930
  on:keydown={(e) => handle_keydown(e)}
795
931
  role="grid"
796
932
  tabindex="0"
797
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}
798
950
  <table
799
951
  bind:contentRect={t_rect}
800
952
  bind:this={table}
@@ -806,40 +958,59 @@
806
958
  <thead>
807
959
  <tr>
808
960
  {#if show_row_numbers}
809
- <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>
810
971
  {/if}
811
972
  {#each _headers as { value, id }, i (id)}
812
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}
813
978
  class:editing={header_edit === i}
814
979
  aria-sort={get_sort_status(value, sort_by, sort_direction)}
815
- 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'};"
816
994
  >
817
995
  <div class="cell-wrap">
818
- <EditableCell
819
- {value}
820
- {latex_delimiters}
821
- {line_breaks}
822
- header
823
- edit={false}
824
- el={null}
825
- {root}
826
- {editable}
827
- />
828
-
829
- <div
830
- class:sorted={sort_by === i}
831
- class:des={sort_by === i && sort_direction === "des"}
832
- class="sort-button {sort_direction} "
833
- >
834
- <svg
835
- width="1em"
836
- height="1em"
837
- viewBox="0 0 9 7"
838
- fill="none"
839
- xmlns="http://www.w3.org/2000/svg"
840
- >
841
- <path d="M4.49999 0L8.3971 6.75H0.602875L4.49999 0Z" />
842
- </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>
843
1014
  </div>
844
1015
  </div>
845
1016
  </th>
@@ -878,9 +1049,12 @@
878
1049
  on:load={({ detail }) =>
879
1050
  handle_file_upload(
880
1051
  detail.data,
881
- col_count,
882
1052
  (head) => {
883
- _headers = make_headers(head);
1053
+ _headers = make_headers(
1054
+ head.map((h) => h ?? ""),
1055
+ col_count,
1056
+ els
1057
+ );
884
1058
  return _headers;
885
1059
  },
886
1060
  (vals) => {
@@ -896,19 +1070,44 @@
896
1070
  bind:actual_height={table_height}
897
1071
  bind:table_scrollbar_width={scrollbar_width}
898
1072
  selected={selected_index}
1073
+ disable_scroll={active_cell_menu !== null ||
1074
+ active_header_menu !== null}
899
1075
  >
900
1076
  {#if label && label.length !== 0}
901
1077
  <caption class="sr-only">{label}</caption>
902
1078
  {/if}
903
1079
  <tr slot="thead">
904
1080
  {#if show_row_numbers}
905
- <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>
906
1091
  {/if}
907
1092
  {#each _headers as { value, id }, i (id)}
908
1093
  <th
1094
+ class:frozen-column={i < actual_pinned_columns}
1095
+ class:last-frozen={i === actual_pinned_columns - 1}
909
1096
  class:focus={header_edit === i || selected_header === i}
910
1097
  aria-sort={get_sort_status(value, sort_by, sort_direction)}
911
- 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'};"
912
1111
  on:click={() => {
913
1112
  toggle_header_button(i);
914
1113
  }}
@@ -928,28 +1127,14 @@
928
1127
  {root}
929
1128
  {editable}
930
1129
  />
931
- <button
932
- class:sorted={sort_by === i}
933
- class:des={sort_by === i && sort_direction === "des"}
934
- class="sort-button {sort_direction}"
935
- tabindex="0"
936
- on:click={(event) => {
937
- event.stopPropagation();
938
- handle_sort(i);
939
- }}
940
- >
941
- <svg
942
- width="1em"
943
- height="1em"
944
- viewBox="0 0 9 7"
945
- fill="none"
946
- xmlns="http://www.w3.org/2000/svg"
947
- >
948
- <path d="M4.49999 0L8.3971 6.75H0.602875L4.49999 0Z" />
949
- </svg>
950
- </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>
951
1137
  </div>
952
-
953
1138
  {#if editable}
954
1139
  <button
955
1140
  class="cell-menu-button"
@@ -962,14 +1147,22 @@
962
1147
  </th>
963
1148
  {/each}
964
1149
  </tr>
965
-
966
1150
  <tr slot="tbody" let:item let:index class:row_odd={index % 2 === 0}>
967
1151
  {#if show_row_numbers}
968
- <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>
969
1159
  {/if}
970
1160
  {#each item as { value, id }, j (id)}
971
1161
  <td
972
- 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}
973
1166
  on:touchstart={(event) => {
974
1167
  const touch = event.touches[0];
975
1168
  const mouseEvent = new MouseEvent("click", {
@@ -986,8 +1179,21 @@
986
1179
  event.stopPropagation();
987
1180
  }}
988
1181
  on:click={(event) => handle_cell_click(event, index, j)}
989
- style:width="var(--cell-width-{j})"
990
- style={styling?.[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)}
991
1197
  class={is_cell_selected([index, j], selected_cells)}
992
1198
  class:menu-active={active_cell_menu &&
993
1199
  active_cell_menu.row === index &&
@@ -1077,6 +1283,14 @@
1077
1283
  {/if}
1078
1284
 
1079
1285
  <style>
1286
+ .label p {
1287
+ position: relative;
1288
+ z-index: var(--layer-4);
1289
+ margin-bottom: var(--size-2);
1290
+ color: var(--block-label-text-color);
1291
+ font-size: var(--block-label-text-size);
1292
+ }
1293
+
1080
1294
  .table-container {
1081
1295
  display: flex;
1082
1296
  flex-direction: column;
@@ -1088,6 +1302,9 @@
1088
1302
  transition: 150ms;
1089
1303
  border: 1px solid var(--border-color-primary);
1090
1304
  border-radius: var(--table-radius);
1305
+ }
1306
+
1307
+ .table-wrap.menu-open {
1091
1308
  overflow: hidden;
1092
1309
  }
1093
1310
 
@@ -1132,8 +1349,7 @@
1132
1349
  thead {
1133
1350
  position: sticky;
1134
1351
  top: 0;
1135
- left: 0;
1136
- z-index: var(--layer-1);
1352
+ z-index: var(--layer-2);
1137
1353
  box-shadow: var(--shadow-drop);
1138
1354
  }
1139
1355
 
@@ -1189,32 +1405,10 @@
1189
1405
  background: var(--table-even-background-fill);
1190
1406
  }
1191
1407
 
1192
- th svg {
1193
- fill: currentColor;
1194
- font-size: 10px;
1195
- }
1196
-
1197
- .sort-button {
1408
+ .sort-buttons {
1198
1409
  display: flex;
1199
- flex: none;
1200
- justify-content: center;
1201
1410
  align-items: center;
1202
- transition: 150ms;
1203
- cursor: pointer;
1204
- padding: var(--size-2);
1205
- color: var(--body-text-color-subdued);
1206
- }
1207
-
1208
- .sort-button:hover {
1209
- color: var(--body-text-color);
1210
- }
1211
-
1212
- .des {
1213
- transform: scaleY(-1);
1214
- }
1215
-
1216
- .sort-button.sorted {
1217
- color: var(--color-accent);
1411
+ flex-shrink: 0;
1218
1412
  }
1219
1413
 
1220
1414
  .editing {
@@ -1233,12 +1427,16 @@
1233
1427
  .header-content {
1234
1428
  display: flex;
1235
1429
  align-items: center;
1430
+ justify-content: space-between;
1236
1431
  overflow: hidden;
1237
1432
  flex-grow: 1;
1238
1433
  min-width: 0;
1239
1434
  white-space: normal;
1240
1435
  overflow-wrap: break-word;
1241
- word-break: break-word;
1436
+ word-break: normal;
1437
+ height: 100%;
1438
+ padding: var(--size-1);
1439
+ gap: var(--size-1);
1242
1440
  }
1243
1441
 
1244
1442
  .row_odd {
@@ -1275,46 +1473,55 @@
1275
1473
 
1276
1474
  .header-row {
1277
1475
  display: flex;
1278
- justify-content: space-between;
1476
+ justify-content: flex-end;
1279
1477
  align-items: center;
1280
1478
  gap: var(--size-2);
1281
- height: var(--size-6);
1282
1479
  min-height: var(--size-6);
1480
+ flex-wrap: nowrap;
1481
+ width: 100%;
1283
1482
  }
1284
1483
 
1285
1484
  .label {
1286
- flex: 1;
1485
+ flex: 1 1 auto;
1486
+ margin-right: auto;
1287
1487
  }
1288
1488
 
1289
1489
  .label p {
1290
1490
  margin: 0;
1291
1491
  color: var(--block-label-text-color);
1292
1492
  font-size: var(--block-label-text-size);
1493
+ line-height: var(--line-sm);
1494
+ }
1495
+
1496
+ .toolbar {
1497
+ flex: 0 0 auto;
1293
1498
  }
1294
1499
 
1295
1500
  .row-number,
1296
1501
  .row-number-header {
1297
- width: var(--size-7);
1298
- min-width: var(--size-7);
1299
1502
  text-align: center;
1300
1503
  background: var(--table-even-background-fill);
1301
- position: sticky;
1302
- left: 0;
1303
1504
  font-size: var(--input-text-size);
1304
1505
  color: var(--body-text-color);
1305
- padding: var(--size-1) var(--size-2);
1506
+ padding: var(--size-1);
1507
+ min-width: var(--size-12);
1508
+ width: var(--size-12);
1306
1509
  overflow: hidden;
1307
1510
  text-overflow: ellipsis;
1308
1511
  white-space: nowrap;
1309
1512
  font-weight: var(--weight-semibold);
1310
1513
  }
1311
1514
 
1312
- .row-number-header {
1313
- 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;
1314
1521
  }
1315
1522
 
1316
- .row-number {
1317
- z-index: var(--layer-1);
1523
+ .row-number-header :global(.sort-icons) {
1524
+ margin-right: 0;
1318
1525
  }
1319
1526
 
1320
1527
  :global(tbody > tr:nth-child(odd)) .row-number {
@@ -1411,4 +1618,68 @@
1411
1618
  .cell-selected.no-top.no-bottom.no-left.no-right {
1412
1619
  box-shadow: none;
1413
1620
  }
1621
+
1622
+ .selection-button {
1623
+ position: absolute;
1624
+ display: flex;
1625
+ align-items: center;
1626
+ justify-content: center;
1627
+ background: var(--color-accent);
1628
+ color: white;
1629
+ border-radius: var(--radius-sm);
1630
+ z-index: var(--layer-4);
1631
+ }
1632
+
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);
1639
+ }
1640
+
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);
1684
+ }
1414
1685
  </style>