@vanduo-oss/framework 1.3.1 → 1.3.3

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,8 @@
18
18
  touchState: null,
19
19
  // Feedback element
20
20
  feedbackElement: null,
21
+ // Shared selector used by init and touch reorder
22
+ containerSelector: '.vd-draggable-container, .vd-draggable-container-vertical',
21
23
 
22
24
  /**
23
25
  * Initialize draggable components
@@ -32,7 +34,7 @@
32
34
  this.initDraggable(element);
33
35
  });
34
36
 
35
- const containers = document.querySelectorAll('.vd-draggable-container, .vd-draggable-container-vertical');
37
+ const containers = document.querySelectorAll(this.containerSelector);
36
38
  containers.forEach(container => {
37
39
  if (!this.instances.has(container)) {
38
40
  this.initContainer(container);
@@ -369,10 +371,16 @@
369
371
  // Don't prevent default here — it blocks scrolling.
370
372
  // We only prevent default in touchmove once drag threshold is reached.
371
373
  const touch = e.touches[0];
374
+ const rect = element.getBoundingClientRect();
372
375
  this.touchState = {
373
376
  element: element,
374
377
  startX: touch.clientX,
375
378
  startY: touch.clientY,
379
+ lastX: touch.clientX,
380
+ lastY: touch.clientY,
381
+ // Keep preview anchored to the original grab point.
382
+ offsetX: touch.clientX - rect.left,
383
+ offsetY: touch.clientY - rect.top,
376
384
  startTime: Date.now(),
377
385
  isDragging: false
378
386
  };
@@ -387,6 +395,8 @@
387
395
  if (!this.touchState) return;
388
396
 
389
397
  const touch = e.touches[0];
398
+ this.touchState.lastX = touch.clientX;
399
+ this.touchState.lastY = touch.clientY;
390
400
  const deltaX = touch.clientX - this.touchState.startX;
391
401
  const deltaY = touch.clientY - this.touchState.startY;
392
402
 
@@ -405,7 +415,10 @@
405
415
  element: element,
406
416
  initialPosition: { x: this.touchState.startX, y: this.touchState.startY },
407
417
  initialBounds: element.getBoundingClientRect(),
408
- data: this.getData(element)
418
+ data: this.getData(element),
419
+ // Preserve where inside the element the drag started for accurate ghost positioning.
420
+ offsetX: this.touchState.offsetX,
421
+ offsetY: this.touchState.offsetY
409
422
  };
410
423
 
411
424
  // Dispatch event
@@ -434,8 +447,10 @@
434
447
  }
435
448
  }));
436
449
 
450
+ this.updateTouchDropZone(touch.clientX, touch.clientY);
451
+
437
452
  // Reorder for touch
438
- const container = element.closest('.vd-draggable-container');
453
+ const container = element.closest(this.containerSelector);
439
454
  if (container && container.contains(element)) {
440
455
  this.handleReorder(container, element, touch.clientX, touch.clientY);
441
456
  }
@@ -451,6 +466,18 @@
451
466
  handleTouchEnd: function (e, element) {
452
467
  if (this.touchState && this.touchState.isDragging) {
453
468
  if (e.cancelable) e.preventDefault();
469
+ const endTouch = e.changedTouches?.[0];
470
+ const endPosition = {
471
+ x: endTouch?.clientX ?? this.touchState.lastX ?? this.touchState.startX,
472
+ y: endTouch?.clientY ?? this.touchState.lastY ?? this.touchState.startY
473
+ };
474
+
475
+ const dropZone = this.resolveDropZoneAtPoint(endPosition.x, endPosition.y) || this.touchState.overZone;
476
+ if (dropZone) {
477
+ this.dispatchDrop(dropZone, endPosition);
478
+ } else if (this.touchState.overZone) {
479
+ this.touchState.overZone.classList.remove('is-drag-over');
480
+ }
454
481
 
455
482
  element.classList.remove('is-dragging');
456
483
  element.classList.add('is-dropped');
@@ -463,7 +490,6 @@
463
490
  }
464
491
 
465
492
  // Dispatch event
466
- const endTouch = e.changedTouches[0];
467
493
  const data = this.currentDrag?.data || this.getData(element);
468
494
  const startX = this.touchState?.startX || 0;
469
495
  const startY = this.touchState?.startY || 0;
@@ -473,10 +499,10 @@
473
499
  detail: {
474
500
  element: element,
475
501
  data: data,
476
- position: { x: endTouch.clientX, y: endTouch.clientY },
502
+ position: endPosition,
477
503
  delta: {
478
- x: endTouch.clientX - startX,
479
- y: endTouch.clientY - startY
504
+ x: endPosition.x - startX,
505
+ y: endPosition.y - startY
480
506
  }
481
507
  }
482
508
  }));
@@ -523,16 +549,79 @@
523
549
  */
