@gradio/dataframe 0.17.14 → 0.17.16

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,32 @@
1
1
  # @gradio/dataframe
2
2
 
3
+ ## 0.17.16
4
+
5
+ ### Fixes
6
+
7
+ - [#11349](https://github.com/gradio-app/gradio/pull/11349) [`bed858b`](https://github.com/gradio-app/gradio/commit/bed858b62b9887e606618bea5fe8a2f212b4a645) - Fix DataFrame Scroll Divergence. Thanks @deckar01!
8
+ - [#11346](https://github.com/gradio-app/gradio/pull/11346) [`3a9a002`](https://github.com/gradio-app/gradio/commit/3a9a0025efd49942303517c5dbd426d41241b7f6) - Hide native Dataframe to screen readers. Thanks @hannahblair!
9
+
10
+ ### Dependency updates
11
+
12
+ - @gradio/markdown-code@0.4.3
13
+ - @gradio/statustracker@0.10.12
14
+ - @gradio/button@0.5.3
15
+ - @gradio/checkbox@0.4.23
16
+
17
+ ## 0.17.15
18
+
19
+ ### Features
20
+
21
+ - [#11237](https://github.com/gradio-app/gradio/pull/11237) [`a6f6b40`](https://github.com/gradio-app/gradio/commit/a6f6b40dda5194fa5bc9926ef67f2a75f503e9a4) - Enhance boolean cell types in `gr.Dataframe`. Thanks @hannahblair!
22
+
23
+ ### Dependency updates
24
+
25
+ - @gradio/upload@0.16.7
26
+ - @gradio/checkbox@0.4.23
27
+ - @gradio/client@1.15.2
28
+ - @gradio/button@0.5.3
29
+
3
30
  ## 0.17.14
4
31
 
5
32
  ### Fixes
@@ -60,11 +60,12 @@
60
60
  name="Interactive dataframe with label"
61
61
  args={{
62
62
  values: [
63
- ["Cat", 5],
64
- ["Horse", 3],
65
- ["Snake", 1]
63
+ ["Cat", 5, true],
64
+ ["Horse", 3, false],
65
+ ["Snake", 1, false]
66
66
  ],
67
- headers: ["Animal", "Votes"],
67
+ headers: ["Animal", "Votes", "Is Pet"],
68
+ datatype: ["str", "number", "bool"],
68
69
  label: "Animals",
69
70
  show_label: true,
70
71
  col_count: [2, "dynamic"],
@@ -93,14 +94,14 @@
93
94
  name="Static dataframe"
94
95
  args={{
95
96
  values: [
96
- ["Cat", 5],
97
- ["Horse", 3],
98
- ["Snake", 1]
97
+ ["Cat", 5, true],
98
+ ["Horse", 3, false],
99
+ ["Snake", 1, false]
99
100
  ],
100
- headers: ["Animal", "Votes"],
101
-
101
+ headers: ["Animal", "Votes", "Is Pet"],
102
+ datatype: ["str", "number", "bool"],
102
103
  label: "Animals",
103
- col_count: [2, "dynamic"],
104
+ col_count: [3, "dynamic"],
104
105
  row_count: [3, "dynamic"],
105
106
  editable: false
106
107
  }}
@@ -0,0 +1,62 @@
1
+ <script>import { BaseCheckbox } from "@gradio/checkbox";
2
+ export let value = false;
3
+ export let editable = true;
4
+ export let on_change;
5
+ $:
6
+ bool_value = typeof value === "string" ? value.toLowerCase() === "true" : !!value;
7
+ function handle_change(event) {
8
+ on_change(event.detail);
9
+ }
10
+ function handle_click(event) {
11
+ event.stopPropagation();
12
+ }
13
+ function handle_keydown(event) {
14
+ if (event.key === "Enter" || event.key === " ") {
15
+ event.stopPropagation();
16
+ }
17
+ }
18
+ </script>
19
+
20
+ <div
21
+ class="bool-cell checkbox"
22
+ on:click={handle_click}
23
+ on:keydown={handle_keydown}
24
+ role="button"
25
+ tabindex="-1"
26
+ >
27
+ <BaseCheckbox
28
+ bind:value={bool_value}
29
+ label=""
30
+ interactive={editable}
31
+ on:change={handle_change}
32
+ />
33
+ </div>
34
+
35
+ <style>
36
+ .bool-cell {
37
+ display: flex;
38
+ align-items: center;
39
+ justify-content: center;
40
+ width: var(--size-full);
41
+ height: var(--size-full);
42
+ }
43
+ .bool-cell :global(input:disabled) {
44
+ opacity: 0.8;
45
+ }
46
+
47
+ .bool-cell.checkbox {
48
+ justify-content: center;
49
+ }
50
+
51
+ .bool-cell :global(label) {
52
+ margin: 0;
53
+ width: 100%;
54
+ display: flex;
55
+ justify-content: center;
56
+ align-items: center;
57
+ }
58
+
59
+ .bool-cell :global(span) {
60
+ display: none;
61
+ }
62
+ </style>
@@ -0,0 +1,18 @@
1
+ import { SvelteComponent } from "svelte";
2
+ declare const __propDef: {
3
+ props: {
4
+ value?: (boolean | string) | undefined;
5
+ editable?: boolean | undefined;
6
+ on_change: (value: boolean) => void;
7
+ };
8
+ events: {
9
+ [evt: string]: CustomEvent<any>;
10
+ };
11
+ slots: {};
12
+ };
13
+ export type BooleanCellProps = typeof __propDef.props;
14
+ export type BooleanCellEvents = typeof __propDef.events;
15
+ export type BooleanCellSlots = typeof __propDef.slots;
16
+ export default class BooleanCell extends SvelteComponent<BooleanCellProps, BooleanCellEvents, BooleanCellSlots> {
17
+ }
18
+ export {};
@@ -1,6 +1,7 @@
1
1
  <script>import { createEventDispatcher } from "svelte";
2
2
  import { MarkdownCode } from "@gradio/markdown-code";
3
3
  import SelectionButtons from "./icons/SelectionButtons.svelte";
4
+ import BooleanCell from "./BooleanCell.svelte";
4
5
  export let edit;
5
6
  export let value = "";
6
7
  export let display_value = null;
@@ -21,9 +22,9 @@ export let show_selection_buttons = false;
21
22
  export let coords;
22
23
  export let on_select_column = null;
23
24
  export let on_select_row = null;
25
+ export let el;
24
26
  const dispatch = createEventDispatcher();
25
27
  let is_expanded = false;
26
- export let el;
27
28
  function truncate_text(text, max_length = null, is_image = false) {
28
29
  if (is_image)
29
30
  return String(text);
@@ -65,9 +66,22 @@ function handle_click() {
65
66
  is_expanded = !is_expanded;
66
67
  }
67
68
  }
69
+ function handle_bool_change(new_value) {
70
+ value = new_value.toString();
71
+ dispatch("blur", {
72
+ blur_event: {
73
+ target: {
74
+ type: "checkbox",
75
+ checked: new_value,
76
+ value: new_value.toString()
77
+ }
78
+ },
79
+ coords
80
+ });
81
+ }
68
82
  </script>
69
83
 
70
- {#if edit}
84
+ {#if edit && datatype !== "bool"}
71
85
  <input
72
86
  readonly={is_static}
73
87
  aria-readonly={is_static}
@@ -86,48 +100,57 @@ function handle_click() {
86
100
  />
87
101
  {/if}
88
102
 
89
- <span
90
- class:dragging={is_dragging}
91
- on:click={handle_click}
92
- on:keydown={handle_keydown}
93
- tabindex="0"
94
- role="button"
95
- class:edit
96
- class:expanded={is_expanded}
97
- class:multiline={header}
98
- on:focus|preventDefault
99
- style={styling}
100
- data-editable={editable}
101
- data-max-chars={max_chars}
102
- data-expanded={is_expanded}
103
- placeholder=" "
104
- class:text={datatype === "str"}
105
- class:wrap={wrap_text}
106
- >
107
- {#if datatype === "image" && components.image}
108
- <svelte:component
109
- this={components.image}
110
- value={{ url: display_text }}
111
- show_label={false}
112
- label="cell-image"
113
- show_download_button={false}
114
- {i18n}
115
- gradio={{ dispatch: () => {} }}
116
- />
117
- {:else if datatype === "html"}
118
- {@html display_text}
119
- {:else if datatype === "markdown"}
120
- <MarkdownCode
121
- message={display_text.toLocaleString()}
122
- {latex_delimiters}
123
- {line_breaks}
124
- chatbot={false}
125
- {root}
126
- />
127
- {:else}
128
- {display_text}
129
- {/if}
130
- </span>
103
+ {#if datatype === "bool"}
104
+ <BooleanCell
105
+ value={String(display_content)}
106
+ {editable}
107
+ on_change={handle_bool_change}
108
+ />
109
+ {:else}
110
+ <span
111
+ class:dragging={is_dragging}
112
+ on:click={handle_click}
113
+ on:keydown={handle_keydown}
114
+ tabindex="0"
115
+ role="button"
116
+ class:edit
117
+ class:expanded={is_expanded}
118
+ class:multiline={header}
119
+ on:focus|preventDefault
120
+ style={styling}
121
+ data-editable={editable}
122
+ data-max-chars={max_chars}
123
+ data-expanded={is_expanded}
124
+ placeholder=" "
125
+ class:text={datatype === "str"}
126
+ class:wrap={wrap_text}
127
+ >
128
+ {#if datatype === "image" && components.image}
129
+ <svelte:component
130
+ this={components.image}
131
+ value={{ url: display_text }}
132
+ show_label={false}
133
+ label="cell-image"
134
+ show_download_button={false}
135
+ {i18n}
136
+ gradio={{ dispatch: () => {} }}
137
+ />
138
+ {:else if datatype === "html"}
139
+ {@html display_text}
140
+ {:else if datatype === "markdown"}
141
+ <MarkdownCode
142
+ message={display_text.toLocaleString()}
143
+ {latex_delimiters}
144
+ {line_breaks}
145
+ chatbot={false}
146
+ {root}
147
+ />
148
+ {:else}
149
+ {display_text}
150
+ {/if}
151
+ </span>
152
+ {/if}
153
+
131
154
  {#if show_selection_buttons && coords && on_select_column && on_select_row}
132
155
  <SelectionButtons
133
156
  position="column"
@@ -103,7 +103,7 @@ onMount(() => {
103
103
  const observer = new IntersectionObserver((entries) => {
104
104
  entries.forEach((entry) => {
105
105
  if (entry.isIntersecting && !is_visible) {
106
- set_cell_widths();
106
+ width_calculated = false;
107
107
  }
108
108
  is_visible = entry.isIntersecting;
109
109
  });
@@ -111,10 +111,17 @@ onMount(() => {
111
111
  observer.observe(parent);
112
112
  document.addEventListener("click", handle_click_outside);
113
113
  window.addEventListener("resize", handle_resize);
114
+ const global_mouse_up = (event) => {
115
+ if (is_dragging || drag_start) {
116
+ handle_mouse_up(event);
117
+ }
118
+ };
119
+ document.addEventListener("mouseup", global_mouse_up);
114
120
  return () => {
115
121
  observer.disconnect();
116
122
  document.removeEventListener("click", handle_click_outside);
117
123
  window.removeEventListener("resize", handle_resize);
124
+ document.removeEventListener("mouseup", global_mouse_up);
118
125
  };
119
126
  });
120
127
  $: {
@@ -170,6 +177,7 @@ $:
170
177
  }
171
178
  last_width_data_length = 0;
172
179
  last_width_column_count = 0;
180
+ width_calculated = false;
173
181
  }
174
182
  }
175
183
  const is_reset = values.length === 0 || values.length === 1 && values[0].length === 0;
@@ -193,8 +201,8 @@ $:
193
201
  if ($df_state.current_search_query) {
194
202
  df_actions.handle_search(null);
195
203
  }
196
- if (parent && cells.length > 0) {
197
- set_cell_widths();
204
+ if (parent && cells.length > 0 && (is_reset || is_different_structure)) {
205
+ width_calculated = false;
198
206
  }
199
207
  }
200
208
  $:
@@ -328,8 +336,18 @@ function handle_click_outside(event) {
328
336
  }
329
337
  $:
330
338
  max = get_max(data);
339
+ let width_calc_timeout;
331
340
  $:
332
- cells[0] && cells[0]?.clientWidth && set_cell_widths();
341
+ if (cells[0] && cells[0]?.clientWidth) {
342
+ clearTimeout(width_calc_timeout);
343
+ width_calc_timeout = setTimeout(() => set_cell_widths(), 100);
344
+ }
345
+ let width_calculated = false;
346
+ $:
347
+ if (cells[0] && !width_calculated) {
348
+ set_cell_widths();
349
+ width_calculated = true;
350
+ }
333
351
  let cells = [];
334
352
  let parent;
335
353
  let table;
@@ -512,6 +530,7 @@ function handle_resize() {
512
530
  selected_cells = [];
513
531
  selected = false;
514
532
  editing = false;
533
+ width_calculated = false;
515
534
  set_cell_widths();
516
535
  }
517
536
  function add_row_at(index, position) {
@@ -615,7 +634,7 @@ function get_cell_display_value(row, col) {
615
634
  role="grid"
616
635
  tabindex="0"
617
636
  >
618
- <table bind:this={table}>
637
+ <table bind:this={table} aria-hidden="true">
619
638
  {#if label && label.length !== 0}
620
639
  <caption class="sr-only">{label}</caption>
621
640
  {/if}
@@ -28,53 +28,64 @@ $:
28
28
  viewport_height = viewport_box?.height || 200;
29
29
  const is_browser = typeof window !== "undefined";
30
30
  const raf = is_browser ? window.requestAnimationFrame : (cb) => cb();
31
- $:
32
- mounted && raf(() => refresh_height_map(sortedItems));
33
- let content_height = 0;
34
- async function refresh_height_map(_items) {
35
- if (viewport_height === 0) {
36
- return;
31
+ $: {
32
+ if (mounted && viewport_height && viewport.offsetParent) {
33
+ sortedItems, raf(refresh_height_map);
37
34
  }
38
- head_height = viewport.querySelector(".thead")?.getBoundingClientRect().height || 0;
39
- await tick();
40
- const { scrollTop } = viewport;
35
+ }
36
+ async function refresh_height_map() {
37
+ if (sortedItems.length < start) {
38
+ await scroll_to_index(sortedItems.length - 1, { behavior: "auto" });
39
+ }
40
+ const scrollTop = Math.max(0, viewport.scrollTop);
41
+ show_scroll_button = scrollTop > 100;
41
42
  table_scrollbar_width = viewport.offsetWidth - viewport.clientWidth;
42
- content_height = top - (scrollTop - head_height);
43
- let i = start;
44
- while (content_height < max_height && i < _items.length) {
45
- let row = rows[i - start];
46
- if (!row) {
47
- end = i + 1;
48
- await tick();
49
- row = rows[i - start];
50
- }
51
- let _h = row?.getBoundingClientRect().height;
52
- if (!_h) {
53
- _h = average_height;
43
+ for (let v = 0; v < rows.length; v += 1) {
44
+ height_map[start + v] = rows[v].getBoundingClientRect().height;
45
+ }
46
+ let i = 0;
47
+ let y = head_height;
48
+ while (i < sortedItems.length) {
49
+ const row_height = height_map[i] || average_height;
50
+ if (y + row_height > scrollTop - max_height) {
51
+ start = i;
52
+ top = y - head_height;
53
+ break;
54
54
  }
55
- const row_height = height_map[i] = _h;
55
+ y += row_height;
56
+ i += 1;
57
+ }
58
+ let content_height = head_height;
59
+ while (i < sortedItems.length) {
60
+ const row_height = height_map[i] || average_height;
56
61
  content_height += row_height;
57
62
  i += 1;
63
+ if (content_height - head_height > 3 * max_height) {
64
+ break;
65
+ }
58
66
  }
59
67
  end = i;
60
- const remaining = _items.length - end;
68
+ const remaining = sortedItems.length - end;
61
69
  const scrollbar_height = viewport.offsetHeight - viewport.clientHeight;
62
70
  if (scrollbar_height > 0) {
63
71
  content_height += scrollbar_height;
64
72
  }
65
73
  let filtered_height_map = height_map.filter((v) => typeof v === "number");
66
- average_height = filtered_height_map.reduce((a, b) => a + b, 0) / filtered_height_map.length;
74
+ average_height = filtered_height_map.reduce((a, b) => a + b, 0) / filtered_height_map.length || 30;
67
75
  bottom = remaining * average_height;
68
- height_map.length = _items.length;
69
- await tick();
70
- if (!max_height) {
71
- actual_height = content_height + 1;
72
- } else if (content_height < max_height) {
73
- actual_height = content_height + 2;
74
- } else {
76
+ if (!isFinite(bottom)) {
77
+ bottom = 2e5;
78
+ }
79
+ height_map.length = sortedItems.length;
80
+ while (i < sortedItems.length) {
81
+ i += 1;
82
+ height_map[i] = average_height;
83
+ }
84
+ if (max_height && content_height > max_height) {
75
85
  actual_height = max_height;
86
+ } else {
87
+ actual_height = content_height;
76
88
  }
77
- await tick();
78
89
  }
79
90
  $:
80
91
  scroll_and_render(selected);
@@ -112,71 +123,6 @@ function is_in_view(n) {
112
123
  }
113
124
  return true;
114
125
  }
115
- function get_computed_px_amount(elem, property) {
116
- if (!elem) {
117
- return 0;
118
- }
119
- const compStyle = getComputedStyle(elem);
120
- let x = parseInt(compStyle.getPropertyValue(property));
121
- return x;
122
- }
123
- async function handle_scroll(e) {
124
- const scroll_top = viewport.scrollTop;
125
- show_scroll_button = scroll_top > 100;
126
- if (show_scroll_button) {
127
- dispatch("scroll_top", scroll_top);
128
- }
129
- rows = contents.children;
130
- const is_start_overflow = sortedItems.length < start;
131
- const row_top_border = get_computed_px_amount(rows[1], "border-top-width");
132
- const actual_border_collapsed_width = 0;
133
- if (is_start_overflow) {
134
- await scroll_to_index(sortedItems.length - 1, { behavior: "auto" });
135
- }
136
- let new_start = 0;
137
- for (let v = 0; v < rows.length; v += 1) {
138
- height_map[start + v] = rows[v].getBoundingClientRect().height;
139
- }
140
- let i = 0;
141
- let y = head_height + row_top_border / 2;
142
- let row_heights = [];
143
- while (i < sortedItems.length) {
144
- const row_height = height_map[i] || average_height;
145
- row_heights[i] = row_height;
146
- if (y + row_height + actual_border_collapsed_width > scroll_top) {
147
- new_start = i;
148
- top = y - (head_height + row_top_border / 2);
149
- break;
150
- }
151
- y += row_height;
152
- i += 1;
153
- }
154
- new_start = Math.max(0, new_start);
155
- while (i < sortedItems.length) {
156
- const row_height = height_map[i] || average_height;
157
- y += row_height;
158
- i += 1;
159
- if (y > scroll_top + viewport_height) {
160
- break;
161
- }
162
- }
163
- start = new_start;
164
- end = i;
165
- const remaining = sortedItems.length - end;
166
- if (end === 0) {
167
- end = 10;
168
- }
169
- average_height = (y - head_height) / end;
170
- let remaining_height = remaining * average_height;
171
- while (i < sortedItems.length) {
172
- i += 1;
173
- height_map[i] = average_height;
174
- }
175
- bottom = remaining_height;
176
- if (!isFinite(bottom)) {
177
- bottom = 2e5;
178
- }
179
- }
180
126
  export async function scroll_to_index(index, opts, align_end = false) {
181
127
  await tick();
182
128
  const _itemHeight = average_height;
@@ -206,7 +152,6 @@ $:
206
152
  onMount(() => {
207
153
  rows = contents.children;
208
154
  mounted = true;
209
- refresh_height_map(items);
210
155
  });
211
156
  </script>
212
157
 
@@ -217,7 +162,7 @@ onMount(() => {
217
162
  class:disable-scroll={disable_scroll}
218
163
  bind:this={viewport}
219
164
  bind:contentRect={viewport_box}
220
- on:scroll={handle_scroll}
165
+ on:scroll={refresh_height_map}
221
166
  style="height: {height}; --bw-svt-p-top: {top}px; --bw-svt-p-bottom: {bottom}px; --bw-svt-head-height: {head_height}px; --bw-svt-foot-height: {foot_height}px; --bw-svt-avg-row-height: {average_height}px; --max-height: {max_height}px"
222
167
  >
223
168
  <thead class="thead" bind:offsetHeight={head_height}>
@@ -43,6 +43,10 @@ export function create_drag_handlers(state, set_is_dragging, set_selected_cells,
43
43
  handle_mouse_move(event) {
44
44
  if (!state.drag_start || !state.mouse_down_pos)
45
45
  return;
46
+ if (!(event.buttons & 1)) {
47
+ end_drag(event);
48
+ return;
49
+ }
46
50
  const dx = Math.abs(event.clientX - state.mouse_down_pos.x);
47
51
  const dy = Math.abs(event.clientY - state.mouse_down_pos.y);
48
52
  if (!state.is_dragging && (dx > 3 || dy > 3)) {
@@ -23,7 +23,7 @@ export async function handle_cell_blur(event, ctx, coords) {
23
23
  const input_el = event.target;
24
24
  if (!input_el || input_el.value === undefined)
25
25
  return;
26
- await save_cell_value(input_el.value, ctx, coords[0], coords[1]);
26
+ await save_cell_value(input_el.type === "checkbox" ? String(input_el.checked) : input_el.value, ctx, coords[0], coords[1]);
27
27
  }
28
28
  function handle_header_navigation(event, ctx) {
29
29
  const state = get(ctx.state);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/dataframe",
3
- "version": "0.17.14",
3
+ "version": "0.17.16",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -18,12 +18,13 @@
18
18
  "katex": "^0.16.7",
19
19
  "marked": "^12.0.0",
20
20
  "@gradio/atoms": "^0.16.1",
21
- "@gradio/button": "^0.5.2",
22
- "@gradio/client": "^1.15.1",
21
+ "@gradio/button": "^0.5.3",
23
22
  "@gradio/icons": "^0.12.0",
24
- "@gradio/markdown-code": "^0.4.3",
25
- "@gradio/upload": "^0.16.6",
26
23
  "@gradio/statustracker": "^0.10.12",
24
+ "@gradio/checkbox": "^0.4.23",
25
+ "@gradio/upload": "^0.16.7",
26
+ "@gradio/markdown-code": "^0.4.3",
27
+ "@gradio/client": "^1.15.2",
27
28
  "@gradio/utils": "^0.10.2"
28
29
  },
29
30
  "exports": {
@@ -43,7 +44,7 @@
43
44
  "svelte": "^4.0.0"
44
45
  },
45
46
  "devDependencies": {
46
- "@gradio/preview": "^0.13.1"
47
+ "@gradio/preview": "^0.13.2"
47
48
  },
48
49
  "repository": {
49
50
  "type": "git",
@@ -0,0 +1,68 @@
1
+ <script lang="ts">
2
+ import { BaseCheckbox } from "@gradio/checkbox";
3
+
4
+ export let value: boolean | string = false;
5
+ export let editable = true;
6
+ export let on_change: (value: boolean) => void;
7
+
8
+ $: bool_value =
9
+ typeof value === "string" ? value.toLowerCase() === "true" : !!value;
10
+
11
+ function handle_change(event: CustomEvent<boolean>): void {
12
+ on_change(event.detail);
13
+ }
14
+
15
+ function handle_click(event: MouseEvent): void {
16
+ event.stopPropagation();
17
+ }
18
+
19
+ function handle_keydown(event: KeyboardEvent): void {
20
+ if (event.key === "Enter" || event.key === " ") {
21
+ event.stopPropagation();
22
+ }
23
+ }
24
+ </script>
25
+
26
+ <div
27
+ class="bool-cell checkbox"
28
+ on:click={handle_click}
29
+ on:keydown={handle_keydown}
30
+ role="button"
31
+ tabindex="-1"
32
+ >
33
+ <BaseCheckbox
34
+ bind:value={bool_value}
35
+ label=""
36
+ interactive={editable}
37
+ on:change={handle_change}
38
+ />
39
+ </div>
40
+
41
+ <style>
42
+ .bool-cell {
43
+ display: flex;
44
+ align-items: center;
45
+ justify-content: center;
46
+ width: var(--size-full);
47
+ height: var(--size-full);
48
+ }
49
+ .bool-cell :global(input:disabled) {
50
+ opacity: 0.8;
51
+ }
52
+
53
+ .bool-cell.checkbox {
54
+ justify-content: center;
55
+ }
56
+
57
+ .bool-cell :global(label) {
58
+ margin: 0;
59
+ width: 100%;
60
+ display: flex;
61
+ justify-content: center;
62
+ align-items: center;
63
+ }
64
+
65
+ .bool-cell :global(span) {
66
+ display: none;
67
+ }
68
+ </style>
@@ -3,6 +3,8 @@
3
3
  import { MarkdownCode } from "@gradio/markdown-code";
4
4
  import type { I18nFormatter } from "@gradio/utils";
5
5
  import SelectionButtons from "./icons/SelectionButtons.svelte";
6
+ import BooleanCell from "./BooleanCell.svelte";
7
+
6
8
  export let edit: boolean;
7
9
  export let value: string | number = "";
8
10
  export let display_value: string | null = null;
@@ -35,6 +37,7 @@
35
37
  export let coords: [number, number];
36
38
  export let on_select_column: ((col: number) => void) | null = null;
37
39
  export let on_select_row: ((row: number) => void) | null = null;
40
+ export let el: HTMLInputElement | null;
38
41
 
39
42
  const dispatch = createEventDispatcher<{
40
43
  blur: { blur_event: FocusEvent; coords: [number, number] };
@@ -43,8 +46,6 @@
43
46
 
44
47
  let is_expanded = false;
45
48
 
46
- export let el: HTMLInputElement | null;
47
-
48
49
  function truncate_text(
49
50
  text: string | number,
50
51
  max_length: number | null = null,
@@ -99,9 +100,23 @@
99
100
  is_expanded = !is_expanded;
100
101
  }
101
102
  }
103
+
104
+ function handle_bool_change(new_value: boolean): void {
105
+ value = new_value.toString();
106
+ dispatch("blur", {
107
+ blur_event: {
108
+ target: {
109
+ type: "checkbox",
110
+ checked: new_value,
111
+ value: new_value.toString()
112
+ }
113
+ } as unknown as FocusEvent,
114
+ coords: coords
115
+ });
116
+ }
102
117
  </script>
103
118
 
104
- {#if edit}
119
+ {#if edit && datatype !== "bool"}
105
120
  <input
106
121
  readonly={is_static}
107
122
  aria-readonly={is_static}
@@ -120,48 +135,57 @@
120
135
  />
121
136
  {/if}
122
137
 
123
- <span
124
- class:dragging={is_dragging}
125
- on:click={handle_click}
126
- on:keydown={handle_keydown}
127
- tabindex="0"
128
- role="button"
129
- class:edit
130
- class:expanded={is_expanded}
131
- class:multiline={header}
132
- on:focus|preventDefault
133
- style={styling}
134
- data-editable={editable}
135
- data-max-chars={max_chars}
136
- data-expanded={is_expanded}
137
- placeholder=" "
138
- class:text={datatype === "str"}
139
- class:wrap={wrap_text}
140
- >
141
- {#if datatype === "image" && components.image}
142
- <svelte:component
143
- this={components.image}
144
- value={{ url: display_text }}
145
- show_label={false}
146
- label="cell-image"
147
- show_download_button={false}
148
- {i18n}
149
- gradio={{ dispatch: () => {} }}
150
- />
151
- {:else if datatype === "html"}
152
- {@html display_text}
153
- {:else if datatype === "markdown"}
154
- <MarkdownCode
155
- message={display_text.toLocaleString()}
156
- {latex_delimiters}
157
- {line_breaks}
158
- chatbot={false}
159
- {root}
160
- />
161
- {:else}
162
- {display_text}
163
- {/if}
164
- </span>
138
+ {#if datatype === "bool"}
139
+ <BooleanCell
140
+ value={String(display_content)}
141
+ {editable}
142
+ on_change={handle_bool_change}
143
+ />
144
+ {:else}
145
+ <span
146
+ class:dragging={is_dragging}
147
+ on:click={handle_click}
148
+ on:keydown={handle_keydown}
149
+ tabindex="0"
150
+ role="button"
151
+ class:edit
152
+ class:expanded={is_expanded}
153
+ class:multiline={header}
154
+ on:focus|preventDefault
155
+ style={styling}
156
+ data-editable={editable}
157
+ data-max-chars={max_chars}
158
+ data-expanded={is_expanded}
159
+ placeholder=" "
160
+ class:text={datatype === "str"}
161
+ class:wrap={wrap_text}
162
+ >
163
+ {#if datatype === "image" && components.image}
164
+ <svelte:component
165
+ this={components.image}
166
+ value={{ url: display_text }}
167
+ show_label={false}
168
+ label="cell-image"
169
+ show_download_button={false}
170
+ {i18n}
171
+ gradio={{ dispatch: () => {} }}
172
+ />
173
+ {:else if datatype === "html"}
174
+ {@html display_text}
175
+ {:else if datatype === "markdown"}
176
+ <MarkdownCode
177
+ message={display_text.toLocaleString()}
178
+ {latex_delimiters}
179
+ {line_breaks}
180
+ chatbot={false}
181
+ {root}
182
+ />
183
+ {:else}
184
+ {display_text}
185
+ {/if}
186
+ </span>
187
+ {/if}
188
+
165
189
  {#if show_selection_buttons && coords && on_select_column && on_select_row}
166
190
  <SelectionButtons
167
191
  position="column"
@@ -120,7 +120,7 @@
120
120
  const observer = new IntersectionObserver((entries) => {
121
121
  entries.forEach((entry) => {
122
122
  if (entry.isIntersecting && !is_visible) {
123
- set_cell_widths();
123
+ width_calculated = false;
124
124
  }
125
125
  is_visible = entry.isIntersecting;
126
126
  });
@@ -129,10 +129,18 @@
129
129
  document.addEventListener("click", handle_click_outside);
130
130
  window.addEventListener("resize", handle_resize);
131
131
 
132
+ const global_mouse_up = (event: MouseEvent): void => {
133
+ if (is_dragging || drag_start) {
134
+ handle_mouse_up(event);
135
+ }
136
+ };
137
+ document.addEventListener("mouseup", global_mouse_up);
138
+
132
139
  return () => {
133
140
  observer.disconnect();
134
141
  document.removeEventListener("click", handle_click_outside);
135
142
  window.removeEventListener("resize", handle_resize);
143
+ document.removeEventListener("mouseup", global_mouse_up);
136
144
  };
137
145
  });
138
146
 
@@ -223,6 +231,7 @@
223
231
  }
224
232
  last_width_data_length = 0;
225
233
  last_width_column_count = 0;
234
+ width_calculated = false;
226
235
  }
227
236
  }
228
237
 
@@ -256,8 +265,8 @@
256
265
  df_actions.handle_search(null);
257
266
  }
258
267
 
259
- if (parent && cells.length > 0) {
260
- set_cell_widths();
268
+ if (parent && cells.length > 0 && (is_reset || is_different_structure)) {
269
+ width_calculated = false;
261
270
  }
262
271
  }
263
272
 
@@ -421,9 +430,17 @@
421
430
 
422
431
  $: max = get_max(data);
423
432
 
424
- // Modify how we trigger cell width calculations
425
- // Only recalculate when cells actually change, not during sort
426
- $: cells[0] && cells[0]?.clientWidth && set_cell_widths();
433
+ let width_calc_timeout: ReturnType<typeof setTimeout>;
434
+ $: if (cells[0] && cells[0]?.clientWidth) {
435
+ clearTimeout(width_calc_timeout);
436
+ width_calc_timeout = setTimeout(() => set_cell_widths(), 100);
437
+ }
438
+
439
+ let width_calculated = false;
440
+ $: if (cells[0] && !width_calculated) {
441
+ set_cell_widths();
442
+ width_calculated = true;
443
+ }
427
444
  let cells: HTMLTableCellElement[] = [];
428
445
  let parent: HTMLDivElement;
429
446
  let table: HTMLTableElement;
@@ -649,6 +666,7 @@
649
666
  selected_cells = [];
650
667
  selected = false;
651
668
  editing = false;
669
+ width_calculated = false;
652
670
  set_cell_widths();
653
671
  }
654
672
 
@@ -762,7 +780,7 @@
762
780
  role="grid"
763
781
  tabindex="0"
764
782
  >
765
- <table bind:this={table}>
783
+ <table bind:this={table} aria-hidden="true">
766
784
  {#if label && label.length !== 0}
767
785
  <caption class="sr-only">{label}</caption>
768
786
  {/if}
@@ -40,43 +40,53 @@
40
40
  ? window.requestAnimationFrame
41
41
  : (cb: (...args: any[]) => void) => cb();
42
42
 
43
- $: mounted && raf(() => refresh_height_map(sortedItems));
44
-
45
- let content_height = 0;
46
- async function refresh_height_map(_items: typeof items): Promise<void> {
47
- if (viewport_height === 0) {
48
- return;
43
+ $: {
44
+ if (mounted && viewport_height && viewport.offsetParent) {
45
+ sortedItems, raf(refresh_height_map);
49
46
  }
47
+ }
50
48
 
51
- // force header height calculation first
52
- head_height =
53
- viewport.querySelector(".thead")?.getBoundingClientRect().height || 0;
54
- await tick();
49
+ async function refresh_height_map(): Promise<void> {
50
+ if (sortedItems.length < start) {
51
+ await scroll_to_index(sortedItems.length - 1, { behavior: "auto" });
52
+ }
55
53
 
56
- const { scrollTop } = viewport;
54
+ const scrollTop = Math.max(0, viewport.scrollTop);
55
+ show_scroll_button = scrollTop > 100;
57
56
  table_scrollbar_width = viewport.offsetWidth - viewport.clientWidth;
58
57
 
59
- content_height = top - (scrollTop - head_height);
60
- let i = start;
61
-
62
- while (content_height < max_height && i < _items.length) {
63
- let row = rows[i - start];
64
- if (!row) {
65
- end = i + 1;
66
- await tick(); // render the newly visible row
67
- row = rows[i - start];
68
- }
69
- let _h = row?.getBoundingClientRect().height;
70
- if (!_h) {
71
- _h = average_height;
58
+ // acquire height map for currently visible rows
59
+ for (let v = 0; v < rows.length; v += 1) {
60
+ height_map[start + v] = rows[v].getBoundingClientRect().height;
61
+ }
62
+ let i = 0;
63
+ let y = head_height;
64
+ // loop items to find new start
65
+ while (i < sortedItems.length) {
66
+ const row_height = height_map[i] || average_height;
67
+ // keep a page of rows buffered above
68
+ if (y + row_height > scrollTop - max_height) {
69
+ start = i;
70
+ top = y - head_height;
71
+ break;
72
72
  }
73
- const row_height = (height_map[i] = _h);
73
+ y += row_height;
74
+ i += 1;
75
+ }
76
+
77
+ let content_height = head_height;
78
+ while (i < sortedItems.length) {
79
+ const row_height = height_map[i] || average_height;
74
80
  content_height += row_height;
75
81
  i += 1;
82
+ // keep a page of rows buffered below
83
+ if (content_height - head_height > 3 * max_height) {
84
+ break;
85
+ }
76
86
  }
77
87
 
78
88
  end = i;
79
- const remaining = _items.length - end;
89
+ const remaining = sortedItems.length - end;
80
90
 
81
91
  const scrollbar_height = viewport.offsetHeight - viewport.clientHeight;
82
92
  if (scrollbar_height > 0) {
@@ -86,20 +96,22 @@
86
96
  let filtered_height_map = height_map.filter((v) => typeof v === "number");
87
97
  average_height =
88
98
  filtered_height_map.reduce((a, b) => a + b, 0) /
89
- filtered_height_map.length;
99
+ filtered_height_map.length || 30;
90
100
 
91
101
  bottom = remaining * average_height;
92
- height_map.length = _items.length;
93
- await tick();
94
- if (!max_height) {
95
- actual_height = content_height + 1;
96
- } else if (content_height < max_height) {
97
- actual_height = content_height + 2;
98
- } else {
102
+ if (!isFinite(bottom)) {
103
+ bottom = 200000;
104
+ }
105
+ height_map.length = sortedItems.length;
106
+ while (i < sortedItems.length) {
107
+ i += 1;
108
+ height_map[i] = average_height;
109
+ }
110
+ if (max_height && content_height > max_height) {
99
111
  actual_height = max_height;
112
+ } else {
113
+ actual_height = content_height;
100
114
  }
101
-
102
- await tick();
103
115
  }
104
116
 
105
117
  $: scroll_and_render(selected);
@@ -144,88 +156,6 @@
144
156
  return true;
145
157
  }
146
158
 
147
- function get_computed_px_amount(elem: HTMLElement, property: string): number {
148
- if (!elem) {
149
- return 0;
150
- }
151
- const compStyle = getComputedStyle(elem);
152
-
153
- let x = parseInt(compStyle.getPropertyValue(property));
154
- return x;
155
- }
156
-
157
- async function handle_scroll(e: Event): Promise<void> {
158
- const scroll_top = viewport.scrollTop;
159
-
160
- show_scroll_button = scroll_top > 100;
161
-
162
- if (show_scroll_button) {
163
- dispatch("scroll_top", scroll_top);
164
- }
165
-
166
- rows = contents.children as HTMLCollectionOf<HTMLTableRowElement>;
167
- const is_start_overflow = sortedItems.length < start;
168
-
169
- const row_top_border = get_computed_px_amount(rows[1], "border-top-width");
170
-
171
- const actual_border_collapsed_width = 0;
172
-
173
- if (is_start_overflow) {
174
- await scroll_to_index(sortedItems.length - 1, { behavior: "auto" });
175
- }
176
-
177
- let new_start = 0;
178
- // acquire height map for currently visible rows
179
- for (let v = 0; v < rows.length; v += 1) {
180
- height_map[start + v] = rows[v].getBoundingClientRect().height;
181
- }
182
- let i = 0;
183
- // start from top: thead, with its borders, plus the first border to afterwards neglect
184
- let y = head_height + row_top_border / 2;
185
- let row_heights = [];
186
- // loop items to find new start
187
- while (i < sortedItems.length) {
188
- const row_height = height_map[i] || average_height;
189
- row_heights[i] = row_height;
190
- // we only want to jump if the full (incl. border) row is away
191
- if (y + row_height + actual_border_collapsed_width > scroll_top) {
192
- // this is the last index still inside the viewport
193
- new_start = i;
194
- top = y - (head_height + row_top_border / 2);
195
- break;
196
- }
197
- y += row_height;
198
- i += 1;
199
- }
200
-
201
- new_start = Math.max(0, new_start);
202
- while (i < sortedItems.length) {
203
- const row_height = height_map[i] || average_height;
204
- y += row_height;
205
- i += 1;
206
- if (y > scroll_top + viewport_height) {
207
- break;
208
- }
209
- }
210
- start = new_start;
211
- end = i;
212
- const remaining = sortedItems.length - end;
213
- if (end === 0) {
214
- end = 10;
215
- }
216
- average_height = (y - head_height) / end;
217
- let remaining_height = remaining * average_height; // 0
218
- // compute height map for remaining items
219
- while (i < sortedItems.length) {
220
- i += 1;
221
- height_map[i] = average_height;
222
- }
223
- bottom = remaining_height;
224
- if (!isFinite(bottom)) {
225
- bottom = 200000;
226
- }
227
- }
228
-
229
159
  export async function scroll_to_index(
230
160
  index: number,
231
161
  opts: ScrollToOptions,
@@ -269,7 +199,6 @@
269
199
  onMount(() => {
270
200
  rows = contents.children as HTMLCollectionOf<HTMLTableRowElement>;
271
201
  mounted = true;
272
- refresh_height_map(items);
273
202
  });
274
203
  </script>
275
204
 
@@ -280,7 +209,7 @@
280
209
  class:disable-scroll={disable_scroll}
281
210
  bind:this={viewport}
282
211
  bind:contentRect={viewport_box}
283
- on:scroll={handle_scroll}
212
+ on:scroll={refresh_height_map}
284
213
  style="height: {height}; --bw-svt-p-top: {top}px; --bw-svt-p-bottom: {bottom}px; --bw-svt-head-height: {head_height}px; --bw-svt-foot-height: {foot_height}px; --bw-svt-avg-row-height: {average_height}px; --max-height: {max_height}px"
285
214
  >
286
215
  <thead class="thead" bind:offsetHeight={head_height}>
@@ -75,6 +75,11 @@ export function create_drag_handlers(
75
75
  handle_mouse_move(event: MouseEvent): void {
76
76
  if (!state.drag_start || !state.mouse_down_pos) return;
77
77
 
78
+ if (!(event.buttons & 1)) {
79
+ end_drag(event);
80
+ return;
81
+ }
82
+
78
83
  const dx = Math.abs(event.clientX - state.mouse_down_pos.x);
79
84
  const dy = Math.abs(event.clientY - state.mouse_down_pos.y);
80
85
 
@@ -37,7 +37,12 @@ export async function handle_cell_blur(
37
37
  const input_el = event.target as HTMLInputElement;
38
38
  if (!input_el || input_el.value === undefined) return;
39
39
 
40
- await save_cell_value(input_el.value, ctx, coords[0], coords[1]);
40
+ await save_cell_value(
41
+ input_el.type === "checkbox" ? String(input_el.checked) : input_el.value,
42
+ ctx,
43
+ coords[0],
44
+ coords[1]
45
+ );
41
46
  }
42
47
 
43
48
  function handle_header_navigation(