@gradio/dataframe 0.13.1 → 0.14.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @gradio/dataframe
2
2
 
3
+ ## 0.14.0
4
+
5
+ ### Features
6
+
7
+ - [#10461](https://github.com/gradio-app/gradio/pull/10461) [`ca7c47e`](https://github.com/gradio-app/gradio/commit/ca7c47e5e50a309cd637c4f928ab90af6355b01d) - Add copy button to dataframe toolbar. Thanks @hannahblair!
8
+ - [#10420](https://github.com/gradio-app/gradio/pull/10420) [`a69b8e8`](https://github.com/gradio-app/gradio/commit/a69b8e83ad7c89c627db2bdd5d25b0142731aaac) - Support column/row deletion in `gr.DataFrame`. Thanks @abidlabs!
9
+
10
+ ### Dependency updates
11
+
12
+ - @gradio/upload@0.14.8
13
+ - @gradio/button@0.4.4
14
+
3
15
  ## 0.13.1
4
16
 
5
17
  ### Features
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ // @ts-nocheck
2
3
  import { Meta, Template, Story } from "@storybook/addon-svelte-csf";
3
4
  import Table from "./shared/Table.svelte";
4
5
  import { within } from "@testing-library/dom";
@@ -10,6 +11,11 @@
10
11
  <Meta
11
12
  title="Components/DataFrame"
12
13
  component={Table}
14
+ parameters={{
15
+ test: {
16
+ dangerouslyIgnoreUnhandledErrors: true // ignore fullscreen permission error
17
+ }
18
+ }}
13
19
  argTypes={{
14
20
  editable: {
15
21
  control: [true, false],
@@ -220,3 +226,33 @@
220
226
  show_fullscreen_button: true
221
227
  }}
222
228
  />
229
+
230
+ <Story
231
+ name="Dataframe toolbar interactions"
232
+ args={{
233
+ col_count: [3, "dynamic"],
234
+ row_count: [2, "dynamic"],
235
+ headers: ["Math", "Reading", "Writifdsfsng"],
236
+ values: [
237
+ [800, 100, 400],
238
+ [200, 800, 700]
239
+ ],
240
+ show_fullscreen_button: true,
241
+ show_copy_button: true
242
+ }}
243
+ play={async ({ canvasElement }) => {
244
+ const canvas = within(canvasElement);
245
+
246
+ const copy_button = canvas.getByRole("button", {
247
+ name: /copy table data/i
248
+ });
249
+ await userEvent.click(copy_button);
250
+
251
+ const fullscreen_button = canvas.getByRole("button", {
252
+ name: /enter fullscreen/i
253
+ });
254
+ await userEvent.click(fullscreen_button);
255
+
256
+ await userEvent.click(fullscreen_button);
257
+ }}
258
+ />
package/Index.svelte CHANGED
@@ -49,6 +49,7 @@
49
49
  export let loading_status: LoadingStatus;
50
50
  export let interactive: boolean;
51
51
  export let show_fullscreen_button = false;
52
+ export let show_copy_button = false;
52
53
 
53
54
  $: _headers = [...(value.headers || headers)];
54
55
  $: cell_values = value.data ? [...value.data] : [];
@@ -105,5 +106,6 @@
105
106
  stream_handler={(...args) => gradio.client.stream(...args)}
106
107
  bind:value_is_output
107
108
  {show_fullscreen_button}
109
+ {show_copy_button}
108
110
  />
109
111
  </Block>
package/dist/Index.svelte CHANGED
@@ -34,6 +34,7 @@ export let max_height = void 0;
34
34
  export let loading_status;
35
35
  export let interactive;
36
36
  export let show_fullscreen_button = false;
37
+ export let show_copy_button = false;
37
38
  $:
38
39
  _headers = [...value.headers || headers];
39
40
  $:
@@ -88,5 +89,6 @@ $:
88
89
  stream_handler={(...args) => gradio.client.stream(...args)}
89
90
  bind:value_is_output
90
91
  {show_fullscreen_button}
92
+ {show_copy_button}
91
93
  />
92
94
  </Block>
@@ -38,6 +38,7 @@ declare const __propDef: {
38
38
  loading_status: LoadingStatus;
39
39
  interactive: boolean;
40
40
  show_fullscreen_button?: boolean | undefined;
41
+ show_copy_button?: boolean | undefined;
41
42
  };
42
43
  events: {
43
44
  [evt: string]: CustomEvent<any>;
@@ -135,4 +136,7 @@ export default class Index extends SvelteComponent<IndexProps, IndexEvents, Inde
135
136
  get show_fullscreen_button(): boolean | undefined;
136
137
  /**accessor*/
137
138
  set show_fullscreen_button(_: boolean | undefined);
139
+ get show_copy_button(): boolean | undefined;
140
+ /**accessor*/
141
+ set show_copy_button(_: boolean | undefined);
138
142
  }
@@ -1,5 +1,5 @@
1
1
  <script>import { onMount } from "svelte";
2
- import Arrow from "./Arrow.svelte";
2
+ import CellMenuIcons from "./CellMenuIcons.svelte";
3
3
  export let x;
4
4
  export let y;
5
5
  export let on_add_row_above;
@@ -9,6 +9,10 @@ export let on_add_column_right;
9
9
  export let row;
10
10
  export let col_count;
11
11
  export let row_count;
12
+ export let on_delete_row;
13
+ export let on_delete_col;
14
+ export let can_delete_rows;
15
+ export let can_delete_cols;
12
16
  export let i18n;
13
17
  let menu_element;
14
18
  $:
@@ -42,23 +46,35 @@ function position_menu() {
42
46
  <div bind:this={menu_element} class="cell-menu">
43
47
  {#if !is_header && can_add_rows}
44
48
  <button on:click={() => on_add_row_above()}>
45
- <Arrow transform="rotate(-90 12 12)" />
49
+ <CellMenuIcons icon="add-row-above" />
46
50
  {i18n("dataframe.add_row_above")}
47
51
  </button>
48
52
  <button on:click={() => on_add_row_below()}>
49
- <Arrow transform="rotate(90 12 12)" />
53
+ <CellMenuIcons icon="add-row-below" />
50
54
  {i18n("dataframe.add_row_below")}
51
55
  </button>
56
+ {#if can_delete_rows}
57
+ <button on:click={on_delete_row} class="delete">
58
+ <CellMenuIcons icon="delete-row" />
59
+ {i18n("dataframe.delete_row")}
60
+ </button>
61
+ {/if}
52
62
  {/if}
53
63
  {#if can_add_columns}
54
64
  <button on:click={() => on_add_column_left()}>
55
- <Arrow transform="rotate(180 12 12)" />
65
+ <CellMenuIcons icon="add-column-left" />
56
66
  {i18n("dataframe.add_column_left")}
57
67
  </button>
58
68
  <button on:click={() => on_add_column_right()}>
59
- <Arrow transform="rotate(0 12 12)" />
69
+ <CellMenuIcons icon="add-column-right" />
60
70
  {i18n("dataframe.add_column_right")}
61
71
  </button>
72
+ {#if can_delete_cols}
73
+ <button on:click={on_delete_col} class="delete">
74
+ <CellMenuIcons icon="delete-column" />
75
+ {i18n("dataframe.delete_column")}
76
+ </button>
77
+ {/if}
62
78
  {/if}
63
79
  </div>
64
80
 
@@ -102,8 +118,4 @@ function position_menu() {
102
118
  fill: currentColor;
103
119
  transition: fill 0.2s;
104
120
  }
105
-
106
- .cell-menu button:hover :global(svg) {
107
- fill: var(--color-accent);
108
- }
109
121
  </style>
@@ -11,6 +11,10 @@ declare const __propDef: {
11
11
  row: number;
12
12
  col_count: [number, "fixed" | "dynamic"];
13
13
  row_count: [number, "fixed" | "dynamic"];
14
+ on_delete_row: () => void;
15
+ on_delete_col: () => void;
16
+ can_delete_rows: boolean;
17
+ can_delete_cols: boolean;
14
18
  i18n: I18nFormatter;
15
19
  };
16
20
  events: {
@@ -0,0 +1,112 @@
1
+ <script>export let icon;
2
+ </script>
3
+
4
+ {#if icon == "add-column-right"}
5
+ <svg viewBox="0 0 24 24" width="16" height="16">
6
+ <rect
7
+ x="4"
8
+ y="6"
9
+ width="4"
10
+ height="12"
11
+ stroke="currentColor"
12
+ stroke-width="2"
13
+ fill="none"
14
+ />
15
+ <path
16
+ d="M12 12H19M16 8L19 12L16 16"
17
+ stroke="currentColor"
18
+ stroke-width="2"
19
+ fill="none"
20
+ stroke-linecap="round"
21
+ />
22
+ </svg>
23
+ {:else if icon == "add-column-left"}
24
+ <svg viewBox="0 0 24 24" width="16" height="16">
25
+ <rect
26
+ x="16"
27
+ y="6"
28
+ width="4"
29
+ height="12"
30
+ stroke="currentColor"
31
+ stroke-width="2"
32
+ fill="none"
33
+ />
34
+ <path
35
+ d="M12 12H5M8 8L5 12L8 16"
36
+ stroke="currentColor"
37
+ stroke-width="2"
38
+ fill="none"
39
+ stroke-linecap="round"
40
+ />
41
+ </svg>
42
+ {:else if icon == "add-row-above"}
43
+ <svg viewBox="0 0 24 24" width="16" height="16">
44
+ <rect
45
+ x="6"
46
+ y="16"
47
+ width="12"
48
+ height="4"
49
+ stroke="currentColor"
50
+ stroke-width="2"
51
+ />
52
+ <path
53
+ d="M12 12V5M8 8L12 5L16 8"
54
+ stroke="currentColor"
55
+ stroke-width="2"
56
+ fill="none"
57
+ stroke-linecap="round"
58
+ />
59
+ </svg>
60
+ {:else if icon == "add-row-below"}
61
+ <svg viewBox="0 0 24 24" width="16" height="16">
62
+ <rect
63
+ x="6"
64
+ y="4"
65
+ width="12"
66
+ height="4"
67
+ stroke="currentColor"
68
+ stroke-width="2"
69
+ />
70
+ <path
71
+ d="M12 12V19M8 16L12 19L16 16"
72
+ stroke="currentColor"
73
+ stroke-width="2"
74
+ fill="none"
75
+ stroke-linecap="round"
76
+ />
77
+ </svg>
78
+ {:else if icon == "delete-row"}
79
+ <svg viewBox="0 0 24 24" width="16" height="16">
80
+ <rect
81
+ x="5"
82
+ y="10"
83
+ width="14"
84
+ height="4"
85
+ stroke="currentColor"
86
+ stroke-width="2"
87
+ />
88
+ <path
89
+ d="M8 7L16 17M16 7L8 17"
90
+ stroke="currentColor"
91
+ stroke-width="2"
92
+ stroke-linecap="round"
93
+ />
94
+ </svg>
95
+ {:else if icon == "delete-column"}
96
+ <svg viewBox="0 0 24 24" width="16" height="16">
97
+ <rect
98
+ x="10"
99
+ y="5"
100
+ width="4"
101
+ height="14"
102
+ stroke="currentColor"
103
+ stroke-width="2"
104
+ />
105
+ <path
106
+ d="M7 8L17 16M17 8L7 16"
107
+ stroke="currentColor"
108
+ stroke-width="2"
109
+ stroke-linecap="round"
110
+ />
111
+ </svg>
112
+ {/if}
@@ -0,0 +1,16 @@
1
+ import { SvelteComponent } from "svelte";
2
+ declare const __propDef: {
3
+ props: {
4
+ icon: string;
5
+ };
6
+ events: {
7
+ [evt: string]: CustomEvent<any>;
8
+ };
9
+ slots: {};
10
+ };
11
+ export type CellMenuIconsProps = typeof __propDef.props;
12
+ export type CellMenuIconsEvents = typeof __propDef.events;
13
+ export type CellMenuIconsSlots = typeof __propDef.slots;
14
+ export default class CellMenuIcons extends SvelteComponent<CellMenuIconsProps, CellMenuIconsEvents, CellMenuIconsSlots> {
15
+ }
16
+ export {};
@@ -7,6 +7,7 @@ import {} from "@gradio/client";
7
7
  import VirtualTable from "./VirtualTable.svelte";
8
8
  import CellMenu from "./CellMenu.svelte";
9
9
  import Toolbar from "./Toolbar.svelte";
10
+ import { copy_table_data } from "./table_utils";
10
11
  export let datatype;
11
12
  export let label = null;
12
13
  export let show_label = true;
@@ -26,6 +27,7 @@ export let show_row_numbers = false;
26
27
  export let upload;
27
28
  export let stream_handler;
28
29
  export let show_fullscreen_button = false;
30
+ export let show_copy_button = false;
29
31
  export let value_is_output = false;
30
32
  let selected = false;
31
33
  let clicked_cell = void 0;
@@ -75,9 +77,7 @@ function make_headers(_head) {
75
77
  }
76
78
  function process_data(_values) {
77
79
  const data_row_length = _values.length;
78
- return Array(
79
- row_count[1] === "fixed" ? row_count[0] : data_row_length < row_count[0] ? row_count[0] : data_row_length
80
- ).fill(0).map(
80
+ return Array(row_count[1] === "fixed" ? row_count[0] : data_row_length).fill(0).map(
81
81
  (_, i) => Array(
82
82
  col_count[1] === "fixed" ? col_count[0] : data_row_length > 0 ? _values[0].length : headers.length
83
83
  ).fill(0).map((_2, j) => {
@@ -574,6 +574,9 @@ function toggle_fullscreen() {
574
574
  function handle_fullscreen_change() {
575
575
  is_fullscreen = !!document.fullscreenElement;
576
576
  }
577
+ async function handle_copy() {
578
+ await copy_table_data(data, _headers);
579
+ }
577
580
  function toggle_header_menu(event, col) {
578
581
  event.stopPropagation();
579
582
  if (active_header_menu && active_header_menu.col === col) {
@@ -593,6 +596,40 @@ function toggle_header_menu(event, col) {
593
596
  afterUpdate(() => {
594
597
  value_is_output = false;
595
598
  });
599
+ async function delete_row(index) {
600
+ parent.focus();
601
+ if (row_count[1] !== "dynamic")
602
+ return;
603
+ if (data.length <= 1)
604
+ return;
605
+ data.splice(index, 1);
606
+ data = data;
607
+ selected = false;
608
+ }
609
+ async function delete_col(index) {
610
+ parent.focus();
611
+ if (col_count[1] !== "dynamic")
612
+ return;
613
+ if (data[0].length <= 1)
614
+ return;
615
+ _headers.splice(index, 1);
616
+ _headers = _headers;
617
+ data.forEach((row) => {
618
+ row.splice(index, 1);
619
+ });
620
+ data = data;
621
+ selected = false;
622
+ }
623
+ function delete_row_at(index) {
624
+ delete_row(index);
625
+ active_cell_menu = null;
626
+ active_header_menu = null;
627
+ }
628
+ function delete_col_at(index) {
629
+ delete_col(index);
630
+ active_cell_menu = null;
631
+ active_header_menu = null;
632
+ }
596
633
  </script>
597
634
 
598
635
  <svelte:window on:resize={() => set_cell_widths()} />
@@ -608,6 +645,8 @@ afterUpdate(() => {
608
645
  {show_fullscreen_button}
609
646
  {is_fullscreen}
610
647
  on:click={toggle_fullscreen}
648
+ on_copy={handle_copy}
649
+ {show_copy_button}
611
650
  />
612
651
  </div>
613
652
  <div
@@ -852,18 +891,22 @@ afterUpdate(() => {
852
891
  </div>
853
892
  </div>
854
893
 
855
- {#if active_cell_menu !== null}
894
+ {#if active_cell_menu}
856
895
  <CellMenu
857
- {i18n}
858
896
  x={active_cell_menu.x}
859
897
  y={active_cell_menu.y}
860
- row={active_cell_menu?.row ?? -1}
898
+ row={active_cell_menu.row}
861
899
  {col_count}
862
900
  {row_count}
863
- on_add_row_above={() => add_row_at(active_cell_menu?.row ?? -1, "above")}
864
- on_add_row_below={() => add_row_at(active_cell_menu?.row ?? -1, "below")}
865
- on_add_column_left={() => add_col_at(active_cell_menu?.col ?? -1, "left")}
866
- on_add_column_right={() => add_col_at(active_cell_menu?.col ?? -1, "right")}
901
+ on_add_row_above={() => add_row_at(active_cell_menu?.row || 0, "above")}
902
+ on_add_row_below={() => add_row_at(active_cell_menu?.row || 0, "below")}
903
+ on_add_column_left={() => add_col_at(active_cell_menu?.col || 0, "left")}
904
+ on_add_column_right={() => add_col_at(active_cell_menu?.col || 0, "right")}
905
+ on_delete_row={() => delete_row_at(active_cell_menu?.row || 0)}
906
+ on_delete_col={() => delete_col_at(active_cell_menu?.col || 0)}
907
+ can_delete_rows={data.length > 1}
908
+ can_delete_cols={data[0].length > 1}
909
+ {i18n}
867
910
  />
868
911
  {/if}
869
912
 
@@ -880,6 +923,10 @@ afterUpdate(() => {
880
923
  on_add_column_left={() => add_col_at(active_header_menu?.col ?? -1, "left")}
881
924
  on_add_column_right={() =>
882
925
  add_col_at(active_header_menu?.col ?? -1, "right")}
926
+ on_delete_row={() => delete_row_at(active_cell_menu?.row ?? -1)}
927
+ on_delete_col={() => delete_col_at(active_header_menu?.col ?? -1)}
928
+ can_delete_rows={false}
929
+ can_delete_cols={data[0].length > 1}
883
930
  />
884
931
  {/if}
885
932
 
@@ -28,6 +28,7 @@ declare const __propDef: {
28
28
  upload: Client["upload"];
29
29
  stream_handler: Client["stream"];
30
30
  show_fullscreen_button?: boolean | undefined;
31
+ show_copy_button?: boolean | undefined;
31
32
  value_is_output?: boolean | undefined;
32
33
  display_value?: (string[][] | null) | undefined;
33
34
  styling?: (string[][] | null) | undefined;
@@ -1,19 +1,59 @@
1
- <script>import { Maximize, Minimize } from "@gradio/icons";
1
+ <script>import { Maximize, Minimize, Copy, Check } from "@gradio/icons";
2
+ import { onDestroy } from "svelte";
2
3
  export let show_fullscreen_button = false;
4
+ export let show_copy_button = false;
3
5
  export let is_fullscreen = false;
6
+ export let on_copy;
7
+ let copied = false;
8
+ let timer;
9
+ function copy_feedback() {
10
+ copied = true;
11
+ if (timer)
12
+ clearTimeout(timer);
13
+ timer = setTimeout(() => {
14
+ copied = false;
15
+ }, 2e3);
16
+ }
17
+ async function handle_copy() {
18
+ await on_copy();
19
+ copy_feedback();
20
+ }
21
+ onDestroy(() => {
22
+ if (timer)
23
+ clearTimeout(timer);
24
+ });
4
25
  </script>
5
26
 
6
- {#if show_fullscreen_button}
7
- <div class="toolbar">
8
- <button class="toolbar-button" on:click>
27
+ <div class="toolbar" role="toolbar" aria-label="Table actions">
28
+ {#if show_copy_button}
29
+ <button
30
+ class="toolbar-button"
31
+ on:click={handle_copy}
32
+ aria-label={copied ? "Copied to clipboard" : "Copy table data"}
33
+ title={copied ? "Copied to clipboard" : "Copy table data"}
34
+ >
35
+ {#if copied}
36
+ <Check />
37
+ {:else}
38
+ <Copy />
39
+ {/if}
40
+ </button>
41
+ {/if}
42
+ {#if show_fullscreen_button}
43
+ <button
44
+ class="toolbar-button"
45
+ on:click
46
+ aria-label={is_fullscreen ? "Exit fullscreen" : "Enter fullscreen"}
47
+ title={is_fullscreen ? "Exit fullscreen" : "Enter fullscreen"}
48
+ >
9
49
  {#if is_fullscreen}
10
50
  <Minimize />
11
51
  {:else}
12
52
  <Maximize />
13
53
  {/if}
14
54
  </button>
15
- </div>
16
- {/if}
55
+ {/if}
56
+ </div>
17
57
 
18
58
  <style>
19
59
  .toolbar {
@@ -2,7 +2,9 @@ import { SvelteComponent } from "svelte";
2
2
  declare const __propDef: {
3
3
  props: {
4
4
  show_fullscreen_button?: boolean | undefined;
5
+ show_copy_button?: boolean | undefined;
5
6
  is_fullscreen?: boolean | undefined;
7
+ on_copy: () => Promise<void>;
6
8
  };
7
9
  events: {
8
10
  click: MouseEvent;
@@ -0,0 +1,6 @@
1
+ import type { HeadersWithIDs } from "./utils";
2
+ export type TableData = {
3
+ value: string | number;
4
+ id: string;
5
+ }[][];
6
+ export declare function copy_table_data(data: TableData, headers?: HeadersWithIDs): Promise<void>;
@@ -0,0 +1,27 @@
1
+ export async function copy_table_data(data, headers) {
2
+ const header_row = headers
3
+ ? headers.map((h) => String(h.value)).join(",")
4
+ : "";
5
+ const table_data = data
6
+ .map((row) => row.map((cell) => String(cell.value)).join(","))
7
+ .join("\n");
8
+ const all_data = header_row ? `${header_row}\n${table_data}` : table_data;
9
+ try {
10
+ if ("clipboard" in navigator) {
11
+ await navigator.clipboard.writeText(all_data);
12
+ }
13
+ else {
14
+ const textArea = document.createElement("textarea");
15
+ textArea.value = all_data;
16
+ textArea.style.position = "absolute";
17
+ textArea.style.left = "-999999px";
18
+ document.body.prepend(textArea);
19
+ textArea.select();
20
+ document.execCommand("copy");
21
+ textArea.remove();
22
+ }
23
+ }
24
+ catch (error) {
25
+ console.error("Failed to copy table data:", error);
26
+ }
27
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/dataframe",
3
- "version": "0.13.1",
3
+ "version": "0.14.0",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -17,14 +17,14 @@
17
17
  "dompurify": "^3.0.3",
18
18
  "katex": "^0.16.7",
19
19
  "marked": "^12.0.0",
20
- "@gradio/button": "^0.4.3",
21
- "@gradio/client": "^1.10.0",
22
20
  "@gradio/atoms": "^0.13.1",
21
+ "@gradio/button": "^0.4.4",
22
+ "@gradio/client": "^1.10.0",
23
23
  "@gradio/icons": "^0.10.0",
24
24
  "@gradio/statustracker": "^0.10.2",
25
- "@gradio/upload": "^0.14.7",
26
25
  "@gradio/markdown-code": "^0.3.0",
27
- "@gradio/utils": "^0.10.0"
26
+ "@gradio/utils": "^0.10.0",
27
+ "@gradio/upload": "^0.14.8"
28
28
  },
29
29
  "exports": {
30
30
  ".": {
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { onMount } from "svelte";
3
- import Arrow from "./Arrow.svelte";
3
+ import CellMenuIcons from "./CellMenuIcons.svelte";
4
4
  import type { I18nFormatter } from "js/utils/src";
5
5
 
6
6
  export let x: number;
@@ -12,6 +12,10 @@
12
12
  export let row: number;
13
13
  export let col_count: [number, "fixed" | "dynamic"];
14
14
  export let row_count: [number, "fixed" | "dynamic"];
15
+ export let on_delete_row: () => void;
16
+ export let on_delete_col: () => void;
17
+ export let can_delete_rows: boolean;
18
+ export let can_delete_cols: boolean;
15
19
 
16
20
  export let i18n: I18nFormatter;
17
21
  let menu_element: HTMLDivElement;
@@ -50,23 +54,35 @@
50
54
  <div bind:this={menu_element} class="cell-menu">
51
55
  {#if !is_header && can_add_rows}
52
56
  <button on:click={() => on_add_row_above()}>
53
- <Arrow transform="rotate(-90 12 12)" />
57
+ <CellMenuIcons icon="add-row-above" />
54
58
  {i18n("dataframe.add_row_above")}
55
59
  </button>
56
60
  <button on:click={() => on_add_row_below()}>
57
- <Arrow transform="rotate(90 12 12)" />
61
+ <CellMenuIcons icon="add-row-below" />
58
62
  {i18n("dataframe.add_row_below")}
59
63
  </button>
64
+ {#if can_delete_rows}
65
+ <button on:click={on_delete_row} class="delete">
66
+ <CellMenuIcons icon="delete-row" />
67
+ {i18n("dataframe.delete_row")}
68
+ </button>
69
+ {/if}
60
70
  {/if}
61
71
  {#if can_add_columns}
62
72
  <button on:click={() => on_add_column_left()}>
63
- <Arrow transform="rotate(180 12 12)" />
73
+ <CellMenuIcons icon="add-column-left" />
64
74
  {i18n("dataframe.add_column_left")}
65
75
  </button>
66
76
  <button on:click={() => on_add_column_right()}>
67
- <Arrow transform="rotate(0 12 12)" />
77
+ <CellMenuIcons icon="add-column-right" />
68
78
  {i18n("dataframe.add_column_right")}
69
79
  </button>
80
+ {#if can_delete_cols}
81
+ <button on:click={on_delete_col} class="delete">
82
+ <CellMenuIcons icon="delete-column" />
83
+ {i18n("dataframe.delete_column")}
84
+ </button>
85
+ {/if}
70
86
  {/if}
71
87
  </div>
72
88
 
@@ -110,8 +126,4 @@
110
126
  fill: currentColor;
111
127
  transition: fill 0.2s;
112
128
  }
113
-
114
- .cell-menu button:hover :global(svg) {
115
- fill: var(--color-accent);
116
- }
117
129
  </style>
@@ -0,0 +1,113 @@
1
+ <script lang="ts">
2
+ export let icon: string;
3
+ </script>
4
+
5
+ {#if icon == "add-column-right"}
6
+ <svg viewBox="0 0 24 24" width="16" height="16">
7
+ <rect
8
+ x="4"
9
+ y="6"
10
+ width="4"
11
+ height="12"
12
+ stroke="currentColor"
13
+ stroke-width="2"
14
+ fill="none"
15
+ />
16
+ <path
17
+ d="M12 12H19M16 8L19 12L16 16"
18
+ stroke="currentColor"
19
+ stroke-width="2"
20
+ fill="none"
21
+ stroke-linecap="round"
22
+ />
23
+ </svg>
24
+ {:else if icon == "add-column-left"}
25
+ <svg viewBox="0 0 24 24" width="16" height="16">
26
+ <rect
27
+ x="16"
28
+ y="6"
29
+ width="4"
30
+ height="12"
31
+ stroke="currentColor"
32
+ stroke-width="2"
33
+ fill="none"
34
+ />
35
+ <path
36
+ d="M12 12H5M8 8L5 12L8 16"
37
+ stroke="currentColor"
38
+ stroke-width="2"
39
+ fill="none"
40
+ stroke-linecap="round"
41
+ />
42
+ </svg>
43
+ {:else if icon == "add-row-above"}
44
+ <svg viewBox="0 0 24 24" width="16" height="16">
45
+ <rect
46
+ x="6"
47
+ y="16"
48
+ width="12"
49
+ height="4"
50
+ stroke="currentColor"
51
+ stroke-width="2"
52
+ />
53
+ <path
54
+ d="M12 12V5M8 8L12 5L16 8"
55
+ stroke="currentColor"
56
+ stroke-width="2"
57
+ fill="none"
58
+ stroke-linecap="round"
59
+ />
60
+ </svg>
61
+ {:else if icon == "add-row-below"}
62
+ <svg viewBox="0 0 24 24" width="16" height="16">
63
+ <rect
64
+ x="6"
65
+ y="4"
66
+ width="12"
67
+ height="4"
68
+ stroke="currentColor"
69
+ stroke-width="2"
70
+ />
71
+ <path
72
+ d="M12 12V19M8 16L12 19L16 16"
73
+ stroke="currentColor"
74
+ stroke-width="2"
75
+ fill="none"
76
+ stroke-linecap="round"
77
+ />
78
+ </svg>
79
+ {:else if icon == "delete-row"}
80
+ <svg viewBox="0 0 24 24" width="16" height="16">
81
+ <rect
82
+ x="5"
83
+ y="10"
84
+ width="14"
85
+ height="4"
86
+ stroke="currentColor"
87
+ stroke-width="2"
88
+ />
89
+ <path
90
+ d="M8 7L16 17M16 7L8 17"
91
+ stroke="currentColor"
92
+ stroke-width="2"
93
+ stroke-linecap="round"
94
+ />
95
+ </svg>
96
+ {:else if icon == "delete-column"}
97
+ <svg viewBox="0 0 24 24" width="16" height="16">
98
+ <rect
99
+ x="10"
100
+ y="5"
101
+ width="4"
102
+ height="14"
103
+ stroke="currentColor"
104
+ stroke-width="2"
105
+ />
106
+ <path
107
+ d="M7 8L17 16M17 8L7 16"
108
+ stroke="currentColor"
109
+ stroke-width="2"
110
+ stroke-linecap="round"
111
+ />
112
+ </svg>
113
+ {/if}
@@ -17,6 +17,7 @@
17
17
  } from "./utils";
18
18
  import CellMenu from "./CellMenu.svelte";
19
19
  import Toolbar from "./Toolbar.svelte";
20
+ import { copy_table_data } from "./table_utils";
20
21
 
21
22
  export let datatype: Datatype | Datatype[];
22
23
  export let label: string | null = null;
@@ -43,6 +44,7 @@
43
44
  export let upload: Client["upload"];
44
45
  export let stream_handler: Client["stream"];
45
46
  export let show_fullscreen_button = false;
47
+ export let show_copy_button = false;
46
48
  export let value_is_output = false;
47
49
 
48
50
  let selected: false | [number, number] = false;
@@ -120,13 +122,7 @@
120
122
  id: string;
121
123
  }[][] {
122
124
  const data_row_length = _values.length;
123
- return Array(
124
- row_count[1] === "fixed"
125
- ? row_count[0]
126
- : data_row_length < row_count[0]
127
- ? row_count[0]
128
- : data_row_length
129
- )
125
+ return Array(row_count[1] === "fixed" ? row_count[0] : data_row_length)
130
126
  .fill(0)
131
127
  .map((_, i) =>
132
128
  Array(
@@ -771,6 +767,10 @@
771
767
  is_fullscreen = !!document.fullscreenElement;
772
768
  }
773
769
 
770
+ async function handle_copy(): Promise<void> {
771
+ await copy_table_data(data, _headers);
772
+ }
773
+
774
774
  function toggle_header_menu(event: MouseEvent, col: number): void {
775
775
  event.stopPropagation();
776
776
  if (active_header_menu && active_header_menu.col === col) {
@@ -791,6 +791,42 @@
791
791
  afterUpdate(() => {
792
792
  value_is_output = false;
793
793
  });
794
+
795
+ async function delete_row(index: number): Promise<void> {
796
+ parent.focus();
797
+ if (row_count[1] !== "dynamic") return;
798
+ if (data.length <= 1) return;
799
+ data.splice(index, 1);
800
+ data = data;
801
+ selected = false;
802
+ }
803
+
804
+ async function delete_col(index: number): Promise<void> {
805
+ parent.focus();
806
+ if (col_count[1] !== "dynamic") return;
807
+ if (data[0].length <= 1) return;
808
+
809
+ _headers.splice(index, 1);
810
+ _headers = _headers;
811
+
812
+ data.forEach((row) => {
813
+ row.splice(index, 1);
814
+ });
815
+ data = data;
816
+ selected = false;
817
+ }
818
+
819
+ function delete_row_at(index: number): void {
820
+ delete_row(index);
821
+ active_cell_menu = null;
822
+ active_header_menu = null;
823
+ }
824
+
825
+ function delete_col_at(index: number): void {
826
+ delete_col(index);
827
+ active_cell_menu = null;
828
+ active_header_menu = null;
829
+ }
794
830
  </script>
795
831
 
796
832
  <svelte:window on:resize={() => set_cell_widths()} />
@@ -806,6 +842,8 @@
806
842
  {show_fullscreen_button}
807
843
  {is_fullscreen}
808
844
  on:click={toggle_fullscreen}
845
+ on_copy={handle_copy}
846
+ {show_copy_button}
809
847
  />
810
848
  </div>
811
849
  <div
@@ -1050,18 +1088,22 @@
1050
1088
  </div>
1051
1089
  </div>
1052
1090
 
1053
- {#if active_cell_menu !== null}
1091
+ {#if active_cell_menu}
1054
1092
  <CellMenu
1055
- {i18n}
1056
1093
  x={active_cell_menu.x}
1057
1094
  y={active_cell_menu.y}
1058
- row={active_cell_menu?.row ?? -1}
1095
+ row={active_cell_menu.row}
1059
1096
  {col_count}
1060
1097
  {row_count}
1061
- on_add_row_above={() => add_row_at(active_cell_menu?.row ?? -1, "above")}
1062
- on_add_row_below={() => add_row_at(active_cell_menu?.row ?? -1, "below")}
1063
- on_add_column_left={() => add_col_at(active_cell_menu?.col ?? -1, "left")}
1064
- on_add_column_right={() => add_col_at(active_cell_menu?.col ?? -1, "right")}
1098
+ on_add_row_above={() => add_row_at(active_cell_menu?.row || 0, "above")}
1099
+ on_add_row_below={() => add_row_at(active_cell_menu?.row || 0, "below")}
1100
+ on_add_column_left={() => add_col_at(active_cell_menu?.col || 0, "left")}
1101
+ on_add_column_right={() => add_col_at(active_cell_menu?.col || 0, "right")}
1102
+ on_delete_row={() => delete_row_at(active_cell_menu?.row || 0)}
1103
+ on_delete_col={() => delete_col_at(active_cell_menu?.col || 0)}
1104
+ can_delete_rows={data.length > 1}
1105
+ can_delete_cols={data[0].length > 1}
1106
+ {i18n}
1065
1107
  />
1066
1108
  {/if}
1067
1109
 
@@ -1078,6 +1120,10 @@
1078
1120
  on_add_column_left={() => add_col_at(active_header_menu?.col ?? -1, "left")}
1079
1121
  on_add_column_right={() =>
1080
1122
  add_col_at(active_header_menu?.col ?? -1, "right")}
1123
+ on_delete_row={() => delete_row_at(active_cell_menu?.row ?? -1)}
1124
+ on_delete_col={() => delete_col_at(active_header_menu?.col ?? -1)}
1125
+ can_delete_rows={false}
1126
+ can_delete_cols={data[0].length > 1}
1081
1127
  />
1082
1128
  {/if}
1083
1129
 
@@ -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,38 @@
1
+ import type { HeadersWithIDs } from "./utils";
2
+
3
+ export type TableData = {
4
+ value: string | number;
5
+ id: string;
6
+ }[][];
7
+
8
+ export async function copy_table_data(
9
+ data: TableData,
10
+ headers?: HeadersWithIDs
11
+ ): Promise<void> {
12
+ const header_row = headers
13
+ ? headers.map((h) => String(h.value)).join(",")
14
+ : "";
15
+ const table_data = data
16
+ .map((row) => row.map((cell) => String(cell.value)).join(","))
17
+ .join("\n");
18
+
19
+ const all_data = header_row ? `${header_row}\n${table_data}` : table_data;
20
+
21
+ try {
22
+ if ("clipboard" in navigator) {
23
+ await navigator.clipboard.writeText(all_data);
24
+ } else {
25
+ const textArea = document.createElement("textarea");
26
+ textArea.value = all_data;
27
+ textArea.style.position = "absolute";
28
+ textArea.style.left = "-999999px";
29
+ document.body.prepend(textArea);
30
+ textArea.select();
31
+
32
+ document.execCommand("copy");
33
+ textArea.remove();
34
+ }
35
+ } catch (error) {
36
+ console.error("Failed to copy table data:", error);
37
+ }
38
+ }
@@ -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>