@gradio/dataframe 0.14.0 → 0.16.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 +45 -0
- package/Dataframe.stories.svelte +283 -7
- package/Index.svelte +22 -3
- package/dist/Index.svelte +18 -4
- package/dist/Index.svelte.d.ts +16 -0
- package/dist/shared/EditableCell.svelte +49 -7
- package/dist/shared/EditableCell.svelte.d.ts +1 -1
- package/dist/shared/Table.svelte +692 -411
- package/dist/shared/Table.svelte.d.ts +4 -0
- package/dist/shared/Toolbar.svelte +122 -30
- package/dist/shared/Toolbar.svelte.d.ts +4 -0
- package/dist/shared/VirtualTable.svelte +70 -26
- package/dist/shared/VirtualTable.svelte.d.ts +1 -0
- package/dist/shared/icons/FilterIcon.svelte +11 -0
- package/dist/shared/icons/FilterIcon.svelte.d.ts +16 -0
- package/dist/shared/icons/SortIcon.svelte +90 -0
- package/dist/shared/icons/SortIcon.svelte.d.ts +20 -0
- package/dist/shared/selection_utils.d.ts +30 -0
- package/dist/shared/selection_utils.js +139 -0
- package/dist/shared/types.d.ts +18 -0
- package/dist/shared/types.js +2 -0
- package/dist/shared/utils/menu_utils.d.ts +42 -0
- package/dist/shared/utils/menu_utils.js +58 -0
- package/dist/shared/utils/sort_utils.d.ts +7 -0
- package/dist/shared/utils/sort_utils.js +39 -0
- package/dist/shared/utils/table_utils.d.ts +12 -0
- package/dist/shared/utils/table_utils.js +148 -0
- package/package.json +8 -8
- package/shared/EditableCell.svelte +55 -7
- package/shared/Table.svelte +762 -478
- package/shared/Toolbar.svelte +125 -30
- package/shared/VirtualTable.svelte +73 -26
- package/shared/icons/FilterIcon.svelte +12 -0
- package/shared/icons/SortIcon.svelte +95 -0
- package/shared/selection_utils.ts +230 -0
- package/shared/types.ts +29 -0
- package/shared/utils/menu_utils.ts +115 -0
- package/shared/utils/sort_utils.test.ts +71 -0
- package/shared/utils/sort_utils.ts +55 -0
- package/shared/utils/table_utils.test.ts +114 -0
- package/shared/utils/table_utils.ts +206 -0
- package/dist/shared/table_utils.d.ts +0 -6
- package/dist/shared/table_utils.js +0 -27
- package/shared/table_utils.ts +0 -38
package/shared/Toolbar.svelte
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { Maximize, Minimize, Copy
|
|
2
|
+
import { Maximize, Minimize, Copy } from "@gradio/icons";
|
|
3
3
|
import { onDestroy } from "svelte";
|
|
4
|
+
import { createEventDispatcher } from "svelte";
|
|
5
|
+
import FilterIcon from "./icons/FilterIcon.svelte";
|
|
4
6
|
|
|
5
7
|
export let show_fullscreen_button = false;
|
|
6
8
|
export let show_copy_button = false;
|
|
9
|
+
export let show_search: "none" | "search" | "filter" = "none";
|
|
7
10
|
export let is_fullscreen = false;
|
|
8
11
|
export let on_copy: () => Promise<void>;
|
|
12
|
+
export let on_commit_filter: () => void;
|
|
13
|
+
|
|
14
|
+
const dispatch = createEventDispatcher<{
|
|
15
|
+
search: string | null;
|
|
16
|
+
}>();
|
|
9
17
|
|
|
10
18
|
let copied = false;
|
|
11
19
|
let timer: ReturnType<typeof setTimeout>;
|
|
20
|
+
export let current_search_query: string | null = null;
|
|
21
|
+
|
|
22
|
+
$: dispatch("search", current_search_query);
|
|
12
23
|
|
|
13
24
|
function copy_feedback(): void {
|
|
14
25
|
copied = true;
|
|
@@ -29,41 +40,70 @@
|
|
|
29
40
|
</script>
|
|
30
41
|
|
|
31
42
|
<div class="toolbar" role="toolbar" aria-label="Table actions">
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class="
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
43
|
+
<div class="toolbar-buttons">
|
|
44
|
+
{#if show_search !== "none"}
|
|
45
|
+
<div class="search-container">
|
|
46
|
+
<input
|
|
47
|
+
type="text"
|
|
48
|
+
bind:value={current_search_query}
|
|
49
|
+
placeholder="Search..."
|
|
50
|
+
class="search-input"
|
|
51
|
+
/>
|
|
52
|
+
{#if current_search_query && show_search === "filter"}
|
|
53
|
+
<button
|
|
54
|
+
class="toolbar-button check-button"
|
|
55
|
+
on:click={on_commit_filter}
|
|
56
|
+
aria-label="Apply filter and update dataframe values"
|
|
57
|
+
title="Apply filter and update dataframe values"
|
|
58
|
+
>
|
|
59
|
+
<FilterIcon />
|
|
60
|
+
</button>
|
|
61
|
+
{/if}
|
|
62
|
+
</div>
|
|
63
|
+
{/if}
|
|
64
|
+
{#if show_copy_button}
|
|
65
|
+
<button
|
|
66
|
+
class="toolbar-button"
|
|
67
|
+
on:click={handle_copy}
|
|
68
|
+
aria-label={copied ? "Copied to clipboard" : "Copy table data"}
|
|
69
|
+
title={copied ? "Copied to clipboard" : "Copy table data"}
|
|
70
|
+
>
|
|
71
|
+
{#if copied}
|
|
72
|
+
<FilterIcon />
|
|
73
|
+
{:else}
|
|
74
|
+
<Copy />
|
|
75
|
+
{/if}
|
|
76
|
+
</button>
|
|
77
|
+
{/if}
|
|
78
|
+
{#if show_fullscreen_button}
|
|
79
|
+
<button
|
|
80
|
+
class="toolbar-button"
|
|
81
|
+
on:click
|
|
82
|
+
aria-label={is_fullscreen ? "Exit fullscreen" : "Enter fullscreen"}
|
|
83
|
+
title={is_fullscreen ? "Exit fullscreen" : "Enter fullscreen"}
|
|
84
|
+
>
|
|
85
|
+
{#if is_fullscreen}
|
|
86
|
+
<Minimize />
|
|
87
|
+
{:else}
|
|
88
|
+
<Maximize />
|
|
89
|
+
{/if}
|
|
90
|
+
</button>
|
|
91
|
+
{/if}
|
|
92
|
+
</div>
|
|
60
93
|
</div>
|
|
61
94
|
|
|
62
95
|
<style>
|
|
63
96
|
.toolbar {
|
|
64
97
|
display: flex;
|
|
65
|
-
|
|
98
|
+
align-items: center;
|
|
99
|
+
gap: var(--size-2);
|
|
100
|
+
flex: 0 0 auto;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.toolbar-buttons {
|
|
104
|
+
display: flex;
|
|
66
105
|
gap: var(--size-1);
|
|
106
|
+
flex-wrap: nowrap;
|
|
67
107
|
}
|
|
68
108
|
|
|
69
109
|
.toolbar-button {
|
|
@@ -90,4 +130,59 @@
|
|
|
90
130
|
width: var(--size-4);
|
|
91
131
|
height: var(--size-4);
|
|
92
132
|
}
|
|
133
|
+
|
|
134
|
+
.search-container {
|
|
135
|
+
position: relative;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.search-input {
|
|
139
|
+
width: var(--size-full);
|
|
140
|
+
height: var(--size-6);
|
|
141
|
+
padding: var(--size-2);
|
|
142
|
+
padding-right: var(--size-8);
|
|
143
|
+
border: 1px solid var(--border-color-primary);
|
|
144
|
+
border-radius: var(--table-radius);
|
|
145
|
+
font-size: var(--text-sm);
|
|
146
|
+
color: var(--body-text-color);
|
|
147
|
+
background: var(--background-fill-secondary);
|
|
148
|
+
transition: all 0.2s ease;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.search-input:hover {
|
|
152
|
+
border-color: var(--border-color-secondary);
|
|
153
|
+
background: var(--background-fill-primary);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.search-input:focus {
|
|
157
|
+
outline: none;
|
|
158
|
+
border-color: var(--color-accent);
|
|
159
|
+
background: var(--background-fill-primary);
|
|
160
|
+
box-shadow: 0 0 0 1px var(--color-accent);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.check-button {
|
|
164
|
+
position: absolute;
|
|
165
|
+
right: var(--size-1);
|
|
166
|
+
top: 50%;
|
|
167
|
+
transform: translateY(-50%);
|
|
168
|
+
background: var(--color-accent);
|
|
169
|
+
color: white;
|
|
170
|
+
border: none;
|
|
171
|
+
width: var(--size-4);
|
|
172
|
+
height: var(--size-4);
|
|
173
|
+
border-radius: var(--radius-sm);
|
|
174
|
+
display: flex;
|
|
175
|
+
align-items: center;
|
|
176
|
+
justify-content: center;
|
|
177
|
+
padding: var(--size-1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.check-button :global(svg) {
|
|
181
|
+
width: var(--size-3);
|
|
182
|
+
height: var(--size-3);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.check-button:hover {
|
|
186
|
+
background: var(--color-accent-soft);
|
|
187
|
+
}
|
|
93
188
|
</style>
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
export let start = 0;
|
|
11
11
|
export let end = 20;
|
|
12
12
|
export let selected: number | false;
|
|
13
|
+
export let disable_scroll = false;
|
|
13
14
|
let height = "100%";
|
|
14
15
|
|
|
15
16
|
let average_height = 30;
|
|
@@ -41,6 +42,11 @@
|
|
|
41
42
|
return;
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
// force header height calculation first
|
|
46
|
+
head_height =
|
|
47
|
+
viewport.querySelector(".thead")?.getBoundingClientRect().height || 0;
|
|
48
|
+
await tick();
|
|
49
|
+
|
|
44
50
|
const { scrollTop } = viewport;
|
|
45
51
|
table_scrollbar_width = viewport.offsetWidth - viewport.clientWidth;
|
|
46
52
|
|
|
@@ -256,29 +262,32 @@
|
|
|
256
262
|
</script>
|
|
257
263
|
|
|
258
264
|
<svelte-virtual-table-viewport>
|
|
259
|
-
<
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
265
|
+
<div>
|
|
266
|
+
<table
|
|
267
|
+
class="table"
|
|
268
|
+
class:disable-scroll={disable_scroll}
|
|
269
|
+
bind:this={viewport}
|
|
270
|
+
bind:contentRect={viewport_box}
|
|
271
|
+
on:scroll={handle_scroll}
|
|
272
|
+
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"
|
|
273
|
+
>
|
|
274
|
+
<thead class="thead" bind:offsetHeight={head_height}>
|
|
275
|
+
<slot name="thead" />
|
|
276
|
+
</thead>
|
|
277
|
+
<tbody bind:this={contents} class="tbody">
|
|
278
|
+
{#if visible.length && visible[0].data.length}
|
|
279
|
+
{#each visible as item (item.data[0].id)}
|
|
280
|
+
<slot name="tbody" item={item.data} index={item.index}>
|
|
281
|
+
Missing Table Row
|
|
282
|
+
</slot>
|
|
283
|
+
{/each}
|
|
284
|
+
{/if}
|
|
285
|
+
</tbody>
|
|
286
|
+
<tfoot class="tfoot" bind:offsetHeight={foot_height}>
|
|
287
|
+
<slot name="tfoot" />
|
|
288
|
+
</tfoot>
|
|
289
|
+
</table>
|
|
290
|
+
</div>
|
|
282
291
|
</svelte-virtual-table-viewport>
|
|
283
292
|
|
|
284
293
|
<style type="text/css">
|
|
@@ -287,7 +296,7 @@
|
|
|
287
296
|
overflow-y: scroll;
|
|
288
297
|
overflow-x: scroll;
|
|
289
298
|
-webkit-overflow-scrolling: touch;
|
|
290
|
-
max-height:
|
|
299
|
+
max-height: var(--max-height);
|
|
291
300
|
box-sizing: border-box;
|
|
292
301
|
display: block;
|
|
293
302
|
padding: 0;
|
|
@@ -335,11 +344,49 @@
|
|
|
335
344
|
background: var(--table-even-background-fill);
|
|
336
345
|
}
|
|
337
346
|
|
|
347
|
+
tbody :global(td.frozen-column) {
|
|
348
|
+
position: sticky;
|
|
349
|
+
z-index: var(--layer-2);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
tbody :global(tr:nth-child(odd)) :global(td.frozen-column) {
|
|
353
|
+
background: var(--table-odd-background-fill);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
tbody :global(tr:nth-child(even)) :global(td.frozen-column) {
|
|
357
|
+
background: var(--table-even-background-fill);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
tbody :global(td.always-frozen) {
|
|
361
|
+
z-index: var(--layer-3);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
tbody :global(td.last-frozen) {
|
|
365
|
+
border-right: 2px solid var(--border-color-primary);
|
|
366
|
+
}
|
|
367
|
+
|
|
338
368
|
thead {
|
|
339
369
|
position: sticky;
|
|
340
370
|
top: 0;
|
|
341
371
|
left: 0;
|
|
342
|
-
z-index: var(--layer-
|
|
343
|
-
|
|
372
|
+
z-index: var(--layer-3);
|
|
373
|
+
background: var(--background-fill-primary);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
thead :global(th) {
|
|
377
|
+
background: var(--table-even-background-fill) !important;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
thead :global(th.frozen-column) {
|
|
381
|
+
position: sticky;
|
|
382
|
+
z-index: var(--layer-4);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
thead :global(th.always-frozen) {
|
|
386
|
+
z-index: var(--layer-5);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.table.disable-scroll {
|
|
390
|
+
overflow: hidden !important;
|
|
344
391
|
}
|
|
345
392
|
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
</script>
|
|
3
|
+
|
|
4
|
+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
5
|
+
<path
|
|
6
|
+
d="M4 4h16v2.67l-6.67 6.67v8L9.33 19v-5.66L2.67 6.67V4h1.33z"
|
|
7
|
+
stroke="currentColor"
|
|
8
|
+
stroke-width="2"
|
|
9
|
+
stroke-linecap="round"
|
|
10
|
+
stroke-linejoin="round"
|
|
11
|
+
/>
|
|
12
|
+
</svg>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { createEventDispatcher } from "svelte";
|
|
3
|
+
import type { I18nFormatter } from "@gradio/utils";
|
|
4
|
+
|
|
5
|
+
type SortDirection = "asc" | "des";
|
|
6
|
+
export let direction: SortDirection | null = null;
|
|
7
|
+
export let i18n: I18nFormatter;
|
|
8
|
+
|
|
9
|
+
const dispatch = createEventDispatcher<{ sort: SortDirection }>();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<div class="sort-icons" role="group" aria-label={i18n("dataframe.sort_column")}>
|
|
13
|
+
<button
|
|
14
|
+
class="sort-button up"
|
|
15
|
+
class:active={direction === "asc"}
|
|
16
|
+
on:click={() => dispatch("sort", "asc")}
|
|
17
|
+
aria-label={i18n("dataframe.sort_ascending")}
|
|
18
|
+
aria-pressed={direction === "asc"}
|
|
19
|
+
>
|
|
20
|
+
<svg
|
|
21
|
+
viewBox="0 0 24 24"
|
|
22
|
+
fill="none"
|
|
23
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
24
|
+
aria-hidden="true"
|
|
25
|
+
focusable="false"
|
|
26
|
+
>
|
|
27
|
+
<path
|
|
28
|
+
d="M7 14l5-5 5 5"
|
|
29
|
+
stroke="currentColor"
|
|
30
|
+
stroke-width="2"
|
|
31
|
+
stroke-linecap="round"
|
|
32
|
+
stroke-linejoin="round"
|
|
33
|
+
/>
|
|
34
|
+
</svg>
|
|
35
|
+
</button>
|
|
36
|
+
<button
|
|
37
|
+
class="sort-button down"
|
|
38
|
+
class:active={direction === "des"}
|
|
39
|
+
on:click={() => dispatch("sort", "des")}
|
|
40
|
+
aria-label={i18n("dataframe.sort_descending")}
|
|
41
|
+
aria-pressed={direction === "des"}
|
|
42
|
+
>
|
|
43
|
+
<svg
|
|
44
|
+
viewBox="0 0 24 24"
|
|
45
|
+
fill="none"
|
|
46
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
47
|
+
aria-hidden="true"
|
|
48
|
+
focusable="false"
|
|
49
|
+
>
|
|
50
|
+
<path
|
|
51
|
+
d="M7 10l5 5 5-5"
|
|
52
|
+
stroke="currentColor"
|
|
53
|
+
stroke-width="2"
|
|
54
|
+
stroke-linecap="round"
|
|
55
|
+
stroke-linejoin="round"
|
|
56
|
+
/>
|
|
57
|
+
</svg>
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<style>
|
|
62
|
+
.sort-icons {
|
|
63
|
+
display: flex;
|
|
64
|
+
flex-direction: column;
|
|
65
|
+
gap: 0;
|
|
66
|
+
margin-right: var(--spacing-md);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.sort-button {
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
justify-content: center;
|
|
73
|
+
padding: 0;
|
|
74
|
+
background: none;
|
|
75
|
+
border: none;
|
|
76
|
+
cursor: pointer;
|
|
77
|
+
opacity: 0.5;
|
|
78
|
+
transition: opacity 150ms;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.sort-button:hover {
|
|
82
|
+
opacity: 0.8;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.sort-button.active {
|
|
86
|
+
opacity: 1;
|
|
87
|
+
color: var(--color-accent);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
svg {
|
|
91
|
+
width: var(--size-3);
|
|
92
|
+
height: var(--size-3);
|
|
93
|
+
display: block;
|
|
94
|
+
}
|
|
95
|
+
</style>
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import type { CellCoordinate, EditingState } from "./types";
|
|
2
|
+
|
|
3
|
+
export type CellData = { id: string; value: string | number };
|
|
4
|
+
|
|
5
|
+
export function is_cell_selected(
|
|
6
|
+
cell: CellCoordinate,
|
|
7
|
+
selected_cells: CellCoordinate[]
|
|
8
|
+
): string {
|
|
9
|
+
const [row, col] = cell;
|
|
10
|
+
if (!selected_cells.some(([r, c]) => r === row && c === col)) return "";
|
|
11
|
+
|
|
12
|
+
const up = selected_cells.some(([r, c]) => r === row - 1 && c === col);
|
|
13
|
+
const down = selected_cells.some(([r, c]) => r === row + 1 && c === col);
|
|
14
|
+
const left = selected_cells.some(([r, c]) => r === row && c === col - 1);
|
|
15
|
+
const right = selected_cells.some(([r, c]) => r === row && c === col + 1);
|
|
16
|
+
|
|
17
|
+
return `cell-selected${up ? " no-top" : ""}${down ? " no-bottom" : ""}${left ? " no-left" : ""}${right ? " no-right" : ""}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function get_range_selection(
|
|
21
|
+
start: CellCoordinate,
|
|
22
|
+
end: CellCoordinate
|
|
23
|
+
): CellCoordinate[] {
|
|
24
|
+
const [start_row, start_col] = start;
|
|
25
|
+
const [end_row, end_col] = end;
|
|
26
|
+
const min_row = Math.min(start_row, end_row);
|
|
27
|
+
const max_row = Math.max(start_row, end_row);
|
|
28
|
+
const min_col = Math.min(start_col, end_col);
|
|
29
|
+
const max_col = Math.max(start_col, end_col);
|
|
30
|
+
|
|
31
|
+
const cells: CellCoordinate[] = [];
|
|
32
|
+
for (let i = min_row; i <= max_row; i++) {
|
|
33
|
+
for (let j = min_col; j <= max_col; j++) {
|
|
34
|
+
cells.push([i, j]);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return cells;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function handle_selection(
|
|
41
|
+
current: CellCoordinate,
|
|
42
|
+
selected_cells: CellCoordinate[],
|
|
43
|
+
event: { shiftKey: boolean; metaKey: boolean; ctrlKey: boolean }
|
|
44
|
+
): CellCoordinate[] {
|
|
45
|
+
if (event.shiftKey && selected_cells.length > 0) {
|
|
46
|
+
return get_range_selection(
|
|
47
|
+
selected_cells[selected_cells.length - 1],
|
|
48
|
+
current
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (event.metaKey || event.ctrlKey) {
|
|
53
|
+
const is_cell_match = ([r, c]: CellCoordinate): boolean =>
|
|
54
|
+
r === current[0] && c === current[1];
|
|
55
|
+
const index = selected_cells.findIndex(is_cell_match);
|
|
56
|
+
return index === -1
|
|
57
|
+
? [...selected_cells, current]
|
|
58
|
+
: selected_cells.filter((_, i) => i !== index);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return [current];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function handle_delete_key(
|
|
65
|
+
data: CellData[][],
|
|
66
|
+
selected_cells: CellCoordinate[]
|
|
67
|
+
): CellData[][] {
|
|
68
|
+
const new_data = data.map((row) => [...row]);
|
|
69
|
+
selected_cells.forEach(([row, col]) => {
|
|
70
|
+
if (new_data[row] && new_data[row][col]) {
|
|
71
|
+
new_data[row][col] = { ...new_data[row][col], value: "" };
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
return new_data;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function handle_editing_state(
|
|
78
|
+
current: CellCoordinate,
|
|
79
|
+
editing: EditingState,
|
|
80
|
+
selected_cells: CellCoordinate[],
|
|
81
|
+
editable: boolean
|
|
82
|
+
): EditingState {
|
|
83
|
+
const [row, col] = current;
|
|
84
|
+
if (!editable) return false;
|
|
85
|
+
|
|
86
|
+
if (editing && editing[0] === row && editing[1] === col) return editing;
|
|
87
|
+
|
|
88
|
+
if (
|
|
89
|
+
selected_cells.length === 1 &&
|
|
90
|
+
selected_cells[0][0] === row &&
|
|
91
|
+
selected_cells[0][1] === col
|
|
92
|
+
) {
|
|
93
|
+
return [row, col];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function should_show_cell_menu(
|
|
100
|
+
cell: CellCoordinate,
|
|
101
|
+
selected_cells: CellCoordinate[],
|
|
102
|
+
editable: boolean
|
|
103
|
+
): boolean {
|
|
104
|
+
const [row, col] = cell;
|
|
105
|
+
return (
|
|
106
|
+
editable &&
|
|
107
|
+
selected_cells.length === 1 &&
|
|
108
|
+
selected_cells[0][0] === row &&
|
|
109
|
+
selected_cells[0][1] === col
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function get_next_cell_coordinates(
|
|
114
|
+
current: CellCoordinate,
|
|
115
|
+
data: CellData[][],
|
|
116
|
+
shift_key: boolean
|
|
117
|
+
): CellCoordinate | false {
|
|
118
|
+
const [row, col] = current;
|
|
119
|
+
const direction = shift_key ? -1 : 1;
|
|
120
|
+
|
|
121
|
+
if (data[row]?.[col + direction]) {
|
|
122
|
+
return [row, col + direction];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const next_row = row + (direction > 0 ? 1 : 0);
|
|
126
|
+
const prev_row = row + (direction < 0 ? -1 : 0);
|
|
127
|
+
|
|
128
|
+
if (direction > 0 && data[next_row]?.[0]) {
|
|
129
|
+
return [next_row, 0];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (direction < 0 && data[prev_row]?.[data[0].length - 1]) {
|
|
133
|
+
return [prev_row, data[0].length - 1];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function move_cursor(
|
|
140
|
+
key: "ArrowRight" | "ArrowLeft" | "ArrowDown" | "ArrowUp",
|
|
141
|
+
current_coords: CellCoordinate,
|
|
142
|
+
data: CellData[][]
|
|
143
|
+
): CellCoordinate | false {
|
|
144
|
+
const dir = {
|
|
145
|
+
ArrowRight: [0, 1],
|
|
146
|
+
ArrowLeft: [0, -1],
|
|
147
|
+
ArrowDown: [1, 0],
|
|
148
|
+
ArrowUp: [-1, 0]
|
|
149
|
+
}[key];
|
|
150
|
+
|
|
151
|
+
const i = current_coords[0] + dir[0];
|
|
152
|
+
const j = current_coords[1] + dir[1];
|
|
153
|
+
|
|
154
|
+
if (i < 0 && j <= 0) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const is_data = data[i]?.[j];
|
|
159
|
+
if (is_data) {
|
|
160
|
+
return [i, j];
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function get_current_indices(
|
|
166
|
+
id: string,
|
|
167
|
+
data: CellData[][]
|
|
168
|
+
): [number, number] {
|
|
169
|
+
return data.reduce(
|
|
170
|
+
(acc, arr, i) => {
|
|
171
|
+
const j = arr.reduce(
|
|
172
|
+
(_acc, _data, k) => (id === _data.id ? k : _acc),
|
|
173
|
+
-1
|
|
174
|
+
);
|
|
175
|
+
return j === -1 ? acc : [i, j];
|
|
176
|
+
},
|
|
177
|
+
[-1, -1]
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function handle_click_outside(
|
|
182
|
+
event: Event,
|
|
183
|
+
parent: HTMLElement
|
|
184
|
+
): boolean {
|
|
185
|
+
const [trigger] = event.composedPath() as HTMLElement[];
|
|
186
|
+
return !parent.contains(trigger);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function select_column(data: any[][], col: number): CellCoordinate[] {
|
|
190
|
+
return Array.from({ length: data.length }, (_, i) => [i, col]);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function select_row(data: any[][], row: number): CellCoordinate[] {
|
|
194
|
+
return Array.from({ length: data[0].length }, (_, i) => [row, i]);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function calculate_selection_positions(
|
|
198
|
+
selected: CellCoordinate,
|
|
199
|
+
data: { id: string; value: string | number }[][],
|
|
200
|
+
els: Record<string, { cell: HTMLTableCellElement | null }>,
|
|
201
|
+
parent: HTMLElement,
|
|
202
|
+
table: HTMLElement
|
|
203
|
+
): { col_pos: string; row_pos: string | undefined } {
|
|
204
|
+
const [row, col] = selected;
|
|
205
|
+
if (!data[row]?.[col]) {
|
|
206
|
+
return { col_pos: "0px", row_pos: undefined };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let offset = 0;
|
|
210
|
+
for (let i = 0; i < col; i++) {
|
|
211
|
+
offset += parseFloat(
|
|
212
|
+
getComputedStyle(parent).getPropertyValue(`--cell-width-${i}`)
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const cell_id = data[row][col].id;
|
|
217
|
+
const cell_el = els[cell_id]?.cell;
|
|
218
|
+
|
|
219
|
+
if (!cell_el) {
|
|
220
|
+
// if we cant get the row position, just return the column position which is static
|
|
221
|
+
return { col_pos: "0px", row_pos: undefined };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const cell_rect = cell_el.getBoundingClientRect();
|
|
225
|
+
const table_rect = table.getBoundingClientRect();
|
|
226
|
+
const col_pos = `${cell_rect.left - table_rect.left + cell_rect.width / 2}px`;
|
|
227
|
+
const relative_top = cell_rect.top - table_rect.top;
|
|
228
|
+
const row_pos = `${relative_top + cell_rect.height / 2}px`;
|
|
229
|
+
return { col_pos, row_pos };
|
|
230
|
+
}
|
package/shared/types.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type CellCoordinate = [number, number];
|
|
2
|
+
export type EditingState = CellCoordinate | false;
|
|
3
|
+
|
|
4
|
+
export type Headers = (string | null)[];
|
|
5
|
+
|
|
6
|
+
export interface HeadersWithIDs {
|
|
7
|
+
id: string;
|
|
8
|
+
value: string;
|
|
9
|
+
}
|
|
10
|
+
[];
|
|
11
|
+
|
|
12
|
+
export interface TableCell {
|
|
13
|
+
id: string;
|
|
14
|
+
value: string | number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type TableData = TableCell[][];
|
|
18
|
+
|
|
19
|
+
export type CountConfig = [number, "fixed" | "dynamic"];
|
|
20
|
+
|
|
21
|
+
export type ElementRefs = Record<
|
|
22
|
+
string,
|
|
23
|
+
{
|
|
24
|
+
cell: null | HTMLTableCellElement;
|
|
25
|
+
input: null | HTMLInputElement;
|
|
26
|
+
}
|
|
27
|
+
>;
|
|
28
|
+
|
|
29
|
+
export type DataBinding = Record<string, TableCell>;
|