@gradio/dataframe 0.3.0-beta.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.
@@ -0,0 +1,985 @@
1
+ <script lang="ts">
2
+ import { createEventDispatcher, tick, onMount } from "svelte";
3
+ import { dsvFormat } from "d3-dsv";
4
+ import { dequal } from "dequal/lite";
5
+ import { copy } from "@gradio/utils";
6
+ import { Upload } from "@gradio/upload";
7
+ import { BaseButton } from "@gradio/button/static";
8
+ import EditableCell from "./EditableCell.svelte";
9
+ import type { SelectData } from "@gradio/utils";
10
+ import type { I18nFormatter } from "js/app/src/gradio_helper";
11
+ import VirtualTable from "./VirtualTable.svelte";
12
+
13
+ type Datatype = "str" | "markdown" | "html" | "number" | "bool" | "date";
14
+
15
+ export let datatype: Datatype | Datatype[];
16
+ export let label: string | null = null;
17
+ export let headers: string[] = [];
18
+ export let values:
19
+ | (string | number)[][]
20
+ | { data: (string | number)[][]; headers: string[] } = [[]];
21
+ export let col_count: [number, "fixed" | "dynamic"];
22
+ export let row_count: [number, "fixed" | "dynamic"];
23
+ export let latex_delimiters: {
24
+ left: string;
25
+ right: string;
26
+ display: boolean;
27
+ }[];
28
+
29
+ export let editable = true;
30
+ export let wrap = false;
31
+ export let root: string;
32
+ export let height: number | undefined = undefined;
33
+ export let i18n: I18nFormatter;
34
+
35
+ let selected: false | [number, number] = false;
36
+
37
+ $: {
38
+ if (values && !Array.isArray(values)) {
39
+ headers = values.headers;
40
+ values = values.data;
41
+ } else if (values === null) {
42
+ values = [];
43
+ }
44
+ }
45
+
46
+ const dispatch = createEventDispatcher<{
47
+ change: { data: (string | number)[][]; headers: string[] };
48
+ select: SelectData;
49
+ }>();
50
+
51
+ let editing: false | [number, number] = false;
52
+
53
+ const get_data_at = (row: number, col: number): string | number =>
54
+ data?.[row]?.[col]?.value;
55
+ $: {
56
+ if (selected !== false) {
57
+ const [row, col] = selected;
58
+ if (!isNaN(row) && !isNaN(col)) {
59
+ dispatch("select", { index: [row, col], value: get_data_at(row, col) });
60
+ }
61
+ }
62
+ }
63
+ let els: Record<
64
+ string,
65
+ { cell: null | HTMLTableCellElement; input: null | HTMLInputElement }
66
+ > = {};
67
+
68
+ let data_binding: Record<string, (typeof data)[0][0]> = {};
69
+
70
+ type Headers = { value: string; id: string }[];
71
+
72
+ function make_id(): string {
73
+ return Math.random().toString(36).substring(2, 15);
74
+ }
75
+ function make_headers(_head: string[]): Headers {
76
+ let _h = _head || [];
77
+ if (col_count[1] === "fixed" && _h.length < col_count[0]) {
78
+ const fill = Array(col_count[0] - _h.length)
79
+ .fill("")
80
+ .map((_, i) => `${i + _h.length}`);
81
+ _h = _h.concat(fill);
82
+ }
83
+
84
+ if (!_h || _h.length === 0) {
85
+ return Array(col_count[0])
86
+ .fill(0)
87
+ .map((_, i) => {
88
+ const _id = make_id();
89
+ els[_id] = { cell: null, input: null };
90
+ return { id: _id, value: JSON.stringify(i + 1) };
91
+ });
92
+ }
93
+ return _h.map((h, i) => {
94
+ const _id = make_id();
95
+ els[_id] = { cell: null, input: null };
96
+ return { id: _id, value: h ?? "" };
97
+ });
98
+ }
99
+
100
+ function process_data(_values: (string | number)[][]): {
101
+ value: string | number;
102
+ id: string;
103
+ }[][] {
104
+ const data_row_length = _values.length;
105
+ return Array(
106
+ row_count[1] === "fixed"
107
+ ? row_count[0]
108
+ : data_row_length < row_count[0]
109
+ ? row_count[0]
110
+ : data_row_length
111
+ )
112
+ .fill(0)
113
+ .map((_, i) =>
114
+ Array(
115
+ col_count[1] === "fixed"
116
+ ? col_count[0]
117
+ : data_row_length > 0
118
+ ? _values[0].length
119
+ : headers.length
120
+ )
121
+ .fill(0)
122
+ .map((_, j) => {
123
+ const id = make_id();
124
+ els[id] = els[id] || { input: null, cell: null };
125
+ const obj = { value: _values?.[i]?.[j] ?? "", id };
126
+ data_binding[id] = obj;
127
+ return obj;
128
+ })
129
+ );
130
+ }
131
+
132
+ let _headers = make_headers(headers);
133
+ let old_headers: string[] | undefined;
134
+
135
+ $: {
136
+ if (!dequal(headers, old_headers)) {
137
+ trigger_headers();
138
+ }
139
+ }
140
+
141
+ function trigger_headers(): void {
142
+ _headers = make_headers(headers);
143
+
144
+ old_headers = headers.slice();
145
+ }
146
+ $: if (!dequal(values, old_val)) {
147
+ data = process_data(values as (string | number)[][]);
148
+ old_val = values as (string | number)[][];
149
+ }
150
+
151
+ let data: { id: string; value: string | number }[][] = [[]];
152
+
153
+ let old_val: undefined | (string | number)[][] = undefined;
154
+
155
+ $: _headers &&
156
+ dispatch("change", {
157
+ data: data.map((r) => r.map(({ value }) => value)),
158
+ headers: _headers.map((h) => h.value)
159
+ });
160
+
161
+ function get_sort_status(
162
+ name: string,
163
+ _sort?: number,
164
+ direction?: SortDirection
165
+ ): "none" | "ascending" | "descending" {
166
+ if (!_sort) return "none";
167
+ if (headers[_sort] === name) {
168
+ if (direction === "asc") return "ascending";
169
+ if (direction === "des") return "descending";
170
+ }
171
+
172
+ return "none";
173
+ }
174
+
175
+ function get_current_indices(id: string): [number, number] {
176
+ return data.reduce(
177
+ (acc, arr, i) => {
178
+ const j = arr.reduce(
179
+ (_acc, _data, k) => (id === _data.id ? k : _acc),
180
+ -1
181
+ );
182
+
183
+ return j === -1 ? acc : [i, j];
184
+ },
185
+ [-1, -1]
186
+ );
187
+ }
188
+
189
+ async function start_edit(i: number, j: number): Promise<void> {
190
+ if (!editable || dequal(editing, [i, j])) return;
191
+
192
+ editing = [i, j];
193
+ }
194
+
195
+ function move_cursor(
196
+ key: "ArrowRight" | "ArrowLeft" | "ArrowDown" | "ArrowUp",
197
+ current_coords: [number, number]
198
+ ): void {
199
+ const dir = {
200
+ ArrowRight: [0, 1],
201
+ ArrowLeft: [0, -1],
202
+ ArrowDown: [1, 0],
203
+ ArrowUp: [-1, 0]
204
+ }[key];
205
+
206
+ const i = current_coords[0] + dir[0];
207
+ const j = current_coords[1] + dir[1];
208
+
209
+ if (i < 0 && j <= 0) {
210
+ selected_header = j;
211
+ selected = false;
212
+ } else {
213
+ const is_data = data[i]?.[j];
214
+ selected = is_data ? [i, j] : selected;
215
+ }
216
+ }
217
+
218
+ let clear_on_focus = false;
219
+ // eslint-disable-next-line complexity
220
+ async function handle_keydown(event: KeyboardEvent): Promise<void> {
221
+ if (selected_header !== false && header_edit === false) {
222
+ switch (event.key) {
223
+ case "ArrowDown":
224
+ selected = [0, selected_header];
225
+ selected_header = false;
226
+ return;
227
+ case "ArrowLeft":
228
+ selected_header =
229
+ selected_header > 0 ? selected_header - 1 : selected_header;
230
+ return;
231
+ case "ArrowRight":
232
+ selected_header =
233
+ selected_header < _headers.length - 1
234
+ ? selected_header + 1
235
+ : selected_header;
236
+ return;
237
+ case "Escape":
238
+ event.preventDefault();
239
+ selected_header = false;
240
+ break;
241
+ case "Enter":
242
+ event.preventDefault();
243
+ break;
244
+ }
245
+ }
246
+ if (!selected) {
247
+ return;
248
+ }
249
+
250
+ const [i, j] = selected;
251
+
252
+ switch (event.key) {
253
+ case "ArrowRight":
254
+ case "ArrowLeft":
255
+ case "ArrowDown":
256
+ case "ArrowUp":
257
+ if (editing) break;
258
+ event.preventDefault();
259
+ move_cursor(event.key, [i, j]);
260
+ break;
261
+
262
+ case "Escape":
263
+ if (!editable) break;
264
+ event.preventDefault();
265
+ editing = false;
266
+ break;
267
+ case "Enter":
268
+ if (!editable) break;
269
+ event.preventDefault();
270
+
271
+ if (event.shiftKey) {
272
+ add_row(i);
273
+ await tick();
274
+
275
+ selected = [i + 1, j];
276
+ } else {
277
+ if (dequal(editing, [i, j])) {
278
+ editing = false;
279
+ await tick();
280
+ selected = [i, j];
281
+ } else {
282
+ editing = [i, j];
283
+ }
284
+ }
285
+
286
+ break;
287
+ case "Backspace":
288
+ if (!editable) break;
289
+ if (!editing) {
290
+ event.preventDefault();
291
+ data[i][j].value = "";
292
+ }
293
+ break;
294
+ case "Delete":
295
+ if (!editable) break;
296
+ if (!editing) {
297
+ event.preventDefault();
298
+ data[i][j].value = "";
299
+ }
300
+ break;
301
+ case "Tab":
302
+ let direction = event.shiftKey ? -1 : 1;
303
+
304
+ let is_data_x = data[i][j + direction];
305
+ let is_data_y =
306
+ data?.[i + direction]?.[direction > 0 ? 0 : _headers.length - 1];
307
+
308
+ if (is_data_x || is_data_y) {
309
+ event.preventDefault();
310
+ selected = is_data_x
311
+ ? [i, j + direction]
312
+ : [i + direction, direction > 0 ? 0 : _headers.length - 1];
313
+ }
314
+ editing = false;
315
+
316
+ break;
317
+ default:
318
+ if (!editable) break;
319
+ if (
320
+ (!editing || (editing && dequal(editing, [i, j]))) &&
321
+ event.key.length === 1
322
+ ) {
323
+ clear_on_focus = true;
324
+ editing = [i, j];
325
+ }
326
+ }
327
+ }
328
+
329
+ async function handle_cell_click(i: number, j: number): Promise<void> {
330
+ if (dequal(editing, [i, j])) return;
331
+ if (dequal(selected, [i, j])) return;
332
+ header_edit = false;
333
+ selected_header = false;
334
+ editing = false;
335
+ selected = [i, j];
336
+ await tick();
337
+ parent.focus();
338
+ }
339
+
340
+ type SortDirection = "asc" | "des";
341
+ let sort_direction: SortDirection | undefined;
342
+ let sort_by: number | undefined;
343
+
344
+ function handle_sort(col: number): void {
345
+ if (typeof sort_by !== "number" || sort_by !== col) {
346
+ sort_direction = "asc";
347
+ sort_by = col;
348
+ } else {
349
+ if (sort_direction === "asc") {
350
+ sort_direction = "des";
351
+ } else if (sort_direction === "des") {
352
+ sort_direction = "asc";
353
+ }
354
+ }
355
+ }
356
+
357
+ let header_edit: number | false;
358
+
359
+ let select_on_focus = false;
360
+ let selected_header: number | false = false;
361
+ async function edit_header(i: number, _select = false): Promise<void> {
362
+ if (!editable || col_count[1] !== "dynamic" || header_edit === i) return;
363
+ selected = false;
364
+ selected_header = i;
365
+ header_edit = i;
366
+ select_on_focus = _select;
367
+ }
368
+
369
+ function end_header_edit(event: KeyboardEvent): void {
370
+ if (!editable) return;
371
+
372
+ switch (event.key) {
373
+ case "Escape":
374
+ case "Enter":
375
+ case "Tab":
376
+ event.preventDefault();
377
+ selected = false;
378
+ selected_header = header_edit;
379
+ header_edit = false;
380
+ parent.focus();
381
+ break;
382
+ }
383
+ }
384
+
385
+ async function add_row(index?: number): Promise<void> {
386
+ parent.focus();
387
+
388
+ if (row_count[1] !== "dynamic") return;
389
+ if (data.length === 0) {
390
+ values = [Array(headers.length).fill("")];
391
+ return;
392
+ }
393
+
394
+ data.splice(
395
+ index ? index + 1 : data.length,
396
+ 0,
397
+ Array(data[0].length)
398
+ .fill(0)
399
+ .map((_, i) => {
400
+ const _id = make_id();
401
+
402
+ els[_id] = { cell: null, input: null };
403
+ return { id: _id, value: "" };
404
+ })
405
+ );
406
+
407
+ data = data;
408
+ selected = [index ? index + 1 : data.length - 1, 0];
409
+ }
410
+
411
+ async function add_col(): Promise<void> {
412
+ parent.focus();
413
+ if (col_count[1] !== "dynamic") return;
414
+ for (let i = 0; i < data.length; i++) {
415
+ const _id = make_id();
416
+ els[_id] = { cell: null, input: null };
417
+ data[i].push({ id: _id, value: "" });
418
+ }
419
+
420
+ headers.push(`Header ${headers.length + 1}`);
421
+
422
+ data = data;
423
+ headers = headers;
424
+
425
+ await tick();
426
+
427
+ requestAnimationFrame(() => {
428
+ edit_header(headers.length - 1, true);
429
+ const new_w = parent.querySelectorAll("tbody")[1].offsetWidth;
430
+ parent.querySelectorAll("table")[1].scrollTo({ left: new_w });
431
+ });
432
+ }
433
+
434
+ function handle_click_outside(event: Event): void {
435
+ event.stopImmediatePropagation();
436
+ const [trigger] = event.composedPath() as HTMLElement[];
437
+ if (parent.contains(trigger)) {
438
+ return;
439
+ }
440
+
441
+ editing = false;
442
+ header_edit = false;
443
+ selected_header = false;
444
+ selected = false;
445
+ }
446
+
447
+ function guess_delimitaor(
448
+ text: string,
449
+ possibleDelimiters: string[]
450
+ ): string[] {
451
+ return possibleDelimiters.filter(weedOut);
452
+
453
+ function weedOut(delimiter: string): boolean {
454
+ var cache = -1;
455
+ return text.split("\n").every(checkLength);
456
+
457
+ function checkLength(line: string): boolean {
458
+ if (!line) {
459
+ return true;
460
+ }
461
+
462
+ var length = line.split(delimiter).length;
463
+ if (cache < 0) {
464
+ cache = length;
465
+ }
466
+ return cache === length && length > 1;
467
+ }
468
+ }
469
+ }
470
+
471
+ function data_uri_to_blob(data_uri: string): Blob {
472
+ const byte_str = atob(data_uri.split(",")[1]);
473
+ const mime_str = data_uri.split(",")[0].split(":")[1].split(";")[0];
474
+
475
+ const ab = new ArrayBuffer(byte_str.length);
476
+ const ia = new Uint8Array(ab);
477
+
478
+ for (let i = 0; i < byte_str.length; i++) {
479
+ ia[i] = byte_str.charCodeAt(i);
480
+ }
481
+
482
+ return new Blob([ab], { type: mime_str });
483
+ }
484
+
485
+ function blob_to_string(blob: Blob): void {
486
+ const reader = new FileReader();
487
+
488
+ function handle_read(e: ProgressEvent<FileReader>): void {
489
+ if (!e?.target?.result || typeof e.target.result !== "string") return;
490
+
491
+ const [delimiter] = guess_delimitaor(e.target.result, [",", "\t"]);
492
+
493
+ const [head, ...rest] = dsvFormat(delimiter).parseRows(e.target.result);
494
+
495
+ _headers = make_headers(
496
+ col_count[1] === "fixed" ? head.slice(0, col_count[0]) : head
497
+ );
498
+
499
+ values = rest;
500
+ reader.removeEventListener("loadend", handle_read);
501
+ }
502
+
503
+ reader.addEventListener("loadend", handle_read);
504
+
505
+ reader.readAsText(blob);
506
+ }
507
+
508
+ let dragging = false;
509
+
510
+ let t_width = 0;
511
+
512
+ function get_max(
513
+ _d: { value: any; id: string }[][]
514
+ ): { value: any; id: string }[] {
515
+ let max = _d[0].slice();
516
+ for (let i = 0; i < _d.length; i++) {
517
+ for (let j = 0; j < _d[i].length; j++) {
518
+ if (`${max[j].value}`.length < `${_d[i][j].value}`.length) {
519
+ max[j] = _d[i][j];
520
+ }
521
+ }
522
+ }
523
+
524
+ return max;
525
+ }
526
+
527
+ $: max = get_max(data);
528
+
529
+ $: cells[0] && set_cell_widths();
530
+ let cells: HTMLTableCellElement[] = [];
531
+ let parent: HTMLDivElement;
532
+
533
+ function set_cell_widths(): void {
534
+ const widths = cells.map((el, i) => {
535
+ return el?.clientWidth || 0;
536
+ });
537
+
538
+ if (widths.length === 0) return;
539
+ for (let i = 0; i < widths.length; i++) {
540
+ parent.style.setProperty(`--cell-width-${i}`, `${widths[i]}px`);
541
+ }
542
+ }
543
+
544
+ let table_height: number = height || 500;
545
+
546
+ function sort_data(
547
+ _data: typeof data,
548
+ col?: number,
549
+ dir?: SortDirection
550
+ ): void {
551
+ const id = selected ? data[selected[0]][selected[1]]?.id : null;
552
+ if (typeof col !== "number" || !dir) {
553
+ return;
554
+ }
555
+ if (dir === "asc") {
556
+ _data.sort((a, b) => (a[col].value < b[col].value ? -1 : 1));
557
+ } else if (dir === "des") {
558
+ _data.sort((a, b) => (a[col].value > b[col].value ? -1 : 1));
559
+ }
560
+
561
+ data = data;
562
+
563
+ if (id) {
564
+ const [i, j] = get_current_indices(id);
565
+ selected = [i, j];
566
+ }
567
+ }
568
+
569
+ $: sort_data(data, sort_by, sort_direction);
570
+
571
+ $: selected_index = !!selected && selected[0];
572
+
573
+ let is_visible = false;
574
+ onMount(() => {
575
+ const observer = new IntersectionObserver((entries, observer) => {
576
+ entries.forEach((entry) => {
577
+ if (entry.isIntersecting && !is_visible) {
578
+ set_cell_widths();
579
+ data = data;
580
+ }
581
+
582
+ is_visible = entry.isIntersecting;
583
+ });
584
+ });
585
+
586
+ observer.observe(parent);
587
+
588
+ return () => {
589
+ observer.disconnect();
590
+ };
591
+ });
592
+ </script>
593
+
594
+ <svelte:window
595
+ on:click={handle_click_outside}
596
+ on:touchstart={handle_click_outside}
597
+ on:resize={() => set_cell_widths()}
598
+ />
599
+
600
+ <div class:label={label && label.length !== 0} use:copy>
601
+ {#if label && label.length !== 0}
602
+ <p>
603
+ {label}
604
+ </p>
605
+ {/if}
606
+ <div
607
+ bind:this={parent}
608
+ class="table-wrap"
609
+ class:dragging
610
+ class:no-wrap={!wrap}
611
+ style="height:{table_height}px"
612
+ on:keydown={(e) => handle_keydown(e)}
613
+ role="grid"
614
+ tabindex="0"
615
+ >
616
+ <table bind:clientWidth={t_width}>
617
+ {#if label && label.length !== 0}
618
+ <caption class="sr-only">{label}</caption>
619
+ {/if}
620
+ <thead>
621
+ <tr>
622
+ {#each _headers as { value, id }, i (id)}
623
+ <th
624
+ class:editing={header_edit === i}
625
+ aria-sort={get_sort_status(value, sort_by, sort_direction)}
626
+ >
627
+ <div class="cell-wrap">
628
+ <EditableCell
629
+ {value}
630
+ {latex_delimiters}
631
+ header
632
+ edit={false}
633
+ el={null}
634
+ />
635
+
636
+ <div
637
+ class:sorted={sort_by === i}
638
+ class:des={sort_by === i && sort_direction === "des"}
639
+ class="sort-button {sort_direction} "
640
+ >
641
+ <svg
642
+ width="1em"
643
+ height="1em"
644
+ viewBox="0 0 9 7"
645
+ fill="none"
646
+ xmlns="http://www.w3.org/2000/svg"
647
+ >
648
+ <path d="M4.49999 0L8.3971 6.75H0.602875L4.49999 0Z" />
649
+ </svg>
650
+ </div>
651
+ </div>
652
+ </th>
653
+ {/each}
654
+ </tr>
655
+ </thead>
656
+ <tbody>
657
+ <tr>
658
+ {#each max as { value, id }, j (id)}
659
+ <td tabindex="-1" bind:this={cells[j]}>
660
+ <div class="cell-wrap">
661
+ <EditableCell
662
+ {value}
663
+ {latex_delimiters}
664
+ datatype={Array.isArray(datatype) ? datatype[j] : datatype}
665
+ edit={false}
666
+ el={null}
667
+ />
668
+ </div>
669
+ </td>
670
+ {/each}
671
+ </tr>
672
+ </tbody>
673
+ </table>
674
+ <Upload
675
+ flex={false}
676
+ center={false}
677
+ boundedheight={false}
678
+ disable_click={true}
679
+ {root}
680
+ on:load={(e) => blob_to_string(data_uri_to_blob(e.detail.data))}
681
+ bind:dragging
682
+ >
683
+ <VirtualTable
684
+ bind:items={data}
685
+ table_width={t_width}
686
+ max_height={height || 500}
687
+ bind:actual_height={table_height}
688
+ selected={selected_index}
689
+ >
690
+ {#if label && label.length !== 0}
691
+ <caption class="sr-only">{label}</caption>
692
+ {/if}
693
+ <tr slot="thead">
694
+ {#each _headers as { value, id }, i (id)}
695
+ <th
696
+ class:focus={header_edit === i || selected_header === i}
697
+ aria-sort={get_sort_status(value, sort_by, sort_direction)}
698
+ style="width: var(--cell-width-{i});"
699
+ >
700
+ <div class="cell-wrap">
701
+ <EditableCell
702
+ bind:value={_headers[i].value}
703
+ bind:el={els[id].input}
704
+ {latex_delimiters}
705
+ edit={header_edit === i}
706
+ on:keydown={end_header_edit}
707
+ on:dblclick={() => edit_header(i)}
708
+ {select_on_focus}
709
+ header
710
+ />
711
+
712
+ <!-- TODO: fix -->
713
+ <!-- svelte-ignore a11y-click-events-have-key-events -->
714
+ <!-- svelte-ignore a11y-no-static-element-interactions-->
715
+ <div
716
+ class:sorted={sort_by === i}
717
+ class:des={sort_by === i && sort_direction === "des"}
718
+ class="sort-button {sort_direction} "
719
+ on:click={() => handle_sort(i)}
720
+ >
721
+ <svg
722
+ width="1em"
723
+ height="1em"
724
+ viewBox="0 0 9 7"
725
+ fill="none"
726
+ xmlns="http://www.w3.org/2000/svg"
727
+ >
728
+ <path d="M4.49999 0L8.3971 6.75H0.602875L4.49999 0Z" />
729
+ </svg>
730
+ </div>
731
+ </div>
732
+ </th>
733
+ {/each}
734
+ </tr>
735
+
736
+ <tr slot="tbody" let:item let:index class:row_odd={index % 2 === 0}>
737
+ {#each item as { value, id }, j (id)}
738
+ <td
739
+ tabindex="0"
740
+ on:touchstart={() => start_edit(index, j)}
741
+ on:click={() => handle_cell_click(index, j)}
742
+ on:dblclick={() => start_edit(index, j)}
743
+ style="width: var(--cell-width-{j});"
744
+ class:focus={dequal(selected, [index, j])}
745
+ >
746
+ <div class="cell-wrap">
747
+ <EditableCell
748
+ bind:value={data[index][j].value}
749
+ bind:el={els[id].input}
750
+ {latex_delimiters}
751
+ edit={dequal(editing, [index, j])}
752
+ datatype={Array.isArray(datatype) ? datatype[j] : datatype}
753
+ on:blur={() => ((clear_on_focus = false), parent.focus())}
754
+ {clear_on_focus}
755
+ />
756
+ </div>
757
+ </td>
758
+ {/each}
759
+ </tr>
760
+ </VirtualTable>
761
+ </Upload>
762
+ </div>
763
+ {#if editable}
764
+ <div class="controls-wrap">
765
+ {#if row_count[1] === "dynamic"}
766
+ <span class="button-wrap">
767
+ <BaseButton
768
+ variant="secondary"
769
+ size="sm"
770
+ on:click={(e) => (e.stopPropagation(), add_row())}
771
+ >
772
+ <svg
773
+ xmlns="http://www.w3.org/2000/svg"
774
+ xmlns:xlink="http://www.w3.org/1999/xlink"
775
+ aria-hidden="true"
776
+ role="img"
777
+ width="1em"
778
+ height="1em"
779
+ preserveAspectRatio="xMidYMid meet"
780
+ viewBox="0 0 32 32"
781
+ >
782
+ <path
783
+ fill="currentColor"
784
+ d="M24.59 16.59L17 24.17V4h-2v20.17l-7.59-7.58L6 18l10 10l10-10l-1.41-1.41z"
785
+ />
786
+ </svg>
787
+ {i18n("dataframe.new_row")}
788
+ </BaseButton>
789
+ </span>
790
+ {/if}
791
+ {#if col_count[1] === "dynamic"}
792
+ <span class="button-wrap">
793
+ <BaseButton
794
+ variant="secondary"
795
+ size="sm"
796
+ on:click={(e) => (e.stopPropagation(), add_col())}
797
+ >
798
+ <svg
799
+ xmlns="http://www.w3.org/2000/svg"
800
+ xmlns:xlink="http://www.w3.org/1999/xlink"
801
+ aria-hidden="true"
802
+ role="img"
803
+ width="1em"
804
+ height="1em"
805
+ preserveAspectRatio="xMidYMid meet"
806
+ viewBox="0 0 32 32"
807
+ >
808
+ <path
809
+ fill="currentColor"
810
+ d="m18 6l-1.43 1.393L24.15 15H4v2h20.15l-7.58 7.573L18 26l10-10L18 6z"
811
+ />
812
+ </svg>
813
+ {i18n("dataframe.new_column")}
814
+ </BaseButton>
815
+ </span>
816
+ {/if}
817
+ </div>
818
+ {/if}
819
+ </div>
820
+
821
+ <style>
822
+ .button-wrap:hover svg {
823
+ color: var(--color-accent);
824
+ }
825
+
826
+ .button-wrap svg {
827
+ margin-right: var(--size-1);
828
+ margin-left: -5px;
829
+ }
830
+
831
+ .label p {
832
+ position: relative;
833
+ z-index: var(--layer-4);
834
+ margin-bottom: var(--size-2);
835
+ color: var(--block-label-text-color);
836
+ font-size: var(--block-label-text-size);
837
+ }
838
+
839
+ .table-wrap {
840
+ position: relative;
841
+ transition: 150ms;
842
+ border: 1px solid var(--border-color-primary);
843
+ border-radius: var(--table-radius);
844
+ overflow: hidden;
845
+ }
846
+
847
+ .table-wrap:focus-within {
848
+ outline: none;
849
+ background-color: none;
850
+ }
851
+
852
+ .dragging {
853
+ border-color: var(--color-accent);
854
+ }
855
+
856
+ .no-wrap {
857
+ white-space: nowrap;
858
+ }
859
+
860
+ table {
861
+ position: absolute;
862
+ opacity: 0;
863
+ transition: 150ms;
864
+ width: var(--size-full);
865
+ table-layout: auto;
866
+ color: var(--body-text-color);
867
+ font-size: var(--input-text-size);
868
+ line-height: var(--line-md);
869
+ font-family: var(--font-mono);
870
+ border-spacing: 0;
871
+ }
872
+
873
+ thead {
874
+ position: sticky;
875
+ top: 0;
876
+ left: 0;
877
+ z-index: var(--layer-1);
878
+ box-shadow: var(--shadow-drop);
879
+ }
880
+
881
+ tr {
882
+ border-bottom: 1px solid var(--border-color-primary);
883
+ text-align: left;
884
+ }
885
+
886
+ tr > * + * {
887
+ border-right-width: 0px;
888
+ border-left-width: 1px;
889
+ border-style: solid;
890
+ border-color: var(--border-color-primary);
891
+ }
892
+
893
+ th,
894
+ td {
895
+ --ring-color: transparent;
896
+ position: relative;
897
+ outline: none;
898
+ box-shadow: inset 0 0 0 1px var(--ring-color);
899
+ padding: 0;
900
+ }
901
+
902
+ th:first-child {
903
+ border-top-left-radius: var(--table-radius);
904
+ }
905
+
906
+ th:last-child {
907
+ border-top-right-radius: var(--table-radius);
908
+ }
909
+
910
+ th.focus,
911
+ td.focus {
912
+ --ring-color: var(--color-accent);
913
+ }
914
+
915
+ tr:last-child td:first-child {
916
+ border-bottom-left-radius: var(--table-radius);
917
+ }
918
+
919
+ tr:last-child td:last-child {
920
+ border-bottom-right-radius: var(--table-radius);
921
+ }
922
+
923
+ tr th {
924
+ background: var(--table-even-background-fill);
925
+ }
926
+
927
+ th svg {
928
+ fill: currentColor;
929
+ font-size: 10px;
930
+ }
931
+
932
+ .sort-button {
933
+ display: flex;
934
+ flex: none;
935
+ justify-content: center;
936
+ align-items: center;
937
+ transition: 150ms;
938
+ cursor: pointer;
939
+ padding: var(--size-2);
940
+ color: var(--body-text-color-subdued);
941
+ line-height: var(--text-sm);
942
+ }
943
+
944
+ .sort-button:hover {
945
+ color: var(--body-text-color);
946
+ }
947
+
948
+ .des {
949
+ transform: scaleY(-1);
950
+ }
951
+
952
+ .sort-button.sorted {
953
+ color: var(--color-accent);
954
+ }
955
+
956
+ .editing {
957
+ background: var(--table-editing);
958
+ }
959
+
960
+ .cell-wrap {
961
+ display: flex;
962
+ align-items: center;
963
+ outline: none;
964
+ height: var(--size-full);
965
+ min-height: var(--size-9);
966
+ }
967
+
968
+ .controls-wrap {
969
+ display: flex;
970
+ justify-content: flex-end;
971
+ padding-top: var(--size-2);
972
+ }
973
+
974
+ .controls-wrap > * + * {
975
+ margin-left: var(--size-1);
976
+ }
977
+
978
+ .row_odd {
979
+ background: var(--table-odd-background-fill);
980
+ }
981
+
982
+ .row_odd.focus {
983
+ background: var(--background-fill-primary);
984
+ }
985
+ </style>