@tencentcloud/web-push 1.0.3 → 1.0.4

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 (42) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +100 -92
  3. package/{dist/index.esm.js → index.esm.js} +1 -1
  4. package/{dist/index.umd.js → index.umd.js} +1 -1
  5. package/package.json +9 -47
  6. package/src/__tests__/index.test.ts +0 -120
  7. package/src/__tests__/integration.test.ts +0 -285
  8. package/src/__tests__/setup.ts +0 -210
  9. package/src/__tests__/types.test.ts +0 -303
  10. package/src/__tests__/web-push-sdk.test.ts +0 -257
  11. package/src/components/message-popup.ts +0 -1007
  12. package/src/core/event-emitter.ts +0 -61
  13. package/src/core/service-worker-manager.ts +0 -614
  14. package/src/core/web-push-sdk.ts +0 -690
  15. package/src/debug/GenerateTestUserSig.js +0 -37
  16. package/src/debug/index.d.ts +0 -6
  17. package/src/debug/index.js +0 -1
  18. package/src/debug/lib-generate-test-usersig-es.min.js +0 -2
  19. package/src/index.ts +0 -9
  20. package/src/service-worker/sw.ts +0 -494
  21. package/src/types/index.ts +0 -2
  22. package/src/types/inner.ts +0 -44
  23. package/src/types/outer.ts +0 -142
  24. package/src/utils/browser-support.ts +0 -412
  25. package/src/utils/logger.ts +0 -66
  26. package/src/utils/storage.ts +0 -51
  27. package/src/utils/validator.ts +0 -267
  28. /package/{dist/index.d.ts → index.d.ts} +0 -0
  29. /package/{dist/src → src}/components/message-popup.d.ts +0 -0
  30. /package/{dist/src → src}/core/event-emitter.d.ts +0 -0
  31. /package/{dist/src → src}/core/service-worker-manager.d.ts +0 -0
  32. /package/{dist/src → src}/core/web-push-sdk.d.ts +0 -0
  33. /package/{dist/src → src}/index.d.ts +0 -0
  34. /package/{dist/src → src}/service-worker/sw.d.ts +0 -0
  35. /package/{dist/src → src}/types/index.d.ts +0 -0
  36. /package/{dist/src → src}/types/inner.d.ts +0 -0
  37. /package/{dist/src → src}/types/outer.d.ts +0 -0
  38. /package/{dist/src → src}/utils/browser-support.d.ts +0 -0
  39. /package/{dist/src → src}/utils/logger.d.ts +0 -0
  40. /package/{dist/src → src}/utils/storage.d.ts +0 -0
  41. /package/{dist/src → src}/utils/validator.d.ts +0 -0
  42. /package/{dist/sw.js → sw.js} +0 -0
