All files / blong-browser/src/design useDesignable.ts

0% Statements 0/107
0% Branches 0/1
0% Functions 0/1
0% Lines 0/107

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108                                                                                                                                                                                                                       
/**
 * useDesignable — makes any component drag-and-drop and selection-aware in design mode.
 * Returns inert values when design mode is inactive — zero cost at runtime.
 *
 * Uses useDraggable + useDroppable instead of useSortable so that:
 * - No automatic sort animation happens (explicit, predictable UX).
 * - Drag data (type, label, sourceId, ...) can be passed to DropZone for validation.
 */
import {useDraggable, useDroppable} from '@dnd-kit/core';
import {useCallback, useMemo} from 'react';
import type {DesignElementType, IDesignElement} from './DesignModeContext.js';
import {useDesignMode} from './useDesignMode.js';

export interface IDesignableResult {
    isSelected: boolean;
    select: () => void;
    /** Drag event handlers (pointer listeners + aria attributes). Spread onto the drag element.
     *  Does NOT include a ref — use setRef for that. */
    dragProps: Record<string, unknown>;
    /** Combined drag+drop ref. Attach to the outermost DOM element of the draggable component.
     *  Null when design mode is inactive. */
    setRef: ((node: HTMLElement | null) => void) | null;
    designClass: string;
    style?: React.CSSProperties;
}

/**
 * @param id        - Unique drag/drop id (e.g. 'card-tree')
 * @param type      - Element type
 * @param extraData - Additional data stored in active.data.current during drag.
 *                    Include at minimum: { label, sourceId } so DropZone can validate.
 */
export function useDesignable(
    id: string,
    type: DesignElementType,
    extraData?: Record<string, unknown>,
): IDesignableResult {
    const {active, selected, select} = useDesignMode();

    const isSelected = selected?.id === id;

    const selectSelf = useCallback(() => {
        select({id, type} as IDesignElement);
    }, [id, type, select]);

    const data = useMemo(
        () => ({type, ...extraData}),
        // eslint-disable-next-line @eslint-react/exhaustive-deps
        [type, JSON.stringify(extraData)],
    );

    // Draggable — picks up the element
    const {
        attributes,
        listeners,
        setNodeRef: setDragRef,
        isDragging,
    } = useDraggable({
        id,
        disabled: !active,
        data,
    });

    // Droppable — same element receives other dragged items (drop-before behaviour)
    const {setNodeRef: setDropRef} = useDroppable({id, disabled: !active});

    // Combine both refs onto the same DOM node
    const setRef = useCallback(
        (node: Element | null) => {
            setDragRef(node as HTMLElement | null);
            setDropRef(node as HTMLElement | null);
        },
        [setDragRef, setDropRef],
    );

    if (!active) {
        return {
            isSelected: false,
            select: () => undefined,
            dragProps: {},
            setRef: null,
            designClass: '',
        };
    }

    const style: React.CSSProperties = {
        opacity: isDragging ? 0.4 : 1,
        // No CSS transform — the DragOverlay shows the ghost instead
    };

    const designClass = [
        'blong-designable',
        isSelected ? 'blong-designable--selected' : '',
        isDragging ? 'blong-designable--dragging' : '',
    ]
        .filter(Boolean)
        .join(' ');

    return {
        isSelected,
        select: selectSelf,
        dragProps: {...attributes, ...listeners},
        setRef,
        designClass,
        style,
    };
}