@gradio/dataframe 0.15.0 → 0.16.1

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 (47) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/Dataframe.stories.svelte +183 -2
  3. package/Example.svelte +7 -0
  4. package/Index.svelte +20 -3
  5. package/dist/Example.svelte +7 -0
  6. package/dist/Index.svelte +16 -4
  7. package/dist/Index.svelte.d.ts +12 -0
  8. package/dist/shared/CellMenu.svelte +1 -1
  9. package/dist/shared/EditableCell.svelte +1 -6
  10. package/dist/shared/Table.svelte +620 -319
  11. package/dist/shared/Table.svelte.d.ts +3 -0
  12. package/dist/shared/Toolbar.svelte +122 -30
  13. package/dist/shared/Toolbar.svelte.d.ts +4 -0
  14. package/dist/shared/VirtualTable.svelte +70 -26
  15. package/dist/shared/VirtualTable.svelte.d.ts +1 -0
  16. package/dist/shared/icons/FilterIcon.svelte +11 -0
  17. package/dist/shared/icons/FilterIcon.svelte.d.ts +16 -0
  18. package/dist/shared/icons/SortIcon.svelte +90 -0
  19. package/dist/shared/icons/SortIcon.svelte.d.ts +20 -0
  20. package/dist/shared/selection_utils.d.ts +12 -2
  21. package/dist/shared/selection_utils.js +33 -5
  22. package/dist/shared/types.d.ts +16 -0
  23. package/dist/shared/types.js +1 -0
  24. package/dist/shared/utils/menu_utils.d.ts +42 -0
  25. package/dist/shared/utils/menu_utils.js +58 -0
  26. package/dist/shared/utils/sort_utils.d.ts +7 -0
  27. package/dist/shared/utils/sort_utils.js +39 -0
  28. package/dist/shared/utils/table_utils.d.ts +12 -0
  29. package/dist/shared/utils/table_utils.js +148 -0
  30. package/package.json +8 -8
  31. package/shared/CellMenu.svelte +1 -1
  32. package/shared/EditableCell.svelte +1 -6
  33. package/shared/Table.svelte +649 -322
  34. package/shared/Toolbar.svelte +125 -30
  35. package/shared/VirtualTable.svelte +73 -26
  36. package/shared/icons/FilterIcon.svelte +12 -0
  37. package/shared/icons/SortIcon.svelte +95 -0
  38. package/shared/selection_utils.ts +51 -9
  39. package/shared/types.ts +27 -0
  40. package/shared/utils/menu_utils.ts +115 -0
  41. package/shared/utils/sort_utils.test.ts +71 -0
  42. package/shared/utils/sort_utils.ts +55 -0
  43. package/shared/utils/table_utils.test.ts +114 -0
  44. package/shared/utils/table_utils.ts +206 -0
  45. package/dist/shared/table_utils.d.ts +0 -12
  46. package/dist/shared/table_utils.js +0 -113
  47. 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)
@@ -138,15 +176,14 @@
138
176
  id: string;
139
177
  }[][] {
140
178
  const data_row_length = _values.length;
179
+ if (data_row_length === 0) return [];
141
180
  return Array(row_count[1] === "fixed" ? row_count[0] : data_row_length)
142
181
  .fill(0)
143
- .map((_, i) =>
144
- Array(
182
+ .map((_, i) => {
183
+ return Array(
145
184
  col_count[1] === "fixed"
146
185
  ? col_count[0]
147
- : data_row_length > 0
148
- ? _values[0].length
149
- : headers.length
186
+ : _values[0].length || headers.length
150
187
  )
151
188
  .fill(0)
152
189
  .map((_, j) => {
@@ -155,16 +192,16 @@
155
192
  const obj = { value: _values?.[i]?.[j] ?? "", id };
156
193
  data_binding[id] = obj;
157
194
  return obj;
158
- })
159
- );
195
+ });
196
+ });
160
197
  }
161
198
 
162
- let _headers = make_headers(headers);
199
+ let _headers = make_headers(headers, col_count, els);
163
200
  let old_headers: string[] = headers;
164
201
 
165
202
  $: {
166
203
  if (!dequal(headers, old_headers)) {
167
- _headers = make_headers(headers);
204
+ _headers = make_headers(headers, col_count, els);
168
205
  old_headers = JSON.parse(JSON.stringify(headers));
169
206
  }
170
207
  }
@@ -181,6 +218,8 @@
181
218
  let previous_data = data.map((row) => row.map((cell) => String(cell.value)));
182
219
 
