@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
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { ComputedRef, ShallowRef } from "vue";
|
|
2
|
+
|
|
3
|
+
//#region src/use-counter.d.ts
|
|
4
|
+
/** Options for {@link useCounter}. */
|
|
5
|
+
interface UseCounterOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Inclusive lower bound applied to every value the counter takes.
|
|
8
|
+
*
|
|
9
|
+
* @default Number.NEGATIVE_INFINITY
|
|
10
|
+
*/
|
|
11
|
+
readonly min?: number;
|
|
12
|
+
/**
|
|
13
|
+
* Inclusive upper bound applied to every value the counter takes.
|
|
14
|
+
*
|
|
15
|
+
* @default Number.POSITIVE_INFINITY
|
|
16
|
+
*/
|
|
17
|
+
readonly max?: number;
|
|
18
|
+
}
|
|
19
|
+
/** Reactive controls returned by {@link useCounter}. */
|
|
20
|
+
interface CounterControls {
|
|
21
|
+
/** Current count. Changes only through the controls, never by assignment. */
|
|
22
|
+
readonly count: Readonly<ShallowRef<number>>;
|
|
23
|
+
/** Whether the count currently sits on the configured lower bound. */
|
|
24
|
+
readonly atMin: ComputedRef<boolean>;
|
|
25
|
+
/** Whether the count currently sits on the configured upper bound. */
|
|
26
|
+
readonly atMax: ComputedRef<boolean>;
|
|
27
|
+
/**
|
|
28
|
+
* Add `delta` (default `1`) to the count and clamp into the bounds.
|
|
29
|
+
*
|
|
30
|
+
* @param delta Amount added; may be negative or infinite.
|
|
31
|
+
* @returns The count after clamping.
|
|
32
|
+
*/
|
|
33
|
+
readonly increment: (delta?: number) => number;
|
|
34
|
+
/**
|
|
35
|
+
* Subtract `delta` (default `1`) from the count and clamp into the bounds.
|
|
36
|
+
*
|
|
37
|
+
* @param delta Amount subtracted; may be negative or infinite.
|
|
38
|
+
* @returns The count after clamping.
|
|
39
|
+
*/
|
|
40
|
+
readonly decrement: (delta?: number) => number;
|
|
41
|
+
/**
|
|
42
|
+
* Assign a value directly, clamped into the bounds.
|
|
43
|
+
*
|
|
44
|
+
* @returns The count after clamping.
|
|
45
|
+
*/
|
|
46
|
+
readonly set: (value: number) => number;
|
|
47
|
+
/**
|
|
48
|
+
* Restore the reset baseline, or establish a new one.
|
|
49
|
+
*
|
|
50
|
+
* Without an argument the count returns to the creation-time initial value
|
|
51
|
+
* (after its original clamping). With an argument, the clamped value
|
|
52
|
+
* becomes both the new count and the baseline used by later `reset()`
|
|
53
|
+
* calls.
|
|
54
|
+
*
|
|
55
|
+
* @param value Replacement baseline.
|
|
56
|
+
* @returns The count after clamping.
|
|
57
|
+
*/
|
|
58
|
+
readonly reset: (value?: number) => number;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Create a clamped counter whose every transition stays inside `[min, max]`.
|
|
62
|
+
*
|
|
63
|
+
* All operations clamp instead of failing, including the initial value, so
|
|
64
|
+
* the count is inside the bounds at every observable moment. Only `NaN` is
|
|
65
|
+
* rejected — silently corrupting the count is never an option. Purely
|
|
66
|
+
* synchronous state: safe during server rendering (no browser globals, no
|
|
67
|
+
* timers) and nothing to dispose, so it works inside and outside reactive
|
|
68
|
+
* scopes alike. Bounds are fixed at creation and not reactive.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* const { count, increment, atMax } = useCounter(9, { min: 0, max: 10 });
|
|
73
|
+
* increment(); // 10
|
|
74
|
+
* increment(); // 10 (clamped)
|
|
75
|
+
* atMax.value; // true
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* @param initial Count before any operation, clamped into the bounds.
|
|
79
|
+
* @default initial 0
|
|
80
|
+
* @param options Inclusive bounds for every value the counter takes.
|
|
81
|
+
* @default options {}
|
|
82
|
+
* @throws `RangeError` tagged `VIZE_COMPOSE_COUNTER_INVALID_RANGE` when a
|
|
83
|
+
* bound is `NaN` or `min` exceeds `max`.
|
|
84
|
+
* @throws `RangeError` tagged `VIZE_COMPOSE_COUNTER_INVALID_VALUE` when an
|
|
85
|
+
* initial value, operand, or arithmetic result is `NaN` (for example
|
|
86
|
+
* incrementing `-Infinity` by `Infinity`); the count is left unchanged.
|
|
87
|
+
* @returns Reactive count, bound flags, and mutation controls.
|
|
88
|
+
*/
|
|
89
|
+
declare function useCounter(initial?: number, options?: UseCounterOptions): CounterControls;
|
|
90
|
+
//#endregion
|
|
91
|
+
export { CounterControls, UseCounterOptions, useCounter };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { computed, shallowRef } from "vue";
|
|
2
|
+
//#region src/use-counter.ts
|
|
3
|
+
/**
|
|
4
|
+
* Create a clamped counter whose every transition stays inside `[min, max]`.
|
|
5
|
+
*
|
|
6
|
+
* All operations clamp instead of failing, including the initial value, so
|
|
7
|
+
* the count is inside the bounds at every observable moment. Only `NaN` is
|
|
8
|
+
* rejected — silently corrupting the count is never an option. Purely
|
|
9
|
+
* synchronous state: safe during server rendering (no browser globals, no
|
|
10
|
+
* timers) and nothing to dispose, so it works inside and outside reactive
|
|
11
|
+
* scopes alike. Bounds are fixed at creation and not reactive.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const { count, increment, atMax } = useCounter(9, { min: 0, max: 10 });
|
|
16
|
+
* increment(); // 10
|
|
17
|
+
* increment(); // 10 (clamped)
|
|
18
|
+
* atMax.value; // true
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @param initial Count before any operation, clamped into the bounds.
|
|
22
|
+
* @default initial 0
|
|
23
|
+
* @param options Inclusive bounds for every value the counter takes.
|
|
24
|
+
* @default options {}
|
|
25
|
+
* @throws `RangeError` tagged `VIZE_COMPOSE_COUNTER_INVALID_RANGE` when a
|
|
26
|
+
* bound is `NaN` or `min` exceeds `max`.
|
|
27
|
+
* @throws `RangeError` tagged `VIZE_COMPOSE_COUNTER_INVALID_VALUE` when an
|
|
28
|
+
* initial value, operand, or arithmetic result is `NaN` (for example
|
|
29
|
+
* incrementing `-Infinity` by `Infinity`); the count is left unchanged.
|
|
30
|
+
* @returns Reactive count, bound flags, and mutation controls.
|
|
31
|
+
*/
|
|
32
|
+
function useCounter(initial = 0, options = {}) {
|
|
33
|
+
const min = requireBound(options.min ?? Number.NEGATIVE_INFINITY, "min");
|
|
34
|
+
const max = requireBound(options.max ?? Number.POSITIVE_INFINITY, "max");
|
|
35
|
+
if (min > max) throw new RangeError(`[VIZE_COMPOSE_COUNTER_INVALID_RANGE] min must not exceed max; received min ${String(min)} and max ${String(max)}`);
|
|
36
|
+
const clamp = (value) => Math.min(max, Math.max(min, value));
|
|
37
|
+
const count = shallowRef(clamp(requireValue(initial)));
|
|
38
|
+
let baseline = count.value;
|
|
39
|
+
const setClamped = (next) => {
|
|
40
|
+
count.value = clamp(requireValue(next));
|
|
41
|
+
return count.value;
|
|
42
|
+
};
|
|
43
|
+
const reset = (value) => {
|
|
44
|
+
const applied = setClamped(value ?? baseline);
|
|
45
|
+
if (value !== void 0) baseline = applied;
|
|
46
|
+
return applied;
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
count,
|
|
50
|
+
atMin: computed(() => count.value === min),
|
|
51
|
+
atMax: computed(() => count.value === max),
|
|
52
|
+
increment: (delta = 1) => setClamped(count.value + requireValue(delta)),
|
|
53
|
+
decrement: (delta = 1) => setClamped(count.value - requireValue(delta)),
|
|
54
|
+
set: setClamped,
|
|
55
|
+
reset
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function requireBound(value, label) {
|
|
59
|
+
if (Number.isNaN(value)) throw new RangeError(`[VIZE_COMPOSE_COUNTER_INVALID_RANGE] ${label} must not be NaN`);
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
function requireValue(value) {
|
|
63
|
+
if (Number.isNaN(value)) throw new RangeError("[VIZE_COMPOSE_COUNTER_INVALID_VALUE] the value is NaN; counter state was left unchanged");
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
export { useCounter };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { TimeoutScheduler } from "./timeout-scheduler.mjs";
|
|
2
|
+
import { MaybeRefOrGetter, ShallowRef } from "vue";
|
|
3
|
+
|
|
4
|
+
//#region src/use-debounced.d.ts
|
|
5
|
+
/** Options for {@link useDebounced}. */
|
|
6
|
+
interface UseDebouncedOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Applies the timing policy when no browser `window` is available.
|
|
9
|
+
*
|
|
10
|
+
* Keep this disabled during server rendering, where the debounced view
|
|
11
|
+
* mirrors the source synchronously instead of starting timers. Enable it
|
|
12
|
+
* for native, desktop, worker, and test runtimes whose scheduler is
|
|
13
|
+
* lifecycle-bound.
|
|
14
|
+
*
|
|
15
|
+
* @default false
|
|
16
|
+
*/
|
|
17
|
+
readonly runOnServer?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Owns the single-shot timer.
|
|
20
|
+
*
|
|
21
|
+
* @default globalThis timer functions
|
|
22
|
+
*/
|
|
23
|
+
readonly scheduler?: TimeoutScheduler;
|
|
24
|
+
}
|
|
25
|
+
/** Reactive debounced view and controls returned by {@link useDebounced}. */
|
|
26
|
+
interface DebouncedControls<Value> {
|
|
27
|
+
/** Readonly view of the source that settles `waitMs` after the last change. */
|
|
28
|
+
readonly debounced: Readonly<ShallowRef<Value>>;
|
|
29
|
+
/** Whether a trailing update is currently scheduled. */
|
|
30
|
+
readonly pending: Readonly<ShallowRef<boolean>>;
|
|
31
|
+
/**
|
|
32
|
+
* Discard the scheduled trailing update and keep the last settled value.
|
|
33
|
+
* Later source changes debounce again as usual.
|
|
34
|
+
*
|
|
35
|
+
* @returns Whether a scheduled update was discarded.
|
|
36
|
+
*/
|
|
37
|
+
readonly cancel: () => boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Apply the current source value immediately instead of waiting out the
|
|
40
|
+
* delay.
|
|
41
|
+
*
|
|
42
|
+
* @returns Whether a scheduled update was applied.
|
|
43
|
+
*/
|
|
44
|
+
readonly flush: () => boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Create a readonly debounced view of a reactive source.
|
|
48
|
+
*
|
|
49
|
+
* The view starts at the current source value. Each source change (observed
|
|
50
|
+
* with `flush: "sync"`, so every synchronous write counts) restarts a
|
|
51
|
+
* single-shot timer of `waitMs` milliseconds; when it fires, the view takes
|
|
52
|
+
* the source value current at that moment. `waitMs` is reactive and is read
|
|
53
|
+
* when a timer is scheduled; changing it does not restart an already-pending
|
|
54
|
+
* timer. A wait of `0` still defers to the next scheduler tick.
|
|
55
|
+
*
|
|
56
|
+
* Server rendering is explicit: without a browser `window` (and with
|
|
57
|
+
* {@link UseDebouncedOptions.runOnServer} disabled) no timer ever starts and
|
|
58
|
+
* the view mirrors the source synchronously, so server-rendered output shows
|
|
59
|
+
* current values and nothing leaks. `pending` stays `false` and the controls
|
|
60
|
+
* report `false` in that mode.
|
|
61
|
+
*
|
|
62
|
+
* Cleanup rule: the watcher and any pending timer are released when the
|
|
63
|
+
* owning reactive scope stops; call inside an active scope. Outside one, the
|
|
64
|
+
* watcher lives as long as the source and `cancel()` only clears the pending
|
|
65
|
+
* timer.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* const query = shallowRef("");
|
|
70
|
+
* const { debounced, flush } = useDebounced(query, 300);
|
|
71
|
+
* query.value = "vize"; // debounced.value still "" for 300ms
|
|
72
|
+
* flush(); // debounced.value === "vize" immediately
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* @param source Reactive source to debounce.
|
|
76
|
+
* @param waitMs Reactive delay in milliseconds; must be finite and at least
|
|
77
|
+
* zero. Fractions are truncated.
|
|
78
|
+
* @param options Runtime scheduling overrides.
|
|
79
|
+
* @default options {}
|
|
80
|
+
* @throws `RangeError` tagged `VIZE_COMPOSE_DEBOUNCE_INVALID_WAIT` when the
|
|
81
|
+
* resolved wait is not finite or is negative, both synchronously at creation
|
|
82
|
+
* (even in mirror mode) and again for every scheduled delay.
|
|
83
|
+
* @returns Readonly debounced view, pending flag, and cancel/flush controls.
|
|
84
|
+
*/
|
|
85
|
+
declare function useDebounced<Value>(source: MaybeRefOrGetter<Value>, waitMs: MaybeRefOrGetter<number>, options?: UseDebouncedOptions): DebouncedControls<Value>;
|
|
86
|
+
//#endregion
|
|
87
|
+
export { DebouncedControls, UseDebouncedOptions, useDebounced };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { tryOnScopeDispose } from "./scope.mjs";
|
|
2
|
+
import { shallowRef, toValue, watch } from "vue";
|
|
3
|
+
//#region src/use-debounced.ts
|
|
4
|
+
const defaultScheduler = {
|
|
5
|
+
setTimeout: (callback, delayMs) => globalThis.setTimeout(callback, delayMs),
|
|
6
|
+
clearTimeout: (handle) => {
|
|
7
|
+
globalThis.clearTimeout(handle);
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Create a readonly debounced view of a reactive source.
|
|
12
|
+
*
|
|
13
|
+
* The view starts at the current source value. Each source change (observed
|
|
14
|
+
* with `flush: "sync"`, so every synchronous write counts) restarts a
|
|
15
|
+
* single-shot timer of `waitMs` milliseconds; when it fires, the view takes
|
|
16
|
+
* the source value current at that moment. `waitMs` is reactive and is read
|
|
17
|
+
* when a timer is scheduled; changing it does not restart an already-pending
|
|
18
|
+
* timer. A wait of `0` still defers to the next scheduler tick.
|
|
19
|
+
*
|
|
20
|
+
* Server rendering is explicit: without a browser `window` (and with
|
|
21
|
+
* {@link UseDebouncedOptions.runOnServer} disabled) no timer ever starts and
|
|
22
|
+
* the view mirrors the source synchronously, so server-rendered output shows
|
|
23
|
+
* current values and nothing leaks. `pending` stays `false` and the controls
|
|
24
|
+
* report `false` in that mode.
|
|
25
|
+
*
|
|
26
|
+
* Cleanup rule: the watcher and any pending timer are released when the
|
|
27
|
+
* owning reactive scope stops; call inside an active scope. Outside one, the
|
|
28
|
+
* watcher lives as long as the source and `cancel()` only clears the pending
|
|
29
|
+
* timer.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* const query = shallowRef("");
|
|
34
|
+
* const { debounced, flush } = useDebounced(query, 300);
|
|
35
|
+
* query.value = "vize"; // debounced.value still "" for 300ms
|
|
36
|
+
* flush(); // debounced.value === "vize" immediately
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @param source Reactive source to debounce.
|
|
40
|
+
* @param waitMs Reactive delay in milliseconds; must be finite and at least
|
|
41
|
+
* zero. Fractions are truncated.
|
|
42
|
+
* @param options Runtime scheduling overrides.
|
|
43
|
+
* @default options {}
|
|
44
|
+
* @throws `RangeError` tagged `VIZE_COMPOSE_DEBOUNCE_INVALID_WAIT` when the
|
|
45
|
+
* resolved wait is not finite or is negative, both synchronously at creation
|
|
46
|
+
* (even in mirror mode) and again for every scheduled delay.
|
|
47
|
+
* @returns Readonly debounced view, pending flag, and cancel/flush controls.
|
|
48
|
+
*/
|
|
49
|
+
function useDebounced(source, waitMs, options = {}) {
|
|
50
|
+
const scheduler = options.scheduler ?? defaultScheduler;
|
|
51
|
+
const debounced = shallowRef(toValue(source));
|
|
52
|
+
const pending = shallowRef(false);
|
|
53
|
+
let handle;
|
|
54
|
+
resolveWaitMs(toValue(waitMs));
|
|
55
|
+
const apply = () => {
|
|
56
|
+
handle = void 0;
|
|
57
|
+
pending.value = false;
|
|
58
|
+
debounced.value = toValue(source);
|
|
59
|
+
};
|
|
60
|
+
const cancel = () => {
|
|
61
|
+
if (!pending.value) return false;
|
|
62
|
+
scheduler.clearTimeout(handle);
|
|
63
|
+
handle = void 0;
|
|
64
|
+
pending.value = false;
|
|
65
|
+
return true;
|
|
66
|
+
};
|
|
67
|
+
const flush = () => {
|
|
68
|
+
if (!pending.value) return false;
|
|
69
|
+
scheduler.clearTimeout(handle);
|
|
70
|
+
apply();
|
|
71
|
+
return true;
|
|
72
|
+
};
|
|
73
|
+
watch(() => toValue(source), (next) => {
|
|
74
|
+
if (typeof window === "undefined" && !(options.runOnServer ?? false)) {
|
|
75
|
+
debounced.value = next;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const delayMs = resolveWaitMs(toValue(waitMs));
|
|
79
|
+
if (pending.value) scheduler.clearTimeout(handle);
|
|
80
|
+
pending.value = true;
|
|
81
|
+
handle = scheduler.setTimeout(apply, delayMs);
|
|
82
|
+
}, { flush: "sync" });
|
|
83
|
+
tryOnScopeDispose(() => {
|
|
84
|
+
cancel();
|
|
85
|
+
});
|
|
86
|
+
return {
|
|
87
|
+
debounced,
|
|
88
|
+
pending,
|
|
89
|
+
cancel,
|
|
90
|
+
flush
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function resolveWaitMs(value) {
|
|
94
|
+
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)}`);
|
|
95
|
+
return Math.trunc(value);
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
export { useDebounced };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { ComputedRef, Ref } from "vue";
|
|
2
|
+
|
|
3
|
+
//#region src/use-history.d.ts
|
|
4
|
+
/** Options for {@link useHistory}. */
|
|
5
|
+
interface UseHistoryOptions<Value> {
|
|
6
|
+
/**
|
|
7
|
+
* Maximum number of undo entries retained; recording a change beyond it
|
|
8
|
+
* drops the oldest entry. The redo stack is bounded by construction, since
|
|
9
|
+
* redo entries only ever come from undone changes. Must be an integer
|
|
10
|
+
* greater than zero and is fixed at creation.
|
|
11
|
+
*
|
|
12
|
+
* @default 100
|
|
13
|
+
*/
|
|
14
|
+
readonly capacity?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Clone applied to every value captured into history and to every value
|
|
17
|
+
* restored out of it, isolating snapshots from later in-place mutation.
|
|
18
|
+
*
|
|
19
|
+
* @default identity — values are stored and restored by reference
|
|
20
|
+
*/
|
|
21
|
+
readonly clone?: (value: Value) => Value;
|
|
22
|
+
}
|
|
23
|
+
/** Reactive undo/redo controls returned by {@link useHistory}. */
|
|
24
|
+
interface HistoryControls {
|
|
25
|
+
/** Whether {@link HistoryControls.undo} currently has an entry to restore. */
|
|
26
|
+
readonly canUndo: ComputedRef<boolean>;
|
|
27
|
+
/** Whether {@link HistoryControls.redo} currently has an entry to restore. */
|
|
28
|
+
readonly canRedo: ComputedRef<boolean>;
|
|
29
|
+
/** Number of retained undo entries. */
|
|
30
|
+
readonly undoCount: ComputedRef<number>;
|
|
31
|
+
/** Number of retained redo entries. */
|
|
32
|
+
readonly redoCount: ComputedRef<number>;
|
|
33
|
+
/**
|
|
34
|
+
* Restore the newest undo entry and move the current value onto the redo
|
|
35
|
+
* stack. The restoring write is not recorded.
|
|
36
|
+
*
|
|
37
|
+
* @returns Whether an entry was restored.
|
|
38
|
+
*/
|
|
39
|
+
readonly undo: () => boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Restore the newest redo entry and move the current value back onto the
|
|
42
|
+
* undo stack. The restoring write is not recorded.
|
|
43
|
+
*
|
|
44
|
+
* @returns Whether an entry was restored.
|
|
45
|
+
*/
|
|
46
|
+
readonly redo: () => boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Group every source write inside `run` into at most one undo entry.
|
|
49
|
+
*
|
|
50
|
+
* The entry restores the value from just before the batch. It is committed
|
|
51
|
+
* only when the final value differs (`Object.is`) from the starting value,
|
|
52
|
+
* and it is committed even when `run` throws, so a partially applied batch
|
|
53
|
+
* stays undoable as one step. Nested calls collapse into the outermost
|
|
54
|
+
* batch. The callback's return value is passed through.
|
|
55
|
+
*/
|
|
56
|
+
readonly batch: <Result>(run: () => Result) => Result;
|
|
57
|
+
/** Drop every undo and redo entry while keeping the current value. */
|
|
58
|
+
readonly clear: () => void;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Record bounded undo/redo history over the writes of a ref.
|
|
62
|
+
*
|
|
63
|
+
* Recording is shallow and identity-based, matching Vue's own change
|
|
64
|
+
* detection: assignments to `source.value` are recorded (observed with
|
|
65
|
+
* `flush: "sync"`, so every synchronous write counts), writes that are
|
|
66
|
+
* `Object.is`-equal to the current value are not changes, and in-place
|
|
67
|
+
* mutations of object values are invisible — pair mutable values with
|
|
68
|
+
* {@link UseHistoryOptions.clone} and reassign. Undoing and redoing restore
|
|
69
|
+
* values through `clone` as well, so snapshots never share identity with the
|
|
70
|
+
* live value unless the default identity clone is kept. When a
|
|
71
|
+
* user-provided `clone` throws, the failed operation leaves history
|
|
72
|
+
* unchanged and the error propagates.
|
|
73
|
+
*
|
|
74
|
+
* Safe during server rendering: no browser globals are read and no timers
|
|
75
|
+
* start. Cleanup rule: when the owning reactive scope stops, recording stops
|
|
76
|
+
* and every retained snapshot is released, so `undo`/`redo` return `false`
|
|
77
|
+
* afterwards; call inside an active scope, or the watcher lives as long as
|
|
78
|
+
* the source.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```ts
|
|
82
|
+
* const text = shallowRef("");
|
|
83
|
+
* const { undo, redo, batch } = useHistory(text);
|
|
84
|
+
* text.value = "a";
|
|
85
|
+
* batch(() => {
|
|
86
|
+
* text.value = "ab";
|
|
87
|
+
* text.value = "abc";
|
|
88
|
+
* });
|
|
89
|
+
* undo(); // text.value === "a" (the batch is one step)
|
|
90
|
+
* redo(); // text.value === "abc"
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
* @param source Ref whose writes are recorded.
|
|
94
|
+
* @param options Retention bound and snapshot cloning.
|
|
95
|
+
* @default options {}
|
|
96
|
+
* @throws `RangeError` tagged `VIZE_COMPOSE_HISTORY_INVALID_CAPACITY` when
|
|
97
|
+
* the capacity is not an integer greater than zero.
|
|
98
|
+
* @throws `Error` tagged `VIZE_COMPOSE_HISTORY_IN_BATCH` when `undo`,
|
|
99
|
+
* `redo`, or `clear` is called inside {@link HistoryControls.batch}, where
|
|
100
|
+
* stack movement would corrupt the pending group.
|
|
101
|
+
* @returns Reactive undo/redo state and controls.
|
|
102
|
+
*/
|
|
103
|
+
declare function useHistory<Value>(source: Ref<Value>, options?: UseHistoryOptions<Value>): HistoryControls;
|
|
104
|
+
//#endregion
|
|
105
|
+
export { HistoryControls, UseHistoryOptions, useHistory };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { tryOnScopeDispose } from "./scope.mjs";
|
|
2
|
+
import { computed, shallowRef, watch } from "vue";
|
|
3
|
+
//#region src/use-history.ts
|
|
4
|
+
/**
|
|
5
|
+
* Record bounded undo/redo history over the writes of a ref.
|
|
6
|
+
*
|
|
7
|
+
* Recording is shallow and identity-based, matching Vue's own change
|
|
8
|
+
* detection: assignments to `source.value` are recorded (observed with
|
|
9
|
+
* `flush: "sync"`, so every synchronous write counts), writes that are
|
|
10
|
+
* `Object.is`-equal to the current value are not changes, and in-place
|
|
11
|
+
* mutations of object values are invisible — pair mutable values with
|
|
12
|
+
* {@link UseHistoryOptions.clone} and reassign. Undoing and redoing restore
|
|
13
|
+
* values through `clone` as well, so snapshots never share identity with the
|
|
14
|
+
* live value unless the default identity clone is kept. When a
|
|
15
|
+
* user-provided `clone` throws, the failed operation leaves history
|
|
16
|
+
* unchanged and the error propagates.
|
|
17
|
+
*
|
|
18
|
+
* Safe during server rendering: no browser globals are read and no timers
|
|
19
|
+
* start. Cleanup rule: when the owning reactive scope stops, recording stops
|
|
20
|
+
* and every retained snapshot is released, so `undo`/`redo` return `false`
|
|
21
|
+
* afterwards; call inside an active scope, or the watcher lives as long as
|
|
22
|
+
* the source.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const text = shallowRef("");
|
|
27
|
+
* const { undo, redo, batch } = useHistory(text);
|
|
28
|
+
* text.value = "a";
|
|
29
|
+
* batch(() => {
|
|
30
|
+
* text.value = "ab";
|
|
31
|
+
* text.value = "abc";
|
|
32
|
+
* });
|
|
33
|
+
* undo(); // text.value === "a" (the batch is one step)
|
|
34
|
+
* redo(); // text.value === "abc"
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @param source Ref whose writes are recorded.
|
|
38
|
+
* @param options Retention bound and snapshot cloning.
|
|
39
|
+
* @default options {}
|
|
40
|
+
* @throws `RangeError` tagged `VIZE_COMPOSE_HISTORY_INVALID_CAPACITY` when
|
|
41
|
+
* the capacity is not an integer greater than zero.
|
|
42
|
+
* @throws `Error` tagged `VIZE_COMPOSE_HISTORY_IN_BATCH` when `undo`,
|
|
43
|
+
* `redo`, or `clear` is called inside {@link HistoryControls.batch}, where
|
|
44
|
+
* stack movement would corrupt the pending group.
|
|
45
|
+
* @returns Reactive undo/redo state and controls.
|
|
46
|
+
*/
|
|
47
|
+
function useHistory(source, options = {}) {
|
|
48
|
+
const capacity = options.capacity ?? 100;
|
|
49
|
+
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)}`);
|
|
50
|
+
const clone = options.clone ?? ((value) => value);
|
|
51
|
+
const undoStack = shallowRef([]);
|
|
52
|
+
const redoStack = shallowRef([]);
|
|
53
|
+
let restoring = false;
|
|
54
|
+
let batchDepth = 0;
|
|
55
|
+
let activeBatch;
|
|
56
|
+
const pushUndo = (entry) => {
|
|
57
|
+
const next = [...undoStack.value, entry];
|
|
58
|
+
undoStack.value = next.length > capacity ? next.slice(next.length - capacity) : next;
|
|
59
|
+
redoStack.value = [];
|
|
60
|
+
};
|
|
61
|
+
const writeSilently = (value) => {
|
|
62
|
+
restoring = true;
|
|
63
|
+
try {
|
|
64
|
+
source.value = value;
|
|
65
|
+
} finally {
|
|
66
|
+
restoring = false;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const requireOutsideBatch = (operation) => {
|
|
70
|
+
if (batchDepth > 0) throw new Error(`[VIZE_COMPOSE_HISTORY_IN_BATCH] ${operation}() is not available inside batch()`);
|
|
71
|
+
};
|
|
72
|
+
const undo = () => {
|
|
73
|
+
requireOutsideBatch("undo");
|
|
74
|
+
const entry = undoStack.value.at(-1);
|
|
75
|
+
if (entry === void 0) return false;
|
|
76
|
+
const restored = clone(entry.value);
|
|
77
|
+
const recorded = { value: clone(source.value) };
|
|
78
|
+
undoStack.value = undoStack.value.slice(0, -1);
|
|
79
|
+
redoStack.value = [...redoStack.value, recorded];
|
|
80
|
+
writeSilently(restored);
|
|
81
|
+
return true;
|
|
82
|
+
};
|
|
83
|
+
const redo = () => {
|
|
84
|
+
requireOutsideBatch("redo");
|
|
85
|
+
const entry = redoStack.value.at(-1);
|
|
86
|
+
if (entry === void 0) return false;
|
|
87
|
+
const restored = clone(entry.value);
|
|
88
|
+
const recorded = { value: clone(source.value) };
|
|
89
|
+
redoStack.value = redoStack.value.slice(0, -1);
|
|
90
|
+
undoStack.value = [...undoStack.value, recorded];
|
|
91
|
+
writeSilently(restored);
|
|
92
|
+
return true;
|
|
93
|
+
};
|
|
94
|
+
const batch = (run) => {
|
|
95
|
+
if (batchDepth === 0) activeBatch = {
|
|
96
|
+
raw: source.value,
|
|
97
|
+
entry: { value: clone(source.value) }
|
|
98
|
+
};
|
|
99
|
+
batchDepth += 1;
|
|
100
|
+
try {
|
|
101
|
+
return run();
|
|
102
|
+
} finally {
|
|
103
|
+
batchDepth -= 1;
|
|
104
|
+
if (batchDepth === 0 && activeBatch !== void 0) {
|
|
105
|
+
const finished = activeBatch;
|
|
106
|
+
activeBatch = void 0;
|
|
107
|
+
if (!Object.is(finished.raw, source.value)) pushUndo(finished.entry);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const clear = () => {
|
|
112
|
+
requireOutsideBatch("clear");
|
|
113
|
+
undoStack.value = [];
|
|
114
|
+
redoStack.value = [];
|
|
115
|
+
};
|
|
116
|
+
const handle = watch(source, (_next, replaced) => {
|
|
117
|
+
if (restoring || batchDepth > 0) return;
|
|
118
|
+
pushUndo({ value: clone(replaced) });
|
|
119
|
+
}, { flush: "sync" });
|
|
120
|
+
tryOnScopeDispose(() => {
|
|
121
|
+
handle.stop();
|
|
122
|
+
undoStack.value = [];
|
|
123
|
+
redoStack.value = [];
|
|
124
|
+
});
|
|
125
|
+
return {
|
|
126
|
+
canUndo: computed(() => undoStack.value.length > 0),
|
|
127
|
+
canRedo: computed(() => redoStack.value.length > 0),
|
|
128
|
+
undoCount: computed(() => undoStack.value.length),
|
|
129
|
+
redoCount: computed(() => redoStack.value.length),
|
|
130
|
+
undo,
|
|
131
|
+
redo,
|
|
132
|
+
batch,
|
|
133
|
+
clear
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
export { useHistory };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { MaybeRefOrGetter, ShallowRef } from "vue";
|
|
2
|
+
|
|
3
|
+
//#region src/use-previous.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Track the value a reactive source held before its latest change.
|
|
6
|
+
*
|
|
7
|
+
* The semantics are precise and arity-based:
|
|
8
|
+
*
|
|
9
|
+
* - Without an `initial` argument the ref holds `undefined` until the source
|
|
10
|
+
* changes for the first time.
|
|
11
|
+
* - With an `initial` argument (including an explicit `undefined` when
|
|
12
|
+
* `Value` allows it) the ref holds that value until the first change, and
|
|
13
|
+
* the return type never widens with `undefined`.
|
|
14
|
+
* - Every synchronous write is observed (`flush: "sync"`), so a sequence of
|
|
15
|
+
* writes in one tick shifts the previous value step by step instead of
|
|
16
|
+
* collapsing into one batch.
|
|
17
|
+
* - Writes whose value is `Object.is`-equal to the current value do not
|
|
18
|
+
* count as changes, matching Vue's own change detection.
|
|
19
|
+
* - Tracking is shallow: reassignments are observed, in-place mutations of
|
|
20
|
+
* object values are not.
|
|
21
|
+
*
|
|
22
|
+
* Safe during server rendering: no browser globals are read and no timers
|
|
23
|
+
* start. The underlying watcher is bound to the current reactive scope and
|
|
24
|
+
* stops with it; call inside an active scope, or the watcher lives as long
|
|
25
|
+
* as the source. A plain non-reactive source never changes, so the ref stays
|
|
26
|
+
* at its initial value.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* const route = shallowRef("/home");
|
|
31
|
+
* const previousRoute = usePrevious(route, "/");
|
|
32
|
+
* route.value = "/settings";
|
|
33
|
+
* previousRoute.value; // "/home"
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @param source Reactive source to observe.
|
|
37
|
+
* @param initial Value reported before the first change.
|
|
38
|
+
* @returns Readonly shallow ref holding the previous source value.
|
|
39
|
+
*/
|
|
40
|
+
declare function usePrevious<Value>(source: MaybeRefOrGetter<Value>): Readonly<ShallowRef<Value | undefined>>;
|
|
41
|
+
declare function usePrevious<Value>(source: MaybeRefOrGetter<Value>, initial: Value): Readonly<ShallowRef<Value>>;
|
|
42
|
+
//#endregion
|
|
43
|
+
export { usePrevious };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { shallowRef, toValue, watch } from "vue";
|
|
2
|
+
//#region src/use-previous.ts
|
|
3
|
+
function usePrevious(source, ...initial) {
|
|
4
|
+
const previous = shallowRef(initial.length === 1 ? initial[0] : void 0);
|
|
5
|
+
watch(() => toValue(source), (_next, replaced) => {
|
|
6
|
+
previous.value = replaced;
|
|
7
|
+
}, { flush: "sync" });
|
|
8
|
+
return previous;
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
export { usePrevious };
|