@gradio/dataframe 0.12.7 → 0.13.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,8 +1,7 @@
1
1
  <script lang="ts">
2
- import { createEventDispatcher, tick, onMount } from "svelte";
2
+ import { afterUpdate, createEventDispatcher, tick, onMount } from "svelte";
3
3
  import { dsvFormat } from "d3-dsv";
4
4
  import { dequal } from "dequal/lite";
5
- import { copy } from "@gradio/utils";
6
5
  import { Upload } from "@gradio/upload";
7
6
 
8
7
  import EditableCell from "./EditableCell.svelte";
@@ -10,8 +9,14 @@
10
9
  import type { I18nFormatter } from "js/core/src/gradio_helper";
11
10
  import { type Client } from "@gradio/client";
12
11
  import VirtualTable from "./VirtualTable.svelte";
13
- import type { Headers, HeadersWithIDs, Metadata, Datatype } from "./utils";
12
+ import type {
13
+ Headers,
14
+ HeadersWithIDs,
15
+ DataframeValue,
16
+ Datatype
17
+ } from "./utils";
14
18
  import CellMenu from "./CellMenu.svelte";
19
+ import Toolbar from "./Toolbar.svelte";
15
20
 
16
21
  export let datatype: Datatype | Datatype[];
17
22
  export let label: string | null = null;
@@ -34,20 +39,21 @@
34
39
  export let max_height = 500;
35
40
  export let line_breaks = true;
36
41
  export let column_widths: string[] = [];
42
+ export let show_row_numbers = false;
37
43
  export let upload: Client["upload"];
38
44
  export let stream_handler: Client["stream"];
45
+ export let show_fullscreen_button = false;
46
+ export let value_is_output = false;
39
47
 
40
48
  let selected: false | [number, number] = false;
49
+ let clicked_cell: { row: number; col: number } | undefined = undefined;
41
50
  export let display_value: string[][] | null = null;
42
51
  export let styling: string[][] | null = null;
43
52
  let t_rect: DOMRectReadOnly;
44
53
 
45
54
  const dispatch = createEventDispatcher<{
46
- change: {
47
- data: (string | number)[][];
48
- headers: string[];
49
- metadata: Metadata;
50
- };
55
+ change: DataframeValue;
56
+ input: undefined;
51
57
  select: SelectData;
52
58
  }>();
53
59
 
@@ -142,38 +148,49 @@
142
148
  }
143
149
 
144
150
  let _headers = make_headers(headers);
145
- let old_headers: string[] | undefined;
151
+ let old_headers: string[] = headers;
146
152
 
147
153
  $: {
148
154
  if (!dequal(headers, old_headers)) {
149
- trigger_headers();
155
+ _headers = make_headers(headers);
156
+ old_headers = JSON.parse(JSON.stringify(headers));
150
157
  }
151
158
  }
152
159
 
153
- function trigger_headers(): void {
154
- _headers = make_headers(headers);
155
-
156
- old_headers = headers.slice();
157
- trigger_change();
158
- }
160
+ let data: { id: string; value: string | number }[][] = [[]];
161
+ let old_val: undefined | (string | number)[][] = undefined;
159
162
 
160
163
  $: if (!dequal(values, old_val)) {
161
164
  data = process_data(values as (string | number)[][]);
162
- old_val = values as (string | number)[][];
165
+ old_val = JSON.parse(JSON.stringify(values)) as (string | number)[][];
163
166
  }
164
167
 
165
- let data: { id: string; value: string | number }[][] = [[]];
166
-
167
- let old_val: undefined | (string | number)[][] = undefined;
168
+ let previous_headers = _headers.map((h) => h.value);
169
+ let previous_data = data.map((row) => row.map((cell) => String(cell.value)));
168
170
 
169
171
  async function trigger_change(): Promise<void> {
170
- dispatch("change", {
171
- data: data.map((r) => r.map(({ value }) => value)),
172
- headers: _headers.map((h) => h.value),
173
- metadata: editable
174
- ? null
175
- : { display_value: display_value, styling: styling }
176
- });
172
+ const current_headers = _headers.map((h) => h.value);
173
+ const current_data = data.map((row) =>
174
+ row.map((cell) => String(cell.value))
175
+ );
176
+
177
+ if (
178
+ !dequal(current_data, previous_data) ||
179
+ !dequal(current_headers, previous_headers)
180
+ ) {
181
+ // We dispatch the value as part of the change event to ensure that the value is updated
182
+ // in the parent component and the updated value is passed into the user's function
183
+ dispatch("change", {
184
+ data: data.map((row) => row.map((cell) => cell.value)),
185
+ headers: _headers.map((h) => h.value),
186
+ metadata: null
187
+ });
188
+ if (!value_is_output) {
189
+ dispatch("input");
190
+ }
191
+ previous_data = current_data;
192
+ previous_headers = current_headers;
193
+ }
177
194
  }
