@gradio/dataframe 0.13.1 → 0.15.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.
@@ -1,6 +1,5 @@
1
1
  <script lang="ts">
2
2
  import { afterUpdate, createEventDispatcher, tick, onMount } from "svelte";
3
- import { dsvFormat } from "d3-dsv";
4
3
  import { dequal } from "dequal/lite";
5
4
  import { Upload } from "@gradio/upload";
6
5
 
@@ -17,6 +16,19 @@
17
16
  } from "./utils";
18
17
  import CellMenu from "./CellMenu.svelte";
19
18
  import Toolbar from "./Toolbar.svelte";
19
+ import type { CellCoordinate, EditingState } from "./types";
20
+ import {
21
+ is_cell_selected,
22
+ handle_selection,
23
+ handle_delete_key,
24
+ should_show_cell_menu,
25
+ get_next_cell_coordinates,
26
+ get_range_selection,
27
+ move_cursor,
28
+ get_current_indices,
29
+ handle_click_outside as handle_click_outside_util
30
+ } from "./selection_utils";
31
+ import { copy_table_data, get_max, handle_file_upload } from "./table_utils";
20
32
 
21
33
  export let datatype: Datatype | Datatype[];
22
34
  export let label: string | null = null;
@@ -43,13 +55,26 @@
43
55
  export let upload: Client["upload"];
44
56
  export let stream_handler: Client["stream"];
45
57
  export let show_fullscreen_button = false;
58
+ export let show_copy_button = false;
46
59
  export let value_is_output = false;
60
+ export let max_chars: number | undefined = undefined;
61
+
62
+ let selected_cells: CellCoordinate[] = [];
63
+ $: selected_cells = [...selected_cells];
64
+ let selected: CellCoordinate | false = false;
65
+ $: selected =
66
+ selected_cells.length > 0
67
+ ? selected_cells[selected_cells.length - 1]
68
+ : false;
47
69
 
48
- let selected: false | [number, number] = false;
49
- let clicked_cell: { row: number; col: number } | undefined = undefined;
50
70
  export let display_value: string[][] | null = null;
51
71
  export let styling: string[][] | null = null;
52
72
  let t_rect: DOMRectReadOnly;
73
+ let els: Record<
74
+ string,
75
+ { cell: null | HTMLTableCellElement; input: null | HTMLInputElement }
76
+ > = {};
77
+ let data_binding: Record<string, (typeof data)[0][0]> = {};
53
78
 
54
79
  const dispatch = createEventDispatcher<{
55
80
  change: DataframeValue;
@@ -57,34 +82,27 @@
57
82
  select: SelectData;
58
83
  }>();
59
84
 
60
- let editing: false | [number, number] = false;
85
+ let editing: EditingState = false;
86
+ let clear_on_focus = false;
87
+ let header_edit: number | false = false;
88
+ let selected_header: number | false = false;
89
+ let active_cell_menu: {
90
+ row: number;
91
+ col: number;
92
+ x: number;
93
+ y: number;
94
+ } | null = null;
95
+ let active_header_menu: {
96
+ col: number;
97
+ x: number;
98
+ y: number;
99
+ } | null = null;
100
+ let is_fullscreen = false;
101
+ let dragging = false;
61
102
 
62
103
  const get_data_at = (row: number, col: number): string | number =>
63
104
  data?.[row]?.[col]?.value;
64
105
 
65
- let last_selected: [number, number] | null = null;
66
-
67
- $: {
68
- if (selected !== false && !dequal(selected, last_selected)) {
69
- const [row, col] = selected;
70
- if (!isNaN(row) && !isNaN(col) && data[row]) {
71
- dispatch("select", {
72
- index: [row, col],
73
- value: get_data_at(row, col),
74
- row_value: data[row].map((d) => d.value)
75
- });
76
- last_selected = selected;
77
- }
78
- }
79
- }
80
-
81
- let els: Record<
82
- string,
83
- { cell: null | HTMLTableCellElement; input: null | HTMLInputElement }
84
- > = {};
85
-
86
- let data_binding: Record<string, (typeof data)[0][0]> = {};
87
-
88
106
  function make_id(): string {
89
107
  return Math.random().toString(36).substring(2, 15);
90
108
  }
@@ -120,13 +138,7 @@
120
138
  id: string;
