833 lines
30 KiB
JavaScript
833 lines
30 KiB
JavaScript
import { debug, debugError } from './debug.js';
|
|
/**
|
|
* 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() {
|
|
this.setupEventListeners();
|
|
|
|
// Restaurer l'état des dossiers au démarrage
|
|
setTimeout(() => this.restoreFolderStates(), 500);
|
|
|
|
debug('FileTree initialized with event delegation');
|
|
}
|
|
|
|
setupEventListeners() {
|
|
// 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;
|
|
}
|
|
|
|
// Supprimer les anciens listeners s'ils existent
|
|
if (this.clickHandler) {
|
|
sidebar.removeEventListener('click', this.clickHandler);
|
|
}
|
|
|
|
// Créer et stocker le handler pour pouvoir le supprimer plus tard
|
|
this.clickHandler = (e) => {
|
|
// Ignorer les clics sur les checkboxes
|
|
if (e.target.classList.contains('selection-checkbox')) {
|
|
return;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
};
|
|
|
|
// Attacher le handler
|
|
sidebar.addEventListener('click', this.clickHandler);
|
|
|
|
// Event listeners délégués pour le drag & drop
|
|
this.setupDelegatedDragAndDrop(sidebar);
|
|
}
|
|
|
|
toggleFolder(header) {
|
|
const folderItem = header.parentElement;
|
|
const children = folderItem.querySelector('.folder-children');
|
|
const toggle = header.querySelector('.folder-toggle');
|
|
const icon = header.querySelector('.folder-icon');
|
|
const folderPath = folderItem.getAttribute('data-path');
|
|
|
|
if (children.style.display === 'none') {
|
|
// Ouvrir le dossier
|
|
children.style.display = 'block';
|
|
toggle.classList.add('expanded');
|
|
icon.innerHTML = '<i data-lucide="folder-open" class="icon-sm"></i>';
|
|
this.saveFolderState(folderPath, true);
|
|
} else {
|
|
// Fermer le dossier
|
|
children.style.display = 'none';
|
|
toggle.classList.remove('expanded');
|
|
icon.innerHTML = '<i data-lucide="folder" class="icon-sm"></i>';
|
|
this.saveFolderState(folderPath, false);
|
|
}
|
|
}
|
|
|
|
saveFolderState(folderPath, isExpanded) {
|
|
if (!folderPath) return;
|
|
const expandedFolders = this.getExpandedFolders();
|
|
if (isExpanded) {
|
|
expandedFolders.add(folderPath);
|
|
} else {
|
|
expandedFolders.delete(folderPath);
|
|
}
|
|
localStorage.setItem('expanded-folders', JSON.stringify([...expandedFolders]));
|
|
}
|
|
|
|
getExpandedFolders() {
|
|
const saved = localStorage.getItem('expanded-folders');
|
|
return saved ? new Set(JSON.parse(saved)) : new Set();
|
|
}
|
|
|
|
restoreFolderStates() {
|
|
const expandedFolders = this.getExpandedFolders();
|
|
document.querySelectorAll('.folder-item').forEach(folderItem => {
|
|
const folderPath = folderItem.getAttribute('data-path');
|
|
if (folderPath && expandedFolders.has(folderPath)) {
|
|
const header = folderItem.querySelector('.folder-header');
|
|
const children = folderItem.querySelector('.folder-children');
|
|
const toggle = header?.querySelector('.folder-toggle');
|
|
const icon = header?.querySelector('.folder-icon');
|
|
|
|
if (children && toggle && icon) {
|
|
children.style.display = 'block';
|
|
toggle.classList.add('expanded');
|
|
icon.innerHTML = '<i data-lucide="folder-open" class="icon-sm"></i>';
|
|
}
|
|
}
|
|
});
|
|
|
|
// Réinitialiser les icônes Lucide
|
|
if (typeof lucide !== 'undefined') {
|
|
lucide.createIcons();
|
|
}
|
|
}
|
|
|
|
setupDelegatedDragAndDrop(sidebar) {
|
|
// Supprimer les anciens handlers s'ils existent
|
|
if (this.dragStartHandler) {
|
|
sidebar.removeEventListener('dragstart', this.dragStartHandler);
|
|
sidebar.removeEventListener('dragend', this.dragEndHandler);
|
|
sidebar.removeEventListener('dragover', this.dragOverHandler);
|
|
sidebar.removeEventListener('dragleave', this.dragLeaveHandler);
|
|
sidebar.removeEventListener('drop', this.dropHandler);
|
|
}
|
|
|
|
// Drag start - délégué pour fichiers et dossiers
|
|
this.dragStartHandler = (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é
|
|
this.dragEndHandler = (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 et la racine
|
|
this.dragOverHandler = (e) => {
|
|
const folderHeader = e.target.closest('.folder-header');
|
|
const rootHeader = e.target.closest('.sidebar-section-header[data-section="notes"]');
|
|
const target = folderHeader || rootHeader;
|
|
if (target) {
|
|
this.handleDragOver(e, target);
|
|
}
|
|
};
|
|
|
|
// Drag leave - délégué
|
|
this.dragLeaveHandler = (e) => {
|
|
const folderHeader = e.target.closest('.folder-header');
|
|
const rootHeader = e.target.closest('.sidebar-section-header[data-section="notes"]');
|
|
const target = folderHeader || rootHeader;
|
|
if (target) {
|
|
this.handleDragLeave(e, target);
|
|
}
|
|
};
|
|
|
|
// Drop - délégué
|
|
this.dropHandler = (e) => {
|
|
const folderHeader = e.target.closest('.folder-header');
|
|
const rootHeader = e.target.closest('.sidebar-section-header[data-section="notes"]');
|
|
const target = folderHeader || rootHeader;
|
|
if (target) {
|
|
this.handleDrop(e, target);
|
|
}
|
|
};
|
|
|
|
// Attacher les handlers
|
|
sidebar.addEventListener('dragstart', this.dragStartHandler);
|
|
sidebar.addEventListener('dragend', this.dragEndHandler);
|
|
sidebar.addEventListener('dragover', this.dragOverHandler);
|
|
sidebar.addEventListener('dragleave', this.dragLeaveHandler);
|
|
sidebar.addEventListener('drop', this.dropHandler);
|
|
|
|
// 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'))) {
|
|
debug('FileTree: afterSwap detected, updating attributes and restoring folder states...');
|
|
this.updateDraggableAttributes();
|
|
setTimeout(() => this.restoreFolderStates(), 50);
|
|
}
|
|
});
|
|
|
|
// É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;
|
|
// Ignorer les swaps de statut (auto-save-status, save-status)
|
|
if (target && target.id === 'file-tree') {
|
|
debug('FileTree: oobAfterSwap detected, updating attributes and restoring folder states...');
|
|
this.updateDraggableAttributes();
|
|
setTimeout(() => this.restoreFolderStates(), 50);
|
|
}
|
|
});
|
|
|
|
// Écouter les restaurations d'historique (bouton retour du navigateur)
|
|
document.body.addEventListener('htmx:historyRestore', () => {
|
|
debug('FileTree: History restored, re-initializing event listeners...');
|
|
// Réinitialiser complètement les event listeners après restauration de l'historique
|
|
setTimeout(() => {
|
|
this.setupEventListeners();
|
|
this.updateDraggableAttributes();
|
|
this.restoreFolderStates();
|
|
}, 50);
|
|
});
|
|
}
|
|
|
|
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';
|
|
|
|
if (header && !isRoot) {
|
|
header.setAttribute('draggable', 'true');
|
|
}
|
|
});
|
|
}
|
|
|
|
handleDragStart(e, type, item) {
|
|
item.classList.add('dragging');
|
|
|
|
let path, name;
|
|
if (type === 'file') {
|
|
path = item.dataset.path;
|
|
name = path.split('/').pop();
|
|
} else if (type === 'folder') {
|
|
const folderItem = item.closest('.folder-item');
|
|
path = folderItem.dataset.path;
|
|
name = folderItem.querySelector('.folder-name').textContent.trim();
|
|
}
|
|
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
e.dataTransfer.setData('text/plain', path);
|
|
e.dataTransfer.setData('application/note-path', path);
|
|
e.dataTransfer.setData('application/note-type', type);
|
|
e.dataTransfer.setData('application/note-name', name);
|
|
|
|
// Stocker le chemin source pour validation
|
|
this.draggedPath = path;
|
|
this.draggedType = type;
|
|
|
|
debug('Drag start:', { type, path, name });
|
|
}
|
|
|
|
handleDragEnd(e) {
|
|
// 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 => {
|
|
f.classList.remove('drag-over');
|
|
});
|
|
|
|
// Supprimer l'indicateur de destination
|
|
const indicator = document.getElementById('drag-destination-indicator');
|
|
if (indicator) {
|
|
indicator.remove();
|
|
}
|
|
|
|
this.draggedPath = null;
|
|
this.draggedType = null;
|
|
}
|
|
|
|
handleDragOver(e, target) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
// Gérer soit un folder-header dans un folder-item, soit la racine (sidebar-section-header)
|
|
const isRoot = target.classList.contains('sidebar-section-header');
|
|
const targetElement = isRoot ? target : target.closest('.folder-item');
|
|
|
|
if (!targetElement) return;
|
|
|
|
const targetPath = targetElement.dataset.path;
|
|
|
|
// Empêcher de déplacer un dossier dans lui-même ou dans ses enfants
|
|
if (this.draggedType === 'folder' && this.draggedPath) {
|
|
if (targetPath === this.draggedPath || targetPath.startsWith(this.draggedPath + '/')) {
|
|
e.dataTransfer.dropEffect = 'none';
|
|
targetElement.classList.remove('drag-over');
|
|
this.removeDestinationIndicator();
|
|
return;
|
|
}
|
|
}
|
|
|
|
e.dataTransfer.dropEffect = 'move';
|
|
|
|
if (targetElement && !targetElement.classList.contains('drag-over')) {
|
|
// Retirer la classe des autres dossiers et de la racine
|
|
document.querySelectorAll('.folder-item.drag-over, .sidebar-section-header.drag-over').forEach(f => {
|
|
if (f !== targetElement) {
|
|
f.classList.remove('drag-over');
|
|
}
|
|
});
|
|
targetElement.classList.add('drag-over');
|
|
|
|
// Afficher l'indicateur de destination
|
|
this.showDestinationIndicator(targetElement, targetPath, isRoot);
|
|
}
|
|
}
|
|
|
|
handleDragLeave(e, target) {
|
|
// Gérer soit un folder-header dans un folder-item, soit la racine (sidebar-section-header)
|
|
const isRoot = target.classList.contains('sidebar-section-header');
|
|
const targetElement = isRoot ? target : target.closest('.folder-item');
|
|
|
|
if (!targetElement) return;
|
|
|
|
// Vérifier que la souris a vraiment quitté l'élément
|
|
const rect = target.getBoundingClientRect();
|
|
if (e.clientX < rect.left || e.clientX >= rect.right ||
|
|
e.clientY < rect.top || e.clientY >= rect.bottom) {
|
|
targetElement.classList.remove('drag-over');
|
|
this.removeDestinationIndicator();
|
|
}
|
|
}
|
|
|
|
showDestinationIndicator(targetElement, targetPath, isRoot) {
|
|
let indicator = document.getElementById('drag-destination-indicator');
|
|
if (!indicator) {
|
|
indicator = document.createElement('div');
|
|
indicator.id = 'drag-destination-indicator';
|
|
indicator.className = 'drag-destination-indicator';
|
|
document.body.appendChild(indicator);
|
|
}
|
|
|
|
const folderName = targetElement.querySelector('.folder-name').textContent.trim();
|
|
const displayPath = isRoot ? 'notes/' : targetPath;
|
|
|
|
indicator.innerHTML = `
|
|
<span class="indicator-icon">📥</span>
|
|
<span class="indicator-text">Déplacer vers: <strong>${folderName}</strong></span>
|
|
<span class="indicator-path">${displayPath}</span>
|
|
`;
|
|
indicator.style.display = 'flex';
|
|
}
|
|
|
|
removeDestinationIndicator() {
|
|
const indicator = document.getElementById('drag-destination-indicator');
|
|
if (indicator) {
|
|
indicator.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
handleDrop(e, target) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
// Gérer soit un folder-header dans un folder-item, soit la racine (sidebar-section-header)
|
|
const isRoot = target.classList.contains('sidebar-section-header');
|
|
const targetElement = isRoot ? target : target.closest('.folder-item');
|
|
|
|
if (!targetElement) return;
|
|
|
|
targetElement.classList.remove('drag-over');
|
|
|
|
// Supprimer l'indicateur de destination
|
|
this.removeDestinationIndicator();
|
|
|
|
const sourcePath = e.dataTransfer.getData('application/note-path') ||
|
|
e.dataTransfer.getData('text/plain');
|
|
const sourceType = e.dataTransfer.getData('application/note-type');
|
|
const targetFolderPath = targetElement.dataset.path;
|
|
|
|
debug('Drop event:', {
|
|
sourcePath,
|
|
sourceType,
|
|
targetFolderPath,
|
|
dataTransfer: e.dataTransfer.types
|
|
});
|
|
|
|
// Validation : sourcePath doit exister, targetFolderPath peut être vide (racine)
|
|
if (!sourcePath || targetFolderPath === undefined || targetFolderPath === null) {
|
|
console.error('Chemins invalides pour le drag & drop', {
|
|
sourcePath,
|
|
targetFolderPath
|
|
});
|
|
alert(`Erreur: source='${sourcePath}', destination='${targetFolderPath}'`);
|
|
return;
|
|
}
|
|
|
|
// Empêcher de déplacer un dossier dans lui-même ou dans ses enfants
|
|
if (sourceType === 'folder') {
|
|
if (targetFolderPath === sourcePath || targetFolderPath.startsWith(sourcePath + '/')) {
|
|
alert('Impossible de déplacer un dossier dans lui-même ou dans un de ses sous-dossiers');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Ne pas déplacer si c'est déjà dans le même dossier parent
|
|
const sourceDir = sourcePath.includes('/') ?
|
|
sourcePath.substring(0, sourcePath.lastIndexOf('/')) : '';
|
|
if (sourceDir === targetFolderPath) {
|
|
debug('Déjà dans le même dossier parent, rien à faire');
|
|
return;
|
|
}
|
|
|
|
// Extraire le nom du fichier/dossier
|
|
const itemName = sourcePath.includes('/') ?
|
|
sourcePath.substring(sourcePath.lastIndexOf('/') + 1) :
|
|
sourcePath;
|
|
|
|
// Construire le chemin de destination
|
|
// Si targetFolderPath est vide (racine), ne pas ajouter de slash
|
|
const destinationPath = targetFolderPath === '' ? itemName : targetFolderPath + '/' + itemName;
|
|
|
|
debug(`Déplacement: ${sourcePath} → ${destinationPath}`);
|
|
this.moveFile(sourcePath, destinationPath);
|
|
}
|
|
|
|
async moveFile(sourcePath, destinationPath) {
|
|
debug('moveFile called:', { sourcePath, destinationPath });
|
|
|
|
try {
|
|
// 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(() => {
|
|
debug(`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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gestion de la création de notes
|
|
*/
|
|
window.showNewNoteModal = function() {
|
|
const modal = document.getElementById('new-note-modal');
|
|
modal.style.display = 'flex';
|
|
setTimeout(() => {
|
|
document.getElementById('note-name').focus();
|
|
}, 100);
|
|
}
|
|
|
|
window.hideNewNoteModal = function() {
|
|
const modal = document.getElementById('new-note-modal');
|
|
modal.style.display = 'none';
|
|
document.getElementById('note-name').value = '';
|
|
}
|
|
|
|
window.handleNewNote = function(event) {
|
|
event.preventDefault();
|
|
|
|
let noteName = document.getElementById('note-name').value.trim();
|
|
|
|
if (!noteName) {
|
|
alert('Veuillez entrer un nom de note');
|
|
return;
|
|
}
|
|
|
|
// Ajouter .md si pas déjà présent
|
|
if (!noteName.endsWith('.md')) {
|
|
noteName += '.md';
|
|
}
|
|
|
|
// Valider le nom (pas de caractères dangereux)
|
|
if (noteName.includes('..') || noteName.includes('\\')) {
|
|
alert('Nom de note invalide. Évitez les caractères \\ et ..');
|
|
return;
|
|
}
|
|
|
|
// Fermer la modale
|
|
window.hideNewNoteModal();
|
|
|
|
// Utiliser HTMX pour charger la note (au lieu de fetch manuel)
|
|
const editorContainer = document.getElementById('editor-container');
|
|
|
|
// Supprimer temporairement le trigger "load" pour éviter qu'il se redéclenche
|
|
editorContainer.removeAttribute('hx-trigger');
|
|
|
|
// Utiliser l'API HTMX pour charger la note
|
|
if (typeof htmx !== 'undefined') {
|
|
htmx.ajax('GET', `/api/notes/${encodeURIComponent(noteName)}`, {
|
|
target: '#editor-container',
|
|
swap: 'innerHTML',
|
|
pushUrl: true
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gestion de la création de dossiers
|
|
*/
|
|
window.showNewFolderModal = function() {
|
|
const modal = document.getElementById('new-folder-modal');
|
|
modal.style.display = 'flex';
|
|
setTimeout(() => {
|
|
document.getElementById('folder-name').focus();
|
|
}, 100);
|
|
}
|
|
|
|
window.hideNewFolderModal = function() {
|
|
const modal = document.getElementById('new-folder-modal');
|
|
modal.style.display = 'none';
|
|
document.getElementById('folder-name').value = '';
|
|
}
|
|
|
|
window.handleNewFolder = async function(event) {
|
|
event.preventDefault();
|
|
|
|
let folderName = document.getElementById('folder-name').value.trim();
|
|
|
|
if (!folderName) {
|
|
alert('Veuillez entrer un nom de dossier');
|
|
return;
|
|
}
|
|
|
|
// Valider le nom (pas de caractères dangereux)
|
|
if (folderName.includes('..') || folderName.includes('\\')) {
|
|
alert('Nom de dossier invalide. Évitez les caractères \\ et ..');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// 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();
|
|
debug(`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);
|
|
}
|
|
}
|
|
|
|
// Fermer les modales avec Escape
|
|
document.addEventListener('keydown', (event) => {
|
|
if (event.key === 'Escape') {
|
|
const folderModal = document.getElementById('new-folder-modal');
|
|
const noteModal = document.getElementById('new-note-modal');
|
|
|
|
if (folderModal && folderModal.style.display === 'flex') {
|
|
window.hideNewFolderModal();
|
|
}
|
|
if (noteModal && noteModal.style.display === 'flex') {
|
|
window.hideNewNoteModal();
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Initialisation automatique
|
|
*/
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
window.fileTree = new FileTree();
|
|
window.selectionManager = new SelectionManager();
|
|
});
|
|
|
|
/**
|
|
* SelectionManager - Gère le mode sélection et la suppression en masse
|
|
*/
|
|
class SelectionManager {
|
|
constructor() {
|
|
this.isSelectionMode = false;
|
|
this.selectedPaths = new Set();
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
// Écouter les événements HTMX pour réinitialiser les listeners après les swaps
|
|
document.body.addEventListener('htmx:afterSwap', (event) => {
|
|
if (event.detail.target.id === 'file-tree' || event.detail.target.closest('#file-tree')) {
|
|
this.attachCheckboxListeners();
|
|
if (this.isSelectionMode) {
|
|
this.showCheckboxes();
|
|
}
|
|
}
|
|
});
|
|
|
|
document.body.addEventListener('htmx:oobAfterSwap', (event) => {
|
|
if (event.detail.target.id === 'file-tree') {
|
|
this.attachCheckboxListeners();
|
|
if (this.isSelectionMode) {
|
|
this.showCheckboxes();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Attacher les listeners initiaux
|
|
setTimeout(() => this.attachCheckboxListeners(), 500);
|
|
}
|
|
|
|
attachCheckboxListeners() {
|
|
const checkboxes = document.querySelectorAll('.selection-checkbox');
|
|
checkboxes.forEach(checkbox => {
|
|
// Retirer l'ancien listener s'il existe
|
|
checkbox.removeEventListener('change', this.handleCheckboxChange);
|
|
// Ajouter le nouveau listener
|
|
checkbox.addEventListener('change', (e) => this.handleCheckboxChange(e));
|
|
});
|
|
}
|
|
|
|
handleCheckboxChange(e) {
|
|
const checkbox = e.target;
|
|
const path = checkbox.dataset.path;
|
|
|
|
if (checkbox.checked) {
|
|
window.selectionManager.selectedPaths.add(path);
|
|
} else {
|
|
window.selectionManager.selectedPaths.delete(path);
|
|
}
|
|
|
|
window.selectionManager.updateToolbar();
|
|
}
|
|
|
|
toggleSelectionMode() {
|
|
this.isSelectionMode = !this.isSelectionMode;
|
|
|
|
if (this.isSelectionMode) {
|
|
this.showCheckboxes();
|
|
document.getElementById('toggle-selection-mode')?.classList.add('active');
|
|
} else {
|
|
this.hideCheckboxes();
|
|
this.clearSelection();
|
|
document.getElementById('toggle-selection-mode')?.classList.remove('active');
|
|
}
|
|
}
|
|
|
|
showCheckboxes() {
|
|
const checkboxes = document.querySelectorAll('.selection-checkbox');
|
|
checkboxes.forEach(checkbox => {
|
|
checkbox.style.display = 'inline-block';
|
|
});
|
|
}
|
|
|
|
hideCheckboxes() {
|
|
const checkboxes = document.querySelectorAll('.selection-checkbox');
|
|
checkboxes.forEach(checkbox => {
|
|
checkbox.style.display = 'none';
|
|
checkbox.checked = false;
|
|
});
|
|
}
|
|
|
|
clearSelection() {
|
|
this.selectedPaths.clear();
|
|
this.updateToolbar();
|
|
}
|
|
|
|
updateToolbar() {
|
|
const toolbar = document.getElementById('selection-toolbar');
|
|
const countSpan = document.getElementById('selection-count');
|
|
|
|
if (this.selectedPaths.size > 0) {
|
|
toolbar.style.display = 'flex';
|
|
countSpan.textContent = `${this.selectedPaths.size} élément(s) sélectionné(s)`;
|
|
} else {
|
|
toolbar.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
showDeleteConfirmationModal() {
|
|
const modal = document.getElementById('delete-confirmation-modal');
|
|
const countSpan = document.getElementById('delete-count');
|
|
const itemsList = document.getElementById('delete-items-list');
|
|
|
|
countSpan.textContent = this.selectedPaths.size;
|
|
|
|
// Générer la liste des éléments à supprimer
|
|
itemsList.innerHTML = '';
|
|
const ul = document.createElement('ul');
|
|
ul.style.margin = '0';
|
|
ul.style.padding = '0 0 0 1.5rem';
|
|
ul.style.color = 'var(--text-primary)';
|
|
|
|
this.selectedPaths.forEach(path => {
|
|
const li = document.createElement('li');
|
|
li.style.marginBottom = '0.5rem';
|
|
|
|
// Déterminer si c'est un dossier
|
|
const checkbox = document.querySelector(`.selection-checkbox[data-path="${path}"]`);
|
|
const isDir = checkbox?.dataset.isDir === 'true';
|
|
|
|
li.innerHTML = `${isDir ? '<i data-lucide="folder" class="icon-sm"></i>' : '<i data-lucide="file-text" class="icon-sm"></i>'} <code>${path}</code>`;
|
|
ul.appendChild(li);
|
|
});
|
|
|
|
itemsList.appendChild(ul);
|
|
modal.style.display = 'flex';
|
|
}
|
|
|
|
hideDeleteConfirmationModal() {
|
|
const modal = document.getElementById('delete-confirmation-modal');
|
|
modal.style.display = 'none';
|
|
}
|
|
|
|
async deleteSelectedItems() {
|
|
const paths = Array.from(this.selectedPaths);
|
|
|
|
if (paths.length === 0) {
|
|
alert('Aucun élément sélectionné');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Construire le corps de la requête au format query string
|
|
// Le backend attend: paths[]=path1&paths[]=path2
|
|
const params = new URLSearchParams();
|
|
paths.forEach(path => {
|
|
params.append('paths[]', path);
|
|
});
|
|
|
|
// Utiliser fetch() avec le corps en query string
|
|
const response = await fetch('/api/files/delete-multiple', {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
},
|
|
body: params.toString()
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const html = await response.text();
|
|
|
|
// Parser le HTML pour trouver les éléments avec hx-swap-oob
|
|
const parser = new DOMParser();
|
|
const doc = parser.parseFromString(html, 'text/html');
|
|
|
|
// Traiter les swaps out-of-band manuellement
|
|
doc.querySelectorAll('[hx-swap-oob]').forEach(element => {
|
|
const targetId = element.id;
|
|
const target = document.getElementById(targetId);
|
|
if (target) {
|
|
target.innerHTML = element.innerHTML;
|
|
// Déclencher l'événement htmx pour que les listeners se réattachent
|
|
htmx.process(target);
|
|
}
|
|
});
|
|
|
|
debug(`${paths.length} élément(s) supprimé(s)`);
|
|
|
|
// Fermer la modale
|
|
this.hideDeleteConfirmationModal();
|
|
|
|
// Réinitialiser la sélection et garder le mode sélection actif
|
|
this.clearSelection();
|
|
|
|
// Réattacher les listeners sur les nouvelles checkboxes
|
|
setTimeout(() => {
|
|
this.attachCheckboxListeners();
|
|
if (this.isSelectionMode) {
|
|
this.showCheckboxes();
|
|
}
|
|
}, 100);
|
|
|
|
} catch (error) {
|
|
console.error('Erreur lors de la suppression:', error);
|
|
alert('Erreur lors de la suppression des éléments: ' + error.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fonctions globales pour les boutons
|
|
*/
|
|
window.toggleSelectionMode = function() {
|
|
window.selectionManager.toggleSelectionMode();
|
|
};
|
|
|
|
window.deleteSelected = function() {
|
|
window.selectionManager.showDeleteConfirmationModal();
|
|
};
|
|
|
|
window.cancelSelection = function() {
|
|
window.selectionManager.toggleSelectionMode();
|
|
};
|
|
|
|
window.hideDeleteConfirmationModal = function() {
|
|
window.selectionManager.hideDeleteConfirmationModal();
|
|
};
|
|
|
|
window.confirmDelete = function() {
|
|
window.selectionManager.deleteSelectedItems();
|
|
}; |