@gradio/dataframe 0.24.4 → 0.24.6

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.
package/Index.svelte CHANGED
@@ -124,5 +124,6 @@
124
124
  oninput={handle_input}
125
125
  onselect={handle_select}
126
126
  onedit={handle_edit}
127
+ onerror={(message) => gradio.dispatch("error", message)}
127
128
  />
128
129
  </Block>
package/dist/Index.svelte CHANGED
@@ -124,5 +124,6 @@
124
124
  oninput={handle_input}
125
125
  onselect={handle_select}
126
126
  onedit={handle_edit}
127
+ onerror={(message) => gradio.dispatch("error", message)}
127
128
  />
128
129
  </Block>
@@ -16,7 +16,6 @@
16
16
  color: var(--body-text-color);
17
17
  cursor: pointer;
18
18
  transition: all 150ms;
19
- margin-top: var(--size-2);
20
19
  z-index: 10;
21
20
  position: relative;
22
21
  pointer-events: auto;
@@ -35,7 +35,7 @@
35
35
  is_cell_selected,
36
36
  handle_click_outside as handle_click_outside_util
37
37
  } from "./utils/selection_utils";
38
- import { copy_table_data, handle_file_upload } from "./utils/table_utils";
38
+ import { copy_table_data, parse_table_file } from "./utils/table_utils";
39
39
  import { gradio_filter_fn } from "./utils/filter";
40
40
  import { create_column_measurement } from "./column_measurement.svelte.js";
41
41
 
@@ -73,7 +73,8 @@
73
73
  onselect,
74
74
  onedit,
75
75
  onsearch,
76
- onfullscreen
76
+ onfullscreen,
77
+ onerror
77
78
  }: {
78
79
  datatype: Datatype | Datatype[];
79
80
  label?: string | null;
@@ -109,6 +110,7 @@
109
110
  onedit?: (detail: EditData) => void;
110
111
  onsearch?: (detail: string | null) => void;
111
112
  onfullscreen?: () => void;
113
+ onerror?: (message: string) => void;
112
114
  } = $props();
113
115
 
114
116
  type GradioRow = Record<string, CellValue> & { _index: number };
@@ -252,6 +254,10 @@
252
254
 
253
255
  let selected_cells: CellCoordinate[] = $state([]);
254
256
  let selected: CellCoordinate | false = $state(false);
257
+ // where a shift+click range starts. `selected` cannot serve, because it
258
+ // follows every click, the shift+clicks that extend the range included.
259
+ // nothing renders from it, so it does not need to be reactive
260
+ let range_anchor: CellCoordinate | false = false;
255
261
  let editing: CellCoordinate | false = $state(false);
256
262
  let header_edit: number | false = $state(false);
257
263
  let selected_header: number | false = $state(false);
@@ -273,7 +279,11 @@
273
279
  let copy_flash = $state(false);
274
280
  let is_dragging = $state(false);
275
281
  let show_scroll_button = $state(false);
276
- let dragging = $state(false); // file drag
282
+ let file_dragging = $state(false);
283
+ let discard_pending_edit = false;
284
+ // document-unique so several dataframes on a page do not describe each
285
+ // other's grids
286
+ const drop_hint_id = $props.id();
277
287
 
278
288
  let parent: HTMLDivElement;
279
289
 
@@ -295,6 +305,12 @@
295
305
  return rows.findIndex((row) => row.original._index === row_index);
296
306
  }
297
307
 