121
139
  }[][] {
122
140
  const data_row_length = _values.length;
123
- return Array(
124
- row_count[1] === "fixed"
125
- ? row_count[0]
126
- : data_row_length < row_count[0]
127
- ? row_count[0]
128
- : data_row_length
129
- )
141
+ return Array(row_count[1] === "fixed" ? row_count[0] : data_row_length)
130
142
  .fill(0)
131
143
  .map((_, i) =>
132
144
  Array(
@@ -203,48 +215,9 @@
203
215
  if (direction === "asc") return "ascending";
204
216
  if (direction === "des") return "descending";
205
217
  }
206
-
207
218
  return "none";
208
219
  }
209
220
 
210
- function get_current_indices(id: string): [number, number] {
211
- return data.reduce(
212
- (acc, arr, i) => {
213
- const j = arr.reduce(
214
- (_acc, _data, k) => (id === _data.id ? k : _acc),
215
- -1
216
- );
217
-
218
- return j === -1 ? acc : [i, j];
219
- },
220
- [-1, -1]
221
- );
222
- }
223
-
224
- function move_cursor(
225
- key: "ArrowRight" | "ArrowLeft" | "ArrowDown" | "ArrowUp",
226
- current_coords: [number, number]
227
- ): void {
228
- const dir = {
229
- ArrowRight: [0, 1],
230
- ArrowLeft: [0, -1],
231
- ArrowDown: [1, 0],
232
- ArrowUp: [-1, 0]
233
- }[key];
234
-
235
- const i = current_coords[0] + dir[0];
236
- const j = current_coords[1] + dir[1];
237
-
238
- if (i < 0 && j <= 0) {
239
- selected_header = j;
240
- selected = false;
241
- } else {
242
- const is_data = data[i]?.[j];
243
- selected = is_data ? [i, j] : selected;
244
- }
245
- }
246
-
247
- let clear_on_focus = false;
248
221
  // eslint-disable-next-line complexity
249
222
  async function handle_keydown(event: KeyboardEvent): Promise<void> {
250
223
  if (selected_header !== false && header_edit === false) {
@@ -272,6 +245,50 @@
272
245
  break;
273
246
  }
274
247
  }
248
+
249
+ if (event.key === "Delete" || event.key === "Backspace") {
250
+ if (!editable) return;
251
+
252
+ if (editing) {
253
+ const [row, col] = editing;
254
+ const input_el = els[data[row][col].id].input;
255
+ if (input_el && input_el.selectionStart !== input_el.selectionEnd) {
256
+ return;
257
+ }
258
+ if (
259
+ event.key === "Delete" &&
260
+ input_el?.selectionStart !== input_el?.value.length
261
+ ) {
262
+ return;
263
+ }
264
+ if (event.key === "Backspace" && input_el?.selectionStart !== 0) {
265
+ return;
266
+ }
267
+ }
268
+
269
+ event.preventDefault();
270
+ if (selected_cells.length > 0) {
271
+ data = handle_delete_key(data, selected_cells);
272
+ dispatch("change", {
273
+ data: data.map((row) => row.map((cell) => cell.value)),
274
+ headers: _headers.map((h) => h.value),
275
+ metadata: null
276
+ });
277
+ if (!value_is_output) {
278
+ dispatch("input");
279
+ }
280
+ }
281
+ return;
282
+ }
283
+
284
+ if (event.key === "c" && (event.metaKey || event.ctrlKey)) {
285
+ event.preventDefault();
286
+ if (selected_cells.length > 0) {
287
+ await handle_copy();
288
+ }
289
+ return;
290
+ }
291
+
275
292
  if (!selected) {
276
293
  return;
277
294
  }
@@ -285,7 +302,29 @@
285
302
  case "ArrowUp":
286
303
  if (editing) break;
287
304
  event.preventDefault();
288
- move_cursor(event.key, [i, j]);
305
+ const next_coords = move_cursor(event.key, [i, j], data);
306
+ if (next_coords) {
307
+ if (event.shiftKey) {
308
+ selected_cells = get_range_selection(
309
+ selected_cells.length > 0 ? selected_cells[0] : [i, j],
310
+ next_coords
311
+ );
312
+ editing = false;
313
+ } else {
314
+ selected_cells = [next_coords];
315
+ editing = next_coords;
316
+ clear_on_focus = false;
317
+ }
318
+ selected = next_coords;
319
+ } else if (
320
+ next_coords === false &&
321
+ event.key === "ArrowUp" &&
322
+ i === 0
323
+ ) {
324
+ selected_header = j;
325
+ selected = false;
326
+ editing = false;
327
+ }
289
328
  break;
290
329
 
291
330
  case "Escape":
@@ -314,39 +353,26 @@
314
353
  selected = [i, j];
315
354
  } else {
316
355
  editing = [i, j];
356
+ clear_on_focus = false;
317
357
  }
318
358
  }
319
-
320
- break;
321
- case "Backspace":
322
- if (!editable) break;
323
- if (!editing) {
324
- event.preventDefault();
325
- data[i][j].value = "";
326
- }
327
- break;
328
- case "Delete":
329
- if (!editable) break;
330
- if (!editing) {
331
- event.preventDefault();
332
- data[i][j].value = "";
333
- }
334
359
  break;
