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, }; } |