mirror of
https://github.com/jerryc127/hexo-theme-butterfly.git
synced 2026-08-08 19:48:42 +08:00
improvement
This commit is contained in:
+133
-156
@@ -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>
|
||||
@@ -165,70 +164,94 @@
|
||||
btf.loadLightbox(document.querySelectorAll('#article-container img:not(.no-lightbox)'))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})()
|
||||
|
||||
+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"]),
|
||||
|
||||
+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) {
|
||||
|
||||
+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) 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 ])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+133
-95
@@ -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,9 +308,25 @@ 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Vendored
+4
-1
@@ -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)
|
||||
@@ -68,6 +69,8 @@ script.
|
||||
!{theme.error_404 && theme.error_404.enable}
|
||||
? pjax.loadUrl('!{url_for("/404.html")}')
|
||||
: window.location.href = e.request.responseURL
|
||||
} else {
|
||||
window.location.href = e.request.responseURL
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user