@rxdrag/website-lib-core 0.0.7 → 0.0.8
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/index.ts +1 -1
- package/package.json +10 -6
- package/src/component-logic/gsap.d.ts +4 -0
- package/src/component-logic/index.ts +8 -0
- package/src/component-logic/link-client.ts +33 -0
- package/src/component-logic/link.ts +50 -0
- package/src/component-logic/modal.ts +36 -0
- package/src/component-logic/motion.ts +272 -0
- package/src/component-logic/number.ts +45 -0
- package/src/component-logic/popover.ts +51 -0
- package/src/component-logic/tabs.ts +10 -0
- package/src/controller/AnimateController.ts +138 -0
- package/src/controller/AosController.ts +240 -0
- package/src/controller/FlipController.ts +339 -0
- package/src/controller/ModalController.ts +127 -0
- package/src/controller/NumberController.ts +161 -0
- package/src/controller/PageLoader.ts +163 -0
- package/src/controller/PopoverController.ts +116 -0
- package/src/controller/TabsController.ts +271 -0
- package/src/controller/applyAnimation.ts +86 -0
- package/src/controller/applyInitialState.ts +79 -0
- package/src/{scripts → controller}/consts.ts +0 -2
- package/src/controller/index.ts +9 -0
- package/src/controller/popup.ts +346 -0
- package/src/controller/utils.ts +48 -0
- package/src/entify/Entify.ts +354 -365
- package/src/entify/IEntify.ts +91 -0
- package/src/entify/index.ts +3 -2
- package/src/entify/lib/newQueryProductOptions.ts +2 -3
- package/src/entify/lib/newQueryProductsMediaOptions.ts +19 -18
- package/src/entify/lib/queryAllProducts.ts +11 -3
- package/src/entify/lib/queryFeaturedProducts.ts +3 -3
- package/src/entify/lib/queryLatestPosts.ts +2 -2
- package/src/entify/lib/queryOneTheme.ts +1 -1
- package/src/entify/lib/queryPostCategories.ts +3 -3
- package/src/entify/lib/queryPostSlugs.ts +2 -2
- package/src/entify/lib/queryPosts.ts +92 -92
- package/src/entify/lib/queryProductCategories.ts +3 -3
- package/src/entify/lib/queryProducts.ts +69 -69
- package/src/entify/lib/queryUserPosts.ts +2 -2
- package/src/entify/lib/searchProducts.ts +2 -2
- package/src/index.ts +3 -1
- package/src/lib/formatDate.ts +15 -0
- package/src/lib/index.ts +3 -0
- package/src/lib/pagination.ts +114 -0
- package/src/lib/utils.ts +119 -0
- package/src/motion/consts.ts +428 -598
- package/src/motion/convertToGsapVars.ts +102 -0
- package/src/motion/index.ts +5 -1
- package/src/motion/normalizeAnimation.ts +28 -0
- package/src/motion/normalizeAosAnimation.ts +22 -0
- package/src/motion/normalizePopupAnimation.ts +24 -0
- package/src/motion/types.ts +133 -46
- package/src/react/components/AttachmentIcon/index.tsx +53 -0
- package/src/react/components/ContactForm/index.tsx +341 -0
- package/src/react/components/Icon/index.tsx +10 -0
- package/src/react/components/Medias/index.tsx +347 -347
- package/src/react/components/ProductCard/ProductCta/index.tsx +7 -5
- package/src/react/components/RichTextOutline/index.tsx +76 -76
- package/src/react/components/Scroller.tsx +5 -1
- package/src/react/components/SearchInput.tsx +36 -34
- package/src/react/components/ToTop.tsx +63 -28
- package/src/react/components/index.ts +3 -1
- package/src/react/hooks/useScroll.ts +16 -10
- package/src/react/components/EnquiryForm/index.tsx +0 -334
- package/src/scripts/actions.ts +0 -304
- package/src/scripts/events.ts +0 -33
- package/src/scripts/index.ts +0 -3
- /package/src/react/components/{EnquiryForm → ContactForm}/Input.tsx +0 -0
- /package/src/react/components/{EnquiryForm → ContactForm}/Submit.tsx +0 -0
- /package/src/react/components/{EnquiryForm → ContactForm}/Textarea.tsx +0 -0
|
@@ -1,334 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
2
|
-
import { Input } from "./Input";
|
|
3
|
-
import { Submit } from "./Submit";
|
|
4
|
-
import { Textarea } from "./Textarea";
|
|
5
|
-
import { onPopupClose, onPopupOpen } from "../../../scripts";
|
|
6
|
-
import { DATA_POPUP_CTA } from "../../../scripts/consts";
|
|
7
|
-
import clsx from "clsx";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* 简单的加密函数,用于生成防机器人的加密字段
|
|
11
|
-
* @param value 需要加密的值
|
|
12
|
-
* @returns 加密后的字符串
|
|
13
|
-
*/
|
|
14
|
-
const encrypt = (value: string, formSalt: string): string => {
|
|
15
|
-
// 获取当前时间戳,精确到分钟级别(防止频繁变化)
|
|
16
|
-
const timestamp = Math.floor(Date.now() / (60 * 1000));
|
|
17
|
-
|
|
18
|
-
// 将时间戳与盐值和输入值组合
|
|
19
|
-
const dataToEncrypt = `${formSalt}:${timestamp}:${value}`;
|
|
20
|
-
|
|
21
|
-
// 使用简单的哈希算法
|
|
22
|
-
let hash = 0;
|
|
23
|
-
for (let i = 0; i < dataToEncrypt.length; i++) {
|
|
24
|
-
const char = dataToEncrypt.charCodeAt(i);
|
|
25
|
-
hash = (hash << 5) - hash + char;
|
|
26
|
-
hash = hash & hash; // 转换为32位整数
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// 转换为16进制字符串并添加时间戳信息
|
|
30
|
-
return `${hash.toString(16)}_${timestamp}`;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* 验证加密字段是否有效
|
|
35
|
-
* @param encryptedValue 加密后的字符串
|
|
36
|
-
* @param originalValue 原始值(如蜜罐字段的值)
|
|
37
|
-
* @param maxAgeMinutes 最大有效时间(分钟)
|
|
38
|
-
* @returns 是否有效
|
|
39
|
-
*/
|
|
40
|
-
export const verifyEncryption = (
|
|
41
|
-
formSalt: string,
|
|
42
|
-
encryptedValue: string = "",
|
|
43
|
-
originalValue: string = "",
|
|
44
|
-
maxAgeMinutes: number = 30
|
|
45
|
-
): boolean => {
|
|
46
|
-
// 解析加密值
|
|
47
|
-
const parts = encryptedValue.split("_");
|
|
48
|
-
if (parts.length !== 2) {
|
|
49
|
-
return false; // 格式不正确
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const [hashString, timestampString] = parts;
|
|
53
|
-
const timestamp = parseInt(timestampString, 10);
|
|
54
|
-
|
|
55
|
-
// 验证时间戳是否在有效期内
|
|
56
|
-
const currentTimestamp = Math.floor(Date.now() / (60 * 1000));
|
|
57
|
-
if (isNaN(timestamp) || currentTimestamp - timestamp > maxAgeMinutes) {
|
|
58
|
-
return false; // 时间戳无效或已过期
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// 重新计算哈希值
|
|
62
|
-
const dataToEncrypt = `${formSalt}:${timestamp}:${originalValue}`;
|
|
63
|
-
let hash = 0;
|
|
64
|
-
for (let i = 0; i < dataToEncrypt.length; i++) {
|
|
65
|
-
const char = dataToEncrypt.charCodeAt(i);
|
|
66
|
-
hash = (hash << 5) - hash + char;
|
|
67
|
-
hash = hash & hash;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// 比较哈希值
|
|
71
|
-
return hashString === hash.toString(16);
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
export interface QuoteRequest {
|
|
75
|
-
name?: string;
|
|
76
|
-
email?: string;
|
|
77
|
-
company?: string;
|
|
78
|
-
message?: string;
|
|
79
|
-
fromCta?: string;
|
|
80
|
-
// 蜜罐字段,用于检测机器人
|
|
81
|
-
phone: string;
|
|
82
|
-
// 一个加密字段,用于防机器人,点击时附加一个加密的字符串
|
|
83
|
-
encryptedField?: string;
|
|
84
|
-
}
|
|
85
|
-
interface FormErrors {
|
|
86
|
-
name?: string;
|
|
87
|
-
email?: string;
|
|
88
|
-
message?: string;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function EnquiryForm(props: {
|
|
92
|
-
submitAlign?: "left" | "center" | "right";
|
|
93
|
-
actionUrl?: string;
|
|
94
|
-
formSalt: string;
|
|
95
|
-
}) {
|
|
96
|
-
const {
|
|
97
|
-
submitAlign = "right",
|
|
98
|
-
actionUrl = "/api/ask-for-quote",
|
|
99
|
-
formSalt,
|
|
100
|
-
} = props;
|
|
101
|
-
const [formData, setFormData] = useState<QuoteRequest>({
|
|
102
|
-
name: "",
|
|
103
|
-
email: "",
|
|
104
|
-
company: "",
|
|
105
|
-
message: "",
|
|
106
|
-
fromCta: "",
|
|
107
|
-
phone: "", // 初始化蜜罐字段
|
|
108
|
-
});
|
|
109
|
-
// 错误状态
|
|
110
|
-
const [errors, setErrors] = useState<FormErrors>({});
|
|
111
|
-
const [submitting, setSubmitting] = useState(false);
|
|
112
|
-
const [submitStatus, setSubmitStatus] = useState<{
|
|
113
|
-
success?: boolean;
|
|
114
|
-
message?: string;
|
|
115
|
-
}>({});
|
|
116
|
-
//最近被点击过的cta
|
|
117
|
-
|
|
118
|
-
useEffect(() => {
|
|
119
|
-
const unsub = onPopupOpen((event) => {
|
|
120
|
-
setFormData((prev) => ({
|
|
121
|
-
...prev,
|
|
122
|
-
fromCta: event.target?.getAttribute(DATA_POPUP_CTA) || undefined,
|
|
123
|
-
}));
|
|
124
|
-
});
|
|
125
|
-
const unsub2 = onPopupClose(() => {
|
|
126
|
-
setFormData((prev) => ({
|
|
127
|
-
...prev,
|
|
128
|
-
fromCta: "",
|
|
129
|
-
}));
|
|
130
|
-
});
|
|
131
|
-
return () => {
|
|
132
|
-
unsub();
|
|
133
|
-
unsub2();
|
|
134
|
-
};
|
|
135
|
-
}, []);
|
|
136
|
-
|
|
137
|
-
// 处理输入变化
|
|
138
|
-
const handleChange = (
|
|
139
|
-
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
|
140
|
-
) => {
|
|
141
|
-
const { name, value } = e.target;
|
|
142
|
-
setFormData((prev) => ({
|
|
143
|
-
...prev,
|
|
144
|
-
[name]: value,
|
|
145
|
-
}));
|
|
146
|
-
|
|
147
|
-
setSubmitStatus({}); // 重置提交状态
|
|
148
|
-
|
|
149
|
-
// 清除对应字段的错误
|
|
150
|
-
if (errors[name as keyof FormErrors]) {
|
|
151
|
-
setErrors((prev) => ({
|
|
152
|
-
...prev,
|
|
153
|
-
[name]: undefined,
|
|
154
|
-
}));
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
// 验证表单
|
|
158
|
-
const validateForm = (): boolean => {
|
|
159
|
-
const newErrors: FormErrors = {};
|
|
160
|
-
|
|
161
|
-
// if (!formData.name?.trim()) {
|
|
162
|
-
// newErrors.name = "Please enter your name";
|
|
163
|
-
// }
|
|
164
|
-
|
|
165
|
-
if (!formData.email?.trim()) {
|
|
166
|
-
newErrors.email = "Please enter your email";
|
|
167
|
-
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
|
168
|
-
newErrors.email = "Please enter a valid email address";
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (!formData.message?.trim()) {
|
|
172
|
-
newErrors.message = "Please enter your message";
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// 检查蜜罐字段 - 如果填写了则表示是机器人
|
|
176
|
-
if (formData.phone) {
|
|
177
|
-
// 悄悄失败,不显示错误信息
|
|
178
|
-
console.log("Honeypot triggered - likely spam submission");
|
|
179
|
-
return false;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
setErrors(newErrors);
|
|
183
|
-
return Object.keys(newErrors).length === 0;
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
const handleClick = async (event: React.MouseEvent) => {
|
|
187
|
-
event.preventDefault(); // 阻止表单默认提交行为
|
|
188
|
-
|
|
189
|
-
// 验证表单
|
|
190
|
-
if (!validateForm()) {
|
|
191
|
-
return; // 如果验证失败,不继续提交
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
try {
|
|
195
|
-
setSubmitting(true);
|
|
196
|
-
setSubmitStatus({}); // 重置提交状态
|
|
197
|
-
|
|
198
|
-
const response = await fetch(actionUrl, {
|
|
199
|
-
method: "POST",
|
|
200
|
-
body: JSON.stringify({
|
|
201
|
-
...formData,
|
|
202
|
-
encryptedField: encrypt(formData.phone, formSalt),
|
|
203
|
-
}),
|
|
204
|
-
headers: {
|
|
205
|
-
"X-Request-URL": window.location.href,
|
|
206
|
-
"Content-Type": "application/json",
|
|
207
|
-
},
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
if (!response.ok) {
|
|
211
|
-
throw new Error(`Server responded with status: ${response.status}`);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const result = await response.json();
|
|
215
|
-
|
|
216
|
-
setSubmitStatus({
|
|
217
|
-
success: result.success,
|
|
218
|
-
message: result.message,
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
if (result.success) {
|
|
222
|
-
// 重置表单
|
|
223
|
-
setFormData({
|
|
224
|
-
name: "",
|
|
225
|
-
email: "",
|
|
226
|
-
company: "",
|
|
227
|
-
message: "",
|
|
228
|
-
fromCta: "产品页面",
|
|
229
|
-
phone: "",
|
|
230
|
-
});
|
|
231
|
-
window.location.href = "/thanks";
|
|
232
|
-
}
|
|
233
|
-
} catch (error) {
|
|
234
|
-
// 如果出现错误,打印错误信息
|
|
235
|
-
console.error("Form submission error:", error);
|
|
236
|
-
setSubmitStatus({
|
|
237
|
-
success: false,
|
|
238
|
-
message:
|
|
239
|
-
error instanceof Error
|
|
240
|
-
? `Error: ${error.message}`
|
|
241
|
-
: "Failed to submit the form. Please try again later.",
|
|
242
|
-
});
|
|
243
|
-
} finally {
|
|
244
|
-
setSubmitting(false);
|
|
245
|
-
}
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
return (
|
|
249
|
-
<div className="py-4 grid max-w-2xl grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-2">
|
|
250
|
-
<Input
|
|
251
|
-
className="sm:col-span-1"
|
|
252
|
-
inputClassName="mt-2 block w-full rounded-md outline-none border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6"
|
|
253
|
-
label="Your Email *"
|
|
254
|
-
labelClassName="block text-sm font-medium leading-6 text-gray-900"
|
|
255
|
-
name="email"
|
|
256
|
-
required={true}
|
|
257
|
-
autoFocus={true}
|
|
258
|
-
type="email"
|
|
259
|
-
value={formData.email}
|
|
260
|
-
onChange={handleChange}
|
|
261
|
-
error={errors.email}
|
|
262
|
-
/>
|
|
263
|
-
<Input
|
|
264
|
-
className="sm:col-span-1"
|
|
265
|
-
inputClassName="mt-2 block w-full rounded-md outline-none border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6"
|
|
266
|
-
label="Your Name"
|
|
267
|
-
labelClassName="block text-sm font-medium leading-6 text-gray-900"
|
|
268
|
-
name="name"
|
|
269
|
-
value={formData.name}
|
|
270
|
-
onChange={handleChange}
|
|
271
|
-
error={errors.name}
|
|
272
|
-
/>
|
|
273
|
-
<Input
|
|
274
|
-
className="col-span-full"
|
|
275
|
-
inputClassName="mt-2 block w-full rounded-md outline-none border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6"
|
|
276
|
-
label="Company"
|
|
277
|
-
labelClassName="block text-sm font-medium leading-6 text-gray-900"
|
|
278
|
-
name="company"
|
|
279
|
-
value={formData.company}
|
|
280
|
-
onChange={handleChange}
|
|
281
|
-
/>
|
|
282
|
-
<Textarea
|
|
283
|
-
className="col-span-full"
|
|
284
|
-
label="Message *"
|
|
285
|
-
labelClassName="block text-sm font-medium leading-6 text-gray-900"
|
|
286
|
-
name="message"
|
|
287
|
-
required={true}
|
|
288
|
-
textareaClassName="mt-2 block w-full rounded-md outline-none border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6"
|
|
289
|
-
value={formData.message}
|
|
290
|
-
onChange={handleChange}
|
|
291
|
-
error={errors.message}
|
|
292
|
-
/>
|
|
293
|
-
{/* 蜜罐字段 - 对用户隐藏但对机器人可见 */}
|
|
294
|
-
<div style={{ display: "none" }}>
|
|
295
|
-
<input
|
|
296
|
-
type="text"
|
|
297
|
-
name="phone"
|
|
298
|
-
value={formData.phone}
|
|
299
|
-
onChange={handleChange}
|
|
300
|
-
tabIndex={-1}
|
|
301
|
-
autoComplete="off"
|
|
302
|
-
/>
|
|
303
|
-
</div>
|
|
304
|
-
<div
|
|
305
|
-
className={clsx("col-span-full flex items-center gap-x-6 px-0 py-2", {
|
|
306
|
-
"justify-start": submitAlign === "left",
|
|
307
|
-
"justify-center": submitAlign === "center",
|
|
308
|
-
"justify-end": submitAlign === "right",
|
|
309
|
-
})}
|
|
310
|
-
>
|
|
311
|
-
{submitStatus.message && !submitStatus.success && (
|
|
312
|
-
<div
|
|
313
|
-
className={`text-sm ${
|
|
314
|
-
submitStatus.success ? "text-green-600" : "text-red-600"
|
|
315
|
-
}`}
|
|
316
|
-
>
|
|
317
|
-
{submitStatus.message}
|
|
318
|
-
</div>
|
|
319
|
-
)}
|
|
320
|
-
<Submit
|
|
321
|
-
className="flex gap-2 items-center relative shadow-sm btn btn-primary btn-lg nowrap"
|
|
322
|
-
title="Send Message"
|
|
323
|
-
spinner={
|
|
324
|
-
<div className="left-8 flex items-center justify-center">
|
|
325
|
-
<div className="animate-spin rounded-full h-5 w-5 border-t-2 border-b-2 border-white" />
|
|
326
|
-
</div>
|
|
327
|
-
}
|
|
328
|
-
submitting={submitting}
|
|
329
|
-
onClick={handleClick}
|
|
330
|
-
/>
|
|
331
|
-
</div>
|
|
332
|
-
</div>
|
|
333
|
-
);
|
|
334
|
-
}
|
package/src/scripts/actions.ts
DELETED
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DATA_POPUP,
|
|
3
|
-
DATA_POPUP_ROLE,
|
|
4
|
-
EVENT_CLOSE,
|
|
5
|
-
EVENT_OPEN,
|
|
6
|
-
EVENT_SELECT,
|
|
7
|
-
EVENT_UNSELECT,
|
|
8
|
-
PopupRole,
|
|
9
|
-
} from "./consts";
|
|
10
|
-
import { $emit, $on, PopupEvent, SelectionEvent } from "./events";
|
|
11
|
-
import { ANIMATIONS, DUAL_ANIMATIONS } from "../motion/consts";
|
|
12
|
-
import { DualAnimation, SimpleAnimation } from "../motion/types";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* 在每次页面切换后执行回调函数
|
|
16
|
-
* 注意:此函数注册的事件监听器不会自动解注册,将在每次页面切换时执行
|
|
17
|
-
*
|
|
18
|
-
* @param init - 每次页面切换时要执行的回调函数
|
|
19
|
-
* @returns 用于手动解注册事件的函数
|
|
20
|
-
*/
|
|
21
|
-
export function onEverySwap(init: () => void) {
|
|
22
|
-
document.addEventListener("astro:after-swap", init);
|
|
23
|
-
// 返回一个函数,用于手动解注册事件
|
|
24
|
-
return () => document.removeEventListener("astro:after-swap", init);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* 初始化动画,在 page-load 事件触发时执行
|
|
29
|
-
*
|
|
30
|
-
* @param init - 初始化动画的回调函数
|
|
31
|
-
*/
|
|
32
|
-
export function onPageLoaded(init: () => void) {
|
|
33
|
-
const doInit = () => {
|
|
34
|
-
init();
|
|
35
|
-
document.removeEventListener("astro:page-load", doInit);
|
|
36
|
-
};
|
|
37
|
-
document.addEventListener("astro:page-load", doInit);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* 用于在脚本中监听弹出层的打开/关闭事件,并处理相关动画效果
|
|
42
|
-
* 注意:此函数主要用于脚本中的事件处理,组件内部已有相应实现,无需使用
|
|
43
|
-
*
|
|
44
|
-
* @param targetKey - 要监听的弹出层的唯一标识
|
|
45
|
-
* @param callback - 弹出层打开时的回调函数,可返回关闭时的清理函数
|
|
46
|
-
* @returns 取消监听的函数
|
|
47
|
-
*
|
|
48
|
-
* @example
|
|
49
|
-
* ```typescript
|
|
50
|
-
* // 监听产品菜单的打开/关闭事件并添加旋转动画
|
|
51
|
-
* onOpen("products-popup", () => {
|
|
52
|
-
* // 打开时顺时针旋转
|
|
53
|
-
* animate("#products-indicator", {
|
|
54
|
-
* rotate: [0, 180],
|
|
55
|
-
* });
|
|
56
|
-
*
|
|
57
|
-
* // 返回关闭时的动画处理函数
|
|
58
|
-
* return () => {
|
|
59
|
-
* animate("#products-indicator", {
|
|
60
|
-
* rotate: [180, 0],
|
|
61
|
-
* });
|
|
62
|
-
* };
|
|
63
|
-
* });
|
|
64
|
-
* ```
|
|
65
|
-
*/
|
|
66
|
-
export function onOpen(
|
|
67
|
-
targetKey: string,
|
|
68
|
-
callback?: (evnet?: PopupEvent) => VoidFunction | void
|
|
69
|
-
) {
|
|
70
|
-
return onPopupOpen((event) => {
|
|
71
|
-
if (event.key === targetKey) {
|
|
72
|
-
return callback?.(event);
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* 用于在脚本中监听弹出层的打开/关闭事件,并处理相关动画效果
|
|
79
|
-
* 注意:此函数主要用于脚本中的事件处理,组件内部已有相应实现,无需使用
|
|
80
|
-
*
|
|
81
|
-
* @param callback - 弹出层打开时的回调函数,可返回关闭时的清理函数
|
|
82
|
-
* @returns 取消监听的函数
|
|
83
|
-
*
|
|
84
|
-
*/
|
|
85
|
-
export function onPopupOpen(
|
|
86
|
-
callback: (event: PopupEvent) => VoidFunction | void
|
|
87
|
-
) {
|
|
88
|
-
let unsubscribe: ((event: PopupEvent) => void) | void;
|
|
89
|
-
|
|
90
|
-
const handleOpen = (event: PopupEvent) => {
|
|
91
|
-
unsubscribe = callback?.(event);
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
const unsubscribeOpen = $on(EVENT_OPEN, handleOpen);
|
|
95
|
-
|
|
96
|
-
const handelClose = (event: PopupEvent) => {
|
|
97
|
-
unsubscribe?.(event);
|
|
98
|
-
};
|
|
99
|
-
const unsubscribeClose = $on(EVENT_CLOSE, handelClose);
|
|
100
|
-
|
|
101
|
-
return () => {
|
|
102
|
-
unsubscribeOpen();
|
|
103
|
-
unsubscribeClose();
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export function onPopupClose(
|
|
108
|
-
callback: (event: PopupEvent) => VoidFunction | void
|
|
109
|
-
) {
|
|
110
|
-
return $on<PopupEvent>(EVENT_CLOSE, callback);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function onSelected(
|
|
114
|
-
callback: (event: SelectionEvent) => VoidFunction | void
|
|
115
|
-
) {
|
|
116
|
-
return $on<SelectionEvent>(EVENT_SELECT, callback);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function onUnSelected(
|
|
120
|
-
callback: (event: SelectionEvent) => VoidFunction | void
|
|
121
|
-
) {
|
|
122
|
-
return $on<SelectionEvent>(EVENT_UNSELECT, callback);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// 使用WeakMap存储事件处理函数引用
|
|
126
|
-
const popoverHandlers = new WeakMap<
|
|
127
|
-
HTMLElement,
|
|
128
|
-
{
|
|
129
|
-
enter: (event: Event) => void;
|
|
130
|
-
leave: (event: Event) => void;
|
|
131
|
-
}
|
|
132
|
-
>();
|
|
133
|
-
|
|
134
|
-
// 使用WeakMap存储Modal触发器的事件处理函数引用
|
|
135
|
-
const modalHandlers = new WeakMap<HTMLElement, (event: Event) => void>();
|
|
136
|
-
|
|
137
|
-
// 使用WeakMap存储Modal关闭按钮的事件处理函数引用
|
|
138
|
-
const modalCloserHandlers = new WeakMap<HTMLElement, (event: Event) => void>();
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* 初始化Popover的鼠标交互事件
|
|
142
|
-
* @param popupKey - 弹出层的唯一标识
|
|
143
|
-
* @param element - 弹出层的 DOM 元素
|
|
144
|
-
*/
|
|
145
|
-
export function initPopover(popupKey: string, element: HTMLElement) {
|
|
146
|
-
// 先移除旧的事件监听器(如果存在)
|
|
147
|
-
const oldHandlers = popoverHandlers.get(element);
|
|
148
|
-
if (oldHandlers) {
|
|
149
|
-
element.removeEventListener("mouseenter", oldHandlers.enter);
|
|
150
|
-
element.removeEventListener("mouseleave", oldHandlers.leave);
|
|
151
|
-
element.removeEventListener("touchstart", oldHandlers.enter);
|
|
152
|
-
element.removeEventListener("touchend", oldHandlers.leave);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// 创建新的事件处理函数
|
|
156
|
-
const handleMouseEnter = (event: Event) => {
|
|
157
|
-
openPopup(popupKey, element);
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
const handleMouseLeave = (event: Event) => {
|
|
161
|
-
closePopup(popupKey);
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
// 保存新的事件处理函数引用
|
|
165
|
-
popoverHandlers.set(element, {
|
|
166
|
-
enter: handleMouseEnter,
|
|
167
|
-
leave: handleMouseLeave,
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
// 添加新的事件监听器
|
|
171
|
-
element.addEventListener("mouseenter", handleMouseEnter);
|
|
172
|
-
element.addEventListener("mouseleave", handleMouseLeave);
|
|
173
|
-
element.addEventListener("touchstart", handleMouseEnter);
|
|
174
|
-
element.addEventListener("touchend", handleMouseLeave);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export function getPopupContainer(popupKey: string) {
|
|
178
|
-
return document.querySelector(
|
|
179
|
-
`[${DATA_POPUP}="${popupKey}"][${DATA_POPUP_ROLE}="${PopupRole.ModalContainer}"], [${DATA_POPUP}="${popupKey}"][${DATA_POPUP_ROLE}="${PopupRole.PopoverContainer}"]`
|
|
180
|
-
) as HTMLElement | undefined;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export function initModal(popupKey: string, trigger: HTMLElement) {
|
|
184
|
-
// 先移除旧的事件监听器(如果存在)
|
|
185
|
-
const oldHandler = modalHandlers.get(trigger);
|
|
186
|
-
if (oldHandler) {
|
|
187
|
-
trigger.removeEventListener("click", oldHandler);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// 创建新的事件处理函数
|
|
191
|
-
const handleTriggerClick = (event: Event) => {
|
|
192
|
-
// 查找 Modal 容器
|
|
193
|
-
openPopup(popupKey, trigger);
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
// 保存新的事件处理函数引用
|
|
197
|
-
modalHandlers.set(trigger, handleTriggerClick);
|
|
198
|
-
|
|
199
|
-
// 添加新的事件监听器
|
|
200
|
-
trigger.addEventListener("click", handleTriggerClick);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* 初始化Modal关闭按钮
|
|
205
|
-
* @param popupKey - 弹出层的唯一标识
|
|
206
|
-
* @param closer - 关闭按钮元素
|
|
207
|
-
*/
|
|
208
|
-
export function initModalCloser(popupKey: string, closer: HTMLElement) {
|
|
209
|
-
// 先移除旧的事件监听器(如果存在)
|
|
210
|
-
const oldHandler = modalCloserHandlers.get(closer);
|
|
211
|
-
if (oldHandler) {
|
|
212
|
-
closer.removeEventListener("click", oldHandler);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// 创建新的事件处理函数
|
|
216
|
-
const handleCloserClick = (event: Event) => {
|
|
217
|
-
closePopup(popupKey);
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
// 保存新的事件处理函数引用
|
|
221
|
-
modalCloserHandlers.set(closer, handleCloserClick);
|
|
222
|
-
|
|
223
|
-
// 添加新的事件监听器
|
|
224
|
-
closer.addEventListener("click", handleCloserClick);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* 打开弹出层
|
|
229
|
-
* @param popupKey
|
|
230
|
-
* @param target
|
|
231
|
-
*/
|
|
232
|
-
export function openPopup(popupKey: string, target: HTMLElement) {
|
|
233
|
-
// 查找 Modal 容器
|
|
234
|
-
const modalContainer = getPopupContainer(popupKey);
|
|
235
|
-
if (modalContainer) {
|
|
236
|
-
modalContainer.classList.add("open");
|
|
237
|
-
$emit(EVENT_OPEN, { key: popupKey, target });
|
|
238
|
-
} else {
|
|
239
|
-
console.error("未找到弹出层", popupKey);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// 选择
|
|
244
|
-
export function select(key: string, target: HTMLElement, selection: string) {
|
|
245
|
-
$emit(EVENT_SELECT, { key, target, selection });
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// 取消选择
|
|
249
|
-
export function unselect(key: string, target: HTMLElement, selection: string) {
|
|
250
|
-
$emit(EVENT_UNSELECT, { key, target, selection });
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* 关闭弹出层
|
|
255
|
-
* @param popupKey - 弹出层的唯一标识
|
|
256
|
-
* @param target - 可选的目标元素,如果提供则使用该元素,否则查找容器
|
|
257
|
-
*/
|
|
258
|
-
export function closePopup(popupKey: string, target?: HTMLElement) {
|
|
259
|
-
const container = target || getPopupContainer(popupKey);
|
|
260
|
-
container?.classList.remove("open");
|
|
261
|
-
$emit(EVENT_CLOSE, { key: popupKey });
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
export function normalizeSimpleAnimation(animation?: SimpleAnimation) {
|
|
265
|
-
if (animation?.preset) {
|
|
266
|
-
const preset = ANIMATIONS[animation?.preset];
|
|
267
|
-
return {
|
|
268
|
-
...preset,
|
|
269
|
-
keyframes: { ...preset.keyframes, ...animation.keyframes },
|
|
270
|
-
transition: { ...preset.transition, ...animation.transition },
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return animation;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
//处理预置动画
|
|
278
|
-
export function normalizeDualAnimation(
|
|
279
|
-
animation?: DualAnimation
|
|
280
|
-
): DualAnimation | undefined {
|
|
281
|
-
if (!animation) {
|
|
282
|
-
return undefined;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// 如果有预设,合并预设和自定义配置
|
|
286
|
-
if (animation.preset) {
|
|
287
|
-
const preset = DUAL_ANIMATIONS[animation.preset];
|
|
288
|
-
return {
|
|
289
|
-
...animation,
|
|
290
|
-
in: animation.in
|
|
291
|
-
? normalizeSimpleAnimation({ ...preset.in, ...animation.in })
|
|
292
|
-
: preset.in,
|
|
293
|
-
out: animation.out
|
|
294
|
-
? normalizeSimpleAnimation({ ...preset.out, ...animation.out })
|
|
295
|
-
: preset.out,
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// 如果没有预设,但有 in/out 配置
|
|
300
|
-
return {
|
|
301
|
-
in: animation.in ? normalizeSimpleAnimation(animation.in) : undefined,
|
|
302
|
-
out: animation.out ? normalizeSimpleAnimation(animation.out) : undefined,
|
|
303
|
-
};
|
|
304
|
-
}
|
package/src/scripts/events.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
export type PopupEvent = {
|
|
2
|
-
//控件组的唯一标识
|
|
3
|
-
key: string;
|
|
4
|
-
//发出事件的element,popover对应Container,modal对应Trigger
|
|
5
|
-
target?: HTMLElement;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
export type SelectionEvent = {
|
|
9
|
-
//控件组的唯一标识
|
|
10
|
-
key: string;
|
|
11
|
-
//发出事件的element
|
|
12
|
-
target: HTMLElement;
|
|
13
|
-
selection: string;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export const $emit = (event: string, options?: PopupEvent | SelectionEvent) => {
|
|
17
|
-
const customEvent = new CustomEvent(event, { detail: options });
|
|
18
|
-
document.dispatchEvent(customEvent);
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export const $on = <T = PopupEvent | SelectionEvent>(
|
|
22
|
-
event: string,
|
|
23
|
-
callback: (options: T) => void
|
|
24
|
-
) => {
|
|
25
|
-
const eventListener = (e: CustomEvent) => {
|
|
26
|
-
callback(e.detail);
|
|
27
|
-
};
|
|
28
|
-
document.addEventListener(event, eventListener as EventListener);
|
|
29
|
-
|
|
30
|
-
return () => {
|
|
31
|
-
document.removeEventListener(event, eventListener as EventListener);
|
|
32
|
-
};
|
|
33
|
-
};
|
package/src/scripts/index.ts
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|