svelte-tably 1.0.0-next.9 → 1.0.1-next.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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +299 -93
  3. package/dist/column/Column.svelte +39 -0
  4. package/dist/column/Column.svelte.d.ts +46 -0
  5. package/dist/column/column-state.svelte.d.ts +138 -0
  6. package/dist/column/column-state.svelte.js +64 -0
  7. package/dist/conditional.svelte.d.ts +10 -0
  8. package/dist/conditional.svelte.js +26 -0
  9. package/dist/expandable/Expandable.svelte +24 -0
  10. package/dist/expandable/Expandable.svelte.d.ts +26 -0
  11. package/dist/expandable/expandable-state.svelte.d.ts +48 -0
  12. package/dist/expandable/expandable-state.svelte.js +27 -0
  13. package/dist/index.d.ts +10 -3
  14. package/dist/index.js +5 -3
  15. package/dist/panel/Panel.svelte +21 -0
  16. package/dist/{Panel.svelte.d.ts → panel/Panel.svelte.d.ts} +2 -28
  17. package/dist/panel/panel-state.svelte.d.ts +25 -0
  18. package/dist/panel/panel-state.svelte.js +18 -0
  19. package/dist/row/Row.svelte +24 -0
  20. package/dist/row/Row.svelte.d.ts +26 -0
  21. package/dist/row/row-state.svelte.d.ts +43 -0
  22. package/dist/row/row-state.svelte.js +28 -0
  23. package/dist/size-tween.svelte.d.ts +16 -0
  24. package/dist/size-tween.svelte.js +33 -0
  25. package/dist/table/Table.svelte +1140 -0
  26. package/dist/table/Table.svelte.d.ts +123 -0
  27. package/dist/table/data.svelte.d.ts +14 -0
  28. package/dist/table/data.svelte.js +81 -0
  29. package/dist/table/table-state.svelte.d.ts +107 -0
  30. package/dist/table/table-state.svelte.js +76 -0
  31. package/dist/table/virtualization.svelte.d.ts +14 -0
  32. package/dist/table/virtualization.svelte.js +86 -0
  33. package/dist/utility.svelte.d.ts +24 -0
  34. package/dist/utility.svelte.js +107 -0
  35. package/package.json +29 -53
  36. package/dist/Column.svelte +0 -164
  37. package/dist/Column.svelte.d.ts +0 -115
  38. package/dist/Panel.svelte +0 -74
  39. package/dist/Table.svelte +0 -906
  40. package/dist/Table.svelte.d.ts +0 -112
