@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/CHANGELOG.md +25 -0
- package/Dataframe.test.ts +1202 -122
- package/Index.svelte +1 -0
- package/dist/Index.svelte +1 -0
- package/dist/shared/EmptyRowButton.svelte +0 -1
- package/dist/shared/Table.svelte +258 -51
- package/dist/shared/Table.svelte.d.ts +1 -0
- package/dist/shared/Toolbar.svelte +2 -3
- package/dist/shared/Toolbar.svelte.d.ts +1 -1
- package/dist/shared/types.d.ts +0 -4
- package/dist/shared/types.js +0 -1
- package/dist/shared/utils/selection_utils.d.ts +1 -1
- package/dist/shared/utils/selection_utils.js +6 -3
- package/dist/shared/utils/table_utils.d.ts +6 -5
- package/dist/shared/utils/table_utils.js +45 -40
- package/dist/shared/utils/utils.d.ts +0 -4
- package/dist/standalone/default_i18n.js +7 -0
- package/dist/types.d.ts +1 -0
- package/package.json +7 -7
- package/shared/EmptyRowButton.svelte +0 -1
- package/shared/Table.svelte +258 -51
- package/shared/Toolbar.svelte +2 -3
- package/shared/types.ts +0 -6
- package/shared/utils/selection_utils.ts +8 -3
- package/shared/utils/table_utils.ts +49 -52
- package/shared/utils/utils.ts +0 -1
- package/standalone/default_i18n.ts +12 -0
- package/test/selection_utils.test.ts +40 -7
- package/test/table_utils.test.ts +92 -28
- package/types.ts +1 -0
package/shared/Table.svelte
CHANGED
|
@@ -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,
|
|
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
|
|
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 &&
|
|
529
|
-
// range
|
|
530
|
-
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
811
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1010
|
-
|
|
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
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
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
|
|
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
|
-
|
|
1182
|
-
|
|
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) =>
|
|
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}>↑</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;
|
package/shared/Toolbar.svelte
CHANGED
|
@@ -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<
|
|
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(() => {
|
package/shared/types.ts
CHANGED
|
@@ -3,12 +3,6 @@ export type EditingState = CellCoordinate | false;
|
|
|
3
3
|
|
|
4
4
|
export type Headers = (string | null)[];
|
|
5
5
|
|
|
6
|
-
export interface HeadersWithIDs {
|
|
7
|
-
id: string;
|
|
8
|
-
value: string;
|
|
9
|
-
}
|
|
10
|
-
[];
|
|
11
|
-
|
|
12
6
|
export type CellValue = string | number | boolean;
|
|
13
7
|
|
|
14
8
|
export interface TableCell {
|
|
@@ -10,15 +10,20 @@ export function is_cell_in_selection(
|
|
|
10
10
|
return selected_cells.some(([r, c]) => r === row && c === col);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
// row_above/row_below are the data indices of the rows the caller is rendering
|
|
14
|
+
// on either side of `cell`, or null at an edge of the view. A search or a sort
|
|
15
|
+
// makes them differ from row ± 1, so only the caller knows them.
|
|
13
16
|
export function is_cell_selected(
|
|
14
17
|
cell: CellCoordinate,
|
|
15
|
-
selected_cells: CellCoordinate[]
|
|
18
|
+
selected_cells: CellCoordinate[],
|
|
19
|
+
row_above: number | null,
|
|
20
|
+
row_below: number | null
|
|
16
21
|
): string {
|
|
17
22
|
const [row, col] = cell;
|
|
18
23
|
if (!selected_cells.some(([r, c]) => r === row && c === col)) return "";
|
|
19
24
|
|
|
20
|
-
const up = selected_cells.some(([r, c]) => r ===
|
|
21
|
-
const down = selected_cells.some(([r, c]) => r ===
|
|
25
|
+
const up = selected_cells.some(([r, c]) => r === row_above && c === col);
|
|
26
|
+
const down = selected_cells.some(([r, c]) => r === row_below && c === col);
|
|
22
27
|
const left = selected_cells.some(([r, c]) => r === row && c === col - 1);
|
|
23
28
|
const right = selected_cells.some(([r, c]) => r === row && c === col + 1);
|
|
24
29
|
|