feat: 重構本地搜索

feat: Algolia 搜索,點擊文章內容也會跳轉到相應頁面
fix: 修復 fullpage loading 顯示滾動條的 bug close #1235
fix: 修復 safari 下,搜索內容被系統搜索框遮擋的 bug
improvement: 手機 safari 橫屏時,網頁全屏幕顯示
improvement: 當沒有開啟 beautify 時, hr 顯示默認的樣式
pull/1242/head
Jerry 2 years ago
parent 63dc605794
commit 8199e25215

@ -56,11 +56,15 @@ algolia_search:
# Local search
local_search:
enable: false
# Preload the search data when the page loads.
preload: false
# Show top n results per article, show all results by setting to -1
top_n_per_article: 1
# Unescape html strings to the readable one.
unescape: false
CDN:
# Docsearch
# https://docsearch.algolia.com/
docsearch:
enable: false
appId:
@ -777,7 +781,7 @@ rightside_item_order:
medium_zoom: false
# fancybox
# http://fancyapps.com/fancybox/3/
# https://fancyapps.com/fancybox/
fancybox: true
# Tag Plugins settings (標籤外掛)

@ -45,6 +45,7 @@ search:
local_search:
input_placeholder: Search for Posts
hits_empty: "We didn't find any results for the search: ${query}"
hits_stats: '${hits} results found'
pagination:
prev: Previous Post

@ -45,6 +45,7 @@ search:
local_search:
input_placeholder: Search for Posts
hits_empty: "We didn't find any results for the search: ${query}"
hits_stats: '${hits} results found'
pagination:
prev: Previous Post

@ -46,6 +46,7 @@ search:
local_search:
input_placeholder: 搜索文章
hits_empty: '找不到您查询的内容:${query}'
hits_stats: '共找到 ${hits} 篇文章'
pagination:
prev: 上一篇

@ -46,7 +46,8 @@ search:
local_search:
input_placeholder: 搜尋文章
hits_empty: '找不到您查詢的內容:${query}'
hits_stats: '共找到 ${hits} 篇文章'
pagination:
prev: 上一篇
next: 下一篇

@ -59,3 +59,5 @@ div
if theme.busuanzi.site_uv || theme.busuanzi.site_pv || theme.busuanzi.page_pv
script(async data-pjax src= theme.asset.busuanzi || '//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js')
!=partial('includes/third-party/search/index', {}, {cache: true})