335
360
  case "Tab":
336
- let direction = event.shiftKey ? -1 : 1;
337
-
338
- let is_data_x = data[i][j + direction];
339
- let is_data_y =
340
- data?.[i + direction]?.[direction > 0 ? 0 : _headers.length - 1];
341
-
342
- if (is_data_x || is_data_y) {
343
- event.preventDefault();
344
- selected = is_data_x
345
- ? [i, j + direction]
346
- : [i + direction, direction > 0 ? 0 : _headers.length - 1];
347
- }
361
+ event.preventDefault();
348
362
  editing = false;
349
-
363
+ const next_cell = get_next_cell_coordinates(
364
+ [i, j],
365
+ data,
366
+ event.shiftKey
367
+ );
368
+ if (next_cell) {
369
+ selected_cells = [next_cell];
370
+ selected = next_cell;
371
+ if (editable) {
372
+ editing = next_cell;
373
+ clear_on_focus = false;
374
+ }
375
+ }
350
376
  break;
351
377
  default:
352
378
  if (!editable) break;
@@ -377,16 +403,12 @@
377
403
  }
378
404
  }
379
405
 
380
- let header_edit: number | false;
381
-
382
- let select_on_focus = false;
383
- let selected_header: number | false = false;
384
406
  async function edit_header(i: number, _select = false): Promise<void> {
385
407
  if (!editable || col_count[1] !== "dynamic" || header_edit === i) return;
386
408
  selected = false;
409
+ selected_cells = [];
387
410
  selected_header = i;
388
411
  header_edit = i;
389
- select_on_focus = _select;
390
412
  }
391
413
 
392
414
  function end_header_edit(event: CustomEvent<KeyboardEvent>): void {
@@ -461,107 +483,14 @@
461
483
  }
462
484
 
463
485
  function handle_click_outside(event: Event): void {
464
- if (
465
- (active_cell_menu &&
466
- !(event.target as HTMLElement).closest(".cell-menu")) ||
467
- (active_header_menu &&
468
- !(event.target as HTMLElement).closest(".cell-menu"))
469
- ) {
486
+ if (handle_click_outside_util(event, parent)) {
487
+ editing = false;
488
+ selected_cells = [];
489
+ header_edit = false;
490
+ selected_header = false;
470
491
  active_cell_menu = null;
471
492
  active_header_menu = null;
472
493
  }
473
-
474
- const [trigger] = event.composedPath() as HTMLElement[];
475
- if (parent.contains(trigger)) {
476
- return;
477
- }
478
-
479
- clicked_cell = undefined;
480
- editing = false;
481
- selected = false;
482
- header_edit = false;
483
- selected_header = false;
484
- active_cell_menu = null;
485
- active_header_menu = null;
486
- }
487
-
488
- function guess_delimitaor(
489
- text: string,
490
- possibleDelimiters: string[]
491
- ): string[] {
492
- return possibleDelimiters.filter(weedOut);
493
-
494
- function weedOut(delimiter: string): boolean {
495
- var cache = -1;
496
- return text.split("\n").every(checkLength);
497
-
498
- function checkLength(line: string): boolean {
499
- if (!line) {
500
- return true;
501
- }
502
-
503
- var length = line.split(delimiter).length;
504
- if (cache < 0) {
505
- cache = length;
506
- }
507
- return cache === length && length > 1;
508
- }
509
- }
510
- }
511
-
512
- function data_uri_to_blob(data_uri: string): Blob {
513
- const byte_str = atob(data_uri.split(",")[1]);
514
- const mime_str = data_uri.split(",")[0].split(":")[1].split(";")[0];
515
-
516
- const ab = new ArrayBuffer(byte_str.length);
517
- const ia = new Uint8Array(ab);
518
-
519
- for (let i = 0; i < byte_str.length; i++) {
520
- ia[i] = byte_str.charCodeAt(i);
521
- }
522
-
523
- return new Blob([ab], { type: mime_str });
524
- }
525
-
526
- function blob_to_string(blob: Blob): void {
527
- const reader = new FileReader();
528
-
529
- function handle_read(e: ProgressEvent<FileReader>): void {
530
- if (!e?.target?.result || typeof e.target.result !== "string") return;
531
-
532
- const [delimiter] = guess_delimitaor(e.target.result, [",", "\t"]);
533
-
534
- const [head, ...rest] = dsvFormat(delimiter).parseRows(e.target.result);
535
-
536
- _headers = make_headers(
537
- col_count[1] === "fixed" ? head.slice(0, col_count[0]) : head
538
- );
539
-
540
- values = rest;
541
- reader.removeEventListener("loadend", handle_read);
542
- }
543
-
544
- reader.addEventListener("loadend", handle_read);
545
-
546
- reader.readAsText(blob);
547
- }
548
-
549
- let dragging = false;
550
-
551
- function get_max(
552
- _d: { value: any; id: string }[][]
553
- ): { value: any; id: string }[] {
554
- if (!_d || _d.length === 0 || !_d[0]) return [];
555
- let max = _d[0].slice();
556
- for (let i = 0; i < _d.length; i++) {
557
- for (let j = 0; j < _d[i].length; j++) {
558
- if (`${max[j].value}`.length < `${_d[i][j].value}`.length) {
559
- max[j] = _d[i][j];
560
- }
561
- }
562
- }
563
-
564
- return max;
565
494
  }
566
495
 
567
496
  $: max = get_max(data);
@@ -632,7 +561,7 @@
632
561
  data = data;
633
562
 
634
563
  if (id) {
635
- const [i, j] = get_current_indices(id);
564
+ const [i, j] = get_current_indices(id, data);
636
565
  selected = [i, j];
637
566
  }
638
567
  }
