customer-chat-sdk 1.1.7 → 1.1.9

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.
@@ -10,20 +10,26 @@ class IconManager {
10
10
  this.badgeElement = null;
11
11
  this.onClickCallback = null;
12
12
  this.notificationCallback = null;
13
- this.isDragging = false;
14
- this.dragStartX = 0;
15
- this.dragStartY = 0;
16
- this.iconStartX = 0;
17
- this.iconStartY = 0;
18
- this.dragMoveHandler = null;
19
- this.dragEndHandler = null;
20
- this.checkDragHandler = null; // 临时拖动检测监听器
21
- this.dragStartHandler = null; // 拖动开始事件监听器
22
- this.touchStartHandler = null; // 触摸开始事件监听器
23
13
  this.iconPosition = null; // 图标位置配置
24
14
  this.debug = false; // debug 模式标志
25
15
  this.isClickEnabled = true; // 是否允许点击(iframe 打开时禁用)
26
16
  this.target = null; // 图标传送目标元素(可以是 HTMLElement 或选择器字符串)
17
+ // 拖动相关状态
18
+ this.isDragging = false;
19
+ this.dragStarted = false; // 是否真正开始了拖拽
20
+ this.hasMoved = false; // 是否移动过
21
+ this.dragOffset = { x: 0, y: 0 }; // 拖动偏移量
22
+ this.lastTouchPosition = { x: 0, y: 0 }; // 最后触摸位置
23
+ this.touchStartTime = 0; // 触摸开始时间
24
+ this.clickThreshold = 15; // 点击阈值(像素)
25
+ // 事件处理器引用(用于清理)
26
+ this.onDragHandler = null;
27
+ this.stopDragHandler = null;
28
+ this.startDragHandler = null;
29
+ this.mouseLeaveHandler = null;
30
+ this.pointerLeaveHandler = null;
31
+ this.blurHandler = null;
32
+ this.dragTimeoutId = null; // 拖动超时定时器
27
33
  this.iconPosition = position || null;
28
34
  this.debug = debug;
29
35
  // 保存 target(可以是 HTMLElement 或字符串选择器)
@@ -124,8 +130,6 @@ class IconManager {
124
130
  });
125
131
  imgContainer.appendChild(iconImg);
126
132
  this.iconElement.appendChild(imgContainer);
127
- // 添加拖动和点击事件
128
- this.setupDragEvents();
129
133
  // 添加到目标元素(如果 target 是字符串,需要重新查找,因为可能在初始化时元素还不存在)
130
134
  const targetElement = this.getTargetElement();
131
135
  if (targetElement) {
@@ -138,6 +142,8 @@ class IconManager {
138
142
  console.warn('Target element not found, icon added to document.body');
139
143
  }
140
144
  }
145
+ // 设置拖动事件
146
+ this.setupDragEvents();
141
147
  if (this.debug) {
142
148
  console.log('CustomerSDK icon displayed');
143
149
  }