178
195
 
179
196
  function get_sort_status(
@@ -204,12 +221,6 @@
204
221
  );
205
222
  }
206
223
 
207
- async function start_edit(i: number, j: number): Promise<void> {
208
- if (!editable || dequal(editing, [i, j])) return;
209
-
210
- editing = [i, j];
211
- }
212
-
213
224
  function move_cursor(
214
225
  key: "ArrowRight" | "ArrowLeft" | "ArrowDown" | "ArrowUp",
215
226
  current_coords: [number, number]
@@ -349,25 +360,6 @@
349
360
  }
350
361
  }
351
362
 
352
- let active_cell: { row: number; col: number } | null = null;
353
-
354
- async function handle_cell_click(i: number, j: number): Promise<void> {
355
- if (active_cell && active_cell.row === i && active_cell.col === j) {
356
- active_cell = null;
357
- } else {
358
- active_cell = { row: i, col: j };
359
- }
360
- if (dequal(editing, [i, j])) return;
361
- header_edit = false;
362
- selected_header = false;
363
- editing = false;
364
- if (!dequal(selected, [i, j])) {
365
- selected = [i, j];
366
- await tick();
367
- parent.focus();
368
- }
369
- }
370
-
371
363
  type SortDirection = "asc" | "des";
372
364
  let sort_direction: SortDirection | undefined;
373
365
  let sort_by: number | undefined;
@@ -440,7 +432,7 @@
440
432
  selected = [index !== undefined ? index : data.length - 1, 0];
441
433
  }
442
434
 
443
- $: (data || selected_header) && trigger_change();
435
+ $: (data || _headers) && trigger_change();
444
436
 
445
437
  async function add_col(index?: number): Promise<void> {
446
438
  parent.focus();
@@ -479,17 +471,16 @@
479
471
  active_header_menu = null;
480
472
  }
481
473
 
482
- event.stopImmediatePropagation();
483
474
  const [trigger] = event.composedPath() as HTMLElement[];
484
475
  if (parent.contains(trigger)) {
485
476
  return;
486
477
  }
487
478
 
479
+ clicked_cell = undefined;
488
480
  editing = false;
481
+ selected = false;
489
482
  header_edit = false;
490
483
  selected_header = false;
491
- reset_selection();
492
- active_cell = null;
493
484
  active_cell_menu = null;
494
485
  active_header_menu = null;
495
486
  }
@@ -560,6 +551,7 @@
560
551
  function get_max(
561
552
  _d: { value: any; id: string }[][]
562
553
  ): { value: any; id: string }[] {
554
+ if (!_d || _d.length === 0 || !_d[0]) return [];
563
555
  let max = _d[0].slice();
564
556
  for (let i = 0; i < _d.length; i++) {
565
557
  for (let j = 0; j < _d[i].length; j++) {
@@ -665,8 +657,18 @@
665
657
 
666
658
  observer.observe(parent);
667
659
 
660
+ document.addEventListener("click", handle_click_outside);
661
+ window.addEventListener("resize", handle_resize);
662
+ document.addEventListener("fullscreenchange", handle_fullscreen_change);
663
+
668
664
  return () => {
669
665
  observer.disconnect();
666
+ document.removeEventListener("click", handle_click_outside);
667
+ window.removeEventListener("resize", handle_resize);
668
+ document.removeEventListener(
669
+ "fullscreenchange",
670
+ handle_fullscreen_change
671
+ );
670
672
  };
671
673
  });
672
674
 
@@ -721,15 +723,6 @@
721
723
  set_cell_widths();
722
724
  }
723
725
 
724
- onMount(() => {
725
- document.addEventListener("click", handle_click_outside);
726
- window.addEventListener("resize", handle_resize);
727
- return () => {
728
- document.removeEventListener("click", handle_click_outside);
729
- window.removeEventListener("resize", handle_resize);
730
- };
731
- });
732
-
733
726
  let active_button: {
734
727
  type: "header" | "cell";
735
728
  row?: number;
@@ -762,6 +755,22 @@
762
755
  y: number;
763
756
  } | null = null;
764
757
 
758
+ let is_fullscreen = false;
759
+
760
+ function toggle_fullscreen(): void {
761
+ if (!document.fullscreenElement) {
762
+ parent.requestFullscreen();
763
+ is_fullscreen = true;
764
+ } else {
765
+ document.exitFullscreen();
766
+ is_fullscreen = false;
767
+ }
768
+ }
769
+
770
+ function handle_fullscreen_change(): void {
771
+ is_fullscreen = !!document.fullscreenElement;
772
+ }
773
+
765
774
  function toggle_header_menu(event: MouseEvent, col: number): void {
766
775
  event.stopPropagation();
767
776
  if (active_header_menu && active_header_menu.col === col) {
@@ -779,24 +788,26 @@
779
788
  }
780
789
  }
