- 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
17 KiB
17 KiB
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:
- Drag n'importe quel bloc H2 vers n'importe quelle position
- Créer des colonnes en droppant sur les bords
- Convertir colonnes → pleine largeur en droppant hors des colonnes
- 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:
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:
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:
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:
// 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:
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:
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:
@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:
.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
<div class="block-wrapper"
data-block-id="{{ block.id }}">
<!-- Content -->
</div>
Bloc dans Colonne
<div class="block-in-column"
data-block-id="{{ block.id }}"
data-column-index="{{ colIndex }}"
data-block-index="{{ blockIndex }}">
<!-- Content -->
</div>
Colonne
<div class="column"
data-column-id="{{ column.id }}"
data-column-index="{{ colIndex }}">
<!-- Blocks -->
</div>
Bloc Colonnes
<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:
- Hover sur n'importe quel bloc → Bouton ⋯ apparaît
- Cliquer et maintenir sur ⋯ → Curseur devient "grabbing"
- Déplacer la souris → Flèche bleue indique la position de drop
- 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:
// 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:
<div class="block-wrapper"
[attr.data-block-id]="block.id"
[attr.data-custom-info]="someInfo">
<!-- Content -->
</div>
Détection personnalisée:
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! 🎯