UNPKG

@carbon/ibm-products

Version:
177 lines (175 loc) 5.54 kB
/** * Copyright IBM Corp. 2020, 2026 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ //#region src/global/js/utils/makeDraggable/makeDraggable.ts /** * Makes a given element draggable using a handle element. * @param {object} draggable - Configuration object for draggable behavior * @param {HTMLElement} draggable.el - The element to make draggable * @param {HTMLElement} [draggable.dragHandle] - Optional handle element for dragging * @param {boolean} [draggable.focusableDragHandle] - Whether the drag handle should be focusable * @param {number} [draggable.dragStep] - Step size for keyboard dragging (default: 10) * @param {number} [draggable.shiftDragStep] - Step size for keyboard dragging with Shift key (default: 50) */ 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" || e.key === " ") { e.preventDefault(); 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); dispatch("dragmove", { direction: "left", distance }); break; case "ArrowRight": currentX += distance; applyTransform(currentX, currentY); dispatch("dragmove", { direction: "right", distance }); break; case "ArrowUp": currentY -= distance; applyTransform(currentX, currentY); dispatch("dragmove", { direction: "up", distance }); break; case "ArrowDown": currentY += distance; applyTransform(currentX, currentY); dispatch("dragmove", { direction: "down", distance }); 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 exports.makeDraggable = makeDraggable;