Files
hexo-theme-butterfly/source/js/main.js
T
Jerry 06f543ed96 由於整合了多個設定並更改了部分設定名稱,升級到 5.0 版本時,請重新設定 _config.yml 文件。
1. 新增 macstyle 設定,取消 mac / mac light 主題設定
2. 整合搜索相關設定
3. 修改程式碼區塊設定
4. 主頁文章新增多種版面配置
5. 新增說說頁面
6. 適配 hexo-blog-encrypt 加密插件
7. 改善手機端目錄的開啟效果
8. 新增平滑滾動功能
9. 支持以程式碼區塊方式撰寫 mermaid 圖表
10. 可自訂文章標題位置
11. 新增程式碼全螢幕按鈕
12. 友情連結頭像改為圓角設計
13. 優化程式碼,使用 hexo-util 的參數和 hexo 內建參數
14. 可自訂搜索框提示文字
15. 未設定選單時,不顯示側邊欄目錄和按鈕
16. 螢幕寬度超過 2000px 時,增加卡片高度
17. 根據語言設定調整字體:簡體中文使用雅黑,其他使用正黑體
18. 更新 plugins.yml
19. 全新的側邊欄界面設計
20. 新增 giscus 的 js 設定
21. 調整 utterances js 的設定位置
22. 新增 utterances option 設定
23. 修改 giscus 的主題設定
24. 多個界面元素改為圓角設計
25. 可選擇圓角或直角界面風格
26. 圖庫加載按鈕新增圖標
27. 改善標籤頁面的滑鼠懸停效果
28. 調整側邊欄的滑鼠懸停效果
29. 微調部分界面元素

1. 修復 Hexo 新版本下 Prism.js 無法正確高亮的問題
2. 修復文章標籤為空時可能出現的錯誤
3. 修正 mermaid 圖表可能出現的錯誤
4. 解決未設定選單時控制台報錯的問題
5. 修復 Algolia 搜索的每頁顯示數量設定無效的問題
6. 解決 Algolia 搜索結果出現滾動條的問題
7. 修正滾動條出現上下按鈕的問題
8. 修復圖庫遠程連結未加前綴的問題
9. 修正 label 標籤外掛右側多餘空格的問題
10. 解決 APlayer 報告內存洩漏的問題

1. 優化 PJAX 下的函數調用
2. 整體代碼優化
3. 提升兼容性
4. 改善 Lighthouse 評分
5. 在 PJAX 關閉時減少不必要的全局變量
6. 優化 Waline 的 import 兼容性
7. 改善頁面進入效果
8. 優化程式碼區塊工具列顯示邏輯
9. 改善不同螢幕寬度下文章標題位置的顯示
10. 優化標籤顏色生成算法,避免過暗或過亮
11. 調整 Artalk 和 Waline 在夜間模式下的字體顏色,與主題保持一致
12. 調整 Algolia 搜索加載動畫位置,避免換行
13. 優化 Algolia 搜索結果為空時的處理
14. 改善系列文章的滑鼠懸停效果
15. 優化 404 頁面代碼
16. 解決搜索和側邊欄開啟時窗口抖動的問題
17. 優化 tabs 標籤外掛的代碼和效能
18. 改善 tabs 中使用 gallery 標籤外掛時的圖片加載邏輯
19. 優化目錄滾動效果,使當前標題保持在中間
20. 調整螢幕寬度超過 1024px 時 gallerygroup 的顯示數量
2024-08-03 19:05:57 +08:00

947 lines
30 KiB
JavaScript