@ -15,7 +15,7 @@
meta(charset='UTF-8')
meta(http-equiv="X-UA-Compatible" content="IE=edge")
meta(name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0")
meta(name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0,viewport-fit=cover")
title= tabTitle
meta(name="author" content=pageAuthor)
meta(name="copyright" content=pageCopyright)

@ -21,9 +21,12 @@
localSearch = JSON.stringify({
path: theme.local_search.CDN ? theme.local_search.CDN : config.root + config.search.path,
preload: theme.local_search.preload,
top_n_per_article: theme.local_search.top_n_per_article,
unescape: theme.local_search.unescape,
languages: {
// search languages
hits_empty: _p("search.local_search.hits_empty"),
hits_stats: _p("search.local_search.hits_stats"),
}
})
}

@ -44,5 +44,4 @@ html(lang=config.language data-theme=theme.display_mode class=htmlClassHideAside
include ./404.pug
include ./rightside.pug
include ./additional-js.pug
!=partial('includes/third-party/search/index', {}, {cache: true})
include ./additional-js.pug

@ -11,15 +11,16 @@
script.
const preloader = {
endLoading: () => {
document.body.style.overflow = 'auto';
document.body.style.overflow = '';
document.getElementById('loading-box').classList.add("loaded")
},
initLoading: () => {
document.body.style.overflow = '';
document.body.style.overflow = 'hidden';
document.getElementById('loading-box').classList.remove("loaded")
}
}
preloader.initLoading()
window.addEventListener('load',()=> { preloader.endLoading() })
if (!{theme.pjax && theme.pjax.enable}) {

@ -15,8 +15,8 @@
.local-search-box
input(placeholder=_p("search.local_search.input_placeholder") type="text").local-search-box--input
hr
#local-search-results
#local-search-results.no-result
#local-search-stats-wrap
#search-mask
script(src=url_for(theme.asset.local_search))

@ -1,6 +1,6 @@
{
"name": "hexo-theme-butterfly",
"version": "4.8.0-b1",
"version": "4.8.0-b2",
"description": "A Simple and Card UI Design theme for Hexo",
"main": "package.json",
"scripts": {

@ -1,11 +1,11 @@
algolia_search:
name: algoliasearch
file: dist/algoliasearch-lite.umd.js
version: 4.14.3
version: 4.15.0
instantsearch:
name: instantsearch.js
file: dist/instantsearch.production.min.js
version: 4.51.1
version: 4.52.0
pjax:
name: pjax
file: pjax.min.js
@ -42,12 +42,12 @@ waline_js:
name: '@waline/client'
file: dist/waline.js
other_name: waline
version: 2.14.7
version: 2.14.8
waline_css:
name: '@waline/client'
file: dist/waline.css
other_name: waline
version: 2.14.7
version: 2.14.8
sharejs:
name: butterfly-extsrc
file: sharejs/dist/js/social-share.min.js
@ -73,7 +73,7 @@ katex_copytex:
mermaid:
name: mermaid
file: dist/mermaid.min.js
version: 9.4.0
version: 9.4.3
canvas_ribbon:
name: butterfly-extsrc
file: dist/canvas-ribbon.min.js
@ -121,12 +121,12 @@ pangu:
fancybox_css:
name: '@fancyapps/ui'
file: dist/fancybox/fancybox.css
version: 5.0.3
version: 5.0.7
other_name: fancyapps-ui
fancybox:
name: '@fancyapps/ui'
file: dist/fancybox/fancybox.umd.js
version: 5.0.3
version: 5.0.7
other_name: fancyapps-ui
medium_zoom:
name: medium-zoom
@ -183,11 +183,11 @@ prismjs_autoloader:
artalk_js:
name: artalk
file: dist/Artalk.js
version: 2.4.4
version: 2.5.0
artalk_css:
name: artalk
file: dist/Artalk.css
version: 2.4.4
version: 2.5.0
pace_js:
name: pace-js
other_name: pace

@ -144,15 +144,26 @@ beautify()
p
margin: 0 0 8px
> :last-child
margin-bottom: 0 !important
hr
margin: 20px 0
if hexo-config('beautify.enable')
if hexo-config('beautify.field') == 'site'
beautify()
else if hexo-config('beautify.field') == 'post'
&.post-content
beautify()
else
hr
margin: 20px 0
border: 1px inset
width 100%
> :last-child
margin-bottom: 0 !important
&:before
content: none
#post
.tag_share

@ -113,4 +113,8 @@ span.mathjax-overflow
content: none
.snackbar-css
border-radius: 5px !important
border-radius: 5px !important
+maxWidth768()
.fancybox__toolbar__column.is-middle
display: none

@ -26,12 +26,15 @@
color: $search-keyword-highlight
font-weight: bold
.algolia-hits-item-title
font-weight: 600
.algolia-hit-item-content
margin: 0 0 8px
word-break: break-word
.ais-Pagination
margin: 20px 0 0
margin: 15px 0 0
padding: 0
text-align: center
@ -61,22 +64,16 @@
.ais-Pagination-item--disabled
visibility: hidden
.algolia-logo
padding-top: 2px
width: 80px
height: 30px
#algolia-hits
> div
overflow-y: scroll
margin: 0 -20px
padding: 0 22px
+minWidth768()
max-height: calc(80vh - 240px)
max-height: calc(80vh - 240px)
+maxWidth768()
height: calc(100vh - 260px)
max-height: none
height: calc(var(--search-height) - 265px)
#algolia-info
div
@ -84,10 +81,7 @@
.algolia-poweredBy
float: right
vertical-align: text-top
.apple
#algolia-search
#algolia-hits
> div
+maxWidth768()
height: calc(90vh - 260px)
svg
height: 1.1em

@ -9,6 +9,7 @@
width: 600px
border-radius: 8px
background: var(--search-bg)
--search-height: 100vh
+maxWidth768()
top: 0

@ -18,7 +18,7 @@
.search-wrap
display: none
.local-search__hit-item
.local-search-hit-item
position: relative
padding-left: 24px
line-height: 1.7
@ -55,16 +55,20 @@
margin: 0 0 8px
word-break: break-word
.search-keyword
color: $search-keyword-highlight
font-weight: bold
.search-result-list
overflow-y: overlay
margin: 0 -20px
padding: 0 22px
max-height: calc(80vh - 130px)
max-height: calc(80vh - 200px)
+maxWidth768()
padding-bottom: 40px
max-height: 75vh !important
max-height: calc(var(--search-height) - 220px) !important
.no-result
& + #local-search-stats-wrap
display: none
.search-keyword
background: transparent
color: $search-keyword-highlight
font-weight: bold

@ -1,10 +1,13 @@
window.addEventListener('load', () => {
const $searchMask = document.getElementById('search-mask')
const $searchDialog = document.querySelector('#algolia-search .search-dialog')
const openSearch = () => {
const bodyStyle = document.body.style
bodyStyle.width = '100%'
bodyStyle.overflow = 'hidden'
btf.animateIn(document.getElementById('search-mask'), 'to_show 0.5s')
btf.animateIn(document.querySelector('#algolia-search .search-dialog'), 'titleScale 0.5s')
btf.animateIn($searchMask, 'to_show 0.5s')
btf.animateIn($searchDialog, 'titleScale 0.5s')
setTimeout(() => { document.querySelector('#algolia-search .ais-SearchBox-input').focus() }, 100)
// shortcut: ESC
@ -14,22 +17,33 @@ window.addEventListener('load', () => {
document.removeEventListener('keydown', f)
}
})
fixSafariHeight()
window.addEventListener('resize', fixSafariHeight)
}
const closeSearch = () => {
const bodyStyle = document.body.style
bodyStyle.width = ''
bodyStyle.overflow = ''
btf.animateOut(document.querySelector('#algolia-search .search-dialog'), 'search_close .5s')
btf.animateOut(document.getElementById('search-mask'), 'to_hide 0.5s')
btf.animateOut($searchDialog, 'search_close .5s')
btf.animateOut($searchMask, 'to_hide 0.5s')
window.removeEventListener('resize', fixSafariHeight)
}
// fix safari
const fixSafariHeight = () => {
if (window.innerWidth < 768) {
$searchDialog.style.setProperty('--search-height', window.innerHeight + 'px')
}
}
const searchClickFn = () => {
document.querySelector('#search-button > .search').addEventListener('click', openSearch)
}
const searchClickFnOnce = () => {
document.getElementById('search-mask').addEventListener('click', closeSearch)
const searchFnOnce = () => {
$searchMask.addEventListener('click', closeSearch)
document.querySelector('#algolia-search .search-close-button').addEventListener('click', closeSearch)
}
@ -102,9 +116,9 @@ window.addEventListener('load', () => {
: ''
return `
<a href="${link}" class="algolia-hit-item-link">
${result.title.value || 'no-title'}
</a>
<p class="algolia-hit-item-content">${content}</p>`
<span class="algolia-hits-item-title">${result.title.value || 'no-title'}</span>
<p class="algolia-hit-item-content">${content}</p>
</a>`
},
empty: function (data) {
return (
@ -150,10 +164,10 @@ window.addEventListener('load', () => {
search.start()
searchClickFn()
searchClickFnOnce()
searchFnOnce()
window.addEventListener('pjax:complete', () => {
getComputedStyle(document.querySelector('#algolia-search .search-dialog')).display === 'block' && closeSearch()
!btf.isHidden($searchMask) && closeSearch()
searchClickFn()
})

@ -1,17 +1,311 @@
/**
* Refer to hexo-generator-searchdb
* https://github.com/next-theme/hexo-generator-searchdb/blob/main/dist/search.js
* Modified by hexo-theme-butterfly
*/
class LocalSearch {
constructor ({
path = '',
unescape = false,
top_n_per_article = 1
}) {
this.path = path
this.unescape = unescape
this.top_n_per_article = top_n_per_article
this.isfetched = false
this.datas = null
}
getIndexByWord (words, text, caseSensitive = false) {
const index = []
const included = new Set()
if (!caseSensitive) {
text = text.toLowerCase()
}
words.forEach(word => {
if (this.unescape) {
const div = document.createElement('div')
div.innerText = word
word = div.innerHTML
}
const wordLen = word.length
if (wordLen === 0) return
let startPosition = 0
let position = -1
if (!caseSensitive) {
word = word.toLowerCase()
}
while ((position = text.indexOf(word, startPosition)) > -1) {
index.push({ position, word })
included.add(word)
startPosition = position + wordLen
}
})
// Sort index by position of keyword
index.sort((left, right) => {
if (left.position !== right.position) {
return left.position - right.position
}
return right.word.length - left.word.length
})
return [index, included]
}
// Merge hits into slices
mergeIntoSlice (start, end, index) {
let item = index[0]
let { position, word } = item
const hits = []
const count = new Set()
while (position + word.length <= end && index.length !== 0) {
count.add(word)
hits.push({
position,
length: word.length
})
const wordEnd = position + word.length
// Move to next position of hit
index.shift()
while (index.length !== 0) {
item = index[0]
position = item.position
word = item.word
if (wordEnd > position) {
index.shift()
} else {
break
}
}
}
return {
hits,
start,
end,
count: count.size
}
}
// Highlight title and content
highlightKeyword (val, slice) {
let result = ''
let index = slice.start
for (const { position, length } of slice.hits) {
result += val.substring(index, position)
index = position + length
result += `<mark class="search-keyword">${val.substr(position, length)}</mark>`
}
result += val.substring(index, slice.end)
return result
}
getResultItems (keywords) {
const resultItems = []
this.datas.forEach(({ title, content, url }) => {
// The number of different keywords included in the article.
const [indexOfTitle, keysOfTitle] = this.getIndexByWord(keywords, title)
const [indexOfContent, keysOfContent] = this.getIndexByWord(keywords, content)
const includedCount = new Set([...keysOfTitle, ...keysOfContent]).size
// Show search results
const hitCount = indexOfTitle.length + indexOfContent.length
if (hitCount === 0) return
const slicesOfTitle = []
if (indexOfTitle.length !== 0) {
slicesOfTitle.push(this.mergeIntoSlice(0, title.length, indexOfTitle))
}
let slicesOfContent = []
while (indexOfContent.length !== 0) {
const item = indexOfContent[0]
const { position } = item
// Cut out 120 characters. The maxlength of .search-input is 80.
const start = Math.max(0, position - 20)
const end = Math.min(content.length, position + 100)
slicesOfContent.push(this.mergeIntoSlice(start, end, indexOfContent))
}
// Sort slices in content by included keywords' count and hits' count
slicesOfContent.sort((left, right) => {
if (left.count !== right.count) {
return right.count - left.count
} else if (left.hits.length !== right.hits.length) {
return right.hits.length - left.hits.length
}
return left.start - right.start
})
// Select top N slices in content
const upperBound = parseInt(this.top_n_per_article, 10)
if (upperBound >= 0) {
slicesOfContent = slicesOfContent.slice(0, upperBound)
}
let resultItem = ''
url = new URL(url, location.origin)
url.searchParams.append('highlight', keywords.join(' '))
if (slicesOfTitle.length !== 0) {
resultItem += `<div class="local-search-hit-item"><a href="${url.href}"><span class="search-result-title">${this.highlightKeyword(title, slicesOfTitle[0])}</span>`
} else {
resultItem += `<div class="local-search-hit-item"><a href="${url.href}"><span class="search-result-title">${title}</span>`
}
slicesOfContent.forEach(slice => {
resultItem += `<p class="search-result">${this.highlightKeyword(content, slice)}...</p></a>`
})
resultItem += '</div>'
resultItems.push({
item: resultItem,
id: resultItems.length,
hitCount,
includedCount
})
})
return resultItems
}
fetchData () {
const isXml = !this.path.endsWith('json')
fetch(this.path)
.then(response => response.text())
.then(res => {
// Get the contents from search data
this.isfetched = true
this.datas = isXml
? [...new DOMParser().parseFromString(res, 'text/xml').querySelectorAll('entry')].map(element => ({
title: element.querySelector('title').textContent,
content: element.querySelector('content').textContent,
url: element.querySelector('url').textContent
}))
: JSON.parse(res)
// Only match articles with non-empty titles
this.datas = this.datas.filter(data => data.title).map(data => {
data.title = data.title.trim()
data.content = data.content ? data.content.trim().replace(/<[^>]+>/g, '') : ''
data.url = decodeURIComponent(data.url).replace(/\/{2,}/g, '/')
return data
})
// Remove loading animation
window.dispatchEvent(new Event('search:loaded'))
})
}
// Highlight by wrapping node in mark elements with the given class name
highlightText (node, slice, className) {
const val = node.nodeValue
let index = slice.start
const children = []
for (const { position, length } of slice.hits) {
const text = document.createTextNode(val.substring(index, position))
index = position + length
const mark = document.createElement('mark')
mark.className = className
mark.appendChild(document.createTextNode(val.substr(position, length)))
children.push(text, mark)
}
node.nodeValue = val.substring(index, slice.end)
children.forEach(element => {
node.parentNode.insertBefore(element, node)
})
}
// Highlight the search words provided in the url in the text
highlightSearchWords (body) {
const params = new URL(location.href).searchParams.get('highlight')
const keywords = params ? params.split(' ') : []
if (!keywords.length || !body) return
const walk = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, null)
const allNodes = []
while (walk.nextNode()) {
if (!walk.currentNode.parentNode.matches('button, select, textarea, .mermaid')) allNodes.push(walk.currentNode)
}
allNodes.forEach(node => {
const [indexOfNode] = this.getIndexByWord(keywords, node.nodeValue)
if (!indexOfNode.length) return
const slice = this.mergeIntoSlice(0, node.nodeValue.length, indexOfNode)
this.highlightText(node, slice, 'search-keyword')
})
}
}
window.addEventListener('load', () => {
// Search
const { path, top_n_per_article, unescape, languages } = GLOBAL_CONFIG.localSearch
const localSearch = new LocalSearch({
path,
top_n_per_article,
unescape
})
const input = document.querySelector('#local-search-input input')
const statsItem = document.getElementById('local-search-stats-wrap')
const $loadingStatus = document.getElementById('loading-status')
const inputEventFunction = () => {
if (!localSearch.isfetched) return
const searchText = input.value.trim().toLowerCase()
if (searchText !== '') $loadingStatus.innerHTML = '<i class="fas fa-spinner fa-pulse"></i>'
const keywords = searchText.split(/[-\s]+/)
const container = document.getElementById('local-search-results')
let resultItems = []
if (searchText.length > 0) {
// Perform local searching
resultItems = localSearch.getResultItems(keywords)
}
if (keywords.length === 1 && keywords[0] === '') {
container.classList.add('no-result')
container.textContent = ''
} else if (resultItems.length === 0) {
container.textContent = ''
statsItem.innerHTML = `<div class="search-result-stats">${languages.hits_empty.replace(/\$\{query}/, searchText)}</div>`
} else {
resultItems.sort((left, right) => {
if (left.includedCount !== right.includedCount) {
return right.includedCount - left.includedCount
} else if (left.hitCount !== right.hitCount) {
return right.hitCount - left.hitCount
}
return right.id - left.id
})
const stats = languages.hits_stats.replace(/\$\{hits}/, resultItems.length)
container.classList.remove('no-result')
container.innerHTML = `<div class="search-result-list">${resultItems.map(result => result.item).join('')}</div>`
statsItem.innerHTML = `<hr><div class="search-result-stats">${stats}</div>`
window.pjax && window.pjax.refresh(container)
}
$loadingStatus.innerHTML = ''
}
let loadFlag = false
let dataObj = []
const $searchMask = document.getElementById('search-mask')
const $searchDialog = document.querySelector('#local-search .search-dialog')
// fix safari
const fixSafariHeight = () => {
if (window.innerWidth < 768) {
$searchDialog.style.setProperty('--search-height', window.innerHeight + 'px')
}
}
const openSearch = () => {
const bodyStyle = document.body.style
bodyStyle.width = '100%'
bodyStyle.overflow = 'hidden'
btf.animateIn($searchMask, 'to_show 0.5s')
btf.animateIn(document.querySelector('#local-search .search-dialog'), 'titleScale 0.5s')
setTimeout(() => { document.querySelector('#local-search-input input').focus() }, 100)
btf.animateIn($searchDialog, 'titleScale 0.5s')
setTimeout(() => { input.focus() }, 300)
if (!loadFlag) {
search()
!localSearch.isfetched && localSearch.fetchData()
input.addEventListener('input', inputEventFunction)
loadFlag = true
}
// shortcut: ESC
@ -21,170 +315,46 @@ window.addEventListener('load', () => {
document.removeEventListener('keydown', f)
}
})
fixSafariHeight()
window.addEventListener('resize', fixSafariHeight)
}
const closeSearch = () => {
const bodyStyle = document.body.style
bodyStyle.width = ''
bodyStyle.overflow = ''
btf.animateOut(document.querySelector('#local-search .search-dialog'), 'search_close .5s')
btf.animateOut($searchDialog, 'search_close .5s')
btf.animateOut($searchMask, 'to_hide 0.5s')
window.removeEventListener('resize', fixSafariHeight)
}
const searchClickFn = () => {
document.querySelector('#search-button > .search').addEventListener('click', openSearch)
}
const searchClickFnOnce = () => {
const searchFnOnce = () => {
document.querySelector('#local-search .search-close-button').addEventListener('click', closeSearch)
$searchMask.addEventListener('click', closeSearch)
if (GLOBAL_CONFIG.localSearch.preload) dataObj = fetchData(GLOBAL_CONFIG.localSearch.path)
}
// check url is json or not
const isJson = url => {
const reg = /\.json$/
return reg.test(url)
}
const fetchData = async (path) => {
let data = []
const response = await fetch(path)
if (isJson(path)) {
data = await response.json()
} else {
const res = await response.text()
const t = await new window.DOMParser().parseFromString(res, 'text/xml')
const a = await t
data = [...a.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()
if (GLOBAL_CONFIG.localSearch.preload) {
localSearch.fetchData()
}
return data
localSearch.highlightSearchWords(document.getElementById('article-container'))
}
const search = () => {
if (!GLOBAL_CONFIG.localSearch.preload) {
dataObj = fetchData(GLOBAL_CONFIG.localSearch.path)
}
const $input = document.querySelector('#local-search-input input')
const $resultContent = document.getElementById('local-search-results')
const $loadingStatus = document.getElementById('loading-status')
$input.addEventListener('input', function () {
const keywords = this.value.trim().toLowerCase().split(/[\s]+/)
if (keywords[0] !== '') $loadingStatus.innerHTML = '<i class="fas fa-spinner fa-pulse"></i>'
else {
$resultContent.innerHTML = ''
return
}
let str = '<div class="search-result-list">'
if (keywords.length <= 0) return
let count = 0
// 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 : GLOBAL_CONFIG.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 < 0 ? 0 : firstOccur - 30
// let end = firstOccur + 50 > dataContent.length ? dataContent.length : firstOccur + 50
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 => {
matchContent = matchContent.replaceAll(keyword, '<span class="search-keyword">' + keyword + '</span>')
dataTitle = dataTitle.replaceAll(keyword, '<span class="search-keyword">' + keyword + '</span>')
})
str += '<div class="local-search__hit-item"><a href="' + dataUrl + '"><span class="search-result-title">' + dataTitle + '</span>'
count += 1
if (dataContent !== '') {
str += '<p class="search-result">' + pre + matchContent + post + '</p>'
}
}
str += '</a></div>'
}
})
if (count === 0) {
str += '<div id="local-search__hits-empty">' + GLOBAL_CONFIG.localSearch.languages.hits_empty.replace(/\$\{query}/, this.value.trim()) +
'</div>'
}
str += '</div>'
$resultContent.innerHTML = str
if (keywords[0] !== '') $loadingStatus.innerHTML = ''
window.pjax && window.pjax.refresh($resultContent)
})
})
}
window.addEventListener('search:loaded', () => {
const $loadDataItem = document.getElementById('loading-database')
$loadDataItem.nextElementSibling.style.display = 'block'
$loadDataItem.remove()
})
searchClickFn()
searchClickFnOnce()
searchFnOnce()
// pjax
window.addEventListener('pjax:complete', () => {
!btf.isHidden($searchMask) && closeSearch()
localSearch.highlightSearchWords(document.getElementById('article-container'))
searchClickFn()
})
})

@ -80,30 +80,19 @@ const btf = {
const day = hour * 24
const month = day * 30
let result
if (more) {
const monthCount = dateDiff / month
const dayCount = dateDiff / day
const hourCount = dateDiff / hour
const minuteCount = dateDiff / minute
if (monthCount > 12) {
result = datePost.toISOString().slice(0, 10)
} else if (monthCount >= 1) {
result = parseInt(monthCount) + ' ' + GLOBAL_CONFIG.date_suffix.month
} else if (dayCount >= 1) {
result = parseInt(dayCount) + ' ' + GLOBAL_CONFIG.date_suffix.day
} else if (hourCount >= 1) {
result = parseInt(hourCount) + ' ' + GLOBAL_CONFIG.date_suffix.hour
} else if (minuteCount >= 1) {
result = parseInt(minuteCount) + ' ' + GLOBAL_CONFIG.date_suffix.min
} else {
result = GLOBAL_CONFIG.date_suffix.just
}
} else {
result = parseInt(dateDiff / day)
}
return result
if (!more) return parseInt(dateDiff / day)
const monthCount = dateDiff / month
const dayCount = dateDiff / day
const hourCount = dateDiff / hour
const minuteCount = dateDiff / minute
if (monthCount > 12) return datePost.toISOString().slice(0, 10)
if (monthCount >= 1) return parseInt(monthCount) + ' ' + GLOBAL_CONFIG.date_suffix.month
if (dayCount >= 1) return parseInt(dayCount) + ' ' + GLOBAL_CONFIG.date_suffix.day
if (hourCount >= 1) return parseInt(hourCount) + ' ' + GLOBAL_CONFIG.date_suffix.hour
if (minuteCount >= 1) return parseInt(minuteCount) + ' ' + GLOBAL_CONFIG.date_suffix.min
return GLOBAL_CONFIG.date_suffix.just
},
loadComment: (dom, callback) => {

Loading…
Cancel
Save