From 439880b08fd30124bfa8e4c73edee217916443f4 Mon Sep 17 00:00:00 2001 From: Mathieu Aumont Date: Tue, 11 Nov 2025 11:35:11 +0100 Subject: [PATCH] Optimisation htmx / js par claude --- frontend/src/editor.js | 10 ++ frontend/src/file-tree.js | 291 +++++++++++++++++++------------------- 2 files changed, 154 insertions(+), 147 deletions(-) diff --git a/frontend/src/editor.js b/frontend/src/editor.js index 53b98ca..fe0f3e0 100644 --- a/frontend/src/editor.js +++ b/frontend/src/editor.js @@ -77,6 +77,8 @@ class MarkdownEditor { if (saveStatus) { saveStatus.textContent = 'Sauvegarde...'; } + // Synchroniser le contenu de CodeMirror vers le textarea + this.syncToTextarea(); form.requestSubmit(); } }, 2000); // Auto-save after 2 seconds of inactivity @@ -88,6 +90,8 @@ class MarkdownEditor { run: () => { const form = this.textarea.closest('form'); if (form) { + // Synchroniser le contenu de CodeMirror vers le textarea + this.syncToTextarea(); form.requestSubmit(); } return true; @@ -209,6 +213,12 @@ class MarkdownEditor { } } + syncToTextarea() { + if (this.editorView && this.textarea) { + this.textarea.value = this.editorView.state.doc.toString(); + } + } + destroy() { if (this._updateTimeout) { clearTimeout(this._updateTimeout); diff --git a/frontend/src/file-tree.js b/frontend/src/file-tree.js index 7eaf936..484b15a 100644 --- a/frontend/src/file-tree.js +++ b/frontend/src/file-tree.js @@ -1,42 +1,47 @@ /** * FileTree - Gère l'arborescence hiérarchique avec drag & drop + * Utilise la délégation d'événements pour éviter les problèmes de listeners perdus */ class FileTree { constructor() { + this.draggedPath = null; + this.draggedType = null; this.init(); } init() { - // Écouter les changements htmx dans le file-tree - document.body.addEventListener('htmx:afterSwap', (event) => { - if (event.detail.target.id === 'file-tree' || event.detail.target.closest('#file-tree')) { - this.setupFolderToggles(); - this.setupDragAndDrop(); - } - }); - - // Setup initial si déjà chargé - if (document.getElementById('file-tree')) { - this.setupFolderToggles(); - this.setupDragAndDrop(); + // Utiliser la délégation d'événements sur le conteneur de la sidebar + // Cela évite de perdre les listeners après les swaps htmx + const sidebar = document.getElementById('sidebar'); + if (!sidebar) { + console.error('FileTree: sidebar not found'); + return; } - } - setupFolderToggles() { - const folderHeaders = document.querySelectorAll('.folder-header'); - - folderHeaders.forEach(header => { - // Éviter d'ajouter plusieurs fois le même listener - if (header.dataset.toggleInitialized === 'true') { + // Event listener délégué pour les clics sur les folder-headers + sidebar.addEventListener('click', (e) => { + // Vérifier d'abord si c'est un folder-header ou un de ses enfants + const folderHeader = e.target.closest('.folder-header'); + if (folderHeader && !e.target.closest('.file-item')) { + e.preventDefault(); + e.stopPropagation(); + this.toggleFolder(folderHeader); return; } - header.dataset.toggleInitialized = 'true'; - header.addEventListener('click', (e) => { - e.stopPropagation(); - this.toggleFolder(header); - }); + // Event listener délégué pour les clics sur les fichiers + const fileItem = e.target.closest('.file-item'); + if (fileItem && !folderHeader) { + // Laisser HTMX gérer le chargement via l'attribut hx-get + // Ne pas bloquer la propagation pour les fichiers + return; + } }); + + // Event listeners délégués pour le drag & drop + this.setupDelegatedDragAndDrop(sidebar); + + console.log('FileTree initialized with event delegation'); } toggleFolder(header) { @@ -58,48 +63,89 @@ class FileTree { } } - setupDragAndDrop() { - const fileItems = document.querySelectorAll('.file-item[draggable="true"]'); + setupDelegatedDragAndDrop(sidebar) { + // Drag start - délégué pour fichiers et dossiers + sidebar.addEventListener('dragstart', (e) => { + const fileItem = e.target.closest('.file-item'); + const folderHeader = e.target.closest('.folder-header'); + + if (fileItem && fileItem.draggable) { + this.handleDragStart(e, 'file', fileItem); + } else if (folderHeader && folderHeader.draggable) { + this.handleDragStart(e, 'folder', folderHeader); + } + }); + + // Drag end - délégué + sidebar.addEventListener('dragend', (e) => { + const fileItem = e.target.closest('.file-item'); + const folderHeader = e.target.closest('.folder-header'); + + if (fileItem || folderHeader) { + this.handleDragEnd(e); + } + }); + + // Drag over - délégué sur les folder-headers + sidebar.addEventListener('dragover', (e) => { + const folderHeader = e.target.closest('.folder-header'); + if (folderHeader) { + this.handleDragOver(e, folderHeader); + } + }); + + // Drag leave - délégué + sidebar.addEventListener('dragleave', (e) => { + const folderHeader = e.target.closest('.folder-header'); + if (folderHeader) { + this.handleDragLeave(e, folderHeader); + } + }); + + // Drop - délégué + sidebar.addEventListener('drop', (e) => { + const folderHeader = e.target.closest('.folder-header'); + if (folderHeader) { + this.handleDrop(e, folderHeader); + } + }); + + // Rendre les dossiers draggables (sauf racine) + this.updateDraggableAttributes(); + + // Écouter les événements HTMX pour mettre à jour les attributs après les swaps + // Plus performant et plus cohérent qu'un MutationObserver + document.body.addEventListener('htmx:afterSwap', (event) => { + // Vérifier si le swap concerne le file-tree + const target = event.detail?.target; + if (target && (target.id === 'file-tree' || target.closest('#file-tree'))) { + this.updateDraggableAttributes(); + } + }); + + // Écouter aussi les swaps out-of-band (oob) qui mettent à jour le file-tree + document.body.addEventListener('htmx:oobAfterSwap', (event) => { + const target = event.detail?.target; + if (target && target.id === 'file-tree') { + this.updateDraggableAttributes(); + } + }); + } + + updateDraggableAttributes() { + // Mettre à jour l'attribut draggable pour les dossiers non-racine const folderItems = document.querySelectorAll('.folder-item'); - - console.log('Setup drag & drop:', { - filesCount: fileItems.length, - foldersCount: folderItems.length - }); - - // Setup drag events pour les fichiers - fileItems.forEach(file => { - file.addEventListener('dragstart', (e) => this.handleDragStart(e, 'file')); - file.addEventListener('dragend', (e) => this.handleDragEnd(e)); - // Empêcher htmx de gérer le clic pendant le drag - file.addEventListener('click', (e) => { - if (e.dataTransfer) { - e.preventDefault(); - } - }, true); - }); - - // Setup drag events pour les dossiers (headers) folderItems.forEach(folder => { const header = folder.querySelector('.folder-header'); const isRoot = folder.dataset.isRoot === 'true'; - // La racine ne doit pas être draggable - if (!isRoot) { + if (header && !isRoot) { header.setAttribute('draggable', 'true'); - header.addEventListener('dragstart', (e) => this.handleDragStart(e, 'folder')); - header.addEventListener('dragend', (e) => this.handleDragEnd(e)); } - - // Tous les dossiers (y compris la racine) sont des drop zones - header.addEventListener('dragover', (e) => this.handleDragOver(e)); - header.addEventListener('dragleave', (e) => this.handleDragLeave(e)); - header.addEventListener('drop', (e) => this.handleDrop(e)); }); } - handleDragStart(e, type) { - const item = e.currentTarget; + handleDragStart(e, type, item) { item.classList.add('dragging'); let path, name; @@ -126,8 +172,14 @@ class FileTree { } handleDragEnd(e) { - const item = e.currentTarget; - item.classList.remove('dragging'); + // Trouver l'élément draggé (fichier ou folder-header) + const fileItem = e.target.closest('.file-item'); + const folderHeader = e.target.closest('.folder-header'); + const item = fileItem || folderHeader; + + if (item) { + item.classList.remove('dragging'); + } // Supprimer les highlights de tous les dossiers document.querySelectorAll('.folder-item.drag-over').forEach(f => { @@ -144,12 +196,13 @@ class FileTree { this.draggedType = null; } - handleDragOver(e) { + handleDragOver(e, folderHeader) { e.preventDefault(); e.stopPropagation(); - const folderHeader = e.currentTarget; const folderItem = folderHeader.closest('.folder-item'); + if (!folderItem) return; + const targetPath = folderItem.dataset.path; // Empêcher de déplacer un dossier dans lui-même ou dans ses enfants @@ -178,18 +231,16 @@ class FileTree { } } - handleDragLeave(e) { - const folderHeader = e.currentTarget; + handleDragLeave(e, folderHeader) { const folderItem = folderHeader.closest('.folder-item'); + if (!folderItem) return; // Vérifier que la souris a vraiment quitté le dossier const rect = folderHeader.getBoundingClientRect(); if (e.clientX < rect.left || e.clientX >= rect.right || e.clientY < rect.top || e.clientY >= rect.bottom) { - if (folderItem) { - folderItem.classList.remove('drag-over'); - this.removeDestinationIndicator(); - } + folderItem.classList.remove('drag-over'); + this.removeDestinationIndicator(); } } @@ -221,12 +272,13 @@ class FileTree { } } - handleDrop(e) { + handleDrop(e, folderHeader) { e.preventDefault(); e.stopPropagation(); - const folderHeader = e.currentTarget; const folderItem = folderHeader.closest('.folder-item'); + if (!folderItem) return; + folderItem.classList.remove('drag-over'); // Supprimer l'indicateur de destination @@ -287,50 +339,19 @@ class FileTree { console.log('moveFile called:', { sourcePath, destinationPath }); try { - const body = new URLSearchParams({ - source: sourcePath, - destination: destinationPath + // Utiliser htmx.ajax() au lieu de fetch() manuel + // HTMX gère automatiquement les swaps oob et le traitement du HTML + // Les attributs draggables seront mis à jour automatiquement via htmx:oobAfterSwap + htmx.ajax('POST', '/api/files/move', { + values: { source: sourcePath, destination: destinationPath }, + swap: 'none' // On ne swap rien directement, le serveur utilise hx-swap-oob + }).then(() => { + console.log(`Fichier déplacé: ${sourcePath} -> ${destinationPath}`); + }).catch((error) => { + console.error('Erreur lors du déplacement:', error); + alert('Erreur lors du déplacement du fichier'); }); - console.log('FormData contents:', { - source: body.get('source'), - destination: body.get('destination') - }); - - const response = await fetch('/api/files/move', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(errorText || 'Erreur lors du déplacement du fichier'); - } - - // La réponse contient déjà le file-tree mis à jour avec hx-swap-oob - const html = await response.text(); - - // Injecter la réponse dans le DOM (htmx le fera automatiquement avec oob) - const temp = document.createElement('div'); - temp.innerHTML = html; - - // Trouver l'élément avec hx-swap-oob - const oobElement = temp.querySelector('[hx-swap-oob]'); - if (oobElement) { - const targetId = oobElement.id; - const target = document.getElementById(targetId); - if (target) { - target.innerHTML = oobElement.innerHTML; - // Réinitialiser les event listeners - this.setupFolderToggles(); - this.setupDragAndDrop(); - } - } - - console.log(`Fichier déplacé: ${sourcePath} -> ${destinationPath}`); } catch (error) { console.error('Erreur lors du déplacement:', error); alert('Erreur lors du déplacement du fichier: ' + error.message); @@ -423,49 +444,25 @@ window.handleNewFolder = async function(event) { // Valider le nom (pas de caractères dangereux) if (folderName.includes('..') || folderName.includes('\\')) { - alert('Nom de dossier invalide. Évitez les caractères \ et ..'); + alert('Nom de dossier invalide. Évitez les caractères \\ et ..'); return; } try { - const body = new URLSearchParams({ - path: folderName + // Utiliser htmx.ajax() au lieu de fetch() manuel + // HTMX gère automatiquement les swaps oob et le traitement du HTML + // Les attributs draggables seront mis à jour automatiquement via htmx:oobAfterSwap + htmx.ajax('POST', '/api/folders/create', { + values: { path: folderName }, + swap: 'none' // On ne swap rien directement, le serveur utilise hx-swap-oob + }).then(() => { + window.hideNewFolderModal(); + console.log(`Dossier créé: ${folderName}`); + }).catch((error) => { + console.error('Erreur lors de la création du dossier:', error); + alert('Erreur lors de la création du dossier'); }); - const response = await fetch('/api/folders/create', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(errorText || 'Erreur lors de la création du dossier'); - } - - // La réponse contient déjà le file-tree mis à jour avec hx-swap-oob - const html = await response.text(); - - // Injecter la réponse dans le DOM - const temp = document.createElement('div'); - temp.innerHTML = html; - - // Trouver l'élément avec hx-swap-oob - const oobElement = temp.querySelector('[hx-swap-oob]'); - if (oobElement) { - const targetId = oobElement.id; - const target = document.getElementById(targetId); - if (target) { - target.innerHTML = oobElement.innerHTML; - // Réinitialiser les event listeners - window.fileTree.setupFolderToggles(); - window.fileTree.setupDragAndDrop(); - } - } - - window.hideNewFolderModal(); } catch (error) { console.error('Erreur lors de la création du dossier:', error); alert('Erreur lors de la création du dossier: ' + error.message);