diff --git a/frontend/js/palette.js b/frontend/js/palette.js index 536bd03..69e51c4 100644 --- a/frontend/js/palette.js +++ b/frontend/js/palette.js @@ -25,6 +25,7 @@ const CMDS = [ { id:'edit-current', label:'✏️ Éditer fichier courant', desc:'Ouvrir le fichier actif dans l\'éditeur', cat:'Fichiers', act:()=>{close();const f=getCurrentFile();if(f)openEditor(f.vault,f.path);else showToast('Aucun fichier ouvert','error');} }, { id:'popout', label:'🪟 Pop-out', desc:'Ouvrir le document dans une fenêtre', cat:'Fichiers', act:()=>{close();const f=getCurrentFile();if(f)window.open(`/popout/${encodeURIComponent(f.vault)}/${encodeURIComponent(f.path)}`,'popout','width=1000,height=700');else showToast('Aucun fichier ouvert','error');} }, { id:'create-file', label:'📄 Créer fichier markdown', desc:'Choisir la vault, naviguer, puis nommer le fichier', cat:'Fichiers', act:()=>startBrowse('create-file') }, + { id:'delete-file', label:'🗑️ Supprimer fichier', desc:'Rechercher un fichier à supprimer', cat:'Fichiers', act:()=>startBrowse('delete-file') }, { id:'delete-cur', label:'🗑️ Supprimer fichier courant', desc:'Supprimer le fichier de l\'onglet actif', cat:'Fichiers', act:async()=>{close();await delCurFile();} }, { id:'create-dir', label:'📁 Créer répertoire', desc:'Choisir la vault, naviguer, puis nommer le dossier', cat:'Répertoires', act:()=>startBrowse('create-dir') }, { id:'delete-dir', label:'🗑️ Supprimer répertoire', desc:'Choisir la vault, naviguer et supprimer', cat:'Répertoires', act:()=>startBrowse('delete-dir') }, @@ -50,8 +51,9 @@ async function renderVaultPicker() { if (_items.length === 0) { _items = [{ type:'_empty', label: 'Aucune vault trouvée', sublabel: '' }]; } + _allItems = _items; render(); - } catch { _items = [{ type:'_error', label: 'Erreur de chargement', sublabel: '' }]; render(); } + } catch { _items = [{ type:'_error', label: 'Erreur de chargement', sublabel: '' }]; _allItems = _items; render(); } } // ── Browse: directory navigation ── @@ -66,8 +68,8 @@ async function renderBrowse() { let acc = ''; for (const s of segs) { acc = acc ? acc+'/'+s : s; html += `/${esc(s)}`; } bc.innerHTML = html; - bc.querySelectorAll('.cp-bc-item[data-path]').forEach(el => el.addEventListener('click', ()=>{ _browse.path=el.dataset.path; renderBrowse(); })); - bc.querySelector('.cp-bc-root').addEventListener('click', ()=>{ _browse.path=''; renderBrowse(); }); + bc.querySelectorAll('.cp-bc-item[data-path]').forEach(el => el.addEventListener('click', ()=>{ _browse.path=el.dataset.path; _browseFilter=''; if(_browseFilterEl)_browseFilterEl.value=''; renderBrowse(); })); + bc.querySelector('.cp-bc-root').addEventListener('click', ()=>{ _browse.path=''; _browseFilter=''; if(_browseFilterEl)_browseFilterEl.value=''; renderBrowse(); }); } try { @@ -76,32 +78,53 @@ async function renderBrowse() { const dirs = items.filter(i => i.type === 'directory' || i.is_dir); const files = items.filter(i => i.type === 'file' && !i.is_dir); - // Action row - const actLabel = { 'create-file':'📄 Créer fichier ici', 'create-dir':'📁 Créer dossier ici', 'delete-dir':'🗑️ Supprimer ce dossier' }; - if (_browse.action === 'delete-dir' && path) { - _items.push({ type:'_action', id:'confirm', label: '🗑️ Supprimer '+ (path.split('/').pop()||path), sublabel: 'Tout le contenu sera supprimé', act:()=>execDelDir(path) }); - } else if (_browse.action !== 'delete-dir') { - _items.push({ type:'_action', id:'confirm', label: actLabel[_browse.action]||'', sublabel: 'Dans '+(path||'racine'), act:()=>askName() }); + // Action row (for create/delete actions) + const actLabel = { + 'create-file':'📄 Créer fichier ici', + 'create-dir':'📁 Créer dossier ici', + 'delete-dir':'🗑️ Supprimer ce dossier', + 'delete-file':'🗑️ Supprimer un fichier dans ce dossier', + 'open-file':'📂 Ouvrir un fichier dans ce dossier' + }; + const isDeleteDir = _browse.action === 'delete-dir' && path; + const isDeleteFile = _browse.action === 'delete-file'; + const isOpenFile = _browse.action === 'open-file'; + const needsName = _browse.action === 'create-file' || _browse.action === 'create-dir'; + + if (isDeleteDir) { + _items.push({ type:'_action', label: '🗑️ Supprimer '+ (path.split('/').pop()||path), sublabel: 'Tout le contenu sera supprimé', act:()=>execDelDir(path) }); + } else if (needsName) { + _items.push({ type:'_action', label: actLabel[_browse.action]||'', sublabel: 'Dans '+(path||'racine'), act:()=>askName() }); } + // For delete-file and open-file, the action row is informational (no direct action — user selects a file below) // Parent if (path) { - const parent = path.split('/').slice(0,-1).join('/'); - _items.push({ type:'_nav', label: '📂 .. (parent)', go: parent }); + _items.push({ type:'_nav', label: '📂 .. (parent)', go: path.split('/').slice(0,-1).join('/') }); } - // Subdirs + // Subdirs (always navigable) for (const d of dirs) { const fp = path ? path+'/'+d.name : d.name; _items.push({ type:'_nav', label: '📁 '+d.name, go: fp }); } - // Files (info) - for (const f of files) _items.push({ type:'_info', label: '📄 '+f.name, sublabel: '' }); + + // Files — clickable for open-file and delete-file, informational otherwise + const fileType = (isOpenFile || isDeleteFile) ? '_file' : '_info'; + for (const f of files) { + const fp = path ? path+'/'+f.name : f.name; + if (isDeleteFile) { + _items.push({ type:'_file', label: '📄 '+f.name, sublabel: fp, act:()=>execDelFile(fp) }); + } else if (isOpenFile) { + _items.push({ type:'_file', label: '📄 '+f.name, sublabel: fp, act:()=>{ close(); openFile(vault, fp); } }); + } else { + _items.push({ type:'_info', label: '📄 '+f.name, sublabel: fp }); + } + } if (_items.length === 0) _items.push({ type:'_empty', label: '📂 Dossier vide', sublabel: '' }); } catch { _items = [{ type:'_error', label: 'Erreur', sublabel: '' }]; } - // Store all items for filtering _allItems = _items; applyBrowseFilter(); } @@ -115,8 +138,12 @@ function applyBrowseFilter() { const sublabel = (it.sublabel || '').toLowerCase(); return label.includes(q) || sublabel.includes(q); }; - // Always keep action row and parent nav - _items = _allItems.filter(it => it.type === '_action' || it.type === '_nav' && it.label.includes('..') || shouldKeep(it)); + // Always keep action row and parent nav and empty/error messages + _items = _allItems.filter(it => + it.type === '_action' || it.type === '_empty' || it.type === '_error' || + (it.type === '_nav' && it.label.includes('..')) || + shouldKeep(it) + ); render(); } @@ -129,7 +156,6 @@ function onBrowseFilterKey(e) { if (e.key === 'Escape') { e.preventDefault(); if (_browseFilter) { - // Clear filter _browseFilter = ''; if (_browseFilterEl) _browseFilterEl.value = ''; applyBrowseFilter(); @@ -138,12 +164,20 @@ function onBrowseFilterKey(e) { } } else if (e.key === 'ArrowDown') { e.preventDefault(); - const first = _results.querySelector('.cp-item'); - if (first) { first.classList.add('cp-selected'); _sel = 0; first.focus ? first.focus() : _overlay.focus(); } + _sel = 0; + updSel(_results.querySelectorAll('.cp-item')); + // Focus overlay so subsequent arrows navigate items + _overlay.focus(); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + const items = _results.querySelectorAll('.cp-item'); + _sel = Math.max(items.length - 1, 0); + updSel(items); + _overlay.focus(); } else if (e.key === 'Enter') { e.preventDefault(); if (_items.length > 0) exec(_sel); - } else if (e.key === 'Backspace' && !_browseFilter && !_browseFilterEl?.value) { + } else if (e.key === 'Backspace' && !_browseFilter && (!_browseFilterEl || !_browseFilterEl.value)) { e.preventDefault(); const segs = _browse.path ? _browse.path.split('/') : []; if (segs.length > 0) { _browse.path = segs.slice(0, -1).join('/'); renderBrowse(); } @@ -224,6 +258,15 @@ async function execDelDir(path) { } catch(e) { showToast('Erreur : '+(e.message||e),'error'); } } +async function execDelFile(path) { + if (!confirm(`Supprimer le fichier "${path}" ?`)) return; + try { + const d = await api(`/api/file/${encodeURIComponent(_browse.vault)}?path=${encodeURIComponent(path)}`, { method:'DELETE' }); + if (d?.status==='ok' || d?.success) { showToast(`🗑️ ${path} supprimé`,'success'); close(); refreshSidebar(); } + else showToast('Erreur','error'); + } catch(e) { showToast('Erreur : '+(e.message||e),'error'); } +} + async function delCurFile() { const f = getCurrentFile(); if (!f) { showToast('Aucun fichier ouvert','error'); return; } @@ -237,7 +280,6 @@ async function delCurFile() { } function refreshSidebar() { - // Try to refresh the vault tree import('./sidebar.js').then(m => { if (m.loadVaults) m.loadVaults(); }).catch(() => {}); } @@ -246,24 +288,22 @@ function createDOM() { if (_overlay) return; _overlay = document.createElement('div'); _overlay.className = 'command-palette-overlay'; - _overlay.tabIndex = -1; // needed to receive keyboard events when input is hidden (browse mode) + _overlay.tabIndex = -1; _overlay.innerHTML = `
Ctrl+Shift+Espace
-
`; + `; _input = _overlay.querySelector('.cp-input'); _browseFilterEl = _overlay.querySelector('#cp-browse-filter'); _results = _overlay.querySelector('.cp-results'); _overlay.addEventListener('click', (e) => { if (e.target === _overlay) close(); }); _input.addEventListener('keydown', onKeyDown); _input.addEventListener('input', onInput); - // Browse filter events if (_browseFilterEl) { _browseFilterEl.addEventListener('input', onBrowseFilterInput); _browseFilterEl.addEventListener('keydown', onBrowseFilterKey); } - // Also listen on overlay for browse modes (input is hidden) _overlay.addEventListener('keydown', onOverlayKeyDown); document.body.appendChild(_overlay); } @@ -298,16 +338,17 @@ function switchMode(m) { _mode = m; if (m === 'browse' || m === 'browse-vault') { _input.style.display = 'none'; _input.disabled = true; - _overlay.focus(); // overlay receives keyboard events when input is hidden - if (m === 'browse') { - // Show browse filter for directory navigation - if (_browseFilterEl) { _browseFilterEl.style.display = ''; _browseFilterEl.value = _browseFilter; _browseFilterEl.focus(); } - } else { - if (_browseFilterEl) _browseFilterEl.style.display = 'none'; + _overlay.focus(); + if (m === 'browse' && _browseFilterEl) { + _browseFilterEl.style.display = ''; + _browseFilterEl.value = _browseFilter; + _browseFilterEl.focus(); + } else if (_browseFilterEl) { + _browseFilterEl.style.display = 'none'; } } else { _input.style.display = ''; _input.disabled = false; - _input.focus(); // restore focus to search input + _input.focus(); if (_browseFilterEl) _browseFilterEl.style.display = 'none'; const bc = document.getElementById('cp-breadcrumb'); if (bc) bc.style.display = 'none'; } @@ -323,11 +364,19 @@ function onInput() { } async function searchFiles(q) { - if (!q) { _items = []; _results.innerHTML = '
Commencez à taper
'; return; } + if (!q) { + // Empty search: show "Browse directories" option + _items = [{ type:'_browse', label:'📂 Parcourir les répertoires...', sublabel:'Naviguer dans les vaults pour trouver un fichier', act:()=>startBrowse('open-file') }]; + render(); + return; + } try { const d = await api(`/api/suggest?q=${encodeURIComponent(q)}&vault=all`); const sug = d.suggestions || []; _items = sug.map(s => ({ type:'file', vault:s.vault, path:s.path, label:s.title||s.path.split('/').pop(), sublabel:`${s.vault}/${s.path}` })); + if (_items.length === 0) { + _items = [{ type:'_browse', label:'📂 Parcourir les répertoires...', sublabel:'Aucun résultat — naviguer manuellement', act:()=>startBrowse('open-file') }]; + } render(); } catch { _results.innerHTML = '
Erreur
'; } } @@ -379,7 +428,6 @@ function scroll() { const s = _results.querySelector('.cp-selected'); if (s) s.s // ── Overlay keydown handler (for browse modes where input is hidden) ── function onOverlayKeyDown(e) { if (_mode !== 'browse' && _mode !== 'browse-vault') return; - // Don't handle if name input or browse filter is active (they have their own handlers) if (document.getElementById('cp-name-input')) return; if (document.activeElement === _browseFilterEl) return; onKeyDown(e); @@ -388,17 +436,17 @@ function onOverlayKeyDown(e) { function exec(idx) { const it = _items[idx]; if (!it) return; if (it.type === 'file') { close(); openFile(it.vault, it.path); } - else if (it.type === 'cmd') { it.act(); } + else if (it.type === 'cmd' || it.type === '_browse') { it.act(); } else if (it.type === '_vault') { _browse.vault = it.vault; switchMode('browse'); renderBrowse(); } - else if (it.type === '_nav') { _browse.path = it.go; renderBrowse(); } - else if (it.type === '_action') { it.act(); } + else if (it.type === '_nav') { _browse.path = it.go; _browseFilter=''; if(_browseFilterEl)_browseFilterEl.value=''; renderBrowse(); } + else if (it.type === '_action' || it.type === '_file') { it.act(); } } -function esc(s) { if (!s) return ''; return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } +function esc(s) { if (!s) return ''; return String(s).replace(/&/g,'&').replace(//g,'>').replace(/\"/g,'"'); } export function initCommandPalette() { document.addEventListener('keydown', (e) => { if ((e.ctrlKey||e.metaKey) && e.shiftKey && !e.altKey && (e.key===' '||e.code==='Space')) { e.preventDefault(); if (isOpen()) close(); else open(''); } if ((e.ctrlKey||e.metaKey) && e.altKey && !e.shiftKey && (e.key===' '||e.code==='Space')) { e.preventDefault(); if (isOpen()) close(); else open('> '); } }); -} \ No newline at end of file +}