mirror of
https://github.com/jerryc127/hexo-theme-butterfly.git
synced 2026-04-10 13:07:06 +08:00
🔍 搜索功能改進:
- 新增本地搜索分頁配置 (enablePagination, hitsPerPage) - 重構 Algolia 搜索邏輯,支援多索引和更好的錯誤處理 - 優化搜索 UI 樣式,包括分頁按鈕和響應式設計 - 改進搜索結果顯示,新增編號和更好的高亮處理 📦 依賴項更新: - 更新 plugins.yml 中的多個插件版本 (abcjs, algolia, aplayer 等) - 更新 package.json 版本號為 5.5.0 🎨 UI/UX 優化: - 改進側邊欄和目錄的動畫效果 - 優化樣式佈局,調整寬度百分比 - 新增說說頁面的分頁導航組件 - 改進右側邊欄按鈕樣式 🐛 錯誤處理和代碼優化: - 修復 Umami Analytics 的錯誤處理和數據驗證 - 改進懶加載圖片的正則表達式,避免匹配腳本標籤 - 移除未使用的變數和改進代碼結構 - 新增說說內容的 Markdown 渲染支援 🔧 其他改進: - 更新翻譯功能,移除箭頭函數語法以提升相容性
This commit is contained in:
@@ -488,6 +488,11 @@ search:
|
|||||||
top_n_per_article: 1
|
top_n_per_article: 1
|
||||||
# Unescape html strings to the readable one.
|
# Unescape html strings to the readable one.
|
||||||
unescape: false
|
unescape: false
|
||||||
|
# Enable pagination for search results
|
||||||
|
pagination:
|
||||||
|
enable: false
|
||||||
|
# Number of search results per page
|
||||||
|
hitsPerPage: 8
|
||||||
CDN:
|
CDN:
|
||||||
|
|
||||||
# Docsearch
|
# Docsearch
|
||||||
@@ -1097,7 +1102,6 @@ CDN:
|
|||||||
# gitalk_css:
|
# gitalk_css:
|
||||||
# giscus:
|
# giscus:
|
||||||
# instantpage:
|
# instantpage:
|
||||||
# instantsearch:
|
|
||||||
# katex:
|
# katex:
|
||||||
# katex_copytex:
|
# katex_copytex:
|
||||||
# lazyload:
|
# lazyload:
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ search:
|
|||||||
pagination:
|
pagination:
|
||||||
prev: Previous
|
prev: Previous
|
||||||
next: Next
|
next: Next
|
||||||
|
page_info: 'Page ${current} of ${total}'
|
||||||
|
|
||||||
comment: Comments
|
comment: Comments
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ search:
|
|||||||
pagination:
|
pagination:
|
||||||
prev: Previous
|
prev: Previous
|
||||||
next: Next
|
next: Next
|
||||||
|
page_info: 'Page ${current} of ${total}'
|
||||||
|
|
||||||
comment: Comments
|
comment: Comments
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ search:
|
|||||||
pagination:
|
pagination:
|
||||||
prev: 前へ
|
prev: 前へ
|
||||||
next: 次へ
|
next: 次へ
|
||||||
|
page_info: '${current} ページ / 合計 ${total} ページ'
|
||||||
|
|
||||||
comment: コメント
|
comment: コメント
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ search:
|
|||||||
pagination:
|
pagination:
|
||||||
prev: 이전
|
prev: 이전
|
||||||
next: 다음
|
next: 다음
|
||||||
|
page_info: '${current} 페이지 / 총 ${total} 페이지'
|
||||||
|
|
||||||
comment: 댓글
|
comment: 댓글
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ search:
|
|||||||
pagination:
|
pagination:
|
||||||
prev: 上一篇
|
prev: 上一篇
|
||||||
next: 下一篇
|
next: 下一篇
|
||||||
|
page_info: '第 ${current} 頁 / 共 ${total} 頁'
|
||||||
|
|
||||||
comment: 评论
|
comment: 评论
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ search:
|
|||||||
pagination:
|
pagination:
|
||||||
prev: 上一頁
|
prev: 上一頁
|
||||||
next: 下一頁
|
next: 下一頁
|
||||||
|
page_info: '第 ${current} 頁 / 共 ${total} 頁'
|
||||||
|
|
||||||
comment: 評論
|
comment: 評論
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ search:
|
|||||||
pagination:
|
pagination:
|
||||||
prev: 上一篇
|
prev: 上一篇
|
||||||
next: 下一篇
|
next: 下一篇
|
||||||
|
page_info: '第 ${current} 頁 / 共 ${total} 頁'
|
||||||
|
|
||||||
comment: 評論
|
comment: 評論
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
hitsPerPage: theme.search.algolia_search.hitsPerPage,
|
hitsPerPage: theme.search.algolia_search.hitsPerPage,
|
||||||
// search languages
|
// search languages
|
||||||
languages: {
|
languages: {
|
||||||
input_placeholder: theme.search.placeholder || _p("search.input_placeholder"),
|
|
||||||
hits_empty: _p("search.algolia_search.hits_empty"),
|
hits_empty: _p("search.algolia_search.hits_empty"),
|
||||||
hits_stats: _p("search.algolia_search.hits_stats"),
|
hits_stats: _p("search.algolia_search.hits_stats"),
|
||||||
}
|
}
|
||||||
@@ -19,12 +18,16 @@
|
|||||||
|
|
||||||
let localSearch = 'undefined'
|
let localSearch = 'undefined'
|
||||||
if (theme.search.use === 'local_search') {
|
if (theme.search.use === 'local_search') {
|
||||||
const { CDN, preload, top_n_per_article, unescape } = theme.search.local_search
|
const { CDN, preload, top_n_per_article, pagination, unescape } = theme.search.local_search
|
||||||
localSearch = JSON.stringify({
|
localSearch = JSON.stringify({
|
||||||
path: CDN || config.root + config.search.path,
|
path: CDN || config.root + config.search.path,
|
||||||
preload,
|
preload,
|
||||||
top_n_per_article,
|
top_n_per_article,
|
||||||
unescape,
|
unescape,
|
||||||
|
pagination: {
|
||||||
|
enable: pagination.enable,
|
||||||
|
hitsPerPage: pagination.hitsPerPage
|
||||||
|
},
|
||||||
languages: {
|
languages: {
|
||||||
// search languages
|
// search languages
|
||||||
hits_empty: _p("search.local_search.hits_empty"),
|
hits_empty: _p("search.local_search.hits_empty"),
|
||||||
|
|||||||
@@ -9,40 +9,43 @@
|
|||||||
- page.toc = false
|
- page.toc = false
|
||||||
|
|
||||||
#article-container
|
#article-container
|
||||||
if page.comments !== false && theme.comments.use
|
if page.shuoshuo_url || (site.data.shuoshuo && site.data.shuoshuo.length)
|
||||||
- commentsJsLoad = true
|
if page.comments !== false && theme.comments.use
|
||||||
|
- commentsJsLoad = true
|
||||||
|
|
||||||
script.
|
script.
|
||||||
(() => {
|
(() => {
|
||||||
const commentDiv = `!{partial('includes/third-party/comments/index', {}, {cache: true})}`
|
const commentDiv = `!{partial('includes/third-party/comments/index', {}, {cache: true})}`
|
||||||
|
|
||||||
const runDestroy = (shuoshuoComment) => {
|
const runDestroy = (shuoshuoComment) => {
|
||||||
if (!shuoshuoComment) return
|
if (!shuoshuoComment) return
|
||||||
|
|
||||||
for (const [key, fn] of Object.entries(shuoshuoComment)) {
|
for (const [key, fn] of Object.entries(shuoshuoComment)) {
|
||||||
if (key.startsWith('destroy')) fn()
|
if (key.startsWith('destroy')) fn()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
window.addCommentToShuoshuo = e => {
|
window.addCommentToShuoshuo = e => {
|
||||||
const btn = e.target.closest('.shuoshuo-comment-btn')
|
const btn = e.target.closest('.shuoshuo-comment-btn')
|
||||||
if (!btn) return
|
if (!btn) return
|
||||||
|
|
||||||
const ele = btn.closest('.container').nextElementSibling
|
const ele = btn.closest('.container').nextElementSibling
|
||||||
const { shuoshuoComment } = window
|
const { shuoshuoComment } = window
|
||||||
const isInclude = ele.classList.contains('no-comment')
|
const isInclude = ele.classList.contains('no-comment')
|
||||||
runDestroy(shuoshuoComment)
|
runDestroy(shuoshuoComment)
|
||||||
if (isInclude) {
|
if (isInclude) {
|
||||||
ele.classList.remove('no-comment')
|
ele.classList.remove('no-comment')
|
||||||
ele.innerHTML = commentDiv
|
ele.innerHTML = commentDiv
|
||||||
const key = `${location.pathname.replace(/\/$/, '')}?key=${ele.getAttribute('data-key')}`
|
const key = `${location.pathname.replace(/\/$/, '')}?key=${ele.getAttribute('data-key')}`
|
||||||
btf.switchComments(ele, key)
|
btf.switchComments(ele, key)
|
||||||
shuoshuoComment.loadComment && shuoshuoComment.loadComment(ele, key)
|
shuoshuoComment.loadComment && shuoshuoComment.loadComment(ele, key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})()
|
||||||
})()
|
|
||||||
|
|
||||||
|
- const localDate = page.shuoshuo_url ? [] : shuoshuoFN(site.data.shuoshuo, page)
|
||||||
|
|
||||||
if page.shuoshuo_url
|
|
||||||
script.
|
script.
|
||||||
(() => {
|
(() => {
|
||||||
const limitConfig = !{ JSON.stringify(page.limit || {}) }
|
const limitConfig = !{ JSON.stringify(page.limit || {}) }
|
||||||
@@ -78,111 +81,217 @@
|
|||||||
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
|
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let currentPage = 1
|
||||||
|
const itemsPerPage = 8
|
||||||
|
let totalPages = 0
|
||||||
|
let data = []
|
||||||
|
let inputEventsAttached = false // Flag to mark if input event listeners have been added
|
||||||
|
|
||||||
|
const renderData = (dataSlice) => {
|
||||||
|
const content = dataSlice.map(item => {
|
||||||
|
const formattedDate = formatToTimeZone(item.date)
|
||||||
|
const tags = item.tags && item.tags.map(tag => `<span class="shuoshuo-tag">${tag}</span>`).join('') || ''
|
||||||
|
const commentButton = item.key && !{commentsJsLoad}
|
||||||
|
? `<div class="shuoshuo-comment-btn" onclick="addCommentToShuoshuo(event)">
|
||||||
|
<i class="fa-solid fa-comments"></i>
|
||||||
|
</div>`
|
||||||
|
: ''
|
||||||
|
const commentContainer = item.key
|
||||||
|
? `<div class="shuoshuo-comment no-comment" data-key="${item.key}"></div>`
|
||||||
|
: ''
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="shuoshuo-item">
|
||||||
|
<div class="container">
|
||||||
|
<div class="shuoshuo-item-header">
|
||||||
|
<div class="shuoshuo-avatar">
|
||||||
|
<img class="no-lightbox" src="${item.avatar || '!{url_for(theme.avatar.img)}'}">
|
||||||
|
</div>
|
||||||
|
<div class="shuoshuo-info">
|
||||||
|
<div class="shuoshuo-author">${item.author || '!{config.author}'}</div>
|
||||||
|
<time class="shuoshuo-date" title="${formattedDate}">
|
||||||
|
${btf.diffDate(formattedDate, true)}
|
||||||
|
</time>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="shuoshuo-content">${item.content}</div>
|
||||||
|
<div class="shuoshuo-footer ${tags ? 'flex-between' : 'flex-end'}">
|
||||||
|
${tags ? `<div class="shuoshuo-tags">${tags}</div>` : ''}
|
||||||
|
${commentButton}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${commentContainer}
|
||||||
|
</div>`
|
||||||
|
}).join('')
|
||||||
|
|
||||||
|
const container = document.getElementById('article-container')
|
||||||
|
container.innerHTML = content
|
||||||
|
|
||||||
|
window.lazyLoadInstance && window.lazyLoadInstance.update()
|
||||||
|
btf.loadLightbox(document.querySelectorAll('#article-container img:not(.no-lightbox)'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderNavigation = () => {
|
||||||
|
const container = document.getElementById('article-container')
|
||||||
|
const existingNav = container.nextElementSibling
|
||||||
|
if (existingNav && existingNav.classList.contains('shuoshuo-navigation')) {
|
||||||
|
existingNav.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageInfoTemplate = '#{__('pagination.page_info')}'
|
||||||
|
const pageInfoText = pageInfoTemplate
|
||||||
|
.replace(/\$\{current}/g, currentPage)
|
||||||
|
.replace(/\$\{total}/g, totalPages)
|
||||||
|
|
||||||
|
const navHtml = `
|
||||||
|
<div class="shuoshuo-navigation">
|
||||||
|
<button onclick="window.shuoshuoPrevPage()" ${currentPage === 1 ? 'disabled' : ''}><i class="fa-solid fa-chevron-left"></i></button>
|
||||||
|
<span class="shuoshuo-page-info">${pageInfoText}</span>
|
||||||
|
<input type="number" class="shuoshuo-page-input" min="1" max="${totalPages}" placeholder="${currentPage}" onkeydown="window.shuoshuoHandleKeyDown(event)">
|
||||||
|
<button onclick="window.shuoshuoNextPage()" ${currentPage === totalPages ? 'disabled' : ''}><i class="fa-solid fa-chevron-right"></i></button>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
container.insertAdjacentHTML('afterend', navHtml)
|
||||||
|
|
||||||
|
// Add input validation event listeners (only once)
|
||||||
|
if (!inputEventsAttached) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const input = document.querySelector('.shuoshuo-page-input')
|
||||||
|
if (input) {
|
||||||
|
// Clear placeholder when clicking the input box
|
||||||
|
input.addEventListener('focus', (event) => {
|
||||||
|
event.target.placeholder = ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// Restore placeholder if no content when losing focus
|
||||||
|
input.addEventListener('blur', (event) => {
|
||||||
|
if (!event.target.value.trim()) {
|
||||||
|
event.target.placeholder = currentPage
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
input.addEventListener('input', (event) => {
|
||||||
|
const value = parseInt(event.target.value) || 0
|
||||||
|
let wasInvalid = false
|
||||||
|
|
||||||
|
if (value > totalPages) {
|
||||||
|
event.target.value = totalPages
|
||||||
|
wasInvalid = true
|
||||||
|
} else if (value < 1 && event.target.value !== '') {
|
||||||
|
event.target.value = 1
|
||||||
|
wasInvalid = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If value is corrected, show red and shake effect
|
||||||
|
if (wasInvalid) {
|
||||||
|
event.target.classList.add('invalid')
|
||||||
|
setTimeout(() => {
|
||||||
|
event.target.classList.remove('invalid')
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
inputEventsAttached = true // Mark that event listeners have been added
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderPage = (page) => {
|
||||||
|
const start = (page - 1) * itemsPerPage
|
||||||
|
const end = start + itemsPerPage
|
||||||
|
const pageData = data.slice(start, end)
|
||||||
|
renderData(pageData)
|
||||||
|
renderNavigation()
|
||||||
|
}
|
||||||
|
|
||||||
|
window.shuoshuoPrevPage = () => {
|
||||||
|
if (currentPage > 1) {
|
||||||
|
currentPage--
|
||||||
|
renderPage(currentPage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.shuoshuoNextPage = () => {
|
||||||
|
if (currentPage < totalPages) {
|
||||||
|
currentPage++
|
||||||
|
renderPage(currentPage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.shuoshuoGoToPage = (page) => {
|
||||||
|
if (typeof page === 'number') {
|
||||||
|
// Directly jump to the specified page
|
||||||
|
if (page >= 1 && page <= totalPages && page !== currentPage) {
|
||||||
|
currentPage = page
|
||||||
|
renderPage(currentPage)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Get page from input box
|
||||||
|
const input = document.querySelector('.shuoshuo-page-input')
|
||||||
|
const inputValue = input.value.trim()
|
||||||
|
const inputPage = inputValue === '' ? currentPage : parseInt(inputValue)
|
||||||
|
if (inputPage >= 1 && inputPage <= totalPages && inputPage !== currentPage) {
|
||||||
|
currentPage = inputPage
|
||||||
|
renderPage(currentPage)
|
||||||
|
} else if (inputValue === '') {
|
||||||
|
// If input box is empty, re-render current page (update placeholder)
|
||||||
|
renderPage(currentPage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.shuoshuoHandleKeyDown = (event) => {
|
||||||
|
const input = event.target
|
||||||
|
const value = input.value + event.key
|
||||||
|
|
||||||
|
// Allow delete, arrow keys, backspace, etc.
|
||||||
|
if (event.key === 'Enter' || event.key === 'Backspace' || event.key === 'Delete' ||
|
||||||
|
event.key === 'ArrowLeft' || event.key === 'ArrowRight' ||
|
||||||
|
event.key === 'Tab' || event.ctrlKey || event.metaKey) {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
window.shuoshuoGoToPage()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow numbers
|
||||||
|
if (!/^\d$/.test(event.key)) {
|
||||||
|
event.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the value after input exceeds the range
|
||||||
|
const newValue = parseInt(value) || 0
|
||||||
|
if (newValue > totalPages || (value.length > 1 && newValue === 0)) {
|
||||||
|
event.preventDefault()
|
||||||
|
// Add red and shake effect
|
||||||
|
input.classList.add('invalid')
|
||||||
|
setTimeout(() => {
|
||||||
|
input.classList.remove('invalid')
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const loadShuoshuo = async () => {
|
const loadShuoshuo = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('!{url_for(page.shuoshuo_url)}')
|
let originData = []
|
||||||
let data = await response.json()
|
if (!{Boolean(page.shuoshuo_url)}) {
|
||||||
|
const response = await fetch('!{url_for(page.shuoshuo_url)}')
|
||||||
data = filterDataByLimit(sortDataByDate(data), limitConfig)
|
originData = await response.json()
|
||||||
|
} else {
|
||||||
const container = document.getElementById('article-container')
|
originData = !{JSON.stringify(localDate)}
|
||||||
let start = 0
|
|
||||||
|
|
||||||
const renderData = (dataSlice) => {
|
|
||||||
const content = dataSlice.map(item => {
|
|
||||||
const formattedDate = formatToTimeZone(item.date)
|
|
||||||
const tags = item.tags && item.tags.map(tag => `<span class="shuoshuo-tag">${tag}</span>`).join('') || ''
|
|
||||||
const commentButton = item.key && !{commentsJsLoad}
|
|
||||||
? `<div class="shuoshuo-comment-btn" onclick="addCommentToShuoshuo(event)">
|
|
||||||
<i class="fa-solid fa-comments"></i>
|
|
||||||
</div>`
|
|
||||||
: ''
|
|
||||||
const commentContainer = item.key
|
|
||||||
? `<div class="shuoshuo-comment no-comment" data-key="${item.key}"></div>`
|
|
||||||
: ''
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="shuoshuo-item">
|
|
||||||
<div class="container">
|
|
||||||
<div class="shuoshuo-item-header">
|
|
||||||
<div class="shuoshuo-avatar">
|
|
||||||
<img class="no-lightbox" src="${item.avatar || '!{url_for(theme.avatar.img)}'}">
|
|
||||||
</div>
|
|
||||||
<div class="shuoshuo-info">
|
|
||||||
<div class="shuoshuo-author">${item.author || '!{config.author}'}</div>
|
|
||||||
<time class="shuoshuo-date" title="${formattedDate}">
|
|
||||||
${btf.diffDate(formattedDate, true)}
|
|
||||||
</time>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="shuoshuo-content">${item.content}</div>
|
|
||||||
<div class="shuoshuo-footer ${tags ? 'flex-between' : 'flex-end'}">
|
|
||||||
${tags ? `<div class="shuoshuo-tags">${tags}</div>` : ''}
|
|
||||||
${commentButton}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
${commentContainer}
|
|
||||||
</div>`
|
|
||||||
}).join('')
|
|
||||||
|
|
||||||
container.insertAdjacentHTML('beforeend', content)
|
|
||||||
|
|
||||||
window.lazyLoadInstance.update()
|
|
||||||
btf.loadLightbox(document.querySelectorAll('#article-container img:not(.no-lightbox)'))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleIntersection = (entries) => {
|
data = filterDataByLimit(sortDataByDate(originData), limitConfig)
|
||||||
if (!entries[0].isIntersecting) return
|
|
||||||
observer.unobserve(entries[0].target)
|
|
||||||
|
|
||||||
const slice = data.slice(start, start + 10)
|
totalPages = Math.ceil(data.length / itemsPerPage)
|
||||||
renderData(slice)
|
|
||||||
start += 10
|
|
||||||
|
|
||||||
if (start < data.length) {
|
renderPage(currentPage)
|
||||||
setTimeout(() => observer.observe(container.lastElementChild), 100)
|
|
||||||
} else {
|
|
||||||
observer.disconnect()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver(handleIntersection, {
|
|
||||||
root: null,
|
|
||||||
rootMargin: '0px',
|
|
||||||
threshold: 1.0
|
|
||||||
})
|
|
||||||
|
|
||||||
renderData(data.slice(start, 10))
|
|
||||||
start += 10
|
|
||||||
|
|
||||||
if (container.lastElementChild) observer.observe(container.lastElementChild)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.pjax ? loadShuoshuo() : window.addEventListener('load', loadShuoshuo)
|
window.pjax ? loadShuoshuo() : window.addEventListener('load', loadShuoshuo)
|
||||||
})()
|
})()
|
||||||
else
|
|
||||||
if site.data.shuoshuo
|
|
||||||
each i in shuoshuoFN(site.data.shuoshuo, page)
|
|
||||||
.shuoshuo-item
|
|
||||||
.container
|
|
||||||
.shuoshuo-item-header
|
|
||||||
.shuoshuo-avatar
|
|
||||||
img.no-lightbox(src=i.avatar || url_for(theme.avatar.img))
|
|
||||||
.shuoshuo-info
|
|
||||||
.shuoshuo-author=i.author || config.author
|
|
||||||
time.shuoshuo-date(title=i.date)=i.date
|
|
||||||
.shuoshuo-content
|
|
||||||
!=markdown(i.content)
|
|
||||||
.shuoshuo-footer(class=i.tags && i.tags.length ? 'flex-between' : 'flex-end')
|
|
||||||
if i.tags
|
|
||||||
.shuoshuo-tags
|
|
||||||
each tag in i.tags
|
|
||||||
span.shuoshuo-tag=tag
|
|
||||||
if i.key && commentsJsLoad
|
|
||||||
.shuoshuo-comment-btn(onclick='addCommentToShuoshuo(event)')
|
|
||||||
i.fa-solid.fa-comments
|
|
||||||
if i.key && commentsJsLoad
|
|
||||||
.shuoshuo-comment.no-comment(data-key=i.key)
|
|
||||||
33
layout/includes/third-party/math/mathjax.pug
vendored
33
layout/includes/third-party/math/mathjax.pug
vendored
@@ -1,19 +1,50 @@
|
|||||||
//- Mathjax 3
|
//- Mathjax 4/5
|
||||||
- const { tags, enableMenu } = theme.math.mathjax
|
- const { tags, enableMenu } = theme.math.mathjax
|
||||||
script.
|
script.
|
||||||
(() => {
|
(() => {
|
||||||
const loadMathjax = () => {
|
const loadMathjax = () => {
|
||||||
if (!window.MathJax) {
|
if (!window.MathJax) {
|
||||||
window.MathJax = {
|
window.MathJax = {
|
||||||
|
loader: {
|
||||||
|
load: [
|
||||||
|
// Four font extension packages (optional)
|
||||||
|
//- '[tex]/bbm',
|
||||||
|
//- '[tex]/bboldx',
|
||||||
|
//- '[tex]/dsfont',
|
||||||
|
'[tex]/mhchem'
|
||||||
|
],
|
||||||
|
paths: {
|
||||||
|
'mathjax-newcm': '[mathjax]/../@mathjax/mathjax-newcm-font',
|
||||||
|
|
||||||
|
//- // Four font extension packages (optional)
|
||||||
|
//- 'mathjax-bbm-extension': '[mathjax]/../@mathjax/mathjax-bbm-font-extension',
|
||||||
|
//- 'mathjax-bboldx-extension': '[mathjax]/../@mathjax/mathjax-bboldx-font-extension',
|
||||||
|
//- 'mathjax-dsfont-extension': '[mathjax]/../@mathjax/mathjax-dsfont-font-extension',
|
||||||
|
'mathjax-mhchem-extension': '[mathjax]/../@mathjax/mathjax-mhchem-font-extension'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
font: 'mathjax-newcm',
|
||||||
|
},
|
||||||
tex: {
|
tex: {
|
||||||
inlineMath: [['$', '$'], ['\\(', '\\)']],
|
inlineMath: [['$', '$'], ['\\(', '\\)']],
|
||||||
tags: '!{tags}',
|
tags: '!{tags}',
|
||||||
|
packages: {
|
||||||
|
'[+]': [
|
||||||
|
'mhchem'
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
chtml: {
|
chtml: {
|
||||||
scale: 1.1
|
scale: 1.1
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
enableMenu: !{enableMenu},
|
enableMenu: !{enableMenu},
|
||||||
|
menuOptions: {
|
||||||
|
settings: {
|
||||||
|
enrich: false // Turn off Braille and voice narration text automatic generation
|
||||||
|
}
|
||||||
|
},
|
||||||
renderActions: {
|
renderActions: {
|
||||||
findScript: [10, doc => {
|
findScript: [10, doc => {
|
||||||
for (const node of document.querySelectorAll('script[type^="math/tex"]')) {
|
for (const node of document.querySelectorAll('script[type^="math/tex"]')) {
|
||||||
|
|||||||
24
layout/includes/third-party/search/algolia.pug
vendored
24
layout/includes/third-party/search/algolia.pug
vendored
@@ -2,21 +2,33 @@
|
|||||||
.search-dialog
|
.search-dialog
|
||||||
nav.search-nav
|
nav.search-nav
|
||||||
span.search-dialog-title= _p('search.title')
|
span.search-dialog-title= _p('search.title')
|
||||||
|
i.fas.fa-spinner.fa-pulse#loading-status(hidden)
|
||||||
button.search-close-button
|
button.search-close-button
|
||||||
i.fas.fa-times
|
i.fas.fa-times
|
||||||
|
|
||||||
.search-wrap
|
#algolia-search-input
|
||||||
#algolia-search-input
|
.ais-SearchBox
|
||||||
|
form.ais-SearchBox-form(action="" role="search" novalidate="")
|
||||||
|
input.ais-SearchBox-input(type="search" placeholder=theme.search.placeholder || _p("search.input_placeholder") autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" maxlength="512" aria-label="Search")
|
||||||
|
button.ais-SearchBox-submit(type="submit" title="Submit the search query" style="display:none;")
|
||||||
|
svg.ais-SearchBox-submitIcon(width="10" height="10" viewBox="0 0 40 40" aria-hidden="true")
|
||||||
|
path(d="M26.804 29.01c-2.832 2.34-6.465 3.746-10.426 3.746C7.333 32.756 0 25.424 0 16.378 0 7.333 7.333 0 16.378 0c9.046 0 16.378 7.333 16.378 16.378 0 3.96-1.406 7.594-3.746 10.426l10.534 10.534c.607.607.61 1.59-.004 2.202-.61.61-1.597.61-2.202.004L26.804 29.01zm-10.426.627c7.323 0 13.26-5.936 13.26-13.26 0-7.32-5.937-13.257-13.26-13.257C9.056 3.12 3.12 9.056 3.12 16.378c0 7.323 5.936 13.26 13.258 13.26z")
|
||||||
hr
|
hr
|
||||||
#algolia-search-results
|
#algolia-search-results
|
||||||
#algolia-hits
|
#algolia-hits
|
||||||
#algolia-pagination
|
#algolia-hits-empty(style="display:none;")
|
||||||
|
.ais-Hits(style="display:none;")
|
||||||
|
ol.ais-Hits-list
|
||||||
|
#algolia-pagination.ais-Pagination(style="display:none;")
|
||||||
|
ul.ais-Pagination-list
|
||||||
#algolia-info
|
#algolia-info
|
||||||
.algolia-stats
|
span.ais-Stats-text
|
||||||
.algolia-poweredBy
|
a.algolia-poweredBy(href="https://www.algolia.com/?utm_source=algoliasearch.js&utm_medium=website&utm_content=localhost&utm_campaign=poweredby" target="_blank" aria-label="Search by Algolia" rel="noopener noreferrer")
|
||||||
|
svg.ais-PoweredBy-logo(height="1.2em" viewBox="0 0 572 64" style="width: auto;")
|
||||||
|
path(fill="#36395A" d="M16 48.3c-3.4 0-6.3-.6-8.7-1.7A12.4 12.4 0 0 1 1.9 42C.6 40 0 38 0 35.4h6.5a6.7 6.7 0 0 0 3.9 6c1.4.7 3.3 1.1 5.6 1.1 2.2 0 4-.3 5.4-1a7 7 0 0 0 3-2.4 6 6 0 0 0 1-3.4c0-1.5-.6-2.8-1.9-3.7-1.3-1-3.3-1.6-5.9-1.8l-4-.4c-3.7-.3-6.6-1.4-8.8-3.4a10 10 0 0 1-3.3-7.9c0-2.4.6-4.6 1.8-6.4a12 12 0 0 1 5-4.3c2.2-1 4.7-1.6 7.5-1.6s5.5.5 7.6 1.6a12 12 0 0 1 5 4.4c1.2 1.8 1.8 4 1.8 6.7h-6.5a6.4 6.4 0 0 0-3.5-5.9c-1-.6-2.6-1-4.4-1s-3.2.3-4.4 1c-1.1.6-2 1.4-2.6 2.4-.5 1-.8 2-.8 3.1a5 5 0 0 0 1.5 3.6c1 1 2.6 1.7 4.7 1.9l4 .3c2.8.2 5.2.8 7.2 1.8 2.1 1 3.7 2.2 4.9 3.8a9.7 9.7 0 0 1 1.7 5.8c0 2.5-.7 4.7-2 6.6a13 13 0 0 1-5.6 4.4c-2.4 1-5.2 1.6-8.4 1.6Zm35.6 0c-2.6 0-4.8-.4-6.7-1.3a13 13 0 0 1-4.7-3.5 17.1 17.1 0 0 1-3.6-10.4v-1c0-2 .3-3.8 1-5.6a13 13 0 0 1 7.3-8.3 15 15 0 0 1 6.3-1.4A13.2 13.2 0 0 1 64 24.3c1 2.2 1.6 4.6 1.6 7.2V34H39.4v-4.3h21.8l-1.8 2.2c0-2-.3-3.7-.9-5.1a7.3 7.3 0 0 0-2.7-3.4c-1.2-.7-2.7-1.1-4.6-1.1s-3.4.4-4.7 1.3a8 8 0 0 0-2.9 3.6c-.6 1.5-.9 3.3-.9 5.4 0 2 .3 3.7 1 5.3a7.9 7.9 0 0 0 2.8 3.7c1.3.8 3 1.3 5 1.3s3.8-.5 5.1-1.3c1.3-1 2.1-2 2.4-3.2h6a11.8 11.8 0 0 1-7 8.7 16 16 0 0 1-6.4 1.2ZM80 48c-2.2 0-4-.3-5.7-1a8.4 8.4 0 0 1-3.7-3.3 9.7 9.7 0 0 1-1.3-5.2c0-2 .5-3.8 1.5-5.2a9 9 0 0 1 4.3-3.1c1.8-.7 4-1 6.7-1H89v4.1h-7.5c-2 0-3.4.5-4.4 1.4-1 1-1.6 2.1-1.6 3.6s.5 2.7 1.6 3.6c1 1 2.5 1.4 4.4 1.4 1.1 0 2.2-.2 3.2-.7 1-.4 1.9-1 2.6-2 .6-1 1-2.4 1-4.2l1.7 2.1c-.2 2-.7 3.8-1.5 5.2a9 9 0 0 1-3.4 3.3 12 12 0 0 1-5.3 1Zm9.5-.7v-8.8h-1v-10c0-1.8-.5-3.2-1.4-4.1-1-1-2.4-1.4-4.2-1.4a142.9 142.9 0 0 0-10.2.4v-5.6a74.8 74.8 0 0 1 8.6-.4c3 0 5.5.4 7.5 1.2s3.4 2 4.4 3.6c1 1.7 1.4 4 1.4 6.7v18.4h-5Zm12.9 0V17.8h5v12.3h-.2c0-4.2 1-7.4 2.8-9.5a11 11 0 0 1 8.3-3.1h1v5.6h-2a9 9 0 0 0-6.3 2.2c-1.5 1.5-2.2 3.6-2.2 6.4v15.6h-6.4Zm34.4 1a15 15 0 0 1-6.6-1.3c-1.9-.9-3.4-2-4.7-3.5a15.5 15.5 0 0 1-2.7-5c-.6-1.7-1-3.6-1-5.4v-1c0-2 .4-3.8 1-5.6a15 15 0 0 1 2.8-4.9c1.3-1.5 2.8-2.6 4.6-3.5a16.4 16.4 0 0 1 13.3.2c2 1 3.5 2.3 4.8 4a12 12 0 0 1 2 6H144c-.2-1.6-1-3-2.2-4.1a7.5 7.5 0 0 0-5.2-1.7 8 8 0 0 0-4.7 1.3 8 8 0 0 0-2.8 3.6 13.8 13.8 0 0 0 0 10.3c.6 1.5 1.5 2.7 2.8 3.6s2.8 1.3 4.8 1.3c1.5 0 2.7-.2 3.8-.8a7 7 0 0 0 2.6-2c.7-1 1-2 1.2-3.2h6.2a11 11 0 0 1-2 6.2 15.1 15.1 0 0 1-11.8 5.5Zm19.7-1v-40h6.4V31h-1.3c0-3 .4-5.5 1.1-7.6a9.7 9.7 0 0 1 3.5-4.8A9.9 9.9 0 0 1 172 17h.3c3.5 0 6 1.1 7.9 3.5 1.7 2.3 2.6 5.7 2.6 10v16.8h-6.4V29.6c0-2.1-.6-3.8-1.8-5a6.4 6.4 0 0 0-4.8-1.8c-2 0-3.7.7-5 2a7.8 7.8 0 0 0-1.9 5.5v17h-6.4Zm63.8 1a12.2 12.2 0 0 1-10.9-6.2 19 19 0 0 1-1.8-7.3h1.4v12.5h-5.1v-40h6.4v19.8l-2 3.5c.2-3.1.8-5.7 1.9-7.7a11 11 0 0 1 4.4-4.5c1.8-1 3.9-1.5 6.1-1.5a13.4 13.4 0 0 1 12.8 9.1c.7 1.9 1 3.8 1 6v1c0 2.2-.3 4.1-1 6a13.6 13.6 0 0 1-13.2 9.4Zm-1.2-5.5a8.4 8.4 0 0 0 7.9-5c.7-1.5 1.1-3.3 1.1-5.3s-.4-3.8-1.1-5.3a8.7 8.7 0 0 0-3.2-3.6 9.6 9.6 0 0 0-9.2-.2 8.5 8.5 0 0 0-3.3 3.2c-.8 1.4-1.3 3-1.3 5v2.3a9 9 0 0 0 1.3 4.8 9 9 0 0 0 3.4 3c1.4.7 2.8 1 4.4 1Zm27.3 3.9-10-28.9h6.5l9.5 28.9h-6Zm-7.5 12.2v-5.7h4.9c1 0 2-.1 2.9-.4a4 4 0 0 0 2-1.4c.4-.7.9-1.6 1.2-2.7l8.6-30.9h6.2l-9.3 32.4a14 14 0 0 1-2.5 5 8.9 8.9 0 0 1-4 2.8c-1.5.6-3.4.9-5.6.9h-4.4Zm9-12.2v-5.2h6.4v5.2H248Z")
|
||||||
|
path(fill="#003DFF" d="M534.4 9.1H528a.8.8 0 0 1-.7-.7V1.8c0-.4.2-.7.6-.8l6.5-1c.4 0 .8.2.9.6v7.8c0 .4-.4.7-.8.7zM428 35.2V.8c0-.5-.3-.8-.7-.8h-.2l-6.4 1c-.4 0-.7.4-.7.8v35c0 1.6 0 11.8 12.3 12.2.5 0 .8-.4.8-.8V43c0-.4-.3-.7-.6-.8-4.5-.5-4.5-6-4.5-7zm106.5-21.8H528c-.4 0-.7.4-.7.8v34c0 .4.3.8.7.8h6.5c.4 0 .8-.4.8-.8v-34c0-.5-.4-.8-.8-.8zm-17.7 21.8V.8c0-.5-.3-.8-.8-.8l-6.5 1c-.4 0-.7.4-.7.8v35c0 1.6 0 11.8 12.3 12.2.4 0 .8-.4.8-.8V43c0-.4-.3-.7-.7-.8-4.4-.5-4.4-6-4.4-7zm-22.2-20.6a16.5 16.5 0 0 1 8.6 9.3c.8 2.2 1.3 4.8 1.3 7.5a19.4 19.4 0 0 1-4.6 12.6 14.8 14.8 0 0 1-5.2 3.6c-2 .9-5.2 1.4-6.8 1.4a21 21 0 0 1-6.7-1.4 15.4 15.4 0 0 1-8.6-9.3 21.3 21.3 0 0 1 0-14.4 15.2 15.2 0 0 1 8.6-9.3c2-.8 4.3-1.2 6.7-1.2s4.6.4 6.7 1.2zm-6.7 27.6c2.7 0 4.7-1 6.2-3s2.2-4.3 2.2-7.8-.7-6.3-2.2-8.3-3.5-3-6.2-3-4.7 1-6.1 3c-1.5 2-2.2 4.8-2.2 8.3s.7 5.8 2.2 7.8 3.5 3 6.2 3zm-88.8-28.8c-6.2 0-11.7 3.3-14.8 8.2a18.6 18.6 0 0 0 4.8 25.2c1.8 1.2 4 1.8 6.2 1.7s.1 0 .1 0h.9c4.2-.7 8-4 9.1-8.1v7.4c0 .4.3.7.8.7h6.4a.7.7 0 0 0 .7-.7V14.2c0-.5-.3-.8-.7-.8h-13.5zm6.3 26.5a9.8 9.8 0 0 1-5.7 2h-.5a10 10 0 0 1-9.2-14c1.4-3.7 5-6.3 9-6.3h6.4v18.3zm152.3-26.5h13.5c.5 0 .8.3.8.7v33.7c0 .4-.3.7-.8.7h-6.4a.7.7 0 0 1-.8-.7v-7.4c-1.2 4-4.8 7.4-9 8h-.1a4.2 4.2 0 0 1-.5.1h-.9a10.3 10.3 0 0 1-7-2.6c-4-3.3-6.5-8.4-6.5-14.2 0-3.7 1-7.2 3-10 3-5 8.5-8.3 14.7-8.3zm.6 28.4c2.2-.1 4.2-.6 5.7-2V21.7h-6.3a9.8 9.8 0 0 0-9 6.4 10.2 10.2 0 0 0 9.1 13.9h.5zM452.8 13.4c-6.2 0-11.7 3.3-14.8 8.2a18.5 18.5 0 0 0 3.6 24.3 10.4 10.4 0 0 0 13 .6c2.2-1.5 3.8-3.7 4.5-6.1v7.8c0 2.8-.8 5-2.2 6.3-1.5 1.5-4 2.2-7.5 2.2l-6-.3c-.3 0-.7.2-.8.5l-1.6 5.5c-.1.4.1.8.5 1h.1c2.8.4 5.5.6 7 .6 6.3 0 11-1.4 14-4.1 2.7-2.5 4.2-6.3 4.5-11.4V14.2c0-.5-.4-.8-.8-.8h-13.5zm6.3 8.2v18.3a9.6 9.6 0 0 1-5.6 2h-1a10.3 10.3 0 0 1-8.8-14c1.4-3.7 5-6.3 9-6.3h6.4zM291 31.5A32 32 0 0 1 322.8 0h30.8c.6 0 1.2.5 1.2 1.2v61.5c0 1.1-1.3 1.7-2.2 1l-19.2-17a18 18 0 0 1-11 3.4 18.1 18.1 0 1 1 18.2-14.8c-.1.4-.5.7-.9.6-.1 0-.3 0-.4-.2l-3.8-3.4c-.4-.3-.6-.8-.7-1.4a12 12 0 1 0-2.4 8.3c.4-.4 1-.5 1.6-.2l14.7 13.1v-46H323a26 26 0 1 0 10 49.7c.8-.4 1.6-.2 2.3.3l3 2.7c.3.2.3.7 0 1l-.2.2a32 32 0 0 1-47.2-28.6z")
|
||||||
|
|
||||||
#search-mask
|
#search-mask
|
||||||
|
|
||||||
script(src=url_for(theme.asset.algolia_search))
|
script(src=url_for(theme.asset.algolia_search))
|
||||||
script(src=url_for(theme.asset.instantsearch))
|
|
||||||
script(src=url_for(theme.asset.algolia_js))
|
script(src=url_for(theme.asset.algolia_js))
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
.search-dialog
|
.search-dialog
|
||||||
nav.search-nav
|
nav.search-nav
|
||||||
span.search-dialog-title= _p('search.title')
|
span.search-dialog-title= _p('search.title')
|
||||||
span#loading-status
|
i.fas.fa-spinner.fa-pulse#loading-status(hidden)
|
||||||
button.search-close-button
|
button.search-close-button
|
||||||
i.fas.fa-times
|
i.fas.fa-times
|
||||||
|
|
||||||
@@ -10,13 +10,15 @@
|
|||||||
i.fas.fa-spinner.fa-pulse
|
i.fas.fa-spinner.fa-pulse
|
||||||
span= ' ' + _p("search.load_data")
|
span= ' ' + _p("search.load_data")
|
||||||
|
|
||||||
.search-wrap
|
.local-search-input
|
||||||
#local-search-input
|
input(placeholder=theme.search.placeholder || _p("search.input_placeholder") type="text")
|
||||||
.local-search-box
|
|
||||||
input(placeholder=theme.search.placeholder || _p("search.input_placeholder") type="text").local-search-box--input
|
|
||||||
hr
|
|
||||||
#local-search-results
|
|
||||||
#local-search-stats-wrap
|
|
||||||
#search-mask
|
|
||||||
|
|
||||||
|
hr
|
||||||
|
|
||||||
|
#local-search-results
|
||||||
|
#local-search-pagination.ais-Pagination(style="display:none;")
|
||||||
|
ul.ais-Pagination-list
|
||||||
|
#local-search-stats
|
||||||
|
|
||||||
|
#search-mask
|
||||||
script(src=url_for(theme.asset.local_search))
|
script(src=url_for(theme.asset.local_search))
|
||||||
97
layout/includes/third-party/umami_analytics.pug
vendored
97
layout/includes/third-party/umami_analytics.pug
vendored
@@ -9,7 +9,11 @@ script.
|
|||||||
const config = !{JSON.stringify(UV_PV)}
|
const config = !{JSON.stringify(UV_PV)}
|
||||||
|
|
||||||
const runTrack = () => {
|
const runTrack = () => {
|
||||||
umami.track(props => ({ ...props, url: window.location.pathname, title: GLOBAL_CONFIG_SITE.title }))
|
if (typeof umami !== 'undefined' && typeof umami.track === 'function') {
|
||||||
|
umami.track(props => ({ ...props, url: window.location.pathname, title: GLOBAL_CONFIG_SITE.title }))
|
||||||
|
} else {
|
||||||
|
console.warn('Umami Analytics: umami.track is not available')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadUmamiJS = () => {
|
const loadUmamiJS = () => {
|
||||||
@@ -17,20 +21,39 @@ script.
|
|||||||
'data-website-id': '!{website_id}',
|
'data-website-id': '!{website_id}',
|
||||||
'data-auto-track': 'false',
|
'data-auto-track': 'false',
|
||||||
...option
|
...option
|
||||||
}).then(runTrack)
|
}).then(() => {
|
||||||
|
runTrack()
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Umami Analytics: Error loading script', error)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getData = async (isPost) => {
|
const getData = async (isPost) => {
|
||||||
const now = Date.now()
|
try {
|
||||||
const keyUrl = isPost ? `&url=${window.location.pathname}` : ''
|
const now = Date.now()
|
||||||
const headerList = { 'Accept': 'application/json' }
|
const keyUrl = isPost ? `&url=${window.location.pathname}` : ''
|
||||||
if (!{isServerURL}) headerList['Authorization'] = `Bearer ${config.token}`
|
const headerList = { 'Accept': 'application/json' }
|
||||||
else headerList['x-umami-api-key'] = config.token
|
|
||||||
const res = await fetch(`!{apiUrl}/websites/!{website_id}/stats?startAt=0000000000&endAt=${now}${keyUrl}`, {
|
if (!{isServerURL}) {
|
||||||
method: "GET",
|
headerList['Authorization'] = `Bearer ${config.token}`
|
||||||
headers: headerList
|
} else {
|
||||||
})
|
headerList['x-umami-api-key'] = config.token
|
||||||
return await res.json()
|
}
|
||||||
|
|
||||||
|
const res = await fetch(`!{apiUrl}/websites/!{website_id}/stats?startAt=0000000000&endAt=${now}${keyUrl}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: headerList
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${res.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await res.json()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Umami Analytics: Failed to fetch data', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const insertData = async () => {
|
const insertData = async () => {
|
||||||
@@ -39,27 +62,49 @@ script.
|
|||||||
const pagePV = document.getElementById('umamiPV')
|
const pagePV = document.getElementById('umamiPV')
|
||||||
if (pagePV) {
|
if (pagePV) {
|
||||||
const data = await getData(true)
|
const data = await getData(true)
|
||||||
pagePV.textContent = data.pageviews.value
|
if (data && data.pageviews && typeof data.pageviews.value !== 'undefined') {
|
||||||
}
|
pagePV.textContent = data.pageviews.value
|
||||||
} else {
|
} else {
|
||||||
const data = (config.site_uv || config.site_pv) && await getData()
|
console.warn('Umami Analytics: Invalid page view data received')
|
||||||
if (config.site_uv) {
|
}
|
||||||
const siteUV = document.getElementById('umami-site-uv')
|
|
||||||
if (siteUV) siteUV.textContent = data.visitors.value
|
|
||||||
}
|
|
||||||
if (config.site_pv) {
|
|
||||||
const sitePV = document.getElementById('umami-site-pv')
|
|
||||||
if (sitePV) sitePV.textContent = data.pageviews.value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to load Umami Analytics:', e)
|
if (config.site_uv || config.site_pv) {
|
||||||
|
const data = await getData(false)
|
||||||
|
|
||||||
|
if (config.site_uv) {
|
||||||
|
const siteUV = document.getElementById('umami-site-uv')
|
||||||
|
if (siteUV && data && data.visitors && typeof data.visitors.value !== 'undefined') {
|
||||||
|
siteUV.textContent = data.visitors.value
|
||||||
|
} else if (siteUV) {
|
||||||
|
console.warn('Umami Analytics: Invalid site UV data received')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.site_pv) {
|
||||||
|
const sitePV = document.getElementById('umami-site-pv')
|
||||||
|
if (sitePV && data && data.pageviews && typeof data.pageviews.value !== 'undefined') {
|
||||||
|
sitePV.textContent = data.pageviews.value
|
||||||
|
} else if (sitePV) {
|
||||||
|
console.warn('Umami Analytics: Invalid site PV data received')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Umami Analytics: Failed to insert data', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
btf.addGlobalFn('pjaxComplete', runTrack, 'umami_analytics_run_track')
|
btf.addGlobalFn('pjaxComplete', runTrack, 'umami_analytics_run_track')
|
||||||
btf.addGlobalFn('pjaxComplete', insertData, 'umami_analytics_insert')
|
btf.addGlobalFn('pjaxComplete', insertData, 'umami_analytics_insert')
|
||||||
|
|
||||||
|
|
||||||
loadUmamiJS()
|
loadUmamiJS()
|
||||||
insertData()
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', insertData)
|
||||||
|
} else {
|
||||||
|
setTimeout(insertData, 100)
|
||||||
|
}
|
||||||
})()
|
})()
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hexo-theme-butterfly",
|
"name": "hexo-theme-butterfly",
|
||||||
"version": "5.5.0-b1",
|
"version": "5.5.0",
|
||||||
"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": {
|
||||||
|
|||||||
40
plugins.yml
40
plugins.yml
@@ -1,15 +1,15 @@
|
|||||||
abcjs_basic_js:
|
abcjs_basic_js:
|
||||||
name: abcjs
|
name: abcjs
|
||||||
file: dist/abcjs-basic-min.js
|
file: dist/abcjs-basic-min.js
|
||||||
version: 6.5.1
|
version: 6.5.2
|
||||||
activate_power_mode:
|
activate_power_mode:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
file: dist/activate-power-mode.min.js
|
file: dist/activate-power-mode.min.js
|
||||||
version: 1.1.4
|
version: 1.1.6
|
||||||
algolia_search:
|
algolia_search:
|
||||||
name: algoliasearch
|
name: algoliasearch
|
||||||
file: dist/lite/builds/browser.umd.js
|
file: dist/lite/builds/browser.umd.js
|
||||||
version: 5.34.1
|
version: 5.37.0
|
||||||
aplayer_css:
|
aplayer_css:
|
||||||
name: aplayer
|
name: aplayer
|
||||||
file: dist/APlayer.min.css
|
file: dist/APlayer.min.css
|
||||||
@@ -33,15 +33,15 @@ blueimp_md5:
|
|||||||
canvas_fluttering_ribbon:
|
canvas_fluttering_ribbon:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
file: dist/canvas-fluttering-ribbon.min.js
|
file: dist/canvas-fluttering-ribbon.min.js
|
||||||
version: 1.1.4
|
version: 1.1.6
|
||||||
canvas_nest:
|
canvas_nest:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
file: dist/canvas-nest.min.js
|
file: dist/canvas-nest.min.js
|
||||||
version: 1.1.4
|
version: 1.1.6
|
||||||
canvas_ribbon:
|
canvas_ribbon:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
file: dist/canvas-ribbon.min.js
|
file: dist/canvas-ribbon.min.js
|
||||||
version: 1.1.4
|
version: 1.1.6
|
||||||
chartjs:
|
chartjs:
|
||||||
name: chart.js
|
name: chart.js
|
||||||
file: dist/chart.umd.js
|
file: dist/chart.umd.js
|
||||||
@@ -49,11 +49,11 @@ chartjs:
|
|||||||
clickShowText:
|
clickShowText:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
file: dist/click-show-text.min.js
|
file: dist/click-show-text.min.js
|
||||||
version: 1.1.4
|
version: 1.1.6
|
||||||
click_heart:
|
click_heart:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
file: dist/click-heart.min.js
|
file: dist/click-heart.min.js
|
||||||
version: 1.1.4
|
version: 1.1.6
|
||||||
disqusjs:
|
disqusjs:
|
||||||
name: disqusjs
|
name: disqusjs
|
||||||
file: dist/browser/disqusjs.es2015.umd.min.js
|
file: dist/browser/disqusjs.es2015.umd.min.js
|
||||||
@@ -80,22 +80,22 @@ egjs_infinitegrid:
|
|||||||
fancybox:
|
fancybox:
|
||||||
name: '@fancyapps/ui'
|
name: '@fancyapps/ui'
|
||||||
file: dist/fancybox/fancybox.umd.js
|
file: dist/fancybox/fancybox.umd.js
|
||||||
version: 6.0.17
|
version: 6.0.29
|
||||||
other_name: fancyapps-ui
|
other_name: fancyapps-ui
|
||||||
fancybox_css:
|
fancybox_css:
|
||||||
name: '@fancyapps/ui'
|
name: '@fancyapps/ui'
|
||||||
file: dist/fancybox/fancybox.css
|
file: dist/fancybox/fancybox.css
|
||||||
version: 6.0.17
|
version: 6.0.29
|
||||||
other_name: fancyapps-ui
|
other_name: fancyapps-ui
|
||||||
fireworks:
|
fireworks:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
file: dist/fireworks.min.js
|
file: dist/fireworks.min.js
|
||||||
version: 1.1.4
|
version: 1.1.6
|
||||||
fontawesome:
|
fontawesome:
|
||||||
name: '@fortawesome/fontawesome-free'
|
name: '@fortawesome/fontawesome-free'
|
||||||
file: css/all.min.css
|
file: css/all.min.css
|
||||||
other_name: font-awesome
|
other_name: font-awesome
|
||||||
version: 6.7.2
|
version: 7.0.1
|
||||||
gitalk:
|
gitalk:
|
||||||
name: gitalk
|
name: gitalk
|
||||||
file: dist/gitalk.min.js
|
file: dist/gitalk.min.js
|
||||||
@@ -108,10 +108,6 @@ instantpage:
|
|||||||
name: instant.page
|
name: instant.page
|
||||||
file: instantpage.js
|
file: instantpage.js
|
||||||
version: 5.2.0
|
version: 5.2.0
|
||||||
instantsearch:
|
|
||||||
name: instantsearch.js
|
|
||||||
file: dist/instantsearch.production.min.js
|
|
||||||
version: 4.79.2
|
|
||||||
katex:
|
katex:
|
||||||
name: katex
|
name: katex
|
||||||
file: dist/katex.min.css
|
file: dist/katex.min.css
|
||||||
@@ -128,8 +124,8 @@ lazyload:
|
|||||||
version: 19.1.3
|
version: 19.1.3
|
||||||
mathjax:
|
mathjax:
|
||||||
name: mathjax
|
name: mathjax
|
||||||
file: es5/tex-mml-chtml.js
|
file: tex-mml-chtml.js
|
||||||
version: 3.2.2
|
version: 4.0.0
|
||||||
medium_zoom:
|
medium_zoom:
|
||||||
name: medium-zoom
|
name: medium-zoom
|
||||||
file: dist/medium-zoom.min.js
|
file: dist/medium-zoom.min.js
|
||||||
@@ -137,11 +133,11 @@ medium_zoom:
|
|||||||
mermaid:
|
mermaid:
|
||||||
name: mermaid
|
name: mermaid
|
||||||
file: dist/mermaid.min.js
|
file: dist/mermaid.min.js
|
||||||
version: 11.9.0
|
version: 11.11.0
|
||||||
meting_js:
|
meting_js:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
file: metingjs/dist/Meting.min.js
|
file: metingjs/dist/Meting.min.js
|
||||||
version: 1.1.4
|
version: 1.1.6
|
||||||
pace_default_css:
|
pace_default_css:
|
||||||
name: pace-js
|
name: pace-js
|
||||||
other_name: pace
|
other_name: pace
|
||||||
@@ -174,11 +170,11 @@ prismjs_lineNumber_js:
|
|||||||
sharejs:
|
sharejs:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
file: sharejs/dist/js/social-share.min.js
|
file: sharejs/dist/js/social-share.min.js
|
||||||
version: 1.1.4
|
version: 1.1.6
|
||||||
sharejs_css:
|
sharejs_css:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
file: sharejs/dist/css/share.min.css
|
file: sharejs/dist/css/share.min.css
|
||||||
version: 1.1.4
|
version: 1.1.6
|
||||||
snackbar:
|
snackbar:
|
||||||
name: node-snackbar
|
name: node-snackbar
|
||||||
file: dist/snackbar.min.js
|
file: dist/snackbar.min.js
|
||||||
|
|||||||
@@ -274,6 +274,10 @@ module.exports = {
|
|||||||
preload: false,
|
preload: false,
|
||||||
top_n_per_article: 1,
|
top_n_per_article: 1,
|
||||||
unescape: false,
|
unescape: false,
|
||||||
|
pagination: {
|
||||||
|
enable: false,
|
||||||
|
hitsPerPage: 8
|
||||||
|
},
|
||||||
CDN: null
|
CDN: null
|
||||||
},
|
},
|
||||||
docsearch: {
|
docsearch: {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
const { deepMerge } = require('hexo-util')
|
const { deepMerge } = require('hexo-util')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const fs = require('fs')
|
|
||||||
|
|
||||||
// Cache default config to avoid repeated file reads
|
// Cache default config to avoid repeated file reads
|
||||||
let cachedDefaultConfig = null
|
let cachedDefaultConfig = null
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Butterfly
|
* Butterfly
|
||||||
* lazyload
|
* Lazyload filter
|
||||||
* replace src to data-lazy-src
|
* Replace src with data-lazy-src for lazy loading
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict'
|
'use strict'
|
||||||
@@ -10,11 +10,28 @@ const urlFor = require('hexo-util').url_for.bind(hexo)
|
|||||||
|
|
||||||
const lazyload = htmlContent => {
|
const lazyload = htmlContent => {
|
||||||
if (hexo.theme.config.lazyload.native) {
|
if (hexo.theme.config.lazyload.native) {
|
||||||
return htmlContent.replace(/(<img.*?)(>)/ig, '$1 loading=\'lazy\'$2')
|
// Use more precise replacement: only replace img tags in HTML, not content inside script tags
|
||||||
|
return htmlContent.replace(/(<img(?![^>]*?\bloading=)(?:\s[^>]*?)?>)(?![^<]*<\/script>)/gi, match => {
|
||||||
|
return match.replace(/>$/, ' loading=\'lazy\'>')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const bg = hexo.theme.config.lazyload.placeholder ? urlFor(hexo.theme.config.lazyload.placeholder) : 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
|
const bg = hexo.theme.config.lazyload.placeholder ? urlFor(hexo.theme.config.lazyload.placeholder) : 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
|
||||||
return htmlContent.replace(/(<img.*? src=)/ig, `$1 "${bg}" data-lazy-src=`)
|
|
||||||
|
// Use more precise replacement: handle src attributes with double and single quotes, but avoid replacing content inside script tags
|
||||||
|
let result = htmlContent
|
||||||
|
|
||||||
|
// Handle src attributes with double quotes
|
||||||
|
result = result.replace(/(<img(?![^>]*?\bdata-lazy-src=)(?:\s[^>]*?)?\ssrc="([^"]+)")(?![^<]*<\/script>)/gi, (match, tag, src) => {
|
||||||
|
return tag.replace(`src="${src}"`, `src="${bg}" data-lazy-src="${src}"`)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle src attributes with single quotes
|
||||||
|
result = result.replace(/(<img(?![^>]*?\bdata-lazy-src=)(?:\s[^>]*?)?\ssrc='([^']+)')(?![^<]*<\/script>)/gi, (match, tag, src) => {
|
||||||
|
return tag.replace(`src='${src}'`, `src='${bg}' data-lazy-src='${src}'`)
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
hexo.extend.filter.register('after_render:html', data => {
|
hexo.extend.filter.register('after_render:html', data => {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ hexo.extend.helper.register('urlNoIndex', function (url = null, trailingIndex =
|
|||||||
})
|
})
|
||||||
|
|
||||||
hexo.extend.helper.register('md5', function (path) {
|
hexo.extend.helper.register('md5', function (path) {
|
||||||
return crypto.createHash('md5').update(decodeURI(this.url_for(path, {relative: false}))).digest('hex')
|
return crypto.createHash('md5').update(decodeURI(this.url_for(path, { relative: false }))).digest('hex')
|
||||||
})
|
})
|
||||||
|
|
||||||
hexo.extend.helper.register('injectHtml', data => {
|
hexo.extend.helper.register('injectHtml', data => {
|
||||||
@@ -70,7 +70,7 @@ hexo.extend.helper.register('findArchivesTitle', function (page, menu, date) {
|
|||||||
const defaultTitle = this._p('page.archives')
|
const defaultTitle = this._p('page.archives')
|
||||||
if (!menu) return defaultTitle
|
if (!menu) return defaultTitle
|
||||||
|
|
||||||
const loop = (m) => {
|
const loop = m => {
|
||||||
for (const key in m) {
|
for (const key in m) {
|
||||||
if (typeof m[key] === 'object') {
|
if (typeof m[key] === 'object') {
|
||||||
const result = loop(m[key])
|
const result = loop(m[key])
|
||||||
@@ -86,7 +86,7 @@ hexo.extend.helper.register('findArchivesTitle', function (page, menu, date) {
|
|||||||
return loop(menu) || defaultTitle
|
return loop(menu) || defaultTitle
|
||||||
})
|
})
|
||||||
|
|
||||||
hexo.extend.helper.register('getBgPath', function(path) {
|
hexo.extend.helper.register('getBgPath', function (path) {
|
||||||
if (!path) return ''
|
if (!path) return ''
|
||||||
|
|
||||||
const absoluteUrlPattern = /^(?:[a-z][a-z\d+.-]*:)?\/\//i
|
const absoluteUrlPattern = /^(?:[a-z][a-z\d+.-]*:)?\/\//i
|
||||||
@@ -132,6 +132,8 @@ hexo.extend.helper.register('shuoshuoFN', (data, page) => {
|
|||||||
finalResult.forEach(item => {
|
finalResult.forEach(item => {
|
||||||
const utcDate = moment.utc(item.date).format('YYYY-MM-DD HH:mm:ss')
|
const utcDate = moment.utc(item.date).format('YYYY-MM-DD HH:mm:ss')
|
||||||
item.date = moment.tz(utcDate, hexo.config.timezone).format('YYYY-MM-DD HH:mm:ss')
|
item.date = moment.tz(utcDate, hexo.config.timezone).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
// markdown
|
||||||
|
item.content = hexo.render.renderSync({ text: item.content, engine: 'markdown' })
|
||||||
})
|
})
|
||||||
|
|
||||||
return finalResult
|
return finalResult
|
||||||
|
|||||||
@@ -181,6 +181,11 @@
|
|||||||
.card-category-list
|
.card-category-list
|
||||||
&.child
|
&.child
|
||||||
padding: 0 0 0 16px
|
padding: 0 0 0 16px
|
||||||
|
overflow: hidden
|
||||||
|
max-height: 0
|
||||||
|
opacity: 0
|
||||||
|
visibility: hidden
|
||||||
|
transition: max-height .3s ease, opacity .3s ease
|
||||||
|
|
||||||
> .parent
|
> .parent
|
||||||
> a
|
> a
|
||||||
@@ -189,7 +194,9 @@
|
|||||||
transform: rotate(-90deg)
|
transform: rotate(-90deg)
|
||||||
|
|
||||||
& + .child
|
& + .child
|
||||||
display: block
|
max-height: 1000px
|
||||||
|
opacity: 1
|
||||||
|
visibility: visible
|
||||||
|
|
||||||
.card-category-list
|
.card-category-list
|
||||||
&-name
|
&-name
|
||||||
@@ -208,7 +215,9 @@
|
|||||||
|
|
||||||
if hexo-config('aside.card_categories.expand') == false
|
if hexo-config('aside.card_categories.expand') == false
|
||||||
> .child
|
> .child
|
||||||
display: none
|
max-height: 0
|
||||||
|
opacity: 0
|
||||||
|
visibility: hidden
|
||||||
|
|
||||||
.card-webinfo
|
.card-webinfo
|
||||||
.webinfo
|
.webinfo
|
||||||
|
|||||||
@@ -38,9 +38,13 @@
|
|||||||
font-size: 16px
|
font-size: 16px
|
||||||
line-height: w
|
line-height: w
|
||||||
addBorderRadius(5)
|
addBorderRadius(5)
|
||||||
|
@extend .btn-effects
|
||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
background-color: var(--btn-hover-color)
|
background-color: var(--btn-hover-color)
|
||||||
|
|
||||||
|
i
|
||||||
|
vertical-align: baseline
|
||||||
|
|
||||||
#mobile-toc-button
|
#mobile-toc-button
|
||||||
display: none
|
display: none
|
||||||
|
|||||||
@@ -52,7 +52,10 @@
|
|||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
background: var(--text-bg-hover)
|
background: var(--text-bg-hover)
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, .1)
|
||||||
color: var(--white)
|
color: var(--white)
|
||||||
|
transition: all .2s ease
|
||||||
|
transform: translateX(3px)
|
||||||
|
|
||||||
i:first-child
|
i:first-child
|
||||||
width: 15%
|
width: 15%
|
||||||
@@ -70,9 +73,25 @@
|
|||||||
transform: rotate(90deg)
|
transform: rotate(90deg)
|
||||||
|
|
||||||
& + .menus_item_child
|
& + .menus_item_child
|
||||||
display: none
|
overflow: hidden
|
||||||
|
max-height: 0
|
||||||
|
opacity: 0
|
||||||
|
transform: scaleY(0)
|
||||||
|
transform-origin: top
|
||||||
|
|
||||||
.menus_item_child
|
.menus_item_child
|
||||||
margin: 0
|
margin: 0
|
||||||
padding-left: 25px
|
padding-left: 25px
|
||||||
list-style: none
|
max-height: 0
|
||||||
|
list-style: none
|
||||||
|
opacity: 0
|
||||||
|
transition: transform .3s ease, opacity .3s ease, max-height .3s ease
|
||||||
|
transform: scaleY(0)
|
||||||
|
transform-origin: top
|
||||||
|
will-change: transform, opacity, max-height
|
||||||
|
|
||||||
|
// 當父元素沒有 .hide 類時,顯示子目錄
|
||||||
|
.site-page.group:not(.hide) + .menus_item_child
|
||||||
|
max-height: 1000px
|
||||||
|
opacity: 1
|
||||||
|
transform: scaleY(1)
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
padding: 20px 5px
|
padding: 20px 5px
|
||||||
|
|
||||||
+minWidth2000()
|
+minWidth2000()
|
||||||
max-width: 70%
|
max-width: 60%
|
||||||
|
|
||||||
& > div:first-child:not(.nc)
|
& > div:first-child:not(.nc)
|
||||||
@extend .cardHover
|
@extend .cardHover
|
||||||
|
|||||||
@@ -76,3 +76,110 @@
|
|||||||
|
|
||||||
&.no-comment
|
&.no-comment
|
||||||
display: none
|
display: none
|
||||||
|
|
||||||
|
.shuoshuo-navigation
|
||||||
|
display: flex
|
||||||
|
justify-content: center
|
||||||
|
align-items: center
|
||||||
|
margin-top: 20px
|
||||||
|
padding: 20px 0
|
||||||
|
|
||||||
|
button
|
||||||
|
display: flex
|
||||||
|
justify-content: center
|
||||||
|
align-items: center
|
||||||
|
width: 2.7em
|
||||||
|
height: 2.7em
|
||||||
|
background-color: var(--btn-bg)
|
||||||
|
color: var(--btn-color)
|
||||||
|
font-size: .9em
|
||||||
|
line-height: 2.5em
|
||||||
|
transition: all .2s ease-in-out
|
||||||
|
addBorderRadius(6)
|
||||||
|
|
||||||
|
&:not(:disabled)
|
||||||
|
@extend .btn-effects
|
||||||
|
|
||||||
|
&:hover:not(:disabled)
|
||||||
|
background-color: var(--btn-hover-color)
|
||||||
|
|
||||||
|
&:disabled
|
||||||
|
background: #f5f5f5
|
||||||
|
color: #ccc
|
||||||
|
opacity: .5
|
||||||
|
cursor: not-allowed
|
||||||
|
|
||||||
|
.shuoshuo-page-info
|
||||||
|
margin: 0 15px
|
||||||
|
color: #858585
|
||||||
|
white-space: nowrap
|
||||||
|
font-size: .9em
|
||||||
|
|
||||||
|
.shuoshuo-page-input
|
||||||
|
margin-right: 12px
|
||||||
|
padding: 0 15px
|
||||||
|
height: 2.7em
|
||||||
|
border: 1px solid var(--btn-bg)
|
||||||
|
background: var(--card-bg)
|
||||||
|
color: #858585
|
||||||
|
text-align: center
|
||||||
|
font-size: .9em
|
||||||
|
transition: all .2s ease-in-out
|
||||||
|
addBorderRadius(6)
|
||||||
|
|
||||||
|
&:focus
|
||||||
|
outline: none
|
||||||
|
border-width: 2px
|
||||||
|
|
||||||
|
&::placeholder
|
||||||
|
color: transparent
|
||||||
|
|
||||||
|
/* 隱藏 number 輸入框的上下箭頭 */
|
||||||
|
&::-webkit-outer-spin-button,
|
||||||
|
&::-webkit-inner-spin-button
|
||||||
|
margin: 0
|
||||||
|
-webkit-appearance: none
|
||||||
|
|
||||||
|
/* Firefox */
|
||||||
|
-moz-appearance: textfield
|
||||||
|
|
||||||
|
/* 當作為頁碼按鈕時的樣式 */
|
||||||
|
&.shuoshuo-page-num
|
||||||
|
min-width: 40px
|
||||||
|
width: 40px
|
||||||
|
border: none
|
||||||
|
background: $light-blue
|
||||||
|
color: var(--white)
|
||||||
|
font-weight: 500
|
||||||
|
cursor: text
|
||||||
|
|
||||||
|
&:focus
|
||||||
|
border: 1px solid $light-blue
|
||||||
|
background: var(--white)
|
||||||
|
color: #333
|
||||||
|
|
||||||
|
/* 超出範圍時的紅色樣式 */
|
||||||
|
&.invalid
|
||||||
|
border-color: #ff4757
|
||||||
|
background-color: #ffeaea
|
||||||
|
color: #ff4757
|
||||||
|
animation: shake .5s ease-in-out
|
||||||
|
|
||||||
|
/* 震動動畫 */
|
||||||
|
@keyframes shake
|
||||||
|
0%,
|
||||||
|
100%
|
||||||
|
transform: translateX(0)
|
||||||
|
|
||||||
|
10%,
|
||||||
|
30%,
|
||||||
|
50%,
|
||||||
|
70%,
|
||||||
|
90%
|
||||||
|
transform: translateX(-2px)
|
||||||
|
|
||||||
|
20%,
|
||||||
|
40%,
|
||||||
|
60%,
|
||||||
|
80%
|
||||||
|
transform: translateX(2px)
|
||||||
|
|||||||
@@ -1,93 +1,13 @@
|
|||||||
#algolia-search
|
#algolia-search
|
||||||
.search-dialog
|
.search-dialog
|
||||||
.ais-SearchBox
|
|
||||||
input
|
|
||||||
padding: 5px 14px
|
|
||||||
width: 100%
|
|
||||||
outline: none
|
|
||||||
border: 2px solid $search-color
|
|
||||||
border-radius: 40px
|
|
||||||
background: var(--search-bg)
|
|
||||||
color: var(--search-input-color)
|
|
||||||
|
|
||||||
.ais-SearchBox-loadingIndicator
|
|
||||||
position: absolute
|
|
||||||
top: 18px
|
|
||||||
left: 67px
|
|
||||||
|
|
||||||
.ais-Hits-list
|
.ais-Hits-list
|
||||||
margin: 0
|
+maxWidth768()
|
||||||
padding: 0
|
min-height: calc(var(--search-height) - 245px)
|
||||||
@extend .list-beauty
|
|
||||||
|
|
||||||
a
|
|
||||||
color: var(--search-a-color)
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
color: $search-color
|
|
||||||
|
|
||||||
mark
|
|
||||||
background: transparent
|
|
||||||
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: 15px 0 0
|
|
||||||
padding: 0
|
|
||||||
text-align: center
|
|
||||||
|
|
||||||
.ais-Pagination-list
|
|
||||||
margin: 0
|
|
||||||
padding: 0
|
|
||||||
list-style: none
|
|
||||||
|
|
||||||
.ais-Pagination-item
|
|
||||||
display: inline
|
|
||||||
margin: 0 4px
|
|
||||||
padding: 0
|
|
||||||
|
|
||||||
.ais-Pagination-link
|
|
||||||
display: inline-block
|
|
||||||
min-width: 24px
|
|
||||||
height: 24px
|
|
||||||
text-align: center
|
|
||||||
line-height: 24px
|
|
||||||
addBorderRadius()
|
|
||||||
|
|
||||||
.ais-Pagination-item--selected
|
|
||||||
a
|
|
||||||
background: $theme-paginator-color
|
|
||||||
color: #eee
|
|
||||||
cursor: default
|
|
||||||
|
|
||||||
.ais-Pagination-item--disabled
|
|
||||||
visibility: hidden
|
|
||||||
|
|
||||||
#algolia-hits
|
|
||||||
> div
|
|
||||||
overflow-y: overlay
|
|
||||||
margin: 0 -20px
|
|
||||||
padding: 0 22px
|
|
||||||
max-height: calc(80vh - 220px)
|
|
||||||
|
|
||||||
+maxWidth768()
|
|
||||||
max-height: none
|
|
||||||
height: calc(var(--search-height) - 235px)
|
|
||||||
|
|
||||||
#algolia-info
|
#algolia-info
|
||||||
div
|
|
||||||
display: inline
|
|
||||||
|
|
||||||
.algolia-poweredBy
|
.algolia-poweredBy
|
||||||
float: right
|
float: right
|
||||||
vertical-align: text-top
|
padding-top: 2px
|
||||||
|
|
||||||
svg
|
svg
|
||||||
height: 1.1em
|
height: 1.1em
|
||||||
@@ -20,22 +20,173 @@
|
|||||||
border-radius: 0
|
border-radius: 0
|
||||||
|
|
||||||
.search-nav
|
.search-nav
|
||||||
margin: 0 0 14px
|
display: flex
|
||||||
|
justify-content: space-between
|
||||||
|
align-items: center
|
||||||
|
margin-bottom: 14px
|
||||||
color: $search-color
|
color: $search-color
|
||||||
font-size: 1.4em
|
font-size: 1.4em
|
||||||
line-height: 1
|
line-height: 1
|
||||||
|
|
||||||
.search-dialog-title
|
.search-dialog-title
|
||||||
margin-right: 10px
|
margin-right: 4px
|
||||||
|
|
||||||
|
#loading-status
|
||||||
|
&[hidden]
|
||||||
|
display: none !important
|
||||||
|
|
||||||
.search-close-button
|
.search-close-button
|
||||||
float: right
|
flex: 1
|
||||||
color: $grey
|
color: $grey
|
||||||
transition: color .2s ease-in-out
|
text-align: right
|
||||||
|
transition: all .2s ease
|
||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
color: $search-color
|
color: $search-color
|
||||||
|
|
||||||
|
.local-search-input,
|
||||||
|
#algolia-search-input
|
||||||
|
margin: 0 auto
|
||||||
|
max-width: 100%
|
||||||
|
width: 100%
|
||||||
|
|
||||||
|
input,
|
||||||
|
.ais-SearchBox-input
|
||||||
|
padding: 5px 14px
|
||||||
|
width: 100%
|
||||||
|
outline: none
|
||||||
|
border: 2px solid $search-color
|
||||||
|
border-radius: 40px
|
||||||
|
background: var(--search-bg)
|
||||||
|
color: var(--search-input-color)
|
||||||
|
-webkit-appearance: none
|
||||||
|
|
||||||
|
&::placeholder
|
||||||
|
color: var(--text-color)
|
||||||
|
|
||||||
|
.search-result-list,
|
||||||
|
.ais-Hits-list
|
||||||
|
overflow-y: overlay
|
||||||
|
margin: 0 -20px
|
||||||
|
padding: 0 22px
|
||||||
|
max-height: calc(80vh - 220px)
|
||||||
|
|
||||||
|
.local-search-hit-item,
|
||||||
|
.ais-Hits-item
|
||||||
|
display: flex
|
||||||
|
align-items: flex-start
|
||||||
|
margin: 3px 0
|
||||||
|
line-height: 1.8
|
||||||
|
transition: all .2s ease-in-out
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
transform: translateY(-1px)
|
||||||
|
|
||||||
|
&:not([value])::before
|
||||||
|
display: none
|
||||||
|
|
||||||
|
&[value]::before
|
||||||
|
display: inline-flex
|
||||||
|
flex-shrink: 0
|
||||||
|
justify-content: center
|
||||||
|
align-items: center
|
||||||
|
margin-right: 6px
|
||||||
|
min-width: 24px
|
||||||
|
color: $search-color
|
||||||
|
content: attr(value) '.'
|
||||||
|
font-weight: bold
|
||||||
|
font-style: italic
|
||||||
|
font-size: .9em
|
||||||
|
|
||||||
|
&::marker
|
||||||
|
content: none
|
||||||
|
|
||||||
|
a
|
||||||
|
flex: 1
|
||||||
|
color: var(--search-a-color)
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
color: $search-color
|
||||||
|
|
||||||
|
.search-result-title,
|
||||||
|
.algolia-hits-item-title
|
||||||
|
font-weight: 600
|
||||||
|
|
||||||
|
.search-result,
|
||||||
|
.algolia-hit-item-content
|
||||||
|
margin: 0 0 8px
|
||||||
|
word-break: break-all
|
||||||
|
font-size: .9em
|
||||||
|
|
||||||
|
.ais-Pagination
|
||||||
|
margin: 15px 0 0
|
||||||
|
padding: 0
|
||||||
|
text-align: center
|
||||||
|
|
||||||
|
.ais-Pagination-list
|
||||||
|
display: flex
|
||||||
|
flex-wrap: wrap
|
||||||
|
justify-content: center
|
||||||
|
align-items: center
|
||||||
|
margin: 0
|
||||||
|
padding: 0
|
||||||
|
list-style: none
|
||||||
|
gap: 6px
|
||||||
|
|
||||||
|
.ais-Pagination-item
|
||||||
|
display: flex
|
||||||
|
padding: 0
|
||||||
|
|
||||||
|
&:not(.ais-Pagination-item--selected):not(.ais-Pagination-item--ellipsis):not(.ais-Pagination-item--disabled)
|
||||||
|
.ais-Pagination-link:hover
|
||||||
|
background: var(--btn-hover-color)
|
||||||
|
transform: translateY(-1px)
|
||||||
|
|
||||||
|
.ais-Pagination-link
|
||||||
|
display: inline-flex
|
||||||
|
justify-content: center
|
||||||
|
align-items: center
|
||||||
|
padding: 4px 8px
|
||||||
|
min-width: 28px
|
||||||
|
height: 28px
|
||||||
|
border-radius: 6px
|
||||||
|
background: var(--btn-bg)
|
||||||
|
color: var(--btn-color)
|
||||||
|
transition: all .2s ease
|
||||||
|
|
||||||
|
&.ais-Pagination-link--disabled
|
||||||
|
opacity: .3
|
||||||
|
cursor: not-allowed
|
||||||
|
|
||||||
|
i
|
||||||
|
font-size: 12px
|
||||||
|
|
||||||
|
.ais-Pagination-item--selected
|
||||||
|
.ais-Pagination-link
|
||||||
|
background: $theme-paginator-color
|
||||||
|
font-weight: 600
|
||||||
|
cursor: default
|
||||||
|
|
||||||
|
.ais-Pagination-item--ellipsis
|
||||||
|
.ais-Pagination-link
|
||||||
|
padding: 4px 2px
|
||||||
|
border: none
|
||||||
|
background: transparent
|
||||||
|
color: var(--text-color)
|
||||||
|
cursor: default
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background: transparent
|
||||||
|
transform: none
|
||||||
|
|
||||||
|
.ais-Pagination-item--disabled
|
||||||
|
.ais-Pagination-link
|
||||||
|
opacity: .4
|
||||||
|
|
||||||
|
+maxWidth768()
|
||||||
|
.ais-Pagination-list
|
||||||
|
gap: 4px
|
||||||
|
|
||||||
hr
|
hr
|
||||||
margin: 15px auto
|
margin: 15px auto
|
||||||
@extend .custom-hr
|
@extend .custom-hr
|
||||||
@@ -50,6 +201,38 @@
|
|||||||
display: none
|
display: none
|
||||||
background: rgba($dark-black, .6)
|
background: rgba($dark-black, .6)
|
||||||
|
|
||||||
|
.search-result-stats,
|
||||||
|
.ais-Stats-text
|
||||||
|
margin: 15px 0 0
|
||||||
|
color: var(--text-color)
|
||||||
|
text-align: center
|
||||||
|
font-size: .9em
|
||||||
|
|
||||||
|
.search-keyword
|
||||||
|
background: transparent
|
||||||
|
color: $search-keyword-highlight
|
||||||
|
font-weight: 600
|
||||||
|
|
||||||
|
.search-loading
|
||||||
|
display: flex
|
||||||
|
justify-content: center
|
||||||
|
align-items: center
|
||||||
|
padding: 20px
|
||||||
|
color: var(--text-color)
|
||||||
|
|
||||||
|
&::before
|
||||||
|
width: 16px
|
||||||
|
height: 16px
|
||||||
|
border: 2px solid var(--text-color)
|
||||||
|
border-top-color: transparent
|
||||||
|
border-radius: 50%
|
||||||
|
content: ''
|
||||||
|
animation: spin 1s linear infinite
|
||||||
|
|
||||||
|
@keyframes spin
|
||||||
|
to
|
||||||
|
transform: rotate(360deg)
|
||||||
|
|
||||||
if hexo-config('search.use') == 'algolia_search'
|
if hexo-config('search.use') == 'algolia_search'
|
||||||
@require 'algolia'
|
@require 'algolia'
|
||||||
else if hexo-config('search.use') == 'local_search'
|
else if hexo-config('search.use') == 'local_search'
|
||||||
|
|||||||
@@ -1,57 +1,18 @@
|
|||||||
#local-search
|
#local-search
|
||||||
.search-dialog
|
.search-dialog
|
||||||
.local-search-box
|
|
||||||
margin: 0 auto
|
|
||||||
max-width: 100%
|
|
||||||
width: 100%
|
|
||||||
|
|
||||||
input
|
|
||||||
padding: 5px 14px
|
|
||||||
width: 100%
|
|
||||||
outline: none
|
|
||||||
border: 2px solid $search-color
|
|
||||||
border-radius: 40px
|
|
||||||
background: var(--search-bg)
|
|
||||||
color: var(--search-input-color)
|
|
||||||
-webkit-appearance: none
|
|
||||||
|
|
||||||
.search-wrap
|
|
||||||
display: none
|
|
||||||
|
|
||||||
.local-search-hit-item
|
|
||||||
margin-left: 24px
|
|
||||||
padding-left: 3px
|
|
||||||
line-height: 1.8
|
|
||||||
|
|
||||||
&::marker
|
|
||||||
color: $search-color
|
|
||||||
font-weight: bold
|
|
||||||
font-style: italic
|
|
||||||
|
|
||||||
a
|
|
||||||
color: var(--search-a-color)
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
color: $search-color
|
|
||||||
|
|
||||||
.search-result-title
|
|
||||||
font-weight: 600
|
|
||||||
|
|
||||||
.search-result
|
|
||||||
margin: 0 0 8px
|
|
||||||
word-break: break-all
|
|
||||||
font-size: .9em
|
|
||||||
|
|
||||||
.search-result-list
|
.search-result-list
|
||||||
overflow-y: overlay
|
|
||||||
margin: 0 -20px
|
|
||||||
padding: 0 22px
|
|
||||||
max-height: calc(80vh - 180px)
|
|
||||||
|
|
||||||
+maxWidth768()
|
+maxWidth768()
|
||||||
max-height: calc(var(--search-height) - 190px) !important
|
if hexo-config('search.local_search.pagination.enable')
|
||||||
|
min-height: calc(var(--search-height) - 255px) !important
|
||||||
|
else
|
||||||
|
max-height: calc(var(--search-height) - 200px) !important
|
||||||
|
|
||||||
.search-keyword
|
#local-search-stats
|
||||||
background: transparent
|
.search-result-stats
|
||||||
color: $search-keyword-highlight
|
text-align: left
|
||||||
font-weight: 600
|
|
||||||
|
.search-keyword
|
||||||
|
font-weight: 600
|
||||||
|
|
||||||
|
#loading-database ~ *
|
||||||
|
visibility: hidden
|
||||||
@@ -698,7 +698,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const addCopyright = () => {
|
const addCopyright = () => {
|
||||||
const { limitCount, languages } = GLOBAL_CONFIG.copyright
|
const { limitCount, languages } = GLOBAL_CONFIG.copyright
|
||||||
|
|
||||||
const handleCopy = (e) => {
|
const handleCopy = e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const copyFont = window.getSelection(0).toString()
|
const copyFont = window.getSelection(0).toString()
|
||||||
let textFont = copyFont
|
let textFont = copyFont
|
||||||
|
|||||||
@@ -26,7 +26,12 @@ window.addEventListener('load', () => {
|
|||||||
const openSearch = () => {
|
const openSearch = () => {
|
||||||
btf.overflowPaddingR.add()
|
btf.overflowPaddingR.add()
|
||||||
animateElements(true)
|
animateElements(true)
|
||||||
setTimeout(() => { document.querySelector('#algolia-search .ais-SearchBox-input').focus() }, 100)
|
showLoading(false)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const searchInput = document.querySelector('#algolia-search-input .ais-SearchBox-input')
|
||||||
|
if (searchInput) searchInput.focus()
|
||||||
|
}, 100)
|
||||||
|
|
||||||
const handleEscape = event => {
|
const handleEscape = event => {
|
||||||
if (event.code === 'Escape') {
|
if (event.code === 'Escape') {
|
||||||
@@ -55,9 +60,38 @@ window.addEventListener('load', () => {
|
|||||||
document.querySelector('#algolia-search .search-close-button').addEventListener('click', closeSearch)
|
document.querySelector('#algolia-search .search-close-button').addEventListener('click', closeSearch)
|
||||||
}
|
}
|
||||||
|
|
||||||
const cutContent = (content) => {
|
const cutContent = content => {
|
||||||
if (!content) return ''
|
if (!content) return ''
|
||||||
const firstOccur = content.indexOf('<mark>')
|
|
||||||
|
let contentStr = ''
|
||||||
|
if (typeof content === 'string') {
|
||||||
|
contentStr = content.trim()
|
||||||
|
} else if (typeof content === 'object') {
|
||||||
|
if (content.value !== undefined) {
|
||||||
|
contentStr = String(content.value).trim()
|
||||||
|
if (!contentStr) return ''
|
||||||
|
} else if (content.matchedWords || content.matchLevel || content.fullyHighlighted !== undefined) {
|
||||||
|
return ''
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
contentStr = JSON.stringify(content).trim()
|
||||||
|
if (contentStr === '{}' || contentStr === '[]' || contentStr === '""') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (content.toString && typeof content.toString === 'function') {
|
||||||
|
contentStr = content.toString().trim()
|
||||||
|
if (contentStr === '[object Object]' || contentStr === '[object Array]') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstOccur = contentStr.indexOf('<mark>')
|
||||||
let start = firstOccur - 30
|
let start = firstOccur - 30
|
||||||
let end = firstOccur + 120
|
let end = firstOccur + 120
|
||||||
let pre = ''
|
let pre = ''
|
||||||
@@ -70,94 +104,454 @@ window.addEventListener('load', () => {
|
|||||||
pre = '...'
|
pre = '...'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (end > content.length) {
|
if (end > contentStr.length) {
|
||||||
end = content.length
|
end = contentStr.length
|
||||||
} else {
|
} else {
|
||||||
post = '...'
|
post = '...'
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${pre}${content.substring(start, end)}${post}`
|
// Ensure we don't cut off HTML tags in the middle
|
||||||
|
let substr = contentStr.substring(start, end)
|
||||||
|
|
||||||
|
// Handle tag completeness
|
||||||
|
// Check for incomplete opening tags at the beginning
|
||||||
|
const firstCloseBracket = substr.indexOf('>')
|
||||||
|
const firstOpenBracket = substr.indexOf('<')
|
||||||
|
|
||||||
|
// If there's a closing bracket but no opening bracket before it, we've cut a tag
|
||||||
|
if (firstCloseBracket !== -1 && (firstOpenBracket === -1 || firstCloseBracket < firstOpenBracket)) {
|
||||||
|
substr = substr.substring(firstCloseBracket + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for incomplete closing tags at the end
|
||||||
|
const lastOpenBracket = substr.lastIndexOf('<')
|
||||||
|
const lastCloseBracket = substr.lastIndexOf('>')
|
||||||
|
|
||||||
|
// If there's an opening bracket after the last closing bracket, we've cut a tag
|
||||||
|
if (lastOpenBracket !== -1 && lastOpenBracket > lastCloseBracket) {
|
||||||
|
substr = substr.substring(0, lastOpenBracket)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Balance tags in the substring
|
||||||
|
const tagStack = []
|
||||||
|
let balancedStr = ''
|
||||||
|
let i = 0
|
||||||
|
|
||||||
|
while (i < substr.length) {
|
||||||
|
if (substr[i] === '<') {
|
||||||
|
// Check if it's a closing tag
|
||||||
|
if (substr[i + 1] === '/') {
|
||||||
|
const closeTagEnd = substr.indexOf('>', i)
|
||||||
|
if (closeTagEnd !== -1) {
|
||||||
|
const closeTagName = substr.substring(i + 2, closeTagEnd)
|
||||||
|
// Remove matching opening tag from stack
|
||||||
|
for (let j = tagStack.length - 1; j >= 0; j--) {
|
||||||
|
if (tagStack[j] === closeTagName) {
|
||||||
|
tagStack.splice(j, 1)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
balancedStr += substr.substring(i, closeTagEnd + 1)
|
||||||
|
i = closeTagEnd + 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else if (substr.substr(i, 2) === '<!' || (substr.indexOf('/>', i) !== -1 && substr.indexOf('/>', i) < substr.indexOf('>', i))) {
|
||||||
|
const tagEnd = substr.indexOf('>', i)
|
||||||
|
if (tagEnd !== -1) {
|
||||||
|
balancedStr += substr.substring(i, tagEnd + 1)
|
||||||
|
i = tagEnd + 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const tagEnd = substr.indexOf('>', i)
|
||||||
|
if (tagEnd !== -1) {
|
||||||
|
const tagName = substr.substring(i + 1, (substr.indexOf(' ', i) > -1 && substr.indexOf(' ', i) < tagEnd)
|
||||||
|
? substr.indexOf(' ', i)
|
||||||
|
: tagEnd).split(/\s/)[0]
|
||||||
|
tagStack.push(tagName)
|
||||||
|
balancedStr += substr.substring(i, tagEnd + 1)
|
||||||
|
i = tagEnd + 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
balancedStr += substr[i]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close any unclosed tags
|
||||||
|
while (tagStack.length > 0) {
|
||||||
|
const tagName = tagStack.pop()
|
||||||
|
balancedStr += `</${tagName}>`
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we removed content from the beginning, add prefix
|
||||||
|
if (start > 0 || pre) {
|
||||||
|
const actualFirstOpenBracket = contentStr.indexOf('<', start > 0 ? start - 30 : 0)
|
||||||
|
const actualFirstMark = contentStr.indexOf('<mark>', start > 0 ? start - 30 : 0)
|
||||||
|
|
||||||
|
if (actualFirstOpenBracket !== -1 &&
|
||||||
|
(actualFirstMark === -1 || actualFirstOpenBracket < actualFirstMark)) {
|
||||||
|
pre = '...'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
substr = balancedStr
|
||||||
|
return `${pre}${substr}${post}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const disableDiv = [
|
// Helper function to handle Algolia highlight results
|
||||||
document.getElementById('algolia-hits'),
|
const extractHighlightValue = highlightObj => {
|
||||||
document.getElementById('algolia-pagination'),
|
if (!highlightObj) return ''
|
||||||
document.querySelector('#algolia-info .algolia-stats')
|
|
||||||
]
|
|
||||||
|
|
||||||
const searchClient = typeof algoliasearch === 'function' ? algoliasearch : window['algoliasearch/lite'].liteClient
|
if (typeof highlightObj === 'string') {
|
||||||
const search = instantsearch({
|
return highlightObj.trim()
|
||||||
indexName,
|
|
||||||
searchClient: searchClient(appId, apiKey),
|
|
||||||
searchFunction (helper) {
|
|
||||||
disableDiv.forEach(item => {
|
|
||||||
item.style.display = helper.state.query ? '' : 'none'
|
|
||||||
})
|
|
||||||
if (helper.state.query) helper.search()
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
const widgets = [
|
if (typeof highlightObj === 'object' && highlightObj.value !== undefined) {
|
||||||
instantsearch.widgets.configure({ hitsPerPage }),
|
return String(highlightObj.value).trim()
|
||||||
instantsearch.widgets.searchBox({
|
}
|
||||||
container: '#algolia-search-input',
|
|
||||||
showReset: false,
|
return ''
|
||||||
showSubmit: false,
|
}
|
||||||
placeholder: languages.input_placeholder,
|
|
||||||
showLoadingIndicator: true
|
// Initialize Algolia client
|
||||||
}),
|
let searchClient
|
||||||
instantsearch.widgets.hits({
|
|
||||||
container: '#algolia-hits',
|
if (window['algoliasearch/lite'] && typeof window['algoliasearch/lite'].liteClient === 'function') {
|
||||||
templates: {
|
searchClient = window['algoliasearch/lite'].liteClient(appId, apiKey)
|
||||||
item (data) {
|
} else if (typeof window.algoliasearch === 'function') {
|
||||||
const link = data.permalink || (GLOBAL_CONFIG.root + data.path)
|
searchClient = window.algoliasearch(appId, apiKey)
|
||||||
const result = data._highlightResult
|
} else {
|
||||||
const content = result.contentStripTruncate
|
return console.error('Algolia search client not found!')
|
||||||
? cutContent(result.contentStripTruncate.value)
|
}
|
||||||
: result.contentStrip
|
|
||||||
? cutContent(result.contentStrip.value)
|
if (!searchClient) {
|
||||||
: result.content
|
return console.error('Failed to initialize Algolia search client')
|
||||||
? cutContent(result.content.value)
|
}
|
||||||
: ''
|
|
||||||
return `
|
// Search state
|
||||||
<a href="${link}" class="algolia-hit-item-link">
|
let currentQuery = ''
|
||||||
<span class="algolia-hits-item-title">${result.title.value || 'no-title'}</span>
|
|
||||||
${content ? `<div class="algolia-hit-item-content">${content}</div>` : ''}
|
// Show loading state
|
||||||
</a>`
|
const showLoading = show => {
|
||||||
},
|
const loadingIndicator = document.getElementById('loading-status')
|
||||||
empty (data) {
|
if (loadingIndicator) {
|
||||||
return `<div id="algolia-hits-empty">${languages.hits_empty.replace(/\$\{query}/, data.query)}</div>`
|
loadingIndicator.hidden = !show
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache frequently used elements
|
||||||
|
const elements = {
|
||||||
|
get searchInput () { return document.querySelector('#algolia-search-input .ais-SearchBox-input') },
|
||||||
|
get hits () { return document.getElementById('algolia-hits') },
|
||||||
|
get hitsEmpty () { return document.getElementById('algolia-hits-empty') },
|
||||||
|
get hitsList () { return document.querySelector('#algolia-hits .ais-Hits-list') },
|
||||||
|
get hitsWrapper () { return document.querySelector('#algolia-hits .ais-Hits') },
|
||||||
|
get pagination () { return document.getElementById('algolia-pagination') },
|
||||||
|
get paginationList () { return document.querySelector('#algolia-pagination .ais-Pagination-list') },
|
||||||
|
get stats () { return document.querySelector('#algolia-info .ais-Stats-text') },
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show/hide search results area
|
||||||
|
const toggleResultsVisibility = hasResults => {
|
||||||
|
elements.pagination.style.display = hasResults ? '' : 'none'
|
||||||
|
elements.stats.style.display = hasResults ? '' : 'none'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render search results
|
||||||
|
const renderHits = (hits, query, page = 0) => {
|
||||||
|
if (hits.length === 0 && query) {
|
||||||
|
elements.hitsEmpty.textContent = languages.hits_empty.replace(/\$\{query}/, query)
|
||||||
|
elements.hitsEmpty.style.display = ''
|
||||||
|
elements.hitsWrapper.style.display = 'none'
|
||||||
|
elements.stats.style.display = 'none'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
elements.hitsEmpty.style.display = 'none'
|
||||||
|
|
||||||
|
const hitsHTML = hits.map((hit, index) => {
|
||||||
|
const itemNumber = page * hitsPerPage + index + 1
|
||||||
|
const link = hit.permalink || (GLOBAL_CONFIG.root + hit.path)
|
||||||
|
const result = hit._highlightResult || hit
|
||||||
|
|
||||||
|
// Content extraction
|
||||||
|
let content = ''
|
||||||
|
try {
|
||||||
|
if (result.contentStripTruncate) {
|
||||||
|
content = cutContent(result.contentStripTruncate)
|
||||||
|
} else if (result.contentStrip) {
|
||||||
|
content = cutContent(result.contentStrip)
|
||||||
|
} else if (result.content) {
|
||||||
|
content = cutContent(result.content)
|
||||||
|
} else if (hit.contentStripTruncate) {
|
||||||
|
content = cutContent(hit.contentStripTruncate)
|
||||||
|
} else if (hit.contentStrip) {
|
||||||
|
content = cutContent(hit.contentStrip)
|
||||||
|
} else if (hit.content) {
|
||||||
|
content = cutContent(hit.content)
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
content = ''
|
||||||
}
|
}
|
||||||
}),
|
|
||||||
instantsearch.widgets.stats({
|
// Title handling
|
||||||
container: '#algolia-info > .algolia-stats',
|
let title = 'no-title'
|
||||||
templates: {
|
try {
|
||||||
text (data) {
|
if (result.title) {
|
||||||
const stats = languages.hits_stats
|
title = extractHighlightValue(result.title) || 'no-title'
|
||||||
.replace(/\$\{hits}/, data.nbHits)
|
} else if (hit.title) {
|
||||||
.replace(/\$\{time}/, data.processingTimeMS)
|
title = extractHighlightValue(hit.title) || 'no-title'
|
||||||
return `<hr>${stats}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!title || title === 'no-title') {
|
||||||
|
if (typeof hit.title === 'string' && hit.title.trim()) {
|
||||||
|
title = hit.title.trim()
|
||||||
|
} else if (hit.title && typeof hit.title === 'object' && hit.title.value) {
|
||||||
|
title = String(hit.title.value).trim() || 'no-title'
|
||||||
|
} else {
|
||||||
|
title = 'no-title'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
title = 'no-title'
|
||||||
}
|
}
|
||||||
}),
|
|
||||||
instantsearch.widgets.poweredBy({
|
return `
|
||||||
container: '#algolia-info > .algolia-poweredBy'
|
<li class="ais-Hits-item" value="${itemNumber}">
|
||||||
}),
|
<a href="${link}" class="algolia-hit-item-link">
|
||||||
instantsearch.widgets.pagination({
|
<span class="algolia-hits-item-title">${title}</span>
|
||||||
container: '#algolia-pagination',
|
${content ? `<div class="algolia-hit-item-content">${content}</div>` : ''}
|
||||||
totalPages: 5,
|
</a>
|
||||||
templates: {
|
</li>`
|
||||||
first: '<i class="fas fa-angle-double-left"></i>',
|
}).join('')
|
||||||
last: '<i class="fas fa-angle-double-right"></i>',
|
|
||||||
previous: '<i class="fas fa-angle-left"></i>',
|
elements.hitsList.innerHTML = hitsHTML
|
||||||
next: '<i class="fas fa-angle-right"></i>'
|
elements.hitsWrapper.style.display = query ? '' : 'none'
|
||||||
|
|
||||||
|
if (hits.length > 0) {
|
||||||
|
elements.stats.style.display = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render pagination
|
||||||
|
const renderPagination = (page, nbPages) => {
|
||||||
|
if (nbPages <= 1) {
|
||||||
|
elements.pagination.style.display = 'none'
|
||||||
|
elements.paginationList.innerHTML = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
elements.pagination.style.display = 'block'
|
||||||
|
|
||||||
|
const isFirstPage = page === 0
|
||||||
|
const isLastPage = page === nbPages - 1
|
||||||
|
|
||||||
|
// Responsive page display
|
||||||
|
const isMobile = window.innerWidth < 768
|
||||||
|
const maxVisiblePages = isMobile ? 3 : 5
|
||||||
|
let startPage = Math.max(0, page - Math.floor(maxVisiblePages / 2))
|
||||||
|
const endPage = Math.min(nbPages - 1, startPage + maxVisiblePages - 1)
|
||||||
|
|
||||||
|
// Adjust starting page to maintain max visible pages
|
||||||
|
if (endPage - startPage + 1 < maxVisiblePages) {
|
||||||
|
startPage = Math.max(0, endPage - maxVisiblePages + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
let pagesHTML = ''
|
||||||
|
|
||||||
|
// Only add ellipsis and first page when there are many pages
|
||||||
|
if (nbPages > maxVisiblePages && startPage > 0) {
|
||||||
|
pagesHTML += `
|
||||||
|
<li class="ais-Pagination-item ais-Pagination-item--page">
|
||||||
|
<a class="ais-Pagination-link" aria-label="Page 1" href="#" data-page="0">1</a>
|
||||||
|
</li>`
|
||||||
|
if (startPage > 1) {
|
||||||
|
pagesHTML += `
|
||||||
|
<li class="ais-Pagination-item ais-Pagination-item--ellipsis">
|
||||||
|
<span class="ais-Pagination-link">...</span>
|
||||||
|
</li>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add middle page numbers
|
||||||
|
for (let i = startPage; i <= endPage; i++) {
|
||||||
|
const isSelected = i === page
|
||||||
|
if (isSelected) {
|
||||||
|
pagesHTML += `
|
||||||
|
<li class="ais-Pagination-item ais-Pagination-item--page ais-Pagination-item--selected">
|
||||||
|
<span class="ais-Pagination-link" aria-label="Page ${i + 1}">${i + 1}</span>
|
||||||
|
</li>`
|
||||||
|
} else {
|
||||||
|
pagesHTML += `
|
||||||
|
<li class="ais-Pagination-item ais-Pagination-item--page">
|
||||||
|
<a class="ais-Pagination-link" aria-label="Page ${i + 1}" href="#" data-page="${i}">${i + 1}</a>
|
||||||
|
</li>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add ellipsis and last page when there are many pages
|
||||||
|
if (nbPages > maxVisiblePages && endPage < nbPages - 1) {
|
||||||
|
if (endPage < nbPages - 2) {
|
||||||
|
pagesHTML += `
|
||||||
|
<li class="ais-Pagination-item ais-Pagination-item--ellipsis">
|
||||||
|
<span class="ais-Pagination-link">...</span>
|
||||||
|
</li>`
|
||||||
|
}
|
||||||
|
pagesHTML += `
|
||||||
|
<li class="ais-Pagination-item ais-Pagination-item--page">
|
||||||
|
<a class="ais-Pagination-link" aria-label="Page ${nbPages}" href="#" data-page="${nbPages - 1}">${nbPages}</a>
|
||||||
|
</li>`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nbPages > 1) {
|
||||||
|
elements.paginationList.innerHTML = `
|
||||||
|
<li class="ais-Pagination-item ais-Pagination-item--previousPage ${isFirstPage ? 'ais-Pagination-item--disabled' : ''}">
|
||||||
|
${isFirstPage
|
||||||
|
? '<span class="ais-Pagination-link ais-Pagination-link--disabled" aria-label="Previous Page"><i class="fas fa-angle-left"></i></span>'
|
||||||
|
: `<a class="ais-Pagination-link" aria-label="Previous Page" href="#" data-page="${page - 1}"><i class="fas fa-angle-left"></i></a>`
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
${pagesHTML}
|
||||||
|
<li class="ais-Pagination-item ais-Pagination-item--nextPage ${isLastPage ? 'ais-Pagination-item--disabled' : ''}">
|
||||||
|
${isLastPage
|
||||||
|
? '<span class="ais-Pagination-link ais-Pagination-link--disabled" aria-label="Next Page"><i class="fas fa-angle-right"></i></span>'
|
||||||
|
: `<a class="ais-Pagination-link" aria-label="Next Page" href="#" data-page="${page + 1}"><i class="fas fa-angle-right"></i></a>`
|
||||||
|
}
|
||||||
|
</li>`
|
||||||
|
elements.pagination.style.display = currentQuery ? '' : 'none'
|
||||||
|
} else {
|
||||||
|
elements.pagination.style.display = 'none'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render statistics
|
||||||
|
const renderStats = (nbHits, processingTimeMS, query) => {
|
||||||
|
if (query) {
|
||||||
|
const stats = languages.hits_stats
|
||||||
|
.replace(/\$\{hits}/, nbHits)
|
||||||
|
.replace(/\$\{time}/, processingTimeMS)
|
||||||
|
elements.stats.innerHTML = `<hr>${stats}`
|
||||||
|
elements.stats.style.display = ''
|
||||||
|
} else {
|
||||||
|
elements.stats.style.display = 'none'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform search
|
||||||
|
const performSearch = async (query, page = 0) => {
|
||||||
|
if (!query.trim()) {
|
||||||
|
currentQuery = ''
|
||||||
|
renderHits([], '', 0)
|
||||||
|
renderPagination(0, 0)
|
||||||
|
renderStats(0, 0, '')
|
||||||
|
toggleResultsVisibility(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading(true)
|
||||||
|
currentQuery = query
|
||||||
|
|
||||||
|
try {
|
||||||
|
let result
|
||||||
|
|
||||||
|
if (searchClient && typeof searchClient.search === 'function') {
|
||||||
|
// v5 multi-index search
|
||||||
|
const searchResult = await searchClient.search([{
|
||||||
|
indexName,
|
||||||
|
query,
|
||||||
|
params: {
|
||||||
|
page,
|
||||||
|
hitsPerPage,
|
||||||
|
highlightPreTag: '<mark>',
|
||||||
|
highlightPostTag: '</mark>',
|
||||||
|
attributesToHighlight: ['title', 'content', 'contentStrip', 'contentStripTruncate']
|
||||||
|
}
|
||||||
|
}])
|
||||||
|
result = searchResult.results[0]
|
||||||
|
} else if (searchClient && typeof searchClient.initIndex === 'function') {
|
||||||
|
// v4 single-index search
|
||||||
|
const index = searchClient.initIndex(indexName)
|
||||||
|
result = await index.search(query, {
|
||||||
|
page,
|
||||||
|
hitsPerPage,
|
||||||
|
highlightPreTag: '<mark>',
|
||||||
|
highlightPostTag: '</mark>',
|
||||||
|
attributesToHighlight: ['title', 'content', 'contentStrip', 'contentStripTruncate']
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
throw new Error('Algolia: No compatible search method available')
|
||||||
|
}
|
||||||
|
|
||||||
|
renderHits(result.hits || [], query, page)
|
||||||
|
|
||||||
|
const actualNbPages = result.nbHits <= hitsPerPage ? 1 : (result.nbPages || 0)
|
||||||
|
renderPagination(page, actualNbPages)
|
||||||
|
renderStats(result.nbHits || 0, result.processingTimeMS || 0, query)
|
||||||
|
|
||||||
|
const hasResults = result.hits && result.hits.length > 0
|
||||||
|
toggleResultsVisibility(hasResults)
|
||||||
|
|
||||||
|
// Refresh Pjax links
|
||||||
|
if (window.pjax) {
|
||||||
|
window.pjax.refresh(document.getElementById('algolia-hits'))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Algolia search error:', error)
|
||||||
|
renderHits([], query, page)
|
||||||
|
renderPagination(0, 0)
|
||||||
|
renderStats(0, 0, query)
|
||||||
|
} finally {
|
||||||
|
showLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounced search
|
||||||
|
let searchTimeout
|
||||||
|
const debouncedSearch = (query, delay = 300) => {
|
||||||
|
clearTimeout(searchTimeout)
|
||||||
|
searchTimeout = setTimeout(() => performSearch(query), delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize search box and events
|
||||||
|
const initializeSearch = () => {
|
||||||
|
showLoading(false)
|
||||||
|
|
||||||
|
if (elements.searchInput) {
|
||||||
|
elements.searchInput.addEventListener('input', e => {
|
||||||
|
const query = e.target.value
|
||||||
|
debouncedSearch(query)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchForm = document.querySelector('#algolia-search-input .ais-SearchBox-form')
|
||||||
|
if (searchForm) {
|
||||||
|
searchForm.addEventListener('submit', e => {
|
||||||
|
e.preventDefault()
|
||||||
|
const query = elements.searchInput.value
|
||||||
|
performSearch(query)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pagination event delegation
|
||||||
|
elements.pagination.addEventListener('click', e => {
|
||||||
|
e.preventDefault()
|
||||||
|
const link = e.target.closest('a[data-page]')
|
||||||
|
if (link) {
|
||||||
|
const page = parseInt(link.dataset.page, 10)
|
||||||
|
if (!isNaN(page) && currentQuery) {
|
||||||
|
performSearch(currentQuery, page)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
]
|
|
||||||
|
|
||||||
search.addWidgets(widgets)
|
// Initial state
|
||||||
search.start()
|
toggleResultsVisibility(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
initializeSearch()
|
||||||
searchClickFn()
|
searchClickFn()
|
||||||
searchFnOnce()
|
searchFnOnce()
|
||||||
|
|
||||||
@@ -165,10 +559,4 @@ window.addEventListener('load', () => {
|
|||||||
if (!btf.isHidden($searchMask)) closeSearch()
|
if (!btf.isHidden($searchMask)) closeSearch()
|
||||||
searchClickFn()
|
searchClickFn()
|
||||||
})
|
})
|
||||||
|
|
||||||
if (window.pjax) {
|
|
||||||
search.on('render', () => {
|
|
||||||
window.pjax.refresh(document.getElementById('algolia-hits'))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -236,40 +236,229 @@ class LocalSearch {
|
|||||||
|
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
// Search
|
// Search
|
||||||
const { path, top_n_per_article, unescape, languages } = GLOBAL_CONFIG.localSearch
|
const { path, top_n_per_article, unescape, languages, pagination } = GLOBAL_CONFIG.localSearch
|
||||||
|
const enablePagination = pagination && pagination.enable
|
||||||
const localSearch = new LocalSearch({
|
const localSearch = new LocalSearch({
|
||||||
path,
|
path,
|
||||||
top_n_per_article,
|
top_n_per_article,
|
||||||
unescape
|
unescape
|
||||||
})
|
})
|
||||||
|
|
||||||
const input = document.querySelector('#local-search-input input')
|
const input = document.querySelector('.local-search-input input')
|
||||||
const statsItem = document.getElementById('local-search-stats-wrap')
|
const statsItem = document.getElementById('local-search-stats')
|
||||||
const $loadingStatus = document.getElementById('loading-status')
|
const $loadingStatus = document.getElementById('loading-status')
|
||||||
const isXml = !path.endsWith('json')
|
const isXml = !path.endsWith('json')
|
||||||
|
|
||||||
|
// Pagination variables (only initialize if pagination is enabled)
|
||||||
|
let currentPage = 0
|
||||||
|
const hitsPerPage = pagination.hitsPerPage || 10
|
||||||
|
|
||||||
|
let currentResultItems = []
|
||||||
|
|
||||||
|
if (!enablePagination) {
|
||||||
|
// If pagination is disabled, we don't need these variables
|
||||||
|
currentPage = undefined
|
||||||
|
currentResultItems = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache frequently used elements
|
||||||
|
const elements = {
|
||||||
|
get pagination () { return document.getElementById('local-search-pagination') },
|
||||||
|
get paginationList () { return document.querySelector('#local-search-pagination .ais-Pagination-list') }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show/hide search results area
|
||||||
|
const toggleResultsVisibility = hasResults => {
|
||||||
|
if (enablePagination) {
|
||||||
|
elements.pagination.style.display = hasResults ? '' : 'none'
|
||||||
|
} else {
|
||||||
|
elements.pagination.style.display = 'none'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render search results for current page
|
||||||
|
const renderResults = (searchText, resultItems) => {
|
||||||
|
const container = document.getElementById('local-search-results')
|
||||||
|
|
||||||
|
// Determine items to display based on pagination mode
|
||||||
|
const itemsToDisplay = enablePagination
|
||||||
|
? currentResultItems.slice(currentPage * hitsPerPage, (currentPage + 1) * hitsPerPage)
|
||||||
|
: resultItems
|
||||||
|
|
||||||
|
// Handle empty page in pagination mode
|
||||||
|
if (enablePagination && itemsToDisplay.length === 0 && currentResultItems.length > 0) {
|
||||||
|
currentPage = 0
|
||||||
|
renderResults(searchText, resultItems)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add numbering to items
|
||||||
|
const numberedItems = itemsToDisplay.map((result, index) => {
|
||||||
|
const itemNumber = enablePagination
|
||||||
|
? currentPage * hitsPerPage + index + 1
|
||||||
|
: index + 1
|
||||||
|
return result.item.replace(
|
||||||
|
'<li class="local-search-hit-item">',
|
||||||
|
`<li class="local-search-hit-item" value="${itemNumber}">`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
container.innerHTML = `<ol class="search-result-list">${numberedItems.join('')}</ol>`
|
||||||
|
|
||||||
|
// Update stats
|
||||||
|
const displayCount = enablePagination ? currentResultItems.length : resultItems.length
|
||||||
|
const stats = languages.hits_stats.replace(/\$\{hits}/, displayCount)
|
||||||
|
statsItem.innerHTML = `<hr><div class="search-result-stats">${stats}</div>`
|
||||||
|
|
||||||
|
// Handle pagination
|
||||||
|
if (enablePagination) {
|
||||||
|
const nbPages = Math.ceil(currentResultItems.length / hitsPerPage)
|
||||||
|
renderPagination(currentPage, nbPages, searchText)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasResults = resultItems.length > 0
|
||||||
|
toggleResultsVisibility(hasResults)
|
||||||
|
|
||||||
|
window.pjax && window.pjax.refresh(container)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render pagination
|
||||||
|
const renderPagination = (page, nbPages, query) => {
|
||||||
|
if (nbPages <= 1) {
|
||||||
|
elements.pagination.style.display = 'none'
|
||||||
|
elements.paginationList.innerHTML = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
elements.pagination.style.display = 'block'
|
||||||
|
|
||||||
|
const isFirstPage = page === 0
|
||||||
|
const isLastPage = page === nbPages - 1
|
||||||
|
|
||||||
|
// Responsive page display
|
||||||
|
const isMobile = window.innerWidth < 768
|
||||||
|
const maxVisiblePages = isMobile ? 3 : 5
|
||||||
|
let startPage = Math.max(0, page - Math.floor(maxVisiblePages / 2))
|
||||||
|
const endPage = Math.min(nbPages - 1, startPage + maxVisiblePages - 1)
|
||||||
|
|
||||||
|
// Adjust starting page to maintain max visible pages
|
||||||
|
if (endPage - startPage + 1 < maxVisiblePages) {
|
||||||
|
startPage = Math.max(0, endPage - maxVisiblePages + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
let pagesHTML = ''
|
||||||
|
|
||||||
|
// Only add ellipsis and first page when there are many pages
|
||||||
|
if (nbPages > maxVisiblePages && startPage > 0) {
|
||||||
|
pagesHTML += `
|
||||||
|
<li class="ais-Pagination-item ais-Pagination-item--page">
|
||||||
|
<a class="ais-Pagination-link" aria-label="Page 1" href="#" data-page="0">1</a>
|
||||||
|
</li>`
|
||||||
|
if (startPage > 1) {
|
||||||
|
pagesHTML += `
|
||||||
|
<li class="ais-Pagination-item ais-Pagination-item--ellipsis">
|
||||||
|
<span class="ais-Pagination-link">...</span>
|
||||||
|
</li>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add middle page numbers
|
||||||
|
for (let i = startPage; i <= endPage; i++) {
|
||||||
|
const isSelected = i === page
|
||||||
|
if (isSelected) {
|
||||||
|
pagesHTML += `
|
||||||
|
<li class="ais-Pagination-item ais-Pagination-item--page ais-Pagination-item--selected">
|
||||||
|
<span class="ais-Pagination-link" aria-label="Page ${i + 1}">${i + 1}</span>
|
||||||
|
</li>`
|
||||||
|
} else {
|
||||||
|
pagesHTML += `
|
||||||
|
<li class="ais-Pagination-item ais-Pagination-item--page">
|
||||||
|
<a class="ais-Pagination-link" aria-label="Page ${i + 1}" href="#" data-page="${i}">${i + 1}</a>
|
||||||
|
</li>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add ellipsis and last page when there are many pages
|
||||||
|
if (nbPages > maxVisiblePages && endPage < nbPages - 1) {
|
||||||
|
if (endPage < nbPages - 2) {
|
||||||
|
pagesHTML += `
|
||||||
|
<li class="ais-Pagination-item ais-Pagination-item--ellipsis">
|
||||||
|
<span class="ais-Pagination-link">...</span>
|
||||||
|
</li>`
|
||||||
|
}
|
||||||
|
pagesHTML += `
|
||||||
|
<li class="ais-Pagination-item ais-Pagination-item--page">
|
||||||
|
<a class="ais-Pagination-link" aria-label="Page ${nbPages}" href="#" data-page="${nbPages - 1}">${nbPages}</a>
|
||||||
|
</li>`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nbPages > 1) {
|
||||||
|
elements.paginationList.innerHTML = `
|
||||||
|
<li class="ais-Pagination-item ais-Pagination-item--previousPage ${isFirstPage ? 'ais-Pagination-item--disabled' : ''}">
|
||||||
|
${isFirstPage
|
||||||
|
? '<span class="ais-Pagination-link ais-Pagination-link--disabled" aria-label="Previous Page"><i class="fas fa-angle-left"></i></span>'
|
||||||
|
: `<a class="ais-Pagination-link" aria-label="Previous Page" href="#" data-page="${page - 1}"><i class="fas fa-angle-left"></i></a>`
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
${pagesHTML}
|
||||||
|
<li class="ais-Pagination-item ais-Pagination-item--nextPage ${isLastPage ? 'ais-Pagination-item--disabled' : ''}">
|
||||||
|
${isLastPage
|
||||||
|
? '<span class="ais-Pagination-link ais-Pagination-link--disabled" aria-label="Next Page"><i class="fas fa-angle-right"></i></span>'
|
||||||
|
: `<a class="ais-Pagination-link" aria-label="Next Page" href="#" data-page="${page + 1}"><i class="fas fa-angle-right"></i></a>`
|
||||||
|
}
|
||||||
|
</li>`
|
||||||
|
} else {
|
||||||
|
elements.pagination.style.display = 'none'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear search results and stats
|
||||||
|
const clearSearchResults = () => {
|
||||||
|
const container = document.getElementById('local-search-results')
|
||||||
|
container.textContent = ''
|
||||||
|
statsItem.textContent = ''
|
||||||
|
toggleResultsVisibility(false)
|
||||||
|
if (enablePagination) {
|
||||||
|
currentResultItems = []
|
||||||
|
currentPage = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show no results message
|
||||||
|
const showNoResults = searchText => {
|
||||||
|
const container = document.getElementById('local-search-results')
|
||||||
|
container.textContent = ''
|
||||||
|
const statsDiv = document.createElement('div')
|
||||||
|
statsDiv.className = 'search-result-stats'
|
||||||
|
statsDiv.textContent = languages.hits_empty.replace(/\$\{query}/, searchText)
|
||||||
|
statsItem.innerHTML = statsDiv.outerHTML
|
||||||
|
toggleResultsVisibility(false)
|
||||||
|
if (enablePagination) {
|
||||||
|
currentResultItems = []
|
||||||
|
currentPage = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const inputEventFunction = () => {
|
const inputEventFunction = () => {
|
||||||
if (!localSearch.isfetched) return
|
if (!localSearch.isfetched) return
|
||||||
let searchText = input.value.trim().toLowerCase()
|
let searchText = input.value.trim().toLowerCase()
|
||||||
isXml && (searchText = searchText.replace(/</g, '<').replace(/>/g, '>'))
|
isXml && (searchText = searchText.replace(/</g, '<').replace(/>/g, '>'))
|
||||||
if (searchText !== '') $loadingStatus.innerHTML = '<i class="fas fa-spinner fa-pulse"></i>'
|
|
||||||
|
if (searchText !== '') $loadingStatus.hidden = false
|
||||||
|
|
||||||
const keywords = searchText.split(/[-\s]+/)
|
const keywords = searchText.split(/[-\s]+/)
|
||||||
const container = document.getElementById('local-search-results')
|
|
||||||
let resultItems = []
|
let resultItems = []
|
||||||
|
|
||||||
if (searchText.length > 0) {
|
if (searchText.length > 0) {
|
||||||
// Perform local searching
|
|
||||||
resultItems = localSearch.getResultItems(keywords)
|
resultItems = localSearch.getResultItems(keywords)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keywords.length === 1 && keywords[0] === '') {
|
if (keywords.length === 1 && keywords[0] === '') {
|
||||||
container.textContent = ''
|
clearSearchResults()
|
||||||
statsItem.textContent = ''
|
|
||||||
} else if (resultItems.length === 0) {
|
} else if (resultItems.length === 0) {
|
||||||
container.textContent = ''
|
showNoResults(searchText)
|
||||||
const statsDiv = document.createElement('div')
|
|
||||||
statsDiv.className = 'search-result-stats'
|
|
||||||
statsDiv.textContent = languages.hits_empty.replace(/\$\{query}/, searchText)
|
|
||||||
statsItem.innerHTML = statsDiv.outerHTML
|
|
||||||
} else {
|
} else {
|
||||||
|
// Sort results by relevance
|
||||||
resultItems.sort((left, right) => {
|
resultItems.sort((left, right) => {
|
||||||
if (left.includedCount !== right.includedCount) {
|
if (left.includedCount !== right.includedCount) {
|
||||||
return right.includedCount - left.includedCount
|
return right.includedCount - left.includedCount
|
||||||
@@ -279,14 +468,14 @@ window.addEventListener('load', () => {
|
|||||||
return right.id - left.id
|
return right.id - left.id
|
||||||
})
|
})
|
||||||
|
|
||||||
const stats = languages.hits_stats.replace(/\$\{hits}/, resultItems.length)
|
if (enablePagination) {
|
||||||
|
currentResultItems = resultItems
|
||||||
container.innerHTML = `<ol class="search-result-list">${resultItems.map(result => result.item).join('')}</ol>`
|
currentPage = 0
|
||||||
statsItem.innerHTML = `<hr><div class="search-result-stats">${stats}</div>`
|
}
|
||||||
window.pjax && window.pjax.refresh(container)
|
renderResults(searchText, resultItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
$loadingStatus.textContent = ''
|
$loadingStatus.hidden = true
|
||||||
}
|
}
|
||||||
|
|
||||||
let loadFlag = false
|
let loadFlag = false
|
||||||
@@ -340,11 +529,29 @@ window.addEventListener('load', () => {
|
|||||||
localSearch.fetchData()
|
localSearch.fetchData()
|
||||||
}
|
}
|
||||||
localSearch.highlightSearchWords(document.getElementById('article-container'))
|
localSearch.highlightSearchWords(document.getElementById('article-container'))
|
||||||
|
|
||||||
|
// Pagination event delegation - only add if pagination is enabled
|
||||||
|
if (enablePagination) {
|
||||||
|
elements.pagination.addEventListener('click', e => {
|
||||||
|
e.preventDefault()
|
||||||
|
const link = e.target.closest('a[data-page]')
|
||||||
|
if (link) {
|
||||||
|
const page = parseInt(link.dataset.page, 10)
|
||||||
|
if (!isNaN(page) && currentResultItems.length > 0) {
|
||||||
|
currentPage = page
|
||||||
|
renderResults(input.value.trim().toLowerCase(), currentResultItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial state
|
||||||
|
toggleResultsVisibility(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('search:loaded', () => {
|
window.addEventListener('search:loaded', () => {
|
||||||
const $loadDataItem = document.getElementById('loading-database')
|
const $loadDataItem = document.getElementById('loading-database')
|
||||||
$loadDataItem.nextElementSibling.style.display = 'block'
|
$loadDataItem.nextElementSibling.style.visibility = 'visible'
|
||||||
$loadDataItem.remove()
|
$loadDataItem.remove()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -106,7 +106,7 @@
|
|||||||
|
|
||||||
loadComment: (dom, callback) => {
|
loadComment: (dom, callback) => {
|
||||||
if ('IntersectionObserver' in window) {
|
if ('IntersectionObserver' in window) {
|
||||||
const observerItem = new IntersectionObserver((entries) => {
|
const observerItem = new IntersectionObserver(entries => {
|
||||||
if (entries[0].isIntersecting) {
|
if (entries[0].isIntersecting) {
|
||||||
callback()
|
callback()
|
||||||
observerItem.disconnect()
|
observerItem.disconnect()
|
||||||
|
|||||||
Reference in New Issue
Block a user