svelte-tably 1.1.1 → 1.2.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.
@@ -1,110 +1,39 @@
1
- import { SvelteComponent } from "svelte";
2
- import { type Snippet } from 'svelte';
1
+ import type { Snippet } from 'svelte';
3
2
  import { TableState, type TableProps } from './table-state.svelte.js';
4
- import { type RowColumnCtx } from '../column/column-state.svelte.js';
3
+ import Column from '../column/Column.svelte';
4
+ import Panel from '../panel/Panel.svelte';
5
+ import Expandable from '../expandable/Expandable.svelte';
6
+ import Row from '../row/Row.svelte';
7
+ type ConstructorReturnType<C extends new (...args: any[]) => any> = C extends new (...args: any[]) => infer K ? K : never;
8
+ type ConstructorParams<C extends new (...args: any[]) => any> = C extends new (...args: infer K) => any ? K : never;
9
+ export type ContentCtx<Item = any> = {
10
+ Column: {
11
+ new <V>(...args: ConstructorParams<typeof Column<Item, V>>): ConstructorReturnType<typeof Column<Item, V>>;
12
+ <V>(...args: Parameters<typeof Column<Item, V>>): ReturnType<typeof Column<Item, V>>;
13
+ };
14
+ Panel: typeof Panel<Item>;
15
+ Expandable: typeof Expandable<Item>;
16
+ Row: typeof Row<Item>;
17
+ readonly table: TableState<Item>;
18
+ };
19
+ export type ContentSnippet<Item = any> = Snippet<[context: ContentCtx<Item>]>;
5
20
  import type { CSVOptions } from './csv.js';
