ObsiViewer/docs/EDITOR_NIMBUS/COLUMNS_ENHANCEMENTS.md
Bruno Charest 5e8cddf92e ```
docs: remove outdated implementation documentation files

- Deleted AI_TOOLS_IMPLEMENTATION.md (296 lines) - outdated AI tools integration guide
- Deleted ALIGN_INDENT_COLUMNS_FIX.md (557 lines) - obsolete column alignment fix documentation
- Deleted BLOCK_COMMENTS_IMPLEMENTATION.md (400 lines) - superseded block comments implementation notes
- Deleted DRAG_DROP_COLUMNS_IMPLEMENTATION.md (500 lines) - outdated drag-and-drop columns guide
- Deleted INLINE_TOOLBAR_IMPLEMENTATION.md (350 lines) - obsol
2025-11-17 10:09:25 -05:00

14 KiB

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:

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:

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):

<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:

<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

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

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

onContentInput(event: Event, blockId: string): void {
  const target = event.target as HTMLElement;
  const text = target.textContent || '';
  this.onBlockUpdate({ text }, blockId);
}

deleteBlockFromColumns() - Suppression avec redistribution

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

@Input({ required: true }) block!: Block<ColumnsProps>;
@Output() update = new EventEmitter<ColumnsProps>();

Méthodes Publiques

// 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

// 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:

  • Suppression d'un bloc vide la colonne
  • Colonne vide est supprimée automatiquement
  • Largeurs redistribuées équitablement
  • Fonctionne avec 2, 3, 4, 5+ colonnes

Drag & Drop:

  • Bouton ⋮⋮ visible au hover
  • Drag entre colonnes fonctionne
  • Drag dans une même colonne fonctionne
  • Curseur change en grabbing
  • Colonnes vides supprimées après drag
  • Largeurs redistribuées après drag

Comportement Uniforme:

  • Headings éditables identiquement
  • Paragraphs éditables identiquement
  • Placeholders cohérents
  • Focus states uniformes
  • 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! 🚀