import { closestBySelector } from "eez-studio-shared/dom"; import { IPointerEvent } from "project-editor/flow/editor/mouse-handler"; export const DRAGGABLE_OVERLAY_ELEMENT_ID = "eez-draggable-overlay-element"; //////////////////////////////////////////////////////////////////////////////// interface DraggableConfig { onDragStart?(e: PointerEvent, x: number, y: number): any; onDragMove?(e: PointerEvent, x: number, y: number, params: any): void; onDragEnd?(e: PointerEvent | undefined, cancel: boolean, params: any): void; onMove?(e: PointerEvent): void; onDraggableWheel?(event: WheelEvent): void; } export class Draggable { element: Element | null = null; overlayElement: HTMLDivElement | null = null; cursor: string | undefined = undefined; dragging: boolean = false; xDragStart: number = 0; yDragStart: number = 0; params: any; savedBodyUserSelect: string | null = null; capturedPointerId: number = 0; lastDragMoveEvent: PointerEvent | undefined; constructor(private config: DraggableConfig) {} attach(element: Element | null) { if (this.dragging) { return; } if (this.element) { this.finishDragging(undefined, true); this.element.removeEventListener("pointerdown", this.onPointerDown); this.element.removeEventListener("pointermove", this.onPointerMove); } this.element = element; if (this.element) { this.element.addEventListener("pointerdown", this.onPointerDown); this.element.addEventListener("pointermove", this.onPointerMove); } } onPointerDown = (e: PointerEvent) => { if (!this.element) { return; } this.finishDragging(undefined, true); const el1 = closestBySelector( e.target, ".eez-flow-editor-capture-pointers" ); const el2 = closestBySelector( e.target, ".eez-flow-editor-not-capture-pointers" ); if (el1 && (!el2 || el2.contains(el1))) { return; } // put in focus first parent with tabindex attribute const parentWithTabindex = closestBySelector( this.element, "[tabindex]" ); if (parentWithTabindex) { parentWithTabindex.focus(); } e.preventDefault(); e.stopPropagation(); this.element.addEventListener("pointerup", this.onPointerUp); this.element.addEventListener("pointercancel", this.onPointerCancel); window.addEventListener("keydown", this.onKeyDown); this.overlayElement = document.createElement("div"); this.overlayElement.id = DRAGGABLE_OVERLAY_ELEMENT_ID; this.overlayElement.style.position = "absolute"; this.overlayElement.style.left = "0"; this.overlayElement.style.top = "0"; this.overlayElement.style.width = "100%"; this.overlayElement.style.height = "100%"; this.overlayElement.style.backgroundColor = "transparent"; this.overlayElement.style.zIndex = "1000"; this.overlayElement.style.backgroundColor = "transparent"; this.overlayElement.style.cursor = this.cursor || "default"; this.overlayElement.addEventListener("pointermove", e => { this.finishDragging(e, true); }); this.overlayElement.addEventListener("wheel", this.onWheel, { passive: false }); this.savedBodyUserSelect = document.body.style.userSelect; document.body.style.userSelect = "none"; document.body.appendChild(this.overlayElement); this.capturedPointerId = e.pointerId; this.element.setPointerCapture(e.pointerId); this.dragging = true; this.lastDragMoveEvent = undefined; this.xDragStart = e.clientX; this.yDragStart = e.clientY; if (this.config.onDragStart) { this.params = this.config.onDragStart( e, this.xDragStart, this.yDragStart ); } else { this.params = undefined; } }; onPointerMove = (e: PointerEvent) => { if (this.dragging) { if (this.config.onDragMove) { this.lastDragMoveEvent = e; this.config.onDragMove( e, e.clientX - this.xDragStart, e.clientY - this.yDragStart, this.params ); } } else { if (this.config.onMove) { this.config.onMove(e); } } }; onPointerUp = (e: PointerEvent) => { this.finishDragging(e, false); }; onPointerCancel = (e: PointerEvent) => { this.finishDragging(e, true); }; onWheel = (e: WheelEvent) => { if (this.config.onDraggableWheel) { this.config.onDraggableWheel(e); } }; onKeyDown = (e: KeyboardEvent) => { if (e.key == "Escape") { e.preventDefault(); e.stopPropagation(); this.finishDragging(undefined, true); } else if (this.dragging) { if (this.config.onDragMove && this.lastDragMoveEvent) { let moveEvent: IPointerEvent = { clientX: this.lastDragMoveEvent.clientX, clientY: this.lastDragMoveEvent.clientY, movementX: this.lastDragMoveEvent.movementX, movementY: this.lastDragMoveEvent.movementY, ctrlKey: e.ctrlKey, shiftKey: e.shiftKey, timeStamp: e.timeStamp }; this.config.onDragMove( moveEvent as PointerEvent, moveEvent.clientX - this.xDragStart, moveEvent.clientY - this.yDragStart, this.params ); } } }; finishDragging = (e: PointerEvent | undefined, cancel: boolean) => { if (!this.dragging) { return; } if (this.config.onDragEnd) { try { this.config.onDragEnd(e, cancel, this.params); } catch {} } if (this.overlayElement) { this.overlayElement.remove(); } document.body.style.userSelect = this.savedBodyUserSelect || ""; if (this.element) { this.element.releasePointerCapture(this.capturedPointerId); this.element.removeEventListener("pointerup", this.onPointerUp); this.element.removeEventListener( "pointercancel", this.onPointerCancel ); } window.removeEventListener("keydown", this.onKeyDown); this.dragging = false; }; }