@@ -644,19 +573,17 @@
644
573
  let is_visible = false;
645
574
 
646
575
  onMount(() => {
647
- const observer = new IntersectionObserver((entries, observer) => {
576
+ const observer = new IntersectionObserver((entries) => {
648
577
  entries.forEach((entry) => {
649
578
  if (entry.isIntersecting && !is_visible) {
650
579
  set_cell_widths();
651
580
  data = data;
652
581
  }
653
-
654
582
  is_visible = entry.isIntersecting;
655
583
  });
656
584
  });
657
585
 
658
586
  observer.observe(parent);
659
-
660
587
  document.addEventListener("click", handle_click_outside);
661
588
  window.addEventListener("resize", handle_resize);
662
589
  document.addEventListener("fullscreenchange", handle_fullscreen_change);
@@ -672,14 +599,43 @@
672
599
  };
673
600
  });
674
601
 
675
- let highlighted_column: number | null = null;
602
+ function handle_cell_click(
603
+ event: MouseEvent,
604
+ row: number,
605
+ col: number
606
+ ): void {
607
+ event.preventDefault();
608
+ event.stopPropagation();
609
+ clear_on_focus = false;
610
+ active_cell_menu = null;
611
+ active_header_menu = null;
612
+ selected_header = false;
613
+ header_edit = false;
676
614
 
677
- let active_cell_menu: {
678
- row: number;
679
- col: number;
680
- x: number;
681
- y: number;
682
- } | null = null;
615
+ selected_cells = handle_selection([row, col], selected_cells, event);
616
+
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;
629
+ }
630
+
631
+ toggle_cell_button(row, col);
632
+
633
+ dispatch("select", {
634
+ index: [row, col],
635
+ value: get_data_at(row, col),
636
+ row_value: data[row].map((d) => d.value)
637
+ });
638
+ }
683
639
 
684
640
  function toggle_cell_menu(event: MouseEvent, row: number, col: number): void {
685
641
  event.stopPropagation();
@@ -693,12 +649,7 @@
693
649
  const cell = (event.target as HTMLElement).closest("td");
694
650
  if (cell) {
695
651
  const rect = cell.getBoundingClientRect();
696
- active_cell_menu = {
697
- row,
698
- col,
699
- x: rect.right,
700
- y: rect.bottom
701
- };
652
+ active_cell_menu = { row, col, x: rect.right, y: rect.bottom };
702
653
  }
703
654
  }
704
655
  }
@@ -730,33 +681,21 @@
730
681
  } | null = null;
731
682
 
732
683
  function toggle_header_button(col: number): void {
733
- if (active_button?.type === "header" && active_button.col === col) {
734
- active_button = null;
735
- } else {
736
- active_button = { type: "header", col };
737
- }
684
+ active_button =
685
+ active_button?.type === "header" && active_button.col === col
686
+ ? null
687
+ : { type: "header", col };
738
688
  }
739
689
 
740
690
  function toggle_cell_button(row: number, col: number): void {
741
- if (
691
+ active_button =
742
692
  active_button?.type === "cell" &&
743
693
  active_button.row === row &&
744
694
  active_button.col === col
745
- ) {
746
- active_button = null;
747
- } else {
748
- active_button = { type: "cell", row, col };
749
- }
695
+ ? null
696
+ : { type: "cell", row, col };
750
697
  }
751
698
 