@@ -146,49 +152,36 @@ class IconManager {
146
152
  * 强制清理所有拖动事件监听器
147
153
  */
148
154
  forceCleanupDragEvents() {
149
- // 强制清理document上的所有事件监听器
150
- if (this.dragMoveHandler) {
151
- try {
152
- document.removeEventListener('mousemove', this.dragMoveHandler);
153
- document.removeEventListener('touchmove', this.dragMoveHandler);
154
- }
155
- catch (e) {
156
- if (this.debug) {
157
- console.warn('Error removing drag move listeners:', e);
158
- }
159
- }
160
- }
161
- if (this.dragEndHandler) {
162
- try {
163
- document.removeEventListener('mouseup', this.dragEndHandler);
164
- document.removeEventListener('touchend', this.dragEndHandler);
165
- }
166
- catch (e) {
167
- if (this.debug) {
168
- console.warn('Error removing drag end listeners:', e);
169
- }
170
- }
171
- }
172
- // 重置拖动状态
173
- this.isDragging = false;
174
- // 恢复图标样式
175
- if (this.iconElement) {
176
- this.iconElement.style.transition = 'transform 0.2s ease';
177
- this.iconElement.style.cursor = 'pointer';
178
- }
155
+ this.cleanupDragEvents();
179
156
  }
180
157
  /**
181
158
  * 隐藏悬浮图标
182
159
  */
183
160
  hide() {
184
- // 清理所有事件监听器
185
- this.forceCleanupDragEvents();
161
+ // 清理拖动事件
162
+ this.cleanupDragEvents();
186
163
  if (this.iconElement) {
164
+ // 在隐藏前保存当前位置(如果图标已经被拖动过)
165
+ const computedStyle = window.getComputedStyle(this.iconElement);
166
+ const left = computedStyle.left;
167
+ const top = computedStyle.top;
168
+ // 如果 left/top 是有效的像素值(不是 auto),保存到 iconPosition
169
+ if (left !== 'auto' && top !== 'auto' && left !== '' && top !== '') {
170
+ const leftValue = parseFloat(left);
171
+ const topValue = parseFloat(top);
172
+ if (!isNaN(leftValue) && !isNaN(topValue)) {
173
+ this.iconPosition = {
174
+ x: leftValue,
175
+ y: topValue
176
+ };
177
+ if (this.debug) {
178
+ console.log('Icon position saved before hide:', this.iconPosition);
179
+ }
180
+ }
181
+ }
187
182
  this.iconElement.remove();
188
183
  this.iconElement = null;
189
184
  // 注意:不清空 onClickCallback,以便再次显示时能继续使用
190
- this.dragMoveHandler = null;
191
- this.dragEndHandler = null;
192
185
  if (this.debug) {
193
186
  console.log('CustomerSDK icon hidden');
194
187
  }
@@ -219,10 +212,7 @@ class IconManager {
219
212
  : position.y;
220
213
  this.iconElement.style.bottom = 'auto';
221
214
  }
222
- // 保存当前位置用于拖动
223
- const rect = this.iconElement.getBoundingClientRect();
224
- this.iconStartX = rect.left;
225
- this.iconStartY = rect.top;
215
+ // 保存当前位置用于拖动(使用 data-x 和 data-y,由 interact.js 管理)
226
216
  }
227
217
  }
228
218
  /**
@@ -279,186 +269,310 @@ class IconManager {
279
269
  }
280
270
  }
281
271
  /**
282
- * 设置拖动和点击事件
272
+ * 设置拖动事件(使用原生事件处理,参考用户提供的代码)
283
273
  */
284
274
  setupDragEvents() {
285
275
  if (!this.iconElement)
286
276
  return;
287
277
  // 绑定事件处理器(用于后续清理)
288
- this.dragMoveHandler = this.handleDragMove.bind(this);
289
- this.dragEndHandler = this.handleDragEnd.bind(this);
290
- this.dragStartHandler = this.handleDragStart.bind(this);
291
- // 只在图标上监听开始事件(保存引用以便后续移除)
292
- this.iconElement.addEventListener('mousedown', this.dragStartHandler);
293
- this.touchStartHandler = this.handleDragStart.bind(this);
294
- this.iconElement.addEventListener('touchstart', this.touchStartHandler, { passive: false });
278
+ this.startDragHandler = this.startDrag.bind(this);
279
+ this.onDragHandler = this.onDrag.bind(this);
280
+ this.stopDragHandler = this.stopDrag.bind(this);
281
+ // 添加事件监听器
282
+ this.iconElement.addEventListener('mousedown', this.startDragHandler);
283
+ this.iconElement.addEventListener('touchstart', this.startDragHandler, { passive: false });
295
284
  }
296
285
  /**
297
286
  * 开始拖动
298
287
  */
299
- handleDragStart(e) {
300
- if (!this.iconElement)
288
+ startDrag(e) {
289
+ if (!this.iconElement || !this.isClickEnabled)
301
290
  return;
302
- // 只处理图标上的事件
303
- if (e.target !== this.iconElement && !this.iconElement.contains(e.target)) {
291
+ // 检查事件目标:如果是 iframe 相关元素,不处理
292
+ const target = e.target;
293
+ if (target && (target.tagName === 'IFRAME' ||
294
+ target.closest('iframe') ||
295
+ target.closest('.customer-sdk-container') ||
296
+ target.closest('.customer-sdk-overlay'))) {
304
297
  return;
305
298
  }
306
299
  e.preventDefault();
307
300
  e.stopPropagation();
301
+ // 重置状态
302
+ this.hasMoved = false;
303
+ this.dragStarted = false;
304
+ this.isDragging = false;
305
+ // 记录开始时间和位置
306
+ this.touchStartTime = Date.now();
308
307
  const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
309
308
  const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
310
- this.isDragging = false;
311
- this.dragStartX = clientX;
312
- this.dragStartY = clientY;
313
- // 获取图标当前位置
314
- const rect = this.iconElement.getBoundingClientRect();
315
- this.iconStartX = rect.left;
316
- this.iconStartY = rect.top;
317
- // 注意:不要在这里立即移除 transition 和设置 cursor
318
- // 只有在真正开始拖动时才修改样式,避免点击时图标位置跳动
319
- // 只在真正开始拖动时添加document事件监听
320
- // 先添加一个临时的move监听器来检测是否真的在拖动
321
- const checkDrag = (moveEvent) => {
322
- // 检测事件目标:如果事件发生在iframe或其他元素上,停止检测拖动
323
- const target = moveEvent.target;
324
- if (target && target !== this.iconElement && !this.iconElement?.contains(target)) {
325
- // 检查是否是iframe相关元素
326
- const isIframeElement = target.tagName === 'IFRAME' ||
327
- target.closest('iframe') ||
328
- target.closest('.customer-sdk-container') ||
329
- target.closest('.customer-sdk-overlay');
330
- if (isIframeElement) {
331
- // 如果事件发生在iframe相关元素上,停止检测并清理监听器
332
- if (this.checkDragHandler) {
333
- if ('touches' in moveEvent) {
334
- document.removeEventListener('touchmove', this.checkDragHandler);
335
- }
336
- else {
337
- document.removeEventListener('mousemove', this.checkDragHandler);
338
- }
339
- this.checkDragHandler = null;
340
- }
341
- return;
309
+ this.lastTouchPosition.x = clientX;
310
+ this.lastTouchPosition.y = clientY;
311
+ try {
312
+ const iconRect = this.iconElement.getBoundingClientRect();
313
+ const container = this.getTargetElement();
314
+ if (!container) {
315
+ if (this.debug) {
316
+ console.warn('Container not found');
342
317
  }
318
+ return;
343
319
  }
344
- const moveX = 'touches' in moveEvent ? moveEvent.touches[0].clientX : moveEvent.clientX;
345
- const moveY = 'touches' in moveEvent ? moveEvent.touches[0].clientY : moveEvent.clientY;
346
- const deltaX = moveX - this.dragStartX;
347
- const deltaY = moveY - this.dragStartY;
348
- // 如果移动距离超过5px,开始拖动
349
- if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
350
- this.isDragging = true;
351
- // 只有在真正开始拖动时才移除 transition 和设置 cursor
352
- if (this.iconElement) {
353
- this.iconElement.style.transition = 'none';
354
- this.iconElement.style.cursor = 'grabbing';
355
- }
356
- // 移除临时监听器
357
- if (this.checkDragHandler) {
358
- if ('touches' in moveEvent) {
359
- document.removeEventListener('touchmove', this.checkDragHandler);
320
+ // 计算拖动偏移量
321
+ this.dragOffset.x = clientX - iconRect.left;
322
+ this.dragOffset.y = clientY - iconRect.top;
323
+ // 注意:不在这里转换位置,只在真正开始拖动时才转换(在 onDrag 中)
324
+ // 添加 document 事件监听器
325
+ if (this.onDragHandler) {
326
+ document.addEventListener('mousemove', this.onDragHandler);
327
+ document.addEventListener('touchmove', this.onDragHandler, { passive: false });
328
+ }
329
+ if (this.stopDragHandler) {
330
+ document.addEventListener('mouseup', this.stopDragHandler);
331
+ document.addEventListener('touchend', this.stopDragHandler);
332
+ }
333
+ // 添加处理 iframe 上事件丢失的机制
334
+ // 1. 监听 mouseleave 和 pointerleave 事件(鼠标离开窗口时停止拖动)
335
+ this.mouseLeaveHandler = (e) => {
336
+ // 只有当鼠标真正离开窗口时才停止拖动
337
+ if (!e.relatedTarget && (e.clientY <= 0 || e.clientX <= 0 ||
338
+ e.clientX >= window.innerWidth || e.clientY >= window.innerHeight)) {
339
+ if (this.debug) {
340
+ console.log('Mouse left window, stopping drag');
360
341
  }
361
- else {
362
- document.removeEventListener('mousemove', this.checkDragHandler);
363
- }
364
- this.checkDragHandler = null;
342
+ this.stopDrag();
365
343
  }
366
- // 添加正式的事件监听器
367
- if (this.dragMoveHandler && this.dragEndHandler) {
368
- if ('touches' in moveEvent) {
369
- document.addEventListener('touchmove', this.dragMoveHandler, { passive: false });
370
- document.addEventListener('touchend', this.dragEndHandler);
344
+ };
345
+ this.pointerLeaveHandler = (e) => {
346
+ // 只有当指针真正离开窗口时才停止拖动
347
+ if (!e.relatedTarget && (e.clientY <= 0 || e.clientX <= 0 ||
348
+ e.clientX >= window.innerWidth || e.clientY >= window.innerHeight)) {
349
+ if (this.debug) {
350
+ console.log('Pointer left window, stopping drag');
371
351
  }
372
- else {
373
- document.addEventListener('mousemove', this.dragMoveHandler);
374
- document.addEventListener('mouseup', this.dragEndHandler);
352
+ this.stopDrag();
353
+ }
354
+ };
355
+ document.addEventListener('mouseleave', this.mouseLeaveHandler);
356
+ document.addEventListener('pointerleave', this.pointerLeaveHandler);
357
+ // 2. 监听 blur 事件(窗口失去焦点时停止拖动)
358
+ this.blurHandler = () => {
359
+ if (this.debug) {
360
+ console.log('Window lost focus, stopping drag');
361
+ }
362
+ this.stopDrag();
363
+ };
364
+ window.addEventListener('blur', this.blurHandler);
365
+ // 3. 添加超时机制(如果一段时间没有收到 mousemove 事件,自动停止拖动)
366
+ // 这可以处理鼠标移动到 iframe 上的情况
367
+ this.dragTimeoutId = window.setTimeout(() => {
368
+ if (this.isDragging) {
369
+ if (this.debug) {
370
+ console.log('Drag timeout, stopping drag (likely mouse moved over iframe)');
375
371
  }
372
+ this.stopDrag();
376
373
  }
374
+ }, 500); // 500ms 没有移动事件,自动停止拖动(处理鼠标移动到 iframe 上的情况)
375
+ if (this.debug) {
376
+ console.log('Drag start');
377
377
  }
378
- };
379
- // 保存 checkDrag 引用,以便后续清理
380
- this.checkDragHandler = checkDrag;
381
- // 添加临时检测监听器
382
- if ('touches' in e) {
383
- document.addEventListener('touchmove', this.checkDragHandler, { passive: false });
384
- document.addEventListener('touchend', this.dragEndHandler);
385
378
  }
386
- else {
387
- document.addEventListener('mousemove', this.checkDragHandler);
388
- document.addEventListener('mouseup', this.dragEndHandler);
379
+ catch (error) {
380
+ if (this.debug) {
381
+ console.error('Error in startDrag:', error);
382
+ }
389
383
  }
390
384
  }
391
385
  /**
392
386
  * 拖动中
393
387
  */
394
- handleDragMove(e) {
395
- if (!this.iconElement || !this.isDragging)
388
+ onDrag(e) {
389
+ if (!this.iconElement)
396
390
  return;
391
+ e.preventDefault();
392
+ // 重置超时定时器(每次移动都重置,确保只有真正停止移动时才触发超时)
393
+ if (this.dragTimeoutId !== null) {
394
+ window.clearTimeout(this.dragTimeoutId);
395
+ this.dragTimeoutId = window.setTimeout(() => {
396
+ if (this.isDragging) {
397
+ if (this.debug) {
398
+ console.log('Drag timeout, stopping drag (likely mouse moved over iframe)');
399
+ }
400
+ this.stopDrag();
401
+ }
402
+ }, 500); // 500ms 没有移动事件,自动停止拖动
403
+ }
397
404
  const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
398
405
  const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
399
- // 注意:拖动过程中不检测 iframe 元素,因为用户可能只是想将图标拖动到 iframe 附近或上方
400
- // 只有在拖动开始时(checkDrag 阶段)才检测 iframe,防止误判为拖动
401
- // 一旦开始拖动,就应该允许拖动到任何位置,包括 iframe 上方
402
- // 计算从拖动开始位置到当前位置的距离
403
- const deltaX = clientX - this.dragStartX;
404
- const deltaY = clientY - this.dragStartY;
405
- // 检测鼠标/手指是否还在图标附近(允许一定的容差范围,比如图标大小的3倍)
406
- const rect = this.iconElement.getBoundingClientRect();
407
- const iconCenterX = rect.left + rect.width / 2;
408
- const iconCenterY = rect.top + rect.height / 2;
409
- const distanceFromIcon = Math.sqrt(Math.pow(clientX - iconCenterX, 2) + Math.pow(clientY - iconCenterY, 2));
410
- const maxDragDistance = Math.max(rect.width, rect.height) * 3; // 允许拖动距离为图标大小的3倍
411
- // 如果鼠标/手指移出图标太远,停止拖动
412
- if (distanceFromIcon > maxDragDistance) {
413
- // 立即结束拖动,清理事件监听器
414
- this.handleDragEnd();
406
+ // 检查是否有足够的移动距离
407
+ const deltaX = Math.abs(clientX - this.lastTouchPosition.x);
408
+ const deltaY = Math.abs(clientY - this.lastTouchPosition.y);
409
+ const totalMovement = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
410
+ if (totalMovement > this.clickThreshold) {
411
+ this.hasMoved = true;
412
+ if (!this.dragStarted) {
413
+ // 开始真正的拖拽
414
+ this.dragStarted = true;
415
+ this.isDragging = true;
416
+ // 如果是第一次拖动,需要转换位置(从 right/bottom left/top)
417
+ const computedStyle = window.getComputedStyle(this.iconElement);
418
+ if (computedStyle.right !== 'auto' || computedStyle.bottom !== 'auto') {
419
+ // 获取当前实际位置
420
+ const currentRect = this.iconElement.getBoundingClientRect();
421
+ const container = this.getTargetElement();
422
+ if (container) {
423
+ const containerRect = container.getBoundingClientRect();
424
+ // 计算相对于容器的位置
425
+ const relativeX = currentRect.left - containerRect.left;
426
+ const relativeY = currentRect.top - containerRect.top;
427
+ // 使用相对位置
428
+ this.iconElement.style.left = `${relativeX}px`;
429
+ this.iconElement.style.top = `${relativeY}px`;
430
+ this.iconElement.style.right = 'auto';
431
+ this.iconElement.style.bottom = 'auto';
432
+ // 重新计算偏移量(相对于容器)
433
+ this.dragOffset.x = clientX - currentRect.left;
434
+ this.dragOffset.y = clientY - currentRect.top;
435
+ }
436
+ }
437
+ // 移除过渡动画,使拖动更流畅
438
+ this.iconElement.style.transition = 'none';
439
+ this.iconElement.style.cursor = 'grabbing';
440
+ if (this.debug) {
441
+ console.log('Drag started (moved > threshold)');
442
+ }
443
+ }
444
+ }
445
+ if (!this.isDragging) {
415
446
  return;
416
447
  }
417
- e.preventDefault();
418
- e.stopPropagation();
419
- // 计算新位置
420
- const newX = this.iconStartX + deltaX;
421
- const newY = this.iconStartY + deltaY;
422
- // 限制在视口内
423
- const maxX = window.innerWidth - this.iconElement.offsetWidth;
424
- const maxY = window.innerHeight - this.iconElement.offsetHeight;
425
- const clampedX = Math.max(0, Math.min(newX, maxX));
426
- const clampedY = Math.max(0, Math.min(newY, maxY));
427
- // 更新位置
428
- this.iconElement.style.left = `${clampedX}px`;
429
- this.iconElement.style.top = `${clampedY}px`;
430
- this.iconElement.style.right = 'auto';
431
- this.iconElement.style.bottom = 'auto';
448
+ try {
449
+ const container = this.getTargetElement();
450
+ if (!container) {
451
+ return;
452
+ }
453
+ const containerRect = container.getBoundingClientRect();
454
+ // 计算新位置
455
+ let newX = clientX - this.dragOffset.x - containerRect.left;
456
+ let newY = clientY - this.dragOffset.y - containerRect.top;
457
+ // 限制在容器内
458
+ const iconWidth = this.iconElement.offsetWidth;
459
+ const iconHeight = this.iconElement.offsetHeight;
460
+ if (container === document.body) {
461
+ // 限制在视口内
462
+ newX = Math.max(0, Math.min(newX, window.innerWidth - iconWidth));
463
+ newY = Math.max(0, Math.min(newY, window.innerHeight - iconHeight));
464
+ }
465
+ else {
466
+ // 限制在容器内
467
+ const containerWidth = containerRect.width;
468
+ const containerHeight = containerRect.height;
469
+ newX = Math.max(0, Math.min(newX, containerWidth - iconWidth));
470
+ newY = Math.max(0, Math.min(newY, containerHeight - iconHeight));
471
+ }
472
+ // 更新位置
473
+ this.iconElement.style.left = `${newX}px`;
474
+ this.iconElement.style.top = `${newY}px`;
475
+ this.iconElement.style.right = 'auto';
476
+ this.iconElement.style.bottom = 'auto';
477
+ // 更新最后位置
478
+ this.lastTouchPosition.x = clientX;
479
+ this.lastTouchPosition.y = clientY;
480
+ }
481
+ catch (error) {
482
+ if (this.debug) {
483
+ console.error('Error in onDrag:', error);
484
+ }
485
+ }
432
486
  }
433
487
  /**
434
- * 结束拖动
488
+ * 停止拖动
435
489
  */
436
- handleDragEnd(_e) {
490
+ stopDrag(_e) {
491
+ this.cleanupDragEvents();
437
492
  if (!this.iconElement)
438
493
  return;
439
- // 清理document上的所有事件监听器(包括临时检测监听器)
440
- if (this.checkDragHandler) {
441
- document.removeEventListener('mousemove', this.checkDragHandler);
442
- document.removeEventListener('touchmove', this.checkDragHandler);
443
- this.checkDragHandler = null;
444
- }
445
- if (this.dragMoveHandler) {
446
- document.removeEventListener('mousemove', this.dragMoveHandler);
447
- document.removeEventListener('touchmove', this.dragMoveHandler);
448
- }
449
- if (this.dragEndHandler) {
450
- document.removeEventListener('mouseup', this.dragEndHandler);
451
- document.removeEventListener('touchend', this.dragEndHandler);
452
- }
453
- // 恢复样式(如果之前被修改过)
454
- // 注意:如果只是点击(没有拖动),这些样式可能没有被修改,但恢复操作是安全的
494
+ // 恢复样式
455
495
  this.iconElement.style.transition = 'transform 0.2s ease';
456
496
  this.iconElement.style.cursor = 'pointer';
457
- // 如果只是点击(没有拖动),触发点击事件
458
- if (!this.isDragging) {
459
- this.handleClick();
497
+ // 检查是否是点击
498
+ const touchDuration = Date.now() - this.touchStartTime;
499
+ const isValidClick = !this.hasMoved && touchDuration < 1000 && !this.dragStarted;
500
+ if (this.debug) {
501
+ console.log('Drag end', {
502
+ hasMoved: this.hasMoved,
503
+ dragStarted: this.dragStarted,
504
+ touchDuration,
505
+ isValidClick
506
+ });
460
507
  }
508
+ // 先保存点击状态,然后重置拖动状态
509
+ const wasClick = isValidClick;
510
+ // 如果真正拖动过,保存当前位置到 iconPosition
511
+ if (this.dragStarted && this.isDragging && this.iconElement) {
512
+ const computedStyle = window.getComputedStyle(this.iconElement);
513
+ const left = computedStyle.left;
514
+ const top = computedStyle.top;
515
+ // 如果 left/top 是有效的像素值,保存到 iconPosition
516
+ if (left !== 'auto' && top !== 'auto') {
517
+ const leftValue = parseFloat(left);
518
+ const topValue = parseFloat(top);
519
+ if (!isNaN(leftValue) && !isNaN(topValue)) {
520
+ this.iconPosition = {
521
+ x: leftValue,
522
+ y: topValue
523
+ };
524
+ if (this.debug) {
525
+ console.log('Icon position saved:', this.iconPosition);
526
+ }
527
+ }
528
+ }
529
+ }
530
+ // 重置状态
531
+ this.hasMoved = false;
461
532
  this.isDragging = false;
533
+ this.dragStarted = false;
534
+ if (wasClick) {
535
+ // 是点击,触发点击事件
536
+ // 延迟触发,确保拖动状态已完全重置,并且没有新的拖动开始
537
+ setTimeout(() => {
538
+ // 再次检查,确保没有在延迟期间开始拖动
539
+ if (this.isClickEnabled && !this.dragStarted && !this.hasMoved && !this.isDragging) {
540
+ this.handleClick();
541
+ }
542
+ }, 100); // 增加延迟到 100ms,确保拖动状态完全重置
543
+ }
544
+ }
545
+ /**
546
+ * 清理拖动事件
547
+ */
548
+ cleanupDragEvents() {
549
+ // 清理拖动事件
550
+ if (this.onDragHandler) {
551
+ document.removeEventListener('mousemove', this.onDragHandler);
552
+ document.removeEventListener('touchmove', this.onDragHandler);
553
+ }
554
+ if (this.stopDragHandler) {
555
+ document.removeEventListener('mouseup', this.stopDragHandler);
556
+ document.removeEventListener('touchend', this.stopDragHandler);
557
+ }
558
+ // 清理处理 iframe 事件丢失的监听器
559
+ if (this.mouseLeaveHandler) {
560
+ document.removeEventListener('mouseleave', this.mouseLeaveHandler);
561
+ this.mouseLeaveHandler = null;
562
+ }
563
+ if (this.pointerLeaveHandler) {
564
+ document.removeEventListener('pointerleave', this.pointerLeaveHandler);
565
+ this.pointerLeaveHandler = null;
566
+ }
567
+ if (this.blurHandler) {
568
+ window.removeEventListener('blur', this.blurHandler);
569
+ this.blurHandler = null;
570
+ }
571
+ // 清理超时定时器
572
+ if (this.dragTimeoutId !== null) {
573
+ window.clearTimeout(this.dragTimeoutId);
574
+ this.dragTimeoutId = null;
575
+ }
462
576
  }
463
577
  /**
464
578
  * 处理点击事件
@@ -478,14 +592,12 @@ class IconManager {
478
592
  disableClick() {
479
593
  this.isClickEnabled = false;
480
594
  // 移除拖动事件监听器
595
+ if (this.iconElement && this.startDragHandler) {
596
+ this.iconElement.removeEventListener('mousedown', this.startDragHandler);
597
+ this.iconElement.removeEventListener('touchstart', this.startDragHandler);
598
+ }
599
+ // 禁用所有鼠标事件(包括点击和拖拽)
481
600
  if (this.iconElement) {
482
- if (this.dragStartHandler) {
483
- this.iconElement.removeEventListener('mousedown', this.dragStartHandler);
484
- }
485
- if (this.touchStartHandler) {
486
- this.iconElement.removeEventListener('touchstart', this.touchStartHandler);
487
- }
488
- // 禁用所有鼠标事件(包括点击和拖拽)
489
601
  this.iconElement.style.pointerEvents = 'none';
490
602
  this.iconElement.style.cursor = 'default';
491
603
  }
@@ -498,14 +610,16 @@ class IconManager {
498
610
  enableClick() {
499
611
  this.isClickEnabled = true;
500
612
  // 重新添加拖动事件监听器
613
+ if (this.iconElement && this.startDragHandler) {
614
+ this.iconElement.addEventListener('mousedown', this.startDragHandler);
615
+ this.iconElement.addEventListener('touchstart', this.startDragHandler, { passive: false });
616
+ }
617
+ else if (this.iconElement) {
618
+ // 如果事件处理器不存在,重新设置
619
+ this.setupDragEvents();
620
+ }
621
+ // 恢复鼠标事件
501
622
  if (this.iconElement) {
502
- if (this.dragStartHandler) {
503
- this.iconElement.addEventListener('mousedown', this.dragStartHandler);
504
- }
505
- if (this.touchStartHandler) {
506
- this.iconElement.addEventListener('touchstart', this.touchStartHandler, { passive: false });
507
- }
508
- // 恢复鼠标事件
509
623
  this.iconElement.style.pointerEvents = 'auto';
510
624
  this.iconElement.style.cursor = 'pointer';
511
625
  }
@@ -609,6 +723,7 @@ class IframeManager {
609
723
  this.isOpen = false;
610
724
  this.isCreated = false;
611
725
  this.debug = false; // debug 模式标志
726
+ this.targetElement = null; // 目标元素(用于自适应宽度)
612
727
  this.config = {
613
728
  src: '',
614
729
  mode: 'auto', // 默认自动检测设备类型
@@ -809,13 +924,44 @@ class IframeManager {
809
924
  'allow-pointer-lock', // 允许指针锁定
810
925
  'allow-storage-access-by-user-activation' // 允许用户激活的存储访问
811
926
  ].join(' '));
927
+ // 获取目标元素(如果提供了 target)
928
+ if (this.config.target) {
929
+ this.targetElement = this.getTargetElement(this.config.target);
930
+ }
812
931
  // 根据设备类型设置模式
813
932
  const actualMode = this.getActualMode();
814
933
  const isPC = actualMode === 'popup';
815
934
  this.iframeElement.scrolling = 'auto'; // PC和移动端都显示滚动条
816
- // PC模式:使用配置的宽度,高度100%;移动端:100%宽度和高度
817
- const containerStyles = isPC ? {
818
- // PC模式:配置的宽度,高度100%
935
+ // 判断是否使用 target 的自适应宽度
936
+ const useTargetWidth = !!this.targetElement && this.targetElement !== document.body;
937
+ // PC模式:如果传了 target,使用 target 的宽度(自适应);否则使用配置的宽度
938
+ // 移动端:如果传了 target,使用 target 的宽度(自适应);否则全屏
939
+ const containerStyles = useTargetWidth ? {
940
+ // 使用 target 的宽度(自适应),PC 和移动端都适用
941
+ // 宽度 100% 自适应 target 的实际宽度(target 本身可能有 max-width 限制)
942
+ width: '100%',
943
+ height: '100%',
944
+ maxWidth: '100%', // 不超过 target 的宽度
945
+ maxHeight: '100%',
946
+ backgroundColor: '#ffffff',
947
+ borderRadius: isPC ? '0' : '12px 12px 0 0',
948
+ boxShadow: isPC ? 'none' : '0 -4px 16px rgba(0, 0, 0, 0.25)',
949
+ border: 'none',
950
+ position: 'absolute', // 相对于 target 元素定位
951
+ zIndex: '999999',
952
+ // 占满 target 元素
953
+ top: '0',
954
+ left: '0',
955
+ bottom: '0',
956
+ right: '0',
957
+ transform: 'none',
958
+ overflow: 'hidden',
959
+ // 初始隐藏的关键样式
960
+ visibility: 'hidden',
961
+ opacity: '0',
962
+ display: 'none'
963
+ } : (isPC ? {
964
+ // PC模式:没有 target,使用配置的宽度,高度100%
819
965
  width: `${this.config.width || 450}px`,
820
966
  height: '100%',
821
967
  maxWidth: '90vw',
@@ -838,7 +984,7 @@ class IframeManager {
838
984
  opacity: '0',
839
985
  display: 'none'
840
986
  } : {
841
- // 移动端全屏模式(强制 100% 宽度和高度)
987
+ // 移动端全屏模式(没有 target,强制 100% 宽度和高度)
842
988
  width: '100%',
843
989
  height: '100%',
844
990
  maxWidth: '100%',
@@ -860,7 +1006,7 @@ class IframeManager {
860
1006
  visibility: 'hidden',
861
1007
  opacity: '0',
862
1008
  display: 'none'
863
- };
1009
+ });
864
1010
  Object.assign(this.containerElement.style, containerStyles);
865
1011
  // iframe填充整个容器
866
1012
  const iframeStyles = {
@@ -879,8 +1025,30 @@ class IframeManager {
879
1025
  this.injectMobileStyles();
880
1026
  }
881
1027
  });
882
- // 关键优化:容器直接添加到body,避免后续移动DOM导致iframe重新加载
883
- document.body.appendChild(this.containerElement);
1028
+ // 添加到目标元素或 body
1029
+ if (this.targetElement && this.targetElement !== document.body) {
1030
+ // 如果提供了 target,添加到 target 元素内
1031
+ // 需要设置 target 为相对定位,以便 iframe 可以相对于它定位
1032
+ const targetStyle = window.getComputedStyle(this.targetElement);
1033
+ if (targetStyle.position === 'static') {
1034
+ // 如果 target 是静态定位,设置为相对定位
1035
+ this.targetElement.style.position = 'relative';
1036
+ if (this.debug) {
1037
+ console.log('Set target element position to relative for iframe container');
1038
+ }
1039
+ }
1040
+ this.targetElement.appendChild(this.containerElement);
1041
+ if (this.debug) {
1042
+ console.log('Iframe container added to target element');
1043
+ }
1044
+ }
1045
+ else {
1046
+ // 没有 target 或 target 是 body,添加到 body
1047
+ document.body.appendChild(this.containerElement);
1048
+ if (this.debug) {
1049
+ console.log('Iframe container added to document.body');
1050
+ }
1051
+ }
884
1052
  if (this.debug) {
885
1053
  console.log('CustomerSDK container created (hidden, ready for SSE)');
886
1054
  }
@@ -1076,6 +1244,35 @@ class IframeManager {
1076
1244
  this.containerElement.style.height = `${height}px`;
1077
1245
  }
1078
1246
  }
1247
+ /**
1248
+ * 获取目标元素(支持字符串选择器或 HTMLElement)
1249
+ */
1250
+ getTargetElement(target) {
1251
+ if (typeof target === 'string') {
1252
+ const element = document.querySelector(target);
1253
+ if (element) {
1254
+ return element;
1255
+ }
1256
+ else {
1257
+ if (this.debug) {
1258
+ console.warn(`Target element not found: ${target}, falling back to document.body`);
1259
+ }
1260
+ return document.body;
1261
+ }
1262
+ }
1263
+ if (target instanceof HTMLElement) {
1264
+ if (document.body.contains(target)) {
1265
+ return target;
1266
+ }
1267
+ else {
1268
+ if (this.debug) {
1269
+ console.warn('Target element no longer in DOM, falling back to document.body');
1270
+ }
1271
+ return document.body;
1272
+ }
1273
+ }
1274
+ return document.body;
1275
+ }
1079
1276
  }
1080
1277
 
1081
1278
  function changeJpegDpi(uint8Array, dpi) {
@@ -20797,33 +20994,19 @@ class CustomerServiceSDK {
20797
20994
  * 初始化 SDK
20798
20995
  * @param config SDK配置
20799
20996
  * @param options UI选项(可选)
20800
- * @param forceReinit 是否强制重新初始化(用于更新token等配置)
20801
20997
  * @returns 返回初始化信息(包含设备ID等)
20802
20998
  */
20803
- async init(config, options, forceReinit = false) {
20804
- // 如果已经初始化且不强制重新初始化,返回之前保存的初始化信息
20805
- if (this.isInitialized && !forceReinit) {
20999
+ async init(config, options) {
21000
+ if (this.isInitialized) {
20806
21001
  if (this.debug) {
20807
- console.warn('CustomerSDK already initialized, returning cached result. Use forceReinit=true to reinitialize.');
21002
+ console.warn('CustomerSDK already initialized');
20808
21003
  }
21004
+ // 如果已经初始化,返回之前保存的初始化信息
20809
21005
  if (this.initResult) {
20810
21006
  return this.initResult;
20811
21007
  }
20812
21008
  throw new Error('SDK already initialized but cannot retrieve initialization info');
20813
21009
  }
20814
- // 如果需要强制重新初始化,先清理旧资源
20815
- if (this.isInitialized && forceReinit) {
20816
- if (this.debug) {
20817
- console.log('Force reinitializing SDK...');
20818
- }
20819
- // 清理旧的 iframe 和截图管理器,但保留图标管理器(保持图标位置等状态)
20820
- this.iframeManager?.destroy();
20821
- this.screenshotManager?.destroy();
20822
- this.iframeManager = null;
20823
- this.screenshotManager = null;
20824
- // 重置初始化标志,但保留图标管理器
20825
- this.isInitialized = false;
20826
- }
20827
21010
  this.config = config;
20828
21011
  this.debug = config.debug ?? false;
20829
21012
  try {
@@ -20848,11 +21031,14 @@ class CustomerServiceSDK {
20848
21031
  this.iconManager = new IconManager(iconPosition, this.debug, iconTarget);
20849
21032
  await this.iconManager.show();
20850
21033
  // 创建iframe管理器(自动检测设备类型)
21034
+ // 如果提供了 target,iframe 会自适应 target 的宽度(PC 和移动端都适用)
21035
+ // 如果没有 target,PC 模式使用默认宽度,移动端全屏
20851
21036
  this.iframeManager = new IframeManager({
20852
21037
  src: iframeUrl,
20853
21038
  mode: 'auto', // 自动根据设备类型选择模式(PC弹窗,移动端全屏)
20854
- width: options?.width || 450, // PC模式宽度(像素,默认450px),移动端不使用
20855
- height: options?.height || 600, // PC模式高度(像素),移动端不使用(强制全屏)
21039
+ width: options?.width || 450, // PC模式宽度(像素,默认450px),仅在未提供 target 时使用
21040
+ height: options?.height || 600, // PC模式高度(像素),仅在未提供 target 时使用
21041
+ target: options?.target, // 目标元素(如果提供,iframe 会自适应其宽度)
20856
21042
  allowClose: true,
20857
21043
  debug: this.debug, // 传递 debug 标志
20858
21044
  onMessage: (messageType, _data) => {
@@ -21057,31 +21243,6 @@ class CustomerServiceSDK {
21057
21243
  console.log('📸 截图配置已更新:', options);
21058
21244
  }
21059
21245
  }
21060
- /**
21061
- * 更新 token(用于用户登录/退出场景)
21062
- * 如果已初始化,会重新创建 iframe 并更新 URL
21063
- * @param token 新的 token(传空字符串或 undefined 表示移除 token)
21064
- * @param options UI选项(可选,用于重新初始化时的配置)
21065
- * @returns 返回更新后的初始化信息
21066
- */
21067
- async updateToken(token, options) {
21068
- if (!this.isInitialized) {
21069
- throw new Error('SDK not initialized. Call init() first.');
21070
- }
21071
- if (!this.config) {
21072
- throw new Error('SDK config not found');
21073
- }
21074
- // 更新配置中的 token
21075
- const updatedConfig = {
21076
- ...this.config,
21077
- token: token && token.trim() !== '' ? token : undefined
21078
- };
21079
- if (this.debug) {
21080
- console.log('Updating token:', token ? 'Token provided' : 'Token removed');
21081
- }
21082
- // 强制重新初始化以应用新的 token
21083
- return await this.init(updatedConfig, options, true);
21084
- }
21085
21246
  /**
21086
21247
  * 销毁 SDK
21087
21248
  */
@@ -21165,14 +21326,13 @@ let globalSDKInstance = null;
21165
21326
  * 初始化 Customer SDK
21166
21327
  * @param config SDK配置
21167
21328
  * @param options UI选项(可选)
21168
- * @param forceReinit 是否强制重新初始化(用于更新token等配置)
21169
21329
  * @returns 返回初始化信息(包含设备ID等)
21170
21330
  */
21171
- const init = async (config, options, forceReinit = false) => {
21331
+ const init = async (config, options) => {
21172
21332
  if (!globalSDKInstance) {
21173
21333
  globalSDKInstance = new CustomerServiceSDK();
21174
21334
  }
21175
- return await globalSDKInstance.init(config, options, forceReinit);
21335
+ return await globalSDKInstance.init(config, options);
21176
21336
  };
21177
21337
  /**
21178
21338
  * 获取全局SDK实例
@@ -21281,16 +21441,6 @@ const updateScreenshotOptions = (options) => {
21281
21441
  const sdk = getInstance();
21282
21442
  sdk.updateScreenshotOptions(options);
21283
21443
  };
21284
- /**
21285
- * 更新 token(用于用户登录/退出场景)
21286
- * @param token 新的 token(传空字符串或 undefined 表示移除 token)
21287
- * @param options UI选项(可选)
21288
- * @returns 返回更新后的初始化信息
21289
- */
21290
- const updateToken = async (token, options) => {
21291
- const sdk = getInstance();
21292
- return await sdk.updateToken(token, options);
21293
- };
21294
21444
  // 默认导出
21295
21445
  var index = {
21296
21446
  init,
@@ -21313,7 +21463,6 @@ var index = {
21313
21463
  enableScreenshot,
21314
21464
  getScreenshotState,
21315
21465
  updateScreenshotOptions,
21316
- updateToken,
21317
21466
  destroy
21318
21467
  };
21319
21468
 
@@ -21340,4 +21489,3 @@ exports.setScreenshotTarget = setScreenshotTarget;
21340
21489
  exports.showIcon = showIcon;
21341
21490
  exports.showNotification = showNotification;
21342
21491
  exports.updateScreenshotOptions = updateScreenshotOptions;
21343
- exports.updateToken = updateToken;