Palette: fix arrow keys in browse filter, add file browsing, add delete-file command
This commit is contained in:
+88
-40
@@ -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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
||||
function esc(s) { if (!s) return ''; return String(s).replace(/&/g,'&').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('> '); }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user