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.
- package/CHANGELOG.md +8 -0
- package/esm/ROUTES.d.ts +10 -2
- package/esm/ROUTES.js +5 -1
- package/esm/auth/Entities.js +1 -0
- package/esm/auth/server/handleAuth.js +2 -1
- package/esm/auth/server/handleGuard.d.ts +10 -4
- package/esm/auth/server/handleGuard.js +8 -41
- package/esm/auth/server/module.d.ts +28 -0
- package/esm/auth/server/module.js +3 -0
- package/esm/auth/static/assets/{Page-B1GE_oYi.d.ts → Page-BgIgl-Te.d.ts} +2 -2
- package/esm/auth/static/assets/{Page-CDHFtYuN.js → Page-BgIgl-Te.js} +1 -1
- package/esm/auth/static/assets/{Page-Dh8pvAo6.d.ts → Page-HDnoBhpE.d.ts} +2 -2
- package/esm/auth/static/assets/{Page-Dh8pvAo6.js → Page-HDnoBhpE.js} +2 -2
- package/esm/auth/static/assets/{Page-CDHFtYuN.d.ts → Page-f5pC21Yg.d.ts} +2 -2
- package/esm/auth/static/assets/{Page-B1GE_oYi.js → Page-f5pC21Yg.js} +1 -1
- package/esm/auth/static/assets/{index-7Nh2ct-y.js → index-DAjei0Ie.js} +2 -2
- package/esm/auth/static/index.html +4 -4
- package/esm/auth/types.d.ts +1 -0
- package/esm/helper.d.ts +1 -1
- package/esm/helper.js +4 -3
- package/esm/index.d.ts +5 -3
- package/esm/index.js +5 -3
- package/esm/mail/server/index.js +2 -1
- package/esm/svelte/FF_Cell.svelte +104 -0
- package/esm/svelte/FF_Cell.svelte.d.ts +24 -0
- package/esm/svelte/FF_Cell_Caption.svelte +20 -0
- package/esm/svelte/FF_Cell_Caption.svelte.d.ts +24 -0
- package/esm/svelte/FF_Cell_Display.svelte +61 -0
- package/esm/svelte/FF_Cell_Display.svelte.d.ts +22 -0
- package/esm/svelte/FF_Cell_Edit.svelte +104 -0
- package/esm/svelte/FF_Cell_Edit.svelte.d.ts +25 -0
- package/esm/svelte/FF_Cell_Error.svelte +20 -0
- package/esm/svelte/FF_Cell_Error.svelte.d.ts +24 -0
- package/esm/svelte/FF_Cell_Hint.svelte +20 -0
- package/esm/svelte/FF_Cell_Hint.svelte.d.ts +24 -0
- package/esm/svelte/FF_Config.svelte +29 -0
- package/esm/svelte/FF_Config.svelte.d.ts +9 -0
- package/esm/svelte/FF_Display.svelte +51 -0
- package/esm/svelte/FF_Display.svelte.d.ts +22 -0
- package/esm/svelte/FF_Edit.svelte +104 -0
- package/esm/svelte/FF_Edit.svelte.d.ts +25 -0
- package/esm/svelte/FF_Error.svelte +23 -0
- package/esm/svelte/FF_Error.svelte.d.ts +22 -0
- package/esm/svelte/FF_Field.svelte +62 -0
- package/esm/svelte/FF_Field.svelte.d.ts +22 -0
- package/esm/svelte/FF_Form.svelte +156 -0
- package/esm/svelte/FF_Form.svelte.d.ts +30 -0
- package/esm/svelte/FF_Grid.svelte +257 -0
- package/esm/svelte/FF_Grid.svelte.d.ts +31 -0
- package/esm/svelte/FF_Hint.svelte +21 -0
- package/esm/svelte/FF_Hint.svelte.d.ts +22 -0
- package/esm/svelte/FF_Label.svelte +23 -0
- package/esm/svelte/FF_Label.svelte.d.ts +22 -0
- package/esm/svelte/FF_Layout.svelte +62 -0
- package/esm/svelte/FF_Layout.svelte.d.ts +24 -0
- package/esm/svelte/FF_Repo.svelte.d.ts +69 -0
- package/esm/svelte/FF_Repo.svelte.js +170 -0
- package/esm/svelte/actions/intersection.d.ts +6 -0
- package/esm/svelte/actions/intersection.js +17 -0
- package/esm/svelte/class/SP.svelte.d.ts +61 -0
- package/esm/svelte/class/SP.svelte.js +412 -0
- package/esm/svelte/customField.d.ts +69 -0
- package/esm/svelte/customField.js +4 -0
- package/esm/svelte/dialog/DialogManagement.svelte +101 -0
- package/esm/svelte/dialog/DialogManagement.svelte.d.ts +18 -0
- package/esm/svelte/dialog/DialogPrimitive.svelte +157 -0
- package/esm/svelte/dialog/DialogPrimitive.svelte.d.ts +38 -0
- package/esm/svelte/dialog/dialog.d.ts +58 -0
- package/esm/svelte/dialog/dialog.js +130 -0
- package/esm/svelte/ff_Config.svelte.d.ts +91 -0
- package/esm/svelte/ff_Config.svelte.js +111 -0
- package/esm/svelte/firstly.css +14 -0
- package/esm/svelte/helpers/debounce.d.ts +1 -0
- package/esm/svelte/helpers/debounce.js +7 -0
- package/esm/svelte/helpers.d.ts +29 -0
- package/esm/svelte/helpers.js +38 -0
- package/esm/svelte/index.d.ts +32 -0
- package/esm/svelte/index.js +29 -0
- package/esm/svelte/tryCatch.d.ts +12 -0
- package/esm/svelte/tryCatch.js +18 -0
- package/esm/ui/Field.svelte +1 -1
- package/esm/ui/Grid.svelte +8 -2
- package/esm/ui/Grid2.svelte +354 -0
- package/esm/ui/Grid2.svelte.d.ts +58 -0
- package/esm/ui/GridLoading.svelte +33 -8
- package/esm/ui/GridLoading.svelte.d.ts +1 -0
- package/esm/ui/GridPaginate.svelte +0 -3
- package/esm/ui/GridPaginate.svelte.d.ts +0 -1
- package/esm/ui/GridPaginate2.svelte +25 -0
- package/esm/ui/GridPaginate2.svelte.d.ts +7 -0
- package/esm/ui/dialog/dialog.d.ts +3 -1
- package/esm/ui/dialog/dialog.js +2 -0
- package/esm/ui/link/LinkPlus.svelte +8 -6
- package/package.json +17 -2
- /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,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
|
+
}
|