svelte-tably 1.1.2 → 1.3.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 */
@@ -104,7 +104,23 @@ export class TableState {
104
104
  }
105
105
  /** Width of each column */
106
106
  columnWidths = $state({});
107
+ #storageKey() {
108
+ if (!this.id)
109
+ return null;
110
+ return `svelte-tably:${this.id}`;
111
+ }
112
+ #getStorage() {
113
+ try {
114
+ return localStorage;
115
+ }
116
+ catch {
117
+ return null;
118
+ }
119
+ }
107
120
  #save() {
121
+ const key = this.#storageKey();
122
+ if (!key)
123
+ return;
108
124
  const content = {
109
125
  columnWidths: this.columnWidths,
110
126
  positions: {
@@ -116,24 +132,56 @@ export class TableState {
116
132
  sortby: this.dataState.sortby,
117
133
  sortReverse: this.dataState.sortReverse
118
134
  };
119
- localStorage.setItem(`svelte-tably:${this.id}`, JSON.stringify(content));
135
+ const storage = this.#getStorage();
136
+ if (!storage)
137
+ return;
138
+ try {
139
+ storage.setItem(key, JSON.stringify(content));
140
+ }
141
+ catch {
142
+ return;
143
+ }
120
144
  }
121
145
  #saving = false;
146
+ #saveTimeout = null;
122
147
  #scheduleSave() {
123
148
  if (this.#saving)
124
149
  return;
125
- if (typeof localStorage === 'undefined')
150
+ if (!this.#storageKey())
151
+ return;
152
+ if (!this.#getStorage())
126
153
  return;
127
154
  this.#saving = true;
128
- setTimeout(() => {
155
+ if (this.#saveTimeout)
156
+ clearTimeout(this.#saveTimeout);
157
+ this.#saveTimeout = setTimeout(() => {
129
158
  this.#saving = false;
130
159
  this.#save();
131
160
  }, 1000);
132
161
  }
133
162
  #load() {
134
- if (typeof localStorage === 'undefined')
163
+ const key = this.#storageKey();
164
+ if (!key)
165
+ return null;
166
+ const storage = this.#getStorage();
167
+ if (!storage)
135
168
  return null;
136
- const item = JSON.parse(localStorage.getItem(`svelte-tably:${this.id}`) || '{}');
169
+ let raw = null;
170
+ try {
171
+ raw = storage.getItem(key);
172
+ }
173
+ catch {
174
+ return null;
175
+ }
176
+ let item;
177
+ try {
178
+ item = JSON.parse(raw || '{}');
179
+ }
180
+ catch {
181
+ return null;
182
+ }
183
+ if (!item || typeof item !== 'object')
184
+ item = {};
137
185
  item.columnWidths ??= {};
138
186
  item.positions ??= {};
139
187
  item.positions.fixed ??= [];
@@ -163,9 +211,20 @@ export class TableState {
163
211
  this.dataState.sortReverse = saved.sortReverse;
164
212
  }
165
213
  }
166
- if (typeof window !== 'undefined') {
167
- window.addEventListener('beforeunload', () => this.#save());
168
- }
214
+ $effect(() => {
215
+ if (typeof window === 'undefined')
216
+ return;
217
+ const handler = () => this.#save();
218
+ window.addEventListener('beforeunload', handler);
219
+ return () => window.removeEventListener('beforeunload', handler);
220
+ });
221
+ $effect(() => {
222
+ return () => {
223
+ if (this.#saveTimeout)
224
+ clearTimeout(this.#saveTimeout);
225
+ this.#saveTimeout = null;
226
+ };
227
+ });
169
228
  $effect(() => {
170
229
  Object.keys(this.columnWidths);
171
230
  // Track order changes by observing the id sequences
@@ -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.2",
3
+ "version": "1.3.0",
4
4
  "description": "A high performant dynamic table for Svelte 5",
5
5
  "license": "MIT",
6
6
  "repository": {