6
- declare class __sveltets_Render<T extends Record<PropertyKey, unknown>> {
21
+ declare class __sveltets_Render<T> {
7
22
  props(): TableProps<T> & {
8
- content?: Snippet<[context: {
9
- Column: {
10
- <V>(internal: unknown, props: {
11
- id: string;
12
- table?: TableState<T> | undefined;
13
- header?: string | Snippet<[ctx: import("../column/column-state.svelte.js").HeaderCtx<T>]> | undefined;
14
- row?: Snippet<[item: T, ctx: RowColumnCtx<T, V>]> | undefined;
15
- statusbar?: Snippet<[ctx: import("../column/column-state.svelte.js").StatusbarCtx<T>]> | undefined;
16
- sticky?: boolean | undefined;
17
- show?: boolean | undefined;
18
- sortby?: boolean | undefined;
19
- width?: number | undefined;
20
- fixed?: boolean | undefined;
21
- value?: ((item: T) => V) | undefined;
22
- sort?: boolean | ((a: V, b: V) => number) | undefined;
23
- resizeable?: boolean | undefined;
24
- filter?: ((value: V) => boolean) | undefined;
25
- style?: string | undefined;
26
- class?: string | undefined;
27
- onclick?: ((event: MouseEvent, rowColumnCtx: RowColumnCtx<T, V>) => void) | undefined;
28
- pad?: "row" | "header" | "both" | undefined;
29
- }): {};
30
- new <V>(options: import("svelte").ComponentConstructorOptions<{
31
- id: string;
32
- table?: TableState<T> | undefined;
33
- header?: string | Snippet<[ctx: import("../column/column-state.svelte.js").HeaderCtx<T>]> | undefined;
34
- row?: Snippet<[item: T, ctx: RowColumnCtx<T, V>]> | undefined;
35
- statusbar?: Snippet<[ctx: import("../column/column-state.svelte.js").StatusbarCtx<T>]> | undefined;
36
- sticky?: boolean | undefined;
37
- show?: boolean | undefined;
38
- sortby?: boolean | undefined;
39
- width?: number | undefined;
40
- fixed?: boolean | undefined;
41
- value?: ((item: T) => V) | undefined;
42
- sort?: boolean | ((a: V, b: V) => number) | undefined;
43
- resizeable?: boolean | undefined;
44
- filter?: ((value: V) => boolean) | undefined;
45
- style?: string | undefined;
46
- class?: string | undefined;
47
- onclick?: ((event: MouseEvent, rowColumnCtx: RowColumnCtx<T, V>) => void) | undefined;
48
- pad?: "row" | "header" | "both" | undefined;
49
- }>): SvelteComponent<{
50
- id: string;
51
- table?: TableState<T> | undefined;
52
- header?: string | Snippet<[ctx: import("../column/column-state.svelte.js").HeaderCtx<T>]> | undefined;
53
- row?: Snippet<[item: T, ctx: RowColumnCtx<T, V>]> | undefined;
54
- statusbar?: Snippet<[ctx: import("../column/column-state.svelte.js").StatusbarCtx<T>]> | undefined;
55
- sticky?: boolean | undefined;
56
- show?: boolean | undefined;
57
- sortby?: boolean | undefined;
58
- width?: number | undefined;
59
- fixed?: boolean | undefined;
60
- value?: ((item: T) => V) | undefined;
61
- sort?: boolean | ((a: V, b: V) => number) | undefined;
62
- resizeable?: boolean | undefined;
63
- filter?: ((value: V) => boolean) | undefined;
64
- style?: string | undefined;
65
- class?: string | undefined;
66
- onclick?: ((event: MouseEvent, rowColumnCtx: RowColumnCtx<T, V>) => void) | undefined;
67
- pad?: "row" | "header" | "both" | undefined;
68
- }, {}, {}> & {
69
- $$bindings?: ReturnType<() => "">;
70
- } & {};
71
- };
72
- Panel: {
73
- (internal: unknown, props: import("../index.js").PanelProps<T>): {};
74
- new (options: import("svelte").ComponentConstructorOptions<import("../index.js").PanelProps<T>>): SvelteComponent<import("../index.js").PanelProps<T>, {}, {}> & {
75
- $$bindings?: ReturnType<() => "">;
76
- };
77
- z_$$bindings?: ReturnType<() => "">;
78
- };
79
- Expandable: {
80
- (internal: unknown, props: import("../index.js").ExpandableProps<T>): {};
81
- new (options: import("svelte").ComponentConstructorOptions<import("../index.js").ExpandableProps<T>>): SvelteComponent<import("../index.js").ExpandableProps<T>, {}, {}> & {
82
- $$bindings?: ReturnType<() => "">;
83
- };
84
- z_$$bindings?: ReturnType<() => "">;
85
- };
86
- Row: {
87
- (internal: unknown, props: import("../index.js").RowProps<T>): {};
88
- new (options: import("svelte").ComponentConstructorOptions<import("../index.js").RowProps<T>>): SvelteComponent<import("../index.js").RowProps<T>, {}, {}> & {
89
- $$bindings?: ReturnType<() => "">;
90
- };
91
- z_$$bindings?: ReturnType<() => "">;
92
- };
93
- readonly table: TableState<T>;
94
- }]> | undefined;
23
+ content?: ContentSnippet<T> | undefined;
95
24
  };
96
25
  events(): {};
97
26
  slots(): {};
98
- bindings(): "table" | "selected" | "panel" | "data";
27
+ bindings(): "table" | "data" | "selected" | "panel";
99
28
  exports(): {
100
29
  toCSV: (options?: CSVOptions<T>) => Promise<string>;
101
30
  };
102
31
  }
103
32
  interface $$IsomorphicComponent {
104
- new <T extends Record<PropertyKey, unknown>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
33
+ new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
105
34
  $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
106
35
  } & ReturnType<__sveltets_Render<T>['exports']>;
107
- <T extends Record<PropertyKey, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
36
+ <T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
108
37
  z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
109
38
  }
110
39
  /**
@@ -115,5 +44,5 @@ interface $$IsomorphicComponent {
115
44
  * <Component />
116
45
  */
117
46
  declare const Table: $$IsomorphicComponent;
118
- type Table<T extends Record<PropertyKey, unknown>> = InstanceType<typeof Table<T>>;
47
+ type Table<T> = InstanceType<typeof Table<T>>;
119
48
  export default Table;
