@rxdrag/website-lib-core 0.0.6 → 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.
Files changed (71) hide show
  1. package/index.ts +1 -1
  2. package/package.json +11 -7
  3. package/src/component-logic/gsap.d.ts +4 -0
  4. package/src/component-logic/index.ts +8 -0
  5. package/src/component-logic/link-client.ts +33 -0
  6. package/src/component-logic/link.ts +50 -0
  7. package/src/component-logic/modal.ts +36 -0
  8. package/src/component-logic/motion.ts +272 -0
  9. package/src/component-logic/number.ts +45 -0
  10. package/src/component-logic/popover.ts +51 -0
  11. package/src/component-logic/tabs.ts +10 -0
  12. package/src/controller/AnimateController.ts +138 -0
  13. package/src/controller/AosController.ts +240 -0
  14. package/src/controller/FlipController.ts +339 -0
  15. package/src/controller/ModalController.ts +127 -0
  16. package/src/controller/NumberController.ts +161 -0
  17. package/src/controller/PageLoader.ts +163 -0
  18. package/src/controller/PopoverController.ts +116 -0
  19. package/src/controller/TabsController.ts +271 -0
  20. package/src/controller/applyAnimation.ts +86 -0
  21. package/src/controller/applyInitialState.ts +79 -0
  22. package/src/{scripts → controller}/consts.ts +0 -2
  23. package/src/controller/index.ts +9 -0
  24. package/src/controller/popup.ts +346 -0
  25. package/src/controller/utils.ts +48 -0
  26. package/src/entify/Entify.ts +354 -365
  27. package/src/entify/IEntify.ts +91 -0
  28. package/src/entify/index.ts +3 -2
  29. package/src/entify/lib/newQueryProductOptions.ts +2 -3
  30. package/src/entify/lib/newQueryProductsMediaOptions.ts +19 -18
  31. package/src/entify/lib/queryAllProducts.ts +11 -3
  32. package/src/entify/lib/queryFeaturedProducts.ts +3 -3
  33. package/src/entify/lib/queryLatestPosts.ts +2 -2
  34. package/src/entify/lib/queryOneTheme.ts +1 -1
  35. package/src/entify/lib/queryPostCategories.ts +3 -3
  36. package/src/entify/lib/queryPostSlugs.ts +2 -2
  37. package/src/entify/lib/queryPosts.ts +92 -92
  38. package/src/entify/lib/queryProductCategories.ts +3 -3
  39. package/src/entify/lib/queryProducts.ts +69 -69
  40. package/src/entify/lib/queryUserPosts.ts +2 -2
  41. package/src/entify/lib/searchProducts.ts +2 -2
  42. package/src/index.ts +3 -1
  43. package/src/lib/formatDate.ts +15 -0
  44. package/src/lib/index.ts +3 -0
  45. package/src/lib/pagination.ts +114 -0
  46. package/src/lib/utils.ts +119 -0
  47. package/src/motion/consts.ts +428 -598
  48. package/src/motion/convertToGsapVars.ts +102 -0
  49. package/src/motion/index.ts +5 -1
  50. package/src/motion/normalizeAnimation.ts +28 -0
  51. package/src/motion/normalizeAosAnimation.ts +22 -0
  52. package/src/motion/normalizePopupAnimation.ts +24 -0
  53. package/src/motion/types.ts +133 -46
  54. package/src/react/components/AttachmentIcon/index.tsx +53 -0
  55. package/src/react/components/ContactForm/index.tsx +341 -0
  56. package/src/react/components/Icon/index.tsx +10 -0
  57. package/src/react/components/Medias/index.tsx +347 -347
  58. package/src/react/components/ProductCard/ProductCta/index.tsx +7 -5
  59. package/src/react/components/RichTextOutline/index.tsx +76 -76
  60. package/src/react/components/Scroller.tsx +5 -1
  61. package/src/react/components/SearchInput.tsx +36 -34
  62. package/src/react/components/ToTop.tsx +63 -28
  63. package/src/react/components/index.ts +3 -1
  64. package/src/react/hooks/useScroll.ts +16 -10
  65. package/src/react/components/EnquiryForm/index.tsx +0 -334
  66. package/src/scripts/actions.ts +0 -304
  67. package/src/scripts/events.ts +0 -33
  68. package/src/scripts/index.ts +0 -3
  69. /package/src/react/components/{EnquiryForm → ContactForm}/Input.tsx +0 -0
  70. /package/src/react/components/{EnquiryForm → ContactForm}/Submit.tsx +0 -0
  71. /package/src/react/components/{EnquiryForm → ContactForm}/Textarea.tsx +0 -0
