donar 0.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/.cspell/custom-dictionary.txt +5 -0
- package/.github/actions/base-check/action.yaml +27 -0
- package/.github/actions/env-setup/action.yaml +74 -0
- package/.github/workflows/pr-check.yaml +33 -0
- package/.github/workflows/release.yaml +112 -0
- package/.gitignore +41 -0
- package/.node-version +1 -0
- package/.npmignore +7 -0
- package/.vscode/extensions.json +10 -0
- package/.vscode/settings.json +44 -0
- package/LICENSE +21 -0
- package/README.md +160 -0
- package/commitlint.config.ts +3 -0
- package/cspell.json +27 -0
- package/dist/components.esm.js +2 -0
- package/dist/css/components.C24nnsjt.css +1 -0
- package/dist/hooks.esm.js +185 -0
- package/dist/index.esm.js +4 -0
- package/dist/js/components.nFDoAkCq.js +198 -0
- package/dist/js/utils.CVb1iSAU.js +330 -0
- package/dist/types/components.d.ts +1 -0
- package/dist/types/hooks.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/src/components/async-custom-show/index.d.ts +22 -0
- package/dist/types/src/components/carousel/hooks.d.ts +74 -0
- package/dist/types/src/components/carousel/index.d.ts +88 -0
- package/dist/types/src/components/custom-show/index.d.ts +21 -0
- package/dist/types/src/components/error-boundary/index.d.ts +31 -0
- package/dist/types/src/components/index.d.ts +8 -0
- package/dist/types/src/hooks/async-guard.d.ts +9 -0
- package/dist/types/src/hooks/index.d.ts +5 -0
- package/dist/types/src/hooks/observer.d.ts +70 -0
- package/dist/types/src/hooks/state.d.ts +44 -0
- package/dist/types/src/hooks/storage.d.ts +25 -0
- package/dist/types/src/hooks/timer.d.ts +16 -0
- package/dist/types/src/index.d.ts +3 -0
- package/dist/types/src/test/emiter-test.d.ts +1 -0
- package/dist/types/src/test/index.d.ts +1 -0
- package/dist/types/src/test/retry-async-test.d.ts +1 -0
- package/dist/types/src/utils/class-singleton.d.ts +6 -0
- package/dist/types/src/utils/concurrency.d.ts +17 -0
- package/dist/types/src/utils/debounce.d.ts +8 -0
- package/dist/types/src/utils/deep-copy.d.ts +11 -0
- package/dist/types/src/utils/dev.d.ts +8 -0
- package/dist/types/src/utils/download.d.ts +15 -0
- package/dist/types/src/utils/event-emitter/helpers.d.ts +36 -0
- package/dist/types/src/utils/event-emitter/index.d.ts +65 -0
- package/dist/types/src/utils/fetch-xhr/helpers.d.ts +28 -0
- package/dist/types/src/utils/fetch-xhr/index.d.ts +25 -0
- package/dist/types/src/utils/hash.d.ts +8 -0
- package/dist/types/src/utils/index.d.ts +15 -0
- package/dist/types/src/utils/is-deep-plain-equal.d.ts +15 -0
- package/dist/types/src/utils/json-convert.d.ts +66 -0
- package/dist/types/src/utils/micro-queue-scheduler.d.ts +14 -0
- package/dist/types/src/utils/raf-interval.d.ts +16 -0
- package/dist/types/src/utils/record-typed-map.d.ts +27 -0
- package/dist/types/src/utils/retry-async.d.ts +9 -0
- package/dist/types/src/utils/thenable.d.ts +15 -0
- package/dist/types/utils.d.ts +1 -0
- package/dist/utils.esm.js +2 -0
- package/eslint.config.ts +48 -0
- package/lint-staged.config.ts +13 -0
- package/oxfmt.config.ts +14 -0
- package/package.json +90 -0
- package/pnpm-workspace.yaml +3 -0
- package/scripts/sync-node-version/index.js +31 -0
- package/simple-git-hooks.js +4 -0
- package/src/components/async-custom-show/index.tsx +37 -0
- package/src/components/carousel/hooks.ts +312 -0
- package/src/components/carousel/index.module.scss +163 -0
- package/src/components/carousel/index.tsx +215 -0
- package/src/components/custom-show/index.tsx +31 -0
- package/src/components/error-boundary/index.tsx +48 -0
- package/src/components/index.ts +11 -0
- package/src/hooks/async-guard.ts +53 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/observer.ts +236 -0
- package/src/hooks/state.ts +140 -0
- package/src/hooks/storage.ts +103 -0
- package/src/hooks/timer.ts +42 -0
- package/src/index.ts +3 -0
- package/src/test/emiter-test.ts +19 -0
- package/src/test/index.ts +35 -0
- package/src/test/retry-async-test.ts +8 -0
- package/src/utils/class-singleton.ts +23 -0
- package/src/utils/concurrency.ts +49 -0
- package/src/utils/debounce.ts +20 -0
- package/src/utils/deep-copy.ts +132 -0
- package/src/utils/dev.ts +16 -0
- package/src/utils/download.ts +39 -0
- package/src/utils/event-emitter/helpers.ts +80 -0
- package/src/utils/event-emitter/index.ts +171 -0
- package/src/utils/fetch-xhr/helpers.ts +85 -0
- package/src/utils/fetch-xhr/index.ts +103 -0
- package/src/utils/hash.ts +25 -0
- package/src/utils/index.ts +18 -0
- package/src/utils/is-deep-plain-equal.ts +45 -0
- package/src/utils/json-convert.ts +257 -0
- package/src/utils/micro-queue-scheduler.ts +38 -0
- package/src/utils/raf-interval.ts +42 -0
- package/src/utils/record-typed-map.ts +38 -0
- package/src/utils/retry-async.ts +30 -0
- package/src/utils/thenable.ts +30 -0
- package/tsconfig.json +43 -0
- package/types/scss.d.ts +10 -0
- package/vite.config.ts +51 -0
package/package.json
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "donar",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Donar: A modern frontend library offering utility functions, React hooks, and components with TypeScript support, ESM only, and tree-shaking capabilities.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"module": "dist/index.esm.js",
|
|
7
|
+
"types": "dist/types/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.esm.js",
|
|
11
|
+
"types": "./dist/types/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./utils": {
|
|
14
|
+
"import": "./dist/utils.esm.js",
|
|
15
|
+
"types": "./dist/types/utils.d.ts"
|
|
16
|
+
},
|
|
17
|
+
"./hooks": {
|
|
18
|
+
"import": "./dist/hooks.esm.js",
|
|
19
|
+
"types": "./dist/types/hooks.d.ts"
|
|
20
|
+
},
|
|
21
|
+
"./components": {
|
|
22
|
+
"import": "./dist/components.esm.js",
|
|
23
|
+
"types": "./dist/types/components.d.ts"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "rimraf dist && vite build",
|
|
28
|
+
"lint": "eslint '{src,apps,packages}/**/*.{ts,tsx,js,jsx,vue}'",
|
|
29
|
+
"lint:fix": "eslint '{src,apps,packages}/**/*.{ts,tsx,js,jsx,vue}' --fix",
|
|
30
|
+
"lint:error": "eslint '{src,apps,packages}/**/*.{ts,tsx,js,jsx,vue}' --quiet",
|
|
31
|
+
"lint:spell": "cspell lint '{src,apps,packages}/**/*.{js,ts,jsx,tsx,mjs,cjs,json,css,less,scss,vue,html,md}'",
|
|
32
|
+
"fmt": "oxfmt '{src,packages,apps}/**/*.*'",
|
|
33
|
+
"fmt:check": "oxfmt --check '{src,packages,apps}/**/*.*'",
|
|
34
|
+
"preinstall": "npm run sync:node-version && (npx -y only-allow pnpm || exit 1)",
|
|
35
|
+
"prepare": "npx simple-git-hooks",
|
|
36
|
+
"sync:node-version": "node scripts/sync-node-version/index.js"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=24.14.0",
|
|
40
|
+
"pnpm": ">=10.32.1"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"react": "^18.0.0",
|
|
44
|
+
"react-dom": "^18.0.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@commitlint/cli": "^20.5.0",
|
|
48
|
+
"@commitlint/config-conventional": "^20.5.0",
|
|
49
|
+
"@eslint/js": "^9.39.4",
|
|
50
|
+
"@types/node": "^24.12.0",
|
|
51
|
+
"@types/react": "^18.3.28",
|
|
52
|
+
"@types/react-dom": "^18.3.7",
|
|
53
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
54
|
+
"cspell": "^9.7.0",
|
|
55
|
+
"eslint": "^9.39.4",
|
|
56
|
+
"eslint-config-prettier": "^10.1.8",
|
|
57
|
+
"eslint-plugin-jsdoc": "^62.8.1",
|
|
58
|
+
"eslint-plugin-react": "^7.37.5",
|
|
59
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
60
|
+
"eslint-plugin-react-refresh": "^0.5.2",
|
|
61
|
+
"globals": "^17.4.0",
|
|
62
|
+
"lint-staged": "^16.4.0",
|
|
63
|
+
"only-allow": "^1.2.2",
|
|
64
|
+
"oxfmt": "^0.42.0",
|
|
65
|
+
"react": "^18.3.1",
|
|
66
|
+
"react-dom": "^18.3.1",
|
|
67
|
+
"rimraf": "^6.1.3",
|
|
68
|
+
"sass-embedded": "^1.98.0",
|
|
69
|
+
"simple-git-hooks": "^2.13.1",
|
|
70
|
+
"typescript": "^6.0.2",
|
|
71
|
+
"typescript-eslint": "^8.57.2",
|
|
72
|
+
"vite": "^8.0.3",
|
|
73
|
+
"vite-plugin-dts": "^4.5.4",
|
|
74
|
+
"vite-plugin-lib-inject-css": "^2.2.2",
|
|
75
|
+
"vitest": "^4.1.2"
|
|
76
|
+
},
|
|
77
|
+
"keywords": [
|
|
78
|
+
"hooks",
|
|
79
|
+
"utils",
|
|
80
|
+
"components",
|
|
81
|
+
"react"
|
|
82
|
+
],
|
|
83
|
+
"author": "sonion",
|
|
84
|
+
"license": "MIT",
|
|
85
|
+
"repository": "https://github.com/sonion028/tonar",
|
|
86
|
+
"bugs": {
|
|
87
|
+
"url": "https://github.com/sonion028/tonar/issues"
|
|
88
|
+
},
|
|
89
|
+
"private": false
|
|
90
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/* global process */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
// 读取 package.json
|
|
6
|
+
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
7
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
8
|
+
|
|
9
|
+
// 提取 node 版本
|
|
10
|
+
const nodeVersion = packageJson.engines?.node;
|
|
11
|
+
|
|
12
|
+
if (!nodeVersion) {
|
|
13
|
+
console.error('Error: No node version found in package.json engines');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 提取版本号(去掉 >= 等前缀)
|
|
18
|
+
const versionMatch = nodeVersion.match(/>=?([0-9]+\.[0-9]+\.[0-9]+)/);
|
|
19
|
+
|
|
20
|
+
if (!versionMatch) {
|
|
21
|
+
console.error('Error: Could not parse node version:', nodeVersion);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const cleanVersion = versionMatch[1];
|
|
26
|
+
|
|
27
|
+
// 写入 .node-version 文件
|
|
28
|
+
const nodeVersionPath = path.join(process.cwd(), '.node-version');
|
|
29
|
+
fs.writeFileSync(nodeVersionPath, cleanVersion, 'utf8');
|
|
30
|
+
|
|
31
|
+
console.log(`Synced node version: ${cleanVersion}`);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type FC, type ReactNode, type JSX, useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export type AsyncCustomShowProps<T> = {
|
|
4
|
+
/** 异步判断 Promise */
|
|
5
|
+
when: Promise<T> | undefined | null | false;
|
|
6
|
+
/** 异步失败时的展示 */
|
|
7
|
+
fallback?: ReactNode;
|
|
8
|
+
/** 异步成功时的展示 */
|
|
9
|
+
children: (value: T | undefined | null | false) => ReactNode;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export interface AsyncCustomShowType<T> extends FC<AsyncCustomShowProps<T>> {
|
|
13
|
+
(props: AsyncCustomShowProps<T>): JSX.Element;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @author sonion
|
|
18
|
+
* @description 异步展示组件
|
|
19
|
+
* @param {AsyncCustomShowProps<T>} props - 组件属性
|
|
20
|
+
* @param {Promise<T> | undefined | null | false} props.when 异步判断 Promise
|
|
21
|
+
* @param {ReactNode} [props.fallback] 异步失败时的展示
|
|
22
|
+
* @param {(value: T | undefined | null | false) => ReactNode} props.children 异步成功时的展示
|
|
23
|
+
*/
|
|
24
|
+
function AsyncCustomShow<T>({
|
|
25
|
+
when,
|
|
26
|
+
fallback,
|
|
27
|
+
children,
|
|
28
|
+
}: AsyncCustomShowProps<T>) {
|
|
29
|
+
const [show, setShow] = useState<Awaited<typeof when>>(void 0);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!when) return;
|
|
32
|
+
Promise.resolve(when).then(setShow);
|
|
33
|
+
}, [when]);
|
|
34
|
+
return <>{show ? children(show) : fallback}</>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default AsyncCustomShow;
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ReactElement,
|
|
3
|
+
cloneElement,
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useMemo,
|
|
8
|
+
} from 'react';
|
|
9
|
+
|
|
10
|
+
import { useCreateSafeRef } from '@/hooks';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @author sonion
|
|
14
|
+
* @description 无动画切换
|
|
15
|
+
* @param {HTMLElement} el - 元素
|
|
16
|
+
* @param {number} index - 要切换的索引
|
|
17
|
+
* @returns {void}
|
|
18
|
+
*/
|
|
19
|
+
const noAnimationSwitch = (el: HTMLElement, index: number) => {
|
|
20
|
+
el.style.cssText = `transition-duration: 0s; --index: ${index}`;
|
|
21
|
+
el.clientLeft; // 触发回流
|
|
22
|
+
el.style.cssText = ''; // 恢复过渡动画
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @author sonion
|
|
27
|
+
* @description 更新翻页按钮的dom操作
|
|
28
|
+
* @param {HTMLElement} wrapper - 容器元素。就是轮播子项的父元素(轮播inner)的父元素
|
|
29
|
+
* @param {Array} showArrow - 左右箭头显示状态
|
|
30
|
+
* @returns {void}
|
|
31
|
+
*/
|
|
32
|
+
const arrowDomOperation = (
|
|
33
|
+
wrapper: HTMLElement,
|
|
34
|
+
showArrow: [boolean, boolean]
|
|
35
|
+
) => {
|
|
36
|
+
const arrow = wrapper.previousElementSibling as HTMLElement;
|
|
37
|
+
if (!arrow) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const [showLeft, showRight] = showArrow;
|
|
41
|
+
arrow.children[0]?.classList.toggle('show', showLeft);
|
|
42
|
+
arrow.children[1]?.classList.toggle('show', showRight);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @author sonion
|
|
47
|
+
* @description 每一次切换是的dom操作(更新主轮播图位置、指示器显示、翻页按钮)
|
|
48
|
+
* @param {HTMLElement} el - 元素
|
|
49
|
+
* @param {number} index - 操作索引
|
|
50
|
+
* @param {Array} showArrow - 左右箭头显示状态
|
|
51
|
+
* @returns {void}
|
|
52
|
+
*/
|
|
53
|
+
const domOperation = (
|
|
54
|
+
el: HTMLElement,
|
|
55
|
+
index: number,
|
|
56
|
+
showArrow: [boolean, boolean]
|
|
57
|
+
) => {
|
|
58
|
+
/** 轮播主要index */
|
|
59
|
+
el.style.setProperty('--index', `${index}`);
|
|
60
|
+
|
|
61
|
+
const wrapper = el.parentElement;
|
|
62
|
+
if (!wrapper) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
/** 设置指示器 */
|
|
66
|
+
const indicator = wrapper.parentElement?.nextElementSibling as HTMLElement;
|
|
67
|
+
if (indicator) {
|
|
68
|
+
indicator.querySelector('.active')?.classList.remove('active');
|
|
69
|
+
indicator.children[
|
|
70
|
+
index >= indicator.children.length ? 0 : index
|
|
71
|
+
]?.classList.add('active');
|
|
72
|
+
}
|
|
73
|
+
/** 设置箭头 */
|
|
74
|
+
arrowDomOperation(wrapper, showArrow);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/* 箭头类型 */
|
|
78
|
+
export type ShowArrowType = 'always' | 'auto' | 'hover' | 'none';
|
|
79
|
+
|
|
80
|
+
type ShowArrowHookParams = {
|
|
81
|
+
/** 箭头类型 */
|
|
82
|
+
showArrow: ShowArrowType;
|
|
83
|
+
/** 轮播图数量 */
|
|
84
|
+
length: number;
|
|
85
|
+
/** 轮播图宽度 */
|
|
86
|
+
cardWidth?: number;
|
|
87
|
+
/** 轮播图容器 */
|
|
88
|
+
wrapperRef?: HTMLElement;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* @author sonion
|
|
92
|
+
* @description 计算是否显示左右箭头
|
|
93
|
+
* @param {ShowArrowHookParams} params - hook参数
|
|
94
|
+
* @param {ShowArrowType} params.showArrow - 箭头类型
|
|
95
|
+
* @param {number} params.length - 轮播图数量
|
|
96
|
+
* @param {number} params.cardWidth - 轮播图宽度
|
|
97
|
+
* @param {HTMLElement} params.wrapperRef - 轮播图容器
|
|
98
|
+
* @returns {(index: number)=>[boolean, boolean]} - 计算是否显示左右箭头的函数
|
|
99
|
+
*/
|
|
100
|
+
export const useShowArrow = ({
|
|
101
|
+
showArrow,
|
|
102
|
+
length,
|
|
103
|
+
cardWidth,
|
|
104
|
+
wrapperRef,
|
|
105
|
+
}: ShowArrowHookParams) => {
|
|
106
|
+
const displayQuantity = useMemo(() => {
|
|
107
|
+
const wrapperWidth =
|
|
108
|
+
(wrapperRef?.parentElement?.parentElement?.clientWidth || cardWidth) ?? 0;
|
|
109
|
+
if (!cardWidth) {
|
|
110
|
+
return 1;
|
|
111
|
+
}
|
|
112
|
+
return Math.floor(wrapperWidth / cardWidth); // 可完整显示的数量
|
|
113
|
+
}, [wrapperRef, cardWidth]);
|
|
114
|
+
|
|
115
|
+
return useCallback<(index: number) => [boolean, boolean]>(
|
|
116
|
+
(index: number) => {
|
|
117
|
+
if (showArrow !== 'auto') {
|
|
118
|
+
return [true, true];
|
|
119
|
+
}
|
|
120
|
+
let isRightShowArrow: boolean;
|
|
121
|
+
if (length > displayQuantity) {
|
|
122
|
+
const lastDisplayedIndex = index + displayQuantity - 1;
|
|
123
|
+
isRightShowArrow = lastDisplayedIndex < length - 1;
|
|
124
|
+
} else {
|
|
125
|
+
isRightShowArrow = false;
|
|
126
|
+
}
|
|
127
|
+
return [index > 0, isRightShowArrow];
|
|
128
|
+
},
|
|
129
|
+
[displayQuantity, length, showArrow]
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export type NonLoopEndFuncType = (
|
|
134
|
+
/** 切换方向 */
|
|
135
|
+
direction: 'prev' | 'next',
|
|
136
|
+
/** 当前索引 */
|
|
137
|
+
current: number,
|
|
138
|
+
/** 轮播图数量 */
|
|
139
|
+
total: number,
|
|
140
|
+
/** 非循环轮播结束时的偏移量 */
|
|
141
|
+
offset: number
|
|
142
|
+
) => void;
|
|
143
|
+
type PlayControlHookParams = {
|
|
144
|
+
/** 轮播图数量 */
|
|
145
|
+
length: number;
|
|
146
|
+
/** 是否无缝轮播 */
|
|
147
|
+
loop: boolean;
|
|
148
|
+
/** 箭头类型 */
|
|
149
|
+
showArrow: ShowArrowType;
|
|
150
|
+
/** 轮播图宽度 */
|
|
151
|
+
cardWidth?: number;
|
|
152
|
+
/** 非循环轮播结束时的回调 */
|
|
153
|
+
onNonLoopEnd?: NonLoopEndFuncType;
|
|
154
|
+
/** 非循环轮播结束时的偏移量 */
|
|
155
|
+
offsetOnEnd?: number;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @author sonion
|
|
160
|
+
* @description 播放轮播图控制
|
|
161
|
+
* @param {PlayControlHookParams} params - hook参数
|
|
162
|
+
* @param {number} params.length - 轮播图数量
|
|
163
|
+
* @param {boolean} params.loop - 是否无缝轮播
|
|
164
|
+
* @param {ShowArrowType} params.showArrow - 箭头类型
|
|
165
|
+
* @param {number} params.cardWidth - 轮播图宽度
|
|
166
|
+
* @param {NonLoopEndFuncType} params.onNonLoopEnd - 非循环轮播结束回调
|
|
167
|
+
* @param {number} params.offsetOnEnd - 非循环轮播结束时的偏移量
|
|
168
|
+
*/
|
|
169
|
+
export const usePlayControl = ({
|
|
170
|
+
length,
|
|
171
|
+
loop,
|
|
172
|
+
showArrow,
|
|
173
|
+
cardWidth,
|
|
174
|
+
onNonLoopEnd,
|
|
175
|
+
offsetOnEnd = 0,
|
|
176
|
+
}: PlayControlHookParams) => {
|
|
177
|
+
const FIRST = 0;
|
|
178
|
+
const LAST = length;
|
|
179
|
+
const indexRef = useRef(FIRST); // 第一张
|
|
180
|
+
const [ref, setRef, isReadyRef] = useCreateSafeRef();
|
|
181
|
+
|
|
182
|
+
// 非循环轮播时,偏移量参数规范化
|
|
183
|
+
offsetOnEnd = ~~offsetOnEnd; // 向下取整数
|
|
184
|
+
// 如偏移量为负数(轮播数再往后,就没了)或大于等于轮播图数量,都相当于不设置偏移
|
|
185
|
+
offsetOnEnd = offsetOnEnd < 0 || offsetOnEnd >= length ? 0 : offsetOnEnd;
|
|
186
|
+
|
|
187
|
+
// 生成计算是否显示左右箭头的函数
|
|
188
|
+
const getShowArrow = useShowArrow({
|
|
189
|
+
showArrow,
|
|
190
|
+
length,
|
|
191
|
+
cardWidth,
|
|
192
|
+
wrapperRef: ref,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// length 变化箭头类型为自动的可能需要重新设置,否则隐藏了的不会更新
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
const wrapper = ref?.parentElement;
|
|
198
|
+
if (showArrow !== 'auto' || !wrapper) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const arrowStatus = getShowArrow(indexRef.current);
|
|
202
|
+
arrowDomOperation(wrapper, arrowStatus);
|
|
203
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
204
|
+
}, [length]);
|
|
205
|
+
|
|
206
|
+
const stepChange = useCallback(
|
|
207
|
+
(direction?: 'prev' | 'next') => {
|
|
208
|
+
if (!ref) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (loop) {
|
|
212
|
+
if (indexRef.current >= LAST && direction !== 'prev') {
|
|
213
|
+
indexRef.current = 0;
|
|
214
|
+
noAnimationSwitch(ref, indexRef.current);
|
|
215
|
+
} else if (indexRef.current <= FIRST && direction === 'prev') {
|
|
216
|
+
indexRef.current = LAST;
|
|
217
|
+
noAnimationSwitch(ref, indexRef.current);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
direction === 'prev' ? indexRef.current-- : indexRef.current++;
|
|
221
|
+
const arrowStatus = getShowArrow(indexRef.current);
|
|
222
|
+
domOperation(ref, indexRef.current, arrowStatus);
|
|
223
|
+
// 不循环轮播 结束事件
|
|
224
|
+
if (!loop && onNonLoopEnd) {
|
|
225
|
+
indexRef.current === length - offsetOnEnd - 1 &&
|
|
226
|
+
onNonLoopEnd(
|
|
227
|
+
direction ?? 'next',
|
|
228
|
+
indexRef.current,
|
|
229
|
+
length,
|
|
230
|
+
offsetOnEnd
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
[length, loop, ref, getShowArrow, offsetOnEnd, onNonLoopEnd, LAST]
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const jumpChange = useCallback(
|
|
238
|
+
(index: number) => {
|
|
239
|
+
const originalIndex = indexRef.current; // 记录更改前索引
|
|
240
|
+
indexRef.current = index < 0 ? 0 : index > length ? length : index;
|
|
241
|
+
const arrowStatus = getShowArrow(indexRef.current);
|
|
242
|
+
ref && domOperation(ref, indexRef.current, arrowStatus);
|
|
243
|
+
if (!loop && onNonLoopEnd) {
|
|
244
|
+
const direction = originalIndex < indexRef.current ? 'next' : 'prev';
|
|
245
|
+
const _triggerIndex = length - offsetOnEnd - 1; // 触发非循环结束需要的索引
|
|
246
|
+
// 当前索引大于等于触发索引,那原索引小于触发索引,才是首次满足,才该触发
|
|
247
|
+
// 当前索引小于等于触发索引,那原索引大于触发索引,才是首次满足,才该触发
|
|
248
|
+
((indexRef.current >= _triggerIndex && originalIndex < _triggerIndex) ||
|
|
249
|
+
(indexRef.current <= _triggerIndex &&
|
|
250
|
+
originalIndex > _triggerIndex)) &&
|
|
251
|
+
onNonLoopEnd(direction, indexRef.current, length, offsetOnEnd);
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
[length, loop, ref, getShowArrow, offsetOnEnd, onNonLoopEnd]
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const getCurrentIndex = useCallback(() => indexRef.current, []);
|
|
258
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
259
|
+
useEffect(() => jumpChange(indexRef.current), [ref]); // 初始化时设置--index
|
|
260
|
+
return [setRef, stepChange, jumpChange, getCurrentIndex, isReadyRef] as const;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* @author sonion
|
|
265
|
+
* @description 创建自增id函数
|
|
266
|
+
* @returns {()=>string} - 自增id函数
|
|
267
|
+
*/
|
|
268
|
+
const createAutoIncrementId = () => {
|
|
269
|
+
const fix = Math.random().toString(36).slice(2);
|
|
270
|
+
let id = 0;
|
|
271
|
+
return () => `${fix}-${++id}`;
|
|
272
|
+
};
|
|
273
|
+
/** 自增id函数 */
|
|
274
|
+
const autoIncrementId = createAutoIncrementId();
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* 子元素重渲染的依赖的类型
|
|
278
|
+
* length 依赖 children的数量
|
|
279
|
+
* child 依赖 children本身的变化
|
|
280
|
+
*/
|
|
281
|
+
export type RerenderType = 'length' | 'child';
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* @author sonion
|
|
285
|
+
* @description 创建无缝轮播结构
|
|
286
|
+
* @param {ReactElement} children - 轮播列表
|
|
287
|
+
* @param {boolean} loop - 是否无缝轮播
|
|
288
|
+
* @param {RerenderType} rerender - 子元素重渲染的依赖类型
|
|
289
|
+
* @returns {ReactElement[]} - 无缝轮播结构
|
|
290
|
+
*/
|
|
291
|
+
export const useLoopChildren = (
|
|
292
|
+
children: ReactElement[],
|
|
293
|
+
loop: boolean,
|
|
294
|
+
rerender: RerenderType
|
|
295
|
+
) =>
|
|
296
|
+
// 依赖 children?.length 或 children 的问题可忽略
|
|
297
|
+
|
|
298
|
+
useMemo(() => {
|
|
299
|
+
if (!Array.isArray(children) || !children.length) {
|
|
300
|
+
throw new Error('Carousel children must be an array');
|
|
301
|
+
}
|
|
302
|
+
if (loop) {
|
|
303
|
+
const first = children[0];
|
|
304
|
+
const clonedChildren = [...children]; // 加到后面,指示器和轮播项目好对应
|
|
305
|
+
// const last = children.at(-1);
|
|
306
|
+
// clonedChildren.unshift(cloneElement(last!, { key: autoIncrementId() }));
|
|
307
|
+
clonedChildren.push(cloneElement(first, { key: autoIncrementId() }));
|
|
308
|
+
return clonedChildren;
|
|
309
|
+
}
|
|
310
|
+
return children;
|
|
311
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
312
|
+
}, [rerender === 'child' ? children : children?.length, loop]);
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
.carousel {
|
|
2
|
+
position: relative;
|
|
3
|
+
overflow: hidden;
|
|
4
|
+
width: 100%; // 没制定宽度,默认100%
|
|
5
|
+
height: 100%;
|
|
6
|
+
|
|
7
|
+
&[style*='--wrapperWidth'] {
|
|
8
|
+
// 指定宽度用变量
|
|
9
|
+
width: calc(var(--wrapperWidth) * 1px);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
&[style*='--wrapperHeight'] {
|
|
13
|
+
// 指定了高度
|
|
14
|
+
height: calc(var(--wrapperHeight) * 1px);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
&:not([style*='--cardHeight']) {
|
|
18
|
+
// 没有指定高度,默认100%
|
|
19
|
+
.wrapper {
|
|
20
|
+
.inner {
|
|
21
|
+
height: 100%;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
&[style*='--cardWidth'] {
|
|
27
|
+
// 指定了单张宽度
|
|
28
|
+
.wrapper {
|
|
29
|
+
width: calc(var(--cardWidth) * 1px); // 和卡片宽度一致,作为逻辑宽度
|
|
30
|
+
.inner {
|
|
31
|
+
transform: translateX(
|
|
32
|
+
calc(var(--index) * (var(--cardWidth) + var(--gapSize, 0)) * -1px)
|
|
33
|
+
);
|
|
34
|
+
& > * {
|
|
35
|
+
flex: 0 0 calc(var(--cardWidth) * 1px);
|
|
36
|
+
height: 100%;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
&:not([style*='--cardWidth']) {
|
|
43
|
+
// 没有指定单张宽度
|
|
44
|
+
.wrapper {
|
|
45
|
+
width: 100%; // 没制定宽度,默认100%
|
|
46
|
+
.inner {
|
|
47
|
+
transform: translateX(
|
|
48
|
+
calc((-100% + (var(--gapSize, 0) * -1px)) * var(--index))
|
|
49
|
+
);
|
|
50
|
+
& > * {
|
|
51
|
+
flex: 0 0 100%;
|
|
52
|
+
height: 100%;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
&:hover > .arrow:global(.hover) {
|
|
59
|
+
// hover时显示箭头
|
|
60
|
+
display: block;
|
|
61
|
+
}
|
|
62
|
+
& > .arrow {
|
|
63
|
+
// 切换箭头
|
|
64
|
+
&:global(.hover) {
|
|
65
|
+
// 有hover类时不显示
|
|
66
|
+
display: none;
|
|
67
|
+
}
|
|
68
|
+
&:global(.none) {
|
|
69
|
+
display: none;
|
|
70
|
+
}
|
|
71
|
+
&:global(.always),
|
|
72
|
+
&:global(.auto) {
|
|
73
|
+
display: block;
|
|
74
|
+
}
|
|
75
|
+
.left,
|
|
76
|
+
.right {
|
|
77
|
+
display: none;
|
|
78
|
+
position: absolute;
|
|
79
|
+
top: 50%;
|
|
80
|
+
transform: translateY(-50%);
|
|
81
|
+
z-index: 9;
|
|
82
|
+
width: 20px;
|
|
83
|
+
height: 20px;
|
|
84
|
+
padding: 14px;
|
|
85
|
+
border: 1px solid rgb(221, 226, 233);
|
|
86
|
+
border-radius: 50%;
|
|
87
|
+
background: rgb(255, 255, 255)
|
|
88
|
+
url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMSAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgaWQ9ImRvd24iPgo8cGF0aCBpZD0iVW5pb24iIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNi43OTE4MiA1Ljc2ODY1QzYuNDAyNzMgNS4zNzk1NiA2LjQwMjczIDQuNzQ4NzEgNi43OTE4MiA0LjM1OTYyTDcuNDk2MzQgMy42NTUxQzcuODg1NDMgMy4yNjYwMSA4LjUxNjI4IDMuMjY2MDEgOC45MDUzNyAzLjY1NTFMMTQuNTQxNSA5LjI5MTI0QzE0LjczNzEgOS40ODY4MyAxNC44MzQ0IDkuNzQzNSAxNC44MzMzIDkuOTk5ODVDMTQuODM0NCAxMC4yNTYyIDE0LjczNzEgMTAuNTEyOSAxNC41NDE1IDEwLjcwODVMOC45MDUzNyAxNi4zNDQ2QzguNTE2MjggMTYuNzMzNyA3Ljg4NTQzIDE2LjczMzcgNy40OTYzNCAxNi4zNDQ2TDYuNzkxODIgMTUuNjQwMUM2LjQwMjczIDE1LjI1MSA2LjQwMjczIDE0LjYyMDEgNi43OTE4MiAxNC4yMzExTDExLjAyMyA5Ljk5OTg1TDYuNzkxODIgNS43Njg2NVoiIGZpbGw9IiMwMDY2RkMiLz4KPC9nPgo8L3N2Zz4K')
|
|
89
|
+
no-repeat center / contain;
|
|
90
|
+
background-origin: content-box;
|
|
91
|
+
box-shadow:
|
|
92
|
+
0px 15px 35px -2px rgba(0, 0, 0, 0.05),
|
|
93
|
+
0px 5px 15px 0px rgba(0, 0, 0, 0.05);
|
|
94
|
+
cursor: pointer;
|
|
95
|
+
}
|
|
96
|
+
// 通过dom操作,所以用global
|
|
97
|
+
& > :global(.show) {
|
|
98
|
+
display: block;
|
|
99
|
+
}
|
|
100
|
+
.left {
|
|
101
|
+
left: 12px;
|
|
102
|
+
transform: translateY(-50%) rotate(180deg);
|
|
103
|
+
}
|
|
104
|
+
.right {
|
|
105
|
+
right: 12px;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
.wrapper {
|
|
109
|
+
// 轮播图容器
|
|
110
|
+
height: 100%;
|
|
111
|
+
.inner {
|
|
112
|
+
// 轮播图内容列表
|
|
113
|
+
display: flex;
|
|
114
|
+
gap: calc(var(--gapSize) * 1px);
|
|
115
|
+
height: calc(var(--cardHeight) * 1px);
|
|
116
|
+
transition: transform 0.3s;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.indicator {
|
|
122
|
+
// 指示器
|
|
123
|
+
position: absolute;
|
|
124
|
+
bottom: -17px;
|
|
125
|
+
left: 50%;
|
|
126
|
+
z-index: 9;
|
|
127
|
+
transform: translateX(-50%);
|
|
128
|
+
display: flex;
|
|
129
|
+
gap: 8px;
|
|
130
|
+
border-radius: 8px;
|
|
131
|
+
&:global(.none) {
|
|
132
|
+
// 指示器为none
|
|
133
|
+
display: none;
|
|
134
|
+
}
|
|
135
|
+
&:global(.line) {
|
|
136
|
+
// 指示器为line
|
|
137
|
+
& > i {
|
|
138
|
+
width: 40px;
|
|
139
|
+
height: 5px;
|
|
140
|
+
border-radius: 8px;
|
|
141
|
+
background-color: var(--Gray-3, rgb(229, 232, 239));
|
|
142
|
+
cursor: pointer;
|
|
143
|
+
// 通过dom操作,所以用global
|
|
144
|
+
&:global(.active) {
|
|
145
|
+
background: var(--byte-plus-blue-brand-primary-60, rgb(0, 102, 252));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
&:global(.dot) {
|
|
150
|
+
// 指示器为dot
|
|
151
|
+
& > i {
|
|
152
|
+
width: 6px;
|
|
153
|
+
height: 6px;
|
|
154
|
+
border-radius: 50%;
|
|
155
|
+
background-color: var(--Gray-3, rgb(229, 232, 239));
|
|
156
|
+
cursor: pointer;
|
|
157
|
+
// 通过dom操作,所以用global
|
|
158
|
+
&:global(.active) {
|
|
159
|
+
background: var(--byte-plus-blue-brand-primary-60, rgb(0, 102, 252));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|