- 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
594 lines
17 KiB
Markdown
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!** 🎯
|