feat: vault picker visuel + modal nom harmonisé + refresh sidebar après suppression
This commit is contained in:
parent
e405bdc929
commit
5932d2fa20
@ -1,273 +1,205 @@
|
||||
/* ObsiGate — Command Palette (Ctrl+Shift+Espace / Ctrl+Alt+Espace)
|
||||
Quick file opener and command runner.
|
||||
Inspired by VS Code's command palette.
|
||||
*/
|
||||
/* ObsiGate — Command Palette (Ctrl+Shift+Espace / Ctrl+Alt+Espace) */
|
||||
import { api } from './auth.js';
|
||||
import { openFile } from './viewer.js';
|
||||
import { showToast, toggleTheme, TabManager } from './ui.js';
|
||||
import { openEditor } from './utils.js';
|
||||
import { state } from './state.js';
|
||||
|
||||
// ── Helper: trigger goHome ──
|
||||
function triggerGoHome() {
|
||||
const el = document.getElementById('header-logo');
|
||||
if (el) el.click();
|
||||
}
|
||||
function triggerGoHome() { const e = document.getElementById('header-logo'); if (e) e.click(); }
|
||||
|
||||
// ── Get current active file ──
|
||||
function getCurrentFile() {
|
||||
const id = TabManager._activeTabId;
|
||||
if (id) {
|
||||
const tab = TabManager._tabs.find(t => t.id === id);
|
||||
if (tab) return { vault: tab.vault, path: tab.path };
|
||||
}
|
||||
if (id) { const t = TabManager._tabs.find(x => x.id === id); if (t) return { vault: t.vault, path: t.path }; }
|
||||
if (state.currentVault && state.currentPath) return { vault: state.currentVault, path: state.currentPath };
|
||||
return null;
|
||||
}
|
||||
|
||||
function getCurrentVault() {
|
||||
const f = getCurrentFile();
|
||||
if (f) return f.vault;
|
||||
if (state.currentVault) return state.currentVault;
|
||||
if (state.allVaults && state.allVaults.length > 0) return state.allVaults[0].name;
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── DOM / state ──
|
||||
let _overlay = null;
|
||||
let _input = null;
|
||||
let _results = null;
|
||||
let _mode = 'files'; // 'files' | 'commands' | 'browse'
|
||||
let _selectedIndex = 0;
|
||||
let _searchTimeout = null;
|
||||
let _lastResults = [];
|
||||
|
||||
// Browse mode state
|
||||
let _browse = {
|
||||
action: null, // 'create-file' | 'create-dir' | 'delete-dir'
|
||||
vault: null,
|
||||
path: '',
|
||||
name: '',
|
||||
};
|
||||
let _overlay = null, _input = null, _results = null, _mode = 'files', _sel = 0, _to = null, _items = [];
|
||||
let _browse = { action: null, vault: null, path: '' };
|
||||
|
||||
// ── Commands ──
|
||||
const COMMANDS = [
|
||||
{ id: 'home', label: '🏠 Accueil', description: "Retour à l'accueil", category: 'Navigation', action: () => { close(); triggerGoHome(); } },
|
||||
{ id: 'edit-current',label: '✏️ Éditer fichier courant',description: 'Ouvrir le fichier actif dans l\'éditeur', category: 'Fichiers', action: () => { close(); const f=getCurrentFile(); if(f) openEditor(f.vault, f.path); else showToast('Aucun fichier ouvert','error'); } },
|
||||
{ id: 'popout', label: '🪟 Pop-out', description: 'Ouvrir le document dans une fenêtre détachée', category: 'Fichiers', action: () => { 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',description: 'Naviguer vers le dossier parent, puis nommer le fichier', category: 'Fichiers', action: () => { startBrowse('create-file'); } },
|
||||
{ id: 'delete-current',label: '🗑️ Supprimer fichier courant',description: 'Supprimer le fichier de l\'onglet actif', category: 'Fichiers', action: async () => { close(); await deleteCurrentFile(); } },
|
||||
{ id: 'create-dir', label: '📁 Créer répertoire', description: 'Naviguer vers le dossier parent, puis nommer le dossier', category: 'Répertoires', action: () => { startBrowse('create-dir'); } },
|
||||
{ id: 'delete-dir', label: '🗑️ Supprimer répertoire', description: 'Naviguer et sélectionner le dossier à supprimer', category: 'Répertoires', action: () => { startBrowse('delete-dir'); } },
|
||||
{ id: 'theme', label: '🌓 Changer le thème', description: 'Basculer clair/sombre', category: 'Actions', action: () => { close(); toggleTheme(); } },
|
||||
{ id: 'reindex', label: '🔄 Réindexer', description: 'Forcer la réindexation complète', category: 'Actions', action: async () => { close(); try{await api('/api/index/reload');showToast('Index rechargé','success');}catch{showToast('Erreur de réindexation','error');} } },
|
||||
{ id: 'help', label: '❓ Aide', description: "Ouvrir le guide d'utilisation", category: 'Système', action: () => { close(); const b=document.getElementById('help-open-btn'); if(b)b.click(); } },
|
||||
{ id: 'config', label: '⚙️ Configuration', description: 'Ouvrir les paramètres', category: 'Système', action: () => { close(); const b=document.getElementById('config-open-btn'); if(b)b.click(); } },
|
||||
const CMDS = [
|
||||
{ id:'home', label:'🏠 Accueil', desc:"Retour à l'accueil", cat:'Navigation', act:()=>{close();triggerGoHome();} },
|
||||
{ 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-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') },
|
||||
{ id:'theme', label:'🌓 Changer le thème', desc:'Basculer clair/sombre', cat:'Actions', act:()=>{close();toggleTheme();} },
|
||||
{ id:'reindex', label:'🔄 Réindexer', desc:'Forcer la réindexation complète', cat:'Actions', act:async()=>{close();try{await api('/api/index/reload');showToast('Index rechargé','success');}catch{showToast('Erreur','error');}} },
|
||||
{ id:'help', label:'❓ Aide', desc:'Ouvrir le guide', cat:'Système', act:()=>{close();const b=document.getElementById('help-open-btn');if(b)b.click();} },
|
||||
{ id:'config', label:'⚙️ Configuration', desc:'Ouvrir les paramètres', cat:'Système', act:()=>{close();const b=document.getElementById('config-open-btn');if(b)b.click();} },
|
||||
];
|
||||
|
||||
// ── Browse mode helpers ──
|
||||
// ── Browse: vault picker ──
|
||||
function startBrowse(action) {
|
||||
const vault = getCurrentVault();
|
||||
if (!vault) { showToast('Aucune vault trouvée', 'error'); return; }
|
||||
_browse = { action, vault, path: '', name: '' };
|
||||
switchMode('browse');
|
||||
_input.value = '';
|
||||
renderBrowse();
|
||||
_browse = { action, vault: null, path: '' };
|
||||
switchMode('browse-vault');
|
||||
renderVaultPicker();
|
||||
}
|
||||
|
||||
function switchMode(newMode) {
|
||||
_mode = newMode;
|
||||
if (_mode === 'browse') {
|
||||
_input.placeholder = 'Naviguez avec ↑↓, Enter pour entrer, Retour arrière pour monter';
|
||||
_input.disabled = true;
|
||||
_input.style.display = 'none';
|
||||
} else {
|
||||
_input.disabled = false;
|
||||
_input.style.display = '';
|
||||
const bc = document.getElementById('cp-breadcrumb');
|
||||
if (bc) bc.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Render directory browser ──
|
||||
async function renderBrowse() {
|
||||
_selectedIndex = 0;
|
||||
_lastResults = [];
|
||||
const { vault, path } = _browse;
|
||||
const breadcrumbEl = document.getElementById('cp-breadcrumb');
|
||||
|
||||
// Show breadcrumb
|
||||
if (breadcrumbEl) {
|
||||
breadcrumbEl.style.display = 'flex';
|
||||
const segments = path ? path.split('/') : [];
|
||||
let html = `<span class="cp-bc-item cp-bc-root">${escapeHtml(vault)}</span>`;
|
||||
let cumul = '';
|
||||
for (const seg of segments) {
|
||||
cumul = cumul ? cumul + '/' + seg : seg;
|
||||
html += `<span class="cp-bc-sep">/</span><span class="cp-bc-item" data-path="${escapeHtml(cumul)}">${escapeHtml(seg)}</span>`;
|
||||
async function renderVaultPicker() {
|
||||
try {
|
||||
const data = await api('/api/vaults');
|
||||
const vaults = Array.isArray(data) ? data : [];
|
||||
_items = vaults.map(v => ({ type:'_vault', vault: v.name, label: (v.type==='DIR'?'📂':'🗂️')+' '+v.name, sublabel: v.file_count+' fichiers' }));
|
||||
if (_items.length === 0) {
|
||||
_items = [{ type:'_empty', label: 'Aucune vault trouvée', sublabel: '' }];
|
||||
}
|
||||
breadcrumbEl.innerHTML = html;
|
||||
render();
|
||||
} catch { _items = [{ type:'_error', label: 'Erreur de chargement', sublabel: '' }]; render(); }
|
||||
}
|
||||
|
||||
// Click on breadcrumb segment to jump
|
||||
breadcrumbEl.querySelectorAll('.cp-bc-item[data-path]').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
_browse.path = el.dataset.path;
|
||||
renderBrowse();
|
||||
});
|
||||
});
|
||||
// ── Browse: directory navigation ──
|
||||
async function renderBrowse() {
|
||||
const { vault, path } = _browse;
|
||||
_items = [];
|
||||
const bc = document.getElementById('cp-breadcrumb');
|
||||
if (bc) {
|
||||
bc.style.display = 'flex';
|
||||
const segs = path ? path.split('/') : [];
|
||||
let html = `<span class="cp-bc-item cp-bc-root" data-vault="1">${esc(vault)}</span>`;
|
||||
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(); });
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await api(`/api/browse/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`);
|
||||
const items = data.items || [];
|
||||
const dirs = items.filter(i => i.type === 'directory');
|
||||
const files = items.filter(i => i.type === 'file');
|
||||
const dirs = items.filter(i => i.type === 'directory' || i.is_dir);
|
||||
const files = items.filter(i => i.type === 'file' && !i.is_dir);
|
||||
|
||||
// Build result list
|
||||
const segments = path ? path.split('/') : [];
|
||||
const breadcrumb = vault + (path ? '/' + path : '');
|
||||
|
||||
// ── Action row (first result) ──
|
||||
// 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) {
|
||||
_lastResults.push({
|
||||
type: '_action', id: 'confirm-delete',
|
||||
label: '🗑️ ' + (path.split('/').pop() || path),
|
||||
sublabel: 'Supprimer ce dossier et tout son contenu (confirmation demandée)',
|
||||
action: () => executeBrowseDelete(path),
|
||||
});
|
||||
} else if (_browse.action === 'create-file' || _browse.action === 'create-dir') {
|
||||
const actionLabel = _browse.action === 'create-file' ? '📄 Créer un fichier ici' : '📁 Créer un dossier ici';
|
||||
_lastResults.push({
|
||||
type: '_action', id: 'confirm-create',
|
||||
label: actionLabel,
|
||||
sublabel: 'Dans ' + (breadcrumb || 'racine de la vault'),
|
||||
action: () => executeBrowseCreate(),
|
||||
});
|
||||
_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() });
|
||||
}
|
||||
|
||||
// ── Parent entry ──
|
||||
// Parent
|
||||
if (path) {
|
||||
const parent = segments.slice(0, -1).join('/');
|
||||
_lastResults.push({
|
||||
type: '_navigate', id: 'parent',
|
||||
label: '📂 .. (dossier parent)',
|
||||
sublabel: '',
|
||||
go: parent,
|
||||
});
|
||||
const parent = path.split('/').slice(0,-1).join('/');
|
||||
_items.push({ type:'_nav', label: '📂 .. (parent)', go: parent });
|
||||
}
|
||||
|
||||
// ── Subdirectories ──
|
||||
// Subdirs
|
||||
for (const d of dirs) {
|
||||
const full = path ? path + '/' + d.name : d.name;
|
||||
_lastResults.push({
|
||||
type: '_navigate', id: 'dir-' + full,
|
||||
label: '📁 ' + d.name,
|
||||
sublabel: '',
|
||||
go: full,
|
||||
});
|
||||
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 (for info, not clickable for creating) ──
|
||||
for (const f of files) {
|
||||
_lastResults.push({
|
||||
type: '_file',
|
||||
label: '📄 ' + f.name,
|
||||
sublabel: '',
|
||||
});
|
||||
}
|
||||
|
||||
if (_lastResults.length === 0) {
|
||||
_lastResults.push({ type: '_empty', label: '📂 Dossier vide', sublabel: '' });
|
||||
}
|
||||
} catch {
|
||||
_lastResults = [{ type: '_error', label: 'Erreur de chargement', sublabel: '' }];
|
||||
}
|
||||
|
||||
renderResults();
|
||||
if (_items.length === 0) _items.push({ type:'_empty', label: '📂 Dossier vide', sublabel: '' });
|
||||
} catch { _items = [{ type:'_error', label: 'Erreur', sublabel: '' }]; }
|
||||
render();
|
||||
}
|
||||
|
||||
async function executeBrowseDelete(path) {
|
||||
if (!confirm(`Supprimer définitivement le dossier "${path}" et TOUT son contenu ?`)) return;
|
||||
// ── Name input modal ──
|
||||
function askName() {
|
||||
const label = _browse.action === 'create-file' ? 'Nom du fichier' : 'Nom du dossier';
|
||||
const ext = _browse.action === 'create-file' ? '(.md ajouté si besoin)' : '';
|
||||
const existingInput = document.getElementById('cp-name-input');
|
||||
if (existingInput) { existingInput.focus(); return; }
|
||||
|
||||
_input.style.display = 'none';
|
||||
const bc = document.getElementById('cp-breadcrumb');
|
||||
if (bc) bc.style.display = 'none';
|
||||
|
||||
const nameDiv = document.createElement('div');
|
||||
nameDiv.id = 'cp-name-input';
|
||||
nameDiv.className = 'cp-name-input';
|
||||
nameDiv.innerHTML = `<input type="text" id="cp-name-field" class="cp-name-field" placeholder="${label} ${ext}" autocomplete="off" spellcheck="false">
|
||||
<span class="cp-name-hint">Enter confirmer · Esc annuler</span>`;
|
||||
_results.before(nameDiv);
|
||||
|
||||
const field = document.getElementById('cp-name-field');
|
||||
field.focus();
|
||||
field.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const name = field.value.trim();
|
||||
if (!name) return;
|
||||
cleanupNameInput();
|
||||
if (_browse.action === 'create-file') execCreateFile(name);
|
||||
else execCreateDir(name);
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
cleanupNameInput();
|
||||
renderBrowse();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function cleanupNameInput() {
|
||||
const el = document.getElementById('cp-name-input');
|
||||
if (el) el.remove();
|
||||
_input.style.display = '';
|
||||
const bc = document.getElementById('cp-breadcrumb');
|
||||
if (bc) bc.style.display = 'flex';
|
||||
}
|
||||
|
||||
// ── Execute actions ──
|
||||
async function execCreateFile(name) {
|
||||
let fp = _browse.path ? _browse.path+'/'+name : name;
|
||||
if (!fp.includes('.')) fp += '.md';
|
||||
try {
|
||||
const data = await api(`/api/directory/${encodeURIComponent(_browse.vault)}?path=${encodeURIComponent(path)}`, { method: 'DELETE' });
|
||||
if (data?.status === 'ok' || data?.success) {
|
||||
showToast(`🗑️ ${path} supprimé`, 'success');
|
||||
close();
|
||||
} else showToast('Erreur de suppression', 'error');
|
||||
} catch (e) { showToast('Erreur : ' + (e.message||e), 'error'); }
|
||||
const d = await api(`/api/file/${encodeURIComponent(_browse.vault)}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({path:fp, content:`# ${name.replace('.md','')}\n\n`}) });
|
||||
if (d?.status==='ok' || d?.success) { showToast(`✅ ${fp} créé`,'success'); close(); openEditor(_browse.vault, fp); }
|
||||
else showToast('Erreur de création','error');
|
||||
} catch(e) { showToast('Erreur : '+(e.message||e),'error'); }
|
||||
}
|
||||
|
||||
async function executeBrowseCreate() {
|
||||
const action = _browse.action;
|
||||
const label = action === 'create-file' ? 'Nom du fichier' : 'Nom du dossier';
|
||||
const name = prompt(label + ' dans ' + (_browse.path || 'racine') + ' :');
|
||||
if (!name) return;
|
||||
if (action === 'create-file') {
|
||||
let filePath = _browse.path ? _browse.path + '/' + name : name;
|
||||
if (!filePath.includes('.')) filePath += '.md';
|
||||
try {
|
||||
const data = await api(`/api/file/${encodeURIComponent(_browse.vault)}`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path: filePath, content: `# ${name.replace('.md','')}\n\n` }),
|
||||
});
|
||||
if (data?.status === 'ok' || data?.success) {
|
||||
showToast(`✅ ${filePath} créé`, 'success');
|
||||
close();
|
||||
openEditor(_browse.vault, filePath);
|
||||
} else showToast('Erreur de création', 'error');
|
||||
} catch (e) { showToast('Erreur : ' + (e.message||e), 'error'); }
|
||||
} else {
|
||||
const dirPath = _browse.path ? _browse.path + '/' + name : name;
|
||||
try {
|
||||
const data = await api(`/api/directory/${encodeURIComponent(_browse.vault)}`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path: dirPath }),
|
||||
});
|
||||
if (data?.status === 'ok' || data?.success) {
|
||||
showToast(`📁 ${dirPath} créé`, 'success');
|
||||
close();
|
||||
} else showToast('Erreur de création', 'error');
|
||||
} catch (e) { showToast('Erreur : ' + (e.message||e), 'error'); }
|
||||
}
|
||||
async function execCreateDir(name) {
|
||||
const dp = _browse.path ? _browse.path+'/'+name : name;
|
||||
try {
|
||||
const d = await api(`/api/directory/${encodeURIComponent(_browse.vault)}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({path:dp}) });
|
||||
if (d?.status==='ok' || d?.success) { showToast(`📁 ${dp} créé`,'success'); close(); refreshSidebar(); }
|
||||
else showToast('Erreur','error');
|
||||
} catch(e) { showToast('Erreur : '+(e.message||e),'error'); }
|
||||
}
|
||||
|
||||
// ── Delete current file ──
|
||||
async function deleteCurrentFile() {
|
||||
async function execDelDir(path) {
|
||||
if (!confirm(`Supprimer "${path}" et TOUT son contenu ?`)) return;
|
||||
try {
|
||||
const d = await api(`/api/directory/${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; }
|
||||
if (!confirm(`Supprimer définitivement "${f.path}" ?`)) return;
|
||||
if (!f) { showToast('Aucun fichier ouvert','error'); return; }
|
||||
if (!confirm(`Supprimer "${f.path}" ?`)) return;
|
||||
try {
|
||||
const data = await api(`/api/file/${encodeURIComponent(f.vault)}?path=${encodeURIComponent(f.path)}`, { method: 'DELETE' });
|
||||
if (data?.status === 'ok' || data?.success) {
|
||||
showToast(`🗑️ ${f.path} supprimé`, 'success');
|
||||
const tab = TabManager._tabs.find(t => t.vault === f.vault && t.path === f.path);
|
||||
if (tab) TabManager.close(tab.id);
|
||||
triggerGoHome();
|
||||
} else showToast('Erreur de suppression', 'error');
|
||||
} catch (e) { showToast('Erreur : ' + (e.message||e), 'error'); }
|
||||
const d = await api(`/api/file/${encodeURIComponent(f.vault)}?path=${encodeURIComponent(f.path)}`, { method:'DELETE' });
|
||||
if (d?.status==='ok' || d?.success) { showToast(`🗑️ ${f.path} supprimé`,'success');
|
||||
const tab = TabManager._tabs.find(t=>t.vault===f.vault&&t.path===f.path); if(tab) TabManager.close(tab.id); triggerGoHome(); refreshSidebar(); }
|
||||
else showToast('Erreur','error');
|
||||
} catch(e) { showToast('Erreur : '+(e.message||e),'error'); }
|
||||
}
|
||||
|
||||
// ── Create DOM ──
|
||||
function refreshSidebar() {
|
||||
// Try to refresh the vault tree
|
||||
import('./sidebar.js').then(m => { if (m.loadVaults) m.loadVaults(); }).catch(() => {});
|
||||
}
|
||||
|
||||
// ── DOM ──
|
||||
function createDOM() {
|
||||
if (_overlay) return;
|
||||
_overlay = document.createElement('div');
|
||||
_overlay.className = 'command-palette-overlay';
|
||||
_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" style="display:none"></div>
|
||||
<span class="cp-hint">Ctrl+Shift+Espace</span>
|
||||
</div>
|
||||
<div class="cp-results"></div>
|
||||
<div class="cp-footer">
|
||||
<span>↑↓ naviguer</span>
|
||||
<span>↵ ouvrir/entrer</span>
|
||||
<span>⌫ monter</span>
|
||||
<span>Esc fermer</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
_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>
|
||||
<div class="cp-results"></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');
|
||||
_results = _overlay.querySelector('.cp-results');
|
||||
_overlay.addEventListener('click', (e) => { if (e.target === _overlay) close(); });
|
||||
@ -276,16 +208,13 @@ function createDOM() {
|
||||
document.body.appendChild(_overlay);
|
||||
}
|
||||
|
||||
export function open(initialQuery = '') {
|
||||
export function open(q = '') {
|
||||
createDOM();
|
||||
_overlay.classList.add('active');
|
||||
_input.value = initialQuery;
|
||||
_mode = initialQuery.startsWith('>') ? 'commands' : 'files';
|
||||
_selectedIndex = 0;
|
||||
_lastResults = [];
|
||||
_results.innerHTML = '';
|
||||
_input.disabled = false;
|
||||
_input.style.display = '';
|
||||
_input.value = q;
|
||||
_mode = q.startsWith('>') ? 'commands' : 'files';
|
||||
_input.disabled = false; _input.style.display = '';
|
||||
_sel = 0; _items = []; _results.innerHTML = '';
|
||||
_input.focus();
|
||||
onInput();
|
||||
}
|
||||
@ -293,138 +222,102 @@ export function open(initialQuery = '') {
|
||||
export function close() {
|
||||
if (!_overlay) return;
|
||||
_overlay.classList.remove('active');
|
||||
if (_searchTimeout) clearTimeout(_searchTimeout);
|
||||
_searchTimeout = null;
|
||||
if (_to) clearTimeout(_to); _to = null;
|
||||
_mode = 'files';
|
||||
const si = document.querySelector('.search-input');
|
||||
if (si) si.focus();
|
||||
cleanupNameInput();
|
||||
const bc = document.getElementById('cp-breadcrumb'); if (bc) bc.style.display = 'none';
|
||||
const si = document.querySelector('.search-input'); if (si) si.focus();
|
||||
}
|
||||
|
||||
function isOpen() { return _overlay && _overlay.classList.contains('active'); }
|
||||
|
||||
function onInput() {
|
||||
if (_mode === 'browse') return;
|
||||
const val = _input.value;
|
||||
const newMode = val.startsWith('>') ? 'commands' : 'files';
|
||||
if (newMode !== _mode) {
|
||||
_mode = newMode;
|
||||
_input.placeholder = _mode === 'commands' ? 'Tapez une commande...' : 'Rechercher un fichier... (tapez > pour les commandes)';
|
||||
function switchMode(m) {
|
||||
_mode = m;
|
||||
if (m === 'browse' || m === 'browse-vault') {
|
||||
_input.style.display = 'none'; _input.disabled = true;
|
||||
} else {
|
||||
_input.style.display = ''; _input.disabled = false;
|
||||
const bc = document.getElementById('cp-breadcrumb'); if (bc) bc.style.display = 'none';
|
||||
}
|
||||
if (_searchTimeout) clearTimeout(_searchTimeout);
|
||||
_searchTimeout = setTimeout(() => {
|
||||
_searchTimeout = null;
|
||||
if (_mode === 'commands') searchCommands(val.slice(1).trim());
|
||||
else searchFiles(val.trim());
|
||||
}, 150);
|
||||
}
|
||||
|
||||
async function searchFiles(query) {
|
||||
if (!query) { _results.innerHTML = '<div class="cp-empty">Commencez à taper pour chercher un fichier</div>'; _lastResults = []; return; }
|
||||
function onInput() {
|
||||
if (_mode === 'browse' || _mode === 'browse-vault') return;
|
||||
const v = _input.value;
|
||||
const nm = v.startsWith('>') ? 'commands' : 'files';
|
||||
if (nm !== _mode) { _mode = nm; _input.placeholder = _mode==='commands'?'Tapez une commande...':'Rechercher un fichier... (tapez > pour les commandes)'; }
|
||||
if (_to) clearTimeout(_to);
|
||||
_to = setTimeout(() => { _to = null; if (_mode==='commands') searchCmd(v.slice(1).trim()); else searchFiles(v.trim()); }, 150);
|
||||
}
|
||||
|
||||
async function searchFiles(q) {
|
||||
if (!q) { _items = []; _results.innerHTML = '<div class="cp-empty">Commencez à taper</div>'; return; }
|
||||
try {
|
||||
const data = await api(`/api/suggest?q=${encodeURIComponent(query)}&vault=all`);
|
||||
const suggestions = data.suggestions || [];
|
||||
_lastResults = suggestions.map(s => ({ type: 'file', vault: s.vault, path: s.path, title: s.title, label: s.title || s.path.split('/').pop(), sublabel: `${s.vault}/${s.path}` }));
|
||||
renderResults();
|
||||
} catch { _results.innerHTML = '<div class="cp-empty cp-error">Erreur de recherche</div>'; }
|
||||
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}` }));
|
||||
render();
|
||||
} catch { _results.innerHTML = '<div class="cp-empty cp-error">Erreur</div>'; }
|
||||
}
|
||||
|
||||
function searchCommands(query) {
|
||||
if (!query) { _lastResults = COMMANDS.map(c => ({ type: 'command', ...c })); renderResults(); return; }
|
||||
const lower = query.toLowerCase();
|
||||
_lastResults = COMMANDS.filter(c => c.label.toLowerCase().includes(lower) || c.description.toLowerCase().includes(lower) || (c.category && c.category.toLowerCase().includes(lower))).map(c => ({ type: 'command', ...c }));
|
||||
renderResults();
|
||||
function searchCmd(q) {
|
||||
if (!q) { _items = CMDS.map(c => ({ type:'cmd', ...c })); render(); return; }
|
||||
const lq = q.toLowerCase();
|
||||
_items = CMDS.filter(c => c.label.toLowerCase().includes(lq)||c.desc.toLowerCase().includes(lq)||(c.cat&&c.cat.toLowerCase().includes(lq))).map(c => ({ type:'cmd', ...c }));
|
||||
render();
|
||||
}
|
||||
|
||||
// ── Render ──
|
||||
function renderResults() {
|
||||
_selectedIndex = 0;
|
||||
if (_lastResults.length === 0) { _results.innerHTML = '<div class="cp-empty">Aucun résultat</div>'; return; }
|
||||
_results.innerHTML = _lastResults.map((item, i) => {
|
||||
const cls = i === 0 ? 'cp-selected' : '';
|
||||
return `<div class="cp-item ${cls}" data-index="${i}"><div class="cp-item-label">${escapeHtml(item.label)}</div><div class="cp-item-sublabel">${escapeHtml(item.sublabel || '')}</div></div>`;
|
||||
}).join('');
|
||||
|
||||
function render() {
|
||||
_sel = 0;
|
||||
if (!_items.length) { _results.innerHTML = '<div class="cp-empty">Aucun résultat</div>'; return; }
|
||||
_results.innerHTML = _items.map((it, i) => `<div class="cp-item ${i===0?'cp-selected':''}" data-idx="${i}"><div class="cp-item-label">${esc(it.label)}</div><div class="cp-item-sublabel">${esc(it.sublabel||'')}</div></div>`).join('');
|
||||
_results.querySelectorAll('.cp-item').forEach(el => {
|
||||
el.addEventListener('click', () => { executeItem(parseInt(el.dataset.index)); });
|
||||
el.addEventListener('mouseenter', () => {
|
||||
document.querySelectorAll('.cp-item').forEach(e => e.classList.remove('cp-selected'));
|
||||
el.classList.add('cp-selected');
|
||||
_selectedIndex = parseInt(el.dataset.index);
|
||||
});
|
||||
el.addEventListener('click', () => exec(parseInt(el.dataset.idx)));
|
||||
el.addEventListener('mouseenter', () => { document.querySelectorAll('.cp-item').forEach(e=>e.classList.remove('cp-selected')); el.classList.add('cp-selected'); _sel = parseInt(el.dataset.idx); });
|
||||
});
|
||||
scrollToSelected();
|
||||
scroll();
|
||||
}
|
||||
|
||||
// ── Keyboard ──
|
||||
function onKeyDown(e) {
|
||||
const items = _results.querySelectorAll('.cp-item');
|
||||
|
||||
if (_mode === 'browse') {
|
||||
if (e.key === 'Escape') {
|
||||
if (_mode === 'browse' || _mode === 'browse-vault') {
|
||||
if (e.key === 'Escape') { e.preventDefault(); close(); }
|
||||
else if (e.key === 'Backspace') {
|
||||
e.preventDefault();
|
||||
close();
|
||||
} else if (e.key === 'Backspace') {
|
||||
e.preventDefault();
|
||||
// Go up one level
|
||||
if (_mode === 'browse-vault') { _mode='commands'; _input.style.display=''; _input.disabled=false; _input.focus(); searchCmd(''); return; }
|
||||
const segs = _browse.path ? _browse.path.split('/') : [];
|
||||
if (segs.length > 0) {
|
||||
_browse.path = segs.slice(0, -1).join('/');
|
||||
renderBrowse();
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
executeItem(_selectedIndex);
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
_selectedIndex = Math.min(_selectedIndex + 1, _lastResults.length - 1);
|
||||
updateSelection(items);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
_selectedIndex = Math.max(_selectedIndex - 1, 0);
|
||||
updateSelection(items);
|
||||
if (segs.length > 0) { _browse.path = segs.slice(0,-1).join('/'); renderBrowse(); }
|
||||
else if (_browse.vault) { switchMode('browse-vault'); renderVaultPicker(); }
|
||||
}
|
||||
else if (e.key === 'Enter') { e.preventDefault(); exec(_sel); }
|
||||
else if (e.key === 'ArrowDown') { e.preventDefault(); _sel = Math.min(_sel+1, _items.length-1); updSel(items); }
|
||||
else if (e.key === 'ArrowUp') { e.preventDefault(); _sel = Math.max(_sel-1, 0); updSel(items); }
|
||||
return;
|
||||
}
|
||||
|
||||
// Non-browse mode
|
||||
if (e.key === 'Escape') { e.preventDefault(); close(); }
|
||||
else if (e.key === 'Enter') { e.preventDefault(); executeItem(_selectedIndex); }
|
||||
else if (e.key === 'ArrowDown') { e.preventDefault(); _selectedIndex = Math.min(_selectedIndex + 1, _lastResults.length - 1); updateSelection(items); }
|
||||
else if (e.key === 'ArrowUp') { e.preventDefault(); _selectedIndex = Math.max(_selectedIndex - 1, 0); updateSelection(items); }
|
||||
else if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
if (_mode === 'files') { _input.value = '> '; setTimeout(() => { _input.selectionStart = _input.selectionEnd = _input.value.length; }, 0); }
|
||||
else { _input.value = ''; }
|
||||
onInput();
|
||||
}
|
||||
else if (e.key === 'Enter') { e.preventDefault(); exec(_sel); }
|
||||
else if (e.key === 'ArrowDown') { e.preventDefault(); _sel = Math.min(_sel+1, _items.length-1); updSel(items); }
|
||||
else if (e.key === 'ArrowUp') { e.preventDefault(); _sel = Math.max(_sel-1, 0); updSel(items); }
|
||||
else if (e.key === 'Tab') { e.preventDefault(); if (_mode==='files') { _input.value='> '; setTimeout(()=>_input.setSelectionRange(2,2),0); } else _input.value=''; onInput(); }
|
||||
}
|
||||
|
||||
function updateSelection(items) { items.forEach((el, i) => el.classList.toggle('cp-selected', i === _selectedIndex)); scrollToSelected(); }
|
||||
function updSel(items) { items.forEach((el,i)=>el.classList.toggle('cp-selected',i===_sel)); scroll(); }
|
||||
function scroll() { const s = _results.querySelector('.cp-selected'); if (s) s.scrollIntoView({block:'nearest'}); }
|
||||
|
||||
function scrollToSelected() { const sel = _results.querySelector('.cp-selected'); if (sel) sel.scrollIntoView({ block: 'nearest' }); }
|
||||
|
||||
function executeItem(index) {
|
||||
const item = _lastResults[index];
|
||||
if (!item) return;
|
||||
|
||||
if (item.type === 'file') { close(); openFile(item.vault, item.path); }
|
||||
else if (item.type === 'command') { item.action(); }
|
||||
else if (item.type === '_navigate') { _browse.path = item.go; renderBrowse(); }
|
||||
else if (item.type === '_action') { item.action(); }
|
||||
// _file and _empty are informational only
|
||||
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 === '_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(); }
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return String(str).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,'"'); }
|
||||
|
||||
// ── Init ──
|
||||
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('> '); }
|
||||
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('> '); }
|
||||
});
|
||||
}
|
||||
@ -6291,7 +6291,7 @@ body.popup-mode .content-area {
|
||||
|
||||
/* ── Breadcrumb ── */
|
||||
.cp-breadcrumb {
|
||||
display: flex;
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 8px 14px;
|
||||
@ -6308,3 +6308,30 @@ body.popup-mode .content-area {
|
||||
.cp-bc-sep { color: var(--text-muted); padding: 0 2px; }
|
||||
.cp-bc-item { cursor: pointer; padding: 1px 4px; border-radius: 3px; }
|
||||
.cp-bc-item:hover { background: var(--bg-hover); }
|
||||
|
||||
/* ── Name input ── */
|
||||
.cp-name-input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
.cp-name-field {
|
||||
width: 100%;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.cp-name-field:focus { border-color: var(--accent); }
|
||||
.cp-name-hint {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user