@gradio/dataframe 0.13.1 → 0.15.0

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.
@@ -1,21 +1,63 @@
1
1
  <script lang="ts">
2
- import { Maximize, Minimize } from "@gradio/icons";
2
+ import { Maximize, Minimize, Copy, Check } from "@gradio/icons";
3
+ import { onDestroy } from "svelte";
3
4
 
4
5
  export let show_fullscreen_button = false;
6
+ export let show_copy_button = false;
5
7
  export let is_fullscreen = false;
8
+ export let on_copy: () => Promise<void>;
9
+
10
+ let copied = false;
11
+ let timer: ReturnType<typeof setTimeout>;
12
+
13
+ function copy_feedback(): void {
14
+ copied = true;
15
+ if (timer) clearTimeout(timer);
16
+ timer = setTimeout(() => {
17
+ copied = false;
18
+ }, 2000);
19
+ }
20
+
21
+ async function handle_copy(): Promise<void> {
22
+ await on_copy();
23
+ copy_feedback();
24
+ }
25
+
26
+ onDestroy(() => {
27
+ if (timer) clearTimeout(timer);
28
+ });
6
29
  </script>
7
30
 
8
- {#if show_fullscreen_button}
9
- <div class="toolbar">
10
- <button class="toolbar-button" on:click>
31
+ <div class="toolbar" role="toolbar" aria-label="Table actions">
32
+ {#if show_copy_button}
33
+ <button
34
+ class="toolbar-button"
35
+ on:click={handle_copy}
36
+ aria-label={copied ? "Copied to clipboard" : "Copy table data"}
37
+ title={copied ? "Copied to clipboard" : "Copy table data"}
38
+ >
39
+ {#if copied}
40
+ <Check />
41
+ {:else}
42
+ <Copy />
43
+ {/if}
44
+ </button>
45
+ {/if}
46
+ {#if show_fullscreen_button}
47
+ <button
48
+ class="toolbar-button"
49
+ on:click
50
+ aria-label={is_fullscreen ? "Exit fullscreen" : "Enter fullscreen"}
51
+ title={is_fullscreen ? "Exit fullscreen" : "Enter fullscreen"}
52
+ >
11
53
  {#if is_fullscreen}
12
54
  <Minimize />
13
55
  {:else}
14
56
  <Maximize />
15
57
  {/if}
16
58
  </button>
17
- </div>
18
- {/if}
59
+ {/if}
60
+ </div>
19
61
 
20
62
  <style>
21
63
  .toolbar {
@@ -0,0 +1,188 @@
1
+ export type CellCoordinate = [number, number];
2
+ export type CellData = { id: string; value: string | number };
3
+ export type EditingState = false | CellCoordinate;
4
+
5
+ export function is_cell_selected(
6
+ cell: CellCoordinate,
7
+ selected_cells: CellCoordinate[]
8
+ ): string {
9
+ const [row, col] = cell;
10
+ if (!selected_cells.some(([r, c]) => r === row && c === col)) return "";
11
+
12
+ const up = selected_cells.some(([r, c]) => r === row - 1 && c === col);
13
+ const down = selected_cells.some(([r, c]) => r === row + 1 && c === col);
14
+ const left = selected_cells.some(([r, c]) => r === row && c === col - 1);
15
+ const right = selected_cells.some(([r, c]) => r === row && c === col + 1);
16
+
17
+ return `cell-selected${up ? " no-top" : ""}${down ? " no-bottom" : ""}${left ? " no-left" : ""}${right ? " no-right" : ""}`;
18
+ }
19
+
20
+ export function get_range_selection(
21
+ start: CellCoordinate,
22
+ end: CellCoordinate
23
+ ): CellCoordinate[] {
24
+ const [start_row, start_col] = start;
25
+ const [end_row, end_col] = end;
26
+ const min_row = Math.min(start_row, end_row);
27
+ const max_row = Math.max(start_row, end_row);
28
+ const min_col = Math.min(start_col, end_col);
29
+ const max_col = Math.max(start_col, end_col);
30
+
31
+ const cells: CellCoordinate[] = [];
32
+ for (let i = min_row; i <= max_row; i++) {
33
+ for (let j = min_col; j <= max_col; j++) {
34
+ cells.push([i, j]);
35
+ }
36
+ }
37
+ return cells;
38
+ }
39
+
40
+ export function handle_selection(
41
+ current: CellCoordinate,
42
+ selected_cells: CellCoordinate[],
43
+ event: { shiftKey: boolean; metaKey: boolean; ctrlKey: boolean }
44
+ ): CellCoordinate[] {
45
+ if (event.shiftKey && selected_cells.length > 0) {
46
+ return get_range_selection(
47
+ selected_cells[selected_cells.length - 1],
48
+ current
49
+ );
50
+ }
51
+
52
+ if (event.metaKey || event.ctrlKey) {
53
+ const index = selected_cells.findIndex(
54
+ ([r, c]) => r === current[0] && c === current[1]
55
+ );
56
+ if (index === -1) {
57
+ return [...selected_cells, current];
58
+ }
59
+ return selected_cells.filter((_, i) => i !== index);
60
+ }
61
+
62
+ return [current];
63
+ }
64
+
65
+ export function handle_delete_key(
66
+ data: CellData[][],
67
+ selected_cells: CellCoordinate[]
68
+ ): CellData[][] {
69
+ const new_data = data.map((row) => [...row]);
70
+ selected_cells.forEach(([row, col]) => {
71
+ if (new_data[row] && new_data[row][col]) {
72
+ new_data[row][col] = { ...new_data[row][col], value: "" };
73
+ }
74
+ });
75
+ return new_data;
76
+ }
77
+
78
+ export function handle_editing_state(
79
+ current: CellCoordinate,
80
+ editing: EditingState,
81
+ selected_cells: CellCoordinate[],
82
+ editable: boolean
83
+ ): EditingState {
84
+ const [row, col] = current;
85
+ if (!editable) return false;
86
+
87
+ if (editing && editing[0] === row && editing[1] === col) return editing;
88
+
89
+ if (
90
+ selected_cells.length === 1 &&
91
+ selected_cells[0][0] === row &&
92
+ selected_cells[0][1] === col
93
+ ) {
94
+ return [row, col];
95
+ }
96
+
97
+ return false;
98
+ }
99
+
100
+ export function should_show_cell_menu(
101
+ cell: CellCoordinate,
102
+ selected_cells: CellCoordinate[],
103
+ editable: boolean
104
+ ): boolean {
105
+ const [row, col] = cell;
106
+ return (
107
+ editable &&
108
+ selected_cells.length === 1 &&
109
+ selected_cells[0][0] === row &&
110
+ selected_cells[0][1] === col
111
+ );
112
+ }
113
+
114
+ export function get_next_cell_coordinates(
115
+ current: CellCoordinate,
116
+ data: CellData[][],
117
+ shift_key: boolean
118
+ ): CellCoordinate | false {
119
+ const [row, col] = current;
120
+ const direction = shift_key ? -1 : 1;
121
+
122
+ if (data[row]?.[col + direction]) {
123
+ return [row, col + direction];
124
+ }
125
+
126
+ const next_row = row + (direction > 0 ? 1 : 0);
127
+ const prev_row = row + (direction < 0 ? -1 : 0);
128
+
129
+ if (direction > 0 && data[next_row]?.[0]) {
130
+ return [next_row, 0];
131
+ }
132
+
133
+ if (direction < 0 && data[prev_row]?.[data[0].length - 1]) {
134
+ return [prev_row, data[0].length - 1];
135
+ }
136
+
137
+ return false;
138
+ }
139
+
140
+ export function move_cursor(
141
+ key: "ArrowRight" | "ArrowLeft" | "ArrowDown" | "ArrowUp",
142
+ current_coords: CellCoordinate,
143
+ data: CellData[][]
144
+ ): CellCoordinate | false {
145
+ const dir = {
146
+ ArrowRight: [0, 1],
147
+ ArrowLeft: [0, -1],
148
+ ArrowDown: [1, 0],
149
+ ArrowUp: [-1, 0]
150
+ }[key];
151
+
152
+ const i = current_coords[0] + dir[0];
153
+ const j = current_coords[1] + dir[1];
154
+
155
+ if (i < 0 && j <= 0) {
156
+ return false;
157
+ }
158
+
159
+ const is_data = data[i]?.[j];
160
+ if (is_data) {
161
+ return [i, j];
162
+ }
163
+ return false;
164
+ }
165
+
166
+ export function get_current_indices(
167
+ id: string,
168
+ data: CellData[][]
169
+ ): [number, number] {
170
+ return data.reduce(
171
+ (acc, arr, i) => {
172
+ const j = arr.reduce(
173
+ (_acc, _data, k) => (id === _data.id ? k : _acc),
174
+ -1
175
+ );
176
+ return j === -1 ? acc : [i, j];
177
+ },
178
+ [-1, -1]
179
+ );
180
+ }
181
+
182
+ export function handle_click_outside(
183
+ event: Event,
184
+ parent: HTMLElement
185
+ ): boolean {
186
+ const [trigger] = event.composedPath() as HTMLElement[];
187
+ return !parent.contains(trigger);
188
+ }
@@ -0,0 +1,148 @@
1
+ import type { HeadersWithIDs } from "./utils";
2
+ import type { CellCoordinate } from "./selection_utils";
3
+ import { dsvFormat } from "d3-dsv";
4
+
5
+ export type TableData = {
6
+ value: string | number;
7
+ id: string;
8
+ }[][];
9
+
10
+ export function get_max(_d: TableData): TableData[0] {
11
+ if (!_d || _d.length === 0 || !_d[0]) return [];
12
+ let max = _d[0].slice();
13
+ for (let i = 0; i < _d.length; i++) {
14
+ for (let j = 0; j < _d[i].length; j++) {
15
+ if (`${max[j].value}`.length < `${_d[i][j].value}`.length) {
16
+ max[j] = _d[i][j];
17
+ }
18
+ }
19
+ }
20
+ return max;
21
+ }
22
+
23
+ export function guess_delimiter(
24
+ text: string,
25
+ possibleDelimiters: string[]
26
+ ): string[] {
27
+ return possibleDelimiters.filter(weedOut);
28
+
29
+ function weedOut(delimiter: string): boolean {
30
+ var cache = -1;
31
+ return text.split("\n").every(checkLength);
32
+
33
+ function checkLength(line: string): boolean {
34
+ if (!line) return true;
35
+ var length = line.split(delimiter).length;
36
+ if (cache < 0) cache = length;
37
+ return cache === length && length > 1;
38
+ }
39
+ }
40
+ }
41
+
42
+ export function data_uri_to_blob(data_uri: string): Blob {
43
+ const byte_str = atob(data_uri.split(",")[1]);
44
+ const mime_str = data_uri.split(",")[0].split(":")[1].split(";")[0];
45
+ const ab = new ArrayBuffer(byte_str.length);
46
+ const ia = new Uint8Array(ab);
47
+ for (let i = 0; i < byte_str.length; i++) {
48
+ ia[i] = byte_str.charCodeAt(i);
49
+ }
50
+ return new Blob([ab], { type: mime_str });
51
+ }
52
+
53
+ export async function copy_table_data(
54
+ data: TableData,
55
+ headers?: HeadersWithIDs,
56
+ selected_cells?: CellCoordinate[]
57
+ ): Promise<void> {
58
+ if (!selected_cells || selected_cells.length === 0) {
59
+ const header_row = headers
60
+ ? headers.map((h) => String(h.value)).join(",")
61
+ : "";
62
+ const table_data = data
63
+ .map((row) => row.map((cell) => String(cell.value)).join(","))
64
+ .join("\n");
65
+
66
+ const all_data = header_row ? `${header_row}\n${table_data}` : table_data;
67
+ await write_to_clipboard(all_data);
68
+ return;
69
+ }
70
+
71
+ const min_row = Math.min(...selected_cells.map(([r]) => r));
72
+ const max_row = Math.max(...selected_cells.map(([r]) => r));
73
+ const min_col = Math.min(...selected_cells.map(([_, c]) => c));
74
+ const max_col = Math.max(...selected_cells.map(([_, c]) => c));
75
+
76
+ const selected_data = [];
77
+ for (let i = min_row; i <= max_row; i++) {
78
+ const row = [];
79
+ for (let j = min_col; j <= max_col; j++) {
80
+ const is_selected = selected_cells.some(([r, c]) => r === i && c === j);
81
+ row.push(is_selected ? String(data[i][j].value) : "");
82
+ }
83
+ selected_data.push(row.join(","));
84
+ }
85
+
86
+ await write_to_clipboard(selected_data.join("\n"));
87
+ }
88
+
89
+ async function write_to_clipboard(csv_data: string): Promise<void> {
90
+ try {
91
+ if ("clipboard" in navigator) {
92
+ await navigator.clipboard.writeText(csv_data);
93
+ } else {
94
+ const textArea = document.createElement("textarea");
95
+ textArea.value = csv_data;
96
+ textArea.style.position = "absolute";
97
+ textArea.style.left = "-999999px";
98
+ document.body.prepend(textArea);
99
+ textArea.select();
100
+
101
+ document.execCommand("copy");
102
+ textArea.remove();
103
+ }
104
+ } catch (error) {
105
+ console.error("Failed to copy table data:", error);
106
+ }
107
+ }
108
+
109
+ export function blob_to_string(
110
+ blob: Blob,
111
+ col_count: [number, "fixed" | "dynamic"],
112
+ make_headers: (head: string[]) => HeadersWithIDs,
113
+ set_values: (values: (string | number)[][]) => void
114
+ ): void {
115
+ const reader = new FileReader();
116
+
117
+ function handle_read(e: ProgressEvent<FileReader>): void {
118
+ if (!e?.target?.result || typeof e.target.result !== "string") return;
119
+
120
+ const [delimiter] = guess_delimiter(e.target.result, [",", "\t"]);
121
+ const [head, ...rest] = dsvFormat(delimiter).parseRows(e.target.result);
122
+
123
+ make_headers(col_count[1] === "fixed" ? head.slice(0, col_count[0]) : head);
124
+ set_values(rest);
125
+ reader.removeEventListener("loadend", handle_read);
126
+ }
127
+
128
+ reader.addEventListener("loadend", handle_read);
129
+ reader.readAsText(blob);
130
+ }
131
+
132
+ export function handle_file_upload(
133
+ data_uri: string,
134
+ col_count: [number, "fixed" | "dynamic"],
135
+ make_headers: (head: string[]) => HeadersWithIDs,
136
+ set_values: (values: (string | number)[][]) => void
137
+ ): void {
138
+ const blob = data_uri_to_blob(data_uri);
139
+ const reader = new FileReader();
140
+ reader.addEventListener("loadend", (e) => {
141
+ if (!e?.target?.result || typeof e.target.result !== "string") return;
142
+ const [delimiter] = guess_delimiter(e.target.result, [",", "\t"]);
143
+ const [head, ...rest] = dsvFormat(delimiter).parseRows(e.target.result);
144
+ make_headers(col_count[1] === "fixed" ? head.slice(0, col_count[0]) : head);
145
+ set_values(rest);
146
+ });
147
+ reader.readAsText(blob);
148
+ }
@@ -0,0 +1,2 @@
1
+ export type CellCoordinate = [number, number];
2
+ export type EditingState = CellCoordinate | false;
@@ -1,9 +0,0 @@
1
- <script>export let transform;
2
- </script>
3
-
4
- <svg viewBox="0 0 24 24" width="16" height="16">
5
- <path
6
- d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"
7
- {transform}
8
- />
9
- </svg>
@@ -1,16 +0,0 @@
1
- import { SvelteComponent } from "svelte";
2
- declare const __propDef: {
3
- props: {
4
- transform: string;
5
- };
6
- events: {
7
- [evt: string]: CustomEvent<any>;
8
- };
9
- slots: {};
10
- };
11
- export type ArrowProps = typeof __propDef.props;
12
- export type ArrowEvents = typeof __propDef.events;
13
- export type ArrowSlots = typeof __propDef.slots;
14
- export default class Arrow extends SvelteComponent<ArrowProps, ArrowEvents, ArrowSlots> {
15
- }
16
- export {};
@@ -1,10 +0,0 @@
1
- <script lang="ts">
2
- export let transform: string;
3
- </script>
4
-
5
- <svg viewBox="0 0 24 24" width="16" height="16">
6
- <path
7
- d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"
8
- {transform}
9
- />
10
- </svg>