UNPKG

@carbon/utilities

Version:

Utilities and helpers to drive consistency across software products using the Carbon Design System

148 lines (147 loc) 4.6 kB
//#region src/makeDraggable/makeDraggable.ts /** * Makes a given element draggable using a handle element. *@param draggable - object which accepts el and optional attributes handle,focusableInHandle,dragStep and shiftDragStep */ const makeDraggable = ({ el, dragHandle, focusableDragHandle, dragStep, shiftDragStep }) => { if (dragHandle) { dragHandle.style.cursor = "move"; el.style.cursor = "default"; } else el.style.cursor = "move"; let isDragging = false; let currentX = 0; let currentY = 0; let initialMouseX = 0; let initialMouseY = 0; let baseMatrix = null; /** * Syncs position and extracts base matrix from computed style. * Reads from getComputedStyle() to include all transform sources (inline, classes, etc.). */ const syncTransformState = () => { const transformString = window.getComputedStyle(el).transform; if (!transformString || transformString === "none") { currentX = 0; currentY = 0; baseMatrix = null; return; } const matrix = new DOMMatrix(transformString); currentX = matrix.m41; currentY = matrix.m42; if (matrix.a === 1 && matrix.b === 0 && matrix.c === 0 && matrix.d === 1 && matrix.e === 0 && matrix.f === 0) baseMatrix = null; else baseMatrix = new DOMMatrix([ matrix.a, matrix.b, matrix.c, matrix.d, 0, 0 ]); }; syncTransformState(); /** * Applies transform by combining translation with base matrix using matrix multiplication. */ const applyTransform = (x, y) => { if (baseMatrix) { const translationMatrix = new DOMMatrix(); translationMatrix.m41 = x; translationMatrix.m42 = y; const combined = translationMatrix.multiply(baseMatrix); el.style.transform = combined.toString(); } else el.style.transform = `translate(${x}px, ${y}px)`; }; const dispatch = (type, detail) => { const eventInit = { detail, bubbles: true }; el.dispatchEvent(new CustomEvent(type, eventInit)); }; const onKeyDown = (e) => { if (e.key === "Enter") { isDragging = !isDragging; if (isDragging) { syncTransformState(); dispatch("dragstart", { keyboard: true }); } else dispatch("dragend", { keyboard: true }); } if (!isDragging) return; const distance = e.shiftKey ? shiftDragStep ?? 32 : dragStep ?? 8; switch (e.key) { case "Enter": case " ": e.preventDefault(); break; case "ArrowLeft": currentX -= distance; applyTransform(currentX, currentY); break; case "ArrowRight": currentX += distance; applyTransform(currentX, currentY); break; case "ArrowUp": currentY -= distance; applyTransform(currentX, currentY); break; case "ArrowDown": currentY += distance; applyTransform(currentX, currentY); break; } }; const onMouseDown = (e) => { const target = e.target; if (!(target instanceof Node)) return; if (!(dragHandle ? dragHandle.contains(target) : el.contains(target))) return; syncTransformState(); initialMouseX = e.clientX; initialMouseY = e.clientY; isDragging = true; dispatch("dragstart", { mouse: true }); document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp, { once: true }); }; const onMouseMove = (e) => { if (!isDragging) return; const dx = e.clientX - initialMouseX; const dy = e.clientY - initialMouseY; applyTransform(currentX + dx, currentY + dy); }; const onMouseUp = (e) => { if (!isDragging) return; const dx = e.clientX - initialMouseX; const dy = e.clientY - initialMouseY; currentX += dx; currentY += dy; isDragging = false; dispatch("dragend", { mouse: true }); document.removeEventListener("mousemove", onMouseMove); }; if (dragHandle) dragHandle.addEventListener("mousedown", onMouseDown); else el.addEventListener("mousedown", onMouseDown); focusableDragHandle?.addEventListener("keydown", onKeyDown); const draggableCleanup = () => { if (dragHandle) dragHandle.removeEventListener("mousedown", onMouseDown); else el.removeEventListener("mousedown", onMouseDown); focusableDragHandle?.removeEventListener("keydown", onKeyDown); document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); }; /** * Re-initialize the draggable position from the element's current computed transform. * Call this if the element has been repositioned externally (e.g., via CSS animation, * class changes, or other scripts) to prevent position jumps on the next drag. */ const init = () => { syncTransformState(); }; return { cleanup: draggableCleanup, init }; }; //#endregion export { makeDraggable };