tona-hooks 1.0.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 guangzan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # tona-hooks
2
+
3
+ 博客园皮肤开发用的 React Hooks 工具库。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install tona-hooks
9
+ ```
10
+
11
+ ## 使用
12
+
13
+ ```javascript
14
+ import { useQueryDOM } from 'tona-hooks'
15
+ ```
@@ -0,0 +1,71 @@
1
+ import { Dispatch, EffectCallback, useLayoutEffect } from "preact/hooks";
2
+ import { SetStateAction } from "preact/compat";
3
+ import { RefObject } from "preact";
4
+
5
+ //#region src/use-ajax-complete.d.ts
6
+
7
+ /**
8
+ * 通用的 Ajax 完成监听 Hook
9
+ * @param config 配置对象
10
+ * @param config.urlPattern 要监听的 URL 模式(支持字符串包含匹配)
11
+ * @param config.onSuccess 成功时的回调函数
12
+ * @param config.onError 错误时的回调函数
13
+ */
14
+ declare function useAjaxComplete(config: {
15
+ urlPattern: string | string[];
16
+ onSuccess?: (response: any, jqXHR: JQuery.jqXHR, option: JQuery.AjaxSettings) => void;
17
+ onError?: (response: any, jqXHR: JQuery.jqXHR, option: JQuery.AjaxSettings) => void;
18
+ }): void;
19
+ //#endregion
20
+ //#region src/use-effect-once.d.ts
21
+ declare const useEffectOnce: (effect: EffectCallback) => void;
22
+ //#endregion
23
+ //#region src/use-event-callback.d.ts
24
+ declare function useEventCallback<Args extends unknown[], R>(fn: (...args: Args) => R): (...args: Args) => R;
25
+ declare function useEventCallback<Args extends unknown[], R>(fn: ((...args: Args) => R) | undefined): ((...args: Args) => R) | undefined;
26
+ //#endregion
27
+ //#region src/use-isomorphic-layout-effect.d.ts
28
+ declare const useIsomorphicLayoutEffect: typeof useLayoutEffect;
29
+ //#endregion
30
+ //#region src/use-query-dom.d.ts
31
+ interface UseQueryDomOptions<T> {
32
+ selector: string;
33
+ observe?: boolean;
34
+ queryFn: (el: Element | null) => T | null;
35
+ /**
36
+ * 监听的 selector 的 DOM 相关的 AJAX 请求 URL
37
+ */
38
+ ajaxUrl?: string | string[];
39
+ }
40
+ interface UseQueryDomResult<T> {
41
+ data: T | null;
42
+ isPending: boolean;
43
+ }
44
+ declare function useQueryDOM<T>({
45
+ selector,
46
+ observe,
47
+ queryFn,
48
+ ajaxUrl
49
+ }: UseQueryDomOptions<T>): UseQueryDomResult<T>;
50
+ //#endregion
51
+ //#region src/use-raf-state.d.ts
52
+ declare const useRafState: <S>(initialState: S | (() => S)) => [S, Dispatch<SetStateAction<S>>];
53
+ //#endregion
54
+ //#region src/use-scroll.d.ts
55
+ interface State$1 {
56
+ x: number;
57
+ y: number;
58
+ }
59
+ declare const useScroll: (ref: RefObject<HTMLElement>) => State$1;
60
+ //#endregion
61
+ //#region src/use-unmount.d.ts
62
+ declare const useUnmount: (fn: () => any) => void;
63
+ //#endregion
64
+ //#region src/use-window-scroll.d.ts
65
+ interface State {
66
+ x: number;
67
+ y: number;
68
+ }
69
+ declare const useWindowScroll: () => State;
70
+ //#endregion
71
+ export { useAjaxComplete, useEffectOnce, useEventCallback, useIsomorphicLayoutEffect, useQueryDOM, useRafState, useScroll, useUnmount, useWindowScroll };
@@ -0,0 +1,71 @@
1
+ import { Dispatch, EffectCallback, useLayoutEffect } from "preact/hooks";
2
+ import { SetStateAction } from "preact/compat";
3
+ import { RefObject } from "preact";
4
+
5
+ //#region src/use-ajax-complete.d.ts
6
+
7
+ /**
8
+ * 通用的 Ajax 完成监听 Hook
9
+ * @param config 配置对象
10
+ * @param config.urlPattern 要监听的 URL 模式(支持字符串包含匹配)
11
+ * @param config.onSuccess 成功时的回调函数
12
+ * @param config.onError 错误时的回调函数
13
+ */
14
+ declare function useAjaxComplete(config: {
15
+ urlPattern: string | string[];
16
+ onSuccess?: (response: any, jqXHR: JQuery.jqXHR, option: JQuery.AjaxSettings) => void;
17
+ onError?: (response: any, jqXHR: JQuery.jqXHR, option: JQuery.AjaxSettings) => void;
18
+ }): void;
19
+ //#endregion
20
+ //#region src/use-effect-once.d.ts
21
+ declare const useEffectOnce: (effect: EffectCallback) => void;
22
+ //#endregion
23
+ //#region src/use-event-callback.d.ts
24
+ declare function useEventCallback<Args extends unknown[], R>(fn: (...args: Args) => R): (...args: Args) => R;
25
+ declare function useEventCallback<Args extends unknown[], R>(fn: ((...args: Args) => R) | undefined): ((...args: Args) => R) | undefined;
26
+ //#endregion
27
+ //#region src/use-isomorphic-layout-effect.d.ts
28
+ declare const useIsomorphicLayoutEffect: typeof useLayoutEffect;
29
+ //#endregion
30
+ //#region src/use-query-dom.d.ts
31
+ interface UseQueryDomOptions<T> {
32
+ selector: string;
33
+ observe?: boolean;
34
+ queryFn: (el: Element | null) => T | null;
35
+ /**
36
+ * 监听的 selector 的 DOM 相关的 AJAX 请求 URL
37
+ */
38
+ ajaxUrl?: string | string[];
39
+ }
40
+ interface UseQueryDomResult<T> {
41
+ data: T | null;
42
+ isPending: boolean;
43
+ }
44
+ declare function useQueryDOM<T>({
45
+ selector,
46
+ observe,
47
+ queryFn,
48
+ ajaxUrl
49
+ }: UseQueryDomOptions<T>): UseQueryDomResult<T>;
50
+ //#endregion
51
+ //#region src/use-raf-state.d.ts
52
+ declare const useRafState: <S>(initialState: S | (() => S)) => [S, Dispatch<SetStateAction<S>>];
53
+ //#endregion
54
+ //#region src/use-scroll.d.ts
55
+ interface State$1 {
56
+ x: number;
57
+ y: number;
58
+ }
59
+ declare const useScroll: (ref: RefObject<HTMLElement>) => State$1;
60
+ //#endregion
61
+ //#region src/use-unmount.d.ts
62
+ declare const useUnmount: (fn: () => any) => void;
63
+ //#endregion
64
+ //#region src/use-window-scroll.d.ts
65
+ interface State {
66
+ x: number;
67
+ y: number;
68
+ }
69
+ declare const useWindowScroll: () => State;
70
+ //#endregion
71
+ export { useAjaxComplete, useEffectOnce, useEventCallback, useIsomorphicLayoutEffect, useQueryDOM, useRafState, useScroll, useUnmount, useWindowScroll };
package/dist/index.js ADDED
@@ -0,0 +1,284 @@
1
+ let preact_hooks = require("preact/hooks");
2
+ let preact_compat = require("preact/compat");
3
+
4
+ //#region src/use-ajax-complete.ts
5
+ /**
6
+ * 通用的 Ajax 完成监听 Hook
7
+ * @param config 配置对象
8
+ * @param config.urlPattern 要监听的 URL 模式(支持字符串包含匹配)
9
+ * @param config.onSuccess 成功时的回调函数
10
+ * @param config.onError 错误时的回调函数
11
+ */
12
+ function useAjaxComplete(config) {
13
+ (0, preact_hooks.useEffect)(() => {
14
+ const { urlPattern, onSuccess, onError } = config;
15
+ const handleAjaxComplete = (_, jqXHR, option) => {
16
+ const url = option?.url;
17
+ if (!url) return;
18
+ if (!(Array.isArray(urlPattern) ? urlPattern : [urlPattern]).some((pattern) => url.includes(pattern))) return;
19
+ const response = jqXHR.responseJSON || jqXHR.responseText;
20
+ const hasError = jqXHR.status >= 400;
21
+ if (hasError && onError) onError(response, jqXHR, option);
22
+ else if (!hasError && onSuccess) onSuccess(response, jqXHR, option);
23
+ };
24
+ $(document).on("ajaxComplete", handleAjaxComplete);
25
+ return () => {
26
+ $(document).off("ajaxComplete", handleAjaxComplete);
27
+ };
28
+ }, [config]);
29
+ }
30
+
31
+ //#endregion
32
+ //#region src/use-effect-once.ts
33
+ const useEffectOnce = (effect) => {
34
+ (0, preact_hooks.useEffect)(effect, []);
35
+ };
36
+
37
+ //#endregion
38
+ //#region src/use-isomorphic-layout-effect.ts
39
+ const useIsomorphicLayoutEffect = typeof window !== "undefined" ? preact_hooks.useLayoutEffect : preact_hooks.useEffect;
40
+
41
+ //#endregion
42
+ //#region src/use-event-callback.ts
43
+ function useEventCallback(fn) {
44
+ const ref = (0, preact_compat.useRef)(() => {
45
+ throw new Error("Cannot call an event handler while rendering.");
46
+ });
47
+ useIsomorphicLayoutEffect(() => {
48
+ ref.current = fn;
49
+ }, [fn]);
50
+ return (0, preact_compat.useCallback)((...args) => ref.current?.(...args), [ref]);
51
+ }
52
+
53
+ //#endregion
54
+ //#region src/use-query-dom.ts
55
+ function useQueryDOM({ selector, observe = false, queryFn, ajaxUrl }) {
56
+ const [data, setData] = (0, preact_hooks.useState)(null);
57
+ const [isPending, setIsPending] = (0, preact_hooks.useState)(false);
58
+ const queryFnRef = (0, preact_hooks.useRef)(queryFn);
59
+ const observerRef = (0, preact_hooks.useRef)(null);
60
+ const ajaxStartRef = (0, preact_hooks.useRef)(0);
61
+ const debug = selector === "#user_icon.navbar-avatar" && (() => {
62
+ try {
63
+ return globalThis.localStorage.getItem("tona-debug-avatar") === "1";
64
+ } catch {
65
+ return false;
66
+ }
67
+ })();
68
+ (0, preact_hooks.useEffect)(() => {
69
+ queryFnRef.current = queryFn;
70
+ }, [queryFn]);
71
+ const queryElement = (0, preact_hooks.useCallback)(() => {
72
+ const element = document.querySelector(selector);
73
+ const nextData = queryFnRef.current(element);
74
+ setData((prev) => {
75
+ if (Object.is(prev, nextData)) return prev;
76
+ return nextData;
77
+ });
78
+ if (debug) console.log("[useQueryDOM]", {
79
+ selector,
80
+ elementFound: Boolean(element),
81
+ nextData,
82
+ time: (/* @__PURE__ */ new Date()).toISOString()
83
+ });
84
+ }, [debug, selector]);
85
+ (0, preact_hooks.useEffect)(() => {
86
+ queryElement();
87
+ if (!observe) return;
88
+ const targetNode = document.querySelector(selector)?.parentElement || document.body;
89
+ const observer = new MutationObserver((records) => {
90
+ if (debug) {
91
+ const summary = records.map((r) => ({
92
+ type: r.type,
93
+ target: r.target instanceof Element ? r.target.tagName : "unknown",
94
+ attributeName: r.attributeName || null,
95
+ addedNodes: r.addedNodes.length,
96
+ removedNodes: r.removedNodes.length
97
+ }));
98
+ console.log("[useQueryDOM:mutation]", {
99
+ selector,
100
+ summary
101
+ });
102
+ }
103
+ queryElement();
104
+ });
105
+ observerRef.current = observer;
106
+ observer.observe(targetNode, {
107
+ childList: true,
108
+ subtree: true,
109
+ attributes: true,
110
+ characterData: false
111
+ });
112
+ return () => {
113
+ observer.disconnect();
114
+ observerRef.current = null;
115
+ };
116
+ }, [
117
+ debug,
118
+ observe,
119
+ queryElement,
120
+ selector
121
+ ]);
122
+ (0, preact_hooks.useEffect)(() => {
123
+ if (!ajaxUrl) return;
124
+ const urls = Array.isArray(ajaxUrl) ? ajaxUrl : [ajaxUrl];
125
+ let timeoutId = null;
126
+ const pendingRequests = /* @__PURE__ */ new Set();
127
+ const clearPending = () => {
128
+ setIsPending(false);
129
+ if (timeoutId) {
130
+ clearTimeout(timeoutId);
131
+ timeoutId = null;
132
+ }
133
+ };
134
+ const checkAllRequestsComplete = () => {
135
+ if (pendingRequests.size === 0) clearPending();
136
+ };
137
+ const handleAjaxSend = (_, __, ajaxOptions) => {
138
+ const url = ajaxOptions?.url || "";
139
+ if (urls.some((targetUrl) => url.includes(targetUrl))) {
140
+ pendingRequests.add(url);
141
+ setIsPending(true);
142
+ ajaxStartRef.current = Date.now();
143
+ if (timeoutId) clearTimeout(timeoutId);
144
+ timeoutId = window.setTimeout(() => {
145
+ pendingRequests.delete(url);
146
+ checkAllRequestsComplete();
147
+ }, 1e4);
148
+ }
149
+ };
150
+ const handleAjaxComplete = (_, __, ajaxOptions) => {
151
+ const url = ajaxOptions?.url || "";
152
+ if (urls.some((targetUrl) => url.includes(targetUrl))) {
153
+ pendingRequests.delete(url);
154
+ setTimeout(() => {
155
+ checkAllRequestsComplete();
156
+ }, 50);
157
+ }
158
+ };
159
+ const handleAjaxError = (_, __, ajaxOptions) => {
160
+ const url = ajaxOptions?.url || "";
161
+ if (urls.some((targetUrl) => url.includes(targetUrl))) {
162
+ pendingRequests.delete(url);
163
+ checkAllRequestsComplete();
164
+ }
165
+ };
166
+ $(document).ajaxSend(handleAjaxSend);
167
+ $(document).ajaxComplete(handleAjaxComplete);
168
+ $(document).ajaxError(handleAjaxError);
169
+ return () => {
170
+ $(document).off("ajaxSend", handleAjaxSend);
171
+ $(document).off("ajaxComplete", handleAjaxComplete);
172
+ $(document).off("ajaxError", handleAjaxError);
173
+ if (timeoutId) clearTimeout(timeoutId);
174
+ pendingRequests.clear();
175
+ };
176
+ }, [ajaxUrl]);
177
+ return {
178
+ data,
179
+ isPending
180
+ };
181
+ }
182
+
183
+ //#endregion
184
+ //#region src/use-unmount.ts
185
+ const useUnmount = (fn) => {
186
+ const fnRef = (0, preact_hooks.useRef)(fn);
187
+ fnRef.current = fn;
188
+ useEffectOnce(() => () => fnRef.current());
189
+ };
190
+
191
+ //#endregion
192
+ //#region src/use-raf-state.ts
193
+ const useRafState = (initialState) => {
194
+ const frame = (0, preact_hooks.useRef)(0);
195
+ const [state, setState] = (0, preact_hooks.useState)(initialState);
196
+ const setRafState = (0, preact_hooks.useCallback)((value) => {
197
+ cancelAnimationFrame(frame.current);
198
+ frame.current = requestAnimationFrame(() => {
199
+ setState(value);
200
+ });
201
+ }, []);
202
+ useUnmount(() => {
203
+ cancelAnimationFrame(frame.current);
204
+ });
205
+ return [state, setRafState];
206
+ };
207
+
208
+ //#endregion
209
+ //#region src/misc/util.ts
210
+ function on(obj, ...args) {
211
+ if (obj?.addEventListener) obj.addEventListener(...args);
212
+ }
213
+ function off(obj, ...args) {
214
+ if (obj?.removeEventListener) obj.removeEventListener(...args);
215
+ }
216
+ const isBrowser = typeof window !== "undefined";
217
+
218
+ //#endregion
219
+ //#region src/use-scroll.ts
220
+ const useScroll = (ref) => {
221
+ if (process.env.NODE_ENV === "development") {
222
+ if (typeof ref !== "object" || typeof ref.current === "undefined") console.error("`useScroll` expects a single ref argument.");
223
+ }
224
+ const [state, setState] = useRafState({
225
+ x: 0,
226
+ y: 0
227
+ });
228
+ (0, preact_hooks.useEffect)(() => {
229
+ const handler = () => {
230
+ if (ref.current) setState({
231
+ x: ref.current.scrollLeft,
232
+ y: ref.current.scrollTop
233
+ });
234
+ };
235
+ if (ref.current) on(ref.current, "scroll", handler, {
236
+ capture: false,
237
+ passive: true
238
+ });
239
+ return () => {
240
+ if (ref.current) off(ref.current, "scroll", handler);
241
+ };
242
+ }, [ref, setState]);
243
+ return state;
244
+ };
245
+
246
+ //#endregion
247
+ //#region src/use-window-scroll.ts
248
+ const useWindowScroll = () => {
249
+ const [state, setState] = useRafState(() => ({
250
+ x: isBrowser ? window.pageXOffset : 0,
251
+ y: isBrowser ? window.pageYOffset : 0
252
+ }));
253
+ (0, preact_hooks.useEffect)(() => {
254
+ const handler = () => {
255
+ setState((state$1) => {
256
+ const { pageXOffset, pageYOffset } = window;
257
+ return state$1.x !== pageXOffset || state$1.y !== pageYOffset ? {
258
+ x: pageXOffset,
259
+ y: pageYOffset
260
+ } : state$1;
261
+ });
262
+ };
263
+ handler();
264
+ on(window, "scroll", handler, {
265
+ capture: false,
266
+ passive: true
267
+ });
268
+ return () => {
269
+ off(window, "scroll", handler);
270
+ };
271
+ }, [setState]);
272
+ return state;
273
+ };
274
+
275
+ //#endregion
276
+ exports.useAjaxComplete = useAjaxComplete;
277
+ exports.useEffectOnce = useEffectOnce;
278
+ exports.useEventCallback = useEventCallback;
279
+ exports.useIsomorphicLayoutEffect = useIsomorphicLayoutEffect;
280
+ exports.useQueryDOM = useQueryDOM;
281
+ exports.useRafState = useRafState;
282
+ exports.useScroll = useScroll;
283
+ exports.useUnmount = useUnmount;
284
+ exports.useWindowScroll = useWindowScroll;
package/dist/index.mjs ADDED
@@ -0,0 +1,274 @@
1
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
2
+ import { useCallback as useCallback$1, useRef as useRef$1 } from "preact/compat";
3
+
4
+ //#region src/use-ajax-complete.ts
5
+ /**
6
+ * 通用的 Ajax 完成监听 Hook
7
+ * @param config 配置对象
8
+ * @param config.urlPattern 要监听的 URL 模式(支持字符串包含匹配)
9
+ * @param config.onSuccess 成功时的回调函数
10
+ * @param config.onError 错误时的回调函数
11
+ */
12
+ function useAjaxComplete(config) {
13
+ useEffect(() => {
14
+ const { urlPattern, onSuccess, onError } = config;
15
+ const handleAjaxComplete = (_, jqXHR, option) => {
16
+ const url = option?.url;
17
+ if (!url) return;
18
+ if (!(Array.isArray(urlPattern) ? urlPattern : [urlPattern]).some((pattern) => url.includes(pattern))) return;
19
+ const response = jqXHR.responseJSON || jqXHR.responseText;
20
+ const hasError = jqXHR.status >= 400;
21
+ if (hasError && onError) onError(response, jqXHR, option);
22
+ else if (!hasError && onSuccess) onSuccess(response, jqXHR, option);
23
+ };
24
+ $(document).on("ajaxComplete", handleAjaxComplete);
25
+ return () => {
26
+ $(document).off("ajaxComplete", handleAjaxComplete);
27
+ };
28
+ }, [config]);
29
+ }
30
+
31
+ //#endregion
32
+ //#region src/use-effect-once.ts
33
+ const useEffectOnce = (effect) => {
34
+ useEffect(effect, []);
35
+ };
36
+
37
+ //#endregion
38
+ //#region src/use-isomorphic-layout-effect.ts
39
+ const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
40
+
41
+ //#endregion
42
+ //#region src/use-event-callback.ts
43
+ function useEventCallback(fn) {
44
+ const ref = useRef$1(() => {
45
+ throw new Error("Cannot call an event handler while rendering.");
46
+ });
47
+ useIsomorphicLayoutEffect(() => {
48
+ ref.current = fn;
49
+ }, [fn]);
50
+ return useCallback$1((...args) => ref.current?.(...args), [ref]);
51
+ }
52
+
53
+ //#endregion
54
+ //#region src/use-query-dom.ts
55
+ function useQueryDOM({ selector, observe = false, queryFn, ajaxUrl }) {
56
+ const [data, setData] = useState(null);
57
+ const [isPending, setIsPending] = useState(false);
58
+ const queryFnRef = useRef(queryFn);
59
+ const observerRef = useRef(null);
60
+ const ajaxStartRef = useRef(0);
61
+ const debug = selector === "#user_icon.navbar-avatar" && (() => {
62
+ try {
63
+ return globalThis.localStorage.getItem("tona-debug-avatar") === "1";
64
+ } catch {
65
+ return false;
66
+ }
67
+ })();
68
+ useEffect(() => {
69
+ queryFnRef.current = queryFn;
70
+ }, [queryFn]);
71
+ const queryElement = useCallback(() => {
72
+ const element = document.querySelector(selector);
73
+ const nextData = queryFnRef.current(element);
74
+ setData((prev) => {
75
+ if (Object.is(prev, nextData)) return prev;
76
+ return nextData;
77
+ });
78
+ if (debug) console.log("[useQueryDOM]", {
79
+ selector,
80
+ elementFound: Boolean(element),
81
+ nextData,
82
+ time: (/* @__PURE__ */ new Date()).toISOString()
83
+ });
84
+ }, [debug, selector]);
85
+ useEffect(() => {
86
+ queryElement();
87
+ if (!observe) return;
88
+ const targetNode = document.querySelector(selector)?.parentElement || document.body;
89
+ const observer = new MutationObserver((records) => {
90
+ if (debug) {
91
+ const summary = records.map((r) => ({
92
+ type: r.type,
93
+ target: r.target instanceof Element ? r.target.tagName : "unknown",
94
+ attributeName: r.attributeName || null,
95
+ addedNodes: r.addedNodes.length,
96
+ removedNodes: r.removedNodes.length
97
+ }));
98
+ console.log("[useQueryDOM:mutation]", {
99
+ selector,
100
+ summary
101
+ });
102
+ }
103
+ queryElement();
104
+ });
105
+ observerRef.current = observer;
106
+ observer.observe(targetNode, {
107
+ childList: true,
108
+ subtree: true,
109
+ attributes: true,
110
+ characterData: false
111
+ });
112
+ return () => {
113
+ observer.disconnect();
114
+ observerRef.current = null;
115
+ };
116
+ }, [
117
+ debug,
118
+ observe,
119
+ queryElement,
120
+ selector
121
+ ]);
122
+ useEffect(() => {
123
+ if (!ajaxUrl) return;
124
+ const urls = Array.isArray(ajaxUrl) ? ajaxUrl : [ajaxUrl];
125
+ let timeoutId = null;
126
+ const pendingRequests = /* @__PURE__ */ new Set();
127
+ const clearPending = () => {
128
+ setIsPending(false);
129
+ if (timeoutId) {
130
+ clearTimeout(timeoutId);
131
+ timeoutId = null;
132
+ }
133
+ };
134
+ const checkAllRequestsComplete = () => {
135
+ if (pendingRequests.size === 0) clearPending();
136
+ };
137
+ const handleAjaxSend = (_, __, ajaxOptions) => {
138
+ const url = ajaxOptions?.url || "";
139
+ if (urls.some((targetUrl) => url.includes(targetUrl))) {
140
+ pendingRequests.add(url);
141
+ setIsPending(true);
142
+ ajaxStartRef.current = Date.now();
143
+ if (timeoutId) clearTimeout(timeoutId);
144
+ timeoutId = window.setTimeout(() => {
145
+ pendingRequests.delete(url);
146
+ checkAllRequestsComplete();
147
+ }, 1e4);
148
+ }
149
+ };
150
+ const handleAjaxComplete = (_, __, ajaxOptions) => {
151
+ const url = ajaxOptions?.url || "";
152
+ if (urls.some((targetUrl) => url.includes(targetUrl))) {
153
+ pendingRequests.delete(url);
154
+ setTimeout(() => {
155
+ checkAllRequestsComplete();
156
+ }, 50);
157
+ }
158
+ };
159
+ const handleAjaxError = (_, __, ajaxOptions) => {
160
+ const url = ajaxOptions?.url || "";
161
+ if (urls.some((targetUrl) => url.includes(targetUrl))) {
162
+ pendingRequests.delete(url);
163
+ checkAllRequestsComplete();
164
+ }
165
+ };
166
+ $(document).ajaxSend(handleAjaxSend);
167
+ $(document).ajaxComplete(handleAjaxComplete);
168
+ $(document).ajaxError(handleAjaxError);
169
+ return () => {
170
+ $(document).off("ajaxSend", handleAjaxSend);
171
+ $(document).off("ajaxComplete", handleAjaxComplete);
172
+ $(document).off("ajaxError", handleAjaxError);
173
+ if (timeoutId) clearTimeout(timeoutId);
174
+ pendingRequests.clear();
175
+ };
176
+ }, [ajaxUrl]);
177
+ return {
178
+ data,
179
+ isPending
180
+ };
181
+ }
182
+
183
+ //#endregion
184
+ //#region src/use-unmount.ts
185
+ const useUnmount = (fn) => {
186
+ const fnRef = useRef(fn);
187
+ fnRef.current = fn;
188
+ useEffectOnce(() => () => fnRef.current());
189
+ };
190
+
191
+ //#endregion
192
+ //#region src/use-raf-state.ts
193
+ const useRafState = (initialState) => {
194
+ const frame = useRef(0);
195
+ const [state, setState] = useState(initialState);
196
+ const setRafState = useCallback((value) => {
197
+ cancelAnimationFrame(frame.current);
198
+ frame.current = requestAnimationFrame(() => {
199
+ setState(value);
200
+ });
201
+ }, []);
202
+ useUnmount(() => {
203
+ cancelAnimationFrame(frame.current);
204
+ });
205
+ return [state, setRafState];
206
+ };
207
+
208
+ //#endregion
209
+ //#region src/misc/util.ts
210
+ function on(obj, ...args) {
211
+ if (obj?.addEventListener) obj.addEventListener(...args);
212
+ }
213
+ function off(obj, ...args) {
214
+ if (obj?.removeEventListener) obj.removeEventListener(...args);
215
+ }
216
+ const isBrowser = typeof window !== "undefined";
217
+
218
+ //#endregion
219
+ //#region src/use-scroll.ts
220
+ const useScroll = (ref) => {
221
+ if (typeof ref !== "object" || typeof ref.current === "undefined") console.error("`useScroll` expects a single ref argument.");
222
+ const [state, setState] = useRafState({
223
+ x: 0,
224
+ y: 0
225
+ });
226
+ useEffect(() => {
227
+ const handler = () => {
228
+ if (ref.current) setState({
229
+ x: ref.current.scrollLeft,
230
+ y: ref.current.scrollTop
231
+ });
232
+ };
233
+ if (ref.current) on(ref.current, "scroll", handler, {
234
+ capture: false,
235
+ passive: true
236
+ });
237
+ return () => {
238
+ if (ref.current) off(ref.current, "scroll", handler);
239
+ };
240
+ }, [ref, setState]);
241
+ return state;
242
+ };
243
+
244
+ //#endregion
245
+ //#region src/use-window-scroll.ts
246
+ const useWindowScroll = () => {
247
+ const [state, setState] = useRafState(() => ({
248
+ x: isBrowser ? window.pageXOffset : 0,
249
+ y: isBrowser ? window.pageYOffset : 0
250
+ }));
251
+ useEffect(() => {
252
+ const handler = () => {
253
+ setState((state$1) => {
254
+ const { pageXOffset, pageYOffset } = window;
255
+ return state$1.x !== pageXOffset || state$1.y !== pageYOffset ? {
256
+ x: pageXOffset,
257
+ y: pageYOffset
258
+ } : state$1;
259
+ });
260
+ };
261
+ handler();
262
+ on(window, "scroll", handler, {
263
+ capture: false,
264
+ passive: true
265
+ });
266
+ return () => {
267
+ off(window, "scroll", handler);
268
+ };
269
+ }, [setState]);
270
+ return state;
271
+ };
272
+
273
+ //#endregion
274
+ export { useAjaxComplete, useEffectOnce, useEventCallback, useIsomorphicLayoutEffect, useQueryDOM, useRafState, useScroll, useUnmount, useWindowScroll };
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "tona-hooks",
3
+ "version": "1.0.1",
4
+ "description": "",
5
+ "author": {
6
+ "name": "guangzan",
7
+ "url": "https://www.cnblogs.com/guangzan",
8
+ "email": "guangzan1999@outlook.com"
9
+ },
10
+ "license": "MIT",
11
+ "homepage": "https://github.com/acnblogs/hooks#readme",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/acnblogs/hooks.git"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/acnblogs/hooks/issues"
18
+ },
19
+ "keywords": [
20
+ "博客园"
21
+ ],
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.mjs",
26
+ "require": "./dist/index.js"
27
+ }
28
+ },
29
+ "main": "./dist/index.mjs",
30
+ "module": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "devDependencies": {
36
+ "tsdown": "latest"
37
+ },
38
+ "peerDependencies": {
39
+ "preact": "^10.28.2"
40
+ },
41
+ "scripts": {
42
+ "dev": "tsdown --watch",
43
+ "build": "tsdown"
44
+ }
45
+ }