| const searchEngines = require('./searchEngines'); |
| const highlightSearchTerm = require('./search/highlight'); |
|
|
| module.exports = function initSearch(state, dom) { |
| const { |
| searchInput, |
| searchBox, |
| searchResultsPage, |
| searchSections, |
| searchEngineToggle, |
| searchEngineToggleIcon, |
| searchEngineToggleLabel, |
| searchEngineDropdown, |
| searchEngineOptions, |
| } = dom; |
|
|
| if (!state.searchIndex) { |
| state.searchIndex = { initialized: false, items: [] }; |
| } |
| if (!state.currentSearchEngine) { |
| state.currentSearchEngine = 'local'; |
| } |
| if (typeof state.isSearchActive !== 'boolean') { |
| state.isSearchActive = false; |
| } |
|
|
| |
| function initSearchIndex() { |
| if (state.searchIndex.initialized) return; |
|
|
| state.searchIndex.items = []; |
|
|
| try { |
| |
| if (!state.pages) { |
| state.pages = document.querySelectorAll('.page'); |
| } |
|
|
| state.pages.forEach((page) => { |
| if (page.id === 'search-results') return; |
|
|
| const pageId = page.id; |
|
|
| page.querySelectorAll('.site-card').forEach((card) => { |
| try { |
| |
| if (card.closest('[data-search-exclude="true"]')) return; |
|
|
| |
| const dataTitle = card.dataset?.name || card.getAttribute('data-name') || ''; |
| const dataDescription = |
| card.dataset?.description || card.getAttribute('data-description') || ''; |
|
|
| const titleText = |
| card.querySelector('h3')?.textContent || |
| card.querySelector('.repo-title')?.textContent || |
| dataTitle; |
| const descriptionText = |
| card.querySelector('p')?.textContent || |
| card.querySelector('.repo-desc')?.textContent || |
| dataDescription; |
|
|
| const title = String(titleText || '').toLowerCase(); |
| const description = String(descriptionText || '').toLowerCase(); |
| const url = card.href || card.getAttribute('href') || '#'; |
| const icon = |
| card.querySelector('i.icon-fallback')?.className || |
| card.querySelector('i')?.className || |
| ''; |
|
|
| |
| state.searchIndex.items.push({ |
| pageId, |
| title, |
| description, |
| url, |
| icon, |
| element: card, |
| |
| searchText: (title + ' ' + description).toLowerCase(), |
| }); |
| } catch (cardError) { |
| console.error('Error processing card:', cardError); |
| } |
| }); |
| }); |
|
|
| state.searchIndex.initialized = true; |
| } catch (error) { |
| console.error('Error initializing search index:', error); |
| state.searchIndex.initialized = true; |
| } |
| } |
|
|
| |
| function performSearch(searchTerm) { |
| |
| if (!state.searchIndex.initialized) { |
| initSearchIndex(); |
| } |
|
|
| searchTerm = searchTerm.toLowerCase().trim(); |
|
|
| |
| if (!searchTerm) { |
| resetSearch(); |
| return; |
| } |
|
|
| if (!state.isSearchActive) { |
| state.isSearchActive = true; |
| } |
|
|
| try { |
| |
| const searchResults = new Map(); |
| let hasResults = false; |
|
|
| |
| const matchedItems = state.searchIndex.items.filter((item) => { |
| return ( |
| item.searchText.includes(searchTerm) || PinyinMatch.match(item.searchText, searchTerm) |
| ); |
| }); |
|
|
| |
| matchedItems.forEach((item) => { |
| if (!searchResults.has(item.pageId)) { |
| searchResults.set(item.pageId, []); |
| } |
| |
| searchResults.get(item.pageId).push(item.element.cloneNode(true)); |
| hasResults = true; |
| }); |
|
|
| |
| requestAnimationFrame(() => { |
| try { |
| |
| searchSections.forEach((section) => { |
| try { |
| const grid = section.querySelector('.sites-grid'); |
| if (grid) { |
| grid.innerHTML = ''; |
| } |
| section.style.display = 'none'; |
| } catch (sectionError) { |
| console.error('Error clearing search section'); |
| } |
| }); |
|
|
| |
| searchResults.forEach((matches, pageId) => { |
| const section = searchResultsPage.querySelector(`[data-section="${pageId}"]`); |
| if (section) { |
| try { |
| const grid = section.querySelector('.sites-grid'); |
| if (grid) { |
| const fragment = document.createDocumentFragment(); |
|
|
| matches.forEach((card) => { |
| |
| highlightSearchTerm(card, searchTerm); |
| fragment.appendChild(card); |
| }); |
|
|
| grid.appendChild(fragment); |
| section.style.display = 'block'; |
| } |
| } catch (gridError) { |
| console.error('Error updating search results grid'); |
| } |
| } |
| }); |
|
|
| |
| const subtitle = searchResultsPage.querySelector('.subtitle'); |
| if (subtitle) { |
| subtitle.textContent = hasResults |
| ? `在所有页面中找到 ${matchedItems.length} 个匹配项` |
| : '未找到匹配的结果'; |
| } |
|
|
| |
| if (state.currentPageId !== 'search-results') { |
| state.currentPageId = 'search-results'; |
| if (!state.pages) state.pages = document.querySelectorAll('.page'); |
| state.pages.forEach((page) => { |
| page.classList.toggle('active', page.id === 'search-results'); |
| }); |
| } |
|
|
| |
| searchBox.classList.toggle('has-results', hasResults); |
| searchBox.classList.toggle('no-results', !hasResults); |
| } catch (uiError) { |
| console.error('Error updating search UI'); |
| } |
| }); |
| } catch (searchError) { |
| console.error('Error performing search'); |
| } |
| } |
|
|
| |
| function resetSearch() { |
| if (!state.isSearchActive) return; |
|
|
| state.isSearchActive = false; |
|
|
| try { |
| requestAnimationFrame(() => { |
| try { |
| |
| searchSections.forEach((section) => { |
| try { |
| const grid = section.querySelector('.sites-grid'); |
| if (grid) { |
| while (grid.firstChild) { |
| grid.removeChild(grid.firstChild); |
| } |
| } |
| section.style.display = 'none'; |
| } catch (sectionError) { |
| console.error('Error clearing search section'); |
| } |
| }); |
|
|
| |
| searchBox.classList.remove('has-results', 'no-results'); |
|
|
| |
| const currentActiveNav = document.querySelector('.nav-item.active'); |
| if (currentActiveNav) { |
| const targetPageId = currentActiveNav.getAttribute('data-page'); |
|
|
| if (targetPageId && state.currentPageId !== targetPageId) { |
| state.currentPageId = targetPageId; |
| if (!state.pages) state.pages = document.querySelectorAll('.page'); |
| state.pages.forEach((page) => { |
| page.classList.toggle('active', page.id === targetPageId); |
| }); |
| } |
| } else { |
| |
| state.currentPageId = state.homePageId; |
| if (!state.pages) state.pages = document.querySelectorAll('.page'); |
| state.pages.forEach((page) => { |
| page.classList.toggle('active', page.id === state.homePageId); |
| }); |
| } |
| } catch (resetError) { |
| console.error('Error resetting search UI'); |
| } |
| }); |
| } catch (error) { |
| console.error('Error in resetSearch'); |
| } |
| } |
|
|
| |
| const debounce = (fn, delay) => { |
| let timer = null; |
| return (...args) => { |
| if (timer) clearTimeout(timer); |
| timer = setTimeout(() => { |
| fn.apply(this, args); |
| timer = null; |
| }, delay); |
| }; |
| }; |
|
|
| const debouncedSearch = debounce(performSearch, 300); |
|
|
| searchInput.addEventListener('input', (e) => { |
| |
| if (state.currentSearchEngine === 'local') { |
| debouncedSearch(e.target.value); |
| } else { |
| |
| if (state.isSearchActive) { |
| resetSearch(); |
| } |
| } |
| }); |
|
|
| |
| function updateSearchEngineUI() { |
| |
| searchEngineOptions.forEach((option) => { |
| option.classList.remove('active'); |
|
|
| |
| if (option.getAttribute('data-engine') === state.currentSearchEngine) { |
| option.classList.add('active'); |
| } |
| }); |
|
|
| |
| const engine = searchEngines[state.currentSearchEngine]; |
| if (!engine) return; |
| const displayName = engine.shortName || engine.name.replace(/搜索$/, ''); |
|
|
| if (searchEngineToggleIcon) { |
| if (engine.iconSvg) { |
| searchEngineToggleIcon.className = 'search-engine-icon search-engine-icon-svg'; |
| searchEngineToggleIcon.innerHTML = engine.iconSvg; |
| } else { |
| searchEngineToggleIcon.innerHTML = ''; |
| searchEngineToggleIcon.className = `search-engine-icon ${engine.icon}`; |
| } |
| } |
| if (searchEngineToggleLabel) { |
| searchEngineToggleLabel.textContent = displayName; |
| } |
| if (searchEngineToggle) { |
| searchEngineToggle.setAttribute('aria-label', `当前搜索引擎:${engine.name},点击切换`); |
| } |
| } |
|
|
| |
| function initSearchEngine() { |
| |
| const savedEngine = localStorage.getItem('searchEngine'); |
| if (savedEngine && searchEngines[savedEngine]) { |
| state.currentSearchEngine = savedEngine; |
| } |
|
|
| |
| updateSearchEngineUI(); |
|
|
| |
| const toggleEngineDropdown = () => { |
| if (!searchEngineDropdown) return; |
| const next = !searchEngineDropdown.classList.contains('active'); |
| searchEngineDropdown.classList.toggle('active', next); |
| if (searchBox) { |
| searchBox.classList.toggle('dropdown-open', next); |
| } |
| if (searchEngineToggle) { |
| searchEngineToggle.setAttribute('aria-expanded', String(next)); |
| } |
| }; |
|
|
| if (searchEngineToggle) { |
| searchEngineToggle.addEventListener('click', (e) => { |
| e.stopPropagation(); |
| toggleEngineDropdown(); |
| }); |
|
|
| |
| searchEngineToggle.addEventListener('keydown', (e) => { |
| if (e.key === 'Enter' || e.key === ' ') { |
| e.preventDefault(); |
| e.stopPropagation(); |
| toggleEngineDropdown(); |
| } |
| }); |
| } |
|
|
| |
| searchEngineOptions.forEach((option) => { |
| |
| if (option.getAttribute('data-engine') === state.currentSearchEngine) { |
| option.classList.add('active'); |
| } |
|
|
| option.addEventListener('click', (e) => { |
| e.stopPropagation(); |
|
|
| |
| const engine = option.getAttribute('data-engine'); |
|
|
| |
| if (engine && searchEngines[engine]) { |
| |
| if (state.currentSearchEngine !== engine && state.isSearchActive) { |
| resetSearch(); |
| } |
|
|
| state.currentSearchEngine = engine; |
| localStorage.setItem('searchEngine', engine); |
|
|
| |
| updateSearchEngineUI(); |
|
|
| |
| if (searchEngineDropdown) { |
| searchEngineDropdown.classList.remove('active'); |
| } |
| if (searchBox) { |
| searchBox.classList.remove('dropdown-open'); |
| } |
| } |
| }); |
| }); |
|
|
| |
| document.addEventListener('click', () => { |
| if (!searchEngineDropdown) return; |
| searchEngineDropdown.classList.remove('active'); |
| if (searchBox) { |
| searchBox.classList.remove('dropdown-open'); |
| } |
| }); |
| } |
|
|
| |
| function executeSearch(searchTerm) { |
| if (!searchTerm.trim()) return; |
|
|
| |
| if (state.currentSearchEngine === 'local') { |
| |
| performSearch(searchTerm); |
| } else { |
| |
| const engine = searchEngines[state.currentSearchEngine]; |
| if (engine && engine.url) { |
| |
| window.open(engine.url + encodeURIComponent(searchTerm), '_blank'); |
| } |
| } |
| } |
|
|
| |
| searchInput.addEventListener('keyup', (e) => { |
| if (e.key === 'Escape') { |
| searchInput.value = ''; |
| resetSearch(); |
| } else if (e.key === 'Enter') { |
| executeSearch(searchInput.value); |
| } |
| }); |
|
|
| |
| searchInput.addEventListener('keydown', (e) => { |
| if (e.key === 'Enter') { |
| e.preventDefault(); |
| } |
| }); |
|
|
| return { |
| initSearchIndex, |
| initSearchEngine, |
| resetSearch, |
| performSearch, |
| }; |
| }; |
|
|