feat: add wordcount helpers, waline lightbox, and extensive bug fixes

- wordcount/min2read/totalcount built-in helpers (replaces hexo-wordcount)
- cloudTags random sort support
- waline lightbox via MutationObserver
- subtitle: seq guard prevents stale API response race
- newest-comments: escapeHtml XSS fix, unified fetchAndRender with cache fallback
- katex: try/catch asset load, encrypt re-render
- chartjs: destroy charts on pjax, fix theme property check
- mathjax: DOMContentLoaded instead of load
- mermaid: clean stale error elements
- medium-zoom: single instance, detach on pjax
- getScrollPercent: WeakMap cache with resize invalidation
- overflowPaddingR: avoid stale DOM refs
- readmode: dedupe button, pjax cleanup
- hexo-blog-decrypt: null-guard translateFn and encryptFn
- tw_cn: expand skipped translate tags
- localStorage: try/catch setItem
- cdn.js: fix [lib|dist]* character class regex bug
- findArchivesTitle: support custom archive_dir
- shuoshuoFN: guard moment.tz when timezone empty
- umami: no-store cache, error handling, path param
- artalk: vote config option
- bump version 5.7.1.260810
This commit is contained in:
myw
2026-08-10 18:02:27 +08:00 Unverified
parent 51d36ef83a
commit 79e16a5cdd
25 changed files with 726 additions and 624 deletions
+9 -3
View File
@@ -622,7 +622,7 @@ document.addEventListener('DOMContentLoaded', () => {
const currentTop = window.scrollY || document.documentElement.scrollTop
if (isToc && GLOBAL_CONFIG.percent.toc) {
$tocPercentage.textContent = btf.getScrollPercent(currentTop, $article)
$tocPercentage.textContent = btf.getScrollPercent(currentTop, $article, false)
}
if (currentTop === 0) {
@@ -662,6 +662,7 @@ document.addEventListener('DOMContentLoaded', () => {
*/
const rightSideFn = {
readmode: () => { // read mode
if (document.querySelector('.exit-readmode')) return
const $body = document.body
const newEle = document.createElement('button')
@@ -677,6 +678,7 @@ document.addEventListener('DOMContentLoaded', () => {
newEle.innerHTML = '<i class="fas fa-sign-out-alt"></i>'
newEle.addEventListener('click', exitReadMode)
$body.appendChild(newEle)
btf.addGlobalFn('pjaxSendOnce', exitReadMode, 'exitReadMode')
},
darkmode: () => { // switch between light and dark mode
const willChangeMode = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'
@@ -1010,8 +1012,12 @@ document.addEventListener('DOMContentLoaded', () => {
// 處理 hexo-blog-encrypt 事件
window.addEventListener('hexo-blog-decrypt', e => {
forPostFn()
window.translateFn.translateInitialization()
Object.values(window.globalFn.encrypt).forEach(fn => {
if (window.translateFn && typeof window.translateFn.translateInitialization === 'function') {
window.translateFn.translateInitialization()
}
const encryptFn = window.globalFn && window.globalFn.encrypt ? window.globalFn.encrypt : {}
Object.values(encryptFn).forEach(fn => {
fn()
})
})
+4 -2
View File
@@ -19,12 +19,14 @@ document.addEventListener('DOMContentLoaded', () => {
return txt
}
const skippedTranslateTags = ['BR', 'HR', 'SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'CODE', 'PRE']
const translateBody = fobj => {
const nodes = typeof fobj === 'object' ? fobj.childNodes : document.body.childNodes
for (const node of nodes) {
// Skip BR, HR tags, or the translate button object
if (['BR', 'HR'].includes(node.tagName) || node === translateButtonObject) continue
// Skip non-translatable tags or the translate button object
if (skippedTranslateTags.includes(node.tagName) || node === translateButtonObject) continue
if (node.nodeType === Node.ELEMENT_NODE) {
const { tagName, title, alt, placeholder, value, type } = node
+58 -42
View File
@@ -56,43 +56,30 @@
}
},
overflowPaddingR: (() => {
let headerElement = null
let menuElement = null
overflowPaddingR: {
add: () => {
const paddingRight = window.innerWidth - document.body.clientWidth
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 (paddingRight > 0) {
document.body.style.paddingRight = `${paddingRight}px`
document.body.style.overflow = 'hidden'
const header = document.getElementById('page-header')
const menu = document.getElementById('menus')
if (header && menu && header.classList.contains('nav-fixed')) {
menu.style.paddingRight = ''
menu.style.paddingRight = `${paddingRight}px`
}
}
},
remove: () => {
document.body.style.paddingRight = ''
document.body.style.overflow = ''
const header = document.getElementById('page-header')
const menu = document.getElementById('menus')
if (header && menu && header.classList.contains('nav-fixed')) {
menu.style.paddingRight = ''
}
}
})(),
},
snackbarShow: (text, showAction = false, duration = 2000) => {
const { position, bgLight, bgDark } = GLOBAL_CONFIG.Snackbar
@@ -200,7 +187,12 @@
const service = GLOBAL_CONFIG.lightbox
if (service === 'medium_zoom') {
mediumZoom(ele, { background: 'var(--zoom-bg)' })
const zoom = window.mediumZoomInstance || (window.mediumZoomInstance = mediumZoom({ background: 'var(--zoom-bg)' }))
zoom.attach(ele)
btf.addGlobalFn('pjaxSendOnce', () => {
window.mediumZoomInstance && window.mediumZoomInstance.detach()
}, 'mediumZoom')
return
}
@@ -246,7 +238,7 @@
},
hideScrollbar: false
}
} else {
} else {
options = {
Hash: false,
Carousel: {
@@ -314,17 +306,41 @@
},
getScrollPercent: (() => {
let docHeight, winHeight, headerHeight, contentMath
let cache = new WeakMap()
return (currentTop, ele) => {
if (!docHeight || ele.clientHeight !== docHeight) {
docHeight = ele.clientHeight
winHeight = window.innerHeight
headerHeight = ele.offsetTop
contentMath = Math.max(docHeight - winHeight, document.documentElement.scrollHeight - winHeight)
window.addEventListener('resize', () => {
cache = new WeakMap()
})
return (currentTop, ele, useDocHeight = ele === document.body) => {
const eleHeight = ele.clientHeight
const winHeight = window.innerHeight
const cacheData = cache.get(ele)
let data = cacheData
if (
!cacheData ||
cacheData.docHeight !== eleHeight ||
cacheData.winHeight !== winHeight ||
cacheData.useDocHeight !== useDocHeight
) {
const headerHeight = ele.offsetTop
const contentMath = useDocHeight
? Math.max(eleHeight - winHeight, document.documentElement.scrollHeight - winHeight)
: Math.max(eleHeight - winHeight, 1)
data = {
docHeight: eleHeight,
winHeight,
useDocHeight,
headerHeight,
contentMath
}
cache.set(ele, data)
}
const scrollPercent = (currentTop - headerHeight) / contentMath
const scrollPercent = (currentTop - data.headerHeight) / data.contentMath
return Math.max(0, Math.min(100, Math.round(scrollPercent * 100)))
}
})(),