firstly 0.0.15 → 0.0.16-next.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.
Files changed (95) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/esm/ROUTES.d.ts +10 -2
  3. package/esm/ROUTES.js +5 -1
  4. package/esm/auth/Entities.js +1 -0
  5. package/esm/auth/server/handleAuth.js +2 -1
  6. package/esm/auth/server/handleGuard.d.ts +10 -4
  7. package/esm/auth/server/handleGuard.js +8 -41
  8. package/esm/auth/server/module.d.ts +28 -0
  9. package/esm/auth/server/module.js +3 -0
  10. package/esm/auth/static/assets/{Page-B1GE_oYi.d.ts → Page-BgIgl-Te.d.ts} +2 -2
  11. package/esm/auth/static/assets/{Page-CDHFtYuN.js → Page-BgIgl-Te.js} +1 -1
  12. package/esm/auth/static/assets/{Page-Dh8pvAo6.d.ts → Page-HDnoBhpE.d.ts} +2 -2
  13. package/esm/auth/static/assets/{Page-Dh8pvAo6.js → Page-HDnoBhpE.js} +2 -2
  14. package/esm/auth/static/assets/{Page-CDHFtYuN.d.ts → Page-f5pC21Yg.d.ts} +2 -2
  15. package/esm/auth/static/assets/{Page-B1GE_oYi.js → Page-f5pC21Yg.js} +1 -1
  16. package/esm/auth/static/assets/{index-7Nh2ct-y.js → index-DAjei0Ie.js} +2 -2
  17. package/esm/auth/static/index.html +4 -4
  18. package/esm/auth/types.d.ts +1 -0
  19. package/esm/helper.d.ts +1 -1
  20. package/esm/helper.js +4 -3
  21. package/esm/index.d.ts +5 -3
  22. package/esm/index.js +5 -3
  23. package/esm/mail/server/index.js +2 -1
  24. package/esm/svelte/FF_Cell.svelte +104 -0
  25. package/esm/svelte/FF_Cell.svelte.d.ts +24 -0
  26. package/esm/svelte/FF_Cell_Caption.svelte +20 -0
  27. package/esm/svelte/FF_Cell_Caption.svelte.d.ts +24 -0
  28. package/esm/svelte/FF_Cell_Display.svelte +61 -0
  29. package/esm/svelte/FF_Cell_Display.svelte.d.ts +22 -0
  30. package/esm/svelte/FF_Cell_Edit.svelte +104 -0
  31. package/esm/svelte/FF_Cell_Edit.svelte.d.ts +25 -0
  32. package/esm/svelte/FF_Cell_Error.svelte +20 -0
  33. package/esm/svelte/FF_Cell_Error.svelte.d.ts +24 -0
  34. package/esm/svelte/FF_Cell_Hint.svelte +20 -0
  35. package/esm/svelte/FF_Cell_Hint.svelte.d.ts +24 -0
  36. package/esm/svelte/FF_Config.svelte +29 -0
  37. package/esm/svelte/FF_Config.svelte.d.ts +9 -0
  38. package/esm/svelte/FF_Display.svelte +51 -0
  39. package/esm/svelte/FF_Display.svelte.d.ts +22 -0
  40. package/esm/svelte/FF_Edit.svelte +104 -0
  41. package/esm/svelte/FF_Edit.svelte.d.ts +25 -0
  42. package/esm/svelte/FF_Error.svelte +23 -0
  43. package/esm/svelte/FF_Error.svelte.d.ts +22 -0
  44. package/esm/svelte/FF_Field.svelte +62 -0
  45. package/esm/svelte/FF_Field.svelte.d.ts +22 -0
  46. package/esm/svelte/FF_Form.svelte +156 -0
  47. package/esm/svelte/FF_Form.svelte.d.ts +30 -0
  48. package/esm/svelte/FF_Grid.svelte +257 -0
  49. package/esm/svelte/FF_Grid.svelte.d.ts +31 -0
  50. package/esm/svelte/FF_Hint.svelte +21 -0
  51. package/esm/svelte/FF_Hint.svelte.d.ts +22 -0
  52. package/esm/svelte/FF_Label.svelte +23 -0
  53. package/esm/svelte/FF_Label.svelte.d.ts +22 -0
  54. package/esm/svelte/FF_Layout.svelte +62 -0
  55. package/esm/svelte/FF_Layout.svelte.d.ts +24 -0
  56. package/esm/svelte/FF_Repo.svelte.d.ts +69 -0
  57. package/esm/svelte/FF_Repo.svelte.js +170 -0
  58. package/esm/svelte/actions/intersection.d.ts +6 -0
  59. package/esm/svelte/actions/intersection.js +17 -0
  60. package/esm/svelte/class/SP.svelte.d.ts +61 -0
  61. package/esm/svelte/class/SP.svelte.js +412 -0
  62. package/esm/svelte/customField.d.ts +69 -0
  63. package/esm/svelte/customField.js +4 -0
  64. package/esm/svelte/dialog/DialogManagement.svelte +101 -0
  65. package/esm/svelte/dialog/DialogManagement.svelte.d.ts +18 -0
  66. package/esm/svelte/dialog/DialogPrimitive.svelte +157 -0
  67. package/esm/svelte/dialog/DialogPrimitive.svelte.d.ts +38 -0
  68. package/esm/svelte/dialog/dialog.d.ts +58 -0
  69. package/esm/svelte/dialog/dialog.js +130 -0
  70. package/esm/svelte/ff_Config.svelte.d.ts +91 -0
  71. package/esm/svelte/ff_Config.svelte.js +111 -0
  72. package/esm/svelte/firstly.css +14 -0
  73. package/esm/svelte/helpers/debounce.d.ts +1 -0
  74. package/esm/svelte/helpers/debounce.js +7 -0
  75. package/esm/svelte/helpers.d.ts +29 -0
  76. package/esm/svelte/helpers.js +38 -0
  77. package/esm/svelte/index.d.ts +32 -0
  78. package/esm/svelte/index.js +29 -0
  79. package/esm/svelte/tryCatch.d.ts +12 -0
  80. package/esm/svelte/tryCatch.js +18 -0
  81. package/esm/ui/Field.svelte +1 -1
  82. package/esm/ui/Grid.svelte +8 -2
  83. package/esm/ui/Grid2.svelte +354 -0
  84. package/esm/ui/Grid2.svelte.d.ts +58 -0
  85. package/esm/ui/GridLoading.svelte +33 -8
  86. package/esm/ui/GridLoading.svelte.d.ts +1 -0
  87. package/esm/ui/GridPaginate.svelte +0 -3
  88. package/esm/ui/GridPaginate.svelte.d.ts +0 -1
  89. package/esm/ui/GridPaginate2.svelte +25 -0
  90. package/esm/ui/GridPaginate2.svelte.d.ts +7 -0
  91. package/esm/ui/dialog/dialog.d.ts +3 -1
  92. package/esm/ui/dialog/dialog.js +2 -0
  93. package/esm/ui/link/LinkPlus.svelte +8 -6
  94. package/package.json +17 -2
  95. /package/esm/auth/static/assets/{index-7Nh2ct-y.d.ts → index-DAjei0Ie.d.ts} +0 -0
