// Adapted from https://github.com/6pac/SlickGrid/blob/master/src/slick.interactions.ts to replace jquery.event.drag export interface DragPosition { startX: number; startY: number; range: DragRange; } export interface DragItem extends DragPosition { dragSource: HTMLElement | Document | null; dragHandle: HTMLElement | null; deltaX: number; deltaY: number; dragTarget: HTMLElement; } export interface DragRange { start: { row?: number; cell?: number; }; end: { row?: number; cell?: number; }; } function windowScrollPosition() { return { left: window.scrollX ?? document.documentElement.scrollLeft ?? 0, top: window.scrollY ?? document.documentElement.scrollTop ?? 0 }; } export interface DraggableOption { /** container DOM element, defaults to "document" */ containerElement?: HTMLElement | Document; /** when defined, will allow dragging from a specific element by using the .matches() query selector. */ allowDragFrom?: string; /** when defined, will allow dragging from a specific element or its closest parent by using the .closest() query selector. */ allowDragFromClosest?: string; /** Defaults to `['ctrlKey', 'metaKey']`, list of keys that when pressed will prevent Draggable events from triggering (e.g. prevent onDrag when Ctrl key is pressed while dragging) */ preventDragFromKeys?: Array<'altKey' | 'ctrlKey' | 'metaKey' | 'shiftKey'>; /** drag initialized callback */ onDragInit?: (e: DragEvent, dd: DragPosition) => boolean | void; /** drag started callback */ onDragStart?: (e: DragEvent, dd: DragPosition) => boolean | void; /** drag callback */ onDrag?: (e: DragEvent, dd: DragPosition) => boolean | void; /** drag ended callback */ onDragEnd?: (e: DragEvent, dd: DragPosition) => boolean | void; } export function Draggable(options: DraggableOption) { let { containerElement } = options; const { onDragInit, onDragStart, onDrag, onDragEnd, preventDragFromKeys } = options; let element: HTMLElement | null; let startX: number; let startY: number; let deltaX: number; let deltaY: number; let dragStarted: boolean; if (!containerElement) { containerElement = document.body; } let dragData: Partial = { dragSource: containerElement, dragHandle: null, }; function init() { if (containerElement) { containerElement.addEventListener('mousedown', userPressed); containerElement.addEventListener('touchstart', userPressed, { passive: true }); } } function executeDragCallbackWhenDefined(callback?: (e: UIEvent, dragData: DragPosition) => boolean | void, evt?: MouseEvent | TouchEvent | KeyboardEvent, dragData?: Partial) { if (typeof callback === 'function') { return callback(evt, dragData as DragPosition); } } function destroy() { if (containerElement) { containerElement.removeEventListener('mousedown', userPressed); containerElement.removeEventListener('touchstart', userPressed); } } /** Do we want to prevent Drag events from happening (for example prevent onDrag when Ctrl key is pressed while dragging) */ function preventDrag(event: MouseEvent | TouchEvent | KeyboardEvent) { let eventPrevented = false; if (preventDragFromKeys) { preventDragFromKeys.forEach(key => { if ((event as any)[key]) { eventPrevented = true; } }); } return eventPrevented; } function userPressed(event: MouseEvent | TouchEvent | KeyboardEvent) { if (!preventDrag(event)) { element = event.target as HTMLElement; const targetEvent: MouseEvent | Touch = ((event as TouchEvent)?.touches?.[0] ?? event) as any; if (!options.allowDragFrom || (options.allowDragFrom && (element.matches(options.allowDragFrom)) || (options.allowDragFromClosest && element.closest(options.allowDragFromClosest)))) { dragData.dragHandle = element as HTMLElement; const winScrollPos = windowScrollPosition(); startX = winScrollPos.left + targetEvent.clientX; startY = winScrollPos.top + targetEvent.clientY; deltaX = targetEvent.clientX - targetEvent.clientX; deltaY = targetEvent.clientY - targetEvent.clientY; dragData = Object.assign(dragData, { deltaX, deltaY, startX, startY, dragTarget: targetEvent.target }); const result = executeDragCallbackWhenDefined(onDragInit, event, dragData); if (result !== false) { document.body.addEventListener('mousemove', userMoved); document.body.addEventListener('touchmove', userMoved, { passive: true }); document.body.addEventListener('mouseup', userReleased); document.body.addEventListener('touchend', userReleased, { passive: true }); document.body.addEventListener('touchcancel', userReleased, { passive: true }); } } } } function userMoved(event: MouseEvent | TouchEvent | KeyboardEvent) { if (!preventDrag(event)) { const targetEvent: MouseEvent | Touch = (event as TouchEvent)?.touches?.[0] ?? event as any; deltaX = targetEvent.clientX - startX; deltaY = targetEvent.clientY - startY; if (!dragStarted) { dragData = Object.assign(dragData, { deltaX, deltaY, startX, startY, dragTarget: targetEvent.target }); executeDragCallbackWhenDefined(onDragStart, event, dragData); dragStarted = true; } dragData = Object.assign(dragData, { deltaX, deltaY, startX, startY, dragTarget: targetEvent.target }); executeDragCallbackWhenDefined(onDrag, event, dragData); } } function userReleased(event: MouseEvent | TouchEvent) { document.body.removeEventListener('mousemove', userMoved); document.body.removeEventListener('touchmove', userMoved); document.body.removeEventListener('mouseup', userReleased); document.body.removeEventListener('touchend', userReleased); document.body.removeEventListener('touchcancel', userReleased); // trigger a dragEnd event only after dragging started and stopped if (dragStarted) { dragData = Object.assign(dragData, { dragTarget: event.target }); executeDragCallbackWhenDefined(onDragEnd, event, dragData as DragItem); dragStarted = false; } } init(); return { destroy }; }