customer-chat-sdk 1.1.10 → 1.1.12
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/dist/core/IconManager.d.ts +21 -0
- package/dist/core/IconManager.d.ts.map +1 -1
- package/dist/customer-sdk.cjs.js +276 -17
- package/dist/customer-sdk.esm.js +276 -17
- package/dist/customer-sdk.min.js +2 -2
- package/package.json +1 -1
package/dist/customer-sdk.esm.js
CHANGED
|
@@ -18,6 +18,22 @@ class IconManager {
|
|
|
18
18
|
this.lastTouchPosition = { x: 0, y: 0 }; // 最后触摸位置
|
|
19
19
|
this.touchStartTime = 0; // 触摸开始时间
|
|
20
20
|
this.clickThreshold = 15; // 点击阈值(像素)
|
|
21
|
+
// 性能优化:缓存容器信息,避免频繁查询 DOM
|
|
22
|
+
this.cachedContainer = null;
|
|
23
|
+
this.cachedContainerRect = null;
|
|
24
|
+
this.cachedIconSize = { width: 0, height: 0 };
|
|
25
|
+
this.lastContainerUpdateTime = 0;
|
|
26
|
+
this.containerUpdateInterval = 100; // 每 100ms 更新一次容器信息(避免频繁重排)
|
|
27
|
+
// 性能优化:缓存所有 iframe 的位置信息(用于检测鼠标是否在 iframe 上)
|
|
28
|
+
this.cachedIframes = [];
|
|
29
|
+
this.lastIframeUpdateTime = 0;
|
|
30
|
+
this.iframeUpdateInterval = 16; // 每 16ms 更新一次 iframe 位置信息(约一帧,更频繁,因为 iframe 可能移动)
|
|
31
|
+
// 性能优化:使用 requestAnimationFrame 节流位置更新
|
|
32
|
+
this.rafId = null;
|
|
33
|
+
this.pendingPosition = { x: 0, y: 0, needsUpdate: false };
|
|
34
|
+
// 主动检测机制:使用 requestAnimationFrame 定期检查鼠标位置(即使没有 mousemove 事件)
|
|
35
|
+
this.activeDetectionRafId = null;
|
|
36
|
+
this.lastMousePosition = { x: 0, y: 0, timestamp: 0 }; // 最后记录的鼠标位置和时间戳
|
|
21
37
|
// 事件处理器引用(用于清理)
|
|
22
38
|
this.onDragHandler = null;
|
|
23
39
|
this.stopDragHandler = null;
|
|
@@ -317,6 +333,26 @@ class IconManager {
|
|
|
317
333
|
this.dragOffset.x = clientX - iconRect.left;
|
|
318
334
|
this.dragOffset.y = clientY - iconRect.top;
|
|
319
335
|
// 注意:不在这里转换位置,只在真正开始拖动时才转换(在 onDrag 中)
|
|
336
|
+
// 性能优化:在拖动开始时预加载所有 iframe 位置信息
|
|
337
|
+
// 这样可以避免在拖动过程中频繁查询 DOM
|
|
338
|
+
const allIframes = document.querySelectorAll('iframe');
|
|
339
|
+
this.cachedIframes = Array.from(allIframes).map(iframe => ({
|
|
340
|
+
element: iframe,
|
|
341
|
+
rect: iframe.getBoundingClientRect()
|
|
342
|
+
}));
|
|
343
|
+
this.lastIframeUpdateTime = Date.now();
|
|
344
|
+
if (this.debug) {
|
|
345
|
+
console.log(`[IconManager] Drag start - Found ${this.cachedIframes.length} iframe(s)`, {
|
|
346
|
+
iframes: this.cachedIframes.map(({ rect }) => ({
|
|
347
|
+
left: rect.left,
|
|
348
|
+
top: rect.top,
|
|
349
|
+
right: rect.right,
|
|
350
|
+
bottom: rect.bottom,
|
|
351
|
+
width: rect.width,
|
|
352
|
+
height: rect.height
|
|
353
|
+
}))
|
|
354
|
+
});
|
|
355
|
+
}
|
|
320
356
|
// 添加 document 事件监听器
|
|
321
357
|
if (this.onDragHandler) {
|
|
322
358
|
document.addEventListener('mousemove', this.onDragHandler);
|
|
@@ -359,7 +395,7 @@ class IconManager {
|
|
|
359
395
|
};
|
|
360
396
|
window.addEventListener('blur', this.blurHandler);
|
|
361
397
|
// 3. 添加超时机制(如果一段时间没有收到 mousemove 事件,自动停止拖动)
|
|
362
|
-
//
|
|
398
|
+
// 优化:缩短超时时间到 50ms,更快检测到事件丢失(特别是嵌套 iframe 场景)
|
|
363
399
|
this.dragTimeoutId = window.setTimeout(() => {
|
|
364
400
|
if (this.isDragging) {
|
|
365
401
|
if (this.debug) {
|
|
@@ -367,9 +403,21 @@ class IconManager {
|
|
|
367
403
|
}
|
|
368
404
|
this.stopDrag();
|
|
369
405
|
}
|
|
370
|
-
},
|
|
406
|
+
}, 50); // 缩短到 50ms,更快检测到事件丢失(特别是嵌套 iframe 场景)
|
|
407
|
+
// 4. 启动主动检测机制:使用 requestAnimationFrame 定期检查鼠标位置
|
|
408
|
+
// 即使没有 mousemove 事件,也能检测到鼠标是否进入 iframe
|
|
409
|
+
this.startActiveDetection();
|
|
371
410
|
if (this.debug) {
|
|
372
|
-
console.log('Drag start'
|
|
411
|
+
console.log('[IconManager] Drag start', {
|
|
412
|
+
startPosition: { x: clientX, y: clientY },
|
|
413
|
+
iconRect: {
|
|
414
|
+
left: iconRect.left,
|
|
415
|
+
top: iconRect.top,
|
|
416
|
+
width: iconRect.width,
|
|
417
|
+
height: iconRect.height
|
|
418
|
+
},
|
|
419
|
+
dragOffset: this.dragOffset
|
|
420
|
+
});
|
|
373
421
|
}
|
|
374
422
|
}
|
|
375
423
|
catch (error) {
|
|
@@ -385,20 +433,92 @@ class IconManager {
|
|
|
385
433
|
if (!this.iconElement)
|
|
386
434
|
return;
|
|
387
435
|
e.preventDefault();
|
|
436
|
+
const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
|
|
437
|
+
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
|
|
438
|
+
// 检测鼠标是否在任何 iframe 上(通过坐标判断)
|
|
439
|
+
// 如果检测到鼠标在 iframe 区域,立即停止拖动
|
|
440
|
+
// 重要:需要检测所有 iframe(包括嵌套的),因为任何 iframe 都会导致事件丢失
|
|
441
|
+
if (this.isDragging) {
|
|
442
|
+
const now = Date.now();
|
|
443
|
+
// 性能优化:缓存 iframe 位置信息,避免频繁查询 DOM
|
|
444
|
+
if (this.cachedIframes.length === 0 ||
|
|
445
|
+
now - this.lastIframeUpdateTime > this.iframeUpdateInterval) {
|
|
446
|
+
// 更新 iframe 缓存
|
|
447
|
+
const allIframes = document.querySelectorAll('iframe');
|
|
448
|
+
const previousCount = this.cachedIframes.length;
|
|
449
|
+
this.cachedIframes = Array.from(allIframes).map(iframe => ({
|
|
450
|
+
element: iframe,
|
|
451
|
+
rect: iframe.getBoundingClientRect()
|
|
452
|
+
}));
|
|
453
|
+
this.lastIframeUpdateTime = now;
|
|
454
|
+
if (this.debug && this.cachedIframes.length !== previousCount) {
|
|
455
|
+
console.log(`[IconManager] Iframe cache updated - Found ${this.cachedIframes.length} iframe(s)`, {
|
|
456
|
+
iframes: this.cachedIframes.map(({ rect }) => ({
|
|
457
|
+
left: rect.left,
|
|
458
|
+
top: rect.top,
|
|
459
|
+
right: rect.right,
|
|
460
|
+
bottom: rect.bottom
|
|
461
|
+
}))
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
// 检查鼠标是否在任何 iframe 上
|
|
466
|
+
for (const { rect, element } of this.cachedIframes) {
|
|
467
|
+
if (clientX >= rect.left &&
|
|
468
|
+
clientX <= rect.right &&
|
|
469
|
+
clientY >= rect.top &&
|
|
470
|
+
clientY <= rect.bottom) {
|
|
471
|
+
// 鼠标在 iframe 上,立即停止拖动
|
|
472
|
+
if (this.debug) {
|
|
473
|
+
console.log('[IconManager] Mouse over iframe detected, stopping drag immediately', {
|
|
474
|
+
mousePosition: { x: clientX, y: clientY },
|
|
475
|
+
iframeRect: {
|
|
476
|
+
left: rect.left,
|
|
477
|
+
top: rect.top,
|
|
478
|
+
right: rect.right,
|
|
479
|
+
bottom: rect.bottom
|
|
480
|
+
},
|
|
481
|
+
iframeSrc: element.src || 'no src'
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
this.stopDrag();
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// 更新最后记录的鼠标位置和时间戳(用于主动检测)
|
|
490
|
+
const now = Date.now();
|
|
491
|
+
const timeSinceLastUpdate = now - this.lastMousePosition.timestamp;
|
|
492
|
+
this.lastMousePosition = {
|
|
493
|
+
x: clientX,
|
|
494
|
+
y: clientY,
|
|
495
|
+
timestamp: now
|
|
496
|
+
};
|
|
497
|
+
// 如果距离上次更新超过 50ms,记录警告(可能事件丢失)
|
|
498
|
+
if (this.debug && timeSinceLastUpdate > 50 && this.lastMousePosition.timestamp > 0) {
|
|
499
|
+
console.warn(`[IconManager] Long gap between mousemove events: ${timeSinceLastUpdate}ms`, {
|
|
500
|
+
lastPosition: { x: this.lastMousePosition.x, y: this.lastMousePosition.y },
|
|
501
|
+
currentPosition: { x: clientX, y: clientY }
|
|
502
|
+
});
|
|
503
|
+
}
|
|
388
504
|
// 重置超时定时器(每次移动都重置,确保只有真正停止移动时才触发超时)
|
|
505
|
+
// 优化:缩短超时时间到 50ms,更快检测到事件丢失(特别是嵌套 iframe 场景)
|
|
389
506
|
if (this.dragTimeoutId !== null) {
|
|
390
507
|
window.clearTimeout(this.dragTimeoutId);
|
|
391
508
|
this.dragTimeoutId = window.setTimeout(() => {
|
|
392
509
|
if (this.isDragging) {
|
|
393
510
|
if (this.debug) {
|
|
394
|
-
|
|
511
|
+
const timeSinceLastMove = Date.now() - this.lastMousePosition.timestamp;
|
|
512
|
+
console.warn('[IconManager] Drag timeout triggered, stopping drag (likely mouse moved over iframe)', {
|
|
513
|
+
timeSinceLastMove,
|
|
514
|
+
lastMousePosition: this.lastMousePosition,
|
|
515
|
+
iframeCount: this.cachedIframes.length
|
|
516
|
+
});
|
|
395
517
|
}
|
|
396
518
|
this.stopDrag();
|
|
397
519
|
}
|
|
398
|
-
},
|
|
520
|
+
}, 50); // 缩短到 50ms,更快检测到事件丢失(特别是嵌套 iframe 场景)
|
|
399
521
|
}
|
|
400
|
-
const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
|
|
401
|
-
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
|
|
402
522
|
// 检查是否有足够的移动距离
|
|
403
523
|
const deltaX = Math.abs(clientX - this.lastTouchPosition.x);
|
|
404
524
|
const deltaY = Math.abs(clientY - this.lastTouchPosition.y);
|
|
@@ -442,17 +562,31 @@ class IconManager {
|
|
|
442
562
|
return;
|
|
443
563
|
}
|
|
444
564
|
try {
|
|
565
|
+
// 性能优化:缓存容器信息,避免频繁查询 DOM
|
|
566
|
+
const now = Date.now();
|
|
445
567
|
const container = this.getTargetElement();
|
|
446
568
|
if (!container) {
|
|
447
569
|
return;
|
|
448
570
|
}
|
|
449
|
-
|
|
571
|
+
// 只在必要时更新容器信息(避免频繁重排)
|
|
572
|
+
if (!this.cachedContainer ||
|
|
573
|
+
this.cachedContainer !== container ||
|
|
574
|
+
now - this.lastContainerUpdateTime > this.containerUpdateInterval) {
|
|
575
|
+
this.cachedContainer = container;
|
|
576
|
+
this.cachedContainerRect = container.getBoundingClientRect();
|
|
577
|
+
this.cachedIconSize = {
|
|
578
|
+
width: this.iconElement.offsetWidth,
|
|
579
|
+
height: this.iconElement.offsetHeight
|
|
580
|
+
};
|
|
581
|
+
this.lastContainerUpdateTime = now;
|
|
582
|
+
}
|
|
583
|
+
const containerRect = this.cachedContainerRect;
|
|
584
|
+
const iconWidth = this.cachedIconSize.width;
|
|
585
|
+
const iconHeight = this.cachedIconSize.height;
|
|
450
586
|
// 计算新位置
|
|
451
587
|
let newX = clientX - this.dragOffset.x - containerRect.left;
|
|
452
588
|
let newY = clientY - this.dragOffset.y - containerRect.top;
|
|
453
589
|
// 限制在容器内
|
|
454
|
-
const iconWidth = this.iconElement.offsetWidth;
|
|
455
|
-
const iconHeight = this.iconElement.offsetHeight;
|
|
456
590
|
if (container === document.body) {
|
|
457
591
|
// 限制在视口内
|
|
458
592
|
newX = Math.max(0, Math.min(newX, window.innerWidth - iconWidth));
|
|
@@ -465,11 +599,22 @@ class IconManager {
|
|
|
465
599
|
newX = Math.max(0, Math.min(newX, containerWidth - iconWidth));
|
|
466
600
|
newY = Math.max(0, Math.min(newY, containerHeight - iconHeight));
|
|
467
601
|
}
|
|
468
|
-
//
|
|
469
|
-
this.
|
|
470
|
-
this.
|
|
471
|
-
this.
|
|
472
|
-
this.
|
|
602
|
+
// 性能优化:使用 requestAnimationFrame 节流位置更新
|
|
603
|
+
this.pendingPosition.x = newX;
|
|
604
|
+
this.pendingPosition.y = newY;
|
|
605
|
+
this.pendingPosition.needsUpdate = true;
|
|
606
|
+
if (this.rafId === null) {
|
|
607
|
+
this.rafId = requestAnimationFrame(() => {
|
|
608
|
+
this.rafId = null;
|
|
609
|
+
if (this.pendingPosition.needsUpdate && this.iconElement && this.isDragging) {
|
|
610
|
+
this.iconElement.style.left = `${this.pendingPosition.x}px`;
|
|
611
|
+
this.iconElement.style.top = `${this.pendingPosition.y}px`;
|
|
612
|
+
this.iconElement.style.right = 'auto';
|
|
613
|
+
this.iconElement.style.bottom = 'auto';
|
|
614
|
+
this.pendingPosition.needsUpdate = false;
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
}
|
|
473
618
|
// 更新最后位置
|
|
474
619
|
this.lastTouchPosition.x = clientX;
|
|
475
620
|
this.lastTouchPosition.y = clientY;
|
|
@@ -484,6 +629,14 @@ class IconManager {
|
|
|
484
629
|
* 停止拖动
|
|
485
630
|
*/
|
|
486
631
|
stopDrag(_e) {
|
|
632
|
+
const stopReason = this.isDragging ? 'drag_stopped' : 'not_dragging';
|
|
633
|
+
const finalPosition = this.iconElement ? {
|
|
634
|
+
left: this.iconElement.style.left,
|
|
635
|
+
top: this.iconElement.style.top,
|
|
636
|
+
computed: window.getComputedStyle(this.iconElement).left !== 'auto'
|
|
637
|
+
? { left: window.getComputedStyle(this.iconElement).left, top: window.getComputedStyle(this.iconElement).top }
|
|
638
|
+
: null
|
|
639
|
+
} : null;
|
|
487
640
|
this.cleanupDragEvents();
|
|
488
641
|
if (!this.iconElement)
|
|
489
642
|
return;
|
|
@@ -494,11 +647,14 @@ class IconManager {
|
|
|
494
647
|
const touchDuration = Date.now() - this.touchStartTime;
|
|
495
648
|
const isValidClick = !this.hasMoved && touchDuration < 1000 && !this.dragStarted;
|
|
496
649
|
if (this.debug) {
|
|
497
|
-
console.log('Drag end', {
|
|
650
|
+
console.log('[IconManager] Drag end', {
|
|
651
|
+
stopReason,
|
|
498
652
|
hasMoved: this.hasMoved,
|
|
499
653
|
dragStarted: this.dragStarted,
|
|
500
654
|
touchDuration,
|
|
501
|
-
isValidClick
|
|
655
|
+
isValidClick,
|
|
656
|
+
finalPosition,
|
|
657
|
+
lastMousePosition: this.lastMousePosition
|
|
502
658
|
});
|
|
503
659
|
}
|
|
504
660
|
// 先保存点击状态,然后重置拖动状态
|
|
@@ -538,6 +694,97 @@ class IconManager {
|
|
|
538
694
|
}, 100); // 增加延迟到 100ms,确保拖动状态完全重置
|
|
539
695
|
}
|
|
540
696
|
}
|
|
697
|
+
/**
|
|
698
|
+
* 启动主动检测机制:使用 requestAnimationFrame 定期检查鼠标位置
|
|
699
|
+
* 即使没有 mousemove 事件,也能检测到鼠标是否进入 iframe
|
|
700
|
+
*/
|
|
701
|
+
startActiveDetection() {
|
|
702
|
+
if (this.activeDetectionRafId !== null) {
|
|
703
|
+
if (this.debug) {
|
|
704
|
+
console.log('[IconManager] Active detection already running');
|
|
705
|
+
}
|
|
706
|
+
return; // 已经在运行
|
|
707
|
+
}
|
|
708
|
+
if (this.debug) {
|
|
709
|
+
console.log('[IconManager] Starting active detection mechanism');
|
|
710
|
+
}
|
|
711
|
+
const checkMousePosition = () => {
|
|
712
|
+
if (!this.isDragging) {
|
|
713
|
+
if (this.debug) {
|
|
714
|
+
console.log('[IconManager] Active detection stopped (drag ended)');
|
|
715
|
+
}
|
|
716
|
+
this.activeDetectionRafId = null;
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
// 检查是否长时间没有收到 mousemove 事件(超过 50ms)
|
|
720
|
+
const now = Date.now();
|
|
721
|
+
const timeSinceLastMove = now - this.lastMousePosition.timestamp;
|
|
722
|
+
if (timeSinceLastMove > 50) {
|
|
723
|
+
// 长时间没有收到事件,可能鼠标已经进入 iframe
|
|
724
|
+
// 使用最后记录的鼠标位置进行检测
|
|
725
|
+
const clientX = this.lastMousePosition.x;
|
|
726
|
+
const clientY = this.lastMousePosition.y;
|
|
727
|
+
if (this.debug) {
|
|
728
|
+
console.log(`[IconManager] Active detection: No mousemove event for ${timeSinceLastMove}ms`, {
|
|
729
|
+
lastMousePosition: { x: clientX, y: clientY },
|
|
730
|
+
timestamp: this.lastMousePosition.timestamp
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
// 更新 iframe 缓存(更频繁,每帧更新)
|
|
734
|
+
if (this.cachedIframes.length === 0 ||
|
|
735
|
+
now - this.lastIframeUpdateTime > this.iframeUpdateInterval) {
|
|
736
|
+
const allIframes = document.querySelectorAll('iframe');
|
|
737
|
+
this.cachedIframes = Array.from(allIframes).map(iframe => ({
|
|
738
|
+
element: iframe,
|
|
739
|
+
rect: iframe.getBoundingClientRect()
|
|
740
|
+
}));
|
|
741
|
+
this.lastIframeUpdateTime = now;
|
|
742
|
+
if (this.debug) {
|
|
743
|
+
console.log(`[IconManager] Active detection: Updated iframe cache (${this.cachedIframes.length} iframes)`);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
// 检查最后记录的鼠标位置是否在任何 iframe 上
|
|
747
|
+
for (const { rect, element } of this.cachedIframes) {
|
|
748
|
+
if (clientX >= rect.left &&
|
|
749
|
+
clientX <= rect.right &&
|
|
750
|
+
clientY >= rect.top &&
|
|
751
|
+
clientY <= rect.bottom) {
|
|
752
|
+
// 鼠标在 iframe 上,立即停止拖动
|
|
753
|
+
if (this.debug) {
|
|
754
|
+
console.log('[IconManager] Active detection: Mouse over iframe detected, stopping drag immediately', {
|
|
755
|
+
mousePosition: { x: clientX, y: clientY },
|
|
756
|
+
iframeRect: {
|
|
757
|
+
left: rect.left,
|
|
758
|
+
top: rect.top,
|
|
759
|
+
right: rect.right,
|
|
760
|
+
bottom: rect.bottom
|
|
761
|
+
},
|
|
762
|
+
iframeSrc: element.src || 'no src',
|
|
763
|
+
timeSinceLastMove
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
this.stopDrag();
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
// 继续检测
|
|
772
|
+
this.activeDetectionRafId = requestAnimationFrame(checkMousePosition);
|
|
773
|
+
};
|
|
774
|
+
this.activeDetectionRafId = requestAnimationFrame(checkMousePosition);
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* 停止主动检测机制
|
|
778
|
+
*/
|
|
779
|
+
stopActiveDetection() {
|
|
780
|
+
if (this.activeDetectionRafId !== null) {
|
|
781
|
+
if (this.debug) {
|
|
782
|
+
console.log('[IconManager] Stopping active detection mechanism');
|
|
783
|
+
}
|
|
784
|
+
cancelAnimationFrame(this.activeDetectionRafId);
|
|
785
|
+
this.activeDetectionRafId = null;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
541
788
|
/**
|
|
542
789
|
* 清理拖动事件
|
|
543
790
|
*/
|
|
@@ -569,6 +816,18 @@ class IconManager {
|
|
|
569
816
|
window.clearTimeout(this.dragTimeoutId);
|
|
570
817
|
this.dragTimeoutId = null;
|
|
571
818
|
}
|
|
819
|
+
// 清理 requestAnimationFrame
|
|
820
|
+
if (this.rafId !== null) {
|
|
821
|
+
cancelAnimationFrame(this.rafId);
|
|
822
|
+
this.rafId = null;
|
|
823
|
+
}
|
|
824
|
+
// 清理主动检测机制
|
|
825
|
+
this.stopActiveDetection();
|
|
826
|
+
// 清理缓存
|
|
827
|
+
this.cachedContainer = null;
|
|
828
|
+
this.cachedContainerRect = null;
|
|
829
|
+
this.cachedIframes = []; // 清理 iframe 缓存
|
|
830
|
+
this.pendingPosition.needsUpdate = false;
|
|
572
831
|
}
|
|
573
832
|
/**
|
|
574
833
|
* 处理点击事件
|