Source: drag.js

/**
 * Drag module
 * @module drag
 */

/**
 * Resets the position and styling of a draggable element.
 * 
 * @param {HTMLElement} interactable - The draggable element to reset.
 */
function resetInteractable(interactable) {
    interactable.style.position = "static";
    interactable.style.left = null;
    interactable.style.top = null;
    interactable.style.zIndex = null;
}

/**
 * Makes an element draggable, applying custom behavior for dragging and dropping.
 * 
 * @param {HTMLElement} interactable - The element to make draggable.
 * @param {function(HTMLElement): boolean} checkDrop - A callback function to check 
 * if the interactable is dropped in a valid drop zone. Should return `true` if valid.
 * @param {function(): boolean} isDraggable - A callback function to determine 
 * if the element can be dragged. Should return `true` if draggable.
 * 
 * @description
 * This function allows an element to be dragged across the screen with the mouse. 
 * During the drag, the element's style changes for visual feedback (e.g., scaling 
 * and reduced opacity). The `checkDrop` callback verifies if the drop is valid, 
 * resetting the element's position if it's not.
 * 
 * @example
 * // Make a patch draggable
 * makeInteractableDraggable(patchElement, checkDropCallback, () => isDraggable);
 */
export function makeInteractableDraggable(interactable, checkDrop, isDraggable) {
    interactable.addEventListener("mousedown", (e) => {
        if (isDraggable()) {
            let offsetX = e.clientX - interactable.offsetLeft;
            let offsetY = e.clientY - interactable.offsetTop;

            interactable.style.position = "absolute";
            interactable.style.zIndex = "1000";
            interactable.style.transition = "none";
            interactable.style.transform = "scale(1.2)";
            interactable.style.opacity = 0.8;

            /**
             * Handles the drag movement by updating the element's position.
             * 
             * @param {MouseEvent} e - The mouse move event.
             */
            function onMouseMove(e) {
                const x = e.clientX - offsetX;
                const y = e.clientY - offsetY;
                interactable.style.left = `${x}px`;
                interactable.style.top = `${y}px`;
            }

            /**
             * Handles the drop event, resetting or finalizing the element's position.
             */
            function onMouseUp() {
                interactable.style.transition = "all 0.3s ease";
                interactable.style.transform = "scale(1)";
                interactable.style.opacity = 1;

                if (!checkDrop(interactable)) {
                    resetInteractable(interactable);
                }

                document.removeEventListener("mousemove", onMouseMove);
                document.removeEventListener("mouseup", onMouseUp);
            }

            document.addEventListener("mousemove", onMouseMove);
            document.addEventListener("mouseup", onMouseUp);
        }
    });
}