/**
* 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,
};
})();