mirror of
https://github.com/penpot/penpot.git
synced 2026-06-11 09:52:12 +00:00
* 🐛 Fix plugin modal drag and close interactions Switch plugin modal dragging to pointer-capture semantics from the header so drag state remains stable when crossing iframe boundaries. Prevent drag start from close-button pointerdown and add regression tests for both non-draggable close-button interaction and close-event dispatch. Signed-off-by: Marek Hrabe <marekhrabe@me.com> * 📚 Update changelog for plugin modal drag fix Document plugin modal drag and close-button interaction fixes in the unreleased changelog. Signed-off-by: Marek Hrabe <marekhrabe@me.com> * 🐛 Simplify plugin modal drag CSS selection rules Keep user-select disabled at the modal wrapper level and keep touch-action scoped to the header drag handle to remove redundant declarations while preserving drag behavior. Signed-off-by: Marek Hrabe <marekhrabe@me.com> --------- Signed-off-by: Marek Hrabe <marekhrabe@me.com>
110 lines
2.6 KiB
TypeScript
110 lines
2.6 KiB
TypeScript
import { parseTranslate } from './parse-translate';
|
|
|
|
type DragHandlerLifecycle = {
|
|
start?: () => void;
|
|
end?: () => void;
|
|
};
|
|
|
|
export const dragHandler = (
|
|
el: HTMLElement,
|
|
target: HTMLElement = el,
|
|
move?: () => void,
|
|
lifecycle?: DragHandlerLifecycle,
|
|
) => {
|
|
let initialTranslate = { x: 0, y: 0 };
|
|
let initialClientPosition = { x: 0, y: 0 };
|
|
let pointerId: number | null = null;
|
|
let dragging = false;
|
|
|
|
const endDrag = () => {
|
|
if (!dragging) {
|
|
return;
|
|
}
|
|
|
|
dragging = false;
|
|
pointerId = null;
|
|
lifecycle?.end?.();
|
|
};
|
|
|
|
const handlePointerMove = (moveEvent: PointerEvent) => {
|
|
if (!dragging || moveEvent.pointerId !== pointerId) {
|
|
return;
|
|
}
|
|
|
|
const { clientX: moveX, clientY: moveY } = moveEvent;
|
|
const deltaX = moveX - initialClientPosition.x + initialTranslate.x;
|
|
const deltaY = moveY - initialClientPosition.y + initialTranslate.y;
|
|
|
|
target.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
|
|
|
|
move?.();
|
|
};
|
|
|
|
const handlePointerUp = (upEvent: PointerEvent) => {
|
|
if (upEvent.pointerId !== pointerId) {
|
|
return;
|
|
}
|
|
|
|
if (el.hasPointerCapture(upEvent.pointerId)) {
|
|
el.releasePointerCapture(upEvent.pointerId);
|
|
}
|
|
|
|
endDrag();
|
|
};
|
|
|
|
const handlePointerCancel = (cancelEvent: PointerEvent) => {
|
|
if (cancelEvent.pointerId !== pointerId) {
|
|
return;
|
|
}
|
|
|
|
endDrag();
|
|
};
|
|
|
|
const handleLostPointerCapture = (lostCaptureEvent: PointerEvent) => {
|
|
if (lostCaptureEvent.pointerId !== pointerId) {
|
|
return;
|
|
}
|
|
|
|
endDrag();
|
|
};
|
|
|
|
const handlePointerDown = (e: PointerEvent) => {
|
|
if (e.button !== 0) {
|
|
return;
|
|
}
|
|
|
|
const fromButton =
|
|
e.target instanceof Element && !!e.target.closest('button');
|
|
|
|
if (fromButton) {
|
|
return;
|
|
}
|
|
|
|
e.preventDefault();
|
|
|
|
initialClientPosition = { x: e.clientX, y: e.clientY };
|
|
initialTranslate = parseTranslate(target);
|
|
|
|
dragging = true;
|
|
pointerId = e.pointerId;
|
|
lifecycle?.start?.();
|
|
|
|
el.setPointerCapture(e.pointerId);
|
|
};
|
|
|
|
el.addEventListener('pointerdown', handlePointerDown);
|
|
el.addEventListener('pointermove', handlePointerMove);
|
|
el.addEventListener('pointerup', handlePointerUp);
|
|
el.addEventListener('pointercancel', handlePointerCancel);
|
|
el.addEventListener('lostpointercapture', handleLostPointerCapture);
|
|
|
|
return () => {
|
|
el.removeEventListener('pointerdown', handlePointerDown);
|
|
el.removeEventListener('pointermove', handlePointerMove);
|
|
el.removeEventListener('pointerup', handlePointerUp);
|
|
el.removeEventListener('pointercancel', handlePointerCancel);
|
|
el.removeEventListener('lostpointercapture', handleLostPointerCapture);
|
|
endDrag();
|
|
};
|
|
};
|