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

99.06% Statements 106/107
30% Branches 3/10
50% Functions 1/2
99.06% Lines 106/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 1081x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x   15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x  
/**
 * 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,
    };
}