@carbon/ibm-products
Version:
Carbon for IBM Products
177 lines (175 loc) • 5.53 kB
JavaScript
/**
* 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
export { makeDraggable };