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