/** * GoingsOn - Command Palette / Global Search (Cmd+K) * Full-text search across tasks, projects, emails, events, contacts. */ (function() { 'use strict'; const esc = GoingsOn.utils.escapeHtml; const escAttr = GoingsOn.utils.escapeAttrValue; let overlay = null; let debounceTimer = null; let selectedIndex = -1; let currentResults = []; // ============ Overlay Creation ============ function createOverlay() { if (overlay) return overlay; overlay = document.createElement('div'); overlay.id = 'command-palette'; overlay.className = 'hidden'; overlay.innerHTML = `
🔍 Esc
Type to search across everything. Use is:overdue tag:work type:task in:ProjectName to filter.
`; overlay.querySelector('.cp-backdrop').addEventListener('click', close); const input = overlay.querySelector('#cp-input'); input.addEventListener('input', onInput); input.addEventListener('keydown', onKeydown); document.body.appendChild(overlay); return overlay; } // ============ Open / Close ============ function open() { createOverlay(); overlay.classList.remove('hidden'); selectedIndex = -1; currentResults = []; const input = overlay.querySelector('#cp-input'); input.value = ''; input.focus(); const results = overlay.querySelector('#cp-results'); results.innerHTML = `
Type to search across everything. Use is:overdue tag:work type:task in:ProjectName to filter.
`; overlay.querySelector('#cp-filters').innerHTML = ''; } function close() { if (overlay) { overlay.classList.add('hidden'); } } function isOpen() { return overlay && !overlay.classList.contains('hidden'); } // ============ Search ============ function onInput() { clearTimeout(debounceTimer); const query = overlay.querySelector('#cp-input').value.trim(); if (!query) { overlay.querySelector('#cp-results').innerHTML = `
Type to search across everything. Use is:overdue tag:work type:task in:ProjectName to filter.
`; overlay.querySelector('#cp-filters').innerHTML = ''; currentResults = []; selectedIndex = -1; return; } debounceTimer = setTimeout(() => runSearch(query), 150); } async function runSearch(query) { try { const response = await GoingsOn.api.search.query({ query: query, limit: 20, offset: 0, }); currentResults = response.results || []; selectedIndex = currentResults.length > 0 ? 0 : -1; renderResults(currentResults, response.total || 0); renderFilters(response.activeFilters || []); } catch (err) { overlay.querySelector('#cp-results').innerHTML = `
Search failed. Try again.
`; } } // ============ Rendering ============ const typeIcons = { task: '☑', project: '⚙', email: '✉', event: '📅', contact: '👤', }; const typeLabels = { task: 'Task', project: 'Project', email: 'Email', event: 'Event', contact: 'Contact', }; function renderResults(results, total) { const container = overlay.querySelector('#cp-results'); if (results.length === 0) { container.innerHTML = '
No results found.
'; return; } let html = ''; for (let i = 0; i < results.length; i++) { const r = results[i]; const icon = typeIcons[r.resultType] || ''; const label = typeLabels[r.resultType] || r.resultType; const selected = i === selectedIndex ? ' cp-selected' : ''; const project = r.projectName ? `${esc(r.projectName)}` : ''; const snippet = r.snippet ? `${esc(r.snippet)}` : ''; html += `
${icon}
${esc(r.title)} ${project}${snippet}
${esc(label)}
`; } if (total > results.length) { html += `
${total - results.length} more results
`; } container.innerHTML = html; } function renderFilters(filters) { const container = overlay.querySelector('#cp-filters'); if (!filters || filters.length === 0) { container.innerHTML = ''; return; } container.innerHTML = filters.map(f => `${esc(f)}`).join(''); } function updateSelection() { if (!overlay) return; const items = overlay.querySelectorAll('.cp-result'); items.forEach((el, i) => { el.classList.toggle('cp-selected', i === selectedIndex); }); if (selectedIndex >= 0 && items[selectedIndex]) { items[selectedIndex].scrollIntoView({ block: 'nearest' }); } } // ============ Keyboard Navigation ============ function onKeydown(e) { if (e.key === 'Escape') { e.preventDefault(); close(); return; } if (e.key === 'ArrowDown') { e.preventDefault(); if (currentResults.length > 0) { selectedIndex = Math.min(selectedIndex + 1, currentResults.length - 1); updateSelection(); } return; } if (e.key === 'ArrowUp') { e.preventDefault(); if (currentResults.length > 0) { selectedIndex = Math.max(selectedIndex - 1, 0); updateSelection(); } return; } if (e.key === 'Enter') { e.preventDefault(); if (selectedIndex >= 0 && selectedIndex < currentResults.length) { navigateToResult(currentResults[selectedIndex]); } return; } } // ============ Navigation ============ function navigateToResult(result) { close(); switch (result.resultType) { case 'task': GoingsOn.navigation.switchView('tasks'); setTimeout(() => GoingsOn.tasks.openEdit(result.id), 100); break; case 'project': GoingsOn.navigation.switchView('projects'); setTimeout(() => GoingsOn.projects.openEdit(result.id), 100); break; case 'email': GoingsOn.navigation.switchView('emails'); setTimeout(() => GoingsOn.emails.open(result.id), 100); break; case 'event': GoingsOn.navigation.switchView('events'); setTimeout(() => GoingsOn.events.openEdit(result.id), 100); break; case 'contact': GoingsOn.navigation.switchView('contacts'); setTimeout(() => GoingsOn.contacts.openEdit(result.id), 100); break; } } // ============ Mouse Handlers ============ function hoverResult(index) { selectedIndex = index; updateSelection(); } function selectResult(index) { if (index >= 0 && index < currentResults.length) { navigateToResult(currentResults[index]); } } // ============ Export ============ GoingsOn.search = { open, close, isOpen, _hover: hoverResult, _select: selectResult, }; })();