# Système de Drag & Drop Unifié ## 🎯 Objectif **Un seul système de drag & drop pour TOUS les blocs**, qu'ils soient en pleine largeur ou dans des colonnes, avec indicateur visuel unifié (flèche bleue). ## ✅ Fonctionnalités Implémentées ### 1. Drag & Drop Unifié **Tous les blocs utilisent DragDropService:** - ✅ Blocs pleine largeur → Autre position pleine largeur - ✅ Blocs pleine largeur → Colonne (n'importe quelle colonne) - ✅ Bloc de colonne → Autre colonne - ✅ Bloc de colonne → Pleine largeur - ✅ Bloc de colonne → Même colonne (réorganisation) ### 2. Indicateur Visuel avec Flèche Bleue **Deux modes d'indicateur:** #### Mode Horizontal (Changement de ligne) ``` aaa ─────────────────► ◄───────────────── (Ligne bleue avec flèches) bbb ``` - Utilisé pour réorganiser des blocs verticalement - Flèches gauche et droite - Couleur: `rgba(56, 189, 248, 0.9)` (bleu) #### Mode Vertical (Création/Ajout dans colonne) ``` ▲ │ (Ligne bleue verticale avec flèches) aaa │ bbb │ ▼ ``` - Utilisé pour créer des colonnes ou ajouter à une colonne existante - Flèches haut et bas - Couleur: `rgba(56, 189, 248, 0.9)` (bleu) ### 3. Flexibilité Totale **Image 2 - Tous les cas supportés:** ``` ┌─────────┐ ┌─────────┐ ┌─────────┐ │ H2 │ 1 │ │ H2 │ 1 │ │ H2 │ 1 │ (Colonnes multiples) └─────────┘ └─────────┘ └─────────┘ ┌─────────┐ ┌─────────┐ │ H2 │ │ H2 │ 1 │ (Mix colonnes + blocs) └─────────┘ └─────────┘ ┌────────────────────────────────────────┐ │ H2 │ (Pleine largeur) └────────────────────────────────────────┘ ``` **Tous les déplacements possibles:** 1. Drag n'importe quel bloc H2 vers n'importe quelle position 2. Créer des colonnes en droppant sur les bords 3. Convertir colonnes → pleine largeur en droppant hors des colonnes 4. Réorganiser dans une même colonne ## 🔧 Architecture Technique ### Service Central: DragDropService **Responsabilités:** - Tracker l'état du drag (`dragging`, `sourceId`, `fromIndex`, `overIndex`) - Calculer la position de l'indicateur (`indicator`) - Détecter le mode de drop (`line`, `column-left`, `column-right`) **Signaux:** ```typescript readonly dragging = signal(false); readonly sourceId = signal(null); readonly fromIndex = signal(-1); readonly overIndex = signal(-1); readonly indicator = signal(null); readonly dropMode = signal<'line' | 'column-left' | 'column-right'>('line'); ``` **Méthodes:** ```typescript beginDrag(id: string, index: number, clientY: number) updatePointer(clientY: number, clientX?: number) endDrag() → { from, to, moved, mode } ``` ### Composants Intégrés #### 1. block-host.component.ts (Blocs Pleine Largeur) **Drag Start:** ```typescript onDragStart(event: MouseEvent): void { this.dragDrop.beginDrag(this.block.id, this.index, event.clientY); const onMove = (e: MouseEvent) => { this.dragDrop.updatePointer(e.clientY, e.clientX); }; const onUp = (e: MouseEvent) => { const { from, to, moved, mode } = this.dragDrop.endDrag(); // Check if dropping into a column const target = document.elementFromPoint(e.clientX, e.clientY); const columnEl = target.closest('[data-column-id]'); if (columnEl) { // Insert into column this.insertIntoColumn(colIndex, blockIndex); } else if (mode === 'column-left' || mode === 'column-right') { // Create new columns this.createColumns(mode, targetBlock); } else { // Regular line move this.documentService.moveBlock(this.block.id, toIndex); } }; } ``` **Détection de Drop dans Colonne:** ```typescript // Check if dropping into a column const columnEl = target.closest('[data-column-id]'); if (columnEl) { const colIndex = parseInt(columnEl.getAttribute('data-column-index') || '0'); const columnsBlockId = columnEl.closest('.block-wrapper[data-block-id]') ?.getAttribute('data-block-id'); // Insert block into column const blockCopy = JSON.parse(JSON.stringify(this.block)); columns[colIndex].blocks.push(blockCopy); // Delete original this.documentService.deleteBlock(this.block.id); } ``` #### 2. columns-block.component.ts (Blocs dans Colonnes) **Drag Start:** ```typescript onDragStart(block: Block, columnIndex: number, blockIndex: number, event: MouseEvent): void { // Store source this.draggedBlock = { block, columnIndex, blockIndex }; // Use DragDropService const virtualIndex = this.getVirtualIndex(columnIndex, blockIndex); this.dragDrop.beginDrag(block.id, virtualIndex, event.clientY); const onMove = (e: MouseEvent) => { this.dragDrop.updatePointer(e.clientY, e.clientX); }; const onUp = (e: MouseEvent) => { const { moved } = this.dragDrop.endDrag(); const target = document.elementFromPoint(e.clientX, e.clientY); const blockEl = target?.closest('[data-block-id]'); if (blockEl) { // Move within columns const targetColIndex = parseInt(blockEl.getAttribute('data-column-index') || '0'); const targetBlockIndex = parseInt(blockEl.getAttribute('data-block-index') || '0'); this.moveBlock(fromCol, fromBlock, targetColIndex, targetBlockIndex); } else { // Convert to full-width this.convertToFullWidth(columnIndex, blockIndex); } }; } ``` **Conversion vers Pleine Largeur:** ```typescript private convertToFullWidth(colIndex: number, blockIndex: number): void { const blockToMove = column.blocks[blockIndex]; // Insert as full-width after columns block const blockCopy = JSON.parse(JSON.stringify(blockToMove)); this.documentService.insertBlock(this.block.id, blockCopy); // Remove from column updatedColumns[colIndex].blocks = column.blocks.filter((_, i) => i !== blockIndex); // Redistribute widths or delete if empty if (nonEmptyColumns.length === 0) { this.documentService.deleteBlock(this.block.id); } else if (nonEmptyColumns.length === 1) { // Convert single column to full-width blocks } else { // Update with redistributed widths const newWidth = 100 / nonEmptyColumns.length; } } ``` #### 3. editor-shell.component.ts (Indicateur Visuel) **Template:** ```html @if (dragDrop.dragging() && dragDrop.indicator()) { @if (dragDrop.indicator()!.mode === 'horizontal') {
} @else {
} } ``` **Styles:** ```css .drop-indicator { position: absolute; pointer-events: none; z-index: 1000; } /* Horizontal indicator */ .drop-indicator.horizontal { height: 3px; background: rgba(56, 189, 248, 0.9); } .drop-indicator.horizontal .arrow.left { left: 0; border-top: 8px solid transparent; border-bottom: 8px solid transparent; border-right: 12px solid rgba(56, 189, 248, 0.9); } .drop-indicator.horizontal .arrow.right { right: 0; border-top: 8px solid transparent; border-bottom: 8px solid transparent; border-left: 12px solid rgba(56, 189, 248, 0.9); } /* Vertical indicator */ .drop-indicator.vertical { width: 3px; background: rgba(56, 189, 248, 0.9); } .drop-indicator.vertical .arrow.top { top: 0; border-left: 8px solid transparent; border-right: 8px solid transparent; border-bottom: 12px solid rgba(56, 189, 248, 0.9); } .drop-indicator.vertical .arrow.bottom { bottom: 0; border-left: 8px solid transparent; border-right: 8px solid transparent; border-top: 12px solid rgba(56, 189, 248, 0.9); } ``` ## 📊 Flux de Données ### Cas 1: Bloc Pleine Largeur → Colonne ``` 1. User drags bloc pleine largeur ↓ 2. onDragStart() in block-host.component.ts → dragDrop.beginDrag() ↓ 3. User moves mouse → dragDrop.updatePointer() → indicator position calculated → Blue arrow displayed ↓ 4. User drops on column → document.elementFromPoint() → target.closest('[data-column-id]') → Found column! ↓ 5. Insert bloc into column → blockCopy created → columns[colIndex].blocks.push(blockCopy) → documentService.updateBlockProps() → documentService.deleteBlock(originalId) ↓ 6. UI updates → Block appears in column → Original block removed ``` ### Cas 2: Bloc de Colonne → Pleine Largeur ``` 1. User drags bloc in column ↓ 2. onDragStart() in columns-block.component.ts → draggedBlock stored → dragDrop.beginDrag() ↓ 3. User moves mouse → dragDrop.updatePointer() → indicator displayed ↓ 4. User drops outside columns → target.closest('[data-column-id]') = null → isOutsideColumns = true ↓ 5. convertToFullWidth() → blockCopy created → documentService.insertBlock(after columnsBlock) → Remove from column → Redistribute widths or delete empty columns ↓ 6. UI updates → Block appears as full-width → Column updated or removed ``` ### Cas 3: Colonne → Colonne ``` 1. User drags bloc in column A ↓ 2. onDragStart() in columns-block.component.ts → draggedBlock = { block, columnIndex: A, blockIndex: X } ↓ 3. User drops on bloc in column B → target.closest('[data-block-id]') → data-column-index = B → data-block-index = Y ↓ 4. moveBlock(A, X, B, Y) → Remove from column A → Insert into column B at position Y → Redistribute widths if needed ↓ 5. UI updates → Block appears in column B → Column A updated ``` ## 🔍 Attributs Data Nécessaires ### Bloc Pleine Largeur ```html
``` ### Bloc dans Colonne ```html
``` ### Colonne ```html
``` ### Bloc Colonnes ```html
``` ## 🧪 Tests à Effectuer ### Test 1: Pleine Largeur → Colonne ``` 1. Créer un bloc H2 en pleine largeur 2. Créer 2 colonnes avec des blocs 3. Drag le bloc H2 vers colonne 1 ✅ Vérifier: Flèche bleue verticale apparaît ✅ Vérifier: Bloc H2 apparaît dans colonne 1 ✅ Vérifier: Original H2 supprimé ``` ### Test 2: Colonne → Pleine Largeur ``` 1. Créer 2 colonnes avec des blocs 2. Drag un bloc de colonne 1 vers zone pleine largeur (hors colonnes) ✅ Vérifier: Flèche bleue horizontale apparaît ✅ Vérifier: Bloc devient pleine largeur ✅ Vérifier: Colonne 1 mise à jour ✅ Vérifier: Si colonne vide, largeur redistribuée ``` ### Test 3: Colonne A → Colonne B ``` 1. Créer 3 colonnes avec plusieurs blocs 2. Drag un bloc de colonne 1 vers colonne 2 ✅ Vérifier: Flèche bleue apparaît dans colonne 2 ✅ Vérifier: Bloc apparaît dans colonne 2 à la position du drop ✅ Vérifier: Bloc supprimé de colonne 1 ``` ### Test 4: Réorganisation dans Même Colonne ``` 1. Créer une colonne avec 4 blocs (pos 0,1,2,3) 2. Drag bloc pos 0 vers pos 2 ✅ Vérifier: Flèche bleue apparaît entre blocs ✅ Vérifier: Bloc se déplace correctement ✅ Vérifier: Ordre: 1,0,2,3 ``` ### Test 5: Création de Colonnes (Existant) ``` 1. Créer 2 blocs H2 pleine largeur 2. Drag bloc 1 vers bord gauche/droit de bloc 2 ✅ Vérifier: Flèche bleue verticale apparaît sur le bord ✅ Vérifier: Colonnes créées avec les 2 blocs ✅ Vérifier: Largeur 50/50 ``` ### Test 6: Types de Blocs Variés ``` 1. Créer colonnes avec: Heading, Paragraph, Code, Image, Table 2. Drag chaque type vers: - Autre colonne - Pleine largeur - Même colonne (réorganisation) ✅ Vérifier: Tous les types fonctionnent ✅ Vérifier: Aucune perte de données ✅ Vérifier: Styles préservés ``` ### Test 7: Indicateur Visuel ``` 1. Drag un bloc (colonne ou pleine largeur) 2. Observer pendant le mouvement ✅ Vérifier: Flèche bleue toujours visible ✅ Vérifier: Position correcte (suit la souris) ✅ Vérifier: Mode horizontal vs vertical selon contexte ✅ Vérifier: Flèches aux extrémités ``` ## 📈 Comparaison Avant/Après | Aspect | Avant | Après | |--------|-------|-------| | **Systèmes de drag** | 2 séparés | 1 unifié ✅ | | **Indicateur visuel** | Aucun | Flèche bleue ✅ | | **Pleine largeur → Colonne** | ❌ Non supporté | ✅ Fonctionnel | | **Colonne → Pleine largeur** | ❌ Non supporté | ✅ Fonctionnel | | **Colonne → Colonne** | ⚠️ Basique | ✅ Complet | | **Réorganisation colonne** | ⚠️ Basique | ✅ Complet | | **Feedback utilisateur** | ❌ Aucun | ✅ Flèche bleue | | **Consistance** | ❌ Différent | ✅ Identique | ## ✅ Avantages du Système Unifié ### 1. Expérience Utilisateur - ✅ **Intuitive** - Un seul comportement pour tous les blocs - ✅ **Feedback visuel** - Flèche bleue indique où le bloc sera placé - ✅ **Flexibilité** - Aucune restriction artificielle - ✅ **Consistance** - Même mécanique partout ### 2. Architecture - ✅ **DRY** - Un seul service (DragDropService) - ✅ **Maintenable** - Logique centralisée - ✅ **Évolutif** - Facile d'ajouter de nouveaux types de blocs - ✅ **Testable** - Service isolé ### 3. Performance - ✅ **Optimisé** - Signals Angular pour réactivité - ✅ **Pas de polling** - Event-driven - ✅ **Pas de duplication** - Code partagé ## 🚀 Utilisation ### Pour l'Utilisateur Final **Drag & Drop Universel:** 1. Hover sur n'importe quel bloc → Bouton ⋯ apparaît 2. Cliquer et maintenir sur ⋯ → Curseur devient "grabbing" 3. Déplacer la souris → **Flèche bleue** indique la position de drop 4. Relâcher → Bloc placé à la position indiquée **Scénarios:** - Drag vers espace vide → Nouveau bloc pleine largeur - Drag vers bord gauche/droit d'un bloc → Crée des colonnes - Drag vers une colonne existante → Ajoute dans la colonne - Drag hors des colonnes → Convertit en pleine largeur - Drag dans même colonne → Réorganise ### Pour les Développeurs **Ajouter un nouveau type de bloc avec drag:** ```typescript // 1. Utiliser le même pattern dans le template // 2. Implémenter onDragStart onDragStart(event: MouseEvent): void { this.dragDrop.beginDrag(this.block.id, this.index, event.clientY); const onMove = (e: MouseEvent) => { this.dragDrop.updatePointer(e.clientY, e.clientX); }; const onUp = (e: MouseEvent) => { const { from, to, moved, mode } = this.dragDrop.endDrag(); // Handle drop... }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp, { once: true }); } ``` **Ajouter des attributs data:** ```html
``` **Détection personnalisée:** ```typescript const target = document.elementFromPoint(e.clientX, e.clientY); const customEl = target.closest('[data-custom-info]'); if (customEl) { const info = customEl.getAttribute('data-custom-info'); // Custom logic... } ``` ## 📚 Fichiers Modifiés ### Services - ✅ `src/app/editor/services/drag-drop.service.ts` - Service central (déjà existant) ### Composants - ✅ `src/app/editor/components/block/block-host.component.ts` - Blocs pleine largeur (modifié) - ✅ `src/app/editor/components/block/blocks/columns-block.component.ts` - Blocs colonnes (refactorisé) - ✅ `src/app/editor/components/editor-shell/editor-shell.component.ts` - Indicateur visuel (déjà existant) ### Documentation - ✅ `docs/UNIFIED_DRAG_DROP_SYSTEM.md` - Ce fichier - ✅ `docs/COLUMNS_UI_IMPROVEMENTS.md` - Améliorations UI précédentes - ✅ `docs/COLUMNS_FIXES_FINAL.md` - Corrections initiales ## 🎉 Résultat Final **Système de drag & drop complètement unifié:** - ✅ **Une seule mécanique** pour tous les blocs - ✅ **Flèche bleue** comme indicateur visuel - ✅ **Flexibilité totale** - Aucune restriction - ✅ **Expérience intuitive** - Cohérent partout **Le comportement est identique que le bloc soit en pleine largeur ou dans une colonne!** 🚀 --- **Rafraîchissez le navigateur et testez le nouveau système de drag & drop!** 🎯