document.addEventListener('DOMContentLoaded', () => {
let headerContentWidth, $nav
let mobileSidebarOpen = false
const adjustMenu = init => {
const getAllWidth = ele => Array.from(ele).reduce((width, i) => width + i.offsetWidth, 0)
if (init) {
const blogInfoWidth = getAllWidth(document.querySelector('#blog-info > a').children)
const menusWidth = getAllWidth(document.getElementById('menus').children)
headerContentWidth = blogInfoWidth + menusWidth
$nav = document.getElementById('nav')
}
const hideMenuIndex = window.innerWidth <= 768 || headerContentWidth > $nav.offsetWidth - 120
$nav.classList.toggle('hide-menu', hideMenuIndex)
}
// 初始化header
const initAdjust = () => {
adjustMenu(true)
$nav.classList.add('show')
}
// sidebar menus
const sidebarFn = {
open: () => {
btf.overflowPaddingR.add()
btf.animateIn(document.getElementById('menu-mask'), 'to_show 0.5s')
document.getElementById('sidebar-menus').classList.add('open')
mobileSidebarOpen = true
},
close: () => {
btf.overflowPaddingR.remove()
btf.animateOut(document.getElementById('menu-mask'), 'to_hide 0.5s')
document.getElementById('sidebar-menus').classList.remove('open')
mobileSidebarOpen = false
}
}
/**
* 首頁top_img底下的箭頭
*/
const scrollDownInIndex = () => {
const handleScrollToDest = () => {
btf.scrollToDest(document.getElementById('content-inner').offsetTop, 300)
}
const $scrollDownEle = document.getElementById('scroll-down')
$scrollDownEle && btf.addEventListenerPjax($scrollDownEle, 'click', handleScrollToDest)
}
/**
* 代碼
* 只適用於Hexo默認的代碼渲染
*/
const addHighlightTool = () => {
const highLight = GLOBAL_CONFIG.highlight
if (!highLight) return
const { highlightCopy, highlightLang, highlightHeightLimit, highlightFullpage, highlightMacStyle, plugin } = highLight
const isHighlightShrink = GLOBAL_CONFIG_SITE.isHighlightShrink
const isShowTool = highlightCopy || highlightLang || isHighlightShrink !== undefined || highlightFullpage || highlightMacStyle
const $figureHighlight = plugin === 'highlight.js' ? document.querySelectorAll('figure.highlight') : document.querySelectorAll('pre[class*="language-"]')
if (!((isShowTool || highlightHeightLimit) && $figureHighlight.length)) return
const isPrismjs = plugin === 'prismjs'
const highlightShrinkClass = isHighlightShrink === true ? 'closed' : ''
const highlightShrinkEle = isHighlightShrink !== undefined ? '<i class="fas fa-angle-down expand"></i>' : ''
const highlightCopyEle = highlightCopy ? '<div class="copy-notice"></div><i class="fas fa-paste copy-button"></i>' : ''
const highlightMacStyleEle = '<div class="macStyle"><div class="mac-close"></div><div class="mac-minimize"></div><div class="mac-maximize"></div></div>'
const highlightFullpageEle = highlightFullpage ? '<i class="fa-solid fa-up-right-and-down-left-from-center fullpage-button"></i>' : ''
const alertInfo = (ele, text) => {
if (GLOBAL_CONFIG.Snackbar !== undefined) {
btf.snackbarShow(text)
} else {
ele.textContent = text
ele.style.opacity = 1
setTimeout(() => { ele.style.opacity = 0 }, 800)
}
}
const copy = ctx => {
if (document.queryCommandSupported && document.queryCommandSupported('copy')) {
document.execCommand('copy')
alertInfo(ctx, GLOBAL_CONFIG.copy.success)
} else {
alertInfo(ctx, GLOBAL_CONFIG.copy.noSupport)
}
}
// click events
const highlightCopyFn = (ele, clickEle) => {
const $buttonParent = ele.parentNode
$buttonParent.classList.add('copy-true')
const selection = window.getSelection()
const range = document.createRange()
const preCodeSelector = isPrismjs ? 'pre code' : 'table .code pre'
range.selectNodeContents($buttonParent.querySelector(`${preCodeSelector}`))
selection.removeAllRanges()
selection.addRange(range)
copy(clickEle.previousElementSibling)
selection.removeAllRanges()
$buttonParent.classList.remove('copy-true')
}
const highlightShrinkFn = ele => ele.classList.toggle('closed')
const codeFullpage = (item, clickEle) => {
const wrapEle = item.closest('figure.highlight')
const isFullpage = wrapEle.classList.toggle('code-fullpage')
document.body.style.overflow = isFullpage ? 'hidden' : ''
clickEle.classList.toggle('fa-down-left-and-up-right-to-center', isFullpage)
clickEle.classList.toggle('fa-up-right-and-down-left-from-center', !isFullpage)
}
const highlightToolsFn = e => {
const $target = e.target.classList
const currentElement = e.currentTarget
if ($target.contains('expand')) highlightShrinkFn(currentElement)
else if ($target.contains('copy-button')) highlightCopyFn(currentElement, e.target)
else if ($target.contains('fullpage-button')) codeFullpage(currentElement, e.target)
}
const expandCode = e => e.currentTarget.classList.toggle('expand-done')
// 獲取隱藏狀態下元素的真實高度
const getActualHeight = item => {
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 style = 'visibility: hidden !important; display: block !important;'
hiddenElements.forEach((originalStyle, elem) => {
elem.setAttribute('style', originalStyle ? originalStyle + ';' + style : style)
})
}
const restore = () => {
hiddenElements.forEach((originalStyle, elem) => {
if (originalStyle === '') elem.removeAttribute('style')
else elem.setAttribute('style', originalStyle)
})
}
fix()
const height = item.offsetHeight
restore()
return height
}
const createEle = (lang, item) => {
const fragment = document.createDocumentFragment()
if (isShowTool) {
const hlTools = document.createElement('div')
hlTools.className = `highlight-tools ${highlightShrinkClass}`
hlTools.innerHTML = highlightMacStyleEle + highlightShrinkEle + lang + highlightCopyEle + highlightFullpageEle
btf.addEventListenerPjax(hlTools, 'click', highlightToolsFn)
fragment.appendChild(hlTools)
}
if (highlightHeightLimit && getActualHeight(item) > highlightHeightLimit + 30) {
const ele = document.createElement('div')
ele.className = 'code-expand-btn'
ele.innerHTML = '<i class="fas fa-angle-double-down"></i>'
btf.addEventListenerPjax(ele, 'click', expandCode)
fragment.appendChild(ele)
}
isPrismjs ? item.parentNode.insertBefore(fragment, item) : item.insertBefore(fragment, item.firstChild)
}
$figureHighlight.forEach(item => {
let langName = ''
if (isPrismjs) btf.wrap(item, 'figure', { class: 'highlight' })
if (!highlightLang) {
createEle('', item)
return
}
if (isPrismjs) {
langName = item.getAttribute('data-language') || 'Code'
} else {
langName = item.getAttribute('class').split(' ')[1]
if (langName === 'plain' || langName === undefined) langName = 'Code'
}
createEle(`<div class="code-lang">${langName}</div>`, item)
})
}
/**
* PhotoFigcaption
*/
const addPhotoFigcaption = () => {
if (!GLOBAL_CONFIG.isPhotoFigcaption) return
document.querySelectorAll('#article-container img').forEach(item => {
const altValue = item.title || item.alt
if (!altValue) return
const ele = document.createElement('div')
ele.className = 'img-alt is-center'
ele.textContent = altValue
item.insertAdjacentElement('afterend', ele)
})
}
/**
* Lightbox
*/
const runLightbox = () => {
btf.loadLightbox(document.querySelectorAll('#article-container img:not(.no-lightbox)'))
}
/**
* justified-gallery 圖庫排版
*/
const fetchUrl = async url => {
const response = await fetch(url)
return await response.json()
}
const runJustifiedGallery = (item, data, isButton = false, tabs) => {
const dataLength = data.length
const ig = new InfiniteGrid.JustifiedInfiniteGrid(item, {
gap: 5,
isConstantSize: true,
sizeRange: [150, 600],
useResizeObserver: true,
observeChildren: true,
useTransform: true
// useRecycle: false
})
const replaceDq = str => str.replace(/"/g, '&quot;') // replace double quotes to &quot;
const getItems = (nextGroupKey, count) => {
const nextItems = []
const startCount = (nextGroupKey - 1) * count
for (let i = 0; i < count; ++i) {
const num = startCount + i
if (num >= dataLength) {
break
}
const item = data[num]
const alt = item.alt ? `alt="${replaceDq(item.alt)}"` : ''
const title = item.title ? `title="${replaceDq(item.title)}"` : ''
nextItems.push(`<div class="item">
<img src="${item.url}" data-grid-maintained-target="true" ${alt + title} />
</div>`)
}
return nextItems
}
const buttonText = GLOBAL_CONFIG.infinitegrid.buttonText
const addButton = item => {
const button = document.createElement('button')
button.innerHTML = buttonText + '<i class="fa-solid fa-arrow-down"></i>'
button.addEventListener('click', e => {
e.target.closest('button').remove()
btf.setLoading.add(item)
appendItem(ig.getGroups().length + 1, 10)
}, { once: true })
item.insertAdjacentElement('afterend', button)
}
const appendItem = (nextGroupKey, count) => {
ig.append(getItems(nextGroupKey, count), nextGroupKey)
}
const maxGroupKey = Math.ceil(dataLength / 10)
let isLayoutHidden = false
const completeFn = e => {
if (tabs) {
const parentNode = item.parentNode
if (isLayoutHidden) {
parentNode.style.visibility = 'visible'
}
if (item.offsetHeight === 0) {
parentNode.style.visibility = 'hidden'
isLayoutHidden = true
}
}
const { updated, isResize, mounted } = e
if (!updated.length || !mounted.length || isResize) {
return
}
btf.loadLightbox(item.querySelectorAll('img:not(.medium-zoom-image)'))
if (ig.getGroups().length === maxGroupKey) {
btf.setLoading.remove(item)
!tabs && ig.off('renderComplete', completeFn)
return
}
if (isButton) {
btf.setLoading.remove(item)
addButton(item)
}
}
const requestAppendFn = btf.debounce(e => {
const nextGroupKey = (+e.groupKey || 0) + 1
appendItem(nextGroupKey, 10)
if (nextGroupKey === maxGroupKey) {
ig.off('requestAppend', requestAppendFn)
}
}, 300)
btf.setLoading.add(item)
ig.on('renderComplete', completeFn)
if (isButton) {
appendItem(1, 10)
} else {
ig.on('requestAppend', requestAppendFn)
ig.renderItems()
}
btf.addGlobalFn('pjaxSendOnce', () => { ig.destroy() })
}
const addJustifiedGallery = async (ele, tabs = false) => {
const init = async () => {
for (const item of ele) {
if (btf.isHidden(item) || item.classList.contains('loaded')) continue
const isButton = item.getAttribute('data-button') === 'true'
const children = item.firstElementChild
const text = children.textContent
children.textContent = ''
item.classList.add('loaded')
try {
const content = item.getAttribute('data-type') === 'url' ? await fetchUrl(text) : JSON.parse(text)
runJustifiedGallery(children, content, isButton, tabs)
} catch (e) {
console.error('Gallery data parsing failed:', e)
}
}
}
if (!ele.length) return
if (typeof InfiniteGrid === 'function') {
init()
} else {
await btf.getScript(`${GLOBAL_CONFIG.infinitegrid.js}`)
init()
}
}
/**
* rightside scroll percent
*/
const rightsideScrollPercent = currentTop => {
const scrollPercent = btf.getScrollPercent(currentTop, document.body)
const goUpElement = document.getElementById('go-up')
if (scrollPercent < 95) {
goUpElement.classList.add('show-percent')
goUpElement.querySelector('.scroll-percent').textContent = scrollPercent
} else {
goUpElement.classList.remove('show-percent')
}
}
/**
* 滾動處理
*/
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 isShowPercent = GLOBAL_CONFIG.percent.rightside
// 檢查文檔高度是否小於視窗高度
const checkDocumentHeight = () => {
if (document.body.scrollHeight <= innerHeight) {
$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
initTop = currentTop
return result
}
let flag = ''
const scrollTask = btf.throttle(() => {
const currentTop = window.scrollY || document.documentElement.scrollTop
const isDown = scrollDirection(currentTop)
if (currentTop > 56) {
if (flag === '') {
$header.classList.add('nav-fixed')
$rightside.classList.add('rightside-show')
}
if (isDown) {
if (flag !== 'down') {
$header.classList.remove('nav-visible')
isChatBtn && window.chatBtn.hide()
flag = 'down'
}
} else {
if (flag !== 'up') {
$header.classList.add('nav-visible')
isChatBtn && window.chatBtn.show()
flag = 'up'
}
}
} else {
flag = ''
if (currentTop === 0) {
$header.classList.remove('nav-fixed', 'nav-visible')
}
$rightside.classList.remove('rightside-show')
}
isShowPercent && rightsideScrollPercent(currentTop)
checkDocumentHeight()
}, 300)
btf.addEventListenerPjax(window, 'scroll', scrollTask, { passive: true })
}
/**
* toc,anchor
*/
const scrollFnToDo = () => {
const isToc = GLOBAL_CONFIG_SITE.isToc
const isAnchor = GLOBAL_CONFIG.isAnchor
const $article = document.getElementById('article-container')
if (!($article && (isToc || isAnchor))) return
let $tocLink, $cardToc, autoScrollToc, $tocPercentage, isExpand
if (isToc) {
const $cardTocLayout = document.getElementById('card-toc')
$cardToc = $cardTocLayout.querySelector('.toc-content')
$tocLink = $cardToc.querySelectorAll('.toc-link')
$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
e.preventDefault()
btf.scrollToDest(btf.getEleTop(document.getElementById(decodeURI(target.getAttribute('href')).replace('#', ''))), 300)
if (window.innerWidth < 900) {
$cardTocLayout.classList.remove('open')
}
}
btf.addEventListenerPjax($cardToc, 'click', tocItemClickFn)
autoScrollToc = item => {
const sidebarHeight = $cardToc.clientHeight
const itemOffsetTop = item.offsetTop
const itemHeight = item.clientHeight
const scrollTop = $cardToc.scrollTop
const offset = itemOffsetTop - scrollTop
const middlePosition = (sidebarHeight - itemHeight) / 2
if (offset !== middlePosition) {
$cardToc.scrollTop = scrollTop + (offset - middlePosition)
}
}
// 處理 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 = ''
const findHeadPosition = top => {
if (top === 0) return false
let currentId = ''
let currentIndex = ''
for (let i = 0; i < $articleList.length; i++) {
const ele = $articleList[i]
if (top > btf.getEleTop(ele) - 80) {
const id = ele.id
currentId = id ? '#' + encodeURI(id) : ''
currentIndex = i
} else {
break
}
}
if (detectItem === currentIndex) return
if (isAnchor) btf.updateAnchor(currentId)
detectItem = currentIndex
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
}
}
}
}
}
// main of scroll
const tocScrollFn = btf.throttle(() => {
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 })
}
const handleThemeChange = mode => {
const globalFn = window.globalFn || {}
const themeChange = globalFn.themeChange || {}
if (!themeChange) {
return
}
Object.keys(themeChange).forEach(key => {
const themeChangeFn = themeChange[key]
if (['disqus', 'disqusjs'].includes(key)) {
setTimeout(() => themeChangeFn(mode), 300)
} else {
themeChangeFn(mode)
}
})
}
/**
* Rightside
*/
const rightSideFn = {
readmode: () => { // read mode
const $body = document.body
$body.classList.add('read-mode')
const newEle = document.createElement('button')
newEle.type = 'button'
newEle.className = 'fas fa-sign-out-alt exit-readmode'
$body.appendChild(newEle)
const clickFn = () => {
$body.classList.remove('read-mode')
newEle.remove()
newEle.removeEventListener('click', clickFn)
}
newEle.addEventListener('click', clickFn)
},
darkmode: () => { // switch between light and dark mode
const willChangeMode = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'
if (willChangeMode === 'dark') {
btf.activateDarkMode()
GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow(GLOBAL_CONFIG.Snackbar.day_to_night)
} else {
btf.activateLightMode()
GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow(GLOBAL_CONFIG.Snackbar.night_to_day)
}
btf.saveToLocal.set('theme', willChangeMode, 2)
handleThemeChange(willChangeMode)
},
'rightside-config': item => { // Show or hide rightside-hide-btn
const hideLayout = item.firstElementChild
if (hideLayout.classList.contains('show')) {
hideLayout.classList.add('status')
setTimeout(() => {
hideLayout.classList.remove('status')
}, 300)
}
hideLayout.classList.toggle('show')
},
'go-up': () => { // Back to top
btf.scrollToDest(0, 500)
},
'hide-aside-btn': () => { // Hide aside
const $htmlDom = document.documentElement.classList
const saveStatus = $htmlDom.contains('hide-aside') ? 'show' : 'hide'
btf.saveToLocal.set('aside-status', saveStatus, 2)
$htmlDom.toggle('hide-aside')
},
'mobile-toc-button': (p, item) => { // Show mobile toc
const tocEle = document.getElementById('card-toc')
tocEle.style.transition = 'transform 0.3s ease-in-out'
const tocEleHeight = tocEle.clientHeight
const btData = item.getBoundingClientRect()
const tocEleBottom = window.innerHeight - btData.bottom - 30
if (tocEleHeight > tocEleBottom) {
tocEle.style.transformOrigin = `right ${tocEleHeight - tocEleBottom - btData.height / 2}px`
}
tocEle.classList.toggle('open')
tocEle.addEventListener('transitionend', () => {
tocEle.style.cssText = ''
}, { once: true })
},
'chat-btn': () => { // Show chat
window.chatBtnFn()
},
translateLink: () => { // switch between traditional and simplified chinese
window.translateFn.translatePage()
}
}
document.getElementById('rightside').addEventListener('click', e => {
const $target = e.target.closest('[id]')
if ($target && rightSideFn[$target.id]) {
rightSideFn[$target.id](e.currentTarget, $target)
}
})
/**
* menu
* 側邊欄sub-menu 展開/收縮
*/
const clickFnOfSubMenu = () => {
const handleClickOfSubMenu = e => {
const target = e.target.closest('.site-page.group')
if (!target) return
target.classList.toggle('hide')
}
const menusItems = document.querySelector('#sidebar-menus .menus_items')
menusItems && menusItems.addEventListener('click', handleClickOfSubMenu)
}
/**
* 手机端目录点击
*/
const openMobileMenu = () => {
const toggleMenu = document.getElementById('toggle-menu')
if (!toggleMenu) return
btf.addEventListenerPjax(toggleMenu, 'click', () => { sidebarFn.open() })
}
/**
* 複製時加上版權信息
*/
const addCopyright = () => {
const { limitCount, languages } = GLOBAL_CONFIG.copyright
const handleCopy = (e) => {
e.preventDefault()
const copyFont = window.getSelection(0).toString()
let textFont = copyFont
if (copyFont.length > limitCount) {
textFont = `${copyFont}\n\n\n${languages.author}\n${languages.link}${window.location.href}\n${languages.source}\n${languages.info}`
}
if (e.clipboardData) {
return e.clipboardData.setData('text', textFont)
} else {
return window.clipboardData.setData('text', textFont)
}
}
document.body.addEventListener('copy', handleCopy)
}
/**
* 網頁運行時間
*/
const addRuntime = () => {
const $runtimeCount = document.getElementById('runtimeshow')
if ($runtimeCount) {
const publishDate = $runtimeCount.getAttribute('data-publishDate')
$runtimeCount.textContent = `${btf.diffDate(publishDate)} ${GLOBAL_CONFIG.runtime}`
}
}
/**
* 最後一次更新時間
*/
const addLastPushDate = () => {
const $lastPushDateItem = document.getElementById('last-push-date')
if ($lastPushDateItem) {
const lastPushDate = $lastPushDateItem.getAttribute('data-lastPushDate')
$lastPushDateItem.textContent = btf.diffDate(lastPushDate, true)
}
}
/**
* table overflow
*/
const addTableWrap = () => {
const $table = document.querySelectorAll('#article-container table')
if (!$table.length) return
$table.forEach(item => {
if (!item.closest('.highlight')) {
btf.wrap(item, 'div', { class: 'table-wrap' })
}
})
}
/**
* tag-hide
*/
const clickFnOfTagHide = () => {
const hideButtons = document.querySelectorAll('#article-container .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 tabsFn = () => {
const navTabsElements = document.querySelectorAll('#article-container .tabs')
if (!navTabsElements.length) return
const setActiveClass = (elements, 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 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 handleToTopClick = tabElement => e => {
if (e.target.closest('button')) {
btf.scrollToDest(btf.getEleTop(tabElement), 300)
}
}
navTabsElements.forEach(tabElement => {
btf.addEventListenerPjax(tabElement.firstElementChild, 'click', handleNavClick)
btf.addEventListenerPjax(tabElement.lastElementChild, 'click', handleToTopClick(tabElement))
})
}
const toggleCardCategory = () => {
const cardCategory = document.querySelector('#aside-cat-list.expandBtn')
if (!cardCategory) return
const handleToggleBtn = e => {
const target = e.target
if (target.nodeName === 'I') {
e.preventDefault()
target.parentNode.classList.toggle('expand')
}
}
btf.addEventListenerPjax(cardCategory, 'click', handleToggleBtn, true)
}
const switchComments = () => {
const switchBtn = document.getElementById('switch-btn')
if (!switchBtn) return
let switchDone = false
const handleSwitchBtn = () => {
document.getElementById('post-comment').classList.toggle('move')
if (!switchDone && typeof loadOtherComment === 'function') {
switchDone = true
loadOtherComment()
}
}
btf.addEventListenerPjax(switchBtn, 'click', handleSwitchBtn)
}
const addPostOutdateNotice = () => {
const { limitDay, messagePrev, messageNext, position } = GLOBAL_CONFIG.noticeOutdate
const diffDay = btf.diffDate(GLOBAL_CONFIG_SITE.postUpdate)
if (diffDay >= limitDay) {
const ele = document.createElement('div')
ele.className = 'post-outdate-notice'
ele.textContent = `${messagePrev} ${diffDay} ${messageNext}`
const $targetEle = document.getElementById('article-container')
if (position === 'top') {
$targetEle.insertBefore(ele, $targetEle.firstChild)
} else {
$targetEle.appendChild(ele)
}
}
}
const lazyloadImg = () => {
window.lazyLoadInstance = new LazyLoad({
elements_selector: 'img',
threshold: 0,
data_src: 'lazy-src'
})
btf.addGlobalFn('pjaxComplete', () => {
window.lazyLoadInstance.update()
}, 'lazyload')
}
const relativeDate = selector => {
selector.forEach(item => {
item.textContent = btf.diffDate(item.getAttribute('datetime'), true)
item.style.display = 'inline'
})
}
const justifiedIndexPostUI = () => {
const recentPostsElement = document.getElementById('recent-posts')
if (!(recentPostsElement && recentPostsElement.classList.contains('masonry'))) return
const init = () => {
const masonryItem = new InfiniteGrid.MasonryInfiniteGrid('.recent-post-items', {
gap: { horizontal: 10, vertical: 20 },
useTransform: true,
useResizeObserver: true
})
masonryItem.renderItems()
btf.addGlobalFn('pjaxCompleteOnce', () => { masonryItem.destroy() }, 'removeJustifiedIndexPostUI')
}
typeof InfiniteGrid === 'function' ? init() : btf.getScript(`${GLOBAL_CONFIG.infinitegrid.js}`).then(init)
}
const unRefreshFn = () => {
window.addEventListener('resize', () => {
adjustMenu(false)
mobileSidebarOpen && btf.isHidden(document.getElementById('toggle-menu')) && sidebarFn.close()
})
const menuMask = document.getElementById('menu-mask')
menuMask && menuMask.addEventListener('click', () => { sidebarFn.close() })
clickFnOfSubMenu()
GLOBAL_CONFIG.islazyload && lazyloadImg()
GLOBAL_CONFIG.copyright !== undefined && addCopyright()
if (GLOBAL_CONFIG.autoDarkmode) {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
if (btf.saveToLocal.get('theme') !== undefined) return
e.matches ? handleThemeChange('dark') : handleThemeChange('light')
})
}
}
const forPostFn = () => {
addHighlightTool()
addPhotoFigcaption()
addJustifiedGallery(document.querySelectorAll('#article-container .gallery-container'))
runLightbox()
scrollFnToDo()
addTableWrap()
clickFnOfTagHide()
tabsFn()
}
const refreshFn = () => {
initAdjust()
justifiedIndexPostUI()
if (GLOBAL_CONFIG_SITE.isPost) {
GLOBAL_CONFIG.noticeOutdate !== undefined && addPostOutdateNotice()
GLOBAL_CONFIG.relativeDate.post && relativeDate(document.querySelectorAll('#post-meta time'))
} else {
GLOBAL_CONFIG.relativeDate.homepage && relativeDate(document.querySelectorAll('#recent-posts time'))
GLOBAL_CONFIG.runtime && addRuntime()
addLastPushDate()
toggleCardCategory()
}
GLOBAL_CONFIG_SITE.isHome && scrollDownInIndex()
scrollFn()
forPostFn()
switchComments()
openMobileMenu()
}
btf.addGlobalFn('pjaxComplete', refreshFn, 'refreshFn')
refreshFn()
unRefreshFn()
// 處理 hexo-blog-encrypt 事件
window.addEventListener('hexo-blog-decrypt', e => {
forPostFn()
window.translateFn.translateInitialization()
Object.values(window.globalFn.encrypt).forEach(fn => {
fn()
})
})
})