@@ -0,0 +1,1140 @@
1
+ <!-- @component
2
+
3
+ This is a description, \
4
+ on how to use this.
5
+
6
+ @example
7
+ <Component />
8
+
9
+ -->
10
+
11
+ <script lang="ts">
12
+ import { type Snippet } from 'svelte'
13
+ import { fly } from 'svelte/transition'
14
+ import { sineInOut } from 'svelte/easing'
15
+ import reorder, { type ItemState } from 'runic-reorder'
16
+ import { Virtualization } from './virtualization.svelte.js'
17
+ import {
18
+ TableState,
19
+ type HeaderSelectCtx,
20
+ type RowCtx,
21
+ type RowSelectCtx,
22
+ type TableProps
23
+ } from './table-state.svelte.js'
24
+ import Panel from '../panel/Panel.svelte'
25
+ import Column from '../column/Column.svelte'
26
+ import { assignDescriptors, capitalize, fromProps, mounted, segmentize } from '../utility.svelte.js'
27
+ import { conditional } from '../conditional.svelte.js'
28
+ import { ColumnState, type RowColumnCtx } from '../column/column-state.svelte.js'
29
+ import Expandable from '../expandable/Expandable.svelte'
30
+ import { SizeTween } from '../size-tween.svelte.js'
31
+ import { on } from 'svelte/events'
32
+ import Row from '../row/Row.svelte'
33
+
34
+ type T = $$Generic<Record<PropertyKey, unknown>>
35
+
36
+ type ConstructorReturnType<T extends new (...args: any[]) => any> =
37
+ T extends new (...args: any[]) => infer K ? K : never
38
+ type ConstructorParams<T extends new (...args: any[]) => any> =
39
+ T extends new (...args: infer K) => any ? K : never
40
+
41
+ type ContentCtx<T extends Record<PropertyKey, unknown>> = {
42
+ Column: {
43
+ new <V>(...args: ConstructorParams<typeof Column<T, V>>): ConstructorReturnType<typeof Column<T, V>>
44
+ <V>(...args: Parameters<typeof Column<T, V>>): ReturnType<typeof Column<T, V>>
45
+ }
46
+ Panel: typeof Panel<T>
47
+ Expandable: typeof Expandable<T>
48
+ Row: typeof Row<T>
49
+ readonly table: TableState<T>
50
+ }
51
+
52
+ type ContentSnippet = Snippet<[context: ContentCtx<T>]>
53
+
54
+ let {
55
+ content,
56
+ selected: _selected = $bindable([]),
57
+ panel: _panel = $bindable(),
58
+ data: _data = $bindable([]),
59
+ ...restProps
60
+ }: TableProps<T> & { content?: ContentSnippet } = $props()
61
+
62
+ const properties = fromProps(restProps, {
63
+ selected: [() => _selected, (v) => (_selected = v)],
64
+ panel: [() => _panel, (v) => (_panel = v)],
65
+ data: [() => _data, (v) => (_data = v)]
66
+ }) as TableProps<T>
67
+
68
+ const mount = mounted()
69
+
70
+ const reorderArea = reorder(rowSnippet)
71
+
72
+ const elements = $state({}) as Record<
73
+ 'headers' | 'statusbar' | 'rows' | 'virtualTop' | 'virtualBottom' | 'selects',
74
+ HTMLElement
75
+ >
76
+
77
+ const table = new TableState<T>(properties) as TableState<T>
78
+
79
+ const virtualization = new Virtualization(table)
80
+
81
+ const panelTween = new SizeTween(() => !!properties.panel)
82
+
83
+ let hoveredRow: T | null = $state(null)
84
+ let hoveredColumn: ColumnState | null = $state(null)
85
+
86
+ /** Order of columns */
87
+ const fixed = $derived(table.positions.fixed)
88
+ const hidden = $derived(table.positions.hidden)
89
+ const notHidden = (column: ColumnState) => !table.positions.hidden.includes(column)
90
+ const sticky = $derived(table.positions.sticky.filter(notHidden))
91
+ const scrolled = $derived(table.positions.scroll.filter(notHidden))
92
+ const columns = $derived([...fixed, ...sticky, ...scrolled])
93
+
94
+ /** Width of each column */
95
+ const columnWidths = $state({}) as Record<string, number>
96
+
97
+ const getWidth = (key: string, def: number = 150) =>
98
+ columnWidths[key] || table.columns[key]?.defaults.width || def
99
+
100
+ /** grid-template-columns for widths */
101
+ const style = $derived.by(() => {
102
+ if (!mount.isMounted) return ''
103
+
104
+ const context = table.row?.snippets.context ? table.row?.options.context.width : ''
105
+
106
+ const templateColumns =
107
+ columns
108
+ .map((column, i, arr) => {
109
+ const width = getWidth(column.id)
110
+ if (i === arr.length - 1) return `minmax(${width}px, 1fr)`
111
+ return `${width}px`
112
+ })
113
+ .join(' ') + context
114
+
115
+ const theadTempla3teColumns = `
116
+ #${table.id} > thead > tr,
117
+ #${table.id} > tfoot > tr {
118
+ grid-template-columns: ${templateColumns};
119
+ }
120
+ `
121
+
122
+ const tbodyTemplateColumns = `
123
+ [data-area-class='${table.id}'] tr.row,
124
+ #${table.id} > tbody::after {
125
+ grid-template-columns: ${templateColumns};
126
+ }
127
+ `
128
+
129
+ let sum = 0
130
+ const stickyLeft = [...fixed, ...sticky]
131
+ .map((column, i, arr) => {
132
+ sum += getWidth(arr[i - 1]?.id, i === 0 ? 0 : undefined)
133
+ return `
134
+ #${table.id} .column.sticky[data-column='${column.id}'],
135
+ [data-svelte-tably='${table.id}'] .column.sticky[data-column='${column.id}'] {
136
+ left: ${sum}px;
137
+ }
138
+ `
139
+ })
140
+ .join('')
141
+
142
+ const columnStyling = columns
143
+ .map((column) =>
144
+ !column.options.style ?
145
+ ''
146
+ : `
147
+ [data-area-class='${table.id}'] .column[data-column='${column.id}'] {
148
+ ${column.options.style}
149
+ }
150
+ `
151
+ )
152
+ .join('')
153
+
154
+ return theadTempla3teColumns + tbodyTemplateColumns + stickyLeft + columnStyling
155
+ })
156
+
157
+ function observeColumnWidth(node: HTMLDivElement, isHeader = false) {
158
+ if (!isHeader) return
159
+
160
+ const key = node.getAttribute('data-column')!
161
+ node.style.width = getWidth(key) + 'px'
162
+
163
+ let mouseup = false
164
+
165
+ const observer = new MutationObserver(() => {
166
+ const width = parseFloat(node.style.width)
167
+ if (width === columnWidths[key]) return
168
+ columnWidths[key] = width
169
+ if (!mouseup) {
170
+ mouseup = true
171
+ window.addEventListener(
172
+ 'click',
173
+ (e) => {
174
+ e.preventDefault()
175
+ e.stopPropagation()
176
+ mouseup = false
177
+ },
178
+ { once: true, capture: true }
179
+ )
180
+ }
181
+ })
182
+
183
+ observer.observe(node, { attributes: true })
184
+ return { destroy: () => observer.disconnect() }
185
+ }
186
+
187
+ let tbody = $state({
188
+ width: 0
189
+ })
190
+ async function onscroll() {
191
+ const target = virtualization.viewport.element!
192
+ if (target.scrollTop !== virtualization.scrollTop) {
193
+ virtualization.scrollTop = target?.scrollTop ?? virtualization.scrollTop
194
+ }
195
+
196
+ if (elements.selects) {
197
+ elements.selects.scrollTop = target?.scrollTop
198
+ }
199
+
200
+ if (!elements.headers) return
201
+ elements.headers.scrollLeft = target.scrollLeft
202
+ elements.statusbar.scrollLeft = target.scrollLeft
203
+ }
204
+
205
+ // * --- CSV --- *
206
+ let csv = $state(false) as false | { selected?: boolean }
207
+ let csvElement = $state() as undefined | HTMLTableElement
208
+ interface CSVOptions {
209
+ /** Semi-colons as separator? */
210
+ semicolon?: boolean
211
+ /** Only selected rows */
212
+ selected?: boolean
213
+ }
214
+ export async function toCSV(opts: CSVOptions = {}) {
215
+ csv = { selected: !!opts.selected }
216
+ let resolve: (value: HTMLTableElement) => void
217
+ const promise = new Promise<HTMLTableElement>((r) => (resolve = r))
218
+
219
+ const clean = $effect.root(() => {
220
+ $effect(() => {
221
+ if (csvElement) {
222
+ resolve(csvElement)
223
+ }
224
+ })
225
+ })
226
+
227
+ let table = await promise
228
+ clean()
229
+
230
+ const separator = opts.semicolon ? ';' : ','
231
+ const rows = Array.from(table.rows)
232
+ const csvRows = [] as string[]
233
+
234
+ for (const row of rows) {
235
+ const cells = Array.from(row.cells)
236
+ const csvCells = cells.map((cell) => {
237
+ let text = cell.textContent?.trim() || ''
238
+
239
+ // Escape double quotes and wrap in quotes if needed
240
+ if (text.includes('"')) {
241
+ text = text.replace(/"/g, '""')
242
+ }
243
+ if (text.includes(separator) || text.includes('"') || text.includes('\n')) {
244
+ text = `"${text}"`
245
+ }
246
+
247
+ return text
248
+ })
249
+ csvRows.push(csvCells.join(separator))
250
+ }
251
+
252
+ csv = false
253
+ return csvRows.join('\n')
254
+ }
255
+ // * --- CSV --- *
256
+
257
+ let expandedRow = $state([]) as T[]
258
+ let expandTick = false
259
+ function toggleExpand(item: T, value?: boolean) {
260
+ if (expandTick) return
261
+ expandTick = true
262
+ requestAnimationFrame(() => (expandTick = false))
263
+
264
+ let indexOf = expandedRow.indexOf(item)
265
+ if (value === undefined) {
266
+ value = indexOf === -1
267
+ }
268
+ if (!value) {
269
+ expandedRow.splice(indexOf, 1)
270
+ return
271
+ }
272
+ if (table.expandable?.options.multiple === true) {
273
+ expandedRow.push(item)
274
+ } else {
275
+ expandedRow[0] = item
276
+ }
277
+ }
278
+
279
+ function addRowColumnEvents(
280
+ node: HTMLTableColElement,
281
+ opts: ['header' | 'row' | 'statusbar', ColumnState, () => RowColumnCtx<T, any>]
282
+ ) {
283
+ const [where, column, value] = opts
284
+ if (where !== 'row') return
285
+ if (column.options.onclick) {
286
+ $effect(() => on(node, 'click', (e) => column.options.onclick!(e, value())))
287
+ }
288
+ }
289
+
290
+ function addRowEvents(node: HTMLTableRowElement, ctx: RowCtx<T>) {
291
+ if (table.row?.events.onclick) {
292
+ $effect(() => on(node, 'click', (e) => table.row?.events.onclick!(e, ctx)))
293
+ }
294
+ if (table.row?.events.oncontextmenu) {
295
+ $effect(() => on(node, 'contextmenu', (e) => table.row?.events.oncontextmenu!(e, ctx)))
296
+ }
297
+ }
298
+ </script>
299
+
300
+ <!---------------------------------------------------->
301
+
302
+ {#if csv !== false}
303
+ {@const renderedColumns = columns.filter((v) => v.id !== '__fixed')}
304
+ <table bind:this={csvElement} hidden>
305
+ <thead>
306
+ <tr>
307
+ {#each renderedColumns as column}
308
+ <th>{@render column.snippets.title()}</th>
309
+ {/each}
310
+ </tr>
311
+ </thead>
312
+ <tbody>
313
+ {#each table.data as row, i}
314
+ {#if (csv.selected && table.selected.includes(row)) || !csv.selected}
315
+ <tr>
316
+ {#each renderedColumns as column}
317
+ <td>
318
+ {#if column.snippets.row}
319
+ {@render column.snippets.row(row, {
320
+ index: i,
321
+ value: column.options.value?.(row),
322
+ columnHovered: false,
323
+ rowHovered: false,
324
+ itemState: {
325
+ index: i,
326
+ dragging: false,
327
+ positioning: false
328
+ } as ItemState<any>,
329
+ selected: false,
330
+ expanded: false
331
+ })}
332
+ {:else}
333
+ {column.options.value?.(row)}
334
+ {/if}
335
+ </td>
336
+ {/each}
337
+ </tr>
338
+ {/if}
339
+ {/each}
340
+ </tbody>
341
+ </table>
342
+ {/if}
343
+
344
+ <svelte:head>
345
+ {@html `<`+`style>${style}</style>`}
346
+ </svelte:head>
347
+
348
+ {#snippet chevronSnippet(rotation: number = 0)}
349
+ <svg
350
+ xmlns="http://www.w3.org/2000/svg"
351
+ width="16"
352
+ height="16"
353
+ viewBox="0 0 16 16"
354
+ style="transform: rotate({rotation}deg)"
355
+ >
356
+ <path
357
+ fill="currentColor"
358
+ d="M3.2 10.26a.75.75 0 0 0 1.06.04L8 6.773l3.74 3.527a.75.75 0 1 0 1.02-1.1l-4.25-4a.75.75 0 0 0-1.02 0l-4.25 4a.75.75 0 0 0-.04 1.06"
359
+ ></path>
360
+ </svg>
361
+ {/snippet}
362
+
363
+ {#snippet dragSnippet()}
364
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" style="opacity: .3">
365
+ <path
366
+ fill="currentColor"
367
+ d="M5.5 5a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3m0 4.5a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3m1.5 3a1.5 1.5 0 1 1-3 0a1.5 1.5 0 0 1 3 0M10.5 5a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3M12 8a1.5 1.5 0 1 1-3 0a1.5 1.5 0 0 1 3 0m-1.5 6a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3"
368
+ ></path>
369
+ </svg>
370
+ {/snippet}
371
+
372
+ {#snippet columnsSnippet(
373
+ renderable: (column: ColumnState) => Snippet<[arg0?: any, arg1?: any]> | undefined,
374
+ arg: null | ((column: ColumnState) => any[]) = null,
375
+ where: 'header' | 'row' | 'statusbar'
376
+ )}
377
+ {@const isHeader = where === 'header'}
378
+ {#each fixed as column, i (column)}
379
+ {#if !hidden.includes(column)}
380
+ {@const args = arg ? arg(column) : []}
381
+ {@const sortable = isHeader && column.options.sort && !table.options.reorderable}
382
+ <svelte:element
383
+ this={isHeader ? 'th' : 'td'}
384
+ class={column.options.class ?? ''}
385
+ class:column={true}
386
+ class:sticky={true}
387
+ class:fixed={true}
388
+ use:addRowColumnEvents={[where, column, () => args[1]]}
389
+ data-column={column.id}
390
+ class:pad={(isHeader && column.options.padHeader) || (!isHeader && column.options.padRow)}
391
+ class:header={isHeader}
392
+ class:sortable
393
+ use:conditional={[isHeader, (node) => table.dataState.sortAction(node, column.id)]}
394
+ onpointerenter={() => (hoveredColumn = column)}
395
+ onpointerleave={() => (hoveredColumn = null)}
396
+ >
397
+ {@render renderable(column)?.(args[0], args[1])}
398
+ {#if isHeader && table.dataState.sortby === column.id && sortable}
399
+ <span class="sorting-icon">
400
+ {@render chevronSnippet(table.dataState.sortReverse ? 0 : 180)}
401
+ </span>
402
+ {/if}
403
+ </svelte:element>
404
+ {/if}
405
+ {/each}
406
+ {#each sticky as column, i (column)}
407
+ {#if !hidden.includes(column)}
408
+ {@const args = arg ? arg(column) : []}
409
+ {@const sortable = isHeader && column.options.sort && !table.options.reorderable}
410
+ <svelte:element
411
+ this={isHeader ? 'th' : 'td'}
412
+ class={column.options.class ?? ''}
413
+ class:column={true}
414
+ class:sticky={true}
415
+ use:addRowColumnEvents={[where, column, () => args[1]]}
416
+ use:observeColumnWidth={isHeader}
417
+ data-column={column.id}
418
+ class:pad={(isHeader && column.options.padHeader) || (!isHeader && column.options.padRow)}
419
+ class:header={isHeader}
420
+ class:resizeable={isHeader && column.options.resizeable && table.options.resizeable}
421
+ class:border={i == sticky.length - 1}
422
+ class:sortable
423
+ use:conditional={[isHeader, (node) => table.dataState.sortAction(node, column.id)]}
424
+ onpointerenter={() => (hoveredColumn = column)}
425
+ onpointerleave={() => (hoveredColumn = null)}
426
+ >
427
+ {@render renderable(column)?.(args[0], args[1])}
428
+ {#if isHeader && table.dataState.sortby === column.id && sortable}
429
+ <span class="sorting-icon">
430
+ {@render chevronSnippet(table.dataState.sortReverse ? 0 : 180)}
431
+ </span>
432
+ {/if}
433
+ </svelte:element>
434
+ {/if}
435
+ {/each}
436
+ {#each scrolled as column, i (column)}
437
+ {#if !hidden.includes(column)}
438
+ {@const args = arg ? arg(column) : []}
439
+ {@const sortable = isHeader && column!.options.sort && !table.options.reorderable}
440
+ <svelte:element
441
+ this={isHeader ? 'th' : 'td'}
442
+ class={column.options.class ?? ''}
443
+ class:column={true}
444
+ data-column={column.id}
445
+ class:pad={(isHeader && column.options.padHeader) || (!isHeader && column.options.padRow)}
446
+ use:addRowColumnEvents={[where, column, () => args[1]]}
447
+ use:observeColumnWidth={isHeader}
448
+ class:resizeable={isHeader && column.options.resizeable && table.options.resizeable}
449
+ class:sortable
450
+ use:conditional={[isHeader, (node) => table.dataState.sortAction(node, column.id)]}
451
+ onpointerenter={() => (hoveredColumn = column)}
452
+ onpointerleave={() => (hoveredColumn = null)}
453
+ >
454
+ {@render renderable(column)?.(args[0], args[1])}
455
+ {#if isHeader && table.dataState.sortby === column.id && sortable}
456
+ <span class="sorting-icon">
457
+ {@render chevronSnippet(table.dataState.sortReverse ? 0 : 180)}
458
+ </span>
459
+ {/if}
460
+ </svelte:element>
461
+ {/if}
462
+ {/each}
463
+ {/snippet}
464
+
465
+ {#snippet defaultRow(item: T, ctx: RowColumnCtx<T, any>)}
466
+ {ctx.value}
467
+ {/snippet}
468
+
469
+ {#snippet rowSnippet(item: T, itemState?: ItemState<T>)}
470
+ {@const index = itemState?.index ?? 0}
471
+
472
+ {@const ctx: RowCtx<T> = {
473
+ get index() {
474
+ return index
475
+ },
476
+ get rowHovered() {
477
+ return hoveredRow === item
478
+ },
479
+ get selected() {
480
+ return table.selected?.includes(item)
481
+ },
482
+ set selected(value) {
483
+ value ?
484
+ table.selected!.push(item)
485
+ : table.selected!.splice(table.selected!.indexOf(item), 1)
486
+ },
487
+ get itemState() {
488
+ return itemState
489
+ },
490
+ get expanded() {
491
+ return expandedRow.includes(item)
492
+ },
493
+ set expanded(value) {
494
+ toggleExpand(item, value)
495
+ }
496
+ }}
497
+
498
+ <tr
499
+ aria-rowindex={index + 1}
500
+ style:opacity={itemState?.positioning ? 0 : 1}
501
+ class="row"
502
+ class:dragging={itemState?.dragging}
503
+ class:selected={table.selected?.includes(item)}
504
+ class:first={index === 0}
505
+ class:last={index === virtualization.area.length - 1}
506
+ {...itemState?.dragging ? { 'data-svelte-tably': table.id } : {}}
507
+ onpointerenter={() => (hoveredRow = item)}
508
+ onpointerleave={() => (hoveredRow = null)}
509
+ use:addRowEvents={ctx}
510
+ onclick={(e) => {
511
+ if (table.expandable?.options.click === true) {
512
+ let target = e.target as HTMLElement
513
+ if (['INPUT', 'TEXTAREA', 'BUTTON', 'A'].includes(target.tagName)) {
514
+ return
515
+ }
516
+ ctx.expanded = !ctx.expanded
517
+ }
518
+ }}
519
+ >
520
+ {@render columnsSnippet(
521
+ (column) => column.snippets.row ?? defaultRow,
522
+ (column) => {
523
+ return [
524
+ item,
525
+ assignDescriptors(
526
+ {
527
+ get value() {
528
+ return column.options.value ? column.options.value(item) : undefined
529
+ },
530
+ get columnHovered() {
531
+ return hoveredColumn === column
532
+ }
533
+ },
534
+ ctx
535
+ )
536
+ ]
537
+ },
538
+ 'row'
539
+ )}
540
+ {#if table.row?.snippets.context}
541
+ {#if table.row?.snippets.contextHeader || !table.row?.options.context.hover || hoveredRow === item}
542
+ <td
543
+ class="context-col"
544
+ class:hover={!table.row?.snippets.contextHeader && table.row?.options.context.hover}
545
+ class:hidden={table.row?.options.context.hover &&
546
+ table.row?.snippets.contextHeader &&
547
+ hoveredRow !== item}
548
+ >
549
+ {@render table.row?.snippets.context?.(item, ctx)}
550
+ </td>
551
+ {/if}
552
+ {/if}
553
+ </tr>
554
+
555
+ {@const expandableTween = new SizeTween(() => table.expandable && expandedRow.includes(item), {
556
+ min: 1,
557
+ duration: table.expandable?.options.slide.duration,
558
+ easing: table.expandable?.options.slide.easing
559
+ })}
560
+ {#if expandableTween.current > 0}
561
+ <tr class="expandable" style="height: {expandableTween.current}px">
562
+ <td colspan={columns.length} style="height: {expandableTween.current}px">
563
+ <div bind:offsetHeight={expandableTween.size} style="width: {tbody.width - 3}px">
564
+ {@render table.expandable!.snippets.content?.(item, ctx)}
565
+ </div>
566
+ </td>
567
+ </tr>
568
+ {/if}
569
+ {/snippet}
570
+
571
+ <table
572
+ id={table.id}
573
+ class="table svelte-tably"
574
+ style="--t: {virtualization.virtualTop}px; --b: {virtualization.virtualBottom}px;"
575
+ aria-rowcount={table.data.length}
576
+ >
577
+ {#if columns.some((v) => v.snippets.header)}
578
+ <thead class="headers" bind:this={elements.headers}>
579
+ <tr style="min-width: {tbody.width}px">
580
+ {@render columnsSnippet(
581
+ (column) => column.snippets.header,
582
+ () => [
583
+ {
584
+ get header() {
585
+ return true
586
+ },
587
+ get data() {
588
+ return table.data
589
+ }
590
+ }
591
+ ],
592
+ 'header'
593
+ )}
594
+ {#if table.row?.snippets.contextHeader}
595
+ <th class="context-col">
596
+ {@render table.row?.snippets.contextHeader()}
597
+ </th>
598
+ {/if}
599
+ </tr>
600
+ <tr style="width:400px;background:none;pointer-events:none;"></tr>
601
+ </thead>
602
+ {/if}
603
+
604
+ <tbody
605
+ class="content"
606
+ use:reorderArea={{ axis: 'y', class: table.id }}
607
+ bind:this={virtualization.viewport.element}
608
+ onscrollcapture={onscroll}
609
+ bind:clientHeight={virtualization.viewport.height}
610
+ bind:clientWidth={tbody.width}
611
+ >
612
+ {#if table.options.reorderable}
613
+ {@render reorderArea({
614
+ get view() {
615
+ return virtualization.area
616
+ },
617
+ get modify() {
618
+ return table.dataState.origin
619
+ },
620
+ get startIndex() {
621
+ return virtualization.topIndex
622
+ }
623
+ })}
624
+ {:else}
625
+ {#each virtualization.area as item, i (item)}
626
+ {@render rowSnippet(item, { index: i + virtualization.topIndex } as ItemState)}
627
+ {/each}
628
+ {/if}
629
+ </tbody>
630
+
631
+ {#if columns.some((v) => v.snippets.statusbar)}
632
+ <tfoot class="statusbar" bind:this={elements.statusbar}>
633
+ <tr>
634
+ {@render columnsSnippet(
635
+ (column) => column.snippets.statusbar,
636
+ () => [
637
+ {
638
+ get data() {
639
+ return table.data
640
+ }
641
+ }
642
+ ],
643
+ 'statusbar'
644
+ )}
645
+ </tr>
646
+ <tr style="width:400px;background:none;pointer-events:none;"></tr>
647
+ </tfoot>
648
+ {/if}
649
+
650
+ <caption class="panel" style="width: {panelTween.current}px;">
651
+ {#if properties.panel && properties.panel in table.panels}
652
+ <div
653
+ class="panel-content"
654
+ bind:offsetWidth={panelTween.size}
655
+ in:fly={{ x: 100, easing: sineInOut, duration: 300 }}
656
+ out:fly={{ x: 100, duration: 200, easing: sineInOut }}
657
+ >
658
+ {@render table.panels[properties.panel].children({
659
+ get table() {
660
+ return table
661
+ },
662
+ get data() {
663
+ return table.data
664
+ }
665
+ })}
666
+ </div>
667
+ {/if}
668
+ </caption>
669
+ <caption
670
+ class="backdrop"
671
+ aria-hidden={properties.panel && table.panels[properties.panel]?.backdrop ? false : true}
672
+ >
673
+ <button
674
+ aria-label="Panel backdrop"
675
+ class="btn-backdrop"
676
+ tabindex="-1"
677
+ onclick={() => (properties.panel = undefined)}
678
+ ></button>
679
+ </caption>
680
+ </table>
681
+
682
+ {#snippet headerSelected(ctx: HeaderSelectCtx<T>)}
683
+ <input type="checkbox" indeterminate={ctx.indeterminate} bind:checked={ctx.isSelected} />
684
+ {/snippet}
685
+
686
+ {#snippet rowSelected(ctx: RowSelectCtx<T>)}
687
+ <input type="checkbox" bind:checked={ctx.isSelected} tabindex="-1" />
688
+ {/snippet}
689
+
690
+ {#if table.options.select || table.options.reorderable || table.expandable}
691
+ {@const { select, reorderable } = table.options}
692
+ {@const expandable = table.expandable}
693
+ {@const {
694
+ show = 'hover',
695
+ style = 'column',
696
+ rowSnippet = rowSelected,
697
+ headerSnippet = headerSelected
698
+ } = typeof select === 'boolean' ? {} : select}
699
+ {#if show !== 'never' || reorderable || expandable?.options.chevron !== 'never'}
700
+ <Column
701
+ id="__fixed"
702
+ {table}
703
+ fixed
704
+ width={Math.max(
705
+ 48,
706
+ 0 +
707
+ (select && show !== 'never' ? 34 : 0) +
708
+ (reorderable ? 34 : 0) +
709
+ (expandable && expandable?.options.chevron !== 'never' ? 34 : 0)
710
+ )}
711
+ resizeable={false}
712
+ >
713
+ {#snippet header()}
714
+ <div class="__fixed">
715
+ {#if reorderable}
716
+ <span style="width: 16px; display: flex; align-items: center;"></span>
717
+ {/if}
718
+ {#if select}
719
+ {@render headerSnippet({
720
+ get isSelected() {
721
+ return table.data.length === table.selected?.length && table.data.length > 0
722
+ },
723
+ set isSelected(value) {
724
+ if (value) {
725
+ table.selected = table.data
726
+ } else {
727
+ table.selected = []
728
+ }
729
+ },
730
+ get selected() {
731
+ return table.selected!
732
+ },
733
+ get indeterminate() {
734
+ return (
735
+ (table.selected?.length || 0) > 0 &&
736
+ table.data.length !== table.selected?.length
737
+ )
738
+ }
739
+ })}
740
+ {/if}
741
+ </div>
742
+ {/snippet}
743
+ {#snippet row(item, row)}
744
+ <div class="__fixed">
745
+ {#if reorderable && row.itemState}
746
+ <span style="width: 16px; display: flex; align-items: center;" use:row.itemState.handle>
747
+ {#if (row.rowHovered && !row.itemState.area.isTarget) || row.itemState.dragging}
748
+ {@render dragSnippet()}
749
+ {/if}
750
+ </span>
751
+ {/if}
752
+ {#if select && (row.selected || show === 'always' || (row.rowHovered && show === 'hover') || row.expanded)}
753
+ {@render rowSnippet({
754
+ get isSelected() {
755
+ return row.selected
756
+ },
757
+ set isSelected(value) {
758
+ row.selected = value
759
+ },
760
+ get row() {
761
+ return row
762
+ },
763
+ get item() {
764
+ return item
765
+ },
766
+ get data() {
767
+ return table.data
768
+ }
769
+ })}
770
+ {/if}
771
+ {#if expandable && expandable?.options.chevron !== 'never'}
772
+ <button class="expand-row" tabindex="-1" onclick={() => (row.expanded = !row.expanded)}>
773
+ {#if row.expanded || expandable.options.chevron === 'always' || (row.rowHovered && expandable.options.chevron === 'hover')}
774
+ {@render chevronSnippet(row.expanded ? 180 : 90)}
775
+ {/if}
776
+ </button>
777
+ {/if}
778
+ </div>
779
+ {/snippet}
780
+ </Column>
781
+ {/if}
782
+ {/if}
783
+
784
+ {#if table.options.auto}
785
+ {#each Object.keys(table.data[0] || {}) as key}
786
+ <Column
787
+ id={key}
788
+ value={(r) => r[key]}
789
+ header={capitalize(segmentize(key))}
790
+ sort={typeof table.data[0]?.[key] === 'number' ?
791
+ (a, b) => a - b
792
+ : (a, b) => String(a).localeCompare(String(b))}
793
+ />
794
+ {/each}
795
+ {/if}
796
+
797
+ {@render content?.({
798
+ Column,
799
+ Panel,
800
+ Expandable,
801
+ Row,
802
+ get table() {
803
+ return table
804
+ }
805
+ })}
806
+
807
+ <!---------------------------------------------------->
808
+ <style>
809
+ .svelte-tably *,
810
+ .svelte-tably {
811
+ box-sizing: border-box;
812
+ background-color: inherit;
813
+ }
814
+
815
+ .context-col {
816
+ display: flex;
817
+ align-items: center;
818
+ justify-content: center;
819
+ position: sticky;
820
+ right: 0;
821
+ height: 100%;
822
+ z-index: 3;
823
+ padding: 0;
824
+
825
+ &.hover {
826
+ position: absolute;
827
+ }
828
+ &.hidden {
829
+ pointer-events: none;
830
+ user-select: none;
831
+ border-left: none;
832
+ background: none;
833
+ > :global(*) {
834
+ opacity: 0;
835
+ }
836
+ }
837
+ }
838
+
839
+ :global(:root) {
840
+ --tably-color: hsl(0, 0%, 0%);
841
+ --tably-bg: hsl(0, 0%, 100%);
842
+ --tably-statusbar: hsl(0, 0%, 98%);
843
+
844
+ --tably-border: hsl(0, 0%, 90%);
845
+ --tably-border-grid: hsl(0, 0%, 98%);
846
+
847
+ --tably-padding-x: 1rem;
848
+ --tably-padding-y: 0.5rem;
849
+
850
+ --tably-radius: 0.25rem;
851
+ }
852
+
853
+ .svelte-tably {
854
+ position: relative;
855
+ overflow: visible;
856
+ }
857
+
858
+ .expandable {
859
+ position: relative;
860
+
861
+ & > td {
862
+ position: sticky;
863
+ left: 1px;
864
+ > div {
865
+ position: absolute;
866
+ overflow: auto;
867
+ top: -1.5px;
868
+ left: 0;
869
+ }
870
+ }
871
+ }
872
+
873
+ .expand-row {
874
+ display: flex;
875
+ justify-content: center;
876
+ align-items: center;
877
+ padding: 0;
878
+ outline: none;
879
+ border: none;
880
+ cursor: pointer;
881
+ background-color: transparent;
882
+ color: inherit;
883
+ width: 20px;
884
+ height: 100%;
885
+
886
+ > svg {
887
+ transition: transform 0.15s ease;
888
+ }
889
+ }
890
+
891
+ caption {
892
+ all: unset;
893
+ }
894
+
895
+ input[type='checkbox'] {
896
+ width: 18px;
897
+ height: 18px;
898
+ cursor: pointer;
899
+ }
900
+
901
+ button.btn-backdrop {
902
+ outline: none;
903
+ border: none;
904
+ cursor: pointer;
905
+ }
906
+
907
+ .sorting-icon {
908
+ align-items: center;
909
+ justify-items: end;
910
+ margin: 0;
911
+ margin-left: auto;
912
+ > svg {
913
+ transition: transform 0.15s ease;
914
+ }
915
+ }
916
+
917
+ th:not(:last-child) .sorting-icon {
918
+ margin-right: var(--tably-padding-x);
919
+ }
920
+
921
+ .__fixed {
922
+ display: flex;
923
+ align-items: center;
924
+ justify-content: center;
925
+ gap: 0.25rem;
926
+ position: absolute;
927
+ top: 0;
928
+ left: 0;
929
+ right: 0;
930
+ bottom: 0;
931
+ width: 100%;
932
+ }
933
+
934
+ tbody::before,
935
+ tbody::after,
936
+ selects::before,
937
+ selects::after {
938
+ content: '';
939
+ display: grid;
940
+ min-height: 100%;
941
+ }
942
+
943
+ tbody::before,
944
+ selects::before {
945
+ height: var(--t);
946
+ }
947
+ tbody::after,
948
+ selects::after {
949
+ height: var(--b);
950
+ }
951
+
952
+ .row:global(:is(a)) {
953
+ color: inherit;
954
+ text-decoration: inherit;
955
+ }
956
+
957
+ .backdrop {
958
+ position: absolute;
959
+ left: 0px;
960
+ top: 0px;
961
+ bottom: 0px;
962
+ right: 0px;
963
+ background-color: hsla(0, 0%, 0%, 0.3);
964
+ z-index: 3;
965
+ opacity: 1;
966
+ transition: 0.15s ease;
967
+ border: none;
968
+ outline: none;
969
+ cursor: pointer;
970
+
971
+ > button {
972
+ position: absolute;
973
+ left: 0px;
974
+ top: 0px;
975
+ bottom: 0px;
976
+ right: 0px;
977
+ }
978
+
979
+ &[aria-hidden='true'] {
980
+ opacity: 0;
981
+ pointer-events: none;
982
+ }
983
+ }
984
+
985
+ .sticky {
986
+ position: sticky;
987
+ /* right: 100px; */
988
+ z-index: 1;
989
+ }
990
+
991
+ .sticky.border {
992
+ border-right: 1px solid var(--tably-border);
993
+ }
994
+
995
+ .headers > tr > .column {
996
+ overflow: hidden;
997
+ padding: var(--tably-padding-y) 0;
998
+ cursor: default;
999
+ user-select: none;
1000
+
1001
+ &:last-child {
1002
+ border-right: none;
1003
+ }
1004
+
1005
+ &.sortable {
1006
+ cursor: pointer;
1007
+ }
1008
+
1009
+ &.resizeable {
1010
+ resize: horizontal;
1011
+ }
1012
+ }
1013
+
1014
+ .table {
1015
+ display: grid;
1016
+ height: auto;
1017
+ max-height: 100%;
1018
+ position: relative;
1019
+
1020
+ color: var(--tably-color);
1021
+ background-color: var(--tably-bg);
1022
+
1023
+ grid-template-areas:
1024
+ 'headers panel'
1025
+ 'rows panel'
1026
+ 'statusbar panel';
1027
+
1028
+ grid-template-columns: auto min-content;
1029
+ grid-template-rows: auto 1fr auto;
1030
+
1031
+ border: 1px solid var(--tably-border);
1032
+ border-radius: var(--tably-radius);
1033
+ }
1034
+
1035
+ .headers {
1036
+ display: flex;
1037
+ grid-area: headers;
1038
+ z-index: 2;
1039
+ overflow: hidden;
1040
+ }
1041
+
1042
+ .headers > tr > .column {
1043
+ width: auto !important;
1044
+ border-bottom: 1px solid var(--tably-border);
1045
+ }
1046
+ .headers > tr > .column,
1047
+ .headers > tr > .context-col {
1048
+ border-bottom: 1px solid var(--tably-border);
1049
+ border-left: 1px solid var(--tably-border-grid);
1050
+ }
1051
+
1052
+ .content {
1053
+ display: grid;
1054
+ grid-auto-rows: max-content;
1055
+
1056
+ grid-area: rows;
1057
+ scrollbar-width: thin;
1058
+ overflow: auto;
1059
+ }
1060
+
1061
+ .statusbar {
1062
+ display: flex;
1063
+ grid-area: statusbar;
1064
+ overflow: hidden;
1065
+ background-color: var(--tably-statusbar);
1066
+ }
1067
+
1068
+ .statusbar > tr > .column {
1069
+ border-top: 1px solid var(--tably-border);
1070
+ padding: calc(var(--tably-padding-y) / 2) 0;
1071
+ }
1072
+
1073
+ .headers > tr,
1074
+ .row,
1075
+ .statusbar > tr {
1076
+ position: relative;
1077
+ display: grid;
1078
+ width: 100%;
1079
+ height: 100%;
1080
+
1081
+ & > .column {
1082
+ display: flex;
1083
+ overflow: hidden;
1084
+
1085
+ &:not(.pad),
1086
+ &.pad > :global(*:first-child) {
1087
+ padding-left: var(--tably-padding-x);
1088
+ }
1089
+ }
1090
+
1091
+ & > *:last-child:not(.context-col) {
1092
+ width: 100%;
1093
+
1094
+ &:not(.pad),
1095
+ &.pad > :global(*:first-child) {
1096
+ padding-right: var(--tably-padding-x);
1097
+ }
1098
+ }
1099
+ }
1100
+
1101
+ .row > .column {
1102
+ background-color: var(--tably-bg);
1103
+ &:not(.pad),
1104
+ &.pad > :global(*:first-child) {
1105
+ padding-top: var(--tably-padding-y);
1106
+ padding-bottom: var(--tably-padding-y);
1107
+ }
1108
+ }
1109
+
1110
+ :global(#runic-drag .row) {
1111
+ border: 1px solid var(--tably-border-grid);
1112
+ border-top: 2px solid var(--tably-border-grid);
1113
+ }
1114
+
1115
+ .row > * {
1116
+ border-left: 1px solid var(--tably-border-grid);
1117
+ border-bottom: 1px solid var(--tably-border-grid);
1118
+ }
1119
+
1120
+ .panel {
1121
+ position: relative;
1122
+ grid-area: panel;
1123
+ height: 100%;
1124
+ overflow: hidden;
1125
+ border-left: 1px solid var(--tably-border);
1126
+
1127
+ z-index: 4;
1128
+
1129
+ > .panel-content {
1130
+ position: absolute;
1131
+ top: 0;
1132
+ right: 0;
1133
+ bottom: 0;
1134
+ width: min-content;
1135
+ overflow: auto;
1136
+ scrollbar-width: thin;
1137
+ padding: var(--tably-padding-y) 0;
1138
+ }
1139
+ }
1140
+ </style>