183
220
  async function trigger_change(): Promise<void> {
221
+ // shouldnt trigger if data changed due to search
222
+ if (current_search_query) return;
184
223
  const current_headers = _headers.map((h) => h.value);
185
224
  const current_data = data.map((row) =>
186
225
  row.map((cell) => String(cell.value))
@@ -312,8 +351,10 @@
312
351
  editing = false;
313
352
  } else {
314
353
  selected_cells = [next_coords];
315
- editing = next_coords;
316
- clear_on_focus = false;
354
+ if (editable) {
355
+ editing = next_coords;
356
+ clear_on_focus = false;
357
+ }
317
358
  }
318
359
  selected = next_coords;
319
360
  } else if (
@@ -333,27 +374,26 @@
333
374
  editing = false;
334
375
  break;
335
376
  case "Enter":
336
- if (!editable) break;
337
377
  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;
378
+ if (editable) {
379
+ if (event.shiftKey) {
380
+ add_row(i);
352
381
  await tick();
353
- selected = [i, j];
382
+ selected = [i + 1, j];
354
383
  } else {
355
- editing = [i, j];
356
- clear_on_focus = false;
384
+ if (dequal(editing, [i, j])) {
385
+ const cell_id = data[i][j].id;
386
+ const input_el = els[cell_id].input;
387
+ if (input_el) {
388
+ data[i][j].value = input_el.value;
389
+ }
390
+ editing = false;
391
+ await tick();
392
+ selected = [i, j];
393
+ } else {
394
+ editing = [i, j];
395
+ clear_on_focus = false;
396
+ }
357
397
  }
358
398
  }
359
399
  break;
@@ -390,15 +430,16 @@
390
430
  let sort_direction: SortDirection | undefined;
391
431
  let sort_by: number | undefined;
392
432
 
393
- function handle_sort(col: number): void {
433
+ function handle_sort(col: number, direction: SortDirection): void {
394
434
  if (typeof sort_by !== "number" || sort_by !== col) {
395
- sort_direction = "asc";
435
+ sort_direction = direction;
396
436
  sort_by = col;
397
- } else {
398
- if (sort_direction === "asc") {
399
- sort_direction = "des";
400
- } else if (sort_direction === "des") {
401
- sort_direction = "asc";
437
+ } else if (sort_by === col) {
438
+ if (sort_direction === direction) {
439
+ sort_direction = undefined;
440
+ sort_by = undefined;
441
+ } else {
442
+ sort_direction = direction;
402
443
  }
403
444
  }
404
445
  }
@@ -431,12 +472,8 @@
431
472
  parent.focus();
432
473
 
433
474
  if (row_count[1] !== "dynamic") return;
434
- if (data.length === 0) {
435
- values = [Array(headers.length).fill("")];
436
- return;
437
- }
438
475
 
439
- const new_row = Array(data[0].length)
476
+ const new_row = Array(data[0]?.length || headers.length)
440
477
  .fill(0)
441
478
  .map((_, i) => {
442
479
  const _id = make_id();
@@ -444,7 +481,9 @@
444
481
  return { id: _id, value: "" };
445
482
  });
446
483
 
447
- if (index !== undefined && index >= 0 && index <= data.length) {
484
+ if (data.length === 0) {
485
+ data = [new_row];
486
+ } else if (index !== undefined && index >= 0 && index <= data.length) {
448
487
  data.splice(index, 0, new_row);
449
488
  } else {
450
489
  data.push(new_row);
@@ -501,16 +540,25 @@
501
540
  let table: HTMLTableElement;
502
541
 
503
542
  function set_cell_widths(): void {
504
- const widths = cells.map((el, i) => {
505
- return el?.clientWidth || 0;
506
- });
543
+ const widths = cells.map((el) => el?.clientWidth || 0);
507
544
  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
- );
545
+
546
+ if (show_row_numbers) {
547
+ parent.style.setProperty(`--cell-width-row-number`, `${widths[0]}px`);
513
548
  }
549
+ const data_cells = show_row_numbers ? widths.slice(1) : widths;
550
+ data_cells.forEach((width, i) => {
551
+ if (!column_widths[i]) {
552
+ parent.style.setProperty(
553
+ `--cell-width-${i}`,
554
+ `${width - scrollbar_width / data_cells.length}px`
555
+ );
556
+ }
557
+ });
558
+ }
559
+
560
+ function get_cell_width(index: number): string {
561
+ return column_widths[index] || `var(--cell-width-${index})`;
514
562
  }
515
563
 
516
564
  let table_height: number =
@@ -525,39 +573,14 @@
525
573
  dir?: SortDirection
526
574
  ): void {
527
575
  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;
576
+ if (selected && selected[0] in _data && selected[1] in _data[selected[0]]) {
577
+ id = _data[selected[0]][selected[1]].id;
531
578
  }
532
579
  if (typeof col !== "number" || !dir) {
533
580
  return;
534
581
  }
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
582
 
583
+ sort_table_data(_data, _display_value, _styling, col, dir);
561
584
  data = data;
562
585
 
563
586
  if (id) {
@@ -604,8 +627,15 @@
604
627
  row: number,
605
628
  col: number
606
629
  ): void {
630
+ if (event.target instanceof HTMLAnchorElement) {
631
+ return;
632
+ }
633
+
607
634
  event.preventDefault();
608
635
  event.stopPropagation();
636
+
637
+ if (show_row_numbers && col === -1) return;
638
+
609
639
  clear_on_focus = false;
610
640
  active_cell_menu = null;
611
641
  active_header_menu = null;
@@ -613,19 +643,22 @@
613
643
  header_edit = false;
614
644
 
615
645
  selected_cells = handle_selection([row, col], selected_cells, event);
646
+ parent.focus();
616
647
 
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;
648
+ if (editable) {
649
+ if (selected_cells.length === 1) {
650
+ editing = [row, col];
651
+ tick().then(() => {
652
+ const input_el = els[data[row][col].id].input;
653
+ if (input_el) {
654
+ input_el.focus();
655
+ input_el.selectionStart = input_el.selectionEnd =
656
+ input_el.value.length;
657
+ }
658
+ });
659
+ } else {
660
+ editing = false;
661
+ }
629
662
  }
630
663
 
631
664
  toggle_cell_button(row, col);
@@ -671,6 +704,9 @@
671
704
  function handle_resize(): void {
672
705
  active_cell_menu = null;
673
706
  active_header_menu = null;
707
+ selected_cells = [];
708
+ selected = false;
709
+ editing = false;
674
710
  set_cell_widths();
675
711
  }
676
712
 
@@ -711,7 +747,11 @@
711
747
  }
712
748
 
713
749
  async function handle_copy(): Promise<void> {
714
- await copy_table_data(data, _headers, selected_cells);
750
+ await copy_table_data(data, selected_cells);
751
+ copy_flash = true;
752
+ setTimeout(() => {
753
+ copy_flash = false;
754
+ }, 800);
715
755
  }
716
756
 
717
757
  function toggle_header_menu(event: MouseEvent, col: number): void {
@@ -743,15 +783,17 @@
743
783
  async function delete_col(index: number): Promise<void> {
744
784
  parent.focus();
745
785
  if (col_count[1] !== "dynamic") return;
746
- if (data[0].length <= 1) return;
786
+ if (_headers.length <= 1) return;
747
787
 
748
788
  _headers.splice(index, 1);
749
789
  _headers = _headers;
750
790
 
751
- data.forEach((row) => {
752
- row.splice(index, 1);
753
- });
754
- data = data;
791
+ if (data.length > 0) {
792
+ data.forEach((row) => {
793
+ row.splice(index, 1);
794
+ });
795
+ data = data;
796
+ }
755
797
  selected = false;
756
798
  }
757
799
 
@@ -766,35 +808,146 @@
766
808
  active_cell_menu = null;
767
809
  active_header_menu = null;
768
810
  }
811
+
812
+ let row_order: number[] = [];
813
+
814
+ $: {
815
+ if (
816
+ typeof sort_by === "number" &&
817
+ sort_direction &&
818
+ sort_by >= 0 &&
819
+ sort_by < data[0].length
820
+ ) {
821
+ const indices = [...Array(data.length)].map((_, i) => i);
822
+ const sort_index = sort_by as number;
823
+ indices.sort((a, b) => {
824
+ const row_a = data[a];
825
+ const row_b = data[b];
826
+ if (
827
+ !row_a ||
828
+ !row_b ||
829
+ sort_index >= row_a.length ||
830
+ sort_index >= row_b.length
831
+ )
832
+ return 0;
833
+ const val_a = row_a[sort_index].value;
834
+ const val_b = row_b[sort_index].value;
835
+ const comp = val_a < val_b ? -1 : val_a > val_b ? 1 : 0;
836
+ return sort_direction === "asc" ? comp : -comp;
837
+ });
838
+ row_order = indices;
839
+ } else {
840
+ row_order = [...Array(data.length)].map((_, i) => i);
841
+ }
842
+ }
843
+
844
+ function handle_select_column(col: number): void {
845
+ selected_cells = select_column(data, col);
846
+ selected = selected_cells[0];
847
+ editing = false;
848
+ }
849
+
850
+ function handle_select_row(row: number): void {
851
+ selected_cells = select_row(data, row);
852
+ selected = selected_cells[0];
853
+ editing = false;
854
+ }
855
+
856
+ let coords: CellCoordinate;
857
+ $: if (selected !== false) coords = selected;
858
+
859
+ $: if (selected !== false) {
860
+ const positions = calculate_selection_positions(
861
+ selected,
862
+ data,
863
+ els,
864
+ parent,
865
+ table
866
+ );
867
+ document.documentElement.style.setProperty(
868
+ "--selected-col-pos",
869
+ positions.col_pos
870
+ );
871
+ if (positions.row_pos) {
872
+ document.documentElement.style.setProperty(
873
+ "--selected-row-pos",
874
+ positions.row_pos
875
+ );
876
+ }
877
+ }
878
+
879
+ let current_search_query: string | null = null;
880
+
881
+ function handle_search(search_query: string | null): void {
882
+ current_search_query = search_query;
883
+ dispatch("search", search_query);
884
+ }
885
+
886
+ function commit_filter(): void {
887
+ if (current_search_query && show_search === "filter") {
888
+ dispatch("change", {
889
+ data: data.map((row) => row.map((cell) => cell.value)),
890
+ headers: _headers.map((h) => h.value),
891
+ metadata: null
892
+ });
893
+ if (!value_is_output) {
894
+ dispatch("input");
895
+ }
896
+ current_search_query = null;
897
+ }
898
+ }
769
899
  </script>
770
900
 
771
901
  <svelte:window on:resize={() => set_cell_widths()} />
772
902
 
773
903
  <div class="table-container">
774
- <div class="header-row">
775
- {#if label && label.length !== 0 && show_label}
776
- <div class="label">
777
- <p>{label}</p>
778
- </div>
779
- {/if}
780
- <Toolbar
781
- {show_fullscreen_button}
782
- {is_fullscreen}
783
- on:click={toggle_fullscreen}
784
- on_copy={handle_copy}
785
- {show_copy_button}
786
- />
787
- </div>
904
+ {#if (label && label.length !== 0 && show_label) || show_fullscreen_button || show_copy_button || show_search !== "none"}
905
+ <div class="header-row">
906
+ {#if label && label.length !== 0 && show_label}
907
+ <div class="label">
908
+ <p>{label}</p>
909
+ </div>
910
+ {/if}
911
+ <Toolbar
912
+ {show_fullscreen_button}
913
+ {is_fullscreen}
914
+ on:click={toggle_fullscreen}
915
+ on_copy={handle_copy}
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}
921
+ />
922
+ </div>
923
+ {/if}
788
924
  <div
789
925
  bind:this={parent}
790
926
  class="table-wrap"
791
927
  class:dragging
792
928
  class:no-wrap={!wrap}
793
- style="height:{table_height}px"
929
+ style="height:{table_height}px;"
930
+ class:menu-open={active_cell_menu || active_header_menu}
794
931
  on:keydown={(e) => handle_keydown(e)}
795
932
  role="grid"
796
933
  tabindex="0"
797
934
  >
935
+ {#if selected !== false && selected_cells.length === 1}
936
+ <button
937
+ class="selection-button selection-button-column"
938
+ on:click|stopPropagation={() => handle_select_column(coords[1])}
939
+ aria-label="Select column"
940
+ >
941
+ &#8942;
942
+ </button>
943
+ <button
944
+ class="selection-button selection-button-row"
945
+ on:click|stopPropagation={() => handle_select_row(coords[0])}
946
+ aria-label="Select row"
947
+ >
948
+ &#8942;
949
+ </button>
950
+ {/if}
798
951
  <table
799
952
  bind:contentRect={t_rect}
800
953
  bind:this={table}
@@ -806,40 +959,59 @@
806
959
  <thead>
807
960
  <tr>
808
961
  {#if show_row_numbers}
809
- <th class="row-number-header"></th>
962
+ <th
963
+ class="row-number-header frozen-column always-frozen"
964
+ style="left: 0;"
965
+ >
966
+ <div class="cell-wrap">
967
+ <div class="header-content">
968
+ <div class="header-text"></div>
969
+ </div>
970
+ </div>
971
+ </th>
810
972
  {/if}
811
973
  {#each _headers as { value, id }, i (id)}
812
974
  <th
975
+ class:frozen-column={i < actual_pinned_columns}
976
+ class:last-frozen={show_row_numbers
977
+ ? i === actual_pinned_columns - 1
978
+ : i === actual_pinned_columns - 1}
813
979
  class:editing={header_edit === i}
814
980
  aria-sort={get_sort_status(value, sort_by, sort_direction)}
815
- style:width={column_widths.length ? column_widths[i] : undefined}
981
+ style="width: {column_widths.length
982
+ ? column_widths[i]
983
+ : undefined}; left: {i < actual_pinned_columns
984
+ ? i === 0
985
+ ? show_row_numbers
986
+ ? 'var(--cell-width-row-number)'
987
+ : '0'
988
+ : `calc(${show_row_numbers ? 'var(--cell-width-row-number) + ' : ''}${Array(
989
+ i
990
+ )
991
+ .fill(0)
992
+ .map((_, idx) => `var(--cell-width-${idx})`)
993
+ .join(' + ')})`
994
+ : 'auto'};"
816
995
  >
817
996
  <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>
997
+ <div class="header-content">
998
+ <EditableCell
999
+ {value}
1000
+ {latex_delimiters}
1001
+ {line_breaks}
1002
+ header
1003
+ edit={false}
1004
+ el={null}
1005
+ {root}
1006
+ {editable}
1007
+ />
1008
+ <div class="sort-buttons">
1009
+ <SortIcon
1010
+ direction={sort_by === i ? sort_direction : null}
1011
+ on:sort={({ detail }) => handle_sort(i, detail)}
1012
+ {i18n}
1013
+ />
1014
+ </div>
843
1015
  </div>
844
1016
  </div>
845
1017
  </th>
@@ -878,9 +1050,12 @@
878
1050
  on:load={({ detail }) =>
879
1051
  handle_file_upload(
880
1052
  detail.data,
881
- col_count,
882
1053
  (head) => {
883
- _headers = make_headers(head);
1054
+ _headers = make_headers(
1055
+ head.map((h) => h ?? ""),
1056
+ col_count,
1057
+ els
1058
+ );
884
1059
  return _headers;
885
1060
  },
886
1061
  (vals) => {
@@ -890,152 +1065,205 @@
890
1065
  bind:dragging
891
1066
  aria_label={i18n("dataframe.drop_to_upload")}
892
1067
  >
893
- <VirtualTable
894
- bind:items={data}
895
- {max_height}
896
- bind:actual_height={table_height}
897
- bind:table_scrollbar_width={scrollbar_width}
898
- selected={selected_index}
899
- >
900
- {#if label && label.length !== 0}
901
- <caption class="sr-only">{label}</caption>
902
- {/if}
903
- <tr slot="thead">
904
- {#if show_row_numbers}
905
- <th class="row-number-header"></th>
1068
+ <div class="table-wrap">
1069
+ <VirtualTable
1070
+ bind:items={data}
1071
+ {max_height}
1072
+ bind:actual_height={table_height}
1073
+ bind:table_scrollbar_width={scrollbar_width}
1074
+ selected={selected_index}
1075
+ disable_scroll={active_cell_menu !== null ||
1076
+ active_header_menu !== null}
1077
+ >
1078
+ {#if label && label.length !== 0}
1079
+ <caption class="sr-only">{label}</caption>
906
1080
  {/if}
907
- {#each _headers as { value, id }, i (id)}
908
- <th
909
- class:focus={header_edit === i || selected_header === i}
910
- aria-sort={get_sort_status(value, sort_by, sort_direction)}
911
- style="width: var(--cell-width-{i});"
912
- on:click={() => {
913
- toggle_header_button(i);
914
- }}
915
- >
916
- <div class="cell-wrap">
917
- <div class="header-content">
1081
+ <tr slot="thead">
1082
+ {#if show_row_numbers}
1083
+ <th
1084
+ class="row-number-header frozen-column always-frozen"
1085
+ style="left: 0;"
1086
+ >
1087
+ <div class="cell-wrap">
1088
+ <div class="header-content">
1089
+ <div class="header-text"></div>
1090
+ </div>
1091
+ </div>
1092
+ </th>
1093
+ {/if}
1094
+ {#each _headers as { value, id }, i (id)}
1095
+ <th
1096
+ class:frozen-column={i < actual_pinned_columns}
1097
+ class:last-frozen={i === actual_pinned_columns - 1}
1098
+ class:focus={header_edit === i || selected_header === i}
1099
+ aria-sort={get_sort_status(value, sort_by, sort_direction)}
1100
+ style="width: {get_cell_width(i)}; left: {i <
1101
+ actual_pinned_columns
1102
+ ? i === 0
1103
+ ? show_row_numbers
1104
+ ? 'var(--cell-width-row-number)'
1105
+ : '0'
1106
+ : `calc(${show_row_numbers ? 'var(--cell-width-row-number) + ' : ''}${Array(
1107
+ i
1108
+ )
1109
+ .fill(0)
1110
+ .map((_, idx) => `var(--cell-width-${idx})`)
1111
+ .join(' + ')})`
1112
+ : 'auto'};"
1113
+ on:click={() => {
1114
+ toggle_header_button(i);
1115
+ }}
1116
+ >
1117
+ <div class="cell-wrap">
1118
+ <div class="header-content">
1119
+ <EditableCell
1120
+ {max_chars}
1121
+ bind:value={_headers[i].value}
1122
+ bind:el={els[id].input}
1123
+ {latex_delimiters}
1124
+ {line_breaks}
1125
+ edit={header_edit === i}
1126
+ on:keydown={end_header_edit}
1127
+ on:dblclick={() => edit_header(i)}
1128
+ header
1129
+ {root}
1130
+ {editable}
1131
+ />
1132
+ <div class="sort-buttons">
1133
+ <SortIcon
1134
+ direction={sort_by === i ? sort_direction : null}
1135
+ on:sort={({ detail }) => handle_sort(i, detail)}
1136
+ {i18n}
1137
+ />
1138
+ </div>
1139
+ </div>
1140
+ {#if editable}
1141
+ <button
1142
+ class="cell-menu-button"
1143
+ on:click={(event) => toggle_header_menu(event, i)}
1144
+ on:touchstart={(event) => {
1145
+ event.preventDefault();
1146
+ const touch = event.touches[0];
1147
+ const mouseEvent = new MouseEvent("click", {
1148
+ clientX: touch.clientX,
1149
+ clientY: touch.clientY,
1150
+ bubbles: true,
1151
+ cancelable: true,
1152
+ view: window
1153
+ });
1154
+ toggle_header_menu(mouseEvent, i);
1155
+ }}
1156
+ >
1157
+ &#8942;
1158
+ </button>
1159
+ {/if}
1160
+ </div>
1161
+ </th>
1162
+ {/each}
1163
+ </tr>
1164
+ <tr slot="tbody" let:item let:index class:row_odd={index % 2 === 0}>
1165
+ {#if show_row_numbers}
1166
+ <td
1167
+ class="row-number frozen-column always-frozen"
1168
+ style="left: 0;"
1169
+ tabindex="-1"
1170
+ >
1171
+ {index + 1}
1172
+ </td>
1173
+ {/if}
1174
+ {#each item as { value, id }, j (id)}
1175
+ <td
1176
+ class:frozen-column={j < actual_pinned_columns}
1177
+ class:last-frozen={j === actual_pinned_columns - 1}
1178
+ tabindex={show_row_numbers && j === 0 ? -1 : 0}
1179
+ bind:this={els[id].cell}
1180
+ on:touchstart={(event) => {
1181
+ const touch = event.touches[0];
1182
+ const mouseEvent = new MouseEvent("click", {
1183
+ clientX: touch.clientX,
1184
+ clientY: touch.clientY,
1185
+ bubbles: true,
1186
+ cancelable: true,
1187
+ view: window
1188
+ });
1189
+ handle_cell_click(mouseEvent, index, j);
1190
+ }}
1191
+ on:mousedown={(event) => {
1192
+ event.preventDefault();
1193
+ event.stopPropagation();
1194
+ }}
1195
+ on:click={(event) => handle_cell_click(event, index, j)}
1196
+ style="width: {get_cell_width(j)}; left: {j <
1197
+ actual_pinned_columns
1198
+ ? j === 0
1199
+ ? show_row_numbers
1200
+ ? 'var(--cell-width-row-number)'
1201
+ : '0'
1202
+ : `calc(${show_row_numbers ? 'var(--cell-width-row-number) + ' : ''}${Array(
1203
+ j
1204
+ )
1205
+ .fill(0)
1206
+ .map((_, idx) => `var(--cell-width-${idx})`)
1207
+ .join(' + ')})`
1208
+ : 'auto'}; {styling?.[index]?.[j] || ''}"
1209
+ class:flash={copy_flash &&
1210
+ is_cell_selected([index, j], selected_cells)}
1211
+ class={is_cell_selected([index, j], selected_cells)}
1212
+ class:menu-active={active_cell_menu &&
1213
+ active_cell_menu.row === index &&
1214
+ active_cell_menu.col === j}
1215
+ >
1216
+ <div class="cell-wrap">
918
1217
  <EditableCell
919
- {max_chars}
920
- bind:value={_headers[i].value}
1218
+ bind:value={data[index][j].value}
921
1219
  bind:el={els[id].input}
1220
+ display_value={display_value?.[index]?.[j]}
922
1221
  {latex_delimiters}
923
1222
  {line_breaks}
924
- edit={header_edit === i}
925
- on:keydown={end_header_edit}
926
- on:dblclick={() => edit_header(i)}
927
- header
928
- {root}
929
1223
  {editable}
930
- />
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);
1224
+ edit={dequal(editing, [index, j])}
1225
+ datatype={Array.isArray(datatype) ? datatype[j] : datatype}
1226
+ on:blur={() => {
1227
+ clear_on_focus = false;
1228
+ parent.focus();
1229
+ }}
1230
+ on:focus={() => {
1231
+ const row = index;
1232
+ const col = j;
1233
+ if (
1234
+ !selected_cells.some(([r, c]) => r === row && c === col)
1235
+ ) {
1236
+ selected_cells = [[row, col]];
1237
+ }
939
1238
  }}
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"
1239
+ {clear_on_focus}
1240
+ {root}
1241
+ {max_chars}
1242
+ />
1243
+ {#if editable && should_show_cell_menu([index, j], selected_cells, editable)}
1244
+ <button
1245
+ class="cell-menu-button"
1246
+ on:click={(event) => toggle_cell_menu(event, index, j)}
947
1247
  >
948
- <path d="M4.49999 0L8.3971 6.75H0.602875L4.49999 0Z" />
949
- </svg>
950
- </button>
1248
+ &#8942;
1249
+ </button>
1250
+ {/if}
951
1251
  </div>
952
-
953
- {#if editable}
954
- <button
955
- class="cell-menu-button"
956
- on:click={(event) => toggle_header_menu(event, i)}
957
- >
958
- &#8942;
959
- </button>
960
- {/if}
961
- </div>
962
- </th>
963
- {/each}
964
- </tr>
965
-
966
- <tr slot="tbody" let:item let:index class:row_odd={index % 2 === 0}>
967
- {#if show_row_numbers}
968
- <td class="row-number" title={`Row ${index + 1}`}>{index + 1}</td>
969
- {/if}
970
- {#each item as { value, id }, j (id)}
971
- <td
972
- tabindex="0"
973
- on:touchstart={(event) => {
974
- const touch = event.touches[0];
975
- const mouseEvent = new MouseEvent("click", {
976
- clientX: touch.clientX,
977
- clientY: touch.clientY,
978
- bubbles: true,
979
- cancelable: true,
980
- view: window
981
- });
982
- handle_cell_click(mouseEvent, index, j);
983
- }}
984
- on:mousedown={(event) => {
985
- event.preventDefault();
986
- event.stopPropagation();
987
- }}
988
- on:click={(event) => handle_cell_click(event, index, j)}
989
- style:width="var(--cell-width-{j})"
990
- style={styling?.[index]?.[j] || ""}
991
- class={is_cell_selected([index, j], selected_cells)}
992
- class:menu-active={active_cell_menu &&
993
- active_cell_menu.row === index &&
994
- active_cell_menu.col === j}
995
- >
996
- <div class="cell-wrap">
997
- <EditableCell
998
- bind:value={data[index][j].value}
999
- bind:el={els[id].input}
1000
- display_value={display_value?.[index]?.[j]}
1001
- {latex_delimiters}
1002
- {line_breaks}
1003
- {editable}
1004
- edit={dequal(editing, [index, j])}
1005
- datatype={Array.isArray(datatype) ? datatype[j] : datatype}
1006
- on:blur={() => {
1007
- clear_on_focus = false;
1008
- parent.focus();
1009
- }}
1010
- on:focus={() => {
1011
- const row = index;
1012
- const col = j;
1013
- if (
1014
- !selected_cells.some(([r, c]) => r === row && c === col)
1015
- ) {
1016
- selected_cells = [[row, col]];
1017
- }
1018
- }}
1019
- {clear_on_focus}
1020
- {root}
1021
- {max_chars}
1022
- />
1023
- {#if editable && should_show_cell_menu([index, j], selected_cells, editable)}
1024
- <button
1025
- class="cell-menu-button"
1026
- on:click={(event) => toggle_cell_menu(event, index, j)}
1027
- >
1028
- &#8942;
1029
- </button>
1030
- {/if}
1031
- </div>
1032
- </td>
1033
- {/each}
1034
- </tr>
1035
- </VirtualTable>
1252
+ </td>
1253
+ {/each}
1254
+ </tr>
1255
+ </VirtualTable>
1256
+ </div>
1036
1257
  </Upload>
1037
1258
  </div>
1038
1259
  </div>
1260
+ {#if data.length === 0 && editable && row_count[1] === "dynamic"}
1261
+ <div class="add-row-container">
1262
+ <button class="add-row-button" on:click={() => add_row()}>
1263
+ <span>+</span>
1264
+ </button>
1265
+ </div>
1266
+ {/if}
1039
1267
 
1040
1268
  {#if active_cell_menu}
1041
1269
  <CellMenu
@@ -1072,11 +1300,19 @@
1072
1300
  on_delete_row={() => delete_row_at(active_cell_menu?.row ?? -1)}
1073
1301
  on_delete_col={() => delete_col_at(active_header_menu?.col ?? -1)}
1074
1302
  can_delete_rows={false}
1075
- can_delete_cols={data[0].length > 1}
1303
+ can_delete_cols={_headers.length > 1}
1076
1304
  />
1077
1305
  {/if}
1078
1306
 
1079
1307
  <style>
1308
+ .label p {
1309
+ position: relative;
1310
+ z-index: var(--layer-4);
1311
+ margin-bottom: var(--size-2);
1312
+ color: var(--block-label-text-color);
1313
+ font-size: var(--block-label-text-size);
1314
+ }
1315
+
1080
1316
  .table-container {
1081
1317
  display: flex;
1082
1318
  flex-direction: column;
@@ -1086,8 +1322,9 @@
1086
1322
  .table-wrap {
1087
1323
  position: relative;
1088
1324
  transition: 150ms;
1089
- border: 1px solid var(--border-color-primary);
1090
- border-radius: var(--table-radius);
1325
+ }
1326
+
1327
+ .table-wrap.menu-open {
1091
1328
  overflow: hidden;
1092
1329
  }
1093
1330
 
@@ -1117,6 +1354,12 @@
1117
1354
  border-collapse: separate;
1118
1355
  }
1119
1356
 
1357
+ .table-wrap > :global(button) {
1358
+ border: 1px solid var(--border-color-primary);
1359
+ border-radius: var(--table-radius);
1360
+ overflow: hidden;
1361
+ }
1362
+
1120
1363
  div:not(.no-wrap) td {
1121
1364
  overflow-wrap: anywhere;
1122
1365
  }
@@ -1132,8 +1375,7 @@
1132
1375
  thead {
1133
1376
  position: sticky;
1134
1377
  top: 0;
1135
- left: 0;
1136
- z-index: var(--layer-1);
1378
+ z-index: var(--layer-2);
1137
1379
  box-shadow: var(--shadow-drop);
1138
1380
  }
1139
1381
 
@@ -1160,10 +1402,12 @@
1160
1402
 
1161
1403
  th:first-child {
1162
1404
  border-top-left-radius: var(--table-radius);
1405
+ border-bottom-left-radius: var(--table-radius);
1163
1406
  }
1164
1407
 
1165
1408
  th:last-child {
1166
1409
  border-top-right-radius: var(--table-radius);
1410
+ border-bottom-right-radius: var(--table-radius);
1167
1411
  }
1168
1412
 
1169
1413
  th.focus,
@@ -1189,32 +1433,11 @@
1189
1433
  background: var(--table-even-background-fill);
1190
1434
  }
1191
1435
 
1192
- th svg {
1193
- fill: currentColor;
1194
- font-size: 10px;
1195
- }
1196
-
1197
- .sort-button {
1436
+ .sort-buttons {
1198
1437
  display: flex;
1199
- flex: none;
1200
- justify-content: center;
1201
1438
  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);
1439
+ flex-shrink: 0;
1440
+ order: -1;
1218
1441
  }
1219
1442
 
1220
1443
  .editing {
@@ -1223,11 +1446,19 @@
1223
1446
 
1224
1447
  .cell-wrap {
1225
1448
  display: flex;
1226
- align-items: flex-start;
1449
+ align-items: center;
1450
+ justify-content: flex-start;
1227
1451
  outline: none;
1228
1452
  min-height: var(--size-9);
1229
1453
  position: relative;
1230
- height: auto;
1454
+ height: 100%;
1455
+ padding: var(--size-2);
1456
+ box-sizing: border-box;
1457
+ margin: 0;
1458
+ gap: var(--size-1);
1459
+ overflow: visible;
1460
+ min-width: 0;
1461
+ border-radius: var(--table-radius);
1231
1462
  }
1232
1463
 
1233
1464
  .header-content {
@@ -1238,7 +1469,9 @@
1238
1469
  min-width: 0;
1239
1470
  white-space: normal;
1240
1471
  overflow-wrap: break-word;
1241
- word-break: break-word;
1472
+ word-break: normal;
1473
+ height: 100%;
1474
+ gap: var(--size-1);
1242
1475
  }
1243
1476
 
1244
1477
  .row_odd {
@@ -1267,7 +1500,8 @@
1267
1500
  transform: translateY(-50%);
1268
1501
  }
1269
1502
 
1270
- .cell-selected .cell-menu-button {
1503
+ .cell-selected .cell-menu-button,
1504
+ th:hover .cell-menu-button {
1271
1505
  display: flex;
1272
1506
  align-items: center;
1273
1507
  justify-content: center;
@@ -1275,46 +1509,55 @@
1275
1509
 
1276
1510
  .header-row {
1277
1511
  display: flex;
1278
- justify-content: space-between;
1512
+ justify-content: flex-end;
1279
1513
  align-items: center;
1280
1514
  gap: var(--size-2);
1281
- height: var(--size-6);
1282
1515
  min-height: var(--size-6);
1516
+ flex-wrap: nowrap;
1517
+ width: 100%;
1283
1518
  }
1284
1519
 
1285
1520
  .label {
1286
- flex: 1;
1521
+ flex: 1 1 auto;
1522
+ margin-right: auto;
1287
1523
  }
1288
1524
 
1289
1525
  .label p {
1290
1526
  margin: 0;
1291
1527
  color: var(--block-label-text-color);
1292
1528
  font-size: var(--block-label-text-size);
1529
+ line-height: var(--line-sm);
1530
+ }
1531
+
1532
+ .toolbar {
1533
+ flex: 0 0 auto;
1293
1534
  }
1294
1535
 
1295
1536
  .row-number,
1296
1537
  .row-number-header {
1297
- width: var(--size-7);
1298
- min-width: var(--size-7);
1299
1538
  text-align: center;
1300
1539
  background: var(--table-even-background-fill);
1301
- position: sticky;
1302
- left: 0;
1303
1540
  font-size: var(--input-text-size);
1304
1541
  color: var(--body-text-color);
1305
- padding: var(--size-1) var(--size-2);
1542
+ padding: var(--size-1);
1543
+ min-width: var(--size-12);
1544
+ width: var(--size-12);
1306
1545
  overflow: hidden;
1307
1546
  text-overflow: ellipsis;
1308
1547
  white-space: nowrap;
1309
1548
  font-weight: var(--weight-semibold);
1310
1549
  }
1311
1550
 
1312
- .row-number-header {
1313
- z-index: var(--layer-2);
1551
+ .row-number-header .header-content {
1552
+ justify-content: space-between;
1553
+ padding: var(--size-1);
1554
+ height: var(--size-9);
1555
+ display: flex;
1556
+ align-items: center;
1314
1557
  }
1315
1558
 
1316
- .row-number {
1317
- z-index: var(--layer-1);
1559
+ .row-number-header :global(.sort-icons) {
1560
+ margin-right: 0;
1318
1561
  }
1319
1562
 
1320
1563
  :global(tbody > tr:nth-child(odd)) .row-number {
@@ -1411,4 +1654,88 @@
1411
1654
  .cell-selected.no-top.no-bottom.no-left.no-right {
1412
1655
  box-shadow: none;
1413
1656
  }
1657
+
1658
+ .selection-button {
1659
+ position: absolute;
1660
+ display: flex;
1661
+ align-items: center;
1662
+ justify-content: center;
1663
+ background: var(--color-accent);
1664
+ color: white;
1665
+ border-radius: var(--radius-sm);
1666
+ z-index: var(--layer-4);
1667
+ }
1668
+
1669
+ .selection-button-column {
1670
+ width: var(--size-3);
1671
+ height: var(--size-5);
1672
+ top: -10px;
1673
+ left: var(--selected-col-pos);
1674
+ transform: rotate(90deg);
1675
+ }
1676
+
1677
+ .selection-button-row {
1678
+ width: var(--size-3);
1679
+ height: var(--size-5);
1680
+ left: -7px;
1681
+ top: calc(var(--selected-row-pos) - var(--size-5) / 2);
1682
+ }
1683
+
1684
+ .table-wrap:not(:focus-within) .selection-button {
1685
+ opacity: 0;
1686
+ pointer-events: none;
1687
+ }
1688
+
1689
+ .flash.cell-selected {
1690
+ animation: flash-color 700ms ease-out;
1691
+ }
1692
+
1693
+ @keyframes flash-color {
1694
+ 0%,
1695
+ 30% {
1696
+ background: var(--color-accent-copied);
1697
+ }
1698
+
1699
+ 100% {
1700
+ background: transparent;
1701
+ }
1702
+ }
1703
+
1704
+ .frozen-column {
1705
+ position: sticky;
1706
+ z-index: var(--layer-2);
1707
+ border-right: 1px solid var(--border-color-primary);
1708
+ }
1709
+
1710
+ tr:nth-child(odd) .frozen-column {
1711
+ background: var(--table-odd-background-fill);
1712
+ }
1713
+
1714
+ tr:nth-child(even) .frozen-column {
1715
+ background: var(--table-even-background-fill);
1716
+ }
1717
+
1718
+ .always-frozen {
1719
+ z-index: var(--layer-3);
1720
+ }
1721
+
1722
+ .add-row-container {
1723
+ margin-top: var(--size-2);
1724
+ }
1725
+
1726
+ .add-row-button {
1727
+ width: 100%;
1728
+ padding: var(--size-1);
1729
+ background: transparent;
1730
+ border: 1px dashed var(--border-color-primary);
1731
+ border-radius: var(--radius-sm);
1732
+ color: var(--body-text-color);
1733
+ cursor: pointer;
1734
+ transition: all 150ms;
1735
+ }
1736
+
1737
+ .add-row-button:hover {
1738
+ background: var(--background-fill-secondary);
1739
+ border-style: solid;
1740
+ }
1414
1741
  </style>