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 +21 -0
- package/README.md +15 -0
- package/dist/index.d.mts +71 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.js +284 -0
- package/dist/index.mjs +274 -0
- package/package.json +45 -0
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
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|