svelte-tably 1.0.0-next.1 → 1.0.0-next.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/README.md +2 -1
- package/dist/Table/Table.svelte +128 -74
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,8 +11,9 @@ A high performant dynamic table
|
|
|
11
11
|
- [x] Re-order columns
|
|
12
12
|
- [x] Resize columns
|
|
13
13
|
- [x] Statusbar
|
|
14
|
-
- [x] Virtual data (for sorting/filtering)
|
|
14
|
+
- [x] "Virtual" data (for sorting/filtering)
|
|
15
15
|
- [x] Panels
|
|
16
|
+
- [x] Virtual elements
|
|
16
17
|
- [ ] sorting
|
|
17
18
|
- [ ] select
|
|
18
19
|
- [ ] filtering
|
package/dist/Table/Table.svelte
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
|
|
34
34
|
<script lang='ts' generics='T extends Record<PropertyKey, unknown>'>
|
|
35
35
|
|
|
36
|
-
import { getContext, setContext, type Snippet } from 'svelte'
|
|
36
|
+
import { getContext, setContext, untrack, type Snippet } from 'svelte'
|
|
37
37
|
import { type Column } from './Column.svelte'
|
|
38
38
|
import { PanelTween, type Panel } from './Panel.svelte'
|
|
39
39
|
import { fly } from 'svelte/transition'
|
|
@@ -55,6 +55,50 @@
|
|
|
55
55
|
|
|
56
56
|
const data = $derived(_data.toSorted())
|
|
57
57
|
|
|
58
|
+
const elements = $state({}) as Record<'headers' | 'statusbar' | 'content', HTMLElement>
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
// * --- Virtualization --- *
|
|
62
|
+
let scrollTop = $state(0)
|
|
63
|
+
let viewportHeight = $state(0)
|
|
64
|
+
|
|
65
|
+
let _heightPerItem = 24
|
|
66
|
+
let renderItemLength = $derived(Math.ceil(Math.max(30, viewportHeight / (_heightPerItem / 3))))
|
|
67
|
+
|
|
68
|
+
let heightPerItem = $derived.by(() => {
|
|
69
|
+
if(!elements.content)
|
|
70
|
+
return 8
|
|
71
|
+
const rows = elements.content.querySelectorAll('.row') as NodeListOf<HTMLDivElement>
|
|
72
|
+
const result = ((
|
|
73
|
+
rows[rows.length - 1].offsetTop - rows[0].offsetTop
|
|
74
|
+
) / rows.length)
|
|
75
|
+
_heightPerItem = result
|
|
76
|
+
return result
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
let virtualTop = $derived.by(() => {
|
|
80
|
+
let spacing = untrack(() => (renderItemLength/3)) * heightPerItem
|
|
81
|
+
let scroll = scrollTop - spacing
|
|
82
|
+
let virtualTop = Math.max(scroll, 0)
|
|
83
|
+
virtualTop -= virtualTop % untrack(() => heightPerItem)
|
|
84
|
+
return virtualTop
|
|
85
|
+
})
|
|
86
|
+
let virtualBottom = $derived.by(() => {
|
|
87
|
+
const virtualBottom = (untrack(() => heightPerItem) * data.length) - virtualTop
|
|
88
|
+
return virtualBottom
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
/** The area of data that is rendered */
|
|
92
|
+
const area = $derived.by(() => {
|
|
93
|
+
const index = (virtualTop / untrack(() => heightPerItem)) || 0
|
|
94
|
+
return data.slice(
|
|
95
|
+
index,
|
|
96
|
+
index + untrack(() => renderItemLength)
|
|
97
|
+
)
|
|
98
|
+
})
|
|
99
|
+
// * --- Virtualization --- *
|
|
100
|
+
|
|
101
|
+
|
|
58
102
|
const table: TableState<T> = $state({
|
|
59
103
|
columns: {},
|
|
60
104
|
panels: {},
|
|
@@ -99,7 +143,6 @@
|
|
|
99
143
|
// * --- *
|
|
100
144
|
|
|
101
145
|
const panelTween = new PanelTween(() => panel)
|
|
102
|
-
const elements = $state({}) as Record<'headers' | 'statusbar', HTMLElement>
|
|
103
146
|
|
|
104
147
|
/** Order of columns */
|
|
105
148
|
const columns = $derived([...table.positions.sticky, ...table.positions.scroll].filter(key => !table.positions.hidden.includes(key)))
|
|
@@ -109,7 +152,7 @@
|
|
|
109
152
|
|
|
110
153
|
/** grid-template-columns for widths */
|
|
111
154
|
const style = $derived(`
|
|
112
|
-
#${id} > .headers, #${id} > .rows > .row, #${id} > .statusbar {
|
|
155
|
+
#${id} > .headers, #${id} > .content > .rows > .row, #${id} > .statusbar, #${id} > .content > .virtual.bottom {
|
|
113
156
|
grid-template-columns: ${columns.map((key, i, arr) => i === arr.length - 1 ? `minmax(${widths[key] || 150}px, 1fr)` : `${widths[key] || 150}px`).join(' ')};
|
|
114
157
|
}
|
|
115
158
|
`)
|
|
@@ -119,13 +162,19 @@
|
|
|
119
162
|
widths[target.getAttribute('data-column')!] = parseFloat(target.style.width)
|
|
120
163
|
})
|
|
121
164
|
|
|
122
|
-
function observe(node: HTMLDivElement,
|
|
165
|
+
function observe(node: HTMLDivElement, isHeader = false) {
|
|
166
|
+
if(!isHeader) return
|
|
123
167
|
observer?.observe(node, {attributes: true})
|
|
124
168
|
return { destroy: () => observer?.disconnect() }
|
|
125
169
|
}
|
|
126
170
|
|
|
127
171
|
function onscroll(event: Event) {
|
|
128
172
|
const target = event.target as HTMLDivElement
|
|
173
|
+
if(target.scrollTop !== scrollTop) {
|
|
174
|
+
scrollTop = target.scrollTop || 0
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if(!elements.headers) return
|
|
129
178
|
elements.headers.scrollLeft = target.scrollLeft
|
|
130
179
|
elements.statusbar.scrollLeft = target.scrollLeft
|
|
131
180
|
}
|
|
@@ -137,66 +186,63 @@
|
|
|
137
186
|
{@html `<style>${style}</style>`}
|
|
138
187
|
</svelte:head>
|
|
139
188
|
|
|
189
|
+
{#snippet columnsSnippet(
|
|
190
|
+
renderable: (column: string) => Snippet<[arg0?: any, arg1?: any, arg2?: any, arg3?: any]> | undefined,
|
|
191
|
+
arg: null | ((column: string) => any[]) = null,
|
|
192
|
+
isHeader = false
|
|
193
|
+
)}
|
|
194
|
+
{#each table.positions.sticky as column, i (column)}
|
|
195
|
+
{#if !table.positions.hidden.includes(column)}
|
|
196
|
+
{@const args = arg ? arg(column) : []}
|
|
197
|
+
{@const props = isHeader ? { 'data-column': column } : {}}
|
|
198
|
+
<div class='column sticky' {...props} use:observe={isHeader} class:border={i == table.positions.sticky.length - 1}>
|
|
199
|
+
{@render renderable(column)?.(args[0], args[1], args[2], args[3])}
|
|
200
|
+
</div>
|
|
201
|
+
{/if}
|
|
202
|
+
{/each}
|
|
203
|
+
{#each table.positions.scroll as column, i (column)}
|
|
204
|
+
{#if !table.positions.hidden.includes(column)}
|
|
205
|
+
{@const args = arg ? arg(column) : []}
|
|
206
|
+
{@const props = isHeader ? { 'data-column': column } : {}}
|
|
207
|
+
<div class='column' {...props} use:observe={isHeader}>
|
|
208
|
+
{@render renderable(column)?.(args[0], args[1], args[2], args[3])}
|
|
209
|
+
</div>
|
|
210
|
+
{/if}
|
|
211
|
+
{/each}
|
|
212
|
+
{/snippet}
|
|
213
|
+
|
|
140
214
|
<div id={id} class='table'>
|
|
141
215
|
|
|
142
216
|
<div class='headers' bind:this={elements.headers}>
|
|
143
|
-
{
|
|
144
|
-
{#if !table.positions.hidden.includes(column)}
|
|
145
|
-
<div class='column sticky' data-column="{column}" use:observe={column}>
|
|
146
|
-
{@render table.columns[column]?.header()}
|
|
147
|
-
</div>
|
|
148
|
-
{/if}
|
|
149
|
-
{/each}
|
|
150
|
-
{#each table.positions.scroll as column, i (column)}
|
|
151
|
-
{#if !table.positions.hidden.includes(column)}
|
|
152
|
-
<div class='column' use:observe={column}>
|
|
153
|
-
{@render table.columns[column]?.header()}
|
|
154
|
-
</div>
|
|
155
|
-
{/if}
|
|
156
|
-
{/each}
|
|
217
|
+
{@render columnsSnippet((column) => table.columns[column]?.header, null, true)}
|
|
157
218
|
</div>
|
|
158
219
|
|
|
159
|
-
<div class=
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
</div>
|
|
179
|
-
{/each}
|
|
220
|
+
<div class='content' {onscroll} bind:clientHeight={viewportHeight} bind:this={elements.content}>
|
|
221
|
+
<div class='virtual top' style='height: {virtualTop}px'></div>
|
|
222
|
+
|
|
223
|
+
<div class="rows">
|
|
224
|
+
{#each area as item, i (item)}
|
|
225
|
+
<div class='row'>
|
|
226
|
+
{@render columnsSnippet(
|
|
227
|
+
(column) => table.columns[column]!.row,
|
|
228
|
+
(column) => {
|
|
229
|
+
const col = table.columns[column]!
|
|
230
|
+
return [item, col.options.value ? col.options.value(item) : undefined]
|
|
231
|
+
}
|
|
232
|
+
)}
|
|
233
|
+
</div>
|
|
234
|
+
{/each}
|
|
235
|
+
</div>
|
|
236
|
+
<div class='virtual bottom' style='height: {virtualBottom}px'>
|
|
237
|
+
{@render columnsSnippet(() => undefined)}
|
|
238
|
+
</div>
|
|
180
239
|
</div>
|
|
181
240
|
|
|
182
241
|
<div class='statusbar' bind:this={elements.statusbar}>
|
|
183
|
-
{
|
|
184
|
-
{#if !table.positions.hidden.includes(column)}
|
|
185
|
-
<div class='column sticky' class:border={i == table.positions.sticky.length - 1}>
|
|
186
|
-
{@render table.columns[column]?.statusbar?.(data)}
|
|
187
|
-
</div>
|
|
188
|
-
{/if}
|
|
189
|
-
{/each}
|
|
190
|
-
{#each table.positions.scroll as column, i (column)}
|
|
191
|
-
{#if !table.positions.hidden.includes(column)}
|
|
192
|
-
<div class='column'>
|
|
193
|
-
{@render table.columns[column]?.statusbar?.(data)}
|
|
194
|
-
</div>
|
|
195
|
-
{/if}
|
|
196
|
-
{/each}
|
|
242
|
+
{@render columnsSnippet((column) => table.columns[column]?.statusbar, () => [data])}
|
|
197
243
|
</div>
|
|
198
244
|
|
|
199
|
-
<div class='panel' style='width: {panelTween.current}px;' style:overflow={panelTween.transitioning ? 'hidden' : 'auto'}>
|
|
245
|
+
<div class='panel' style='width: {panelTween.current + 30}px;' style:overflow={panelTween.transitioning ? 'hidden' : 'auto'}>
|
|
200
246
|
{#if panel && panel in table.panels}
|
|
201
247
|
<div
|
|
202
248
|
class="panel-content"
|
|
@@ -226,16 +272,16 @@
|
|
|
226
272
|
position: sticky;
|
|
227
273
|
left: 0px;
|
|
228
274
|
/* right: 100px; */
|
|
229
|
-
background-color:
|
|
275
|
+
background-color: var(--tably-bg, hsl(0, 0%, 100%));
|
|
230
276
|
z-index: 1;
|
|
231
277
|
}
|
|
232
278
|
|
|
233
279
|
.sticky.border {
|
|
234
|
-
border-right: 1px solid
|
|
280
|
+
border-right: 1px solid var(--tably-border, hsl(0, 0%, 90%));
|
|
235
281
|
}
|
|
236
282
|
|
|
237
283
|
.headers > .column {
|
|
238
|
-
border-right: 1px solid
|
|
284
|
+
border-right: 1px solid var(--tably-border, hsl(0, 0%, 90%));
|
|
239
285
|
resize: horizontal;
|
|
240
286
|
overflow: hidden;
|
|
241
287
|
padding: var(--padding-y) 0;
|
|
@@ -249,6 +295,7 @@
|
|
|
249
295
|
--header-height: 2.5rem;
|
|
250
296
|
|
|
251
297
|
display: grid;
|
|
298
|
+
height: 100%;
|
|
252
299
|
|
|
253
300
|
grid-template-areas:
|
|
254
301
|
"headers panel"
|
|
@@ -259,7 +306,7 @@
|
|
|
259
306
|
grid-template-columns: auto min-content;
|
|
260
307
|
grid-template-rows: auto 1fr auto;
|
|
261
308
|
|
|
262
|
-
border: 1px solid
|
|
309
|
+
border: 1px solid var(--tably-border, hsl(0, 0%, 90%));
|
|
263
310
|
border-radius: .25rem;
|
|
264
311
|
|
|
265
312
|
max-height: 100%;
|
|
@@ -269,32 +316,38 @@
|
|
|
269
316
|
grid-area: headers;
|
|
270
317
|
z-index: 2;
|
|
271
318
|
overflow: hidden;
|
|
272
|
-
padding-right: 1rem;
|
|
273
319
|
}
|
|
274
320
|
|
|
275
321
|
.headers > .column {
|
|
276
322
|
width: auto !important;
|
|
277
|
-
background-color:
|
|
278
|
-
border-bottom: 1px solid
|
|
323
|
+
background-color: var(--tably-bg, hsl(0, 0%, 100%));
|
|
324
|
+
border-bottom: 1px solid var(--tably-border, hsl(0, 0%, 90%));
|
|
279
325
|
}
|
|
280
326
|
|
|
281
|
-
.
|
|
327
|
+
.content {
|
|
282
328
|
grid-area: rows;
|
|
283
329
|
display: grid;
|
|
284
|
-
overflow: auto;
|
|
285
330
|
scrollbar-width: thin;
|
|
286
|
-
|
|
331
|
+
overflow: auto;
|
|
332
|
+
height: 100%;
|
|
333
|
+
grid-template-rows: auto auto 1fr;
|
|
334
|
+
|
|
335
|
+
> .rows, > .virtual.bottom {
|
|
336
|
+
display: grid;
|
|
337
|
+
}
|
|
338
|
+
> .virtual.bottom {
|
|
339
|
+
min-height: 100%;
|
|
340
|
+
}
|
|
287
341
|
}
|
|
288
342
|
|
|
289
343
|
.statusbar {
|
|
290
344
|
grid-area: statusbar;
|
|
291
345
|
overflow: hidden;
|
|
292
|
-
padding-right: 1rem;
|
|
293
346
|
}
|
|
294
347
|
|
|
295
348
|
.statusbar > .column {
|
|
296
|
-
background-color:
|
|
297
|
-
border-top: 1px solid
|
|
349
|
+
background-color: var(--tably-bg-statusbar, hsl(0, 0%, 99%));
|
|
350
|
+
border-top: 1px solid var(--tably-border, hsl(0, 0%, 90%));
|
|
298
351
|
padding: calc(var(--padding-y) / 2) 0;
|
|
299
352
|
}
|
|
300
353
|
|
|
@@ -315,12 +368,12 @@
|
|
|
315
368
|
}
|
|
316
369
|
}
|
|
317
370
|
|
|
318
|
-
.row:nth-child(1) > * {
|
|
371
|
+
/* .row:nth-child(1) > * {
|
|
319
372
|
padding-top: calc(var(--padding-y) + var(--gap));
|
|
320
373
|
}
|
|
321
374
|
.row:nth-last-child(1) > * {
|
|
322
375
|
padding-bottom: calc(var(--padding-y) + var(--gap));
|
|
323
|
-
}
|
|
376
|
+
} */
|
|
324
377
|
|
|
325
378
|
.row > * {
|
|
326
379
|
padding: var(--gap) 0;
|
|
@@ -329,19 +382,20 @@
|
|
|
329
382
|
.panel {
|
|
330
383
|
position: relative;
|
|
331
384
|
grid-area: panel;
|
|
332
|
-
width: var(--panel);
|
|
333
385
|
height: 100%;
|
|
334
|
-
background-color:
|
|
386
|
+
background-color: var(--tably-bg, hsl(0, 0%, 100%));
|
|
335
387
|
|
|
336
|
-
border-left: 1px solid
|
|
388
|
+
border-left: 1px solid var(--tably-border, hsl(0, 0%, 90%));
|
|
389
|
+
scrollbar-gutter: stable both-edges;
|
|
390
|
+
scrollbar-width: thin;
|
|
337
391
|
|
|
338
392
|
> .panel-content {
|
|
339
393
|
position: absolute;
|
|
340
394
|
top: 0;
|
|
341
395
|
right: 0;
|
|
342
396
|
width: min-content;
|
|
343
|
-
overflow:
|
|
344
|
-
padding: var(--padding-y)
|
|
397
|
+
overflow: auto;
|
|
398
|
+
padding: var(--padding-y) 0;
|
|
345
399
|
}
|
|
346
400
|
}
|
|
347
401
|
|