@vvi/react-hooks 1.0.0
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/CHANGELOG.md +5 -0
- package/LICENSE +16 -0
- package/README.md +33 -0
- package/cjs/index.js +15 -0
- package/cjs/use-lazy-ripple.js +67 -0
- package/cjs/use-ripples.js +66 -0
- package/cjs/useAnimationFrame.js +137 -0
- package/cjs/useInputIsComposing.js +68 -0
- package/cjs/useTimeId.js +53 -0
- package/es/index.d.ts +17 -0
- package/es/index.js +5 -0
- package/es/use-lazy-ripple.d.ts +13 -0
- package/es/use-lazy-ripple.js +65 -0
- package/es/use-ripples.d.ts +31 -0
- package/es/use-ripples.js +61 -0
- package/es/useAnimationFrame.d.ts +73 -0
- package/es/useAnimationFrame.js +135 -0
- package/es/useInputIsComposing.d.ts +43 -0
- package/es/useInputIsComposing.js +66 -0
- package/es/useTimeId.d.ts +42 -0
- package/es/useTimeId.js +51 -0
- package/es/utils/dog.d.ts +11 -0
- package/package.json +118 -0
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright ©️ <2026> <MrMudBean>
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
16
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# react-hooks
|
|
2
|
+
|
|
3
|
+
[&label=版本号&labelColor=rgb(73,73,228)&color=rgb(0,0,0)>)](https://www.npmjs.com/package/@vvi/react-hooks) [?logo=github>)](https://github.com/MrMudBean/react-hooks/issues)
|
|
4
|
+
|
|
5
|
+
业务无关的 hooks 。
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install --save @vvi/react-hooks
|
|
11
|
+
|
|
12
|
+
# pnpm
|
|
13
|
+
pnpm add --save @vvi/react-hooks
|
|
14
|
+
|
|
15
|
+
# yarn
|
|
16
|
+
yarn add --save @vvi/react-hooks
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 使用
|
|
20
|
+
|
|
21
|
+
提供了几个简单的与业务无关的用于把玩的 react hooks :
|
|
22
|
+
|
|
23
|
+
- **`useTimeId`** :一个 `useRef` 数值,可用于储存 `setTimeout`、`setInterval` 的返回值,自动在卸载时调用 `clearTimeout(id.current)`
|
|
24
|
+
- **`useInputIsComposing`** : 储存了当前绑定了 ref 的输入框是否完成了输入的 `usrRef` 数据。可在桌面端设备上中文输入判定是否完成输入选择
|
|
25
|
+
- **`useAnimationFrame`** :一个 `useRef` 数据,数据储存了 `requestAnimationFrame`
|
|
26
|
+
- **`useRipples`** :一个 `useRef` 数据。储存了 [Ripples](https://www.npmjs.com/package/@vvi/ripple) 实例
|
|
27
|
+
- **`useLazyRipples`** :一个 `useRef` 数据,储存了 [Ripple](https://www.npmjs.com/package/@vvi/ripple) 实例
|
|
28
|
+
|
|
29
|
+
## 状态
|
|
30
|
+
|
|
31
|
+
此软件包是 `MrMudBean` 生态系统的一部分。
|
|
32
|
+
它使用严格的 TypeScript 编写,并通过 Rollup 构建进行验证。
|
|
33
|
+
虽然单元测试较少,但 API 稳定,并在生产环境中大量使用。
|
package/cjs/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var useAnimationFrame = require('./useAnimationFrame.js');
|
|
4
|
+
var useRipples = require('./use-ripples.js');
|
|
5
|
+
var useLazyRipple = require('./use-lazy-ripple.js');
|
|
6
|
+
var useInputIsComposing = require('./useInputIsComposing.js');
|
|
7
|
+
var useTimeId = require('./useTimeId.js');
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
exports.useAnimationFrame = useAnimationFrame.useAnimationFrame;
|
|
12
|
+
exports.useRipples = useRipples.useRipples;
|
|
13
|
+
exports.useLazyRipples = useLazyRipple.useLazyRipples;
|
|
14
|
+
exports.useInputIsComposing = useInputIsComposing.useInputIsComposing;
|
|
15
|
+
exports.useTimeId = useTimeId.useTimeId;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var is = require('@vvi/is');
|
|
5
|
+
var react = require('react');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ## 懒加载使用涟漪 hook
|
|
9
|
+
* @param canvas
|
|
10
|
+
* @param option
|
|
11
|
+
*/
|
|
12
|
+
function useLazyRipples(canvas, option) {
|
|
13
|
+
/** react dom */
|
|
14
|
+
const ripples = react.useRef(null);
|
|
15
|
+
/** 当前是否被卸载 */
|
|
16
|
+
const isUnmounted = react.useRef(true);
|
|
17
|
+
/** 是否已加载 */
|
|
18
|
+
const isLoaded = react.useRef(false);
|
|
19
|
+
// 加载状态
|
|
20
|
+
const [isLoading, setIsLoading] = react.useState(false);
|
|
21
|
+
// 错误状态
|
|
22
|
+
const [error, setError] = react.useState(null);
|
|
23
|
+
// 参数
|
|
24
|
+
const optionRef = react.useRef(option);
|
|
25
|
+
/** 卸载 */
|
|
26
|
+
function unmounted() {
|
|
27
|
+
isUnmounted.current = true;
|
|
28
|
+
setTimeout(() => {
|
|
29
|
+
// 当前是被卸载状态
|
|
30
|
+
if (isUnmounted.current) {
|
|
31
|
+
ripples.current?.destroy();
|
|
32
|
+
}
|
|
33
|
+
}, 0);
|
|
34
|
+
}
|
|
35
|
+
// 更新 option,防止加载 option 为旧数据
|
|
36
|
+
react.useEffect(() => {
|
|
37
|
+
optionRef.current = option;
|
|
38
|
+
}, [option]);
|
|
39
|
+
react.useEffect(() => {
|
|
40
|
+
isUnmounted.current = false;
|
|
41
|
+
/** 非空检验(这里一般都是有值的,除非故障) */
|
|
42
|
+
if (is.isNull(canvas.current) || isLoaded.current) {
|
|
43
|
+
return unmounted;
|
|
44
|
+
}
|
|
45
|
+
// 设置加载状态
|
|
46
|
+
setIsLoading(true);
|
|
47
|
+
isLoaded.current = true;
|
|
48
|
+
import('@vvi/ripple')
|
|
49
|
+
.then(module => {
|
|
50
|
+
if (is.isNull(canvas.current) || isUnmounted.current) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
ripples.current = new module.Ripples(canvas.current, optionRef.current);
|
|
54
|
+
return;
|
|
55
|
+
})
|
|
56
|
+
.catch(err => {
|
|
57
|
+
setError(err);
|
|
58
|
+
})
|
|
59
|
+
.finally(() => {
|
|
60
|
+
setIsLoading(false);
|
|
61
|
+
});
|
|
62
|
+
return unmounted;
|
|
63
|
+
}, []);
|
|
64
|
+
return { ripples, isLoading, error };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
exports.useLazyRipples = useLazyRipples;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var is = require('@vvi/is');
|
|
5
|
+
var ripple = require('@vvi/ripple');
|
|
6
|
+
var react = require('react');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @module @vvi/react-hooks/index
|
|
10
|
+
* @file use-ripples.ts
|
|
11
|
+
* @description ripple 的主要核心逻辑
|
|
12
|
+
* @author MrMudBean <Mr.MudBean@outlook.com>
|
|
13
|
+
* @copyright 2026 ©️ MrMudBean
|
|
14
|
+
* @since 2025-06-20 01:37
|
|
15
|
+
* @version 2.0.0-alpha.0
|
|
16
|
+
* @lastModified 2026-07-07 09:14
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* ## 使用绘制 ripples 上一层
|
|
20
|
+
* @param canvas `usrRef` 包裹的 `HTMLCanvasElement`,用于绘制图像
|
|
21
|
+
* @param option 初始化的
|
|
22
|
+
* @see [useRipples](https://lmssee.com/custom-hooks/use-ripples)
|
|
23
|
+
* @see [JQuery ripple](https://github.com/sirxemic/jquery.ripples)
|
|
24
|
+
* @example
|
|
25
|
+
* 下面是在 <BackgroundRipple> 中使用
|
|
26
|
+
* ```ts
|
|
27
|
+
* import { useRipples } from '@vvi/react-hooks';
|
|
28
|
+
*
|
|
29
|
+
* export function BackgroundRipple(props: BackgroundRipplesProps) {
|
|
30
|
+
*
|
|
31
|
+
* // canvas 元素
|
|
32
|
+
* const canvas = useRef<HTMLCanvasElement>(null);
|
|
33
|
+
*
|
|
34
|
+
* // 使用 ripples
|
|
35
|
+
* const ripplesRef = useRipples(canvas, props);
|
|
36
|
+
*
|
|
37
|
+
* return (<>
|
|
38
|
+
* <canvas ref={canvas}></canvas>
|
|
39
|
+
* {props.children}
|
|
40
|
+
* </>);
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
function useRipples(canvas, option) {
|
|
45
|
+
/** react dom */
|
|
46
|
+
const ripples = react.useRef(null);
|
|
47
|
+
// 初始化数据
|
|
48
|
+
react.useEffect(() => {
|
|
49
|
+
/** 非空检验(这里一般都是有值的,除非故障) */
|
|
50
|
+
if (is.isNull(canvas.current))
|
|
51
|
+
return;
|
|
52
|
+
try {
|
|
53
|
+
ripples.current = new ripple.Ripples(canvas.current, option);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
}
|
|
57
|
+
return () => ripples.current?.destroy();
|
|
58
|
+
}, []);
|
|
59
|
+
return ripples;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
Object.defineProperty(exports, "Ripples", {
|
|
63
|
+
enumerable: true,
|
|
64
|
+
get: function () { return ripple.Ripples; }
|
|
65
|
+
});
|
|
66
|
+
exports.useRipples = useRipples;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var is = require('@vvi/is');
|
|
5
|
+
var react = require('react');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @module @vvi/react-hooks/useAnimationFrame
|
|
9
|
+
* @file useAnimationFrame.ts
|
|
10
|
+
* @description 使用 animationFrame
|
|
11
|
+
* @author MrMudBean <Mr.MudBean@outlook.com>
|
|
12
|
+
* @copyright 2026 ©️ MrMudBean
|
|
13
|
+
* @since 2025-01-07 11:23
|
|
14
|
+
* @version 2.0.0-alpha.0
|
|
15
|
+
* @lastModified 2026-07-06 17:16
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* # 该钩子在组件卸载时会自动调用 `window.cancelAnimationFrame` 清理传入的回调方法
|
|
19
|
+
*
|
|
20
|
+
* 默认是传入即启动调用方法
|
|
21
|
+
* @description 创建一个会自己关闭(组建卸载时)的 animationFrame。当然,可以通过返回值手动终止
|
|
22
|
+
* @param callback 使用回调函数。回调函数有两个参数,除了默认的时间,还有另一个用于暂停后的记时时间。如果没有主动暂停,两者时间总是趋近于相等的(可能有其他延迟造成时间差越来越大)。如果没有暂停需求,使用第一个默认时间就可以了。
|
|
23
|
+
* @param [option=false] 使用第二参数,可指定是否立即执行及是否仅执行一次
|
|
24
|
+
* @returns 返回值包含执行状态
|
|
25
|
+
* @see [useAnimationFrame](https://npm.lmssee.com/react-hooks/use-animation-frame)
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* import { useAnimationFrame } from '@vvi/react-hooks';
|
|
29
|
+
* ...
|
|
30
|
+
*
|
|
31
|
+
* export function Home () {
|
|
32
|
+
* // 如果没有主动暂停的需要,直接使用默认的 time 最好。如果中间有使用暂停。那么 time 值是不连贯的,而 runTime 是连贯的
|
|
33
|
+
* const result = useAnimationFrame((time ,runTime) => {
|
|
34
|
+
* ... //
|
|
35
|
+
* });
|
|
36
|
+
*
|
|
37
|
+
* return <>
|
|
38
|
+
*
|
|
39
|
+
* <button onclick={() => result.cancel()}>暂停</button>
|
|
40
|
+
* <button onclick={() => result.render()}>恢复</button>
|
|
41
|
+
* </>
|
|
42
|
+
* }
|
|
43
|
+
* ...
|
|
44
|
+
* ```
|
|
45
|
+
* 当然,可使用第二参数执行立即执行还是仅执行一次(作用不大,不与赘述)
|
|
46
|
+
*/
|
|
47
|
+
function useAnimationFrame(callback, option = false) {
|
|
48
|
+
if (is.isBoolean(option)) {
|
|
49
|
+
option = {
|
|
50
|
+
immediately: option,
|
|
51
|
+
once: false,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
else if (is.isPlainObject(option)) {
|
|
55
|
+
option = {
|
|
56
|
+
once: is.isBoolean(option.once) ? option.once : false,
|
|
57
|
+
immediately: is.isBoolean(option.immediately) ? option.immediately : false,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
option = {
|
|
62
|
+
immediately: false,
|
|
63
|
+
once: false,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** */
|
|
67
|
+
const animationFrame = react.useRef({
|
|
68
|
+
id: 0,
|
|
69
|
+
immediately: option.immediately ?? true,
|
|
70
|
+
once: option.once ?? false,
|
|
71
|
+
noun: false,
|
|
72
|
+
time: 0,
|
|
73
|
+
cancelTime: 0,
|
|
74
|
+
firstRunEffect: true,
|
|
75
|
+
isUnmounted: true,
|
|
76
|
+
result: {
|
|
77
|
+
cancel() {
|
|
78
|
+
const { id } = animationFrame.current;
|
|
79
|
+
if (id)
|
|
80
|
+
window.cancelAnimationFrame(id);
|
|
81
|
+
animationFrame.current.result.canceled = true;
|
|
82
|
+
},
|
|
83
|
+
canceled: false,
|
|
84
|
+
render() {
|
|
85
|
+
const { current } = animationFrame;
|
|
86
|
+
const { result } = current;
|
|
87
|
+
current.cancelTime = performance.now();
|
|
88
|
+
result.cancel();
|
|
89
|
+
result.canceled = false;
|
|
90
|
+
current.id = window.requestAnimationFrame(_);
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
const { current } = animationFrame;
|
|
95
|
+
/** 转化为函数 */
|
|
96
|
+
const _ = react.useCallback((time) => {
|
|
97
|
+
const { result } = current;
|
|
98
|
+
const _time = time - current.cancelTime;
|
|
99
|
+
current.time += _time < 0 ? 16.7 : _time;
|
|
100
|
+
callback(time, current.time);
|
|
101
|
+
// 如果不是仅执行一次,进入下一次执行
|
|
102
|
+
if (!current.once)
|
|
103
|
+
result.render();
|
|
104
|
+
}, [callback]);
|
|
105
|
+
// 首次执行
|
|
106
|
+
if (current.immediately && !current.noun && current.result.canceled) {
|
|
107
|
+
current.noun = true;
|
|
108
|
+
current.result.render();
|
|
109
|
+
}
|
|
110
|
+
react.useEffect(() => {
|
|
111
|
+
/// 非仅执行一次再次这里需要执行
|
|
112
|
+
if (!current.once) {
|
|
113
|
+
if (current.firstRunEffect) {
|
|
114
|
+
current.result.render();
|
|
115
|
+
current.firstRunEffect = false; // 标记为不允许执行
|
|
116
|
+
}
|
|
117
|
+
else if (!current.firstRunEffect && !current.result.canceled) {
|
|
118
|
+
/** 非第一次执行 */
|
|
119
|
+
current.result.render();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}, [callback]);
|
|
123
|
+
/// 在组件退出时保证能正确的退出
|
|
124
|
+
react.useEffect(() => {
|
|
125
|
+
current.isUnmounted = false;
|
|
126
|
+
return () => {
|
|
127
|
+
current.isUnmounted = true;
|
|
128
|
+
setTimeout(() => {
|
|
129
|
+
if (current.isUnmounted)
|
|
130
|
+
current.result.cancel();
|
|
131
|
+
}, 0);
|
|
132
|
+
};
|
|
133
|
+
}, []);
|
|
134
|
+
return current.result;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
exports.useAnimationFrame = useAnimationFrame;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var react = require('react');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @module @vvi/react-hooks/useInputIsComposing
|
|
8
|
+
* @file useInputIsComposing.ts
|
|
9
|
+
* @description 使用输入框是否完成输入状态
|
|
10
|
+
* @author MrMudBean <Mr.MudBean@outlook.com>
|
|
11
|
+
* @copyright 2026 ©️ MrMudBean
|
|
12
|
+
* @since 2025-01-07 11:23
|
|
13
|
+
* @version 2.0.0-alpha.0
|
|
14
|
+
* @lastModified 2026-07-06 17:16
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* # 导出一个使用 `useRef` 创建的 `boolean`
|
|
18
|
+
*
|
|
19
|
+
* *由于是包裹在 RefObject 之中,判断时务必使用 `isComposing.current` 进行判断*
|
|
20
|
+
* @description 用于判断当前的输入状态是否结束
|
|
21
|
+
* @param inputRef [RefObject<HTMLInputElement | HTMLTextAreaElement>] 输入框的 ref
|
|
22
|
+
* @returns RefObject<boolean>
|
|
23
|
+
* @see [useInputIscComposing](https://npm.lmssee.com/react-hooks/use-input-is-composing)
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { useInputIsComposing } from '@vvi/react-hooks';
|
|
27
|
+
*
|
|
28
|
+
* ...
|
|
29
|
+
* const inputRef = useRef<HTMLInputElement>(null);
|
|
30
|
+
*
|
|
31
|
+
* const inputIsComposing = useInputIsComposing(inputRef);
|
|
32
|
+
* ...
|
|
33
|
+
* function enterDown(e: React.KeyboardEvent<HTMLInputElement>) {
|
|
34
|
+
* if (e.key === 'Enter') {
|
|
35
|
+
* if (isComposing.current) {
|
|
36
|
+
* console.log("此时此景,按回车键说明为了从候选词中挑选");
|
|
37
|
+
* } else {
|
|
38
|
+
* console.log("输入完毕,敲回车是为了看一些开发者是否绑定了其他事件");
|
|
39
|
+
* }
|
|
40
|
+
* }
|
|
41
|
+
* }
|
|
42
|
+
* ...
|
|
43
|
+
* <input type="text" onKeyDown={enterDown} ref={inputRef} />
|
|
44
|
+
* ...
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
function useInputIsComposing(inputRef) {
|
|
48
|
+
/** 当前输入框是否输入模式结束 */
|
|
49
|
+
const isComposing = react.useRef(false);
|
|
50
|
+
/** 监视当前编辑器输入状态 */
|
|
51
|
+
react.useEffect(() => {
|
|
52
|
+
const _i = inputRef.current; /// 当前元素
|
|
53
|
+
if (!_i)
|
|
54
|
+
return; /// 没有(不可能事件但是)获取元素直接返回
|
|
55
|
+
const handleCompositionStart = () => (isComposing.current = true); /// 键盘输入
|
|
56
|
+
const handleCompositionEnd = () => (isComposing.current = false); /// 键盘输入结束
|
|
57
|
+
// _i.focus(); /// 让该元素显示时自动聚焦
|
|
58
|
+
_i.addEventListener('compositionstart', handleCompositionStart); /// 监听键盘输入事件
|
|
59
|
+
_i.addEventListener('compositionend', handleCompositionEnd); /// 监听键盘输入结束事件
|
|
60
|
+
return () => {
|
|
61
|
+
_i.removeEventListener('compositionstart', handleCompositionStart); /// 取消监听键盘输入事件
|
|
62
|
+
_i.removeEventListener('compositionend', handleCompositionEnd); /// 取消监听键盘输入结束事件
|
|
63
|
+
};
|
|
64
|
+
}, []);
|
|
65
|
+
return isComposing;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
exports.useInputIsComposing = useInputIsComposing;
|
package/cjs/useTimeId.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var react = require('react');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @module @vvi/react-hooks/useTimeId
|
|
8
|
+
* @file useTimeId.ts
|
|
9
|
+
* @description 使用定时器返回的时间戳
|
|
10
|
+
* @author MrMudBean <Mr.MudBean@outlook.com>
|
|
11
|
+
* @copyright 2026 ©️ MrMudBean
|
|
12
|
+
* @since 2025-01-07 11:20
|
|
13
|
+
* @version 2.0.0-alpha.0
|
|
14
|
+
* @lastModified 2026-07-06 17:13
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* # 导出一个使用 `useRef` 创建的 `NodeJS.Timeout`
|
|
18
|
+
*
|
|
19
|
+
* 该数值在组件卸载时会自动调用 `clearTimeout` 清理
|
|
20
|
+
* @see [useTimeId](https://npm.lmssee.com/react-hooks/use-time-id)
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { useTimeId } from '@vvi/react-hooks';
|
|
24
|
+
*
|
|
25
|
+
* ...
|
|
26
|
+
*
|
|
27
|
+
* const timeoutId = useTimeId();
|
|
28
|
+
*
|
|
29
|
+
* useEffect(()=>{
|
|
30
|
+
* timeoutId.current = setTimeout(()=>{
|
|
31
|
+
* ...
|
|
32
|
+
* } ,delay);
|
|
33
|
+
* });
|
|
34
|
+
* ...
|
|
35
|
+
* ```
|
|
36
|
+
* 其实,正确的用法是这样的:
|
|
37
|
+
* ```ts
|
|
38
|
+
* useEffect(()=>{
|
|
39
|
+
* const timeId = setTimeout(()=>{
|
|
40
|
+
* ...
|
|
41
|
+
* }, delay) ;
|
|
42
|
+
*
|
|
43
|
+
* return ()=> timeId && clearTimeout(timeId);
|
|
44
|
+
* });
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
function useTimeId() {
|
|
48
|
+
const timeId = react.useRef(undefined);
|
|
49
|
+
react.useEffect(() => () => (timeId.current && clearTimeout(timeId.current)) || undefined, []);
|
|
50
|
+
return timeId;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
exports.useTimeId = useTimeId;
|
package/es/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* @module @react-hooks/index
|
|
4
|
+
* @file index.ts
|
|
5
|
+
* @description _
|
|
6
|
+
* @author Mr.MudBean <Mr.MudBean@outlook.com>
|
|
7
|
+
* @license MIT
|
|
8
|
+
* @copyright 2026 ©️ Mr.MudBean
|
|
9
|
+
* @since 2026-07-06 16:13
|
|
10
|
+
* @version 0.0.0
|
|
11
|
+
* @lastModified 2026-07-06 16:18
|
|
12
|
+
*/
|
|
13
|
+
export { useAnimationFrame } from './useAnimationFrame';
|
|
14
|
+
export { useRipples } from './use-ripples';
|
|
15
|
+
export { useLazyRipples } from './use-lazy-ripple';
|
|
16
|
+
export { useInputIsComposing } from './useInputIsComposing';
|
|
17
|
+
export { useTimeId } from './useTimeId';
|
package/es/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { useAnimationFrame } from './useAnimationFrame.js';
|
|
2
|
+
export { useRipples } from './use-ripples.js';
|
|
3
|
+
export { useLazyRipples } from './use-lazy-ripple.js';
|
|
4
|
+
export { useInputIsComposing } from './useInputIsComposing.js';
|
|
5
|
+
export { useTimeId } from './useTimeId.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Ripples } from '@vvi/ripple';
|
|
2
|
+
import type { RipplesOptions } from '@vvi/ripple';
|
|
3
|
+
import type { RefObject } from 'react';
|
|
4
|
+
/**
|
|
5
|
+
* ## 懒加载使用涟漪 hook
|
|
6
|
+
* @param canvas
|
|
7
|
+
* @param option
|
|
8
|
+
*/
|
|
9
|
+
export declare function useLazyRipples(canvas: RefObject<HTMLCanvasElement | null>, option?: RipplesOptions): {
|
|
10
|
+
ripples: RefObject<Ripples | null>;
|
|
11
|
+
isLoading: boolean;
|
|
12
|
+
error: unknown;
|
|
13
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { isNull } from '@vvi/is';
|
|
3
|
+
import { useRef, useState, useEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ## 懒加载使用涟漪 hook
|
|
7
|
+
* @param canvas
|
|
8
|
+
* @param option
|
|
9
|
+
*/
|
|
10
|
+
function useLazyRipples(canvas, option) {
|
|
11
|
+
/** react dom */
|
|
12
|
+
const ripples = useRef(null);
|
|
13
|
+
/** 当前是否被卸载 */
|
|
14
|
+
const isUnmounted = useRef(true);
|
|
15
|
+
/** 是否已加载 */
|
|
16
|
+
const isLoaded = useRef(false);
|
|
17
|
+
// 加载状态
|
|
18
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
19
|
+
// 错误状态
|
|
20
|
+
const [error, setError] = useState(null);
|
|
21
|
+
// 参数
|
|
22
|
+
const optionRef = useRef(option);
|
|
23
|
+
/** 卸载 */
|
|
24
|
+
function unmounted() {
|
|
25
|
+
isUnmounted.current = true;
|
|
26
|
+
setTimeout(() => {
|
|
27
|
+
// 当前是被卸载状态
|
|
28
|
+
if (isUnmounted.current) {
|
|
29
|
+
ripples.current?.destroy();
|
|
30
|
+
}
|
|
31
|
+
}, 0);
|
|
32
|
+
}
|
|
33
|
+
// 更新 option,防止加载 option 为旧数据
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
optionRef.current = option;
|
|
36
|
+
}, [option]);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
isUnmounted.current = false;
|
|
39
|
+
/** 非空检验(这里一般都是有值的,除非故障) */
|
|
40
|
+
if (isNull(canvas.current) || isLoaded.current) {
|
|
41
|
+
return unmounted;
|
|
42
|
+
}
|
|
43
|
+
// 设置加载状态
|
|
44
|
+
setIsLoading(true);
|
|
45
|
+
isLoaded.current = true;
|
|
46
|
+
import('@vvi/ripple')
|
|
47
|
+
.then(module => {
|
|
48
|
+
if (isNull(canvas.current) || isUnmounted.current) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
ripples.current = new module.Ripples(canvas.current, optionRef.current);
|
|
52
|
+
return;
|
|
53
|
+
})
|
|
54
|
+
.catch(err => {
|
|
55
|
+
setError(err);
|
|
56
|
+
})
|
|
57
|
+
.finally(() => {
|
|
58
|
+
setIsLoading(false);
|
|
59
|
+
});
|
|
60
|
+
return unmounted;
|
|
61
|
+
}, []);
|
|
62
|
+
return { ripples, isLoading, error };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export { useLazyRipples };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Ripples, type RipplesOptions } from '@vvi/ripple';
|
|
2
|
+
import type { RefObject } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* ## 使用绘制 ripples 上一层
|
|
5
|
+
* @param canvas `usrRef` 包裹的 `HTMLCanvasElement`,用于绘制图像
|
|
6
|
+
* @param option 初始化的
|
|
7
|
+
* @see [useRipples](https://lmssee.com/custom-hooks/use-ripples)
|
|
8
|
+
* @see [JQuery ripple](https://github.com/sirxemic/jquery.ripples)
|
|
9
|
+
* @example
|
|
10
|
+
* 下面是在 <BackgroundRipple> 中使用
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { useRipples } from '@vvi/react-hooks';
|
|
13
|
+
*
|
|
14
|
+
* export function BackgroundRipple(props: BackgroundRipplesProps) {
|
|
15
|
+
*
|
|
16
|
+
* // canvas 元素
|
|
17
|
+
* const canvas = useRef<HTMLCanvasElement>(null);
|
|
18
|
+
*
|
|
19
|
+
* // 使用 ripples
|
|
20
|
+
* const ripplesRef = useRipples(canvas, props);
|
|
21
|
+
*
|
|
22
|
+
* return (<>
|
|
23
|
+
* <canvas ref={canvas}></canvas>
|
|
24
|
+
* {props.children}
|
|
25
|
+
* </>);
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare function useRipples(canvas: RefObject<HTMLCanvasElement | null>, option?: RipplesOptions): RefObject<Ripples | null>;
|
|
30
|
+
export { Ripples };
|
|
31
|
+
export type { RipplesOptions };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { isNull } from '@vvi/is';
|
|
3
|
+
import { Ripples } from '@vvi/ripple';
|
|
4
|
+
export { Ripples } from '@vvi/ripple';
|
|
5
|
+
import { useRef, useEffect } from 'react';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @module @vvi/react-hooks/index
|
|
9
|
+
* @file use-ripples.ts
|
|
10
|
+
* @description ripple 的主要核心逻辑
|
|
11
|
+
* @author MrMudBean <Mr.MudBean@outlook.com>
|
|
12
|
+
* @copyright 2026 ©️ MrMudBean
|
|
13
|
+
* @since 2025-06-20 01:37
|
|
14
|
+
* @version 2.0.0-alpha.0
|
|
15
|
+
* @lastModified 2026-07-07 09:14
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* ## 使用绘制 ripples 上一层
|
|
19
|
+
* @param canvas `usrRef` 包裹的 `HTMLCanvasElement`,用于绘制图像
|
|
20
|
+
* @param option 初始化的
|
|
21
|
+
* @see [useRipples](https://lmssee.com/custom-hooks/use-ripples)
|
|
22
|
+
* @see [JQuery ripple](https://github.com/sirxemic/jquery.ripples)
|
|
23
|
+
* @example
|
|
24
|
+
* 下面是在 <BackgroundRipple> 中使用
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { useRipples } from '@vvi/react-hooks';
|
|
27
|
+
*
|
|
28
|
+
* export function BackgroundRipple(props: BackgroundRipplesProps) {
|
|
29
|
+
*
|
|
30
|
+
* // canvas 元素
|
|
31
|
+
* const canvas = useRef<HTMLCanvasElement>(null);
|
|
32
|
+
*
|
|
33
|
+
* // 使用 ripples
|
|
34
|
+
* const ripplesRef = useRipples(canvas, props);
|
|
35
|
+
*
|
|
36
|
+
* return (<>
|
|
37
|
+
* <canvas ref={canvas}></canvas>
|
|
38
|
+
* {props.children}
|
|
39
|
+
* </>);
|
|
40
|
+
* }
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
function useRipples(canvas, option) {
|
|
44
|
+
/** react dom */
|
|
45
|
+
const ripples = useRef(null);
|
|
46
|
+
// 初始化数据
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
/** 非空检验(这里一般都是有值的,除非故障) */
|
|
49
|
+
if (isNull(canvas.current))
|
|
50
|
+
return;
|
|
51
|
+
try {
|
|
52
|
+
ripples.current = new Ripples(canvas.current, option);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
}
|
|
56
|
+
return () => ripples.current?.destroy();
|
|
57
|
+
}, []);
|
|
58
|
+
return ripples;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { useRipples };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @vvi/react-hooks/useAnimationFrame
|
|
3
|
+
* @file useAnimationFrame.ts
|
|
4
|
+
* @description 使用 animationFrame
|
|
5
|
+
* @author MrMudBean <Mr.MudBean@outlook.com>
|
|
6
|
+
* @copyright 2026 ©️ MrMudBean
|
|
7
|
+
* @since 2025-01-07 11:23
|
|
8
|
+
* @version 2.0.0-alpha.0
|
|
9
|
+
* @lastModified 2026-07-06 17:16
|
|
10
|
+
*/
|
|
11
|
+
/** 使用动画结果 */
|
|
12
|
+
export interface UseAnimationFrameResult {
|
|
13
|
+
/** 取消执行动画 */
|
|
14
|
+
cancel(): void;
|
|
15
|
+
/** 当前取消的状态 */
|
|
16
|
+
canceled: boolean;
|
|
17
|
+
/** 再次执行 */
|
|
18
|
+
render(): void;
|
|
19
|
+
}
|
|
20
|
+
export interface UseAnimationFrame {
|
|
21
|
+
/** 当前动画的 id */
|
|
22
|
+
id: null | number;
|
|
23
|
+
/** 执行的时间 */
|
|
24
|
+
time: number;
|
|
25
|
+
/** 暂停的时间 */
|
|
26
|
+
cancelTime: number;
|
|
27
|
+
/** 返回值 */
|
|
28
|
+
result: UseAnimationFrameResult;
|
|
29
|
+
immediately: boolean;
|
|
30
|
+
/** 是否完成了额外的首次执行 */
|
|
31
|
+
noun: boolean;
|
|
32
|
+
/** 是否仅执行一次 */
|
|
33
|
+
once: boolean;
|
|
34
|
+
/** 是否首次执行 effect */
|
|
35
|
+
firstRunEffect: boolean;
|
|
36
|
+
/** 是否已卸载 */
|
|
37
|
+
isUnmounted: boolean;
|
|
38
|
+
}
|
|
39
|
+
export type AnimationFrameOption = boolean | {
|
|
40
|
+
immediately?: boolean;
|
|
41
|
+
once?: boolean;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* # 该钩子在组件卸载时会自动调用 `window.cancelAnimationFrame` 清理传入的回调方法
|
|
45
|
+
*
|
|
46
|
+
* 默认是传入即启动调用方法
|
|
47
|
+
* @description 创建一个会自己关闭(组建卸载时)的 animationFrame。当然,可以通过返回值手动终止
|
|
48
|
+
* @param callback 使用回调函数。回调函数有两个参数,除了默认的时间,还有另一个用于暂停后的记时时间。如果没有主动暂停,两者时间总是趋近于相等的(可能有其他延迟造成时间差越来越大)。如果没有暂停需求,使用第一个默认时间就可以了。
|
|
49
|
+
* @param [option=false] 使用第二参数,可指定是否立即执行及是否仅执行一次
|
|
50
|
+
* @returns 返回值包含执行状态
|
|
51
|
+
* @see [useAnimationFrame](https://npm.lmssee.com/react-hooks/use-animation-frame)
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* import { useAnimationFrame } from '@vvi/react-hooks';
|
|
55
|
+
* ...
|
|
56
|
+
*
|
|
57
|
+
* export function Home () {
|
|
58
|
+
* // 如果没有主动暂停的需要,直接使用默认的 time 最好。如果中间有使用暂停。那么 time 值是不连贯的,而 runTime 是连贯的
|
|
59
|
+
* const result = useAnimationFrame((time ,runTime) => {
|
|
60
|
+
* ... //
|
|
61
|
+
* });
|
|
62
|
+
*
|
|
63
|
+
* return <>
|
|
64
|
+
*
|
|
65
|
+
* <button onclick={() => result.cancel()}>暂停</button>
|
|
66
|
+
* <button onclick={() => result.render()}>恢复</button>
|
|
67
|
+
* </>
|
|
68
|
+
* }
|
|
69
|
+
* ...
|
|
70
|
+
* ```
|
|
71
|
+
* 当然,可使用第二参数执行立即执行还是仅执行一次(作用不大,不与赘述)
|
|
72
|
+
*/
|
|
73
|
+
export declare function useAnimationFrame(callback: (time: number, runtime: number) => void, option?: AnimationFrameOption): UseAnimationFrameResult;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { isBoolean, isPlainObject } from '@vvi/is';
|
|
3
|
+
import { useRef, useCallback, useEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @module @vvi/react-hooks/useAnimationFrame
|
|
7
|
+
* @file useAnimationFrame.ts
|
|
8
|
+
* @description 使用 animationFrame
|
|
9
|
+
* @author MrMudBean <Mr.MudBean@outlook.com>
|
|
10
|
+
* @copyright 2026 ©️ MrMudBean
|
|
11
|
+
* @since 2025-01-07 11:23
|
|
12
|
+
* @version 2.0.0-alpha.0
|
|
13
|
+
* @lastModified 2026-07-06 17:16
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* # 该钩子在组件卸载时会自动调用 `window.cancelAnimationFrame` 清理传入的回调方法
|
|
17
|
+
*
|
|
18
|
+
* 默认是传入即启动调用方法
|
|
19
|
+
* @description 创建一个会自己关闭(组建卸载时)的 animationFrame。当然,可以通过返回值手动终止
|
|
20
|
+
* @param callback 使用回调函数。回调函数有两个参数,除了默认的时间,还有另一个用于暂停后的记时时间。如果没有主动暂停,两者时间总是趋近于相等的(可能有其他延迟造成时间差越来越大)。如果没有暂停需求,使用第一个默认时间就可以了。
|
|
21
|
+
* @param [option=false] 使用第二参数,可指定是否立即执行及是否仅执行一次
|
|
22
|
+
* @returns 返回值包含执行状态
|
|
23
|
+
* @see [useAnimationFrame](https://npm.lmssee.com/react-hooks/use-animation-frame)
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { useAnimationFrame } from '@vvi/react-hooks';
|
|
27
|
+
* ...
|
|
28
|
+
*
|
|
29
|
+
* export function Home () {
|
|
30
|
+
* // 如果没有主动暂停的需要,直接使用默认的 time 最好。如果中间有使用暂停。那么 time 值是不连贯的,而 runTime 是连贯的
|
|
31
|
+
* const result = useAnimationFrame((time ,runTime) => {
|
|
32
|
+
* ... //
|
|
33
|
+
* });
|
|
34
|
+
*
|
|
35
|
+
* return <>
|
|
36
|
+
*
|
|
37
|
+
* <button onclick={() => result.cancel()}>暂停</button>
|
|
38
|
+
* <button onclick={() => result.render()}>恢复</button>
|
|
39
|
+
* </>
|
|
40
|
+
* }
|
|
41
|
+
* ...
|
|
42
|
+
* ```
|
|
43
|
+
* 当然,可使用第二参数执行立即执行还是仅执行一次(作用不大,不与赘述)
|
|
44
|
+
*/
|
|
45
|
+
function useAnimationFrame(callback, option = false) {
|
|
46
|
+
if (isBoolean(option)) {
|
|
47
|
+
option = {
|
|
48
|
+
immediately: option,
|
|
49
|
+
once: false,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
else if (isPlainObject(option)) {
|
|
53
|
+
option = {
|
|
54
|
+
once: isBoolean(option.once) ? option.once : false,
|
|
55
|
+
immediately: isBoolean(option.immediately) ? option.immediately : false,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
option = {
|
|
60
|
+
immediately: false,
|
|
61
|
+
once: false,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** */
|
|
65
|
+
const animationFrame = useRef({
|
|
66
|
+
id: 0,
|
|
67
|
+
immediately: option.immediately ?? true,
|
|
68
|
+
once: option.once ?? false,
|
|
69
|
+
noun: false,
|
|
70
|
+
time: 0,
|
|
71
|
+
cancelTime: 0,
|
|
72
|
+
firstRunEffect: true,
|
|
73
|
+
isUnmounted: true,
|
|
74
|
+
result: {
|
|
75
|
+
cancel() {
|
|
76
|
+
const { id } = animationFrame.current;
|
|
77
|
+
if (id)
|
|
78
|
+
window.cancelAnimationFrame(id);
|
|
79
|
+
animationFrame.current.result.canceled = true;
|
|
80
|
+
},
|
|
81
|
+
canceled: false,
|
|
82
|
+
render() {
|
|
83
|
+
const { current } = animationFrame;
|
|
84
|
+
const { result } = current;
|
|
85
|
+
current.cancelTime = performance.now();
|
|
86
|
+
result.cancel();
|
|
87
|
+
result.canceled = false;
|
|
88
|
+
current.id = window.requestAnimationFrame(_);
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
const { current } = animationFrame;
|
|
93
|
+
/** 转化为函数 */
|
|
94
|
+
const _ = useCallback((time) => {
|
|
95
|
+
const { result } = current;
|
|
96
|
+
const _time = time - current.cancelTime;
|
|
97
|
+
current.time += _time < 0 ? 16.7 : _time;
|
|
98
|
+
callback(time, current.time);
|
|
99
|
+
// 如果不是仅执行一次,进入下一次执行
|
|
100
|
+
if (!current.once)
|
|
101
|
+
result.render();
|
|
102
|
+
}, [callback]);
|
|
103
|
+
// 首次执行
|
|
104
|
+
if (current.immediately && !current.noun && current.result.canceled) {
|
|
105
|
+
current.noun = true;
|
|
106
|
+
current.result.render();
|
|
107
|
+
}
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
/// 非仅执行一次再次这里需要执行
|
|
110
|
+
if (!current.once) {
|
|
111
|
+
if (current.firstRunEffect) {
|
|
112
|
+
current.result.render();
|
|
113
|
+
current.firstRunEffect = false; // 标记为不允许执行
|
|
114
|
+
}
|
|
115
|
+
else if (!current.firstRunEffect && !current.result.canceled) {
|
|
116
|
+
/** 非第一次执行 */
|
|
117
|
+
current.result.render();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}, [callback]);
|
|
121
|
+
/// 在组件退出时保证能正确的退出
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
current.isUnmounted = false;
|
|
124
|
+
return () => {
|
|
125
|
+
current.isUnmounted = true;
|
|
126
|
+
setTimeout(() => {
|
|
127
|
+
if (current.isUnmounted)
|
|
128
|
+
current.result.cancel();
|
|
129
|
+
}, 0);
|
|
130
|
+
};
|
|
131
|
+
}, []);
|
|
132
|
+
return current.result;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export { useAnimationFrame };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @vvi/react-hooks/useInputIsComposing
|
|
3
|
+
* @file useInputIsComposing.ts
|
|
4
|
+
* @description 使用输入框是否完成输入状态
|
|
5
|
+
* @author MrMudBean <Mr.MudBean@outlook.com>
|
|
6
|
+
* @copyright 2026 ©️ MrMudBean
|
|
7
|
+
* @since 2025-01-07 11:23
|
|
8
|
+
* @version 2.0.0-alpha.0
|
|
9
|
+
* @lastModified 2026-07-06 17:16
|
|
10
|
+
*/
|
|
11
|
+
import type { RefObject } from 'react';
|
|
12
|
+
/**
|
|
13
|
+
* # 导出一个使用 `useRef` 创建的 `boolean`
|
|
14
|
+
*
|
|
15
|
+
* *由于是包裹在 RefObject 之中,判断时务必使用 `isComposing.current` 进行判断*
|
|
16
|
+
* @description 用于判断当前的输入状态是否结束
|
|
17
|
+
* @param inputRef [RefObject<HTMLInputElement | HTMLTextAreaElement>] 输入框的 ref
|
|
18
|
+
* @returns RefObject<boolean>
|
|
19
|
+
* @see [useInputIscComposing](https://npm.lmssee.com/react-hooks/use-input-is-composing)
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* import { useInputIsComposing } from '@vvi/react-hooks';
|
|
23
|
+
*
|
|
24
|
+
* ...
|
|
25
|
+
* const inputRef = useRef<HTMLInputElement>(null);
|
|
26
|
+
*
|
|
27
|
+
* const inputIsComposing = useInputIsComposing(inputRef);
|
|
28
|
+
* ...
|
|
29
|
+
* function enterDown(e: React.KeyboardEvent<HTMLInputElement>) {
|
|
30
|
+
* if (e.key === 'Enter') {
|
|
31
|
+
* if (isComposing.current) {
|
|
32
|
+
* console.log("此时此景,按回车键说明为了从候选词中挑选");
|
|
33
|
+
* } else {
|
|
34
|
+
* console.log("输入完毕,敲回车是为了看一些开发者是否绑定了其他事件");
|
|
35
|
+
* }
|
|
36
|
+
* }
|
|
37
|
+
* }
|
|
38
|
+
* ...
|
|
39
|
+
* <input type="text" onKeyDown={enterDown} ref={inputRef} />
|
|
40
|
+
* ...
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export declare function useInputIsComposing(inputRef: RefObject<HTMLInputElement | HTMLTextAreaElement | null>): RefObject<boolean>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useRef, useEffect } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @module @vvi/react-hooks/useInputIsComposing
|
|
6
|
+
* @file useInputIsComposing.ts
|
|
7
|
+
* @description 使用输入框是否完成输入状态
|
|
8
|
+
* @author MrMudBean <Mr.MudBean@outlook.com>
|
|
9
|
+
* @copyright 2026 ©️ MrMudBean
|
|
10
|
+
* @since 2025-01-07 11:23
|
|
11
|
+
* @version 2.0.0-alpha.0
|
|
12
|
+
* @lastModified 2026-07-06 17:16
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* # 导出一个使用 `useRef` 创建的 `boolean`
|
|
16
|
+
*
|
|
17
|
+
* *由于是包裹在 RefObject 之中,判断时务必使用 `isComposing.current` 进行判断*
|
|
18
|
+
* @description 用于判断当前的输入状态是否结束
|
|
19
|
+
* @param inputRef [RefObject<HTMLInputElement | HTMLTextAreaElement>] 输入框的 ref
|
|
20
|
+
* @returns RefObject<boolean>
|
|
21
|
+
* @see [useInputIscComposing](https://npm.lmssee.com/react-hooks/use-input-is-composing)
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* import { useInputIsComposing } from '@vvi/react-hooks';
|
|
25
|
+
*
|
|
26
|
+
* ...
|
|
27
|
+
* const inputRef = useRef<HTMLInputElement>(null);
|
|
28
|
+
*
|
|
29
|
+
* const inputIsComposing = useInputIsComposing(inputRef);
|
|
30
|
+
* ...
|
|
31
|
+
* function enterDown(e: React.KeyboardEvent<HTMLInputElement>) {
|
|
32
|
+
* if (e.key === 'Enter') {
|
|
33
|
+
* if (isComposing.current) {
|
|
34
|
+
* console.log("此时此景,按回车键说明为了从候选词中挑选");
|
|
35
|
+
* } else {
|
|
36
|
+
* console.log("输入完毕,敲回车是为了看一些开发者是否绑定了其他事件");
|
|
37
|
+
* }
|
|
38
|
+
* }
|
|
39
|
+
* }
|
|
40
|
+
* ...
|
|
41
|
+
* <input type="text" onKeyDown={enterDown} ref={inputRef} />
|
|
42
|
+
* ...
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
function useInputIsComposing(inputRef) {
|
|
46
|
+
/** 当前输入框是否输入模式结束 */
|
|
47
|
+
const isComposing = useRef(false);
|
|
48
|
+
/** 监视当前编辑器输入状态 */
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const _i = inputRef.current; /// 当前元素
|
|
51
|
+
if (!_i)
|
|
52
|
+
return; /// 没有(不可能事件但是)获取元素直接返回
|
|
53
|
+
const handleCompositionStart = () => (isComposing.current = true); /// 键盘输入
|
|
54
|
+
const handleCompositionEnd = () => (isComposing.current = false); /// 键盘输入结束
|
|
55
|
+
// _i.focus(); /// 让该元素显示时自动聚焦
|
|
56
|
+
_i.addEventListener('compositionstart', handleCompositionStart); /// 监听键盘输入事件
|
|
57
|
+
_i.addEventListener('compositionend', handleCompositionEnd); /// 监听键盘输入结束事件
|
|
58
|
+
return () => {
|
|
59
|
+
_i.removeEventListener('compositionstart', handleCompositionStart); /// 取消监听键盘输入事件
|
|
60
|
+
_i.removeEventListener('compositionend', handleCompositionEnd); /// 取消监听键盘输入结束事件
|
|
61
|
+
};
|
|
62
|
+
}, []);
|
|
63
|
+
return isComposing;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { useInputIsComposing };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @vvi/react-hooks/useTimeId
|
|
3
|
+
* @file useTimeId.ts
|
|
4
|
+
* @description 使用定时器返回的时间戳
|
|
5
|
+
* @author MrMudBean <Mr.MudBean@outlook.com>
|
|
6
|
+
* @copyright 2026 ©️ MrMudBean
|
|
7
|
+
* @since 2025-01-07 11:20
|
|
8
|
+
* @version 2.0.0-alpha.0
|
|
9
|
+
* @lastModified 2026-07-06 17:13
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* # 导出一个使用 `useRef` 创建的 `NodeJS.Timeout`
|
|
13
|
+
*
|
|
14
|
+
* 该数值在组件卸载时会自动调用 `clearTimeout` 清理
|
|
15
|
+
* @see [useTimeId](https://npm.lmssee.com/react-hooks/use-time-id)
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { useTimeId } from '@vvi/react-hooks';
|
|
19
|
+
*
|
|
20
|
+
* ...
|
|
21
|
+
*
|
|
22
|
+
* const timeoutId = useTimeId();
|
|
23
|
+
*
|
|
24
|
+
* useEffect(()=>{
|
|
25
|
+
* timeoutId.current = setTimeout(()=>{
|
|
26
|
+
* ...
|
|
27
|
+
* } ,delay);
|
|
28
|
+
* });
|
|
29
|
+
* ...
|
|
30
|
+
* ```
|
|
31
|
+
* 其实,正确的用法是这样的:
|
|
32
|
+
* ```ts
|
|
33
|
+
* useEffect(()=>{
|
|
34
|
+
* const timeId = setTimeout(()=>{
|
|
35
|
+
* ...
|
|
36
|
+
* }, delay) ;
|
|
37
|
+
*
|
|
38
|
+
* return ()=> timeId && clearTimeout(timeId);
|
|
39
|
+
* });
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export declare function useTimeId(): import("react").RefObject<number | undefined>;
|
package/es/useTimeId.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useRef, useEffect } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @module @vvi/react-hooks/useTimeId
|
|
6
|
+
* @file useTimeId.ts
|
|
7
|
+
* @description 使用定时器返回的时间戳
|
|
8
|
+
* @author MrMudBean <Mr.MudBean@outlook.com>
|
|
9
|
+
* @copyright 2026 ©️ MrMudBean
|
|
10
|
+
* @since 2025-01-07 11:20
|
|
11
|
+
* @version 2.0.0-alpha.0
|
|
12
|
+
* @lastModified 2026-07-06 17:13
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* # 导出一个使用 `useRef` 创建的 `NodeJS.Timeout`
|
|
16
|
+
*
|
|
17
|
+
* 该数值在组件卸载时会自动调用 `clearTimeout` 清理
|
|
18
|
+
* @see [useTimeId](https://npm.lmssee.com/react-hooks/use-time-id)
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* import { useTimeId } from '@vvi/react-hooks';
|
|
22
|
+
*
|
|
23
|
+
* ...
|
|
24
|
+
*
|
|
25
|
+
* const timeoutId = useTimeId();
|
|
26
|
+
*
|
|
27
|
+
* useEffect(()=>{
|
|
28
|
+
* timeoutId.current = setTimeout(()=>{
|
|
29
|
+
* ...
|
|
30
|
+
* } ,delay);
|
|
31
|
+
* });
|
|
32
|
+
* ...
|
|
33
|
+
* ```
|
|
34
|
+
* 其实,正确的用法是这样的:
|
|
35
|
+
* ```ts
|
|
36
|
+
* useEffect(()=>{
|
|
37
|
+
* const timeId = setTimeout(()=>{
|
|
38
|
+
* ...
|
|
39
|
+
* }, delay) ;
|
|
40
|
+
*
|
|
41
|
+
* return ()=> timeId && clearTimeout(timeId);
|
|
42
|
+
* });
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
function useTimeId() {
|
|
46
|
+
const timeId = useRef(undefined);
|
|
47
|
+
useEffect(() => () => (timeId.current && clearTimeout(timeId.current)) || undefined, []);
|
|
48
|
+
return timeId;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export { useTimeId };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @react-hooks/qqi
|
|
3
|
+
* @file dog.ts
|
|
4
|
+
* @description _
|
|
5
|
+
* @author Mr.MudBean <Mr.MudBean@outlook.com>
|
|
6
|
+
* @copyright 2026 ©️ Mr.MudBean
|
|
7
|
+
* @since 2026-07-06 16:04
|
|
8
|
+
* @version 0.0.0
|
|
9
|
+
* @lastModified 2026-07-06 16:05
|
|
10
|
+
*/
|
|
11
|
+
export declare const dog: import("@vvi/log").DevLog;
|
package/package.json
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vvi/react-hooks",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "cjs/index.js",
|
|
6
|
+
"module": "es/index.js",
|
|
7
|
+
"types": "es/index.d.ts",
|
|
8
|
+
"author": {
|
|
9
|
+
"name": "泥豆君",
|
|
10
|
+
"email": "Mr.MudBean@outlook.com",
|
|
11
|
+
"url": "https://mudbean.cn"
|
|
12
|
+
},
|
|
13
|
+
"description": "React hooks of vvi",
|
|
14
|
+
"sideEffects": false,
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"files": [
|
|
17
|
+
"cjs",
|
|
18
|
+
"es",
|
|
19
|
+
"LICENSE",
|
|
20
|
+
"README.md",
|
|
21
|
+
"CHANGELOG.md"
|
|
22
|
+
],
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"import": {
|
|
26
|
+
"default": "./es/index.js",
|
|
27
|
+
"types": "./es/index.d.ts"
|
|
28
|
+
},
|
|
29
|
+
"require": {
|
|
30
|
+
"default": "./cjs/index.js",
|
|
31
|
+
"types": "./es/index.d.ts"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"./use-lazy-ripple": {
|
|
35
|
+
"import": {
|
|
36
|
+
"default": "./es/use-lazy-ripple.js",
|
|
37
|
+
"types": "./es/use-lazy-ripple.d.ts"
|
|
38
|
+
},
|
|
39
|
+
"require": {
|
|
40
|
+
"default": "./cjs/use-lazy-ripple.js",
|
|
41
|
+
"types": "./es/use-lazy-ripple.d.ts"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"./use-ripples": {
|
|
45
|
+
"import": {
|
|
46
|
+
"default": "./es/use-ripples.js",
|
|
47
|
+
"types": "./es/use-ripples.d.ts"
|
|
48
|
+
},
|
|
49
|
+
"require": {
|
|
50
|
+
"default": "./cjs/use-ripples.js",
|
|
51
|
+
"types": "./es/use-ripples.d.ts"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"./useAnimationFrame": {
|
|
55
|
+
"import": {
|
|
56
|
+
"default": "./es/useAnimationFrame.js",
|
|
57
|
+
"types": "./es/useAnimationFrame.d.ts"
|
|
58
|
+
},
|
|
59
|
+
"require": {
|
|
60
|
+
"default": "./cjs/useAnimationFrame.js",
|
|
61
|
+
"types": "./es/useAnimationFrame.d.ts"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"./useInputIsComposing": {
|
|
65
|
+
"import": {
|
|
66
|
+
"default": "./es/useInputIsComposing.js",
|
|
67
|
+
"types": "./es/useInputIsComposing.d.ts"
|
|
68
|
+
},
|
|
69
|
+
"require": {
|
|
70
|
+
"default": "./cjs/useInputIsComposing.js",
|
|
71
|
+
"types": "./es/useInputIsComposing.d.ts"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"./useTimeId": {
|
|
75
|
+
"import": {
|
|
76
|
+
"default": "./es/useTimeId.js",
|
|
77
|
+
"types": "./es/useTimeId.d.ts"
|
|
78
|
+
},
|
|
79
|
+
"require": {
|
|
80
|
+
"default": "./cjs/useTimeId.js",
|
|
81
|
+
"types": "./es/useTimeId.d.ts"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"keywords": [
|
|
86
|
+
"react-hooks",
|
|
87
|
+
"mudbean",
|
|
88
|
+
"vvi"
|
|
89
|
+
],
|
|
90
|
+
"homepage": "https://npm.lmssee.com/react-hooks",
|
|
91
|
+
"dependencies": {
|
|
92
|
+
"@vvi/is": "^2.0.5",
|
|
93
|
+
"@vvi/ripple": "^1.0.0",
|
|
94
|
+
"@vvi/state": "^0.0.2"
|
|
95
|
+
},
|
|
96
|
+
"peerDependencies": {
|
|
97
|
+
"react": "^19.2"
|
|
98
|
+
},
|
|
99
|
+
"bugs": {
|
|
100
|
+
"url": "https://github.com/MrMudBean/react-hooks/issues",
|
|
101
|
+
"email": "Mr.MudBean@outlook.com"
|
|
102
|
+
},
|
|
103
|
+
"repository": {
|
|
104
|
+
"type": "git",
|
|
105
|
+
"url": "git+https://github.com/MrMudBean/react-hooks.git"
|
|
106
|
+
},
|
|
107
|
+
"publishConfig": {
|
|
108
|
+
"access": "public",
|
|
109
|
+
"registry": "https://registry.npmjs.org/"
|
|
110
|
+
},
|
|
111
|
+
"browserslist": [
|
|
112
|
+
"> 1%",
|
|
113
|
+
"last 2 versions"
|
|
114
|
+
],
|
|
115
|
+
"engines": {
|
|
116
|
+
"node": ">=18.0.0"
|
|
117
|
+
}
|
|
118
|
+
}
|