UNPKG

@serenity-is/sleekgrid

Version:

A modern Data Grid / Spreadsheet component

182 lines (151 loc) 7.03 kB
// Adapted from https://github.com/6pac/SlickGrid/blob/master/src/slick.interactions.ts to replace jquery.event.drag.js export interface DragItem { dragSource: HTMLElement | Document | null; dragHandle: HTMLElement | null; deltaX: number; deltaY: number; range: DragRange; target: HTMLElement; startX: number; startY: number; } 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 DragPosition { startX: number; startY: number; range: DragRange; } 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 originaldd: Partial<DragItem> = { dragSource: containerElement, dragHandle: null, }; function init() { if (containerElement) { containerElement.addEventListener('mousedown', userPressed); containerElement.addEventListener('touchstart', userPressed, { passive: true }); } } function executeDragCallbackWhenDefined(callback?: (e: DragEvent, dd: DragItem) => boolean | void, evt?: MouseEvent | Touch | TouchEvent | KeyboardEvent, dd?: DragItem) { if (typeof callback === 'function') { return callback(evt as DragEvent, dd); } } 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; const { target } = targetEvent; if (!options.allowDragFrom || (options.allowDragFrom && (element.matches(options.allowDragFrom)) || (options.allowDragFromClosest && element.closest(options.allowDragFromClosest)))) { originaldd.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; originaldd = Object.assign(originaldd, { deltaX, deltaY, startX, startY, target }); const result = executeDragCallbackWhenDefined(onDragInit as (e: DragEvent, dd: DragPosition) => boolean | void, event, originaldd as DragItem); 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; const { target } = targetEvent; if (!dragStarted) { originaldd = Object.assign(originaldd, { deltaX, deltaY, startX, startY, target }); executeDragCallbackWhenDefined(onDragStart, event, originaldd as DragItem); dragStarted = true; } originaldd = Object.assign(originaldd, { deltaX, deltaY, startX, startY, target }); executeDragCallbackWhenDefined(onDrag, event, originaldd as DragItem); } } 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) { const { target } = event; originaldd = Object.assign(originaldd, { target }); executeDragCallbackWhenDefined(onDragEnd, event, originaldd as DragItem); dragStarted = false; } } init(); return { destroy }; }