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 | 211x 211x 211x 1x 211x 181x 211x 211x 211x 211x 208x 1x 3x 211x 211x | /**
* 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,
};
}
|