Palette: fix arrow keys in browse filter, add file browsing, add delete-file command
CI / lint (push) Failing after 4s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / security (push) Successful in 8s

This commit is contained in:
2026-06-03 10:41:17 -04:00
parent 81e53a930a
commit 447dfbdea2
+88 -40
View File
@@ -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 += `<span class="cp-bc-sep">/</span><span class="cp-bc-item" data-path="${esc(acc)}">${esc(s)}</span>`; }
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 = `<div class="command-palette">
<div class="cp-header"><input type="text" class="cp-input" placeholder="Rechercher un fichier... (tapez > pour les commandes)" spellcheck="false" autocomplete="off"><div class="cp-breadcrumb" id="cp-breadcrumb"></div><span class="cp-hint">Ctrl+Shift+Espace</span></div>
<input type="text" class="cp-browse-filter" id="cp-browse-filter" placeholder="Filtrer dossiers/fichiers..." spellcheck="false" autocomplete="off" style="display:none">
<div class="cp-results"></div>
<div class="cp-footer"><span id="cp-footer-hint">↑↓ naviguer</span><span>↵ ouvrir/entrer</span><span>⌫ monter</span><span>Esc fermer</span></div></div>`;
<div class="cp-footer"><span>↑↓ naviguer</span><span>↵ ouvrir/entrer</span><span>⌫ monter</span><span>Esc fermer</span></div></div>`;
_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 = '<div class="cp-empty">Commencez à taper</div>'; 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 = '<div class="cp-empty cp-error">Erreur</div>'; }
}
@@ -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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
function esc(s) { if (!s) return ''; return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\"/g,'&quot;'); }
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('> '); }
});
}
}