308
+ // for the whole-selection sweeps, where one findIndex per cell would make
309
+ // Delete and copy quadratic on a column selection
310
+ function visible_row_set(): Set<number> {
311
+ return new Set(rows.map((row) => row.original._index));
312
+ }
313
+
298
314
  function is_active_cell(row: number, col: number): boolean {
299
315
  return !!(
300
316
  keyboard_active &&
@@ -322,6 +338,7 @@
322
338
 
323
339
  function set_active_cell(coord: CellCoordinate, move_focus = true): void {
324
340
  selected = coord;
341
+ range_anchor = coord;
325
342
  selected_cells = [coord];
326
343
  selected_header = false;
327
344
  header_edit = false;
@@ -341,6 +358,7 @@
341
358
  function handle_header_focus(col: number): void {
342
359
  if (header_edit !== false) return;
343
360
  selected = false;
361
+ range_anchor = false;
344
362
  selected_cells = [];
345
363
  selected_header = col;
346
364
  }
@@ -513,7 +531,8 @@
513
531
  function handle_cell_click(
514
532
  event: MouseEvent,
515
533
  row: number,
516
- col: number
534
+ col: number,
535
+ view_index: number
517
536
  ): void {
518
537
  const col_is_static =
519
538
  !editable ||
@@ -525,25 +544,37 @@
525
544
  event.stopPropagation();
526
545
 
527
546
  const coord: CellCoordinate = [row, col];
528
- if (event.shiftKey && selected) {
529
- // range select
530
- const [r1, c1] = selected;
531
- const [r2, c2] = coord;
532
- const new_cells: CellCoordinate[] = [];
533
- for (let r = Math.min(r1, r2); r <= Math.max(r1, r2); r++) {
534
- for (let c = Math.min(c1, c2); c <= Math.max(c1, c2); c++) {
535
- new_cells.push([r, c]);
547
+ if (event.shiftKey && range_anchor) {
548
+ // the range runs from the anchor, and the search may have hidden its
549
+ // row since
550
+ const from_row = visible_row_position(range_anchor[0]);
551
+ if (from_row === -1) {
552
+ selected_cells = [coord];
553
+ range_anchor = coord;
554
+ } else {
555
+ const c1 = range_anchor[1];
556
+ const new_cells: CellCoordinate[] = [];
557
+ for (
558
+ let p = Math.min(from_row, view_index);
559
+ p <= Math.max(from_row, view_index);
560
+ p++
561
+ ) {
562
+ for (let c = Math.min(c1, col); c <= Math.max(c1, col); c++) {
563
+ new_cells.push([rows[p].original._index, c]);
564
+ }
536
565
  }
566
+ selected_cells = new_cells;
537
567
  }
538
- selected_cells = new_cells;
539
568
  } else if (event.metaKey || event.ctrlKey) {
540
569
  // toggle select
541
570
  const exists = selected_cells.some(([r, c]) => r === row && c === col);
542
571
  selected_cells = exists
543
572
  ? selected_cells.filter(([r, c]) => !(r === row && c === col))
544
573
  : [...selected_cells, coord];
574
+ range_anchor = coord;
545
575
  } else {
546
576
  selected_cells = [coord];
577
+ range_anchor = coord;
547
578
  }
548
579
 
549
580
  selected = coord;
@@ -578,12 +609,21 @@
578
609
  blur_event: FocusEvent;
579
610
  coords: [number, number];
580
611
  }): void {
612
+ // an import replaces the table under an open editor, and EditableCell
613
+ // commits when it leaves edit mode either way. that half-typed value
614
+ // belongs to the table that just went away
615
+ if (discard_pending_edit) return;
616
+
581
617
  const { coords } = detail;
582
618
  const input_el = detail.blur_event.target as HTMLTextAreaElement;
583
619
  if (!input_el || input_el.value === undefined) return;
584
620
 
585
621
  const [row, col] = coords;
586
- const old_value = values?.[row]?.[col];
622
+ // and deleting a row leaves the editor's row behind. a column short of
623
+ // the header is not the same thing: a value can arrive ragged, and the
624
+ // cell past the end of its own row still renders and still takes an edit
625
+ if (!values?.[row]) return;
626
+ const old_value = values[row][col];
587
627
  const new_value = input_el.value;
588
628
 
589
629
  if (String(old_value) !== String(new_value)) {
@@ -607,6 +647,7 @@
607
647
 
608
648
  editing = false;
609
649
  selected = false;
650
+ range_anchor = false;
610
651
  selected_cells = [];
611
652
  active_cell_menu = null;
612
653
  active_header_menu = null;
@@ -803,25 +844,51 @@
803
844
  push_change(filtered_values);
804
845
  }
805
846
 
806
- async function handle_copy(): Promise<void> {
847
+ // false when nothing reached the clipboard, so the toolbar can hold back its
848
+ // "Copied to clipboard" state
849
+ async function handle_copy(): Promise<boolean> {
850
+ const visible = visible_row_set();
851
+ const cells =
852
+ selected_cells.length > 0
853
+ ? selected_cells.filter(([r]) => visible.has(r))
854
+ : rows.flatMap((row) => {
855
+ const r = row.original._index;
856
+ return (values[r] ?? []).map((_, c) => [r, c] as CellCoordinate);
857
+ });
858
+ if (cells.length === 0) return false;
859
+
807
860
  const data_for_copy = values.map((row) =>
808
861
  row.map((val, j) => ({ id: `${j}`, value: val }))
809
862
  );
810
- const cells_to_copy = selected_cells.length > 0 ? selected_cells : null;
811
- await copy_table_data(data_for_copy, cells_to_copy);
863
+ try {
864
+ await copy_table_data(data_for_copy, cells);
865
+ } catch (err) {
866
+ // the write can be refused outright, on an insecure origin or without
867
+ // permission. logged because this catches anything else thrown too
868
+ console.error(err);
869
+ return false;
870
+ }
812
871
  copy_flash = true;
813
872
  setTimeout(() => (copy_flash = false), 800);
873
+ return true;
874
+ }
875
+
876
+ // every one of these holds a row or column index, so they have to go
877
+ // whenever the table stops being the one they were recorded against
878
+ function reset_interaction_state(): void {
879
+ selected_cells = [];
880
+ selected = false;
881
+ range_anchor = false;
882
+ editing = false;
883
+ header_edit = false;
884
+ selected_header = false;
885
+ active_cell_menu = null;
886
+ active_header_menu = null;
814
887
  }
815
888
 
816
889
  function handle_click_outside(event: Event): void {
817
890
  if (handle_click_outside_util(event, parent)) {
818
- selected_cells = [];
819
- selected = false;
820
- editing = false;
821
- header_edit = false;
822
- selected_header = false;
823
- active_cell_menu = null;
824
- active_header_menu = null;
891
+ reset_interaction_state();
825
892
  }
826
893
  }
827
894
 
@@ -895,6 +962,7 @@
895
962
  set_active_cell([rows[row_position - 1].original._index, col]);
896
963
  } else {
897
964
  selected = false;
965
+ range_anchor = false;
898
966
  selected_cells = [];
899
967
  selected_header = col;
900
968
  focus_header(col);
@@ -1001,13 +1069,28 @@
1001
1069
  if (!editing && editable) {
1002
1070
  e.preventDefault();
1003
1071
  const new_values = values.map((value_row) => [...value_row]);
1072
+ const visible = visible_row_set();
1073
+ let cleared = false;
1004
1074
  selected_cells.forEach(([selected_row, selected_col]) => {
1005
- if (!is_static_column(selected_col)) {
1075
+ // undefined is a cell past the end of a short row, or a row a
1076
+ // shrunk table no longer has; a null is a value, so it is cleared
1077
+ const current = new_values[selected_row]?.[selected_col];
1078
+ if (
1079
+ !is_static_column(selected_col) &&
1080
+ visible.has(selected_row) &&
1081
+ current !== "" &&
1082
+ current !== undefined
1083
+ ) {
1006
1084
  new_values[selected_row][selected_col] = "";
1085
+ cleared = true;
1007
1086
  }
1008
1087
  });
1009
- values = new_values;
1010
- push_change(new_values);
1088
+ // pushing with nothing written would fire change and input for an
1089
+ // identical table
1090
+ if (cleared) {
1091
+ values = new_values;
1092
+ push_change(new_values);
1093
+ }
1011
1094
  }
1012
1095
  break;
1013
1096
  default:
@@ -1051,21 +1134,87 @@
1051
1134
  }
1052
1135
  }
1053
1136
 
1054
- function on_file_upload(file_data: any): void {
1055
- handle_file_upload(
1056
- typeof file_data === "string" ? file_data : (file_data?.data ?? ""),
1057
- (head) => {
1058
- headers = head.map((h: any) => h ?? "");
1059
- return (headers as string[]).map((h: string, i: number) => ({
1060
- id: `h_${i}`,
1061
- value: h
1062
- }));
1063
- },
1064
- (vals) => {
1065
- values = vals;
1066
- push_change(vals, headers as string[]);
1067
- }
1137
+ function interpolate(
1138
+ message: string,
1139
+ values: Record<string, string | number>
1140
+ ): string {
1141
+ return message.replace(/\{(\w+)\}/g, (_, key) => String(values[key]));
1142
+ }
1143
+
1144
+ function apply_imported_table(
1145
+ new_headers: (string | null)[],
1146
+ new_values: CellValue[][]
1147
+ ): void {
1148
+ if (!new_headers.length) {
1149
+ throw new Error(i18n("dataframe.import_empty_file"));
1150
+ }
1151
+ // a file of separators alone parses into a header of blank names, and
1152
+ // importing it would replace the table with unnamed columns
1153
+ if (new_headers.every((h) => !h?.trim())) {
1154
+ throw new Error(i18n("dataframe.import_no_column_names"));
1155
+ }
1156
+ // a row the header cannot account for would reach the backend as a ragged
1157
+ // value, and the column checks below only see the header
1158
+ const ragged = new_values.findIndex(
1159
+ (row) => row.length !== new_headers.length
1068
1160
  );
1161
+ if (ragged !== -1) {
1162
+ throw new Error(
1163
+ interpolate(i18n("dataframe.import_ragged_row"), {
1164
+ line: ragged + 2,
1165
+ fields: new_values[ragged].length,
1166
+ columns: new_headers.length
1167
+ })
1168
+ );
1169
+ }
1170
+ // the menu paths already refuse to change a fixed shape or write to a
1171
+ // read-only column, so an import must not be the way around them
1172
+ if (static_columns.length > 0) {
1173
+ throw new Error(i18n("dataframe.import_static_columns"));
1174
+ }
1175
+ if (col_count[1] === "fixed" && new_headers.length !== col_count[0]) {
1176
+ throw new Error(
1177
+ interpolate(i18n("dataframe.import_fixed_columns"), {
1178
+ expected: col_count[0],
1179
+ actual: new_headers.length
1180
+ })
1181
+ );
1182
+ }
1183
+ if (row_count[1] === "fixed" && new_values.length !== row_count[0]) {
1184
+ throw new Error(
1185
+ interpolate(i18n("dataframe.import_fixed_rows"), {
1186
+ expected: row_count[0],
1187
+ actual: new_values.length
1188
+ })
1189
+ );
1190
+ }
1191
+
1192
+ discard_pending_edit = true;
1193
+ headers = new_headers;
1194
+ values = new_values;
1195
+ reset_interaction_state();
1196
+ // tanstack keys these by positional col_N, so they would land on whichever
1197
+ // column now sits at that index. a search is worse than misplaced: it hides
1198
+ // imported rows, and commit_filter then drops the hidden ones from the value
1199
+ sorting = [];
1200
+ column_filters = [];
1201
+ global_filter = "";
1202
+ push_change(new_values, headers as string[]);
1203
+ tick().then(() => (discard_pending_edit = false));
1204
+ }
1205
+
1206
+ // undefined when every dropped file was filtered out by `filetype`
1207
+ function on_file_upload(file: File | undefined): void {
1208
+ if (!editable || !file) return;
1209
+ parse_table_file(file)
1210
+ .then(({ headers: new_headers, values: new_values }) =>
1211
+ apply_imported_table(new_headers, new_values)
1212
+ )
1213
+ .catch((e) => {
1214
+ const message = e instanceof Error ? e.message : String(e);
1215
+ if (onerror) onerror(message);
1216
+ else console.error(message);
1217
+ });
1069
1218
  }
1070
1219
 
1071
1220
  onMount(() => {
@@ -1133,7 +1282,11 @@
1133
1282
  }}
1134
1283
  />
1135
1284
 
1136
- <div class="table-container" class:fullscreen>
1285
+ <div
1286
+ class="table-container"
1287
+ class:fullscreen
1288
+ class:no-rows={values.length === 0}
1289
+ >
1137
1290
  {#if (label && label.length !== 0 && show_label) || (buttons === null ? true : buttons.includes("fullscreen")) || (buttons === null ? true : buttons.includes("copy")) || show_search !== "none"}
1138
1291
  <div class="header-row">
1139
1292
  {#if label && label.length !== 0 && show_label}
@@ -1159,6 +1312,7 @@
1159
1312
  bind:this={parent}
1160
1313
  class="table-wrap"
1161
1314
  class:dragging={is_dragging}
1315
+ class:file-dragging={file_dragging && editable}
1162
1316
  class:menu-open={active_cell_menu || active_header_menu}
1163
1317
  onkeydown={handle_keydown}
1164
1318
  role="grid"
@@ -1166,6 +1320,7 @@
1166
1320
  aria-rowcount={rows.length + 1}
1167
1321
  aria-colcount={resolved_headers.length + Number(show_row_numbers)}
1168
1322
  aria-readonly={!editable}
1323
+ aria-describedby={editable ? drop_hint_id : undefined}
1169
1324
  tabindex={rows.length === 0 ? 0 : -1}
1170
1325
  style="--df-max-col-width: {viewport_width}px;"
1171
1326
  >
@@ -1176,10 +1331,12 @@
1176
1331
  center={false}
1177
1332
  boundedheight={false}
1178
1333
  disable_click={true}
1334
+ format="blob"
1335
+ filetype={[".csv", ".tsv"]}
1179
1336
  {root}
1180
1337
  onload={on_file_upload}
1181
- bind:dragging
1182
- aria_label={i18n("dataframe.drop_to_upload")}
1338
+ onerror={editable ? onerror : undefined}
1339
+ bind:dragging={file_dragging}
1183
1340
  tab_index={-1}
1184
1341
  container_element="div"
1185
1342
  >
@@ -1281,6 +1438,10 @@
1281
1438
  {#each virtual_items as virtual_row (virtual_row.key)}
1282
1439
  {@const row = rows[virtual_row.index]}
1283
1440
  {@const row_idx = row?.original._index ?? virtual_row.index}
1441
+ {@const row_above =
1442
+ rows[virtual_row.index - 1]?.original._index ?? null}
1443
+ {@const row_below =
1444
+ rows[virtual_row.index + 1]?.original._index ?? null}
1284
1445
  {#if row}
1285
1446
  <div
1286
1447
  class="virtual-row"
@@ -1325,7 +1486,9 @@
1325
1486
  cell_style={get_styling(row_idx, col_idx)}
1326
1487
  selection_classes={is_cell_selected(
1327
1488
  [row_idx, col_idx],
1328
- selected_cells
1489
+ selected_cells,
1490
+ row_above,
1491
+ row_below
1329
1492
  )}
1330
1493
  is_active={is_active_cell(row_idx, col_idx)}
1331
1494
  aria_row_index={virtual_row.index + 2}
@@ -1357,7 +1520,8 @@
1357
1520
  {components}
1358
1521
  {is_dragging}
1359
1522
  wrap_text={wrap}
1360
- onmousedown={(e) => handle_cell_click(e, row_idx, col_idx)}
1523
+ onmousedown={(e) =>
1524
+ handle_cell_click(e, row_idx, col_idx, virtual_row.index)}
1361
1525
  ondblclick={(e) =>
1362
1526
  handle_cell_dblclick(e, row_idx, col_idx)}
1363
1527
  oncontextmenu={(e) => {
@@ -1396,6 +1560,19 @@
1396
1560
  <button class="scroll-top-button" onclick={scroll_to_top}>&uarr;</button>
1397
1561
  {/if}
1398
1562
  </div>
1563
+
1564
+ <!-- outside the grid: #13729 took the label off a span like this one and put it
1565
+ on the grid as an attribute, and a test pins that there is no sr-only text
1566
+ inside. a description has no equally well supported attribute form -->
1567
+ {#if editable}
1568
+ <span id={drop_hint_id} class="drop-hint"
1569
+ >{i18n("dataframe.drop_to_upload")}</span
1570
+ >
1571
+ {/if}
1572
+
1573
+ {#if values.length === 0 && editable && row_count[1] === "dynamic"}
1574
+ <EmptyRowButton on_click={() => add_row()} />
1575
+ {/if}
1399
1576
  </div>
1400
1577
 
1401
1578
  {#if active_cell_menu || active_header_menu}
@@ -1462,10 +1639,6 @@
1462
1639
  />
1463
1640
  {/if}
1464
1641
 
1465
- {#if values.length === 0 && editable && row_count[1] === "dynamic"}
1466
- <EmptyRowButton on_click={() => add_row()} />
1467
- {/if}
1468
-
1469
1642
  <style>
1470
1643
  .table-container {
1471
1644
  display: flex;
@@ -1502,10 +1675,23 @@
1502
1675
  min-height: 0;
1503
1676
  }
1504
1677
 
1678
+ /* An empty body would otherwise fill the screen and carry the add-row button off it.
1679
+ Gated on the data alone, so empty tables that show no button stop stretching too. */
1680
+ .table-container.fullscreen.no-rows .table-wrap,
1681
+ .table-container.fullscreen.no-rows .table-wrap > :global(*),
1682
+ .table-container.fullscreen.no-rows .virtual-table-viewport {
1683
+ flex: 0 0 auto;
1684
+ }
1685
+
1505
1686
  .table-wrap {
1506
1687
  position: relative;
1507
- transition: 150ms;
1508
1688
  width: 100%;
1689
+ /* the duration used to stand alone, which means every property. that
1690
+ swept the drag outline in from `currentcolor`, near-white in dark
1691
+ mode, before it reached the accent. the list is what the fullscreen
1692
+ rules above actually change */
1693
+ transition-property: flex-grow, flex-shrink, flex-basis, min-height;
1694
+ transition-duration: 150ms;
1509
1695
  }
1510
1696
 
1511
1697
  /* Constrain Upload component wrapper */
@@ -1521,6 +1707,27 @@
1521
1707
  outline: none;
1522
1708
  }
1523
1709
 
1710
+ /* after :focus-within, which is equally specific and would otherwise
1711
+ clear the outline whenever a cell is focused. drawn inside the box so
1712
+ hovering a file over the table shifts nothing */
1713
+ .table-wrap.file-dragging {
1714
+ outline: 2px solid var(--color-accent);
1715
+ outline-offset: -2px;
1716
+ /* matches the .upload-container border the outline covers */
1717
+ border-radius: var(--table-radius);
1718
+ }
1719
+
1720
+ /* an aria-describedby target, so it has to stay in the accessibility tree
1721
+ rather than be hidden with display: none */
1722
+ .drop-hint {
1723
+ position: absolute;
1724
+ clip-path: inset(50%);
1725
+ width: 1px;
1726
+ height: 1px;
1727
+ overflow: hidden;
1728
+ white-space: nowrap;
1729
+ }
1730
+
1524
1731
  .table-wrap.dragging {
1525
1732
  cursor: crosshair !important;
1526
1733
  user-select: none;
@@ -42,6 +42,7 @@ type $$ComponentProps = {
42
42
  onedit?: (detail: EditData) => void;
43
43
  onsearch?: (detail: string | null) => void;
44
44
  onfullscreen?: () => void;
45
+ onerror?: (message: string) => void;
45
46
  };
46
47
  declare const Table: import("svelte").Component<$$ComponentProps, {}, "headers" | "values" | "value_is_output">;
47
48
  type Table = ReturnType<typeof Table>;
@@ -18,7 +18,7 @@
18
18
  show_copy_button?: boolean;
19
19
  show_search?: "none" | "search" | "filter";
20
20
  fullscreen?: boolean;
21
- on_copy: () => Promise<void>;
21
+ on_copy: () => Promise<boolean>;
22
22
  on_commit_filter: () => void;
23
23
  current_search_query?: string | null;
24
24
  onsearch?: (query: string | null) => void;
@@ -48,8 +48,7 @@
48
48
  }
49
49
 
50
50
  async function handle_copy(): Promise<void> {
51
- await on_copy();
52
- copy_feedback();
51
+ if (await on_copy()) copy_feedback();
53
52
  }
54
53
 
55
54
  $effect(() => {
@@ -3,7 +3,7 @@ type $$ComponentProps = {
3
3
  show_copy_button?: boolean;
4
4
  show_search?: "none" | "search" | "filter";
5
5
  fullscreen?: boolean;
6
- on_copy: () => Promise<void>;
6
+ on_copy: () => Promise<boolean>;
7
7
  on_commit_filter: () => void;
8
8
  current_search_query?: string | null;
9
9
  onsearch?: (query: string | null) => void;
@@ -1,10 +1,6 @@
1
1
  export type CellCoordinate = [number, number];
2
2
  export type EditingState = CellCoordinate | false;
3
3
  export type Headers = (string | null)[];
4
- export interface HeadersWithIDs {
5
- id: string;
6
- value: string;
7
- }
8
4
  export type CellValue = string | number | boolean;
9
5
  export interface TableCell {
10
6
  id: string;
@@ -1,2 +1 @@
1
- [];
2
1
  export {};
@@ -4,7 +4,7 @@ export type CellData = {
4
4
  value: CellValue;
5
5
  };
6
6
  export declare function is_cell_in_selection(coords: [number, number], selected_cells: [number, number][]): boolean;
7
- export declare function is_cell_selected(cell: CellCoordinate, selected_cells: CellCoordinate[]): string;
7
+ export declare function is_cell_selected(cell: CellCoordinate, selected_cells: CellCoordinate[], row_above: number | null, row_below: number | null): string;
8
8
  export declare function get_range_selection(start: CellCoordinate, end: CellCoordinate): CellCoordinate[];
9
9
  export declare function handle_selection(current: CellCoordinate, selected_cells: CellCoordinate[], event: {
10
10
  shiftKey: boolean;
@@ -2,12 +2,15 @@ export function is_cell_in_selection(coords, selected_cells) {
2
2
  const [row, col] = coords;
3
3
  return selected_cells.some(([r, c]) => r === row && c === col);
4
4
  }
5
- export function is_cell_selected(cell, selected_cells) {
5
+ // row_above/row_below are the data indices of the rows the caller is rendering
6
+ // on either side of `cell`, or null at an edge of the view. A search or a sort
7
+ // makes them differ from row ± 1, so only the caller knows them.
8
+ export function is_cell_selected(cell, selected_cells, row_above, row_below) {
6
9
  const [row, col] = cell;
7
10
  if (!selected_cells.some(([r, c]) => r === row && c === col))
8
11
  return "";
9
- const up = selected_cells.some(([r, c]) => r === row - 1 && c === col);
10
- const down = selected_cells.some(([r, c]) => r === row + 1 && c === col);
12
+ const up = selected_cells.some(([r, c]) => r === row_above && c === col);
13
+ const down = selected_cells.some(([r, c]) => r === row_below && c === col);
11
14
  const left = selected_cells.some(([r, c]) => r === row && c === col - 1);
12
15
  const right = selected_cells.some(([r, c]) => r === row && c === col + 1);
13
16
  return `cell-selected${up ? " no-top" : ""}${down ? " no-bottom" : ""}${left ? " no-left" : ""}${right ? " no-right" : ""}`;
@@ -1,7 +1,8 @@
1
- import type { CellValue, Headers, HeadersWithIDs, TableData } from "../types";
1
+ import type { CellValue, Headers, TableData } from "../types";
2
2
  export declare function make_cell_id(row: number, col: number): string;
3
3
  export declare function make_header_id(col: number): string;
4
- export declare function copy_table_data(data: TableData, selected_cells: [number, number][] | null): Promise<void>;
5
- export declare function guess_delimiter(text: string, possibleDelimiters: string[]): string[];
6
- export declare function data_uri_to_blob(data_uri: string): Blob;
7
- export declare function handle_file_upload(data_uri: string, update_headers: (headers: Headers) => HeadersWithIDs[], update_values: (values: CellValue[][]) => void): void;
4
+ export declare function copy_table_data(data: TableData, selected_cells: [number, number][]): Promise<void>;
5
+ export declare function parse_table_file(file: File): Promise<{
6
+ headers: Headers;
7
+ values: CellValue[][];
8
+ }>;