mirror of
https://github.com/jerryc127/hexo-theme-butterfly.git
synced 2026-08-08 11:38:42 +08:00
Compare commits
@@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Setup .npmrc file to publish to npm
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
@@ -124,7 +124,6 @@ npm install hexo-renderer-pug hexo-renderer-stylus --save
|
||||
- [x] **Theme Customization** - Custom site color schemes
|
||||
- [x] **Chart Support** - Mermaid flowcharts/Chart.js data charts
|
||||
- [x] **Music Notation** - ABCJS music notation support
|
||||
- [x] **Music Player** - APlayer/Meting music playback
|
||||
- [x] **Article Series** - Series article organization
|
||||
- [x] **Instantpage** - Page preloading acceleration
|
||||
- [x] **Snackbar** - Elegant notification messages
|
||||
|
||||
@@ -124,7 +124,6 @@ npm install hexo-renderer-pug hexo-renderer-stylus --save
|
||||
- [x] **主題定製** - 自訂網站配色方案
|
||||
- [x] **圖表支援** - Mermaid 流程圖/Chart.js 數據圖表
|
||||
- [x] **音樂符號** - ABCJS 音樂記譜法支援
|
||||
- [x] **音樂播放器** - APlayer/Meting 音樂播放功能
|
||||
- [x] **系列文章** - 系列文章組織功能
|
||||
- [x] **Instantpage** - 頁面預載入加速
|
||||
- [x] **Snackbar** - 優雅的提示訊息
|
||||
|
||||
+5
-9
@@ -659,7 +659,7 @@ artalk:
|
||||
# --------------------------------------
|
||||
|
||||
chat:
|
||||
# Choose: chatra/tidio/crisp
|
||||
# Choose: chatra/tidio/crisp/knocket
|
||||
# Leave it empty if you don't need chat
|
||||
use:
|
||||
# Chat Button [recommend]
|
||||
@@ -680,6 +680,10 @@ tidio:
|
||||
crisp:
|
||||
website_id:
|
||||
|
||||
# https://trtc.io/solutions/knocket
|
||||
knocket:
|
||||
identifier:
|
||||
|
||||
# --------------------------------------
|
||||
# Analysis
|
||||
# --------------------------------------
|
||||
@@ -989,11 +993,6 @@ pjax:
|
||||
exclude:
|
||||
# - /xxxxxx/
|
||||
|
||||
# Inject the css and script (aplayer/meting)
|
||||
aplayerInject:
|
||||
enable: false
|
||||
per_page: true
|
||||
|
||||
# Snackbar - Toast Notification
|
||||
# https://github.com/polonel/SnackBar
|
||||
# position: top-left / top-center / top-right / bottom-left / bottom-center / bottom-right
|
||||
@@ -1084,8 +1083,6 @@ CDN:
|
||||
# activate_power_mode:
|
||||
# algolia_js:
|
||||
# algolia_search:
|
||||
# aplayer_css:
|
||||
# aplayer_js:
|
||||
# artalk_css:
|
||||
# artalk_js:
|
||||
# blueimp_md5:
|
||||
@@ -1118,7 +1115,6 @@ CDN:
|
||||
# mathjax:
|
||||
# medium_zoom:
|
||||
# mermaid:
|
||||
# meting_js:
|
||||
# prismjs_autoloader:
|
||||
# prismjs_js:
|
||||
# prismjs_lineNumber_js:
|
||||
|
||||
@@ -41,10 +41,6 @@ div
|
||||
!= partial("includes/third-party/effect", {}, { cache: true })
|
||||
!= partial("includes/third-party/chat/index", {}, { cache: true })
|
||||
|
||||
if theme.aplayerInject && theme.aplayerInject.enable
|
||||
if theme.pjax.enable || theme.aplayerInject.per_page || page.aplayer
|
||||
include ./third-party/aplayer.pug
|
||||
|
||||
if theme.pjax.enable
|
||||
!= partial("includes/third-party/pjax", {}, { cache: true })
|
||||
|
||||
|
||||
+134
-157
@@ -54,6 +54,8 @@
|
||||
(() => {
|
||||
const limitConfig = !{ JSON.stringify(page.limit || {}) }
|
||||
|
||||
const escapeHtml = str => String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''')
|
||||
|
||||
const sortDataByDate = data => data.sort((a, b) => new Date(b.date) - new Date(a.date))
|
||||
|
||||
const filterDataByLimit = (data, limit) => {
|
||||
@@ -64,49 +66,47 @@
|
||||
return data.filter(item => new Date(item.date) >= limitDate)
|
||||
}
|
||||
return data
|
||||
};
|
||||
}
|
||||
|
||||
const dateFormatter = new Intl.DateTimeFormat('en-GB', {
|
||||
timeZone: !{JSON.stringify(config.timezone || '')} || Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
})
|
||||
|
||||
const formatToTimeZone = (date) => {
|
||||
const fullDate = date.length === 10 ? `${date} 00:00:00` : date
|
||||
const visitorTimeZone = '#{config.timezone}' || Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
const options = {
|
||||
timeZone: visitorTimeZone,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
}
|
||||
const [day, month, year, hour, minute, second] = new Intl.DateTimeFormat('en-GB', options)
|
||||
.format(new Date(fullDate))
|
||||
.match(/\d+/g)
|
||||
const [day, month, year, hour, minute, second] = dateFormatter.format(new Date(fullDate)).match(/\d+/g)
|
||||
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
|
||||
}
|
||||
|
||||
const addLazyload = str => {
|
||||
const config = {
|
||||
const lazyConfig = {
|
||||
enable: !{Boolean(enable)},
|
||||
native: !{Boolean(native)},
|
||||
field: '!{field}',
|
||||
placeholder: '!{url_for(placeholder)}',
|
||||
field: !{JSON.stringify(field || '')},
|
||||
placeholder: !{JSON.stringify(url_for(placeholder))},
|
||||
}
|
||||
|
||||
if (!config.enable || config.field !== 'site') return str
|
||||
if (!lazyConfig.enable || lazyConfig.field !== 'site' || str.indexOf('<img') === -1) return str
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(str, 'text/html')
|
||||
const images = doc.querySelectorAll('img')
|
||||
|
||||
images.forEach(img => {
|
||||
if (config.native) {
|
||||
if (lazyConfig.native) {
|
||||
img.setAttribute('loading', 'lazy')
|
||||
} else {
|
||||
const src = img.getAttribute('src')
|
||||
img.setAttribute('data-lazy-src', src)
|
||||
|
||||
if (config.placeholder) {
|
||||
img.setAttribute('src', config.placeholder)
|
||||
if (lazyConfig.placeholder) {
|
||||
img.setAttribute('src', lazyConfig.placeholder)
|
||||
} else {
|
||||
img.removeAttribute('src')
|
||||
}
|
||||
@@ -119,19 +119,18 @@
|
||||
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}
|
||||
const tags = item.tags && item.tags.map(tag => `<span class="shuoshuo-tag">${escapeHtml(tag)}</span>`).join('') || ''
|
||||
const commentButton = item.key && !{commentsJsLoad || false}
|
||||
? `<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>`
|
||||
? `<div class="shuoshuo-comment no-comment" data-key="${escapeHtml(item.key)}"></div>`
|
||||
: ''
|
||||
|
||||
return `
|
||||
@@ -139,10 +138,10 @@
|
||||
<div class="container">
|
||||
<div class="shuoshuo-item-header">
|
||||
<div class="shuoshuo-avatar">
|
||||
<img class="no-lightbox" src="${item.avatar || '!{url_for(theme.avatar.img)}'}">
|
||||
<img class="no-lightbox" src="${item.avatar ? escapeHtml(item.avatar) : !{JSON.stringify(url_for(theme.avatar.img))}}">
|
||||
</div>
|
||||
<div class="shuoshuo-info">
|
||||
<div class="shuoshuo-author">${item.author || '!{config.author}'}</div>
|
||||
<div class="shuoshuo-author">${item.author ? escapeHtml(item.author) : !{JSON.stringify(config.author || '')}}</div>
|
||||
<time class="shuoshuo-date" title="${formattedDate}">
|
||||
${btf.diffDate(formattedDate, true)}
|
||||
</time>
|
||||
@@ -162,73 +161,97 @@
|
||||
container.innerHTML = content
|
||||
|
||||
window.lazyLoadInstance && window.lazyLoadInstance.update()
|
||||
btf.loadLightbox(document.querySelectorAll('#article-container img:not(.no-lightbox)'))
|
||||
document.dispatchEvent(new Event('shuoshuo:rendered'))
|
||||
}
|
||||
|
||||
const setupNavEvents = (nav) => {
|
||||
nav.querySelector('.shuoshuo-prev-btn').addEventListener('click', () => {
|
||||
if (currentPage > 1) { currentPage--; renderPage(currentPage) }
|
||||
})
|
||||
nav.querySelector('.shuoshuo-next-btn').addEventListener('click', () => {
|
||||
if (currentPage < totalPages) { currentPage++; renderPage(currentPage) }
|
||||
})
|
||||
|
||||
const input = nav.querySelector('.shuoshuo-page-input')
|
||||
|
||||
input.addEventListener('focus', e => { e.target.placeholder = '' })
|
||||
input.addEventListener('blur', e => {
|
||||
if (!e.target.value.trim()) e.target.placeholder = currentPage
|
||||
})
|
||||
|
||||
input.addEventListener('input', e => {
|
||||
const value = parseInt(e.target.value) || 0
|
||||
let wasInvalid = false
|
||||
|
||||
if (value > totalPages) { e.target.value = totalPages; wasInvalid = true }
|
||||
else if (value < 1 && e.target.value !== '') { e.target.value = 1; wasInvalid = true }
|
||||
|
||||
if (wasInvalid) {
|
||||
e.target.classList.add('invalid')
|
||||
setTimeout(() => e.target.classList.remove('invalid'), 500)
|
||||
}
|
||||
})
|
||||
|
||||
input.addEventListener('keydown', e => {
|
||||
const value = e.target.value + e.key
|
||||
|
||||
if (e.key === 'Enter' || e.key === 'Backspace' || e.key === 'Delete' ||
|
||||
e.key === 'ArrowLeft' || e.key === 'ArrowRight' ||
|
||||
e.key === 'Tab' || e.ctrlKey || e.metaKey) {
|
||||
if (e.key === 'Enter') {
|
||||
const inputValue = e.target.value.trim()
|
||||
const inputPage = inputValue === '' ? currentPage : parseInt(inputValue)
|
||||
if (inputPage >= 1 && inputPage <= totalPages && inputPage !== currentPage) {
|
||||
currentPage = inputPage
|
||||
renderPage(currentPage)
|
||||
} else if (inputValue === '') {
|
||||
renderPage(currentPage)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!/^\d$/.test(e.key)) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
const newValue = parseInt(value) || 0
|
||||
if (newValue > totalPages || (value.length > 1 && newValue === 0)) {
|
||||
e.preventDefault()
|
||||
e.target.classList.add('invalid')
|
||||
setTimeout(() => e.target.classList.remove('invalid'), 500)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const renderNavigation = () => {
|
||||
const container = document.getElementById('article-container')
|
||||
const existingNav = container.nextElementSibling
|
||||
if (existingNav && existingNav.classList.contains('shuoshuo-navigation')) {
|
||||
existingNav.remove()
|
||||
}
|
||||
|
||||
let nav = container.nextElementSibling
|
||||
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)
|
||||
if (!nav || !nav.classList.contains('shuoshuo-navigation')) {
|
||||
nav = document.createElement('div')
|
||||
nav.className = 'shuoshuo-navigation'
|
||||
nav.innerHTML = `
|
||||
<button class="shuoshuo-prev-btn"><i class="fa-solid fa-chevron-left"></i></button>
|
||||
<span class="shuoshuo-page-info"></span>
|
||||
<input type="number" class="shuoshuo-page-input" min="1">
|
||||
<button class="shuoshuo-next-btn"><i class="fa-solid fa-chevron-right"></i></button>
|
||||
`
|
||||
container.insertAdjacentElement('afterend', nav)
|
||||
setupNavEvents(nav)
|
||||
}
|
||||
|
||||
nav.querySelector('.shuoshuo-page-info').textContent = pageInfoText
|
||||
nav.querySelector('.shuoshuo-prev-btn').disabled = currentPage === 1
|
||||
nav.querySelector('.shuoshuo-next-btn').disabled = currentPage === totalPages
|
||||
const input = nav.querySelector('.shuoshuo-page-input')
|
||||
input.max = totalPages
|
||||
input.placeholder = currentPage
|
||||
}
|
||||
|
||||
const renderPage = (page) => {
|
||||
@@ -239,79 +262,14 @@
|
||||
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 container = document.getElementById('article-container')
|
||||
try {
|
||||
let originData = []
|
||||
if (!{Boolean(page.shuoshuo_url)}) {
|
||||
container.innerHTML = '<div class="shuoshuo-loading"><i class="fa-solid fa-circle-notch fa-spin"></i></div>'
|
||||
const response = await fetch('!{url_for(page.shuoshuo_url)}')
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
||||
originData = await response.json()
|
||||
} else {
|
||||
const dataElement = document.getElementById('shuoshuo-data')
|
||||
@@ -319,14 +277,33 @@
|
||||
}
|
||||
|
||||
data = filterDataByLimit(sortDataByDate(originData), limitConfig)
|
||||
|
||||
totalPages = Math.ceil(data.length / itemsPerPage)
|
||||
|
||||
if (data.length === 0) {
|
||||
container.innerHTML = '<div class="shuoshuo-empty"></div>'
|
||||
return
|
||||
}
|
||||
|
||||
renderPage(currentPage)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
container.innerHTML = '<div class="shuoshuo-error"><i class="fa-solid fa-circle-exclamation"></i></div>'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
window.pjax ? loadShuoshuo() : window.addEventListener('load', loadShuoshuo)
|
||||
})()
|
||||
const pjaxCleanup = () => {
|
||||
const container = document.getElementById('article-container')
|
||||
if (container) {
|
||||
const nav = container.nextElementSibling
|
||||
if (nav && nav.classList.contains('shuoshuo-navigation')) nav.remove()
|
||||
}
|
||||
document.removeEventListener('pjax:send', pjaxCleanup)
|
||||
}
|
||||
|
||||
if (window.pjax) {
|
||||
document.addEventListener('pjax:send', pjaxCleanup)
|
||||
loadShuoshuo()
|
||||
} else {
|
||||
window.addEventListener('load', loadShuoshuo)
|
||||
}
|
||||
})()
|
||||
|
||||
+5
-2
@@ -2,8 +2,11 @@ script.
|
||||
(() => {
|
||||
const abcjsInit = () => {
|
||||
const abcjsFn = () => {
|
||||
const $article = document.getElementById("article-container")
|
||||
if (!$article || $article.querySelector(".hbe-container")) return
|
||||
|
||||
setTimeout(() => {
|
||||
const sheets = document.querySelectorAll(".abc-music-sheet")
|
||||
const sheets = $article.querySelectorAll(".abc-music-sheet")
|
||||
for (let i = 0; i < sheets.length; i++) {
|
||||
const ele = sheets[i]
|
||||
if (ele.children.length > 0) continue
|
||||
@@ -29,7 +32,7 @@ script.
|
||||
}, 100)
|
||||
}
|
||||
|
||||
if (typeof ABCJS === "object") {
|
||||
if (typeof ABCJS === "object" && ABCJS !== null) {
|
||||
abcjsFn()
|
||||
} else {
|
||||
btf.getScript("!{url_for(theme.asset.abcjs_basic_js)}").then(abcjsFn)
|
||||
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
link(rel='stylesheet' href=url_for(theme.asset.aplayer_css) media="print" onload="this.media='all'")
|
||||
script(src=url_for(theme.asset.aplayer_js))
|
||||
script(src=url_for(theme.asset.meting_js))
|
||||
if theme.pjax.enable
|
||||
script.
|
||||
(() => {
|
||||
const destroyAplayer = () => {
|
||||
if (window.aplayers) {
|
||||
for (let i = 0; i < window.aplayers.length; i++) {
|
||||
if (!window.aplayers[i].options.fixed) {
|
||||
window.aplayers[i].destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const runMetingJS = () => {
|
||||
typeof loadMeting === 'function' && document.getElementsByClassName('aplayer').length && loadMeting()
|
||||
}
|
||||
|
||||
btf.addGlobalFn('pjaxSend', destroyAplayer, 'destroyAplayer')
|
||||
btf.addGlobalFn('pjaxComplete', loadMeting, 'runMetingJS')
|
||||
})()
|
||||
+1
-1
@@ -8,7 +8,7 @@ script.
|
||||
|
||||
const res = await fetch(`!{serverURL}/api/comment?type=count&url=${keyArray}`, { method: 'GET' })
|
||||
const result = await res.json()
|
||||
|
||||
|
||||
result.data.forEach((count, index) => {
|
||||
eleGroup[index].textContent = count
|
||||
})
|
||||
|
||||
+13
-7
@@ -6,10 +6,14 @@ script.
|
||||
(window.Chatra.q = window.Chatra.q || []).push(arguments)
|
||||
}
|
||||
|
||||
btf.getScript('https://call.chatra.io/chatra.js').then(() => {
|
||||
const isChatBtn = !{theme.chat.rightside_button}
|
||||
const isChatHideShow = !{theme.chat.button_hide_show}
|
||||
const isChatBtn = !{theme.chat.rightside_button}
|
||||
const isChatHideShow = !{theme.chat.button_hide_show}
|
||||
|
||||
if (isChatBtn) {
|
||||
window.ChatraSetup = { startHidden: true }
|
||||
}
|
||||
|
||||
btf.getScript('https://call.chatra.io/chatra.js').then(() => {
|
||||
if (isChatBtn) {
|
||||
const close = () => {
|
||||
Chatra('minimizeWidget')
|
||||
@@ -21,11 +25,13 @@ script.
|
||||
Chatra('show')
|
||||
}
|
||||
|
||||
window.ChatraSetup = { startHidden: true }
|
||||
|
||||
window.chatBtnFn = () => document.getElementById('chatra').classList.contains('chatra--expanded') ? close() : open()
|
||||
window.chatBtnFn = () => {
|
||||
const el = document.getElementById('chatra')
|
||||
return el && el.classList.contains('chatra--expanded') ? close() : open()
|
||||
}
|
||||
|
||||
document.getElementById('chat-btn').style.display = 'block'
|
||||
const chatBtn = document.getElementById('chat-btn')
|
||||
if (chatBtn) chatBtn.style.display = 'block'
|
||||
} else if (isChatHideShow) {
|
||||
window.chatBtn = {
|
||||
hide: () => Chatra('hide'),
|
||||
|
||||
+2
-1
@@ -21,7 +21,8 @@ script.
|
||||
|
||||
window.chatBtnFn = () => $crisp.is("chat:visible") ? close() : open()
|
||||
|
||||
document.getElementById('chat-btn').style.display = 'block'
|
||||
const chatBtn = document.getElementById('chat-btn')
|
||||
if (chatBtn) chatBtn.style.display = 'block'
|
||||
} else if (isChatHideShow) {
|
||||
window.chatBtn = {
|
||||
hide: () => $crisp.push(["do", "chat:hide"]),
|
||||
|
||||
+4
-2
@@ -3,5 +3,7 @@ case theme.chat.use
|
||||
include ./chatra.pug
|
||||
when 'tidio'
|
||||
include ./tidio.pug
|
||||
when 'crisp'
|
||||
include ./crisp.pug
|
||||
when 'crisp'
|
||||
include ./crisp.pug
|
||||
when 'knocket'
|
||||
include ./knocket.pug
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
//- Knocket live chat — https://trtc.io/solutions/knocket
|
||||
script.
|
||||
(() => {
|
||||
const identifier = !{JSON.stringify(theme.knocket.identifier || '').replace(/</g, '\\u003c')}
|
||||
if (!identifier) return
|
||||
|
||||
btf.getScript(`https://trtc.io/knocket-sdk/sdk.js?identifier=${encodeURIComponent(identifier)}`).then(() => {
|
||||
const isChatBtn = !{theme.chat.rightside_button}
|
||||
const isChatHideShow = !{theme.chat.button_hide_show}
|
||||
|
||||
let widget = null
|
||||
const getWidget = () => widget || (widget = document.getElementById('contact-widget-auto'))
|
||||
|
||||
if (isChatBtn) {
|
||||
const hide = () => { const w = getWidget(); if (w) w.style.display = 'none' }
|
||||
const show = () => { const w = getWidget(); if (w) w.style.display = '' }
|
||||
|
||||
// Hide the native Knocket floating button
|
||||
const observer = new MutationObserver(() => {
|
||||
if (getWidget()) { hide(); observer.disconnect() }
|
||||
})
|
||||
observer.observe(document.body, { childList: true, subtree: true })
|
||||
hide()
|
||||
|
||||
window.chatBtnFn = () => {
|
||||
const w = getWidget()
|
||||
if (!w) return
|
||||
w.style.display = w.style.display === 'none' ? '' : 'none'
|
||||
}
|
||||
|
||||
const chatBtn = document.getElementById('chat-btn')
|
||||
if (chatBtn) chatBtn.style.display = 'block'
|
||||
} else if (isChatHideShow) {
|
||||
window.chatBtn = {
|
||||
hide: () => { const w = getWidget(); if (w) w.style.display = 'none' },
|
||||
show: () => { const w = getWidget(); if (w) w.style.display = '' }
|
||||
}
|
||||
}
|
||||
}).catch(error => {
|
||||
console.warn('[Knocket] Failed to load the chat SDK.', error)
|
||||
})
|
||||
})()
|
||||
+3
-3
@@ -10,7 +10,7 @@ script.
|
||||
window.tidioChatApi.hide()
|
||||
isShow = false
|
||||
}
|
||||
|
||||
|
||||
const open = () => {
|
||||
window.tidioChatApi.open()
|
||||
window.tidioChatApi.show()
|
||||
@@ -32,8 +32,8 @@ script.
|
||||
isShow ? close() : open()
|
||||
}
|
||||
|
||||
document.getElementById('chat-btn').style.display = 'block'
|
||||
|
||||
const chatBtn = document.getElementById('chat-btn')
|
||||
if (chatBtn) chatBtn.style.display = 'block'
|
||||
} else if (isChatHideShow) {
|
||||
window.chatBtn = {
|
||||
hide: () => window.tidioChatApi && window.tidioChatApi.hide(),
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
script.
|
||||
(() => {
|
||||
const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'== 'shuoshuo'
|
||||
const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'
|
||||
const dqOption = !{JSON.stringify(dqOption)}
|
||||
|
||||
const destroyDisqusjs = () => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
script.
|
||||
(()=>{
|
||||
const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'== 'shuoshuo'
|
||||
const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'
|
||||
|
||||
const loadFBComment = (el = document, path) => {
|
||||
if (isShuoshuo) {
|
||||
|
||||
@@ -43,4 +43,13 @@ hr.custom-hr
|
||||
when 'Remark42'
|
||||
#remark42
|
||||
when 'Artalk'
|
||||
if theme.artalk.vote
|
||||
.artalk-vote
|
||||
span.artalk-page-vote-up
|
||||
i.fas.fa-thumbs-up
|
||||
span.artalk-page-vote-up-count
|
||||
span.artalk-page-vote-down
|
||||
i.fas.fa-thumbs-down
|
||||
span.artalk-page-vote-down-count
|
||||
#artalk-wrap
|
||||
|
||||
|
||||
+20
-17
@@ -2,7 +2,22 @@
|
||||
- const { tags, enableMenu } = theme.math.mathjax
|
||||
script.
|
||||
(() => {
|
||||
const changeScriptToMath = article => {
|
||||
article.querySelectorAll('script[type^="math/tex"]').forEach(el => {
|
||||
const display = /mode=display/.test(el.type)
|
||||
const node = document.createElement(display ? 'div' : 'span')
|
||||
node.textContent = display
|
||||
? `$$${el.textContent}$$`
|
||||
: `$${el.textContent}$`
|
||||
el.parentNode.replaceChild(node, el)
|
||||
})
|
||||
}
|
||||
|
||||
const loadMathjax = () => {
|
||||
const article = document.getElementById('article-container')
|
||||
if (!article || article.querySelector('.hbe-container')) return
|
||||
changeScriptToMath(article)
|
||||
|
||||
if (!window.MathJax) {
|
||||
window.MathJax = {
|
||||
loader: {
|
||||
@@ -11,7 +26,8 @@ script.
|
||||
//- '[tex]/bbm',
|
||||
//- '[tex]/bboldx',
|
||||
//- '[tex]/dsfont',
|
||||
'[tex]/mhchem'
|
||||
'[tex]/mhchem',
|
||||
'ui/lazy'
|
||||
],
|
||||
paths: {
|
||||
'mathjax-newcm': '[mathjax]/../@mathjax/mathjax-newcm-font',
|
||||
@@ -39,25 +55,13 @@ script.
|
||||
scale: 1.1
|
||||
},
|
||||
options: {
|
||||
lazyMargin: '200px',
|
||||
enableMenu: !{enableMenu},
|
||||
menuOptions: {
|
||||
settings: {
|
||||
enrich: false // Turn off Braille and voice narration text automatic generation
|
||||
}
|
||||
},
|
||||
renderActions: {
|
||||
findScript: [10, doc => {
|
||||
for (const node of document.querySelectorAll('script[type^="math/tex"]')) {
|
||||
const display = !!node.type.match(/; *mode=display/)
|
||||
const math = new doc.options.MathItem(node.textContent, doc.inputJax[0], display)
|
||||
const text = document.createTextNode('')
|
||||
node.parentNode.replaceChild(text, node)
|
||||
math.start = {node: text, delim: '', n: 0}
|
||||
math.end = {node: text, delim: '', n: 0}
|
||||
doc.math.push(math)
|
||||
}
|
||||
}, '']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,9 +71,8 @@ script.
|
||||
script.async = true
|
||||
document.head.appendChild(script)
|
||||
} else {
|
||||
MathJax.startup.document.state(0)
|
||||
MathJax.texReset()
|
||||
MathJax.typesetPromise()
|
||||
MathJax.typesetClear()
|
||||
MathJax.typesetPromise([ article ])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+140
-99
@@ -51,7 +51,7 @@ script.
|
||||
clone.setAttribute('viewBox', initViewBox.join(' '))
|
||||
}
|
||||
if (!clone.getAttribute('xmlns')) clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg')
|
||||
if (!clone.getAttribute('xmlns:xlink') && clone.outerHTML.includes('xlink:')) {
|
||||
if (!clone.getAttribute('xmlns:xlink') && clone.innerHTML.includes('xlink:')) {
|
||||
clone.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink')
|
||||
}
|
||||
// inject background to match current theme
|
||||
@@ -70,7 +70,8 @@ script.
|
||||
const blob = new Blob([htmlSource], { type: 'text/html;charset=utf-8' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
window.open(url, '_blank', 'noopener')
|
||||
setTimeout(() => URL.revokeObjectURL(url), 30000)
|
||||
|
||||
setTimeout(() => URL.revokeObjectURL(url), 5000)
|
||||
}
|
||||
|
||||
const attachMermaidViewerButton = wrap => {
|
||||
@@ -91,10 +92,6 @@ script.
|
||||
const svg = wrap.__mermaidOriginalSvg || wrap.querySelector('svg')
|
||||
if (!svg) return
|
||||
const initViewBox = wrap.__mermaidInitViewBox
|
||||
if (typeof svg === 'string') {
|
||||
openSvgInNewTab({ source: svg, initViewBox })
|
||||
return
|
||||
}
|
||||
openSvgInNewTab({ source: svg, initViewBox })
|
||||
})
|
||||
btn.__mermaidViewerBound = true
|
||||
@@ -111,6 +108,13 @@ script.
|
||||
}
|
||||
|
||||
const initMermaidGestures = wrap => {
|
||||
// Clean up previous event listeners and pending frames
|
||||
if (wrap.__mermaidAbortController) {
|
||||
wrap.__mermaidAbortController.abort()
|
||||
if (wrap.__mermaidRafId) cancelAnimationFrame(wrap.__mermaidRafId)
|
||||
}
|
||||
const ac = new AbortController()
|
||||
wrap.__mermaidAbortController = ac
|
||||
const svg = wrap.querySelector('svg')
|
||||
if (!svg) return
|
||||
|
||||
@@ -119,158 +123,177 @@ script.
|
||||
wrap.__mermaidInitViewBox = initVb
|
||||
wrap.__mermaidCurViewBox = initVb.slice()
|
||||
setSvgViewBox(svg, initVb)
|
||||
// Disable default gestures to prevent scroll chaining and pinch-zoom penetration in Chrome
|
||||
svg.style.touchAction = 'none'
|
||||
|
||||
// Avoid binding multiple times on themeChange/pjax
|
||||
if (wrap.__mermaidGestureBound) return
|
||||
wrap.__mermaidGestureBound = true
|
||||
// Cache BoundingClientRect, throttled on scroll to reduce reflow
|
||||
let cachedRect = svg.getBoundingClientRect()
|
||||
let rectDirty = false
|
||||
const markRectDirty = () => { rectDirty = true }
|
||||
window.addEventListener('resize', markRectDirty, { signal: ac.signal })
|
||||
window.addEventListener('scroll', markRectDirty, { signal: ac.signal, capture: true })
|
||||
const getRect = () => {
|
||||
if (rectDirty) {
|
||||
cachedRect = svg.getBoundingClientRect()
|
||||
rectDirty = false
|
||||
}
|
||||
return cachedRect
|
||||
}
|
||||
|
||||
// Helper: map client (viewport) coordinate -> viewBox coordinate
|
||||
const clientToViewBox = (clientX, clientY) => {
|
||||
const rect = svg.getBoundingClientRect()
|
||||
const vb = wrap.__mermaidCurViewBox || getSvgViewBox(svg)
|
||||
const x = vb[0] + (clientX - rect.left) * (vb[2] / rect.width)
|
||||
const y = vb[1] + (clientY - rect.top) * (vb[3] / rect.height)
|
||||
return { x, y, rect, vb }
|
||||
// Precompute clamp bounds from initial viewBox
|
||||
const minW = initVb[2] * 0.1
|
||||
const maxW = initVb[2] * 10
|
||||
const minH = initVb[3] * 0.1
|
||||
const maxH = initVb[3] * 10
|
||||
const clampVb = vb => {
|
||||
const out = vb.slice()
|
||||
out[2] = clamp(out[2], minW, maxW)
|
||||
out[3] = clamp(out[3], minH, maxH)
|
||||
return out
|
||||
}
|
||||
|
||||
// Throttle DOM updates using requestAnimationFrame
|
||||
let pendingVb = null
|
||||
let rafId = null
|
||||
const applyVb = () => {
|
||||
if (pendingVb) {
|
||||
wrap.__mermaidCurViewBox = pendingVb
|
||||
setSvgViewBox(svg, pendingVb)
|
||||
pendingVb = null
|
||||
}
|
||||
rafId = null
|
||||
wrap.__mermaidRafId = null
|
||||
}
|
||||
const setCurVb = vb => {
|
||||
pendingVb = clampVb(vb)
|
||||
if (!rafId) {
|
||||
rafId = requestAnimationFrame(applyVb)
|
||||
wrap.__mermaidRafId = rafId
|
||||
}
|
||||
}
|
||||
|
||||
const state = {
|
||||
pointers: new Map(),
|
||||
startVb: null,
|
||||
startDist: 0,
|
||||
startCenter: null
|
||||
}
|
||||
|
||||
const clampVb = vb => {
|
||||
const init = wrap.__mermaidInitViewBox || vb
|
||||
const minW = init[2] * 0.1
|
||||
const maxW = init[2] * 10
|
||||
const minH = init[3] * 0.1
|
||||
const maxH = init[3] * 10
|
||||
vb[2] = clamp(vb[2], minW, maxW)
|
||||
vb[3] = clamp(vb[3], minH, maxH)
|
||||
return vb
|
||||
}
|
||||
|
||||
const setCurVb = vb => {
|
||||
vb = clampVb(vb)
|
||||
wrap.__mermaidCurViewBox = vb
|
||||
setSvgViewBox(svg, vb)
|
||||
lastPointerX: 0,
|
||||
lastPointerY: 0
|
||||
}
|
||||
|
||||
const onPointerDown = e => {
|
||||
// Allow only primary button for mouse
|
||||
if (e.pointerType === 'mouse' && e.button !== 0) return
|
||||
svg.setPointerCapture(e.pointerId)
|
||||
const curVb = wrap.__mermaidCurViewBox
|
||||
state.pointers.set(e.pointerId, { x: e.clientX, y: e.clientY })
|
||||
|
||||
if (state.pointers.size === 1) {
|
||||
state.startVb = (wrap.__mermaidCurViewBox || getSvgViewBox(svg)).slice()
|
||||
state.startVb = curVb.slice()
|
||||
state.lastPointerX = e.clientX
|
||||
state.lastPointerY = e.clientY
|
||||
} else if (state.pointers.size === 2) {
|
||||
const pts = [...state.pointers.values()]
|
||||
const dx = pts[0].x - pts[1].x
|
||||
const dy = pts[0].y - pts[1].y
|
||||
state.startDist = Math.hypot(dx, dy)
|
||||
state.startVb = (wrap.__mermaidCurViewBox || getSvgViewBox(svg)).slice()
|
||||
state.startCenter = { x: (pts[0].x + pts[1].x) / 2, y: (pts[0].y + pts[1].y) / 2 }
|
||||
state.startVb = curVb.slice()
|
||||
}
|
||||
}
|
||||
|
||||
const onPointerMove = e => {
|
||||
if (!state.pointers.has(e.pointerId)) return
|
||||
state.pointers.set(e.pointerId, { x: e.clientX, y: e.clientY })
|
||||
|
||||
// Pan with 1 pointer
|
||||
const curVb = wrap.__mermaidCurViewBox
|
||||
const rect = getRect()
|
||||
if (state.pointers.size === 1 && state.startVb) {
|
||||
const p = [...state.pointers.values()][0]
|
||||
const prev = { x: e.clientX - e.movementX, y: e.clientY - e.movementY }
|
||||
// movementX/Y unreliable on touch, compute from stored last position
|
||||
const last = wrap.__mermaidLastSinglePointer || p
|
||||
const dxClient = p.x - last.x
|
||||
const dyClient = p.y - last.y
|
||||
wrap.__mermaidLastSinglePointer = p
|
||||
|
||||
const { rect } = clientToViewBox(p.x, p.y)
|
||||
const vb = (wrap.__mermaidCurViewBox || getSvgViewBox(svg)).slice()
|
||||
const dx = dxClient * (vb[2] / rect.width)
|
||||
const dy = dyClient * (vb[3] / rect.height)
|
||||
setCurVb([vb[0] - dx, vb[1] - dy, vb[2], vb[3]])
|
||||
const p = state.pointers.values().next().value
|
||||
const dxClient = p.x - state.lastPointerX
|
||||
const dyClient = p.y - state.lastPointerY
|
||||
state.lastPointerX = p.x
|
||||
state.lastPointerY = p.y
|
||||
const dx = dxClient * (curVb[2] / rect.width)
|
||||
const dy = dyClient * (curVb[3] / rect.height)
|
||||
setCurVb([curVb[0] - dx, curVb[1] - dy, curVb[2], curVb[3]])
|
||||
return
|
||||
}
|
||||
|
||||
// Pinch zoom with 2 pointers
|
||||
if (state.pointers.size === 2 && state.startVb && state.startDist > 0) {
|
||||
const pts = [...state.pointers.values()]
|
||||
const dx = pts[0].x - pts[1].x
|
||||
const dy = pts[0].y - pts[1].y
|
||||
const dist = Math.hypot(dx, dy)
|
||||
if (!dist) return
|
||||
const factor = state.startDist / dist // dist bigger => zoom in (viewBox smaller)
|
||||
|
||||
const factor = state.startDist / dist
|
||||
const cx = (pts[0].x + pts[1].x) / 2
|
||||
const cy = (pts[0].y + pts[1].y) / 2
|
||||
const centerClient = { x: cx, y: cy }
|
||||
|
||||
const pxy = clientToViewBox(centerClient.x, centerClient.y)
|
||||
const cpx = pxy.x
|
||||
const cpy = pxy.y
|
||||
|
||||
const vb = zoomAtPoint(state.startVb, factor, cpx, cpy)
|
||||
setCurVb(vb)
|
||||
const px = curVb[0] + (cx - rect.left) * (curVb[2] / rect.width)
|
||||
const py = curVb[1] + (cy - rect.top) * (curVb[3] / rect.height)
|
||||
setCurVb(zoomAtPoint(state.startVb, factor, px, py))
|
||||
}
|
||||
}
|
||||
|
||||
const onPointerUpOrCancel = e => {
|
||||
// Release PointerCapture to avoid event capture anomalies
|
||||
if (svg.hasPointerCapture && svg.hasPointerCapture(e.pointerId)) {
|
||||
svg.releasePointerCapture(e.pointerId)
|
||||
}
|
||||
state.pointers.delete(e.pointerId)
|
||||
if (state.pointers.size === 0) {
|
||||
state.startVb = null
|
||||
state.startDist = 0
|
||||
state.startCenter = null
|
||||
wrap.__mermaidLastSinglePointer = null
|
||||
} else if (state.pointers.size === 1) {
|
||||
// reset single pointer baseline to avoid jump
|
||||
wrap.__mermaidLastSinglePointer = [...state.pointers.values()][0]
|
||||
const p = state.pointers.values().next().value
|
||||
state.lastPointerX = p.x
|
||||
state.lastPointerY = p.y
|
||||
}
|
||||
}
|
||||
|
||||
// Wheel zoom (mouse/trackpad)
|
||||
const onWheel = e => {
|
||||
// ctrlKey on mac trackpad pinch; we treat both as zoom
|
||||
// Prevent event bubbling from triggering external scroll
|
||||
e.preventDefault()
|
||||
const delta = e.deltaY
|
||||
const zoomFactor = delta > 0 ? 1.1 : 0.9
|
||||
const { x, y } = clientToViewBox(e.clientX, e.clientY)
|
||||
const vb = (wrap.__mermaidCurViewBox || getSvgViewBox(svg)).slice()
|
||||
setCurVb(zoomAtPoint(vb, zoomFactor, x, y))
|
||||
e.stopPropagation()
|
||||
// Normalize deltaY across deltaMode (Chrome uses pixels, Safari uses lines)
|
||||
let delta = e.deltaY
|
||||
if (e.deltaMode === 1) delta *= 16
|
||||
else if (e.deltaMode === 2) delta *= 400
|
||||
|
||||
// Continuous zoom factor: smoother than fixed 1.1/0.9 steps
|
||||
const zoomFactor = Math.pow(1.001, delta)
|
||||
|
||||
const curVb = wrap.__mermaidCurViewBox
|
||||
const rect = getRect()
|
||||
const px = curVb[0] + (e.clientX - rect.left) * (curVb[2] / rect.width)
|
||||
const py = curVb[1] + (e.clientY - rect.top) * (curVb[3] / rect.height)
|
||||
setCurVb(zoomAtPoint(curVb, zoomFactor, px, py))
|
||||
}
|
||||
|
||||
const onDblClick = () => {
|
||||
const init = wrap.__mermaidInitViewBox
|
||||
if (!init) return
|
||||
wrap.__mermaidCurViewBox = init.slice()
|
||||
setSvgViewBox(svg, init)
|
||||
if (init) setCurVb(init)
|
||||
}
|
||||
|
||||
svg.addEventListener('pointerdown', onPointerDown)
|
||||
svg.addEventListener('pointermove', onPointerMove)
|
||||
svg.addEventListener('pointerup', onPointerUpOrCancel)
|
||||
svg.addEventListener('pointercancel', onPointerUpOrCancel)
|
||||
svg.addEventListener('wheel', onWheel, { passive: false })
|
||||
svg.addEventListener('dblclick', onDblClick)
|
||||
svg.addEventListener('pointerdown', onPointerDown, { signal: ac.signal })
|
||||
svg.addEventListener('pointermove', onPointerMove, { signal: ac.signal })
|
||||
svg.addEventListener('pointerup', onPointerUpOrCancel, { signal: ac.signal })
|
||||
svg.addEventListener('pointercancel', onPointerUpOrCancel, { signal: ac.signal })
|
||||
svg.addEventListener('wheel', onWheel, { passive: false, signal: ac.signal })
|
||||
svg.addEventListener('dblclick', onDblClick, { signal: ac.signal })
|
||||
}
|
||||
|
||||
const runMermaid = ele => {
|
||||
window.loadMermaid = true
|
||||
const theme = document.documentElement.getAttribute('data-theme') === 'dark' ? '!{theme.mermaid.theme.dark}' : '!{theme.mermaid.theme.light}'
|
||||
|
||||
ele.forEach((item, index) => {
|
||||
const mermaidSrc = item.firstElementChild
|
||||
|
||||
// Clear old render (themeChange/pjax will rerun)
|
||||
// Clean up event listeners before removing old SVG
|
||||
if (item.__mermaidAbortController) {
|
||||
item.__mermaidAbortController.abort()
|
||||
}
|
||||
const oldSvg = item.querySelector('svg')
|
||||
if (oldSvg) oldSvg.remove()
|
||||
item.__mermaidGestureBound = false
|
||||
|
||||
const config = mermaidSrc.dataset.config ? JSON.parse(mermaidSrc.dataset.config) : {}
|
||||
let config = {}
|
||||
try {
|
||||
config = mermaidSrc.dataset.config ? JSON.parse(mermaidSrc.dataset.config) : {}
|
||||
} catch (e) {
|
||||
console.warn('[mermaid] failed to parse dataset.config:', e)
|
||||
}
|
||||
if (!config.theme) {
|
||||
config.theme = theme
|
||||
}
|
||||
@@ -278,7 +301,6 @@ script.
|
||||
const mermaidID = `mermaid-${index}`
|
||||
const mermaidDefinition = mermaidThemeConfig + mermaidSrc.textContent
|
||||
|
||||
const renderFn = mermaid.render(mermaidID, mermaidDefinition)
|
||||
const renderMermaid = svg => {
|
||||
mermaidSrc.insertAdjacentHTML('afterend', svg)
|
||||
if (!{theme.mermaid.zoom_pan}) initMermaidGestures(item)
|
||||
@@ -286,14 +308,30 @@ script.
|
||||
if (!{theme.mermaid.open_in_new_tab}) attachMermaidViewerButton(item)
|
||||
}
|
||||
|
||||
const handleError = err => {
|
||||
console.error(`[mermaid] render failed for block #${index}:`, err)
|
||||
const errorEl = document.createElement('div')
|
||||
errorEl.className = 'mermaid-error'
|
||||
errorEl.textContent = `Mermaid render error: ${err.message || err}`
|
||||
mermaidSrc.insertAdjacentElement('afterend', errorEl)
|
||||
}
|
||||
|
||||
// mermaid v9 and v10 compatibility
|
||||
typeof renderFn === 'string' ? renderMermaid(renderFn) : renderFn.then(({ svg }) => renderMermaid(svg))
|
||||
try {
|
||||
const renderFn = mermaid.render(mermaidID, mermaidDefinition)
|
||||
// mermaid v9 and v10 compatibility
|
||||
if (typeof renderFn === 'string') {
|
||||
renderMermaid(renderFn)
|
||||
} else {
|
||||
renderFn.then(({ svg }) => renderMermaid(svg)).catch(handleError)
|
||||
}
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const codeToMermaid = () => {
|
||||
const codeMermaidEle = document.querySelectorAll('pre > code.mermaid')
|
||||
const codeToMermaid = $article => {
|
||||
const codeMermaidEle = $article.querySelectorAll('pre > code.mermaid')
|
||||
if (codeMermaidEle.length === 0) return
|
||||
|
||||
codeMermaidEle.forEach(ele => {
|
||||
@@ -309,8 +347,11 @@ script.
|
||||
}
|
||||
|
||||
const loadMermaid = () => {
|
||||
if (!{theme.mermaid.code_write}) codeToMermaid()
|
||||
const $mermaid = document.querySelectorAll('#article-container .mermaid-wrap')
|
||||
const $article = document.getElementById('article-container')
|
||||
if (!$article) return
|
||||
|
||||
if (!{theme.mermaid.code_write}) codeToMermaid($article)
|
||||
const $mermaid = $article.querySelectorAll('.mermaid-wrap')
|
||||
if ($mermaid.length === 0) return
|
||||
|
||||
const runMermaidFn = () => runMermaid($mermaid)
|
||||
|
||||
Vendored
+8
-4
@@ -1,4 +1,4 @@
|
||||
- var pjaxExclude = 'a:not([target="_blank"])'
|
||||
- var pjaxExclude = 'a:not([target="_blank"]):not([data-fancybox])'
|
||||
if theme.pjax.exclude
|
||||
each val in theme.pjax.exclude
|
||||
- pjaxExclude += `:not([href="${val}"])`
|
||||
@@ -53,8 +53,9 @@ script.
|
||||
document.addEventListener('pjax:complete', () => {
|
||||
btf.removeGlobalFnEvent('pjaxCompleteOnce')
|
||||
document.querySelectorAll('script[data-pjax]').forEach(item => {
|
||||
if (!item.parentNode) return
|
||||
const newScript = document.createElement('script')
|
||||
const content = item.text || item.textContent || item.innerHTML || ""
|
||||
const content = item.text || item.textContent || ''
|
||||
Array.from(item.attributes).forEach(attr => newScript.setAttribute(attr.name, attr.value))
|
||||
newScript.appendChild(document.createTextNode(content))
|
||||
item.parentNode.replaceChild(newScript, item)
|
||||
@@ -64,10 +65,13 @@ script.
|
||||
})
|
||||
|
||||
document.addEventListener('pjax:error', e => {
|
||||
if (e.request.status === 404) {
|
||||
const responseURL = e.request && e.request.responseURL
|
||||
if (e.request && e.request.status === 404) {
|
||||
!{theme.error_404 && theme.error_404.enable}
|
||||
? pjax.loadUrl('!{url_for("/404.html")}')
|
||||
: window.location.href = e.request.responseURL
|
||||
: responseURL && (window.location.href = responseURL)
|
||||
} else if (responseURL) {
|
||||
window.location.href = responseURL
|
||||
}
|
||||
})
|
||||
})
|
||||
+5
-1
@@ -5,11 +5,15 @@
|
||||
if (syntax_highlighter === 'prismjs' || enable) && !preprocess
|
||||
script.
|
||||
(() => {
|
||||
const $article = document.getElementById('article-container')
|
||||
if (!$article || $article.querySelector('.hbe-container')) return
|
||||
|
||||
window.Prism = window.Prism || {}
|
||||
window.Prism.manual = true
|
||||
|
||||
const highlightAll = () => {
|
||||
window.Prism.highlightAll()
|
||||
// window.Prism.highlightAll()
|
||||
window.Prism.highlightAllUnder($article, true)
|
||||
}
|
||||
|
||||
window.addEventListener('load', highlightAll)
|
||||
|
||||
+5
-5
@@ -44,7 +44,7 @@ script.
|
||||
}
|
||||
}
|
||||
}
|
||||
btf.addGlobalFn('pjaxSendOnce', () => { typed.destroy() }, 'typedDestroy')
|
||||
btf.addGlobalFn('pjaxSendOnce', () => { typed && typed.destroy() }, 'typedDestroy')
|
||||
|
||||
case source
|
||||
when 1
|
||||
@@ -53,8 +53,8 @@ case source
|
||||
fetch('https://v1.hitokoto.cn')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const from = '出自 ' + data.from
|
||||
typedJSFn.processSubtitle(data.hitokoto, [from])
|
||||
const from = data.from ? '出自 ' + data.from : ''
|
||||
typedJSFn.processSubtitle(data.hitokoto, from ? [from] : [])
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to get the Hitokoto API:', err)
|
||||
@@ -79,7 +79,7 @@ case source
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to get the Yiyan API:', err)
|
||||
typedJSFn.processSubtitle(!{JSON.stringify(subContent.length)})
|
||||
typedJSFn.processSubtitle(!{JSON.stringify(subContent)})
|
||||
})
|
||||
}
|
||||
typedJSFn.run(subtitleType)
|
||||
@@ -99,7 +99,7 @@ case source
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to get the Jinrishici API:', err)
|
||||
typedJSFn.processSubtitle(!{JSON.stringify(subContent.length)})
|
||||
typedJSFn.processSubtitle(!{JSON.stringify(subContent)})
|
||||
})
|
||||
}
|
||||
typedJSFn.run(subtitleType)
|
||||
|
||||
+116
-110
@@ -1,110 +1,116 @@
|
||||
- let { serverURL, script_name, website_id, option, UV_PV } = theme.umami_analytics
|
||||
- const isServerURL = !!serverURL
|
||||
- const baseURL = serverURL ? serverURL.replace(/\/$/, '') : 'https://cloud.umami.is'
|
||||
- const apiUrl = serverURL ? serverURL.replace(/\/$/, '') + '/api' : 'https://api.umami.is/v1'
|
||||
|
||||
script.
|
||||
(() => {
|
||||
const option = !{JSON.stringify(option)}
|
||||
const config = !{JSON.stringify(UV_PV)}
|
||||
|
||||
const runTrack = () => {
|
||||
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 = () => {
|
||||
btf.getScript('!{baseURL}/!{script_name}', {
|
||||
'data-website-id': '!{website_id}',
|
||||
'data-auto-track': 'false',
|
||||
...option
|
||||
}).then(() => {
|
||||
runTrack()
|
||||
}).catch(error => {
|
||||
console.error('Umami Analytics: Error loading script', error)
|
||||
})
|
||||
}
|
||||
|
||||
const getData = async (isPost) => {
|
||||
try {
|
||||
const now = Date.now()
|
||||
const keyUrl = isPost ? `&url=${window.location.pathname}&path=${window.location.pathname}` : ''
|
||||
const headerList = { 'Accept': 'application/json' }
|
||||
|
||||
if (!{isServerURL}) {
|
||||
headerList['Authorization'] = `Bearer ${config.token}`
|
||||
} else {
|
||||
headerList['x-umami-api-key'] = config.token
|
||||
}
|
||||
|
||||
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 () => {
|
||||
try {
|
||||
if (GLOBAL_CONFIG_SITE.pageType === 'post' && config.page_pv) {
|
||||
const pagePV = document.getElementById('umamiPV')
|
||||
if (pagePV) {
|
||||
const data = await getData(true)
|
||||
if (data && data.pageviews) {
|
||||
pagePV.textContent = typeof data.pageviews.value !== 'undefined' ? data.pageviews.value : data.pageviews
|
||||
} else {
|
||||
console.warn('Umami Analytics: Invalid page view data received')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
siteUV.textContent = typeof data.visitors.value !== 'undefined' ? data.visitors.value : data.visitors
|
||||
} 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) {
|
||||
sitePV.textContent = typeof data.pageviews.value !== 'undefined' ? data.pageviews.value : data.pageviews
|
||||
} 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', insertData, 'umami_analytics_insert')
|
||||
|
||||
|
||||
loadUmamiJS()
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', insertData)
|
||||
} else {
|
||||
setTimeout(insertData, 100)
|
||||
}
|
||||
})()
|
||||
- let { serverURL, script_name, website_id, option, UV_PV } = theme.umami_analytics
|
||||
- const isServerURL = !!serverURL
|
||||
- const baseURL = serverURL ? serverURL.replace(/\/$/, '') : 'https://cloud.umami.is'
|
||||
- const apiUrl = serverURL ? serverURL.replace(/\/$/, '') + '/api' : 'https://api.umami.is/v1'
|
||||
|
||||
script.
|
||||
(() => {
|
||||
const option = !{JSON.stringify(option)}
|
||||
const config = !{JSON.stringify(UV_PV)}
|
||||
|
||||
const runTrack = () => {
|
||||
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 = () => {
|
||||
btf.getScript('!{baseURL}/!{script_name}', {
|
||||
'data-website-id': '!{website_id}',
|
||||
'data-auto-track': 'false',
|
||||
...option
|
||||
}).then(() => {
|
||||
runTrack()
|
||||
}).catch(error => {
|
||||
console.error('Umami Analytics: Error loading script', error)
|
||||
})
|
||||
}
|
||||
|
||||
const extractValue = stat => (stat && typeof stat.value !== 'undefined') ? stat.value : stat
|
||||
|
||||
const getData = async (isPost) => {
|
||||
try {
|
||||
const now = Date.now()
|
||||
const pathQuery = isPost ? `&url=${window.location.pathname}&path=${window.location.pathname}` : ''
|
||||
const headers = {
|
||||
'Accept': 'application/json',
|
||||
[!{isServerURL} ? 'Authorization' : 'x-umami-api-key']: !{isServerURL} ? `Bearer ${config.token}` : config.token
|
||||
}
|
||||
|
||||
const res = await fetch(`!{apiUrl}/websites/!{website_id}/stats?startAt=0000000000&endAt=${now}${pathQuery}`, {
|
||||
method: 'GET',
|
||||
headers
|
||||
})
|
||||
|
||||
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 () => {
|
||||
try {
|
||||
const tasks = []
|
||||
|
||||
if (GLOBAL_CONFIG_SITE.pageType === 'post' && config.page_pv) {
|
||||
tasks.push((async () => {
|
||||
const pagePV = document.getElementById('umamiPV')
|
||||
if (pagePV) {
|
||||
const data = await getData(true)
|
||||
if (data && data.pageviews) {
|
||||
pagePV.textContent = extractValue(data.pageviews)
|
||||
} else {
|
||||
console.warn('Umami Analytics: Invalid page view data received')
|
||||
}
|
||||
}
|
||||
})())
|
||||
}
|
||||
|
||||
if (config.site_uv || config.site_pv) {
|
||||
tasks.push((async () => {
|
||||
const data = await getData(false)
|
||||
|
||||
if (config.site_uv) {
|
||||
const siteUV = document.getElementById('umami-site-uv')
|
||||
if (siteUV && data && data.visitors) {
|
||||
siteUV.textContent = extractValue(data.visitors)
|
||||
} 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) {
|
||||
sitePV.textContent = extractValue(data.pageviews)
|
||||
} else if (sitePV) {
|
||||
console.warn('Umami Analytics: Invalid site PV data received')
|
||||
}
|
||||
}
|
||||
})())
|
||||
}
|
||||
|
||||
await Promise.all(tasks)
|
||||
} catch (error) {
|
||||
console.error('Umami Analytics: Failed to insert data', error)
|
||||
}
|
||||
}
|
||||
|
||||
btf.addGlobalFn('pjaxComplete', runTrack, 'umami_analytics_run_track')
|
||||
btf.addGlobalFn('pjaxComplete', insertData, 'umami_analytics_insert')
|
||||
|
||||
loadUmamiJS()
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', insertData)
|
||||
} else {
|
||||
setTimeout(insertData, 100)
|
||||
}
|
||||
})()
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hexo-theme-butterfly",
|
||||
"version": "5.5.5",
|
||||
"version": "5.7.0",
|
||||
"description": "A Simple and Card UI Design theme for Hexo",
|
||||
"main": "package.json",
|
||||
"scripts": {
|
||||
@@ -26,7 +26,7 @@
|
||||
"hexo-renderer-pug": "^3.0.0",
|
||||
"hexo-renderer-stylus": "^3.0.1",
|
||||
"hexo-util": "^4.0.0",
|
||||
"moment-timezone": "^0.6.2"
|
||||
"moment-timezone": "^0.6.3"
|
||||
},
|
||||
"homepage": "https://butterfly.js.org/",
|
||||
"author": "Jerry <my@crazywong.com>",
|
||||
|
||||
+14
-26
@@ -1,7 +1,7 @@
|
||||
abcjs_basic_js:
|
||||
name: abcjs
|
||||
file: dist/abcjs-basic-min.js
|
||||
version: 6.6.3
|
||||
version: 6.6.4
|
||||
activate_power_mode:
|
||||
name: butterfly-extsrc
|
||||
file: dist/activate-power-mode.min.js
|
||||
@@ -9,23 +9,15 @@ activate_power_mode:
|
||||
algolia_search:
|
||||
name: algoliasearch
|
||||
file: dist/lite/builds/browser.umd.js
|
||||
version: 5.53.0
|
||||
aplayer_css:
|
||||
name: aplayer
|
||||
file: dist/APlayer.min.css
|
||||
version: 1.10.1
|
||||
aplayer_js:
|
||||
name: aplayer
|
||||
file: dist/APlayer.min.js
|
||||
version: 1.10.1
|
||||
version: 5.56.0
|
||||
artalk_css:
|
||||
name: artalk
|
||||
file: dist/Artalk.css
|
||||
version: 2.9.1
|
||||
version: 2.10.0
|
||||
artalk_js:
|
||||
name: artalk
|
||||
file: dist/Artalk.js
|
||||
version: 2.9.1
|
||||
version: 2.10.0
|
||||
blueimp_md5:
|
||||
name: blueimp-md5
|
||||
file: js/md5.min.js
|
||||
@@ -66,12 +58,12 @@ docsearch_css:
|
||||
name: '@docsearch/css'
|
||||
other_name: docsearch-css
|
||||
file: dist/style.css
|
||||
version: 4.6.3
|
||||
version: 4.7.0
|
||||
docsearch_js:
|
||||
name: '@docsearch/js'
|
||||
other_name: docsearch-js
|
||||
file: dist/umd/index.js
|
||||
version: 4.6.3
|
||||
version: 4.7.0
|
||||
egjs_infinitegrid:
|
||||
name: '@egjs/infinitegrid'
|
||||
other_name: egjs-infinitegrid
|
||||
@@ -80,13 +72,13 @@ egjs_infinitegrid:
|
||||
fancybox:
|
||||
name: '@fancyapps/ui'
|
||||
file: dist/fancybox/fancybox.umd.js
|
||||
version: 6.1.14
|
||||
other_name: fancyapps-ui
|
||||
version: 6.1.14
|
||||
fancybox_css:
|
||||
name: '@fancyapps/ui'
|
||||
file: dist/fancybox/fancybox.css
|
||||
version: 6.1.14
|
||||
other_name: fancyapps-ui
|
||||
version: 6.1.14
|
||||
fireworks:
|
||||
name: butterfly-extsrc
|
||||
file: dist/fireworks.min.js
|
||||
@@ -95,7 +87,7 @@ fontawesome:
|
||||
name: '@fortawesome/fontawesome-free'
|
||||
file: css/all.min.css
|
||||
other_name: font-awesome
|
||||
version: 7.2.0
|
||||
version: 7.3.1
|
||||
gitalk:
|
||||
name: gitalk
|
||||
file: dist/gitalk.min.js
|
||||
@@ -112,12 +104,12 @@ katex:
|
||||
name: katex
|
||||
file: dist/katex.min.css
|
||||
other_name: KaTeX
|
||||
version: 0.17.0
|
||||
version: 0.18.1
|
||||
katex_copytex:
|
||||
name: katex
|
||||
file: dist/contrib/copy-tex.min.js
|
||||
other_name: KaTeX
|
||||
version: 0.17.0
|
||||
version: 0.18.1
|
||||
lazyload:
|
||||
name: vanilla-lazyload
|
||||
file: dist/lazyload.iife.min.js
|
||||
@@ -125,7 +117,7 @@ lazyload:
|
||||
mathjax:
|
||||
name: mathjax
|
||||
file: tex-mml-chtml.js
|
||||
version: 4.1.2
|
||||
version: 4.1.3
|
||||
medium_zoom:
|
||||
name: medium-zoom
|
||||
file: dist/medium-zoom.min.js
|
||||
@@ -133,11 +125,7 @@ medium_zoom:
|
||||
mermaid:
|
||||
name: mermaid
|
||||
file: dist/mermaid.min.js
|
||||
version: 11.15.0
|
||||
meting_js:
|
||||
name: butterfly-extsrc
|
||||
file: metingjs/dist/Meting.min.js
|
||||
version: 1.1.6
|
||||
version: 11.16.0
|
||||
pace_default_css:
|
||||
name: pace-js
|
||||
other_name: pace
|
||||
@@ -186,7 +174,7 @@ snackbar_css:
|
||||
twikoo:
|
||||
name: twikoo
|
||||
file: dist/twikoo.all.min.js
|
||||
version: 1.7.11
|
||||
version: 1.7.15
|
||||
typed:
|
||||
name: typed.js
|
||||
file: dist/typed.umd.js
|
||||
|
||||
@@ -378,6 +378,7 @@ module.exports = {
|
||||
server: null,
|
||||
site: null,
|
||||
visitor: false,
|
||||
vote: false,
|
||||
option: null
|
||||
},
|
||||
chat: {
|
||||
@@ -394,6 +395,9 @@ module.exports = {
|
||||
crisp: {
|
||||
website_id: null
|
||||
},
|
||||
knocket: {
|
||||
identifier: null
|
||||
},
|
||||
google_tag_manager: {
|
||||
tag_id: null,
|
||||
domain: 'https://www.googletagmanager.com'
|
||||
@@ -552,10 +556,6 @@ module.exports = {
|
||||
enable: false,
|
||||
exclude: null
|
||||
},
|
||||
aplayerInject: {
|
||||
enable: false,
|
||||
per_page: true
|
||||
},
|
||||
snackbar: {
|
||||
enable: false,
|
||||
position: 'bottom-left',
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
'use strict'
|
||||
|
||||
hexo.extend.generator.register('404', function (locals) {
|
||||
if (!hexo.theme.config.error_404.enable) return
|
||||
const error404 = hexo.theme.config.error_404 || {}
|
||||
if (!error404.enable) return
|
||||
return {
|
||||
path: '404.html',
|
||||
layout: ['page'],
|
||||
|
||||
@@ -8,11 +8,19 @@
|
||||
const { version } = require('../../package.json')
|
||||
const path = require('path')
|
||||
|
||||
// Cache plugins.yml to avoid re-rendering on every generate
|
||||
let cachedPlugins = null
|
||||
const loadPlugins = hexo => {
|
||||
if (cachedPlugins) return cachedPlugins
|
||||
cachedPlugins = hexo.render.renderSync({ path: path.join(hexo.theme_dir, '/plugins.yml'), engine: 'yaml' })
|
||||
return cachedPlugins
|
||||
}
|
||||
|
||||
hexo.extend.filter.register('before_generate', () => {
|
||||
const themeConfig = hexo.theme.config
|
||||
const { CDN } = themeConfig
|
||||
|
||||
const thirdPartySrc = hexo.render.renderSync({ path: path.join(hexo.theme_dir, '/plugins.yml'), engine: 'yaml' })
|
||||
const thirdPartySrc = loadPlugins(hexo)
|
||||
const internalSrc = {
|
||||
main: {
|
||||
name: 'hexo-theme-butterfly',
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
'use strict'
|
||||
|
||||
hexo.extend.filter.register('stylus:renderer', style => {
|
||||
const { syntax_highlighter: syntaxHighlighter, highlight, prismjs } = hexo.config
|
||||
const { syntax_highlighter: syntaxHighlighter, highlight = {}, prismjs = {} } = hexo.config
|
||||
let { enable: highlightEnable, line_number: highlightLineNumber } = highlight
|
||||
let { enable: prismjsEnable, line_number: prismjsLineNumber } = prismjs
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ const lazyload = htmlContent => {
|
||||
// Handle src attributes with double quotes, single quotes, or no quotes (unified approach)
|
||||
// Matches: src="..." or src='...' or src=... (e.g., after minification by hexo-minify)
|
||||
return htmlContent.replace(/(<img(?![^>]*?\bdata-lazy-src=)(?:\s[^>]*?)?\ssrc=)(?:"([^"]*)"|'([^']*)'|([^\s>]+))(?![^<]*<\/script>)/gi, (match, prefix, srcDoubleQuote, srcSingleQuote, srcNoQuote) => {
|
||||
const src = srcDoubleQuote || srcSingleQuote || srcNoQuote
|
||||
const src = srcDoubleQuote ?? srcSingleQuote ?? srcNoQuote
|
||||
return `${prefix}"${bg}" data-lazy-src="${src}"`
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,33 +6,46 @@
|
||||
|
||||
hexo.extend.generator.register('post', locals => {
|
||||
const imgTestReg = /\.(png|jpe?g|gif|svg|webp|avif)(\?.*)?$/i
|
||||
const remoteImgReg = /^(?:https?:)?\/\//i
|
||||
const dataImgReg = /^data:image\//i
|
||||
|
||||
const { post_asset_folder: postAssetFolder } = hexo.config
|
||||
const { cover: { default_cover: defaultCover } } = hexo.theme.config
|
||||
|
||||
const isImage = value => {
|
||||
return typeof value === 'string' &&
|
||||
(remoteImgReg.test(value) || dataImgReg.test(value) || imgTestReg.test(value))
|
||||
}
|
||||
|
||||
function * createCoverGenerator () {
|
||||
if (!defaultCover) {
|
||||
if (!defaultCover || (Array.isArray(defaultCover) && defaultCover.length === 0)) {
|
||||
while (true) yield false
|
||||
}
|
||||
|
||||
if (!Array.isArray(defaultCover)) {
|
||||
while (true) yield defaultCover
|
||||
}
|
||||
|
||||
const coverCount = defaultCover.length
|
||||
if (coverCount === 1) {
|
||||
if (defaultCover.length === 1) {
|
||||
while (true) yield defaultCover[0]
|
||||
}
|
||||
|
||||
const coverCount = defaultCover.length
|
||||
const maxHistory = Math.min(3, coverCount - 1)
|
||||
const history = []
|
||||
|
||||
while (true) {
|
||||
let index
|
||||
|
||||
do {
|
||||
index = Math.floor(Math.random() * coverCount)
|
||||
} while (history.includes(index))
|
||||
|
||||
history.push(index)
|
||||
if (history.length > maxHistory) history.shift()
|
||||
|
||||
if (history.length > maxHistory) {
|
||||
history.shift()
|
||||
}
|
||||
|
||||
yield defaultCover[index]
|
||||
}
|
||||
@@ -40,32 +53,31 @@ hexo.extend.generator.register('post', locals => {
|
||||
|
||||
const coverGenerator = createCoverGenerator()
|
||||
|
||||
const resolvePostAsset = (value, postPath) => {
|
||||
if (
|
||||
!postAssetFolder ||
|
||||
typeof value !== 'string' ||
|
||||
value.includes('/') ||
|
||||
!imgTestReg.test(value)
|
||||
) {
|
||||
return value
|
||||
}
|
||||
|
||||
return `${postPath}${value}`
|
||||
}
|
||||
|
||||
const handleImg = data => {
|
||||
let { cover: coverVal, top_img: topImg, pagination_cover: paginationCover } = data
|
||||
data.top_img = resolvePostAsset(data.top_img, data.path)
|
||||
data.cover = resolvePostAsset(data.cover, data.path)
|
||||
data.pagination_cover = resolvePostAsset(data.pagination_cover, data.path)
|
||||
|
||||
// Add path to top_img and cover if post_asset_folder is enabled
|
||||
if (postAssetFolder) {
|
||||
if (topImg && topImg.indexOf('/') === -1 && imgTestReg.test(topImg)) {
|
||||
data.top_img = `${data.path}${topImg}`
|
||||
}
|
||||
if (coverVal && coverVal.indexOf('/') === -1 && imgTestReg.test(coverVal)) {
|
||||
data.cover = `${data.path}${coverVal}`
|
||||
}
|
||||
if (paginationCover && paginationCover.indexOf('/') === -1 && imgTestReg.test(paginationCover)) {
|
||||
data.pagination_cover = `${data.path}${paginationCover}`
|
||||
}
|
||||
if (data.cover === false) return data
|
||||
|
||||
if (!data.cover) {
|
||||
data.cover = coverGenerator.next().value
|
||||
}
|
||||
|
||||
if (coverVal === false) return data
|
||||
|
||||
// If cover is not set, use random cover
|
||||
if (!coverVal) {
|
||||
const randomCover = coverGenerator.next().value
|
||||
data.cover = randomCover
|
||||
coverVal = randomCover
|
||||
}
|
||||
|
||||
if (coverVal && (coverVal.indexOf('//') !== -1 || imgTestReg.test(coverVal))) {
|
||||
if (isImage(data.cover)) {
|
||||
data.cover_type = 'img'
|
||||
}
|
||||
|
||||
@@ -75,16 +87,23 @@ hexo.extend.generator.register('post', locals => {
|
||||
const posts = locals.posts.sort('date').toArray()
|
||||
const { length } = posts
|
||||
|
||||
return posts.map((post, i) => {
|
||||
if (i) post.prev = posts[i - 1]
|
||||
if (i < length - 1) post.next = posts[i + 1]
|
||||
return posts.map((post, index) => {
|
||||
const data = post
|
||||
|
||||
post.__post = true
|
||||
if (index > 0) {
|
||||
data.prev = posts[index - 1]
|
||||
}
|
||||
|
||||
if (index < length - 1) {
|
||||
data.next = posts[index + 1]
|
||||
}
|
||||
|
||||
data.__post = true
|
||||
|
||||
return {
|
||||
data: handleImg(post),
|
||||
data: handleImg(data),
|
||||
layout: 'post',
|
||||
path: post.path
|
||||
path: data.path
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -69,18 +69,14 @@ hexo.extend.helper.register('aside_archives', function (options = {}) {
|
||||
|
||||
// Use template literal for better readability
|
||||
const archiveHeader = `
|
||||
<div class="item-headline">
|
||||
<i class="fas fa-archive"></i>
|
||||
<span>${_p('aside.card_archives')}</span>
|
||||
${
|
||||
data.length > limitedData.length
|
||||
? `<a class="card-more-btn" href="${urlFor(archiveDir)}/"
|
||||
<div class="item-headline"><i class="fas fa-archive"></i><span>${_p('aside.card_archives')}</span>${
|
||||
data.length > limitedData.length
|
||||
? `<a class="card-more-btn" href="${urlFor(archiveDir)}/"
|
||||
title="${_p('aside.more_button')}">
|
||||
<i class="fas fa-angle-right"></i>
|
||||
</a>`
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
: ''
|
||||
}</div>
|
||||
`
|
||||
|
||||
// Use map for generating list items, join for performance
|
||||
|
||||
@@ -89,11 +89,7 @@ hexo.extend.helper.register('aside_categories', function (categories, options =
|
||||
<i class="fas fa-angle-right"></i></a>`
|
||||
: ''
|
||||
|
||||
return `<div class="item-headline">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
<span>${this._p('aside.card_categories')}</span>
|
||||
${moreButton}
|
||||
</div>
|
||||
return `<div class="item-headline"><i class="fas fa-folder-open"></i><span>${this._p('aside.card_categories')}</span>${moreButton}</div>
|
||||
<ul class="card-category-list${isExpand && list.result ? ' expandBtn' : ''}" id="aside-cat-list">
|
||||
${list.result}
|
||||
</ul>`
|
||||
|
||||
@@ -17,12 +17,11 @@ hexo.extend.helper.register('getArchiveLength', function () {
|
||||
const m = date.month() + 1
|
||||
const d = date.date()
|
||||
|
||||
if (yearly) {
|
||||
const keyYear = `${y}`
|
||||
map.set(keyYear, (map.get(keyYear) || 0) + 1)
|
||||
}
|
||||
// Always track year so year archive pages can be counted
|
||||
const keyYear = `${y}`
|
||||
map.set(keyYear, (map.get(keyYear) || 0) + 1)
|
||||
|
||||
if (monthly) {
|
||||
if (monthly || daily) {
|
||||
const keyMonth = `${y}-${m}`
|
||||
map.set(keyMonth, (map.get(keyMonth) || 0) + 1)
|
||||
}
|
||||
@@ -37,8 +36,8 @@ hexo.extend.helper.register('getArchiveLength', function () {
|
||||
|
||||
// Determine the appropriate key to fetch based on current page context
|
||||
let key
|
||||
if (yearly && year) key = `${year}`
|
||||
if (monthly && month) key = `${year}-${month}`
|
||||
if (yearly || monthly || daily) key = `${year}`
|
||||
if ((monthly || daily) && month) key = `${year}-${month}`
|
||||
if (daily && day) key = `${year}-${month}-${day}`
|
||||
|
||||
// Return the count for the current period or default to the total posts
|
||||
|
||||
@@ -11,46 +11,87 @@ hexo.extend.helper.register('inject_head_js', function () {
|
||||
const createCustomJs = () => `
|
||||
const saveToLocal = {
|
||||
set: (key, value, ttl) => {
|
||||
if (!ttl) return
|
||||
const expiry = Date.now() + ttl * 86400000
|
||||
localStorage.setItem(key, JSON.stringify({ value, expiry }))
|
||||
const data = { value }
|
||||
|
||||
if (ttl != null) {
|
||||
data.expiry = Date.now() + ttl * 86400000
|
||||
}
|
||||
|
||||
localStorage.setItem(key, JSON.stringify(data))
|
||||
},
|
||||
get: key => {
|
||||
const itemStr = localStorage.getItem(key)
|
||||
if (!itemStr) return undefined
|
||||
const { value, expiry } = JSON.parse(itemStr)
|
||||
if (Date.now() > expiry) {
|
||||
if (!itemStr) return
|
||||
|
||||
try {
|
||||
const data = JSON.parse(itemStr)
|
||||
|
||||
if (data.expiry && Date.now() > data.expiry) {
|
||||
localStorage.removeItem(key)
|
||||
return
|
||||
}
|
||||
|
||||
return data.value
|
||||
} catch {
|
||||
localStorage.removeItem(key)
|
||||
return undefined
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
const scriptCache = new Map()
|
||||
const cssCache = new Map()
|
||||
window.btf = {
|
||||
saveToLocal,
|
||||
getScript: (url, attr = {}) => new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script')
|
||||
script.src = url
|
||||
script.async = true
|
||||
Object.entries(attr).forEach(([key, val]) => script.setAttribute(key, val))
|
||||
script.onload = script.onreadystatechange = () => {
|
||||
if (!script.readyState || /loaded|complete/.test(script.readyState)) resolve()
|
||||
getScript: (url, attr = {}) => {
|
||||
if (scriptCache.has(url)) {
|
||||
return scriptCache.get(url)
|
||||
}
|
||||
script.onerror = reject
|
||||
document.head.appendChild(script)
|
||||
}),
|
||||
getCSS: (url, id) => new Promise((resolve, reject) => {
|
||||
const link = document.createElement('link')
|
||||
link.rel = 'stylesheet'
|
||||
link.href = url
|
||||
if (id) link.id = id
|
||||
link.onload = link.onreadystatechange = () => {
|
||||
if (!link.readyState || /loaded|complete/.test(link.readyState)) resolve()
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script')
|
||||
|
||||
script.src = url
|
||||
script.async = true
|
||||
|
||||
for (const key in attr) {
|
||||
script.setAttribute(key, attr[key])
|
||||
}
|
||||
|
||||
script.onload = resolve
|
||||
script.onerror = reject
|
||||
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
|
||||
scriptCache.set(url, promise)
|
||||
|
||||
return promise
|
||||
},
|
||||
getCSS: (url, id) => {
|
||||
if (cssCache.has(url)) {
|
||||
return cssCache.get(url)
|
||||
}
|
||||
link.onerror = reject
|
||||
document.head.appendChild(link)
|
||||
}),
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
const link = document.createElement('link')
|
||||
|
||||
link.rel = 'stylesheet'
|
||||
link.href = url
|
||||
|
||||
if (id) {
|
||||
link.id = id
|
||||
}
|
||||
|
||||
link.onload = resolve
|
||||
link.onerror = reject
|
||||
|
||||
document.head.appendChild(link)
|
||||
})
|
||||
|
||||
cssCache.set(url, promise)
|
||||
|
||||
return promise
|
||||
},
|
||||
addGlobalFn: (key, fn, name = false, parent = window) => {
|
||||
if (!${pjax.enable} && key.startsWith('pjax')) return
|
||||
const globalFn = parent.globalFn || {}
|
||||
@@ -65,16 +106,17 @@ hexo.extend.helper.register('inject_head_js', function () {
|
||||
if (!darkmode.enable) return ''
|
||||
|
||||
let darkmodeJs = `
|
||||
const metaThemeColor = document.querySelector('meta[name="theme-color"]')
|
||||
const activateDarkMode = () => {
|
||||
document.documentElement.setAttribute('data-theme', 'dark')
|
||||
if (document.querySelector('meta[name="theme-color"]') !== null) {
|
||||
document.querySelector('meta[name="theme-color"]').setAttribute('content', '${themeColorDark}')
|
||||
document.documentElement.dataset.theme = 'dark'
|
||||
if (metaThemeColor !== null) {
|
||||
metaThemeColor.setAttribute('content', '${themeColorDark}')
|
||||
}
|
||||
}
|
||||
const activateLightMode = () => {
|
||||
document.documentElement.setAttribute('data-theme', 'light')
|
||||
if (document.querySelector('meta[name="theme-color"]') !== null) {
|
||||
document.querySelector('meta[name="theme-color"]').setAttribute('content', '${themeColorLight}')
|
||||
document.documentElement.dataset.theme = 'light'
|
||||
if (metaThemeColor !== null) {
|
||||
metaThemeColor.setAttribute('content', '${themeColorLight}')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,10 +137,15 @@ hexo.extend.helper.register('inject_head_js', function () {
|
||||
else if (mediaQueryDark.matches) activateDarkMode()
|
||||
else {
|
||||
const hour = new Date().getHours()
|
||||
const isNight = hour <= ${start} || hour >= ${end}
|
||||
const start = ${start}
|
||||
const end = ${end}
|
||||
const isNight =
|
||||
start < end
|
||||
? hour < start || hour >= end
|
||||
: hour >= start || hour < end
|
||||
isNight ? activateDarkMode() : activateLightMode()
|
||||
}
|
||||
mediaQueryDark.addEventListener('change', () => {
|
||||
mediaQueryDark.addEventListener('change', e => {
|
||||
if (saveToLocal.get('theme') === undefined) {
|
||||
e.matches ? activateDarkMode() : activateLightMode()
|
||||
}
|
||||
@@ -111,7 +158,12 @@ hexo.extend.helper.register('inject_head_js', function () {
|
||||
case 2:
|
||||
darkmodeJs += `
|
||||
const hour = new Date().getHours()
|
||||
const isNight = hour <= ${start} || hour >= ${end}
|
||||
const start = ${start}
|
||||
const end = ${end}
|
||||
const isNight =
|
||||
start < end
|
||||
? hour < start || hour >= end
|
||||
: hour >= start || hour < end
|
||||
if (theme === undefined) isNight ? activateDarkMode() : activateLightMode()
|
||||
else theme === 'light' ? activateLightMode() : activateDarkMode()
|
||||
`
|
||||
|
||||
+44
-13
@@ -11,6 +11,8 @@ const colorPattern = /^(#|rgb|rgba|hsl|hsla)/i
|
||||
const simpleFilePattern = /\.(png|jpg|jpeg|gif|bmp|webp|svg|tiff)$/i
|
||||
const archiveRegex = /\/archives\//
|
||||
|
||||
const { version: themeVersion } = require('../../package.json')
|
||||
|
||||
hexo.extend.helper.register('truncate', truncateContent)
|
||||
|
||||
hexo.extend.helper.register('postDesc', data => {
|
||||
@@ -58,8 +60,6 @@ hexo.extend.helper.register('cloudTags', function (options = {}) {
|
||||
|
||||
const userColors = normalizeColors(custom_colors)
|
||||
|
||||
const resolveColorClass = (idx) => `tag-color-${idx % userColors.length}`
|
||||
|
||||
const generateStyle = (size, unit, page, color) => {
|
||||
const colorStyle = page === 'tags' ? `background-color: ${color};` : `color: ${color};`
|
||||
return `font-size: ${parseFloat(size.toFixed(2))}${unit}; ${colorStyle}`
|
||||
@@ -70,10 +70,9 @@ hexo.extend.helper.register('cloudTags', function (options = {}) {
|
||||
const size = minfontsize + ((maxfontsize - minfontsize) * ratio)
|
||||
|
||||
if (userColors && userColors.length) {
|
||||
const colorClass = resolveColorClass(idx)
|
||||
const color = userColors[idx % userColors.length]
|
||||
const style = generateStyle(size, unit, page, color)
|
||||
return `<a href="${env.url_for(tag.path)}" class="tag-cloud-item ${colorClass}" style="${style}">${tag.name}</a>`
|
||||
return `<a href="${env.url_for(tag.path)}" class="tag-cloud-item" style="${style}">${tag.name}</a>`
|
||||
}
|
||||
|
||||
const color = getRandomColor()
|
||||
@@ -105,13 +104,13 @@ hexo.extend.helper.register('findArchivesTitle', function (page, menu, date) {
|
||||
if (!menu) return defaultTitle
|
||||
|
||||
const loop = m => {
|
||||
for (const key in m) {
|
||||
if (typeof m[key] === 'object') {
|
||||
const result = loop(m[key])
|
||||
for (const [key, value] of Object.entries(m)) {
|
||||
if (value && typeof value === 'object') {
|
||||
const result = loop(value)
|
||||
if (result) return result
|
||||
}
|
||||
|
||||
if (archiveRegex.test(m[key])) {
|
||||
if (typeof value === 'string' && archiveRegex.test(value)) {
|
||||
return key
|
||||
}
|
||||
}
|
||||
@@ -154,11 +153,44 @@ hexo.extend.helper.register('shuoshuoFN', (data, page) => {
|
||||
|
||||
// This is a hack method, because hexo treats time as UTC time
|
||||
// so you need to manually convert the time zone
|
||||
const timezone = hexo.config.timezone
|
||||
processedData.forEach(item => {
|
||||
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')
|
||||
// markdown
|
||||
const parsed = moment.utc(item.date)
|
||||
item.date = moment.tz(parsed.format('YYYY-MM-DD HH:mm:ss'), timezone).format('YYYY-MM-DD HH:mm:ss')
|
||||
|
||||
// Render the content using Hexo's rendering engine to process any tags or markdown
|
||||
const mockPost = {
|
||||
content: item.content,
|
||||
source: item.source || page.source || '',
|
||||
full_source: page.full_source || '',
|
||||
path: page.path || '',
|
||||
layout: item.layout || page.layout || 'post',
|
||||
raw: item.raw || item.content,
|
||||
draft: false,
|
||||
published: true
|
||||
}
|
||||
try {
|
||||
hexo.execFilterSync('before_post_render', mockPost, { context: hexo })
|
||||
} catch (e) {
|
||||
// ignore third-party plugin errors, fallback to markdown-only rendering
|
||||
}
|
||||
item.content = mockPost.content
|
||||
|
||||
const codeBlocks = []
|
||||
item.content = item.content.replace(/<hexoPostRenderCodeBlock>([\s\S]*?)<\/hexoPostRenderCodeBlock>/g, (_, c) => {
|
||||
codeBlocks.push(c)
|
||||
return `<!--CODEBLOCK_${codeBlocks.length - 1}-->`
|
||||
})
|
||||
|
||||
try {
|
||||
item.content = hexo.extend.tag.env.renderString(item.content, {})
|
||||
} catch (e) {
|
||||
// ignore tag plugin errors, fallback to markdown-only rendering
|
||||
}
|
||||
|
||||
item.content = hexo.render.renderSync({ text: item.content, engine: 'markdown' })
|
||||
|
||||
item.content = item.content.replace(/<!--CODEBLOCK_(\d+)-->/g, (_, i) => codeBlocks[+i])
|
||||
})
|
||||
|
||||
return processedData
|
||||
@@ -179,8 +211,7 @@ hexo.extend.helper.register('getPageType', (page, isHome) => {
|
||||
})
|
||||
|
||||
hexo.extend.helper.register('getVersion', () => {
|
||||
const { version } = require('../../package.json')
|
||||
return { hexo: hexo.version, theme: version }
|
||||
return { hexo: hexo.version, theme: themeVersion }
|
||||
})
|
||||
|
||||
hexo.extend.helper.register('safeJSON', data => {
|
||||
|
||||
@@ -23,7 +23,6 @@ hexo.extend.helper.register('related_posts', function (currentPost) {
|
||||
if (relatedPosts.has(post.path)) {
|
||||
relatedPosts.get(post.path).weight += 1
|
||||
} else {
|
||||
const getPostDesc = post.postDesc || postDesc(post, hexo)
|
||||
relatedPosts.set(post.path, {
|
||||
title: post.title,
|
||||
path: post.path,
|
||||
@@ -32,7 +31,7 @@ hexo.extend.helper.register('related_posts', function (currentPost) {
|
||||
weight: 1,
|
||||
updated: post.updated,
|
||||
created: post.date,
|
||||
postDesc: getPostDesc,
|
||||
post,
|
||||
random: Math.random()
|
||||
})
|
||||
}
|
||||
@@ -61,12 +60,15 @@ hexo.extend.helper.register('related_posts', function (currentPost) {
|
||||
result += `<div class="headline"><i class="fas fa-thumbs-up fa-fw"></i><span>${headlineLang}</span></div>`
|
||||
result += '<div class="relatedPosts-list">'
|
||||
|
||||
for (let i = 0; i < Math.min(relatedPostsList.length, limitNum); i++) {
|
||||
let { cover, title, path, cover_type, created, updated, postDesc } = relatedPostsList[i]
|
||||
const max = Math.min(relatedPostsList.length, limitNum)
|
||||
for (let i = 0; i < max; i++) {
|
||||
const item = relatedPostsList[i]
|
||||
let { cover, title, path, cover_type, created, updated, post } = item
|
||||
const { escape_html, url_for, date } = this
|
||||
cover = cover || 'var(--default-bg-color)'
|
||||
title = escape_html(title)
|
||||
const className = postDesc ? 'pagination-related' : 'pagination-related no-desc'
|
||||
const desc = post.postDesc || postDesc(post, hexo)
|
||||
const className = desc ? 'pagination-related' : 'pagination-related no-desc'
|
||||
result += `<a class="${className}" href="${url_for(path)}" title="${title}">`
|
||||
if (cover_type === 'img') {
|
||||
result += `<img class="cover" src="${url_for(cover)}" alt="cover">`
|
||||
@@ -80,8 +82,8 @@ hexo.extend.helper.register('related_posts', function (currentPost) {
|
||||
}
|
||||
result += `<div class="info-item-2">${title}</div></div>`
|
||||
|
||||
if (postDesc) {
|
||||
result += `<div class="info-2"><div class="info-item-1">${postDesc}</div></div>`
|
||||
if (desc) {
|
||||
result += `<div class="info-2"><div class="info-item-1">${desc}</div></div>`
|
||||
}
|
||||
result += '</div></a>'
|
||||
}
|
||||
|
||||
+24
-22
@@ -1,22 +1,24 @@
|
||||
'use strict'
|
||||
|
||||
hexo.extend.helper.register('groupPosts', function () {
|
||||
const getGroupArray = array => {
|
||||
return array.reduce((groups, item) => {
|
||||
const key = item.series
|
||||
if (key) {
|
||||
groups[key] = groups[key] || []
|
||||
groups[key].push(item)
|
||||
}
|
||||
return groups
|
||||
}, {})
|
||||
}
|
||||
|
||||
const sortPosts = posts => {
|
||||
const { orderBy = 'date', order = 1 } = this.theme.aside.card_post_series
|
||||
if (orderBy === 'title') return posts.sort('title', order)
|
||||
return posts.sort('date', order)
|
||||
}
|
||||
|
||||
return getGroupArray(sortPosts(this.site.posts))
|
||||
})
|
||||
'use strict'
|
||||
|
||||
// Read pre-built groups from tag/series.js (hexo._seriesGroups)
|
||||
// and sort each group according to the sidebar card config.
|
||||
hexo.extend.helper.register('groupPosts', function () {
|
||||
const groups = hexo._seriesGroups || {}
|
||||
const { orderBy = 'date', order = 1 } = this.theme.aside.card_post_series
|
||||
|
||||
const result = {}
|
||||
Object.keys(groups).forEach(key => {
|
||||
const arr = groups[key].slice() // copy to avoid mutating the shared data
|
||||
if (orderBy === 'title') {
|
||||
arr.sort((a, b) => {
|
||||
const cmp = a.title.toUpperCase().localeCompare(b.title.toUpperCase())
|
||||
return order === 1 ? cmp : -cmp
|
||||
})
|
||||
} else {
|
||||
arr.sort((a, b) => order === 1 ? a.date - b.date : b.date - a.date)
|
||||
}
|
||||
result[key] = arr
|
||||
})
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
@@ -7,15 +7,16 @@
|
||||
|
||||
'use strict'
|
||||
|
||||
const urlFor = require('hexo-util').url_for.bind(hexo)
|
||||
const { url_for, escapeHTML } = require('hexo-util')
|
||||
const urlFor = url_for.bind(hexo)
|
||||
|
||||
const btn = args => {
|
||||
const [url = '', text = '', icon = '', option = ''] = args.join(' ').split(',').map(arg => arg.trim())
|
||||
|
||||
const iconHTML = icon ? `<i class="${icon}"></i>` : ''
|
||||
const textHTML = text ? `<span>${text}</span>` : ''
|
||||
const iconHTML = icon ? `<i class="${escapeHTML(icon)}"></i>` : ''
|
||||
const textHTML = text ? `<span>${escapeHTML(text)}</span>` : ''
|
||||
|
||||
return `<a class="btn-beautify ${option}" href="${urlFor(url)}" title="${text}">${iconHTML}${textHTML}</a>`
|
||||
return `<a class="btn-beautify ${escapeHTML(option)}" href="${urlFor(url)}" title="${escapeHTML(text)}">${iconHTML}${textHTML}</a>`
|
||||
}
|
||||
|
||||
hexo.extend.tag.register('btn', btn, { ends: false })
|
||||
|
||||
@@ -4,24 +4,25 @@
|
||||
|
||||
'use strict'
|
||||
|
||||
const urlFor = require('hexo-util').url_for.bind(hexo)
|
||||
const { url_for, escapeHTML } = require('hexo-util')
|
||||
const urlFor = url_for.bind(hexo)
|
||||
|
||||
const flinkFn = (args, content) => {
|
||||
const data = hexo.render.renderSync({ text: content, engine: 'yaml' })
|
||||
let result = ''
|
||||
|
||||
data.forEach(item => {
|
||||
const className = item.class_name ? `<div class="flink-name">${item.class_name}</div>` : ''
|
||||
const classDesc = item.class_desc ? `<div class="flink-desc">${item.class_desc}</div>` : ''
|
||||
const className = item.class_name ? `<div class="flink-name">${escapeHTML(item.class_name)}</div>` : ''
|
||||
const classDesc = item.class_desc ? `<div class="flink-desc">${escapeHTML(item.class_desc)}</div>` : ''
|
||||
|
||||
const listResult = item.link_list.map(link => `
|
||||
<div class="flink-list-item">
|
||||
<a href="${link.link}" title="${link.name}" target="_blank">
|
||||
<a href="${escapeHTML(link.link)}" title="${escapeHTML(link.name)}" target="_blank">
|
||||
<div class="flink-item-icon">
|
||||
<img class="no-lightbox" src="${link.avatar}" onerror='this.onerror=null;this.src="${urlFor(hexo.theme.config.error_img.flink)}"' alt="${link.name}" />
|
||||
<img class="no-lightbox" src="${escapeHTML(link.avatar)}" onerror='this.onerror=null;this.src="${urlFor(hexo.theme.config.error_img.flink)}"' alt="${escapeHTML(link.name)}" />
|
||||
</div>
|
||||
<div class="flink-item-name">${link.name}</div>
|
||||
<div class="flink-item-desc" title="${link.descr}">${link.descr}</div>
|
||||
<div class="flink-item-name">${escapeHTML(link.name)}</div>
|
||||
<div class="flink-item-desc" title="${escapeHTML(link.descr)}">${escapeHTML(link.descr)}</div>
|
||||
</a>
|
||||
</div>`).join('')
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ const parseGalleryArgs = args => {
|
||||
const parseImageContent = content => {
|
||||
const images = []
|
||||
let match
|
||||
IMAGE_REGEX.lastIndex = 0
|
||||
|
||||
while ((match = IMAGE_REGEX.exec(content)) !== null) {
|
||||
images.push({
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
|
||||
'use strict'
|
||||
|
||||
const parseArgs = args => args.join(' ').split(',')
|
||||
const parseArgs = args => args.join(' ').split(',').map(s => s.trim())
|
||||
|
||||
const generateStyle = (bg, color) => {
|
||||
let style = 'style="'
|
||||
|
||||
+3
-13
@@ -5,20 +5,10 @@
|
||||
|
||||
'use strict'
|
||||
|
||||
const { escapeHTML } = require('hexo-util')
|
||||
|
||||
const score = (args, content) => {
|
||||
// Escape HTML tags and some special characters, including curly braces
|
||||
const escapeHtmlTags = s => {
|
||||
const lookup = {
|
||||
'&': '&',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'{': '{',
|
||||
'}': '}'
|
||||
}
|
||||
return s.replace(/[&"'<>{}]/g, c => lookup[c])
|
||||
}
|
||||
const escapeHtmlTags = s => escapeHTML(s).replace(/[{}]/g, c => c === '{' ? '{' : '}')
|
||||
|
||||
const trimmed = content.trim()
|
||||
// Split content using six dashes as a delimiter
|
||||
|
||||
+88
-63
@@ -1,63 +1,88 @@
|
||||
/**
|
||||
* series plugin
|
||||
* Syntax:
|
||||
* {% series [series name] %}
|
||||
* Usage:
|
||||
* {% series %}
|
||||
* {% series series1 %}
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const urlFor = require('hexo-util').url_for.bind(hexo)
|
||||
const groups = {}
|
||||
|
||||
hexo.extend.filter.register('before_post_render', data => {
|
||||
if (!hexo.theme.config.series.enable) return data
|
||||
|
||||
const { layout, series } = data
|
||||
if (layout === 'post' && series) {
|
||||
if (!groups[series]) groups[series] = []
|
||||
groups[series].push({
|
||||
title: data.title,
|
||||
path: data.path,
|
||||
date: data.date.unix()
|
||||
})
|
||||
}
|
||||
return data
|
||||
})
|
||||
|
||||
function series (args) {
|
||||
const { series } = hexo.theme.config
|
||||
if (!series.enable) {
|
||||
hexo.log.warn('Series plugin is disabled in the theme config')
|
||||
return ''
|
||||
}
|
||||
|
||||
const seriesArr = args.length ? groups[args[0]] : groups[this.series]
|
||||
|
||||
if (!seriesArr) {
|
||||
hexo.log.warn(`There is no series named "${args[0]}"`)
|
||||
return ''
|
||||
}
|
||||
|
||||
const isAsc = (series.order || 1) === 1 // 1: asc, -1: desc
|
||||
const isSortByTitle = series.orderBy === 'title'
|
||||
|
||||
const compareFn = (a, b) => {
|
||||
const itemA = isSortByTitle ? a.title.toUpperCase() : a.date
|
||||
const itemB = isSortByTitle ? b.title.toUpperCase() : b.date
|
||||
|
||||
return itemA < itemB ? (isAsc ? -1 : 1) : itemA > itemB ? (isAsc ? 1 : -1) : 0
|
||||
}
|
||||
|
||||
seriesArr.sort(compareFn)
|
||||
|
||||
const listItems = seriesArr.map(ele =>
|
||||
`<li><a href="${urlFor(ele.path)}" title="${ele.title}">${ele.title}</a></li>`
|
||||
).join('')
|
||||
|
||||
return series.number ? `<ol class="series-items">${listItems}</ol>` : `<ul class="series-items">${listItems}</ul>`
|
||||
}
|
||||
|
||||
hexo.extend.tag.register('series', series, { ends: false })
|
||||
/**
|
||||
* series plugin
|
||||
* Syntax:
|
||||
* {% series [series name] %}
|
||||
* Usage:
|
||||
* {% series %}
|
||||
* {% series series1 %}
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const urlFor = require('hexo-util').url_for.bind(hexo)
|
||||
|
||||
// Iterate all posts once and group by series name.
|
||||
// Result is stored on hexo._seriesGroups, shared by both tag and helper.
|
||||
function buildSeriesGroups () {
|
||||
const groups = {}
|
||||
const posts = hexo.model('Post').toArray()
|
||||
for (let i = 0, len = posts.length; i < len; i++) {
|
||||
const p = posts[i]
|
||||
if (p.series) {
|
||||
if (!groups[p.series]) groups[p.series] = []
|
||||
groups[p.series].push({
|
||||
title: p.title,
|
||||
path: p.path,
|
||||
date: p.date.valueOf()
|
||||
})
|
||||
}
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
// Build once before the first render (hexo generate / hexo server initial load).
|
||||
// before_generate runs after tag rendering, so we need this as a fallback.
|
||||
hexo.extend.filter.register('before_post_render', data => {
|
||||
if (!hexo.theme.config.series.enable) return data
|
||||
if (!hexo._seriesGroups) {
|
||||
hexo._seriesGroups = buildSeriesGroups()
|
||||
}
|
||||
return data
|
||||
})
|
||||
|
||||
// Rebuild after each generate cycle in hexo server to reflect latest post data.
|
||||
hexo.extend.filter.register('before_generate', () => {
|
||||
if (!hexo.theme.config.series.enable) return
|
||||
hexo._seriesGroups = buildSeriesGroups()
|
||||
})
|
||||
|
||||
function series (args) {
|
||||
const { series } = hexo.theme.config
|
||||
if (!series.enable) {
|
||||
hexo.log.warn('Series plugin is disabled in the theme config')
|
||||
return ''
|
||||
}
|
||||
|
||||
const groupName = args.length ? args[0] : this.series
|
||||
|
||||
if (!groupName) {
|
||||
hexo.log.warn('No series specified')
|
||||
return ''
|
||||
}
|
||||
|
||||
const groups = hexo._seriesGroups || {}
|
||||
const source = groups[groupName]
|
||||
|
||||
if (!source || !source.length) {
|
||||
hexo.log.warn(`There is no series named "${groupName}"`)
|
||||
return ''
|
||||
}
|
||||
|
||||
const isAsc = (series.order || 1) === 1
|
||||
const isSortByTitle = series.orderBy === 'title'
|
||||
|
||||
// .slice() to copy before sorting, so we don't mutate the shared data
|
||||
const seriesArr = source.slice().sort((a, b) => {
|
||||
const itemA = isSortByTitle ? a.title.toUpperCase() : a.date
|
||||
const itemB = isSortByTitle ? b.title.toUpperCase() : b.date
|
||||
return itemA < itemB ? (isAsc ? -1 : 1) : itemA > itemB ? (isAsc ? 1 : -1) : 0
|
||||
})
|
||||
|
||||
const listItems = seriesArr.map(ele =>
|
||||
`<li><a href="${urlFor(ele.path)}" title="${ele.title}">${ele.title}</a></li>`
|
||||
).join('')
|
||||
|
||||
return series.number ? `<ol class="series-items">${listItems}</ol>` : `<ul class="series-items">${listItems}</ul>`
|
||||
}
|
||||
|
||||
hexo.extend.tag.register('series', series, { ends: false })
|
||||
|
||||
@@ -131,6 +131,10 @@ body
|
||||
*::-webkit-scrollbar-track
|
||||
background-color: transparent
|
||||
|
||||
// make the scrollbar transparent when fancybox is open
|
||||
html.with-fancybox
|
||||
--scrollbar-color: transparent
|
||||
|
||||
input::placeholder
|
||||
color: var(--font-color)
|
||||
|
||||
|
||||
@@ -28,6 +28,107 @@
|
||||
.wl-comment-actions > button:not(last-child)
|
||||
padding-right: 4px
|
||||
|
||||
if hexo-config('artalk.vote')
|
||||
.artalk-vote
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
margin: 4px 0 20px
|
||||
gap: 10px
|
||||
|
||||
.artalk-page-vote-up,
|
||||
.artalk-page-vote-down
|
||||
position: relative
|
||||
display: inline-flex
|
||||
align-items: center
|
||||
overflow: hidden
|
||||
padding: 5px 16px
|
||||
border: 1px solid alpha($theme-color, .15)
|
||||
gap: 6px
|
||||
addBorderRadius(8)
|
||||
background: var(--card-bg)
|
||||
color: var(--font-color)
|
||||
font-weight: 500
|
||||
font-size: .95em
|
||||
cursor: pointer
|
||||
transition: all .2s ease
|
||||
user-select: none
|
||||
|
||||
i,
|
||||
.artalk-page-vote-up-count,
|
||||
.artalk-page-vote-down-count
|
||||
position: relative
|
||||
z-index: 1
|
||||
|
||||
i
|
||||
font-size: 1em
|
||||
line-height: 1
|
||||
|
||||
.artalk-page-vote-up-count,
|
||||
.artalk-page-vote-down-count
|
||||
min-width: 1em
|
||||
text-align: center
|
||||
font-weight: 600
|
||||
font-size: .9em
|
||||
font-variant-numeric: tabular-nums
|
||||
|
||||
&::after
|
||||
position: absolute
|
||||
top: 50%
|
||||
left: 50%
|
||||
z-index: 0
|
||||
width: 8px
|
||||
height: 8px
|
||||
border-radius: 50%
|
||||
content: ''
|
||||
opacity: 0
|
||||
pointer-events: none
|
||||
|
||||
&:hover
|
||||
border-color: $theme-color
|
||||
|
||||
&:active
|
||||
transform: scale(.95)
|
||||
|
||||
&::after
|
||||
animation: artalkVoteRipple .6s ease-out forwards
|
||||
|
||||
.artalk-page-vote-up
|
||||
&:hover,
|
||||
&.active
|
||||
border-color: $theme-color
|
||||
background: alpha($theme-color, .1)
|
||||
color: $theme-color
|
||||
|
||||
&::after
|
||||
background: alpha($theme-color, .5)
|
||||
|
||||
.artalk-page-vote-down
|
||||
&:hover,
|
||||
&.active
|
||||
border-color: $light-red
|
||||
background: alpha($light-red, .1)
|
||||
color: $light-red
|
||||
|
||||
&::after
|
||||
background: alpha($light-red, .5)
|
||||
|
||||
+maxWidth600()
|
||||
gap: 8px
|
||||
|
||||
.artalk-page-vote-up,
|
||||
.artalk-page-vote-down
|
||||
padding: 4px 12px
|
||||
|
||||
@keyframes artalkVoteRipple
|
||||
0%
|
||||
opacity: .8
|
||||
transform: translate(-50%, -50%) scale(0)
|
||||
|
||||
100%
|
||||
opacity: 0
|
||||
transform: translate(-50%, -50%) scale(20)
|
||||
|
||||
if hexo-config('valine.bg')
|
||||
#vcomment
|
||||
textarea
|
||||
@@ -193,26 +294,6 @@ if hexo-config('math.use')
|
||||
right: 0
|
||||
bottom: 0
|
||||
|
||||
.aplayer
|
||||
color: $font-black
|
||||
|
||||
.container
|
||||
.aplayer
|
||||
margin: 0 0 20px
|
||||
|
||||
if hexo-config('beautify.enable')
|
||||
ol,
|
||||
ul
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
li
|
||||
margin: 0
|
||||
padding: 0 15px
|
||||
|
||||
&:before
|
||||
content: none
|
||||
|
||||
.snackbar-container.snackbar-css
|
||||
addBorderRadius(5)
|
||||
opacity: .85 !important
|
||||
|
||||
@@ -105,9 +105,6 @@ if hexo-config('darkmode.enable') || hexo-config('display_mode') == 'dark'
|
||||
code
|
||||
background: $code-background
|
||||
|
||||
.aplayer
|
||||
filter: brightness(.8)
|
||||
|
||||
kbd
|
||||
border-color: #696969
|
||||
background-color: #525252
|
||||
|
||||
+197
-171
@@ -2,18 +2,25 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
let headerContentWidth, $nav
|
||||
let mobileSidebarOpen = false
|
||||
|
||||
// rightsideScrollPercent
|
||||
let goUpElement = null
|
||||
let scrollPercentElement = null
|
||||
|
||||
const adjustMenu = init => {
|
||||
const getAllWidth = ele => Array.from(ele).reduce((width, i) => width + i.offsetWidth, 0)
|
||||
let hideMenuIndex = false
|
||||
|
||||
if (init) {
|
||||
const blogInfoWidth = getAllWidth(document.querySelector('#blog-info > a').children)
|
||||
const menusWidth = getAllWidth(document.getElementById('menus').children)
|
||||
const blogInfoWidth = Array.from(document.querySelector('#blog-info > a').children).reduce((w, i) => w + i.offsetWidth, 0)
|
||||
const menusWidth = Array.from(document.getElementById('menus').children).reduce((w, i) => w + i.offsetWidth, 0)
|
||||
headerContentWidth = blogInfoWidth + menusWidth
|
||||
$nav = document.getElementById('nav')
|
||||
}
|
||||
|
||||
const hideMenuIndex = window.innerWidth <= 768 || headerContentWidth > $nav.offsetWidth - 120
|
||||
$nav.classList.toggle('hide-menu', hideMenuIndex)
|
||||
hideMenuIndex = window.innerWidth <= 768 || headerContentWidth > $nav.offsetWidth - 120
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
$nav.classList.toggle('hide-menu', hideMenuIndex)
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化header
|
||||
@@ -54,7 +61,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
* 代碼
|
||||
* 只適用於Hexo默認的代碼渲染
|
||||
*/
|
||||
const addHighlightTool = () => {
|
||||
const addHighlightTool = $article => {
|
||||
const highLight = GLOBAL_CONFIG.highlight
|
||||
if (!highLight) return
|
||||
|
||||
@@ -64,8 +71,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const isNotHighlightJs = plugin !== 'highlight.js'
|
||||
const isPrismjs = plugin === 'prismjs'
|
||||
const $figureHighlight = isNotHighlightJs
|
||||
? Array.from(document.querySelectorAll('code[class*="language-"]')).map(code => code.parentElement)
|
||||
: document.querySelectorAll('figure.highlight')
|
||||
? Array.from($article.querySelectorAll('code[class*="language-"]')).map(code => code.parentElement)
|
||||
: $article.querySelectorAll('figure.highlight')
|
||||
|
||||
if (!((isShowTool || highlightHeightLimit) && $figureHighlight.length)) return
|
||||
|
||||
@@ -167,33 +174,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// 獲取隱藏狀態下元素的真實高度
|
||||
const getActualHeight = item => {
|
||||
if (item.offsetHeight > 0) return item.offsetHeight
|
||||
const hiddenElements = new Map()
|
||||
|
||||
const fix = () => {
|
||||
let current = item
|
||||
while (current !== document.body && current != null) {
|
||||
if (window.getComputedStyle(current).display === 'none') {
|
||||
hiddenElements.set(current, current.getAttribute('style') || '')
|
||||
}
|
||||
current = current.parentNode
|
||||
}
|
||||
const clone = item.cloneNode(true)
|
||||
|
||||
const style = 'visibility: hidden !important; display: block !important;'
|
||||
hiddenElements.forEach((originalStyle, elem) => {
|
||||
elem.setAttribute('style', originalStyle ? originalStyle + ';' + style : style)
|
||||
})
|
||||
}
|
||||
clone.style.cssText = `
|
||||
position: absolute !important;
|
||||
visibility: hidden !important;
|
||||
display: block !important;
|
||||
left: 0 !important;
|
||||
top: 0 !important;
|
||||
pointer-events: none !important;
|
||||
z-index: -1 !important;
|
||||
margin: 0 !important;
|
||||
`
|
||||
|
||||
const restore = () => {
|
||||
hiddenElements.forEach((originalStyle, elem) => {
|
||||
if (originalStyle === '') elem.removeAttribute('style')
|
||||
else elem.setAttribute('style', originalStyle)
|
||||
})
|
||||
}
|
||||
|
||||
fix()
|
||||
const height = item.offsetHeight
|
||||
restore()
|
||||
item.parentNode.insertBefore(clone, item)
|
||||
const height = clone.offsetHeight
|
||||
clone.remove()
|
||||
return height
|
||||
}
|
||||
|
||||
@@ -244,9 +241,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
/**
|
||||
* PhotoFigcaption
|
||||
*/
|
||||
const addPhotoFigcaption = () => {
|
||||
const addPhotoFigcaption = $article => {
|
||||
if (!GLOBAL_CONFIG.isPhotoFigcaption) return
|
||||
document.querySelectorAll('#article-container img').forEach(item => {
|
||||
$article.querySelectorAll('img').forEach(item => {
|
||||
const altValue = item.title || item.alt
|
||||
if (!altValue) return
|
||||
const ele = document.createElement('div')
|
||||
@@ -259,8 +256,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
/**
|
||||
* Lightbox
|
||||
*/
|
||||
const runLightbox = () => {
|
||||
btf.loadLightbox(document.querySelectorAll('#article-container img:not(.no-lightbox)'))
|
||||
const runLightbox = $article => {
|
||||
btf.loadLightbox($article.querySelectorAll('img:not(.no-lightbox)'))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -270,18 +267,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const fetchUrl = async url => {
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch URL:', error)
|
||||
return []
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const runJustifiedGallery = (container, data, config) => {
|
||||
const { isButton, limit, firstLimit, tabs } = config
|
||||
const { isButton, tabs } = config
|
||||
const limit = Math.max(1, Number(config.limit) || 20)
|
||||
const firstLimit = Math.max(1, Number(config.firstLimit) || limit)
|
||||
|
||||
const dataLength = data.length
|
||||
const maxGroupKey = Math.ceil((dataLength - firstLimit) / limit + 1)
|
||||
const maxGroupKey = dataLength
|
||||
? Math.ceil(Math.max(0, dataLength - firstLimit) / limit) + 1
|
||||
: 0
|
||||
|
||||
// Gallery configuration
|
||||
const igConfig = {
|
||||
@@ -298,13 +300,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
let isLayoutHidden = false
|
||||
|
||||
// Utility functions
|
||||
const sanitizeString = str => (str && str.replace(/"/g, '"')) || ''
|
||||
const sanitizeString = str => String(str ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
|
||||
const createImageItem = item => {
|
||||
const alt = item.alt ? `alt="${sanitizeString(item.alt)}"` : ''
|
||||
const title = item.title ? `title="${sanitizeString(item.title)}"` : ''
|
||||
const url = item.url ? sanitizeString(item.url) : ''
|
||||
return `<div class="item">
|
||||
<img src="${item.url}" data-grid-maintained-target="true" ${alt} ${title} />
|
||||
<img src="${url}" data-grid-maintained-target="true" ${alt} ${title} />
|
||||
</div>`
|
||||
}
|
||||
|
||||
@@ -373,11 +381,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
btf.setLoading.add(container)
|
||||
ig.on('renderComplete', handleRenderComplete)
|
||||
|
||||
if (isButton) {
|
||||
if (isButton && dataLength) {
|
||||
appendItems(1, firstLimit, true)
|
||||
} else {
|
||||
} else if (dataLength) {
|
||||
ig.on('requestAppend', handleRequestAppend)
|
||||
ig.renderItems()
|
||||
} else {
|
||||
btf.setLoading.remove(container)
|
||||
}
|
||||
|
||||
btf.addGlobalFn('pjaxSendOnce', () => ig.destroy())
|
||||
@@ -400,11 +410,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const container = element.firstElementChild
|
||||
const content = container.textContent
|
||||
container.textContent = ''
|
||||
element.classList.add('loaded')
|
||||
|
||||
try {
|
||||
const data = element.getAttribute('data-type') === 'url' ? await fetchUrl(content) : JSON.parse(content)
|
||||
if (!Array.isArray(data)) throw new TypeError('Gallery data must be an array')
|
||||
runJustifiedGallery(container, data, config)
|
||||
element.classList.add('loaded')
|
||||
} catch (error) {
|
||||
console.error('Gallery data parsing failed:', error)
|
||||
}
|
||||
@@ -424,11 +434,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
*/
|
||||
const rightsideScrollPercent = currentTop => {
|
||||
const scrollPercent = btf.getScrollPercent(currentTop, document.body)
|
||||
const goUpElement = document.getElementById('go-up')
|
||||
|
||||
if (!goUpElement || !scrollPercentElement) return
|
||||
if (scrollPercent < 95) {
|
||||
goUpElement.classList.add('show-percent')
|
||||
goUpElement.querySelector('.scroll-percent').textContent = scrollPercent
|
||||
scrollPercentElement.textContent = scrollPercent
|
||||
} else {
|
||||
goUpElement.classList.remove('show-percent')
|
||||
}
|
||||
@@ -439,24 +449,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
*/
|
||||
const scrollFn = () => {
|
||||
const $rightside = document.getElementById('rightside')
|
||||
const innerHeight = window.innerHeight + 56
|
||||
let initTop = 0
|
||||
const $header = document.getElementById('page-header')
|
||||
const isChatBtn = typeof chatBtn !== 'undefined'
|
||||
const isChatBtn = typeof window.chatBtn !== 'undefined'
|
||||
const isShowPercent = GLOBAL_CONFIG.percent.rightside
|
||||
|
||||
// 檢查文檔高度是否小於視窗高度
|
||||
const checkDocumentHeight = () => {
|
||||
if (document.body.scrollHeight <= innerHeight) {
|
||||
if (document.body.scrollHeight <= window.innerHeight + 56) {
|
||||
$rightside.classList.add('rightside-show')
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果文檔高度小於視窗高度,直接返回
|
||||
if (checkDocumentHeight()) return
|
||||
|
||||
// find the scroll direction
|
||||
const scrollDirection = currentTop => {
|
||||
const result = currentTop > initTop // true is down & false is up
|
||||
@@ -465,7 +471,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
let flag = ''
|
||||
const scrollTask = btf.throttle(() => {
|
||||
const scrollTask = btf.rafThrottle(() => {
|
||||
if (checkDocumentHeight()) return
|
||||
|
||||
const currentTop = window.scrollY || document.documentElement.scrollTop
|
||||
const isDown = scrollDirection(currentTop)
|
||||
if (currentTop > 56) {
|
||||
@@ -496,19 +504,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
isShowPercent && rightsideScrollPercent(currentTop)
|
||||
checkDocumentHeight()
|
||||
}, 300)
|
||||
})
|
||||
|
||||
checkDocumentHeight()
|
||||
btf.addEventListenerPjax(window, 'scroll', scrollTask, { passive: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* toc,anchor
|
||||
* toc, anchor
|
||||
*/
|
||||
const scrollFnToDo = () => {
|
||||
const scrollFnToDo = $article => {
|
||||
const isToc = GLOBAL_CONFIG_SITE.isToc
|
||||
const isAnchor = GLOBAL_CONFIG.isAnchor
|
||||
const $article = document.getElementById('article-container')
|
||||
|
||||
if (!($article && (isToc || isAnchor))) return
|
||||
|
||||
@@ -521,7 +528,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
$tocPercentage = $cardTocLayout.querySelector('.toc-percentage')
|
||||
isExpand = $cardToc.classList.contains('is-expand')
|
||||
|
||||
// toc元素點擊
|
||||
const tocItemClickFn = e => {
|
||||
const target = e.target.closest('.toc-link')
|
||||
if (!target) return
|
||||
@@ -548,101 +554,105 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 處理 hexo-blog-encrypt 事件
|
||||
$cardToc.style.display = 'block'
|
||||
}
|
||||
|
||||
// find head position & add active class
|
||||
const $articleList = $article.querySelectorAll('h1,h2,h3,h4,h5,h6')
|
||||
let detectItem = ''
|
||||
if (!$articleList.length) return
|
||||
|
||||
// Optimization: Cache header positions
|
||||
let headerList = []
|
||||
const updateHeaderPositions = () => {
|
||||
headerList = Array.from($articleList).map(ele => ({
|
||||
ele,
|
||||
top: btf.getEleTop(ele),
|
||||
id: ele.id
|
||||
}))
|
||||
}
|
||||
let activeTocItem = null
|
||||
let activeParentItems = []
|
||||
|
||||
updateHeaderPositions()
|
||||
const throttledUpdate = btf.throttle(updateHeaderPositions, 200)
|
||||
btf.addEventListenerPjax(window, 'resize', throttledUpdate)
|
||||
const updateTocUI = currentId => {
|
||||
const encodedAnchor = currentId ? '#' + encodeURI(decodeURI(currentId)) : ''
|
||||
if (isAnchor) btf.updateAnchor(encodedAnchor)
|
||||
|
||||
if ('ResizeObserver' in window) {
|
||||
const observer = new ResizeObserver(throttledUpdate)
|
||||
observer.observe($article)
|
||||
btf.addGlobalFn('pjaxSendOnce', () => { observer.disconnect() })
|
||||
}
|
||||
if (!isToc) return
|
||||
|
||||
const findHeadPosition = top => {
|
||||
if (top === 0) return false
|
||||
if (activeTocItem) activeTocItem.classList.remove('active')
|
||||
activeParentItems.forEach(i => i.classList.remove('active'))
|
||||
activeParentItems = []
|
||||
|
||||
let currentId = ''
|
||||
let currentIndex = ''
|
||||
|
||||
for (let i = 0; i < headerList.length; i++) {
|
||||
const item = headerList[i]
|
||||
if (top > item.top - 80) {
|
||||
currentId = item.id ? '#' + encodeURI(item.id) : ''
|
||||
currentIndex = i
|
||||
} else {
|
||||
break
|
||||
}
|
||||
if (!currentId) {
|
||||
activeTocItem = null
|
||||
return
|
||||
}
|
||||
|
||||
if (detectItem === currentIndex) return
|
||||
const targetLink = Array.from($tocLink).find(link => {
|
||||
const href = link.getAttribute('href')
|
||||
if (!href) return false
|
||||
return decodeURI(href).replace('#', '') === decodeURI(currentId)
|
||||
})
|
||||
|
||||
if (isAnchor) btf.updateAnchor(currentId)
|
||||
if (!targetLink) return
|
||||
|
||||
detectItem = currentIndex
|
||||
targetLink.classList.add('active')
|
||||
activeTocItem = targetLink
|
||||
setTimeout(() => autoScrollToc(targetLink), 0)
|
||||
|
||||
if (isToc) {
|
||||
$cardToc.querySelectorAll('.active').forEach(i => i.classList.remove('active'))
|
||||
|
||||
if (currentId) {
|
||||
const currentActive = $tocLink[currentIndex]
|
||||
currentActive.classList.add('active')
|
||||
|
||||
setTimeout(() => autoScrollToc(currentActive), 0)
|
||||
|
||||
if (!isExpand) {
|
||||
let parent = currentActive.parentNode
|
||||
while (!parent.matches('.toc')) {
|
||||
if (parent.matches('li')) parent.classList.add('active')
|
||||
parent = parent.parentNode
|
||||
}
|
||||
if (!isExpand) {
|
||||
let parent = targetLink.parentNode
|
||||
while (!parent.matches('.toc')) {
|
||||
if (parent.matches('li')) {
|
||||
parent.classList.add('active')
|
||||
activeParentItems.push(parent)
|
||||
}
|
||||
parent = parent.parentNode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// main of scroll
|
||||
const tocScrollFn = btf.throttle(() => {
|
||||
const observerOptions = {
|
||||
root: null,
|
||||
rootMargin: '-60px 0px -80% 0px',
|
||||
threshold: 0
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
updateTocUI(entry.target.id)
|
||||
}
|
||||
})
|
||||
}, observerOptions)
|
||||
|
||||
$articleList.forEach(ele => observer.observe(ele))
|
||||
|
||||
const scrollHandler = btf.rafThrottle(() => {
|
||||
const currentTop = window.scrollY || document.documentElement.scrollTop
|
||||
|
||||
if (isToc && GLOBAL_CONFIG.percent.toc) {
|
||||
$tocPercentage.textContent = btf.getScrollPercent(currentTop, $article)
|
||||
}
|
||||
findHeadPosition(currentTop)
|
||||
}, 100)
|
||||
|
||||
btf.addEventListenerPjax(window, 'scroll', tocScrollFn, { passive: true })
|
||||
if (currentTop === 0) {
|
||||
updateTocUI('')
|
||||
} else if (currentTop + window.innerHeight >= document.documentElement.scrollHeight - 10) {
|
||||
const lastHeader = $articleList[$articleList.length - 1]
|
||||
updateTocUI(lastHeader.id)
|
||||
}
|
||||
})
|
||||
|
||||
btf.addEventListenerPjax(window, 'scroll', scrollHandler, { passive: true })
|
||||
|
||||
btf.addGlobalFn('pjaxSendOnce', () => {
|
||||
observer.disconnect()
|
||||
})
|
||||
}
|
||||
|
||||
const handleThemeChange = mode => {
|
||||
const globalFn = window.globalFn || {}
|
||||
const themeChange = globalFn.themeChange || {}
|
||||
if (!themeChange) {
|
||||
return
|
||||
}
|
||||
const themeChange = globalFn.themeChange
|
||||
if (!themeChange) return
|
||||
|
||||
Object.keys(themeChange).forEach(key => {
|
||||
const themeChangeFn = themeChange[key]
|
||||
const fn = themeChange[key]
|
||||
if (typeof fn !== 'function') return
|
||||
|
||||
if (['disqus', 'disqusjs'].includes(key)) {
|
||||
setTimeout(() => themeChangeFn(mode), 300)
|
||||
setTimeout(() => fn(mode), 300)
|
||||
} else {
|
||||
themeChangeFn(mode)
|
||||
fn(mode)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -804,8 +814,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
/**
|
||||
* table overflow
|
||||
*/
|
||||
const addTableWrap = () => {
|
||||
const $table = document.querySelectorAll('#article-container table')
|
||||
const addTableWrap = $article => {
|
||||
const $table = $article.querySelectorAll('table')
|
||||
if (!$table.length) return
|
||||
|
||||
$table.forEach(item => {
|
||||
@@ -815,52 +825,54 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* tag-hide
|
||||
*/
|
||||
const clickFnOfTagHide = () => {
|
||||
const hideButtons = document.querySelectorAll('#article-container .hide-button')
|
||||
const clickFnOfTagHide = $article => {
|
||||
const hideButtons = $article.querySelectorAll('.hide-button')
|
||||
if (!hideButtons.length) return
|
||||
hideButtons.forEach(item => item.addEventListener('click', e => {
|
||||
const currentTarget = e.currentTarget
|
||||
currentTarget.classList.add('open')
|
||||
addJustifiedGallery(currentTarget.nextElementSibling.querySelectorAll('.gallery-container'))
|
||||
}, { once: true }))
|
||||
|
||||
const handleClickOfTagHide = e => {
|
||||
const button = e.target.closest('.hide-button')
|
||||
if (!button) return
|
||||
button.classList.add('open')
|
||||
addJustifiedGallery(button.nextElementSibling.querySelectorAll('.gallery-container'))
|
||||
}
|
||||
|
||||
btf.addEventListenerPjax($article, 'click', handleClickOfTagHide)
|
||||
}
|
||||
|
||||
const tabsFn = () => {
|
||||
const navTabsElements = document.querySelectorAll('#article-container .tabs')
|
||||
if (!navTabsElements.length) return
|
||||
const tabsFn = $article => {
|
||||
if (!$article.querySelector('.tabs')) return
|
||||
|
||||
const setActiveClass = (elements, activeIndex) => {
|
||||
elements.forEach((el, index) => {
|
||||
el.classList.toggle('active', index === activeIndex)
|
||||
})
|
||||
elements.forEach((el, index) => el.classList.toggle('active', index === activeIndex))
|
||||
}
|
||||
|
||||
const handleNavClick = e => {
|
||||
const target = e.target.closest('button')
|
||||
if (!target || target.classList.contains('active')) return
|
||||
const handleClick = e => {
|
||||
const tabsRoot = e.target.closest('.tabs')
|
||||
if (!tabsRoot) return
|
||||
|
||||
const navItems = [...e.currentTarget.children]
|
||||
const tabContents = [...e.currentTarget.nextElementSibling.children]
|
||||
const indexOfButton = navItems.indexOf(target)
|
||||
setActiveClass(navItems, indexOfButton)
|
||||
e.currentTarget.classList.remove('no-default')
|
||||
setActiveClass(tabContents, indexOfButton)
|
||||
addJustifiedGallery(tabContents[indexOfButton].querySelectorAll('.gallery-container'), true)
|
||||
}
|
||||
const navContainer = tabsRoot.firstElementChild
|
||||
const toTopContainer = tabsRoot.lastElementChild
|
||||
|
||||
const handleToTopClick = tabElement => e => {
|
||||
if (e.target.closest('button')) {
|
||||
btf.scrollToDest(btf.getEleTop(tabElement), 300)
|
||||
if (navContainer.contains(e.target)) {
|
||||
const target = e.target.closest('button')
|
||||
if (!target || target.classList.contains('active')) return
|
||||
|
||||
const navItems = [...navContainer.children]
|
||||
const tabContents = [...navContainer.nextElementSibling.children]
|
||||
const indexOfButton = navItems.indexOf(target)
|
||||
setActiveClass(navItems, indexOfButton)
|
||||
navContainer.classList.remove('no-default')
|
||||
setActiveClass(tabContents, indexOfButton)
|
||||
addJustifiedGallery(tabContents[indexOfButton].querySelectorAll('.gallery-container'), true)
|
||||
return
|
||||
}
|
||||
|
||||
if (toTopContainer.contains(e.target) && e.target.closest('button')) {
|
||||
btf.scrollToDest(btf.getEleTop(tabsRoot), 300)
|
||||
}
|
||||
}
|
||||
|
||||
navTabsElements.forEach(tabElement => {
|
||||
btf.addEventListenerPjax(tabElement.firstElementChild, 'click', handleNavClick)
|
||||
btf.addEventListenerPjax(tabElement.lastElementChild, 'click', handleToTopClick(tabElement))
|
||||
})
|
||||
btf.addEventListenerPjax($article, 'click', handleClick)
|
||||
}
|
||||
|
||||
const toggleCardCategory = () => {
|
||||
@@ -926,10 +938,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
const unRefreshFn = () => {
|
||||
window.addEventListener('resize', () => {
|
||||
const resizeHandler = btf.rafThrottle(() => {
|
||||
adjustMenu(false)
|
||||
mobileSidebarOpen && btf.isHidden(document.getElementById('toggle-menu')) && sidebarFn.close()
|
||||
if (mobileSidebarOpen && btf.isHidden(document.getElementById('toggle-menu'))) {
|
||||
sidebarFn.close()
|
||||
}
|
||||
})
|
||||
window.addEventListener('resize', resizeHandler, { passive: true })
|
||||
|
||||
const menuMask = document.getElementById('menu-mask')
|
||||
menuMask && menuMask.addEventListener('click', () => { sidebarFn.close() })
|
||||
@@ -947,18 +962,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
const forPostFn = () => {
|
||||
addHighlightTool()
|
||||
addPhotoFigcaption()
|
||||
addJustifiedGallery(document.querySelectorAll('#article-container .gallery-container'))
|
||||
runLightbox()
|
||||
scrollFnToDo()
|
||||
addTableWrap()
|
||||
clickFnOfTagHide()
|
||||
tabsFn()
|
||||
const $article = document.getElementById('article-container')
|
||||
if (!$article || $article.querySelector('.hbe-container')) return
|
||||
|
||||
addHighlightTool($article)
|
||||
addPhotoFigcaption($article)
|
||||
addJustifiedGallery($article.querySelectorAll('.gallery-container'))
|
||||
runLightbox($article)
|
||||
scrollFnToDo($article)
|
||||
addTableWrap($article)
|
||||
clickFnOfTagHide($article)
|
||||
tabsFn($article)
|
||||
}
|
||||
|
||||
const refreshFn = () => {
|
||||
initAdjust()
|
||||
goUpElement = document.getElementById('go-up')
|
||||
scrollPercentElement = goUpElement?.querySelector('.scroll-percent')
|
||||
|
||||
justifiedIndexPostUI()
|
||||
|
||||
if (GLOBAL_CONFIG_SITE.pageType === 'post') {
|
||||
@@ -974,8 +995,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
GLOBAL_CONFIG_SITE.pageType === 'home' && scrollDownInIndex()
|
||||
scrollFn()
|
||||
|
||||
forPostFn()
|
||||
GLOBAL_CONFIG_SITE.pageType !== 'shuoshuo' && btf.switchComments(document)
|
||||
if (GLOBAL_CONFIG_SITE.pageType !== 'shuoshuo') {
|
||||
forPostFn()
|
||||
btf.switchComments(document)
|
||||
}
|
||||
|
||||
openMobileMenu()
|
||||
}
|
||||
|
||||
@@ -991,4 +1015,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
fn()
|
||||
})
|
||||
})
|
||||
|
||||
document.addEventListener('shuoshuo:rendered', forPostFn)
|
||||
})
|
||||
|
||||
+471
-562
File diff suppressed because it is too large
Load Diff
+567
-567
File diff suppressed because it is too large
Load Diff
+24
-11
File diff suppressed because one or more lines are too long
+53
-24
@@ -45,29 +45,55 @@
|
||||
}
|
||||
},
|
||||
|
||||
overflowPaddingR: {
|
||||
add: () => {
|
||||
const paddingRight = window.innerWidth - document.body.clientWidth
|
||||
|
||||
if (paddingRight > 0) {
|
||||
document.body.style.paddingRight = `${paddingRight}px`
|
||||
document.body.style.overflow = 'hidden'
|
||||
const menuElement = document.querySelector('#page-header.nav-fixed #menus')
|
||||
if (menuElement) {
|
||||
menuElement.style.paddingRight = `${paddingRight}px`
|
||||
}
|
||||
}
|
||||
},
|
||||
remove: () => {
|
||||
document.body.style.paddingRight = ''
|
||||
document.body.style.overflow = ''
|
||||
const menuElement = document.querySelector('#page-header.nav-fixed #menus')
|
||||
if (menuElement) {
|
||||
menuElement.style.paddingRight = ''
|
||||
}
|
||||
rafThrottle: fn => {
|
||||
let rafId = null
|
||||
return (...args) => {
|
||||
if (rafId) return
|
||||
rafId = requestAnimationFrame(() => {
|
||||
fn(...args)
|
||||
rafId = null
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
overflowPaddingR: (() => {
|
||||
let headerElement = null
|
||||
let menuElement = null
|
||||
|
||||
const getElements = () => {
|
||||
if (!headerElement) {
|
||||
headerElement = document.getElementById('page-header')
|
||||
}
|
||||
if (!menuElement) {
|
||||
menuElement = document.getElementById('menus')
|
||||
}
|
||||
return { headerElement, menuElement }
|
||||
}
|
||||
|
||||
return {
|
||||
add: () => {
|
||||
const paddingRight = window.innerWidth - document.body.clientWidth
|
||||
|
||||
if (paddingRight > 0) {
|
||||
document.body.style.paddingRight = `${paddingRight}px`
|
||||
document.body.style.overflow = 'hidden'
|
||||
const { headerElement: header, menuElement: menu } = getElements()
|
||||
if (header && menu && header.classList.contains('nav-fixed')) {
|
||||
menu.style.paddingRight = `${paddingRight}px`
|
||||
}
|
||||
}
|
||||
},
|
||||
remove: () => {
|
||||
document.body.style.paddingRight = ''
|
||||
document.body.style.overflow = ''
|
||||
const { headerElement: header, menuElement: menu } = getElements()
|
||||
if (header && menu && header.classList.contains('nav-fixed')) {
|
||||
menu.style.paddingRight = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
})(),
|
||||
|
||||
snackbarShow: (text, showAction = false, duration = 2000) => {
|
||||
const { position, bgLight, bgDark } = GLOBAL_CONFIG.Snackbar
|
||||
const bg = document.documentElement.getAttribute('data-theme') === 'light' ? bgLight : bgDark
|
||||
@@ -133,7 +159,8 @@
|
||||
const animate = currentTime => {
|
||||
const timeElapsed = currentTime - startTime
|
||||
const progress = Math.min(timeElapsed / time, 1)
|
||||
window.scrollTo(0, currentPos + (pos - currentPos) * progress)
|
||||
const easedProgress = 1 - Math.pow(1 - progress, 4) // easeOutQuart
|
||||
window.scrollTo(0, currentPos + (pos - currentPos) * easedProgress)
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate)
|
||||
}
|
||||
@@ -216,9 +243,10 @@
|
||||
],
|
||||
right: ['slideshow', 'thumbs', 'close']
|
||||
}
|
||||
}
|
||||
},
|
||||
hideScrollbar: false
|
||||
}
|
||||
} else {
|
||||
} else {
|
||||
options = {
|
||||
Hash: false,
|
||||
Carousel: {
|
||||
@@ -247,7 +275,8 @@
|
||||
maxScale: 4
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
hideScrollbar: false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user