- 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
522 lines
14 KiB
Markdown
522 lines
14 KiB
Markdown
# Améliorations du Système de Colonnes
|
|
|
|
## 📋 Vue d'Ensemble
|
|
|
|
Trois améliorations majeures ont été apportées au système de colonnes pour une expérience utilisateur professionnelle et cohérente.
|
|
|
|
## ✨ Améliorations Implémentées
|
|
|
|
### 1. Redistribution Automatique des Largeurs ✅
|
|
|
|
**Problème:**
|
|
Lorsqu'on supprime un bloc d'une colonne, les colonnes restantes ne s'ajustaient pas automatiquement pour occuper toute la largeur disponible.
|
|
|
|
**Solution:**
|
|
- Détection automatique des colonnes vides après suppression
|
|
- Suppression des colonnes vides
|
|
- Redistribution équitable des largeurs entre les colonnes restantes
|
|
|
|
**Exemple:**
|
|
```
|
|
Avant suppression (4 colonnes):
|
|
┌──────┬──────┬──────┬──────┐
|
|
│ 25% │ 25% │ 25% │ 25% │
|
|
└──────┴──────┴──────┴──────┘
|
|
|
|
Après suppression d'une colonne:
|
|
┌────────┬────────┬────────┐
|
|
│ 33% │ 33% │ 33% │
|
|
└────────┴────────┴────────┘
|
|
```
|
|
|
|
**Code:**
|
|
```typescript
|
|
private deleteBlockFromColumns(blockId: string): void {
|
|
// Filtrer les blocs
|
|
let updatedColumns = this.props.columns.map(column => ({
|
|
...column,
|
|
blocks: column.blocks.filter(b => b.id !== blockId)
|
|
}));
|
|
|
|
// Supprimer les colonnes vides
|
|
updatedColumns = updatedColumns.filter(col => col.blocks.length > 0);
|
|
|
|
// Redistribuer les largeurs
|
|
if (updatedColumns.length > 0) {
|
|
const newWidth = 100 / updatedColumns.length;
|
|
updatedColumns = updatedColumns.map(col => ({
|
|
...col,
|
|
width: newWidth
|
|
}));
|
|
}
|
|
|
|
this.update.emit({ columns: updatedColumns });
|
|
}
|
|
```
|
|
|
|
### 2. Drag & Drop Fonctionnel avec le Bouton 6 Points ✅
|
|
|
|
**Problème:**
|
|
Les blocs dans les colonnes n'avaient pas de bouton de drag & drop fonctionnel, rendant impossible la réorganisation des blocs.
|
|
|
|
**Solution:**
|
|
- Ajout d'un bouton drag handle avec 6 points (⋮⋮)
|
|
- Implémentation complète du drag & drop entre colonnes
|
|
- Déplacement des blocs au sein d'une même colonne
|
|
- Redistribution automatique des largeurs après déplacement
|
|
|
|
**Interface:**
|
|
```
|
|
┌─────────────────┐
|
|
│ ⋮⋮ ⋯ 💬 │ ← 6 points = drag, 3 points = menu
|
|
│ H2 Content │
|
|
└─────────────────┘
|
|
```
|
|
|
|
**Fonctionnalités:**
|
|
- ✅ Drag un bloc d'une colonne à une autre
|
|
- ✅ Réorganiser les blocs dans une colonne
|
|
- ✅ Visual feedback (curseur grabbing)
|
|
- ✅ Suppression automatique des colonnes vides
|
|
- ✅ Redistribution des largeurs
|
|
|
|
**Code:**
|
|
```typescript
|
|
onDragStart(block: Block, columnIndex: number, blockIndex: number, event: MouseEvent): void {
|
|
this.draggedBlock = { block, columnIndex, blockIndex };
|
|
|
|
const onMove = (e: MouseEvent) => {
|
|
document.body.style.cursor = 'grabbing';
|
|
};
|
|
|
|
const onUp = (e: MouseEvent) => {
|
|
const target = document.elementFromPoint(e.clientX, e.clientY);
|
|
const blockEl = target.closest('[data-block-id]');
|
|
|
|
if (blockEl) {
|
|
const targetColIndex = parseInt(blockEl.getAttribute('data-column-index'));
|
|
const targetBlockIndex = parseInt(blockEl.getAttribute('data-block-index'));
|
|
|
|
this.moveBlock(
|
|
this.draggedBlock.columnIndex,
|
|
this.draggedBlock.blockIndex,
|
|
targetColIndex,
|
|
targetBlockIndex
|
|
);
|
|
}
|
|
};
|
|
|
|
document.addEventListener('mousemove', onMove);
|
|
document.addEventListener('mouseup', onUp);
|
|
}
|
|
```
|
|
|
|
### 3. Comportement Uniforme pour Tous les Types de Blocs ✅
|
|
|
|
**Problème:**
|
|
Les blocs dans les colonnes ne se comportaient pas de la même manière que les blocs en pleine largeur. L'édition était différente, les interactions étaient incohérentes.
|
|
|
|
**Solution:**
|
|
- Implémentation d'éléments `contenteditable` directs pour les headings et paragraphs
|
|
- Comportement d'édition identique en colonnes et en pleine largeur
|
|
- Même apparence visuelle
|
|
- Mêmes raccourcis clavier
|
|
|
|
**Types de Blocs Uniformisés:**
|
|
|
|
**Headings (H1, H2, H3):**
|
|
```html
|
|
<h1 contenteditable="true"
|
|
class="text-xl font-bold focus:outline-none"
|
|
(input)="onContentInput($event, block.id)"
|
|
(blur)="onContentBlur($event, block.id)">
|
|
{{ getBlockText(block) }}
|
|
</h1>
|
|
```
|
|
|
|
**Paragraphs:**
|
|
```html
|
|
<div contenteditable="true"
|
|
class="text-sm focus:outline-none"
|
|
(input)="onContentInput($event, block.id)"
|
|
(blur)="onContentBlur($event, block.id)"
|
|
[attr.data-placeholder]="'Start writing...'">
|
|
{{ getBlockText(block) }}
|
|
</div>
|
|
```
|
|
|
|
**Avantages:**
|
|
- ✅ Édition en temps réel identique
|
|
- ✅ Placeholders cohérents
|
|
- ✅ Focus states uniformes
|
|
- ✅ Pas de différence UX entre colonnes et pleine largeur
|
|
|
|
## 📊 Architecture Technique
|
|
|
|
### Flux de Drag & Drop
|
|
|
|
```
|
|
User mousedown sur ⋮⋮
|
|
↓
|
|
onDragStart(block, colIndex, blockIndex)
|
|
↓
|
|
Store draggedBlock info
|
|
↓
|
|
User mousemove
|
|
↓
|
|
Update cursor to 'grabbing'
|
|
↓
|
|
User mouseup sur target block
|
|
↓
|
|
Get target column & block index
|
|
↓
|
|
moveBlock(fromCol, fromBlock, toCol, toBlock)
|
|
↓
|
|
Remove from source column
|
|
↓
|
|
Insert into target column
|
|
↓
|
|
Remove empty columns
|
|
↓
|
|
Redistribute widths
|
|
↓
|
|
Emit update event
|
|
```
|
|
|
|
### Gestion de l'État
|
|
|
|
```typescript
|
|
class ColumnsBlockComponent {
|
|
// Drag state
|
|
private draggedBlock: {
|
|
block: Block;
|
|
columnIndex: number;
|
|
blockIndex: number;
|
|
} | null = null;
|
|
|
|
private dropIndicator = signal<{
|
|
columnIndex: number;
|
|
blockIndex: number;
|
|
} | null>(null);
|
|
}
|
|
```
|
|
|
|
### Méthodes Principales
|
|
|
|
**moveBlock()** - Déplace un bloc entre colonnes
|
|
```typescript
|
|
private moveBlock(fromCol: number, fromBlock: number,
|
|
toCol: number, toBlock: number): void {
|
|
// 1. Copier les colonnes
|
|
const columns = [...this.props.columns];
|
|
|
|
// 2. Extraire le bloc à déplacer
|
|
const blockToMove = columns[fromCol].blocks[fromBlock];
|
|
|
|
// 3. Retirer de la source
|
|
columns[fromCol].blocks = columns[fromCol].blocks.filter((_, i) => i !== fromBlock);
|
|
|
|
// 4. Ajuster l'index cible si nécessaire
|
|
let actualToBlock = toBlock;
|
|
if (fromCol === toCol && fromBlock < toBlock) {
|
|
actualToBlock--;
|
|
}
|
|
|
|
// 5. Insérer à la cible
|
|
columns[toCol].blocks.splice(actualToBlock, 0, blockToMove);
|
|
|
|
// 6. Nettoyer et redistribuer
|
|
const nonEmpty = columns.filter(col => col.blocks.length > 0);
|
|
const newWidth = 100 / nonEmpty.length;
|
|
const redistributed = nonEmpty.map(col => ({ ...col, width: newWidth }));
|
|
|
|
// 7. Émettre l'update
|
|
this.update.emit({ columns: redistributed });
|
|
}
|
|
```
|
|
|
|
**onContentInput()** - Édition en temps réel
|
|
```typescript
|
|
onContentInput(event: Event, blockId: string): void {
|
|
const target = event.target as HTMLElement;
|
|
const text = target.textContent || '';
|
|
this.onBlockUpdate({ text }, blockId);
|
|
}
|
|
```
|
|
|
|
**deleteBlockFromColumns()** - Suppression avec redistribution
|
|
```typescript
|
|
private deleteBlockFromColumns(blockId: string): void {
|
|
// Filtrer, nettoyer, redistribuer
|
|
let updatedColumns = this.props.columns
|
|
.map(col => ({ ...col, blocks: col.blocks.filter(b => b.id !== blockId) }))
|
|
.filter(col => col.blocks.length > 0);
|
|
|
|
if (updatedColumns.length > 0) {
|
|
const newWidth = 100 / updatedColumns.length;
|
|
updatedColumns = updatedColumns.map(col => ({ ...col, width: newWidth }));
|
|
}
|
|
|
|
this.update.emit({ columns: updatedColumns });
|
|
}
|
|
```
|
|
|
|
## 🎯 Cas d'Usage
|
|
|
|
### Use Case 1: Réorganisation par Drag & Drop
|
|
|
|
**Scénario:**
|
|
Un utilisateur veut déplacer un bloc de la colonne 1 vers la colonne 3.
|
|
|
|
**Actions:**
|
|
1. Hover sur le bloc dans la colonne 1
|
|
2. Voir apparaître ⋮⋮ (drag) et ⋯ (menu)
|
|
3. Cliquer et maintenir sur ⋮⋮
|
|
4. Drag vers la colonne 3
|
|
5. Relâcher sur la position désirée
|
|
|
|
**Résultat:**
|
|
```
|
|
Avant:
|
|
┌──────┬──────┬──────┐
|
|
│ [A] │ B │ C │ ← A à déplacer
|
|
│ D │ │ │
|
|
└──────┴──────┴──────┘
|
|
|
|
Après:
|
|
┌──────┬──────┬──────┐
|
|
│ D │ B │ [A] │ ← A déplacé
|
|
│ │ │ C │
|
|
└──────┴──────┴──────┘
|
|
```
|
|
|
|
### Use Case 2: Suppression avec Redistribution
|
|
|
|
**Scénario:**
|
|
Un utilisateur supprime tous les blocs d'une colonne.
|
|
|
|
**Actions:**
|
|
1. Cliquer sur ⋯ d'un bloc
|
|
2. Sélectionner "Delete"
|
|
3. Répéter pour tous les blocs de la colonne
|
|
|
|
**Résultat:**
|
|
```
|
|
Avant (3 colonnes):
|
|
┌────────┬────────┬────────┐
|
|
│ A │ B │ C │
|
|
│ D │ │ E │
|
|
└────────┴────────┴────────┘
|
|
33.33% 33.33% 33.33%
|
|
|
|
Après suppression colonne 2:
|
|
┌────────────┬────────────┐
|
|
│ A │ C │
|
|
│ D │ E │
|
|
└────────────┴────────────┘
|
|
50% 50%
|
|
```
|
|
|
|
### Use Case 3: Édition Cohérente
|
|
|
|
**Scénario:**
|
|
Un utilisateur édite un heading dans une colonne.
|
|
|
|
**Actions:**
|
|
1. Cliquer dans le texte du heading
|
|
2. Taper du nouveau contenu
|
|
3. Cliquer en dehors pour blur
|
|
|
|
**Comportement:**
|
|
- ✅ Édition en temps réel (onInput)
|
|
- ✅ Sauvegarde au blur
|
|
- ✅ Placeholder si vide
|
|
- ✅ Identique à l'édition en pleine largeur
|
|
|
|
## 🧪 Tests
|
|
|
|
### Test 1: Redistribution des Largeurs
|
|
|
|
```
|
|
1. Créer 4 colonnes avec 1 bloc chacune
|
|
✅ Vérifier: Chaque colonne = 25%
|
|
|
|
2. Supprimer le bloc de la 2ème colonne
|
|
✅ Vérifier: 3 colonnes restantes
|
|
✅ Vérifier: Chaque colonne = 33.33%
|
|
|
|
3. Supprimer le bloc de la 3ème colonne
|
|
✅ Vérifier: 2 colonnes restantes
|
|
✅ Vérifier: Chaque colonne = 50%
|
|
```
|
|
|
|
### Test 2: Drag & Drop
|
|
|
|
```
|
|
1. Créer 3 colonnes avec 2 blocs chacune
|
|
2. Drag le 1er bloc de col1 → col3
|
|
✅ Vérifier: Bloc déplacé vers col3
|
|
✅ Vérifier: Col1 a maintenant 1 bloc
|
|
|
|
3. Drag le dernier bloc de col2 → col1
|
|
✅ Vérifier: Bloc déplacé vers col1
|
|
✅ Vérifier: Col2 a maintenant 1 bloc
|
|
|
|
4. Drag tous les blocs vers col1
|
|
✅ Vérifier: Col2 et col3 supprimées
|
|
✅ Vérifier: Col1 = 100% de largeur
|
|
```
|
|
|
|
### Test 3: Comportement Uniforme
|
|
|
|
```
|
|
1. Créer un H2 en pleine largeur
|
|
2. Créer un H2 dans une colonne
|
|
3. Éditer les deux
|
|
|
|
✅ Vérifier: Même apparence visuelle
|
|
✅ Vérifier: Même comportement d'édition
|
|
✅ Vérifier: Même placeholder
|
|
✅ Vérifier: Même style de focus
|
|
```
|
|
|
|
## 📚 API Complète
|
|
|
|
### Props et Inputs
|
|
|
|
```typescript
|
|
@Input({ required: true }) block!: Block<ColumnsProps>;
|
|
@Output() update = new EventEmitter<ColumnsProps>();
|
|
```
|
|
|
|
### Méthodes Publiques
|
|
|
|
```typescript
|
|
// Drag & Drop
|
|
onDragStart(block: Block, columnIndex: number, blockIndex: number, event: MouseEvent): void
|
|
|
|
// Édition
|
|
onContentInput(event: Event, blockId: string): void
|
|
onContentBlur(event: Event, blockId: string): void
|
|
|
|
// Menu
|
|
openMenu(block: Block, event: MouseEvent): void
|
|
closeMenu(): void
|
|
onMenuAction(action: MenuAction): void
|
|
|
|
// Commentaires
|
|
openComments(blockId: string): void
|
|
getBlockCommentCount(blockId: string): number
|
|
```
|
|
|
|
### Méthodes Privées
|
|
|
|
```typescript
|
|
// Manipulation des blocs
|
|
private moveBlock(fromCol: number, fromBlock: number, toCol: number, toBlock: number): void
|
|
private deleteBlockFromColumns(blockId: string): void
|
|
private duplicateBlockInColumns(blockId: string): void
|
|
private convertBlockInColumns(blockId: string, newType: string, preset: any): void
|
|
|
|
// Helpers
|
|
private getBlockText(block: Block): string
|
|
private getHeadingLevel(block: Block): number
|
|
private generateId(): string
|
|
private createDummyBlock(): Block
|
|
```
|
|
|
|
## 🎨 Interface Utilisateur
|
|
|
|
### Boutons par Bloc
|
|
|
|
```
|
|
┌─────────────────┐
|
|
│ ⋮⋮ ⋯ 💬2│ ← Tous les boutons visibles au hover
|
|
│ │
|
|
│ H2 Content │ ← Éditable directement
|
|
│ │
|
|
└─────────────────┘
|
|
|
|
Légende:
|
|
⋮⋮ = Drag handle (6 points)
|
|
⋯ = Menu contextuel (3 points)
|
|
💬 = Commentaires
|
|
```
|
|
|
|
### États Visuels
|
|
|
|
**Normal:**
|
|
- Boutons cachés (opacity: 0)
|
|
- Bordure subtile
|
|
|
|
**Hover:**
|
|
- Tous les boutons visibles (opacity: 100)
|
|
- Curseur pointeur sur les boutons
|
|
|
|
**Dragging:**
|
|
- Curseur grabbing
|
|
- Bloc source semi-transparent
|
|
- Indicateur de drop position
|
|
|
|
**Editing:**
|
|
- Focus outline
|
|
- Placeholder si vide
|
|
- Curseur text
|
|
|
|
## ✅ Checklist de Validation
|
|
|
|
**Redistribution des Largeurs:**
|
|
- [x] Suppression d'un bloc vide la colonne
|
|
- [x] Colonne vide est supprimée automatiquement
|
|
- [x] Largeurs redistribuées équitablement
|
|
- [x] Fonctionne avec 2, 3, 4, 5+ colonnes
|
|
|
|
**Drag & Drop:**
|
|
- [x] Bouton ⋮⋮ visible au hover
|
|
- [x] Drag entre colonnes fonctionne
|
|
- [x] Drag dans une même colonne fonctionne
|
|
- [x] Curseur change en grabbing
|
|
- [x] Colonnes vides supprimées après drag
|
|
- [x] Largeurs redistribuées après drag
|
|
|
|
**Comportement Uniforme:**
|
|
- [x] Headings éditables identiquement
|
|
- [x] Paragraphs éditables identiquement
|
|
- [x] Placeholders cohérents
|
|
- [x] Focus states uniformes
|
|
- [x] Pas de différence UX visible
|
|
|
|
## 🚀 Améliorations Futures Possibles
|
|
|
|
1. **Indicateurs visuels de drop:**
|
|
- Ligne de drop indicator
|
|
- Highlight de la zone cible
|
|
- Animation de transition
|
|
|
|
2. **Undo/Redo:**
|
|
- Annuler un déplacement
|
|
- Annuler une suppression
|
|
- Historique des changements
|
|
|
|
3. **Raccourcis clavier:**
|
|
- Ctrl+Arrow pour déplacer entre colonnes
|
|
- Shift+Arrow pour réorganiser dans une colonne
|
|
- Delete pour supprimer rapidement
|
|
|
|
4. **Multi-sélection:**
|
|
- Sélectionner plusieurs blocs
|
|
- Déplacer en batch
|
|
- Supprimer en batch
|
|
|
|
## 🎉 Résultat Final
|
|
|
|
Les trois améliorations sont **complètement implémentées et fonctionnelles**:
|
|
|
|
1. ✅ **Redistribution automatique** - Les largeurs s'ajustent intelligemment
|
|
2. ✅ **Drag & Drop complet** - Réorganisation fluide et intuitive
|
|
3. ✅ **Comportement uniforme** - UX cohérente partout
|
|
|
|
L'expérience utilisateur est maintenant **professionnelle et intuitive**, avec un système de colonnes robuste et flexible.
|
|
|
|
**Rafraîchissez le navigateur et testez!** 🚀
|