@gradio/dataframe 0.23.1 → 0.23.3
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 +24 -0
- package/Dataframe.test.ts +232 -1
- package/Index.svelte +0 -1
- package/dist/Index.svelte +0 -1
- package/dist/shared/BooleanCell.svelte +0 -1
- package/dist/shared/DataCell.svelte +88 -3
- package/dist/shared/DataCell.svelte.d.ts +1 -0
- package/dist/shared/EditableCell.svelte +52 -29
- package/dist/shared/EditableCell.svelte.d.ts +2 -0
- package/dist/shared/HeaderCell.svelte +67 -5
- package/dist/shared/HeaderCell.svelte.d.ts +4 -0
- package/dist/shared/Table.svelte +166 -50
- package/dist/shared/column_measurement.svelte.d.ts +1 -0
- package/dist/shared/column_measurement.svelte.js +86 -13
- package/package.json +9 -9
- package/shared/BooleanCell.svelte +0 -1
- package/shared/DataCell.svelte +88 -3
- package/shared/EditableCell.svelte +52 -29
- package/shared/HeaderCell.svelte +67 -5
- package/shared/Table.svelte +166 -50
- package/shared/column_measurement.svelte.ts +98 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# @gradio/dataframe
|
|
2
2
|
|
|
3
|
+
## 0.23.3
|
|
4
|
+
|
|
5
|
+
### Dependency updates
|
|
6
|
+
|
|
7
|
+
- @gradio/client@2.2.1
|
|
8
|
+
- @gradio/upload@0.17.10
|
|
9
|
+
- @gradio/markdown-code@0.8.0
|
|
10
|
+
|
|
11
|
+
## 0.23.2
|
|
12
|
+
|
|
13
|
+
### Fixes
|
|
14
|
+
|
|
15
|
+
- [#13250](https://github.com/gradio-app/gradio/pull/13250) [`2e9eb51`](https://github.com/gradio-app/gradio/commit/2e9eb51d4da5e4a997ac2fc18e5814bacf6f818d) - Restore select-all checkbox in boolean column headers of `gr.Dataframe`. Thanks @pngwn!
|
|
16
|
+
- [#13259](https://github.com/gradio-app/gradio/pull/13259) [`b5e6491`](https://github.com/gradio-app/gradio/commit/b5e64918b721550bc3762169fda81c5270904eaa) - Dataframe: allow native context menu and text selection on static cells. Thanks @pngwn!
|
|
17
|
+
- [#13305](https://github.com/gradio-app/gradio/pull/13305) [`1d5994d`](https://github.com/gradio-app/gradio/commit/1d5994da102bd9bf2367f58789de508b1727b1bf) - Dataframe: fix extreme rendering slowdown with `datatype="date"` (and any future dtype with asymmetric string casts) by firing `EditableCell`'s shim-blur only on edit teardown instead of every render. Also makes the hidden sizing-row computation faster by avoiding Date rendering for every entry. Thanks @pngwn!
|
|
18
|
+
- [#13303](https://github.com/gradio-app/gradio/pull/13303) [`79c5776`](https://github.com/gradio-app/gradio/commit/79c577680978adfb5eeaa01944635e14dc67e5f1) - Rework `Dataframe` wrapping, truncation, and column width sizing. Thanks @pngwn!
|
|
19
|
+
|
|
20
|
+
### Dependency updates
|
|
21
|
+
|
|
22
|
+
- @gradio/atoms@0.24.0
|
|
23
|
+
- @gradio/statustracker@0.14.1
|
|
24
|
+
- @gradio/checkbox@0.7.0
|
|
25
|
+
- @gradio/upload@0.17.9
|
|
26
|
+
|
|
3
27
|
## 0.23.1
|
|
4
28
|
|
|
5
29
|
### Dependency updates
|
package/Dataframe.test.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { test, describe, afterEach, expect } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
cleanup,
|
|
4
|
+
render,
|
|
5
|
+
fireEvent,
|
|
6
|
+
waitFor,
|
|
7
|
+
within
|
|
8
|
+
} from "@self/tootils/render";
|
|
3
9
|
import { tick } from "svelte";
|
|
4
10
|
|
|
5
11
|
import Dataframe from "./Index.svelte";
|
|
@@ -579,6 +585,67 @@ describe("Add/remove rows and columns", () => {
|
|
|
579
585
|
});
|
|
580
586
|
});
|
|
581
587
|
|
|
588
|
+
describe("Cell menu visibility on static cells", () => {
|
|
589
|
+
afterEach(() => cleanup());
|
|
590
|
+
|
|
591
|
+
test("context menu does not open on a static column cell", async () => {
|
|
592
|
+
const { container } = await render(Dataframe, {
|
|
593
|
+
...default_props,
|
|
594
|
+
static_columns: [0]
|
|
595
|
+
});
|
|
596
|
+
await wait();
|
|
597
|
+
|
|
598
|
+
const cell = get_cell(container, 0, 0)!;
|
|
599
|
+
await fireEvent.contextMenu(cell);
|
|
600
|
+
await wait();
|
|
601
|
+
|
|
602
|
+
expect(within(document.body).queryByRole("menu")).toBeNull();
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
test("context menu does not open when interactive is false", async () => {
|
|
606
|
+
const { container } = await render(Dataframe, {
|
|
607
|
+
...default_props,
|
|
608
|
+
interactive: false,
|
|
609
|
+
editable: false
|
|
610
|
+
});
|
|
611
|
+
await wait();
|
|
612
|
+
|
|
613
|
+
const cell = get_cell(container, 0, 0)!;
|
|
614
|
+
await fireEvent.contextMenu(cell);
|
|
615
|
+
await wait();
|
|
616
|
+
|
|
617
|
+
expect(within(document.body).queryByRole("menu")).toBeNull();
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
test("context menu does open on an editable, non-static cell", async () => {
|
|
621
|
+
const { container } = await render(Dataframe, {
|
|
622
|
+
...default_props,
|
|
623
|
+
row_count: [3, "dynamic"] as [number, "fixed" | "dynamic"]
|
|
624
|
+
});
|
|
625
|
+
await wait();
|
|
626
|
+
|
|
627
|
+
const cell = get_cell(container, 0, 0)!;
|
|
628
|
+
await fireEvent.contextMenu(cell);
|
|
629
|
+
await wait();
|
|
630
|
+
|
|
631
|
+
expect(within(document.body).queryByRole("menu")).not.toBeNull();
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
test("cell menu button does not render on a static column cell", async () => {
|
|
635
|
+
const { container } = await render(Dataframe, {
|
|
636
|
+
...default_props,
|
|
637
|
+
static_columns: [0]
|
|
638
|
+
});
|
|
639
|
+
await wait();
|
|
640
|
+
|
|
641
|
+
const cell = get_cell(container, 0, 0)!;
|
|
642
|
+
await fireEvent.mouseDown(cell);
|
|
643
|
+
await wait();
|
|
644
|
+
|
|
645
|
+
expect(cell.querySelector(".cell-menu-button")).toBeNull();
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
|
|
582
649
|
describe("Delete/clear cells", () => {
|
|
583
650
|
afterEach(() => cleanup());
|
|
584
651
|
|
|
@@ -788,3 +855,167 @@ describe("Copy", () => {
|
|
|
788
855
|
});
|
|
789
856
|
});
|
|
790
857
|
});
|
|
858
|
+
|
|
859
|
+
describe("Boolean column select-all header checkbox", () => {
|
|
860
|
+
afterEach(() => cleanup());
|
|
861
|
+
|
|
862
|
+
const bool_props = {
|
|
863
|
+
...default_props,
|
|
864
|
+
value: {
|
|
865
|
+
data: [
|
|
866
|
+
["Alice", true, false],
|
|
867
|
+
["Bob", false, false],
|
|
868
|
+
["Carol", true, false]
|
|
869
|
+
],
|
|
870
|
+
headers: ["Name", "Active", "Admin"],
|
|
871
|
+
metadata: null
|
|
872
|
+
},
|
|
873
|
+
datatype: ["str", "bool", "bool"] as any,
|
|
874
|
+
col_count: [3, "fixed"] as [number, "fixed" | "dynamic"]
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
function get_header(container: HTMLElement, col_idx: number): HTMLElement {
|
|
878
|
+
return container.querySelector<HTMLElement>(
|
|
879
|
+
`th[data-heading='${col_idx}']`
|
|
880
|
+
)!;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function get_header_checkbox(
|
|
884
|
+
container: HTMLElement,
|
|
885
|
+
col_idx: number
|
|
886
|
+
): HTMLInputElement {
|
|
887
|
+
return within(get_header(container, col_idx)).getByTestId(
|
|
888
|
+
"checkbox"
|
|
889
|
+
) as HTMLInputElement;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
test("renders checkbox in bool column header (interactive mode)", async () => {
|
|
893
|
+
const { container } = await render(Dataframe, bool_props);
|
|
894
|
+
await wait();
|
|
895
|
+
|
|
896
|
+
expect(get_header_checkbox(container, 1)).toBeInTheDocument();
|
|
897
|
+
expect(get_header_checkbox(container, 2)).toBeInTheDocument();
|
|
898
|
+
|
|
899
|
+
expect(
|
|
900
|
+
within(get_header(container, 0)).queryByTestId("checkbox")
|
|
901
|
+
).not.toBeInTheDocument();
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
test("does not render checkbox when not interactive", async () => {
|
|
905
|
+
const { container } = await render(Dataframe, {
|
|
906
|
+
...bool_props,
|
|
907
|
+
interactive: false
|
|
908
|
+
});
|
|
909
|
+
await wait();
|
|
910
|
+
|
|
911
|
+
expect(
|
|
912
|
+
within(get_header(container, 1)).queryByTestId("checkbox")
|
|
913
|
+
).not.toBeInTheDocument();
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
test("header checkbox reflects indeterminate state for mixed column", async () => {
|
|
917
|
+
const { container } = await render(Dataframe, bool_props);
|
|
918
|
+
await wait();
|
|
919
|
+
|
|
920
|
+
const active = get_header_checkbox(container, 1);
|
|
921
|
+
expect(active.indeterminate).toBe(true);
|
|
922
|
+
expect(active.checked).toBe(false);
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
test("header checkbox is checked when all column values are true", async () => {
|
|
926
|
+
const { container } = await render(Dataframe, {
|
|
927
|
+
...bool_props,
|
|
928
|
+
value: {
|
|
929
|
+
data: [
|
|
930
|
+
["Alice", true],
|
|
931
|
+
["Bob", true]
|
|
932
|
+
],
|
|
933
|
+
headers: ["Name", "Active"],
|
|
934
|
+
metadata: null
|
|
935
|
+
},
|
|
936
|
+
datatype: ["str", "bool"] as any,
|
|
937
|
+
col_count: [2, "fixed"] as [number, "fixed" | "dynamic"]
|
|
938
|
+
});
|
|
939
|
+
await wait();
|
|
940
|
+
|
|
941
|
+
const active = get_header_checkbox(container, 1);
|
|
942
|
+
expect(active.checked).toBe(true);
|
|
943
|
+
expect(active.indeterminate).toBe(false);
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
test("clicking indeterminate header checkbox sets all column cells to true", async () => {
|
|
947
|
+
const { container, listen } = await render(Dataframe, bool_props);
|
|
948
|
+
await wait();
|
|
949
|
+
|
|
950
|
+
const change = listen("change");
|
|
951
|
+
await fireEvent.click(get_header_checkbox(container, 1));
|
|
952
|
+
await wait();
|
|
953
|
+
|
|
954
|
+
const data = change.mock.calls.at(-1)?.[0].data;
|
|
955
|
+
expect(data).toEqual([
|
|
956
|
+
["Alice", true, false],
|
|
957
|
+
["Bob", true, false],
|
|
958
|
+
["Carol", true, false]
|
|
959
|
+
]);
|
|
960
|
+
|
|
961
|
+
const updated = get_header_checkbox(container, 1);
|
|
962
|
+
expect(updated.checked).toBe(true);
|
|
963
|
+
expect(updated.indeterminate).toBe(false);
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
test("clicking fully-checked header checkbox sets all column cells to false", async () => {
|
|
967
|
+
const { container, listen } = await render(Dataframe, {
|
|
968
|
+
...bool_props,
|
|
969
|
+
value: {
|
|
970
|
+
data: [
|
|
971
|
+
["Alice", true, false],
|
|
972
|
+
["Bob", true, false]
|
|
973
|
+
],
|
|
974
|
+
headers: ["Name", "Active", "Admin"],
|
|
975
|
+
metadata: null
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
await wait();
|
|
979
|
+
|
|
980
|
+
const change = listen("change");
|
|
981
|
+
await fireEvent.click(get_header_checkbox(container, 1));
|
|
982
|
+
await wait();
|
|
983
|
+
|
|
984
|
+
const data = change.mock.calls.at(-1)?.[0].data;
|
|
985
|
+
expect(data).toEqual([
|
|
986
|
+
["Alice", false, false],
|
|
987
|
+
["Bob", false, false]
|
|
988
|
+
]);
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
test("toggling one bool header does not affect cells in other bool columns", async () => {
|
|
992
|
+
const { container, listen } = await render(Dataframe, {
|
|
993
|
+
...bool_props,
|
|
994
|
+
value: {
|
|
995
|
+
data: [
|
|
996
|
+
["Alice", false, true],
|
|
997
|
+
["Bob", false, true],
|
|
998
|
+
["Carol", false, true]
|
|
999
|
+
],
|
|
1000
|
+
headers: ["Name", "Active", "Admin"],
|
|
1001
|
+
metadata: null
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
await wait();
|
|
1005
|
+
|
|
1006
|
+
const change = listen("change");
|
|
1007
|
+
await fireEvent.click(get_header_checkbox(container, 1));
|
|
1008
|
+
await wait();
|
|
1009
|
+
|
|
1010
|
+
const data = change.mock.calls.at(-1)?.[0].data;
|
|
1011
|
+
expect(data).toEqual([
|
|
1012
|
+
["Alice", true, true],
|
|
1013
|
+
["Bob", true, true],
|
|
1014
|
+
["Carol", true, true]
|
|
1015
|
+
]);
|
|
1016
|
+
|
|
1017
|
+
const admin = get_header_checkbox(container, 2);
|
|
1018
|
+
expect(admin.checked).toBe(true);
|
|
1019
|
+
expect(admin.indeterminate).toBe(false);
|
|
1020
|
+
});
|
|
1021
|
+
});
|
package/Index.svelte
CHANGED
package/dist/Index.svelte
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
show_menu_button = false,
|
|
21
21
|
show_selection_buttons = false,
|
|
22
22
|
is_first_column = false,
|
|
23
|
+
is_solo = false,
|
|
23
24
|
latex_delimiters,
|
|
24
25
|
line_breaks = true,
|
|
25
26
|
editable = true,
|
|
@@ -50,6 +51,7 @@
|
|
|
50
51
|
show_menu_button?: boolean;
|
|
51
52
|
show_selection_buttons?: boolean;
|
|
52
53
|
is_first_column?: boolean;
|
|
54
|
+
is_solo?: boolean;
|
|
53
55
|
latex_delimiters: { left: string; right: string; display: boolean }[];
|
|
54
56
|
line_breaks?: boolean;
|
|
55
57
|
editable?: boolean;
|
|
@@ -71,21 +73,38 @@
|
|
|
71
73
|
} = $props();
|
|
72
74
|
|
|
73
75
|
let is_selected = $derived(selection_classes !== "");
|
|
76
|
+
|
|
77
|
+
// lock the cell-wrap's height to its pre-selection value so the
|
|
78
|
+
// row doesn't shift when the span goes position: absolute.
|
|
79
|
+
let wrap_el: HTMLDivElement | undefined = $state();
|
|
80
|
+
let locked_height = $state("");
|
|
81
|
+
|
|
82
|
+
// pre is weird but runs _before_ DOM updates
|
|
83
|
+
$effect.pre(() => {
|
|
84
|
+
if (is_solo && wrap_el) {
|
|
85
|
+
locked_height = `height: ${wrap_el.offsetHeight}px;`;
|
|
86
|
+
} else {
|
|
87
|
+
locked_height = "";
|
|
88
|
+
}
|
|
89
|
+
});
|
|
74
90
|
</script>
|
|
75
91
|
|
|
76
92
|
<div
|
|
77
93
|
class="body-cell {selection_classes}"
|
|
78
94
|
class:flash={is_flash}
|
|
79
95
|
class:first-column={is_first_column}
|
|
96
|
+
class:static={is_static}
|
|
97
|
+
class:cell-solo={is_solo}
|
|
80
98
|
data-row={row_idx}
|
|
81
99
|
data-col={col_idx}
|
|
82
100
|
data-testid={`cell-${row_idx}-${col_idx}`}
|
|
101
|
+
tabindex={is_static ? -1 : undefined}
|
|
83
102
|
{onmousedown}
|
|
84
103
|
{ondblclick}
|
|
85
104
|
{oncontextmenu}
|
|
86
105
|
style="{col_style} {cell_style}"
|
|
87
106
|
>
|
|
88
|
-
<div class="cell-wrap">
|
|
107
|
+
<div class="cell-wrap" bind:this={wrap_el} style={locked_height}>
|
|
89
108
|
<EditableCell
|
|
90
109
|
{value}
|
|
91
110
|
{display_value}
|
|
@@ -105,6 +124,7 @@
|
|
|
105
124
|
{on_select_row}
|
|
106
125
|
{is_dragging}
|
|
107
126
|
{wrap_text}
|
|
127
|
+
expanded={is_solo}
|
|
108
128
|
/>
|
|
109
129
|
{#if show_menu_button}
|
|
110
130
|
<CellMenuButton on_click={on_menu_click} />
|
|
@@ -120,8 +140,14 @@
|
|
|
120
140
|
inset 1px 0 0 var(--border-color-primary),
|
|
121
141
|
inset 0 0 0 1px var(--ring-color);
|
|
122
142
|
padding: 0;
|
|
123
|
-
overflow:
|
|
143
|
+
overflow: hidden;
|
|
124
144
|
box-sizing: border-box;
|
|
145
|
+
user-select: none;
|
|
146
|
+
min-width: 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.body-cell.static {
|
|
150
|
+
user-select: text;
|
|
125
151
|
}
|
|
126
152
|
|
|
127
153
|
.body-cell.first-column {
|
|
@@ -145,6 +171,65 @@
|
|
|
145
171
|
position: relative;
|
|
146
172
|
}
|
|
147
173
|
|
|
174
|
+
.body-cell.cell-solo {
|
|
175
|
+
z-index: 10;
|
|
176
|
+
overflow: visible;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.body-cell.cell-solo .cell-wrap {
|
|
180
|
+
overflow: visible;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.body-cell.cell-solo :global(.cell-wrap > span) {
|
|
184
|
+
position: absolute;
|
|
185
|
+
top: 0;
|
|
186
|
+
left: 0;
|
|
187
|
+
width: 100%;
|
|
188
|
+
min-height: 100%;
|
|
189
|
+
padding: var(--size-2);
|
|
190
|
+
box-sizing: border-box;
|
|
191
|
+
white-space: normal;
|
|
192
|
+
overflow: visible;
|
|
193
|
+
text-overflow: clip;
|
|
194
|
+
overflow-wrap: break-word;
|
|
195
|
+
word-break: break-word;
|
|
196
|
+
background: var(--background-fill-primary);
|
|
197
|
+
box-shadow:
|
|
198
|
+
inset 0 2px 0 0 var(--color-accent),
|
|
199
|
+
inset 0 -2px 0 0 var(--color-accent),
|
|
200
|
+
inset 2px 0 0 0 var(--color-accent),
|
|
201
|
+
inset -2px 0 0 0 var(--color-accent),
|
|
202
|
+
0 6px 12px -4px rgba(0, 0, 0, 0.22);
|
|
203
|
+
z-index: 1;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.body-cell.cell-solo :global(.cell-wrap > textarea) {
|
|
207
|
+
position: absolute;
|
|
208
|
+
top: 0;
|
|
209
|
+
left: 0;
|
|
210
|
+
width: 100%;
|
|
211
|
+
min-height: 100%;
|
|
212
|
+
padding: var(--size-2);
|
|
213
|
+
padding-left: var(--size-2);
|
|
214
|
+
margin: 0;
|
|
215
|
+
box-sizing: border-box;
|
|
216
|
+
transform: none;
|
|
217
|
+
background: var(--background-fill-primary);
|
|
218
|
+
box-shadow:
|
|
219
|
+
inset 0 2px 0 0 var(--color-accent),
|
|
220
|
+
inset 0 -2px 0 0 var(--color-accent),
|
|
221
|
+
inset 2px 0 0 0 var(--color-accent),
|
|
222
|
+
inset -2px 0 0 0 var(--color-accent),
|
|
223
|
+
0 6px 12px -4px rgba(0, 0, 0, 0.22);
|
|
224
|
+
z-index: 2;
|
|
225
|
+
field-sizing: content;
|
|
226
|
+
height: auto;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.body-cell.cell-solo :global(.selection-button) {
|
|
230
|
+
z-index: 3;
|
|
231
|
+
}
|
|
232
|
+
|
|
148
233
|
.body-cell.cell-selected.no-top {
|
|
149
234
|
--sel-top: inset 0 0 0 0 transparent;
|
|
150
235
|
}
|
|
@@ -182,7 +267,7 @@
|
|
|
182
267
|
padding: var(--size-2);
|
|
183
268
|
box-sizing: border-box;
|
|
184
269
|
gap: var(--size-1);
|
|
185
|
-
overflow:
|
|
270
|
+
overflow: hidden;
|
|
186
271
|
min-width: 0;
|
|
187
272
|
height: 100%;
|
|
188
273
|
}
|
|
@@ -21,13 +21,15 @@
|
|
|
21
21
|
i18n,
|
|
22
22
|
is_dragging = false,
|
|
23
23
|
wrap_text = false,
|
|
24
|
+
expanded = false,
|
|
24
25
|
show_selection_buttons = false,
|
|
25
26
|
coords,
|
|
26
27
|
on_select_column = null,
|
|
27
28
|
on_select_row = null,
|
|
28
29
|
el = $bindable(null),
|
|
29
30
|
onblur,
|
|
30
|
-
onkeydown
|
|
31
|
+
onkeydown,
|
|
32
|
+
pad_left = false
|
|
31
33
|
}: {
|
|
32
34
|
edit: boolean;
|
|
33
35
|
value?: CellValue;
|
|
@@ -55,6 +57,7 @@
|
|
|
55
57
|
i18n: I18nFormatter;
|
|
56
58
|
is_dragging?: boolean;
|
|
57
59
|
wrap_text?: boolean;
|
|
60
|
+
expanded?: boolean;
|
|
58
61
|
show_selection_buttons?: boolean;
|
|
59
62
|
coords: [number, number];
|
|
60
63
|
on_select_column?: ((col: number) => void) | null;
|
|
@@ -65,6 +68,7 @@
|
|
|
65
68
|
coords: [number, number];
|
|
66
69
|
}) => void;
|
|
67
70
|
onkeydown?: (event: KeyboardEvent) => void;
|
|
71
|
+
pad_left: boolean;
|
|
68
72
|
} = $props();
|
|
69
73
|
|
|
70
74
|
function truncate_text(
|
|
@@ -79,7 +83,9 @@
|
|
|
79
83
|
return str.slice(0, max_length) + "...";
|
|
80
84
|
}
|
|
81
85
|
|
|
82
|
-
let should_truncate = $derived(
|
|
86
|
+
let should_truncate = $derived(
|
|
87
|
+
!edit && !expanded && max_chars !== null && max_chars > 0
|
|
88
|
+
);
|
|
83
89
|
|
|
84
90
|
let display_content = $derived(
|
|
85
91
|
editable ? value : display_value !== null ? display_value : value
|
|
@@ -99,6 +105,22 @@
|
|
|
99
105
|
return {};
|
|
100
106
|
}
|
|
101
107
|
|
|
108
|
+
// grow the edit-mode textarea to fit its content so the overlay
|
|
109
|
+
// expands the same way the display span does.
|
|
110
|
+
function use_autosize(node: HTMLTextAreaElement): any {
|
|
111
|
+
function resize(): void {
|
|
112
|
+
node.style.height = "auto";
|
|
113
|
+
node.style.height = `${node.scrollHeight}px`;
|
|
114
|
+
}
|
|
115
|
+
node.addEventListener("input", resize);
|
|
116
|
+
requestAnimationFrame(resize);
|
|
117
|
+
return {
|
|
118
|
+
destroy() {
|
|
119
|
+
node.removeEventListener("input", resize);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
102
124
|
function handle_blur(event: FocusEvent): void {
|
|
103
125
|
onblur?.({
|
|
104
126
|
blur_event: event,
|
|
@@ -114,10 +136,12 @@
|
|
|
114
136
|
handle_blur({ target: { value } } as unknown as FocusEvent);
|
|
115
137
|
}
|
|
116
138
|
|
|
139
|
+
// returning cleanup from the effect fires the blur only when leaving edit mode, not on every render
|
|
117
140
|
$effect(() => {
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
141
|
+
if (edit) {
|
|
142
|
+
return () => {
|
|
143
|
+
handle_blur({ target: { value } } as unknown as FocusEvent);
|
|
144
|
+
};
|
|
121
145
|
}
|
|
122
146
|
});
|
|
123
147
|
</script>
|
|
@@ -135,7 +159,9 @@
|
|
|
135
159
|
onmousedown={(e: MouseEvent) => e.stopPropagation()}
|
|
136
160
|
onclick={(e: MouseEvent) => e.stopPropagation()}
|
|
137
161
|
use:use_focus
|
|
162
|
+
use:use_autosize
|
|
138
163
|
onkeydown={handle_keydown}
|
|
164
|
+
class:pad_left
|
|
139
165
|
/>
|
|
140
166
|
{/if}
|
|
141
167
|
|
|
@@ -149,7 +175,7 @@
|
|
|
149
175
|
role="button"
|
|
150
176
|
class:edit
|
|
151
177
|
class:expanded={edit}
|
|
152
|
-
class:
|
|
178
|
+
class:header
|
|
153
179
|
onfocus={(e) => e.preventDefault()}
|
|
154
180
|
style={styling}
|
|
155
181
|
data-editable={editable}
|
|
@@ -205,7 +231,7 @@
|
|
|
205
231
|
textarea {
|
|
206
232
|
position: absolute;
|
|
207
233
|
flex: 1 1 0%;
|
|
208
|
-
transform:
|
|
234
|
+
transform: translate(0.2px, -0.5px);
|
|
209
235
|
outline: none;
|
|
210
236
|
border: none;
|
|
211
237
|
background: transparent;
|
|
@@ -217,24 +243,33 @@
|
|
|
217
243
|
font-size: inherit;
|
|
218
244
|
font-weight: inherit;
|
|
219
245
|
line-height: var(--line-lg);
|
|
246
|
+
left: var(--size-2);
|
|
247
|
+
user-select: text;
|
|
220
248
|
}
|
|
221
249
|
|
|
222
250
|
textarea:focus {
|
|
223
251
|
outline: none;
|
|
224
252
|
}
|
|
225
253
|
|
|
254
|
+
textarea.pad_left {
|
|
255
|
+
margin-left: var(--size-7);
|
|
256
|
+
}
|
|
257
|
+
|
|
226
258
|
span {
|
|
227
259
|
flex: 1 1 0%;
|
|
228
260
|
position: relative;
|
|
229
261
|
display: block;
|
|
230
262
|
outline: none;
|
|
231
|
-
-webkit-user-select: text;
|
|
232
|
-
-moz-user-select: text;
|
|
233
|
-
-ms-user-select: text;
|
|
234
|
-
user-select: text;
|
|
235
263
|
cursor: text;
|
|
236
264
|
width: 100%;
|
|
237
|
-
|
|
265
|
+
min-width: 0;
|
|
266
|
+
white-space: nowrap;
|
|
267
|
+
overflow: hidden;
|
|
268
|
+
text-overflow: ellipsis;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.header {
|
|
272
|
+
font-weight: var(--weight-bold);
|
|
238
273
|
}
|
|
239
274
|
|
|
240
275
|
span.text.expanded {
|
|
@@ -245,21 +280,6 @@
|
|
|
245
280
|
overflow: visible;
|
|
246
281
|
}
|
|
247
282
|
|
|
248
|
-
.multiline {
|
|
249
|
-
white-space: pre;
|
|
250
|
-
overflow: hidden;
|
|
251
|
-
text-overflow: ellipsis;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
.header {
|
|
255
|
-
transform: translateX(0);
|
|
256
|
-
font-weight: var(--weight-bold);
|
|
257
|
-
white-space: nowrap;
|
|
258
|
-
overflow: hidden;
|
|
259
|
-
text-overflow: ellipsis;
|
|
260
|
-
margin-left: var(--size-1);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
283
|
.edit {
|
|
264
284
|
opacity: 0;
|
|
265
285
|
pointer-events: none;
|
|
@@ -267,6 +287,8 @@
|
|
|
267
287
|
|
|
268
288
|
span :global(img) {
|
|
269
289
|
max-height: 100px;
|
|
290
|
+
max-width: 100%;
|
|
291
|
+
height: auto;
|
|
270
292
|
width: auto;
|
|
271
293
|
object-fit: contain;
|
|
272
294
|
}
|
|
@@ -278,8 +300,9 @@
|
|
|
278
300
|
.wrap,
|
|
279
301
|
.wrap.expanded {
|
|
280
302
|
white-space: normal;
|
|
281
|
-
|
|
303
|
+
overflow: visible;
|
|
304
|
+
text-overflow: clip;
|
|
282
305
|
overflow-wrap: break-word;
|
|
283
|
-
word-
|
|
306
|
+
word-break: break-word;
|
|
284
307
|
}
|
|
285
308
|
</style>
|
|
@@ -20,6 +20,7 @@ type $$ComponentProps = {
|
|
|
20
20
|
i18n: I18nFormatter;
|
|
21
21
|
is_dragging?: boolean;
|
|
22
22
|
wrap_text?: boolean;
|
|
23
|
+
expanded?: boolean;
|
|
23
24
|
show_selection_buttons?: boolean;
|
|
24
25
|
coords: [number, number];
|
|
25
26
|
on_select_column?: ((col: number) => void) | null;
|
|
@@ -30,6 +31,7 @@ type $$ComponentProps = {
|
|
|
30
31
|
coords: [number, number];
|
|
31
32
|
}) => void;
|
|
32
33
|
onkeydown?: (event: KeyboardEvent) => void;
|
|
34
|
+
pad_left: boolean;
|
|
33
35
|
};
|
|
34
36
|
declare const EditableCell: import("svelte").Component<$$ComponentProps, {}, "value" | "el">;
|
|
35
37
|
type EditableCell = ReturnType<typeof EditableCell>;
|