feat: 6 nouvelles commandes palette — créer/supprimer fichiers/dossiers, éditer, pop-out
This commit is contained in:
parent
120c7433f8
commit
34d89706be
@ -1,10 +1,11 @@
|
||||
/* ObsiGate — Command Palette (Ctrl+P)
|
||||
/* ObsiGate — Command Palette (Ctrl+Shift+Espace / Ctrl+Alt+Espace)
|
||||
Quick file opener and command runner.
|
||||
Inspired by VS Code's Ctrl+P / Ctrl+Shift+P.
|
||||
Inspired by VS Code's command palette.
|
||||
*/
|
||||
import { api } from './auth.js';
|
||||
import { openFile } from './viewer.js';
|
||||
import { showToast, toggleTheme } from './ui.js';
|
||||
import { showToast, toggleTheme, TabManager } from './ui.js';
|
||||
import { openEditor } from './utils.js';
|
||||
import { state } from './state.js';
|
||||
|
||||
// ── Helper: trigger goHome by clicking the logo ──
|
||||
@ -13,24 +14,187 @@ function triggerGoHome() {
|
||||
if (logo) logo.click();
|
||||
}
|
||||
|
||||
// ── DOM cache (created in open) ──
|
||||
// ── Get current active file from tabs ──
|
||||
function getCurrentFile() {
|
||||
const activeId = TabManager._activeTabId;
|
||||
if (activeId) {
|
||||
const tab = TabManager._tabs.find(t => t.id === activeId);
|
||||
if (tab) return { vault: tab.vault, path: tab.path };
|
||||
}
|
||||
// Fallback to state
|
||||
if (state.currentVault && state.currentPath) {
|
||||
return { vault: state.currentVault, path: state.currentPath };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Get current vault from tabs ──
|
||||
function getCurrentVault() {
|
||||
const file = getCurrentFile();
|
||||
if (file) return file.vault;
|
||||
if (state.currentVault) return state.currentVault;
|
||||
if (state.allVaults && state.allVaults.length > 0) return state.allVaults[0].name;
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Prompt for vault if ambiguous ──
|
||||
function promptVault(hint) {
|
||||
const vault = getCurrentVault();
|
||||
if (vault) return vault;
|
||||
return prompt(`${hint} — Nom de la vault :`) || '';
|
||||
}
|
||||
|
||||
// ── DOM cache ──
|
||||
let _overlay = null;
|
||||
let _input = null;
|
||||
let _results = null;
|
||||
let _mode = 'files'; // 'files' | 'commands'
|
||||
let _mode = 'files';
|
||||
let _selectedIndex = 0;
|
||||
let _searchTimeout = null;
|
||||
let _lastResults = [];
|
||||
|
||||
// ── Available commands ──
|
||||
const COMMANDS = [
|
||||
{ id: 'home', label: '🏠 Accueil', description: 'Retour à l\'accueil', action: () => { close(); triggerGoHome(); } },
|
||||
{ id: 'theme', label: '🌓 Changer le thème', description: 'Basculer clair/sombre', action: () => { close(); toggleTheme(); } },
|
||||
{ id: 'reindex', label: '🔄 Réindexer', description: 'Forcer la réindexation complète', 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 l\'aide', action: () => { close(); const btn = document.getElementById('help-open-btn'); if (btn) btn.click(); } },
|
||||
{ id: 'config', label: '⚙️ Configuration', description: 'Ouvrir les paramètres', action: () => { close(); const btn = document.getElementById('config-open-btn'); if (btn) btn.click(); } },
|
||||
// ── Navigation ──
|
||||
{ id: 'home', label: '🏠 Accueil', description: 'Retour à l\'accueil', category: 'Navigation',
|
||||
action: () => { close(); triggerGoHome(); } },
|
||||
|
||||
// ── Fichiers ──
|
||||
{ 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 courant dans une nouvelle fenêtre', category: 'Fichiers',
|
||||
action: () => { close(); const f = getCurrentFile(); if (f) window.open(`/popout/${encodeURIComponent(f.vault)}/${encodeURIComponent(f.path)}`, `popout_${f.vault}_${f.path.replace(/[^a-zA-Z0-9]/g, '_')}`, 'width=1000,height=700,menubar=no,toolbar=no,location=no,status=no,resizable=yes,scrollbars=no'); else showToast('Aucun fichier ouvert', 'error'); } },
|
||||
|
||||
{ id: 'create-file', label: '📄 Créer fichier markdown', description: 'Créer un nouveau fichier .md et l\'ouvrir dans l\'éditeur', category: 'Fichiers',
|
||||
action: async () => { close(); await createFile(); } },
|
||||
|
||||
{ id: 'delete-current', label: '🗑️ Supprimer fichier courant', description: 'Supprimer le fichier ouvert dans l\'onglet actif', category: 'Fichiers',
|
||||
action: async () => { close(); await deleteCurrentFile(); } },
|
||||
|
||||
// ── Répertoires ──
|
||||
{ id: 'create-dir', label: '📁 Créer répertoire', description: 'Créer un nouveau dossier', category: 'Répertoires',
|
||||
action: async () => { close(); await createDirectory(); } },
|
||||
|
||||
{ id: 'delete-dir', label: '🗑️ Supprimer répertoire', description: 'Supprimer un dossier et son contenu', category: 'Répertoires',
|
||||
action: async () => { close(); await deleteDirectory(); } },
|
||||
|
||||
// ── Actions ──
|
||||
{ id: 'theme', label: '🌓 Changer le thème', description: 'Basculer entre thème clair et sombre', category: 'Actions',
|
||||
action: () => { close(); toggleTheme(); } },
|
||||
|
||||
{ id: 'reindex', label: '🔄 Réindexer', description: 'Forcer la réindexation complète des vaults', category: 'Actions',
|
||||
action: async () => { close(); try { await api('/api/index/reload'); showToast('Index rechargé', 'success'); } catch { showToast('Erreur de réindexation', 'error'); } } },
|
||||
|
||||
// ── Aide & Config ──
|
||||
{ id: 'help', label: '❓ Aide', description: 'Ouvrir le guide d\'utilisation', category: 'Système',
|
||||
action: () => { close(); const btn = document.getElementById('help-open-btn'); if (btn) btn.click(); } },
|
||||
|
||||
{ id: 'config', label: '⚙️ Configuration', description: 'Ouvrir les paramètres', category: 'Système',
|
||||
action: () => { close(); const btn = document.getElementById('config-open-btn'); if (btn) btn.click(); } },
|
||||
];
|
||||
|
||||
// ── Create file action ──
|
||||
async function createFile() {
|
||||
const vault = promptVault('Créer un fichier');
|
||||
if (!vault) return;
|
||||
|
||||
let path = prompt('Chemin du fichier (ex: notes/mon-fichier) :');
|
||||
if (!path) return;
|
||||
|
||||
// Add .md extension if none
|
||||
if (!path.includes('.')) path += '.md';
|
||||
|
||||
try {
|
||||
const data = await api(`/api/file/${encodeURIComponent(vault)}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path, content: `# ${path.split('/').pop().replace('.md', '')}\n\n` }),
|
||||
});
|
||||
if (data?.status === 'ok' || data?.success) {
|
||||
showToast(`✅ ${path} créé`, 'success');
|
||||
openEditor(vault, path);
|
||||
} else {
|
||||
showToast('Erreur de création', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(`Erreur : ${e.message || e}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Create directory action ──
|
||||
async function createDirectory() {
|
||||
const vault = promptVault('Créer un dossier');
|
||||
if (!vault) return;
|
||||
|
||||
const path = prompt('Chemin du dossier (ex: notes/projets) :');
|
||||
if (!path) return;
|
||||
|
||||
try {
|
||||
const data = await api(`/api/directory/${encodeURIComponent(vault)}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path }),
|
||||
});
|
||||
if (data?.status === 'ok' || data?.success) {
|
||||
showToast(`📁 ${path} créé`, 'success');
|
||||
} else {
|
||||
showToast('Erreur de création', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(`Erreur : ${e.message || e}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Delete current file action ──
|
||||
async function deleteCurrentFile() {
|
||||
const f = getCurrentFile();
|
||||
if (!f) { showToast('Aucun fichier ouvert', 'error'); return; }
|
||||
|
||||
if (!confirm(`Supprimer définitivement "${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');
|
||||
// Close the tab
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Delete directory action ──
|
||||
async function deleteDirectory() {
|
||||
const vault = promptVault('Supprimer un dossier');
|
||||
if (!vault) return;
|
||||
|
||||
const path = prompt('Chemin du dossier à supprimer (ex: notes/obsolète) :');
|
||||
if (!path) return;
|
||||
|
||||
if (!confirm(`Supprimer définitivement le dossier "${path}" et TOUT son contenu ?`)) return;
|
||||
|
||||
try {
|
||||
const data = await api(`/api/directory/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (data?.status === 'ok' || data?.success) {
|
||||
showToast(`🗑️ ${path} supprimé`, 'success');
|
||||
} else {
|
||||
showToast('Erreur de suppression', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(`Erreur : ${e.message || e}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Create DOM structure ──
|
||||
function createDOM() {
|
||||
if (_overlay) return;
|
||||
@ -56,19 +220,16 @@ function createDOM() {
|
||||
_input = _overlay.querySelector('.cp-input');
|
||||
_results = _overlay.querySelector('.cp-results');
|
||||
|
||||
// Click outside to close (on overlay, not the palette dialog)
|
||||
_overlay.addEventListener('click', (e) => {
|
||||
if (e.target === _overlay) close();
|
||||
});
|
||||
|
||||
// Keyboard events
|
||||
_input.addEventListener('keydown', onKeyDown);
|
||||
_input.addEventListener('input', onInput);
|
||||
|
||||
document.body.appendChild(_overlay);
|
||||
}
|
||||
|
||||
// ── Open / Close ──
|
||||
export function open(initialQuery = '') {
|
||||
createDOM();
|
||||
_overlay.classList.add('active');
|
||||
@ -78,7 +239,6 @@ export function open(initialQuery = '') {
|
||||
_lastResults = [];
|
||||
_results.innerHTML = '';
|
||||
_input.focus();
|
||||
// Trigger initial search
|
||||
onInput();
|
||||
}
|
||||
|
||||
@ -87,7 +247,6 @@ export function close() {
|
||||
_overlay.classList.remove('active');
|
||||
if (_searchTimeout) clearTimeout(_searchTimeout);
|
||||
_searchTimeout = null;
|
||||
// Restore focus
|
||||
const searchInput = document.querySelector('.search-input');
|
||||
if (searchInput) searchInput.focus();
|
||||
}
|
||||
@ -96,11 +255,8 @@ function isOpen() {
|
||||
return _overlay && _overlay.classList.contains('active');
|
||||
}
|
||||
|
||||
// ── Input handler ──
|
||||
function onInput() {
|
||||
const val = _input.value;
|
||||
|
||||
// Detect mode change
|
||||
const newMode = val.startsWith('>') ? 'commands' : 'files';
|
||||
if (newMode !== _mode) {
|
||||
_mode = newMode;
|
||||
@ -120,7 +276,6 @@ function onInput() {
|
||||
}, 150);
|
||||
}
|
||||
|
||||
// ── Search files (via /api/suggest) ──
|
||||
async function searchFiles(query) {
|
||||
if (!query) {
|
||||
_results.innerHTML = '<div class="cp-empty">Commencez à taper pour chercher un fichier</div>';
|
||||
@ -145,7 +300,6 @@ async function searchFiles(query) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Search commands ──
|
||||
function searchCommands(query) {
|
||||
if (!query) {
|
||||
_lastResults = COMMANDS.map(c => ({ type: 'command', ...c }));
|
||||
@ -155,12 +309,11 @@ function searchCommands(query) {
|
||||
|
||||
const lower = query.toLowerCase();
|
||||
_lastResults = COMMANDS
|
||||
.filter(c => c.label.toLowerCase().includes(lower) || c.description.toLowerCase().includes(lower))
|
||||
.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();
|
||||
}
|
||||
|
||||
// ── Render results list ──
|
||||
function renderResults() {
|
||||
_selectedIndex = 0;
|
||||
|
||||
@ -176,7 +329,6 @@ function renderResults() {
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Click handler
|
||||
_results.querySelectorAll('.cp-item').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const idx = parseInt(el.dataset.index);
|
||||
@ -192,7 +344,6 @@ function renderResults() {
|
||||
scrollToSelected();
|
||||
}
|
||||
|
||||
// ── Keyboard navigation ──
|
||||
function onKeyDown(e) {
|
||||
const items = _results.querySelectorAll('.cp-item');
|
||||
|
||||
@ -212,10 +363,8 @@ function onKeyDown(e) {
|
||||
updateSelection(items);
|
||||
} else if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
// Switch between files/commands mode
|
||||
if (_mode === 'files') {
|
||||
_input.value = '> ';
|
||||
// Move cursor to end
|
||||
setTimeout(() => {
|
||||
_input.selectionStart = _input.selectionEnd = _input.value.length;
|
||||
}, 0);
|
||||
@ -240,7 +389,6 @@ function scrollToSelected() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Execute ──
|
||||
function executeItem(index) {
|
||||
const item = _lastResults[index];
|
||||
if (!item) return;
|
||||
@ -253,7 +401,6 @@ function executeItem(index) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── HTML escaping ──
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return String(str)
|
||||
@ -263,15 +410,14 @@ function escapeHtml(str) {
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// ── Init: Register global keyboard shortcut ──
|
||||
export function initCommandPalette() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// Ctrl+Shift+Espace — open file palette
|
||||
// Ctrl+Shift+Espace — file palette
|
||||
if ((e.ctrlKey || e.metaKey) && e.shiftKey && !e.altKey && (e.key === ' ' || e.code === 'Space')) {
|
||||
e.preventDefault();
|
||||
if (isOpen()) { close(); } else { open(''); }
|
||||
}
|
||||
// Ctrl+Alt+Espace — open command palette
|
||||
// Ctrl+Alt+Espace — command palette
|
||||
if ((e.ctrlKey || e.metaKey) && e.altKey && !e.shiftKey && (e.key === ' ' || e.code === 'Space')) {
|
||||
e.preventDefault();
|
||||
if (isOpen()) { close(); } else { open('> '); }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user