diff --git a/dom/directives.ts b/dom/directives.ts index f803cb2..c8df9d3 100644 --- a/dom/directives.ts +++ b/dom/directives.ts @@ -1660,6 +1660,15 @@ function attachBoxDragSelect( let startClientX = 0; let startClientY = 0; let pointerId = -1; + // pointerType + lastMove let us recover a TOUCH drag that iOS Safari + // cancels mid-gesture. iOS fires pointercancel / yanks pointer capture on + // long, fast touch-drags (it re-interprets them as a scroll/swipe), which + // would otherwise discard a large region the user deliberately drew. For + // touch we finalize WITH the last-known position instead of dropping it; + // mouse/pen keep the strict discard. (The area threshold in finalize still + // rejects an accidental tiny box, so a stray cancel can't create a region.) + let pointerType = ""; + let lastMove: PointerEvent | null = null; // Capture the parent at pointerdown time so a server diff that moves // the host to a NEW parent mid-drag doesn't split the drag across // two positioning contexts. updateOverlay positions against this @@ -1701,8 +1710,10 @@ function attachBoxDragSelect( const capturedPointerId = pointerId; const rect = startRect; pointerId = -1; + pointerType = ""; dragParent = null; startRect = null; + lastMove = null; try { el.releasePointerCapture(capturedPointerId); } catch { @@ -1820,6 +1831,8 @@ function attachBoxDragSelect( startClientX = e.clientX; startClientY = e.clientY; pointerId = e.pointerId; + pointerType = e.pointerType; + lastMove = e; dragParent = parent; startRect = el.getBoundingClientRect(); let captureOk = false; @@ -1915,6 +1928,7 @@ function attachBoxDragSelect( finalize(null, false); return; } + lastMove = e; updateOverlay(e); }; @@ -1929,7 +1943,9 @@ function attachBoxDragSelect( const onPointerCancel = (e: PointerEvent) => { if (e.pointerId !== pointerId) return; - finalize(e, false); + // iOS cancels long touch-drags it re-reads as a swipe; keep the region + // the user drew rather than dropping it (mouse/pen still discard). + finalize(e, pointerType === "touch"); }; // lostpointercapture handles the rare case where the platform yanks // capture (OS gesture, another setPointerCapture call). Guard on @@ -1937,7 +1953,13 @@ function attachBoxDragSelect( // DIFFERENT pointer on the same element, and we mustn't cancel // our in-progress drag because of an unrelated release. const onLostCapture = (e: PointerEvent) => { - if (e.pointerId === pointerId) finalize(null, false); + if (e.pointerId !== pointerId) return; + // Same touch-recovery as pointercancel: finalize from the last move so a + // capture yank mid-touch-drag keeps the region instead of dropping it. + // (lastMove is non-null whenever a drag is active — set on pointerdown — + // and finalize tolerates a null event anyway, so no extra guard is needed.) + const touch = pointerType === "touch"; + finalize(touch ? lastMove : null, touch); }; el.addEventListener("pointerdown", onPointerDown); @@ -2068,6 +2090,201 @@ export function teardownRegionSelectForRoot(rootElement: Element): void { } } +// proxyBridgeArmed mirrors the area/region-select singletons: one entry per +// armed `lvt-fx:proxy-bridge` element, swept on disconnect. sync runs on every +// render to re-apply the pin-layer transform morphdom wipes + relay a "locate +// this annotation" request to the iframe. +type ProxyBridgeEntry = BoxDragEntry & { sync: () => void }; +const proxyBridgeArmed = new Map(); + +/** + * Apply proxy-bridge directives — the `--external` live-site bridge. + * `lvt-fx:proxy-bridge=""` on the preview stage (the element + * wrapping the proxied `