752
- let active_header_menu: {
753
- col: number;
754
- x: number;
755
- y: number;
756
- } | null = null;
757
-
758
- let is_fullscreen = false;
759
-
760
699
  function toggle_fullscreen(): void {
761
700
  if (!document.fullscreenElement) {
762
701
  parent.requestFullscreen();
@@ -771,6 +710,10 @@
771
710
  is_fullscreen = !!document.fullscreenElement;
772
711
  }
773
712
 
713
+ async function handle_copy(): Promise<void> {
714
+ await copy_table_data(data, _headers, selected_cells);
715
+ }
716
+
774
717
  function toggle_header_menu(event: MouseEvent, col: number): void {
775
718
  event.stopPropagation();
776
719
  if (active_header_menu && active_header_menu.col === col) {
@@ -779,11 +722,7 @@
779
722
  const header = (event.target as HTMLElement).closest("th");
780
723
  if (header) {
781
724
  const rect = header.getBoundingClientRect();
782
- active_header_menu = {
783
- col,
784
- x: rect.right,
785
- y: rect.bottom
786
- };
725
+ active_header_menu = { col, x: rect.right, y: rect.bottom };
787
726
  }
788
727
  }
789
728
  }
@@ -791,6 +730,42 @@
791
730
  afterUpdate(() => {
792
731
  value_is_output = false;
793
732
  });
733
+
734
+ async function delete_row(index: number): Promise<void> {
735
+ parent.focus();
736
+ if (row_count[1] !== "dynamic") return;
737
+ if (data.length <= 1) return;
738
+ data.splice(index, 1);
739
+ data = data;
740
+ selected = false;
741
+ }
742
+
743
+ async function delete_col(index: number): Promise<void> {
744
+ parent.focus();
745
+ if (col_count[1] !== "dynamic") return;
746
+ if (data[0].length <= 1) return;
747
+
748
+ _headers.splice(index, 1);
749
+ _headers = _headers;
750
+
751
+ data.forEach((row) => {
752
+ row.splice(index, 1);
753
+ });
754
+ data = data;
755
+ selected = false;
756
+ }
757
+
758
+ function delete_row_at(index: number): void {
759
+ delete_row(index);
760
+ active_cell_menu = null;
761
+ active_header_menu = null;
762
+ }
763
+
764
+ function delete_col_at(index: number): void {
765
+ delete_col(index);
766
+ active_cell_menu = null;
767
+ active_header_menu = null;
768
+ }
794
769
  </script>
795
770
 
796
771
  <svelte:window on:resize={() => set_cell_widths()} />
@@ -806,6 +781,8 @@
806
781
  {show_fullscreen_button}
807
782
  {is_fullscreen}
808
783
  on:click={toggle_fullscreen}
784
+ on_copy={handle_copy}
785
+ {show_copy_button}
809
786
  />
810
787
  </div>
811
788
  <div
@@ -846,6 +823,7 @@
846
823
  edit={false}
847
824
  el={null}
848
825
  {root}
826
+ {editable}
849
827
  />
850
828
 
851
829
  <div
@@ -881,6 +859,7 @@
881
859
  edit={false}
882
860
  el={null}
883
861
  {root}
862
+ {editable}
884
863
  />
885
864
  </div>
886
865
  </td>
@@ -896,8 +875,20 @@
896
875
  boundedheight={false}
897
876
  disable_click={true}
898
877
  {root}
899
- on:load={(e) => blob_to_string(data_uri_to_blob(e.detail.data))}
878
+ on:load={({ detail }) =>
879
+ handle_file_upload(
880
+ detail.data,
881
+ col_count,
882
+ (head) => {
883
+ _headers = make_headers(head);
884
+ return _headers;
885
+ },
886
+ (vals) => {
887
+ values = vals;
888
+ }
889
+ )}
900
890
  bind:dragging
891
+ aria_label={i18n("dataframe.drop_to_upload")}
901
892
  >
902
893
  <VirtualTable
903
894
  bind:items={data}
@@ -925,6 +916,7 @@
925
916
  <div class="cell-wrap">
926
917
  <div class="header-content">
927
918
  <EditableCell
919
+ {max_chars}
928
920
  bind:value={_headers[i].value}
929
921
  bind:el={els[id].input}
930
922
  {latex_delimiters}
@@ -934,6 +926,7 @@
934
926
  on:dblclick={() => edit_header(i)}
935
927
  header
936
928
  {root}
929
+ {editable}
937
930
  />
938
931
  <button
939
932
  class:sorted={sort_by === i}
@@ -962,7 +955,7 @@
962
955
  class="cell-menu-button"
963
956
  on:click={(event) => toggle_header_menu(event, i)}
964
957
  >
965
-
958
+ &#8942;
966
959
  </button>
967
960
  {/if}
968
961
  </div>
@@ -978,40 +971,24 @@
978
971
  <td
979
972
  tabindex="0"
980
973
  on:touchstart={(event) => {
981
- event.preventDefault();
982
- event.stopPropagation();
983
- clear_on_focus = false;
984
- clicked_cell = { row: index, col: j };
985
- selected = [index, j];
986
- selected_header = false;
987
- header_edit = false;
988
- if (editable) {
989
- editing = [index, j];
990
- }
991
- toggle_cell_button(index, j);
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);
992
983
  }}
