const $searchMask = document.getElementById('search-mask'), $searchDialog = document.querySelector('#local-search .search-dialog'), $input = document.querySelector('#search-input'), $resultContent = document.getElementById('search-results'), $loadingStatus = document.getElementById('loading-status') let dataObj = null class search { static openSearch() { utils.fadeIn($searchMask, '0.5') utils.fadeIn($searchDialog, '0.5') setTimeout(() => { document.querySelector('#search-input').focus() }, 100) search.search() document.addEventListener('keydown', function f(event) { if (event.code === 'Escape') { closeSearch() document.removeEventListener('keydown', f) } }) } static closeSearch() { utils.fadeOut($searchDialog, '0.5') utils.fadeOut($searchMask, '0.5') } static async fetchData(path) { let data = [] const response = await fetch(path) const res = await new window.DOMParser().parseFromString(await response.text(), 'text/xml') data = [...res.querySelectorAll('entry')].map(item => { return { title: item.querySelector('title').textContent, content: item.querySelector('content') && item.querySelector('content').textContent, url: item.querySelector('url').textContent } }) if (response.ok) { const $loadDataItem = document.getElementById('loading-database') $loadDataItem.nextElementSibling.style.display = 'block' $loadDataItem.remove() } return data } static search() { if (!GLOBALCONFIG.localsearch.preload && dataObj === null) dataObj = this.fetchData(GLOBALCONFIG.localsearch.path) $input.addEventListener('input', function type() { const keywords = this.value.trim().toLowerCase().split(/[\s]+/) if (keywords[0] !== '') $loadingStatus.innerHTML = '加载中' else { $resultContent.innerHTML = '' return } if (keywords.length <= 0) return let count = 0, str = '
' // perform local searching dataObj.then(data => { data.forEach(data => { let isMatch = true let dataTitle = data.title ? data.title.trim().toLowerCase() : '' const dataContent = data.content ? data.content.trim().replace(/<[^>]+>/g, '').toLowerCase() : '' const dataUrl = data.url.startsWith('/') ? data.url : GLOBALCONFIG.root + data.url let indexTitle = -1 let indexContent = -1 let firstOccur = -1 // only match articles with not empty titles and contents if (dataTitle !== '' || dataContent !== '') { keywords.forEach((keyword, i) => { indexTitle = dataTitle.indexOf(keyword) indexContent = dataContent.indexOf(keyword) if (indexTitle < 0 && indexContent < 0) { isMatch = false } else { if (indexContent < 0) { indexContent = 0 } if (i === 0) { firstOccur = indexContent } } }) } else { isMatch = false } // show search results if (isMatch) { if (firstOccur >= 0) { // cut out 130 characters let start = firstOccur - 30 let end = firstOccur + 100 let pre = '' let post = '' if (start < 0) { start = 0 } if (start === 0) { end = 100 } else { pre = '...' } if (end > dataContent.length) { end = dataContent.length } else { post = '...' } let matchContent = dataContent.substring(start, end) // highlight all keywords keywords.forEach(keyword => { const regex = new RegExp(`(?!<[^>]*?)(${keyword})(?![^<]*?>)`, 'gi') matchContent = matchContent.replaceAll(regex, '$1') dataTitle = dataTitle.replaceAll(regex, '$1') }) str += '
' + dataTitle + '' count += 1 if (dataContent !== '') { str += '
' + pre + matchContent + post + '
' } } str += '
' } }) if (count === 0) { str += `
${GLOBALCONFIG.lang.search.empty}
` } else { str += `
${GLOBALCONFIG.lang.search.hit.replace('${query}', '' + count + '')}
` } str += '
' $resultContent.innerHTML = str if (keywords[0] !== '') $loadingStatus.innerHTML = '' }) }) } } const searchClickFn = () => { document.querySelector('#search-button > .search').addEventListener('click', search.openSearch) } const searchClickFnOnce = () => { document.querySelector('#local-search .search-close-button').addEventListener('click', search.closeSearch) $searchMask.addEventListener('click', search.closeSearch) if (GLOBALCONFIG.localsearch.preload) dataObj = search.fetchData(GLOBALCONFIG.localsearch.path) } window.addEventListener('load', () => { searchClickFn() searchClickFnOnce() }) window.addEventListener('pjax:complete', () => { searchClickFn() })