1- import React , { useEffect } from "react" ;
1+ import React , { useEffect , useMemo , useState , createContext } from "react" ;
22import { Block , BlockDefinition } from "./components/Blocks/Definition" ;
33import { useFormBuilderStore } from "./stores/block.store" ;
44import { createBlockFromTemplate } from "./utilities/block.utiles" ;
5- import { AddMenu } from "./components/AddMenu" ;
65import { BLOCK_COMPONENTS } from "./components/BlockRegistry" ;
76import { Empty } from "./components/Empty" ;
87
8+ import {
9+ DndContext ,
10+ DragStartEvent ,
11+ DragEndEvent ,
12+ DragOverlay ,
13+ PointerSensor ,
14+ useSensor ,
15+ useSensors ,
16+ closestCenter ,
17+ UniqueIdentifier ,
18+ } from "@dnd-kit/core" ;
19+
20+ import {
21+ SortableContext ,
22+ useSortable ,
23+ verticalListSortingStrategy ,
24+ arrayMove ,
25+ } from "@dnd-kit/sortable" ;
26+
27+ import { CSS } from "@dnd-kit/utilities" ;
28+ import { AddMenu } from "./components/AddMenu" ;
29+
930type Props = {
10- onChange : ( blocks : Block [ ] ) => void
31+ onChange : ( blocks : Block [ ] ) => void ;
1132 json : Block [ ] ;
12- }
33+ } ;
34+
35+ export const DragHandleContext = createContext < {
36+ attributes ?: Record < string , any > ;
37+ listeners ?: Record < string , any > ;
38+ setActivatorNodeRef ?: ( node : HTMLElement | null ) => void ;
39+ isDragging ?: boolean ;
40+ } > ( { } ) ;
41+
42+ const SortableItem : React . FC < { id : UniqueIdentifier ; children : React . ReactNode } > = (
43+ {
44+ id,
45+ children,
46+ } ) => {
47+ const {
48+ attributes,
49+ listeners,
50+ setNodeRef,
51+ setActivatorNodeRef,
52+ transform,
53+ transition,
54+ isDragging,
55+ } = useSortable ( { id} ) ;
56+
57+ const style : React . CSSProperties = {
58+ transform : CSS . Transform . toString ( transform ) ,
59+ transition,
60+ opacity : isDragging ? 0.6 : 1 ,
61+ } ;
62+
63+ return (
64+ < DragHandleContext . Provider
65+ value = { { attributes, listeners, setActivatorNodeRef, isDragging} }
66+ >
67+ < div
68+ ref = { setNodeRef }
69+ style = { style }
70+ data-block-id = { String ( id ) }
71+ >
72+ { children }
73+ </ div >
74+ </ DragHandleContext . Provider >
75+ ) ;
76+ } ;
1377
1478export const FormBuilder = ( { onChange, json} : Props ) => {
1579 const { blocks, addBlock, setBlocks} = useFormBuilderStore ( ) ;
80+ const [ activeId , setActiveId ] = useState < UniqueIdentifier | null > ( null ) ;
81+ const [ activeSize , setActiveSize ] = useState < { width : number ; height : number } | null > ( null ) ;
82+
83+ const sensors = useSensors (
84+ useSensor ( PointerSensor , {
85+ activationConstraint : {
86+ distance : 2 ,
87+ } ,
88+ } )
89+ ) ;
1690
1791 const handleAddAt = ( afterIndex : number , def : BlockDefinition ) => {
1892 const newBlock = createBlockFromTemplate ( def ) ;
@@ -24,35 +98,118 @@ export const FormBuilder = ({onChange, json}: Props) => {
2498 } , [ ] ) ;
2599
26100 useEffect ( ( ) => {
27- console . log ( blocks ) ;
28101 onChange ( blocks ) ;
29102 } , [ blocks ] ) ;
30103
104+ const ids = useMemo ( ( ) => blocks . map ( ( b ) => b . id ) , [ blocks ] ) ;
105+
106+ const onDragStart = ( event : DragStartEvent ) => {
107+ setActiveId ( event . active . id ) ;
108+
109+ const el = document . querySelector < HTMLElement > ( `[data-block-id="${ String ( event . active . id ) } "]` ) ;
110+ if ( el ) {
111+ const rect = el . getBoundingClientRect ( ) ;
112+ setActiveSize ( { width : rect . width , height : rect . height } ) ;
113+ } else {
114+ setActiveSize ( null ) ;
115+ }
116+ } ;
117+
118+ const onDragEnd = ( event : DragEndEvent ) => {
119+ const { active, over} = event ;
120+
121+ if ( over && active . id !== over . id ) {
122+ const oldIndex = blocks . findIndex ( ( b ) => b . id === active . id ) ;
123+ const newIndex = blocks . findIndex ( ( b ) => b . id === over . id ) ;
124+
125+ if ( oldIndex !== - 1 && newIndex !== - 1 ) {
126+ const reordered = arrayMove ( blocks , oldIndex , newIndex ) ;
127+ setBlocks ( reordered ) ;
128+ }
129+ }
130+
131+ setActiveId ( null ) ;
132+ setActiveSize ( null ) ;
133+ } ;
134+
135+ const onDragCancel = ( ) => {
136+ setActiveId ( null ) ;
137+ setActiveSize ( null ) ;
138+ } ;
139+
31140 return (
32- < div className = "border border-dashed border-gray-200 rounded-lg p-4 space-y-4" >
33- { blocks . length === 0 ? (
34- < Empty onPick = { ( def ) => handleAddAt ( - 1 , def ) } />
35- ) : (
36- < >
37- {
38- blocks . map ( ( block , idx ) => {
141+ < DndContext
142+ sensors = { sensors }
143+ collisionDetection = { closestCenter }
144+ onDragStart = { onDragStart }
145+ onDragEnd = { onDragEnd }
146+ onDragCancel = { onDragCancel }
147+ >
148+ < div className = "border border-dashed border-gray-200 rounded-lg p-4 space-y-4" >
149+ { blocks . length === 0 ? (
150+ < Empty onPick = { ( def ) => handleAddAt ( - 1 , def ) } />
151+ ) : (
152+ < SortableContext items = { ids } strategy = { verticalListSortingStrategy } >
153+ { blocks . map ( ( block , idx ) => {
39154 const Component = BLOCK_COMPONENTS [ block . type ] ;
40-
41155 if ( ! Component ) {
42156 console . error ( `Composant non trouvé pour le type: ${ block . type } ` ) ;
43157 return null ;
44158 }
45-
46159 return (
47- < Component { ...block } key = { idx } />
48- ) ;
49- } )
50- }
51- </ >
52- ) }
53- </ div >
54- )
55- ;
160+ < SortableItem key = { block . id } id = { block . id } >
161+ < div className = "group relative" >
162+
163+ < div
164+ className = "absolute -right-3 -top-3 opacity-0 group-hover:opacity-100 transition-opacity" >
165+ < AddMenu
166+ onPick = { ( def ) => handleAddAt ( idx , def ) }
167+ placeholder = "Rechercher un type…"
168+ placement = { 'right' }
169+ >
170+ < button
171+ type = "button"
172+ className = "rounded-full border border-gray-300 bg-appolo-700 shadow-sm p-2 hover:bg-appolo-500 cursor-pointer"
173+ title = "Ajouter un bloc"
174+ >
175+ < svg width = "16" height = "16" viewBox = "0 0 24 24"
176+ className = "text-white" >
177+ < path fill = "currentColor"
178+ d = "M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z" />
179+ </ svg >
180+ </ button >
181+ </ AddMenu >
182+
183+ </ div >
184+
185+ < Component { ...block } />
186+ </ div >
187+ </ SortableItem >
188+ )
189+ ;
190+ } ) }
191+ </ SortableContext >
192+ ) }
193+ </ div >
194+
195+ < DragOverlay dropAnimation = { null } >
196+ { activeId && activeSize ? (
197+ < div
198+ className = "bg-appolo-50"
199+ style = { {
200+ width : activeSize . width ,
201+ height : activeSize . height ,
202+ borderRadius : 8 ,
203+ background : "" ,
204+ opacity : .6 ,
205+ boxShadow :
206+ "0 4px 14px rgba(0,0,0,0.12), 0 2px 6px rgba(0,0,0,0.08)" ,
207+ } }
208+ />
209+ ) : null }
210+ </ DragOverlay >
211+ </ DndContext >
212+ ) ;
56213} ;
57214
58215export default FormBuilder ;
0 commit comments