Optimisation htmx / js par claude
This commit is contained in:
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
Reference in New Issue
Block a user