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 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
@@ -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, column: string) {
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
- {#each table.positions.sticky as column, i (column)}
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="rows" {onscroll}>
160
- {#each data as item}
161
- <div class='row'>
162
- {#each table.positions.sticky as column, i (column)}
163
- {#if !table.positions.hidden.includes(column)}
164
- {@const col = table.columns[column]}
165
- <div class='column sticky' class:border={i == table.positions.sticky.length - 1}>
166
- {@render col.row(item, col.options.value ? col.options.value(item) : undefined)}
167
- </div>
168
- {/if}
169
- {/each}
170
- {#each table.positions.scroll as column, i (column)}
171
- {#if !table.positions.hidden.includes(column)}
172
- {@const col = table.columns[column]}
173
- <div class='column'>
174
- {@render col.row(item, col.options.value ? col.options.value(item) : undefined)}
175
- </div>
176
- {/if}
177
- {/each}
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
- {#each table.positions.sticky as column, i (column)}
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: white;
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 hsla(0, 0%, 90%);
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 hsla(0, 0%, 90%);
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 hsla(0, 0%, 90%);
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: hsla(0, 0%, 100%);
278
- border-bottom: 1px solid hsla(0, 0%, 90%);
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
- .rows {
327
+ .content {
282
328
  grid-area: rows;
283
329
  display: grid;
284
- overflow: auto;
285
330
  scrollbar-width: thin;
286
- background-color: hsla(0, 0%, 100%);
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: hsla(0, 0%, 99%);
297
- border-top: 1px solid hsla(0, 0%, 90%);
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: white;
386
+ background-color: var(--tably-bg, hsl(0, 0%, 100%));
335
387
 
336
- border-left: 1px solid hsla(0, 0%, 90%);
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: hidden;
344
- padding: var(--padding-y) var(--padding-x);
397
+ overflow: auto;
398
+ padding: var(--padding-y) 0;
345
399
  }
346
400
  }
347
401
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-tably",
3
- "version": "1.0.0-next.1",
3
+ "version": "1.0.0-next.3",
4
4
  "repository": "github:refzlund/svelte-tably",
5
5
  "homepage": "https://github.com/Refzlund/svelte-tably",
6
6
  "bugs": {