ObsiViewer/docs/UNIFIED_DRAG_DROP_SYSTEM.md
Bruno Charest ee3085ce38 feat: add Nimbus Editor with Unsplash integration
- Integrated Unsplash API for image search functionality with environment configuration
- Added new Nimbus Editor page component with navigation from sidebar and mobile drawer
- Enhanced TOC with highlight animation for editor heading navigation
- Improved CDK overlay z-index hierarchy for proper menu layering
- Removed obsolete logging validation script
2025-11-11 11:38:27 -05:00

594 lines
17 KiB
Markdown

# 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<string | null>(null);
readonly fromIndex = signal<number>(-1);
readonly overIndex = signal<number>(-1);
readonly indicator = signal<IndicatorRect | null>(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') {
<!-- Horizontal indicator (line change) -->
<div class="drop-indicator horizontal"
[style.top.px]="dragDrop.indicator()!.top"
[style.left.px]="dragDrop.indicator()!.left"
[style.width.px]="dragDrop.indicator()!.width">
<span class="arrow left"></span>
<span class="arrow right"></span>
</div>
} @else {
<!-- Vertical indicator (column) -->
<div class="drop-indicator vertical"
[style.top.px]="dragDrop.indicator()!.top"
[style.left.px]="dragDrop.indicator()!.left"
[style.height.px]="dragDrop.indicator()!.height">
<span class="arrow top"></span>
<span class="arrow bottom"></span>
</div>
}
}
```
**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
<div class="block-wrapper"
data-block-id="{{ block.id }}">
<!-- Content -->
</div>
```
### Bloc dans Colonne
```html
<div class="block-in-column"
data-block-id="{{ block.id }}"
data-column-index="{{ colIndex }}"
data-block-index="{{ blockIndex }}">
<!-- Content -->
</div>
```
### Colonne
```html
<div class="column"
data-column-id="{{ column.id }}"
data-column-index="{{ colIndex }}">
<!-- Blocks -->
</div>
```
### Bloc Colonnes
```html
<div class="block-wrapper"
data-block-id="{{ columnsBlock.id }}">
<div class="columns-container">
<!-- Columns -->
</div>
</div>
```
## 🧪 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
<button (mousedown)="onDragStart($event)"></button>
// 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
<div class="block-wrapper"
[attr.data-block-id]="block.id"
[attr.data-custom-info]="someInfo">
<!-- Content -->
</div>
```
**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!** 🎯