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