781
790
 
782
- function reset_selection(): void {
783
- selected = false;
784
- last_selected = null;
785
- }
791
+ afterUpdate(() => {
792
+ value_is_output = false;
793
+ });
786
794
  </script>
787
795
 
788
- <svelte:window
789
- on:click={handle_click_outside}
790
- on:touchstart={handle_click_outside}
791
- on:resize={() => set_cell_widths()}
792
- />
793
-
794
- <div class:label={label && label.length !== 0} use:copy>
795
- {#if label && label.length !== 0 && show_label}
796
- <p>
797
- {label}
798
- </p>
799
- {/if}
796
+ <svelte:window on:resize={() => set_cell_widths()} />
797
+
798
+ <div class="table-container">
799
+ <div class="header-row">
800
+ {#if label && label.length !== 0 && show_label}
801
+ <div class="label">
802
+ <p>{label}</p>
803
+ </div>
804
+ {/if}
805
+ <Toolbar
806
+ {show_fullscreen_button}
807
+ {is_fullscreen}
808
+ on:click={toggle_fullscreen}
809
+ />
810
+ </div>
800
811
  <div
801
812
  bind:this={parent}
802
813
  class="table-wrap"
@@ -817,6 +828,9 @@
817
828
  {/if}
818
829
  <thead>
819
830
  <tr>
831
+ {#if show_row_numbers}
832
+ <th class="row-number-header"></th>
833
+ {/if}
820
834
  {#each _headers as { value, id }, i (id)}
821
835
  <th
822
836
  class:editing={header_edit === i}
@@ -896,6 +910,9 @@
896
910
  <caption class="sr-only">{label}</caption>
897
911
  {/if}
898
912
  <tr slot="thead">
913
+ {#if show_row_numbers}
914
+ <th class="row-number-header"></th>
915
+ {/if}
899
916
  {#each _headers as { value, id }, i (id)}
900
917
  <th
901
918
  class:focus={header_edit === i || selected_header === i}
@@ -915,17 +932,14 @@
915
932
  edit={header_edit === i}
916
933
  on:keydown={end_header_edit}
917
934
  on:dblclick={() => edit_header(i)}
918
- {select_on_focus}
919
935
  header
920
936
  {root}
921
937
  />
922
- <!-- TODO: fix -->
923
- <!-- svelte-ignore a11y-click-events-have-key-events -->
924
- <!-- svelte-ignore a11y-no-static-element-interactions-->
925
- <div
938
+ <button
926
939
  class:sorted={sort_by === i}
927
940
  class:des={sort_by === i && sort_direction === "des"}
928
941
  class="sort-button {sort_direction}"
942
+ tabindex="0"
929
943
  on:click={(event) => {
930
944
  event.stopPropagation();
931
945
  handle_sort(i);
@@ -940,7 +954,7 @@
940
954
  >
941
955
  <path d="M4.49999 0L8.3971 6.75H0.602875L4.49999 0Z" />
942
956
  </svg>
943
- </div>
957
+ </button>
944
958
  </div>
945
959
 
946
960
  {#if editable}
@@ -957,15 +971,44 @@
957
971
  </tr>
958
972
 
959
973
  <tr slot="tbody" let:item let:index class:row_odd={index % 2 === 0}>
974
+ {#if show_row_numbers}
975
+ <td class="row-number" title={`Row ${index + 1}`}>{index + 1}</td>
976
+ {/if}
960
977
  {#each item as { value, id }, j (id)}
961
978
  <td
962
979
  tabindex="0"
963
- on:touchstart={() => start_edit(index, j)}
964
- on:click={() => {
965
- handle_cell_click(index, j);
980
+ on:touchstart={(event) => {
981
+ event.preventDefault();
982
+ event.stopPropagation();
983
+ clear_on_focus = false;
984
+ clicked_cell = { row: index, col: j };
985
+ selected = [index, j];
986
+ selected_header = false;
987
+ header_edit = false;
988
+ if (editable) {
989
+ editing = [index, j];
990
+ }
991
+ toggle_cell_button(index, j);
992
+ }}
993
+ on:mousedown={(event) => {
994
+ event.preventDefault();
995
+ event.stopPropagation();
996
+ }}
997
+ on:click={(event) => {
998
+ event.preventDefault();
999
+ event.stopPropagation();
1000
+ clear_on_focus = false;
1001
+ active_cell_menu = null;
1002
+ active_header_menu = null;
1003
+ clicked_cell = { row: index, col: j };
1004
+ selected = [index, j];
1005
+ selected_header = false;
1006
+ header_edit = false;
1007
+ if (editable) {
1008
+ editing = [index, j];
1009
+ }
966
1010
  toggle_cell_button(index, j);
967
1011
  }}
968
- on:dblclick={() => start_edit(index, j)}
969
1012
  style:width="var(--cell-width-{j})"
970
1013
  style={styling?.[index]?.[j] || ""}
971
1014
  class:focus={dequal(selected, [index, j])}
@@ -983,7 +1026,10 @@
983
1026
  {editable}
984
1027
  edit={dequal(editing, [index, j])}
985
1028
  datatype={Array.isArray(datatype) ? datatype[j] : datatype}
986
- on:blur={() => ((clear_on_focus = false), parent.focus())}
1029
+ on:blur={() => {
1030
+ clear_on_focus = false;
1031
+ parent.focus();
1032
+ }}
987
1033
  {clear_on_focus}
988
1034
  {root}
989
1035
  />
@@ -1250,4 +1296,60 @@
1250
1296
  overflow-wrap: break-word;
1251
1297
  word-break: break-word;
1252
1298
  }
1299
+
1300
+ .table-container {
1301
+ display: flex;
1302
+ flex-direction: column;
1303
+ gap: var(--size-2);
1304
+ }
1305
+
1306
+ .row-number,
1307
+ .row-number-header {
1308
+ width: var(--size-7);
1309
+ min-width: var(--size-7);
1310
+ text-align: center;
1311
+ background: var(--table-even-background-fill);
1312
+ position: sticky;
1313
+ left: 0;
1314
+ font-size: var(--input-text-size);
1315
+ color: var(--body-text-color);
1316
+ padding: var(--size-1) var(--size-2);
1317
+ overflow: hidden;
1318
+ text-overflow: ellipsis;
1319
+ white-space: nowrap;
1320
+ font-weight: var(--weight-semibold);
1321
+ }
1322
+
1323
+ .row-number-header {
1324
+ z-index: var(--layer-2);
1325
+ }
1326
+
1327
+ .row-number {
1328
+ z-index: var(--layer-1);
1329
+ }
1330
+
1331
+ :global(tbody > tr:nth-child(odd)) .row-number {
1332
+ background: var(--table-odd-background-fill);
1333
+ }
1334
+
1335
+ .header-row {
1336
+ display: flex;
1337
+ justify-content: space-between;
1338
+ align-items: center;
1339
+ gap: var(--size-2);
1340
+ height: var(--size-6);
1341
+ min-height: var(--size-6);
1342
+ }
1343
+
1344
+ .label {
1345
+ flex: 1;
1346
+ }
1347
+
1348
+ .label p {
1349
+ position: relative;
1350
+ z-index: var(--layer-4);
1351
+ margin: 0;
1352
+ color: var(--block-label-text-color);
1353
+ font-size: var(--block-label-text-size);
1354
+ }
1253
1355
  </style>
@@ -0,0 +1,51 @@
1
+ <script lang="ts">
2
+ import { Maximize, Minimize } from "@gradio/icons";
3
+
4
+ export let show_fullscreen_button = false;
5
+ export let is_fullscreen = false;
6
+ </script>
7
+
8
+ {#if show_fullscreen_button}
9
+ <div class="toolbar">
10
+ <button class="toolbar-button" on:click>
11
+ {#if is_fullscreen}
12
+ <Minimize />
13
+ {:else}
14
+ <Maximize />
15
+ {/if}
16
+ </button>
17
+ </div>
18
+ {/if}
19
+
20
+ <style>
21
+ .toolbar {
22
+ display: flex;
23
+ justify-content: flex-end;
24
+ gap: var(--size-1);
25
+ }
26
+
27
+ .toolbar-button {
28
+ display: flex;
29
+ align-items: center;
30
+ justify-content: center;
31
+ width: var(--size-6);
32
+ height: var(--size-6);
33
+ padding: var(--size-1);
34
+ border: none;
35
+ border-radius: var(--radius-sm);
36
+ background: transparent;
37
+ color: var(--body-text-color-subdued);
38
+ cursor: pointer;
39
+ transition: all 0.2s;
40
+ }
41
+
42
+ .toolbar-button:hover {
43
+ background: var(--background-fill-secondary);
44
+ color: var(--body-text-color);
45
+ }
46
+
47
+ .toolbar-button :global(svg) {
48
+ width: var(--size-4);
49
+ height: var(--size-4);
50
+ }
51
+ </style>
package/shared/utils.ts CHANGED
@@ -5,3 +5,8 @@ export type Metadata = {
5
5
  [key: string]: string[][] | null;
6
6
  } | null;
7
7
  export type HeadersWithIDs = { value: string; id: string }[];
8
+ export type DataframeValue = {
9
+ data: Data;
10
+ headers: Headers;
11
+ metadata: Metadata;
12
+ };