svelte-tably 1.0.0-next.8 → 1.0.0-next.9

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/dist/Table.svelte CHANGED
@@ -8,13 +8,15 @@
8
8
 
9
9
  -->
10
10
 
11
- <script module lang='ts'>
12
-
13
- export interface TableState<T extends Record<PropertyKey, any> = Record<PropertyKey, any>> {
14
- columns: Record<string, TColumn<T, unknown>>
11
+ <script module lang="ts">
12
+ export interface TableState<
13
+ T extends Record<PropertyKey, any> = Record<PropertyKey, any>
14
+ > {
15
+ columns: Record<string, ColumnState<T>>
15
16
  panels: Record<string, TPanel<T>>
16
17
  selected: T[] | null
17
18
  sortby?: string
19
+ sortReverse: boolean
18
20
  positions: {
19
21
  sticky: string[]
20
22
  scroll: string[]
@@ -25,7 +27,7 @@
25
27
  readonly data: T[]
26
28
  /** Rows become anchors */
27
29
  readonly href?: (item: T) => string
28
- addColumn(key: string, options: TColumn<T, unknown>): void
30
+ addColumn(key: string, options: ColumnState<T>): void
29
31
  removeColumn(key: string): void
30
32
  }
31
33
 
@@ -33,13 +35,13 @@
33
35
  return getContext<TableState<T>>('svelte5-table')
34
36
  }
35
37
 
36
- export type HeaderSelectCtx<T = any> = {
37
- isSelected: boolean,
38
+ export type HeaderSelectCtx<T = any> = {
39
+ isSelected: boolean
38
40
  /** The list of selected items */
39
41
  readonly selected: T[]
40
42
  /**
41
43
  * See [MDN :indeterminate](https://developer.mozilla.org/en-US/docs/Web/CSS/:indeterminate)
42
- */
44
+ */
43
45
  readonly indeterminate: boolean
44
46
  }
45
47
 
@@ -49,22 +51,33 @@
49
51
  data: T[]
50
52
  isSelected: boolean
51
53
  }
52
-
53
54
  </script>
54
55
 
55
- <script lang='ts' generics='T extends Record<PropertyKey, unknown>'>
56
-
56
+ <script lang="ts">
57
57
  import { getContext, onMount, setContext, tick, untrack, type Snippet } from 'svelte'
58
- import Column, { type RowCtx, type Column as TColumn } from './Column.svelte'
58
+ import Column, { type ColumnProps, type RowCtx, type ColumnState } from './Column.svelte'
59
59
  import Panel, { PanelTween, type Panel as TPanel } from './Panel.svelte'
60
60
  import { fly } from 'svelte/transition'
61
61
  import { sineInOut } from 'svelte/easing'
62
- import type { get } from 'svelte/store'
62
+ import { on } from 'svelte/events'
63
63
 
64
-
64
+ type T = $$Generic<Record<PropertyKey, unknown>>
65
+
66
+ type ConstructorReturnType<T extends new (...args: any[]) => any> = T extends new (...args: any[]) => infer K ? K : never
67
+ type ConstructorParams<T extends new (...args: any[]) => any> = T extends new (...args: infer K) => any ? K : never
68
+
69
+ type ContentCtx<T extends Record<PropertyKey, unknown>> = {
70
+ Column: {
71
+ new <V>(...args: ConstructorParams<typeof Column<T, V>>): ConstructorReturnType<typeof Column<T, V>>
72
+ <V>(...args: Parameters<typeof Column<T, V>>): ReturnType<typeof Column<T, V>>
73
+ }
74
+ Panel: typeof Panel
75
+ readonly table: TableState<T>
76
+ readonly data: T[]
77
+ }
65
78
 
66
79
  interface Props {
67
- content: Snippet<[context: { Column: typeof Column<T>, Panel: typeof Panel, readonly table: TableState<T>, readonly data: T[] }]>
80
+ content: Snippet<[context: ContentCtx<T>]>
68
81
 
69
82
  panel?: string
70
83
  data?: T[]
@@ -73,38 +86,40 @@
73
86
  /**
74
87
  * Can you change the width of the columns?
75
88
  * @default true
76
- */
89
+ */
77
90
  resizeable?: boolean
78
91
 
79
92
  selected?: T[]
80
- select?: boolean | {
81
- /**
82
- * The style, in which the selection is shown
83
- *
84
- * NOTE: If using `edge` | 'side', "show" will always be `hover`. This is due to
85
- * an inconsistency/limitation of matching the scroll between the selection div and the rows.
86
- *
87
- * @default 'column'
88
- */
89
- style?: 'column'
90
- /**
91
- * When to show the row-select, when not selected?
92
- * @default 'hover'
93
- */
94
- show?: 'hover' | 'always' | 'never'
95
- /**
96
- * Custom snippet
97
- */
98
- headerSnippet?: Snippet<[context: HeaderSelectCtx]>
99
- rowSnippet?: Snippet<[context: RowSelectCtx<T>]>
100
- }
93
+ select?:
94
+ | boolean
95
+ | {
96
+ /**
97
+ * The style, in which the selection is shown
98
+ *
99
+ * NOTE: If using `edge` | 'side', "show" will always be `hover`. This is due to
100
+ * an inconsistency/limitation of matching the scroll between the selection div and the rows.
101
+ *
102
+ * @default 'column'
103
+ */
104
+ style?: 'column'
105
+ /**
106
+ * When to show the row-select, when not selected?
107
+ * @default 'hover'
108
+ */
109
+ show?: 'hover' | 'always' | 'never'
110
+ /**
111
+ * Custom snippet
112
+ */
113
+ headerSnippet?: Snippet<[context: HeaderSelectCtx]>
114
+ rowSnippet?: Snippet<[context: RowSelectCtx<T>]>
115
+ }
101
116
  // | {
102
117
  // /**
103
118
  // * The style, in which the selection is shown
104
- // *
119
+ // *
105
120
  // * NOTE: If using `edge` | 'side', "show" will always be `hover`. This is due to
106
121
  // * an inconsistency/limitation of matching the scroll between the selection div and the rows.
107
- // *
122
+ // *
108
123
  // * @default 'column'
109
124
  // */
110
125
  // style?: 'edge' | 'side'
@@ -133,24 +148,85 @@
133
148
  selected = $bindable([]),
134
149
  panel = $bindable(),
135
150
  data: _data = [],
136
- id = Array.from({length: 12}, () => String.fromCharCode(Math.floor(Math.random() * 26) + 97)).join(''),
151
+ id = Array.from({ length: 12 }, () => String.fromCharCode(Math.floor(Math.random() * 26) + 97)).join(''),
137
152
  href,
138
153
  resizeable = true,
139
154
  select
140
155
  }: Props = $props()
141
-
156
+
142
157
  let mounted = $state(false)
143
- onMount(() => mounted = true)
158
+ onMount(() => (mounted = true))
144
159
 
145
160
  const data = $derived([..._data])
146
161
 
147
- const elements = $state({}) as Record<'headers' | 'statusbar' | 'rows' | 'virtualTop' | 'virtualBottom' | 'selects', HTMLElement>
162
+ const elements = $state({}) as Record<
163
+ 'headers' | 'statusbar' | 'rows' | 'virtualTop' | 'virtualBottom' | 'selects',
164
+ HTMLElement
165
+ >
148
166
 
167
+ let cols: TableState<T>['columns'] = $state({})
168
+ let positions: TableState<T>['positions'] = $state({
169
+ fixed: [],
170
+ sticky: [],
171
+ scroll: [],
172
+ hidden: [],
173
+ toggle(key) {
174
+ if (table.positions.hidden.includes(key))
175
+ table.positions.hidden = table.positions.hidden.filter((column) => column !== key)
176
+ else table.positions.hidden.push(key)
177
+ }
178
+ })
179
+
180
+ const table: TableState<T> = $state({
181
+ columns: cols,
182
+ selected,
183
+ panels: {},
184
+ positions,
185
+ sortReverse: false,
186
+ get href() {
187
+ return href
188
+ },
189
+ get data() {
190
+ return data
191
+ },
192
+ get resizeable() {
193
+ return resizeable
194
+ },
195
+ addColumn(key, column) {
196
+ table.columns[key] = column
197
+
198
+ if (column.defaults.sort) sortBy(key)
199
+
200
+ if (column.fixed) {
201
+ // @ts-expect-error
202
+ table.positions.fixed.push(key)
203
+ return
204
+ }
205
+
206
+ if (!column.defaults.show) table.positions.hidden.push(key)
207
+
208
+ if (column.defaults.sticky) table.positions.sticky.push(key)
209
+ else table.positions.scroll.push(key)
210
+ },
211
+ removeColumn(key) {
212
+ delete table.columns[key]
213
+ // @ts-expect-error fixed is not typed
214
+ table.positions.fixed = table.positions.fixed.filter((column) => column !== key)
215
+ table.positions.sticky = table.positions.sticky.filter((column) => column !== key)
216
+ table.positions.scroll = table.positions.scroll.filter((column) => column !== key)
217
+ table.positions.hidden = table.positions.hidden.filter((column) => column !== key)
218
+ }
219
+ })
220
+
221
+ setContext('svelte5-table', table)
222
+
223
+ // * --- *
149
224
 
150
225
  // * --- Virtualization --- *
226
+ // #region Virtualization
151
227
  let scrollTop = $state(0)
152
228
  let viewportHeight = $state(0)
153
-
229
+
154
230
  let heightPerItem = $state(8)
155
231
 
156
232
  const spacing = () => viewportHeight / 2
@@ -161,7 +237,7 @@
161
237
  return result
162
238
  })