@@ -0,0 +1,170 @@
1
+ import { repo as remultRepo, } from 'remult';
2
+ import { Log } from '@kitql/helpers';
3
+ import { tryCatch, tryCatchSync } from './';
4
+ export class FF_Repo {
5
+ ent;
6
+ #repo;
7
+ #paginator;
8
+ #findOptions;
9
+ #queryOptions;
10
+ fields;
11
+ metadata;
12
+ loading = $state({
13
+ init: false,
14
+ fetching: false,
15
+ more: false,
16
+ saving: false,
17
+ deleting: false,
18
+ });
19
+ items = $state(undefined);
20
+ aggregates = $state(undefined);
21
+ hasNextPage = $state(undefined);
22
+ item = $state(undefined);
23
+ // errors = $state<ErrorInfo<Entity> | undefined>(undefined)
24
+ globalError = $state(undefined);
25
+ loadingEnd = (toRet) => {
26
+ this.loading = {
27
+ init: false,
28
+ fetching: false,
29
+ more: false,
30
+ saving: false,
31
+ deleting: false,
32
+ };
33
+ return toRet;
34
+ };
35
+ constructor(ent, o) {
36
+ this.ent = ent;
37
+ this.#repo = remultRepo(ent);
38
+ this.fields = this.#repo.fields;
39
+ this.metadata = this.#repo.metadata;
40
+ this.#paginator = undefined;
41
+ this.#findOptions = o?.findOptions;
42
+ this.#queryOptions = o?.queryOptions;
43
+ this.item = o?.item;
44
+ if (o?.findOptions !== undefined && !o.findOptions.skipAutoFetch) {
45
+ this.loading.init = true;
46
+ this.find(o.findOptions);
47
+ }
48
+ else if (o?.queryOptions !== undefined && !o.queryOptions.skipAutoFetch) {
49
+ this.loading.init = true;
50
+ this.query(o.queryOptions);
51
+ }
52
+ else {
53
+ this.loadingEnd();
54
+ }
55
+ }
56
+ async find(options) {
57
+ this.loading.fetching = true;
58
+ const { data, error } = await tryCatch(this.#repo.find({
59
+ ...this.#findOptions,
60
+ ...options,
61
+ }));
62
+ if (error) {
63
+ this.globalError = error.message;
64
+ return this.loadingEnd();
65
+ }
66
+ this.items = data;
67
+ return this.loadingEnd(data);
68
+ }
69
+ async query(options) {
70
+ this.loading = {
71
+ ...this.loading,
72
+ fetching: true,
73
+ init: this.items === undefined,
74
+ };
75
+ // REMULT P1: add test for dynamic orderBy in remult
76
+ // Looks like only the default orderby of the entity is working
77
+ const { data: queryResult, error: queryResultError } = tryCatchSync(() => this.#repo.query({
78
+ pageSize: 2,
79
+ ...this.#queryOptions,
80
+ ...options,
81
+ // Yes, we always want to aggregate to get at least the $count!
82
+ // And empty object is giving us that
83
+ aggregate: {
84
+ ...this.#queryOptions?.aggregate,
85
+ },
86
+ }));
87
+ if (queryResultError) {
88
+ this.globalError = queryResultError.message;
89
+ return this.loadingEnd();
90
+ }
91
+ const { data: paginator, error: paginatorError } = await tryCatch(queryResult.paginator());
92
+ if (paginatorError) {
93
+ this.globalError = paginatorError.message;
94
+ return this.loadingEnd();
95
+ }
96
+ this.#paginator = paginator;
97
+ this.items = this.#paginator.items;
98
+ // @ts-expect-error - We know the structure will match due to how we define the types
99
+ this.aggregates = this.#paginator.aggregates;
100
+ this.hasNextPage = this.#paginator.hasNextPage && this.aggregates.$count > this.items.length;
101
+ return this.loadingEnd({
102
+ items: this.items,
103
+ aggregates: this.aggregates,
104
+ hasNextPage: this.hasNextPage,
105
+ });
106
+ }
107
+ async queryMore() {
108
+ if (this.#paginator === undefined) {
109
+ new Log('FF_Repo').error('No paginator found');
110
+ return undefined;
111
+ }
112
+ if (this.loading.more) {
113
+ // already in progress...
114
+ return undefined;
115
+ }
116
+ this.loading = {
117
+ ...this.loading,
118
+ fetching: true,
119
+ more: true,
120
+ };
121
+ const { data: nextPage, error: nextPageError } = await tryCatch(this.#paginator.nextPage());
122
+ if (nextPageError) {
123
+ this.globalError = nextPageError.message;
124
+ return this.loadingEnd();
125
+ }
126
+ this.#paginator = nextPage;
127
+ this.items?.push(...nextPage.items);
128
+ this.hasNextPage = this.#paginator.hasNextPage && this.aggregates.$count > this.items.length;
129
+ return this.loadingEnd({
130
+ items: this.items,
131
+ aggregates: this.aggregates,
132
+ hasNextPage: this.hasNextPage,
133
+ });
134
+ }
135
+ create(...args) {
136
+ this.item = this.#repo.create(...args);
137
+ return this.item;
138
+ }
139
+ async delete(...args) {
140
+ this.loading.deleting = true;
141
+ await this.#repo.delete(...args);
142
+ // REMULT P4: return the deleted item ?
143
+ if (typeof args[0] === 'string') {
144
+ this.items = this.items?.filter((i) => this.metadata.idMetadata.getId(i) !== args[0]);
145
+ }
146
+ else {
147
+ this.items = this.items?.filter((i) => this.metadata.idMetadata.getId(i) !== this.metadata.idMetadata.getId(args[0]));
148
+ }
149
+ if (this.aggregates) {
150
+ this.aggregates.$count = this.aggregates.$count - 1;
151
+ }
152
+ return this.loadingEnd();
153
+ }
154
+ getLayout = (o) => {
155
+ const layout = this.metadata.options.ui?.getLayout?.(o);
156
+ if (layout) {
157
+ return layout;
158
+ }
159
+ return {
160
+ key: o?.key ?? 'default',
161
+ type: o?.type ?? 'grid',
162
+ groups: [
163
+ {
164
+ key: o?.key ?? 'default',
165
+ fields: this.#repo.fields.toArray().filter((c) => c.apiUpdateAllowed()),
166
+ },
167
+ ],
168
+ };
169
+ };
170
+ }
@@ -0,0 +1,6 @@
1
+ import type { Action } from 'svelte/action';
2
+ interface Attributes {
3
+ onintersecting: (e: CustomEvent<IntersectionObserverEntry>) => void;
4
+ }
5
+ export declare const intersection: Action<HTMLElement, IntersectionObserverInit | undefined, Attributes>;
6
+ export {};
@@ -0,0 +1,17 @@
1
+ import { getScrollParent } from '@layerstack/utils';
2
+ // to bring back to https://github.com/techniq/layerstack/blob/main/packages/svelte-actions/src/lib/observer.ts to have correct typing in Svelte 5
3
+ export const intersection = (node, options = {}) => {
4
+ const scrollParent = getScrollParent(node);
5
+ // Use viewport (null) if scrollParent = `<body>`
6
+ const root = scrollParent === document.body ? null : scrollParent;
7
+ const observer = new IntersectionObserver((entries, observer) => {
8
+ const entry = entries[0];
9
+ node.dispatchEvent(new CustomEvent('intersecting', { detail: entry }));
10
+ }, { root, ...options });
11
+ observer.observe(node);
12
+ return {
13
+ destroy() {
14
+ observer.disconnect();
15
+ },
16
+ };
17
+ };
@@ -0,0 +1,61 @@
1
+ import { goto } from '$app/navigation';
2
+ type ParamType = 'string' | 'number' | 'boolean' | 'array' | 'object';
3
+ export interface ParamDefinition<TValue> {
4
+ type?: ParamType;
5
+ /** Alternative key to use in URL search params */
6
+ key?: string;
7
+ /** Custom function to convert value to URL string (for storage in URL only) */
8
+ encode?: (obj: TValue) => string | undefined;
9
+ /** Custom function to parse URL value to the correct type (for internal use) */
10
+ decode?: (str: string | undefined) => TValue;
11
+ /** Debounce the URL update for this parameter (milliseconds or true for default) */
12
+ debounce?: number | boolean;
13
+ }
14
+ /**
15
+ * SearchParams class for handling URL search parameters with Svelte 5 runes
16
+ * Provides automatic binding and URL updates
17
+ */
18
+ export declare class SP<T extends Record<string, any>> {
19
+ private defaults;
20
+ private options;
21
+ private paramValues;
22
+ private debouncedValues;
23
+ computing: boolean;
24
+ private debouncedToURL;
25
+ private sharedDebouncePending;
26
+ private activeDebounces;
27
+ private longestDebounceTime;
28
+ private finalizeDebounce;
29
+ private _obj;
30
+ get obj(): T;
31
+ get computed(): T;
32
+ get rawStr(): Record<keyof T, string | undefined>;
33
+ private config;
34
+ private keyMap;
35
+ /**
36
+ * Create a new SearchParams instance
37
+ * @param defaults Object with default values (also defines the structure of type T)
38
+ * @param options Object containing parameter definitions and configuration
39
+ */
40
+ constructor(defaults: T, options?: {
41
+ config?: Partial<{
42
+ [K in keyof T]: Partial<ParamDefinition<T[K]>>;
43
+ }>;
44
+ gotoOpts?: Parameters<typeof goto>[1];
45
+ });
46
+ /**
47
+ * Load parameter values from URL
48
+ * Always store the decoded objects, not the URL string representations
49
+ */
50
+ private fromURL;
51
+ /**
52
+ * Update URL with current parameter values
53
+ * Only uses encoding for URL representation, doesn't modify internal state
54
+ */
55
+ private toURL;
56
+ /**
57
+ * Reset all parameters to their default values
58
+ */
59
+ reset(): void;
60
+ }
61
+ export {};
@@ -0,0 +1,412 @@
1
+ // FIXME
2
+ // Why packages/ui pnpm check fails with goto and page?
3
+ // - OK it's not a sveltekit project... But this is never used! So why is it checking it ?!
4
+ // @ts-ignore
5
+ import { goto } from '$app/navigation';
6
+ // @ts-ignore
7
+ import { page } from '$app/state';
8
+ import { debounce } from '../helpers/debounce.js';
9
+ const CONFIG_DELIMITER = ';';
10
+ /**
11
+ * SearchParams class for handling URL search parameters with Svelte 5 runes
12
+ * Provides automatic binding and URL updates
13
+ */
14
+ export class SP {
15
+ defaults;
16
+ options;
17
+ // Internal state container - always stores decoded objects
18
+ paramValues = $state({});
19
+ // Track debounced values separately - also always decoded objects
20
+ debouncedValues = $state({});
21
+ // Flag indicating when debouncing is active
22
+ computing = $state(false);
23
+ // Store debounced toURL functions for each parameter
24
+ debouncedToURL = {};
25
+ // Shared debounce mechanism for all parameters
26
+ sharedDebouncePending = $state(false);
27
+ activeDebounces = $state({});
28
+ longestDebounceTime = $state(0);
29
+ // This will be called after all individual debounces have completed
30
+ finalizeDebounce = () => {
31
+ // Only proceed if all tracked debounces have completed
32
+ if (Object.values(this.activeDebounces).some((active) => active)) {
33
+ return;
34
+ }
35
+ // Apply all pending changes
36
+ for (const key of Object.keys(this.paramValues)) {
37
+ this.debouncedValues[key] = this.paramValues[key];
38
+ }
39
+ // Update URL and reset flags
40
+ this.sharedDebouncePending = false;
41
+ this.computing = false;
42
+ this.longestDebounceTime = 0;
43
+ this.toURL();
44
+ };
45
+ // Created proxy object for direct param access
46
+ _obj = {};
47
+ // Expose public properties via getters
48
+ get obj() {
49
+ return this._obj;
50
+ }
51
+ get computed() {
52
+ return this._obj.computed;
53
+ }
54
+ get rawStr() {
55
+ return this._obj.rawStr;
56
+ }
57
+ // Config for each param, with defaults applied
58
+ config;
59
+ // Maps from URL key to object key
60
+ keyMap = {};
61
+ /**
62
+ * Create a new SearchParams instance
63
+ * @param defaults Object with default values (also defines the structure of type T)
64
+ * @param options Object containing parameter definitions and configuration
65
+ */
66
+ constructor(defaults, options = {}) {
67
+ this.defaults = defaults;
68
+ this.options = options;
69
+ // Initialize definitions from defaults
70
+ this.config = {};
71
+ // Create definitions based on default values
72
+ for (const [key, value] of Object.entries(defaults)) {
73
+ // Determine the type based on the default value
74
+ let type = 'string';
75
+ if (typeof value === 'number')
76
+ type = 'number';
77
+ else if (typeof value === 'boolean')
78
+ type = 'boolean';
79
+ else if (Array.isArray(value))
80
+ type = 'array';
81
+ else if (typeof value === 'object' && value !== null)
82
+ type = 'object';
83
+ // Create base definition
84
+ this.config[key] = {
85
+ type,
86
+ };
87
+ }
88
+ // Apply custom definitions on top of auto-generated ones
89
+ if (options.config) {
90
+ for (const [key, def] of Object.entries(options.config)) {
91
+ if (this.config[key]) {
92
+ this.config[key] = {
93
+ ...this.config[key],
94
+ ...def,
95
+ };
96
+ }
97
+ }
98
+ }
99
+ // Set keyMap
100
+ for (const [key, def] of Object.entries(this.config)) {
101
+ // Build key mapping for URL params
102
+ this.keyMap[key] = def.key || key;
103
+ }
104
+ // Initialize values from definitions with default values
105
+ // Always store the actual objects, not their string representations
106
+ for (const [key] of Object.entries(this.config)) {
107
+ this.paramValues[key] = defaults[key];
108
+ this.debouncedValues[key] = defaults[key];
109
+ this.activeDebounces[key] = false;
110
+ // Create debounced functions for each parameter that needs it
111
+ const def = this.config[key];
112
+ if (def.debounce) {
113
+ const delay = typeof def.debounce === 'number' ? def.debounce : 444;
114
+ this.debouncedToURL[key] = debounce(() => {
115
+ // Mark this specific debounce as complete
116
+ this.activeDebounces[key] = false;
117
+ // Check if this is the last active debounce
118
+ this.finalizeDebounce();
119
+ }, delay);
120
+ }
121
+ }
122
+ // Create the nested structure for obj.computed and obj.rawStr
123
+ this._obj.computed = {};
124
+ this._obj.rawStr = {};
125
+ // Create proxy object for direct parameter access
126
+ for (const key of Object.keys(this.config)) {
127
+ // Main parameter accessor - This always returns the decoded object
128
+ Object.defineProperty(this._obj, key, {
129
+ get: () => this.paramValues[key],
130
+ set: (value) => {
131
+ // Always store the actual object value
132
+ this.paramValues[key] = value;
133
+ const def = this.config[key];
134
+ if (def.debounce) {
135
+ // Start computing
136
+ this.computing = true;
137
+ // Track this debounce
138
+ const delay = typeof def.debounce === 'number' ? def.debounce : 444;
139
+ this.activeDebounces[key] = true;
140
+ this.sharedDebouncePending = true;
141
+ // Track the longest debounce time
142
+ if (delay > this.longestDebounceTime) {
143
+ this.longestDebounceTime = delay;
144
+ }
145
+ // Trigger individual debounce
146
+ if (this.debouncedToURL[key]) {
147
+ this.debouncedToURL[key]();
148
+ }
149
+ }
150
+ else {
151
+ // No debounce for this parameter, update immediately
152
+ this.debouncedValues[key] = value;
153
+ // If there are no active debounces, just update right away
154
+ if (!this.sharedDebouncePending) {
155
+ this.toURL();
156
+ }
157
+ }
158
+ },
159
+ enumerable: true,
160
+ });
161
+ // Add debounced value accessor in the computed object
162
+ Object.defineProperty(this._obj.computed, key, {
163
+ get: () => this.debouncedValues[key],
164
+ enumerable: true,
165
+ });
166
+ // Add ID accessor in the rawStr object
167
+ Object.defineProperty(this._obj.rawStr, key, {
168
+ get: () => {
169
+ const value = this.debouncedValues[key];
170
+ const def = this.config[key];
171
+ // Skip undefined or null values
172
+ if (value === undefined || value === null) {
173
+ return undefined;
174
+ }
175
+ // If there's a custom encode function, use it
176
+ if (def.encode) {
177
+ return def.encode(value);
178
+ }
179
+ // Otherwise use default conversion based on type
180
+ switch (def.type) {
181
+ case 'array':
182
+ return Array.isArray(value) ? value.join(CONFIG_DELIMITER) : String(value);
183
+ case 'object':
184
+ return JSON.stringify(value);
185
+ default:
186
+ // Handle primitives
187
+ return String(value);
188
+ }
189
+ },
190
+ set: (idValue) => {
191
+ const def = this.config[key];
192
+ // If the ID is undefined, set the value to undefined
193
+ if (idValue === undefined) {
194
+ this.paramValues[key] = undefined;
195
+ this.debouncedValues[key] = undefined;
196
+ this.toURL();
197
+ return;
198
+ }
199
+ // We need to convert the ID to the full object
200
+ // If there's a decode function, we can use that
201
+ if (def.decode) {
202
+ // Use decode to convert from the string ID to the full object
203
+ const fullObject = def.decode(idValue.toString());
204
+ this.paramValues[key] = fullObject;
205
+ this.debouncedValues[key] = fullObject;
206
+ this.toURL();
207
+ }
208
+ else {
209
+ // No decode function, just set the ID value directly
210
+ let value = idValue;
211
+ // Try to convert based on type
212
+ switch (def.type) {
213
+ case 'number':
214
+ value = parseFloat(idValue.toString());
215
+ if (isNaN(value))
216
+ value = idValue;
217
+ break;
218
+ case 'boolean':
219
+ value = idValue === 'true';
220
+ break;
221
+ case 'array':
222
+ if (typeof idValue === 'string') {
223
+ value = idValue.split(CONFIG_DELIMITER).filter(Boolean);
224
+ }
225
+ break;
226
+ case 'object':
227
+ if (typeof idValue === 'string') {
228
+ try {
229
+ value = JSON.parse(idValue);
230
+ }
231
+ catch (e) {
232
+ console.error(`Error parsing JSON for param ${key}:`, e);
233
+ value = idValue;
234
+ }
235
+ }
236
+ break;
237
+ }
238
+ this.paramValues[key] = value;
239
+ this.debouncedValues[key] = value;
240
+ this.toURL();
241
+ }
242
+ },
243
+ enumerable: true,
244
+ });
245
+ }
246
+ // Load values from URL after setting up the structure
247
+ this.fromURL();
248
+ }
249
+ /**
250
+ * Load parameter values from URL
251
+ * Always store the decoded objects, not the URL string representations
252
+ */
253
+ fromURL() {
254
+ const params = page.url.searchParams;
255
+ for (const [propKey, tmpDef] of Object.entries(this.config)) {
256
+ const def = tmpDef;
257
+ const urlKey = this.keyMap[propKey]; // Get the URL parameter key
258
+ const paramValue = params.get(urlKey);
259
+ if (paramValue !== null) {
260
+ // If there is a decode function, always use it to get the proper object
261
+ if (def.decode) {
262
+ const decodedValue = def.decode(paramValue);
263
+ this.paramValues[propKey] = decodedValue;
264
+ this.debouncedValues[propKey] = decodedValue;
265
+ continue;
266
+ }
267
+ // Otherwise use default conversion based on type
268
+ switch (def.type) {
269
+ case 'number': {
270
+ const num = parseFloat(paramValue);
271
+ if (!isNaN(num)) {
272
+ this.paramValues[propKey] = num;
273
+ this.debouncedValues[propKey] = num;
274
+ }
275
+ break;
276
+ }
277
+ case 'boolean':
278
+ this.paramValues[propKey] = paramValue === 'true';
279
+ this.debouncedValues[propKey] = this.paramValues[propKey];
280
+ break;
281
+ case 'array':
282
+ this.paramValues[propKey] = paramValue.split(CONFIG_DELIMITER).filter(Boolean);
283
+ this.debouncedValues[propKey] = this.paramValues[propKey];
284
+ break;
285
+ case 'object':
286
+ try {
287
+ this.paramValues[propKey] = JSON.parse(paramValue);
288
+ this.debouncedValues[propKey] = this.paramValues[propKey];
289
+ }
290
+ catch (e) {
291
+ console.error(`Error parsing JSON for param ${propKey}:`, e);
292
+ // Keep default value on error
293
+ }
294
+ break;
295
+ case 'string':
296
+ default:
297
+ this.paramValues[propKey] = paramValue;
298
+ this.debouncedValues[propKey] = paramValue;
299
+ break;
300
+ }
301
+ }
302
+ }
303
+ }
304
+ /**
305
+ * Update URL with current parameter values
306
+ * Only uses encoding for URL representation, doesn't modify internal state
307
+ */
308
+ toURL() {
309
+ if (typeof window === 'undefined')
310
+ return;
311
+ // If there's a shared debounce pending, don't update URL yet
312
+ if (this.sharedDebouncePending) {
313
+ return;
314
+ }
315
+ const params = new URLSearchParams(window.location.search);
316
+ for (const [propKey, value] of Object.entries(this.debouncedValues)) {
317
+ // Skip undefined or null values
318
+ if (value === undefined || value === null) {
319
+ params.delete(this.keyMap[propKey]);
320
+ continue;
321
+ }
322
+ // Get the definition and URL key
323
+ const def = this.config[propKey];
324
+ if (!def)
325
+ continue;
326
+ const urlKey = this.keyMap[propKey];
327
+ const defaultValue = this.defaults[propKey];
328
+ // Encode the current value
329
+ let encodedValue;
330
+ if (def.encode) {
331
+ encodedValue = def.encode(value);
332
+ if (!encodedValue) {
333
+ params.delete(urlKey);
334
+ continue;
335
+ }
336
+ }
337
+ else {
338
+ // Otherwise use default conversion based on type
339
+ switch (def.type) {
340
+ case 'array':
341
+ encodedValue = Array.isArray(value) ? value.join(CONFIG_DELIMITER) : String(value);
342
+ break;
343
+ case 'object':
344
+ encodedValue = JSON.stringify(value);
345
+ break;
346
+ default:
347
+ // Handle primitives
348
+ encodedValue = String(value);
349
+ break;
350
+ }
351
+ }
352
+ // Encode the default value for comparison
353
+ let encodedDefault;
354
+ if (defaultValue !== undefined && defaultValue !== null) {
355
+ if (def.encode) {
356
+ encodedDefault = def.encode(defaultValue);
357
+ }
358
+ else {
359
+ switch (def.type) {
360
+ case 'array':
361
+ encodedDefault = Array.isArray(defaultValue)
362
+ ? defaultValue.join(CONFIG_DELIMITER)
363
+ : String(defaultValue);
364
+ break;
365
+ case 'object':
366
+ encodedDefault = JSON.stringify(defaultValue);
367
+ break;
368
+ default:
369
+ encodedDefault = String(defaultValue);
370
+ break;
371
+ }
372
+ }
373
+ }
374
+ // Skip values that match their encoded defaults
375
+ if (encodedValue === encodedDefault) {
376
+ params.delete(urlKey);
377
+ continue;
378
+ }
379
+ // Set the encoded value in URL
380
+ params.set(urlKey, encodedValue);
381
+ }
382
+ const strSearch = params.toString() ? '?' + params.toString() : '';
383
+ // Don't do the goto if the search params haven't changed!
384
+ if (strSearch === window.location.search)
385
+ return;
386
+ goto(`${window.location.pathname}${strSearch}`, {
387
+ keepFocus: true,
388
+ replaceState: true,
389
+ noScroll: true,
390
+ ...this.options.gotoOpts,
391
+ });
392
+ // Reset computing flag after URL update is complete
393
+ this.computing = false;
394
+ }
395
+ /**
396
+ * Reset all parameters to their default values
397
+ */
398
+ reset() {
399
+ // Reset both sets of values to defaults immediately
400
+ for (const [key] of Object.entries(this.config)) {
401
+ this.paramValues[key] = this.defaults[key];
402
+ this.debouncedValues[key] = this.defaults[key];
403
+ this.activeDebounces[key] = false;
404
+ }
405
+ // Cancel any pending debounce
406
+ this.sharedDebouncePending = false;
407
+ this.longestDebounceTime = 0;
408
+ // Update URL immediately without debounce
409
+ this.computing = false;
410
+ this.toURL();
411
+ }
412
+ }