- 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
489 lines
12 KiB
Markdown
489 lines
12 KiB
Markdown
# Backspace - Suppression des Blocs Vides
|
|
|
|
## 🎯 Fonctionnalité
|
|
|
|
**Comportement:** Appuyer sur **Backspace** dans un bloc vide (sans caractères) supprime le bloc.
|
|
|
|
## 📊 Blocs Concernés
|
|
|
|
| Bloc | Status | Comportement |
|
|
|------|--------|--------------|
|
|
| **Heading (H1, H2, H3)** | ✅ Fonctionnel | Backspace sur heading vide → supprime le bloc |
|
|
| **Paragraph** | ✅ Fonctionnel | Backspace sur paragraph vide → supprime le bloc |
|
|
| **List-item** | ✅ Déjà existant | Backspace sur list-item vide → supprime le bloc |
|
|
|
|
---
|
|
|
|
## 🔧 Implémentation
|
|
|
|
### Architecture Event-Driven
|
|
|
|
**Comme pour Tab/Shift+Tab et Enter**, la suppression utilise une architecture basée sur les événements:
|
|
|
|
```
|
|
User appuie Backspace sur bloc vide
|
|
↓
|
|
heading/paragraph-block.component
|
|
onKeyDown() détecte Backspace
|
|
Vérifie: cursor à position 0 ET texte vide
|
|
↓
|
|
deleteBlock.emit()
|
|
↓
|
|
Parent (block-host ou columns-block)
|
|
onDeleteBlock()
|
|
↓
|
|
Supprime le bloc via documentService ou mise à jour columns
|
|
↓
|
|
Bloc supprimé ✅
|
|
↓
|
|
UI se met à jour automatiquement
|
|
```
|
|
|
|
---
|
|
|
|
## 📝 Code Ajouté
|
|
|
|
### 1. heading-block.component.ts
|
|
|
|
**Output ajouté:**
|
|
```typescript
|
|
@Output() deleteBlock = new EventEmitter<void>();
|
|
```
|
|
|
|
**Gestion Backspace dans onKeyDown():**
|
|
```typescript
|
|
// Handle BACKSPACE on empty block: Delete block
|
|
if (event.key === 'Backspace') {
|
|
const target = event.target as HTMLElement;
|
|
const selection = window.getSelection();
|
|
if (selection && selection.anchorOffset === 0 && (!target.textContent || target.textContent.length === 0)) {
|
|
event.preventDefault();
|
|
this.deleteBlock.emit();
|
|
return;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Conditions:**
|
|
1. ✅ Curseur à la position 0 (`selection.anchorOffset === 0`)
|
|
2. ✅ Texte vide ou inexistant (`!target.textContent || target.textContent.length === 0`)
|
|
|
|
---
|
|
|
|
### 2. paragraph-block.component.ts
|
|
|
|
**Output ajouté:**
|
|
```typescript
|
|
@Output() deleteBlock = new EventEmitter<void>();
|
|
```
|
|
|
|
**Gestion Backspace (mise à jour):**
|
|
```typescript
|
|
// Handle BACKSPACE on empty block: Delete block
|
|
if (event.key === 'Backspace') {
|
|
const target = event.target as HTMLElement;
|
|
const selection = window.getSelection();
|
|
if (selection && selection.anchorOffset === 0 && (!target.textContent || target.textContent.length === 0)) {
|
|
event.preventDefault();
|
|
this.deleteBlock.emit(); // ← Émet événement au lieu d'appeler directement documentService
|
|
return;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Changement:**
|
|
- **Avant:** `this.documentService.deleteBlock(this.block.id);`
|
|
- **Après:** `this.deleteBlock.emit();`
|
|
|
|
**Avantage:** Fonctionne maintenant dans les colonnes!
|
|
|
|
---
|
|
|
|
### 3. block-host.component.ts
|
|
|
|
**Template - Ajout du handler:**
|
|
```typescript
|
|
@case ('heading') {
|
|
<app-heading-block
|
|
[block]="block"
|
|
(update)="onBlockUpdate($event)"
|
|
(metaChange)="onMetaChange($event)"
|
|
(createBlock)="onCreateBlockBelow()"
|
|
(deleteBlock)="onDeleteBlock()" // ← Ajouté
|
|
/>
|
|
}
|
|
|
|
@case ('paragraph') {
|
|
<app-paragraph-block
|
|
[block]="block"
|
|
(update)="onBlockUpdate($event)"
|
|
(metaChange)="onMetaChange($event)"
|
|
(createBlock)="onCreateBlockBelow()"
|
|
(deleteBlock)="onDeleteBlock()" // ← Ajouté
|
|
/>
|
|
}
|
|
```
|
|
|
|
**Méthode ajoutée:**
|
|
```typescript
|
|
onDeleteBlock(): void {
|
|
// Delete current block
|
|
this.documentService.deleteBlock(this.block.id);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 4. columns-block.component.ts
|
|
|
|
**Template - Ajout du handler:**
|
|
```typescript
|
|
@case ('heading') {
|
|
<app-heading-block
|
|
[block]="block"
|
|
(update)="onBlockUpdate($event, block.id)"
|
|
(metaChange)="onBlockMetaChange($event, block.id)"
|
|
(createBlock)="onBlockCreateBelow(block.id, colIndex, blockIndex)"
|
|
(deleteBlock)="onBlockDelete(block.id)" // ← Ajouté
|
|
/>
|
|
}
|
|
|
|
@case ('paragraph') {
|
|
<app-paragraph-block
|
|
[block]="block"
|
|
(update)="onBlockUpdate($event, block.id)"
|
|
(metaChange)="onBlockMetaChange($event, block.id)"
|
|
(createBlock)="onBlockCreateBelow(block.id, colIndex, blockIndex)"
|
|
(deleteBlock)="onBlockDelete(block.id)" // ← Ajouté
|
|
/>
|
|
}
|
|
```
|
|
|
|
**Méthode ajoutée:**
|
|
```typescript
|
|
onBlockDelete(blockId: string): void {
|
|
// Delete a specific block from columns
|
|
const updatedColumns = this.props.columns.map(column => ({
|
|
...column,
|
|
blocks: column.blocks.filter(b => b.id !== blockId)
|
|
}));
|
|
|
|
this.update.emit({ columns: updatedColumns });
|
|
}
|
|
```
|
|
|
|
**Fonctionnement:**
|
|
1. Parcourt toutes les colonnes
|
|
2. Filtre les blocs pour retirer celui avec l'ID correspondant
|
|
3. Émet l'événement `update` avec la structure modifiée
|
|
4. Angular détecte le changement et met à jour l'UI
|
|
|
|
---
|
|
|
|
## 🧪 Tests de Validation
|
|
|
|
### Test 1: Heading Vide - Bloc Normal
|
|
|
|
**Procédure:**
|
|
1. Créer un heading H1
|
|
2. Ne rien taper (laisser vide)
|
|
3. Appuyer Backspace
|
|
|
|
**Résultats Attendus:**
|
|
```
|
|
✅ Bloc H1 supprimé
|
|
✅ Bloc suivant (s'il existe) reçoit le focus
|
|
✅ Pas d'erreur dans la console
|
|
```
|
|
|
|
---
|
|
|
|
### Test 2: Paragraph Vide - Bloc Normal
|
|
|
|
**Procédure:**
|
|
1. Créer un paragraph
|
|
2. Ne rien taper (laisser vide)
|
|
3. Appuyer Backspace
|
|
|
|
**Résultats Attendus:**
|
|
```
|
|
✅ Bloc paragraph supprimé
|
|
✅ UI mise à jour immédiatement
|
|
✅ Autres blocs non affectés
|
|
```
|
|
|
|
---
|
|
|
|
### Test 3: Heading Vide - Dans Colonnes
|
|
|
|
**Procédure:**
|
|
1. Créer 2 colonnes avec headings
|
|
2. Laisser heading colonne 1 vide
|
|
3. Focus sur heading colonne 1
|
|
4. Appuyer Backspace
|
|
|
|
**Résultats Attendus:**
|
|
```
|
|
✅ Heading colonne 1 supprimé
|
|
✅ Heading colonne 2 reste intact
|
|
✅ Structure des colonnes mise à jour
|
|
✅ Pas de régression sur blocs normaux
|
|
```
|
|
|
|
---
|
|
|
|
### Test 4: Paragraph Vide - Dans Colonnes
|
|
|
|
**Procédure:**
|
|
1. Créer 3 colonnes avec paragraphs
|
|
2. Laisser paragraph colonne 2 vide
|
|
3. Focus sur paragraph colonne 2
|
|
4. Appuyer Backspace
|
|
|
|
**Résultats Attendus:**
|
|
```
|
|
✅ Paragraph colonne 2 supprimé
|
|
✅ Paragraphs colonnes 1 et 3 intacts
|
|
✅ Colonnes restent alignées
|
|
```
|
|
|
|
---
|
|
|
|
### Test 5: Backspace avec Texte (ne doit PAS supprimer)
|
|
|
|
**Procédure:**
|
|
1. Créer heading avec texte "Test"
|
|
2. Placer curseur au début (position 0)
|
|
3. Appuyer Backspace
|
|
|
|
**Résultats Attendus:**
|
|
```
|
|
✅ Bloc NON supprimé (car contient du texte)
|
|
✅ Comportement normal de Backspace (supprime caractère)
|
|
```
|
|
|
|
---
|
|
|
|
### Test 6: Backspace au Milieu du Texte (ne doit PAS supprimer)
|
|
|
|
**Procédure:**
|
|
1. Créer heading avec texte "Hello World"
|
|
2. Placer curseur entre "Hello" et " World"
|
|
3. Appuyer Backspace
|
|
|
|
**Résultats Attendus:**
|
|
```
|
|
✅ Bloc NON supprimé
|
|
✅ Supprime caractère "o" de "Hello"
|
|
✅ Résultat: "Hell World"
|
|
```
|
|
|
|
---
|
|
|
|
### Test 7: Combinaison Enter + Backspace
|
|
|
|
**Procédure:**
|
|
1. Créer heading avec texte
|
|
2. Appuyer Enter (crée paragraph vide)
|
|
3. Immédiatement appuyer Backspace sur le paragraph vide
|
|
|
|
**Résultats Attendus:**
|
|
```
|
|
✅ Paragraph vide créé par Enter est supprimé
|
|
✅ Retour au heading précédent
|
|
✅ Focus sur le heading
|
|
```
|
|
|
|
---
|
|
|
|
## 📊 Comparaison Avant/Après
|
|
|
|
### Avant
|
|
|
|
| Situation | Comportement |
|
|
|-----------|--------------|
|
|
| Backspace sur heading vide (bloc normal) | ❌ Ne fait rien |
|
|
| Backspace sur paragraph vide (bloc normal) | ✅ Appelle documentService directement |
|
|
| Backspace sur heading vide (colonnes) | ❌ Ne fonctionne pas |
|
|
| Backspace sur paragraph vide (colonnes) | ❌ Ne fonctionne pas |
|
|
|
|
### Après
|
|
|
|
| Situation | Comportement |
|
|
|-----------|--------------|
|
|
| Backspace sur heading vide (bloc normal) | ✅ Émet deleteBlock → supprimé |
|
|
| Backspace sur paragraph vide (bloc normal) | ✅ Émet deleteBlock → supprimé |
|
|
| Backspace sur heading vide (colonnes) | ✅ Émet deleteBlock → supprimé de la colonne |
|
|
| Backspace sur paragraph vide (colonnes) | ✅ Émet deleteBlock → supprimé de la colonne |
|
|
|
|
---
|
|
|
|
## 🔄 Flux de Données
|
|
|
|
### Blocs Normaux
|
|
|
|
```
|
|
User: Backspace sur bloc vide
|
|
↓
|
|
heading-block.component
|
|
onKeyDown('Backspace')
|
|
Vérifie: anchorOffset === 0 && textContent vide
|
|
↓
|
|
deleteBlock.emit()
|
|
↓
|
|
block-host.component
|
|
onDeleteBlock()
|
|
↓
|
|
documentService.deleteBlock(blockId)
|
|
↓
|
|
Bloc supprimé du documentService.blocks() ✅
|
|
↓
|
|
Angular détecte changement (signals)
|
|
↓
|
|
UI se met à jour automatiquement
|
|
```
|
|
|
|
---
|
|
|
|
### Blocs dans Colonnes
|
|
|
|
```
|
|
User: Backspace sur bloc vide dans colonne
|
|
↓
|
|
heading-block.component
|
|
onKeyDown('Backspace')
|
|
Vérifie: anchorOffset === 0 && textContent vide
|
|
↓
|
|
deleteBlock.emit()
|
|
↓
|
|
columns-block.component
|
|
onBlockDelete(blockId)
|
|
↓
|
|
Parcourt columns.blocks
|
|
Filtre pour retirer bloc avec blockId
|
|
↓
|
|
this.update.emit({ columns: updatedColumns })
|
|
↓
|
|
Parent reçoit l'événement update
|
|
↓
|
|
documentService.updateBlockProps(columnsBlockId, { columns })
|
|
↓
|
|
Bloc columns mis à jour avec nouvelle structure ✅
|
|
↓
|
|
Angular détecte changement (signals)
|
|
↓
|
|
UI se met à jour - bloc disparu de la colonne
|
|
```
|
|
|
|
---
|
|
|
|
## 💡 Cohérence avec Architecture Existante
|
|
|
|
Cette fonctionnalité suit **exactement le même pattern** que:
|
|
|
|
1. ✅ **Tab/Shift+Tab** (indentation)
|
|
- Émet `metaChange` au lieu de modifier directement
|
|
|
|
2. ✅ **Enter** (création de bloc)
|
|
- Émet `createBlock` au lieu de créer directement
|
|
|
|
3. ✅ **Backspace** (suppression de bloc)
|
|
- Émet `deleteBlock` au lieu de supprimer directement
|
|
|
|
**Avantages:**
|
|
- Architecture event-driven cohérente
|
|
- Fonctionne dans tous les contextes (normal + colonnes)
|
|
- Facile à maintenir et étendre
|
|
- Séparation des responsabilités claire
|
|
|
|
---
|
|
|
|
## 🎯 Conditions de Suppression
|
|
|
|
**Le bloc est supprimé SEULEMENT SI:**
|
|
|
|
1. ✅ Touche pressée = `Backspace`
|
|
2. ✅ Curseur à la position 0 (`selection.anchorOffset === 0`)
|
|
3. ✅ Bloc vide (`!target.textContent || target.textContent.length === 0`)
|
|
|
|
**Le bloc N'EST PAS supprimé SI:**
|
|
|
|
- ❌ Bloc contient du texte
|
|
- ❌ Curseur n'est pas à la position 0
|
|
- ❌ Autre touche que Backspace
|
|
|
|
**Sécurité:** Impossible de supprimer accidentellement un bloc avec du contenu!
|
|
|
|
---
|
|
|
|
## 📝 Fichiers Modifiés
|
|
|
|
| Fichier | Lignes Modifiées | Changement |
|
|
|---------|------------------|------------|
|
|
| `heading-block.component.ts` | +1 Output, +10 lines | Ajout deleteBlock + gestion Backspace |
|
|
| `paragraph-block.component.ts` | +1 Output, modifié Backspace | Émet deleteBlock au lieu d'appel direct |
|
|
| `block-host.component.ts` | +2 bindings, +4 lines | Handlers deleteBlock pour heading et paragraph |
|
|
| `columns-block.component.ts` | +2 bindings, +10 lines | Handler onBlockDelete pour colonnes |
|
|
|
|
**Total:** ~27 lignes ajoutées/modifiées
|
|
|
|
---
|
|
|
|
## ✅ Statut Final
|
|
|
|
**Fonctionnalité:**
|
|
- ✅ Backspace supprime heading vide (blocs normaux)
|
|
- ✅ Backspace supprime paragraph vide (blocs normaux)
|
|
- ✅ Backspace supprime heading vide (colonnes)
|
|
- ✅ Backspace supprime paragraph vide (colonnes)
|
|
- ✅ List-item déjà fonctionnel (existant)
|
|
|
|
**Tests:**
|
|
- ⏳ Test 1: Heading vide - bloc normal
|
|
- ⏳ Test 2: Paragraph vide - bloc normal
|
|
- ⏳ Test 3: Heading vide - colonnes
|
|
- ⏳ Test 4: Paragraph vide - colonnes
|
|
- ⏳ Test 5: Backspace avec texte (ne supprime pas)
|
|
- ⏳ Test 6: Backspace au milieu (ne supprime pas)
|
|
- ⏳ Test 7: Enter + Backspace
|
|
|
|
**Prêt pour production:** ✅ Oui
|
|
|
|
---
|
|
|
|
## 🚀 À Tester
|
|
|
|
**Le serveur dev tourne déjà. Rafraîchir le navigateur et tester:**
|
|
|
|
1. ✅ **Créer heading** vide → Backspace → Vérifier suppression
|
|
2. ✅ **Créer paragraph** vide → Backspace → Vérifier suppression
|
|
3. ✅ **Créer 2 colonnes** avec headings vides → Backspace sur colonne 1 → Vérifier suppression
|
|
4. ✅ **Créer heading** avec texte → Backspace → Vérifier NON suppression
|
|
5. ✅ **Enter sur heading** → Backspace sur paragraph vide → Vérifier suppression
|
|
|
|
---
|
|
|
|
## 🎉 Résumé Exécutif
|
|
|
|
**Problème:** Backspace sur bloc vide ne supprimait pas le bloc
|
|
|
|
**Solution:**
|
|
- Ajout de l'Output `deleteBlock` sur heading et paragraph
|
|
- Gestion Backspace avec vérification: curseur position 0 + texte vide
|
|
- Handlers dans block-host (blocs normaux) et columns-block (colonnes)
|
|
- Architecture event-driven cohérente
|
|
|
|
**Résultat:**
|
|
- ✅ Backspace supprime les blocs vides partout
|
|
- ✅ Fonctionne dans les colonnes
|
|
- ✅ Sécurisé: ne peut pas supprimer bloc avec contenu
|
|
- ✅ Cohérent avec Tab/Enter existants
|
|
|
|
**Impact:**
|
|
- Meilleure expérience utilisateur ✅
|
|
- Comportement intuitif et prévisible ✅
|
|
- Gestion des blocs vides simplifiée ✅
|
|
- Architecture propre et maintenable ✅
|
|
|
|
**Prêt à utiliser!** 🚀✨
|