163
239
  let virtualBottom = $derived.by(() => {
164
- let result = (heightPerItem * data.length) - virtualTop - spacing() * 4
240
+ let result = heightPerItem * data.length - virtualTop - spacing() * 4
165
241
  result = Math.max(result, 0)
166
242
  return result
167
243
  })
@@ -170,23 +246,23 @@
170
246
 
171
247
  /** The area of data being rendered */
172
248
  let area = $derived.by(() => {
173
- const index = (virtualTop / heightPerItem) || 0
249
+ table.sortReverse
250
+ table.sortby
251
+ const index = virtualTop / heightPerItem || 0
174
252
  const end = index + renderItemLength
175
- const result = data.slice(
176
- index,
177
- end
178
- )
253
+ const result = data.slice(index, end)
179
254
  return result
180
255
  })
181
256
 
182
257
  function calculateHeightPerItem() {
183
- if(!elements.rows) {
258
+ if (!elements.rows) {
184
259
  heightPerItem = 8
185
260
  return
186
261
  }
187
262
  tick().then(() => {
188
263
  const firstRow = elements.rows.children[0].getBoundingClientRect().top
189
- const lastRow = elements.rows.children[elements.rows.children.length - 1].getBoundingClientRect().bottom
264
+ const lastRow =
265
+ elements.rows.children[elements.rows.children.length - 1].getBoundingClientRect().bottom
190
266
  heightPerItem = (lastRow - firstRow) / area.length
191
267
  })
192
268
  }
@@ -195,71 +271,49 @@
195
271
  data
196
272
  untrack(calculateHeightPerItem)
197
273
  })
