@reause/shared 0.1.2
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/LICENSE +21 -0
- package/dist/index.d.ts +2713 -0
- package/dist/index.iife.js +3381 -0
- package/dist/index.iife.min.js +11 -0
- package/dist/index.js +3091 -0
- package/package.json +27 -0
|
@@ -0,0 +1,3381 @@
|
|
|
1
|
+
(function(exports, react) {
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
//#region \0rolldown/runtime.js
|
|
4
|
+
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region createEventHook/index.tsx
|
|
7
|
+
/**
|
|
8
|
+
* Utility for creating event hooks
|
|
9
|
+
*
|
|
10
|
+
* @see https://vueuse.org/createEventHook
|
|
11
|
+
*
|
|
12
|
+
* @__NO_SIDE_EFFECTS__
|
|
13
|
+
*/
|
|
14
|
+
function createEventHook() {
|
|
15
|
+
const fns = /* @__PURE__ */ new Set();
|
|
16
|
+
const off = (fn) => {
|
|
17
|
+
fns.delete(fn);
|
|
18
|
+
};
|
|
19
|
+
const clear = () => {
|
|
20
|
+
fns.clear();
|
|
21
|
+
};
|
|
22
|
+
const on = (fn) => {
|
|
23
|
+
fns.add(fn);
|
|
24
|
+
const offFn = () => off(fn);
|
|
25
|
+
return { off: offFn };
|
|
26
|
+
};
|
|
27
|
+
const trigger = (...args) => {
|
|
28
|
+
return Promise.all(Array.from(fns).map((fn) => fn(...args)));
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
on,
|
|
32
|
+
off,
|
|
33
|
+
trigger,
|
|
34
|
+
clear
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region createGlobalState/index.tsx
|
|
39
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
40
|
+
function createGlobalState(initialState) {
|
|
41
|
+
let state = typeof initialState === "function" ? initialState() : initialState;
|
|
42
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
43
|
+
const subscribe = (listener) => {
|
|
44
|
+
listeners.add(listener);
|
|
45
|
+
return () => {
|
|
46
|
+
listeners.delete(listener);
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
const getSnapshot = () => state;
|
|
50
|
+
const setState = (update) => {
|
|
51
|
+
state = typeof update === "function" ? update(state) : update;
|
|
52
|
+
for (const listener of listeners) listener();
|
|
53
|
+
};
|
|
54
|
+
return function useGlobalState() {
|
|
55
|
+
const snapshot = (0, react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
|
|
56
|
+
return (0, react.useMemo)(() => [snapshot, setState], [snapshot, setState]);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region ../../node_modules/.pnpm/react@19.2.8/node_modules/react/cjs/react-jsx-runtime.development.js
|
|
61
|
+
/**
|
|
62
|
+
* @license React
|
|
63
|
+
* react-jsx-runtime.development.js
|
|
64
|
+
*
|
|
65
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
66
|
+
*
|
|
67
|
+
* This source code is licensed under the MIT license found in the
|
|
68
|
+
* LICENSE file in the root directory of this source tree.
|
|
69
|
+
*/
|
|
70
|
+
var require_react_jsx_runtime_development = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
71
|
+
(function() {
|
|
72
|
+
function getComponentNameFromType(type) {
|
|
73
|
+
if (null == type) return null;
|
|
74
|
+
if ("function" === typeof type) return type.$$typeof === REACT_CLIENT_REFERENCE ? null : type.displayName || type.name || null;
|
|
75
|
+
if ("string" === typeof type) return type;
|
|
76
|
+
switch (type) {
|
|
77
|
+
case REACT_FRAGMENT_TYPE: return "Fragment";
|
|
78
|
+
case REACT_PROFILER_TYPE: return "Profiler";
|
|
79
|
+
case REACT_STRICT_MODE_TYPE: return "StrictMode";
|
|
80
|
+
case REACT_SUSPENSE_TYPE: return "Suspense";
|
|
81
|
+
case REACT_SUSPENSE_LIST_TYPE: return "SuspenseList";
|
|
82
|
+
case REACT_ACTIVITY_TYPE: return "Activity";
|
|
83
|
+
}
|
|
84
|
+
if ("object" === typeof type) switch ("number" === typeof type.tag && console.error("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."), type.$$typeof) {
|
|
85
|
+
case REACT_PORTAL_TYPE: return "Portal";
|
|
86
|
+
case REACT_CONTEXT_TYPE: return type.displayName || "Context";
|
|
87
|
+
case REACT_CONSUMER_TYPE: return (type._context.displayName || "Context") + ".Consumer";
|
|
88
|
+
case REACT_FORWARD_REF_TYPE:
|
|
89
|
+
var innerType = type.render;
|
|
90
|
+
type = type.displayName;
|
|
91
|
+
type || (type = innerType.displayName || innerType.name || "", type = "" !== type ? "ForwardRef(" + type + ")" : "ForwardRef");
|
|
92
|
+
return type;
|
|
93
|
+
case REACT_MEMO_TYPE: return innerType = type.displayName || null, null !== innerType ? innerType : getComponentNameFromType(type.type) || "Memo";
|
|
94
|
+
case REACT_LAZY_TYPE:
|
|
95
|
+
innerType = type._payload;
|
|
96
|
+
type = type._init;
|
|
97
|
+
try {
|
|
98
|
+
return getComponentNameFromType(type(innerType));
|
|
99
|
+
} catch (x) {}
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
function testStringCoercion(value) {
|
|
104
|
+
return "" + value;
|
|
105
|
+
}
|
|
106
|
+
function checkKeyStringCoercion(value) {
|
|
107
|
+
try {
|
|
108
|
+
testStringCoercion(value);
|
|
109
|
+
var JSCompiler_inline_result = !1;
|
|
110
|
+
} catch (e) {
|
|
111
|
+
JSCompiler_inline_result = !0;
|
|
112
|
+
}
|
|
113
|
+
if (JSCompiler_inline_result) {
|
|
114
|
+
JSCompiler_inline_result = console;
|
|
115
|
+
var JSCompiler_temp_const = JSCompiler_inline_result.error;
|
|
116
|
+
var JSCompiler_inline_result$jscomp$0 = "function" === typeof Symbol && Symbol.toStringTag && value[Symbol.toStringTag] || value.constructor.name || "Object";
|
|
117
|
+
JSCompiler_temp_const.call(JSCompiler_inline_result, "The provided key is an unsupported type %s. This value must be coerced to a string before using it here.", JSCompiler_inline_result$jscomp$0);
|
|
118
|
+
return testStringCoercion(value);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function getTaskName(type) {
|
|
122
|
+
if (type === REACT_FRAGMENT_TYPE) return "<>";
|
|
123
|
+
if ("object" === typeof type && null !== type && type.$$typeof === REACT_LAZY_TYPE) return "<...>";
|
|
124
|
+
try {
|
|
125
|
+
var name = getComponentNameFromType(type);
|
|
126
|
+
return name ? "<" + name + ">" : "<...>";
|
|
127
|
+
} catch (x) {
|
|
128
|
+
return "<...>";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function getOwner() {
|
|
132
|
+
var dispatcher = ReactSharedInternals.A;
|
|
133
|
+
return null === dispatcher ? null : dispatcher.getOwner();
|
|
134
|
+
}
|
|
135
|
+
function UnknownOwner() {
|
|
136
|
+
return Error("react-stack-top-frame");
|
|
137
|
+
}
|
|
138
|
+
function hasValidKey(config) {
|
|
139
|
+
if (hasOwnProperty.call(config, "key")) {
|
|
140
|
+
var getter = Object.getOwnPropertyDescriptor(config, "key").get;
|
|
141
|
+
if (getter && getter.isReactWarning) return !1;
|
|
142
|
+
}
|
|
143
|
+
return void 0 !== config.key;
|
|
144
|
+
}
|
|
145
|
+
function defineKeyPropWarningGetter(props, displayName) {
|
|
146
|
+
function warnAboutAccessingKey() {
|
|
147
|
+
specialPropKeyWarningShown || (specialPropKeyWarningShown = !0, console.error("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props)", displayName));
|
|
148
|
+
}
|
|
149
|
+
warnAboutAccessingKey.isReactWarning = !0;
|
|
150
|
+
Object.defineProperty(props, "key", {
|
|
151
|
+
get: warnAboutAccessingKey,
|
|
152
|
+
configurable: !0
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
function elementRefGetterWithDeprecationWarning() {
|
|
156
|
+
var componentName = getComponentNameFromType(this.type);
|
|
157
|
+
didWarnAboutElementRef[componentName] || (didWarnAboutElementRef[componentName] = !0, console.error("Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release."));
|
|
158
|
+
componentName = this.props.ref;
|
|
159
|
+
return void 0 !== componentName ? componentName : null;
|
|
160
|
+
}
|
|
161
|
+
function ReactElement(type, key, props, owner, debugStack, debugTask) {
|
|
162
|
+
var refProp = props.ref;
|
|
163
|
+
type = {
|
|
164
|
+
$$typeof: REACT_ELEMENT_TYPE,
|
|
165
|
+
type,
|
|
166
|
+
key,
|
|
167
|
+
props,
|
|
168
|
+
_owner: owner
|
|
169
|
+
};
|
|
170
|
+
null !== (void 0 !== refProp ? refProp : null) ? Object.defineProperty(type, "ref", {
|
|
171
|
+
enumerable: !1,
|
|
172
|
+
get: elementRefGetterWithDeprecationWarning
|
|
173
|
+
}) : Object.defineProperty(type, "ref", {
|
|
174
|
+
enumerable: !1,
|
|
175
|
+
value: null
|
|
176
|
+
});
|
|
177
|
+
type._store = {};
|
|
178
|
+
Object.defineProperty(type._store, "validated", {
|
|
179
|
+
configurable: !1,
|
|
180
|
+
enumerable: !1,
|
|
181
|
+
writable: !0,
|
|
182
|
+
value: 0
|
|
183
|
+
});
|
|
184
|
+
Object.defineProperty(type, "_debugInfo", {
|
|
185
|
+
configurable: !1,
|
|
186
|
+
enumerable: !1,
|
|
187
|
+
writable: !0,
|
|
188
|
+
value: null
|
|
189
|
+
});
|
|
190
|
+
Object.defineProperty(type, "_debugStack", {
|
|
191
|
+
configurable: !1,
|
|
192
|
+
enumerable: !1,
|
|
193
|
+
writable: !0,
|
|
194
|
+
value: debugStack
|
|
195
|
+
});
|
|
196
|
+
Object.defineProperty(type, "_debugTask", {
|
|
197
|
+
configurable: !1,
|
|
198
|
+
enumerable: !1,
|
|
199
|
+
writable: !0,
|
|
200
|
+
value: debugTask
|
|
201
|
+
});
|
|
202
|
+
Object.freeze && (Object.freeze(type.props), Object.freeze(type));
|
|
203
|
+
return type;
|
|
204
|
+
}
|
|
205
|
+
function jsxDEVImpl(type, config, maybeKey, isStaticChildren, debugStack, debugTask) {
|
|
206
|
+
var children = config.children;
|
|
207
|
+
if (void 0 !== children) if (isStaticChildren) if (isArrayImpl(children)) {
|
|
208
|
+
for (isStaticChildren = 0; isStaticChildren < children.length; isStaticChildren++) validateChildKeys(children[isStaticChildren]);
|
|
209
|
+
Object.freeze && Object.freeze(children);
|
|
210
|
+
} else console.error("React.jsx: Static children should always be an array. You are likely explicitly calling React.jsxs or React.jsxDEV. Use the Babel transform instead.");
|
|
211
|
+
else validateChildKeys(children);
|
|
212
|
+
if (hasOwnProperty.call(config, "key")) {
|
|
213
|
+
children = getComponentNameFromType(type);
|
|
214
|
+
var keys = Object.keys(config).filter(function(k) {
|
|
215
|
+
return "key" !== k;
|
|
216
|
+
});
|
|
217
|
+
isStaticChildren = 0 < keys.length ? "{key: someKey, " + keys.join(": ..., ") + ": ...}" : "{key: someKey}";
|
|
218
|
+
didWarnAboutKeySpread[children + isStaticChildren] || (keys = 0 < keys.length ? "{" + keys.join(": ..., ") + ": ...}" : "{}", console.error("A props object containing a \"key\" prop is being spread into JSX:\n let props = %s;\n <%s {...props} />\nReact keys must be passed directly to JSX without using spread:\n let props = %s;\n <%s key={someKey} {...props} />", isStaticChildren, children, keys, children), didWarnAboutKeySpread[children + isStaticChildren] = !0);
|
|
219
|
+
}
|
|
220
|
+
children = null;
|
|
221
|
+
void 0 !== maybeKey && (checkKeyStringCoercion(maybeKey), children = "" + maybeKey);
|
|
222
|
+
hasValidKey(config) && (checkKeyStringCoercion(config.key), children = "" + config.key);
|
|
223
|
+
if ("key" in config) {
|
|
224
|
+
maybeKey = {};
|
|
225
|
+
for (var propName in config) "key" !== propName && (maybeKey[propName] = config[propName]);
|
|
226
|
+
} else maybeKey = config;
|
|
227
|
+
children && defineKeyPropWarningGetter(maybeKey, "function" === typeof type ? type.displayName || type.name || "Unknown" : type);
|
|
228
|
+
return ReactElement(type, children, maybeKey, getOwner(), debugStack, debugTask);
|
|
229
|
+
}
|
|
230
|
+
function validateChildKeys(node) {
|
|
231
|
+
isValidElement(node) ? node._store && (node._store.validated = 1) : "object" === typeof node && null !== node && node.$$typeof === REACT_LAZY_TYPE && ("fulfilled" === node._payload.status ? isValidElement(node._payload.value) && node._payload.value._store && (node._payload.value._store.validated = 1) : node._store && (node._store.validated = 1));
|
|
232
|
+
}
|
|
233
|
+
function isValidElement(object) {
|
|
234
|
+
return "object" === typeof object && null !== object && object.$$typeof === REACT_ELEMENT_TYPE;
|
|
235
|
+
}
|
|
236
|
+
var React = require("react"), REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element"), REACT_PORTAL_TYPE = Symbol.for("react.portal"), REACT_FRAGMENT_TYPE = Symbol.for("react.fragment"), REACT_STRICT_MODE_TYPE = Symbol.for("react.strict_mode"), REACT_PROFILER_TYPE = Symbol.for("react.profiler"), REACT_CONSUMER_TYPE = Symbol.for("react.consumer"), REACT_CONTEXT_TYPE = Symbol.for("react.context"), REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref"), REACT_SUSPENSE_TYPE = Symbol.for("react.suspense"), REACT_SUSPENSE_LIST_TYPE = Symbol.for("react.suspense_list"), REACT_MEMO_TYPE = Symbol.for("react.memo"), REACT_LAZY_TYPE = Symbol.for("react.lazy"), REACT_ACTIVITY_TYPE = Symbol.for("react.activity"), REACT_CLIENT_REFERENCE = Symbol.for("react.client.reference"), ReactSharedInternals = React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, hasOwnProperty = Object.prototype.hasOwnProperty, isArrayImpl = Array.isArray, createTask = console.createTask ? console.createTask : function() {
|
|
237
|
+
return null;
|
|
238
|
+
};
|
|
239
|
+
React = { react_stack_bottom_frame: function(callStackForError) {
|
|
240
|
+
return callStackForError();
|
|
241
|
+
} };
|
|
242
|
+
var specialPropKeyWarningShown;
|
|
243
|
+
var didWarnAboutElementRef = {};
|
|
244
|
+
var unknownOwnerDebugStack = React.react_stack_bottom_frame.bind(React, UnknownOwner)();
|
|
245
|
+
var unknownOwnerDebugTask = createTask(getTaskName(UnknownOwner));
|
|
246
|
+
var didWarnAboutKeySpread = {};
|
|
247
|
+
exports.Fragment = REACT_FRAGMENT_TYPE;
|
|
248
|
+
exports.jsx = function(type, config, maybeKey) {
|
|
249
|
+
var trackActualOwner = 1e4 > ReactSharedInternals.recentlyCreatedOwnerStacks++;
|
|
250
|
+
return jsxDEVImpl(type, config, maybeKey, !1, trackActualOwner ? Error("react-stack-top-frame") : unknownOwnerDebugStack, trackActualOwner ? createTask(getTaskName(type)) : unknownOwnerDebugTask);
|
|
251
|
+
};
|
|
252
|
+
exports.jsxs = function(type, config, maybeKey) {
|
|
253
|
+
var trackActualOwner = 1e4 > ReactSharedInternals.recentlyCreatedOwnerStacks++;
|
|
254
|
+
return jsxDEVImpl(type, config, maybeKey, !0, trackActualOwner ? Error("react-stack-top-frame") : unknownOwnerDebugStack, trackActualOwner ? createTask(getTaskName(type)) : unknownOwnerDebugTask);
|
|
255
|
+
};
|
|
256
|
+
})();
|
|
257
|
+
}));
|
|
258
|
+
//#endregion
|
|
259
|
+
//#region createInjectionState/index.tsx
|
|
260
|
+
var import_jsx_runtime = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
261
|
+
module.exports = require_react_jsx_runtime_development();
|
|
262
|
+
})))();
|
|
263
|
+
function createInjectionState(composable, options) {
|
|
264
|
+
var _options$injectionKey;
|
|
265
|
+
const InjectionContext = (_options$injectionKey = options === null || options === void 0 ? void 0 : options.injectionKey) !== null && _options$injectionKey !== void 0 ? _options$injectionKey : (0, react.createContext)(options === null || options === void 0 ? void 0 : options.defaultValue);
|
|
266
|
+
function Provider(props) {
|
|
267
|
+
const { children, ...rest } = props;
|
|
268
|
+
const state = composable(rest);
|
|
269
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InjectionContext.Provider, {
|
|
270
|
+
value: state,
|
|
271
|
+
children
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
Provider.displayName = composable.name ? `${composable.name}Provider` : "InjectionStateProvider";
|
|
275
|
+
const useInjectedState = () => (0, react.useContext)(InjectionContext);
|
|
276
|
+
return [Provider, useInjectedState];
|
|
277
|
+
}
|
|
278
|
+
//#endregion
|
|
279
|
+
//#region createSharedHook/index.tsx
|
|
280
|
+
/**
|
|
281
|
+
* Make a composable function usable with multiple React components.
|
|
282
|
+
*
|
|
283
|
+
* Map from @vueuse/shared `createSharedComposable`
|
|
284
|
+
* Mapping: upstream runs the composable once inside a detached
|
|
285
|
+
* `effectScope(true)`, counts the subscribers and stops the scope when the
|
|
286
|
+
* last consumer leaves. React has no `effectScope`, so the same lifetime is
|
|
287
|
+
* expressed by an external store held in the closure of one
|
|
288
|
+
* `createSharedHook` call — the `state` snapshot, a `Set` of `listeners`, the
|
|
289
|
+
* `refCount` and the optional `cleanup` — which every consumer reads through
|
|
290
|
+
* `useSyncExternalStore`.
|
|
291
|
+
*
|
|
292
|
+
* The shared instance is created by the **first consumer to render** (the
|
|
293
|
+
* "creator"): it runs the wrapped hook on every one of its renders — the
|
|
294
|
+
* wrapped hook is therefore free to use React hooks internally — assigning
|
|
295
|
+
* the result to `state`, and `useLayoutEffect` publishes the latest value to
|
|
296
|
+
* every other consumer after commit. Every later consumer never calls the
|
|
297
|
+
* wrapped hook (both call patterns are stable per consumer, so the hook order
|
|
298
|
+
* never changes across renders); it just reads the published snapshot. The
|
|
299
|
+
* creator assigns `state` *before* `useSyncExternalStore` reads its snapshot
|
|
300
|
+
* in the same render, so every consumer — creator included — receives the
|
|
301
|
+
* shared value on its very first render, never `undefined`.
|
|
302
|
+
*
|
|
303
|
+
* The creator deliberately **does not register a store listener**: it already
|
|
304
|
+
* re-renders on its own state changes (the wrapped hook's setters belong to
|
|
305
|
+
* its component) and re-publishes afterwards, so a notification would only
|
|
306
|
+
* re-render it from its own publish. `useSyncExternalStore` requires the
|
|
307
|
+
* snapshot to stay reference-stable between real changes ("the result of
|
|
308
|
+
* getSnapshot should be cached"): every creator render assigns a fresh
|
|
309
|
+
* reference, so a subscribed creator would see "the store changed" forever and
|
|
310
|
+
* loop. The creator still counts toward `refCount`, so teardown timing is
|
|
311
|
+
* exact — it just never receives notifications.
|
|
312
|
+
*
|
|
313
|
+
* Deviations from upstream:
|
|
314
|
+
* - upstream runs the composable exactly once, with the first caller's
|
|
315
|
+
* arguments; here the creator re-runs the wrapped hook on every one of its
|
|
316
|
+
* renders (React hooks cannot be called outside a render), so while the
|
|
317
|
+
* creator stays mounted the shared value keeps tracking its latest render.
|
|
318
|
+
* - **frozen after the creator unmounts**: the wrapped hook's setters belong
|
|
319
|
+
* to the creator's component, so if the creator unmounts while other
|
|
320
|
+
* consumers remain mounted, the shared value stops updating — it freezes at
|
|
321
|
+
* the last published value. The instance itself lives on until the last
|
|
322
|
+
* consumer unmounts (upstream lifetime parity).
|
|
323
|
+
* - `getServerSnapshot` returns the same snapshot as the client: server
|
|
324
|
+
* rendering yields the uninitialized value, the client fills it in after
|
|
325
|
+
* hydration, and `useSyncExternalStore` handles the mismatch.
|
|
326
|
+
* - Teardown has no `tryOnScopeDispose` to hook into, so the optional
|
|
327
|
+
* `cleanup` argument is called — and the state dropped — when the last
|
|
328
|
+
* consumer unmounts; a later mount starts a fresh instance, exactly like
|
|
329
|
+
* upstream's `scope.stop()` followed by `state = undefined`.
|
|
330
|
+
*
|
|
331
|
+
* ```tsx
|
|
332
|
+
* const useSharedMouse = createSharedHook(useMouse)
|
|
333
|
+
*
|
|
334
|
+
* // CompA — const { x, y } = useSharedMouse()
|
|
335
|
+
* // CompB — const { x, y } = useSharedMouse() // same state, no new listeners
|
|
336
|
+
* ```
|
|
337
|
+
*
|
|
338
|
+
* @see https://vueuse.org/createSharedComposable
|
|
339
|
+
* @param hook The composable to share across every consumer of the returned
|
|
340
|
+
* hook. It runs on every render of the first consumer (the creator).
|
|
341
|
+
* @param cleanup Called when the last consumer unmounts, before the shared
|
|
342
|
+
* state is dropped — the place to undo whatever `hook` set up outside React.
|
|
343
|
+
*/
|
|
344
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
345
|
+
function createSharedHook(hook, cleanup) {
|
|
346
|
+
let state;
|
|
347
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
348
|
+
let refCount = 0;
|
|
349
|
+
let teardown = cleanup;
|
|
350
|
+
let creatorId;
|
|
351
|
+
const notify = () => {
|
|
352
|
+
for (const listener of listeners) listener();
|
|
353
|
+
};
|
|
354
|
+
const subscribe = (listener) => {
|
|
355
|
+
listeners.add(listener);
|
|
356
|
+
refCount += 1;
|
|
357
|
+
return () => {
|
|
358
|
+
listeners.delete(listener);
|
|
359
|
+
refCount -= 1;
|
|
360
|
+
if (refCount <= 0) {
|
|
361
|
+
state = void 0;
|
|
362
|
+
refCount = 0;
|
|
363
|
+
creatorId = void 0;
|
|
364
|
+
teardown === null || teardown === void 0 || teardown();
|
|
365
|
+
teardown = void 0;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
};
|
|
369
|
+
const creatorSubscribe = (_listener) => {
|
|
370
|
+
refCount += 1;
|
|
371
|
+
return () => {
|
|
372
|
+
refCount -= 1;
|
|
373
|
+
if (refCount <= 0) {
|
|
374
|
+
state = void 0;
|
|
375
|
+
refCount = 0;
|
|
376
|
+
creatorId = void 0;
|
|
377
|
+
teardown === null || teardown === void 0 || teardown();
|
|
378
|
+
teardown = void 0;
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
};
|
|
382
|
+
const getSnapshot = () => state;
|
|
383
|
+
const getServerSnapshot = getSnapshot;
|
|
384
|
+
return function useSharedHook(...args) {
|
|
385
|
+
const id = (0, react.useRef)(Symbol("createSharedHook")).current;
|
|
386
|
+
if (creatorId === void 0) creatorId = id;
|
|
387
|
+
const isCreator = creatorId === id;
|
|
388
|
+
if (isCreator) state = hook(...args);
|
|
389
|
+
(0, react.useLayoutEffect)(() => {
|
|
390
|
+
if (isCreator) notify();
|
|
391
|
+
});
|
|
392
|
+
return (0, react.useSyncExternalStore)(isCreator ? creatorSubscribe : subscribe, getSnapshot, getServerSnapshot);
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
//#endregion
|
|
396
|
+
//#region utils/index.tsx
|
|
397
|
+
function promiseTimeout(ms, throwOnTimeout = false, reason = "Timeout") {
|
|
398
|
+
return new Promise((resolve, reject) => {
|
|
399
|
+
if (throwOnTimeout) setTimeout(reject, ms, reason);
|
|
400
|
+
else setTimeout(resolve, ms);
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Create singleton promise function
|
|
405
|
+
*
|
|
406
|
+
* @example
|
|
407
|
+
* ```
|
|
408
|
+
* const promise = createSingletonPromise(async () => { ... })
|
|
409
|
+
*
|
|
410
|
+
* await promise()
|
|
411
|
+
* await promise() // all of them will be bind to a single promise instance
|
|
412
|
+
* await promise() // and be resolved together
|
|
413
|
+
* ```
|
|
414
|
+
*/
|
|
415
|
+
function createSingletonPromise(fn) {
|
|
416
|
+
let _promise;
|
|
417
|
+
function wrapper() {
|
|
418
|
+
if (!_promise) _promise = fn();
|
|
419
|
+
return _promise;
|
|
420
|
+
}
|
|
421
|
+
wrapper.reset = async () => {
|
|
422
|
+
const _prev = _promise;
|
|
423
|
+
_promise = void 0;
|
|
424
|
+
if (_prev) await _prev;
|
|
425
|
+
};
|
|
426
|
+
return wrapper;
|
|
427
|
+
}
|
|
428
|
+
function increaseWithUnit(target, delta) {
|
|
429
|
+
var _target$match;
|
|
430
|
+
if (typeof target === "number") return target + delta;
|
|
431
|
+
const value = ((_target$match = target.match(/^-?\d+\.?\d*/)) === null || _target$match === void 0 ? void 0 : _target$match[0]) || "";
|
|
432
|
+
const unit = target.slice(value.length);
|
|
433
|
+
const result = Number.parseFloat(value) + delta;
|
|
434
|
+
if (Number.isNaN(result)) return target;
|
|
435
|
+
return result + unit;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Get a px value for SSR use, do not rely on this method outside of SSR as REM
|
|
439
|
+
* unit is assumed at 16px, which might not be the case on the client
|
|
440
|
+
*
|
|
441
|
+
* @example pxValue('37rem') // 592
|
|
442
|
+
* @example pxValue('500px') // 500
|
|
443
|
+
*/
|
|
444
|
+
function pxValue(px) {
|
|
445
|
+
return px.endsWith("rem") ? Number.parseFloat(px) * 16 : Number.parseFloat(px);
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Create a new subset object by giving keys
|
|
449
|
+
*/
|
|
450
|
+
function objectPick(obj, keys, omitUndefined = false) {
|
|
451
|
+
return keys.reduce((n, k) => {
|
|
452
|
+
if (k in obj) {
|
|
453
|
+
if (!omitUndefined || obj[k] !== void 0) n[k] = obj[k];
|
|
454
|
+
}
|
|
455
|
+
return n;
|
|
456
|
+
}, {});
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Create a new subset object by omit giving keys
|
|
460
|
+
*/
|
|
461
|
+
function objectOmit(obj, keys, omitUndefined = false) {
|
|
462
|
+
return Object.fromEntries(Object.entries(obj).filter(([key, value]) => {
|
|
463
|
+
return (!omitUndefined || value !== void 0) && !keys.includes(key);
|
|
464
|
+
}));
|
|
465
|
+
}
|
|
466
|
+
function toArray(value) {
|
|
467
|
+
return Array.isArray(value) ? value : [value];
|
|
468
|
+
}
|
|
469
|
+
const isClient$1 = typeof window !== "undefined" && typeof document !== "undefined";
|
|
470
|
+
const isDef = (val) => typeof val !== "undefined";
|
|
471
|
+
const assert = (condition, ...infos) => {
|
|
472
|
+
if (!condition) console.warn(...infos);
|
|
473
|
+
};
|
|
474
|
+
const toString$1 = Object.prototype.toString;
|
|
475
|
+
const isObject = (val) => toString$1.call(val) === "[object Object]";
|
|
476
|
+
const now = () => Date.now();
|
|
477
|
+
const timestamp = () => +Date.now();
|
|
478
|
+
const clamp = (n, min, max) => Math.min(max, Math.max(min, n));
|
|
479
|
+
const rand = (min, max) => {
|
|
480
|
+
min = Math.ceil(min);
|
|
481
|
+
max = Math.floor(max);
|
|
482
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
483
|
+
};
|
|
484
|
+
const hasOwn = (val, key) => Object.hasOwn(val, key);
|
|
485
|
+
const isIOS = /* #__PURE__ */ getIsIOS();
|
|
486
|
+
function getIsIOS() {
|
|
487
|
+
var _window, _window2, _window3;
|
|
488
|
+
return isClient$1 && !!((_window = window) === null || _window === void 0 || (_window = _window.navigator) === null || _window === void 0 ? void 0 : _window.userAgent) && (/iP(?:ad|hone|od)/.test(window.navigator.userAgent) || ((_window2 = window) === null || _window2 === void 0 || (_window2 = _window2.navigator) === null || _window2 === void 0 ? void 0 : _window2.maxTouchPoints) > 2 && /iPad|Macintosh/.test((_window3 = window) === null || _window3 === void 0 ? void 0 : _window3.navigator.userAgent));
|
|
489
|
+
}
|
|
490
|
+
function cacheStringFunction(fn) {
|
|
491
|
+
const cache = Object.create(null);
|
|
492
|
+
return ((str) => {
|
|
493
|
+
return cache[str] || (cache[str] = fn(str));
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
const hyphenateRE = /\B([A-Z])/g;
|
|
497
|
+
const hyphenate = cacheStringFunction((str) => str.replace(hyphenateRE, "-$1").toLowerCase());
|
|
498
|
+
/**
|
|
499
|
+
* Type guard for React ref objects (`RefObject` — `{ current }` holders).
|
|
500
|
+
* Callback refs are functions and cannot be read synchronously, so they are
|
|
501
|
+
* not ref-like.
|
|
502
|
+
*/
|
|
503
|
+
function isRefLike(value) {
|
|
504
|
+
return value !== null && value !== void 0 && typeof value === "object" && "current" in value;
|
|
505
|
+
}
|
|
506
|
+
function toValue(value) {
|
|
507
|
+
if (Array.isArray(value) && value.length === 2 && typeof value[1] === "function") return value[0];
|
|
508
|
+
if (value !== null && value !== void 0 && typeof value === "object" && "value" in value && !("addEventListener" in value)) return value.value;
|
|
509
|
+
if (typeof value === "function") return value();
|
|
510
|
+
if (isRefLike(value)) return value.current;
|
|
511
|
+
return value;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Write a value back through a writable `State<T>` source — a ref-like
|
|
515
|
+
* `.current`, a `[value, setter]` tuple or a `{ value, onChange }` pair.
|
|
516
|
+
* Plain values and getters have no write path and are skipped. This is the
|
|
517
|
+
* write-side counterpart of `toValue`; hooks that push values into a
|
|
518
|
+
* `State<T>` import it from here rather than re-implementing the branches.
|
|
519
|
+
*/
|
|
520
|
+
function writeState(source, value) {
|
|
521
|
+
if (source === null || source === void 0) return;
|
|
522
|
+
if (Array.isArray(source) && source.length === 2 && typeof source[1] === "function") {
|
|
523
|
+
source[1](value);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
if (isRefLike(source)) {
|
|
527
|
+
source.current = value;
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (typeof source === "object" && !Array.isArray(source) && "value" in source && !("addEventListener" in source)) {
|
|
531
|
+
var _onChange;
|
|
532
|
+
(_onChange = source.onChange) === null || _onChange === void 0 || _onChange.call(source, value);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
//#endregion
|
|
536
|
+
//#region isDefined/index.tsx
|
|
537
|
+
function isDefined(v) {
|
|
538
|
+
return (isRefLike(v) ? v.current : v) != null;
|
|
539
|
+
}
|
|
540
|
+
//#endregion
|
|
541
|
+
//#region makeDestructurable/index.tsx
|
|
542
|
+
/**
|
|
543
|
+
* Make isomorphic destructurable for object and array at the same time —
|
|
544
|
+
* React port of VueUse's `makeDestructurable` (a pure utility function, so it
|
|
545
|
+
* maps 1:1 with no React adaptation). See this blog for the underlying idea:
|
|
546
|
+
* https://antfu.me/posts/destructuring-with-object-or-array/
|
|
547
|
+
*
|
|
548
|
+
* Map from @vueuse/shared `makeDestructurable`
|
|
549
|
+
* Upstream semantics are kept verbatim: given `(obj, arr)` the returned value
|
|
550
|
+
* can be destructured as an object (`const { foo, bar } = obj`) or as an array
|
|
551
|
+
* (`const [foo, bar] = obj`) — the array mode is backed by a non-enumerable
|
|
552
|
+
* `Symbol.iterator` defined on a shallow clone of `obj` (spread
|
|
553
|
+
* `{ ...obj }`); `Object.assign` appears only in the no-Symbol SSR fallback.
|
|
554
|
+
*
|
|
555
|
+
* @example
|
|
556
|
+
* const foo = { name: 'foo' }
|
|
557
|
+
* const bar = 1024
|
|
558
|
+
* const obj = makeDestructurable({ foo, bar } as const, [foo, bar] as const)
|
|
559
|
+
* const { foo: f1, bar: b1 } = obj // object destructuring
|
|
560
|
+
* const [f2, b2] = obj // array destructuring
|
|
561
|
+
*/
|
|
562
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
563
|
+
function makeDestructurable(obj, arr) {
|
|
564
|
+
if (typeof Symbol !== "undefined") {
|
|
565
|
+
const clone = { ...obj };
|
|
566
|
+
Object.defineProperty(clone, Symbol.iterator, {
|
|
567
|
+
enumerable: false,
|
|
568
|
+
value() {
|
|
569
|
+
let index = 0;
|
|
570
|
+
return { next: () => ({
|
|
571
|
+
value: arr[index++],
|
|
572
|
+
done: index > arr.length
|
|
573
|
+
}) };
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
return clone;
|
|
577
|
+
} else return Object.assign([...arr], obj);
|
|
578
|
+
}
|
|
579
|
+
//#endregion
|
|
580
|
+
//#region syncState/index.tsx
|
|
581
|
+
const neverObserved$1 = Symbol("reause.syncState.neverObserved");
|
|
582
|
+
function classifyWritable(source) {
|
|
583
|
+
if (source === null || source === void 0) return "readonly";
|
|
584
|
+
if (isRefLike(source)) return "sync";
|
|
585
|
+
if (Array.isArray(source) && source.length === 2 && typeof source[1] === "function") return "async";
|
|
586
|
+
if (typeof source === "object" && !Array.isArray(source) && "value" in source && !("addEventListener" in source)) return "async";
|
|
587
|
+
return "readonly";
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Two-way state synchronization — keeps two writable `State<T>` sources in
|
|
591
|
+
* sync, with optional direction and value transforms.
|
|
592
|
+
*
|
|
593
|
+
* Map from @vueuse/shared `syncRef`
|
|
594
|
+
* (`source/vueuse/packages/shared/syncRef/`), renamed `syncState` for the
|
|
595
|
+
* React port: the two sides are `State<T>` sources — a `[value, setter]`
|
|
596
|
+
* tuple, a `{ value, onChange }` pair, a ref-like `{ current }`, a getter or
|
|
597
|
+
* a plain value — instead of Vue refs. Each side is read with `toValue` and
|
|
598
|
+
* written back through its writable form (tuple setter / `onChange` /
|
|
599
|
+
* `.current`); plain values and getters have no write path, so that side is
|
|
600
|
+
* treated as read-only (the sync becomes one-way for it).
|
|
601
|
+
*
|
|
602
|
+
* React Hook adaptation: upstream drives both sides through Vue's reactive
|
|
603
|
+
* `watchPausable`, pausing all watchers while writing so a side never echoes
|
|
604
|
+
* its own write back. React has no reactive system, so `syncState` is
|
|
605
|
+
* implemented as a hook (call it unconditionally at the top of a component).
|
|
606
|
+
* A `useEffect` that runs after every commit compares each side's resolved
|
|
607
|
+
* value with the last observed one via `Object.is` and mirrors the changed
|
|
608
|
+
* side into the other — through the optional `transform` convertors when
|
|
609
|
+
* given — recording the value it just wrote as already observed on the
|
|
610
|
+
* receiving side (the React analogue of upstream's pause/resume). Ref-like
|
|
611
|
+
* `.current` writes are synchronous and need no absorption; writes through a
|
|
612
|
+
* setter / `onChange` are asynchronous, so until the target's value reflects
|
|
613
|
+
* the write the stale pre-write value is absorbed and never mistaken for an
|
|
614
|
+
* external change. Read-only sides (plain values / getters) are never marked
|
|
615
|
+
* as written, so a changing source keeps propagating. The initial sync
|
|
616
|
+
* (upstream default `immediate: true`) runs in the mount effect and cascades
|
|
617
|
+
* ltr before rtl,
|
|
618
|
+
* matching upstream's watcher creation order. Because the observation happens
|
|
619
|
+
* post-commit, an external mutation is only adopted on the render that
|
|
620
|
+
* follows it — the mutation itself never schedules a render, so a bare
|
|
621
|
+
* `.current` write outside of React is not observed (see the maintainer
|
|
622
|
+
* notes on reause #40 / #41). The returned `stop` function tears the
|
|
623
|
+
* synchronization down; the effect also stops doing any work once the owning
|
|
624
|
+
* component unmounts.
|
|
625
|
+
*
|
|
626
|
+
* @example
|
|
627
|
+
* const [a, setA] = useState('a')
|
|
628
|
+
* const [b, setB] = useState('b')
|
|
629
|
+
*
|
|
630
|
+
* const stop = syncState([a, setA], [b, setB])
|
|
631
|
+
*
|
|
632
|
+
* console.log(a) // a
|
|
633
|
+
*
|
|
634
|
+
* setB('foo') // then the component re-renders
|
|
635
|
+
* console.log(a) // foo
|
|
636
|
+
*
|
|
637
|
+
* setA('bar') // then the component re-renders
|
|
638
|
+
* console.log(b) // bar
|
|
639
|
+
*
|
|
640
|
+
* stop()
|
|
641
|
+
*/
|
|
642
|
+
function syncState(left, right, options = {}) {
|
|
643
|
+
var _transform$ltr, _transform$rtl;
|
|
644
|
+
const { immediate = true, direction = "both", transform = {} } = options;
|
|
645
|
+
const leftRef = (0, react.useRef)(left);
|
|
646
|
+
leftRef.current = left;
|
|
647
|
+
const rightRef = (0, react.useRef)(right);
|
|
648
|
+
rightRef.current = right;
|
|
649
|
+
const transformLTR = (_transform$ltr = transform.ltr) !== null && _transform$ltr !== void 0 ? _transform$ltr : ((v) => v);
|
|
650
|
+
const transformRTL = (_transform$rtl = transform.rtl) !== null && _transform$rtl !== void 0 ? _transform$rtl : ((v) => v);
|
|
651
|
+
const lastLeftRef = (0, react.useRef)(neverObserved$1);
|
|
652
|
+
const lastRightRef = (0, react.useRef)(neverObserved$1);
|
|
653
|
+
const pendingLeftRef = (0, react.useRef)(null);
|
|
654
|
+
const pendingRightRef = (0, react.useRef)(null);
|
|
655
|
+
const stoppedRef = (0, react.useRef)(false);
|
|
656
|
+
(0, react.useEffect)(() => {
|
|
657
|
+
if (stoppedRef.current) return;
|
|
658
|
+
const leftState = leftRef.current;
|
|
659
|
+
const rightState = rightRef.current;
|
|
660
|
+
const l = toValue(leftState);
|
|
661
|
+
const r = toValue(rightState);
|
|
662
|
+
const ltrActive = direction === "both" || direction === "ltr";
|
|
663
|
+
const rtlActive = direction === "both" || direction === "rtl";
|
|
664
|
+
if (lastLeftRef.current === neverObserved$1 && lastRightRef.current === neverObserved$1) {
|
|
665
|
+
let lastLeft = l;
|
|
666
|
+
let lastRight = r;
|
|
667
|
+
if (immediate) {
|
|
668
|
+
if (ltrActive) {
|
|
669
|
+
const newRight = transformLTR(l);
|
|
670
|
+
if (!Object.is(r, newRight)) {
|
|
671
|
+
const rightKind = classifyWritable(rightState);
|
|
672
|
+
if (rightKind !== "readonly") {
|
|
673
|
+
writeState(rightState, newRight);
|
|
674
|
+
lastRight = newRight;
|
|
675
|
+
if (rightKind === "async") pendingRightRef.current = {
|
|
676
|
+
before: r,
|
|
677
|
+
after: newRight
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
if (rtlActive) {
|
|
682
|
+
const newLeft = transformRTL(newRight);
|
|
683
|
+
if (!Object.is(l, newLeft)) {
|
|
684
|
+
const leftKind = classifyWritable(leftState);
|
|
685
|
+
if (leftKind !== "readonly") {
|
|
686
|
+
writeState(leftState, newLeft);
|
|
687
|
+
lastLeft = newLeft;
|
|
688
|
+
if (leftKind === "async") pendingLeftRef.current = {
|
|
689
|
+
before: l,
|
|
690
|
+
after: newLeft
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
} else {
|
|
696
|
+
const newLeft = transformRTL(r);
|
|
697
|
+
if (!Object.is(l, newLeft)) {
|
|
698
|
+
const leftKind = classifyWritable(leftState);
|
|
699
|
+
if (leftKind !== "readonly") {
|
|
700
|
+
writeState(leftState, newLeft);
|
|
701
|
+
lastLeft = newLeft;
|
|
702
|
+
if (leftKind === "async") pendingLeftRef.current = {
|
|
703
|
+
before: l,
|
|
704
|
+
after: newLeft
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
lastLeftRef.current = lastLeft;
|
|
711
|
+
lastRightRef.current = lastRight;
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
const leftPending = pendingLeftRef.current;
|
|
715
|
+
const rightPending = pendingRightRef.current;
|
|
716
|
+
const leftChanged = !Object.is(lastLeftRef.current, l) && !(leftPending && Object.is(leftPending.before, l));
|
|
717
|
+
const rightChanged = !Object.is(lastRightRef.current, r) && !(rightPending && Object.is(rightPending.before, r));
|
|
718
|
+
if (leftPending && !Object.is(leftPending.before, l)) pendingLeftRef.current = null;
|
|
719
|
+
if (rightPending && !Object.is(rightPending.before, r)) pendingRightRef.current = null;
|
|
720
|
+
if (ltrActive && leftChanged) {
|
|
721
|
+
lastLeftRef.current = l;
|
|
722
|
+
const converted = transformLTR(l);
|
|
723
|
+
if (!Object.is(r, converted)) {
|
|
724
|
+
const rightKind = classifyWritable(rightState);
|
|
725
|
+
if (rightKind !== "readonly") {
|
|
726
|
+
writeState(rightState, converted);
|
|
727
|
+
lastRightRef.current = converted;
|
|
728
|
+
if (rightKind === "async") pendingRightRef.current = {
|
|
729
|
+
before: r,
|
|
730
|
+
after: converted
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
if (rtlActive && rightChanged) {
|
|
736
|
+
lastRightRef.current = r;
|
|
737
|
+
const converted = transformRTL(r);
|
|
738
|
+
if (!Object.is(l, converted)) {
|
|
739
|
+
const leftKind = classifyWritable(leftState);
|
|
740
|
+
if (leftKind !== "readonly") {
|
|
741
|
+
writeState(leftState, converted);
|
|
742
|
+
lastLeftRef.current = converted;
|
|
743
|
+
if (leftKind === "async") pendingLeftRef.current = {
|
|
744
|
+
before: l,
|
|
745
|
+
after: converted
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
return () => {
|
|
752
|
+
stoppedRef.current = true;
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
//#endregion
|
|
756
|
+
//#region syncStates/index.tsx
|
|
757
|
+
const neverObserved = Symbol("reause.syncStates.neverObserved");
|
|
758
|
+
/**
|
|
759
|
+
* Keep target state(s) in sync with a source value — React port of VueUse's
|
|
760
|
+
* `syncRefs`.
|
|
761
|
+
*
|
|
762
|
+
* Map from @vueuse/shared `syncRefs`
|
|
763
|
+
* (`source/vueuse/packages/shared/syncRefs/`), renamed `syncStates` for the
|
|
764
|
+
* React port: the source is a `State<T>` — a plain value, getter, ref-like,
|
|
765
|
+
* `[value, setter]` tuple or `{ value, onChange }` pair (upstream:
|
|
766
|
+
* `WatchSource`) — resolved with `toValue`; the targets are writable
|
|
767
|
+
* `State<T>` sources written back through their writable form (tuple setter /
|
|
768
|
+
* `onChange` / `.current`); upstream's `flush` / `deep` / `immediate` options
|
|
769
|
+
* are kept for signature compatibility.
|
|
770
|
+
*
|
|
771
|
+
* React Hook adaptation: upstream syncs through Vue's reactive `watch`, and
|
|
772
|
+
* React has no reactive system — so `syncStates` is implemented as a hook
|
|
773
|
+
* (call it unconditionally at the top of a component). Internally a
|
|
774
|
+
* `useEffect` that runs after every commit compares the resolved source value
|
|
775
|
+
* with the last observed one via `Object.is`; a change is written through to
|
|
776
|
+
* all targets. Because the observation happens post-commit, the caller must
|
|
777
|
+
* re-render (e.g. `setState`) for a new source value to reach the targets —
|
|
778
|
+
* a bare mutation outside of React is never observed (see the maintainer
|
|
779
|
+
* notes on reause #40 / #41). The returned `stop` function tears the
|
|
780
|
+
* synchronization down; the effect also stops doing any work once the owning
|
|
781
|
+
* component unmounts.
|
|
782
|
+
*
|
|
783
|
+
* @example
|
|
784
|
+
* function Form() {
|
|
785
|
+
* const [source, setSource] = useState('hello')
|
|
786
|
+
* const [target, setTarget] = useState('target')
|
|
787
|
+
*
|
|
788
|
+
* const stop = syncStates(source, [target, setTarget])
|
|
789
|
+
*
|
|
790
|
+
* // during the first render `target` is still 'target' — the sync effect
|
|
791
|
+
* // runs after the commit, so the source reaches the target only once the
|
|
792
|
+
* // component has mounted (target === 'hello' afterwards).
|
|
793
|
+
* // Calling `setSource('foo')` re-renders and the effect then copies 'foo'
|
|
794
|
+
* // into the target state on the following commit.
|
|
795
|
+
*
|
|
796
|
+
* stop()
|
|
797
|
+
* }
|
|
798
|
+
*/
|
|
799
|
+
function syncStates(source, targets, options = {}) {
|
|
800
|
+
const { immediate = true } = options;
|
|
801
|
+
const sourceRef = (0, react.useRef)(source);
|
|
802
|
+
sourceRef.current = source;
|
|
803
|
+
const isTupleTarget = Array.isArray(targets) && targets.length === 2 && typeof targets[1] === "function";
|
|
804
|
+
const targetsArray = Array.isArray(targets) && !isTupleTarget ? targets : [targets];
|
|
805
|
+
const targetsRef = (0, react.useRef)(targetsArray);
|
|
806
|
+
targetsRef.current = targetsArray;
|
|
807
|
+
const lastValueRef = (0, react.useRef)(neverObserved);
|
|
808
|
+
const stoppedRef = (0, react.useRef)(false);
|
|
809
|
+
(0, react.useEffect)(() => {
|
|
810
|
+
if (stoppedRef.current) return;
|
|
811
|
+
const value = toValue(sourceRef.current);
|
|
812
|
+
const last = lastValueRef.current;
|
|
813
|
+
lastValueRef.current = value;
|
|
814
|
+
if (last === neverObserved) {
|
|
815
|
+
if (!immediate) return;
|
|
816
|
+
} else if (Object.is(last, value)) return;
|
|
817
|
+
targetsRef.current.forEach((target) => {
|
|
818
|
+
if (!Object.is(toValue(target), value)) writeState(target, value);
|
|
819
|
+
});
|
|
820
|
+
});
|
|
821
|
+
return (0, react.useCallback)(() => {
|
|
822
|
+
stoppedRef.current = true;
|
|
823
|
+
}, []);
|
|
824
|
+
}
|
|
825
|
+
//#endregion
|
|
826
|
+
//#region until/index.tsx
|
|
827
|
+
/**
|
|
828
|
+
* Polling interval (ms) used to resolve `until` promises. React has no
|
|
829
|
+
* reactive watch, so the port re-reads the source at this fixed interval —
|
|
830
|
+
* the same polling approach `useFetch` uses for its `refetch` watch.
|
|
831
|
+
*/
|
|
832
|
+
const UNTIL_POLL_INTERVAL = 50;
|
|
833
|
+
/**
|
|
834
|
+
* Minimal structural equality — `Object.is` for primitives (so `NaN` equals
|
|
835
|
+
* `NaN`), arrays compared by length and element, plain objects by own-key
|
|
836
|
+
* count and value. Used by `changedTimes` when `deep: true`.
|
|
837
|
+
*/
|
|
838
|
+
function deepEquals(a, b) {
|
|
839
|
+
if (Object.is(a, b)) return true;
|
|
840
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
841
|
+
const aIsArray = Array.isArray(a);
|
|
842
|
+
const bIsArray = Array.isArray(b);
|
|
843
|
+
if (aIsArray !== bIsArray) return false;
|
|
844
|
+
if (aIsArray && bIsArray) {
|
|
845
|
+
const arrA = a;
|
|
846
|
+
const arrB = b;
|
|
847
|
+
if (arrA.length !== arrB.length) return false;
|
|
848
|
+
return arrA.every((item, index) => deepEquals(item, arrB[index]));
|
|
849
|
+
}
|
|
850
|
+
const keysA = Object.keys(a);
|
|
851
|
+
const keysB = Object.keys(b);
|
|
852
|
+
if (keysA.length !== keysB.length) return false;
|
|
853
|
+
return keysA.every((key) => deepEquals(a[key], b[key]));
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Clone used to snapshot the source between polls when `changedTimes` runs
|
|
857
|
+
* with `deep: true` — the poller re-reads the same reference, so a reference
|
|
858
|
+
* copy could never see an in-place mutation.
|
|
859
|
+
*/
|
|
860
|
+
function cloneDeep(value) {
|
|
861
|
+
if (value === null || typeof value !== "object") return value;
|
|
862
|
+
if (Array.isArray(value)) return value.map((item) => cloneDeep(item));
|
|
863
|
+
const result = {};
|
|
864
|
+
for (const key of Object.keys(value)) result[key] = cloneDeep(value[key]);
|
|
865
|
+
return result;
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Resolve the accepted `until` source — a plain value or a zero-argument
|
|
869
|
+
* getter (the React-idiomatic live source; a `Ref` / `{ current }` object is
|
|
870
|
+
* not accepted — pass `() => ref.current`).
|
|
871
|
+
*
|
|
872
|
+
* NOTE: a source value that *is* a function is treated as a getter and
|
|
873
|
+
* invoked (the same ambiguity `toValue` has).
|
|
874
|
+
*/
|
|
875
|
+
function resolveSource(source) {
|
|
876
|
+
return typeof source === "function" ? source() : source;
|
|
877
|
+
}
|
|
878
|
+
function createUntil(r, isNot = false) {
|
|
879
|
+
function toMatch(condition, { timeout, throwOnTimeout } = {}) {
|
|
880
|
+
let stop = null;
|
|
881
|
+
const promises = [new Promise((resolve) => {
|
|
882
|
+
let settled = false;
|
|
883
|
+
const check = () => {
|
|
884
|
+
if (settled) return;
|
|
885
|
+
const value = resolveSource(r);
|
|
886
|
+
if (condition(value) !== isNot) {
|
|
887
|
+
settled = true;
|
|
888
|
+
stop === null || stop === void 0 || stop();
|
|
889
|
+
resolve(value);
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
check();
|
|
893
|
+
if (!settled) {
|
|
894
|
+
const timer = setInterval(check, UNTIL_POLL_INTERVAL);
|
|
895
|
+
stop = () => clearInterval(timer);
|
|
896
|
+
}
|
|
897
|
+
})];
|
|
898
|
+
if (timeout != null) promises.push(promiseTimeout(timeout, throwOnTimeout).then(() => resolveSource(r)).finally(() => stop === null || stop === void 0 ? void 0 : stop()));
|
|
899
|
+
return Promise.race(promises);
|
|
900
|
+
}
|
|
901
|
+
function toBe(value, options) {
|
|
902
|
+
return toMatch((v) => v === value, options);
|
|
903
|
+
}
|
|
904
|
+
function toBeTruthy(options) {
|
|
905
|
+
return toMatch((v) => Boolean(v), options);
|
|
906
|
+
}
|
|
907
|
+
function toBeNull(options) {
|
|
908
|
+
return toBe(null, options);
|
|
909
|
+
}
|
|
910
|
+
function toBeUndefined(options) {
|
|
911
|
+
return toBe(void 0, options);
|
|
912
|
+
}
|
|
913
|
+
function toBeNaN(options) {
|
|
914
|
+
return toMatch(Number.isNaN, options);
|
|
915
|
+
}
|
|
916
|
+
function toContains(value, options) {
|
|
917
|
+
return toMatch((v) => {
|
|
918
|
+
return Array.from(v).includes(value);
|
|
919
|
+
}, options);
|
|
920
|
+
}
|
|
921
|
+
function changed(options) {
|
|
922
|
+
return changedTimes(1, options);
|
|
923
|
+
}
|
|
924
|
+
function changedTimes(n = 1, options) {
|
|
925
|
+
var _options$deep;
|
|
926
|
+
let count = 0;
|
|
927
|
+
let hasBaseline = false;
|
|
928
|
+
let lastValue;
|
|
929
|
+
const deep = (_options$deep = options === null || options === void 0 ? void 0 : options.deep) !== null && _options$deep !== void 0 ? _options$deep : false;
|
|
930
|
+
const snapshot = (value) => deep ? cloneDeep(value) : value;
|
|
931
|
+
return toMatch((v) => {
|
|
932
|
+
if (!hasBaseline) {
|
|
933
|
+
hasBaseline = true;
|
|
934
|
+
lastValue = snapshot(v);
|
|
935
|
+
return count >= n;
|
|
936
|
+
}
|
|
937
|
+
if (deep ? !deepEquals(lastValue, v) : !Object.is(lastValue, v)) {
|
|
938
|
+
count += 1;
|
|
939
|
+
lastValue = snapshot(v);
|
|
940
|
+
}
|
|
941
|
+
return count >= n;
|
|
942
|
+
}, options);
|
|
943
|
+
}
|
|
944
|
+
if (Array.isArray(resolveSource(r))) return {
|
|
945
|
+
toMatch,
|
|
946
|
+
toContains,
|
|
947
|
+
changed,
|
|
948
|
+
changedTimes,
|
|
949
|
+
get not() {
|
|
950
|
+
return createUntil(r, !isNot);
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
else return {
|
|
954
|
+
toMatch,
|
|
955
|
+
toBe,
|
|
956
|
+
toBeTruthy,
|
|
957
|
+
toBeNull,
|
|
958
|
+
toBeNaN,
|
|
959
|
+
toBeUndefined,
|
|
960
|
+
changed,
|
|
961
|
+
changedTimes,
|
|
962
|
+
get not() {
|
|
963
|
+
return createUntil(r, !isNot);
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
function until(r) {
|
|
968
|
+
return createUntil(r);
|
|
969
|
+
}
|
|
970
|
+
//#endregion
|
|
971
|
+
//#region useArrayDifference/index.tsx
|
|
972
|
+
function defaultComparator(value, othVal) {
|
|
973
|
+
return value === othVal;
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* React port of VueUse's `useArrayDifference`.
|
|
977
|
+
*
|
|
978
|
+
* Map from @vueuse/shared `useArrayDifference`
|
|
979
|
+
* Mapping: upstream wraps the diff passes in `computed(...)` and returns a
|
|
980
|
+
* `ComputedRef`; React has no reactive value tracking, so this is a plain
|
|
981
|
+
* function recomputed on every render over the plain `list` / `values` arrays
|
|
982
|
+
* the caller passes — pass state arrays and the difference is re-diffed on the
|
|
983
|
+
* next render, no `.value` on the result. The same three call shapes as
|
|
984
|
+
* upstream are supported: plain diff, diff by `key`, and diff by `compareFn`,
|
|
985
|
+
* plus the `{ symmetric }` option.
|
|
986
|
+
*
|
|
987
|
+
* @see https://vueuse.org/shared/useArrayDifference/
|
|
988
|
+
*
|
|
989
|
+
* @example
|
|
990
|
+
* const list = [{ id: 1 }, { id: 2 }, { id: 3 }]
|
|
991
|
+
* useArrayDifference(list, [{ id: 3 }]) // [{ id: 1 }, { id: 2 }]
|
|
992
|
+
* useArrayDifference(list, [{ id: 3 }], 'id') // diff by key
|
|
993
|
+
* useArrayDifference(list, [{ id: 3 }], (a, b) => a.id === b.id, { symmetric: true })
|
|
994
|
+
*/
|
|
995
|
+
function useArrayDifference(...args) {
|
|
996
|
+
var _args$, _args$2;
|
|
997
|
+
const list = args[0];
|
|
998
|
+
const values = args[1];
|
|
999
|
+
let compareFn = (_args$ = args[2]) !== null && _args$ !== void 0 ? _args$ : defaultComparator;
|
|
1000
|
+
const { symmetric = false } = (_args$2 = args[3]) !== null && _args$2 !== void 0 ? _args$2 : {};
|
|
1001
|
+
if (typeof compareFn === "string") {
|
|
1002
|
+
const key = compareFn;
|
|
1003
|
+
compareFn = (value, othVal) => value[key] === othVal[key];
|
|
1004
|
+
}
|
|
1005
|
+
const diff1 = list.filter((x) => values.findIndex((y) => compareFn(x, y)) === -1);
|
|
1006
|
+
if (symmetric) {
|
|
1007
|
+
const diff2 = values.filter((x) => list.findIndex((y) => compareFn(x, y)) === -1);
|
|
1008
|
+
return [...diff1, ...diff2];
|
|
1009
|
+
} else return diff1;
|
|
1010
|
+
}
|
|
1011
|
+
//#endregion
|
|
1012
|
+
//#region useArrayEvery/index.tsx
|
|
1013
|
+
/**
|
|
1014
|
+
* React port of VueUse's `useArrayEvery`.
|
|
1015
|
+
*
|
|
1016
|
+
* Map from @vueuse/shared `useArrayEvery`
|
|
1017
|
+
* Mapping: upstream wraps `toValue(list).every(...)` in `computed(() => ...)`
|
|
1018
|
+
* and returns a `ComputedRef`; React has no reactive value tracking, so this
|
|
1019
|
+
* is a plain function recomputed on every render over the plain `list` array
|
|
1020
|
+
* the caller passes. Hold the array in `useState` (or any render-scoped value)
|
|
1021
|
+
* and pass a new array to observe a change — the result recomputes on the next
|
|
1022
|
+
* render. The predicate may return any value (coerced by truthiness, like
|
|
1023
|
+
* `Array.prototype.every`).
|
|
1024
|
+
*
|
|
1025
|
+
* @see https://vueuse.org/shared/useArrayEvery/
|
|
1026
|
+
*
|
|
1027
|
+
* @example
|
|
1028
|
+
* const [list, setList] = useState([0, 2, 4])
|
|
1029
|
+
* useArrayEvery(list, val => val % 2 === 0) // true
|
|
1030
|
+
* setList([0, 2, 5]) // false on the next render
|
|
1031
|
+
*
|
|
1032
|
+
* @param list - the array was called upon.
|
|
1033
|
+
* @param fn - a function to test each element.
|
|
1034
|
+
*
|
|
1035
|
+
* @returns **true** if the `fn` function returns a **truthy** value for every element from the array. Otherwise, **false**.
|
|
1036
|
+
*/
|
|
1037
|
+
function useArrayEvery(list, fn) {
|
|
1038
|
+
return list.every(fn);
|
|
1039
|
+
}
|
|
1040
|
+
//#endregion
|
|
1041
|
+
//#region useArrayFilter/index.tsx
|
|
1042
|
+
function useArrayFilter(list, fn) {
|
|
1043
|
+
return list.filter(fn);
|
|
1044
|
+
}
|
|
1045
|
+
//#endregion
|
|
1046
|
+
//#region useArrayFind/index.tsx
|
|
1047
|
+
/**
|
|
1048
|
+
* React port of VueUse's `useArrayFind`.
|
|
1049
|
+
*
|
|
1050
|
+
* Map from @vueuse/shared `useArrayFind`
|
|
1051
|
+
* Mapping: upstream wraps `toValue(list).find(...)` in `computed(() => ...)`
|
|
1052
|
+
* and returns a `ComputedRef`; React has no reactive value tracking, so this
|
|
1053
|
+
* is a plain function recomputed on every render over the plain `list` array
|
|
1054
|
+
* the caller passes. Hold the array in `useState` and pass a new array to
|
|
1055
|
+
* observe a change — the first match is returned on the next render.
|
|
1056
|
+
*
|
|
1057
|
+
* @see https://vueuse.org/shared/useArrayFind/
|
|
1058
|
+
*
|
|
1059
|
+
* @example
|
|
1060
|
+
* const [list, setList] = useState([1, -1, 2])
|
|
1061
|
+
* useArrayFind(list, val => val > 0) // 1
|
|
1062
|
+
* setList([3, -1, 2]) // 3 on the next render
|
|
1063
|
+
*/
|
|
1064
|
+
function useArrayFind(list, fn) {
|
|
1065
|
+
return list.find(fn);
|
|
1066
|
+
}
|
|
1067
|
+
//#endregion
|
|
1068
|
+
//#region useArrayFindIndex/index.tsx
|
|
1069
|
+
/**
|
|
1070
|
+
* React port of VueUse's `useArrayFindIndex`.
|
|
1071
|
+
*
|
|
1072
|
+
* Map from @vueuse/shared `useArrayFindIndex`
|
|
1073
|
+
* Mapping: upstream wraps `toValue(list).findIndex(...)` in `computed(...)`
|
|
1074
|
+
* and accepts a `RefOrValue`; React has no reactive value tracking, so
|
|
1075
|
+
* this is a plain function that recomputes the index on every render — pass
|
|
1076
|
+
* a state array (upstream: reactive array) and re-render with new state to
|
|
1077
|
+
* see the updated result. The return is a plain number, no `.value`.
|
|
1078
|
+
*
|
|
1079
|
+
* @example
|
|
1080
|
+
* const [list, setList] = useState([0, 2, 4, 6, 8])
|
|
1081
|
+
* useArrayFindIndex(list, i => i % 2 === 0) // 0
|
|
1082
|
+
*
|
|
1083
|
+
* setList([1, 3, 5, 7, 9]) // result === -1 on the next render
|
|
1084
|
+
*
|
|
1085
|
+
* @param list - the array was called upon.
|
|
1086
|
+
* @param fn - a function to test each element.
|
|
1087
|
+
*
|
|
1088
|
+
* @returns the index of the first element in the array that passes the test. Otherwise, "-1".
|
|
1089
|
+
*/
|
|
1090
|
+
function useArrayFindIndex(list, fn) {
|
|
1091
|
+
return list.findIndex(fn);
|
|
1092
|
+
}
|
|
1093
|
+
//#endregion
|
|
1094
|
+
//#region useArrayFindLast/index.tsx
|
|
1095
|
+
/**
|
|
1096
|
+
* Loop equivalent of `Array.prototype.findLast` — upstream ships the same
|
|
1097
|
+
* fallback for runtimes without the native method (e.g. node < 18); the repo
|
|
1098
|
+
* targets lib ES2022, where the native method is not available.
|
|
1099
|
+
*/
|
|
1100
|
+
function findLast(array, fn) {
|
|
1101
|
+
for (let index = array.length - 1; index >= 0; index--) if (fn(array[index], index, array)) return array[index];
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* React port of VueUse's `useArrayFindLast`.
|
|
1105
|
+
*
|
|
1106
|
+
* Map from @vueuse/shared `useArrayFindLast`
|
|
1107
|
+
* Mapping: upstream wraps native `Array.prototype.findLast` (with a loop
|
|
1108
|
+
* fallback for runtimes without it) in `computed(() => ...)` and returns a
|
|
1109
|
+
* `ComputedRef`; React has no reactive value tracking, so this is a plain
|
|
1110
|
+
* function recomputed on every render over the plain `list` array the caller
|
|
1111
|
+
* passes — the loop helper stands in for the native method since the repo
|
|
1112
|
+
* targets lib ES2022. Hold the array in `useState` and pass a new array to
|
|
1113
|
+
* observe a change — the last match is returned on the next render.
|
|
1114
|
+
*
|
|
1115
|
+
* @see https://vueuse.org/shared/useArrayFindLast/
|
|
1116
|
+
*
|
|
1117
|
+
* @example
|
|
1118
|
+
* const [list, setList] = useState([1, -1, 2])
|
|
1119
|
+
* useArrayFindLast(list, val => val > 0) // 2
|
|
1120
|
+
* setList([1, -1, -2]) // 1 on the next render
|
|
1121
|
+
*/
|
|
1122
|
+
function useArrayFindLast(list, fn) {
|
|
1123
|
+
return findLast(list, fn);
|
|
1124
|
+
}
|
|
1125
|
+
//#endregion
|
|
1126
|
+
//#region useArrayIncludes/index.tsx
|
|
1127
|
+
const toString = Object.prototype.toString;
|
|
1128
|
+
function isObject$1(val) {
|
|
1129
|
+
return toString.call(val) === "[object Object]";
|
|
1130
|
+
}
|
|
1131
|
+
function containsProp(obj, ...props) {
|
|
1132
|
+
return props.some((k) => k in obj);
|
|
1133
|
+
}
|
|
1134
|
+
function isArrayIncludesOptions(obj) {
|
|
1135
|
+
return isObject$1(obj) && containsProp(obj, "formIndex", "comparator");
|
|
1136
|
+
}
|
|
1137
|
+
function useArrayIncludes(...args) {
|
|
1138
|
+
var _comparator;
|
|
1139
|
+
const list = args[0];
|
|
1140
|
+
const value = args[1];
|
|
1141
|
+
let comparator = args[2];
|
|
1142
|
+
let formIndex = 0;
|
|
1143
|
+
if (isArrayIncludesOptions(comparator)) {
|
|
1144
|
+
var _comparator$fromIndex;
|
|
1145
|
+
formIndex = (_comparator$fromIndex = comparator.fromIndex) !== null && _comparator$fromIndex !== void 0 ? _comparator$fromIndex : 0;
|
|
1146
|
+
comparator = comparator.comparator;
|
|
1147
|
+
}
|
|
1148
|
+
if (typeof comparator === "string") {
|
|
1149
|
+
const key = comparator;
|
|
1150
|
+
comparator = (element, value) => element[key] === value;
|
|
1151
|
+
}
|
|
1152
|
+
comparator = (_comparator = comparator) !== null && _comparator !== void 0 ? _comparator : ((element, value) => element === value);
|
|
1153
|
+
return list.slice(formIndex).some((element, index, arr) => comparator(element, value, index, arr));
|
|
1154
|
+
}
|
|
1155
|
+
//#endregion
|
|
1156
|
+
//#region useArrayJoin/index.tsx
|
|
1157
|
+
/**
|
|
1158
|
+
* React port of VueUse's `useArrayJoin`.
|
|
1159
|
+
*
|
|
1160
|
+
* Map from @vueuse/shared `useArrayJoin`
|
|
1161
|
+
* Mapping: upstream wraps `toValue(list).map(i => toValue(i)).join(toValue(separator))`
|
|
1162
|
+
* in `computed(...)` and accepts a `RefOrValue`; React has no reactive
|
|
1163
|
+
* value tracking, so this is a plain function that recomputes the join on
|
|
1164
|
+
* every render — pass a state array (upstream: reactive array) and re-render
|
|
1165
|
+
* with new state to see the updated result. The return is a plain string,
|
|
1166
|
+
* no `.value`.
|
|
1167
|
+
*
|
|
1168
|
+
* `list` holds plain values only: the elements are joined with
|
|
1169
|
+
* `Array.prototype.join`, so no per-element unwrap happens (upstream
|
|
1170
|
+
* `toValue`s each element). A function element would be stringified to its
|
|
1171
|
+
* source instead of invoked.
|
|
1172
|
+
*
|
|
1173
|
+
* @example
|
|
1174
|
+
* const [list, setList] = useState(['foo', 0, { prop: 'val' }])
|
|
1175
|
+
* useArrayJoin(list) // 'foo,0,[object Object]'
|
|
1176
|
+
* useArrayJoin(list, '--') // 'foo--0--[object Object]'
|
|
1177
|
+
*
|
|
1178
|
+
* setList([...list, 'bar']) // result === 'foo--0--[object Object]--bar' on the next render
|
|
1179
|
+
*
|
|
1180
|
+
* @param list - the array was called upon.
|
|
1181
|
+
* @param separator - a string to separate each pair of adjacent elements of the array. If omitted, the array elements are separated with a comma (",").
|
|
1182
|
+
*
|
|
1183
|
+
* @returns a string with all array elements joined. If `list.length` is 0, the empty string is returned.
|
|
1184
|
+
*/
|
|
1185
|
+
function useArrayJoin(list, separator) {
|
|
1186
|
+
return list.join(separator);
|
|
1187
|
+
}
|
|
1188
|
+
//#endregion
|
|
1189
|
+
//#region useArrayMap/index.tsx
|
|
1190
|
+
/**
|
|
1191
|
+
* Reactive `Array.map`
|
|
1192
|
+
*
|
|
1193
|
+
* Map from @vueuse/shared `useArrayMap`
|
|
1194
|
+
* React port of VueUse's `useArrayMap`.
|
|
1195
|
+
*
|
|
1196
|
+
* Mapping: Vue's `computed` → recompute per render and return a plain array
|
|
1197
|
+
* (no `.value`) over the plain `list` array the caller passes.
|
|
1198
|
+
* Pass a `useState` array directly — the result updates on the next render.
|
|
1199
|
+
*
|
|
1200
|
+
* @example
|
|
1201
|
+
* const [list, setList] = useState([0, 1, 2, 3, 4])
|
|
1202
|
+
* const result = useArrayMap(list, i => i * 2) // [0, 2, 4, 6, 8]
|
|
1203
|
+
* setList(list.slice(0, -1)) // result: [0, 2, 4, 6] on the next render
|
|
1204
|
+
*/
|
|
1205
|
+
function useArrayMap(list, fn) {
|
|
1206
|
+
return list.map(fn);
|
|
1207
|
+
}
|
|
1208
|
+
//#endregion
|
|
1209
|
+
//#region useArrayReduce/index.tsx
|
|
1210
|
+
function useArrayReduce(list, reducer, ...args) {
|
|
1211
|
+
const reduceCallback = (sum, value, index) => reducer(sum, value, index);
|
|
1212
|
+
const initial = typeof args[0] === "function" ? args[0]() : args[0];
|
|
1213
|
+
return args.length ? list.reduce(reduceCallback, initial) : list.reduce(reduceCallback);
|
|
1214
|
+
}
|
|
1215
|
+
//#endregion
|
|
1216
|
+
//#region useArraySome/index.tsx
|
|
1217
|
+
/**
|
|
1218
|
+
* React port of VueUse's `useArraySome`.
|
|
1219
|
+
*
|
|
1220
|
+
* Map from @vueuse/shared `useArraySome`
|
|
1221
|
+
* Mapping: `computed(() => ...)` → recompute on every render — the result is a
|
|
1222
|
+
* plain `boolean` (no `.value`, no caching) computed from the plain `list`
|
|
1223
|
+
* array the caller passes. Hold the array in `useState` and pass a new array
|
|
1224
|
+
* to observe a change; the result recomputes on the next render.
|
|
1225
|
+
*
|
|
1226
|
+
* @see https://vueuse.org/shared/useArraySome/
|
|
1227
|
+
* @param list - the array was called upon.
|
|
1228
|
+
* @param fn - a function to test each element.
|
|
1229
|
+
*
|
|
1230
|
+
* @returns **true** if the `fn` function returns a **truthy** value for any element from the array. Otherwise, **false**.
|
|
1231
|
+
*
|
|
1232
|
+
* @example
|
|
1233
|
+
* const [list, setList] = useState([0, 2, 4, 6, 8])
|
|
1234
|
+
* const result = useArraySome(list, i => i > 10) // false
|
|
1235
|
+
* setList([...list, 11]) // result === true on the next render
|
|
1236
|
+
*/
|
|
1237
|
+
function useArraySome(list, fn) {
|
|
1238
|
+
return list.some(fn);
|
|
1239
|
+
}
|
|
1240
|
+
//#endregion
|
|
1241
|
+
//#region useArrayUnique/index.tsx
|
|
1242
|
+
/**
|
|
1243
|
+
* Reactive `Array.unique`
|
|
1244
|
+
*
|
|
1245
|
+
* Map from @vueuse/shared `useArrayUnique`
|
|
1246
|
+
* React port of VueUse's `useArrayUnique`.
|
|
1247
|
+
*
|
|
1248
|
+
* Mapping: upstream wraps `toValue(list)` in `computed(() => ...)` and returns
|
|
1249
|
+
* a `ComputedRef`; React has no reactive value tracking, so this is a plain
|
|
1250
|
+
* function recomputed on every render over the plain `list` array the caller
|
|
1251
|
+
* passes — the result is a deduped plain array (no `.value`, no caching).
|
|
1252
|
+
* Duplicate detection uses a `Set` of the values (reference identity for
|
|
1253
|
+
* objects) unless a custom `compareFn` is given — same as upstream. Hold the
|
|
1254
|
+
* array in `useState` and pass a new array to observe a change.
|
|
1255
|
+
*
|
|
1256
|
+
* @see https://vueuse.org/shared/useArrayUnique/
|
|
1257
|
+
*
|
|
1258
|
+
* @example
|
|
1259
|
+
* const [list, setList] = useState([0, 2, 2, 4, 4, 4])
|
|
1260
|
+
* const result = useArrayUnique(list) // [0, 2, 4]
|
|
1261
|
+
*
|
|
1262
|
+
* setList([0, 2, 4, 6, 6]) // result === [0, 2, 4, 6] on the next render
|
|
1263
|
+
*/
|
|
1264
|
+
function useArrayUnique(list, compareFn) {
|
|
1265
|
+
return compareFn ? uniqueElementsBy(list, compareFn) : uniq(list);
|
|
1266
|
+
}
|
|
1267
|
+
function uniq(array) {
|
|
1268
|
+
return Array.from(new Set(array));
|
|
1269
|
+
}
|
|
1270
|
+
function uniqueElementsBy(array, fn) {
|
|
1271
|
+
return array.reduce((acc, v) => {
|
|
1272
|
+
if (!acc.some((x) => fn(v, x, array))) acc.push(v);
|
|
1273
|
+
return acc;
|
|
1274
|
+
}, []);
|
|
1275
|
+
}
|
|
1276
|
+
//#endregion
|
|
1277
|
+
//#region useControllableState/index.tsx
|
|
1278
|
+
function isTuple(state) {
|
|
1279
|
+
return Array.isArray(state) && state.length === 2 && typeof state[1] === "function";
|
|
1280
|
+
}
|
|
1281
|
+
function isObjectState$1(state) {
|
|
1282
|
+
return typeof state === "object" && state !== null && !Array.isArray(state) && "value" in state;
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Combine controlled and uncontrolled state sources.
|
|
1286
|
+
*
|
|
1287
|
+
* `state` is resolved with `toValue` on every render. A tuple
|
|
1288
|
+
* `[value, setter]` or a `{ value, onChange }` pair is always controlled: the
|
|
1289
|
+
* current value is the resolved source and `setValue` writes through to the
|
|
1290
|
+
* tuple setter / `onChange`. With `passive: true` a plain value, getter, or
|
|
1291
|
+
* ref source is uncontrolled — the hook initializes from the source and local
|
|
1292
|
+
* updates persist, and external source changes are synced back (honoring
|
|
1293
|
+
* `shouldUpdate`). With the default `passive: false` such a source is
|
|
1294
|
+
* controlled (the external value wins on every render); `setValue` then has
|
|
1295
|
+
* no channel back to the caller, so it warns instead of silently discarding
|
|
1296
|
+
* the update — pass a tuple, a `{ value, onChange }` pair, or use
|
|
1297
|
+
* `passive: true` to write. `defaultValue` (value or lazy initializer) seeds
|
|
1298
|
+
* the internal state of uncontrolled sources; `shouldUpdate(prev, next)`
|
|
1299
|
+
* guards every commit, including the passive sync.
|
|
1300
|
+
*/
|
|
1301
|
+
function useControllableState(state, options = {}) {
|
|
1302
|
+
const { defaultValue, shouldUpdate = (prev, next) => !Object.is(prev, next), passive = false } = options;
|
|
1303
|
+
const controlled = isTuple(state) || isObjectState$1(state) || !passive;
|
|
1304
|
+
const externalValue = toValue(state);
|
|
1305
|
+
const initial = defaultValue === void 0 ? externalValue : typeof defaultValue === "function" ? defaultValue() : defaultValue;
|
|
1306
|
+
const [internal, setInternal] = (0, react.useState)(initial);
|
|
1307
|
+
const value = controlled ? externalValue : internal;
|
|
1308
|
+
const valueRef = (0, react.useRef)(value);
|
|
1309
|
+
const previousExternalRef = (0, react.useRef)(externalValue);
|
|
1310
|
+
valueRef.current = value;
|
|
1311
|
+
const stateRef = (0, react.useRef)(state);
|
|
1312
|
+
stateRef.current = state;
|
|
1313
|
+
(0, react.useEffect)(() => {
|
|
1314
|
+
const canSync = typeof state === "function" || isRefLike(state) || externalValue === null || typeof externalValue !== "object";
|
|
1315
|
+
if (passive && canSync && !isTuple(state) && !isObjectState$1(state) && !Object.is(previousExternalRef.current, externalValue) && shouldUpdate(valueRef.current, externalValue)) setInternal(externalValue);
|
|
1316
|
+
previousExternalRef.current = externalValue;
|
|
1317
|
+
}, [
|
|
1318
|
+
externalValue,
|
|
1319
|
+
passive,
|
|
1320
|
+
shouldUpdate
|
|
1321
|
+
]);
|
|
1322
|
+
return [value, (0, react.useCallback)((action) => {
|
|
1323
|
+
var _currentState$onChang;
|
|
1324
|
+
const prev = valueRef.current;
|
|
1325
|
+
const next = typeof action === "function" ? action(prev) : action;
|
|
1326
|
+
if (!shouldUpdate(prev, next)) return;
|
|
1327
|
+
const currentState = stateRef.current;
|
|
1328
|
+
if (isTuple(currentState)) currentState[1](next);
|
|
1329
|
+
else if (isObjectState$1(currentState)) (_currentState$onChang = currentState.onChange) === null || _currentState$onChang === void 0 || _currentState$onChang.call(currentState, next);
|
|
1330
|
+
else if (!controlled) setInternal(next);
|
|
1331
|
+
else assert(false, "useControllableState: `setValue` on a controlled source without a write channel (a plain value, getter, or ref with `passive: false`) is ignored. Pass a [value, setter] tuple, a { value, onChange } pair, or use `passive: true`.");
|
|
1332
|
+
}, [controlled, shouldUpdate])];
|
|
1333
|
+
}
|
|
1334
|
+
//#endregion
|
|
1335
|
+
//#region useCounter/index.tsx
|
|
1336
|
+
/**
|
|
1337
|
+
* React port of VueUse's `useCounter`.
|
|
1338
|
+
*
|
|
1339
|
+
* Map from @vueuse/shared `useCounter`
|
|
1340
|
+
* Mapping: `ref(initialValue)` → `useState`, mutation functions become
|
|
1341
|
+
* stable `useCallback`s; options are kept in refs so callbacks stay stable.
|
|
1342
|
+
*
|
|
1343
|
+
* @example
|
|
1344
|
+
* const { count, inc, dec, set, reset } = useCounter(10, { min: 0, max: 100 })
|
|
1345
|
+
*/
|
|
1346
|
+
function useCounter(initialValue = 0, options = {}) {
|
|
1347
|
+
const { min = Number.NEGATIVE_INFINITY, max = Number.POSITIVE_INFINITY } = options;
|
|
1348
|
+
const minRef = (0, react.useRef)(min);
|
|
1349
|
+
const maxRef = (0, react.useRef)(max);
|
|
1350
|
+
const initialRef = (0, react.useRef)(toValue(initialValue));
|
|
1351
|
+
const [count, setCount] = useControllableState(initialValue, { passive: true });
|
|
1352
|
+
const set = (0, react.useCallback)((value) => {
|
|
1353
|
+
setCount((current) => {
|
|
1354
|
+
const next = clamp(value, minRef.current, maxRef.current);
|
|
1355
|
+
return current === next ? current : next;
|
|
1356
|
+
});
|
|
1357
|
+
}, []);
|
|
1358
|
+
const get = (0, react.useCallback)(() => count, [count]);
|
|
1359
|
+
return {
|
|
1360
|
+
count,
|
|
1361
|
+
inc: (0, react.useCallback)((delta = 1) => {
|
|
1362
|
+
setCount((current) => {
|
|
1363
|
+
const next = clamp(current + delta, minRef.current, maxRef.current);
|
|
1364
|
+
return current === next ? current : next;
|
|
1365
|
+
});
|
|
1366
|
+
}, []),
|
|
1367
|
+
dec: (0, react.useCallback)((delta = 1) => {
|
|
1368
|
+
setCount((current) => {
|
|
1369
|
+
const next = clamp(current - delta, minRef.current, maxRef.current);
|
|
1370
|
+
return current === next ? current : next;
|
|
1371
|
+
});
|
|
1372
|
+
}, []),
|
|
1373
|
+
get,
|
|
1374
|
+
set,
|
|
1375
|
+
reset: (0, react.useCallback)((val) => {
|
|
1376
|
+
const target = val === void 0 ? initialRef.current : val;
|
|
1377
|
+
initialRef.current = target;
|
|
1378
|
+
const next = clamp(target, minRef.current, maxRef.current);
|
|
1379
|
+
setCount((current) => current === next ? current : next);
|
|
1380
|
+
return next;
|
|
1381
|
+
}, [])
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
//#endregion
|
|
1385
|
+
//#region useDateFormat/index.tsx
|
|
1386
|
+
const REGEX_PARSE = /^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[T\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/i;
|
|
1387
|
+
const REGEX_FORMAT = /[YMDHhms]o|\[([^\]]+)\]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a{1,2}|A{1,2}|m{1,2}|s{1,2}|Z{1,2}|z{1,4}|SSS/g;
|
|
1388
|
+
function defaultMeridiem(hours, minutes, isLowercase, hasPeriod) {
|
|
1389
|
+
let m = hours < 12 ? "AM" : "PM";
|
|
1390
|
+
if (hasPeriod) m = m.split("").reduce((acc, curr) => acc += `${curr}.`, "");
|
|
1391
|
+
return isLowercase ? m.toLowerCase() : m;
|
|
1392
|
+
}
|
|
1393
|
+
function formatOrdinal(num) {
|
|
1394
|
+
const suffixes = [
|
|
1395
|
+
"th",
|
|
1396
|
+
"st",
|
|
1397
|
+
"nd",
|
|
1398
|
+
"rd"
|
|
1399
|
+
];
|
|
1400
|
+
const v = num % 100;
|
|
1401
|
+
return num + (suffixes[(v - 20) % 10] || suffixes[v] || suffixes[0]);
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Unwrap the house input convention — a plain value, a ref-like `{ current }`
|
|
1405
|
+
* or a getter function (house replacement for Vue's `toValue` /
|
|
1406
|
+
* `RefOrValue<T>`).
|
|
1407
|
+
*/
|
|
1408
|
+
function formatDate(date, formatStr, options = {}) {
|
|
1409
|
+
var _options$customMeridi;
|
|
1410
|
+
const years = date.getFullYear();
|
|
1411
|
+
const month = date.getMonth();
|
|
1412
|
+
const days = date.getDate();
|
|
1413
|
+
const hours = date.getHours();
|
|
1414
|
+
const minutes = date.getMinutes();
|
|
1415
|
+
const seconds = date.getSeconds();
|
|
1416
|
+
const milliseconds = date.getMilliseconds();
|
|
1417
|
+
const day = date.getDay();
|
|
1418
|
+
const meridiem = (_options$customMeridi = options.customMeridiem) !== null && _options$customMeridi !== void 0 ? _options$customMeridi : defaultMeridiem;
|
|
1419
|
+
const stripTimeZone = (dateString) => {
|
|
1420
|
+
var _dateString$split$;
|
|
1421
|
+
return (_dateString$split$ = dateString.split(" ")[1]) !== null && _dateString$split$ !== void 0 ? _dateString$split$ : "";
|
|
1422
|
+
};
|
|
1423
|
+
const matches = {
|
|
1424
|
+
Yo: () => formatOrdinal(years),
|
|
1425
|
+
YY: () => String(years).slice(-2),
|
|
1426
|
+
YYYY: () => years,
|
|
1427
|
+
M: () => month + 1,
|
|
1428
|
+
Mo: () => formatOrdinal(month + 1),
|
|
1429
|
+
MM: () => `${month + 1}`.padStart(2, "0"),
|
|
1430
|
+
MMM: () => date.toLocaleDateString(options.locales, { month: "short" }),
|
|
1431
|
+
MMMM: () => date.toLocaleDateString(options.locales, { month: "long" }),
|
|
1432
|
+
D: () => String(days),
|
|
1433
|
+
Do: () => formatOrdinal(days),
|
|
1434
|
+
DD: () => `${days}`.padStart(2, "0"),
|
|
1435
|
+
H: () => String(hours),
|
|
1436
|
+
Ho: () => formatOrdinal(hours),
|
|
1437
|
+
HH: () => `${hours}`.padStart(2, "0"),
|
|
1438
|
+
h: () => `${hours % 12 || 12}`.padStart(1, "0"),
|
|
1439
|
+
ho: () => formatOrdinal(hours % 12 || 12),
|
|
1440
|
+
hh: () => `${hours % 12 || 12}`.padStart(2, "0"),
|
|
1441
|
+
m: () => String(minutes),
|
|
1442
|
+
mo: () => formatOrdinal(minutes),
|
|
1443
|
+
mm: () => `${minutes}`.padStart(2, "0"),
|
|
1444
|
+
s: () => String(seconds),
|
|
1445
|
+
so: () => formatOrdinal(seconds),
|
|
1446
|
+
ss: () => `${seconds}`.padStart(2, "0"),
|
|
1447
|
+
SSS: () => `${milliseconds}`.padStart(3, "0"),
|
|
1448
|
+
d: () => day,
|
|
1449
|
+
dd: () => date.toLocaleDateString(options.locales, { weekday: "narrow" }),
|
|
1450
|
+
ddd: () => date.toLocaleDateString(options.locales, { weekday: "short" }),
|
|
1451
|
+
dddd: () => date.toLocaleDateString(options.locales, { weekday: "long" }),
|
|
1452
|
+
A: () => meridiem(hours, minutes),
|
|
1453
|
+
AA: () => meridiem(hours, minutes, false, true),
|
|
1454
|
+
a: () => meridiem(hours, minutes, true),
|
|
1455
|
+
aa: () => meridiem(hours, minutes, true, true),
|
|
1456
|
+
z: () => stripTimeZone(date.toLocaleDateString(options.locales, { timeZoneName: "shortOffset" })),
|
|
1457
|
+
zz: () => stripTimeZone(date.toLocaleDateString(options.locales, { timeZoneName: "shortOffset" })),
|
|
1458
|
+
zzz: () => stripTimeZone(date.toLocaleDateString(options.locales, { timeZoneName: "shortOffset" })),
|
|
1459
|
+
zzzz: () => stripTimeZone(date.toLocaleDateString(options.locales, { timeZoneName: "longOffset" }))
|
|
1460
|
+
};
|
|
1461
|
+
return formatStr.replace(REGEX_FORMAT, (match, $1) => {
|
|
1462
|
+
var _ref, _matches$match;
|
|
1463
|
+
return (_ref = $1 !== null && $1 !== void 0 ? $1 : (_matches$match = matches[match]) === null || _matches$match === void 0 ? void 0 : _matches$match.call(matches)) !== null && _ref !== void 0 ? _ref : match;
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
function normalizeDate(date) {
|
|
1467
|
+
if (date === null) return /* @__PURE__ */ new Date(NaN);
|
|
1468
|
+
if (date === void 0) return /* @__PURE__ */ new Date();
|
|
1469
|
+
if (date instanceof Date) return new Date(date);
|
|
1470
|
+
if (typeof date === "string" && !/Z$/i.test(date)) {
|
|
1471
|
+
const d = date.match(REGEX_PARSE);
|
|
1472
|
+
if (d) {
|
|
1473
|
+
const m = Number(d[2]) - 1 || 0;
|
|
1474
|
+
const ms = Number((d[7] || "0").substring(0, 3));
|
|
1475
|
+
return new Date(Number(d[1]), m, Number(d[3]) || 1, Number(d[4]) || 0, Number(d[5]) || 0, Number(d[6]) || 0, ms);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
return new Date(date);
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Get the formatted date according to the string of tokens passed in.
|
|
1482
|
+
*
|
|
1483
|
+
* Map from @vueuse/shared `useDateFormat`.
|
|
1484
|
+
*
|
|
1485
|
+
* React divergence: upstream wraps the result in a Vue `computed` and returns
|
|
1486
|
+
* `ComputedRef<string>` — this port returns a PLAIN STRING. Call it during
|
|
1487
|
+
* render and pass plain values (e.g. your `useState` date); the string is
|
|
1488
|
+
* recomputed on every render with fresh inputs. Do not read `.value` from it.
|
|
1489
|
+
*
|
|
1490
|
+
* Inputs (`date`, `formatStr`, `options.locales`) are plain read-only values
|
|
1491
|
+
* — pass `ref.current` or the state value; a `MaybeRefOrGetter` source must be
|
|
1492
|
+
* resolved by the caller (upstream types them `MaybeRefOrGetter`).
|
|
1493
|
+
*
|
|
1494
|
+
* Supported tokens (mirroring upstream 1:1, default format `HH:mm:ss`):
|
|
1495
|
+
* `Yo YY YYYY` — year · `M Mo MM MMM MMMM` — month (locale-aware short/long
|
|
1496
|
+
* names via `Intl`) · `D Do DD` — day of month · `H Ho HH` — 24-hour clock ·
|
|
1497
|
+
* `h ho hh` — 12-hour clock · `m mo mm` — minutes · `s so ss` — seconds ·
|
|
1498
|
+
* `SSS` — milliseconds (3 digits) · `d dd ddd dddd` — weekday (locale-aware
|
|
1499
|
+
* via `Intl`) · `A AA a aa` — meridiem, customizable via
|
|
1500
|
+
* `options.customMeridiem` · `z zz zzz zzzz` — timezone offset names
|
|
1501
|
+
* (`shortOffset` / `longOffset` via `toLocaleString`). Text wrapped in
|
|
1502
|
+
* brackets (`[...]`) is output literally as an escape sequence.
|
|
1503
|
+
*
|
|
1504
|
+
* @see https://vueuse.org/useDateFormat
|
|
1505
|
+
* @param date - The date to format, can either be a `Date` object, a timestamp, or a string
|
|
1506
|
+
* @param formatStr - The combination of tokens to format the date
|
|
1507
|
+
* @param options - UseDateFormatOptions
|
|
1508
|
+
*
|
|
1509
|
+
* @__NO_SIDE_EFFECTS__
|
|
1510
|
+
*/
|
|
1511
|
+
function useDateFormat(date, formatStr = "HH:mm:ss", options = {}) {
|
|
1512
|
+
return formatDate(normalizeDate(date), formatStr, options);
|
|
1513
|
+
}
|
|
1514
|
+
//#endregion
|
|
1515
|
+
//#region useDebounceFn/index.tsx
|
|
1516
|
+
/**
|
|
1517
|
+
* Debounce execution of a function — React port of VueUse's `useDebounceFn`.
|
|
1518
|
+
*
|
|
1519
|
+
* Map from @vueuse/shared `useDebounceFn`
|
|
1520
|
+
* Mapping: upstream builds `createFilterWrapper(debounceFilter(ms, options), fn)`
|
|
1521
|
+
* so every call returns a promise and the wrapper carries `cancel` / `flush` /
|
|
1522
|
+
* `isPending`. This port builds the same wrapper once (`useMemo`) so its
|
|
1523
|
+
* identity is stable across renders; the latest `fn` / `ms` / `options` are
|
|
1524
|
+
* mirrored into refs so every call sees fresh values. `ms` accepts a number or
|
|
1525
|
+
* a ref-like `{ current }` (upstream: `RefOrValue<number>`) and is re-read on
|
|
1526
|
+
* every call. `isPending` becomes a non-reactive getter (React has no reactive
|
|
1527
|
+
* refs); promise settlement mirrors upstream — a regular debounce resolves
|
|
1528
|
+
* with the result, a superseded/canceled call settles with `undefined` (or
|
|
1529
|
+
* rejects with `rejectOnCancel`), and the `maxWait` trailing edge runs the
|
|
1530
|
+
* latest invocation but settles the pending promise without its result.
|
|
1531
|
+
* Pending timers are cleared when the component unmounts (upstream leaves
|
|
1532
|
+
* disposal to the effect scope).
|
|
1533
|
+
*
|
|
1534
|
+
* @example
|
|
1535
|
+
* const debouncedFn = useDebounceFn(() => { ... }, 1000)
|
|
1536
|
+
* debouncedFn()
|
|
1537
|
+
* debouncedFn.cancel()
|
|
1538
|
+
* debouncedFn.flush()
|
|
1539
|
+
*/
|
|
1540
|
+
function useDebounceFn(fn, ms = 200, options = {}) {
|
|
1541
|
+
const fnRef = (0, react.useRef)(fn);
|
|
1542
|
+
fnRef.current = fn;
|
|
1543
|
+
const msRef = (0, react.useRef)(ms);
|
|
1544
|
+
msRef.current = ms;
|
|
1545
|
+
const optionsRef = (0, react.useRef)(options);
|
|
1546
|
+
optionsRef.current = options;
|
|
1547
|
+
const timerRef = (0, react.useRef)(null);
|
|
1548
|
+
const maxTimerRef = (0, react.useRef)(null);
|
|
1549
|
+
const pendingRef = (0, react.useRef)(false);
|
|
1550
|
+
const onCancelRef = (0, react.useRef)(noop);
|
|
1551
|
+
const onFlushRef = (0, react.useRef)(noop);
|
|
1552
|
+
const lastInvokeRef = (0, react.useRef)(noop);
|
|
1553
|
+
const debounced = (0, react.useMemo)(() => {
|
|
1554
|
+
const clearTimers = () => {
|
|
1555
|
+
if (timerRef.current !== null) {
|
|
1556
|
+
clearTimeout(timerRef.current);
|
|
1557
|
+
timerRef.current = null;
|
|
1558
|
+
}
|
|
1559
|
+
if (maxTimerRef.current !== null) {
|
|
1560
|
+
clearTimeout(maxTimerRef.current);
|
|
1561
|
+
maxTimerRef.current = null;
|
|
1562
|
+
}
|
|
1563
|
+
};
|
|
1564
|
+
const settleCurrent = (settleRef) => {
|
|
1565
|
+
const callback = settleRef.current;
|
|
1566
|
+
onCancelRef.current = noop;
|
|
1567
|
+
onFlushRef.current = noop;
|
|
1568
|
+
pendingRef.current = false;
|
|
1569
|
+
callback();
|
|
1570
|
+
};
|
|
1571
|
+
const handler = (invoke) => {
|
|
1572
|
+
const duration = toValue(msRef.current);
|
|
1573
|
+
const maxDuration = toValue(optionsRef.current.maxWait);
|
|
1574
|
+
if (timerRef.current !== null) {
|
|
1575
|
+
clearTimeout(timerRef.current);
|
|
1576
|
+
timerRef.current = null;
|
|
1577
|
+
settleCurrent(onCancelRef);
|
|
1578
|
+
}
|
|
1579
|
+
if (duration === void 0 || duration <= 0 || maxDuration !== void 0 && maxDuration <= 0) {
|
|
1580
|
+
clearTimers();
|
|
1581
|
+
pendingRef.current = false;
|
|
1582
|
+
try {
|
|
1583
|
+
return Promise.resolve(invoke());
|
|
1584
|
+
} catch (error) {
|
|
1585
|
+
return Promise.reject(error);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
pendingRef.current = true;
|
|
1589
|
+
return new Promise((resolve, reject) => {
|
|
1590
|
+
onCancelRef.current = optionsRef.current.rejectOnCancel ? reject : () => resolve(void 0);
|
|
1591
|
+
onFlushRef.current = () => resolve(invoke());
|
|
1592
|
+
lastInvokeRef.current = invoke;
|
|
1593
|
+
if (maxDuration !== void 0 && maxTimerRef.current === null) maxTimerRef.current = setTimeout(() => {
|
|
1594
|
+
maxTimerRef.current = null;
|
|
1595
|
+
if (timerRef.current !== null) {
|
|
1596
|
+
clearTimeout(timerRef.current);
|
|
1597
|
+
timerRef.current = null;
|
|
1598
|
+
}
|
|
1599
|
+
const invoke = lastInvokeRef.current;
|
|
1600
|
+
settleCurrent(onCancelRef);
|
|
1601
|
+
invoke();
|
|
1602
|
+
}, maxDuration);
|
|
1603
|
+
timerRef.current = setTimeout(() => {
|
|
1604
|
+
timerRef.current = null;
|
|
1605
|
+
if (maxTimerRef.current !== null) {
|
|
1606
|
+
clearTimeout(maxTimerRef.current);
|
|
1607
|
+
maxTimerRef.current = null;
|
|
1608
|
+
}
|
|
1609
|
+
settleCurrent(onFlushRef);
|
|
1610
|
+
}, duration);
|
|
1611
|
+
});
|
|
1612
|
+
};
|
|
1613
|
+
const cancel = () => {
|
|
1614
|
+
clearTimers();
|
|
1615
|
+
settleCurrent(onCancelRef);
|
|
1616
|
+
};
|
|
1617
|
+
const flush = () => {
|
|
1618
|
+
if (!pendingRef.current) return;
|
|
1619
|
+
clearTimers();
|
|
1620
|
+
settleCurrent(onFlushRef);
|
|
1621
|
+
};
|
|
1622
|
+
const wrapped = ((...args) => {
|
|
1623
|
+
return handler(() => fnRef.current(...args));
|
|
1624
|
+
});
|
|
1625
|
+
wrapped.cancel = cancel;
|
|
1626
|
+
wrapped.flush = flush;
|
|
1627
|
+
Object.defineProperty(wrapped, "isPending", {
|
|
1628
|
+
enumerable: true,
|
|
1629
|
+
get: () => pendingRef.current
|
|
1630
|
+
});
|
|
1631
|
+
return wrapped;
|
|
1632
|
+
}, []);
|
|
1633
|
+
(0, react.useEffect)(() => debounced.cancel, [debounced]);
|
|
1634
|
+
return debounced;
|
|
1635
|
+
}
|
|
1636
|
+
//#endregion
|
|
1637
|
+
//#region useInterval/index.tsx
|
|
1638
|
+
function useInterval(interval = 1e3, options = {}) {
|
|
1639
|
+
const { controls: exposeControls = false, immediate = true, callback, immediateCallback = false } = options;
|
|
1640
|
+
const intervalRef = (0, react.useRef)(interval);
|
|
1641
|
+
intervalRef.current = interval;
|
|
1642
|
+
const callbackRef = (0, react.useRef)(callback);
|
|
1643
|
+
callbackRef.current = callback;
|
|
1644
|
+
const immediateCallbackRef = (0, react.useRef)(immediateCallback);
|
|
1645
|
+
immediateCallbackRef.current = immediateCallback;
|
|
1646
|
+
const [counter, setCounter] = (0, react.useState)(0);
|
|
1647
|
+
const counterRef = (0, react.useRef)(0);
|
|
1648
|
+
const [isActive, setIsActive] = (0, react.useState)(() => immediate && toValue(interval) > 0);
|
|
1649
|
+
const isActiveRef = (0, react.useRef)(false);
|
|
1650
|
+
const timerRef = (0, react.useRef)(null);
|
|
1651
|
+
const tick = (0, react.useCallback)(() => {
|
|
1652
|
+
var _callbackRef$current;
|
|
1653
|
+
counterRef.current += 1;
|
|
1654
|
+
setCounter(counterRef.current);
|
|
1655
|
+
(_callbackRef$current = callbackRef.current) === null || _callbackRef$current === void 0 || _callbackRef$current.call(callbackRef, counterRef.current);
|
|
1656
|
+
}, []);
|
|
1657
|
+
const reset = (0, react.useCallback)(() => {
|
|
1658
|
+
counterRef.current = 0;
|
|
1659
|
+
setCounter(0);
|
|
1660
|
+
}, []);
|
|
1661
|
+
const pause = (0, react.useCallback)(() => {
|
|
1662
|
+
isActiveRef.current = false;
|
|
1663
|
+
setIsActive(false);
|
|
1664
|
+
if (timerRef.current !== null) {
|
|
1665
|
+
clearInterval(timerRef.current);
|
|
1666
|
+
timerRef.current = null;
|
|
1667
|
+
}
|
|
1668
|
+
}, []);
|
|
1669
|
+
const resume = (0, react.useCallback)(() => {
|
|
1670
|
+
const ms = toValue(intervalRef.current);
|
|
1671
|
+
if (ms <= 0) return;
|
|
1672
|
+
isActiveRef.current = true;
|
|
1673
|
+
setIsActive(true);
|
|
1674
|
+
if (immediateCallbackRef.current) tick();
|
|
1675
|
+
if (timerRef.current !== null) {
|
|
1676
|
+
clearInterval(timerRef.current);
|
|
1677
|
+
timerRef.current = null;
|
|
1678
|
+
}
|
|
1679
|
+
timerRef.current = setInterval(tick, ms);
|
|
1680
|
+
}, [tick]);
|
|
1681
|
+
const startedRef = (0, react.useRef)(false);
|
|
1682
|
+
(0, react.useEffect)(() => {
|
|
1683
|
+
if (immediate && !startedRef.current) {
|
|
1684
|
+
startedRef.current = true;
|
|
1685
|
+
resume();
|
|
1686
|
+
}
|
|
1687
|
+
return pause;
|
|
1688
|
+
}, [immediate, resume]);
|
|
1689
|
+
const mountedRef = (0, react.useRef)(false);
|
|
1690
|
+
const intervalMs = toValue(interval);
|
|
1691
|
+
(0, react.useEffect)(() => {
|
|
1692
|
+
if (!mountedRef.current) {
|
|
1693
|
+
mountedRef.current = true;
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
if (isActiveRef.current) resume();
|
|
1697
|
+
}, [intervalMs, resume]);
|
|
1698
|
+
if (exposeControls) return {
|
|
1699
|
+
counter,
|
|
1700
|
+
reset,
|
|
1701
|
+
isActive,
|
|
1702
|
+
pause,
|
|
1703
|
+
resume
|
|
1704
|
+
};
|
|
1705
|
+
return counter;
|
|
1706
|
+
}
|
|
1707
|
+
//#endregion
|
|
1708
|
+
//#region useIntervalFn/index.tsx
|
|
1709
|
+
/**
|
|
1710
|
+
* React port of VueUse's `useIntervalFn` — wrapper for `setInterval` with
|
|
1711
|
+
* controls.
|
|
1712
|
+
*
|
|
1713
|
+
* Map from @vueuse/shared `useIntervalFn`
|
|
1714
|
+
* Mapping: upstream accepts `RefOrValue<number>` for the interval — this
|
|
1715
|
+
* port accepts a plain `number`. `isActive` is a boolean state (upstream: a
|
|
1716
|
+
* readonly shallow ref), also mirrored in a ref so `resume()` can check it
|
|
1717
|
+
* synchronously right after `immediateCallback` fires the callback — the
|
|
1718
|
+
* callback may `pause()` itself ("pause in callback"). The timer is scheduled
|
|
1719
|
+
* in a mount effect (upstream starts synchronously during setup) and cleared
|
|
1720
|
+
* on unmount via effect cleanup; changing the interval while active restarts
|
|
1721
|
+
* the timer (upstream: a `watch` on the interval calls `resume()`). The
|
|
1722
|
+
* callback, interval and options are kept in refs so every tick and restart
|
|
1723
|
+
* uses the newest ones.
|
|
1724
|
+
*
|
|
1725
|
+
* @example
|
|
1726
|
+
* const { isActive, pause, resume } = useIntervalFn(() => { ... }, 1000)
|
|
1727
|
+
*/
|
|
1728
|
+
function useIntervalFn(cb, interval = 1e3, options = {}) {
|
|
1729
|
+
const { immediate = true, immediateCallback = false } = options;
|
|
1730
|
+
const [isActive, setIsActive] = (0, react.useState)(false);
|
|
1731
|
+
const isActiveRef = (0, react.useRef)(false);
|
|
1732
|
+
const cbRef = (0, react.useRef)(cb);
|
|
1733
|
+
const intervalRef = (0, react.useRef)(interval);
|
|
1734
|
+
const immediateCallbackRef = (0, react.useRef)(immediateCallback);
|
|
1735
|
+
const timerRef = (0, react.useRef)(null);
|
|
1736
|
+
cbRef.current = cb;
|
|
1737
|
+
intervalRef.current = interval;
|
|
1738
|
+
immediateCallbackRef.current = immediateCallback;
|
|
1739
|
+
const immediateRef = (0, react.useRef)(immediate);
|
|
1740
|
+
function clean() {
|
|
1741
|
+
if (timerRef.current) {
|
|
1742
|
+
clearInterval(timerRef.current);
|
|
1743
|
+
timerRef.current = null;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
function setActive(active) {
|
|
1747
|
+
isActiveRef.current = active;
|
|
1748
|
+
setIsActive(active);
|
|
1749
|
+
}
|
|
1750
|
+
const pause = (0, react.useCallback)(() => {
|
|
1751
|
+
setActive(false);
|
|
1752
|
+
clean();
|
|
1753
|
+
}, []);
|
|
1754
|
+
const resume = (0, react.useCallback)(() => {
|
|
1755
|
+
const intervalValue = intervalRef.current;
|
|
1756
|
+
if (intervalValue <= 0) return;
|
|
1757
|
+
setActive(true);
|
|
1758
|
+
if (immediateCallbackRef.current) cbRef.current();
|
|
1759
|
+
clean();
|
|
1760
|
+
if (isActiveRef.current) timerRef.current = setInterval(() => cbRef.current(), intervalValue);
|
|
1761
|
+
}, []);
|
|
1762
|
+
(0, react.useEffect)(() => {
|
|
1763
|
+
if (immediateRef.current) resume();
|
|
1764
|
+
return () => {
|
|
1765
|
+
clean();
|
|
1766
|
+
};
|
|
1767
|
+
}, [resume]);
|
|
1768
|
+
const mountedRef = (0, react.useRef)(false);
|
|
1769
|
+
(0, react.useEffect)(() => {
|
|
1770
|
+
if (!mountedRef.current) {
|
|
1771
|
+
mountedRef.current = true;
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
if (isActiveRef.current) resume();
|
|
1775
|
+
}, [interval, resume]);
|
|
1776
|
+
return {
|
|
1777
|
+
isActive,
|
|
1778
|
+
pause,
|
|
1779
|
+
resume
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
//#endregion
|
|
1783
|
+
//#region useLastChanged/index.tsx
|
|
1784
|
+
function useLastChanged(value, options = {}) {
|
|
1785
|
+
const [lastChanged, setLastChanged] = (0, react.useState)(() => {
|
|
1786
|
+
var _options$initialValue;
|
|
1787
|
+
return (_options$initialValue = options.initialValue) !== null && _options$initialValue !== void 0 ? _options$initialValue : null;
|
|
1788
|
+
});
|
|
1789
|
+
const prevValue = (0, react.useRef)(value);
|
|
1790
|
+
(0, react.useEffect)(() => {
|
|
1791
|
+
if (!Object.is(prevValue.current, value)) {
|
|
1792
|
+
prevValue.current = value;
|
|
1793
|
+
setLastChanged(timestamp());
|
|
1794
|
+
}
|
|
1795
|
+
});
|
|
1796
|
+
return lastChanged;
|
|
1797
|
+
}
|
|
1798
|
+
//#endregion
|
|
1799
|
+
//#region useListener/index.tsx
|
|
1800
|
+
/**
|
|
1801
|
+
* React port of the `useListener` protocol — bind a callback to an event
|
|
1802
|
+
* registration function returned by a reause hook, with automatic cleanup
|
|
1803
|
+
* on unmount.
|
|
1804
|
+
*
|
|
1805
|
+
* Map from @reause/shared `useListener` (protocol: #129)
|
|
1806
|
+
* Motivation: hooks like `useFileDialog` return `onChange` / `onCancel`
|
|
1807
|
+
* registration functions (upstream `EventHookOn`). In Vue those auto-clean
|
|
1808
|
+
* via the effect scope; in React we need a hook to own that lifecycle.
|
|
1809
|
+
* `useListener` registers `cb` with `on` on mount and, when `on` returns an
|
|
1810
|
+
* `off` function, calls it on unmount, so listeners are cleaned up and
|
|
1811
|
+
* callbacks never fire after the component is gone. (An `on` that returns
|
|
1812
|
+
* nothing provides no cleanup — nothing can be released.) The callback is
|
|
1813
|
+
* kept in a ref, so changing `cb` across renders does not re-register — the
|
|
1814
|
+
* latest callback is used by the already-registered listener. If `on` itself
|
|
1815
|
+
* changes (a new hook instance), the effect re-runs and re-registers.
|
|
1816
|
+
*
|
|
1817
|
+
* @example
|
|
1818
|
+
* const { files, open, onChange } = useFileDialog()
|
|
1819
|
+
* useListener(onChange, (files) => { console.log(files) })
|
|
1820
|
+
*/
|
|
1821
|
+
function useListener(on, cb) {
|
|
1822
|
+
const cbRef = (0, react.useRef)(cb);
|
|
1823
|
+
cbRef.current = cb;
|
|
1824
|
+
(0, react.useEffect)(() => {
|
|
1825
|
+
if (typeof on !== "function") return;
|
|
1826
|
+
const result = on(((...args) => cbRef.current(...args)));
|
|
1827
|
+
return () => {
|
|
1828
|
+
var _result$off;
|
|
1829
|
+
result === null || result === void 0 || (_result$off = result.off) === null || _result$off === void 0 || _result$off.call(result);
|
|
1830
|
+
};
|
|
1831
|
+
}, [on]);
|
|
1832
|
+
}
|
|
1833
|
+
//#endregion
|
|
1834
|
+
//#region useMount/index.tsx
|
|
1835
|
+
/**
|
|
1836
|
+
* React port of react-use's `useMount`.
|
|
1837
|
+
*
|
|
1838
|
+
* Map from react-use `useMount`.
|
|
1839
|
+
* Runs `fn` exactly once after the component mounts.
|
|
1840
|
+
*
|
|
1841
|
+
* @example
|
|
1842
|
+
* useMount(() => {
|
|
1843
|
+
* trackPageView()
|
|
1844
|
+
* })
|
|
1845
|
+
*/
|
|
1846
|
+
function useMount(fn) {
|
|
1847
|
+
(0, react.useEffect)(() => {
|
|
1848
|
+
fn();
|
|
1849
|
+
}, []);
|
|
1850
|
+
}
|
|
1851
|
+
//#endregion
|
|
1852
|
+
//#region useStateAutoReset/index.tsx
|
|
1853
|
+
/**
|
|
1854
|
+
* A state which will be reset to the default value after some time.
|
|
1855
|
+
*
|
|
1856
|
+
* Map from @vueuse/shared `refAutoReset`
|
|
1857
|
+
* (`source/vueuse/packages/shared/refAutoReset/`). Upstream returns a single
|
|
1858
|
+
* writable Vue ref; per this repo's `useState*` family convention the return
|
|
1859
|
+
* is the React `[value, setValue]` tuple — `value` is the state, `setValue`
|
|
1860
|
+
* is a `useState`-style setter (value or updater form, `Dispatch<SetStateAction>`)
|
|
1861
|
+
* that also (re)schedules a timer to restore `defaultValue` after `afterMs`
|
|
1862
|
+
* milliseconds. `defaultValue` accepts the shared `State<T>` form (plain value,
|
|
1863
|
+
* lazy getter, ref-like object, state tuple, or controlled `{ value, onChange }` pair).
|
|
1864
|
+
* `afterMs` accepts the shared `RefOrValue<number>` form and is resolved with `toValue` at fire time
|
|
1865
|
+
* (upstream: `toValue`); the pending timer is cleared on unmount (upstream:
|
|
1866
|
+
* `tryOnScopeDispose`, timers in the effect scope). The deprecated `autoResetRef`
|
|
1867
|
+
* alias is not ported.
|
|
1868
|
+
*
|
|
1869
|
+
* @param defaultValue The value which will be set.
|
|
1870
|
+
* @param afterMs A zero-or-greater delay in milliseconds.
|
|
1871
|
+
* @example
|
|
1872
|
+
* const [message, setMessage] = useStateAutoReset('default message', 1000)
|
|
1873
|
+
*
|
|
1874
|
+
* function handleMessage() {
|
|
1875
|
+
* setMessage('message has set') // resets to 'default message' after 1000ms
|
|
1876
|
+
* }
|
|
1877
|
+
*/
|
|
1878
|
+
function useStateAutoReset(defaultValue, afterMs = 1e4) {
|
|
1879
|
+
const [value, setValue] = useControllableState(defaultValue, { passive: true });
|
|
1880
|
+
const defaultValueRef = (0, react.useRef)(defaultValue);
|
|
1881
|
+
const afterMsRef = (0, react.useRef)(afterMs);
|
|
1882
|
+
const timerRef = (0, react.useRef)(null);
|
|
1883
|
+
defaultValueRef.current = defaultValue;
|
|
1884
|
+
afterMsRef.current = afterMs;
|
|
1885
|
+
const scheduleReset = (0, react.useCallback)(() => {
|
|
1886
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
1887
|
+
timerRef.current = setTimeout(() => {
|
|
1888
|
+
timerRef.current = null;
|
|
1889
|
+
setValue(toValue(defaultValueRef.current));
|
|
1890
|
+
}, toValue(afterMsRef.current));
|
|
1891
|
+
}, []);
|
|
1892
|
+
const setValueWithReset = (0, react.useCallback)((next) => {
|
|
1893
|
+
setValue(next);
|
|
1894
|
+
scheduleReset();
|
|
1895
|
+
}, [scheduleReset]);
|
|
1896
|
+
(0, react.useEffect)(() => () => {
|
|
1897
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
1898
|
+
}, []);
|
|
1899
|
+
return [value, setValueWithReset];
|
|
1900
|
+
}
|
|
1901
|
+
//#endregion
|
|
1902
|
+
//#region useStateDebounced/index.tsx
|
|
1903
|
+
/**
|
|
1904
|
+
* Debounce updates of a state value — React port of VueUse's `refDebounced`.
|
|
1905
|
+
*
|
|
1906
|
+
* Map from @vueuse/shared `refDebounced`
|
|
1907
|
+
* Mapping: upstream takes a Vue `Ref<T>` and returns a readonly ref that only
|
|
1908
|
+
* flips to the latest source value once it stops changing for `ms` (a watcher
|
|
1909
|
+
* hands every change to `useDebounceFn`). The naming follows this repo's
|
|
1910
|
+
* `ref* → useState*` rule (`refDebounced` → `useStateDebounced`), the Vue
|
|
1911
|
+
* `Ref<T>` input becomes a plain initial value, and the readonly ref becomes
|
|
1912
|
+
* an extra state slot — so the hook returns the tuple
|
|
1913
|
+
* `[value, setValue, debounced]`:
|
|
1914
|
+
*
|
|
1915
|
+
* ```ts
|
|
1916
|
+
* const [input, setInput, debounced] = useStateDebounced('foo', 1000)
|
|
1917
|
+
*
|
|
1918
|
+
* setInput('bar')
|
|
1919
|
+
* console.log(debounced) // 'foo' — flips to 'bar' once the debounce elapses
|
|
1920
|
+
* ```
|
|
1921
|
+
*
|
|
1922
|
+
* `value` is the source state, `setValue` its setter, and `debounced` lags
|
|
1923
|
+
* behind it by `ms`. Writes settle through a `useDebounceFn` updater, so a
|
|
1924
|
+
* burst of writes collapses into a single trailing update carrying the last
|
|
1925
|
+
* written value. `ms` (and `options.maxWait`) accept a plain number or a
|
|
1926
|
+
* ref-like `{ current }` (upstream: `RefOrValue<number>`) and are re-read on
|
|
1927
|
+
* every write; pending timers are cleared when the component unmounts
|
|
1928
|
+
* (upstream disposes with the effect scope). Note: a write only schedules the
|
|
1929
|
+
* debounce when the value actually changes — writing the same value is
|
|
1930
|
+
* skipped by `useControllableState`'s `Object.is` guard, so the pending timer
|
|
1931
|
+
* is not re-delayed (upstream's `watch` re-delays on every source write, even
|
|
1932
|
+
* unchanged ones).
|
|
1933
|
+
*
|
|
1934
|
+
* @example
|
|
1935
|
+
* ```ts
|
|
1936
|
+
* const [value, setValue, debounced] = useStateDebounced('foo', 1000)
|
|
1937
|
+
* ```
|
|
1938
|
+
*/
|
|
1939
|
+
function useStateDebounced(value, ms = 200, options = {}) {
|
|
1940
|
+
const [state, setState] = useControllableState(value, { passive: true });
|
|
1941
|
+
const [debounced, setDebounced] = (0, react.useState)(state);
|
|
1942
|
+
const stateRef = (0, react.useRef)(state);
|
|
1943
|
+
stateRef.current = state;
|
|
1944
|
+
const updater = useDebounceFn(() => {
|
|
1945
|
+
setDebounced(stateRef.current);
|
|
1946
|
+
}, ms, options);
|
|
1947
|
+
const isFirstRunRef = (0, react.useRef)(true);
|
|
1948
|
+
(0, react.useEffect)(() => {
|
|
1949
|
+
if (isFirstRunRef.current) {
|
|
1950
|
+
isFirstRunRef.current = false;
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
updater();
|
|
1954
|
+
}, [state, updater]);
|
|
1955
|
+
return [
|
|
1956
|
+
state,
|
|
1957
|
+
setState,
|
|
1958
|
+
debounced
|
|
1959
|
+
];
|
|
1960
|
+
}
|
|
1961
|
+
//#endregion
|
|
1962
|
+
//#region useStateDefault/index.tsx
|
|
1963
|
+
/**
|
|
1964
|
+
* A state tuple (`[value, setter]`) — mirrors `toValue`'s tuple branch.
|
|
1965
|
+
*/
|
|
1966
|
+
function isStateTuple(source) {
|
|
1967
|
+
return Array.isArray(source) && source.length === 2 && typeof source[1] === "function";
|
|
1968
|
+
}
|
|
1969
|
+
/**
|
|
1970
|
+
* A `{ value, onChange }` source — mirrors `toValue`'s object branch (the
|
|
1971
|
+
* `addEventListener` guard keeps DOM-ish objects out, like `toValue`).
|
|
1972
|
+
*/
|
|
1973
|
+
function isObjectState(source) {
|
|
1974
|
+
return typeof source === "object" && source !== null && !Array.isArray(source) && "value" in source && !("addEventListener" in source);
|
|
1975
|
+
}
|
|
1976
|
+
/**
|
|
1977
|
+
* A ref-like `{ current }` source.
|
|
1978
|
+
*/
|
|
1979
|
+
function isRefState(source) {
|
|
1980
|
+
return source !== null && source !== void 0 && typeof source === "object" && "current" in source;
|
|
1981
|
+
}
|
|
1982
|
+
/**
|
|
1983
|
+
* Apply default value to a ref-like source — React port of VueUse's
|
|
1984
|
+
* `refDefault` renamed to `useStateDefault` (this repo's naming for the
|
|
1985
|
+
* `ref*` family; upstream's single writable computed ref becomes a tuple).
|
|
1986
|
+
*
|
|
1987
|
+
* Map from @vueuse/shared `refDefault`
|
|
1988
|
+
* Mapping: upstream derives a writable `computed` from a source
|
|
1989
|
+
* `Ref<T | undefined | null>` — it reads `source.value ?? defaultValue` and
|
|
1990
|
+
* writes back to `source.value`. This port accepts a `State<T | undefined |
|
|
1991
|
+
* null>` — a plain value, a ref-like object (`{ current }`, e.g. the first
|
|
1992
|
+
* tuple element of `useStorage`), a getter, a `[value, setter]` tuple or a
|
|
1993
|
+
* `{ value, onChange }` pair — and returns the React tuple
|
|
1994
|
+
* `const [value, setValue] = useStateDefault(raw, 'default')`. `value` is
|
|
1995
|
+
* derived on every render from the source through `toValue` (`source.current
|
|
1996
|
+
* ?? defaultValue`), so it always reflects the source's current value —
|
|
1997
|
+
* including writes made from outside the component; `setValue` resolves the
|
|
1998
|
+
* next value (value or updater form), writes it through to the source (its
|
|
1999
|
+
* `current`, its setter or its `onChange`) and bumps a local version counter
|
|
2000
|
+
* so the derived `value` re-renders. SSR-safe: nothing touches the DOM and the
|
|
2001
|
+
* first server render already shows the default.
|
|
2002
|
+
*
|
|
2003
|
+
* @param source The `State<T | undefined | null>` source holding the
|
|
2004
|
+
* value — read through `toValue` on every render and
|
|
2005
|
+
* written back to `current` / the tuple setter /
|
|
2006
|
+
* `onChange` on `setValue`.
|
|
2007
|
+
* @param defaultValue The value displayed while the source is `null` or
|
|
2008
|
+
* `undefined`.
|
|
2009
|
+
* @return A tuple `[value, setValue]` — the current value (source value or
|
|
2010
|
+
* `defaultValue`) and its setter.
|
|
2011
|
+
*
|
|
2012
|
+
* @example
|
|
2013
|
+
* const raw = { current: undefined as string | undefined }
|
|
2014
|
+
* const [value, setValue] = useStateDefault(raw, 'default')
|
|
2015
|
+
*
|
|
2016
|
+
* setValue('hello')
|
|
2017
|
+
* console.log(value) // 'hello' after the next render (React derives at render)
|
|
2018
|
+
*
|
|
2019
|
+
* setValue(undefined)
|
|
2020
|
+
* console.log(value) // 'default' after the next render
|
|
2021
|
+
*/
|
|
2022
|
+
function useStateDefault(source, defaultValue) {
|
|
2023
|
+
var _toValue;
|
|
2024
|
+
const sourceRef = (0, react.useRef)(source);
|
|
2025
|
+
sourceRef.current = source;
|
|
2026
|
+
const [, setVersion] = (0, react.useState)(0);
|
|
2027
|
+
const bump = () => {
|
|
2028
|
+
setVersion((current) => current + 1);
|
|
2029
|
+
};
|
|
2030
|
+
const setValue = (0, react.useCallback)((next) => {
|
|
2031
|
+
const current = toValue(sourceRef.current);
|
|
2032
|
+
const resolved = typeof next === "function" ? next(current) : next;
|
|
2033
|
+
const currentSource = sourceRef.current;
|
|
2034
|
+
if (isStateTuple(currentSource)) {
|
|
2035
|
+
currentSource[1](resolved);
|
|
2036
|
+
bump();
|
|
2037
|
+
} else if (isObjectState(currentSource)) {
|
|
2038
|
+
var _currentSource$onChan;
|
|
2039
|
+
(_currentSource$onChan = currentSource.onChange) === null || _currentSource$onChan === void 0 || _currentSource$onChan.call(currentSource, resolved);
|
|
2040
|
+
bump();
|
|
2041
|
+
} else if (isRefState(currentSource)) {
|
|
2042
|
+
currentSource.current = resolved;
|
|
2043
|
+
bump();
|
|
2044
|
+
}
|
|
2045
|
+
}, []);
|
|
2046
|
+
return [(_toValue = toValue(source)) !== null && _toValue !== void 0 ? _toValue : defaultValue, setValue];
|
|
2047
|
+
}
|
|
2048
|
+
//#endregion
|
|
2049
|
+
//#region useStateManualReset/index.tsx
|
|
2050
|
+
/**
|
|
2051
|
+
* A controlled source — a `[value, setter]` tuple or a `{ value, onChange }`
|
|
2052
|
+
* object. These have no stored default of their own: `toValue` returns the
|
|
2053
|
+
* live value, so resetting to it would be a no-op. Reset therefore restores
|
|
2054
|
+
* the initial argument value instead.
|
|
2055
|
+
*/
|
|
2056
|
+
function isControlledSource$1(state) {
|
|
2057
|
+
return Array.isArray(state) && state.length === 2 && typeof state[1] === "function" || typeof state === "object" && state !== null && !Array.isArray(state) && "value" in state;
|
|
2058
|
+
}
|
|
2059
|
+
/**
|
|
2060
|
+
* React port of VueUse's `refManualReset`.
|
|
2061
|
+
*
|
|
2062
|
+
* Map from @vueuse/shared `refManualReset`
|
|
2063
|
+
* (`source/vueuse/packages/shared/refManualReset/`). Create a state with
|
|
2064
|
+
* manual reset functionality — any update can be reverted back to the initial
|
|
2065
|
+
* value with the returned `reset` function.
|
|
2066
|
+
*
|
|
2067
|
+
* Upstream returns a writable Vue `Ref<T>` extended with a `reset` method
|
|
2068
|
+
* (built on `customRef`). Per this repo's naming rules the port is renamed to
|
|
2069
|
+
* `useStateManualReset` and the ref becomes a `[value, setValue, reset]`
|
|
2070
|
+
* tuple: the second element is the plain `useState` setter (value or updater
|
|
2071
|
+
* form), and `reset` restores the default value.
|
|
2072
|
+
*
|
|
2073
|
+
* The state input accepts the shared `State<T>` form: a value, getter, ref-like
|
|
2074
|
+
* object, state tuple, or controlled `{ value, onChange }` object. `reset`
|
|
2075
|
+
* re-reads the input on every call, so plain, getter and ref-like sources
|
|
2076
|
+
* reset to the latest source value (matching upstream's
|
|
2077
|
+
* `value = toValue(defaultValue)`); tuple / `{ value, onChange }` (controlled)
|
|
2078
|
+
* sources have no stored default, so they restore the initial argument value.
|
|
2079
|
+
*
|
|
2080
|
+
* @example
|
|
2081
|
+
* const [message, setMessage, resetMessage] = useStateManualReset('default message')
|
|
2082
|
+
* setMessage('message has set')
|
|
2083
|
+
* resetMessage()
|
|
2084
|
+
* console.log(message) // 'default message'
|
|
2085
|
+
*/
|
|
2086
|
+
function useStateManualReset(value) {
|
|
2087
|
+
const valueRef = (0, react.useRef)(value);
|
|
2088
|
+
valueRef.current = value;
|
|
2089
|
+
const initialDefaultRef = (0, react.useRef)(toValue(value));
|
|
2090
|
+
const [state, setState] = useControllableState(value, { passive: true });
|
|
2091
|
+
return [
|
|
2092
|
+
state,
|
|
2093
|
+
setState,
|
|
2094
|
+
(0, react.useCallback)(() => {
|
|
2095
|
+
const current = valueRef.current;
|
|
2096
|
+
setState(isControlledSource$1(current) ? initialDefaultRef.current : toValue(current));
|
|
2097
|
+
}, [])
|
|
2098
|
+
];
|
|
2099
|
+
}
|
|
2100
|
+
//#endregion
|
|
2101
|
+
//#region useThrottleFn/index.tsx
|
|
2102
|
+
/**
|
|
2103
|
+
* Throttle execution of a function — React port of VueUse's `useThrottleFn`.
|
|
2104
|
+
* Especially useful for rate limiting execution of handlers on events like
|
|
2105
|
+
* resize and scroll.
|
|
2106
|
+
*
|
|
2107
|
+
* Map from @vueuse/shared `useThrottleFn`
|
|
2108
|
+
* Mapping: upstream builds `createFilterWrapper(throttleFilter(ms, trailing,
|
|
2109
|
+
* leading, rejectOnCancel), fn)` and returns a plain `PromisifyFn<T>` — the
|
|
2110
|
+
* throttled wrapper carries no `cancel` / `flush` / `isPending` (unlike the
|
|
2111
|
+
* debounce filter, upstream's `throttleFilter` is not cancelable), so this
|
|
2112
|
+
* port mirrors that: the return value is the wrapped function and nothing
|
|
2113
|
+
* more. The wrapper is built once (`useMemo`) so its identity is stable
|
|
2114
|
+
* across renders — safe to add/remove in effects; the latest `fn` / `ms` /
|
|
2115
|
+
* `trailing` / `leading` / `rejectOnCancel` are mirrored into refs so every
|
|
2116
|
+
* call sees fresh values (upstream captures the flags once, at filter
|
|
2117
|
+
* creation). `ms` accepts a number or a ref-like `{ current: number }`
|
|
2118
|
+
* (upstream: `RefOrValue<number>`) and is re-read on every call. The
|
|
2119
|
+
* throttle filter logic is inlined (upstream: `utils/filters.ts`
|
|
2120
|
+
* `throttleFilter` — leading/trailing timestamps with a trailing invoke on
|
|
2121
|
+
* window end). The wrapper is cleaned up on unmount: any pending trailing
|
|
2122
|
+
* timer is cleared when the component unmounts — a React hygiene measure;
|
|
2123
|
+
* upstream registers no disposal at all (`@__NO_SIDE_EFFECTS__`), so a
|
|
2124
|
+
* pending call would still fire there after teardown.
|
|
2125
|
+
*
|
|
2126
|
+
* @param fn A function to be executed after delay milliseconds. The `this` context and all arguments are passed through, as-is,
|
|
2127
|
+
* to `callback` when the throttled-function is executed.
|
|
2128
|
+
* @param ms A zero-or-greater delay in milliseconds. For event callbacks, values around 100 or 250 (or even higher) are most useful.
|
|
2129
|
+
* (default value: 200)
|
|
2130
|
+
*
|
|
2131
|
+
* @param [trailing] if true, call fn again after the time is up (default value: true)
|
|
2132
|
+
*
|
|
2133
|
+
* @param [leading] if true, call fn on the leading edge of the ms timeout (default value: true)
|
|
2134
|
+
*
|
|
2135
|
+
* @param [rejectOnCancel] if true, reject the last call if it's been cancel (default value: false)
|
|
2136
|
+
*
|
|
2137
|
+
* @return A new, throttled, function.
|
|
2138
|
+
*
|
|
2139
|
+
* @example
|
|
2140
|
+
* const throttledFn = useThrottleFn(() => { ... }, 1000)
|
|
2141
|
+
* throttledFn()
|
|
2142
|
+
*/
|
|
2143
|
+
function useThrottleFn(fn, ms = 200, trailing = true, leading = true, rejectOnCancel = false) {
|
|
2144
|
+
const fnRef = (0, react.useRef)(fn);
|
|
2145
|
+
fnRef.current = fn;
|
|
2146
|
+
const msRef = (0, react.useRef)(ms);
|
|
2147
|
+
msRef.current = ms;
|
|
2148
|
+
const trailingRef = (0, react.useRef)(trailing);
|
|
2149
|
+
trailingRef.current = trailing;
|
|
2150
|
+
const leadingRef = (0, react.useRef)(leading);
|
|
2151
|
+
leadingRef.current = leading;
|
|
2152
|
+
const rejectOnCancelRef = (0, react.useRef)(rejectOnCancel);
|
|
2153
|
+
rejectOnCancelRef.current = rejectOnCancel;
|
|
2154
|
+
const { throttled, clear } = (0, react.useMemo)(() => {
|
|
2155
|
+
let lastExec = 0;
|
|
2156
|
+
let timer;
|
|
2157
|
+
let isLeading = true;
|
|
2158
|
+
let lastRejector = noop;
|
|
2159
|
+
let lastValue;
|
|
2160
|
+
const clear = () => {
|
|
2161
|
+
if (timer !== void 0) {
|
|
2162
|
+
clearTimeout(timer);
|
|
2163
|
+
timer = void 0;
|
|
2164
|
+
lastRejector();
|
|
2165
|
+
lastRejector = noop;
|
|
2166
|
+
}
|
|
2167
|
+
};
|
|
2168
|
+
const handler = (_invoke) => {
|
|
2169
|
+
const duration = toValue(msRef.current);
|
|
2170
|
+
const elapsed = Date.now() - lastExec;
|
|
2171
|
+
const invoke = () => {
|
|
2172
|
+
return lastValue = _invoke();
|
|
2173
|
+
};
|
|
2174
|
+
clear();
|
|
2175
|
+
if (duration === void 0 || duration <= 0) {
|
|
2176
|
+
lastExec = Date.now();
|
|
2177
|
+
return invoke();
|
|
2178
|
+
}
|
|
2179
|
+
if (elapsed > duration) {
|
|
2180
|
+
lastExec = Date.now();
|
|
2181
|
+
if (leadingRef.current || !isLeading) invoke();
|
|
2182
|
+
} else if (trailingRef.current) lastValue = new Promise((resolve, reject) => {
|
|
2183
|
+
lastRejector = rejectOnCancelRef.current ? reject : resolve;
|
|
2184
|
+
timer = setTimeout(() => {
|
|
2185
|
+
lastExec = Date.now();
|
|
2186
|
+
isLeading = true;
|
|
2187
|
+
resolve(invoke());
|
|
2188
|
+
clear();
|
|
2189
|
+
}, Math.max(0, duration - elapsed));
|
|
2190
|
+
});
|
|
2191
|
+
if (!leadingRef.current && timer === void 0) timer = setTimeout(() => {
|
|
2192
|
+
isLeading = true;
|
|
2193
|
+
}, duration);
|
|
2194
|
+
isLeading = false;
|
|
2195
|
+
return lastValue;
|
|
2196
|
+
};
|
|
2197
|
+
const throttled = ((...args) => {
|
|
2198
|
+
return new Promise((resolve, reject) => {
|
|
2199
|
+
Promise.resolve(handler(() => fnRef.current(...args))).then(resolve).catch(reject);
|
|
2200
|
+
});
|
|
2201
|
+
});
|
|
2202
|
+
return {
|
|
2203
|
+
throttled,
|
|
2204
|
+
clear
|
|
2205
|
+
};
|
|
2206
|
+
}, []);
|
|
2207
|
+
(0, react.useEffect)(() => () => clear(), [clear]);
|
|
2208
|
+
return throttled;
|
|
2209
|
+
}
|
|
2210
|
+
//#endregion
|
|
2211
|
+
//#region useStateThrottled/index.tsx
|
|
2212
|
+
/**
|
|
2213
|
+
* Throttle changing of a state value — React port of VueUse's `refThrottled`.
|
|
2214
|
+
*
|
|
2215
|
+
* The `value` argument accepts any `State<T>` supported by
|
|
2216
|
+
* `useControllableState`: a plain value, lazy initializer, controlled tuple,
|
|
2217
|
+
* or `{ value, onChange }` source. The returned tuple contains the current
|
|
2218
|
+
* value, its setter, and a throttled mirror.
|
|
2219
|
+
*
|
|
2220
|
+
* A `delay <= 0` short-circuits like upstream (`if (delay <= 0) return value`):
|
|
2221
|
+
* the throttled element is the input itself — no throttling, no timers.
|
|
2222
|
+
*
|
|
2223
|
+
* @param value State source accepted by `useControllableState`.
|
|
2224
|
+
* @param delay Delay in milliseconds between commits (default: 200).
|
|
2225
|
+
* @param trailing Whether to commit the latest value after the window (default: true).
|
|
2226
|
+
* @param leading Whether to commit on the leading edge (default: true).
|
|
2227
|
+
*/
|
|
2228
|
+
function useStateThrottled(value, delay = 200, trailing = true, leading = true) {
|
|
2229
|
+
const [input, setInput] = useControllableState(value, { passive: true });
|
|
2230
|
+
const [throttled, setThrottled] = (0, react.useState)(input);
|
|
2231
|
+
const inputRef = (0, react.useRef)(input);
|
|
2232
|
+
inputRef.current = input;
|
|
2233
|
+
const delayRef = (0, react.useRef)(delay);
|
|
2234
|
+
delayRef.current = delay;
|
|
2235
|
+
const throttledFn = useThrottleFn(() => {
|
|
2236
|
+
setThrottled(inputRef.current);
|
|
2237
|
+
}, delay, trailing, leading);
|
|
2238
|
+
const isFirstRunRef = (0, react.useRef)(true);
|
|
2239
|
+
(0, react.useEffect)(() => {
|
|
2240
|
+
if (isFirstRunRef.current) {
|
|
2241
|
+
isFirstRunRef.current = false;
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
if (delayRef.current > 0) throttledFn();
|
|
2245
|
+
}, [input, throttledFn]);
|
|
2246
|
+
if (delay <= 0) return [
|
|
2247
|
+
input,
|
|
2248
|
+
setInput,
|
|
2249
|
+
input
|
|
2250
|
+
];
|
|
2251
|
+
return [
|
|
2252
|
+
input,
|
|
2253
|
+
setInput,
|
|
2254
|
+
throttled
|
|
2255
|
+
];
|
|
2256
|
+
}
|
|
2257
|
+
//#endregion
|
|
2258
|
+
//#region useStateWithControl/index.tsx
|
|
2259
|
+
/**
|
|
2260
|
+
* A controlled source — a `[value, setter]` tuple or a `{ value, onChange }`
|
|
2261
|
+
* object — whose value is owned by the caller (re-rendered externally).
|
|
2262
|
+
*/
|
|
2263
|
+
function isControlledSource(state) {
|
|
2264
|
+
return Array.isArray(state) && state.length === 2 && typeof state[1] === "function" || typeof state === "object" && state !== null && !Array.isArray(state) && "value" in state;
|
|
2265
|
+
}
|
|
2266
|
+
/**
|
|
2267
|
+
* Fine-grained controls over a state and its re-renders — React port of
|
|
2268
|
+
* VueUse's `refWithControl`.
|
|
2269
|
+
*
|
|
2270
|
+
* Map from @vueuse/shared `refWithControl`
|
|
2271
|
+
* (`source/vueuse/packages/shared/refWithControl/`). Upstream returns a single
|
|
2272
|
+
* writable Vue `Ref` extended with `get` / `set` / `untrackedGet` /
|
|
2273
|
+
* `silentSet` / `peek` / `lay`. This port owns the state like a `useState` and
|
|
2274
|
+
* returns the React tuple `const [num, setNum, control] = useStateWithControl(0)`
|
|
2275
|
+
* — the name follows this repo's `ref*` → `useState*` mapping rule. `setNum`
|
|
2276
|
+
* behaves like a normal `setState` (value or updater form — the updater base
|
|
2277
|
+
* is the current internal value, which may be ahead of the rendered value
|
|
2278
|
+
* after a silent write), while `control`
|
|
2279
|
+
* keeps the fine-grained get/set pair: `set(value, false)` (and `lay` /
|
|
2280
|
+
* `silentSet`) updates the value without re-rendering (upstream: without
|
|
2281
|
+
* triggering reactivity), and `peek` / `untrackedGet` read it back — in React
|
|
2282
|
+
* there is no dependency tracking during render, so those are plain aliases
|
|
2283
|
+
* for the current value. `reset()` (a small addition, upstream has no
|
|
2284
|
+
* equivalent) restores the initial value and participates in the change
|
|
2285
|
+
* callbacks (`onBeforeChange` can dismiss it, `onChanged` fires when
|
|
2286
|
+
* accepted). Option names are kept from upstream:
|
|
2287
|
+
* `onBeforeChange` can dismiss a change by returning `false`, and `onChanged`
|
|
2288
|
+
* fires synchronously after an accepted change.
|
|
2289
|
+
*
|
|
2290
|
+
* @param state State source: a plain value, getter, ref-like value, state
|
|
2291
|
+
* tuple, or `{ value, onChange }` controllable state.
|
|
2292
|
+
* @param options
|
|
2293
|
+
* @return A tuple `[value, setValue, control]` — the current value, a
|
|
2294
|
+
* `setState`-like setter and the fine-grained control object.
|
|
2295
|
+
*
|
|
2296
|
+
* @example
|
|
2297
|
+
* const [num, setNum, control] = useStateWithControl(0)
|
|
2298
|
+
*
|
|
2299
|
+
* setNum(42) // just like a normal useState setter
|
|
2300
|
+
* control.set(30, false) // set the value without re-rendering
|
|
2301
|
+
* control.peek() // get the value without tracking
|
|
2302
|
+
*/
|
|
2303
|
+
function useStateWithControl(state, options = {}) {
|
|
2304
|
+
const { onBeforeChange, onChanged } = options;
|
|
2305
|
+
const initialRef = (0, react.useRef)(toValue(state));
|
|
2306
|
+
const sourceRef = (0, react.useRef)(initialRef.current);
|
|
2307
|
+
const [value, setState] = useControllableState(state, { passive: true });
|
|
2308
|
+
const lastTriggeredRef = (0, react.useRef)(void 0);
|
|
2309
|
+
if (isControlledSource(state) && !Object.is(value, lastTriggeredRef.current)) {
|
|
2310
|
+
sourceRef.current = value;
|
|
2311
|
+
lastTriggeredRef.current = value;
|
|
2312
|
+
}
|
|
2313
|
+
const callbacksRef = (0, react.useRef)({
|
|
2314
|
+
onBeforeChange,
|
|
2315
|
+
onChanged
|
|
2316
|
+
});
|
|
2317
|
+
callbacksRef.current = {
|
|
2318
|
+
onBeforeChange,
|
|
2319
|
+
onChanged
|
|
2320
|
+
};
|
|
2321
|
+
const set = (0, react.useCallback)((nextValue, triggering = true) => {
|
|
2322
|
+
var _callbacksRef$current, _callbacksRef$current2, _callbacksRef$current3, _callbacksRef$current4;
|
|
2323
|
+
if (nextValue === sourceRef.current) return;
|
|
2324
|
+
const old = sourceRef.current;
|
|
2325
|
+
if (((_callbacksRef$current = (_callbacksRef$current2 = callbacksRef.current).onBeforeChange) === null || _callbacksRef$current === void 0 ? void 0 : _callbacksRef$current.call(_callbacksRef$current2, nextValue, old)) === false) return;
|
|
2326
|
+
sourceRef.current = nextValue;
|
|
2327
|
+
(_callbacksRef$current3 = (_callbacksRef$current4 = callbacksRef.current).onChanged) === null || _callbacksRef$current3 === void 0 || _callbacksRef$current3.call(_callbacksRef$current4, nextValue, old);
|
|
2328
|
+
if (triggering) {
|
|
2329
|
+
lastTriggeredRef.current = nextValue;
|
|
2330
|
+
setState(nextValue);
|
|
2331
|
+
}
|
|
2332
|
+
}, []);
|
|
2333
|
+
const get = (0, react.useCallback)((_tracking = true) => {
|
|
2334
|
+
return sourceRef.current;
|
|
2335
|
+
}, []);
|
|
2336
|
+
const untrackedGet = (0, react.useCallback)(() => get(false), [get]);
|
|
2337
|
+
const silentSet = (0, react.useCallback)((nextValue) => set(nextValue, false), [set]);
|
|
2338
|
+
const peek = (0, react.useCallback)(() => get(false), [get]);
|
|
2339
|
+
const lay = (0, react.useCallback)((nextValue) => set(nextValue, false), [set]);
|
|
2340
|
+
const reset = (0, react.useCallback)(() => set(initialRef.current), [set]);
|
|
2341
|
+
return [
|
|
2342
|
+
value,
|
|
2343
|
+
(0, react.useCallback)((nextValue) => {
|
|
2344
|
+
const resolved = typeof nextValue === "function" ? nextValue(sourceRef.current) : nextValue;
|
|
2345
|
+
set(resolved);
|
|
2346
|
+
}, [set]),
|
|
2347
|
+
{
|
|
2348
|
+
get,
|
|
2349
|
+
set,
|
|
2350
|
+
untrackedGet,
|
|
2351
|
+
silentSet,
|
|
2352
|
+
peek,
|
|
2353
|
+
lay,
|
|
2354
|
+
reset
|
|
2355
|
+
}
|
|
2356
|
+
];
|
|
2357
|
+
}
|
|
2358
|
+
//#endregion
|
|
2359
|
+
//#region useTimeout/index.tsx
|
|
2360
|
+
function useTimeout(interval = 1e3, options = {}) {
|
|
2361
|
+
const { controls: exposeControls = false, callback, immediate = true, immediateCallback = false } = options;
|
|
2362
|
+
const [isPending, setIsPending] = (0, react.useState)(immediate);
|
|
2363
|
+
const intervalRef = (0, react.useRef)(interval);
|
|
2364
|
+
intervalRef.current = interval;
|
|
2365
|
+
const callbackRef = (0, react.useRef)(callback);
|
|
2366
|
+
callbackRef.current = callback;
|
|
2367
|
+
const immediateCallbackRef = (0, react.useRef)(immediateCallback);
|
|
2368
|
+
immediateCallbackRef.current = immediateCallback;
|
|
2369
|
+
const timerRef = (0, react.useRef)(null);
|
|
2370
|
+
const stop = (0, react.useCallback)(() => {
|
|
2371
|
+
if (timerRef.current !== null) {
|
|
2372
|
+
clearTimeout(timerRef.current);
|
|
2373
|
+
timerRef.current = null;
|
|
2374
|
+
}
|
|
2375
|
+
setIsPending(false);
|
|
2376
|
+
}, []);
|
|
2377
|
+
const start = (0, react.useCallback)(() => {
|
|
2378
|
+
var _callbackRef$current;
|
|
2379
|
+
if (immediateCallbackRef.current) (_callbackRef$current = callbackRef.current) === null || _callbackRef$current === void 0 || _callbackRef$current.call(callbackRef);
|
|
2380
|
+
if (timerRef.current !== null) {
|
|
2381
|
+
clearTimeout(timerRef.current);
|
|
2382
|
+
timerRef.current = null;
|
|
2383
|
+
}
|
|
2384
|
+
setIsPending(true);
|
|
2385
|
+
const delay = toValue(intervalRef.current);
|
|
2386
|
+
timerRef.current = setTimeout(() => {
|
|
2387
|
+
var _callbackRef$current2;
|
|
2388
|
+
timerRef.current = null;
|
|
2389
|
+
setIsPending(false);
|
|
2390
|
+
(_callbackRef$current2 = callbackRef.current) === null || _callbackRef$current2 === void 0 || _callbackRef$current2.call(callbackRef);
|
|
2391
|
+
}, delay);
|
|
2392
|
+
}, []);
|
|
2393
|
+
(0, react.useEffect)(() => {
|
|
2394
|
+
if (immediate) start();
|
|
2395
|
+
return stop;
|
|
2396
|
+
}, []);
|
|
2397
|
+
const ready = !isPending;
|
|
2398
|
+
if (exposeControls) return {
|
|
2399
|
+
ready,
|
|
2400
|
+
isPending,
|
|
2401
|
+
start,
|
|
2402
|
+
stop
|
|
2403
|
+
};
|
|
2404
|
+
return ready;
|
|
2405
|
+
}
|
|
2406
|
+
//#endregion
|
|
2407
|
+
//#region useTimeoutFn/index.tsx
|
|
2408
|
+
/**
|
|
2409
|
+
* React port of VueUse's `useTimeoutFn` — wrapper for `setTimeout` with
|
|
2410
|
+
* controls.
|
|
2411
|
+
*
|
|
2412
|
+
* Map from @vueuse/shared `useTimeoutFn`
|
|
2413
|
+
* Mapping: upstream accepts `RefOrValue<number>` for the interval — this
|
|
2414
|
+
* port accepts a plain `number`. `isPending` becomes a boolean state
|
|
2415
|
+
* (upstream: a readonly shallow ref) that starts `false` and is set inside
|
|
2416
|
+
* the mount effect — like upstream's `shallowRef(false)` + `isClient` gate,
|
|
2417
|
+
* the server render does not report pending. `immediateCallback` runs the
|
|
2418
|
+
* callback synchronously on `start` (before the timer is armed). The timer
|
|
2419
|
+
* is scheduled in a mount effect (upstream starts synchronously during
|
|
2420
|
+
* setup) and a pending timer is cleared on unmount via effect cleanup. The
|
|
2421
|
+
* latest callback and interval are kept in refs so restarts always use the
|
|
2422
|
+
* newest ones.
|
|
2423
|
+
*
|
|
2424
|
+
* @example
|
|
2425
|
+
* const { isPending, start, stop } = useTimeoutFn(() => { ... }, 3000)
|
|
2426
|
+
*/
|
|
2427
|
+
function useTimeoutFn(cb, interval, options = {}) {
|
|
2428
|
+
const { immediate = true, immediateCallback = false } = options;
|
|
2429
|
+
const [isPending, setIsPending] = (0, react.useState)(false);
|
|
2430
|
+
const cbRef = (0, react.useRef)(cb);
|
|
2431
|
+
const intervalRef = (0, react.useRef)(interval);
|
|
2432
|
+
const immediateCallbackRef = (0, react.useRef)(immediateCallback);
|
|
2433
|
+
const timerRef = (0, react.useRef)(null);
|
|
2434
|
+
cbRef.current = cb;
|
|
2435
|
+
intervalRef.current = interval;
|
|
2436
|
+
immediateCallbackRef.current = immediateCallback;
|
|
2437
|
+
function clear() {
|
|
2438
|
+
if (timerRef.current) {
|
|
2439
|
+
clearTimeout(timerRef.current);
|
|
2440
|
+
timerRef.current = null;
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
const stop = (0, react.useCallback)(() => {
|
|
2444
|
+
setIsPending(false);
|
|
2445
|
+
clear();
|
|
2446
|
+
}, []);
|
|
2447
|
+
const start = (0, react.useCallback)((...args) => {
|
|
2448
|
+
if (immediateCallbackRef.current) cbRef.current();
|
|
2449
|
+
clear();
|
|
2450
|
+
setIsPending(true);
|
|
2451
|
+
timerRef.current = setTimeout(() => {
|
|
2452
|
+
timerRef.current = null;
|
|
2453
|
+
setIsPending(false);
|
|
2454
|
+
cbRef.current(...args);
|
|
2455
|
+
}, intervalRef.current);
|
|
2456
|
+
}, []);
|
|
2457
|
+
(0, react.useEffect)(() => {
|
|
2458
|
+
if (immediate) start();
|
|
2459
|
+
return () => {
|
|
2460
|
+
clear();
|
|
2461
|
+
};
|
|
2462
|
+
}, [immediate, start]);
|
|
2463
|
+
return {
|
|
2464
|
+
isPending,
|
|
2465
|
+
start,
|
|
2466
|
+
stop
|
|
2467
|
+
};
|
|
2468
|
+
}
|
|
2469
|
+
//#endregion
|
|
2470
|
+
//#region useToggle/index.tsx
|
|
2471
|
+
/**
|
|
2472
|
+
* React port of VueUse's `useToggle` — a toggler between a truthy and a falsy
|
|
2473
|
+
* value, both configurable.
|
|
2474
|
+
*
|
|
2475
|
+
* Map from @vueuse/shared `useToggle`
|
|
2476
|
+
* Mapping: `ref(initialValue)` → `useControllableState(initialValue)`,
|
|
2477
|
+
* `toggle()` → stable `useCallback`; accepts the full `State<T>` input.
|
|
2478
|
+
* `truthyValue` / `falsyValue` are plain values (upstream: `MaybeRefOrGetter` —
|
|
2479
|
+
* reactive refs/getters are not supported, see `RefOrValue`). Upstream's
|
|
2480
|
+
* `toggle` returns the new value synchronously; React state updates are async,
|
|
2481
|
+
* so here `toggle` is `() => void` and the new value is read from `value` on
|
|
2482
|
+
* the next render. Like upstream, a bare `toggle()` flips between
|
|
2483
|
+
* `truthyValue` and `falsyValue`, `toggle(value)` (including an explicit
|
|
2484
|
+
* `undefined`) forces the value, and a function argument is applied as a
|
|
2485
|
+
* functional update (React adaptation).
|
|
2486
|
+
*
|
|
2487
|
+
* @example
|
|
2488
|
+
* const [value, toggle] = useToggle()
|
|
2489
|
+
* toggle() // false → true
|
|
2490
|
+
* toggle(false) // force to false
|
|
2491
|
+
*
|
|
2492
|
+
* const [status, toggleStatus] = useToggle('on', { truthyValue: 'on', falsyValue: 'off' })
|
|
2493
|
+
* toggleStatus() // 'on' → 'off'
|
|
2494
|
+
*/
|
|
2495
|
+
function useToggle(initialValue = false, options = {}) {
|
|
2496
|
+
const { truthyValue = true, falsyValue = false } = options;
|
|
2497
|
+
const [state, setState] = useControllableState(initialValue, { passive: true });
|
|
2498
|
+
const truthyRef = (0, react.useRef)(truthyValue);
|
|
2499
|
+
truthyRef.current = truthyValue;
|
|
2500
|
+
const falsyRef = (0, react.useRef)(falsyValue);
|
|
2501
|
+
falsyRef.current = falsyValue;
|
|
2502
|
+
return [state, (0, react.useCallback)((...args) => {
|
|
2503
|
+
const hasValue = args.length > 0;
|
|
2504
|
+
const value = args[0];
|
|
2505
|
+
setState((current) => {
|
|
2506
|
+
if (hasValue) {
|
|
2507
|
+
if (typeof value === "function") return value(current);
|
|
2508
|
+
return value;
|
|
2509
|
+
}
|
|
2510
|
+
return Object.is(current, truthyRef.current) ? falsyRef.current : truthyRef.current;
|
|
2511
|
+
});
|
|
2512
|
+
}, [setState])];
|
|
2513
|
+
}
|
|
2514
|
+
//#endregion
|
|
2515
|
+
//#region useToNumber/index.tsx
|
|
2516
|
+
/**
|
|
2517
|
+
* React port of VueUse's `useToNumber`.
|
|
2518
|
+
*
|
|
2519
|
+
* Map from @vueuse/shared `useToNumber`
|
|
2520
|
+
* Mapping: `ComputedRef<number>` → plain number recomputed from the current
|
|
2521
|
+
* value on every render (accepts `number | string`); no hook state needed.
|
|
2522
|
+
*
|
|
2523
|
+
* @__NO_SIDE_EFFECTS__
|
|
2524
|
+
* @example
|
|
2525
|
+
* useToNumber('123') // 123
|
|
2526
|
+
* useToNumber('0xFA', { method: 'parseInt', radix: 16 }) // 250
|
|
2527
|
+
*/
|
|
2528
|
+
function useToNumber(value, options = {}) {
|
|
2529
|
+
const { method = "parseFloat", radix, nanToZero } = options;
|
|
2530
|
+
let resolved = value;
|
|
2531
|
+
if (typeof method === "function") resolved = method(resolved);
|
|
2532
|
+
else if (typeof resolved === "string") resolved = Number[method](resolved, radix);
|
|
2533
|
+
if (nanToZero && Number.isNaN(resolved)) resolved = 0;
|
|
2534
|
+
return resolved;
|
|
2535
|
+
}
|
|
2536
|
+
//#endregion
|
|
2537
|
+
//#region useToString/index.tsx
|
|
2538
|
+
/**
|
|
2539
|
+
* React port of VueUse's `useToString`.
|
|
2540
|
+
*
|
|
2541
|
+
* Map from @vueuse/shared `useToString`
|
|
2542
|
+
* Mapping: VueUse wraps the template-literal coercion in `computed(() => ...)`
|
|
2543
|
+
* and accepts a `MaybeRefOrGetter`; React has no reactive value tracking, so
|
|
2544
|
+
* this is a plain function returning the stringified value directly.
|
|
2545
|
+
*
|
|
2546
|
+
* The `value` param is a PLAIN read-only value — pass `ref.current` or the
|
|
2547
|
+
* state value. Getter inputs are intentionally not supported (getters were
|
|
2548
|
+
* removed repo-wide); unlike upstream, a getter passed here is coerced as-is
|
|
2549
|
+
* (its source text), not invoked.
|
|
2550
|
+
*
|
|
2551
|
+
* @example
|
|
2552
|
+
* useToString(123.345) // '123.345'
|
|
2553
|
+
* useToString('hi') // 'hi'
|
|
2554
|
+
* useToString({ foo: 'hi' }) // '[object Object]'
|
|
2555
|
+
*/
|
|
2556
|
+
function useToString(value) {
|
|
2557
|
+
return `${value}`;
|
|
2558
|
+
}
|
|
2559
|
+
//#endregion
|
|
2560
|
+
//#region useUnmount/index.tsx
|
|
2561
|
+
/**
|
|
2562
|
+
* React port of react-use's `useUnmount`.
|
|
2563
|
+
*
|
|
2564
|
+
* Map from react-use `useUnmount`
|
|
2565
|
+
* Mapping: react-use's `useUnmount` keeps the callback in a `useRef`,
|
|
2566
|
+
* reassigning it on every render so the newest callback is invoked, and runs
|
|
2567
|
+
* it via an empty-dependency `useEffect` cleanup (react-use's `useEffectOnce`
|
|
2568
|
+
* is just `useEffect(effect, [])`). This port follows the same semantics.
|
|
2569
|
+
*
|
|
2570
|
+
* @example
|
|
2571
|
+
* useUnmount(() => cleanup())
|
|
2572
|
+
*/
|
|
2573
|
+
function useUnmount(fn) {
|
|
2574
|
+
const fnRef = (0, react.useRef)(fn);
|
|
2575
|
+
fnRef.current = fn;
|
|
2576
|
+
(0, react.useEffect)(() => () => fnRef.current(), []);
|
|
2577
|
+
}
|
|
2578
|
+
//#endregion
|
|
2579
|
+
//#region useUpdate/index.tsx
|
|
2580
|
+
const updateReducer = (num) => (num + 1) % 1e6;
|
|
2581
|
+
/**
|
|
2582
|
+
* React port of react-use's `useUpdate`.
|
|
2583
|
+
*
|
|
2584
|
+
* Map from react-use `useUpdate`
|
|
2585
|
+
* Mapping: `useReducer` with a wrapping counter — the returned function
|
|
2586
|
+
* dispatches an update that forces a re-render and is stable across renders.
|
|
2587
|
+
*
|
|
2588
|
+
* @example
|
|
2589
|
+
* const update = useUpdate()
|
|
2590
|
+
* update() // forces a re-render
|
|
2591
|
+
*/
|
|
2592
|
+
function useUpdate() {
|
|
2593
|
+
const [, update] = (0, react.useReducer)(updateReducer, 0);
|
|
2594
|
+
return update;
|
|
2595
|
+
}
|
|
2596
|
+
//#endregion
|
|
2597
|
+
//#region useWatch/index.tsx
|
|
2598
|
+
function useWatch(source, callback, options = {}) {
|
|
2599
|
+
const firstRender = (0, react.useRef)(true);
|
|
2600
|
+
const oldValueRef = (0, react.useRef)(void 0);
|
|
2601
|
+
(0, react.useEffect)(() => {
|
|
2602
|
+
const oldValue = oldValueRef.current;
|
|
2603
|
+
const first = firstRender.current;
|
|
2604
|
+
firstRender.current = false;
|
|
2605
|
+
if (!first || options.immediate) callback(source, oldValue);
|
|
2606
|
+
oldValueRef.current = source;
|
|
2607
|
+
}, Array.isArray(source) ? source : [source]);
|
|
2608
|
+
}
|
|
2609
|
+
//#endregion
|
|
2610
|
+
//#region useWatchArray/index.tsx
|
|
2611
|
+
/**
|
|
2612
|
+
* React port of VueUse's `watchArray` — watch for an array with additions and removals.
|
|
2613
|
+
*
|
|
2614
|
+
* Mapping: built on the house `useWatch` — the list is a plain array value tracked across
|
|
2615
|
+
* renders, `useWatch` handles the change detection, and the previous list is diffed against
|
|
2616
|
+
* the next one with item-identity matching (like upstream) so the callback receives
|
|
2617
|
+
* `(newList, oldList, added, removed)`. The list is wrapped as a single-element watch
|
|
2618
|
+
* source (`[list]`) so `useWatch` tracks it by reference identity instead of spreading a
|
|
2619
|
+
* variable-length list into its dependency list (React requires a constant deps size).
|
|
2620
|
+
*
|
|
2621
|
+
* Divergences from the upstream Vue API:
|
|
2622
|
+
* - `source` is a plain array value — Vue's `WatchSource` forms (ref / getter / reactive)
|
|
2623
|
+
* have no React equivalent, compute the array during render and pass it directly.
|
|
2624
|
+
* - The list is tracked by reference identity: replacing it with a new array fires the
|
|
2625
|
+
* callback even when the items are identical (like a Vue ref reassignment), while
|
|
2626
|
+
* re-renders that keep the same array reference do not fire.
|
|
2627
|
+
* - In-place mutations (`push` / `splice`) do not re-render — produce a new array
|
|
2628
|
+
* (`setList([...list, item])`) to trigger the watch.
|
|
2629
|
+
* - The upstream `onCleanup` callback parameter is not ported — `useWatch` has no
|
|
2630
|
+
* watch-cleanup equivalent, use `useEffect` cleanup in the component instead.
|
|
2631
|
+
* - The return value is `void` — upstream returns a stop `WatchHandle`; watching
|
|
2632
|
+
* ends when the component unmounts.
|
|
2633
|
+
*
|
|
2634
|
+
* @example
|
|
2635
|
+
* ```ts
|
|
2636
|
+
* useWatchArray(list, (newList, oldList, added, removed) => {
|
|
2637
|
+
* console.log('added:', added, 'removed:', removed)
|
|
2638
|
+
* })
|
|
2639
|
+
* ```
|
|
2640
|
+
*/
|
|
2641
|
+
function useWatchArray(source, cb, options) {
|
|
2642
|
+
useWatch([source], (watched, watchedOld) => {
|
|
2643
|
+
var _watchedOld$;
|
|
2644
|
+
const newList = watched[0];
|
|
2645
|
+
const prevList = [...(_watchedOld$ = watchedOld === null || watchedOld === void 0 ? void 0 : watchedOld[0]) !== null && _watchedOld$ !== void 0 ? _watchedOld$ : []];
|
|
2646
|
+
const oldListRemains = Array.from({ length: prevList.length });
|
|
2647
|
+
const added = [];
|
|
2648
|
+
for (const obj of newList) {
|
|
2649
|
+
let found = false;
|
|
2650
|
+
for (let i = 0; i < prevList.length; i++) if (!oldListRemains[i] && obj === prevList[i]) {
|
|
2651
|
+
oldListRemains[i] = true;
|
|
2652
|
+
found = true;
|
|
2653
|
+
break;
|
|
2654
|
+
}
|
|
2655
|
+
if (!found) added.push(obj);
|
|
2656
|
+
}
|
|
2657
|
+
cb(newList, prevList, added, prevList.filter((_, i) => !oldListRemains[i]));
|
|
2658
|
+
}, options);
|
|
2659
|
+
}
|
|
2660
|
+
//#endregion
|
|
2661
|
+
//#region useWatchAtMost/index.tsx
|
|
2662
|
+
/**
|
|
2663
|
+
* React port of VueUse's `watchAtMost` — `watch` with the number of times
|
|
2664
|
+
* triggered.
|
|
2665
|
+
*
|
|
2666
|
+
* Map from @vueuse/shared `watchAtMost`
|
|
2667
|
+
* Mapping: built on the house `useWatch`. The callback is wrapped with a fire
|
|
2668
|
+
* counter: each invocation increments the `count` state (exposed in the
|
|
2669
|
+
* return so components re-render), and once the limit — `options.count` — is
|
|
2670
|
+
* reached the wrapper marks the watcher as stopped so further source changes
|
|
2671
|
+
* are ignored. A manual `stop()` has the same effect before the limit.
|
|
2672
|
+
*
|
|
2673
|
+
* Divergences from upstream:
|
|
2674
|
+
* - upstream stops the underlying watcher via `stop()` scheduled on
|
|
2675
|
+
* `nextTick`; this port keeps the effect registered but the wrapped callback
|
|
2676
|
+
* becomes a no-op — observable behavior is identical (the callback fires at
|
|
2677
|
+
* most `count` times).
|
|
2678
|
+
* - `pause` / `resume` (upstream: inherited from `watchWithFilter`) are ported
|
|
2679
|
+
* as a skip flag on the wrapped callback — React has no watcher to detach,
|
|
2680
|
+
* but the observable behavior matches the Pausable controls.
|
|
2681
|
+
* - upstream's `count` return is a shallow ref; here it is React state so
|
|
2682
|
+
* reads re-render.
|
|
2683
|
+
* - upstream's `WatchWithFilterOptions` members beyond `immediate` (`deep`,
|
|
2684
|
+
* `flush`, `onTrack`, `onTrigger`) are not accepted — they are not
|
|
2685
|
+
* expressible in React (no reactive graph, no configurable commit, no
|
|
2686
|
+
* reactivity bookkeeping), and the option type rejects them.
|
|
2687
|
+
*
|
|
2688
|
+
* @example
|
|
2689
|
+
* ```tsx
|
|
2690
|
+
* const { count, stop } = useWatchAtMost(num, (value, oldValue) => {
|
|
2691
|
+
* console.log(value, oldValue)
|
|
2692
|
+
* }, { count: 3 })
|
|
2693
|
+
* ```
|
|
2694
|
+
*/
|
|
2695
|
+
function useWatchAtMost(source, callback, options) {
|
|
2696
|
+
const { count: maxCount, ...watchOptions } = options;
|
|
2697
|
+
const [count, setCount] = (0, react.useState)(0);
|
|
2698
|
+
const firedRef = (0, react.useRef)(0);
|
|
2699
|
+
const stoppedRef = (0, react.useRef)(false);
|
|
2700
|
+
const pausedRef = (0, react.useRef)(false);
|
|
2701
|
+
const maxCountRef = (0, react.useRef)(maxCount);
|
|
2702
|
+
maxCountRef.current = maxCount;
|
|
2703
|
+
const stop = (0, react.useCallback)(() => {
|
|
2704
|
+
stoppedRef.current = true;
|
|
2705
|
+
}, []);
|
|
2706
|
+
const pause = (0, react.useCallback)(() => {
|
|
2707
|
+
pausedRef.current = true;
|
|
2708
|
+
}, []);
|
|
2709
|
+
const resume = (0, react.useCallback)(() => {
|
|
2710
|
+
pausedRef.current = false;
|
|
2711
|
+
}, []);
|
|
2712
|
+
function wrapped(value, oldValue) {
|
|
2713
|
+
if (stoppedRef.current || pausedRef.current) return;
|
|
2714
|
+
firedRef.current += 1;
|
|
2715
|
+
setCount(firedRef.current);
|
|
2716
|
+
callback(value, oldValue);
|
|
2717
|
+
if (firedRef.current >= maxCountRef.current) stoppedRef.current = true;
|
|
2718
|
+
}
|
|
2719
|
+
useWatch(source, wrapped, watchOptions);
|
|
2720
|
+
return {
|
|
2721
|
+
count,
|
|
2722
|
+
stop,
|
|
2723
|
+
pause,
|
|
2724
|
+
resume
|
|
2725
|
+
};
|
|
2726
|
+
}
|
|
2727
|
+
//#endregion
|
|
2728
|
+
//#region useWatchDebounced/index.tsx
|
|
2729
|
+
function useWatchDebounced(source, callback, options = {}) {
|
|
2730
|
+
const { debounce = 0, maxWait, rejectOnCancel } = options;
|
|
2731
|
+
useWatch(source, useDebounceFn((value, oldValue) => callback(value, oldValue), debounce, {
|
|
2732
|
+
maxWait,
|
|
2733
|
+
rejectOnCancel
|
|
2734
|
+
}), { immediate: options.immediate });
|
|
2735
|
+
}
|
|
2736
|
+
//#endregion
|
|
2737
|
+
//#region useWatchDeep/index.tsx
|
|
2738
|
+
/**
|
|
2739
|
+
* Structural equality, mirroring the semantics of test `toEqual`: primitives
|
|
2740
|
+
* are compared with `Object.is`, and `Date`, `RegExp`, `Array`, `Map`, `Set`
|
|
2741
|
+
* and objects (plain or class instances) are compared by contents. Functions
|
|
2742
|
+
* compare by reference, and `Map` keys are matched by reference because key
|
|
2743
|
+
* lookups cannot deep-match, while `Map` values and `Set` items are compared
|
|
2744
|
+
* deeply.
|
|
2745
|
+
*
|
|
2746
|
+
* Shared single source of truth — used by {@link useWatchDeep} and imported
|
|
2747
|
+
* from `@reause/shared` by core hooks that need deep change detection
|
|
2748
|
+
* (e.g. `useCloned`).
|
|
2749
|
+
*/
|
|
2750
|
+
function deepEqual(a, b) {
|
|
2751
|
+
if (Object.is(a, b)) return true;
|
|
2752
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
|
|
2753
|
+
const aRecord = a;
|
|
2754
|
+
const bRecord = b;
|
|
2755
|
+
if (aRecord.constructor !== bRecord.constructor) return false;
|
|
2756
|
+
if (a instanceof Date) return a.getTime() === b.getTime();
|
|
2757
|
+
if (a instanceof RegExp) return a.source === b.source && a.flags === b.flags;
|
|
2758
|
+
if (Array.isArray(a)) {
|
|
2759
|
+
const bArray = b;
|
|
2760
|
+
return a.length === bArray.length && a.every((item, index) => deepEqual(item, bArray[index]));
|
|
2761
|
+
}
|
|
2762
|
+
if (a instanceof Map) {
|
|
2763
|
+
const bMap = b;
|
|
2764
|
+
if (a.size !== bMap.size) return false;
|
|
2765
|
+
for (const [key, value] of a) if (!bMap.has(key) || !deepEqual(value, bMap.get(key))) return false;
|
|
2766
|
+
return true;
|
|
2767
|
+
}
|
|
2768
|
+
if (a instanceof Set) {
|
|
2769
|
+
const bSet = b;
|
|
2770
|
+
if (a.size !== bSet.size) return false;
|
|
2771
|
+
const unmatched = [...bSet];
|
|
2772
|
+
for (const item of a) {
|
|
2773
|
+
const index = unmatched.findIndex((candidate) => deepEqual(item, candidate));
|
|
2774
|
+
if (index === -1) return false;
|
|
2775
|
+
unmatched.splice(index, 1);
|
|
2776
|
+
}
|
|
2777
|
+
return true;
|
|
2778
|
+
}
|
|
2779
|
+
const aKeys = Object.keys(aRecord);
|
|
2780
|
+
if (aKeys.length !== Object.keys(bRecord).length) return false;
|
|
2781
|
+
return aKeys.every((key) => Object.hasOwn(bRecord, key) && deepEqual(aRecord[key], bRecord[key]));
|
|
2782
|
+
}
|
|
2783
|
+
/**
|
|
2784
|
+
* Deep clone pairing with {@link deepEqual}'s type coverage — `Date`, `RegExp`,
|
|
2785
|
+
* `Array`, `Map`, `Set` and objects (plain or class instances) are copied
|
|
2786
|
+
* structurally, primitives and functions pass through. Used to snapshot a live
|
|
2787
|
+
* value into an isolated baseline for change detection (e.g. `useCloned`'s
|
|
2788
|
+
* source / cloned baselines, which must stay unaffected by in-place mutations).
|
|
2789
|
+
*/
|
|
2790
|
+
function deepClone(value) {
|
|
2791
|
+
if (value === null || typeof value !== "object") return value;
|
|
2792
|
+
if (value instanceof Date) return new Date(value.getTime());
|
|
2793
|
+
if (value instanceof RegExp) return new RegExp(value.source, value.flags);
|
|
2794
|
+
if (Array.isArray(value)) return value.map((item) => deepClone(item));
|
|
2795
|
+
if (value instanceof Map) {
|
|
2796
|
+
const result = /* @__PURE__ */ new Map();
|
|
2797
|
+
for (const [key, item] of value) result.set(key, deepClone(item));
|
|
2798
|
+
return result;
|
|
2799
|
+
}
|
|
2800
|
+
if (value instanceof Set) {
|
|
2801
|
+
const result = /* @__PURE__ */ new Set();
|
|
2802
|
+
for (const item of value) result.add(deepClone(item));
|
|
2803
|
+
return result;
|
|
2804
|
+
}
|
|
2805
|
+
const result = Object.create(Object.getPrototypeOf(value));
|
|
2806
|
+
for (const key of Object.keys(value)) result[key] = deepClone(value[key]);
|
|
2807
|
+
return result;
|
|
2808
|
+
}
|
|
2809
|
+
function useWatchDeep(source, callback, options = {}) {
|
|
2810
|
+
const immediateCall = (0, react.useRef)(options.immediate === true);
|
|
2811
|
+
useWatch(source, (value, oldValue) => {
|
|
2812
|
+
if (immediateCall.current) {
|
|
2813
|
+
immediateCall.current = false;
|
|
2814
|
+
callback(value, oldValue);
|
|
2815
|
+
return;
|
|
2816
|
+
}
|
|
2817
|
+
if (!deepEqual(value, oldValue)) callback(value, oldValue);
|
|
2818
|
+
}, options);
|
|
2819
|
+
}
|
|
2820
|
+
//#endregion
|
|
2821
|
+
//#region useWatchIgnorable/index.tsx
|
|
2822
|
+
function useWatchIgnorable(source, callback, options = {}) {
|
|
2823
|
+
const { immediate, once } = options;
|
|
2824
|
+
const lastSeenRef = (0, react.useRef)(source);
|
|
2825
|
+
const snapshotRef = (0, react.useRef)(source);
|
|
2826
|
+
const ignoreRef = (0, react.useRef)(false);
|
|
2827
|
+
const stoppedRef = (0, react.useRef)(false);
|
|
2828
|
+
useWatch(source, (value, oldValue) => {
|
|
2829
|
+
lastSeenRef.current = value;
|
|
2830
|
+
const ignore = ignoreRef.current;
|
|
2831
|
+
ignoreRef.current = false;
|
|
2832
|
+
if (ignore || stoppedRef.current) return;
|
|
2833
|
+
callback(value, oldValue);
|
|
2834
|
+
if (once) stoppedRef.current = true;
|
|
2835
|
+
}, { immediate });
|
|
2836
|
+
(0, react.useEffect)(() => {
|
|
2837
|
+
if (ignoreRef.current && Object.is(source, snapshotRef.current)) ignoreRef.current = false;
|
|
2838
|
+
});
|
|
2839
|
+
return {
|
|
2840
|
+
ignoreUpdates: (0, react.useCallback)((updater) => {
|
|
2841
|
+
snapshotRef.current = lastSeenRef.current;
|
|
2842
|
+
updater();
|
|
2843
|
+
ignoreRef.current = true;
|
|
2844
|
+
}, []),
|
|
2845
|
+
ignorePrevAsyncUpdates: (0, react.useCallback)(() => {
|
|
2846
|
+
snapshotRef.current = lastSeenRef.current;
|
|
2847
|
+
ignoreRef.current = true;
|
|
2848
|
+
}, []),
|
|
2849
|
+
stop: (0, react.useCallback)(() => {
|
|
2850
|
+
stoppedRef.current = true;
|
|
2851
|
+
}, [])
|
|
2852
|
+
};
|
|
2853
|
+
}
|
|
2854
|
+
//#endregion
|
|
2855
|
+
//#region useWatchImmediate/index.tsx
|
|
2856
|
+
function useWatchImmediate(source, callback) {
|
|
2857
|
+
useWatch(source, callback, { immediate: true });
|
|
2858
|
+
}
|
|
2859
|
+
//#endregion
|
|
2860
|
+
//#region useWatchOnce/index.tsx
|
|
2861
|
+
/**
|
|
2862
|
+
* Shorthand for watching value with `{ once: true }` — the callback fires at
|
|
2863
|
+
* most once (the first matching change) and the watcher stops afterwards —
|
|
2864
|
+
* React port of VueUse's `watchOnce`.
|
|
2865
|
+
*
|
|
2866
|
+
* Map from @vueuse/shared `watchOnce`.
|
|
2867
|
+
*
|
|
2868
|
+
* Mapping: upstream is a shorthand for
|
|
2869
|
+
* `watch(source, cb, { ...options, once: true })`. This port builds the same
|
|
2870
|
+
* shorthand on the house `useWatch` (like `useWatchAtMost` does with
|
|
2871
|
+
* `count: 1`): the callback is wrapped with a `stopped` ref — the first
|
|
2872
|
+
* invocation forwards `(value, oldValue)` and marks the watcher stopped, so
|
|
2873
|
+
* every further source change is ignored. An `immediate: true` call counts
|
|
2874
|
+
* toward the once, matching upstream.
|
|
2875
|
+
*
|
|
2876
|
+
* Divergences from upstream:
|
|
2877
|
+
* - Returns `{ stop }` instead of the full Vue `WatchHandle` — `stop` disables
|
|
2878
|
+
* further fires early (matching upstream's `stop()`); disposal otherwise
|
|
2879
|
+
* follows the component lifecycle.
|
|
2880
|
+
* - The source is a plain value (or array of values) tracked across renders —
|
|
2881
|
+
* Vue's `WatchSource` forms (ref / getter / reactive) have no React
|
|
2882
|
+
* equivalent, and the `deep` / `flush` watch options don't apply.
|
|
2883
|
+
*
|
|
2884
|
+
* @example
|
|
2885
|
+
* ```ts
|
|
2886
|
+
* useWatchOnce(count, (value, oldValue) => console.log(value, oldValue))
|
|
2887
|
+
* ```
|
|
2888
|
+
*/
|
|
2889
|
+
function useWatchOnce(source, callback, options = {}) {
|
|
2890
|
+
const stoppedRef = (0, react.useRef)(false);
|
|
2891
|
+
const stop = (0, react.useCallback)(() => {
|
|
2892
|
+
stoppedRef.current = true;
|
|
2893
|
+
}, []);
|
|
2894
|
+
function wrapped(value, oldValue) {
|
|
2895
|
+
if (stoppedRef.current) return;
|
|
2896
|
+
stoppedRef.current = true;
|
|
2897
|
+
callback(value, oldValue);
|
|
2898
|
+
}
|
|
2899
|
+
useWatch(source, wrapped, options);
|
|
2900
|
+
return { stop };
|
|
2901
|
+
}
|
|
2902
|
+
//#endregion
|
|
2903
|
+
//#region useWatchPausable/index.tsx
|
|
2904
|
+
function useWatchPausable(source, callback, options = {}) {
|
|
2905
|
+
const { initialState = "active", immediate } = options;
|
|
2906
|
+
const [isActive, setIsActive] = (0, react.useState)(initialState === "active");
|
|
2907
|
+
const activeRef = (0, react.useRef)(initialState === "active");
|
|
2908
|
+
const stoppedRef = (0, react.useRef)(false);
|
|
2909
|
+
const pause = (0, react.useCallback)(() => {
|
|
2910
|
+
activeRef.current = false;
|
|
2911
|
+
setIsActive(false);
|
|
2912
|
+
}, []);
|
|
2913
|
+
const resume = (0, react.useCallback)(() => {
|
|
2914
|
+
activeRef.current = true;
|
|
2915
|
+
setIsActive(true);
|
|
2916
|
+
}, []);
|
|
2917
|
+
const stop = (0, react.useCallback)(() => {
|
|
2918
|
+
stoppedRef.current = true;
|
|
2919
|
+
}, []);
|
|
2920
|
+
useWatch(source, (current, oldValue) => {
|
|
2921
|
+
if (!activeRef.current || stoppedRef.current) return;
|
|
2922
|
+
callback(current, oldValue);
|
|
2923
|
+
}, { immediate });
|
|
2924
|
+
return {
|
|
2925
|
+
pause,
|
|
2926
|
+
resume,
|
|
2927
|
+
isActive,
|
|
2928
|
+
stop
|
|
2929
|
+
};
|
|
2930
|
+
}
|
|
2931
|
+
//#endregion
|
|
2932
|
+
//#region useWatchThrottled/index.tsx
|
|
2933
|
+
function useWatchThrottled(source, callback, options = {}) {
|
|
2934
|
+
const { throttle = 0, trailing = true, leading = true } = options;
|
|
2935
|
+
useWatch(source, useThrottleFn((value, oldValue) => callback(value, oldValue), throttle, trailing, leading), { immediate: options.immediate });
|
|
2936
|
+
}
|
|
2937
|
+
//#endregion
|
|
2938
|
+
//#region useWatchTriggerable/index.tsx
|
|
2939
|
+
function useWatchTriggerable(source, callback, options = {}) {
|
|
2940
|
+
const { immediate } = options;
|
|
2941
|
+
const [, forceCommit] = (0, react.useState)(0);
|
|
2942
|
+
const cleanupFnRef = (0, react.useRef)(void 0);
|
|
2943
|
+
const onCleanup = (0, react.useCallback)((cleanupFn) => {
|
|
2944
|
+
cleanupFnRef.current = cleanupFn;
|
|
2945
|
+
}, []);
|
|
2946
|
+
const triggerableCallback = (0, react.useCallback)((value, oldValue) => {
|
|
2947
|
+
const cleanupFn = cleanupFnRef.current;
|
|
2948
|
+
if (cleanupFn) {
|
|
2949
|
+
cleanupFnRef.current = void 0;
|
|
2950
|
+
cleanupFn();
|
|
2951
|
+
}
|
|
2952
|
+
return callback(value, oldValue, onCleanup);
|
|
2953
|
+
}, [callback, onCleanup]);
|
|
2954
|
+
const lastSeenRef = (0, react.useRef)(source);
|
|
2955
|
+
const snapshotRef = (0, react.useRef)(source);
|
|
2956
|
+
const ignoreRef = (0, react.useRef)(false);
|
|
2957
|
+
const stoppedRef = (0, react.useRef)(false);
|
|
2958
|
+
useWatch(source, (value, oldValue) => {
|
|
2959
|
+
lastSeenRef.current = value;
|
|
2960
|
+
const ignore = ignoreRef.current;
|
|
2961
|
+
ignoreRef.current = false;
|
|
2962
|
+
if (ignore || stoppedRef.current) return;
|
|
2963
|
+
triggerableCallback(value, oldValue);
|
|
2964
|
+
}, { immediate });
|
|
2965
|
+
(0, react.useEffect)(() => {
|
|
2966
|
+
if (ignoreRef.current && isSameSource(source, snapshotRef.current)) ignoreRef.current = false;
|
|
2967
|
+
});
|
|
2968
|
+
const ignoreUpdates = (0, react.useCallback)((updater) => {
|
|
2969
|
+
snapshotRef.current = lastSeenRef.current;
|
|
2970
|
+
updater();
|
|
2971
|
+
ignoreRef.current = true;
|
|
2972
|
+
forceCommit((x) => x + 1);
|
|
2973
|
+
}, [forceCommit]);
|
|
2974
|
+
const ignorePrevAsyncUpdates = (0, react.useCallback)(() => {
|
|
2975
|
+
snapshotRef.current = lastSeenRef.current;
|
|
2976
|
+
ignoreRef.current = true;
|
|
2977
|
+
forceCommit((x) => x + 1);
|
|
2978
|
+
}, [forceCommit]);
|
|
2979
|
+
const stop = (0, react.useCallback)(() => {
|
|
2980
|
+
stoppedRef.current = true;
|
|
2981
|
+
}, []);
|
|
2982
|
+
return {
|
|
2983
|
+
trigger: (0, react.useCallback)(() => {
|
|
2984
|
+
let result;
|
|
2985
|
+
ignoreUpdates(() => {
|
|
2986
|
+
result = triggerableCallback(source, getOldValue(source));
|
|
2987
|
+
});
|
|
2988
|
+
return result;
|
|
2989
|
+
}, [
|
|
2990
|
+
source,
|
|
2991
|
+
triggerableCallback,
|
|
2992
|
+
ignoreUpdates
|
|
2993
|
+
]),
|
|
2994
|
+
ignoreUpdates,
|
|
2995
|
+
ignorePrevAsyncUpdates,
|
|
2996
|
+
stop
|
|
2997
|
+
};
|
|
2998
|
+
}
|
|
2999
|
+
function getOldValue(source) {
|
|
3000
|
+
return Array.isArray(source) ? source.map(() => void 0) : void 0;
|
|
3001
|
+
}
|
|
3002
|
+
function isSameSource(a, b) {
|
|
3003
|
+
if (Object.is(a, b)) return true;
|
|
3004
|
+
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, index) => Object.is(item, b[index]));
|
|
3005
|
+
}
|
|
3006
|
+
//#endregion
|
|
3007
|
+
//#region useWatchWithFilter/index.tsx
|
|
3008
|
+
function bypassFilter(invoke) {
|
|
3009
|
+
invoke();
|
|
3010
|
+
}
|
|
3011
|
+
/**
|
|
3012
|
+
* Create an EventFilter that debounce the events — in-house port of upstream
|
|
3013
|
+
* `@vueuse/shared` `debounceFilter` (trailing edge + `maxWait`).
|
|
3014
|
+
*
|
|
3015
|
+
* Mapping: same collapsing semantics as upstream (a newer call supersedes the
|
|
3016
|
+
* pending one; the `maxWait` timer survives re-scheduling and forces the call
|
|
3017
|
+
* with the latest `invoke`). Divergences: the promise-settlement plumbing
|
|
3018
|
+
* (`lastRejector` / `rejectOnCancel`) is dropped — the house `EventFilter`
|
|
3019
|
+
* contract returns `void` and the watch path consumes no promise, so
|
|
3020
|
+
* `rejectOnCancel` has no observable effect — and `isPending` is a plain
|
|
3021
|
+
* getter instead of a reactive ref. `ms` accepts a plain number or a React
|
|
3022
|
+
* ref (upstream: `RefOrValue<number>`) and is re-read on every call. Pending
|
|
3023
|
+
* timers are cleared by `cancel()` — the `useWatchWithFilter` hook calls it
|
|
3024
|
+
* on stop / unmount.
|
|
3025
|
+
*
|
|
3026
|
+
* @example
|
|
3027
|
+
* ```ts
|
|
3028
|
+
* useWatchWithFilter(input, callback, { eventFilter: debounceFilter(300, { maxWait: 1000 }) })
|
|
3029
|
+
* ```
|
|
3030
|
+
*/
|
|
3031
|
+
function debounceFilter(ms = 200, options = {}) {
|
|
3032
|
+
let timer;
|
|
3033
|
+
let maxTimer;
|
|
3034
|
+
let pending = false;
|
|
3035
|
+
let lastInvoker;
|
|
3036
|
+
const clearTimers = () => {
|
|
3037
|
+
if (timer !== void 0) {
|
|
3038
|
+
clearTimeout(timer);
|
|
3039
|
+
timer = void 0;
|
|
3040
|
+
}
|
|
3041
|
+
if (maxTimer !== void 0) {
|
|
3042
|
+
clearTimeout(maxTimer);
|
|
3043
|
+
maxTimer = void 0;
|
|
3044
|
+
}
|
|
3045
|
+
};
|
|
3046
|
+
const handler = (invoke) => {
|
|
3047
|
+
const duration = toValue(ms);
|
|
3048
|
+
const maxDuration = toValue(options.maxWait);
|
|
3049
|
+
if (timer !== void 0) {
|
|
3050
|
+
clearTimeout(timer);
|
|
3051
|
+
timer = void 0;
|
|
3052
|
+
}
|
|
3053
|
+
if (duration === void 0 || duration <= 0 || maxDuration !== void 0 && maxDuration <= 0) {
|
|
3054
|
+
clearTimers();
|
|
3055
|
+
pending = false;
|
|
3056
|
+
invoke();
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
3059
|
+
pending = true;
|
|
3060
|
+
lastInvoker = invoke;
|
|
3061
|
+
if (maxDuration !== void 0 && maxTimer === void 0) maxTimer = setTimeout(() => {
|
|
3062
|
+
maxTimer = void 0;
|
|
3063
|
+
if (timer !== void 0) {
|
|
3064
|
+
clearTimeout(timer);
|
|
3065
|
+
timer = void 0;
|
|
3066
|
+
}
|
|
3067
|
+
pending = false;
|
|
3068
|
+
lastInvoker === null || lastInvoker === void 0 || lastInvoker();
|
|
3069
|
+
}, maxDuration);
|
|
3070
|
+
timer = setTimeout(() => {
|
|
3071
|
+
timer = void 0;
|
|
3072
|
+
if (maxTimer !== void 0) {
|
|
3073
|
+
clearTimeout(maxTimer);
|
|
3074
|
+
maxTimer = void 0;
|
|
3075
|
+
}
|
|
3076
|
+
pending = false;
|
|
3077
|
+
invoke();
|
|
3078
|
+
}, duration);
|
|
3079
|
+
};
|
|
3080
|
+
const filter = handler;
|
|
3081
|
+
filter.cancel = () => {
|
|
3082
|
+
clearTimers();
|
|
3083
|
+
pending = false;
|
|
3084
|
+
lastInvoker = void 0;
|
|
3085
|
+
};
|
|
3086
|
+
filter.flush = () => {
|
|
3087
|
+
if (!pending) return;
|
|
3088
|
+
clearTimers();
|
|
3089
|
+
pending = false;
|
|
3090
|
+
const invoker = lastInvoker;
|
|
3091
|
+
lastInvoker = void 0;
|
|
3092
|
+
invoker === null || invoker === void 0 || invoker();
|
|
3093
|
+
};
|
|
3094
|
+
Object.defineProperty(filter, "isPending", {
|
|
3095
|
+
enumerable: true,
|
|
3096
|
+
get: () => pending
|
|
3097
|
+
});
|
|
3098
|
+
return filter;
|
|
3099
|
+
}
|
|
3100
|
+
/**
|
|
3101
|
+
* Create an EventFilter that throttle the events — in-house port of upstream
|
|
3102
|
+
* `@vueuse/shared` `throttleFilter` (leading/trailing edges with a trailing
|
|
3103
|
+
* invoke on window end).
|
|
3104
|
+
*
|
|
3105
|
+
* Mapping: same collapsing semantics as upstream — a call inside the throttle
|
|
3106
|
+
* window re-schedules the trailing timer with the remaining time, collapsing
|
|
3107
|
+
* bursts into one trailing call carrying the latest `invoke`. Divergences:
|
|
3108
|
+
* the promise-settlement plumbing (`rejectOnCancel`, upstream's fourth
|
|
3109
|
+
* parameter) is dropped — the house `EventFilter` contract returns `void` —
|
|
3110
|
+
* and the object options form is not ported (positional
|
|
3111
|
+
* `throttleFilter(ms, trailing, leading)` like the house `useThrottleFn`).
|
|
3112
|
+
* `ms` accepts a plain number or a React ref (upstream:
|
|
3113
|
+
* `RefOrValue<number>`) and is re-read on every call.
|
|
3114
|
+
*
|
|
3115
|
+
* @example
|
|
3116
|
+
* ```ts
|
|
3117
|
+
* useWatchWithFilter(scrollY, callback, { eventFilter: throttleFilter(100, true, false) })
|
|
3118
|
+
* ```
|
|
3119
|
+
*/
|
|
3120
|
+
function throttleFilter(ms = 200, trailing = true, leading = true) {
|
|
3121
|
+
let lastExec = 0;
|
|
3122
|
+
let timer;
|
|
3123
|
+
let isLeading = true;
|
|
3124
|
+
const clear = () => {
|
|
3125
|
+
if (timer !== void 0) {
|
|
3126
|
+
clearTimeout(timer);
|
|
3127
|
+
timer = void 0;
|
|
3128
|
+
}
|
|
3129
|
+
};
|
|
3130
|
+
return (invoke) => {
|
|
3131
|
+
const duration = toValue(ms);
|
|
3132
|
+
const elapsed = Date.now() - lastExec;
|
|
3133
|
+
clear();
|
|
3134
|
+
if (duration === void 0 || duration <= 0) {
|
|
3135
|
+
lastExec = Date.now();
|
|
3136
|
+
invoke();
|
|
3137
|
+
return;
|
|
3138
|
+
}
|
|
3139
|
+
if (elapsed > duration) {
|
|
3140
|
+
lastExec = Date.now();
|
|
3141
|
+
if (leading || !isLeading) invoke();
|
|
3142
|
+
} else if (trailing) timer = setTimeout(() => {
|
|
3143
|
+
lastExec = Date.now();
|
|
3144
|
+
isLeading = true;
|
|
3145
|
+
invoke();
|
|
3146
|
+
clear();
|
|
3147
|
+
}, Math.max(0, duration - elapsed));
|
|
3148
|
+
if (!leading && timer === void 0) timer = setTimeout(() => {
|
|
3149
|
+
isLeading = true;
|
|
3150
|
+
}, duration);
|
|
3151
|
+
isLeading = false;
|
|
3152
|
+
};
|
|
3153
|
+
}
|
|
3154
|
+
/**
|
|
3155
|
+
* `watch` with additional EventFilter control — React port of VueUse's
|
|
3156
|
+
* `watchWithFilter`.
|
|
3157
|
+
* Map from @vueuse/shared watchWithFilter.
|
|
3158
|
+
*
|
|
3159
|
+
* Mapping: upstream builds `watch(source, createFilterWrapper(eventFilter, cb),
|
|
3160
|
+
* watchOptions)` — the event filter wraps the watch trigger, so every source
|
|
3161
|
+
* change hands an `invoke` closure to the filter, which decides whether and
|
|
3162
|
+
* when the callback actually runs. This port builds the same wrapper on the
|
|
3163
|
+
* house `useWatch` (Vue's reactive dependency tracking becomes the effect
|
|
3164
|
+
* dependency list): every source change invokes the captured `eventFilter`
|
|
3165
|
+
* with an `invoke` closure carrying the latest `(value, oldValue)` pair. The
|
|
3166
|
+
* hook holds no state of its own — the source is the caller's own value — and
|
|
3167
|
+
* returns a `stop` function (upstream's `WatchHandle`, reduced to the stop
|
|
3168
|
+
* capability): after `stop()`, further source changes and any pending
|
|
3169
|
+
* filtered invocation no longer fire the callback, and cancelable filters
|
|
3170
|
+
* (`debounceFilter`) are cancelled outright.
|
|
3171
|
+
*
|
|
3172
|
+
* Divergences from upstream:
|
|
3173
|
+
* - React batching: source changes made in the same tick collapse into a
|
|
3174
|
+
* single effect run, so the filter sees ONE trigger where Vue's watcher
|
|
3175
|
+
* would fire per mutation. For a trailing filter the collapsed call is
|
|
3176
|
+
* identical (the latest `(value, oldValue)` pair); a leading-edge filter
|
|
3177
|
+
* fires at most once per tick instead of once per mutation.
|
|
3178
|
+
* - `deep` is not ported: React values are not deeply reactive. The source is
|
|
3179
|
+
* tracked by reference across renders (the effect dependency list), so
|
|
3180
|
+
* mutating an object in place is invisible and `deep: true` would have
|
|
3181
|
+
* nothing to recurse into — watch a derived primitive (or key) instead.
|
|
3182
|
+
* The same applies to the `flush` watch option: React effects always run
|
|
3183
|
+
* after the commit, there is no pre/post/sync choice.
|
|
3184
|
+
* - The filter instance is captured once on mount (upstream evaluates watch
|
|
3185
|
+
* options once during setup) — an inline `debounceFilter(ms)` is safe; use
|
|
3186
|
+
* a getter-based delay for dynamic values.
|
|
3187
|
+
* - `stop()` also suppresses a pending filtered invocation (upstream: an
|
|
3188
|
+
* already-scheduled filtered invoke still fires after stop), and pending
|
|
3189
|
+
* timers are cancelled when the component unmounts (upstream leaves
|
|
3190
|
+
* disposal to the effect scope).
|
|
3191
|
+
* - The promise-settlement plumbing of upstream filters (`lastRejector` /
|
|
3192
|
+
* `rejectOnCancel`) is dropped — the house `EventFilter` contract returns
|
|
3193
|
+
* `void`, so `rejectOnCancel` has no observable effect.
|
|
3194
|
+
*
|
|
3195
|
+
* @example
|
|
3196
|
+
* ```ts
|
|
3197
|
+
* const stop = useWatchWithFilter(count, (value, oldValue) => console.log(value, oldValue))
|
|
3198
|
+
* useWatchWithFilter(count, callback, { eventFilter: debounceFilter(300) })
|
|
3199
|
+
* stop()
|
|
3200
|
+
* ```
|
|
3201
|
+
*/
|
|
3202
|
+
function useWatchWithFilter(source, callback, options = {}) {
|
|
3203
|
+
const { eventFilter = bypassFilter, immediate = false } = options;
|
|
3204
|
+
const stoppedRef = (0, react.useRef)(false);
|
|
3205
|
+
const callbackRef = (0, react.useRef)(callback);
|
|
3206
|
+
callbackRef.current = callback;
|
|
3207
|
+
const filterRef = (0, react.useRef)(eventFilter);
|
|
3208
|
+
function wrapped(value, oldValue) {
|
|
3209
|
+
if (stoppedRef.current) return;
|
|
3210
|
+
filterRef.current(() => {
|
|
3211
|
+
if (stoppedRef.current) return;
|
|
3212
|
+
callbackRef.current(value, oldValue);
|
|
3213
|
+
});
|
|
3214
|
+
}
|
|
3215
|
+
useWatch(source, wrapped, { immediate });
|
|
3216
|
+
const stop = (0, react.useCallback)(() => {
|
|
3217
|
+
stoppedRef.current = true;
|
|
3218
|
+
const cancelable = filterRef.current;
|
|
3219
|
+
if (typeof cancelable.cancel === "function") cancelable.cancel();
|
|
3220
|
+
}, []);
|
|
3221
|
+
(0, react.useEffect)(() => stop, [stop]);
|
|
3222
|
+
return stop;
|
|
3223
|
+
}
|
|
3224
|
+
//#endregion
|
|
3225
|
+
//#region useWhenever/index.tsx
|
|
3226
|
+
/**
|
|
3227
|
+
* React port of VueUse's `whenever`.
|
|
3228
|
+
*
|
|
3229
|
+
* Map from @vueuse/shared `whenever`
|
|
3230
|
+
* Mapping: upstream `whenever` is Vue's `watch` plus a truthy guard — the
|
|
3231
|
+
* callback runs every time the source CHANGES to a truthy value (a re-render
|
|
3232
|
+
* with the same truthy value never fires). In React this becomes a `useEffect`
|
|
3233
|
+
* watching `[value]`: the initial mount is skipped unless `immediate` (which
|
|
3234
|
+
* fires with `oldValue` `undefined`), later runs fire when the value is truthy
|
|
3235
|
+
* and actually changed, and the previous value is tracked in a ref updated on
|
|
3236
|
+
* every run — mirroring `watch`'s `oldValue`, which advances through falsy
|
|
3237
|
+
* values too. The callback is kept in a ref so re-renders always invoke the
|
|
3238
|
+
* newest one.
|
|
3239
|
+
*
|
|
3240
|
+
* The `once` option stops the watch after the first truthy fire — expressible
|
|
3241
|
+
* in React as a one-shot flag consulted by the effect, mirroring upstream's
|
|
3242
|
+
* `if (options?.once) nextTick(() => stop())`.
|
|
3243
|
+
*
|
|
3244
|
+
* The return value is a `stop` function — upstream's `WatchHandle`, reduced to
|
|
3245
|
+
* the stop capability (house `useWatch` has no stop-handle infrastructure).
|
|
3246
|
+
* `stop()` is also called when the component unmounts.
|
|
3247
|
+
*
|
|
3248
|
+
* The upstream 3-arg callback `(value, oldValue, onInvalidate)` becomes a
|
|
3249
|
+
* 2-arg `(value, oldValue)` in this port — `onInvalidate` (Vue's effect
|
|
3250
|
+
* invalidation registration) has no React equivalent, so it is dropped.
|
|
3251
|
+
*
|
|
3252
|
+
* @see https://vueuse.org/shared/whenever/
|
|
3253
|
+
*
|
|
3254
|
+
* @example
|
|
3255
|
+
* useWhenever(ready, () => console.log(state))
|
|
3256
|
+
* useWhenever(ready, () => console.log(state), { immediate: true })
|
|
3257
|
+
* useWhenever(ready, () => console.log(state), { once: true })
|
|
3258
|
+
*/
|
|
3259
|
+
function useWhenever(value, cb, options) {
|
|
3260
|
+
const cbRef = (0, react.useRef)(cb);
|
|
3261
|
+
cbRef.current = cb;
|
|
3262
|
+
const oldValueRef = (0, react.useRef)(void 0);
|
|
3263
|
+
const isFirstRenderRef = (0, react.useRef)(true);
|
|
3264
|
+
const stoppedRef = (0, react.useRef)(false);
|
|
3265
|
+
(0, react.useEffect)(() => {
|
|
3266
|
+
if (stoppedRef.current) return;
|
|
3267
|
+
const isFirstRender = isFirstRenderRef.current;
|
|
3268
|
+
isFirstRenderRef.current = false;
|
|
3269
|
+
const isMountFire = isFirstRender && (options === null || options === void 0 ? void 0 : options.immediate) === true;
|
|
3270
|
+
const isChange = !Object.is(oldValueRef.current, value);
|
|
3271
|
+
if (value && (isMountFire || !isFirstRender && isChange)) {
|
|
3272
|
+
if (options === null || options === void 0 ? void 0 : options.once) stoppedRef.current = true;
|
|
3273
|
+
cbRef.current(value, oldValueRef.current);
|
|
3274
|
+
}
|
|
3275
|
+
oldValueRef.current = value;
|
|
3276
|
+
}, [value]);
|
|
3277
|
+
const stop = (0, react.useCallback)(() => {
|
|
3278
|
+
stoppedRef.current = true;
|
|
3279
|
+
}, []);
|
|
3280
|
+
(0, react.useEffect)(() => stop, [stop]);
|
|
3281
|
+
return stop;
|
|
3282
|
+
}
|
|
3283
|
+
//#endregion
|
|
3284
|
+
//#region index.ts
|
|
3285
|
+
/**
|
|
3286
|
+
* @reause/shared — React port of @vueuse/shared
|
|
3287
|
+
* Shared utilities shared across all reause packages.
|
|
3288
|
+
*
|
|
3289
|
+
* Mapping note: @vueuse/shared exposes pure utilities + composables that
|
|
3290
|
+
* don't depend on the renderer. In the React world those become either
|
|
3291
|
+
* plain functions (no hook) or hooks without rendering logic.
|
|
3292
|
+
*/
|
|
3293
|
+
const isClient = typeof window !== "undefined";
|
|
3294
|
+
function noop() {}
|
|
3295
|
+
//#endregion
|
|
3296
|
+
exports.assert = assert;
|
|
3297
|
+
exports.clamp = clamp;
|
|
3298
|
+
exports.createEventHook = createEventHook;
|
|
3299
|
+
exports.createGlobalState = createGlobalState;
|
|
3300
|
+
exports.createInjectionState = createInjectionState;
|
|
3301
|
+
exports.createSharedHook = createSharedHook;
|
|
3302
|
+
exports.createSingletonPromise = createSingletonPromise;
|
|
3303
|
+
exports.debounceFilter = debounceFilter;
|
|
3304
|
+
exports.deepClone = deepClone;
|
|
3305
|
+
exports.deepEqual = deepEqual;
|
|
3306
|
+
exports.formatDate = formatDate;
|
|
3307
|
+
exports.hasOwn = hasOwn;
|
|
3308
|
+
exports.hyphenate = hyphenate;
|
|
3309
|
+
exports.increaseWithUnit = increaseWithUnit;
|
|
3310
|
+
exports.isClient = isClient;
|
|
3311
|
+
exports.isDef = isDef;
|
|
3312
|
+
exports.isDefined = isDefined;
|
|
3313
|
+
exports.isIOS = isIOS;
|
|
3314
|
+
exports.isObject = isObject;
|
|
3315
|
+
exports.isRefLike = isRefLike;
|
|
3316
|
+
exports.makeDestructurable = makeDestructurable;
|
|
3317
|
+
exports.noop = noop;
|
|
3318
|
+
exports.normalizeDate = normalizeDate;
|
|
3319
|
+
exports.now = now;
|
|
3320
|
+
exports.objectOmit = objectOmit;
|
|
3321
|
+
exports.objectPick = objectPick;
|
|
3322
|
+
exports.promiseTimeout = promiseTimeout;
|
|
3323
|
+
exports.pxValue = pxValue;
|
|
3324
|
+
exports.rand = rand;
|
|
3325
|
+
exports.syncState = syncState;
|
|
3326
|
+
exports.syncStates = syncStates;
|
|
3327
|
+
exports.throttleFilter = throttleFilter;
|
|
3328
|
+
exports.timestamp = timestamp;
|
|
3329
|
+
exports.toArray = toArray;
|
|
3330
|
+
exports.toValue = toValue;
|
|
3331
|
+
exports.until = until;
|
|
3332
|
+
exports.useArrayDifference = useArrayDifference;
|
|
3333
|
+
exports.useArrayEvery = useArrayEvery;
|
|
3334
|
+
exports.useArrayFilter = useArrayFilter;
|
|
3335
|
+
exports.useArrayFind = useArrayFind;
|
|
3336
|
+
exports.useArrayFindIndex = useArrayFindIndex;
|
|
3337
|
+
exports.useArrayFindLast = useArrayFindLast;
|
|
3338
|
+
exports.useArrayIncludes = useArrayIncludes;
|
|
3339
|
+
exports.useArrayJoin = useArrayJoin;
|
|
3340
|
+
exports.useArrayMap = useArrayMap;
|
|
3341
|
+
exports.useArrayReduce = useArrayReduce;
|
|
3342
|
+
exports.useArraySome = useArraySome;
|
|
3343
|
+
exports.useArrayUnique = useArrayUnique;
|
|
3344
|
+
exports.useControllableState = useControllableState;
|
|
3345
|
+
exports.useCounter = useCounter;
|
|
3346
|
+
exports.useDateFormat = useDateFormat;
|
|
3347
|
+
exports.useDebounceFn = useDebounceFn;
|
|
3348
|
+
exports.useInterval = useInterval;
|
|
3349
|
+
exports.useIntervalFn = useIntervalFn;
|
|
3350
|
+
exports.useLastChanged = useLastChanged;
|
|
3351
|
+
exports.useListener = useListener;
|
|
3352
|
+
exports.useMount = useMount;
|
|
3353
|
+
exports.useStateAutoReset = useStateAutoReset;
|
|
3354
|
+
exports.useStateDebounced = useStateDebounced;
|
|
3355
|
+
exports.useStateDefault = useStateDefault;
|
|
3356
|
+
exports.useStateManualReset = useStateManualReset;
|
|
3357
|
+
exports.useStateThrottled = useStateThrottled;
|
|
3358
|
+
exports.useStateWithControl = useStateWithControl;
|
|
3359
|
+
exports.useThrottleFn = useThrottleFn;
|
|
3360
|
+
exports.useTimeout = useTimeout;
|
|
3361
|
+
exports.useTimeoutFn = useTimeoutFn;
|
|
3362
|
+
exports.useToNumber = useToNumber;
|
|
3363
|
+
exports.useToString = useToString;
|
|
3364
|
+
exports.useToggle = useToggle;
|
|
3365
|
+
exports.useUnmount = useUnmount;
|
|
3366
|
+
exports.useUpdate = useUpdate;
|
|
3367
|
+
exports.useWatch = useWatch;
|
|
3368
|
+
exports.useWatchArray = useWatchArray;
|
|
3369
|
+
exports.useWatchAtMost = useWatchAtMost;
|
|
3370
|
+
exports.useWatchDebounced = useWatchDebounced;
|
|
3371
|
+
exports.useWatchDeep = useWatchDeep;
|
|
3372
|
+
exports.useWatchIgnorable = useWatchIgnorable;
|
|
3373
|
+
exports.useWatchImmediate = useWatchImmediate;
|
|
3374
|
+
exports.useWatchOnce = useWatchOnce;
|
|
3375
|
+
exports.useWatchPausable = useWatchPausable;
|
|
3376
|
+
exports.useWatchThrottled = useWatchThrottled;
|
|
3377
|
+
exports.useWatchTriggerable = useWatchTriggerable;
|
|
3378
|
+
exports.useWatchWithFilter = useWatchWithFilter;
|
|
3379
|
+
exports.useWhenever = useWhenever;
|
|
3380
|
+
exports.writeState = writeState;
|
|
3381
|
+
})(this.reause = this.reause || {}, React);
|