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

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(() => {
@@ -18,14 +18,15 @@
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,18 @@
223
230
  // * --- CSV --- *
224
231
 
225
232
 
226
- // * --- Expandable --- *
227
- let expandedRow = $state() as undefined | T
233
+ let expandedRow = $state([]) as T[]
234
+
235
+ function addRowColumnEvents(
236
+ node: HTMLTableColElement,
237
+ opts: ['header' | 'row' | 'statusbar', ColumnState, () => RowColumnCtx<T, any>]
238
+ ) {
239
+ const [where, column, value] = opts
240
+ if(where !== 'row') return
241
+ if(column.options.onclick) {
242
+ $effect(() => on(node, 'click', e => column.options.onclick!(e, value())))
243
+ }
244
+ }
228
245
 
229
246
  </script>
230
247
 
@@ -251,7 +268,8 @@
251
268
  value: column.options.value?.(row),
252
269
  isHovered: false,
253
270
  itemState: { index: i, dragging: false, positioning: false } as ItemState<any>,
254
- selected: false
271
+ selected: false,
272
+ expanded: false
255
273
  })}
256
274
  </td>
257
275
  {/each}
@@ -266,19 +284,17 @@
266
284
  {@html `<style>${style}</style>`}
267
285
  </svelte:head>
268
286
 
