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.
@@ -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
- // 这可以处理鼠标移动到 iframe 上的情况
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
- }, 500); // 500ms 没有移动事件,自动停止拖动(处理鼠标移动到 iframe 上的情况)
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
- console.log('Drag timeout, stopping drag (likely mouse moved over iframe)');
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
- }, 500); // 500ms 没有移动事件,自动停止拖动
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
- const containerRect = container.getBoundingClientRect();
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.iconElement.style.left = `${newX}px`;
470
- this.iconElement.style.top = `${newY}px`;
471
- this.iconElement.style.right = 'auto';
472
- this.iconElement.style.bottom = 'auto';
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
  * 处理点击事件