CI / lint (push) Failing after 9s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / e2e (push) Has been skipped
CI / security (push) Successful in 12s
Desktop Build / build-windows (push) Has been cancelled
Desktop Build / build-linux (push) Has been cancelled
C1b: Sous-menu « Déplacer vers... » (flyout hover) — liste les autres panneaux
D2: Drag depuis l'arborescence vers panneau — format 'tree:{vault,path}'
E3: Rendu paresseux — display:none sur contenu des panneaux inactifs
E4: Verrouillage éditeur — même fichier déjà ouvert → focus le panneau existant
G3: Bouton reset (🔄 Réinitialiser les panneaux) dans la palette
ROADMAP: section 75 marquée ✅ Complété (reste I2/I3 tests)
598 lines
29 KiB
JavaScript
598 lines
29 KiB
JavaScript
/* ObsiGate — Command Palette (Ctrl+Shift+Espace / Ctrl+Alt+Espace) */
|
|
import { api } from './auth.js';
|
|
import { showToast, toggleTheme, TabManager } from './ui.js';
|
|
import { openEditor } from './utils.js';
|
|
import { state } from './state.js';
|
|
import { t } from './i18n.js';
|
|
|
|
function triggerGoHome() { const e = document.getElementById('header-logo'); if (e) e.click(); }
|
|
|
|
function getCurrentFile() {
|
|
const id = TabManager._activeTabId;
|
|
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;
|
|
}
|
|
|
|
// ── Query parser: +word = required, -word = exclude, word = required ──
|
|
function parseQuery(raw) {
|
|
const positive = [];
|
|
const negative = [];
|
|
const parts = (raw || '').split(/\s+/).filter(Boolean);
|
|
for (const p of parts) {
|
|
if (p.startsWith('-') && p.length > 1) negative.push(p.slice(1).toLowerCase());
|
|
else if (p.startsWith('+')) positive.push(p.slice(1).toLowerCase());
|
|
else positive.push(p.toLowerCase());
|
|
}
|
|
return { positive, negative };
|
|
}
|
|
|
|
// ── DOM / state ──
|
|
let _overlay = null, _input = null, _results = null, _mode = 'files', _sel = 0, _to = null, _items = [];
|
|
let _browse = { action: null, vault: null, path: '' };
|
|
let _browseFilterEl = null, _browseFilter = '', _allItems = [];
|
|
let _browseFilterTimer = null; // debounce for global search
|
|
|
|
// ── Commands ── (lazy: built on first access so i18n is ready)
|
|
let _cmdsCache = null;
|
|
function getCMDS() {
|
|
if (_cmdsCache) return _cmdsCache;
|
|
_cmdsCache = [
|
|
{ id:'home', label:t('palette.cmd_back_home'), desc:t('palette.cmd_back_home_desc'), cat:'Navigation', act:()=>{close();triggerGoHome();} },
|
|
{ id:'edit-current', label:'✏️ '+t('editor.edit'), desc:t('editor.edit_current_desc'), cat:'Fichiers', act:()=>{close();const f=getCurrentFile();if(f)openEditor(f.vault,f.path);else showToast(t('toast.no_open_file'),'error');} },
|
|
{ id:'popout', label:'🪟 Pop-out', desc:t('palette.popout_desc'), cat:'Fichiers', act:()=>{close();const f=getCurrentFile();if(f)window.open(`/popout/${encodeURIComponent(f.vault)}/${encodeURIComponent(f.path)}?theme=${document.documentElement.getAttribute('data-theme')||'dark'}`,'popout','width=1000,height=700');else showToast(t('toast.no_open_file'),'error');} },
|
|
{ id:'create-file', label:t('palette.cmd_create_file'), desc:t('palette.cmd_create_file_desc'), cat:'Fichiers', act:()=>startBrowse('create-file') },
|
|
{ id:'delete-file', label:'🗑️ '+t('file.delete'), desc:t('palette.delete_file_desc'), cat:'Fichiers', act:()=>startBrowse('delete-file') },
|
|
{ id:'delete-cur', label:'🗑️ '+t('palette.delete_current'), desc:t('palette.delete_current_desc'), cat:'Fichiers', act:async()=>{close();await delCurFile();} },
|
|
{ id:'close-cur', label:'✕ '+t('palette.close_current'), desc:t('palette.close_current_desc'), cat:'Fichiers', act:()=>{close();const f=getCurrentFile();if(f){const tab=TabManager._tabs.find(t=>t.vault===f.vault&&t.path===f.path);if(tab)TabManager.close(tab.id);else triggerGoHome();}else{showToast(t('toast.no_open_file'),'error');}} },
|
|
{ id:'create-dir', label:t('palette.cmd_create_dir'), desc:t('palette.cmd_create_dir_desc'), cat:'Répertoires', act:()=>startBrowse('create-dir') },
|
|
{ id:'delete-dir', label:t('palette.cmd_delete_dir'), desc:t('palette.cmd_delete_dir_desc'), cat:'Répertoires', act:()=>startBrowse('delete-dir') },
|
|
{ id:'theme', label:t('palette.cmd_theme'), desc:t('palette.cmd_theme_desc'), cat:'Actions', act:()=>{close();toggleTheme();} },
|
|
{ id:'reindex', label:'🔄 '+t('vault.reindex'), desc:t('palette.reindex_desc'), cat:'Actions', act:async()=>{close();try{await api('/api/index/reload');showToast(t('sync.index_reloaded'),'success');}catch{showToast(t('common.error'),'error');}} },
|
|
{ id:'help', label:'❓ '+t('help.help_title'), desc:t('palette.help_desc'), cat:'Système', act:()=>{close();const b=document.getElementById('help-open-btn');if(b)b.click();} },
|
|
{ id:'config', label:t('palette.cmd_config'), desc:t('palette.cmd_config_desc'), cat:'Système', act:()=>{close();const b=document.getElementById('config-open-btn');if(b)b.click();} },
|
|
// Pane / Split commands
|
|
{ id:'split-right', label:'⊞→ Diviser à droite', desc:'Crée un panneau à droite avec cet onglet', cat:'Panneaux', act:()=>{close();if(window.PaneManager)window.PaneManager.splitRight();} },
|
|
{ id:'split-down', label:'⊞↓ Diviser en bas', desc:'Crée un panneau en dessous avec cet onglet', cat:'Panneaux', act:()=>{close();if(window.PaneManager)window.PaneManager.splitDown();} },
|
|
{ id:'close-pane', label:'✕ Fermer le panneau actif', desc:'Ferme le panneau actif et tous ses onglets', cat:'Panneaux', act:()=>{close();if(window.PaneManager&&window.PaneManager.isSplit())window.PaneManager.closeActivePane();} },
|
|
{ id:'focus-next', label:'→ Panneau suivant', desc:'Active le panneau suivant (Ctrl+Alt+→)', cat:'Panneaux', act:()=>{close();if(window.PaneManager&&window.PaneManager.isSplit()){const n=(window.PaneManager.activePaneId+1)%window.PaneManager.panes.length;window.PaneManager.setActivePane(n);}} },
|
|
{ id:'focus-prev', label:'← Panneau précédent', desc:'Active le panneau précédent (Ctrl+Alt+←)', cat:'Panneaux', act:()=>{close();if(window.PaneManager&&window.PaneManager.isSplit()){const n=(window.PaneManager.activePaneId-1+window.PaneManager.panes.length)%window.PaneManager.panes.length;window.PaneManager.setActivePane(n);}} },
|
|
{ id:'reset-panes', label:'🔄 Réinitialiser les panneaux', desc:'Ferme tous les panneaux et revient au mode single-pane', cat:'Panneaux', act:()=>{close();if(window.PaneManager){while(window.PaneManager.isSplit()){window.PaneManager.closePane(window.PaneManager.panes.length-1);}localStorage.removeItem('obsigate-panes');}} },
|
|
];
|
|
return _cmdsCache;
|
|
}
|
|
|
|
// ── Browse: vault picker ──
|
|
function startBrowse(action) {
|
|
_browse = { action, vault: null, path: '' };
|
|
_browseFilter = ''; _allItems = [];
|
|
switchMode('browse-vault');
|
|
renderVaultPicker();
|
|
}
|
|
|
|
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: t('palette.vault_count', {count: v.file_count}) }));
|
|
if (_items.length === 0) {
|
|
_items = [{ type:'_empty', label: t('palette.no_vaults'), sublabel: '' }];
|
|
}
|
|
_allItems = _items;
|
|
render();
|
|
} catch { _items = [{ type:'_error', label: t('palette.error_loading'), sublabel: '' }]; _allItems = _items; render(); }
|
|
}
|
|
|
|
// ── 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; _browseFilter=''; if(_browseFilterEl)_browseFilterEl.value=''; renderBrowse(); }));
|
|
bc.querySelector('.cp-bc-root').addEventListener('click', ()=>{ _browse.path=''; _browseFilter=''; if(_browseFilterEl)_browseFilterEl.value=''; 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' || i.is_dir);
|
|
const files = items.filter(i => i.type === 'file' && !i.is_dir);
|
|
|
|
// Action row
|
|
const actLabel = {
|
|
'create-file': t('palette.cmd_create_file'),
|
|
'create-dir': t('palette.cmd_create_dir'),
|
|
'delete-dir': t('palette.cmd_delete_dir'),
|
|
'delete-file': t('palette.delete_file_desc'),
|
|
'open-file': t('palette.browse_open_file')
|
|
};
|
|
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: '🗑️ '+t('file.delete')+' '+(path.split('/').pop()||path), sublabel: t('palette.dir_delete_confirm'), act:()=>execDelDir(path) });
|
|
} else if (needsName) {
|
|
_items.push({ type:'_action', label: actLabel[_browse.action]||'', sublabel: t('palette.browse_in', {path: path||t('palette.browse_root')}), act:()=>askName() });
|
|
}
|
|
|
|
// Parent
|
|
if (path) {
|
|
_items.push({ type:'_nav', label: '📂 .. (parent)', go: path.split('/').slice(0,-1).join('/') });
|
|
}
|
|
|
|
// Subdirs
|
|
for (const d of dirs) {
|
|
const fp = path ? path+'/'+d.name : d.name;
|
|
_items.push({ type:'_nav', label: '📁 '+d.name, go: fp });
|
|
}
|
|
|
|
// Files
|
|
for (const f of files) {
|
|
const fp = path ? path+'/'+f.name : f.name;
|
|
if (isDeleteFile) {
|
|
_items.push({ type:'_file', label: '📄 '+f.name, sublabel: f.vault+'/'+fp, act:()=>execDelFile(fp) });
|
|
} else if (isOpenFile) {
|
|
_items.push({ type:'_file', label: '📄 '+f.name, sublabel: f.vault+'/'+fp, act:()=>{ close(); TabManager.openPersistent(vault, fp); } });
|
|
} else {
|
|
_items.push({ type:'_info', label: '📄 '+f.name, sublabel: fp });
|
|
}
|
|
}
|
|
|
|
if (_items.length === 0) _items.push({ type:'_empty', label: t('palette.empty_dir'), sublabel: '' });
|
|
} catch { _items = [{ type:'_error', label: t('common.error'), sublabel: '' }]; }
|
|
|
|
_allItems = _items;
|
|
applyBrowseFilter();
|
|
}
|
|
|
|
// ── Browse filter: local when empty, global suggest when text ──
|
|
function applyBrowseFilter() {
|
|
const q = _browseFilter.trim();
|
|
if (!q) {
|
|
// Empty filter: show local directory items
|
|
_items = _allItems;
|
|
render();
|
|
return;
|
|
}
|
|
// Has filter text: debounce and search globally
|
|
if (_browseFilterTimer) clearTimeout(_browseFilterTimer);
|
|
_browseFilterTimer = setTimeout(() => searchBrowseGlobal(q), 200);
|
|
}
|
|
|
|
async function searchBrowseGlobal(rawQ) {
|
|
const { positive, negative } = parseQuery(rawQ);
|
|
const action = _browse.action;
|
|
const isDeleteFile = action === 'delete-file';
|
|
const isOpenFile = action === 'open-file';
|
|
const isDeleteDir = action === 'delete-dir';
|
|
const needsName = action === 'create-file' || action === 'create-dir';
|
|
|
|
try {
|
|
const q = positive.join(' ');
|
|
const vault = _browse.vault || 'all';
|
|
const d = await api(`/api/suggest?q=${encodeURIComponent(q)}&vault=${encodeURIComponent(vault)}`);
|
|
const sug = (d.suggestions || []).filter(s => {
|
|
const fullPath = (s.vault+'/'+s.path).toLowerCase();
|
|
// Client-side negative filter
|
|
for (const n of negative) {
|
|
if (fullPath.includes(n)) return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
if (sug.length === 0) {
|
|
_items = [{ type:'_empty', label: t('palette.no_results', {query: rawQ}), sublabel: '' }];
|
|
render();
|
|
return;
|
|
}
|
|
|
|
_items = [];
|
|
|
|
// Extract unique directories from result paths
|
|
const dirSet = new Set();
|
|
for (const s of sug) {
|
|
const parts = s.path.split('/');
|
|
// Build parent directories
|
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
dirSet.add(s.vault + '|' + parts.slice(0, i + 1).join('/'));
|
|
}
|
|
}
|
|
|
|
// Show directories as navigable
|
|
const dirs = [...dirSet].map(d => {
|
|
const [v, p] = d.split('|');
|
|
const name = p.split('/').pop();
|
|
return { type:'_nav', label:'📁 '+name, sublabel:v+'/'+p, go:p, vault:v };
|
|
}).sort((a,b) => a.sublabel.localeCompare(b.sublabel));
|
|
|
|
for (const d of dirs) _items.push(d);
|
|
|
|
// Show files
|
|
for (const s of sug) {
|
|
const fp = s.path;
|
|
const name = fp.split('/').pop();
|
|
if (isDeleteFile) {
|
|
_items.push({ type:'_file', label:'📄 '+name, sublabel:s.vault+'/'+fp, act:()=>execDelFileBrowsed(s.vault, fp) });
|
|
} else if (isOpenFile) {
|
|
_items.push({ type:'_file', label:'📄 '+name, sublabel:s.vault+'/'+fp, act:()=>{ close(); TabManager.openPersistent(s.vault, fp); } });
|
|
} else if (isDeleteDir) {
|
|
_items.push({ type:'_file', label:'📁 '+name, sublabel:s.vault+'/'+fp, act:()=>execDelDirBrowsed(s.vault, fp) });
|
|
} else {
|
|
_items.push({ type:'_file', label:'📄 '+name, sublabel:s.vault+'/'+fp, act:()=>{ close(); TabManager.openPersistent(s.vault, fp); } });
|
|
}
|
|
}
|
|
|
|
render();
|
|
} catch { _items = [{ type:'_error', label: t('palette.search_error'), sublabel: '' }]; render(); }
|
|
}
|
|
|
|
// Navigate to a directory from global search
|
|
function execNavGlobal(vault, goPath) {
|
|
_browse.vault = vault;
|
|
_browse.path = goPath;
|
|
_browseFilter = '';
|
|
if (_browseFilterEl) _browseFilterEl.value = '';
|
|
renderBrowse();
|
|
}
|
|
|
|
async function execDelFileBrowsed(vault, path) {
|
|
if (!confirm(t("confirm.delete_file_named", {path: vault + "/" + path}))) return;
|
|
try {
|
|
const d = await api(`/api/file/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`, { method:'DELETE' });
|
|
if (d?.status==='ok' || d?.success) { showToast(`🗑️ ${path} supprimé`,'success'); close(); refreshSidebar(); }
|
|
else showToast(t('common.error'),'error');
|
|
} catch(e) { showToast('Erreur : '+(e.message||e),'error'); }
|
|
}
|
|
|
|
async function execDelDirBrowsed(vault, path) {
|
|
if (!confirm(t("confirm.delete_dir_named", {path: path}))) return;
|
|
try {
|
|
const d = await api(`/api/directory/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`, { method:'DELETE' });
|
|
if (d?.status==='ok' || d?.success) { showToast(`🗑️ ${path} supprimé`,'success'); close(); refreshSidebar(); }
|
|
else showToast(t('common.error'),'error');
|
|
} catch(e) { showToast('Erreur : '+(e.message||e),'error'); }
|
|
}
|
|
|
|
function onBrowseFilterInput() {
|
|
_browseFilter = _browseFilterEl ? _browseFilterEl.value : '';
|
|
applyBrowseFilter();
|
|
}
|
|
|
|
function onBrowseFilterKey(e) {
|
|
if (e.key === 'Escape') {
|
|
e.preventDefault();
|
|
if (_browseFilter) {
|
|
_browseFilter = '';
|
|
if (_browseFilterEl) _browseFilterEl.value = '';
|
|
applyBrowseFilter();
|
|
} else {
|
|
close();
|
|
}
|
|
} else if (e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
_sel = 0;
|
|
updSel(_results.querySelectorAll('.cp-item'));
|
|
_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 || !_browseFilterEl.value)) {
|
|
e.preventDefault();
|
|
const segs = _browse.path ? _browse.path.split('/') : [];
|
|
if (segs.length > 0) { _browse.path = segs.slice(0, -1).join('/'); renderBrowse(); }
|
|
else if (_browse.vault) { switchMode('browse-vault'); renderVaultPicker(); }
|
|
}
|
|
}
|
|
|
|
// ── 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();
|
|
_overlay.focus();
|
|
renderBrowse();
|
|
}
|
|
});
|
|
}
|
|
|
|
function cleanupNameInput() {
|
|
const el = document.getElementById('cp-name-input');
|
|
if (el) el.remove();
|
|
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 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(t('toast.create_error'),'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(t('common.error'),'error');
|
|
} catch(e) { showToast('Erreur : '+(e.message||e),'error'); }
|
|
}
|
|
|
|
async function execDelDir(path) {
|
|
if (!confirm(t("confirm.delete_dir_named", {path: path}))) 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(t('common.error'),'error');
|
|
} catch(e) { showToast('Erreur : '+(e.message||e),'error'); }
|
|
}
|
|
|
|
async function execDelFile(path) {
|
|
if (!confirm(t("confirm.delete_file_named", {path: 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(t('common.error'),'error');
|
|
} catch(e) { showToast('Erreur : '+(e.message||e),'error'); }
|
|
}
|
|
|
|
async function delCurFile() {
|
|
const f = getCurrentFile();
|
|
if (!f) { showToast(t('toast.no_open_file'),'error'); return; }
|
|
if (!confirm(t("confirm.delete_file_named", {path: f.path}))) return;
|
|
try {
|
|
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(t('common.error'),'error');
|
|
} catch(e) { showToast('Erreur : '+(e.message||e),'error'); }
|
|
}
|
|
|
|
function refreshSidebar() {
|
|
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.tabIndex = -1;
|
|
_overlay.innerHTML = `<div class="command-palette">
|
|
<div class="cp-header"><input type="text" class="cp-input" placeholder="Rechercher un fichier... (+inclure -exclure)" 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 (+mot -mot) — recherche globale..." spellcheck="false" autocomplete="off" style="display:none">
|
|
<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');
|
|
_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);
|
|
if (_browseFilterEl) {
|
|
_browseFilterEl.addEventListener('input', onBrowseFilterInput);
|
|
_browseFilterEl.addEventListener('keydown', onBrowseFilterKey);
|
|
}
|
|
_overlay.addEventListener('keydown', onOverlayKeyDown);
|
|
document.body.appendChild(_overlay);
|
|
}
|
|
|
|
export function open(q = '') {
|
|
createDOM();
|
|
_overlay.classList.add('active');
|
|
_input.value = q;
|
|
_mode = q.startsWith('>') ? 'commands' : 'files';
|
|
_input.disabled = false; _input.style.display = '';
|
|
_sel = 0; _items = []; _results.innerHTML = '';
|
|
_input.focus();
|
|
onInput();
|
|
}
|
|
|
|
export function close() {
|
|
if (!_overlay) return;
|
|
_overlay.classList.remove('active');
|
|
if (_to) clearTimeout(_to); _to = null;
|
|
if (_browseFilterTimer) clearTimeout(_browseFilterTimer);
|
|
_mode = 'files';
|
|
_browseFilter = '';
|
|
_allItems = [];
|
|
cleanupNameInput();
|
|
if (_browseFilterEl) { _browseFilterEl.style.display = 'none'; _browseFilterEl.value = ''; }
|
|
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 switchMode(m) {
|
|
_mode = m;
|
|
if (m === 'browse' || m === 'browse-vault') {
|
|
_input.style.display = 'none'; _input.disabled = true;
|
|
_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();
|
|
if (_browseFilterEl) _browseFilterEl.style.display = 'none';
|
|
const bc = document.getElementById('cp-breadcrumb'); if (bc) bc.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
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... (+inclure -exclure)'; }
|
|
if (_to) clearTimeout(_to);
|
|
_to = setTimeout(() => { _to = null; if (_mode==='commands') searchCmd(v.slice(1).trim()); else searchFiles(v.trim()); }, 150);
|
|
}
|
|
|
|
async function searchFiles(rawQ) {
|
|
if (!rawQ) {
|
|
_items = [{ type:'_browse', label:'📂 Parcourir les répertoires...', sublabel:'Naviguer dans les vaults pour trouver un fichier', act:()=>startBrowse('open-file') }];
|
|
render();
|
|
return;
|
|
}
|
|
|
|
const { positive, negative } = parseQuery(rawQ);
|
|
|
|
try {
|
|
const q = positive.join(' ');
|
|
const d = await api(`/api/suggest?q=${encodeURIComponent(q)}&vault=all`);
|
|
let sug = d.suggestions || [];
|
|
|
|
// Client-side negative filter
|
|
if (negative.length > 0) {
|
|
sug = sug.filter(s => {
|
|
const fullPath = (s.vault+'/'+s.path).toLowerCase();
|
|
for (const n of negative) {
|
|
if (fullPath.includes(n)) return false;
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
|
|
if (sug.length === 0) {
|
|
_items = [{ type:'_browse', label:'📂 Parcourir les répertoires...', sublabel:'Aucun résultat — naviguer manuellement', act:()=>startBrowse('open-file') }];
|
|
} else {
|
|
_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 searchCmd(q) {
|
|
if (!q) { _items = getCMDS().map(c => ({ type:'cmd', ...c })); render(); return; }
|
|
const lq = q.toLowerCase();
|
|
_items = getCMDS().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();
|
|
}
|
|
|
|
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', () => 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); });
|
|
});
|
|
scroll();
|
|
}
|
|
|
|
function onKeyDown(e) {
|
|
const items = _results.querySelectorAll('.cp-item');
|
|
if (_mode === 'browse' || _mode === 'browse-vault') {
|
|
if (e.key === 'Escape') { e.preventDefault(); close(); }
|
|
else if (e.key === 'Backspace') {
|
|
e.preventDefault();
|
|
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 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;
|
|
}
|
|
if (e.key === 'Escape') { e.preventDefault(); close(); }
|
|
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 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'}); }
|
|
|
|
// ── Overlay keydown handler (for browse modes where input is hidden) ──
|
|
function onOverlayKeyDown(e) {
|
|
if (_mode !== 'browse' && _mode !== 'browse-vault') return;
|
|
if (document.getElementById('cp-name-input')) return;
|
|
if (document.activeElement === _browseFilterEl) return;
|
|
onKeyDown(e);
|
|
}
|
|
|
|
function exec(idx) {
|
|
const it = _items[idx]; if (!it) return;
|
|
if (it.type === 'file') {
|
|
close();
|
|
if (window.innerWidth <= 768) {
|
|
TabManager.openPersistent(it.vault, it.path);
|
|
} else {
|
|
TabManager.openPreview(it.vault, it.path);
|
|
}
|
|
}
|
|
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') {
|
|
// Check if this is a global search result with vault info
|
|
if (it.vault && it.vault !== _browse.vault) { _browse.vault = it.vault; }
|
|
_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,'"'); }
|
|
|
|
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('> '); }
|
|
});
|
|
}
|