524
550
  handleDrop: function (e, zone) {
525
551
  e.preventDefault();
526
- zone.classList.remove('is-drag-over');
552
+ this.dispatchDrop(zone, { x: e.clientX, y: e.clientY });
553
+ },
527
554
 
528
- // Dispatch event
555
+ /**
556
+ * Resolve a drop zone from viewport coordinates
557
+ * @param {number} x
558
+ * @param {number} y
559
+ * @returns {HTMLElement|null}
560
+ */
561
+ resolveDropZoneAtPoint: function (x, y) {
562
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
563
+
564
+ // Prefer the full stacking list so overlays/top elements don't hide real drop targets.
565
+ if (typeof document.elementsFromPoint === 'function') {
566
+ const stacked = document.elementsFromPoint(x, y);
567
+ for (const element of stacked) {
568
+ const zone = element.closest('.vd-drop-zone');
569
+ if (zone) return zone;
570
+ }
571
+ }
572
+
573
+ const target = document.elementFromPoint(x, y);
574
+ const targetZone = target ? target.closest('.vd-drop-zone') : null;
575
+ if (targetZone) return targetZone;
576
+
577
+ // Last-resort fallback for mobile emulation edge cases.
578
+ const zones = document.querySelectorAll('.vd-drop-zone');
579
+ for (const zone of zones) {
580
+ const rect = zone.getBoundingClientRect();
581
+ if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
582
+ return zone;
583
+ }
584
+ }
585
+
586
+ return null;
587
+ },
588
+
589
+ /**
590
+ * Track and update active drop-zone hover state on touch devices
591
+ * @param {number} x
592
+ * @param {number} y
593
+ */
594
+ updateTouchDropZone: function (x, y) {
595
+ if (!this.touchState) return;
596
+
597
+ const nextZone = this.resolveDropZoneAtPoint(x, y);
598
+ const prevZone = this.touchState.overZone || null;
599
+
600
+ if (prevZone && prevZone !== nextZone) {
601
+ prevZone.classList.remove('is-drag-over');
602
+ }
603
+
604
+ if (nextZone && nextZone !== prevZone) {
605
+ nextZone.classList.add('is-drag-over');
606
+ }
607
+
608
+ this.touchState.overZone = nextZone || null;
609
+ },
610
+
611
+ /**
612
+ * Dispatch a normalized drop event for mouse and touch flows
613
+ * @param {HTMLElement} zone
614
+ * @param {{x:number, y:number}} position
615
+ */
616
+ dispatchDrop: function (zone, position) {
617
+ zone.classList.remove('is-drag-over');
529
618
  zone.dispatchEvent(new CustomEvent('draggable:drop', {
530
619
  bubbles: true,
531
620
  detail: {
532
621
  zone: zone,
533
622
  element: this.currentDrag?.element,
534
623
  data: this.currentDrag?.data,
535
- position: { x: e.clientX, y: e.clientY }
624
+ position: position
536
625
  }
537
626
  }));
538
627
  },
@@ -650,9 +739,11 @@
650
739
  this.feedbackElement.appendChild(clone);
651
740
 
652
741
  // Set styles
742
+ const offsetX = this.currentDrag.offsetX ?? 20;
743
+ const offsetY = this.currentDrag.offsetY ?? 20;
653
744
  Object.assign(this.feedbackElement.style, {
654
- left: (x - 20) + 'px',
655
- top: (y - 20) + 'px',
745
+ left: (x - offsetX) + 'px',
746
+ top: (y - offsetY) + 'px',
656
747
  width: rect.width + 'px',
657
748
  height: rect.height + 'px'
658
749
  });