@gradio/dataframe 0.24.3 → 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 +33 -0
- package/Dataframe.test.ts +1048 -68
- package/dist/shared/BooleanCell.svelte +1 -0
- package/dist/shared/CellMenuButton.svelte +8 -1
- package/dist/shared/CellMenuButton.svelte.d.ts +1 -0
- package/dist/shared/DataCell.svelte +26 -2
- package/dist/shared/DataCell.svelte.d.ts +4 -0
- package/dist/shared/EditableCell.svelte +1 -2
- package/dist/shared/EmptyRowButton.svelte +0 -1
- package/dist/shared/HeaderCell.svelte +17 -3
- package/dist/shared/HeaderCell.svelte.d.ts +3 -1
- package/dist/shared/Table.svelte +472 -158
- package/dist/shared/Toolbar.svelte +2 -3
- package/dist/shared/Toolbar.svelte.d.ts +1 -1
- package/dist/shared/icons/SelectionButtons.svelte +1 -0
- 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 +8 -8
- package/shared/BooleanCell.svelte +1 -0
- package/shared/CellMenuButton.svelte +8 -1
- package/shared/DataCell.svelte +26 -2
- package/shared/EditableCell.svelte +1 -2
- package/shared/EmptyRowButton.svelte +0 -1
- package/shared/HeaderCell.svelte +17 -3
- package/shared/Table.svelte +472 -158
- package/shared/Toolbar.svelte +2 -3
- package/shared/icons/SelectionButtons.svelte +1 -0
- 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,9 +252,20 @@
|
|
|
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);
|
|
262
|
+
let keyboard_header: number | false = $derived(
|
|
263
|
+
selected_header !== false &&
|
|
264
|
+
selected_header >= 0 &&
|
|
265
|
+
selected_header < resolved_headers.length
|
|
266
|
+
? selected_header
|
|
267
|
+
: false
|
|
268
|
+
);
|
|
258
269
|
let active_cell_menu: {
|
|
259
270
|
row: number;
|
|
260
271
|
col: number;
|
|
@@ -270,10 +281,148 @@
|
|
|
270
281
|
|
|
271
282
|
let parent: HTMLDivElement;
|
|
272
283
|
|
|
284
|
+
let keyboard_active = $derived.by((): CellCoordinate | false => {
|
|
285
|
+
if (selected) {
|
|
286
|
+
const selected_row = selected[0];
|
|
287
|
+
const is_visible = rows.some(
|
|
288
|
+
(row) => row.original._index === selected_row
|
|
289
|
+
);
|
|
290
|
+
if (is_visible) return selected;
|
|
291
|
+
}
|
|
292
|
+
if (rows.length > 0 && resolved_headers.length > 0) {
|
|
293
|
+
return [rows[0].original._index, 0];
|
|
294
|
+
}
|
|
295
|
+
return false;
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
function visible_row_position(row_index: number): number {
|
|
299
|
+
return rows.findIndex((row) => row.original._index === row_index);
|
|
300
|
+
}
|
|
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
|
+
|
|
308
|
+
function is_active_cell(row: number, col: number): boolean {
|
|
309
|
+
return !!(
|
|
310
|
+
keyboard_active &&
|
|
311
|
+
keyboard_active[0] === row &&
|
|
312
|
+
keyboard_active[1] === col &&
|
|
313
|
+
keyboard_header === false
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function focus_cell(coord: CellCoordinate): Promise<void> {
|
|
318
|
+
await tick();
|
|
319
|
+
parent
|
|
320
|
+
?.querySelector<HTMLElement>(
|
|
321
|
+
`[data-testid="cell-${coord[0]}-${coord[1]}"]`
|
|
322
|
+
)
|
|
323
|
+
?.focus();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function focus_header(col: number): Promise<void> {
|
|
327
|
+
await tick();
|
|
328
|
+
parent
|
|
329
|
+
?.querySelector<HTMLElement>(`[data-testid="header-${col}"]`)
|
|
330
|
+
?.focus();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function set_active_cell(coord: CellCoordinate, move_focus = true): void {
|
|
334
|
+
selected = coord;
|
|
335
|
+
range_anchor = coord;
|
|
336
|
+
selected_cells = [coord];
|
|
337
|
+
selected_header = false;
|
|
338
|
+
header_edit = false;
|
|
339
|
+
const row_position = visible_row_position(coord[0]);
|
|
340
|
+
if (row_position >= 0) {
|
|
341
|
+
virtualizer.instance.scrollToIndex(row_position, { align: "auto" });
|
|
342
|
+
}
|
|
343
|
+
if (move_focus) focus_cell(coord);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function handle_cell_focus(row: number, col: number): void {
|
|
347
|
+
if (editing) return;
|
|
348
|
+
if (selected && selected[0] === row && selected[1] === col) return;
|
|
349
|
+
set_active_cell([row, col], false);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function handle_header_focus(col: number): void {
|
|
353
|
+
if (header_edit !== false) return;
|
|
354
|
+
selected = false;
|
|
355
|
+
range_anchor = false;
|
|
356
|
+
selected_cells = [];
|
|
357
|
+
selected_header = col;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function dispatch_select(row: number, col: number): void {
|
|
361
|
+
onselect?.({
|
|
362
|
+
index: [row, col],
|
|
363
|
+
value: values?.[row]?.[col],
|
|
364
|
+
row_value: values?.[row] ?? [],
|
|
365
|
+
col_value: values?.map((value_row) => value_row[col]) ?? []
|
|
366
|
+
} as any);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function adjacent_visible_cell(
|
|
370
|
+
coord: CellCoordinate,
|
|
371
|
+
backwards: boolean
|
|
372
|
+
): CellCoordinate | false {
|
|
373
|
+
const [row, col] = coord;
|
|
374
|
+
const row_position = visible_row_position(row);
|
|
375
|
+
if (row_position === -1) return false;
|
|
376
|
+
|
|
377
|
+
if (backwards) {
|
|
378
|
+
if (col > 0) return [row, col - 1];
|
|
379
|
+
if (row_position > 0) {
|
|
380
|
+
return [
|
|
381
|
+
rows[row_position - 1].original._index,
|
|
382
|
+
resolved_headers.length - 1
|
|
383
|
+
];
|
|
384
|
+
}
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (col < resolved_headers.length - 1) return [row, col + 1];
|
|
389
|
+
if (row_position < rows.length - 1) {
|
|
390
|
+
return [rows[row_position + 1].original._index, 0];
|
|
391
|
+
}
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function focus_outside_grid(backwards: boolean): void {
|
|
396
|
+
tick().then(() => {
|
|
397
|
+
const candidates = Array.from(
|
|
398
|
+
document.querySelectorAll<HTMLElement>(
|
|
399
|
+
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
400
|
+
)
|
|
401
|
+
).filter(
|
|
402
|
+
(element) =>
|
|
403
|
+
!parent.contains(element) && element.getClientRects().length > 0
|
|
404
|
+
);
|
|
405
|
+
const position = backwards
|
|
406
|
+
? Node.DOCUMENT_POSITION_PRECEDING
|
|
407
|
+
: Node.DOCUMENT_POSITION_FOLLOWING;
|
|
408
|
+
const ordered = backwards ? candidates.reverse() : candidates;
|
|
409
|
+
ordered
|
|
410
|
+
.find((element) => parent.compareDocumentPosition(element) & position)
|
|
411
|
+
?.focus();
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
273
415
|
function get_dtype(col: number): Datatype {
|
|
274
416
|
return Array.isArray(datatype) ? (datatype[col] ?? "str") : datatype;
|
|
275
417
|
}
|
|
276
418
|
|
|
419
|
+
function is_static_column(col: number): boolean {
|
|
420
|
+
return (
|
|
421
|
+
static_columns.includes(col) ||
|
|
422
|
+
static_columns.includes(resolved_headers[col])
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
277
426
|
type SizingEntry = { val: string; col_idx: number; dtype: Datatype };
|
|
278
427
|
|
|
279
428
|
// heading multipliers for markdown/html block elements that render
|
|
@@ -376,7 +525,8 @@
|
|
|
376
525
|
function handle_cell_click(
|
|
377
526
|
event: MouseEvent,
|
|
378
527
|
row: number,
|
|
379
|
-
col: number
|
|
528
|
+
col: number,
|
|
529
|
+
view_index: number
|
|
380
530
|
): void {
|
|
381
531
|
const col_is_static =
|
|
382
532
|
!editable ||
|
|
@@ -388,25 +538,37 @@
|
|
|
388
538
|
event.stopPropagation();
|
|
389
539
|
|
|
390
540
|
const coord: CellCoordinate = [row, col];
|
|
391
|
-
if (event.shiftKey &&
|
|
392
|
-
// range
|
|
393
|
-
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
+
}
|
|
399
559
|
}
|
|
560
|
+
selected_cells = new_cells;
|
|
400
561
|
}
|
|
401
|
-
selected_cells = new_cells;
|
|
402
562
|
} else if (event.metaKey || event.ctrlKey) {
|
|
403
563
|
// toggle select
|
|
404
564
|
const exists = selected_cells.some(([r, c]) => r === row && c === col);
|
|
405
565
|
selected_cells = exists
|
|
406
566
|
? selected_cells.filter(([r, c]) => !(r === row && c === col))
|
|
407
567
|
: [...selected_cells, coord];
|
|
568
|
+
range_anchor = coord;
|
|
408
569
|
} else {
|
|
409
570
|
selected_cells = [coord];
|
|
571
|
+
range_anchor = coord;
|
|
410
572
|
}
|
|
411
573
|
|
|
412
574
|
selected = coord;
|
|
@@ -418,16 +580,8 @@
|
|
|
418
580
|
// click selects, does NOT enter edit mode (double-click or typing does)
|
|
419
581
|
editing = false;
|
|
420
582
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
value: values?.[row]?.[col],
|
|
424
|
-
row_value: values?.[row] ?? [],
|
|
425
|
-
col_value: values?.map((r) => r[col]) ?? []
|
|
426
|
-
} as any);
|
|
427
|
-
|
|
428
|
-
if (!col_is_static) {
|
|
429
|
-
tick().then(() => parent?.focus());
|
|
430
|
-
}
|
|
583
|
+
dispatch_select(row, col);
|
|
584
|
+
focus_cell(coord);
|
|
431
585
|
}
|
|
432
586
|
|
|
433
587
|
function handle_cell_dblclick(
|
|
@@ -475,22 +629,27 @@
|
|
|
475
629
|
if (event.target instanceof HTMLAnchorElement) return;
|
|
476
630
|
event.preventDefault();
|
|
477
631
|
event.stopPropagation();
|
|
478
|
-
if (!editable) return;
|
|
479
632
|
|
|
480
633
|
editing = false;
|
|
481
634
|
selected = false;
|
|
635
|
+
range_anchor = false;
|
|
482
636
|
selected_cells = [];
|
|
483
637
|
active_cell_menu = null;
|
|
484
638
|
active_header_menu = null;
|
|
485
639
|
selected_header = col;
|
|
486
|
-
header_edit = editable ? col : false;
|
|
487
|
-
|
|
640
|
+
header_edit = editable && !is_static_column(col) ? col : false;
|
|
641
|
+
if (header_edit === false) focus_header(col);
|
|
488
642
|
}
|
|
489
643
|
|
|
490
|
-
function end_header_edit(
|
|
491
|
-
if (["Escape", "Enter", "Tab"].includes(key)) {
|
|
644
|
+
function end_header_edit(event: KeyboardEvent): void {
|
|
645
|
+
if (["Escape", "Enter", "Tab"].includes(event.key)) {
|
|
646
|
+
event.preventDefault();
|
|
492
647
|
header_edit = false;
|
|
493
|
-
|
|
648
|
+
if (event.key === "Tab") {
|
|
649
|
+
focus_outside_grid(event.shiftKey);
|
|
650
|
+
} else if (selected_header !== false) {
|
|
651
|
+
focus_header(selected_header);
|
|
652
|
+
}
|
|
494
653
|
}
|
|
495
654
|
}
|
|
496
655
|
|
|
@@ -565,8 +724,7 @@
|
|
|
565
724
|
}
|
|
566
725
|
values = new_values;
|
|
567
726
|
push_change(new_values);
|
|
568
|
-
|
|
569
|
-
parent?.focus();
|
|
727
|
+
set_active_cell([index ?? new_values.length - 1, 0]);
|
|
570
728
|
}
|
|
571
729
|
|
|
572
730
|
function add_col(index?: number): void {
|
|
@@ -583,20 +741,33 @@
|
|
|
583
741
|
values = new_values;
|
|
584
742
|
headers = new_headers;
|
|
585
743
|
push_change(new_values, new_headers);
|
|
586
|
-
|
|
744
|
+
const new_col = index ?? new_headers.length - 1;
|
|
745
|
+
if (selected) {
|
|
746
|
+
set_active_cell([selected[0], new_col]);
|
|
747
|
+
} else {
|
|
748
|
+
selected_header = new_col;
|
|
749
|
+
header_edit = false;
|
|
750
|
+
focus_header(new_col);
|
|
751
|
+
}
|
|
587
752
|
}
|
|
588
753
|
|
|
589
754
|
function delete_row_at(index: number): void {
|
|
590
755
|
if (values.length <= 1) return;
|
|
756
|
+
const target_col = selected ? selected[1] : 0;
|
|
591
757
|
values = [...values.slice(0, index), ...values.slice(index + 1)];
|
|
592
758
|
push_change(values);
|
|
593
759
|
active_cell_menu = null;
|
|
594
760
|
active_header_menu = null;
|
|
761
|
+
set_active_cell([
|
|
762
|
+
Math.min(index, values.length - 1),
|
|
763
|
+
Math.min(target_col, resolved_headers.length - 1)
|
|
764
|
+
]);
|
|
595
765
|
}
|
|
596
766
|
|
|
597
767
|
function delete_col_at(index: number): void {
|
|
598
768
|
if (col_count[1] !== "dynamic") return;
|
|
599
769
|
if ((values[0]?.length ?? 0) <= 1) return;
|
|
770
|
+
const target_row = selected ? selected[0] : (rows[0]?.original._index ?? 0);
|
|
600
771
|
values = values.map((row) => [
|
|
601
772
|
...row.slice(0, index),
|
|
602
773
|
...row.slice(index + 1)
|
|
@@ -608,9 +779,8 @@
|
|
|
608
779
|
push_change(values, headers as string[]);
|
|
609
780
|
active_cell_menu = null;
|
|
610
781
|
active_header_menu = null;
|
|
611
|
-
selected = false;
|
|
612
|
-
selected_cells = [];
|
|
613
782
|
editing = false;
|
|
783
|
+
set_active_cell([target_row, Math.min(index, headers.length - 1)]);
|
|
614
784
|
}
|
|
615
785
|
|
|
616
786
|
function add_row_at(index: number, position: "above" | "below"): void {
|
|
@@ -659,20 +829,40 @@
|
|
|
659
829
|
push_change(filtered_values);
|
|
660
830
|
}
|
|
661
831
|
|
|
662
|
-
|
|
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
|
+
|
|
663
845
|
const data_for_copy = values.map((row) =>
|
|
664
846
|
row.map((val, j) => ({ id: `${j}`, value: val }))
|
|
665
847
|
);
|
|
666
|
-
|
|
667
|
-
|
|
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
|
+
}
|
|
668
856
|
copy_flash = true;
|
|
669
857
|
setTimeout(() => (copy_flash = false), 800);
|
|
858
|
+
return true;
|
|
670
859
|
}
|
|
671
860
|
|
|
672
861
|
function handle_click_outside(event: Event): void {
|
|
673
862
|
if (handle_click_outside_util(event, parent)) {
|
|
674
863
|
selected_cells = [];
|
|
675
864
|
selected = false;
|
|
865
|
+
range_anchor = false;
|
|
676
866
|
editing = false;
|
|
677
867
|
header_edit = false;
|
|
678
868
|
selected_header = false;
|
|
@@ -682,137 +872,222 @@
|
|
|
682
872
|
}
|
|
683
873
|
|
|
684
874
|
function handle_keydown(e: KeyboardEvent): void {
|
|
685
|
-
if (
|
|
875
|
+
if (e.target instanceof HTMLAnchorElement) return;
|
|
876
|
+
if (!selected && keyboard_header === false) return;
|
|
686
877
|
|
|
687
|
-
const num_rows = rows.length;
|
|
688
878
|
const num_cols = resolved_headers.length;
|
|
689
879
|
|
|
690
|
-
if (
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
if (
|
|
694
|
-
editing &&
|
|
695
|
-
(e.key === "ArrowUp" ||
|
|
696
|
-
e.key === "ArrowDown" ||
|
|
697
|
-
e.key === "ArrowLeft" ||
|
|
698
|
-
e.key === "ArrowRight")
|
|
699
|
-
) {
|
|
700
|
-
return;
|
|
701
|
-
}
|
|
702
|
-
|
|
880
|
+
if (keyboard_header !== false) {
|
|
881
|
+
if (header_edit !== false) return;
|
|
882
|
+
const col = keyboard_header;
|
|
703
883
|
switch (e.key) {
|
|
704
|
-
case "
|
|
884
|
+
case "ArrowDown": {
|
|
705
885
|
e.preventDefault();
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
selected_cells = [selected];
|
|
709
|
-
virtualizer.instance.scrollToIndex(row - 1, { align: "auto" });
|
|
710
|
-
}
|
|
711
|
-
break;
|
|
712
|
-
case "ArrowDown":
|
|
713
|
-
e.preventDefault();
|
|
714
|
-
if (row < num_rows - 1) {
|
|
715
|
-
selected = [row + 1, col];
|
|
716
|
-
selected_cells = [selected];
|
|
717
|
-
virtualizer.instance.scrollToIndex(row + 1, { align: "auto" });
|
|
718
|
-
}
|
|
886
|
+
const first_row = rows[0]?.original._index;
|
|
887
|
+
if (first_row !== undefined) set_active_cell([first_row, col]);
|
|
719
888
|
break;
|
|
889
|
+
}
|
|
720
890
|
case "ArrowLeft":
|
|
721
891
|
e.preventDefault();
|
|
722
892
|
if (col > 0) {
|
|
723
|
-
|
|
724
|
-
|
|
893
|
+
selected_header = col - 1;
|
|
894
|
+
focus_header(col - 1);
|
|
725
895
|
}
|
|
726
896
|
break;
|
|
727
897
|
case "ArrowRight":
|
|
728
898
|
e.preventDefault();
|
|
729
899
|
if (col < num_cols - 1) {
|
|
730
|
-
|
|
731
|
-
|
|
900
|
+
selected_header = col + 1;
|
|
901
|
+
focus_header(col + 1);
|
|
732
902
|
}
|
|
733
903
|
break;
|
|
734
|
-
case "
|
|
904
|
+
case "Enter":
|
|
905
|
+
case "F2":
|
|
735
906
|
e.preventDefault();
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
}
|
|
744
|
-
selected_cells = [selected];
|
|
745
|
-
if (was_editing) {
|
|
746
|
-
const tab_col = (selected as CellCoordinate)[1];
|
|
747
|
-
const tab_static =
|
|
748
|
-
static_columns.includes(tab_col) ||
|
|
749
|
-
static_columns.includes(resolved_headers[tab_col]);
|
|
750
|
-
editing = editable && !tab_static ? selected : false;
|
|
751
|
-
} else {
|
|
752
|
-
editing = false;
|
|
907
|
+
if (editable && !is_static_column(col)) header_edit = col;
|
|
908
|
+
break;
|
|
909
|
+
case " ":
|
|
910
|
+
case "Spacebar":
|
|
911
|
+
e.preventDefault();
|
|
912
|
+
if (editable && !is_static_column(col) && get_dtype(col) === "bool") {
|
|
913
|
+
handle_select_all(col, get_select_all_state(col) !== "checked");
|
|
753
914
|
}
|
|
754
|
-
if (!editing) tick().then(() => parent?.focus());
|
|
755
915
|
break;
|
|
916
|
+
}
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
if (!selected) return;
|
|
921
|
+
const [row, col] = selected;
|
|
922
|
+
const row_position = visible_row_position(row);
|
|
923
|
+
if (row_position === -1) return;
|
|
924
|
+
|
|
925
|
+
if (
|
|
926
|
+
editing &&
|
|
927
|
+
(e.key === "ArrowUp" ||
|
|
928
|
+
e.key === "ArrowDown" ||
|
|
929
|
+
e.key === "ArrowLeft" ||
|
|
930
|
+
e.key === "ArrowRight" ||
|
|
931
|
+
e.key === "Home" ||
|
|
932
|
+
e.key === "End")
|
|
933
|
+
) {
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
switch (e.key) {
|
|
938
|
+
case "ArrowUp": {
|
|
939
|
+
e.preventDefault();
|
|
940
|
+
if (row_position > 0) {
|
|
941
|
+
set_active_cell([rows[row_position - 1].original._index, col]);
|
|
942
|
+
} else {
|
|
943
|
+
selected = false;
|
|
944
|
+
range_anchor = false;
|
|
945
|
+
selected_cells = [];
|
|
946
|
+
selected_header = col;
|
|
947
|
+
focus_header(col);
|
|
756
948
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
949
|
+
break;
|
|
950
|
+
}
|
|
951
|
+
case "ArrowDown":
|
|
952
|
+
e.preventDefault();
|
|
953
|
+
if (row_position < rows.length - 1) {
|
|
954
|
+
set_active_cell([rows[row_position + 1].original._index, col]);
|
|
955
|
+
}
|
|
956
|
+
break;
|
|
957
|
+
case "ArrowLeft":
|
|
958
|
+
e.preventDefault();
|
|
959
|
+
if (col > 0) set_active_cell([row, col - 1]);
|
|
960
|
+
break;
|
|
961
|
+
case "ArrowRight":
|
|
962
|
+
e.preventDefault();
|
|
963
|
+
if (col < num_cols - 1) set_active_cell([row, col + 1]);
|
|
964
|
+
break;
|
|
965
|
+
case "Home":
|
|
966
|
+
e.preventDefault();
|
|
967
|
+
set_active_cell(
|
|
968
|
+
e.ctrlKey || e.metaKey ? [rows[0].original._index, 0] : [row, 0]
|
|
969
|
+
);
|
|
970
|
+
break;
|
|
971
|
+
case "End":
|
|
972
|
+
e.preventDefault();
|
|
973
|
+
set_active_cell(
|
|
974
|
+
e.ctrlKey || e.metaKey
|
|
975
|
+
? [rows[rows.length - 1].original._index, num_cols - 1]
|
|
976
|
+
: [row, num_cols - 1]
|
|
977
|
+
);
|
|
978
|
+
break;
|
|
979
|
+
case "Tab": {
|
|
980
|
+
if (!editing) return;
|
|
981
|
+
e.preventDefault();
|
|
982
|
+
const next_cell = adjacent_visible_cell([row, col], e.shiftKey);
|
|
983
|
+
editing = false;
|
|
984
|
+
if (!next_cell) {
|
|
985
|
+
focus_outside_grid(e.shiftKey);
|
|
986
|
+
break;
|
|
987
|
+
}
|
|
988
|
+
set_active_cell(next_cell, false);
|
|
989
|
+
const next_col = next_cell[1];
|
|
990
|
+
const next_is_static = is_static_column(next_col);
|
|
991
|
+
if (editable && !next_is_static && get_dtype(next_col) !== "bool") {
|
|
992
|
+
editing = next_cell;
|
|
993
|
+
} else {
|
|
994
|
+
focus_cell(next_cell);
|
|
995
|
+
}
|
|
996
|
+
break;
|
|
997
|
+
}
|
|
998
|
+
case "Enter":
|
|
999
|
+
if (editing && e.shiftKey) return;
|
|
1000
|
+
e.preventDefault();
|
|
1001
|
+
if (editing) {
|
|
1002
|
+
editing = false;
|
|
1003
|
+
focus_cell([row, col]);
|
|
1004
|
+
} else {
|
|
1005
|
+
dispatch_select(row, col);
|
|
1006
|
+
const enter_static = is_static_column(col);
|
|
1007
|
+
if (editable && !enter_static && get_dtype(col) !== "bool") {
|
|
1008
|
+
editing = [row, col];
|
|
761
1009
|
}
|
|
1010
|
+
}
|
|
1011
|
+
break;
|
|
1012
|
+
case "F2":
|
|
1013
|
+
e.preventDefault();
|
|
1014
|
+
if (editing) {
|
|
1015
|
+
editing = false;
|
|
1016
|
+
focus_cell([row, col]);
|
|
1017
|
+
} else if (
|
|
1018
|
+
editable &&
|
|
1019
|
+
!is_static_column(col) &&
|
|
1020
|
+
get_dtype(col) !== "bool"
|
|
1021
|
+
) {
|
|
1022
|
+
editing = [row, col];
|
|
1023
|
+
}
|
|
1024
|
+
break;
|
|
1025
|
+
case " ":
|
|
1026
|
+
case "Spacebar":
|
|
1027
|
+
if (!editing) {
|
|
762
1028
|
e.preventDefault();
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
tick().then(() => parent?.focus());
|
|
770
|
-
} else if (editable) {
|
|
771
|
-
const enter_static =
|
|
772
|
-
static_columns.includes(col) ||
|
|
773
|
-
static_columns.includes(resolved_headers[col]);
|
|
774
|
-
if (!enter_static) {
|
|
775
|
-
editing = [row, col];
|
|
776
|
-
}
|
|
1029
|
+
const col_is_static = is_static_column(col);
|
|
1030
|
+
if (editable && !col_is_static && get_dtype(col) === "bool") {
|
|
1031
|
+
const new_values = values.map((value_row) => [...value_row]);
|
|
1032
|
+
new_values[row][col] = !new_values[row][col];
|
|
1033
|
+
values = new_values;
|
|
1034
|
+
push_change(new_values);
|
|
777
1035
|
}
|
|
778
|
-
|
|
779
|
-
|
|
1036
|
+
dispatch_select(row, col);
|
|
1037
|
+
}
|
|
1038
|
+
break;
|
|
1039
|
+
case "Escape":
|
|
1040
|
+
if (editing) {
|
|
1041
|
+
e.preventDefault();
|
|
780
1042
|
editing = false;
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
1043
|
+
focus_cell([row, col]);
|
|
1044
|
+
}
|
|
1045
|
+
break;
|
|
1046
|
+
case "Delete":
|
|
1047
|
+
case "Backspace":
|
|
1048
|
+
if (!editing && editable) {
|
|
1049
|
+
e.preventDefault();
|
|
1050
|
+
const new_values = values.map((value_row) => [...value_row]);
|
|
1051
|
+
const visible = visible_row_set();
|
|
1052
|
+
let cleared = false;
|
|
1053
|
+
selected_cells.forEach(([selected_row, selected_col]) => {
|
|
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
|
+
) {
|
|
1063
|
+
new_values[selected_row][selected_col] = "";
|
|
1064
|
+
cleared = true;
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
// pushing with nothing written would fire change and input for an
|
|
1068
|
+
// identical table
|
|
1069
|
+
if (cleared) {
|
|
793
1070
|
values = new_values;
|
|
794
1071
|
push_change(new_values);
|
|
795
1072
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
)
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
if ((e.ctrlKey || e.metaKey) && e.key === "c") {
|
|
813
|
-
handle_copy();
|
|
814
|
-
}
|
|
1073
|
+
}
|
|
1074
|
+
break;
|
|
1075
|
+
default:
|
|
1076
|
+
if (
|
|
1077
|
+
editable &&
|
|
1078
|
+
!editing &&
|
|
1079
|
+
e.key.length === 1 &&
|
|
1080
|
+
!e.ctrlKey &&
|
|
1081
|
+
!e.metaKey &&
|
|
1082
|
+
!is_static_column(col) &&
|
|
1083
|
+
get_dtype(col) !== "bool"
|
|
1084
|
+
) {
|
|
1085
|
+
editing = [row, col];
|
|
1086
|
+
}
|
|
1087
|
+
break;
|
|
815
1088
|
}
|
|
1089
|
+
|
|
1090
|
+
if ((e.ctrlKey || e.metaKey) && e.key === "c") handle_copy();
|
|
816
1091
|
}
|
|
817
1092
|
|
|
818
1093
|
function handle_scroll(): void {
|
|
@@ -888,10 +1163,12 @@
|
|
|
888
1163
|
let disable_scroll = $derived(
|
|
889
1164
|
active_cell_menu !== null || active_header_menu !== null
|
|
890
1165
|
);
|
|
891
|
-
let selected_index = $derived(
|
|
1166
|
+
let selected_index = $derived(
|
|
1167
|
+
selected !== false ? visible_row_position(selected[0]) : false
|
|
1168
|
+
);
|
|
892
1169
|
|
|
893
1170
|
$effect(() => {
|
|
894
|
-
if (typeof selected_index === "number") {
|
|
1171
|
+
if (typeof selected_index === "number" && selected_index >= 0) {
|
|
895
1172
|
virtualizer.instance.scrollToIndex(selected_index, { align: "auto" });
|
|
896
1173
|
}
|
|
897
1174
|
});
|
|
@@ -918,7 +1195,11 @@
|
|
|
918
1195
|
}}
|
|
919
1196
|
/>
|
|
920
1197
|
|
|
921
|
-
<div
|
|
1198
|
+
<div
|
|
1199
|
+
class="table-container"
|
|
1200
|
+
class:fullscreen
|
|
1201
|
+
class:no-rows={values.length === 0}
|
|
1202
|
+
>
|
|
922
1203
|
{#if (label && label.length !== 0 && show_label) || (buttons === null ? true : buttons.includes("fullscreen")) || (buttons === null ? true : buttons.includes("copy")) || show_search !== "none"}
|
|
923
1204
|
<div class="header-row">
|
|
924
1205
|
{#if label && label.length !== 0 && show_label}
|
|
@@ -947,7 +1228,11 @@
|
|
|
947
1228
|
class:menu-open={active_cell_menu || active_header_menu}
|
|
948
1229
|
onkeydown={handle_keydown}
|
|
949
1230
|
role="grid"
|
|
950
|
-
|
|
1231
|
+
aria-label={label || "Dataframe"}
|
|
1232
|
+
aria-rowcount={rows.length + 1}
|
|
1233
|
+
aria-colcount={resolved_headers.length + Number(show_row_numbers)}
|
|
1234
|
+
aria-readonly={!editable}
|
|
1235
|
+
tabindex={rows.length === 0 ? 0 : -1}
|
|
951
1236
|
style="--df-max-col-width: {viewport_width}px;"
|
|
952
1237
|
>
|
|
953
1238
|
<Upload
|
|
@@ -961,6 +1246,8 @@
|
|
|
961
1246
|
onload={on_file_upload}
|
|
962
1247
|
bind:dragging
|
|
963
1248
|
aria_label={i18n("dataframe.drop_to_upload")}
|
|
1249
|
+
tab_index={-1}
|
|
1250
|
+
container_element="div"
|
|
964
1251
|
>
|
|
965
1252
|
<div
|
|
966
1253
|
class="virtual-table-viewport"
|
|
@@ -968,18 +1255,19 @@
|
|
|
968
1255
|
bind:this={scroll_container}
|
|
969
1256
|
bind:clientWidth={viewport_width}
|
|
970
1257
|
onscroll={handle_scroll}
|
|
1258
|
+
role="none"
|
|
971
1259
|
style="max-height: {max_height}px;"
|
|
972
|
-
role="grid"
|
|
973
1260
|
>
|
|
974
|
-
{#if label && label.length !== 0}
|
|
975
|
-
<span class="sr-only">{label}</span>
|
|
976
|
-
{/if}
|
|
977
1261
|
<!-- header row: uses table layout to auto-size columns by content -->
|
|
978
|
-
<table class="header-table" bind:this={header_table_el}>
|
|
979
|
-
<thead>
|
|
980
|
-
<tr bind:this={header_row_el}>
|
|
1262
|
+
<table class="header-table" bind:this={header_table_el} role="none">
|
|
1263
|
+
<thead role="rowgroup">
|
|
1264
|
+
<tr bind:this={header_row_el} role="row" aria-rowindex="1">
|
|
981
1265
|
{#if show_row_numbers}
|
|
982
|
-
<th
|
|
1266
|
+
<th
|
|
1267
|
+
class="row-number-header"
|
|
1268
|
+
role="columnheader"
|
|
1269
|
+
aria-colindex="1"> </th
|
|
1270
|
+
>
|
|
983
1271
|
{/if}
|
|
984
1272
|
{#each header_groups as headerGroup (headerGroup.id)}
|
|
985
1273
|
{#each headerGroup.headers as header (header.id)}
|
|
@@ -988,8 +1276,9 @@
|
|
|
988
1276
|
<HeaderCell
|
|
989
1277
|
value={String(header.column.columnDef.header ?? "")}
|
|
990
1278
|
{col_idx}
|
|
1279
|
+
aria_col_index={col_idx + (show_row_numbers ? 2 : 1)}
|
|
991
1280
|
is_editing={header_edit === col_idx}
|
|
992
|
-
is_selected={
|
|
1281
|
+
is_selected={keyboard_header === col_idx}
|
|
993
1282
|
is_static={!!(header.column.columnDef.meta as any)
|
|
994
1283
|
?.isStatic}
|
|
995
1284
|
is_bool={get_dtype(col_idx) === "bool"}
|
|
@@ -1007,6 +1296,7 @@
|
|
|
1007
1296
|
{i18n}
|
|
1008
1297
|
wrap_text={wrap}
|
|
1009
1298
|
onclick={handle_header_click}
|
|
1299
|
+
onfocus={handle_header_focus}
|
|
1010
1300
|
on_menu_click={toggle_header_menu}
|
|
1011
1301
|
on_end_edit={end_header_edit}
|
|
1012
1302
|
on_select_all={handle_select_all}
|
|
@@ -1049,6 +1339,7 @@
|
|
|
1049
1339
|
<!-- table body: absolutely positioned rows (standard tanstack virtual pattern) -->
|
|
1050
1340
|
<div
|
|
1051
1341
|
class="virtual-body"
|
|
1342
|
+
role="none"
|
|
1052
1343
|
style="height: {total_size}px; position: relative; flex-shrink: 0; width: {measurement.total_header_width
|
|
1053
1344
|
? `${measurement.total_header_width}px`
|
|
1054
1345
|
: '100%'};"
|
|
@@ -1056,11 +1347,17 @@
|
|
|
1056
1347
|
{#each virtual_items as virtual_row (virtual_row.key)}
|
|
1057
1348
|
{@const row = rows[virtual_row.index]}
|
|
1058
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}
|
|
1059
1354
|
{#if row}
|
|
1060
1355
|
<div
|
|
1061
1356
|
class="virtual-row"
|
|
1062
1357
|
class:row-odd={virtual_row.index % 2 !== 0}
|
|
1063
1358
|
data-index={virtual_row.index}
|
|
1359
|
+
role="row"
|
|
1360
|
+
aria-rowindex={virtual_row.index + 2}
|
|
1064
1361
|
style="position: absolute; top: 0; left: 0; width: 100%; transform: translateY({virtual_row.start}px);{selected_cells.some(
|
|
1065
1362
|
([r]) => r === row_idx
|
|
1066
1363
|
)
|
|
@@ -1073,6 +1370,8 @@
|
|
|
1073
1370
|
class="row-number-cell"
|
|
1074
1371
|
data-row={row_idx}
|
|
1075
1372
|
data-col="row-number"
|
|
1373
|
+
role="rowheader"
|
|
1374
|
+
aria-colindex="1"
|
|
1076
1375
|
style="flex: 0 0 {measurement.row_num_width}px; width: {measurement.row_num_width}px;"
|
|
1077
1376
|
>
|
|
1078
1377
|
{row_idx + 1}
|
|
@@ -1091,12 +1390,17 @@
|
|
|
1091
1390
|
datatype={get_dtype(col_idx)}
|
|
1092
1391
|
{row_idx}
|
|
1093
1392
|
{col_idx}
|
|
1393
|
+
aria_col_index={col_idx + (show_row_numbers ? 2 : 1)}
|
|
1094
1394
|
col_style={measurement.get_col_style(ci)}
|
|
1095
1395
|
cell_style={get_styling(row_idx, col_idx)}
|
|
1096
1396
|
selection_classes={is_cell_selected(
|
|
1097
1397
|
[row_idx, col_idx],
|
|
1098
|
-
selected_cells
|
|
1398
|
+
selected_cells,
|
|
1399
|
+
row_above,
|
|
1400
|
+
row_below
|
|
1099
1401
|
)}
|
|
1402
|
+
is_active={is_active_cell(row_idx, col_idx)}
|
|
1403
|
+
aria_row_index={virtual_row.index + 2}
|
|
1100
1404
|
is_editing={!!(
|
|
1101
1405
|
editing &&
|
|
1102
1406
|
editing[0] === row_idx &&
|
|
@@ -1125,7 +1429,8 @@
|
|
|
1125
1429
|
{components}
|
|
1126
1430
|
{is_dragging}
|
|
1127
1431
|
wrap_text={wrap}
|
|
1128
|
-
onmousedown={(e) =>
|
|
1432
|
+
onmousedown={(e) =>
|
|
1433
|
+
handle_cell_click(e, row_idx, col_idx, virtual_row.index)}
|
|
1129
1434
|
ondblclick={(e) =>
|
|
1130
1435
|
handle_cell_dblclick(e, row_idx, col_idx)}
|
|
1131
1436
|
oncontextmenu={(e) => {
|
|
@@ -1136,11 +1441,12 @@
|
|
|
1136
1441
|
e.preventDefault();
|
|
1137
1442
|
toggle_cell_menu(e, row_idx, col_idx);
|
|
1138
1443
|
}}
|
|
1444
|
+
onfocus={() => handle_cell_focus(row_idx, col_idx)}
|
|
1139
1445
|
onblur={handle_blur}
|
|
1140
1446
|
on_menu_click={(e) => toggle_cell_menu(e, row_idx, col_idx)}
|
|
1141
1447
|
on_select_column={(c) => {
|
|
1142
1448
|
selected_cells = rows.map(
|
|
1143
|
-
(
|
|
1449
|
+
(row) => [row.original._index, c] as CellCoordinate
|
|
1144
1450
|
);
|
|
1145
1451
|
selected = selected_cells[0];
|
|
1146
1452
|
}}
|
|
@@ -1163,6 +1469,10 @@
|
|
|
1163
1469
|
<button class="scroll-top-button" onclick={scroll_to_top}>↑</button>
|
|
1164
1470
|
{/if}
|
|
1165
1471
|
</div>
|
|
1472
|
+
|
|
1473
|
+
{#if values.length === 0 && editable && row_count[1] === "dynamic"}
|
|
1474
|
+
<EmptyRowButton on_click={() => add_row()} />
|
|
1475
|
+
{/if}
|
|
1166
1476
|
</div>
|
|
1167
1477
|
|
|
1168
1478
|
{#if active_cell_menu || active_header_menu}
|
|
@@ -1229,10 +1539,6 @@
|
|
|
1229
1539
|
/>
|
|
1230
1540
|
{/if}
|
|
1231
1541
|
|
|
1232
|
-
{#if values.length === 0 && editable && row_count[1] === "dynamic"}
|
|
1233
|
-
<EmptyRowButton on_click={() => add_row()} />
|
|
1234
|
-
{/if}
|
|
1235
|
-
|
|
1236
1542
|
<style>
|
|
1237
1543
|
.table-container {
|
|
1238
1544
|
display: flex;
|
|
@@ -1269,6 +1575,14 @@
|
|
|
1269
1575
|
min-height: 0;
|
|
1270
1576
|
}
|
|
1271
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
|
+
|
|
1272
1586
|
.table-wrap {
|
|
1273
1587
|
position: relative;
|
|
1274
1588
|
transition: 150ms;
|
|
@@ -1298,7 +1612,7 @@
|
|
|
1298
1612
|
user-select: none;
|
|
1299
1613
|
}
|
|
1300
1614
|
|
|
1301
|
-
.table-wrap > :global(
|
|
1615
|
+
.table-wrap > :global(.upload-container) {
|
|
1302
1616
|
border: 1px solid var(--border-color-primary);
|
|
1303
1617
|
border-radius: var(--table-radius);
|
|
1304
1618
|
overflow: hidden;
|