@@ -0,0 +1,240 @@
1
+ import { merge } from "lodash-es";
2
+ import {
3
+ AnimationConfig,
4
+ AosAnimationConfig,
5
+ DATA_MOTION_INVIEW,
6
+ } from "../motion";
7
+ import { applyAnimation } from "./applyAnimation";
8
+
9
+ export class AosController {
10
+ private static instances: Record<string, AosController> = {};
11
+ private doc?: Document;
12
+ private unmountHandlers: Array<() => void> = [];
13
+ private intersectionObserver?: IntersectionObserver;
14
+
15
+ private constructor(doc?: Document) {
16
+ this.doc = doc;
17
+ }
18
+
19
+ /**
20
+ * 获取 AosController 实例
21
+ * @param key 实例的唯一标识,默认为 "runtime"
22
+ * @param doc 文档对象
23
+ * @returns AosController 实例
24
+ */
25
+ public static getInstance(
26
+ key: string = "runtime",
27
+ doc?: Document
28
+ ): AosController {
29
+ if (!AosController.instances[key]) {
30
+ AosController.instances[key] = new AosController(doc);
31
+ } else if (doc) {
32
+ AosController.instances[key].setDoc(doc);
33
+ }
34
+ return AosController.instances[key];
35
+ }
36
+
37
+ /**
38
+ * 销毁指定 key 的实例
39
+ * @param key 实例的唯一标识
40
+ */
41
+ public static destroyInstance(key: string = "runtime"): void {
42
+ if (AosController.instances[key]) {
43
+ AosController.instances[key].destroy();
44
+ delete AosController.instances[key];
45
+ }
46
+ }
47
+
48
+ /**
49
+ * 重置所有实例
50
+ */
51
+ public static resetAll(): void {
52
+ Object.keys(AosController.instances).forEach((key) => {
53
+ AosController.destroyInstance(key);
54
+ });
55
+ }
56
+
57
+ /**
58
+ * 设置文档对象
59
+ * @param doc 文档对象
60
+ */
61
+ private setDoc(doc?: Document) {
62
+ this.doc = doc;
63
+ }
64
+
65
+ /**
66
+ * 挂载 AOS 动画控制器
67
+ * @param clientDoc 文档对象,默认为 document
68
+ */
69
+ public mount(): void {
70
+ // 如果没有可用的文档对象,则直接返回
71
+ if (!this.doc) return;
72
+
73
+ // 清理之前的观察器和处理函数
74
+ this.destroy();
75
+
76
+ // 创建 IntersectionObserver 用于检测元素进入视口
77
+ this.intersectionObserver = new IntersectionObserver(
78
+ (entries) => {
79
+ entries.forEach((entry) => {
80
+ if (entry.isIntersecting) {
81
+ const element = entry.target as HTMLElement;
82
+ this.handleElementEnterViewport(element);
83
+ } else {
84
+ const element = entry.target as HTMLElement;
85
+ this.handleElementLeaveViewport(element);
86
+ }
87
+ });
88
+ },
89
+ {
90
+ threshold: 0.1, // 元素10%可见时触发
91
+ root: null, // 使用视口作为根
92
+ }
93
+ );
94
+
95
+ // 查找所有的data-motion-inview元素
96
+ const inviewElements = this.doc.querySelectorAll(`[${DATA_MOTION_INVIEW}]`);
97
+
98
+ inviewElements.forEach((element) => {
99
+ if (
100
+ this.doc?.defaultView &&
101
+ element instanceof this.doc.defaultView.HTMLElement &&
102
+ element.dataset.motionInview
103
+ ) {
104
+ try {
105
+ // 初始化元素样式
106
+ this.initElementStyle(element as HTMLElement);
107
+
108
+ // 添加到观察器
109
+ this.intersectionObserver?.observe(element);
110
+
111
+ // 添加清理函数
112
+ this.unmountHandlers.push(() => {
113
+ this.intersectionObserver?.unobserve(element);
114
+ });
115
+ } catch (error) {
116
+ console.error("初始化元素AOS动画时出错:", element, error);
117
+ }
118
+ }
119
+ });
120
+ }
121
+
122
+ /**
123
+ * 初始化元素样式
124
+ * @param element 要初始化的元素
125
+ */
126
+ private initElementStyle(element: HTMLElement): void {
127
+ try {
128
+ const inViewAnimation = element.dataset.motionInview
129
+ ? (JSON.parse(element.dataset.motionInview) as
130
+ | AosAnimationConfig
131
+ | undefined)
132
+ : undefined;
133
+
134
+ if (!inViewAnimation) return;
135
+
136
+ // 如果设置了once属性,初始化时可能需要设置元素透明度为0
137
+ if (
138
+ inViewAnimation.once &&
139
+ inViewAnimation.enter &&
140
+ !element.dataset.motionViewportEnter
141
+ ) {
142
+ // 如果是淡入动画,初始透明度可能需要设置为0
143
+ if (!element.style.opacity && inViewAnimation.enter.opacity === 1) {
144
+ element.style.opacity = "0";
145
+ }
146
+ }
147
+ } catch (error) {
148
+ console.error("初始化元素样式时出错:", element, error);
149
+ }
150
+ }
151
+
152
+ /**
153
+ * 处理元素进入视口
154
+ * @param element 进入视口的元素
155
+ */
156
+ private handleElementEnterViewport(element: HTMLElement): void {
157
+ try {
158
+ const animation = element.dataset.motion
159
+ ? (JSON.parse(element.dataset.motion) as AnimationConfig | undefined)
160
+ : undefined;
161
+ const inViewAnimation = element.dataset.motionInview
162
+ ? (JSON.parse(element.dataset.motionInview) as
163
+ | AosAnimationConfig
164
+ | undefined)
165
+ : undefined;
166
+
167
+ if (!inViewAnimation) return;
168
+
169
+ const needAction =
170
+ (!element.dataset.motionViewportEnter && inViewAnimation.once) ||
171
+ !inViewAnimation.once;
172
+
173
+ if (inViewAnimation.enter && needAction) {
174
+ element.dataset.motionViewportEnter = "true";
175
+ applyAnimation(element, merge(animation, inViewAnimation.enter), {
176
+ ensureVisibleOnComplete: inViewAnimation.once,
177
+ });
178
+
179
+ // 如果是一次性动画,不再观察
180
+ if (inViewAnimation.once) {
181
+ this.intersectionObserver?.unobserve(element);
182
+ }
183
+ }
184
+ } catch (error) {
185
+ console.error("处理元素进入视口时出错:", element, error);
186
+ }
187
+ }
188
+
189
+ /**
190
+ * 处理元素离开视口
191
+ * @param element 离开视口的元素
192
+ */
193
+ private handleElementLeaveViewport(element: HTMLElement): void {
194
+ try {
195
+ const animation = element.dataset.motion
196
+ ? (JSON.parse(element.dataset.motion) as AnimationConfig | undefined)
197
+ : undefined;
198
+ const inViewAnimation = element.dataset.motionInview
199
+ ? (JSON.parse(element.dataset.motionInview) as
200
+ | AosAnimationConfig
201
+ | undefined)
202
+ : undefined;
203
+
204
+ if (!inViewAnimation || inViewAnimation.once) return;
205
+
206
+ if (inViewAnimation.exit) {
207
+ applyAnimation(element, merge(animation, inViewAnimation.exit));
208
+ }
209
+ } catch (error) {
210
+ console.error("处理元素离开视口时出错:", element, error);
211
+ }
212
+ }
213
+
214
+ /**
215
+ * 销毁 AOS 动画控制器
216
+ */
217
+ public destroy(): void {
218
+ // 断开所有观察
219
+ if (this.intersectionObserver) {
220
+ this.intersectionObserver.disconnect();
221
+ this.intersectionObserver = undefined;
222
+ }
223
+
224
+ // 执行所有卸载处理函数
225
+ this.unmountHandlers.forEach((handler) => {
226
+ try {
227
+ handler();
228
+ } catch (error) {
229
+ console.error("执行卸载处理函数失败", error);
230
+ }
231
+ });
232
+ this.unmountHandlers = [];
233
+ }
234
+ }
235
+
236
+ // 导出默认实例,方便直接使用
237
+ export const aos = AosController.getInstance(
238
+ "runtime",
239
+ typeof document !== "undefined" ? document : undefined
240
+ );
@@ -0,0 +1,339 @@
1
+ import { DATA_MOTION_FLIP } from "../component-logic/tabs";
2
+
3
+ // 全局 Flip 控制器
4
+ export class FlipController {
5
+ // 单例实例
6
+ private static instances: Record<string, FlipController> = {};
7
+ private constructor(private doc?: Document) {}
8
+
9
+ // 记录元素的上一个位置(相对于文档的绝对位置)
10
+ private previousPositions = new Map<
11
+ string,
12
+ {
13
+ left: number;
14
+ top: number;
15
+ width: number;
16
+ height: number;
17
+ wasVisible?: boolean; // 标记元素是否曾经可见
18
+ }
19
+ >();
20
+ // 标记正在处理的元素,防止递归循环
21
+ private processingElements = new Set<HTMLElement>();
22
+ // 观察器
23
+ private resizeObserver: ResizeObserver | null = null;
24
+
25
+ // 获取单例实例
26
+ static getInstance(key: string = "runtime", doc?: Document): FlipController {
27
+ if (!FlipController.instances[key]) {
28
+ FlipController.instances[key] = new FlipController(doc);
29
+ } else if (doc) {
30
+ FlipController.instances[key].setDoc(doc);
31
+ }
32
+ return FlipController.instances[key];
33
+ }
34
+ // 初始化控制器
35
+ mount() {
36
+ this.unmount();
37
+ // 设置观察器
38
+ this.setupObservers();
39
+ // 初始化元素
40
+ this.initElements();
41
+ }
42
+
43
+ unmount() {
44
+ // 移除观察器
45
+ this.resizeObserver?.disconnect();
46
+ this.resizeObserver = null;
47
+ // 移除事件监听器
48
+ this.doc?.defaultView?.removeEventListener("scroll", this.handleScroll);
49
+ // 清理数据结构
50
+ this.previousPositions.clear();
51
+ this.processingElements.clear();
52
+ }
53
+
54
+ // 销毁指定实例
55
+ static destroyInstance(key: string = "runtime") {
56
+ if (FlipController.instances[key]) {
57
+ FlipController.instances[key].unmount();
58
+ delete FlipController.instances[key];
59
+ }
60
+ }
61
+
62
+ // 销毁所有实例
63
+ static destroyAllInstances() {
64
+ Object.keys(FlipController.instances).forEach((key) => {
65
+ FlipController.instances[key].unmount();
66
+ });
67
+ FlipController.instances = {};
68
+ }
69
+
70
+ private setDoc(doc?: Document) {
71
+ this.doc = doc;
72
+ }
73
+
74
+ // 滚动事件处理函数
75
+ private handleScroll = () => {
76
+ // 页面滚动时,重新计算所有需要动画的元素
77
+ this.doc
78
+ ?.querySelectorAll<HTMLElement>("[data-flip]")
79
+ .forEach((element) => {
80
+ if (this.previousPositions.has(element.dataset.flip || "")) {
81
+ this.performAnimation(element);
82
+ }
83
+ });
84
+ };
85
+
86
+ // 设置观察器
87
+ private setupObservers() {
88
+ // 设置 ResizeObserver 监控元素尺寸变化
89
+ this.resizeObserver = new ResizeObserver((entries) => {
90
+ entries.forEach((entry) => {
91
+ if (
92
+ this.doc?.defaultView?.HTMLElement &&
93
+ entry.target instanceof this.doc?.defaultView?.HTMLElement
94
+ ) {
95
+ this.performAnimation(entry.target);
96
+ }
97
+ });
98
+ });
99
+
100
+ // 监听滚动事件 - 滚动不会影响相对于文档的绝对位置,所以不需要特殊处理
101
+ this.doc?.defaultView?.addEventListener("scroll", this.handleScroll, {
102
+ passive: true,
103
+ });
104
+ }
105
+
106
+ // 初始化元素,记录初始位置
107
+ private initElements() {
108
+ // 查找所有带有 data-flip 属性的元素
109
+ const flipElements = this.doc?.querySelectorAll<HTMLElement>("[data-flip]");
110
+ flipElements?.forEach((element) => {
111
+ // 添加到 ResizeObserver 监控
112
+ if (this.resizeObserver) {
113
+ this.resizeObserver.observe(element);
114
+ }
115
+ });
116
+
117
+ // 添加 IntersectionObserver 来监测元素进入视窗
118
+ if (typeof IntersectionObserver !== "undefined" && this.doc) {
119
+ const intersectionObserver = new IntersectionObserver(
120
+ (entries) => {
121
+ entries.forEach((entry) => {
122
+ if (entry.isIntersecting && entry.target instanceof HTMLElement) {
123
+ const flipKey = entry.target.dataset.flip;
124
+ if (flipKey) {
125
+ const position = this.previousPositions.get(flipKey);
126
+ if (position && position.wasVisible === false) {
127
+ // 元素首次进入视窗,更新位置记录
128
+ this.recordPosition(entry.target);
129
+ }
130
+ }
131
+ }
132
+ });
133
+ },
134
+ { threshold: 0.1 }
135
+ );
136
+
137
+ // 观察所有 flip 元素
138
+ flipElements?.forEach((element) => {
139
+ intersectionObserver.observe(element);
140
+ });
141
+ }
142
+
143
+ // 延迟记录初始位置,确保元素已经渲染完成
144
+ setTimeout(() => {
145
+ // 记录每个元素的初始位置
146
+ flipElements?.forEach((element) => {
147
+ this.recordPosition(element);
148
+ });
149
+ }, 100); // 给予足够的时间让元素渲染完成
150
+ }
151
+
152
+ // 记录元素位置
153
+ private recordPosition(element: HTMLElement) {
154
+ const flipKey = element.dataset.flip;
155
+ if (!flipKey) return;
156
+
157
+ // 检查元素是否可见
158
+ const isCurrentlyVisible = this.isVisible(element);
159
+ // 获取之前的位置记录
160
+ const previousPosition = this.previousPositions.get(flipKey);
161
+
162
+ // 如果元素不可见且没有之前的记录,我们仍然记录它的位置
163
+ // 但标记它为不可见,这样当它首次出现时我们可以决定是否应用动画
164
+ if (!isCurrentlyVisible && !previousPosition) {
165
+ // 继续执行,记录位置
166
+ }
167
+
168
+ // 获取元素相对于视口的位置
169
+ const rect = element.getBoundingClientRect();
170
+
171
+ // 计算元素相对于文档的绝对位置
172
+ const absoluteLeft = rect.left + (this.doc?.defaultView?.scrollX || 0);
173
+ const absoluteTop = rect.top + (this.doc?.defaultView?.scrollY || 0);
174
+
175
+ // 记录元素相对于文档的绝对位置
176
+ this.previousPositions.set(flipKey, {
177
+ left: absoluteLeft,
178
+ top: absoluteTop,
179
+ width: rect.width,
180
+ height: rect.height,
181
+ wasVisible: isCurrentlyVisible, // 记录元素是否可见
182
+ });
183
+ }
184
+
185
+ // 检查元素是否可见
186
+ private isVisible(element: HTMLElement): boolean {
187
+ const rect = element.getBoundingClientRect();
188
+ return (
189
+ rect.width > 0 &&
190
+ rect.height > 0 &&
191
+ rect.top < (this.doc?.defaultView?.innerHeight || 0) &&
192
+ rect.bottom > 0 &&
193
+ rect.left < (this.doc?.defaultView?.innerWidth || 0) &&
194
+ rect.right > 0
195
+ );
196
+ }
197
+
198
+ // 执行 FLIP 动画
199
+ performAnimation(element: HTMLElement) {
200
+ // 防止重复处理同一元素
201
+ if (this.processingElements.has(element)) {
202
+ return;
203
+ }
204
+
205
+ // 获取元素的 flip key
206
+ const flipKey = element.dataset.flip;
207
+ if (!flipKey) {
208
+ return;
209
+ }
210
+
211
+ // 检查元素是否可见
212
+ if (!this.isVisible(element)) {
213
+ return;
214
+ }
215
+
216
+ // 记录当前正在处理的元素
217
+ this.processingElements.add(element);
218
+
219
+ // 获取元素的当前位置(相对于视口)
220
+ const currentRect = element.getBoundingClientRect();
221
+
222
+ // 检查当前位置是否有效
223
+ if (currentRect.width === 0 || currentRect.height === 0) {
224
+ this.processingElements.delete(element);
225
+ return;
226
+ }
227
+
228
+ // 计算当前元素相对于文档的绝对位置
229
+ const currentAbsoluteLeft =
230
+ currentRect.left + (this.doc?.defaultView?.scrollX || 0);
231
+ const currentAbsoluteTop =
232
+ currentRect.top + (this.doc?.defaultView?.scrollY || 0);
233
+
234
+ // 获取元素的上一个位置
235
+ const previousPosition = this.previousPositions.get(flipKey);
236
+
237
+ // 如果没有上一个位置,记录当前位置并返回
238
+ if (!previousPosition) {
239
+ this.recordPosition(element);
240
+ this.processingElements.delete(element);
241
+ return;
242
+ }
243
+
244
+ // 如果元素之前未曾可见(首次进入视窗),我们可能想要跳过动画
245
+ // 或者应用特殊的入场动画
246
+ if (this.isVisible(element) && previousPosition.wasVisible === false) {
247
+ // 更新位置记录,标记为可见
248
+ this.recordPosition(element);
249
+ this.processingElements.delete(element);
250
+ return;
251
+ }
252
+
253
+ // 检查上一个位置是否有效
254
+ if (previousPosition.width === 0 || previousPosition.height === 0) {
255
+ this.recordPosition(element);
256
+ this.processingElements.delete(element);
257
+ return;
258
+ }
259
+
260
+ // 计算位置差异(基于绝对位置,不需要考虑滚动)
261
+ const deltaX = previousPosition.left - currentAbsoluteLeft;
262
+ const deltaY = previousPosition.top - currentAbsoluteTop;
263
+ const scaleX = previousPosition.width / currentRect.width;
264
+ const scaleY = previousPosition.height / currentRect.height;
265
+
266
+ // 如果位置没有变化,跳过动画
267
+ if (
268
+ Math.abs(deltaX) < 1 &&
269
+ Math.abs(deltaY) < 1 &&
270
+ Math.abs(scaleX - 1) < 0.01 &&
271
+ Math.abs(scaleY - 1) < 0.01
272
+ ) {
273
+ this.recordPosition(element);
274
+ this.processingElements.delete(element);
275
+ return;
276
+ }
277
+
278
+ try {
279
+ // 使用原生 Web Animation API 执行动画
280
+
281
+ // 先将元素设置为初始位置
282
+ element.style.transform = `translate(${deltaX}px, ${deltaY}px) scale(${scaleX}, ${scaleY})`;
283
+ element.style.transformOrigin = "0 0";
284
+
285
+ // 使用 requestAnimationFrame 确保初始状态已应用
286
+ requestAnimationFrame(() => {
287
+ const container = element.closest(`[${DATA_MOTION_FLIP}]`);
288
+ const transionJSON = (container as HTMLElement | null)?.getAttribute(
289
+ DATA_MOTION_FLIP
290
+ );
291
+ const transion = transionJSON
292
+ ? JSON.parse(transionJSON)
293
+ : {
294
+ duration: 300,
295
+ };
296
+ // 创建动画
297
+ const animation = element.animate(
298
+ [
299
+ {
300
+ transform: `translate(${deltaX}px, ${deltaY}px) scale(${scaleX}, ${scaleY})`,
301
+ },
302
+ { transform: "translate(0, 0) scale(1, 1)" },
303
+ ],
304
+ transion
305
+ );
306
+
307
+ // 动画完成后的处理
308
+ animation.onfinish = () => {
309
+ // 清除样式并更新位置
310
+ element.style.transform = "";
311
+ element.style.transformOrigin = "";
312
+ this.recordPosition(element);
313
+ this.processingElements.delete(element);
314
+ };
315
+
316
+ // 设置一个备用的超时处理,以防动画事件不触发
317
+ setTimeout(() => {
318
+ if (this.processingElements.has(element)) {
319
+ element.style.transform = "";
320
+ element.style.transformOrigin = "";
321
+ this.recordPosition(element);
322
+ this.processingElements.delete(element);
323
+ }
324
+ }, 350); // 略长于动画时间
325
+ });
326
+ } catch (error) {
327
+ // 出错时也要清理
328
+ element.style.transform = "";
329
+ element.style.transformOrigin = "";
330
+ this.recordPosition(element);
331
+ this.processingElements.delete(element);
332
+ }
333
+ }
334
+ }
335
+
336
+ export const flip = FlipController.getInstance(
337
+ "runtime",
338
+ typeof document !== "undefined" ? document : undefined
339
+ );
@@ -0,0 +1,127 @@
1
+ import { DATA_POPUP, DATA_POPUP_ROLE, PopupRole } from "./consts";
2
+ import { PopupController } from "./popup";
3
+
4
+ export class ModalController extends PopupController {
5
+ private static instances: Record<string, ModalController> = {};
6
+
7
+ private constructor(doc?: Document) {
8
+ super(doc);
9
+ }
10
+
11
+ public static getInstance(
12
+ key: string = "runtime",
13
+ doc?: Document
14
+ ): ModalController {
15
+ if (!ModalController.instances[key]) {
16
+ ModalController.instances[key] = new ModalController(doc);
17
+ } else if (doc) {
18
+ ModalController.instances[key].setDoc(doc);
19
+ }
20
+ return ModalController.instances[key];
21
+ }
22
+
23
+ /**
24
+ * 销毁指定 key 的实例
25
+ * @param key 实例的唯一标识
26
+ */
27
+ public static destroyInstance(key: string = "runtime"): void {
28
+ if (ModalController.instances[key]) {
29
+ ModalController.instances[key].destroy();
30
+ delete ModalController.instances[key];
31
+ }
32
+ }
33
+
34
+ private setDoc(doc?: Document) {
35
+ this.doc = doc;
36
+ }
37
+
38
+ mount() {
39
+ super.mount();
40
+ const popups = this.doc?.querySelectorAll(`[${DATA_POPUP}]`);
41
+ popups?.forEach((popup) => {
42
+ const popupKey = popup.getAttribute(DATA_POPUP);
43
+ //处理鼠标交互事件
44
+ if (popupKey && popup) {
45
+ if (popup.getAttribute(DATA_POPUP_ROLE) === PopupRole.ModalTrigger) {
46
+ const unsub = this.initModalTrigger(popupKey, popup as HTMLElement);
47
+ this.unmountHandlers.push(unsub);
48
+ }
49
+ }
50
+ //获取所有 ModalCloser 实例,并处理点击事件
51
+ //TODO:未处理嵌套跟Modal外部关闭的情况
52
+ const closers = popup?.querySelectorAll(
53
+ `[${DATA_POPUP_ROLE}="${PopupRole.ModalCloser}"]`
54
+ );
55
+
56
+ closers?.forEach((closer) => {
57
+ // 使用initModalCloser函数处理关闭按钮事件
58
+ if (popupKey) {
59
+ const unsub = this.initModalCloser(popupKey, closer as HTMLElement);
60
+ this.unmountHandlers.push(unsub);
61
+ }
62
+ });
63
+ });
64
+ }
65
+ /**
66
+ * 初始化Modal的点击交互事件
67
+ * @param popupKey - 弹出层的唯一标识
68
+ * @param trigger - 触发器元素
69
+ * @returns 清理函数,用于移除事件监听器
70
+ */
71
+ public initModalTrigger(popupKey: string, trigger: HTMLElement): () => void {
72
+ if (!this.doc) return () => {};
73
+
74
+ try {
75
+ // 创建新的事件处理函数
76
+ const handleTriggerClick = () => {
77
+ // 查找 Modal 容器
78
+ this.open(popupKey, trigger);
79
+ };
80
+
81
+ // 添加新的事件监听器
82
+ trigger.addEventListener("click", handleTriggerClick);
83
+
84
+ return () => {
85
+ trigger.removeEventListener("click", handleTriggerClick);
86
+ };
87
+ } catch (error) {
88
+ console.error(`初始化Modal事件失败: ${popupKey}`, error);
89
+ return () => {};
90
+ }
91
+ }
92
+
93
+ /**
94
+ * 初始化Modal关闭按钮的点击交互事件
95
+ * @param popupKey - 弹出层的唯一标识
96
+ * @param closer - 关闭按钮元素
97
+ * @returns 清理函数,用于移除事件监听器
98
+ */
99
+ public initModalCloser(popupKey: string, closer: HTMLElement): () => void {
100
+ if (!this.doc) return () => {};
101
+
102
+ try {
103
+ // 创建新的事件处理函数
104
+ const handleCloserClick = () => {
105
+ this.close(popupKey);
106
+ };
107
+
108
+ // 添加新的事件监听器
109
+ closer.addEventListener("click", handleCloserClick);
110
+
111
+ return () => {
112
+ closer.removeEventListener("click", handleCloserClick);
113
+ };
114
+ } catch (error) {
115
+ console.error(`初始化Modal关闭按钮事件失败: ${popupKey}`, error);
116
+ return () => {};
117
+ }
118
+ }
119
+ unmount(): void {
120
+ super.unmount();
121
+ }
122
+ }
123
+
124
+ export const modal = ModalController.getInstance(
125
+ "runtime",
126
+ typeof document !== "undefined" ? document : undefined
127
+ );