Optimisation htmx / js par claude

This commit is contained in:
2025-11-11 11:35:11 +01:00
parent cd9a96c760
commit 439880b08f
2 changed files with 154 additions and 147 deletions

View File

@ -77,6 +77,8 @@ class MarkdownEditor {
if (saveStatus) { if (saveStatus) {
saveStatus.textContent = 'Sauvegarde...'; saveStatus.textContent = 'Sauvegarde...';
} }
// Synchroniser le contenu de CodeMirror vers le textarea
this.syncToTextarea();
form.requestSubmit(); form.requestSubmit();
} }
}, 2000); // Auto-save after 2 seconds of inactivity }, 2000); // Auto-save after 2 seconds of inactivity
@ -88,6 +90,8 @@ class MarkdownEditor {
run: () => { run: () => {
const form = this.textarea.closest('form'); const form = this.textarea.closest('form');
if (form) { if (form) {
// Synchroniser le contenu de CodeMirror vers le textarea
this.syncToTextarea();
form.requestSubmit(); form.requestSubmit();
} }
return true; return true;
@ -209,6 +213,12 @@ class MarkdownEditor {
} }
} }
syncToTextarea() {
if (this.editorView && this.textarea) {
this.textarea.value = this.editorView.state.doc.toString();
}
}
destroy() { destroy() {
if (this._updateTimeout) { if (this._updateTimeout) {
clearTimeout(this._updateTimeout); clearTimeout(this._updateTimeout);

View File

@ -1,42 +1,47 @@
/** /**
* FileTree - Gère l'arborescence hiérarchique avec drag & drop * 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 { class FileTree {
constructor() { constructor() {
this.draggedPath = null;
this.draggedType = null;
this.init(); this.init();
} }
init() { init() {
// Écouter les changements htmx dans le file-tree // Utiliser la délégation d'événements sur le conteneur de la sidebar
document.body.addEventListener('htmx:afterSwap', (event) => { // Cela évite de perdre les listeners après les swaps htmx
if (event.detail.target.id === 'file-tree' || event.detail.target.closest('#file-tree')) { const sidebar = document.getElementById('sidebar');
this.setupFolderToggles(); if (!sidebar) {
this.setupDragAndDrop(); console.error('FileTree: sidebar not found');
}
});
// Setup initial si déjà chargé
if (document.getElementById('file-tree')) {
this.setupFolderToggles();
this.setupDragAndDrop();
}
}
setupFolderToggles() {
const folderHeaders = document.querySelectorAll('.folder-header');
folderHeaders.forEach(header => {
// Éviter d'ajouter plusieurs fois le même listener
if (header.dataset.toggleInitialized === 'true') {
return; return;
} }
header.dataset.toggleInitialized = 'true';
header.addEventListener('click', (e) => { // 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(); e.stopPropagation();
this.toggleFolder(header); this.toggleFolder(folderHeader);
}); return;
}
// 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) { toggleFolder(header) {
@ -58,48 +63,89 @@ class FileTree {
} }
} }
setupDragAndDrop() { setupDelegatedDragAndDrop(sidebar) {
const fileItems = document.querySelectorAll('.file-item[draggable="true"]'); // Drag start - délégué pour fichiers et dossiers
const folderItems = document.querySelectorAll('.folder-item'); sidebar.addEventListener('dragstart', (e) => {
const fileItem = e.target.closest('.file-item');
const folderHeader = e.target.closest('.folder-header');
console.log('Setup drag & drop:', { if (fileItem && fileItem.draggable) {
filesCount: fileItems.length, this.handleDragStart(e, 'file', fileItem);
foldersCount: folderItems.length } else if (folderHeader && folderHeader.draggable) {
}); this.handleDragStart(e, 'folder', folderHeader);
// 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) // 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');
folderItems.forEach(folder => { folderItems.forEach(folder => {
const header = folder.querySelector('.folder-header'); const header = folder.querySelector('.folder-header');
const isRoot = folder.dataset.isRoot === 'true'; const isRoot = folder.dataset.isRoot === 'true';
// La racine ne doit pas être draggable if (header && !isRoot) {
if (!isRoot) {
header.setAttribute('draggable', 'true'); 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) { handleDragStart(e, type, item) {
const item = e.currentTarget;
item.classList.add('dragging'); item.classList.add('dragging');
let path, name; let path, name;
@ -126,8 +172,14 @@ class FileTree {
} }
handleDragEnd(e) { handleDragEnd(e) {
const item = e.currentTarget; // 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'); item.classList.remove('dragging');
}
// Supprimer les highlights de tous les dossiers // Supprimer les highlights de tous les dossiers
document.querySelectorAll('.folder-item.drag-over').forEach(f => { document.querySelectorAll('.folder-item.drag-over').forEach(f => {
@ -144,12 +196,13 @@ class FileTree {
this.draggedType = null; this.draggedType = null;
} }
handleDragOver(e) { handleDragOver(e, folderHeader) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const folderHeader = e.currentTarget;
const folderItem = folderHeader.closest('.folder-item'); const folderItem = folderHeader.closest('.folder-item');
if (!folderItem) return;
const targetPath = folderItem.dataset.path; const targetPath = folderItem.dataset.path;
// Empêcher de déplacer un dossier dans lui-même ou dans ses enfants // Empêcher de déplacer un dossier dans lui-même ou dans ses enfants
@ -178,20 +231,18 @@ class FileTree {
} }
} }
handleDragLeave(e) { handleDragLeave(e, folderHeader) {
const folderHeader = e.currentTarget;
const folderItem = folderHeader.closest('.folder-item'); const folderItem = folderHeader.closest('.folder-item');
if (!folderItem) return;
// Vérifier que la souris a vraiment quitté le dossier // Vérifier que la souris a vraiment quitté le dossier
const rect = folderHeader.getBoundingClientRect(); const rect = folderHeader.getBoundingClientRect();
if (e.clientX < rect.left || e.clientX >= rect.right || if (e.clientX < rect.left || e.clientX >= rect.right ||
e.clientY < rect.top || e.clientY >= rect.bottom) { e.clientY < rect.top || e.clientY >= rect.bottom) {
if (folderItem) {
folderItem.classList.remove('drag-over'); folderItem.classList.remove('drag-over');
this.removeDestinationIndicator(); this.removeDestinationIndicator();
} }
} }
}
showDestinationIndicator(folderItem, targetPath) { showDestinationIndicator(folderItem, targetPath) {
let indicator = document.getElementById('drag-destination-indicator'); let indicator = document.getElementById('drag-destination-indicator');
@ -221,12 +272,13 @@ class FileTree {
} }
} }
handleDrop(e) { handleDrop(e, folderHeader) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const folderHeader = e.currentTarget;
const folderItem = folderHeader.closest('.folder-item'); const folderItem = folderHeader.closest('.folder-item');
if (!folderItem) return;
folderItem.classList.remove('drag-over'); folderItem.classList.remove('drag-over');
// Supprimer l'indicateur de destination // Supprimer l'indicateur de destination
@ -287,50 +339,19 @@ class FileTree {
console.log('moveFile called:', { sourcePath, destinationPath }); console.log('moveFile called:', { sourcePath, destinationPath });
try { try {
const body = new URLSearchParams({ // Utiliser htmx.ajax() au lieu de fetch() manuel
source: sourcePath, // HTMX gère automatiquement les swaps oob et le traitement du HTML
destination: destinationPath // Les attributs draggables seront mis à jour automatiquement via htmx:oobAfterSwap
}); htmx.ajax('POST', '/api/files/move', {
values: { source: sourcePath, destination: destinationPath },
console.log('FormData contents:', { swap: 'none' // On ne swap rien directement, le serveur utilise hx-swap-oob
source: body.get('source'), }).then(() => {
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}`); 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');
});
} catch (error) { } catch (error) {
console.error('Erreur lors du déplacement:', error); console.error('Erreur lors du déplacement:', error);
alert('Erreur lors du déplacement du fichier: ' + error.message); 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) // Valider le nom (pas de caractères dangereux)
if (folderName.includes('..') || folderName.includes('\\')) { 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; return;
} }
try { try {
const body = new URLSearchParams({ // Utiliser htmx.ajax() au lieu de fetch() manuel
path: folderName // 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', {
const response = await fetch('/api/folders/create', { values: { path: folderName },
method: 'POST', swap: 'none' // On ne swap rien directement, le serveur utilise hx-swap-oob
headers: { }).then(() => {
'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(); 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');
});
} catch (error) { } catch (error) {
console.error('Erreur lors de la création du dossier:', error); console.error('Erreur lors de la création du dossier:', error);
alert('Erreur lors de la création du dossier: ' + error.message); alert('Erreur lors de la création du dossier: ' + error.message);