274
+ // #endregion
198
275
  // * --- Virtualization --- *
199
276
 
200
- let cols: TableState<T>['columns'] = $state({})
201
- let positions: TableState<T>['positions'] = $state({
202
- fixed: [],
203
- sticky: [],
204
- scroll: [],
205
- hidden: [],
206
- toggle(key) {
207
- if(table.positions.hidden.includes(key))
208
- table.positions.hidden = table.positions.hidden.filter(column => column !== key)
209
- else
210
- table.positions.hidden.push(key)
211
- }
212
- })
213
277
 
214
278
 
215
- const table: TableState<T> = $state({
216
- columns: cols,
217
- selected,
218
- panels: {},
219
- positions,
220
- get href() {
221
- return href
222
- },
223
- get data() {
224
- return data
225
- },
226
- get resizeable() {
227
- return resizeable
228
- },
229
- addColumn(key, column) {
230
- table.columns[key] = column
279
+ function sortBy(column: string) {
280
+ const { sorting, value } = table.columns[column]!.options
281
+ if(!sorting || !value) return
231
282
 
232
- if(column.defaults.sort)
233
- table.sortby = key
234
-
235
- if(column.fixed) {
236
- // @ts-expect-error
237
- table.positions.fixed.push(key)
238
- return
239
- }
283
+ if (table.sortby === column) {
284
+ table.sortReverse = !table.sortReverse
285
+ }
286
+ else {
287
+ table.sortReverse = false
288
+ table.sortby = column
289
+ }
290
+ }
291
+ function sortAction(node: HTMLElement, column: string) {
292
+ $effect(() => on(node, 'click', () => sortBy(column)))
293
+ }
240
294
 
241
- if(!column.defaults.show)
242
- table.positions.hidden.push(key)
243
-
244
- if(column.defaults.sticky)
245
- table.positions.sticky.push(key)
246
- else
247
- table.positions.scroll.push(key)
248
- },
249
- removeColumn(key) {
250
- delete table.columns[key]
251
- // @ts-expect-error fixed is not typed
252
- table.positions.fixed = table.positions.fixed.filter(column => column !== key)
253
- table.positions.sticky = table.positions.sticky.filter(column => column !== key)
254
- table.positions.scroll = table.positions.scroll.filter(column => column !== key)
255
- table.positions.hidden = table.positions.hidden.filter(column => column !== key)
295
+ function sortTable() {
296
+ if (!table.sortby) return
297
+ const column = table.columns[table.sortby]
298
+ let { sorting, value } = column.options
299
+ if(!sorting || !value) return
300
+ if(sorting === true) {
301
+ sorting = (a, b) => String(a).localeCompare(String(b))
256
302
  }
257
- })
303
+ if(table.sortReverse) {
304
+ data.sort((a, b) => sorting(value(b), value(a)))
305
+ } else {
306
+ data.sort((a, b) => sorting(value(a), value(b)))
307
+ }
308
+ }
258
309
 
259
- setContext('svelte5-table', table)
310
+ $effect.pre(() => {
311
+ data
312
+ table.sortby
313
+ table.sortReverse
314
+ untrack(sortTable)
315
+ })
260
316
 
261
- // * --- *
262
-
263
317
  const panelTween = new PanelTween(() => panel, 24)
264
318
 
265
319
  let hoveredRow: T | null = $state(null)
@@ -273,47 +327,49 @@
273
327
  const notHidden = (key: string) => !positions.hidden.includes(key)
274
328
  const sticky = $derived(positions.sticky.filter(notHidden))
275
329
  const scrolled = $derived(positions.scroll.filter(notHidden))
276
- const columns = $derived([ ...fixed, ...sticky, ...scrolled ])
330
+ const columns = $derived([...fixed, ...sticky, ...scrolled])
277
331
 
278
332
  /** Width of each column */
279
333
  const columnWidths = $state({}) as Record<string, number>
280
334
 
281
- const getWidth = (key: string, def: number = 150) => columnWidths[key] || table.columns[key]?.defaults.width || def
335
+ const getWidth = (key: string, def: number = 150) =>
336
+ columnWidths[key] || table.columns[key]?.defaults.width || def
282
337
 
283
338
  /** grid-template-columns for widths */
284
339
  const style = $derived.by(() => {
285
- if(!mounted) return ''
340
+ if (!mounted) return ''
286
341
  const templateColumns = `
287
342
  #${id} > .headers,
288
343
  #${id} > tbody > .row,
289
344
  #${id} > tfoot > tr,
290
345
  #${id} > .content > .virtual.bottom {
291
- grid-template-columns: ${
292
- columns.map((key, i, arr) => {
346
+ grid-template-columns: ${columns
347
+ .map((key, i, arr) => {
293
348
  const width = getWidth(key)
294
- if(i === arr.length - 1)
295
- return `minmax(${width}px, 1fr)`
349
+ if (i === arr.length - 1) return `minmax(${width}px, 1fr)`
296
350
  return `${width}px`
297
- }).join(' ')
298
- };
351
+ })
352
+ .join(' ')};
299
353
  }
300
354
  `
301
-
355
+
302
356
  let sum = 0
303
- const stickyLeft = [...fixed, ...sticky].map((key, i, arr) => {
304
- sum += getWidth(arr[i - 1], i === 0 ? 0 : undefined)
305
- return `
357
+ const stickyLeft = [...fixed, ...sticky]
358
+ .map((key, i, arr) => {
359
+ sum += getWidth(arr[i - 1], i === 0 ? 0 : undefined)
360
+ return `
306
361
  #${id} .column.sticky[data-column='${key}'] {
307
362
  left: ${sum}px;
308
363
  }
309
364
  `
310
- }).join('')
365
+ })
366
+ .join('')
311
367
 
312
368
  return templateColumns + stickyLeft
313
369
  })
314
370
 
315
371
  function observeColumnWidth(node: HTMLDivElement, isHeader = false) {
316
- if(!isHeader) return
372
+ if (!isHeader) return
317
373
 
318
374
  const key = node.getAttribute('data-column')!
319
375
  node.style.width = getWidth(key) + 'px'
@@ -322,214 +378,276 @@
322
378
  columnWidths[key] = parseFloat(node.style.width)
323
379
  })
324
380
 
325
- observer.observe(node, {attributes: true})
381
+ observer.observe(node, { attributes: true })
326
382
  return { destroy: () => observer.disconnect() }
327
383
  }
328
384
 
329
385
  async function onscroll() {
330
386
  const target = elements.rows
331
- if(target.scrollTop !== scrollTop) {
387
+ if (target.scrollTop !== scrollTop) {
332
388
  scrollTop = target?.scrollTop ?? scrollTop
333
389
  }
334
390
 
335
- if(elements.selects) {
391
+ if (elements.selects) {
336
392
  elements.selects.scrollTop = target?.scrollTop
337
393
  }
338
-
339
- if(!elements.headers) return
394
+
395
+ if (!elements.headers) return
340
396
  elements.headers.scrollLeft = target.scrollLeft
341
397
  elements.statusbar.scrollLeft = target.scrollLeft
342
398
  }
343
399
 
344
-
345
- export {
346
- selected,
347
- positions,
348
- data,
349
- href,
350
- cols as columns
351
- }
352
-
400
+ export { selected, positions, data, href, cols as columns }
353
401
  </script>
402
+
354
403
  <!---------------------------------------------------->
355
404
 
356
405
  <svelte:head>
357
406
  {@html `<style>${style}</style>`}
358
407
  </svelte:head>
359
408
 
409
+ {#snippet chevronSnippet(reversed: boolean)}
410
+ <svg
411
+ class='sorting-icon'
412
+ class:reversed
413
+ xmlns="http://www.w3.org/2000/svg"
414
+ width="16"
415
+ height="16"
416
+ viewBox="0 0 16 16"
417
+ style='margin: auto; margin-right: var(--tably-padding-x, 1rem);'
418
+ >
419
+ <path
420
+ fill="currentColor"
421
+ d="M3.2 5.74a.75.75 0 0 1 1.06-.04L8 9.227L11.74 5.7a.75.75 0 1 1 1.02 1.1l-4.25 4a.75.75 0 0 1-1.02 0l-4.25-4a.75.75 0 0 1-.04-1.06"
422
+ />
423
+ </svg>
424
+ {/snippet}
425
+
360
426
  {#snippet columnsSnippet(
361
- renderable: (column: string) => Snippet<[arg0?: any, arg1?: any]> | undefined,
427
+ renderable: (column: string) => Snippet<[arg0?: any, arg1?: any]> | undefined,
362
428
  arg: null | ((column: string) => any[]) = null,
363
429
  isHeader = false
364
430
  )}
365
431
  {#each fixed as column, i (column)}
366
432
  {#if !hidden.includes(column)}
367
433
  {@const args = arg ? arg(column) : []}
434
+ {@const sortable = isHeader && table.columns[column]!.options.sorting}
435
+ {@const sortClick = isHeader ? sortAction : ()=>{}}
368
436
  <svelte:element
369
437
  this={isHeader ? 'th' : 'td'}
370
- class='column sticky fixed'
438
+ class="column sticky fixed"
371
439
  data-column={column}
372
440
  class:header={isHeader}
441
+ class:sortable={sortable}
442
+ use:sortClick={column}
373
443
  >
374
444
  {@render renderable(column)?.(args[0], args[1])}
445
+ {#if isHeader && table.sortby === column && sortable}
446
+ {@render chevronSnippet(table.sortReverse)}
447
+ {/if}
375
448
  </svelte:element>
376
449
  {/if}
377
450
  {/each}
378
451
  {#each sticky as column, i (column)}
379
452
  {#if !hidden.includes(column)}
380
453
  {@const args = arg ? arg(column) : []}
454
+ {@const sortable = isHeader && table.columns[column]!.options.sorting}
455
+ {@const sortClick = isHeader ? sortAction : ()=>{}}
381
456
  <svelte:element
382
457
  this={isHeader ? 'th' : 'td'}
383
- class='column sticky'
458
+ class="column sticky"
384
459
  use:observeColumnWidth={isHeader}
385
460
  data-column={column}
386
461
  class:header={isHeader}
387
462
  class:resizeable={isHeader && table.columns[column].options.resizeable && table.resizeable}
388
463
  class:border={i == sticky.length - 1}
464
+ class:sortable={sortable}
465
+ use:sortClick={column}
389
466
  >
390
467
  {@render renderable(column)?.(args[0], args[1])}
468
+ {#if isHeader && table.sortby === column && sortable}
469
+ {@render chevronSnippet(table.sortReverse)}
470
+ {/if}
391
471
  </svelte:element>
392
472
  {/if}
393
473
  {/each}
394
474
  {#each scrolled as column, i (column)}
395
475
  {#if !hidden.includes(column)}
396
476
  {@const args = arg ? arg(column) : []}
477
+ {@const sortable = isHeader && table.columns[column]!.options.sorting}
478
+ {@const sortClick = isHeader ? sortAction : ()=>{}}
397
479
  <svelte:element
398
480
  this={isHeader ? 'th' : 'td'}
399
- class='column'
481
+ class="column"
400
482
  data-column={column}
401
483
  use:observeColumnWidth={isHeader}
402
484
  class:resizeable={isHeader && table.columns[column].options.resizeable && table.resizeable}
485
+ class:sortable={sortable}
486
+ use:sortClick={column}
403
487
  >
404
488
  {@render renderable(column)?.(args[0], args[1])}
489
+ {#if isHeader && table.sortby === column && sortable}
490
+ {@render chevronSnippet(table.sortReverse)}
491
+ {/if}
405
492
  </svelte:element>
406
493
  {/if}
407
494
  {/each}
408
495
  {/snippet}
409
496
 
410
497
  <table
411
- id={id}
412
- class='table svelte-tably'
413
- style='--t: {virtualTop}px; --b: {virtualBottom}px;'
414
- aria-rowcount='{data.length}'
498
+ {id}
499
+ class="table svelte-tably"
500
+ style="--t: {virtualTop}px; --b: {virtualBottom}px;"
501
+ aria-rowcount={data.length}
415
502
  >
416
-
417
- <thead class='headers' bind:this={elements.headers}>
418
- {@render columnsSnippet((column) => table.columns[column]?.header, () => [true], true)}
503
+ <thead class="headers" bind:this={elements.headers}>
504
+ {@render columnsSnippet(
505
+ (column) => table.columns[column]?.header,
506
+ () => [true],
507
+ true
508
+ )}
419
509
  </thead>
420
510
 
421
- <tbody class='content' bind:this={elements.rows} onscrollcapture={onscroll} bind:clientHeight={viewportHeight}>
511
+ <tbody class="content" bind:this={elements.rows} onscrollcapture={onscroll} bind:clientHeight={viewportHeight}>
422
512
  {#each area as item, i (item)}
423
513
  {@const props = table.href ? { href: table.href(item) } : {}}
424
514
  {@const index = data.indexOf(item) + 1}
425
515
  <svelte:element
426
516
  this={table.href ? 'a' : 'tr'}
427
- class='row'
517
+ class="row"
428
518
  class:hover={hoveredRow === item}
429
519
  class:selected={table.selected?.includes(item)}
430
520
  class:first={i === 0}
431
521
  class:last={i === area.length - 1}
432
522
  {...props}
433
- aria-rowindex='{index}'
434
- onpointerenter={() => hoveredRow = item}
435
- onpointerleave={() => hoveredRow = null}
523
+ aria-rowindex={index}
524
+ onpointerenter={() => (hoveredRow = item)}
525
+ onpointerleave={() => (hoveredRow = null)}
436
526
  >
437
527
  {@render columnsSnippet(
438
528
  (column) => table.columns[column]!.row,
439
529
  (column) => {
440
530
  const col = table.columns[column]!
441
- return [item, {
442
- get index() { return index - 1 },
443
- get value() { return col.options.value ? col.options.value(item) : undefined },
444
- get isHovered() { return hoveredRow === item },
445
- get selected() { return table.selected?.includes(item) },
446
- set selected(value) { value ? table.selected!.push(item) : table.selected!.splice(table.selected!.indexOf(item), 1) }
447
- }]
531
+ return [
532
+ item,
533
+ {
534
+ get index() {
535
+ return index - 1
536
+ },
537
+ get value() {
538
+ return col.options.value ? col.options.value(item) : undefined
539
+ },
540
+ get isHovered() {
541
+ return hoveredRow === item
542
+ },
543
+ get selected() {
544
+ return table.selected?.includes(item)
545
+ },
546
+ set selected(value) {
547
+ value ?
548
+ table.selected!.push(item)
549
+ : table.selected!.splice(table.selected!.indexOf(item), 1)
550
+ }
551
+ }
552
+ ]
448
553
  }
449
554
  )}
450
555
  </svelte:element>
451
556
  {/each}
452
557
  </tbody>
453
-
454
- <tfoot class='statusbar' bind:this={elements.statusbar}>
558
+
559
+ <tfoot class="statusbar" bind:this={elements.statusbar}>
455
560
  <tr>
456
561
  {@render columnsSnippet((column) => table.columns[column]?.statusbar)}
457
562
  </tr>
458
563
  </tfoot>
459
564
 
460
- <caption class='panel' style='width: {(panelTween.current)}px;' style:overflow={panelTween.transitioning ? 'hidden' : 'auto'}>
565
+ <caption
566
+ class="panel"
567
+ style="width: {panelTween.current}px;"
568
+ style:overflow={panelTween.transitioning ? 'hidden' : 'auto'}
569
+ >
461
570
  {#if panel && panel in table.panels}
462
- <div
463
- class='panel-content'
571
+ <div
572
+ class="panel-content"
464
573
  bind:clientWidth={panelTween.width}
465
- in:fly={{ x: 100, easing: sineInOut, duration:300 }}
466
- out:fly={{ x:100, duration:200, easing: sineInOut }}
574
+ in:fly={{ x: 100, easing: sineInOut, duration: 300 }}
575
+ out:fly={{ x: 100, duration: 200, easing: sineInOut }}
467
576
  >
468
- {@render table.panels[panel].content({ get table() { return table }, get data() { return data } })}
577
+ {@render table.panels[panel].content({
578
+ get table() {
579
+ return table
580
+ },
581
+ get data() {
582
+ return data
583
+ }
584
+ })}
469
585
  </div>
470
586
  {/if}
471
587
  </caption>
472
- <caption
473
- class='backdrop'
474
- aria-hidden={panel && table.panels[panel]?.backdrop ? false : true}
475
- >
476
- <button
477
- aria-label='Panel backdrop'
478
- tabindex='-1'
479
- onclick={() => panel = undefined}
480
- ></button>
588
+ <caption class="backdrop" aria-hidden={panel && table.panels[panel]?.backdrop ? false : true}>
589
+ <button aria-label="Panel backdrop" tabindex="-1" onclick={() => (panel = undefined)}></button>
481
590
  </caption>
482
591
  </table>
483
592
 
484
593
  {#snippet headerSelected(ctx: HeaderSelectCtx<T>)}
485
- <input
486
- type='checkbox'
487
- indeterminate={ctx.indeterminate}
488
- bind:checked={ctx.isSelected}
489
- />
594
+ <input type="checkbox" indeterminate={ctx.indeterminate} bind:checked={ctx.isSelected} />
490
595
  {/snippet}
491
596
 
492
597
  {#snippet rowSelected(ctx: RowSelectCtx<T>)}
493
- <input
494
- type='checkbox'
495
- bind:checked={ctx.isSelected}
496
- />
598
+ <input type="checkbox" bind:checked={ctx.isSelected} />
497
599
  {/snippet}
498
600
 
499
601
  {#if select}
500
- {@const { show = 'hover', style = 'column', rowSnippet = rowSelected, headerSnippet = headerSelected } = typeof select === 'boolean' ? {} : select}
602
+ {@const {
603
+ show = 'hover',
604
+ style = 'column',
605
+ rowSnippet = rowSelected,
606
+ headerSnippet = headerSelected
607
+ } = typeof select === 'boolean' ? {} : select}
501
608
  {#if show !== 'never'}
502
- <Column id='__fixed' {table} fixed width={56} resizeable={false}>
609
+ <Column id="__fixed" {table} fixed width={56} resizeable={false}>
503
610
  {#snippet header()}
504
- {@render headerSnippet({
505
- get isSelected() {
506
- return table.data.length === table.selected?.length
507
- },
508
- set isSelected(value) {
509
- if(value) {
510
- table.selected = table.data
511
- } else {
512
- table.selected = []
611
+ <div class="__fixed">
612
+ {@render headerSnippet({
613
+ get isSelected() {
614
+ return table.data.length === table.selected?.length
615
+ },
616
+ set isSelected(value) {
617
+ if (value) {
618
+ table.selected = table.data
619
+ } else {
620
+ table.selected = []
621
+ }
622
+ },
623
+ get selected() {
624
+ return table.selected!
625
+ },
626
+ get indeterminate() {
627
+ return (table.selected?.length || 0) > 0 && table.data.length !== table.selected?.length
513
628
  }
514
- },
515
- get selected() {
516
- return table.selected!
517
- },
518
- get indeterminate() {
519
- return (table.selected?.length || 0) > 0
520
- && table.data.length !== table.selected?.length
521
- }
522
- })}
629
+ })}
630
+ </div>
523
631
  {/snippet}
524
632
  {#snippet row(item, row)}
525
- <div class='__fixed'>
633
+ <div class="__fixed">
526
634
  {#if row.selected || show === 'always' || (row.isHovered && show === 'hover')}
527
635
  {@render rowSnippet({
528
- get isSelected() { return row.selected },
529
- set isSelected(value) { row.selected = value },
530
- get row() { return row },
531
- get item() { return item },
532
- get data() { return table.data }
636
+ get isSelected() {
637
+ return row.selected
638
+ },
639
+ set isSelected(value) {
640
+ row.selected = value
641
+ },
642
+ get row() {
643
+ return row
644
+ },
645
+ get item() {
646
+ return item
647
+ },
648
+ get data() {
649
+ return table.data
650
+ }
533
651
  })}
534
652
  {/if}
535
653
  </div>
@@ -538,15 +656,21 @@
538
656
  {/if}
539
657
  {/if}
540
658
 
541
- {@render content?.({ Column, Panel, get table() { return table }, get data() { return data } })}
542
-
543
-
659
+ {@render content?.({
660
+ Column,
661
+ Panel,
662
+ get table() {
663
+ return table
664
+ },
665
+ get data() {
666
+ return data
667
+ }
668
+ })}
544
669
 
545
670
  <!---------------------------------------------------->
546
671
  <style>
547
-
548
- .svelte-tably *, .svelte-tably {
549
- all: unset;
672
+ .svelte-tably *,
673
+ .svelte-tably {
550
674
  box-sizing: border-box;
551
675
  background-color: inherit;
552
676
  }
@@ -557,49 +681,61 @@
557
681
  }
558
682
 
559
683
  input[type='checkbox'] {
560
- all: revert;
561
684
  width: 18px;
562
685
  height: 18px;
563
686
  cursor: pointer;
564
687
  }
565
688
 
689
+ .sorting-icon {
690
+ transition: transform .15s ease;
691
+ transform: rotateZ(0deg);
692
+ &.reversed {
693
+ transform: rotateZ(-180deg);
694
+ }
695
+ }
696
+
566
697
  .__fixed {
567
698
  display: flex;
568
699
  align-items: center;
569
700
  justify-content: center;
570
- gap: .5rem;
701
+ gap: 0.5rem;
571
702
  position: absolute;
572
- top: 0; left: 0;
573
- right: 0; bottom: 0;
703
+ top: 0;
704
+ left: 0;
705
+ right: 0;
706
+ bottom: 0;
574
707
  width: 100%;
575
708
  }
576
709
 
577
710
  .first .__fixed {
578
- top: var(--tably-padding-y, .5rem);
711
+ top: var(--tably-padding-y, 0.5rem);
579
712
  }
580
713
  .last .__fixed {
581
- bottom: var(--tably-padding-y, .5rem);
714
+ bottom: var(--tably-padding-y, 0.5rem);
582
715
  }
583
716
 
584
- tbody::before, tbody::after, selects::before, selects::after {
717
+ tbody::before,
718
+ tbody::after,
719
+ selects::before,
720
+ selects::after {
585
721
  content: '';
586
722
  display: grid;
587
723
  min-height: 100%;
588
724
  }
589
725
 
590
- tbody::before, selects::before {
726
+ tbody::before,
727
+ selects::before {
591
728
  height: var(--t);
592
729
  }
593
- tbody::after, selects::after {
730
+ tbody::after,
731
+ selects::after {
594
732
  height: var(--b);
595
- }
733
+ }
596
734
 
597
735
  a.row {
598
736
  color: inherit;
599
737
  text-decoration: inherit;
600
738
  }
601
-
602
-
603
739
 
604
740
  .backdrop {
605
741
  position: absolute;
@@ -607,10 +743,10 @@
607
743
  top: 0px;
608
744
  bottom: 0px;
609
745
  right: 0px;
610
- background-color: hsla(0, 0%, 0%, .3);
746
+ background-color: hsla(0, 0%, 0%, 0.3);
611
747
  z-index: 3;
612
748
  opacity: 1;
613
- transition: .15s ease;
749
+ transition: 0.15s ease;
614
750
  border: none;
615
751
  outline: none;
616
752
  cursor: pointer;
@@ -629,7 +765,8 @@
629
765
  }
630
766
  }
631
767
 
632
- .headers, .statusbar {
768
+ .headers,
769
+ .statusbar {
633
770
  /* So that the scrollbar doesn't cause the headers/statusbar to shift */
634
771
  padding-right: 11px;
635
772
  }
@@ -652,29 +789,34 @@
652
789
  .headers > .column {
653
790
  border-right: 1px solid var(--tably-border, hsl(0, 0%, 90%));
654
791
  overflow: hidden;
655
- padding: var(--tably-padding-y, .5rem) 0;
792
+ padding: var(--tably-padding-y, 0.5rem) 0;
793
+ cursor: default;
794
+ user-select: none;
795
+
796
+ &.sortable {
797
+ cursor: pointer;
798
+ }
656
799
 
657
800
  &.resizeable {
658
801
  resize: horizontal;
659
802
  }
660
803
  }
661
-
804
+
662
805
  .table {
663
806
  display: grid;
664
807
  height: 100%;
665
808
  position: relative;
666
809
 
667
- grid-template-areas:
810
+ grid-template-areas:
668
811
  'headers panel'
669
812
  'rows panel'
670
- 'statusbar panel'
671
- ;
813
+ 'statusbar panel';
672
814
 
673
815
  grid-template-columns: auto min-content;
674
816
  grid-template-rows: auto 1fr auto;
675
817
 
676
818
  border: 1px solid var(--tably-border, hsl(0, 0%, 90%));
677
- border-radius: var(--tably-radius, .25rem);
819
+ border-radius: var(--tably-radius, 0.25rem);
678
820
 
679
821
  max-height: 100%;
680
822
  }
@@ -708,10 +850,12 @@
708
850
 
709
851
  .statusbar > tr > .column {
710
852
  border-top: 1px solid var(--tably-border, hsl(0, 0%, 90%));
711
- padding: calc(var(--tably-padding-y, .5rem) / 2) 0;
853
+ padding: calc(var(--tably-padding-y, 0.5rem) / 2) 0;
712
854
  }
713
855
 
714
- .headers, .row, .statusbar > tr {
856
+ .headers,
857
+ .row,
858
+ .statusbar > tr {
715
859
  position: relative;
716
860
  display: grid;
717
861
  width: 100%;
@@ -730,14 +874,14 @@
730
874
  }
731
875
 
732
876
  .row:first-child > * {
733
- padding-top: calc(var(--tably-padding-y, .5rem) + calc(var(--tably-padding-y, .5rem) / 2));
877
+ padding-top: calc(var(--tably-padding-y, 0.5rem) + calc(var(--tably-padding-y, 0.5rem) / 2));
734
878
  }
735
879
  .row:last-child > * {
736
- padding-bottom: calc(var(--tably-padding-y, .5rem) + calc(var(--tably-padding-y, .5rem) / 2));
880
+ padding-bottom: calc(var(--tably-padding-y, 0.5rem) + calc(var(--tably-padding-y, 0.5rem) / 2));
737
881
  }
738
882
 
739
883
  .row > * {
740
- padding: calc(var(--tably-padding-y, .5rem) / 2) 0;
884
+ padding: calc(var(--tably-padding-y, 0.5rem) / 2) 0;
741
885
  }
742
886
 
743
887
  .panel {
@@ -756,8 +900,7 @@
756
900
  right: 0;
757
901
  width: min-content;
758
902
  overflow: auto;
759
- padding: var(--tably-padding-y, .5rem) 0;
903
+ padding: var(--tably-padding-y, 0.5rem) 0;
760
904
  }
761
905
  }
762
-
763
- </style>
906
+ </style>