@gradio/dataframe 0.24.4 → 0.24.5
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 +12 -0
- package/Dataframe.test.ts +590 -36
- package/dist/shared/EmptyRowButton.svelte +0 -1
- package/dist/shared/Table.svelte +104 -23
- package/dist/shared/Toolbar.svelte +2 -3
- package/dist/shared/Toolbar.svelte.d.ts +1 -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 +1 -1
- package/dist/shared/utils/table_utils.js +8 -5
- package/package.json +5 -5
- package/shared/EmptyRowButton.svelte +0 -1
- package/shared/Table.svelte +104 -23
- package/shared/Toolbar.svelte +2 -3
- package/shared/utils/selection_utils.ts +8 -3
- package/shared/utils/table_utils.ts +11 -8
- package/test/selection_utils.test.ts +40 -7
package/dist/shared/Table.svelte
CHANGED
|
@@ -252,6 +252,10 @@
|
|
|
252
252
|
|
|
253
253
|
let selected_cells: CellCoordinate[] = $state([]);
|
|
254
254
|
let selected: CellCoordinate | false = $state(false);
|
|
255
|
+
// where a shift+click range starts. `selected` cannot serve, because it
|
|
256
|
+
// follows every click, the shift+clicks that extend the range included.
|
|
257
|
+
// nothing renders from it, so it does not need to be reactive
|
|
258
|
+
let range_anchor: CellCoordinate | false = false;
|
|
255
259
|
let editing: CellCoordinate | false = $state(false);
|
|
256
260
|
let header_edit: number | false = $state(false);
|
|
257
261
|
let selected_header: number | false = $state(false);
|
|
@@ -295,6 +299,12 @@
|
|
|
295
299
|
return rows.findIndex((row) => row.original._index === row_index);
|
|
296
300
|
}
|
|
297
301
|
|
|
302
|
+
// for the whole-selection sweeps, where one findIndex per cell would make
|
|
303
|
+
// Delete and copy quadratic on a column selection
|
|
304
|
+
function visible_row_set(): Set<number> {
|
|
305
|
+
return new Set(rows.map((row) => row.original._index));
|
|
306
|
+
}
|
|
307
|
+
|
|
298
308
|
function is_active_cell(row: number, col: number): boolean {
|
|
299
309
|
return !!(
|
|
300
310
|
keyboard_active &&
|
|
@@ -322,6 +332,7 @@
|
|
|
322
332
|
|
|
323
333
|
function set_active_cell(coord: CellCoordinate, move_focus = true): void {
|
|
324
334
|
selected = coord;
|
|
335
|
+
range_anchor = coord;
|
|
325
336
|
selected_cells = [coord];
|
|
326
337
|
selected_header = false;
|
|
327
338
|
header_edit = false;
|
|
@@ -341,6 +352,7 @@
|
|
|
341
352
|
function handle_header_focus(col: number): void {
|
|
342
353
|
if (header_edit !== false) return;
|
|
343
354
|
selected = false;
|
|
355
|
+
range_anchor = false;
|
|
344
356
|
selected_cells = [];
|
|
345
357
|
selected_header = col;
|
|
346
358
|
}
|
|
@@ -513,7 +525,8 @@
|
|
|
513
525
|
function handle_cell_click(
|
|
514
526
|
event: MouseEvent,
|
|
515
527
|
row: number,
|
|
516
|
-
col: number
|
|
528
|
+
col: number,
|
|
529
|
+
view_index: number
|
|
517
530
|
): void {
|
|
518
531
|
const col_is_static =
|
|
519
532
|
!editable ||
|
|
@@ -525,25 +538,37 @@
|
|
|
525
538
|
event.stopPropagation();
|
|
526
539
|
|
|
527
540
|
const coord: CellCoordinate = [row, col];
|
|
528
|
-
if (event.shiftKey &&
|
|
529
|
-
// range
|
|
530
|
-
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
541
|
+
if (event.shiftKey && range_anchor) {
|
|
542
|
+
// the range runs from the anchor, and the search may have hidden its
|
|
543
|
+
// row since
|
|
544
|
+
const from_row = visible_row_position(range_anchor[0]);
|
|
545
|
+
if (from_row === -1) {
|
|
546
|
+
selected_cells = [coord];
|
|
547
|
+
range_anchor = coord;
|
|
548
|
+
} else {
|
|
549
|
+
const c1 = range_anchor[1];
|
|
550
|
+
const new_cells: CellCoordinate[] = [];
|
|
551
|
+
for (
|
|
552
|
+
let p = Math.min(from_row, view_index);
|
|
553
|
+
p <= Math.max(from_row, view_index);
|
|
554
|
+
p++
|
|
555
|
+
) {
|
|
556
|
+
for (let c = Math.min(c1, col); c <= Math.max(c1, col); c++) {
|
|
557
|
+
new_cells.push([rows[p].original._index, c]);
|
|
558
|
+
}
|
|
536
559
|
}
|
|
560
|
+
selected_cells = new_cells;
|
|
537
561
|
}
|
|
538
|
-
selected_cells = new_cells;
|
|
539
562
|
} else if (event.metaKey || event.ctrlKey) {
|
|
540
563
|
// toggle select
|
|
541
564
|
const exists = selected_cells.some(([r, c]) => r === row && c === col);
|
|
542
565
|
selected_cells = exists
|
|
543
566
|
? selected_cells.filter(([r, c]) => !(r === row && c === col))
|
|
544
567
|
: [...selected_cells, coord];
|
|
568
|
+
range_anchor = coord;
|
|
545
569
|
} else {
|
|
546
570
|
selected_cells = [coord];
|
|
571
|
+
range_anchor = coord;
|
|
547
572
|
}
|
|
548
573
|
|
|
549
574
|
selected = coord;
|
|
@@ -607,6 +632,7 @@
|
|
|
607
632
|
|
|
608
633
|
editing = false;
|
|
609
634
|
selected = false;
|
|
635
|
+
range_anchor = false;
|
|
610
636
|
selected_cells = [];
|
|
611
637
|
active_cell_menu = null;
|
|
612
638
|
active_header_menu = null;
|
|
@@ -803,20 +829,40 @@
|
|
|
803
829
|
push_change(filtered_values);
|
|
804
830
|
}
|
|
805
831
|
|
|
806
|
-
|
|
832
|
+
// false when nothing reached the clipboard, so the toolbar can hold back its
|
|
833
|
+
// "Copied to clipboard" state
|
|
834
|
+
async function handle_copy(): Promise<boolean> {
|
|
835
|
+
const visible = visible_row_set();
|
|
836
|
+
const cells =
|
|
837
|
+
selected_cells.length > 0
|
|
838
|
+
? selected_cells.filter(([r]) => visible.has(r))
|
|
839
|
+
: rows.flatMap((row) => {
|
|
840
|
+
const r = row.original._index;
|
|
841
|
+
return (values[r] ?? []).map((_, c) => [r, c] as CellCoordinate);
|
|
842
|
+
});
|
|
843
|
+
if (cells.length === 0) return false;
|
|
844
|
+
|
|
807
845
|
const data_for_copy = values.map((row) =>
|
|
808
846
|
row.map((val, j) => ({ id: `${j}`, value: val }))
|
|
809
847
|
);
|
|
810
|
-
|
|
811
|
-
|
|
848
|
+
try {
|
|
849
|
+
await copy_table_data(data_for_copy, cells);
|
|
850
|
+
} catch (err) {
|
|
851
|
+
// the write can be refused outright, on an insecure origin or without
|
|
852
|
+
// permission. logged because this catches anything else thrown too
|
|
853
|
+
console.error(err);
|
|
854
|
+
return false;
|
|
855
|
+
}
|
|
812
856
|
copy_flash = true;
|
|
813
857
|
setTimeout(() => (copy_flash = false), 800);
|
|
858
|
+
return true;
|
|
814
859
|
}
|
|
815
860
|
|
|
816
861
|
function handle_click_outside(event: Event): void {
|
|
817
862
|
if (handle_click_outside_util(event, parent)) {
|
|
818
863
|
selected_cells = [];
|
|
819
864
|
selected = false;
|
|
865
|
+
range_anchor = false;
|
|
820
866
|
editing = false;
|
|
821
867
|
header_edit = false;
|
|
822
868
|
selected_header = false;
|
|
@@ -895,6 +941,7 @@
|
|
|
895
941
|
set_active_cell([rows[row_position - 1].original._index, col]);
|
|
896
942
|
} else {
|
|
897
943
|
selected = false;
|
|
944
|
+
range_anchor = false;
|
|
898
945
|
selected_cells = [];
|
|
899
946
|
selected_header = col;
|
|
900
947
|
focus_header(col);
|
|
@@ -1001,13 +1048,28 @@
|
|
|
1001
1048
|
if (!editing && editable) {
|
|
1002
1049
|
e.preventDefault();
|
|
1003
1050
|
const new_values = values.map((value_row) => [...value_row]);
|
|
1051
|
+
const visible = visible_row_set();
|
|
1052
|
+
let cleared = false;
|
|
1004
1053
|
selected_cells.forEach(([selected_row, selected_col]) => {
|
|
1005
|
-
|
|
1054
|
+
// undefined is a cell past the end of a short row, or a row a
|
|
1055
|
+
// shrunk table no longer has; a null is a value, so it is cleared
|
|
1056
|
+
const current = new_values[selected_row]?.[selected_col];
|
|
1057
|
+
if (
|
|
1058
|
+
!is_static_column(selected_col) &&
|
|
1059
|
+
visible.has(selected_row) &&
|
|
1060
|
+
current !== "" &&
|
|
1061
|
+
current !== undefined
|
|
1062
|
+
) {
|
|
1006
1063
|
new_values[selected_row][selected_col] = "";
|
|
1064
|
+
cleared = true;
|
|
1007
1065
|
}
|
|
1008
1066
|
});
|
|
1009
|
-
|
|
1010
|
-
|
|
1067
|
+
// pushing with nothing written would fire change and input for an
|
|
1068
|
+
// identical table
|
|
1069
|
+
if (cleared) {
|
|
1070
|
+
values = new_values;
|
|
1071
|
+
push_change(new_values);
|
|
1072
|
+
}
|
|
1011
1073
|
}
|
|
1012
1074
|
break;
|
|
1013
1075
|
default:
|
|
@@ -1133,7 +1195,11 @@
|
|
|
1133
1195
|
}}
|
|
1134
1196
|
/>
|
|
1135
1197
|
|
|
1136
|
-
<div
|
|
1198
|
+
<div
|
|
1199
|
+
class="table-container"
|
|
1200
|
+
class:fullscreen
|
|
1201
|
+
class:no-rows={values.length === 0}
|
|
1202
|
+
>
|
|
1137
1203
|
{#if (label && label.length !== 0 && show_label) || (buttons === null ? true : buttons.includes("fullscreen")) || (buttons === null ? true : buttons.includes("copy")) || show_search !== "none"}
|
|
1138
1204
|
<div class="header-row">
|
|
1139
1205
|
{#if label && label.length !== 0 && show_label}
|
|
@@ -1281,6 +1347,10 @@
|
|
|
1281
1347
|
{#each virtual_items as virtual_row (virtual_row.key)}
|
|
1282
1348
|
{@const row = rows[virtual_row.index]}
|
|
1283
1349
|
{@const row_idx = row?.original._index ?? virtual_row.index}
|
|
1350
|
+
{@const row_above =
|
|
1351
|
+
rows[virtual_row.index - 1]?.original._index ?? null}
|
|
1352
|
+
{@const row_below =
|
|
1353
|
+
rows[virtual_row.index + 1]?.original._index ?? null}
|
|
1284
1354
|
{#if row}
|
|
1285
1355
|
<div
|
|
1286
1356
|
class="virtual-row"
|
|
@@ -1325,7 +1395,9 @@
|
|
|
1325
1395
|
cell_style={get_styling(row_idx, col_idx)}
|
|
1326
1396
|
selection_classes={is_cell_selected(
|
|
1327
1397
|
[row_idx, col_idx],
|
|
1328
|
-
selected_cells
|
|
1398
|
+
selected_cells,
|
|
1399
|
+
row_above,
|
|
1400
|
+
row_below
|
|
1329
1401
|
)}
|
|
1330
1402
|
is_active={is_active_cell(row_idx, col_idx)}
|
|
1331
1403
|
aria_row_index={virtual_row.index + 2}
|
|
@@ -1357,7 +1429,8 @@
|
|
|
1357
1429
|
{components}
|
|
1358
1430
|
{is_dragging}
|
|
1359
1431
|
wrap_text={wrap}
|
|
1360
|
-
onmousedown={(e) =>
|
|
1432
|
+
onmousedown={(e) =>
|
|
1433
|
+
handle_cell_click(e, row_idx, col_idx, virtual_row.index)}
|
|
1361
1434
|
ondblclick={(e) =>
|
|
1362
1435
|
handle_cell_dblclick(e, row_idx, col_idx)}
|
|
1363
1436
|
oncontextmenu={(e) => {
|
|
@@ -1396,6 +1469,10 @@
|
|
|
1396
1469
|
<button class="scroll-top-button" onclick={scroll_to_top}>↑</button>
|
|
1397
1470
|
{/if}
|
|
1398
1471
|
</div>
|
|
1472
|
+
|
|
1473
|
+
{#if values.length === 0 && editable && row_count[1] === "dynamic"}
|
|
1474
|
+
<EmptyRowButton on_click={() => add_row()} />
|
|
1475
|
+
{/if}
|
|
1399
1476
|
</div>
|
|
1400
1477
|
|
|
1401
1478
|
{#if active_cell_menu || active_header_menu}
|
|
@@ -1462,10 +1539,6 @@
|
|
|
1462
1539
|
/>
|
|
1463
1540
|
{/if}
|
|
1464
1541
|
|
|
1465
|
-
{#if values.length === 0 && editable && row_count[1] === "dynamic"}
|
|
1466
|
-
<EmptyRowButton on_click={() => add_row()} />
|
|
1467
|
-
{/if}
|
|
1468
|
-
|
|
1469
1542
|
<style>
|
|
1470
1543
|
.table-container {
|
|
1471
1544
|
display: flex;
|
|
@@ -1502,6 +1575,14 @@
|
|
|
1502
1575
|
min-height: 0;
|
|
1503
1576
|
}
|
|
1504
1577
|
|
|
1578
|
+
/* An empty body would otherwise fill the screen and carry the add-row button off it.
|
|
1579
|
+
Gated on the data alone, so empty tables that show no button stop stretching too. */
|
|
1580
|
+
.table-container.fullscreen.no-rows .table-wrap,
|
|
1581
|
+
.table-container.fullscreen.no-rows .table-wrap > :global(*),
|
|
1582
|
+
.table-container.fullscreen.no-rows .virtual-table-viewport {
|
|
1583
|
+
flex: 0 0 auto;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1505
1586
|
.table-wrap {
|
|
1506
1587
|
position: relative;
|
|
1507
1588
|
transition: 150ms;
|
|
@@ -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(() => {
|
|
@@ -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<
|
|
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;
|
|
@@ -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
|
-
|
|
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 ===
|
|
10
|
-
const down = selected_cells.some(([r, c]) => r ===
|
|
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,7 @@
|
|
|
1
1
|
import type { CellValue, Headers, HeadersWithIDs, 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][]
|
|
4
|
+
export declare function copy_table_data(data: TableData, selected_cells: [number, number][]): Promise<void>;
|
|
5
5
|
export declare function guess_delimiter(text: string, possibleDelimiters: string[]): string[];
|
|
6
6
|
export declare function data_uri_to_blob(data_uri: string): Blob;
|
|
7
7
|
export declare function handle_file_upload(data_uri: string, update_headers: (headers: Headers) => HeadersWithIDs[], update_values: (values: CellValue[][]) => void): void;
|
|
@@ -8,11 +8,12 @@ export function make_header_id(col) {
|
|
|
8
8
|
export async function copy_table_data(data, selected_cells) {
|
|
9
9
|
if (!data || !data.length)
|
|
10
10
|
return;
|
|
11
|
-
const
|
|
12
|
-
data.flatMap((row, r) => row.map((_, c) => [r, c]));
|
|
13
|
-
const csv = cells_to_copy.reduce((acc, [row, col]) => {
|
|
11
|
+
const csv = selected_cells.reduce((acc, [row, col]) => {
|
|
14
12
|
acc[row] = acc[row] || {};
|
|
15
|
-
|
|
13
|
+
// only the headers are padded out to the column count on the way in, so
|
|
14
|
+
// a selection can name a cell a short row does not have
|
|
15
|
+
const cell = data[row]?.[col];
|
|
16
|
+
const value = cell ? String(cell.value) : "";
|
|
16
17
|
acc[row][col] =
|
|
17
18
|
value.includes(",") || value.includes('"') || value.includes("\n")
|
|
18
19
|
? `"${value.replace(/"/g, '""')}"`
|
|
@@ -22,7 +23,9 @@ export async function copy_table_data(data, selected_cells) {
|
|
|
22
23
|
const rows = Object.keys(csv).sort((a, b) => +a - +b);
|
|
23
24
|
if (!rows.length)
|
|
24
25
|
return;
|
|
25
|
-
|
|
26
|
+
// every column any selected row contributes: taking them from the first row
|
|
27
|
+
// alone drops the rest of a selection that is not rectangular there
|
|
28
|
+
const cols = Array.from(new Set(rows.flatMap((r) => Object.keys(csv[r])))).sort((a, b) => +a - +b);
|
|
26
29
|
const text = rows
|
|
27
30
|
.map((r) => cols.map((c) => csv[r][c] || "").join(","))
|
|
28
31
|
.join("\n");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gradio/dataframe",
|
|
3
|
-
"version": "0.24.
|
|
3
|
+
"version": "0.24.5",
|
|
4
4
|
"description": "Gradio UI packages",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "",
|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
"@gradio/atoms": "^0.26.2",
|
|
19
19
|
"@gradio/checkbox": "^0.8.3",
|
|
20
20
|
"@gradio/button": "^0.8.2",
|
|
21
|
-
"@gradio/
|
|
22
|
-
"@gradio/client": "^2.6.0",
|
|
21
|
+
"@gradio/client": "^2.7.0",
|
|
23
22
|
"@gradio/markdown-code": "^0.10.1",
|
|
23
|
+
"@gradio/icons": "^0.16.0",
|
|
24
24
|
"@gradio/upload": "^0.18.3",
|
|
25
25
|
"@gradio/statustracker": "^0.15.3",
|
|
26
|
-
"@gradio/utils": "^0.14.
|
|
26
|
+
"@gradio/utils": "^0.14.2"
|
|
27
27
|
},
|
|
28
28
|
"exports": {
|
|
29
29
|
".": {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"svelte": "^5.48.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@gradio/preview": "^0.17.
|
|
45
|
+
"@gradio/preview": "^0.17.1"
|
|
46
46
|
},
|
|
47
47
|
"repository": {
|
|
48
48
|
"type": "git",
|
package/shared/Table.svelte
CHANGED
|
@@ -252,6 +252,10 @@
|
|
|
252
252
|
|
|
253
253
|
let selected_cells: CellCoordinate[] = $state([]);
|
|
254
254
|
let selected: CellCoordinate | false = $state(false);
|
|
255
|
+
// where a shift+click range starts. `selected` cannot serve, because it
|
|
256
|
+
// follows every click, the shift+clicks that extend the range included.
|
|
257
|
+
// nothing renders from it, so it does not need to be reactive
|
|
258
|
+
let range_anchor: CellCoordinate | false = false;
|
|
255
259
|
let editing: CellCoordinate | false = $state(false);
|
|
256
260
|
let header_edit: number | false = $state(false);
|
|
257
261
|
let selected_header: number | false = $state(false);
|
|
@@ -295,6 +299,12 @@
|
|
|
295
299
|
return rows.findIndex((row) => row.original._index === row_index);
|
|
296
300
|
}
|
|
297
301
|
|
|
302
|
+
// for the whole-selection sweeps, where one findIndex per cell would make
|
|
303
|
+
// Delete and copy quadratic on a column selection
|
|
304
|
+
function visible_row_set(): Set<number> {
|
|
305
|
+
return new Set(rows.map((row) => row.original._index));
|
|
306
|
+
}
|
|
307
|
+
|
|
298
308
|
function is_active_cell(row: number, col: number): boolean {
|
|
299
309
|
return !!(
|
|
300
310
|
keyboard_active &&
|
|
@@ -322,6 +332,7 @@
|
|
|
322
332
|
|
|
323
333
|
function set_active_cell(coord: CellCoordinate, move_focus = true): void {
|
|
324
334
|
selected = coord;
|
|
335
|
+
range_anchor = coord;
|
|
325
336
|
selected_cells = [coord];
|
|
326
337
|
selected_header = false;
|
|
327
338
|
header_edit = false;
|
|
@@ -341,6 +352,7 @@
|
|
|
341
352
|
function handle_header_focus(col: number): void {
|
|
342
353
|
if (header_edit !== false) return;
|
|
343
354
|
selected = false;
|
|
355
|
+
range_anchor = false;
|
|
344
356
|
selected_cells = [];
|
|
345
357
|
selected_header = col;
|
|
346
358
|
}
|
|
@@ -513,7 +525,8 @@
|
|
|
513
525
|
function handle_cell_click(
|
|
514
526
|
event: MouseEvent,
|
|
515
527
|
row: number,
|
|
516
|
-
col: number
|
|
528
|
+
col: number,
|
|
529
|
+
view_index: number
|
|
517
530
|
): void {
|
|
518
531
|
const col_is_static =
|
|
519
532
|
!editable ||
|
|
@@ -525,25 +538,37 @@
|
|
|
525
538
|
event.stopPropagation();
|
|
526
539
|
|
|
527
540
|
const coord: CellCoordinate = [row, col];
|
|
528
|
-
if (event.shiftKey &&
|
|
529
|
-
// range
|
|
530
|
-
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
541
|
+
if (event.shiftKey && range_anchor) {
|
|
542
|
+
// the range runs from the anchor, and the search may have hidden its
|
|
543
|
+
// row since
|
|
544
|
+
const from_row = visible_row_position(range_anchor[0]);
|
|
545
|
+
if (from_row === -1) {
|
|
546
|
+
selected_cells = [coord];
|
|
547
|
+
range_anchor = coord;
|
|
548
|
+
} else {
|
|
549
|
+
const c1 = range_anchor[1];
|
|
550
|
+
const new_cells: CellCoordinate[] = [];
|
|
551
|
+
for (
|
|
552
|
+
let p = Math.min(from_row, view_index);
|
|
553
|
+
p <= Math.max(from_row, view_index);
|
|
554
|
+
p++
|
|
555
|
+
) {
|
|
556
|
+
for (let c = Math.min(c1, col); c <= Math.max(c1, col); c++) {
|
|
557
|
+
new_cells.push([rows[p].original._index, c]);
|
|
558
|
+
}
|
|
536
559
|
}
|
|
560
|
+
selected_cells = new_cells;
|
|
537
561
|
}
|
|
538
|
-
selected_cells = new_cells;
|
|
539
562
|
} else if (event.metaKey || event.ctrlKey) {
|
|
540
563
|
// toggle select
|
|
541
564
|
const exists = selected_cells.some(([r, c]) => r === row && c === col);
|
|
542
565
|
selected_cells = exists
|
|
543
566
|
? selected_cells.filter(([r, c]) => !(r === row && c === col))
|
|
544
567
|
: [...selected_cells, coord];
|
|
568
|
+
range_anchor = coord;
|
|
545
569
|
} else {
|
|
546
570
|
selected_cells = [coord];
|
|
571
|
+
range_anchor = coord;
|
|
547
572
|
}
|
|
548
573
|
|
|
549
574
|
selected = coord;
|
|
@@ -607,6 +632,7 @@
|
|
|
607
632
|
|
|
608
633
|
editing = false;
|
|
609
634
|
selected = false;
|
|
635
|
+
range_anchor = false;
|
|
610
636
|
selected_cells = [];
|
|
611
637
|
active_cell_menu = null;
|
|
612
638
|
active_header_menu = null;
|
|
@@ -803,20 +829,40 @@
|
|
|
803
829
|
push_change(filtered_values);
|
|
804
830
|
}
|
|
805
831
|
|
|
806
|
-
|
|
832
|
+
// false when nothing reached the clipboard, so the toolbar can hold back its
|
|
833
|
+
// "Copied to clipboard" state
|
|
834
|
+
async function handle_copy(): Promise<boolean> {
|
|
835
|
+
const visible = visible_row_set();
|
|
836
|
+
const cells =
|
|
837
|
+
selected_cells.length > 0
|
|
838
|
+
? selected_cells.filter(([r]) => visible.has(r))
|
|
839
|
+
: rows.flatMap((row) => {
|
|
840
|
+
const r = row.original._index;
|
|
841
|
+
return (values[r] ?? []).map((_, c) => [r, c] as CellCoordinate);
|
|
842
|
+
});
|
|
843
|
+
if (cells.length === 0) return false;
|
|
844
|
+
|
|
807
845
|
const data_for_copy = values.map((row) =>
|
|
808
846
|
row.map((val, j) => ({ id: `${j}`, value: val }))
|
|
809
847
|
);
|
|
810
|
-
|
|
811
|
-
|
|
848
|
+
try {
|
|
849
|
+
await copy_table_data(data_for_copy, cells);
|
|
850
|
+
} catch (err) {
|
|
851
|
+
// the write can be refused outright, on an insecure origin or without
|
|
852
|
+
// permission. logged because this catches anything else thrown too
|
|
853
|
+
console.error(err);
|
|
854
|
+
return false;
|
|
855
|
+
}
|
|
812
856
|
copy_flash = true;
|
|
813
857
|
setTimeout(() => (copy_flash = false), 800);
|
|
858
|
+
return true;
|
|
814
859
|
}
|
|
815
860
|
|
|
816
861
|
function handle_click_outside(event: Event): void {
|
|
817
862
|
if (handle_click_outside_util(event, parent)) {
|
|
818
863
|
selected_cells = [];
|
|
819
864
|
selected = false;
|
|
865
|
+
range_anchor = false;
|
|
820
866
|
editing = false;
|
|
821
867
|
header_edit = false;
|
|
822
868
|
selected_header = false;
|
|
@@ -895,6 +941,7 @@
|
|
|
895
941
|
set_active_cell([rows[row_position - 1].original._index, col]);
|
|
896
942
|
} else {
|
|
897
943
|
selected = false;
|
|
944
|
+
range_anchor = false;
|
|
898
945
|
selected_cells = [];
|
|
899
946
|
selected_header = col;
|
|
900
947
|
focus_header(col);
|
|
@@ -1001,13 +1048,28 @@
|
|
|
1001
1048
|
if (!editing && editable) {
|
|
1002
1049
|
e.preventDefault();
|
|
1003
1050
|
const new_values = values.map((value_row) => [...value_row]);
|
|
1051
|
+
const visible = visible_row_set();
|
|
1052
|
+
let cleared = false;
|
|
1004
1053
|
selected_cells.forEach(([selected_row, selected_col]) => {
|
|
1005
|
-
|
|
1054
|
+
// undefined is a cell past the end of a short row, or a row a
|
|
1055
|
+
// shrunk table no longer has; a null is a value, so it is cleared
|
|
1056
|
+
const current = new_values[selected_row]?.[selected_col];
|
|
1057
|
+
if (
|
|
1058
|
+
!is_static_column(selected_col) &&
|
|
1059
|
+
visible.has(selected_row) &&
|
|
1060
|
+
current !== "" &&
|
|
1061
|
+
current !== undefined
|
|
1062
|
+
) {
|
|
1006
1063
|
new_values[selected_row][selected_col] = "";
|
|
1064
|
+
cleared = true;
|
|
1007
1065
|
}
|
|
1008
1066
|
});
|
|
1009
|
-
|
|
1010
|
-
|
|
1067
|
+
// pushing with nothing written would fire change and input for an
|
|
1068
|
+
// identical table
|
|
1069
|
+
if (cleared) {
|
|
1070
|
+
values = new_values;
|
|
1071
|
+
push_change(new_values);
|
|
1072
|
+
}
|
|
1011
1073
|
}
|
|
1012
1074
|
break;
|
|
1013
1075
|
default:
|
|
@@ -1133,7 +1195,11 @@
|
|
|
1133
1195
|
}}
|
|
1134
1196
|
/>
|
|
1135
1197
|
|
|
1136
|
-
<div
|
|
1198
|
+
<div
|
|
1199
|
+
class="table-container"
|
|
1200
|
+
class:fullscreen
|
|
1201
|
+
class:no-rows={values.length === 0}
|
|
1202
|
+
>
|
|
1137
1203
|
{#if (label && label.length !== 0 && show_label) || (buttons === null ? true : buttons.includes("fullscreen")) || (buttons === null ? true : buttons.includes("copy")) || show_search !== "none"}
|
|
1138
1204
|
<div class="header-row">
|
|
1139
1205
|
{#if label && label.length !== 0 && show_label}
|
|
@@ -1281,6 +1347,10 @@
|
|
|
1281
1347
|
{#each virtual_items as virtual_row (virtual_row.key)}
|
|
1282
1348
|
{@const row = rows[virtual_row.index]}
|
|
1283
1349
|
{@const row_idx = row?.original._index ?? virtual_row.index}
|
|
1350
|
+
{@const row_above =
|
|
1351
|
+
rows[virtual_row.index - 1]?.original._index ?? null}
|
|
1352
|
+
{@const row_below =
|
|
1353
|
+
rows[virtual_row.index + 1]?.original._index ?? null}
|
|
1284
1354
|
{#if row}
|
|
1285
1355
|
<div
|
|
1286
1356
|
class="virtual-row"
|
|
@@ -1325,7 +1395,9 @@
|
|
|
1325
1395
|
cell_style={get_styling(row_idx, col_idx)}
|
|
1326
1396
|
selection_classes={is_cell_selected(
|
|
1327
1397
|
[row_idx, col_idx],
|
|
1328
|
-
selected_cells
|
|
1398
|
+
selected_cells,
|
|
1399
|
+
row_above,
|
|
1400
|
+
row_below
|
|
1329
1401
|
)}
|
|
1330
1402
|
is_active={is_active_cell(row_idx, col_idx)}
|
|
1331
1403
|
aria_row_index={virtual_row.index + 2}
|
|
@@ -1357,7 +1429,8 @@
|
|
|
1357
1429
|
{components}
|
|
1358
1430
|
{is_dragging}
|
|
1359
1431
|
wrap_text={wrap}
|
|
1360
|
-
onmousedown={(e) =>
|
|
1432
|
+
onmousedown={(e) =>
|
|
1433
|
+
handle_cell_click(e, row_idx, col_idx, virtual_row.index)}
|
|
1361
1434
|
ondblclick={(e) =>
|
|
1362
1435
|
handle_cell_dblclick(e, row_idx, col_idx)}
|
|
1363
1436
|
oncontextmenu={(e) => {
|
|
@@ -1396,6 +1469,10 @@
|
|
|
1396
1469
|
<button class="scroll-top-button" onclick={scroll_to_top}>↑</button>
|
|
1397
1470
|
{/if}
|
|
1398
1471
|
</div>
|
|
1472
|
+
|
|
1473
|
+
{#if values.length === 0 && editable && row_count[1] === "dynamic"}
|
|
1474
|
+
<EmptyRowButton on_click={() => add_row()} />
|
|
1475
|
+
{/if}
|
|
1399
1476
|
</div>
|
|
1400
1477
|
|
|
1401
1478
|
{#if active_cell_menu || active_header_menu}
|
|
@@ -1462,10 +1539,6 @@
|
|
|
1462
1539
|
/>
|
|
1463
1540
|
{/if}
|
|
1464
1541
|
|
|
1465
|
-
{#if values.length === 0 && editable && row_count[1] === "dynamic"}
|
|
1466
|
-
<EmptyRowButton on_click={() => add_row()} />
|
|
1467
|
-
{/if}
|
|
1468
|
-
|
|
1469
1542
|
<style>
|
|
1470
1543
|
.table-container {
|
|
1471
1544
|
display: flex;
|
|
@@ -1502,6 +1575,14 @@
|
|
|
1502
1575
|
min-height: 0;
|
|
1503
1576
|
}
|
|
1504
1577
|
|
|
1578
|
+
/* An empty body would otherwise fill the screen and carry the add-row button off it.
|
|
1579
|
+
Gated on the data alone, so empty tables that show no button stop stretching too. */
|
|
1580
|
+
.table-container.fullscreen.no-rows .table-wrap,
|
|
1581
|
+
.table-container.fullscreen.no-rows .table-wrap > :global(*),
|
|
1582
|
+
.table-container.fullscreen.no-rows .virtual-table-viewport {
|
|
1583
|
+
flex: 0 0 auto;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1505
1586
|
.table-wrap {
|
|
1506
1587
|
position: relative;
|
|
1507
1588
|
transition: 150ms;
|