svelte-tably 1.0.0-next.13 → 1.0.0-next.15

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.
@@ -40,6 +40,9 @@ export class ColumnState {
40
40
  filter: this.#props.filter,
41
41
  value: this.#props.value,
42
42
  resizeable: this.#props.resizeable ?? true,
43
+ style: this.#props.style,
44
+ class: this.#props.class,
45
+ onclick: this.#props.onclick
43
46
  });
44
47
  toggleVisiblity() {
45
48
  const index = this.table.positions.hidden.indexOf(this);
@@ -1,12 +1,19 @@
1
1
  import { TableState } from '../table/table.svelte.js';
2
+ import { sineInOut } from 'svelte/easing';
2
3
  export class ExpandableState {
3
4
  #table;
4
- #props = $state({});
5
+ #props = {};
5
6
  snippets = $derived({
6
7
  content: this.#props.content
7
8
  });
8
9
  options = $derived({
9
- slide: this.#props.slide ?? 200
10
+ slide: {
11
+ duration: this.#props.slide?.duration ?? 150,
12
+ easing: this.#props.slide?.easing ?? sineInOut
13
+ },
14
+ click: this.#props.click ?? true,
15
+ chevron: this.#props.chevron ?? 'hover',
16
+ multiple: this.#props.multiple ?? false
10
17
  });
11
18
  constructor(props) {
12
19
  this.#props = props;
@@ -1,6 +1,8 @@
1
+ import type { EasingFunction } from 'svelte/transition';
1
2
  interface SizeOptions {
2
3
  min?: number;
3
4
  duration?: number;
5
+ easing?: EasingFunction;
4
6
  }
5
7
  export declare class SizeTween {
6
8
  #private;
@@ -16,6 +16,9 @@ export class SizeTween {
16
16
  if ('duration' in opts) {
17
17
  this.#tweenOptions.duration = opts.duration;
18
18
  }
19
+ if ('easing' in opts) {
20
+ this.#tweenOptions.easing = opts.easing;
21
+ }
19
22
  untrack(() => {
20
23
  if (cb()) {
21
24
  requestAnimationFrame(() => {
@@ -13,19 +13,20 @@
13
13
  </script>
14
14
 
15
15
  <script lang="ts">
16
- import { untrack, type Snippet } from 'svelte'
16
+ import { tick, untrack, type Snippet } from 'svelte'
17
17
  import { fly } from 'svelte/transition'
18
18
  import { sineInOut } from 'svelte/easing'
19
19
  import reorder, { type ItemState } from 'runic-reorder'
20
20
  import { Virtualization } from './virtualization.svelte.js'
21
- import { TableState, type HeaderSelectCtx, type RowSelectCtx, type TableProps } from './table.svelte.js'
21
+ import { TableState, type HeaderSelectCtx, type RowCtx, type RowSelectCtx, type TableProps } from './table.svelte.js'
22
22
  import Panel, { PanelTween } from '../panel/Panel.svelte'
23
23
  import Column from '../column/Column.svelte'
24
- import { fromProps, mounted } from '../utility.svelte.js'
24
+ import { assignDescriptors, fromProps, mounted } from '../utility.svelte.js'
25
25
  import { conditional } from '../conditional.svelte.js'
26
- import { ColumnState } from '../column/column.svelte.js'
26
+ import { ColumnState, type RowColumnCtx } from '../column/column.svelte.js'
27
27
  import Expandable from '../expandable/Expandable.svelte'
28
28
  import { SizeTween } from '../size-tween.svelte.js'
29
+ import { on } from 'svelte/events'
29
30
 
30
31
  type T = $$Generic<Record<PropertyKey, unknown>>
31
32
 
@@ -122,7 +123,13 @@
122
123
  })
123
124
  .join('')
124
125
 
125
- return templateColumns + stickyLeft
126
+ const columnStyling = columns.map(column => !column.options.style ? '' : `
127
+ #${table.id} .column[data-column='${column.id}'] {
128
+ ${column.options.style}
129
+ }
130
+ `).join('')
131
+
132
+ return templateColumns + stickyLeft + columnStyling
126
133
  })
127
134
 
128
135
  function observeColumnWidth(node: HTMLDivElement, isHeader = false) {
@@ -223,8 +230,39 @@
223
230
  // * --- CSV --- *
224
231
 
225
232
 
226
- // * --- Expandable --- *
227
- let expandedRow = $state() as undefined | T
233
+ let expandedRow = $state([]) as T[]
234
+ let expandTick = false
235
+ function toggleExpand(item: T, value?: boolean) {
236
+ if(expandTick) return
237
+ expandTick = true
238
+ requestAnimationFrame(() => expandTick = false)
239
+
240
+ let indexOf = expandedRow.indexOf(item)
241
+ if(value === undefined) {
242
+ value = indexOf === -1
243
+ }
244
+ if(!value) {
245
+ expandedRow.splice(indexOf, 1)
246
+ return
247
+ }
248
+ if(table.expandable?.options.multiple === true) {
249
+ expandedRow.push(item)
250
+ }
251
+ else {
252
+ expandedRow[0] = item
253
+ }
254
+ }
255
+
256
+ function addRowColumnEvents(
257
+ node: HTMLTableColElement,
258
+ opts: ['header' | 'row' | 'statusbar', ColumnState, () => RowColumnCtx<T, any>]
259
+ ) {
260
+ const [where, column, value] = opts
261
+ if(where !== 'row') return
262
+ if(column.options.onclick) {
263
+ $effect(() => on(node, 'click', e => column.options.onclick!(e, value())))
264
+ }
265
+ }
228
266
 
229
267
  </script>
230
268
 
@@ -251,7 +289,8 @@
251
289
  value: column.options.value?.(row),
252
290
  isHovered: false,
253
291
  itemState: { index: i, dragging: false, positioning: false } as ItemState<any>,
254
- selected: false
292
+ selected: false,
293
+ expanded: false
255
294
  })}
256
295
  </td>
257
296
  {/each}
@@ -266,19 +305,17 @@
266
305
  {@html `<style>${style}</style>`}
267
306
  </svelte:head>
268
307
 
269
- {#snippet chevronSnippet(reversed: boolean)}
308
+ {#snippet chevronSnippet(rotation: number = 0)}
270
309
  <svg
271
- class="sorting-icon"
272
- class:reversed
273
310
  xmlns="http://www.w3.org/2000/svg"
274
311
  width="16"
275
312
  height="16"
276
313
  viewBox="0 0 16 16"
277
- style="margin: auto; margin-right: var(--tably-padding-x, 1rem);"
314
+ style="transform: rotate({rotation}deg)"
278
315
  >
279
316
  <path
280
317
  fill="currentColor"
281
- 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"
318
+ 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"
282
319
  ></path>
283
320
  </svg>
284
321
  {/snippet}
@@ -295,15 +332,20 @@
295
332
  {#snippet columnsSnippet(
296
333
  renderable: (column: ColumnState) => Snippet<[arg0?: any, arg1?: any]> | undefined,
297
334
  arg: null | ((column: ColumnState) => any[]) = null,
298
- isHeader = false
335
+ where: 'header' | 'row' | 'statusbar'
299
336
  )}
337
+ {@const isHeader = where === 'header'}
300
338
  {#each fixed as column, i (column)}
301
339
  {#if !hidden.includes(column)}
302
340
  {@const args = arg ? arg(column) : []}
303
341
  {@const sortable = isHeader && column.options.sort && !table.options.reorderable}
304
342
  <svelte:element
305
343
  this={isHeader ? 'th' : 'td'}
306
- class="column sticky fixed"
344
+ class={column.options.class ?? ''}
345
+ class:column={true}
346
+ class:sticky={true}
347
+ class:fixed={true}
348
+ use:addRowColumnEvents={[where, column, () => args[1]]}
307
349
  data-column={column.id}
308
350
  class:header={isHeader}
309
351
  class:sortable
@@ -311,7 +353,9 @@
311
353
  >
312
354
  {@render renderable(column)?.(args[0], args[1])}
313
355
  {#if isHeader && data.sortby === column.id && sortable}
314
- {@render chevronSnippet(data.sortReverse)}
356
+ <span class='sorting-icon'>
357
+ {@render chevronSnippet(data.sortReverse ? 0 : 180)}
358
+ </span>
315
359
  {/if}
316
360
  </svelte:element>
317
361
  {/if}
@@ -322,7 +366,10 @@
322
366
  {@const sortable = isHeader && column.options.sort && !table.options.reorderable}
323
367
  <svelte:element
324
368
  this={isHeader ? 'th' : 'td'}
325
- class="column sticky"
369
+ class={column.options.class ?? ''}
370
+ class:column={true}
371
+ class:sticky={true}
372
+ use:addRowColumnEvents={[where, column, () => args[1]]}
326
373
  use:observeColumnWidth={isHeader}
327
374
  data-column={column.id}
328
375
  class:header={isHeader}
@@ -333,7 +380,9 @@
333
380
  >
334
381
  {@render renderable(column)?.(args[0], args[1])}
335
382
  {#if isHeader && data.sortby === column.id && sortable}
336
- {@render chevronSnippet(data.sortReverse)}
383
+ <span class='sorting-icon'>
384
+ {@render chevronSnippet(data.sortReverse ? 0 : 180)}
385
+ </span>
337
386
  {/if}
338
387
  </svelte:element>
339
388
  {/if}
@@ -344,8 +393,10 @@
344
393
  {@const sortable = isHeader && column!.options.sort && !table.options.reorderable}
345
394
  <svelte:element
346
395
  this={isHeader ? 'th' : 'td'}
347
- class="column"
396
+ class={column.options.class ?? ''}
397
+ class:column={true}
348
398
  data-column={column.id}
399
+ use:addRowColumnEvents={[where, column, () => args[1]]}
349
400
  use:observeColumnWidth={isHeader}
350
401
  class:resizeable={isHeader && column.options.resizeable && table.options.resizeable}
351
402
  class:sortable
@@ -353,7 +404,9 @@
353
404
  >
354
405
  {@render renderable(column)?.(args[0], args[1])}
355
406
  {#if isHeader && data.sortby === column.id && sortable}
356
- {@render chevronSnippet(data.sortReverse)}
407
+ <span class='sorting-icon'>
408
+ {@render chevronSnippet(data.sortReverse ? 0 : 180)}
409
+ </span>
357
410
  {/if}
358
411
  </svelte:element>
359
412
  {/if}
@@ -361,8 +414,34 @@
361
414
  {/snippet}
362
415
 
363
416
  {#snippet rowSnippet(item: T, itemState?: ItemState<T>)}
364
- {@const i = itemState?.index ?? 0}
365
- {@const index = (itemState?.index ?? 0)}
417
+ {@const index = itemState?.index ?? 0}
418
+
419
+ {@const ctx: RowCtx<T> = {
420
+ get index() {
421
+ return index
422
+ },
423
+ get isHovered() {
424
+ return hoveredRow === item
425
+ },
426
+ get selected() {
427
+ return table.selected?.includes(item)
428
+ },
429
+ set selected(value) {
430
+ value ?
431
+ table.selected!.push(item)
432
+ : table.selected!.splice(table.selected!.indexOf(item), 1)
433
+ },
434
+ get itemState() {
435
+ return itemState
436
+ },
437
+ get expanded() {
438
+ return expandedRow.includes(item)
439
+ },
440
+ set expanded(value) {
441
+ toggleExpand(item, value)
442
+ }
443
+ }}
444
+
366
445
  <tr
367
446
  aria-rowindex={index + 1}
368
447
  data-svelte-tably={table.id}
@@ -371,19 +450,19 @@
371
450
  class:hover={hoveredRow === item}
372
451
  class:dragging={itemState?.dragging}
373
452
  class:selected={table.selected?.includes(item)}
374
- class:first={i === 0}
375
- class:last={i === virtualization.area.length - 1}
453
+ class:first={index === 0}
454
+ class:last={index === virtualization.area.length - 1}
376
455
  {...(table.options.href ? { href: table.options.href(item) } : {})}
377
456
  {...(itemState?.dragging ? { 'data-svelte-tably': table.id } : {})}
378
457
  onpointerenter={() => (hoveredRow = item)}
379
458
  onpointerleave={() => (hoveredRow = null)}
380
459
  onclick={(e) => {
381
- if (table.expandable) {
460
+ if (table.expandable?.options.click === true) {
382
461
  let target = e.target as HTMLElement
383
462
  if(['INPUT', 'TEXTAREA', 'BUTTON', 'A'].includes(target.tagName)) {
384
463
  return
385
464
  }
386
- expandedRow = expandedRow === item ? undefined : item
465
+ ctx.expanded = !ctx.expanded
387
466
  }
388
467
  }}
389
468
  >
@@ -392,34 +471,20 @@
392
471
  (column) => {
393
472
  return [
394
473
  item,
395
- {
396
- get index() {
397
- return index
398
- },
474
+ assignDescriptors({
399
475
  get value() {
400
476
  return column.options.value ? column.options.value(item) : undefined
401
- },
402
- get isHovered() {
403
- return hoveredRow === item
404
- },
405
- get selected() {
406
- return table.selected?.includes(item)
407
- },
408
- set selected(value) {
409
- value ?
410
- table.selected!.push(item)
411
- : table.selected!.splice(table.selected!.indexOf(item), 1)
412
- },
413
- get itemState() {
414
- return itemState
415
477
  }
416
- }
478
+ }, ctx)
417
479
  ]
418
- }
480
+ },
481
+ 'row'
419
482
  )}
420
483
  </tr>
421
484
 
422
- {@const expandableTween = new SizeTween(() => table.expandable && expandedRow === item, { min: 1, duration: 150 })}
485
+ {@const expandableTween = new SizeTween(
486
+ () => table.expandable && expandedRow.includes(item),
487
+ { min: 1, duration: table.expandable?.options.slide.duration, easing: table.expandable?.options.slide.easing })}
423
488
  {#if expandableTween.current > 0}
424
489
  <tr class='expandable' style='height: {expandableTween.current}px'>
425
490
  <td
@@ -428,13 +493,9 @@
428
493
  >
429
494
  <div
430
495
  bind:offsetHeight={expandableTween.size}
431
- style='width: {tbody.width - 2}px'
496
+ style='width: {tbody.width - 3}px'
432
497
  >
433
- {@render table.expandable!.snippets.content?.(item, {
434
- close() {
435
- expandedRow = undefined
436
- }
437
- })}
498
+ {@render table.expandable!.snippets.content?.(item, ctx)}
438
499
  </div>
439
500
  </td>
440
501
  </tr>
@@ -447,16 +508,18 @@
447
508
  style='--t: {virtualization.virtualTop}px; --b: {virtualization.virtualBottom}px;'
448
509
  aria-rowcount={data.current.length}
449
510
  >
450
- <thead class='headers' bind:this={elements.headers}>
451
- {@render columnsSnippet(
452
- (column) => column.snippets.header,
453
- () => [{
454
- get header() { return true },
455
- get data() { return data.current }
456
- }],
457
- true
458
- )}
459
- </thead>
511
+ {#if columns.some(v => v.snippets.header)}
512
+ <thead class='headers' bind:this={elements.headers}>
513
+ {@render columnsSnippet(
514
+ (column) => column.snippets.header,
515
+ () => [{
516
+ get header() { return true },
517
+ get data() { return data.current }
518
+ }],
519
+ 'header'
520
+ )}
521
+ </thead>
522
+ {/if}
460
523
 
461
524
  <tbody
462
525
  class='content'
@@ -479,22 +542,25 @@
479
542
  }
480
543
  })}
481
544
  {:else}
482
- {#each virtualization.area as item, i (item)}
483
- {@render rowSnippet(item, { index: i + virtualization.topIndex } as ItemState)}
484
- {/each}
545
+ {#each virtualization.area as item, i (item)}
546
+ {@render rowSnippet(item, { index: i + virtualization.topIndex } as ItemState)}
547
+ {/each}
485
548
  {/if}
486
549
  </tbody>
487
550
 
488
- <tfoot class='statusbar' bind:this={elements.statusbar}>
489
- <tr>
490
- {@render columnsSnippet(
491
- (column) => column.snippets.statusbar,
492
- () => [{
493
- get data() { return data.current }
494
- }]
495
- )}
496
- </tr>
497
- </tfoot>
551
+ {#if columns.some(v => v.snippets.statusbar)}
552
+ <tfoot class='statusbar' bind:this={elements.statusbar}>
553
+ <tr>
554
+ {@render columnsSnippet(
555
+ (column) => column.snippets.statusbar,
556
+ () => [{
557
+ get data() { return data.current }
558
+ }],
559
+ 'statusbar'
560
+ )}
561
+ </tr>
562
+ </tfoot>
563
+ {/if}
498
564
 
499
565
  <caption
500
566
  class='panel'
@@ -532,20 +598,25 @@
532
598
  <input type='checkbox' bind:checked={ctx.isSelected} />
533
599
  {/snippet}
534
600
 
535
- {#if table.options.select || table.options.reorderable}
601
+ {#if table.options.select || table.options.reorderable || table.expandable}
536
602
  {@const { select, reorderable } = table.options}
603
+ {@const expandable = table.expandable}
537
604
  {@const {
538
605
  show = 'hover',
539
606
  style = 'column',
540
607
  rowSnippet = rowSelected,
541
608
  headerSnippet = headerSelected
542
609
  } = typeof select === 'boolean' ? {} : select}
543
- {#if show !== 'never' || reorderable}
610
+ {#if show !== 'never' || reorderable || expandable?.options.chevron !== 'never'}
544
611
  <Column
545
612
  id='__fixed'
546
613
  {table}
547
614
  fixed
548
- width={Math.max(56, (select && show !== 'never' ? 34 : 0) + (reorderable ? 34 : 0))}
615
+ width={Math.max(56, 0
616
+ + (select && show !== 'never' ? 34 : 0)
617
+ + (reorderable ? 34 : 0)
618
+ + (expandable?.options.chevron !== 'never' ? 34 : 0)
619
+ )}
549
620
  resizeable={false}
550
621
  >
551
622
  {#snippet header()}
@@ -580,14 +651,14 @@
580
651
  {/snippet}
581
652
  {#snippet row(item, row)}
582
653
  <div class='__fixed'>
583
- {#if reorderable}
654
+ {#if reorderable && row.itemState}
584
655
  <span style='width: 16px; display: flex; align-items: center;' use:row.itemState.handle>
585
- {#if (row.isHovered && !row.itemState?.area.isTarget) || row.itemState.dragging}
656
+ {#if (row.isHovered && !row.itemState.area.isTarget) || row.itemState.dragging}
586
657
  {@render dragSnippet()}
587
658
  {/if}
588
659
  </span>
589
660
  {/if}
590
- {#if select && (row.selected || show === 'always' || (row.isHovered && show === 'hover'))}
661
+ {#if select && (row.selected || show === 'always' || (row.isHovered && show === 'hover') || row.expanded)}
591
662
  {@render rowSnippet({
592
663
  get isSelected() {
593
664
  return row.selected
@@ -606,6 +677,11 @@
606
677
  }
607
678
  })}
608
679
  {/if}
680
+ {#if expandable && (row.expanded || expandable.options.chevron === 'always' || (row.isHovered && expandable.options.chevron === 'hover'))}
681
+ <button class='expand-row' onclick={() => row.expanded = !row.expanded}>
682
+ {@render chevronSnippet(row.expanded ? 180 : 90)}
683
+ </button>
684
+ {/if}
609
685
  </div>
610
686
  {/snippet}
611
687
  </Column>
@@ -646,12 +722,30 @@
646
722
  > div {
647
723
  position: absolute;
648
724
  overflow: auto;
649
- top: 0;
725
+ top: -1.5px;
650
726
  left: 0;
651
727
  }
652
728
  }
653
729
  }
654
730
 
731
+ .expand-row {
732
+ display: flex;
733
+ justify-content: center;
734
+ align-items: center;
735
+ padding: 0;
736
+ outline: none;
737
+ border: none;
738
+ cursor: pointer;
739
+ background-color: transparent;
740
+ color: inherit;
741
+ width: 22px;
742
+ height: 100%;
743
+
744
+ > svg {
745
+ transition: transform 0.15s ease;
746
+ }
747
+ }
748
+
655
749
  caption {
656
750
  all: unset;
657
751
  }
@@ -669,10 +763,13 @@
669
763
  }
670
764
 
671
765
  .sorting-icon {
672
- transition: transform 0.15s ease;
673
- transform: rotateZ(0deg);
674
- &.reversed {
675
- transform: rotateZ(-180deg);
766
+ align-items: center;
767
+ justify-items: end;
768
+ margin: 0;
769
+ margin-left: auto;
770
+ margin-right: var(--tably-padding-x, 1rem);
771
+ > svg {
772
+ transition: transform 0.15s ease;
676
773
  }
677
774
  }
678
775
 
@@ -13,4 +13,5 @@ export declare function getters<T extends AnyRecord>(obj: T): { readonly [K in k
13
13
  type SetterRecord = Record<PropertyKey, [() => any, (v: any) => void]>;
14
14
  export declare function withSetters<T extends SetterRecord>(obj: T): T;
15
15
  export declare function fromProps<T extends AnyRecord, B extends SetterRecord>(props: T, boundProps?: B): Simplify<{ [K in keyof B]: ReturnType<B[K][0]>; } & { readonly [K in keyof T]: T[K]; }>;
16
+ export declare function assignDescriptors<T extends AnyRecord, B extends AnyRecord>(target: T, source: B): T & B;
16
17
  export {};
@@ -66,3 +66,15 @@ export function fromProps(props, boundProps) {
66
66
  ...withSetters(boundProps ?? {})
67
67
  });
68
68
  }
69
+ export function assignDescriptors(target, source) {
70
+ for (const key of Object.keys(source)) {
71
+ const descriptor = Object.getOwnPropertyDescriptor(source, key);
72
+ if (descriptor) {
73
+ Object.defineProperty(target, key, descriptor);
74
+ }
75
+ else {
76
+ target[key] = source[key]; // Copy regular values if descriptor is missing
77
+ }
78
+ }
79
+ return target;
80
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-tably",
3
- "version": "1.0.0-next.13",
3
+ "version": "1.0.0-next.15",
4
4
  "repository": "github:refzlund/svelte-tably",
5
5
  "homepage": "https://github.com/Refzlund/svelte-tably",
6
6
  "bugs": {