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) {
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);

View File

@ -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();
}
}
setupFolderToggles() {
const folderHeaders = document.querySelectorAll('.folder-header');
folderHeaders.forEach(header => {
// Éviter d'ajouter plusieurs fois le même listener
if (header.dataset.toggleInitialized === 'true') {
// 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;
}
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();
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) {
@ -58,48 +63,89 @@ class FileTree {
}
}
setupDragAndDrop() {
const fileItems = document.querySelectorAll('.file-item[draggable="true"]');
const folderItems = document.querySelectorAll('.folder-item');
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');
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();
if (fileItem && fileItem.draggable) {
this.handleDragStart(e, 'file', fileItem);
} else if (folderHeader && folderHeader.draggable) {
this.handleDragStart(e, 'folder', folderHeader);
}
}, 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 => {
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;
// 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,20 +231,18 @@ 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();
}
}
}
showDestinationIndicator(folderItem, targetPath) {
let indicator = document.getElementById('drag-destination-indicator');
@ -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
});
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();
}
}
// 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');
});
} 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
});
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();
}
}
// 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');
});
} catch (error) {
console.error('Erreur lors de la création du dossier:', error);
alert('Erreur lors de la création du dossier: ' + error.message);