Merge branch 'dev' into patch-abcjs

This commit is contained in:
Jerry Wong 2023-04-03 18:53:40 +08:00 committed by GitHub
commit 09b7342882
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 425 additions and 227 deletions

View File

@ -56,11 +56,15 @@ algolia_search:
# Local search # Local search
local_search: local_search:
enable: false enable: false
# Preload the search data when the page loads.
preload: false 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: CDN:
# Docsearch # Docsearch
# https://docsearch.algolia.com/
docsearch: docsearch:
enable: false enable: false
appId: appId:
@ -777,7 +781,7 @@ rightside_item_order:
medium_zoom: false medium_zoom: false
# fancybox # fancybox
# http://fancyapps.com/fancybox/3/ # https://fancyapps.com/fancybox/
fancybox: true fancybox: true
# Tag Plugins settings (標籤外掛) # Tag Plugins settings (標籤外掛)

View File

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

View File

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

View File

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

View File

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

View File

@ -15,7 +15,7 @@
meta(charset='UTF-8') meta(charset='UTF-8')
meta(http-equiv="X-UA-Compatible" content="IE=edge") 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 title= tabTitle
meta(name="author" content=pageAuthor) meta(name="author" content=pageAuthor)
meta(name="copyright" content=pageCopyright) meta(name="copyright" content=pageCopyright)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -121,4 +121,8 @@ span.mathjax-overflow
transition: opacity .3s transition: opacity .3s
&.abcjs-container &.abcjs-container
opacity: 1 opacity: 1
+maxWidth768()
.fancybox__toolbar__column.is-middle
display: none

View File

@ -18,7 +18,7 @@
padding: 20px 5px padding: 20px 5px
+minWidth2000() +minWidth2000()
max-width: 1500px max-width: 1700px
& > div:first-child:not(.recent-posts) & > div:first-child:not(.recent-posts)
@extend .cardHover @extend .cardHover

View File

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

View File

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

View File

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

View File

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

View File

@ -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', () => { 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 loadFlag = false
let dataObj = []
const $searchMask = document.getElementById('search-mask') 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 openSearch = () => {
const bodyStyle = document.body.style const bodyStyle = document.body.style
bodyStyle.width = '100%' bodyStyle.width = '100%'
bodyStyle.overflow = 'hidden' bodyStyle.overflow = 'hidden'
btf.animateIn($searchMask, 'to_show 0.5s') btf.animateIn($searchMask, 'to_show 0.5s')
btf.animateIn(document.querySelector('#local-search .search-dialog'), 'titleScale 0.5s') btf.animateIn($searchDialog, 'titleScale 0.5s')
setTimeout(() => { document.querySelector('#local-search-input input').focus() }, 100) setTimeout(() => { input.focus() }, 300)
if (!loadFlag) { if (!loadFlag) {
search() !localSearch.isfetched && localSearch.fetchData()
input.addEventListener('input', inputEventFunction)
loadFlag = true loadFlag = true
} }
// shortcut: ESC // shortcut: ESC
@ -21,170 +315,46 @@ window.addEventListener('load', () => {
document.removeEventListener('keydown', f) document.removeEventListener('keydown', f)
} }
}) })
fixSafariHeight()
window.addEventListener('resize', fixSafariHeight)
} }
const closeSearch = () => { const closeSearch = () => {
const bodyStyle = document.body.style const bodyStyle = document.body.style
bodyStyle.width = '' bodyStyle.width = ''
bodyStyle.overflow = '' 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') btf.animateOut($searchMask, 'to_hide 0.5s')
window.removeEventListener('resize', fixSafariHeight)
} }
const searchClickFn = () => { const searchClickFn = () => {
document.querySelector('#search-button > .search').addEventListener('click', openSearch) document.querySelector('#search-button > .search').addEventListener('click', openSearch)
} }
const searchClickFnOnce = () => { const searchFnOnce = () => {
document.querySelector('#local-search .search-close-button').addEventListener('click', closeSearch) document.querySelector('#local-search .search-close-button').addEventListener('click', closeSearch)
$searchMask.addEventListener('click', closeSearch) $searchMask.addEventListener('click', closeSearch)
if (GLOBAL_CONFIG.localSearch.preload) dataObj = fetchData(GLOBAL_CONFIG.localSearch.path) if (GLOBAL_CONFIG.localSearch.preload) {
} localSearch.fetchData()
// 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) { localSearch.highlightSearchWords(document.getElementById('article-container'))
const $loadDataItem = document.getElementById('loading-database')
$loadDataItem.nextElementSibling.style.display = 'block'
$loadDataItem.remove()
}
return data
} }
const search = () => { window.addEventListener('search:loaded', () => {
if (!GLOBAL_CONFIG.localSearch.preload) { const $loadDataItem = document.getElementById('loading-database')
dataObj = fetchData(GLOBAL_CONFIG.localSearch.path) $loadDataItem.nextElementSibling.style.display = 'block'
} $loadDataItem.remove()
})
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)
})
})
}
searchClickFn() searchClickFn()
searchClickFnOnce() searchFnOnce()
// pjax // pjax
window.addEventListener('pjax:complete', () => { window.addEventListener('pjax:complete', () => {
!btf.isHidden($searchMask) && closeSearch() !btf.isHidden($searchMask) && closeSearch()
localSearch.highlightSearchWords(document.getElementById('article-container'))
searchClickFn() searchClickFn()
}) })
}) })

View File

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