Premier commit déjà bien avancé

This commit is contained in:
2025-11-10 18:33:24 +01:00
commit db4f0508cb
652 changed files with 440521 additions and 0 deletions

392
frontend/src/file-tree.js Normal file
View 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();
});