121 lines
3.9 KiB
TypeScript
121 lines
3.9 KiB
TypeScript
// Forwards the widget's raw touch stream to the Vojo host so the
|
|
// swipe-back-from-widget gesture works across the iframe boundary. An
|
|
// iframe is a separate browsing context — touches inside it NEVER bubble
|
|
// to the host document, so without this the host's interactive-pop
|
|
// gesture (src/app/components/swipe-back) is dead over the widget body.
|
|
//
|
|
// Protocol: `{ api: 'io.vojo.bot-widget', action: 'swipe-touch',
|
|
// data: { phase, x, y } }` posted to the parent with the pinned
|
|
// `parentOrigin` (same side-channel + origin discipline as
|
|
// `open-external-url` in widget-api.ts). Coordinates are IFRAME-local
|
|
// clientX/Y; the host offsets them by the iframe's viewport rect.
|
|
//
|
|
// The host owns the real gesture state machine (dead-zone axis resolve,
|
|
// edge guard, distance commit). The ONLY logic duplicated here is the
|
|
// axis resolution needed to call preventDefault locally — the host
|
|
// cannot cancel this document's scroll, so once a single-finger drag
|
|
// resolves as horizontal-rightward we must suppress our own default
|
|
// handling or the widget's vertical scroll would fight the card slide.
|
|
// Thresholds mirror the host's swipe-back/geometry.ts: keep in sync.
|
|
const DEAD_ZONE_PX = 12;
|
|
|
|
type Phase = 'start' | 'move' | 'end' | 'cancel';
|
|
|
|
export function installSwipeForwarder(parentOrigin: string): void {
|
|
const post = (phase: Phase, x: number, y: number): void => {
|
|
window.parent.postMessage(
|
|
{ api: 'io.vojo.bot-widget', action: 'swipe-touch', data: { phase, x, y } },
|
|
parentOrigin
|
|
);
|
|
};
|
|
|
|
let tracking = false;
|
|
let bailed = false;
|
|
let engaged = false;
|
|
let startX = 0;
|
|
let startY = 0;
|
|
|
|
const cancel = (x: number, y: number): void => {
|
|
if (tracking && !bailed) post('cancel', x, y);
|
|
tracking = false;
|
|
bailed = true;
|
|
};
|
|
|
|
document.addEventListener(
|
|
'touchstart',
|
|
(e) => {
|
|
if (e.touches.length !== 1) {
|
|
cancel(0, 0);
|
|
return;
|
|
}
|
|
const t = e.touches[0];
|
|
tracking = true;
|
|
bailed = false;
|
|
engaged = false;
|
|
startX = t.clientX;
|
|
startY = t.clientY;
|
|
post('start', t.clientX, t.clientY);
|
|
},
|
|
{ passive: true }
|
|
);
|
|
|
|
document.addEventListener(
|
|
'touchmove',
|
|
(e) => {
|
|
if (!tracking || bailed) return;
|
|
if (e.touches.length !== 1) {
|
|
const t = e.touches[0];
|
|
cancel(t.clientX, t.clientY);
|
|
return;
|
|
}
|
|
const t = e.touches[0];
|
|
if (!engaged) {
|
|
const dx = t.clientX - startX;
|
|
const dy = t.clientY - startY;
|
|
if (Math.abs(dx) < DEAD_ZONE_PX && Math.abs(dy) < DEAD_ZONE_PX) {
|
|
// Still inside the dead-zone — keep feeding the host (its own
|
|
// machine waits the same way) but make no local decision yet.
|
|
post('move', t.clientX, t.clientY);
|
|
return;
|
|
}
|
|
// Vertical-dominant or leftward: the gesture is the widget's own
|
|
// (scroll / horizontal UI). Stop forwarding — the host's machine
|
|
// bails identically from the same data; the cancel is belt and
|
|
// braces against threshold drift.
|
|
if (Math.abs(dy) >= Math.abs(dx) || dx <= 0) {
|
|
cancel(t.clientX, t.clientY);
|
|
return;
|
|
}
|
|
engaged = true;
|
|
}
|
|
// Horizontal-rightward drag — the host owns it now. Suppress the
|
|
// widget's own scroll for the rest of the touch.
|
|
if (e.cancelable) e.preventDefault();
|
|
post('move', t.clientX, t.clientY);
|
|
},
|
|
{ passive: false }
|
|
);
|
|
|
|
document.addEventListener(
|
|
'touchend',
|
|
(e) => {
|
|
if (!tracking || bailed) {
|
|
tracking = false;
|
|
return;
|
|
}
|
|
tracking = false;
|
|
const t = e.changedTouches[0];
|
|
post('end', t?.clientX ?? startX, t?.clientY ?? startY);
|
|
},
|
|
{ passive: true }
|
|
);
|
|
|
|
document.addEventListener(
|
|
'touchcancel',
|
|
(e) => {
|
|
const t = e.changedTouches[0];
|
|
cancel(t?.clientX ?? startX, t?.clientY ?? startY);
|
|
},
|
|
{ passive: true }
|
|
);
|
|
}
|