@@ -1,1007 +0,0 @@
1
- import { StandardMessage, HtmlMessage } from '../types';
2
- import { logger } from '../utils/logger';
3
-
4
- export interface PopupConfig {
5
- /** 是否启用弹窗 */
6
- enabled?: boolean;
7
- /** 弹窗显示时长(毫秒),0 表示不自动关闭 */
8
- duration?: number;
9
- /** 弹窗位置 */
10
- position?:
11
- | 'center'
12
- | 'top-left'
13
- | 'top-center'
14
- | 'top-right'
15
- | 'right-center'
16
- | 'bottom-right'
17
- | 'bottom-center'
18
- | 'bottom-left'
19
- | 'left-center';
20
- /** 最大显示数量 */
21
- maxCount?: number;
22
- /** 是否显示关闭按钮 */
23
- showCloseButton?: boolean;
24
- /** 是否允许点击弹窗 */
25
- clickable?: boolean;
26
- /** 自定义渲染函数 */
27
- customRender?: (message: any, container: HTMLElement) => HTMLElement;
28
- /** 点击回调 */
29
- onClick?: (message: any, popup: HTMLElement) => void;
30
- /** 关闭回调 */
31
- onClose?: (message: any, popup: HTMLElement) => void;
32
- }
33
-
34
- export class MessagePopup {
35
- private container: HTMLElement | null = null;
36
- private popups: Map<string, HTMLElement> = new Map();
37
- private timers: Map<string, NodeJS.Timeout> = new Map(); // 存储定时器
38
- private messageHandlers: Map<string, (event: MessageEvent) => void> =
39
- new Map(); // 存储消息监听器
40
- private animationTimers: Map<string, NodeJS.Timeout> = new Map(); // 存储动画定时器
41
- private adjustHeightTimers: Map<string, NodeJS.Timeout> = new Map(); // 存储高度调整定时器
42
- private config: Required<PopupConfig>;
43
- private readonly defaultConfig: Required<PopupConfig> = {
44
- enabled: true,
45
- duration: 0, // 默认不自动关闭
46
- position: 'top-right',
47
- maxCount: 5,
48
- showCloseButton: true,
49
- clickable: true,
50
- customRender: undefined as any,
51
- onClick: undefined as any,
52
- onClose: undefined as any,
53
- };
54
-
55
- constructor(config: PopupConfig = {}) {
56
- this.config = { ...this.defaultConfig, ...config };
57
- this.init();
58
- }
59
-
60
- private init(): void {
61
- if (!this.config.enabled) return;
62
-
63
- // 创建容器
64
- this.createContainer();
65
-
66
- // 注入样式
67
- this.injectStyles();
68
- }
69
-
70
- private createContainer(): void {
71
- // 检查是否已存在容器
72
- const existingContainer = document.getElementById(
73
- 'web-push-popup-container'
74
- );
75
- if (existingContainer) {
76
- this.container = existingContainer;
77
- return;
78
- }
79
-
80
- this.container = document.createElement('div');
81
- this.container.id = 'web-push-popup-container';
82
- this.container.className = `web-push-popup-container web-push-popup-${this.config.position}`;
83
-
84
- document.body.appendChild(this.container);
85
- }
86
-
87
- private injectStyles(): void {
88
- // 检查是否已注入样式
89
- if (document.getElementById('web-push-popup-styles')) return;
90
-
91
- const style = document.createElement('style');
92
- style.id = 'web-push-popup-styles';
93
- style.textContent = `
94
- .web-push-popup-container {
95
- position: fixed;
96
- z-index: 10000;
97
- pointer-events: none;
98
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
99
- }
100
-
101
- .web-push-popup-top-left {
102
- top: 20px;
103
- left: 20px;
104
- }
105
-
106
- .web-push-popup-top-center {
107
- top: 20px;
108
- left: 50%;
109
- transform: translateX(-50%);
110
- }
111
-
112
- .web-push-popup-top-right {
113
- top: 20px;
114
- right: 20px;
115
- }
116
-
117
- .web-push-popup-right-center {
118
- top: 50%;
119
- right: 20px;
120
- transform: translateY(-50%);
121
- }
122
-
123
- .web-push-popup-bottom-right {
124
- bottom: 20px;
125
- right: 20px;
126
- }
127
-
128
- .web-push-popup-bottom-center {
129
- bottom: 20px;
130
- left: 50%;
131
- transform: translateX(-50%);
132
- }
133
-
134
- .web-push-popup-bottom-left {
135
- bottom: 20px;
136
- left: 20px;
137
- }
138
-
139
- .web-push-popup-left-center {
140
- top: 50%;
141
- left: 20px;
142
- transform: translateY(-50%);
143
- }
144
-
145
- .web-push-popup-center {
146
- top: 50%;
147
- left: 50%;
148
- transform: translate(-50%, -50%);
149
- }
150
-
151
- .web-push-popup {
152
- background: #ffffff;
153
- border-radius: 16px;
154
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
155
- margin-bottom: 10px;
156
- padding: 24px;
157
- min-width: 300px;
158
- max-width: 480px;
159
- pointer-events: auto;
160
- cursor: pointer;
161
- transition: all 0.3s ease;
162
- border: 1px solid #e1e5e9;
163
- position: relative;
164
- animation: web-push-popup-slide-in 0.3s ease-out;
165
- }
166
-
167
- .web-push-popup:hover {
168
- box-shadow: 0 6px 25px rgba(0, 0, 0, 0.2);
169
- transform: translateY(-2px);
170
- }
171
-
172
- .web-push-popup.web-push-popup-closing {
173
- animation: web-push-popup-slide-out 0.3s ease-in forwards;
174
- }
175
-
176
- @keyframes web-push-popup-slide-in {
177
- from {
178
- opacity: 0;
179
- transform: translateX(100%);
180
- }
181
- to {
182
- opacity: 1;
183
- transform: translateX(0);
184
- }
185
- }
186
-
187
- @keyframes web-push-popup-slide-out {
188
- from {
189
- opacity: 1;
190
- transform: translateX(0);
191
- }
192
- to {
193
- opacity: 0;
194
- transform: translateX(100%);
195
- }
196
- }
197
-
198
- .web-push-popup-header {
199
- display: flex;
200
- align-items: center;
201
- margin-bottom: 8px;
202
- }
203
-
204
- .web-push-popup-info {
205
- flex: 1;
206
- min-width: 0;
207
- }
208
-
209
- .web-push-popup-title {
210
- font-size: 14px;
211
- font-weight: 600;
212
- color: #1a1a1a;
213
- margin: 0 0 2px 0;
214
- white-space: nowrap;
215
- overflow: hidden;
216
- text-overflow: ellipsis;
217
- }
218
-
219
- .web-push-popup-subtitle {
220
- font-size: 12px;
221
- color: #666;
222
- margin: 0;
223
- }
224
-
225
- .web-push-popup-close {
226
- position: absolute;
227
- top: 8px;
228
- right: 8px;
229
- background: none;
230
- border: none;
231
- font-size: 18px;
232
- color: #999;
233
- cursor: pointer;
234
- padding: 4px;
235
- line-height: 1;
236
- border-radius: 4px;
237
- transition: all 0.2s ease;
238
- }
239
-
240
- .web-push-popup-close:hover {
241
- background: #f5f5f5;
242
- color: #333;
243
- }
244
-
245
- .web-push-popup-content {
246
- font-size: 14px;
247
- color: #333;
248
- line-height: 1.4;
249
- word-wrap: break-word;
250
- margin: 0;
251
- }
252
-
253
- .web-push-popup-html-container {
254
- margin-bottom: 10px;
255
- pointer-events: auto;
256
- position: relative;
257
- animation: web-push-popup-slide-in 0.3s ease-out;
258
- }
259
-
260
- .web-push-popup-html-container.web-push-popup-closing {
261
- animation: web-push-popup-slide-out 0.3s ease-in forwards;
262
- }
263
- `;
264
-
265
- document.head.appendChild(style);
266
- }
267
-
268
- public show(message: StandardMessage | HtmlMessage): void {
269
- if (!this.config.enabled || !this.container) return;
270
-
271
- try {
272
- if (this.popups.size >= this.config.maxCount) {
273
- this.removeOldestPopup();
274
- }
275
-
276
- if (this.config.customRender) {
277
- const popup = this.config.customRender(message, this.container);
278
- this.addPopup(message.id, popup, message.MsgContent.Duration);
279
- return;
280
- }
281
-
282
- let popup: HTMLElement;
283
-
284
- if (message.MsgType === 'standard') {
285
- popup = this.createStandardPopup(message, message.id);
286
- } else {
287
- popup = this.createHtmlPopup(message, message.id);
288
- }
289
-
290
- this.applyPlacement(message.MsgContent.Placement ?? 0);
291
-
292
- this.addPopup(message.id, popup, message.MsgContent.Duration);
293
- } catch (error) {
294
- logger.error('Failed to show message popup', error);
295
- }
296
- }
297
-
298
- private createStandardPopup(
299
- message: StandardMessage,
300
- messageId: string
301
- ): HTMLElement {
302
- const popup = document.createElement('div');
303
- popup.className = 'web-push-popup web-push-popup-standard';
304
- popup.dataset.messageId = messageId;
305
-
306
- if (message.MsgContent.Image) {
307
- const imageContainer = document.createElement('div');
308
- imageContainer.className = 'web-push-popup-image';
309
- imageContainer.style.cssText = `
310
- text-align: center;
311
- margin-bottom: 20px;
312
- `;
313
-
314
- const img = document.createElement('img');
315
- img.src = message.MsgContent.Image.URL;
316
- img.alt = 'Message Image';
317
- img.style.cssText = `
318
- width: auto;
319
- height: auto;
320
- max-width: 432px;
321
- max-height: 222px;
322
- border-radius: 4px;
323
- object-fit: cover;
324
- cursor: pointer;
325
- display: block;
326
- margin: 0 auto;
327
- `;
328
-
329
- img.onclick = (e) => {
330
- e.stopPropagation();
331
- if (message.MsgContent.Image?.LinkURL) {
332
- window.open(message.MsgContent.Image.LinkURL, '_blank');
333
- }
334
- if (this.config.onClick) {
335
- this.config.onClick(message, popup);
336
- }
337
- this.close(messageId);
338
- };
339
-
340
- imageContainer.appendChild(img);
341
- popup.appendChild(imageContainer);
342
- }
343
-
344
- if (message.MsgContent.Text) {
345
- const contentArea = document.createElement('div');
346
- contentArea.className = 'web-push-popup-content-area';
347
-
348
- if (message.MsgContent.Text.Title) {
349
- const title = document.createElement('div');
350
- title.className = 'web-push-popup-standard-title';
351
- title.textContent = message.MsgContent.Text.Title;
352
- title.style.cssText = `
353
- font-size: 18px;
354
- line-height: 26px;
355
- font-weight: 600;
356
- color: #000000E5;
357
- margin-bottom: 8px;
358
- max-width: 432px;
359
- overflow: hidden;
360
- text-overflow: ellipsis;
361
- display: -webkit-box;
362
- -webkit-line-clamp: 2;
363
- -webkit-box-orient: vertical;
364
- word-wrap: break-word;
365
- `;
366
- contentArea.appendChild(title);
367
- }
368
-
369
- // 描述
370
- if (message.MsgContent.Text.Desc) {
371
- const desc = document.createElement('div');
372
- desc.className = 'web-push-popup-standard-desc';
373
- desc.textContent = message.MsgContent.Text.Desc;
374
- desc.style.cssText = `
375
- font-size: 14px;
376
- font-weight: 400;
377
- color: #000000B2;
378
- line-height: 22px;
379
- max-width: 432px;
380
- overflow: hidden;
381
- text-overflow: ellipsis;
382
- display: -webkit-box;
383
- -webkit-line-clamp: 6;
384
- -webkit-box-orient: vertical;
385
- word-wrap: break-word;
386
- `;
387
- contentArea.appendChild(desc);
388
- }
389
- popup.appendChild(contentArea);
390
- }
391
-
392
- if (message.MsgContent.Button && message.MsgContent.Button.length > 0) {
393
- const buttonContainer = document.createElement('div');
394
- buttonContainer.className = 'web-push-popup-buttons';
395
- buttonContainer.style.cssText = `
396
- display: flex;
397
- gap: 12px;
398
- margin-top: 24px;
399
- flex-wrap: wrap;
400
- `;
401
-
402
- message.MsgContent.Button.forEach((buttonConfig, index) => {
403
- const button = document.createElement('button');
404
- button.className = 'web-push-popup-button';
405
- button.textContent = buttonConfig.Text;
406
- button.style.cssText = `
407
- flex: 1;
408
- min-width: 80px;
409
- padding: 8px 16px;
410
- border: 1px solid ${index === 0 ? '#E6E9EF' : '#0052D9'};
411
- background: ${index === 0 ? 'transparent' : '#0052D9'};
412
- color: ${index === 0 ? '#000000E5' : '#FFFFFFE5'};
413
- border-radius: 4px;
414
- font-size: 14px;
415
- font-weight: 400;
416
- line-height: 22px;
417
- cursor: pointer;
418
- transition: all 0.2s ease;
419
- `;
420
-
421
- if (buttonConfig.Icon) {
422
- const icon = document.createElement('img');
423
- icon.src = buttonConfig.Icon;
424
- icon.style.cssText = `
425
- width: 16px;
426
- height: 16px;
427
- margin-right: 6px;
428
- vertical-align: middle;
429
- `;
430
- button.insertBefore(icon, button.firstChild);
431
- }
432
-
433
- button.onclick = (e) => {
434
- e.stopPropagation();
435
- if (buttonConfig.Url) {
436
- window.open(buttonConfig.Url, '_blank');
437
- }
438
-
439
- if (this.config.onClick) {
440
- this.config.onClick(message, popup);
441
- }
442
- this.close(messageId);
443
- };
444
-
445
- button.onmouseenter = () => {
446
- button.style.transform = 'translateY(-1px)';
447
- button.style.boxShadow = '0 2px 8px rgba(0,122,255,0.3)';
448
- };
449
- button.onmouseleave = () => {
450
- button.style.transform = 'translateY(0)';
451
- button.style.boxShadow = 'none';
452
- };
453
-
454
- buttonContainer.appendChild(button);
455
- });
456
-
457
- popup.appendChild(buttonContainer);
458
- }
459
-
460
- if (message.MsgContent.Close === 1 && this.config.showCloseButton) {
461
- const closeBtn = document.createElement('button');
462
- closeBtn.className = 'web-push-popup-close';
463
- closeBtn.innerHTML = '×';
464
- closeBtn.style.cssText = `
465
- position: absolute;
466
- top: 8px;
467
- right: 8px;
468
- width: 24px;
469
- height: 24px;
470
- border: none;
471
- background: rgba(0,0,0,0.1);
472
- color: #666;
473
- border-radius: 50%;
474
- cursor: pointer;
475
- font-size: 16px;
476
- display: flex;
477
- align-items: center;
478
- justify-content: center;
479
- transition: all 0.2s ease;
480
- `;
481
- closeBtn.onclick = (e) => {
482
- e.stopPropagation();
483
- this.close(messageId);
484
- };
485
- closeBtn.onmouseenter = () => {
486
- closeBtn.style.background = 'rgba(0,0,0,0.2)';
487
- };
488
- closeBtn.onmouseleave = () => {
489
- closeBtn.style.background = 'rgba(0,0,0,0.1)';
490
- };
491
- popup.appendChild(closeBtn);
492
- }
493
-
494
- if (this.config.clickable) {
495
- popup.onclick = () => {
496
- if (this.config.onClick) {
497
- this.config.onClick(message, popup);
498
- }
499
- this.close(messageId);
500
- };
501
- }
502
-
503
- return popup;
504
- }
505
-
506
- private createHtmlPopup(
507
- message: HtmlMessage,
508
- messageId: string
509
- ): HTMLElement {
510
- const popup = document.createElement('div');
511
- popup.className = 'web-push-popup-html-container';
512
- popup.dataset.messageId = messageId;
513
- popup.style.cssText = `
514
- pointer-events: auto;
515
- position: relative;
516
- `;
517
-
518
- try {
519
- const base64ToUtf8 = (base64Str: string): string => {
520
- try {
521
- const binaryStr = atob(base64Str);
522
-
523
- const bytes = new Uint8Array(binaryStr.length);
524
- for (let i = 0; i < binaryStr.length; i++) {
525
- bytes[i] = binaryStr.charCodeAt(i);
526
- }
527
-
528
- const decoder = new TextDecoder('utf-8');
529
- return decoder.decode(bytes);
530
- } catch (error) {
531
- logger.warn('UTF-8 解码失败,使用原始 atob 解码:', error);
532
- return atob(base64Str);
533
- }
534
- };
535
-
536
- const decodedHtml = base64ToUtf8(message.MsgContent.Html);
537
-
538
- // 使用iframe来完全隔离HTML内容
539
- const iframe = document.createElement('iframe');
540
- iframe.className = 'web-push-popup-html-iframe';
541
- iframe.style.cssText = `
542
- width: 100%;
543
- min-height: 200px;
544
- max-height: 400px;
545
- border: none;
546
- background: transparent;
547
- overflow: auto;
548
- display: block;
549
- `;
550
-
551
- // 设置iframe安全属性
552
- iframe.setAttribute('sandbox', 'allow-same-origin allow-scripts');
553
- iframe.setAttribute('loading', 'eager');
554
-
555
- popup.appendChild(iframe);
556
-
557
- const messageHandler = (event: MessageEvent) => {
558
- if (event.source === iframe.contentWindow) {
559
- if (event.data && event.data.type === 'iframe-click') {
560
- if (this.config.onClick) {
561
- this.config.onClick(message, popup);
562
- }
563
- this.close(messageId);
564
- } else if (
565
- event.data &&
566
- event.data.type === 'iframe-content-loaded'
567
- ) {
568
- this.adjustIframeHeight(iframe);
569
- }
570
- }
571
- };
572
-
573
- window.addEventListener('message', messageHandler);
574
- this.messageHandlers.set(messageId, messageHandler);
575
-
576
- const writeContent = () => {
577
- try {
578
- const iframeDoc =
579
- iframe.contentDocument || iframe.contentWindow?.document;
580
- if (iframeDoc) {
581
- const fullHtml = this.createIframeHtml(decodedHtml, messageId);
582
- iframeDoc.open();
583
- iframeDoc.write(fullHtml);
584
- iframeDoc.close();
585
-
586
- const heightTimer = setTimeout(() => {
587
- this.adjustIframeHeight(iframe);
588
- this.adjustHeightTimers.delete(messageId);
589
- }, 100);
590
- this.adjustHeightTimers.set(messageId, heightTimer);
591
- }
592
- } catch (error) {
593
- logger.error('Failed to write content to iframe', error);
594
- }
595
- };
596
-
597
- // 等待iframe加载完成后写入内容
598
- iframe.onload = writeContent;
599
-
600
- // 如果iframe没有src,直接写入内容
601
- const writeTimer = setTimeout(() => {
602
- writeContent();
603
- this.adjustHeightTimers.delete(`${messageId}_write`);
604
- }, 0);
605
- this.adjustHeightTimers.set(`${messageId}_write`, writeTimer);
606
- } catch (error) {
607
- logger.error('Failed to decode HTML message', error);
608
-
609
- const errorDiv = document.createElement('div');
610
- errorDiv.className = 'web-push-popup-error';
611
- errorDiv.textContent = '消息内容解析失败';
612
- errorDiv.style.cssText = `
613
- padding: 16px;
614
- color: #ff4444;
615
- text-align: center;
616
- font-size: 14px;
617
- `;
618
- popup.appendChild(errorDiv);
619
- }
620
-
621
- if (this.config.clickable) {
622
- popup.onclick = () => {
623
- if (this.config.onClick) {
624
- this.config.onClick(message, popup);
625
- }
626
- this.close(messageId);
627
- };
628
- }
629
-
630
- return popup;
631
- }
632
-
633
- private addPopup(
634
- messageId: string,
635
- popup: HTMLElement,
636
- messageDuration?: number
637
- ): void {
638
- if (!this.container) return;
639
-
640
- this.popups.set(messageId, popup);
641
- this.container.appendChild(popup);
642
-
643
- // 自动关闭 - 优先使用消息中的duration(秒),然后使用全局配置(毫秒)
644
- const duration =
645
- messageDuration !== undefined
646
- ? messageDuration * 1000 // 消息中的duration是秒,需要转换为毫秒
647
- : this.config.duration; // 全局配置已经是毫秒
648
-
649
- if (duration > 0) {
650
- const timer = setTimeout(() => {
651
- // 定时器自然执行完毕,清理引用
652
- this.timers.delete(messageId);
653
- this.close(messageId);
654
- }, duration);
655
- this.timers.set(messageId, timer);
656
- }
657
- }
658
-
659
- private removeOldestPopup(): void {
660
- const firstKey = this.popups.keys().next().value;
661
- if (firstKey) {
662
- this.close(firstKey);
663
- }
664
- }
665
-
666
- public close(messageId: string): void {
667
- const popup = this.popups.get(messageId);
668
- if (!popup) return;
669
-
670
- try {
671
- // 清理定时器
672
- const timer = this.timers.get(messageId);
673
- if (timer) {
674
- clearTimeout(timer);
675
- this.timers.delete(messageId);
676
- }
677
-
678
- // 清理高度调整定时器
679
- const heightTimer = this.adjustHeightTimers.get(messageId);
680
- if (heightTimer) {
681
- clearTimeout(heightTimer);
682
- this.adjustHeightTimers.delete(messageId);
683
- }
684
-
685
- // 清理写入内容定时器
686
- const writeTimer = this.adjustHeightTimers.get(`${messageId}_write`);
687
- if (writeTimer) {
688
- clearTimeout(writeTimer);
689
- this.adjustHeightTimers.delete(`${messageId}_write`);
690
- }
691
-
692
- // 清理消息监听器
693
- const messageHandler = this.messageHandlers.get(messageId);
694
- if (messageHandler) {
695
- window.removeEventListener('message', messageHandler);
696
- this.messageHandlers.delete(messageId);
697
- }
698
-
699
- // 添加关闭动画
700
- popup.classList.add('web-push-popup-closing');
701
-
702
- // 触发关闭回调
703
- if (this.config.onClose) {
704
- this.config.onClose({ ID: messageId } as any, popup);
705
- }
706
-
707
- // 动画结束后移除元素
708
- const animationTimer = setTimeout(() => {
709
- if (popup.parentNode) {
710
- popup.parentNode.removeChild(popup);
711
- }
712
- this.popups.delete(messageId);
713
- this.animationTimers.delete(messageId);
714
- }, 300);
715
- this.animationTimers.set(messageId, animationTimer);
716
- } catch (error) {
717
- logger.error('Failed to close popup', error);
718
- // 强制移除
719
- if (popup.parentNode) {
720
- popup.parentNode.removeChild(popup);
721
- }
722
- this.popups.delete(messageId);
723
-
724
- // 清理所有相关定时器
725
- const timer = this.timers.get(messageId);
726
- if (timer) {
727
- clearTimeout(timer);
728
- this.timers.delete(messageId);
729
- }
730
-
731
- const heightTimer = this.adjustHeightTimers.get(messageId);
732
- if (heightTimer) {
733
- clearTimeout(heightTimer);
734
- this.adjustHeightTimers.delete(messageId);
735
- }
736
-
737
- const writeTimer = this.adjustHeightTimers.get(`${messageId}_write`);
738
- if (writeTimer) {
739
- clearTimeout(writeTimer);
740
- this.adjustHeightTimers.delete(`${messageId}_write`);
741
- }
742
-
743
- const animationTimer = this.animationTimers.get(messageId);
744
- if (animationTimer) {
745
- clearTimeout(animationTimer);
746
- this.animationTimers.delete(messageId);
747
- }
748
-
749
- // 清理消息监听器
750
- const messageHandler = this.messageHandlers.get(messageId);
751
- if (messageHandler) {
752
- window.removeEventListener('message', messageHandler);
753
- this.messageHandlers.delete(messageId);
754
- }
755
- }
756
- }
757
-
758
- /**
759
- * 检查指定消息是否正在显示弹窗
760
- * @param messageId 消息ID
761
- * @returns 是否正在显示
762
- */
763
- public hasMessage(messageId: string): boolean {
764
- return this.popups.has(messageId);
765
- }
766
-
767
- public closeAll(): void {
768
- for (const messageId of this.popups.keys()) {
769
- this.close(messageId);
770
- }
771
- }
772
-
773
- public updateConfig(newConfig: Partial<PopupConfig>): void {
774
- this.config = { ...this.config, ...newConfig };
775
-
776
- // 如果禁用了弹窗,关闭所有现有弹窗
777
- if (!this.config.enabled) {
778
- this.closeAll();
779
- }
780
- }
781
-
782
- public destroy(): void {
783
- this.closeAll();
784
-
785
- // 清理所有剩余的定时器
786
- this.timers.forEach((timer) => clearTimeout(timer));
787
- this.timers.clear();
788
-
789
- // 清理所有动画定时器
790
- this.animationTimers.forEach((timer) => clearTimeout(timer));
791
- this.animationTimers.clear();
792
-
793
- // 清理所有高度调整定时器
794
- this.adjustHeightTimers.forEach((timer) => clearTimeout(timer));
795
- this.adjustHeightTimers.clear();
796
-
797
- // 清理所有消息监听器
798
- this.messageHandlers.forEach((handler) =>
799
- window.removeEventListener('message', handler)
800
- );
801
- this.messageHandlers.clear();
802
-
803
- if (this.container && this.container.parentNode) {
804
- this.container.parentNode.removeChild(this.container);
805
- }
806
-
807
- const styles = document.getElementById('web-push-popup-styles');
808
- if (styles && styles.parentNode) {
809
- styles.parentNode.removeChild(styles);
810
- }
811
-
812
- this.container = null;
813
- this.popups.clear();
814
- }
815
-
816
- /**
817
- * 根据 Placement 值应用容器位置
818
- * 0-中间,1-左上,2-上中,3-右上,4-右中,5-右下,6-下中,7-左下,8-左中
819
- */
820
- private applyPlacement(placement: number): void {
821
- if (!this.container) return;
822
-
823
- // 移除所有位置类
824
- this.container.classList.remove(
825
- 'web-push-popup-center',
826
- 'web-push-popup-top-left',
827
- 'web-push-popup-top-center',
828
- 'web-push-popup-top-right',
829
- 'web-push-popup-right-center',
830
- 'web-push-popup-bottom-right',
831
- 'web-push-popup-bottom-center',
832
- 'web-push-popup-bottom-left',
833
- 'web-push-popup-left-center'
834
- );
835
-
836
- // 根据 placement 值添加对应的位置类
837
- switch (placement) {
838
- case 0: // 中间
839
- this.container.classList.add('web-push-popup-center');
840
- break;
841
- case 1: // 左上
842
- this.container.classList.add('web-push-popup-top-left');
843
- break;
844
- case 2: // 上中
845
- this.container.classList.add('web-push-popup-top-center');
846
- break;
847
- case 3: // 右上
848
- this.container.classList.add('web-push-popup-top-right');
849
- break;
850
- case 4: // 右中
851
- this.container.classList.add('web-push-popup-right-center');
852
- break;
853
- case 5: // 右下
854
- this.container.classList.add('web-push-popup-bottom-right');
855
- break;
856
- case 6: // 下中
857
- this.container.classList.add('web-push-popup-bottom-center');
858
- break;
859
- case 7: // 左下
860
- this.container.classList.add('web-push-popup-bottom-left');
861
- break;
862
- case 8: // 左中
863
- this.container.classList.add('web-push-popup-left-center');
864
- break;
865
- default: // 默认使用配置中的位置
866
- this.container.classList.add(`web-push-popup-${this.config.position}`);
867
- break;
868
- }
869
- }
870
-
871
- /**
872
- * 调整iframe高度以适应内容
873
- */
874
- private adjustIframeHeight(iframe: HTMLIFrameElement): void {
875
- try {
876
- const iframeDoc =
877
- iframe.contentDocument || iframe.contentWindow?.document;
878
- if (!iframeDoc) return;
879
-
880
- // 获取iframe内容的实际高度
881
- const body = iframeDoc.body;
882
- const html = iframeDoc.documentElement;
883
-
884
- if (body && html) {
885
- const contentHeight = Math.max(
886
- body.scrollHeight,
887
- body.offsetHeight,
888
- html.clientHeight,
889
- html.scrollHeight,
890
- html.offsetHeight
891
- );
892
-
893
- // 设置合理的高度范围
894
- const minHeight = 100;
895
- const maxHeight = 400;
896
- const finalHeight = Math.min(
897
- Math.max(contentHeight, minHeight),
898
- maxHeight
899
- );
900
-
901
- iframe.style.height = `${finalHeight}px`;
902
-
903
- // 如果内容超过最大高度,启用滚动
904
- if (contentHeight > maxHeight) {
905
- iframe.style.overflow = 'auto';
906
- // 在iframe内部也启用滚动
907
- body.style.overflow = 'auto';
908
- } else {
909
- iframe.style.overflow = 'hidden';
910
- body.style.overflow = 'hidden';
911
- }
912
- }
913
- } catch (error) {
914
- logger.warn('Failed to adjust iframe height', error);
915
- // 如果调整失败,使用默认高度
916
- iframe.style.height = '200px';
917
- iframe.style.overflow = 'auto';
918
- }
919
- }
920
-
921
- /**
922
- * 创建iframe的完整HTML文档
923
- */
924
- private createIframeHtml(bodyContent: string, messageId?: string): string {
925
- return `
926
- <!DOCTYPE html>
927
- <html>
928
- <head>
929
- <meta charset="utf-8">
930
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
931
- <style>
932
- * {
933
- box-sizing: border-box;
934
- }
935
- html, body {
936
- margin: 0;
937
- padding: 8px;
938
- width: 100%;
939
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
940
- font-size: 14px;
941
- line-height: 1.4;
942
- cursor: pointer;
943
- }
944
- body {
945
- min-height: auto;
946
- overflow-wrap: break-word;
947
- word-wrap: break-word;
948
- }
949
- img {
950
- max-width: 100%;
951
- height: auto;
952
- }
953
- a {
954
- color: #1976d2;
955
- text-decoration: none;
956
- }
957
- a:hover {
958
- text-decoration: underline;
959
- }
960
- </style>
961
- </head>
962
- <body>
963
- ${bodyContent}
964
- <script>
965
- // 确保所有链接在新窗口打开
966
- document.addEventListener('DOMContentLoaded', function() {
967
- const links = document.querySelectorAll('a');
968
- links.forEach(function(link) {
969
- link.target = '_blank';
970
- link.rel = 'noopener noreferrer';
971
- });
972
-
973
- // 添加点击事件监听器
974
- ${
975
- messageId
976
- ? `
977
- document.addEventListener('click', function(e) {
978
- window.parent.postMessage({
979
- type: 'iframe-click',
980
- messageId: '${messageId}'
981
- }, '*');
982
- });
983
- `
984
- : ''
985
- }
986
-
987
- // 通知父页面内容已加载,可以调整高度
988
- var contentLoadedTimer = setTimeout(function() {
989
- if (window.parent && window.parent !== window) {
990
- try {
991
- window.parent.postMessage({
992
- type: 'iframe-content-loaded',
993
- height: document.body.scrollHeight
994
- }, '*');
995
- } catch (e) {
996
- // 忽略跨域错误
997
- }
998
- }
999
- }, 100);
1000
- });
1001
-
1002
- // 注意:不要重新定义 window.parent,否则会阻止与父页面的通信
1003
- </script>
1004
- </body>
1005
- </html>`;
1006
- }
1007
- }