@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
@@ -6,6 +6,7 @@ import {} from "@gradio/client";
6
6
  import VirtualTable from "./VirtualTable.svelte";
7
7
  import CellMenu from "./CellMenu.svelte";
8
8
  import Toolbar from "./Toolbar.svelte";
9
+ import SortIcon from "./icons/SortIcon.svelte";
9
10
  import {
10
11
  is_cell_selected,
11
12
  handle_selection,
@@ -15,9 +16,17 @@ import {
15
16
  get_range_selection,
16
17
  move_cursor,
17
18
  get_current_indices,
18
- handle_click_outside as handle_click_outside_util
19
+ handle_click_outside as handle_click_outside_util,
20
+ select_column,
21
+ select_row,
22
+ calculate_selection_positions
19
23
  } from "./selection_utils";
20
- import { copy_table_data, get_max, handle_file_upload } from "./table_utils";
24
+ import {
25
+ copy_table_data,
26
+ get_max,
27
+ handle_file_upload,
28
+ sort_table_data
29
+ } from "./utils/table_utils";
21
30
  export let datatype;
22
31
  export let label = null;
23
32
  export let show_label = true;
@@ -40,6 +49,11 @@ export let show_fullscreen_button = false;
40
49
  export let show_copy_button = false;
41
50
  export let value_is_output = false;
42
51
  export let max_chars = void 0;
52
+ export let show_search = "none";
53
+ export let pinned_columns = 0;
54
+ let actual_pinned_columns = 0;
55
+ $:
56
+ actual_pinned_columns = pinned_columns && data?.[0]?.length ? Math.min(pinned_columns, data[0].length) : 0;
43
57
  let selected_cells = [];
44
58
  $:
45
59
  selected_cells = [...selected_cells];
@@ -60,48 +74,60 @@ let active_cell_menu = null;
60
74
  let active_header_menu = null;
61
75
  let is_fullscreen = false;
62
76
  let dragging = false;
77
+ let copy_flash = false;
78
+ let color_accent_copied;
79
+ onMount(() => {
80
+ const color = getComputedStyle(document.documentElement).getPropertyValue("--color-accent").trim();
81
+ color_accent_copied = color + "40";
82
+ document.documentElement.style.setProperty(
83
+ "--color-accent-copied",
84
+ color_accent_copied
85
+ );
86
+ });
63
87
  const get_data_at = (row, col) => data?.[row]?.[col]?.value;
64
88
  function make_id() {
65
89
  return Math.random().toString(36).substring(2, 15);
66
90
  }
67
- function make_headers(_head) {
91
+ function make_headers(_head, col_count2, els2) {
68
92
  let _h = _head || [];
69
- if (col_count[1] === "fixed" && _h.length < col_count[0]) {
70
- const fill = Array(col_count[0] - _h.length).fill("").map((_, i) => `${i + _h.length}`);
93
+ if (col_count2[1] === "fixed" && _h.length < col_count2[0]) {
94
+ const fill = Array(col_count2[0] - _h.length).fill("").map((_, i) => `${i + _h.length}`);
71
95
  _h = _h.concat(fill);
72
96
  }
73
97
  if (!_h || _h.length === 0) {
74
- return Array(col_count[0]).fill(0).map((_, i) => {
98
+ return Array(col_count2[0]).fill(0).map((_, i) => {
75
99
  const _id = make_id();
76
- els[_id] = { cell: null, input: null };
100
+ els2[_id] = { cell: null, input: null };
77
101
  return { id: _id, value: JSON.stringify(i + 1) };
78
102
  });
79
103
  }
80
104
  return _h.map((h, i) => {
81
105
  const _id = make_id();
82
- els[_id] = { cell: null, input: null };
106
+ els2[_id] = { cell: null, input: null };
83
107
  return { id: _id, value: h ?? "" };
84
108
  });
85
109
  }
86
110
  function process_data(_values) {
87
111
  const data_row_length = _values.length;
88
- return Array(row_count[1] === "fixed" ? row_count[0] : data_row_length).fill(0).map(
89
- (_, i) => Array(
90
- col_count[1] === "fixed" ? col_count[0] : data_row_length > 0 ? _values[0].length : headers.length
112
+ if (data_row_length === 0)
113
+ return [];
114
+ return Array(row_count[1] === "fixed" ? row_count[0] : data_row_length).fill(0).map((_, i) => {
115
+ return Array(
116
+ col_count[1] === "fixed" ? col_count[0] : _values[0].length || headers.length
91
117
  ).fill(0).map((_2, j) => {
92
118
  const id = make_id();
93
119
  els[id] = els[id] || { input: null, cell: null };
94
120
  const obj = { value: _values?.[i]?.[j] ?? "", id };
95
121
  data_binding[id] = obj;
96
122
  return obj;
97
- })
98
- );
123
+ });
124
+ });
99
125
  }
100
- let _headers = make_headers(headers);
126
+ let _headers = make_headers(headers, col_count, els);
101
127
  let old_headers = headers;
102
128
  $: {
103
129
  if (!dequal(headers, old_headers)) {
104
- _headers = make_headers(headers);
130
+ _headers = make_headers(headers, col_count, els);
105
131
  old_headers = JSON.parse(JSON.stringify(headers));
106
132
  }
107
133
  }
@@ -115,6 +141,8 @@ $:
115
141
  let previous_headers = _headers.map((h) => h.value);
116
142
  let previous_data = data.map((row) => row.map((cell) => String(cell.value)));
117
143
  async function trigger_change() {
144
+ if (current_search_query)
145
+ return;
118
146
  const current_headers = _headers.map((h) => h.value);
119
147
  const current_data = data.map(
120
148
  (row) => row.map((cell) => String(cell.value))
@@ -224,8 +252,10 @@ async function handle_keydown(event) {
224
252
  editing = false;
225
253
  } else {
226
254
  selected_cells = [next_coords];
227
- editing = next_coords;
228
- clear_on_focus = false;
255
+ if (editable) {
256
+ editing = next_coords;
257
+ clear_on_focus = false;
258
+ }
229
259
  }
230
260
  selected = next_coords;
231
261
  } else if (next_coords === false && event.key === "ArrowUp" && i === 0) {
@@ -241,26 +271,26 @@ async function handle_keydown(event) {
241
271
  editing = false;
242
272
  break;
243
273
  case "Enter":
244
- if (!editable)
245
- break;
246
274
  event.preventDefault();
247
- if (event.shiftKey) {
248
- add_row(i);
249
- await tick();
250
- selected = [i + 1, j];
251
- } else {
252
- if (dequal(editing, [i, j])) {
253
- const cell_id = data[i][j].id;
254
- const input_el = els[cell_id].input;
255
- if (input_el) {
256
- data[i][j].value = input_el.value;
257
- }
258
- editing = false;
275
+ if (editable) {
276
+ if (event.shiftKey) {
277
+ add_row(i);
259
278
  await tick();
260
- selected = [i, j];
279
+ selected = [i + 1, j];
261
280
  } else {
262
- editing = [i, j];
263
- clear_on_focus = false;
281
+ if (dequal(editing, [i, j])) {
282
+ const cell_id = data[i][j].id;
283
+ const input_el = els[cell_id].input;
284
+ if (input_el) {
285
+ data[i][j].value = input_el.value;
286
+ }
287
+ editing = false;
288
+ await tick();
289
+ selected = [i, j];
290
+ } else {
291
+ editing = [i, j];
292
+ clear_on_focus = false;
293
+ }
264
294
  }
265
295
  }
266
296
  break;
@@ -292,15 +322,16 @@ async function handle_keydown(event) {
292
322
  }
293
323
  let sort_direction;
294
324
  let sort_by;
295
- function handle_sort(col) {
325
+ function handle_sort(col, direction) {
296
326
  if (typeof sort_by !== "number" || sort_by !== col) {
297
- sort_direction = "asc";
327
+ sort_direction = direction;
298
328
  sort_by = col;
299
- } else {
300
- if (sort_direction === "asc") {
301
- sort_direction = "des";
302
- } else if (sort_direction === "des") {
303
- sort_direction = "asc";
329
+ } else if (sort_by === col) {
330
+ if (sort_direction === direction) {
331
+ sort_direction = void 0;
332
+ sort_by = void 0;
333
+ } else {
334
+ sort_direction = direction;
304
335
  }
305
336
  }
306
337
  }
@@ -331,16 +362,14 @@ async function add_row(index) {
331
362
  parent.focus();
332
363
  if (row_count[1] !== "dynamic")
333
364
  return;
334
- if (data.length === 0) {
335
- values = [Array(headers.length).fill("")];
336
- return;
337
- }
338
- const new_row = Array(data[0].length).fill(0).map((_, i) => {
365
+ const new_row = Array(data[0]?.length || headers.length).fill(0).map((_, i) => {
339
366
  const _id = make_id();
340
367
  els[_id] = { cell: null, input: null };
341
368
  return { id: _id, value: "" };
342
369
  });
343
- if (index !== void 0 && index >= 0 && index <= data.length) {
370
+ if (data.length === 0) {
371
+ data = [new_row];
372
+ } else if (index !== void 0 && index >= 0 && index <= data.length) {
344
373
  data.splice(index, 0, new_row);
345
374
  } else {
346
375
  data.push(new_row);
@@ -388,50 +417,36 @@ let cells = [];
388
417
  let parent;
389
418
  let table;
390
419
  function set_cell_widths() {
391
- const widths = cells.map((el, i) => {
392
- return el?.clientWidth || 0;
393
- });
420
+ const widths = cells.map((el) => el?.clientWidth || 0);
394
421
  if (widths.length === 0)
395
422
  return;
396
- for (let i = 0; i < widths.length; i++) {
397
- parent.style.setProperty(
398
- `--cell-width-${i}`,
399
- `${widths[i] - scrollbar_width / widths.length}px`
400
- );
423
+ if (show_row_numbers) {
424
+ parent.style.setProperty(`--cell-width-row-number`, `${widths[0]}px`);
401
425
  }
426
+ const data_cells = show_row_numbers ? widths.slice(1) : widths;
427
+ data_cells.forEach((width, i) => {
428
+ if (!column_widths[i]) {
429
+ parent.style.setProperty(
430
+ `--cell-width-${i}`,
431
+ `${width - scrollbar_width / data_cells.length}px`
432
+ );
433
+ }
434
+ });
435
+ }
436
+ function get_cell_width(index) {
437
+ return column_widths[index] || `var(--cell-width-${index})`;
402
438
  }
403
439
  let table_height = values.slice(0, max_height / values.length * 37).length * 37 + 37;
404
440
  let scrollbar_width = 0;
405
441
  function sort_data(_data, _display_value, _styling, col, dir) {
406
442
  let id = null;
407
- if (selected && selected[0] in data && selected[1] in data[selected[0]]) {
408
- id = data[selected[0]][selected[1]].id;
443
+ if (selected && selected[0] in _data && selected[1] in _data[selected[0]]) {
444
+ id = _data[selected[0]][selected[1]].id;
409
445
  }
410
446
  if (typeof col !== "number" || !dir) {
411
447
  return;
412
448
  }
413
- const indices = [...Array(_data.length).keys()];
414
- if (dir === "asc") {
415
- indices.sort(
416
- (i, j) => _data[i][col].value < _data[j][col].value ? -1 : 1
417
- );
418
- } else if (dir === "des") {
419
- indices.sort(
420
- (i, j) => _data[i][col].value > _data[j][col].value ? -1 : 1
421
- );
422
- } else {
423
- return;
424
- }
425
- const temp_data = [..._data];
426
- const temp_display_value = _display_value ? [..._display_value] : null;
427
- const temp_styling = _styling ? [..._styling] : null;
428
- indices.forEach((originalIndex, sortedIndex) => {
429
- _data[sortedIndex] = temp_data[originalIndex];
430
- if (_display_value && temp_display_value)
431
- _display_value[sortedIndex] = temp_display_value[originalIndex];
432
- if (_styling && temp_styling)
433
- _styling[sortedIndex] = temp_styling[originalIndex];
434
- });
449
+ sort_table_data(_data, _display_value, _styling, col, dir);
435
450
  data = data;
436
451
  if (id) {
437
452
  const [i, j] = get_current_indices(id, data);
@@ -468,25 +483,33 @@ onMount(() => {
468
483
  };
469
484
  });
470
485
  function handle_cell_click(event, row, col) {
486
+ if (event.target instanceof HTMLAnchorElement) {
487
+ return;
488
+ }
471
489
  event.preventDefault();
472
490
  event.stopPropagation();
491
+ if (show_row_numbers && col === -1)
492
+ return;
473
493
  clear_on_focus = false;
474
494
  active_cell_menu = null;
475
495
  active_header_menu = null;
476
496
  selected_header = false;
477
497
  header_edit = false;
478
498
  selected_cells = handle_selection([row, col], selected_cells, event);
479
- if (selected_cells.length === 1 && editable) {
480
- editing = [row, col];
481
- tick().then(() => {
482
- const input_el = els[data[row][col].id].input;
483
- if (input_el) {
484
- input_el.focus();
485
- input_el.selectionStart = input_el.selectionEnd = input_el.value.length;
486
- }
487
- });
488
- } else {
489
- editing = false;
499
+ parent.focus();
500
+ if (editable) {
501
+ if (selected_cells.length === 1) {
502
+ editing = [row, col];
503
+ tick().then(() => {
504
+ const input_el = els[data[row][col].id].input;
505
+ if (input_el) {
506
+ input_el.focus();
507
+ input_el.selectionStart = input_el.selectionEnd = input_el.value.length;
508
+ }
509
+ });
510
+ } else {
511
+ editing = false;
512
+ }
490
513
  }
491
514
  toggle_cell_button(row, col);
492
515
  dispatch("select", {
@@ -522,6 +545,9 @@ function add_col_at(index, position) {
522
545
  function handle_resize() {
523
546
  active_cell_menu = null;
524
547
  active_header_menu = null;
548
+ selected_cells = [];
549
+ selected = false;
550
+ editing = false;
525
551
  set_cell_widths();
526
552
  }
527
553
  let active_button = null;
@@ -544,7 +570,11 @@ function handle_fullscreen_change() {
544
570
  is_fullscreen = !!document.fullscreenElement;
545
571
  }
546
572
  async function handle_copy() {
547
- await copy_table_data(data, _headers, selected_cells);
573
+ await copy_table_data(data, selected_cells);
574
+ copy_flash = true;
575
+ setTimeout(() => {
576
+ copy_flash = false;
577
+ }, 800);
548
578
  }
549
579
  function toggle_header_menu(event, col) {
550
580
  event.stopPropagation();
@@ -575,14 +605,16 @@ async function delete_col(index) {
575
605
  parent.focus();
576
606
  if (col_count[1] !== "dynamic")
577
607
  return;
578
- if (data[0].length <= 1)
608
+ if (_headers.length <= 1)
579
609
  return;
580
610
  _headers.splice(index, 1);
581
611
  _headers = _headers;
582
- data.forEach((row) => {
583
- row.splice(index, 1);
584
- });
585
- data = data;
612
+ if (data.length > 0) {
613
+ data.forEach((row) => {
614
+ row.splice(index, 1);
615
+ });
616
+ data = data;
617
+ }
586
618
  selected = false;
587
619
  }
588
620
  function delete_row_at(index) {
@@ -595,35 +627,130 @@ function delete_col_at(index) {
595
627
  active_cell_menu = null;
596
628
  active_header_menu = null;
597
629
  }
630
+ let row_order = [];
631
+ $: {
632
+ if (typeof sort_by === "number" && sort_direction && sort_by >= 0 && sort_by < data[0].length) {
633
+ const indices = [...Array(data.length)].map((_, i) => i);
634
+ const sort_index = sort_by;
635
+ indices.sort((a, b) => {
636
+ const row_a = data[a];
637
+ const row_b = data[b];
638
+ if (!row_a || !row_b || sort_index >= row_a.length || sort_index >= row_b.length)
639
+ return 0;
640
+ const val_a = row_a[sort_index].value;
641
+ const val_b = row_b[sort_index].value;
642
+ const comp = val_a < val_b ? -1 : val_a > val_b ? 1 : 0;
643
+ return sort_direction === "asc" ? comp : -comp;
644
+ });
645
+ row_order = indices;
646
+ } else {
647
+ row_order = [...Array(data.length)].map((_, i) => i);
648
+ }
649
+ }
650
+ function handle_select_column(col) {
651
+ selected_cells = select_column(data, col);
652
+ selected = selected_cells[0];
653
+ editing = false;
654
+ }
655
+ function handle_select_row(row) {
656
+ selected_cells = select_row(data, row);
657
+ selected = selected_cells[0];
658
+ editing = false;
659
+ }
660
+ let coords;
661
+ $:
662
+ if (selected !== false)
663
+ coords = selected;
664
+ $:
665
+ if (selected !== false) {
666
+ const positions = calculate_selection_positions(
667
+ selected,
668
+ data,
669
+ els,
670
+ parent,
671
+ table
672
+ );
673
+ document.documentElement.style.setProperty(
674
+ "--selected-col-pos",
675
+ positions.col_pos
676
+ );
677
+ if (positions.row_pos) {
678
+ document.documentElement.style.setProperty(
679
+ "--selected-row-pos",
680
+ positions.row_pos
681
+ );
682
+ }
683
+ }
684
+ let current_search_query = null;
685
+ function handle_search(search_query) {
686
+ current_search_query = search_query;
687
+ dispatch("search", search_query);
688
+ }
689
+ function commit_filter() {
690
+ if (current_search_query && show_search === "filter") {
691
+ dispatch("change", {
692
+ data: data.map((row) => row.map((cell) => cell.value)),
693
+ headers: _headers.map((h) => h.value),
694
+ metadata: null
695
+ });
696
+ if (!value_is_output) {
697
+ dispatch("input");
698
+ }
699
+ current_search_query = null;
700
+ }
701
+ }
598
702
  </script>
599
703
 
600
704
  <svelte:window on:resize={() => set_cell_widths()} />
601
705
 
602
706
  <div class="table-container">
603
- <div class="header-row">
604
- {#if label && label.length !== 0 && show_label}
605
- <div class="label">
606
- <p>{label}</p>
607
- </div>
608
- {/if}
609
- <Toolbar
610
- {show_fullscreen_button}
611
- {is_fullscreen}
612
- on:click={toggle_fullscreen}
613
- on_copy={handle_copy}
614
- {show_copy_button}
615
- />
616
- </div>
707
+ {#if (label && label.length !== 0 && show_label) || show_fullscreen_button || show_copy_button || show_search !== "none"}
708
+ <div class="header-row">
709
+ {#if label && label.length !== 0 && show_label}
710
+ <div class="label">
711
+ <p>{label}</p>
712
+ </div>
713
+ {/if}
714
+ <Toolbar
715
+ {show_fullscreen_button}
716
+ {is_fullscreen}
717
+ on:click={toggle_fullscreen}
718
+ on_copy={handle_copy}
719
+ {show_copy_button}
720
+ {show_search}
721
+ on:search={(e) => handle_search(e.detail)}
722
+ on_commit_filter={commit_filter}
723
+ {current_search_query}
724
+ />
725
+ </div>
726
+ {/if}
617
727
  <div
618
728
  bind:this={parent}
619
729
  class="table-wrap"
620
730
  class:dragging
621
731
  class:no-wrap={!wrap}
622
- style="height:{table_height}px"
732
+ style="height:{table_height}px;"
733
+ class:menu-open={active_cell_menu || active_header_menu}
623
734
  on:keydown={(e) => handle_keydown(e)}
624
735
  role="grid"
625
736
  tabindex="0"
626
737
  >
738
+ {#if selected !== false && selected_cells.length === 1}
739
+ <button
740
+ class="selection-button selection-button-column"
741
+ on:click|stopPropagation={() => handle_select_column(coords[1])}
742
+ aria-label="Select column"
743
+ >
744
+ &#8942;
745
+ </button>
746
+ <button
747
+ class="selection-button selection-button-row"
748
+ on:click|stopPropagation={() => handle_select_row(coords[0])}
749
+ aria-label="Select row"
750
+ >
751
+ &#8942;
752
+ </button>
753
+ {/if}
627
754
  <table
628
755
  bind:contentRect={t_rect}
629
756
  bind:this={table}
@@ -635,40 +762,59 @@ function delete_col_at(index) {
635
762
  <thead>
636
763
  <tr>
637
764
  {#if show_row_numbers}
638
- <th class="row-number-header"></th>
765
+ <th
766
+ class="row-number-header frozen-column always-frozen"
767
+ style="left: 0;"
768
+ >
769
+ <div class="cell-wrap">
770
+ <div class="header-content">
771
+ <div class="header-text"></div>
772
+ </div>
773
+ </div>
774
+ </th>
639
775
  {/if}
640
776
  {#each _headers as { value, id }, i (id)}
641
777
  <th
778
+ class:frozen-column={i < actual_pinned_columns}
779
+ class:last-frozen={show_row_numbers
780
+ ? i === actual_pinned_columns - 1
781
+ : i === actual_pinned_columns - 1}
642
782
  class:editing={header_edit === i}
643
783
  aria-sort={get_sort_status(value, sort_by, sort_direction)}
644
- style:width={column_widths.length ? column_widths[i] : undefined}
784
+ style="width: {column_widths.length
785
+ ? column_widths[i]
786
+ : undefined}; left: {i < actual_pinned_columns
787
+ ? i === 0
788
+ ? show_row_numbers
789
+ ? 'var(--cell-width-row-number)'
790
+ : '0'
791
+ : `calc(${show_row_numbers ? 'var(--cell-width-row-number) + ' : ''}${Array(
792
+ i
793
+ )
794
+ .fill(0)
795
+ .map((_, idx) => `var(--cell-width-${idx})`)
796
+ .join(' + ')})`
797
+ : 'auto'};"
645
798
  >
646
799
  <div class="cell-wrap">
647
- <EditableCell
648
- {value}
649
- {latex_delimiters}
650
- {line_breaks}
651
- header
652
- edit={false}
653
- el={null}
654
- {root}
655
- {editable}
656
- />
657
-
658
- <div
659
- class:sorted={sort_by === i}
660
- class:des={sort_by === i && sort_direction === "des"}
661
- class="sort-button {sort_direction} "
662
- >
663
- <svg
664
- width="1em"
665
- height="1em"
666
- viewBox="0 0 9 7"
667
- fill="none"
668
- xmlns="http://www.w3.org/2000/svg"
669
- >
670
- <path d="M4.49999 0L8.3971 6.75H0.602875L4.49999 0Z" />
671
- </svg>
800
+ <div class="header-content">
801
+ <EditableCell
802
+ {value}
803
+ {latex_delimiters}
804
+ {line_breaks}
805
+ header
806
+ edit={false}
807
+ el={null}
808
+ {root}
809
+ {editable}
810
+ />
811
+ <div class="sort-buttons">
812
+ <SortIcon
813
+ direction={sort_by === i ? sort_direction : null}
814
+ on:sort={({ detail }) => handle_sort(i, detail)}
815
+ {i18n}
816
+ />
817
+ </div>
672
818
  </div>
673
819
  </div>
674
820
  </th>
@@ -707,9 +853,12 @@ function delete_col_at(index) {
707
853
  on:load={({ detail }) =>
708
854
  handle_file_upload(
709
855
  detail.data,
710
- col_count,
711
856
  (head) => {
712
- _headers = make_headers(head);
857
+ _headers = make_headers(
858
+ head.map((h) => h ?? ""),
859
+ col_count,
860
+ els
861
+ );
713
862
  return _headers;
714
863
  },
715
864
  (vals) => {
@@ -719,152 +868,205 @@ function delete_col_at(index) {
719
868
  bind:dragging
720
869
  aria_label={i18n("dataframe.drop_to_upload")}
721
870
  >
722
- <VirtualTable
723
- bind:items={data}
724
- {max_height}
725
- bind:actual_height={table_height}
726
- bind:table_scrollbar_width={scrollbar_width}
727
- selected={selected_index}
728
- >
729
- {#if label && label.length !== 0}
730
- <caption class="sr-only">{label}</caption>
731
- {/if}
732
- <tr slot="thead">
733
- {#if show_row_numbers}
734
- <th class="row-number-header"></th>
871
+ <div class="table-wrap">
872
+ <VirtualTable
873
+ bind:items={data}
874
+ {max_height}
875
+ bind:actual_height={table_height}
876
+ bind:table_scrollbar_width={scrollbar_width}
877
+ selected={selected_index}
878
+ disable_scroll={active_cell_menu !== null ||
879
+ active_header_menu !== null}
880
+ >
881
+ {#if label && label.length !== 0}
882
+ <caption class="sr-only">{label}</caption>
735
883
  {/if}
736
- {#each _headers as { value, id }, i (id)}
737
- <th
738
- class:focus={header_edit === i || selected_header === i}
739
- aria-sort={get_sort_status(value, sort_by, sort_direction)}
740
- style="width: var(--cell-width-{i});"
741
- on:click={() => {
742
- toggle_header_button(i);
743
- }}
744
- >
745
- <div class="cell-wrap">
746
- <div class="header-content">
884
+ <tr slot="thead">
885
+ {#if show_row_numbers}
886
+ <th
887
+ class="row-number-header frozen-column always-frozen"
888
+ style="left: 0;"
889
+ >
890
+ <div class="cell-wrap">
891
+ <div class="header-content">
892
+ <div class="header-text"></div>
893
+ </div>
894
+ </div>
895
+ </th>
896
+ {/if}
897
+ {#each _headers as { value, id }, i (id)}
898
+ <th
899
+ class:frozen-column={i < actual_pinned_columns}
900
+ class:last-frozen={i === actual_pinned_columns - 1}
901
+ class:focus={header_edit === i || selected_header === i}
902
+ aria-sort={get_sort_status(value, sort_by, sort_direction)}
903
+ style="width: {get_cell_width(i)}; left: {i <
904
+ actual_pinned_columns
905
+ ? i === 0
906
+ ? show_row_numbers
907
+ ? 'var(--cell-width-row-number)'
908
+ : '0'
909
+ : `calc(${show_row_numbers ? 'var(--cell-width-row-number) + ' : ''}${Array(
910
+ i
911
+ )
912
+ .fill(0)
913
+ .map((_, idx) => `var(--cell-width-${idx})`)
914
+ .join(' + ')})`
915
+ : 'auto'};"
916
+ on:click={() => {
917
+ toggle_header_button(i);
918
+ }}
919
+ >
920
+ <div class="cell-wrap">
921
+ <div class="header-content">
922
+ <EditableCell
923
+ {max_chars}
924
+ bind:value={_headers[i].value}
925
+ bind:el={els[id].input}
926
+ {latex_delimiters}
927
+ {line_breaks}
928
+ edit={header_edit === i}
929
+ on:keydown={end_header_edit}
930
+ on:dblclick={() => edit_header(i)}
931
+ header
932
+ {root}
933
+ {editable}
934
+ />
935
+ <div class="sort-buttons">
936
+ <SortIcon
937
+ direction={sort_by === i ? sort_direction : null}
938
+ on:sort={({ detail }) => handle_sort(i, detail)}
939
+ {i18n}
940
+ />
941
+ </div>
942
+ </div>
943
+ {#if editable}
944
+ <button
945
+ class="cell-menu-button"
946
+ on:click={(event) => toggle_header_menu(event, i)}
947
+ on:touchstart={(event) => {
948
+ event.preventDefault();
949
+ const touch = event.touches[0];
950
+ const mouseEvent = new MouseEvent("click", {
951
+ clientX: touch.clientX,
952
+ clientY: touch.clientY,
953
+ bubbles: true,
954
+ cancelable: true,
955
+ view: window
956
+ });
957
+ toggle_header_menu(mouseEvent, i);
958
+ }}
959
+ >
960
+ &#8942;
961
+ </button>
962
+ {/if}
963
+ </div>
964
+ </th>
965
+ {/each}
966
+ </tr>
967
+ <tr slot="tbody" let:item let:index class:row_odd={index % 2 === 0}>
968
+ {#if show_row_numbers}
969
+ <td
970
+ class="row-number frozen-column always-frozen"
971
+ style="left: 0;"
972
+ tabindex="-1"
973
+ >
974
+ {index + 1}
975
+ </td>
976
+ {/if}
977
+ {#each item as { value, id }, j (id)}
978
+ <td
979
+ class:frozen-column={j < actual_pinned_columns}
980
+ class:last-frozen={j === actual_pinned_columns - 1}
981
+ tabindex={show_row_numbers && j === 0 ? -1 : 0}
982
+ bind:this={els[id].cell}
983
+ on:touchstart={(event) => {
984
+ const touch = event.touches[0];
985
+ const mouseEvent = new MouseEvent("click", {
986
+ clientX: touch.clientX,
987
+ clientY: touch.clientY,
988
+ bubbles: true,
989
+ cancelable: true,
990
+ view: window
991
+ });
992
+ handle_cell_click(mouseEvent, index, j);
993
+ }}
994
+ on:mousedown={(event) => {
995
+ event.preventDefault();
996
+ event.stopPropagation();
997
+ }}
998
+ on:click={(event) => handle_cell_click(event, index, j)}
999
+ style="width: {get_cell_width(j)}; left: {j <
1000
+ actual_pinned_columns
1001
+ ? j === 0
1002
+ ? show_row_numbers
1003
+ ? 'var(--cell-width-row-number)'
1004
+ : '0'
1005
+ : `calc(${show_row_numbers ? 'var(--cell-width-row-number) + ' : ''}${Array(
1006
+ j
1007
+ )
1008
+ .fill(0)
1009
+ .map((_, idx) => `var(--cell-width-${idx})`)
1010
+ .join(' + ')})`
1011
+ : 'auto'}; {styling?.[index]?.[j] || ''}"
1012
+ class:flash={copy_flash &&
1013
+ is_cell_selected([index, j], selected_cells)}
1014
+ class={is_cell_selected([index, j], selected_cells)}
1015
+ class:menu-active={active_cell_menu &&
1016
+ active_cell_menu.row === index &&
1017
+ active_cell_menu.col === j}
1018
+ >
1019
+ <div class="cell-wrap">
747
1020
  <EditableCell
748
- {max_chars}
749
- bind:value={_headers[i].value}
1021
+ bind:value={data[index][j].value}
750
1022
  bind:el={els[id].input}
1023
+ display_value={display_value?.[index]?.[j]}
751
1024
  {latex_delimiters}
752
1025
  {line_breaks}
753
- edit={header_edit === i}
754
- on:keydown={end_header_edit}
755
- on:dblclick={() => edit_header(i)}
756
- header
757
- {root}
758
1026
  {editable}
759
- />
760
- <button
761
- class:sorted={sort_by === i}
762
- class:des={sort_by === i && sort_direction === "des"}
763
- class="sort-button {sort_direction}"
764
- tabindex="0"
765
- on:click={(event) => {
766
- event.stopPropagation();
767
- handle_sort(i);
1027
+ edit={dequal(editing, [index, j])}
1028
+ datatype={Array.isArray(datatype) ? datatype[j] : datatype}
1029
+ on:blur={() => {
1030
+ clear_on_focus = false;
1031
+ parent.focus();
768
1032
  }}
769
- >
770
- <svg
771
- width="1em"
772
- height="1em"
773
- viewBox="0 0 9 7"
774
- fill="none"
775
- xmlns="http://www.w3.org/2000/svg"
1033
+ on:focus={() => {
1034
+ const row = index;
1035
+ const col = j;
1036
+ if (
1037
+ !selected_cells.some(([r, c]) => r === row && c === col)
1038
+ ) {
1039
+ selected_cells = [[row, col]];
1040
+ }
1041
+ }}
1042
+ {clear_on_focus}
1043
+ {root}
1044
+ {max_chars}
1045
+ />
1046
+ {#if editable && should_show_cell_menu([index, j], selected_cells, editable)}
1047
+ <button
1048
+ class="cell-menu-button"
1049
+ on:click={(event) => toggle_cell_menu(event, index, j)}
776
1050
  >
777
- <path d="M4.49999 0L8.3971 6.75H0.602875L4.49999 0Z" />
778
- </svg>
779
- </button>
1051
+ &#8942;
1052
+ </button>
1053
+ {/if}
780
1054
  </div>
781
-
782
- {#if editable}
783
- <button
784
- class="cell-menu-button"
785
- on:click={(event) => toggle_header_menu(event, i)}
786
- >
787
- &#8942;
788
- </button>
789
- {/if}
790
- </div>
791
- </th>
792
- {/each}
793
- </tr>
794
-
795
- <tr slot="tbody" let:item let:index class:row_odd={index % 2 === 0}>
796
- {#if show_row_numbers}
797
- <td class="row-number" title={`Row ${index + 1}`}>{index + 1}</td>
798
- {/if}
799
- {#each item as { value, id }, j (id)}
800
- <td
801
- tabindex="0"
802
- on:touchstart={(event) => {
803
- const touch = event.touches[0];
804
- const mouseEvent = new MouseEvent("click", {
805
- clientX: touch.clientX,
806
- clientY: touch.clientY,
807
- bubbles: true,
808
- cancelable: true,
809
- view: window
810
- });
811
- handle_cell_click(mouseEvent, index, j);
812
- }}
813
- on:mousedown={(event) => {
814
- event.preventDefault();
815
- event.stopPropagation();
816
- }}
817
- on:click={(event) => handle_cell_click(event, index, j)}
818
- style:width="var(--cell-width-{j})"
819
- style={styling?.[index]?.[j] || ""}
820
- class={is_cell_selected([index, j], selected_cells)}
821
- class:menu-active={active_cell_menu &&
822
- active_cell_menu.row === index &&
823
- active_cell_menu.col === j}
824
- >
825
- <div class="cell-wrap">
826
- <EditableCell
827
- bind:value={data[index][j].value}
828
- bind:el={els[id].input}
829
- display_value={display_value?.[index]?.[j]}
830
- {latex_delimiters}
831
- {line_breaks}
832
- {editable}
833
- edit={dequal(editing, [index, j])}
834
- datatype={Array.isArray(datatype) ? datatype[j] : datatype}
835
- on:blur={() => {
836
- clear_on_focus = false;
837
- parent.focus();
838
- }}
839
- on:focus={() => {
840
- const row = index;
841
- const col = j;
842
- if (
843
- !selected_cells.some(([r, c]) => r === row && c === col)
844
- ) {
845
- selected_cells = [[row, col]];
846
- }
847
- }}
848
- {clear_on_focus}
849
- {root}
850
- {max_chars}
851
- />
852
- {#if editable && should_show_cell_menu([index, j], selected_cells, editable)}
853
- <button
854
- class="cell-menu-button"
855
- on:click={(event) => toggle_cell_menu(event, index, j)}
856
- >
857
- &#8942;
858
- </button>
859
- {/if}
860
- </div>
861
- </td>
862
- {/each}
863
- </tr>
864
- </VirtualTable>
1055
+ </td>
1056
+ {/each}
1057
+ </tr>
1058
+ </VirtualTable>
1059
+ </div>
865
1060
  </Upload>
866
1061
  </div>
867
1062
  </div>
1063
+ {#if data.length === 0 && editable && row_count[1] === "dynamic"}
1064
+ <div class="add-row-container">
1065
+ <button class="add-row-button" on:click={() => add_row()}>
1066
+ <span>+</span>
1067
+ </button>
1068
+ </div>
1069
+ {/if}
868
1070
 
869
1071
  {#if active_cell_menu}
870
1072
  <CellMenu
@@ -901,11 +1103,19 @@ function delete_col_at(index) {
901
1103
  on_delete_row={() => delete_row_at(active_cell_menu?.row ?? -1)}
902
1104
  on_delete_col={() => delete_col_at(active_header_menu?.col ?? -1)}
903
1105
  can_delete_rows={false}
904
- can_delete_cols={data[0].length > 1}
1106
+ can_delete_cols={_headers.length > 1}
905
1107
  />
906
1108
  {/if}
907
1109
 
908
1110
  <style>
1111
+ .label p {
1112
+ position: relative;
1113
+ z-index: var(--layer-4);
1114
+ margin-bottom: var(--size-2);
1115
+ color: var(--block-label-text-color);
1116
+ font-size: var(--block-label-text-size);
1117
+ }
1118
+
909
1119
  .table-container {
910
1120
  display: flex;
911
1121
  flex-direction: column;
@@ -915,8 +1125,9 @@ function delete_col_at(index) {
915
1125
  .table-wrap {
916
1126
  position: relative;
917
1127
  transition: 150ms;
918
- border: 1px solid var(--border-color-primary);
919
- border-radius: var(--table-radius);
1128
+ }
1129
+
1130
+ .table-wrap.menu-open {
920
1131
  overflow: hidden;
921
1132
  }
922
1133
 
@@ -946,6 +1157,12 @@ function delete_col_at(index) {
946
1157
  border-collapse: separate;
947
1158
  }
948
1159
 
1160
+ .table-wrap > :global(button) {
1161
+ border: 1px solid var(--border-color-primary);
1162
+ border-radius: var(--table-radius);
1163
+ overflow: hidden;
1164
+ }
1165
+
949
1166
  div:not(.no-wrap) td {
950
1167
  overflow-wrap: anywhere;
951
1168
  }
@@ -961,8 +1178,7 @@ function delete_col_at(index) {
961
1178
  thead {
962
1179
  position: sticky;
963
1180
  top: 0;
964
- left: 0;
965
- z-index: var(--layer-1);
1181
+ z-index: var(--layer-2);
966
1182
  box-shadow: var(--shadow-drop);
967
1183
  }
968
1184
 
@@ -989,10 +1205,12 @@ function delete_col_at(index) {
989
1205
 
990
1206
  th:first-child {
991
1207
  border-top-left-radius: var(--table-radius);
1208
+ border-bottom-left-radius: var(--table-radius);
992
1209
  }
993
1210
 
994
1211
  th:last-child {
995
1212
  border-top-right-radius: var(--table-radius);
1213
+ border-bottom-right-radius: var(--table-radius);
996
1214
  }
997
1215
 
998
1216
  th.focus,
@@ -1018,32 +1236,11 @@ function delete_col_at(index) {
1018
1236
  background: var(--table-even-background-fill);
1019
1237
  }
1020
1238
 
1021
- th svg {
1022
- fill: currentColor;
1023
- font-size: 10px;
1024
- }
1025
-
1026
- .sort-button {
1239
+ .sort-buttons {
1027
1240
  display: flex;
1028
- flex: none;
1029
- justify-content: center;
1030
1241
  align-items: center;
1031
- transition: 150ms;
1032
- cursor: pointer;
1033
- padding: var(--size-2);
1034
- color: var(--body-text-color-subdued);
1035
- }
1036
-
1037
- .sort-button:hover {
1038
- color: var(--body-text-color);
1039
- }
1040
-
1041
- .des {
1042
- transform: scaleY(-1);
1043
- }
1044
-
1045
- .sort-button.sorted {
1046
- color: var(--color-accent);
1242
+ flex-shrink: 0;
1243
+ order: -1;
1047
1244
  }
1048
1245
 
1049
1246
  .editing {
@@ -1052,11 +1249,19 @@ function delete_col_at(index) {
1052
1249
 
1053
1250
  .cell-wrap {
1054
1251
  display: flex;
1055
- align-items: flex-start;
1252
+ align-items: center;
1253
+ justify-content: flex-start;
1056
1254
  outline: none;
1057
1255
  min-height: var(--size-9);
1058
1256
  position: relative;
1059
- height: auto;
1257
+ height: 100%;
1258
+ padding: var(--size-2);
1259
+ box-sizing: border-box;
1260
+ margin: 0;
1261
+ gap: var(--size-1);
1262
+ overflow: visible;
1263
+ min-width: 0;
1264
+ border-radius: var(--table-radius);
1060
1265
  }
1061
1266
 
1062
1267
  .header-content {
@@ -1067,7 +1272,9 @@ function delete_col_at(index) {
1067
1272
  min-width: 0;
1068
1273
  white-space: normal;
1069
1274
  overflow-wrap: break-word;
1070
- word-break: break-word;
1275
+ word-break: normal;
1276
+ height: 100%;
1277
+ gap: var(--size-1);
1071
1278
  }
1072
1279
 
1073
1280
  .row_odd {
@@ -1096,7 +1303,8 @@ function delete_col_at(index) {
1096
1303
  transform: translateY(-50%);
1097
1304
  }
1098
1305
 
1099
- .cell-selected .cell-menu-button {
1306
+ .cell-selected .cell-menu-button,
1307
+ th:hover .cell-menu-button {
1100
1308
  display: flex;
1101
1309
  align-items: center;
1102
1310
  justify-content: center;
@@ -1104,46 +1312,55 @@ function delete_col_at(index) {
1104
1312
 
1105
1313
  .header-row {
1106
1314
  display: flex;
1107
- justify-content: space-between;
1315
+ justify-content: flex-end;
1108
1316
  align-items: center;
1109
1317
  gap: var(--size-2);
1110
- height: var(--size-6);
1111
1318
  min-height: var(--size-6);
1319
+ flex-wrap: nowrap;
1320
+ width: 100%;
1112
1321
  }
1113
1322
 
1114
1323
  .label {
1115
- flex: 1;
1324
+ flex: 1 1 auto;
1325
+ margin-right: auto;
1116
1326
  }
1117
1327
 
1118
1328
  .label p {
1119
1329
  margin: 0;
1120
1330
  color: var(--block-label-text-color);
1121
1331
  font-size: var(--block-label-text-size);
1332
+ line-height: var(--line-sm);
1333
+ }
1334
+
1335
+ .toolbar {
1336
+ flex: 0 0 auto;
1122
1337
  }
1123
1338
 
1124
1339
  .row-number,
1125
1340
  .row-number-header {
1126
- width: var(--size-7);
1127
- min-width: var(--size-7);
1128
1341
  text-align: center;
1129
1342
  background: var(--table-even-background-fill);
1130
- position: sticky;
1131
- left: 0;
1132
1343
  font-size: var(--input-text-size);
1133
1344
  color: var(--body-text-color);
1134
- padding: var(--size-1) var(--size-2);
1345
+ padding: var(--size-1);
1346
+ min-width: var(--size-12);
1347
+ width: var(--size-12);
1135
1348
  overflow: hidden;
1136
1349
  text-overflow: ellipsis;
1137
1350
  white-space: nowrap;
1138
1351
  font-weight: var(--weight-semibold);
1139
1352
  }
1140
1353
 
1141
- .row-number-header {
1142
- z-index: var(--layer-2);
1354
+ .row-number-header .header-content {
1355
+ justify-content: space-between;
1356
+ padding: var(--size-1);
1357
+ height: var(--size-9);
1358
+ display: flex;
1359
+ align-items: center;
1143
1360
  }
1144
1361
 
1145
- .row-number {
1146
- z-index: var(--layer-1);
1362
+ .row-number-header :global(.sort-icons) {
1363
+ margin-right: 0;
1147
1364
  }
1148
1365
 
1149
1366
  :global(tbody > tr:nth-child(odd)) .row-number {
@@ -1240,4 +1457,88 @@ function delete_col_at(index) {
1240
1457
  .cell-selected.no-top.no-bottom.no-left.no-right {
1241
1458
  box-shadow: none;
1242
1459
  }
1460
+
1461
+ .selection-button {
1462
+ position: absolute;
1463
+ display: flex;
1464
+ align-items: center;
1465
+ justify-content: center;
1466
+ background: var(--color-accent);
1467
+ color: white;
1468
+ border-radius: var(--radius-sm);
1469
+ z-index: var(--layer-4);
1470
+ }
1471
+
1472
+ .selection-button-column {
1473
+ width: var(--size-3);
1474
+ height: var(--size-5);
1475
+ top: -10px;
1476
+ left: var(--selected-col-pos);
1477
+ transform: rotate(90deg);
1478
+ }
1479
+
1480
+ .selection-button-row {
1481
+ width: var(--size-3);
1482
+ height: var(--size-5);
1483
+ left: -7px;
1484
+ top: calc(var(--selected-row-pos) - var(--size-5) / 2);
1485
+ }
1486
+
1487
+ .table-wrap:not(:focus-within) .selection-button {
1488
+ opacity: 0;
1489
+ pointer-events: none;
1490
+ }
1491
+
1492
+ .flash.cell-selected {
1493
+ animation: flash-color 700ms ease-out;
1494
+ }
1495
+
1496
+ @keyframes flash-color {
1497
+ 0%,
1498
+ 30% {
1499
+ background: var(--color-accent-copied);
1500
+ }
1501
+
1502
+ 100% {
1503
+ background: transparent;
1504
+ }
1505
+ }
1506
+
1507
+ .frozen-column {
1508
+ position: sticky;
1509
+ z-index: var(--layer-2);
1510
+ border-right: 1px solid var(--border-color-primary);
1511
+ }
1512
+
1513
+ tr:nth-child(odd) .frozen-column {
1514
+ background: var(--table-odd-background-fill);
1515
+ }
1516
+
1517
+ tr:nth-child(even) .frozen-column {
1518
+ background: var(--table-even-background-fill);
1519
+ }
1520
+
1521
+ .always-frozen {
1522
+ z-index: var(--layer-3);
1523
+ }
1524
+
1525
+ .add-row-container {
1526
+ margin-top: var(--size-2);
1527
+ }
1528
+
1529
+ .add-row-button {
1530
+ width: 100%;
1531
+ padding: var(--size-1);
1532
+ background: transparent;
1533
+ border: 1px dashed var(--border-color-primary);
1534
+ border-radius: var(--radius-sm);
1535
+ color: var(--body-text-color);
1536
+ cursor: pointer;
1537
+ transition: all 150ms;
1538
+ }
1539
+
1540
+ .add-row-button:hover {
1541
+ background: var(--background-fill-secondary);
1542
+ border-style: solid;
1543
+ }
1243
1544
  </style>