269
- {#snippet chevronSnippet(reversed: boolean)}
287
+ {#snippet chevronSnippet(rotation: number = 0)}
270
288
  <svg
271
- class="sorting-icon"
272
- class:reversed
273
289
  xmlns="http://www.w3.org/2000/svg"
274
290
  width="16"
275
291
  height="16"
276
292
  viewBox="0 0 16 16"
277
- style="margin: auto; margin-right: var(--tably-padding-x, 1rem);"
293
+ style="transform: rotate({rotation}deg)"
278
294
  >
279
295
  <path
280
296
  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"
297
+ 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
298
  ></path>
283
299
  </svg>
284
300
  {/snippet}
@@ -295,15 +311,20 @@
295
311
  {#snippet columnsSnippet(
296
312
  renderable: (column: ColumnState) => Snippet<[arg0?: any, arg1?: any]> | undefined,
297
313
  arg: null | ((column: ColumnState) => any[]) = null,
298
- isHeader = false
314
+ where: 'header' | 'row' | 'statusbar'
299
315
  )}
316
+ {@const isHeader = where === 'header'}
300
317
  {#each fixed as column, i (column)}
301
318
  {#if !hidden.includes(column)}
302
319
  {@const args = arg ? arg(column) : []}
303
320
  {@const sortable = isHeader && column.options.sort && !table.options.reorderable}
304
321
  <svelte:element
305
322
  this={isHeader ? 'th' : 'td'}
306
- class="column sticky fixed"
323
+ class={column.options.class ?? ''}
324
+ class:column={true}
325
+ class:sticky={true}
326
+ class:fixed={true}
327
+ use:addRowColumnEvents={[where, column, () => args[1]]}
307
328
  data-column={column.id}
308
329
  class:header={isHeader}
309
330
  class:sortable
@@ -311,7 +332,9 @@
311
332
  >
312
333
  {@render renderable(column)?.(args[0], args[1])}
313
334
  {#if isHeader && data.sortby === column.id && sortable}
314
- {@render chevronSnippet(data.sortReverse)}
335
+ <span class='sorting-icon'>
336
+ {@render chevronSnippet(data.sortReverse ? 0 : 180)}
337
+ </span>
315
338
  {/if}
316
339
  </svelte:element>
317
340
  {/if}
@@ -322,7 +345,10 @@
322
345
  {@const sortable = isHeader && column.options.sort && !table.options.reorderable}
323
346
  <svelte:element
324
347
  this={isHeader ? 'th' : 'td'}
325
- class="column sticky"
348
+ class={column.options.class ?? ''}
349
+ class:column={true}
350
+ class:sticky={true}
351
+ use:addRowColumnEvents={[where, column, () => args[1]]}
326
352
  use:observeColumnWidth={isHeader}
327
353
  data-column={column.id}
328
354
  class:header={isHeader}
@@ -333,7 +359,9 @@
333
359
  >
334
360
  {@render renderable(column)?.(args[0], args[1])}
335
361
  {#if isHeader && data.sortby === column.id && sortable}
336
- {@render chevronSnippet(data.sortReverse)}
362
+ <span class='sorting-icon'>
363
+ {@render chevronSnippet(data.sortReverse ? 0 : 180)}
364
+ </span>
337
365
  {/if}
338
366
  </svelte:element>
339
367
  {/if}
@@ -344,8 +372,10 @@
344
372
  {@const sortable = isHeader && column!.options.sort && !table.options.reorderable}
345
373
  <svelte:element
346
374
  this={isHeader ? 'th' : 'td'}
347
- class="column"
375
+ class={column.options.class ?? ''}
376
+ class:column={true}
348
377
  data-column={column.id}
378
+ use:addRowColumnEvents={[where, column, () => args[1]]}
349
379
  use:observeColumnWidth={isHeader}
350
380
  class:resizeable={isHeader && column.options.resizeable && table.options.resizeable}
351
381
  class:sortable
@@ -353,7 +383,9 @@
353
383
  >
354
384
  {@render renderable(column)?.(args[0], args[1])}
355
385
  {#if isHeader && data.sortby === column.id && sortable}
356
- {@render chevronSnippet(data.sortReverse)}
386
+ <span class='sorting-icon'>
387
+ {@render chevronSnippet(data.sortReverse ? 0 : 180)}
388
+ </span>
357
389
  {/if}
358
390
  </svelte:element>
359
391
  {/if}
@@ -361,8 +393,51 @@
361
393
  {/snippet}
362
394
 
363
395
  {#snippet rowSnippet(item: T, itemState?: ItemState<T>)}
364
- {@const i = itemState?.index ?? 0}
365
- {@const index = (itemState?.index ?? 0)}
396
+ {@const index = itemState?.index ?? 0}
397
+ {@const toggleExpand = (value?: boolean) => {
398
+ let indexOf = expandedRow.indexOf(item)
399
+ if(value !== undefined) {
400
+ value = indexOf === -1
401
+ }
402
+
403
+ if(!value) {
404
+ expandedRow.splice(indexOf, 1)
405
+ return
406
+ }
407
+ if(table.expandable?.options.multiple === true) {
408
+ expandedRow.push(item)
409
+ }
410
+ else {
411
+ expandedRow[0] = item
412
+ }
413
+ }}
414
+
415
+ {@const ctx: RowCtx<T> = {
416
+ get index() {
417
+ return index
418
+ },
419
+ get isHovered() {
420
+ return hoveredRow === item
421
+ },
422
+ get selected() {
423
+ return table.selected?.includes(item)
424
+ },
425
+ set selected(value) {
426
+ value ?
427
+ table.selected!.push(item)
428
+ : table.selected!.splice(table.selected!.indexOf(item), 1)
429
+ },
430
+ get itemState() {
431
+ return itemState
432
+ },
433
+ get expanded() {
434
+ return expandedRow.includes(item)
435
+ },
436
+ set expanded(value) {
437
+ toggleExpand(value)
438
+ }
439
+ }}
440
+
366
441
  <tr
367
442
  aria-rowindex={index + 1}
368
443
  data-svelte-tably={table.id}
@@ -371,19 +446,19 @@
371
446
  class:hover={hoveredRow === item}
372
447
  class:dragging={itemState?.dragging}
373
448
  class:selected={table.selected?.includes(item)}
374
- class:first={i === 0}
375
- class:last={i === virtualization.area.length - 1}
449
+ class:first={index === 0}
450
+ class:last={index === virtualization.area.length - 1}
376
451
  {...(table.options.href ? { href: table.options.href(item) } : {})}
377
452
  {...(itemState?.dragging ? { 'data-svelte-tably': table.id } : {})}
378
453
  onpointerenter={() => (hoveredRow = item)}
379
454
  onpointerleave={() => (hoveredRow = null)}
380
455
  onclick={(e) => {
381
- if (table.expandable) {
456
+ if (table.expandable?.options.click === true) {
382
457
  let target = e.target as HTMLElement
383
458
  if(['INPUT', 'TEXTAREA', 'BUTTON', 'A'].includes(target.tagName)) {
384
459
  return
385
460
  }
386
- expandedRow = expandedRow === item ? undefined : item
461
+ toggleExpand()
387
462
  }
388
463
  }}
389
464
  >
@@ -392,34 +467,20 @@
392
467
  (column) => {
393
468
  return [
394
469
  item,
395
- {
396
- get index() {
397
- return index
398
- },
470
+ assignDescriptors({
399
471
  get value() {
400
472
  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
473
  }
416
- }
474
+ }, ctx)
417
475
  ]
418
- }
476
+ },
477
+ 'row'
419
478
  )}
420
479
  </tr>
421
480
 
422
- {@const expandableTween = new SizeTween(() => table.expandable && expandedRow === item, { min: 1, duration: 150 })}
481
+ {@const expandableTween = new SizeTween(
482
+ () => table.expandable && expandedRow.includes(item),
483
+ { min: 1, duration: table.expandable?.options.slide.duration, easing: table.expandable?.options.slide.easing })}
423
484
  {#if expandableTween.current > 0}
424
485
  <tr class='expandable' style='height: {expandableTween.current}px'>
425
486
  <td
@@ -428,13 +489,9 @@
428
489
  >
429
490
  <div
430
491
  bind:offsetHeight={expandableTween.size}
431
- style='width: {tbody.width - 2}px'
492
+ style='width: {tbody.width - 3}px'
432
493
  >
433
- {@render table.expandable!.snippets.content?.(item, {
434
- close() {
435
- expandedRow = undefined
436
- }
437
- })}
494
+ {@render table.expandable!.snippets.content?.(item, ctx)}
438
495
  </div>
439
496
  </td>
440
497
  </tr>
@@ -447,16 +504,18 @@
447
504
  style='--t: {virtualization.virtualTop}px; --b: {virtualization.virtualBottom}px;'
448
505
  aria-rowcount={data.current.length}
449
506
  >
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>
507
+ {#if columns.some(v => v.snippets.header)}
508
+ <thead class='headers' bind:this={elements.headers}>
509
+ {@render columnsSnippet(
510
+ (column) => column.snippets.header,
511
+ () => [{
512
+ get header() { return true },
513
+ get data() { return data.current }
514
+ }],
515
+ 'header'
516
+ )}
517
+ </thead>
518
+ {/if}
460
519
 
461
520
  <tbody
462
521
  class='content'
@@ -479,22 +538,25 @@
479
538
  }
480
539
  })}
481
540
  {:else}
482
- {#each virtualization.area as item, i (item)}
483
- {@render rowSnippet(item, { index: i + virtualization.topIndex } as ItemState)}
484
- {/each}
541
+ {#each virtualization.area as item, i (item)}
542
+ {@render rowSnippet(item, { index: i + virtualization.topIndex } as ItemState)}
543
+ {/each}
485
544
  {/if}
486
545
  </tbody>
487
546
 
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>
547
+ {#if columns.some(v => v.snippets.statusbar)}
548
+ <tfoot class='statusbar' bind:this={elements.statusbar}>
549
+ <tr>
550
+ {@render columnsSnippet(
551
+ (column) => column.snippets.statusbar,
552
+ () => [{
553
+ get data() { return data.current }
554
+ }],
555
+ 'statusbar'
556
+ )}
557
+ </tr>
558
+ </tfoot>
559
+ {/if}
498
560
 
499
561
  <caption
500
562
  class='panel'
@@ -532,20 +594,25 @@
532
594
  <input type='checkbox' bind:checked={ctx.isSelected} />
533
595
  {/snippet}
534
596
 
535
- {#if table.options.select || table.options.reorderable}
597
+ {#if table.options.select || table.options.reorderable || table.expandable}
536
598
  {@const { select, reorderable } = table.options}
599
+ {@const expandable = table.expandable}
537
600
  {@const {
538
601
  show = 'hover',
539
602
  style = 'column',
540
603
  rowSnippet = rowSelected,
541
604
  headerSnippet = headerSelected
542
605
  } = typeof select === 'boolean' ? {} : select}
543
- {#if show !== 'never' || reorderable}
606
+ {#if show !== 'never' || reorderable || expandable?.options.chevron !== 'never'}
544
607
  <Column
545
608
  id='__fixed'
546
609
  {table}
547
610
  fixed
548
- width={Math.max(56, (select && show !== 'never' ? 34 : 0) + (reorderable ? 34 : 0))}
611
+ width={Math.max(56, 0
612
+ + (select && show !== 'never' ? 34 : 0)
613
+ + (reorderable ? 34 : 0)
614
+ + (expandable?.options.chevron !== 'never' ? 34 : 0)
615
+ )}
549
616
  resizeable={false}
550
617
  >
551
618
  {#snippet header()}
@@ -580,14 +647,14 @@
580
647
  {/snippet}
581
648
  {#snippet row(item, row)}
582
649
  <div class='__fixed'>
583
- {#if reorderable}
650
+ {#if reorderable && row.itemState}
584
651
  <span style='width: 16px; display: flex; align-items: center;' use:row.itemState.handle>
585
- {#if (row.isHovered && !row.itemState?.area.isTarget) || row.itemState.dragging}
652
+ {#if (row.isHovered && !row.itemState.area.isTarget) || row.itemState.dragging}
586
653
  {@render dragSnippet()}
587
654
  {/if}
588
655
  </span>
589
656
  {/if}
590
- {#if select && (row.selected || show === 'always' || (row.isHovered && show === 'hover'))}
657
+ {#if select && (row.selected || show === 'always' || (row.isHovered && show === 'hover') || row.expanded)}
591
658
  {@render rowSnippet({
592
659
  get isSelected() {
593
660
  return row.selected
@@ -606,6 +673,11 @@
606
673
  }
607
674
  })}
608
675
  {/if}
676
+ {#if expandable && (row.expanded || expandable.options.chevron === 'always' || (row.isHovered && expandable.options.chevron === 'hover'))}
677
+ <button class='expand-row' onclick={() => row.expanded = !row.expanded}>
678
+ {@render chevronSnippet(row.expanded ? 180 : 90)}
679
+ </button>
680
+ {/if}
609
681
  </div>
610
682
  {/snippet}
611
683
  </Column>
@@ -646,12 +718,30 @@
646
718
  > div {
647
719
  position: absolute;
648
720
  overflow: auto;
649
- top: 0;
721
+ top: -1.5px;
650
722
  left: 0;
651
723
  }
652
724
  }
653
725
  }
654
726
 
727
+ .expand-row {
728
+ display: flex;
729
+ justify-content: center;
730
+ align-items: center;
731
+ padding: 0;
732
+ outline: none;
733
+ border: none;
734
+ cursor: pointer;
735
+ background-color: transparent;
736
+ color: inherit;
737
+ width: 22px;
738
+ height: 100%;
739
+
740
+ > svg {
741
+ transition: transform 0.15s ease;
742
+ }
743
+ }
744
+
655
745
  caption {
656
746
  all: unset;
657
747
  }
@@ -669,10 +759,13 @@
669
759
  }
670
760
 
671
761
  .sorting-icon {
672
- transition: transform 0.15s ease;
673
- transform: rotateZ(0deg);
674
- &.reversed {
675
- transform: rotateZ(-180deg);
762
+ align-items: center;
763
+ justify-items: end;
764
+ margin: 0;
765
+ margin-left: auto;
766
+ margin-right: var(--tably-padding-x, 1rem);
767
+ > svg {
768
+ transition: transform 0.15s ease;
676
769
  }
677
770
  }
678
771
 
@@ -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.14",
4
4
  "repository": "github:refzlund/svelte-tably",
5
5
  "homepage": "https://github.com/Refzlund/svelte-tably",
6
6
  "bugs": {