@@ -51,7 +51,7 @@ export class Data {
51
51
  constructor(table, props) {
52
52
  this.#table = table;
53
53
  this.origin = props.data;
54
- this.sorted = this.origin.toSorted();
54
+ this.sorted = [...this.origin];
55
55
  this.filtered = this.sorted;
56
56
  $effect(() => {
57
57
  this.origin = props.data;
@@ -67,15 +67,28 @@ export class Data {
67
67
  const table = this.#table;
68
68
  if (props.reorderable)
69
69
  return;
70
- const filters = [...props.filters ?? []];
70
+ // Track dependencies explicitly
71
+ props.filters;
72
+ this.sorted;
71
73
  for (const key in table.columns) {
72
- const filter = table.columns[key].options.filter;
73
- const valueOf = table.columns[key].options.value;
74
- if (filter && valueOf) {
75
- filters.push((item) => filter(valueOf(item)));
76
- }
74
+ table.columns[key].options.filter;
75
+ table.columns[key].options.value;
77
76
  }
78
- this.filtered = filters.length === 0 ? this.sorted : this.sorted.filter((value) => filters.every((filter) => filter(value)));
77
+ const filters = untrack(() => {
78
+ const all = [...props.filters ?? []];
79
+ for (const key in table.columns) {
80
+ const filter = table.columns[key].options.filter;
81
+ const valueOf = table.columns[key].options.value;
82
+ if (filter && valueOf) {
83
+ all.push((item) => filter(valueOf(item)));
84
+ }
85
+ }
86
+ return all;
87
+ });
88
+ this.filtered =
89
+ filters.length === 0 ?
90
+ this.sorted
91
+ : this.sorted.filter((value) => filters.every((filter) => filter(value)));
79
92
  });
80
93
  }
81
94
  }
@@ -5,7 +5,7 @@ import { Data } from './data.svelte.js';
5
5
  import type { ExpandableState } from '../expandable/expandable-state.svelte.js';
6
6
  import type { ItemState } from 'runic-reorder';
7
7
  import type { RowState } from '../row/row-state.svelte.js';
8
- import { CSVOptions } from "./csv.js";
8
+ import type { CSVOptions } from './csv.js';
9
9
  export type HeaderSelectCtx<T = any> = {
10
10
  isSelected: boolean;
11
11
  /** The list of selected items */
@@ -1,10 +1,10 @@
1
1
  import type { TableState } from './table-state.svelte.js';
2
- export declare class Virtualization<T extends Record<PropertyKey, unknown>> {
2
+ export declare class Virtualization<T extends any> {
3
3
  #private;
4
4
  scrollTop: number;
5
5
  viewport: {
6
6
  height: number;
7
- element: HTMLDivElement | null;
7
+ element: HTMLElement | null;
8
8
  };
9
9
  get topIndex(): number;
10
10
  get virtualTop(): number;
@@ -1,4 +1,4 @@
1
- import { tick, untrack } from 'svelte';
1
+ import { untrack } from 'svelte';
2
2
  export class Virtualization {
3
3
  scrollTop = $state(0);
4
4
  viewport = $state({
@@ -25,47 +25,86 @@ export class Virtualization {
25
25
  requestAnimationFrame(() => ticked = true);
26
26
  });
27
27
  });
28
+ let measureRaf = 0;
29
+ let measureRun = 0;
28
30
  $effect(() => {
29
31
  if (!ticked)
30
32
  return;
31
33
  table.dataState.current;
34
+ this.viewport.element;
35
+ this.viewport.height;
36
+ let aborted = false;
37
+ const run = ++measureRun;
32
38
  untrack(() => {
33
- if (!this.viewport.element) {
39
+ const target = this.viewport.element;
40
+ if (!target) {
34
41
  this.#heightPerItem = 8;
35
42
  return;
36
43
  }
37
- tick().then(() => {
38
- const target = this.viewport.element;
39
- if (target.children.length === 0)
44
+ if (measureRaf)
45
+ cancelAnimationFrame(measureRaf);
46
+ measureRaf = requestAnimationFrame(() => {
47
+ if (aborted)
40
48
  return;
41
- const firstRow = target.children[0]?.getBoundingClientRect().top;
42
- const lastRow = target.children[target.children.length - 1].getBoundingClientRect().bottom;
43
- this.#heightPerItem = (lastRow - firstRow) / this.#area.length;
49
+ if (run !== measureRun)
50
+ return;
51
+ const el = this.viewport.element;
52
+ if (!el)
53
+ return;
54
+ const rowEls = el.querySelectorAll(':scope > tr.row');
55
+ const children = rowEls.length > 0 ? rowEls : el.children;
56
+ const count = children.length;
57
+ if (count === 0)
58
+ return;
59
+ const first = children[0]?.getBoundingClientRect().top;
60
+ const last = children[count - 1]?.getBoundingClientRect().bottom;
61
+ if (first === undefined || last === undefined)
62
+ return;
63
+ const height = (last - first) / count;
64
+ if (!Number.isFinite(height) || height <= 0)
65
+ return;
66
+ // Avoid tiny oscillations causing endless updates.
67
+ if (Math.abs(height - this.#heightPerItem) < 0.25)
68
+ return;
69
+ this.#heightPerItem = height;
44
70
  });
45
71
  });
72
+ return () => {
73
+ aborted = true;
74
+ if (measureRaf)
75
+ cancelAnimationFrame(measureRaf);
76
+ };
46
77
  });
47
- let waitAnimationFrame = false;
78
+ let virtualRaf = 0;
48
79
  $effect(() => {
49
80
  if (!ticked)
50
81
  return;
51
82
  this.scrollTop;
52
83
  this.#heightPerItem;
84
+ this.viewport.height;
53
85
  table.dataState.current.length;
54
- table.dataState.current;
55
86
  untrack(() => {
56
- if (!waitAnimationFrame) {
57
- setTimeout(() => {
58
- waitAnimationFrame = false;
59
- let virtualTop = Math.max(this.scrollTop - this.#spacing, 0);
60
- virtualTop -= virtualTop % this.#heightPerItem;
61
- this.#virtualTop = virtualTop;
62
- let virtualBottom = this.#heightPerItem * table.dataState.current.length - virtualTop - this.#spacing * 4;
63
- virtualBottom = Math.max(virtualBottom, 0);
64
- this.#virtualBottom = virtualBottom;
65
- }, 1000 / 60);
66
- }
67
- waitAnimationFrame = true;
87
+ if (virtualRaf)
88
+ cancelAnimationFrame(virtualRaf);
89
+ virtualRaf = requestAnimationFrame(() => {
90
+ virtualRaf = 0;
91
+ const heightPerItem = this.#heightPerItem || 8;
92
+ const spacing = this.#spacing;
93
+ let virtualTop = Math.max(this.scrollTop - spacing, 0);
94
+ if (heightPerItem > 0) {
95
+ virtualTop -= virtualTop % heightPerItem;
96
+ }
97
+ this.#virtualTop = virtualTop;
98
+ let virtualBottom = heightPerItem * table.dataState.current.length - virtualTop - spacing * 4;
99
+ if (!Number.isFinite(virtualBottom))
100
+ virtualBottom = 0;
101
+ this.#virtualBottom = Math.max(virtualBottom, 0);
102
+ });
68
103
  });
104
+ return () => {
105
+ if (virtualRaf)
106
+ cancelAnimationFrame(virtualRaf);
107
+ };
69
108
  });
70
109
  $effect(() => {
71
110
  if (!ticked)
@@ -74,10 +113,15 @@ export class Virtualization {
74
113
  table.dataState.sortby;
75
114
  this.#heightPerItem;
76
115
  this.#virtualTop;
116
+ this.viewport.height;
117
+ this.#renderItemLength;
77
118
  table.dataState.current.length;
78
119
  table.dataState.current;
79
120
  untrack(() => {
80
- this.#topIndex = Math.round(this.#virtualTop / this.#heightPerItem || 0);
121
+ const heightPerItem = this.#heightPerItem || 8;
122
+ this.#topIndex = Math.round(this.#virtualTop / heightPerItem || 0);
123
+ if (this.#topIndex < 0)
124
+ this.#topIndex = 0;
81
125
  const end = this.#topIndex + this.#renderItemLength;
82
126
  this.#area = table.dataState.current.slice(this.#topIndex, end);
83
127
  });
@@ -1,4 +1,4 @@
1
- import { Snippet } from 'svelte';
1
+ import { type Snippet } from 'svelte';
2
2
  export type Simplify<T> = T extends infer V ? {
3
3
  [K in keyof V]: V[K];
4
4
  } : never;
@@ -4,7 +4,7 @@ export function pick(item, keys) {
4
4
  }
5
5
  export function boundPick(item, keys) {
6
6
  const obj = {};
7
- for (const key in keys) {
7
+ for (const key of keys) {
8
8
  obj[key] = [() => item[key], (v) => item[key] = v];
9
9
  }
10
10
  return Object.defineProperties({}, withSetters(obj));
@@ -30,7 +30,7 @@ export function boundAssign(item, props = {}) {
30
30
  boundAssign(item[key], value);
31
31
  continue;
32
32
  }
33
- Object.defineProperty(item, value, {
33
+ Object.defineProperty(item, key, {
34
34
  get() { return props[key]; },
35
35
  set(v) { props[key] = v; }
36
36
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-tably",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "A high performant dynamic table for Svelte 5",
5
5
  "license": "MIT",
6
6
  "repository": {