@cfasim-ui/docs 0.4.11 → 0.4.13
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/charts/BarChart/BarChart.md +54 -1
- package/charts/BarChart/BarChart.vue +180 -28
- package/charts/ChoroplethMap/ChoroplethMap.md +8 -2
- package/charts/ChoroplethMap/ChoroplethMap.vue +89 -7
- package/charts/LineChart/LineChart.md +138 -9
- package/charts/LineChart/LineChart.vue +295 -87
- package/charts/_shared/ChartAnnotations.vue +56 -17
- package/charts/_shared/annotations.ts +14 -9
- package/charts/_shared/chartProps.ts +39 -0
- package/charts/_shared/dateAxis.ts +507 -0
- package/charts/_shared/index.ts +22 -0
- package/charts/_shared/useChartFoundation.ts +3 -0
- package/charts/_shared/useChartPadding.ts +53 -5
- package/charts/hsa-mapping.ts +1 -0
- package/charts/index.ts +0 -1
- package/index.json +1 -1
- package/package.json +1 -1
- package/shared/index.ts +6 -1
- package/shared/useUrlParams.ts +320 -42
package/shared/useUrlParams.ts
CHANGED
|
@@ -28,6 +28,50 @@ export interface UrlParamsRoute {
|
|
|
28
28
|
*/
|
|
29
29
|
export type DefaultsInput<T> = T | Ref<T> | (() => T | undefined);
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* A path into the params object. Either a top-level key (autocompleted from
|
|
33
|
+
* `T`) or a dotted path to a nested leaf (`"nested.foo"`). Matches the leaf
|
|
34
|
+
* exactly or any descendant when used as `include`/`ignore`.
|
|
35
|
+
*/
|
|
36
|
+
export type ParamPath<T> = keyof T | (string & {});
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Custom serializer/deserializer pair for a single path. Lets the consumer
|
|
40
|
+
* round-trip a value through one URL key as an opaque string instead of
|
|
41
|
+
* letting `useUrlParams` flatten through it. Use for tagged unions,
|
|
42
|
+
* polymorphic objects, or anything else whose shape can change at runtime —
|
|
43
|
+
* the default leaf-flattening can only enumerate fields present in
|
|
44
|
+
* `defaults`, so variants that aren't the default variant lose their
|
|
45
|
+
* fields.
|
|
46
|
+
*/
|
|
47
|
+
export type ParamCodec = {
|
|
48
|
+
serialize: (value: unknown) => string;
|
|
49
|
+
deserialize: (raw: string) => unknown;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Map of dotted path → codec. A path with a codec is treated as a single
|
|
54
|
+
* atomic leaf: its whole subtree round-trips through one URL key, and any
|
|
55
|
+
* descendant URL keys are ignored. Top-level keys (`"infectionRate"`) and
|
|
56
|
+
* nested paths (`"model.infectionRate"`) both work; segments in the path
|
|
57
|
+
* use `.` as the separator, matching the same convention as
|
|
58
|
+
* `include`/`ignore`.
|
|
59
|
+
*/
|
|
60
|
+
export type ParamCodecs = Record<string, ParamCodec>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Convenience codec that uses `JSON.stringify` / `JSON.parse`. Suitable
|
|
64
|
+
* for tagged unions and small object/array values. Note that comparison
|
|
65
|
+
* during URL writes is string-equality on the stringified form, so two
|
|
66
|
+
* objects with the same fields in different key orders will look distinct;
|
|
67
|
+
* provide a custom codec with a normalized stringifier if you need
|
|
68
|
+
* order-insensitive comparison.
|
|
69
|
+
*/
|
|
70
|
+
export const jsonCodec: ParamCodec = {
|
|
71
|
+
serialize: (value) => JSON.stringify(value),
|
|
72
|
+
deserialize: (raw) => JSON.parse(raw),
|
|
73
|
+
};
|
|
74
|
+
|
|
31
75
|
export type UrlParamsOptions<T> = {
|
|
32
76
|
debounceMs?: number;
|
|
33
77
|
/**
|
|
@@ -39,14 +83,23 @@ export type UrlParamsOptions<T> = {
|
|
|
39
83
|
router?: UrlParamsRouter;
|
|
40
84
|
route?: UrlParamsRoute;
|
|
41
85
|
/**
|
|
42
|
-
* Only sync these
|
|
43
|
-
*
|
|
86
|
+
* Only sync these paths. Accepts top-level keys or dotted paths to nested
|
|
87
|
+
* leaves; a parent path implicitly covers all of its descendants.
|
|
44
88
|
*/
|
|
45
|
-
include?:
|
|
89
|
+
include?: ParamPath<T>[];
|
|
46
90
|
/**
|
|
47
|
-
* Sync all
|
|
91
|
+
* Sync all paths except these. Same matching rules as `include`. Ignored
|
|
92
|
+
* if `include` is provided.
|
|
48
93
|
*/
|
|
49
|
-
ignore?:
|
|
94
|
+
ignore?: ParamPath<T>[];
|
|
95
|
+
/**
|
|
96
|
+
* Per-path codecs. A path listed here is round-tripped as one opaque
|
|
97
|
+
* string under that key (using the codec's `serialize`/`deserialize`)
|
|
98
|
+
* instead of being flattened. URL keys nested inside a codec path are
|
|
99
|
+
* ignored, so a leftover `foo.bar=1` from an earlier shape doesn't
|
|
100
|
+
* punch through the atomic value.
|
|
101
|
+
*/
|
|
102
|
+
codecs?: ParamCodecs;
|
|
50
103
|
};
|
|
51
104
|
|
|
52
105
|
export type ResetOptions = {
|
|
@@ -74,16 +127,169 @@ export function deserialize(raw: string, defaultValue: unknown): unknown {
|
|
|
74
127
|
return raw;
|
|
75
128
|
}
|
|
76
129
|
|
|
130
|
+
// A "container" is any non-null object — plain object or array. A "branch"
|
|
131
|
+
// is a container we recurse into when flattening defaults: plain objects
|
|
132
|
+
// always recurse; arrays only recurse when at least one element is itself a
|
|
133
|
+
// plain object (so `[1, 2, 3]` stays a comma-joined leaf, while
|
|
134
|
+
// `[{foo: 1}, {foo: 2}]` becomes `items.0.foo=…&items.1.foo=…`).
|
|
135
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
136
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isContainer(
|
|
140
|
+
value: unknown,
|
|
141
|
+
): value is Record<string, unknown> | unknown[] {
|
|
142
|
+
return value !== null && typeof value === "object";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isBranch(value: unknown): boolean {
|
|
146
|
+
if (isPlainObject(value)) return true;
|
|
147
|
+
// Recurse into arrays whose elements are themselves containers (plain
|
|
148
|
+
// objects or arrays), so array-of-arrays and array-of-objects map onto
|
|
149
|
+
// indexed dotted paths. Arrays of primitives stay as comma-joined leaves.
|
|
150
|
+
if (Array.isArray(value)) return value.some(isContainer);
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function* flattenLeaves(
|
|
155
|
+
obj: Record<string, unknown> | unknown[],
|
|
156
|
+
prefix: string[] = [],
|
|
157
|
+
stopPaths?: Set<string>,
|
|
158
|
+
): Generator<[string[], unknown]> {
|
|
159
|
+
const entries: Array<[string, unknown]> = Array.isArray(obj)
|
|
160
|
+
? obj.map((v, i) => [String(i), v])
|
|
161
|
+
: Object.entries(obj);
|
|
162
|
+
for (const [k, v] of entries) {
|
|
163
|
+
const path = [...prefix, k];
|
|
164
|
+
// `stopPaths` (codec paths) halt recursion: the whole subtree at that
|
|
165
|
+
// path is yielded as a single leaf value so a codec can serialize it
|
|
166
|
+
// atomically.
|
|
167
|
+
if (isBranch(v) && !stopPaths?.has(path.join("."))) {
|
|
168
|
+
yield* flattenLeaves(
|
|
169
|
+
v as Record<string, unknown> | unknown[],
|
|
170
|
+
path,
|
|
171
|
+
stopPaths,
|
|
172
|
+
);
|
|
173
|
+
} else {
|
|
174
|
+
yield [path, v];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// True iff some proper-prefix ancestor of `segments` is a codec path. Used
|
|
180
|
+
// to drop URL keys that would punch through a codec boundary (e.g. a stale
|
|
181
|
+
// `infectionRate.value=0.5` left over from before infectionRate gained a
|
|
182
|
+
// codec).
|
|
183
|
+
function pathIsInsideAnyCodec(
|
|
184
|
+
segments: string[],
|
|
185
|
+
codecPaths: Set<string>,
|
|
186
|
+
): boolean {
|
|
187
|
+
for (let i = 1; i < segments.length; i++) {
|
|
188
|
+
if (codecPaths.has(segments.slice(0, i).join("."))) return true;
|
|
189
|
+
}
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function getAtSegments(obj: unknown, segments: string[]): unknown {
|
|
194
|
+
let cur: unknown = obj;
|
|
195
|
+
for (const p of segments) {
|
|
196
|
+
if (!isContainer(cur)) return undefined;
|
|
197
|
+
cur = (cur as Record<string, unknown>)[p];
|
|
198
|
+
}
|
|
199
|
+
return cur;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Sets `value` at `segments` inside `obj`. When `defaults` is supplied,
|
|
203
|
+
// missing intermediate slots match the defaults' shape (array vs object) at
|
|
204
|
+
// the same path — so a URL like `items.0.foo=5` rebuilds an array, not an
|
|
205
|
+
// object with numeric keys.
|
|
206
|
+
function setAtSegments(
|
|
207
|
+
obj: Record<string, unknown> | unknown[],
|
|
208
|
+
segments: string[],
|
|
209
|
+
value: unknown,
|
|
210
|
+
defaults?: unknown,
|
|
211
|
+
): void {
|
|
212
|
+
let cur: Record<string, unknown> | unknown[] = obj;
|
|
213
|
+
let defCur: unknown = defaults;
|
|
214
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
215
|
+
const p = segments[i];
|
|
216
|
+
const defNext = isContainer(defCur)
|
|
217
|
+
? (defCur as Record<string, unknown>)[p]
|
|
218
|
+
: undefined;
|
|
219
|
+
const slot = (cur as Record<string, unknown>)[p];
|
|
220
|
+
if (!isContainer(slot)) {
|
|
221
|
+
(cur as Record<string, unknown>)[p] = Array.isArray(defNext) ? [] : {};
|
|
222
|
+
}
|
|
223
|
+
cur = (cur as Record<string, unknown>)[p] as
|
|
224
|
+
| Record<string, unknown>
|
|
225
|
+
| unknown[];
|
|
226
|
+
defCur = defNext;
|
|
227
|
+
}
|
|
228
|
+
(cur as Record<string, unknown>)[segments[segments.length - 1]] = value;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Encode/decode segment arrays to/from URL query keys. Each segment is
|
|
232
|
+
// percent-encoded so a literal `.` inside a key name won't collide with the
|
|
233
|
+
// nesting separator. `encodeURIComponent` leaves `.` alone, so we replace
|
|
234
|
+
// it manually to `%2E`. The encoded key still passes cleanly through
|
|
235
|
+
// `URLSearchParams` and vue-router.
|
|
236
|
+
function encodeKey(segments: string[]): string {
|
|
237
|
+
return segments
|
|
238
|
+
.map((s) => encodeURIComponent(s).replace(/\./g, "%2E"))
|
|
239
|
+
.join(".");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function decodeKey(key: string): string[] {
|
|
243
|
+
return key.split(".").map(decodeURIComponent);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Recursive merge used to layer defaults onto the cloned current params.
|
|
247
|
+
// Descends into matching containers (objects and arrays) so sibling
|
|
248
|
+
// branches survive. URL overrides bypass this and use `setAtSegments`
|
|
249
|
+
// directly, since a comma-encoded leaf array must replace its slot
|
|
250
|
+
// wholesale rather than merge element-wise with the default.
|
|
251
|
+
function deepMerge(
|
|
252
|
+
target: Record<string, unknown> | unknown[],
|
|
253
|
+
source: Record<string, unknown> | unknown[],
|
|
254
|
+
stopPaths?: Set<string>,
|
|
255
|
+
prefix: string[] = [],
|
|
256
|
+
): Record<string, unknown> | unknown[] {
|
|
257
|
+
for (const [k, v] of Object.entries(source)) {
|
|
258
|
+
const slot = (target as Record<string, unknown>)[k];
|
|
259
|
+
const path = stopPaths ? [...prefix, k] : prefix;
|
|
260
|
+
const atStop = stopPaths?.has(path.join("."));
|
|
261
|
+
if (!atStop && isContainer(v) && isContainer(slot)) {
|
|
262
|
+
deepMerge(slot, v, stopPaths, path);
|
|
263
|
+
} else {
|
|
264
|
+
// Codec paths are atomic: overwrite the target subtree with source's
|
|
265
|
+
// value instead of merging the two variants field-by-field (which
|
|
266
|
+
// would produce a malformed hybrid for tagged unions).
|
|
267
|
+
(target as Record<string, unknown>)[k] = v;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return target;
|
|
271
|
+
}
|
|
272
|
+
|
|
77
273
|
export function paramsToQuery<T extends object>(
|
|
78
274
|
params: T,
|
|
79
275
|
defaults: T,
|
|
276
|
+
codecs?: ParamCodecs,
|
|
80
277
|
): Record<string, string> {
|
|
81
278
|
const query: Record<string, string> = {};
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
279
|
+
const stopPaths = codecs ? new Set(Object.keys(codecs)) : undefined;
|
|
280
|
+
for (const [segments, defaultLeaf] of flattenLeaves(
|
|
281
|
+
defaults as Record<string, unknown>,
|
|
282
|
+
[],
|
|
283
|
+
stopPaths,
|
|
284
|
+
)) {
|
|
285
|
+
const paramLeaf = getAtSegments(params, segments);
|
|
286
|
+
if (paramLeaf === undefined) continue;
|
|
287
|
+
const codec = codecs?.[segments.join(".")];
|
|
288
|
+
const serializeFn = codec ? codec.serialize : serialize;
|
|
289
|
+
const serialized = serializeFn(paramLeaf);
|
|
290
|
+
const defaultSerialized = serializeFn(defaultLeaf);
|
|
85
291
|
if (serialized !== defaultSerialized) {
|
|
86
|
-
query[
|
|
292
|
+
query[encodeKey(segments)] = serialized;
|
|
87
293
|
}
|
|
88
294
|
}
|
|
89
295
|
return query;
|
|
@@ -92,13 +298,30 @@ export function paramsToQuery<T extends object>(
|
|
|
92
298
|
export function queryToParams<T extends object>(
|
|
93
299
|
query: Record<string, unknown>,
|
|
94
300
|
defaults: T,
|
|
301
|
+
codecs?: ParamCodecs,
|
|
95
302
|
): Partial<T> {
|
|
96
303
|
const result: Record<string, unknown> = {};
|
|
97
|
-
const
|
|
304
|
+
const codecPaths = codecs ? new Set(Object.keys(codecs)) : undefined;
|
|
98
305
|
for (const [key, raw] of Object.entries(query)) {
|
|
99
|
-
if (!(key in defaultsRecord)) continue;
|
|
100
306
|
if (typeof raw !== "string") continue;
|
|
101
|
-
|
|
307
|
+
const segments = decodeKey(key);
|
|
308
|
+
const pathStr = segments.join(".");
|
|
309
|
+
const codec = codecs?.[pathStr];
|
|
310
|
+
if (codec) {
|
|
311
|
+
try {
|
|
312
|
+
setAtSegments(result, segments, codec.deserialize(raw), defaults);
|
|
313
|
+
} catch {
|
|
314
|
+
// Malformed codec value — skip rather than corrupt the params.
|
|
315
|
+
}
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
// Drop descendants of a codec path; the codec owns the whole subtree.
|
|
319
|
+
if (codecPaths && pathIsInsideAnyCodec(segments, codecPaths)) continue;
|
|
320
|
+
const defaultLeaf = getAtSegments(defaults, segments);
|
|
321
|
+
// Drop unknown dotted keys: matches the flat behavior and keeps the
|
|
322
|
+
// params shape stable.
|
|
323
|
+
if (defaultLeaf === undefined) continue;
|
|
324
|
+
setAtSegments(result, segments, deserialize(raw, defaultLeaf), defaults);
|
|
102
325
|
}
|
|
103
326
|
return result as Partial<T>;
|
|
104
327
|
}
|
|
@@ -127,22 +350,43 @@ function toGetter<T>(input: DefaultsInput<T>): () => T | undefined {
|
|
|
127
350
|
return () => input as T;
|
|
128
351
|
}
|
|
129
352
|
|
|
130
|
-
|
|
353
|
+
// `include`/`ignore` entries are dotted strings using `.` as a JS-side path
|
|
354
|
+
// separator (one segment per key). A path is in scope iff it (or any
|
|
355
|
+
// ancestor) matches an include entry, or it (and all its ancestors) avoid
|
|
356
|
+
// every ignore entry. `include` wins when both are supplied.
|
|
357
|
+
function segmentsStartWith(path: string[], prefix: string[]): boolean {
|
|
358
|
+
if (prefix.length > path.length) return false;
|
|
359
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
360
|
+
if (path[i] !== prefix[i]) return false;
|
|
361
|
+
}
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function pathInScope(
|
|
366
|
+
path: string[],
|
|
367
|
+
include?: string[][],
|
|
368
|
+
ignore?: string[][],
|
|
369
|
+
): boolean {
|
|
370
|
+
if (include) return include.some((entry) => segmentsStartWith(path, entry));
|
|
371
|
+
if (ignore) return !ignore.some((entry) => segmentsStartWith(path, entry));
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function filterDefaults<T extends object>(
|
|
131
376
|
obj: T,
|
|
132
|
-
include?:
|
|
133
|
-
ignore?:
|
|
377
|
+
include?: string[][],
|
|
378
|
+
ignore?: string[][],
|
|
379
|
+
stopPaths?: Set<string>,
|
|
134
380
|
): T {
|
|
135
381
|
if (!include && !ignore) return obj;
|
|
136
|
-
const src = obj as Record<string, unknown>;
|
|
137
382
|
const out: Record<string, unknown> = {};
|
|
138
|
-
for (const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
out[k] = src[k];
|
|
383
|
+
for (const [path, leaf] of flattenLeaves(
|
|
384
|
+
obj as Record<string, unknown>,
|
|
385
|
+
[],
|
|
386
|
+
stopPaths,
|
|
387
|
+
)) {
|
|
388
|
+
if (pathInScope(path, include, ignore)) {
|
|
389
|
+
setAtSegments(out, path, leaf, obj);
|
|
146
390
|
}
|
|
147
391
|
}
|
|
148
392
|
return out as T;
|
|
@@ -151,7 +395,8 @@ function filterKeys<T extends object>(
|
|
|
151
395
|
/**
|
|
152
396
|
* Syncs a reactive params object with the URL query string. Only values
|
|
153
397
|
* that differ from defaults appear in the URL. Accepts either a `ref()`
|
|
154
|
-
* or a `reactive()` object.
|
|
398
|
+
* or a `reactive()` object. Nested objects are encoded as dotted keys
|
|
399
|
+
* (`nested.foo=5`); arrays of primitives remain comma-joined leaves.
|
|
155
400
|
*
|
|
156
401
|
* For async defaults (e.g. loaded from WASM/network), pass a getter or ref
|
|
157
402
|
* that returns `undefined` until ready, and call `hydrate()` once defaults
|
|
@@ -171,11 +416,16 @@ export function useUrlParams<T extends object>(
|
|
|
171
416
|
options: UrlParamsOptions<T> = {},
|
|
172
417
|
) {
|
|
173
418
|
const debounceMs = options.debounceMs ?? 300;
|
|
174
|
-
const { router, route, include, ignore } = options;
|
|
419
|
+
const { router, route, include, ignore, codecs } = options;
|
|
420
|
+
const includeSegments = include?.map((e) => String(e).split("."));
|
|
421
|
+
const ignoreSegments = ignore?.map((e) => String(e).split("."));
|
|
422
|
+
const codecPaths = codecs ? new Set(Object.keys(codecs)) : undefined;
|
|
175
423
|
const getDefaults = toGetter<T>(defaults);
|
|
176
424
|
const scopedDefaults = (): T | undefined => {
|
|
177
425
|
const d = getDefaults();
|
|
178
|
-
return d === undefined
|
|
426
|
+
return d === undefined
|
|
427
|
+
? undefined
|
|
428
|
+
: filterDefaults(d, includeSegments, ignoreSegments, codecPaths);
|
|
179
429
|
};
|
|
180
430
|
|
|
181
431
|
function readQuery(): Record<string, unknown> {
|
|
@@ -195,18 +445,24 @@ export function useUrlParams<T extends object>(
|
|
|
195
445
|
return isRef(params) ? params.value : params;
|
|
196
446
|
}
|
|
197
447
|
|
|
198
|
-
function apply(overrides:
|
|
448
|
+
function apply(overrides: Array<[string[], unknown]>, d: T) {
|
|
199
449
|
// Layer order (later wins): current params -> defaults -> URL overrides.
|
|
200
450
|
// Starting from current preserves keys outside the sync scope (i.e.
|
|
201
451
|
// those filtered out by `include`/`ignore`), which matters when `params`
|
|
202
|
-
// is a ref and the whole object gets replaced below.
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
452
|
+
// is a ref and the whole object gets replaced below. Defaults deep-merge
|
|
453
|
+
// so sibling branches survive; URL overrides are applied at leaf
|
|
454
|
+
// precision so a leaf array like `tags=1,2` replaces the whole leaf
|
|
455
|
+
// instead of merging element-wise with the default array.
|
|
456
|
+
const current = structuredClone(toRaw(read())) as Record<string, unknown>;
|
|
457
|
+
deepMerge(
|
|
458
|
+
current,
|
|
459
|
+
structuredClone(toRaw(d)) as Record<string, unknown>,
|
|
460
|
+
codecPaths,
|
|
461
|
+
);
|
|
462
|
+
for (const [segments, value] of overrides) {
|
|
463
|
+
setAtSegments(current, segments, value, d);
|
|
464
|
+
}
|
|
465
|
+
const merged = current as T;
|
|
210
466
|
if (isRef(params)) {
|
|
211
467
|
params.value = merged;
|
|
212
468
|
} else {
|
|
@@ -214,13 +470,36 @@ export function useUrlParams<T extends object>(
|
|
|
214
470
|
}
|
|
215
471
|
}
|
|
216
472
|
|
|
473
|
+
function readOverrides(d: T): Array<[string[], unknown]> {
|
|
474
|
+
const out: Array<[string[], unknown]> = [];
|
|
475
|
+
for (const [key, raw] of Object.entries(readQuery())) {
|
|
476
|
+
if (typeof raw !== "string") continue;
|
|
477
|
+
const segments = decodeKey(key);
|
|
478
|
+
const pathStr = segments.join(".");
|
|
479
|
+
const codec = codecs?.[pathStr];
|
|
480
|
+
if (codec) {
|
|
481
|
+
try {
|
|
482
|
+
out.push([segments, codec.deserialize(raw)]);
|
|
483
|
+
} catch {
|
|
484
|
+
// skip malformed codec values
|
|
485
|
+
}
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
if (codecPaths && pathIsInsideAnyCodec(segments, codecPaths)) continue;
|
|
489
|
+
const defaultLeaf = getAtSegments(d, segments);
|
|
490
|
+
if (defaultLeaf === undefined) continue;
|
|
491
|
+
out.push([segments, deserialize(raw, defaultLeaf)]);
|
|
492
|
+
}
|
|
493
|
+
return out;
|
|
494
|
+
}
|
|
495
|
+
|
|
217
496
|
let hydrated = false;
|
|
218
497
|
|
|
219
498
|
function hydrate(): boolean {
|
|
220
499
|
const d = scopedDefaults();
|
|
221
500
|
if (d === undefined) return false;
|
|
222
|
-
const overrides =
|
|
223
|
-
if (
|
|
501
|
+
const overrides = readOverrides(d);
|
|
502
|
+
if (overrides.length > 0) apply(overrides, d);
|
|
224
503
|
hydrated = true;
|
|
225
504
|
return true;
|
|
226
505
|
}
|
|
@@ -232,8 +511,7 @@ export function useUrlParams<T extends object>(
|
|
|
232
511
|
}
|
|
233
512
|
const d = scopedDefaults();
|
|
234
513
|
if (d === undefined) return;
|
|
235
|
-
|
|
236
|
-
apply(overrides, d);
|
|
514
|
+
apply(readOverrides(d), d);
|
|
237
515
|
}
|
|
238
516
|
|
|
239
517
|
onMounted(() => {
|
|
@@ -250,7 +528,7 @@ export function useUrlParams<T extends object>(
|
|
|
250
528
|
debounceTimer = setTimeout(() => {
|
|
251
529
|
const d = scopedDefaults();
|
|
252
530
|
if (d === undefined) return;
|
|
253
|
-
writeQuery(paramsToQuery(read(), d));
|
|
531
|
+
writeQuery(paramsToQuery(read(), d, codecs));
|
|
254
532
|
}, debounceMs);
|
|
255
533
|
},
|
|
256
534
|
{ deep: true },
|
|
@@ -281,7 +559,7 @@ export function useUrlParams<T extends object>(
|
|
|
281
559
|
const { clearUrl = true } = opts;
|
|
282
560
|
const d = scopedDefaults();
|
|
283
561
|
if (d === undefined) return;
|
|
284
|
-
apply(
|
|
562
|
+
apply([], d);
|
|
285
563
|
if (clearUrl) {
|
|
286
564
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
287
565
|
writeQuery({});
|