993
984
  on:mousedown={(event) => {
994
985
  event.preventDefault();
995
986
  event.stopPropagation();
996
987
  }}
997
- on:click={(event) => {
998
- event.preventDefault();
999
- event.stopPropagation();
1000
- clear_on_focus = false;
1001
- active_cell_menu = null;
1002
- active_header_menu = null;
1003
- clicked_cell = { row: index, col: j };
1004
- selected = [index, j];
1005
- selected_header = false;
1006
- header_edit = false;
1007
- if (editable) {
1008
- editing = [index, j];
1009
- }
1010
- toggle_cell_button(index, j);
1011
- }}
988
+ on:click={(event) => handle_cell_click(event, index, j)}
1012
989
  style:width="var(--cell-width-{j})"
1013
990
  style={styling?.[index]?.[j] || ""}
1014
- class:focus={dequal(selected, [index, j])}
991
+ class={is_cell_selected([index, j], selected_cells)}
1015
992
  class:menu-active={active_cell_menu &&
1016
993
  active_cell_menu.row === index &&
1017
994
  active_cell_menu.col === j}
@@ -1030,15 +1007,25 @@
1030
1007
  clear_on_focus = false;
1031
1008
  parent.focus();
1032
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
+ }}
1033
1019
  {clear_on_focus}
1034
1020
  {root}
1021
+ {max_chars}
1035
1022
  />
