Premier commit déjà bien avancé
This commit is contained in:
392
frontend/src/file-tree.js
Normal file
392
frontend/src/file-tree.js
Normal file
@ -0,0 +1,392 @@
|
||||
/**
|
||||
* FileTree - Gère l'arborescence hiérarchique avec drag & drop
|
||||
*/
|
||||
class FileTree {
|
||||
constructor() {
|
||||
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') {
|
||||
return;
|
||||
}
|
||||
header.dataset.toggleInitialized = 'true';
|
||||
|
||||
header.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.toggleFolder(header);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
toggleFolder(header) {
|
||||
const folderItem = header.parentElement;
|
||||
const children = folderItem.querySelector('.folder-children');
|
||||
const toggle = header.querySelector('.folder-toggle');
|
||||
const icon = header.querySelector('.folder-icon');
|
||||
|
||||
if (children.style.display === 'none') {
|
||||
// Ouvrir le dossier
|
||||
children.style.display = 'block';
|
||||
toggle.classList.add('expanded');
|
||||
icon.textContent = '📂';
|
||||
} else {
|
||||
// Fermer le dossier
|
||||
children.style.display = 'none';
|
||||
toggle.classList.remove('expanded');
|
||||
icon.textContent = '📁';
|
||||
}
|
||||
}
|
||||
|
||||
setupDragAndDrop() {
|
||||
const fileItems = document.querySelectorAll('.file-item[draggable="true"]');
|
||||
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.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 drop zones pour les dossiers
|
||||
folderItems.forEach(folder => {
|
||||
const header = folder.querySelector('.folder-header');
|
||||
header.addEventListener('dragover', (e) => this.handleDragOver(e));
|
||||
header.addEventListener('dragleave', (e) => this.handleDragLeave(e));
|
||||
header.addEventListener('drop', (e) => this.handleDrop(e));
|
||||
});
|
||||
}
|
||||
|
||||
handleDragStart(e) {
|
||||
const item = e.target;
|
||||
item.classList.add('dragging');
|
||||
|
||||
const path = item.dataset.path;
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', path);
|
||||
e.dataTransfer.setData('application/note-path', path);
|
||||
}
|
||||
|
||||
handleDragEnd(e) {
|
||||
const item = e.target;
|
||||
item.classList.remove('dragging');
|
||||
|
||||
// Supprimer les highlights de tous les dossiers
|
||||
document.querySelectorAll('.folder-item.drag-over').forEach(f => {
|
||||
f.classList.remove('drag-over');
|
||||
});
|
||||
}
|
||||
|
||||
handleDragOver(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
|
||||
const folderHeader = e.currentTarget;
|
||||
const folderItem = folderHeader.closest('.folder-item');
|
||||
if (folderItem && !folderItem.classList.contains('drag-over')) {
|
||||
folderItem.classList.add('drag-over');
|
||||
}
|
||||
}
|
||||
|
||||
handleDragLeave(e) {
|
||||
e.stopPropagation();
|
||||
const folderHeader = e.currentTarget;
|
||||
const folderItem = folderHeader.closest('.folder-item');
|
||||
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleDrop(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const folderHeader = e.currentTarget;
|
||||
const folderItem = folderHeader.closest('.folder-item');
|
||||
folderItem.classList.remove('drag-over');
|
||||
|
||||
const sourcePath = e.dataTransfer.getData('application/note-path') ||
|
||||
e.dataTransfer.getData('text/plain');
|
||||
const targetFolderPath = folderItem.dataset.path;
|
||||
|
||||
console.log('Drop event:', {
|
||||
sourcePath,
|
||||
targetFolderPath,
|
||||
dataTransfer: e.dataTransfer.types,
|
||||
folderItem: folderItem
|
||||
});
|
||||
|
||||
if (!sourcePath || !targetFolderPath) {
|
||||
console.error('Chemins invalides pour le drag & drop', {
|
||||
sourcePath,
|
||||
targetFolderPath,
|
||||
folderItemDataset: folderItem.dataset
|
||||
});
|
||||
alert(`Erreur: source='${sourcePath}', destination='${targetFolderPath}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ne pas déplacer si c'est le même dossier
|
||||
const sourceDir = sourcePath.includes('/') ?
|
||||
sourcePath.substring(0, sourcePath.lastIndexOf('/')) : '';
|
||||
if (sourceDir === targetFolderPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extraire le nom du fichier
|
||||
const fileName = sourcePath.includes('/') ?
|
||||
sourcePath.substring(sourcePath.lastIndexOf('/') + 1) :
|
||||
sourcePath;
|
||||
const destinationPath = targetFolderPath + '/' + fileName;
|
||||
|
||||
this.moveFile(sourcePath, destinationPath);
|
||||
}
|
||||
|
||||
async moveFile(sourcePath, destinationPath) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
});
|
||||
Reference in New Issue
Block a user