@vizejs/composable 0.345.0 → 0.347.7
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/dist/abort-signal.d.mts +96 -0
- package/dist/abort-signal.mjs +203 -0
- package/dist/async-resource.d.mts +97 -0
- package/dist/async-resource.mjs +99 -0
- package/dist/capability-BNpkvSy5.d.mts +93 -0
- package/dist/capability.d.mts +2 -0
- package/dist/capability.mjs +37 -0
- package/dist/catalog-CuWhR-0q.d.mts +506 -0
- package/dist/catalog-DOXTbk4r.mjs +529 -0
- package/dist/catalog.d.mts +2 -0
- package/dist/catalog.mjs +2 -0
- package/dist/disposal-scope.d.mts +93 -0
- package/dist/disposal-scope.mjs +119 -0
- package/dist/event-listener.d.mts +85 -0
- package/dist/event-listener.mjs +55 -0
- package/dist/index.d.mts +19 -786
- package/dist/index.mjs +19 -751
- package/dist/locale.d.mts +65 -0
- package/dist/locale.mjs +72 -0
- package/dist/media-query.d.mts +56 -0
- package/dist/media-query.mjs +56 -0
- package/dist/retry-async.d.mts +91 -0
- package/dist/retry-async.mjs +136 -0
- package/dist/retry-delay.d.mts +73 -0
- package/dist/retry-delay.mjs +50 -0
- package/dist/scope.d.mts +18 -0
- package/dist/scope.mjs +23 -0
- package/dist/timeout-scheduler.d.mts +18 -0
- package/dist/timeout-scheduler.mjs +1 -0
- package/dist/use-counter.d.mts +91 -0
- package/dist/use-counter.mjs +67 -0
- package/dist/use-debounced.d.mts +87 -0
- package/dist/use-debounced.mjs +98 -0
- package/dist/use-history.d.mts +105 -0
- package/dist/use-history.mjs +137 -0
- package/dist/use-previous.d.mts +43 -0
- package/dist/use-previous.mjs +11 -0
- package/dist/use-throttled.d.mts +107 -0
- package/dist/use-throttled.mjs +124 -0
- package/dist/use-toggle.d.mts +45 -0
- package/dist/use-toggle.mjs +34 -0
- package/package.json +91 -1
package/dist/index.mjs
CHANGED
|
@@ -1,751 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return true;
|
|
21
|
-
}
|
|
22
|
-
//#endregion
|
|
23
|
-
//#region src/async-resource.ts
|
|
24
|
-
/**
|
|
25
|
-
* Create a scoped, abortable asynchronous resource with latest-result-wins
|
|
26
|
-
* state. Every execution returns a discriminated result, so cancellation,
|
|
27
|
-
* supersession, loader failure, and successful `undefined` data stay distinct.
|
|
28
|
-
*
|
|
29
|
-
* When created inside an active reactive scope (and `scope` is enabled), the
|
|
30
|
-
* active execution is aborted when that scope stops; outside a scope,
|
|
31
|
-
* cancellation ownership stays with the caller. The execute promise never
|
|
32
|
-
* rejects — synchronous and asynchronous loader failures both settle into
|
|
33
|
-
* the `"error"` result. Safe during server rendering: no browser globals are
|
|
34
|
-
* read and abort reasons use the runtime-native `DOMException`.
|
|
35
|
-
*
|
|
36
|
-
* @param loader Asynchronous loader receiving the abort context first.
|
|
37
|
-
* @param options Data retention, supersession, and scope behavior.
|
|
38
|
-
* @default options {}
|
|
39
|
-
* @returns Reactive state and controls for the loader.
|
|
40
|
-
*/
|
|
41
|
-
function useAsyncResource(loader, options = {}) {
|
|
42
|
-
const data = shallowRef(options.initialData);
|
|
43
|
-
const error = shallowRef(void 0);
|
|
44
|
-
const status = shallowRef("idle");
|
|
45
|
-
const pending = computed(() => status.value === "pending");
|
|
46
|
-
let generation = 0;
|
|
47
|
-
let active;
|
|
48
|
-
const cancel = (reason = createAbortReason("The execution was cancelled.")) => {
|
|
49
|
-
if (active === void 0) return false;
|
|
50
|
-
generation += 1;
|
|
51
|
-
active.controller.abort(reason);
|
|
52
|
-
active = void 0;
|
|
53
|
-
status.value = "cancelled";
|
|
54
|
-
return true;
|
|
55
|
-
};
|
|
56
|
-
const execute = async (...arguments_) => {
|
|
57
|
-
if ((options.cancelPrevious ?? true) && active !== void 0) {
|
|
58
|
-
active.superseded = true;
|
|
59
|
-
active.controller.abort(createAbortReason("A newer execution started."));
|
|
60
|
-
}
|
|
61
|
-
const record = {
|
|
62
|
-
generation: ++generation,
|
|
63
|
-
controller: new AbortController(),
|
|
64
|
-
superseded: false
|
|
65
|
-
};
|
|
66
|
-
active = record;
|
|
67
|
-
error.value = void 0;
|
|
68
|
-
status.value = "pending";
|
|
69
|
-
if (!(options.keepData ?? true)) data.value = void 0;
|
|
70
|
-
try {
|
|
71
|
-
const result = await loader({ signal: record.controller.signal }, ...arguments_);
|
|
72
|
-
if (record.generation !== generation) return executionAfterInvalidation(record);
|
|
73
|
-
data.value = result;
|
|
74
|
-
status.value = "success";
|
|
75
|
-
return {
|
|
76
|
-
status: "success",
|
|
77
|
-
data: result
|
|
78
|
-
};
|
|
79
|
-
} catch (cause) {
|
|
80
|
-
if (record.generation !== generation || record.controller.signal.aborted) return executionAfterInvalidation(record);
|
|
81
|
-
error.value = cause;
|
|
82
|
-
status.value = "error";
|
|
83
|
-
return {
|
|
84
|
-
status: "error",
|
|
85
|
-
error: cause
|
|
86
|
-
};
|
|
87
|
-
} finally {
|
|
88
|
-
if (active === record) active = void 0;
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
const reset = () => {
|
|
92
|
-
cancel(createAbortReason("The resource was reset."));
|
|
93
|
-
data.value = options.initialData;
|
|
94
|
-
error.value = void 0;
|
|
95
|
-
status.value = "idle";
|
|
96
|
-
};
|
|
97
|
-
if (options.scope ?? true) tryOnScopeDispose(() => cancel(createAbortReason("The reactive scope was disposed.")));
|
|
98
|
-
return {
|
|
99
|
-
data,
|
|
100
|
-
error,
|
|
101
|
-
status,
|
|
102
|
-
pending,
|
|
103
|
-
execute,
|
|
104
|
-
cancel,
|
|
105
|
-
reset
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
function executionAfterInvalidation(execution) {
|
|
109
|
-
if (execution.superseded || !execution.controller.signal.aborted) return { status: "superseded" };
|
|
110
|
-
return {
|
|
111
|
-
status: "cancelled",
|
|
112
|
-
reason: execution.controller.signal.reason
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
function createAbortReason(message) {
|
|
116
|
-
return new DOMException(message, "AbortError");
|
|
117
|
-
}
|
|
118
|
-
//#endregion
|
|
119
|
-
//#region src/event-listener.ts
|
|
120
|
-
function useEventListener(target, event, listener, options = {}) {
|
|
121
|
-
const { capture = false, once = false, passive = false, signal, immediate = true, flush = "pre" } = options;
|
|
122
|
-
const isListening = ref(false);
|
|
123
|
-
const eventOptions = {
|
|
124
|
-
capture,
|
|
125
|
-
passive,
|
|
126
|
-
...signal ? { signal } : {}
|
|
127
|
-
};
|
|
128
|
-
let disposed = false;
|
|
129
|
-
let stopWatch;
|
|
130
|
-
const stop = () => {
|
|
131
|
-
stopWatch?.stop();
|
|
132
|
-
stopWatch = void 0;
|
|
133
|
-
isListening.value = false;
|
|
134
|
-
};
|
|
135
|
-
const start = () => {
|
|
136
|
-
if (disposed || stopWatch || signal?.aborted) return false;
|
|
137
|
-
stopWatch = watch(() => toValue(target), (next, _previous, onCleanup) => {
|
|
138
|
-
isListening.value = false;
|
|
139
|
-
if (!next || signal?.aborted) return;
|
|
140
|
-
const invoke = (nativeEvent) => {
|
|
141
|
-
if (once) stop();
|
|
142
|
-
listener(nativeEvent);
|
|
143
|
-
};
|
|
144
|
-
const onAbort = () => stop();
|
|
145
|
-
next.addEventListener(event, invoke, eventOptions);
|
|
146
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
147
|
-
isListening.value = true;
|
|
148
|
-
onCleanup(() => {
|
|
149
|
-
next.removeEventListener(event, invoke, capture);
|
|
150
|
-
signal?.removeEventListener("abort", onAbort);
|
|
151
|
-
isListening.value = false;
|
|
152
|
-
});
|
|
153
|
-
}, {
|
|
154
|
-
flush,
|
|
155
|
-
immediate: true
|
|
156
|
-
});
|
|
157
|
-
return true;
|
|
158
|
-
};
|
|
159
|
-
tryOnScopeDispose(() => {
|
|
160
|
-
disposed = true;
|
|
161
|
-
stop();
|
|
162
|
-
});
|
|
163
|
-
if (immediate) start();
|
|
164
|
-
return {
|
|
165
|
-
isListening: readonly(isListening),
|
|
166
|
-
start,
|
|
167
|
-
stop
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
//#endregion
|
|
171
|
-
//#region src/locale.ts
|
|
172
|
-
const FORMATTER_CACHE_LIMIT = 32;
|
|
173
|
-
/**
|
|
174
|
-
* Create reactive locale metadata and platform-native formatter factories.
|
|
175
|
-
*
|
|
176
|
-
* Equivalent formatter options reuse instances. The bounded cache follows the
|
|
177
|
-
* active locale automatically and prevents repeated constructor overhead in
|
|
178
|
-
* reactive render paths.
|
|
179
|
-
*
|
|
180
|
-
* Detection is lazy and guarded: the default detector reads
|
|
181
|
-
* `navigator.language` behind a `typeof` check at call time, so importing and
|
|
182
|
-
* calling this during server rendering is safe, and runtimes without a
|
|
183
|
-
* `navigator` resolve to the fallback locale. The composable owns no timers
|
|
184
|
-
* or listeners, so no scope cleanup is required.
|
|
185
|
-
*
|
|
186
|
-
* @param source Reactive locale source. Empty values defer to the detector.
|
|
187
|
-
* @param options Detection and fallback behavior.
|
|
188
|
-
* @default options {}
|
|
189
|
-
* @throws `RangeError` on first read of the reactive values when the winning
|
|
190
|
-
* candidate is not a structurally valid locale identifier.
|
|
191
|
-
* @returns Reactive locale metadata and cached formatter factories.
|
|
192
|
-
*/
|
|
193
|
-
function useLocale(source, options = {}) {
|
|
194
|
-
const locale = computed(() => {
|
|
195
|
-
const candidate = (source === void 0 ? void 0 : toValue(source)) ?? (options.detect ?? detectBrowserLocale)() ?? options.fallback ?? "en";
|
|
196
|
-
return candidate instanceof Intl.Locale ? candidate.toString() : new Intl.Locale(candidate).toString();
|
|
197
|
-
});
|
|
198
|
-
const details = computed(() => new Intl.Locale(locale.value));
|
|
199
|
-
const direction = computed(() => details.value.getTextInfo().direction ?? "ltr");
|
|
200
|
-
const number = createFormatterCache((activeLocale, formatOptions) => new Intl.NumberFormat(activeLocale, formatOptions));
|
|
201
|
-
const dateTime = createFormatterCache((activeLocale, formatOptions) => new Intl.DateTimeFormat(activeLocale, formatOptions));
|
|
202
|
-
const list = createFormatterCache((activeLocale, formatOptions) => new Intl.ListFormat(activeLocale, formatOptions));
|
|
203
|
-
const relativeTime = createFormatterCache((activeLocale, formatOptions) => new Intl.RelativeTimeFormat(activeLocale, formatOptions));
|
|
204
|
-
return {
|
|
205
|
-
locale,
|
|
206
|
-
details,
|
|
207
|
-
direction,
|
|
208
|
-
number: (formatOptions) => number(locale.value, formatOptions),
|
|
209
|
-
dateTime: (formatOptions) => dateTime(locale.value, formatOptions),
|
|
210
|
-
list: (formatOptions) => list(locale.value, formatOptions),
|
|
211
|
-
relativeTime: (formatOptions) => relativeTime(locale.value, formatOptions)
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
function detectBrowserLocale() {
|
|
215
|
-
return typeof navigator === "undefined" ? void 0 : navigator.language;
|
|
216
|
-
}
|
|
217
|
-
function createFormatterCache(create) {
|
|
218
|
-
const cache = /* @__PURE__ */ new Map();
|
|
219
|
-
return (locale, options) => {
|
|
220
|
-
const key = `${locale}\u0000${serializeOptions(options)}`;
|
|
221
|
-
const cached = cache.get(key);
|
|
222
|
-
if (cached !== void 0) {
|
|
223
|
-
cache.delete(key);
|
|
224
|
-
cache.set(key, cached);
|
|
225
|
-
return cached;
|
|
226
|
-
}
|
|
227
|
-
const formatter = create(locale, options);
|
|
228
|
-
if (cache.size >= FORMATTER_CACHE_LIMIT) {
|
|
229
|
-
const oldest = cache.keys().next().value;
|
|
230
|
-
if (oldest !== void 0) cache.delete(oldest);
|
|
231
|
-
}
|
|
232
|
-
cache.set(key, formatter);
|
|
233
|
-
return formatter;
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
function serializeOptions(options) {
|
|
237
|
-
if (options === void 0) return "";
|
|
238
|
-
return JSON.stringify(Object.entries(options).sort(([left], [right]) => left.localeCompare(right)));
|
|
239
|
-
}
|
|
240
|
-
//#endregion
|
|
241
|
-
//#region src/media-query.ts
|
|
242
|
-
/**
|
|
243
|
-
* Evaluate a reactive media query without requiring browser globals.
|
|
244
|
-
*
|
|
245
|
-
* During server rendering (or whenever no capability host resolves) the ref
|
|
246
|
-
* holds the configured server value and no subscription is created. The
|
|
247
|
-
* change subscription follows the reactive query and host: each
|
|
248
|
-
* re-evaluation removes the previous listener, and the final listener is
|
|
249
|
-
* removed when the owning reactive scope stops. Call inside an active scope
|
|
250
|
-
* so the subscription is released. A host whose matcher throws propagates
|
|
251
|
-
* the error to the active effect run; the browser default never throws.
|
|
252
|
-
*
|
|
253
|
-
* @param query Reactive media-query source.
|
|
254
|
-
* @param options Runtime capability and server-rendered fallback.
|
|
255
|
-
* @default options {}
|
|
256
|
-
* @returns Readonly ref that is `true` while the query matches.
|
|
257
|
-
*/
|
|
258
|
-
function useMediaQuery(query, options = {}) {
|
|
259
|
-
const matches = ref(options.ssrValue ?? false);
|
|
260
|
-
watchEffect((onCleanup) => {
|
|
261
|
-
const host = options.host === void 0 ? browserMediaQueryHost() : toValue(options.host);
|
|
262
|
-
if (!host) {
|
|
263
|
-
matches.value = options.ssrValue ?? false;
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
const media = host.matchMedia(toValue(query));
|
|
267
|
-
const update = () => {
|
|
268
|
-
matches.value = media.matches;
|
|
269
|
-
};
|
|
270
|
-
update();
|
|
271
|
-
media.addEventListener("change", update);
|
|
272
|
-
onCleanup(() => media.removeEventListener("change", update));
|
|
273
|
-
});
|
|
274
|
-
return readonly(matches);
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Return the reactive user motion preference.
|
|
278
|
-
*
|
|
279
|
-
* Shares {@link useMediaQuery} semantics: during server rendering the
|
|
280
|
-
* preference is `"no-preference"` unless `ssrValue` is `true`, and the
|
|
281
|
-
* underlying subscription is removed when the owning reactive scope stops.
|
|
282
|
-
*
|
|
283
|
-
* @param options Runtime capability and server-rendered fallback.
|
|
284
|
-
* @default options {}
|
|
285
|
-
* @returns Computed preference for `(prefers-reduced-motion: reduce)`.
|
|
286
|
-
*/
|
|
287
|
-
function useReducedMotion(options = {}) {
|
|
288
|
-
const reduced = useMediaQuery("(prefers-reduced-motion: reduce)", options);
|
|
289
|
-
return computed(() => reduced.value ? "reduce" : "no-preference");
|
|
290
|
-
}
|
|
291
|
-
function browserMediaQueryHost() {
|
|
292
|
-
return typeof window !== "undefined" && typeof window.matchMedia === "function" ? window : void 0;
|
|
293
|
-
}
|
|
294
|
-
//#endregion
|
|
295
|
-
//#region src/use-counter.ts
|
|
296
|
-
/**
|
|
297
|
-
* Create a clamped counter whose every transition stays inside `[min, max]`.
|
|
298
|
-
*
|
|
299
|
-
* All operations clamp instead of failing, including the initial value, so
|
|
300
|
-
* the count is inside the bounds at every observable moment. Only `NaN` is
|
|
301
|
-
* rejected — silently corrupting the count is never an option. Purely
|
|
302
|
-
* synchronous state: safe during server rendering (no browser globals, no
|
|
303
|
-
* timers) and nothing to dispose, so it works inside and outside reactive
|
|
304
|
-
* scopes alike. Bounds are fixed at creation and not reactive.
|
|
305
|
-
*
|
|
306
|
-
* @example
|
|
307
|
-
* ```ts
|
|
308
|
-
* const { count, increment, atMax } = useCounter(9, { min: 0, max: 10 });
|
|
309
|
-
* increment(); // 10
|
|
310
|
-
* increment(); // 10 (clamped)
|
|
311
|
-
* atMax.value; // true
|
|
312
|
-
* ```
|
|
313
|
-
*
|
|
314
|
-
* @param initial Count before any operation, clamped into the bounds.
|
|
315
|
-
* @default initial 0
|
|
316
|
-
* @param options Inclusive bounds for every value the counter takes.
|
|
317
|
-
* @default options {}
|
|
318
|
-
* @throws `RangeError` tagged `VIZE_COMPOSE_COUNTER_INVALID_RANGE` when a
|
|
319
|
-
* bound is `NaN` or `min` exceeds `max`.
|
|
320
|
-
* @throws `RangeError` tagged `VIZE_COMPOSE_COUNTER_INVALID_VALUE` when an
|
|
321
|
-
* initial value, operand, or arithmetic result is `NaN` (for example
|
|
322
|
-
* incrementing `-Infinity` by `Infinity`); the count is left unchanged.
|
|
323
|
-
* @returns Reactive count, bound flags, and mutation controls.
|
|
324
|
-
*/
|
|
325
|
-
function useCounter(initial = 0, options = {}) {
|
|
326
|
-
const min = requireBound(options.min ?? Number.NEGATIVE_INFINITY, "min");
|
|
327
|
-
const max = requireBound(options.max ?? Number.POSITIVE_INFINITY, "max");
|
|
328
|
-
if (min > max) throw new RangeError(`[VIZE_COMPOSE_COUNTER_INVALID_RANGE] min must not exceed max; received min ${String(min)} and max ${String(max)}`);
|
|
329
|
-
const clamp = (value) => Math.min(max, Math.max(min, value));
|
|
330
|
-
const count = shallowRef(clamp(requireValue(initial)));
|
|
331
|
-
let baseline = count.value;
|
|
332
|
-
const setClamped = (next) => {
|
|
333
|
-
count.value = clamp(requireValue(next));
|
|
334
|
-
return count.value;
|
|
335
|
-
};
|
|
336
|
-
const reset = (value) => {
|
|
337
|
-
const applied = setClamped(value ?? baseline);
|
|
338
|
-
if (value !== void 0) baseline = applied;
|
|
339
|
-
return applied;
|
|
340
|
-
};
|
|
341
|
-
return {
|
|
342
|
-
count,
|
|
343
|
-
atMin: computed(() => count.value === min),
|
|
344
|
-
atMax: computed(() => count.value === max),
|
|
345
|
-
increment: (delta = 1) => setClamped(count.value + requireValue(delta)),
|
|
346
|
-
decrement: (delta = 1) => setClamped(count.value - requireValue(delta)),
|
|
347
|
-
set: setClamped,
|
|
348
|
-
reset
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
function requireBound(value, label) {
|
|
352
|
-
if (Number.isNaN(value)) throw new RangeError(`[VIZE_COMPOSE_COUNTER_INVALID_RANGE] ${label} must not be NaN`);
|
|
353
|
-
return value;
|
|
354
|
-
}
|
|
355
|
-
function requireValue(value) {
|
|
356
|
-
if (Number.isNaN(value)) throw new RangeError("[VIZE_COMPOSE_COUNTER_INVALID_VALUE] the value is NaN; counter state was left unchanged");
|
|
357
|
-
return value;
|
|
358
|
-
}
|
|
359
|
-
//#endregion
|
|
360
|
-
//#region src/use-debounced.ts
|
|
361
|
-
const defaultScheduler$1 = {
|
|
362
|
-
setTimeout: (callback, delayMs) => globalThis.setTimeout(callback, delayMs),
|
|
363
|
-
clearTimeout: (handle) => {
|
|
364
|
-
globalThis.clearTimeout(handle);
|
|
365
|
-
}
|
|
366
|
-
};
|
|
367
|
-
/**
|
|
368
|
-
* Create a readonly debounced view of a reactive source.
|
|
369
|
-
*
|
|
370
|
-
* The view starts at the current source value. Each source change (observed
|
|
371
|
-
* with `flush: "sync"`, so every synchronous write counts) restarts a
|
|
372
|
-
* single-shot timer of `waitMs` milliseconds; when it fires, the view takes
|
|
373
|
-
* the source value current at that moment. `waitMs` is reactive and is read
|
|
374
|
-
* when a timer is scheduled; changing it does not restart an already-pending
|
|
375
|
-
* timer. A wait of `0` still defers to the next scheduler tick.
|
|
376
|
-
*
|
|
377
|
-
* Server rendering is explicit: without a browser `window` (and with
|
|
378
|
-
* {@link UseDebouncedOptions.runOnServer} disabled) no timer ever starts and
|
|
379
|
-
* the view mirrors the source synchronously, so server-rendered output shows
|
|
380
|
-
* current values and nothing leaks. `pending` stays `false` and the controls
|
|
381
|
-
* report `false` in that mode.
|
|
382
|
-
*
|
|
383
|
-
* Cleanup rule: the watcher and any pending timer are released when the
|
|
384
|
-
* owning reactive scope stops; call inside an active scope. Outside one, the
|
|
385
|
-
* watcher lives as long as the source and `cancel()` only clears the pending
|
|
386
|
-
* timer.
|
|
387
|
-
*
|
|
388
|
-
* @example
|
|
389
|
-
* ```ts
|
|
390
|
-
* const query = shallowRef("");
|
|
391
|
-
* const { debounced, flush } = useDebounced(query, 300);
|
|
392
|
-
* query.value = "vize"; // debounced.value still "" for 300ms
|
|
393
|
-
* flush(); // debounced.value === "vize" immediately
|
|
394
|
-
* ```
|
|
395
|
-
*
|
|
396
|
-
* @param source Reactive source to debounce.
|
|
397
|
-
* @param waitMs Reactive delay in milliseconds; must be finite and at least
|
|
398
|
-
* zero. Fractions are truncated.
|
|
399
|
-
* @param options Runtime scheduling overrides.
|
|
400
|
-
* @default options {}
|
|
401
|
-
* @throws `RangeError` tagged `VIZE_COMPOSE_DEBOUNCE_INVALID_WAIT` when the
|
|
402
|
-
* resolved wait is not finite or is negative, both synchronously at creation
|
|
403
|
-
* (even in mirror mode) and again for every scheduled delay.
|
|
404
|
-
* @returns Readonly debounced view, pending flag, and cancel/flush controls.
|
|
405
|
-
*/
|
|
406
|
-
function useDebounced(source, waitMs, options = {}) {
|
|
407
|
-
const scheduler = options.scheduler ?? defaultScheduler$1;
|
|
408
|
-
const debounced = shallowRef(toValue(source));
|
|
409
|
-
const pending = shallowRef(false);
|
|
410
|
-
let handle;
|
|
411
|
-
resolveWaitMs$1(toValue(waitMs));
|
|
412
|
-
const apply = () => {
|
|
413
|
-
handle = void 0;
|
|
414
|
-
pending.value = false;
|
|
415
|
-
debounced.value = toValue(source);
|
|
416
|
-
};
|
|
417
|
-
const cancel = () => {
|
|
418
|
-
if (!pending.value) return false;
|
|
419
|
-
scheduler.clearTimeout(handle);
|
|
420
|
-
handle = void 0;
|
|
421
|
-
pending.value = false;
|
|
422
|
-
return true;
|
|
423
|
-
};
|
|
424
|
-
const flush = () => {
|
|
425
|
-
if (!pending.value) return false;
|
|
426
|
-
scheduler.clearTimeout(handle);
|
|
427
|
-
apply();
|
|
428
|
-
return true;
|
|
429
|
-
};
|
|
430
|
-
watch(() => toValue(source), (next) => {
|
|
431
|
-
if (typeof window === "undefined" && !(options.runOnServer ?? false)) {
|
|
432
|
-
debounced.value = next;
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
const delayMs = resolveWaitMs$1(toValue(waitMs));
|
|
436
|
-
if (pending.value) scheduler.clearTimeout(handle);
|
|
437
|
-
pending.value = true;
|
|
438
|
-
handle = scheduler.setTimeout(apply, delayMs);
|
|
439
|
-
}, { flush: "sync" });
|
|
440
|
-
tryOnScopeDispose(() => {
|
|
441
|
-
cancel();
|
|
442
|
-
});
|
|
443
|
-
return {
|
|
444
|
-
debounced,
|
|
445
|
-
pending,
|
|
446
|
-
cancel,
|
|
447
|
-
flush
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
|
-
function resolveWaitMs$1(value) {
|
|
451
|
-
if (!Number.isFinite(value) || value < 0) throw new RangeError(`[VIZE_COMPOSE_DEBOUNCE_INVALID_WAIT] waitMs must be finite and at least zero; received ${String(value)}`);
|
|
452
|
-
return Math.trunc(value);
|
|
453
|
-
}
|
|
454
|
-
//#endregion
|
|
455
|
-
//#region src/use-history.ts
|
|
456
|
-
/**
|
|
457
|
-
* Record bounded undo/redo history over the writes of a ref.
|
|
458
|
-
*
|
|
459
|
-
* Recording is shallow and identity-based, matching Vue's own change
|
|
460
|
-
* detection: assignments to `source.value` are recorded (observed with
|
|
461
|
-
* `flush: "sync"`, so every synchronous write counts), writes that are
|
|
462
|
-
* `Object.is`-equal to the current value are not changes, and in-place
|
|
463
|
-
* mutations of object values are invisible — pair mutable values with
|
|
464
|
-
* {@link UseHistoryOptions.clone} and reassign. Undoing and redoing restore
|
|
465
|
-
* values through `clone` as well, so snapshots never share identity with the
|
|
466
|
-
* live value unless the default identity clone is kept. When a
|
|
467
|
-
* user-provided `clone` throws, the failed operation leaves history
|
|
468
|
-
* unchanged and the error propagates.
|
|
469
|
-
*
|
|
470
|
-
* Safe during server rendering: no browser globals are read and no timers
|
|
471
|
-
* start. Cleanup rule: when the owning reactive scope stops, recording stops
|
|
472
|
-
* and every retained snapshot is released, so `undo`/`redo` return `false`
|
|
473
|
-
* afterwards; call inside an active scope, or the watcher lives as long as
|
|
474
|
-
* the source.
|
|
475
|
-
*
|
|
476
|
-
* @example
|
|
477
|
-
* ```ts
|
|
478
|
-
* const text = shallowRef("");
|
|
479
|
-
* const { undo, redo, batch } = useHistory(text);
|
|
480
|
-
* text.value = "a";
|
|
481
|
-
* batch(() => {
|
|
482
|
-
* text.value = "ab";
|
|
483
|
-
* text.value = "abc";
|
|
484
|
-
* });
|
|
485
|
-
* undo(); // text.value === "a" (the batch is one step)
|
|
486
|
-
* redo(); // text.value === "abc"
|
|
487
|
-
* ```
|
|
488
|
-
*
|
|
489
|
-
* @param source Ref whose writes are recorded.
|
|
490
|
-
* @param options Retention bound and snapshot cloning.
|
|
491
|
-
* @default options {}
|
|
492
|
-
* @throws `RangeError` tagged `VIZE_COMPOSE_HISTORY_INVALID_CAPACITY` when
|
|
493
|
-
* the capacity is not an integer greater than zero.
|
|
494
|
-
* @throws `Error` tagged `VIZE_COMPOSE_HISTORY_IN_BATCH` when `undo`,
|
|
495
|
-
* `redo`, or `clear` is called inside {@link HistoryControls.batch}, where
|
|
496
|
-
* stack movement would corrupt the pending group.
|
|
497
|
-
* @returns Reactive undo/redo state and controls.
|
|
498
|
-
*/
|
|
499
|
-
function useHistory(source, options = {}) {
|
|
500
|
-
const capacity = options.capacity ?? 100;
|
|
501
|
-
if (!Number.isInteger(capacity) || capacity < 1) throw new RangeError(`[VIZE_COMPOSE_HISTORY_INVALID_CAPACITY] capacity must be an integer greater than zero; received ${String(capacity)}`);
|
|
502
|
-
const clone = options.clone ?? ((value) => value);
|
|
503
|
-
const undoStack = shallowRef([]);
|
|
504
|
-
const redoStack = shallowRef([]);
|
|
505
|
-
let restoring = false;
|
|
506
|
-
let batchDepth = 0;
|
|
507
|
-
let activeBatch;
|
|
508
|
-
const pushUndo = (entry) => {
|
|
509
|
-
const next = [...undoStack.value, entry];
|
|
510
|
-
undoStack.value = next.length > capacity ? next.slice(next.length - capacity) : next;
|
|
511
|
-
redoStack.value = [];
|
|
512
|
-
};
|
|
513
|
-
const writeSilently = (value) => {
|
|
514
|
-
restoring = true;
|
|
515
|
-
try {
|
|
516
|
-
source.value = value;
|
|
517
|
-
} finally {
|
|
518
|
-
restoring = false;
|
|
519
|
-
}
|
|
520
|
-
};
|
|
521
|
-
const requireOutsideBatch = (operation) => {
|
|
522
|
-
if (batchDepth > 0) throw new Error(`[VIZE_COMPOSE_HISTORY_IN_BATCH] ${operation}() is not available inside batch()`);
|
|
523
|
-
};
|
|
524
|
-
const undo = () => {
|
|
525
|
-
requireOutsideBatch("undo");
|
|
526
|
-
const entry = undoStack.value.at(-1);
|
|
527
|
-
if (entry === void 0) return false;
|
|
528
|
-
const restored = clone(entry.value);
|
|
529
|
-
const recorded = { value: clone(source.value) };
|
|
530
|
-
undoStack.value = undoStack.value.slice(0, -1);
|
|
531
|
-
redoStack.value = [...redoStack.value, recorded];
|
|
532
|
-
writeSilently(restored);
|
|
533
|
-
return true;
|
|
534
|
-
};
|
|
535
|
-
const redo = () => {
|
|
536
|
-
requireOutsideBatch("redo");
|
|
537
|
-
const entry = redoStack.value.at(-1);
|
|
538
|
-
if (entry === void 0) return false;
|
|
539
|
-
const restored = clone(entry.value);
|
|
540
|
-
const recorded = { value: clone(source.value) };
|
|
541
|
-
redoStack.value = redoStack.value.slice(0, -1);
|
|
542
|
-
undoStack.value = [...undoStack.value, recorded];
|
|
543
|
-
writeSilently(restored);
|
|
544
|
-
return true;
|
|
545
|
-
};
|
|
546
|
-
const batch = (run) => {
|
|
547
|
-
if (batchDepth === 0) activeBatch = {
|
|
548
|
-
raw: source.value,
|
|
549
|
-
entry: { value: clone(source.value) }
|
|
550
|
-
};
|
|
551
|
-
batchDepth += 1;
|
|
552
|
-
try {
|
|
553
|
-
return run();
|
|
554
|
-
} finally {
|
|
555
|
-
batchDepth -= 1;
|
|
556
|
-
if (batchDepth === 0 && activeBatch !== void 0) {
|
|
557
|
-
const finished = activeBatch;
|
|
558
|
-
activeBatch = void 0;
|
|
559
|
-
if (!Object.is(finished.raw, source.value)) pushUndo(finished.entry);
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
};
|
|
563
|
-
const clear = () => {
|
|
564
|
-
requireOutsideBatch("clear");
|
|
565
|
-
undoStack.value = [];
|
|
566
|
-
redoStack.value = [];
|
|
567
|
-
};
|
|
568
|
-
const handle = watch(source, (_next, replaced) => {
|
|
569
|
-
if (restoring || batchDepth > 0) return;
|
|
570
|
-
pushUndo({ value: clone(replaced) });
|
|
571
|
-
}, { flush: "sync" });
|
|
572
|
-
tryOnScopeDispose(() => {
|
|
573
|
-
handle.stop();
|
|
574
|
-
undoStack.value = [];
|
|
575
|
-
redoStack.value = [];
|
|
576
|
-
});
|
|
577
|
-
return {
|
|
578
|
-
canUndo: computed(() => undoStack.value.length > 0),
|
|
579
|
-
canRedo: computed(() => redoStack.value.length > 0),
|
|
580
|
-
undoCount: computed(() => undoStack.value.length),
|
|
581
|
-
redoCount: computed(() => redoStack.value.length),
|
|
582
|
-
undo,
|
|
583
|
-
redo,
|
|
584
|
-
batch,
|
|
585
|
-
clear
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
|
-
//#endregion
|
|
589
|
-
//#region src/use-previous.ts
|
|
590
|
-
function usePrevious(source, ...initial) {
|
|
591
|
-
const previous = shallowRef(initial.length === 1 ? initial[0] : void 0);
|
|
592
|
-
watch(() => toValue(source), (_next, replaced) => {
|
|
593
|
-
previous.value = replaced;
|
|
594
|
-
}, { flush: "sync" });
|
|
595
|
-
return previous;
|
|
596
|
-
}
|
|
597
|
-
//#endregion
|
|
598
|
-
//#region src/use-throttled.ts
|
|
599
|
-
const defaultScheduler = {
|
|
600
|
-
setTimeout: (callback, delayMs) => globalThis.setTimeout(callback, delayMs),
|
|
601
|
-
clearTimeout: (handle) => {
|
|
602
|
-
globalThis.clearTimeout(handle);
|
|
603
|
-
}
|
|
604
|
-
};
|
|
605
|
-
/**
|
|
606
|
-
* Create a readonly throttled view of a reactive source.
|
|
607
|
-
*
|
|
608
|
-
* Changes are observed with `flush: "sync"`, so every synchronous write
|
|
609
|
-
* counts. Outside a cooldown window, a change applies immediately when
|
|
610
|
-
* {@link UseThrottledOptions.leading} is enabled (otherwise it waits as a
|
|
611
|
-
* trailing update) and opens a window of `waitMs` milliseconds. Changes
|
|
612
|
-
* inside a window are collected as the trailing candidate; when the window
|
|
613
|
-
* ends with a candidate waiting, the source value current at that moment is
|
|
614
|
-
* applied and the next window opens back to back, keeping applications
|
|
615
|
-
* spaced by `waitMs`. A window that ends without a candidate closes silently.
|
|
616
|
-
* `waitMs` is reactive and is read each time a window opens; changing it
|
|
617
|
-
* never disturbs an already-open window. A wait of `0` still defers trailing
|
|
618
|
-
* updates to the next scheduler tick.
|
|
619
|
-
*
|
|
620
|
-
* Server rendering is explicit: without a browser `window` (and with
|
|
621
|
-
* {@link UseThrottledOptions.runOnServer} disabled) no timer ever starts and
|
|
622
|
-
* the view mirrors the source synchronously, so server-rendered output shows
|
|
623
|
-
* current values and nothing leaks. `pending` stays `false` and the controls
|
|
624
|
-
* report `false` in that mode.
|
|
625
|
-
*
|
|
626
|
-
* Cleanup rule: the watcher and any open window timer are released when the
|
|
627
|
-
* owning reactive scope stops; call inside an active scope. Outside one, the
|
|
628
|
-
* watcher lives as long as the source and `cancel()` only clears the window.
|
|
629
|
-
*
|
|
630
|
-
* @example
|
|
631
|
-
* ```ts
|
|
632
|
-
* const scrollY = shallowRef(0);
|
|
633
|
-
* const { throttled } = useThrottled(scrollY, 100);
|
|
634
|
-
* scrollY.value = 40; // applied immediately (leading edge)
|
|
635
|
-
* scrollY.value = 80; // applied when the 100ms window ends
|
|
636
|
-
* ```
|
|
637
|
-
*
|
|
638
|
-
* @param source Reactive source to throttle.
|
|
639
|
-
* @param waitMs Reactive cooldown in milliseconds; must be finite and at
|
|
640
|
-
* least zero. Fractions are truncated.
|
|
641
|
-
* @param options Edge policy and runtime scheduling overrides.
|
|
642
|
-
* @default options {}
|
|
643
|
-
* @throws `RangeError` tagged `VIZE_COMPOSE_THROTTLE_INVALID_WAIT` when the
|
|
644
|
-
* resolved wait is not finite or is negative, both synchronously at creation
|
|
645
|
-
* (even in mirror mode) and again each time a window opens.
|
|
646
|
-
* @throws `TypeError` tagged `VIZE_COMPOSE_THROTTLE_INVALID_EDGES` when both
|
|
647
|
-
* `leading` and `trailing` are disabled, because updates could then never
|
|
648
|
-
* propagate.
|
|
649
|
-
* @returns Readonly throttled view, pending flag, and cancel/flush controls.
|
|
650
|
-
*/
|
|
651
|
-
function useThrottled(source, waitMs, options = {}) {
|
|
652
|
-
const leading = options.leading ?? true;
|
|
653
|
-
const trailing = options.trailing ?? true;
|
|
654
|
-
if (!leading && !trailing) throw new TypeError("[VIZE_COMPOSE_THROTTLE_INVALID_EDGES] at least one of leading or trailing must be enabled");
|
|
655
|
-
const scheduler = options.scheduler ?? defaultScheduler;
|
|
656
|
-
const throttled = shallowRef(toValue(source));
|
|
657
|
-
const pending = shallowRef(false);
|
|
658
|
-
let windowHandle;
|
|
659
|
-
let windowOpen = false;
|
|
660
|
-
resolveWaitMs(toValue(waitMs));
|
|
661
|
-
const openWindow = () => {
|
|
662
|
-
windowOpen = true;
|
|
663
|
-
windowHandle = scheduler.setTimeout(() => {
|
|
664
|
-
windowHandle = void 0;
|
|
665
|
-
if (!pending.value) {
|
|
666
|
-
windowOpen = false;
|
|
667
|
-
return;
|
|
668
|
-
}
|
|
669
|
-
pending.value = false;
|
|
670
|
-
throttled.value = toValue(source);
|
|
671
|
-
openWindow();
|
|
672
|
-
}, resolveWaitMs(toValue(waitMs)));
|
|
673
|
-
};
|
|
674
|
-
const closeWindow = () => {
|
|
675
|
-
if (windowOpen) scheduler.clearTimeout(windowHandle);
|
|
676
|
-
windowHandle = void 0;
|
|
677
|
-
windowOpen = false;
|
|
678
|
-
pending.value = false;
|
|
679
|
-
};
|
|
680
|
-
const cancel = () => {
|
|
681
|
-
const hadTrailing = pending.value;
|
|
682
|
-
closeWindow();
|
|
683
|
-
return hadTrailing;
|
|
684
|
-
};
|
|
685
|
-
const flush = () => {
|
|
686
|
-
if (!pending.value) return false;
|
|
687
|
-
closeWindow();
|
|
688
|
-
throttled.value = toValue(source);
|
|
689
|
-
return true;
|
|
690
|
-
};
|
|
691
|
-
watch(() => toValue(source), (next) => {
|
|
692
|
-
if (typeof window === "undefined" && !(options.runOnServer ?? false)) {
|
|
693
|
-
throttled.value = next;
|
|
694
|
-
return;
|
|
695
|
-
}
|
|
696
|
-
if (windowOpen) {
|
|
697
|
-
if (trailing) pending.value = true;
|
|
698
|
-
return;
|
|
699
|
-
}
|
|
700
|
-
if (leading) throttled.value = next;
|
|
701
|
-
else pending.value = true;
|
|
702
|
-
openWindow();
|
|
703
|
-
}, { flush: "sync" });
|
|
704
|
-
tryOnScopeDispose(() => {
|
|
705
|
-
cancel();
|
|
706
|
-
});
|
|
707
|
-
return {
|
|
708
|
-
throttled,
|
|
709
|
-
pending,
|
|
710
|
-
cancel,
|
|
711
|
-
flush
|
|
712
|
-
};
|
|
713
|
-
}
|
|
714
|
-
function resolveWaitMs(value) {
|
|
715
|
-
if (!Number.isFinite(value) || value < 0) throw new RangeError(`[VIZE_COMPOSE_THROTTLE_INVALID_WAIT] waitMs must be finite and at least zero; received ${String(value)}`);
|
|
716
|
-
return Math.trunc(value);
|
|
717
|
-
}
|
|
718
|
-
//#endregion
|
|
719
|
-
//#region src/use-toggle.ts
|
|
720
|
-
/**
|
|
721
|
-
* Create owned boolean state with an inverting control.
|
|
722
|
-
*
|
|
723
|
-
* Purely synchronous state: safe during server rendering (no browser
|
|
724
|
-
* globals, no timers) and nothing to dispose, so it works inside and
|
|
725
|
-
* outside reactive scopes alike.
|
|
726
|
-
*
|
|
727
|
-
* @example
|
|
728
|
-
* ```ts
|
|
729
|
-
* const { state: open, toggle } = useToggle();
|
|
730
|
-
* toggle(); // true
|
|
731
|
-
* toggle(false); // false
|
|
732
|
-
* open.value; // false
|
|
733
|
-
* ```
|
|
734
|
-
*
|
|
735
|
-
* @param initial State before the first toggle.
|
|
736
|
-
* @default initial false
|
|
737
|
-
* @returns The writable state and its toggle control.
|
|
738
|
-
*/
|
|
739
|
-
function useToggle(initial = false) {
|
|
740
|
-
const state = shallowRef(initial);
|
|
741
|
-
const toggle = (force) => {
|
|
742
|
-
state.value = force ?? !state.value;
|
|
743
|
-
return state.value;
|
|
744
|
-
};
|
|
745
|
-
return {
|
|
746
|
-
state,
|
|
747
|
-
toggle
|
|
748
|
-
};
|
|
749
|
-
}
|
|
750
|
-
//#endregion
|
|
751
|
-
export { tryOnScopeDispose, useAsyncResource, useCounter, useDebounced, useEventListener, useHistory, useLocale, useMediaQuery, usePrevious, useReducedMotion, useThrottled, useToggle };
|
|
1
|
+
import { anyAbortSignal, deadlineAbortSignal, timeoutAbortSignal } from "./abort-signal.mjs";
|
|
2
|
+
import { tryOnScopeDispose } from "./scope.mjs";
|
|
3
|
+
import { useAsyncResource } from "./async-resource.mjs";
|
|
4
|
+
import { availableCapability, isCapabilityAvailable, isCapabilityUnavailable, unavailableCapability } from "./capability.mjs";
|
|
5
|
+
import { t as COMPOSABLE_CATALOG } from "./catalog-DOXTbk4r.mjs";
|
|
6
|
+
import { DISPOSAL_ERROR_CODE, DisposalError, createDisposalScope } from "./disposal-scope.mjs";
|
|
7
|
+
import { useEventListener } from "./event-listener.mjs";
|
|
8
|
+
import { useLocale } from "./locale.mjs";
|
|
9
|
+
import { useMediaQuery, useReducedMotion } from "./media-query.mjs";
|
|
10
|
+
import { calculateRetryDelay } from "./retry-delay.mjs";
|
|
11
|
+
import { retryAsync } from "./retry-async.mjs";
|
|
12
|
+
import "./timeout-scheduler.mjs";
|
|
13
|
+
import { useCounter } from "./use-counter.mjs";
|
|
14
|
+
import { useDebounced } from "./use-debounced.mjs";
|
|
15
|
+
import { useHistory } from "./use-history.mjs";
|
|
16
|
+
import { usePrevious } from "./use-previous.mjs";
|
|
17
|
+
import { useThrottled } from "./use-throttled.mjs";
|
|
18
|
+
import { useToggle } from "./use-toggle.mjs";
|
|
19
|
+
export { COMPOSABLE_CATALOG, DISPOSAL_ERROR_CODE, DisposalError, anyAbortSignal, availableCapability, calculateRetryDelay, createDisposalScope, deadlineAbortSignal, isCapabilityAvailable, isCapabilityUnavailable, retryAsync, timeoutAbortSignal, tryOnScopeDispose, unavailableCapability, useAsyncResource, useCounter, useDebounced, useEventListener, useHistory, useLocale, useMediaQuery, usePrevious, useReducedMotion, useThrottled, useToggle };
|