/* ═══════════════════════════════════════════════════════════════ LearnSpace Global Search — /js/search.js Include on any sidebar page after api.js. Opens with Ctrl+K or click on search button in sidebar. ═══════════════════════════════════════════════════════════════ */ (function () { if (!window.LS) return; const ICONS = { lesson: 'book-open', course: 'graduation-cap', file: 'file-text', question: 'help-circle', }; const LABELS = { lesson: 'Уроки', course: 'Курсы', file: 'Файлы', question: 'Вопросы', }; let _overlay = null; let _input = null; let _results = null; let _timer = null; let _items = []; let _activeIdx = -1; function esc(s) { return String(s || '').replace(/&/g, '&').replace(//g, '>'); } function build() { if (_overlay) return; _overlay = document.createElement('div'); _overlay.className = 'gs-overlay'; _overlay.innerHTML = `
ESC
Начните вводить для поиска
`; document.body.appendChild(_overlay); _input = _overlay.querySelector('.gs-input'); _results = _overlay.querySelector('.gs-results'); // Close on backdrop click _overlay.addEventListener('click', e => { if (e.target === _overlay) close(); }); // Event delegation for result clicks — один listener вместо N _results.addEventListener('click', e => { const item = e.target.closest('.gs-item'); if (item) window.location.href = item.dataset.url; }); // Input handling _input.addEventListener('input', () => { clearTimeout(_timer); _timer = setTimeout(doSearch, 250); }); // Keyboard nav _input.addEventListener('keydown', e => { if (e.key === 'Escape') { close(); return; } if (e.key === 'ArrowDown') { e.preventDefault(); navigate(1); return; } if (e.key === 'ArrowUp') { e.preventDefault(); navigate(-1); return; } if (e.key === 'Enter') { e.preventDefault(); const active = _items[_activeIdx]; if (active) window.location.href = active.url; return; } }); } async function doSearch() { const q = _input.value.trim(); if (q.length < 2) { _results.innerHTML = '
Начните вводить для поиска
'; _items = []; _activeIdx = -1; return; } try { const data = await LS.globalSearch(q); _items = data.results || []; _activeIdx = -1; render(); } catch { _results.innerHTML = '
Ошибка поиска
'; } } function render() { if (!_items.length) { _results.innerHTML = '
Ничего не найдено
'; return; } // Group by type const groups = {}; for (const item of _items) { if (!groups[item.type]) groups[item.type] = []; groups[item.type].push(item); } let html = ''; let idx = 0; for (const [type, items] of Object.entries(groups)) { html += `
${LABELS[type] || type}
`; for (const item of items) { const iconCls = `gs-icon-${type}`; const iconName = ICONS[type] || 'bookmark'; html += `
${esc(item.title)}
${item.subtitle ? `
${esc(item.subtitle)}
` : ''}
`; idx++; } } _results.innerHTML = html; // Lucide icons if (window.lucide) lucide.createIcons({ nodes: _results.querySelectorAll('[data-lucide]') }); } function navigate(dir) { const total = _items.length; if (!total) return; const prev = _results.querySelector('.gs-item.active'); if (prev) prev.classList.remove('active'); _activeIdx = (_activeIdx + dir + total) % total; const next = _results.querySelector(`.gs-item[data-idx="${_activeIdx}"]`); if (next) { next.classList.add('active'); next.scrollIntoView({ block: 'nearest' }); } } function open() { build(); _overlay.classList.add('open'); _input.value = ''; _results.innerHTML = '
Начните вводить для поиска
'; _items = []; _activeIdx = -1; setTimeout(() => _input.focus(), 50); } function close() { if (_overlay) _overlay.classList.remove('open'); } // Global shortcut: Ctrl+K document.addEventListener('keydown', e => { if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); if (_overlay?.classList.contains('open')) close(); else open(); } }); // Expose window.lsSearchOpen = open; window.lsSearchClose = close; })();