1036
- {#if editable}
1023
+ {#if editable && should_show_cell_menu([index, j], selected_cells, editable)}
1037
1024
  <button
1038
1025
  class="cell-menu-button"
1039
1026
  on:click={(event) => toggle_cell_menu(event, index, j)}
1040
1027
  >
1041
-
1028
+ &#8942;
1042
1029
  </button>
1043
1030
  {/if}
1044
1031
  </div>
@@ -1050,18 +1037,22 @@
1050
1037
  </div>
1051
1038
  </div>
1052
1039
 
1053
- {#if active_cell_menu !== null}
1040
+ {#if active_cell_menu}
1054
1041
  <CellMenu
1055
- {i18n}
1056
1042
  x={active_cell_menu.x}
1057
1043
  y={active_cell_menu.y}
1058
- row={active_cell_menu?.row ?? -1}
1044
+ row={active_cell_menu.row}
1059
1045
  {col_count}
1060
1046
  {row_count}
1061
- on_add_row_above={() => add_row_at(active_cell_menu?.row ?? -1, "above")}
1062
- on_add_row_below={() => add_row_at(active_cell_menu?.row ?? -1, "below")}
1063
- on_add_column_left={() => add_col_at(active_cell_menu?.col ?? -1, "left")}
1064
- on_add_column_right={() => add_col_at(active_cell_menu?.col ?? -1, "right")}
1047
+ on_add_row_above={() => add_row_at(active_cell_menu?.row || 0, "above")}
1048
+ on_add_row_below={() => add_row_at(active_cell_menu?.row || 0, "below")}
1049
+ on_add_column_left={() => add_col_at(active_cell_menu?.col || 0, "left")}
1050
+ on_add_column_right={() => add_col_at(active_cell_menu?.col || 0, "right")}
1051
+ on_delete_row={() => delete_row_at(active_cell_menu?.row || 0)}
1052
+ on_delete_col={() => delete_col_at(active_cell_menu?.col || 0)}
1053
+ can_delete_rows={data.length > 1}
1054
+ can_delete_cols={data[0].length > 1}
1055
+ {i18n}
1065
1056
  />
1066
1057
  {/if}
1067
1058
 
@@ -1078,25 +1069,18 @@
1078
1069
  on_add_column_left={() => add_col_at(active_header_menu?.col ?? -1, "left")}
1079
1070
  on_add_column_right={() =>
1080
1071
  add_col_at(active_header_menu?.col ?? -1, "right")}
1072
+ on_delete_row={() => delete_row_at(active_cell_menu?.row ?? -1)}
1073
+ on_delete_col={() => delete_col_at(active_header_menu?.col ?? -1)}
1074
+ can_delete_rows={false}
1075
+ can_delete_cols={data[0].length > 1}
1081
1076
  />
1082
1077
  {/if}
1083
1078
 
1084
1079
  <style>
1085
- .button-wrap:hover svg {
1086
- color: var(--color-accent);
1087
- }
1088
-
1089
- .button-wrap svg {
1090
- margin-right: var(--size-1);
1091
- margin-left: -5px;
1092
- }
1093
-
1094
- .label p {
1095
- position: relative;
1096
- z-index: var(--layer-4);
1097
- margin-bottom: var(--size-2);
1098
- color: var(--block-label-text-color);
1099
- font-size: var(--block-label-text-size);
1080
+ .table-container {
1081
+ display: flex;
1082
+ flex-direction: column;
1083
+ gap: var(--size-2);
1100
1084
  }
1101
1085
 
1102
1086
  .table-wrap {
@@ -1109,7 +1093,6 @@
1109
1093
 
1110
1094
  .table-wrap:focus-within {
1111
1095
  outline: none;
1112
- background-color: none;
1113
1096
  }
1114
1097
 
1115
1098
  .dragging {
@@ -1131,6 +1114,7 @@
1131
1114
  line-height: var(--line-md);
1132
1115
  font-family: var(--font-mono);
1133
1116
  border-spacing: 0;
1117
+ border-collapse: separate;
1134
1118
  }
1135
1119
 
1136
1120
  div:not(.no-wrap) td {
@@ -1185,6 +1169,12 @@
1185
1169
  th.focus,
1186
1170
  td.focus {
1187
1171
  --ring-color: var(--color-accent);
1172
+ box-shadow: inset 0 0 0 2px var(--ring-color);
1173
+ z-index: var(--layer-1);
1174
+ }
1175
+
1176
+ th.focus {
1177
+ z-index: var(--layer-2);
1188
1178
  }
1189
1179
 
1190
1180
  tr:last-child td:first-child {
@@ -1213,7 +1203,6 @@
1213
1203
  cursor: pointer;
1214
1204
  padding: var(--size-2);
1215
1205
  color: var(--body-text-color-subdued);
1216
- line-height: var(--text-sm);
1217
1206
  }
1218
1207
 
1219
1208
  .sort-button:hover {
@@ -1234,11 +1223,11 @@
1234
1223
 
1235
1224
  .cell-wrap {
1236
1225
  display: flex;
1237
- align-items: center;
1226
+ align-items: flex-start;
1238
1227
  outline: none;
1239
- height: var(--size-full);
1240
1228
  min-height: var(--size-9);
1241
- overflow: hidden;
1229
+ position: relative;
1230
+ height: auto;
1242
1231
  }
1243
1232
 
1244
1233
  .header-content {
@@ -1247,12 +1236,9 @@
1247
1236
  overflow: hidden;
1248
1237
  flex-grow: 1;
1249
1238
  min-width: 0;
1250
- }
1251
-
1252
- .controls-wrap {
1253
- display: flex;
1254
- justify-content: flex-end;
1255
- padding-top: var(--size-2);
1239
+ white-space: normal;
1240
+ overflow-wrap: break-word;
1241
+ word-break: break-word;
1256
1242
  }
1257
1243
 
1258
1244
  .row_odd {
@@ -1263,10 +1249,6 @@
1263
1249
  background: var(--background-fill-primary);
1264
1250
  }
1265
1251
 
1266
- table {
1267
- border-collapse: separate;
1268
- }
1269
-
1270
1252
  .cell-menu-button {
1271
1253
  flex-shrink: 0;
1272
1254
  display: none;
@@ -1279,28 +1261,35 @@
1279
1261
  padding: 0;
1280
1262
  margin-right: var(--spacing-sm);
1281
1263
  z-index: var(--layer-1);
1264
+ position: absolute;
1265
+ right: var(--size-1);
1266
+ top: 50%;
1267
+ transform: translateY(-50%);
1282
1268
  }
1283
1269
 
1284
- .cell-menu-button:hover {
1285
- background-color: var(--color-bg-hover);
1270
+ .cell-selected .cell-menu-button {
1271
+ display: flex;
1272
+ align-items: center;
1273
+ justify-content: center;
1286
1274
  }
1287
1275
 
1288
- td.focus .cell-menu-button {
1276
+ .header-row {
1289
1277
  display: flex;
1278
+ justify-content: space-between;
1290
1279
  align-items: center;
1291
- justify-content: center;
1280
+ gap: var(--size-2);
1281
+ height: var(--size-6);
1282
+ min-height: var(--size-6);
1292
1283
  }
1293
1284
 
1294
- th .header-content {
1295
- white-space: normal;
1296
- overflow-wrap: break-word;
1297
- word-break: break-word;
1285
+ .label {
1286
+ flex: 1;
1298
1287
  }
1299
1288
 
1300
- .table-container {
1301
- display: flex;
1302
- flex-direction: column;
1303
- gap: var(--size-2);
1289
+ .label p {
1290
+ margin: 0;
1291
+ color: var(--block-label-text-color);
1292
+ font-size: var(--block-label-text-size);
1304
1293
  }
1305
1294
 
1306
1295
  .row-number,
@@ -1332,24 +1321,94 @@
1332
1321
  background: var(--table-odd-background-fill);
1333
1322
  }
1334
1323
 
1335
- .header-row {
1336
- display: flex;
1337
- justify-content: space-between;
1338
- align-items: center;
1339
- gap: var(--size-2);
1340
- height: var(--size-6);
1341
- min-height: var(--size-6);
1324
+ .cell-selected {
1325
+ --ring-color: var(--color-accent);
1326
+ box-shadow: inset 0 0 0 2px var(--ring-color);
1327
+ z-index: var(--layer-1);
1328
+ position: relative;
1342
1329
  }
1343
1330
 
1344
- .label {
1345
- flex: 1;
1331
+ .cell-selected.no-top {
1332
+ box-shadow:
1333
+ inset 2px 0 0 var(--ring-color),
1334
+ inset -2px 0 0 var(--ring-color),
1335
+ inset 0 -2px 0 var(--ring-color);
1346
1336
  }
1347
1337
 
1348
- .label p {
1349
- position: relative;
1350
- z-index: var(--layer-4);
1351
- margin: 0;
1352
- color: var(--block-label-text-color);
1353
- font-size: var(--block-label-text-size);
1338
+ .cell-selected.no-bottom {
1339
+ box-shadow:
1340
+ inset 2px 0 0 var(--ring-color),
1341
+ inset -2px 0 0 var(--ring-color),
1342
+ inset 0 2px 0 var(--ring-color);
1343
+ }
1344
+
1345
+ .cell-selected.no-left {
1346
+ box-shadow:
1347
+ inset 0 2px 0 var(--ring-color),
1348
+ inset -2px 0 0 var(--ring-color),
1349
+ inset 0 -2px 0 var(--ring-color);
1350
+ }
1351
+
1352
+ .cell-selected.no-right {
1353
+ box-shadow:
1354
+ inset 0 2px 0 var(--ring-color),
1355
+ inset 2px 0 0 var(--ring-color),
1356
+ inset 0 -2px 0 var(--ring-color);
1357
+ }
1358
+
1359
+ .cell-selected.no-top.no-left {
1360
+ box-shadow:
1361
+ inset -2px 0 0 var(--ring-color),
1362
+ inset 0 -2px 0 var(--ring-color);
1363
+ }
1364
+
1365
+ .cell-selected.no-top.no-right {
1366
+ box-shadow:
1367
+ inset 2px 0 0 var(--ring-color),
1368
+ inset 0 -2px 0 var(--ring-color);
1369
+ }
1370
+
1371
+ .cell-selected.no-bottom.no-left {
1372
+ box-shadow:
1373
+ inset -2px 0 0 var(--ring-color),
1374
+ inset 0 2px 0 var(--ring-color);
1375
+ }
1376
+
1377
+ .cell-selected.no-bottom.no-right {
1378
+ box-shadow:
1379
+ inset 2px 0 0 var(--ring-color),
1380
+ inset 0 2px 0 var(--ring-color);
1381
+ }
1382
+
1383
+ .cell-selected.no-top.no-bottom {
1384
+ box-shadow:
1385
+ inset 2px 0 0 var(--ring-color),
1386
+ inset -2px 0 0 var(--ring-color);
1387
+ }
1388
+
1389
+ .cell-selected.no-left.no-right {
1390
+ box-shadow:
1391
+ inset 0 2px 0 var(--ring-color),
1392
+ inset 0 -2px 0 var(--ring-color);
1393
+ }
1394
+
1395
+ .cell-selected.no-top.no-left.no-right {
1396
+ box-shadow: inset 0 -2px 0 var(--ring-color);
1397
+ }
1398
+
1399
+ .cell-selected.no-bottom.no-left.no-right {
1400
+ box-shadow: inset 0 2px 0 var(--ring-color);
1401
+ }
1402
+
1403
+ .cell-selected.no-left.no-top.no-bottom {
1404
+ box-shadow: inset -2px 0 0 var(--ring-color);
1405
+ }
1406
+
1407
+ .cell-selected.no-right.no-top.no-bottom {
1408
+ box-shadow: inset 2px 0 0 var(--ring-color);
1409
+ }
1410
+
1411
+ .cell-selected.no-top.no-bottom.no-left.no-right {
1412
+ box-shadow: none;
1354
1413
  }
1355
1414
  </style>