879 lines
27 KiB
JavaScript
879 lines
27 KiB
JavaScript
|
document.addEventListener('DOMContentLoaded', function () {
|
||
|
let headerContentWidth, $nav
|
||
|
let mobileSidebarOpen = false
|
||
|
|
||
|
const adjustMenu = init => {
|
||
|
const getAllWidth = ele => {
|
||
|
return 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.sidebarPaddingR()
|
||
|
document.body.style.overflow = 'hidden'
|
||
|
btf.animateIn(document.getElementById('menu-mask'), 'to_show 0.5s')
|
||
|
document.getElementById('sidebar-menus').classList.add('open')
|
||
|
mobileSidebarOpen = true
|
||
|
},
|
||
|
close: () => {
|
||
|
const $body = document.body
|
||
|
$body.style.overflow = ''
|
||
|
$body.style.paddingRight = ''
|
||
|
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, plugin } = highLight
|
||
|
const isHighlightShrink = GLOBAL_CONFIG_SITE.isHighlightShrink
|
||
|
const isShowTool = highlightCopy || highlightLang || isHighlightShrink !== undefined
|
||
|
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 alertInfo = (ele, text) => {
|
||
|
if (GLOBAL_CONFIG.Snackbar !== undefined) {
|
||
|
btf.snackbarShow(text)
|
||
|
} else {
|
||
|
const prevEle = ele.previousElementSibling
|
||
|
prevEle.textContent = text
|
||
|
prevEle.style.opacity = 1
|
||
|
setTimeout(() => { prevEle.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 => {
|
||
|
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.querySelectorAll(`${preCodeSelector}`)[0])
|
||
|
selection.removeAllRanges()
|
||
|
selection.addRange(range)
|
||
|
copy(ele.lastChild)
|
||
|
selection.removeAllRanges()
|
||
|
$buttonParent.classList.remove('copy-true')
|
||
|
}
|
||
|
|
||
|
const highlightShrinkFn = ele => {
|
||
|
ele.classList.toggle('closed')
|
||
|
}
|
||
|
|
||
|
const highlightToolsFn = function (e) {
|
||
|
const $target = e.target.classList
|
||
|
if ($target.contains('expand')) highlightShrinkFn(this)
|
||
|
else if ($target.contains('copy-button')) highlightCopyFn(this)
|
||
|
}
|
||
|
|
||
|
const expandCode = function () {
|
||
|
this.classList.toggle('expand-done')
|
||
|
}
|
||
|
|
||
|
const createEle = (lang, item, service) => {
|
||
|
const fragment = document.createDocumentFragment()
|
||
|
|
||
|
if (isShowTool) {
|
||
|
const hlTools = document.createElement('div')
|
||
|
hlTools.className = `highlight-tools ${highlightShrinkClass}`
|
||
|
hlTools.innerHTML = highlightShrinkEle + lang + highlightCopyEle
|
||
|
btf.addEventListenerPjax(hlTools, 'click', highlightToolsFn)
|
||
|
fragment.appendChild(hlTools)
|
||
|
}
|
||
|
|
||
|
if (highlightHeightLimit && item.offsetHeight > 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)
|
||
|
}
|
||
|
|
||
|
if (service === 'hl') {
|
||
|
item.insertBefore(fragment, item.firstChild)
|
||
|
} else {
|
||
|
item.parentNode.insertBefore(fragment, item)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (isPrismjs) {
|
||
|
$figureHighlight.forEach(item => {
|
||
|
if (highlightLang) {
|
||
|
const langName = item.getAttribute('data-language') || 'Code'
|
||
|
const highlightLangEle = `<div class="code-lang">${langName}</div>`
|
||
|
btf.wrap(item, 'figure', { class: 'highlight' })
|
||
|
createEle(highlightLangEle, item)
|
||
|
} else {
|
||
|
btf.wrap(item, 'figure', { class: 'highlight' })
|
||
|
createEle('', item)
|
||
|
}
|
||
|
})
|
||
|
} else {
|
||
|
$figureHighlight.forEach(item => {
|
||
|
if (highlightLang) {
|
||
|
let langName = item.getAttribute('class').split(' ')[1]
|
||
|
if (langName === 'plain' || langName === undefined) langName = 'Code'
|
||
|
const highlightLangEle = `<div class="code-lang">${langName}</div>`
|
||
|
createEle(highlightLangEle, item, 'hl')
|
||
|
} else {
|
||
|
createEle('', item, 'hl')
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* PhotoFigcaption
|
||
|
*/
|
||
|
const addPhotoFigcaption = () => {
|
||
|
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
|
||
|
})
|
||
|
|
||
|
if (tabs) {
|
||
|
btf.addGlobalFn('igOfTabs', () => { ig.destroy() }, false, tabs)
|
||
|
}
|
||
|
|
||
|
const replaceDq = str => str.replace(/"/g, '"') // replace double quotes to "
|
||
|
|
||
|
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.textContent = buttonText
|
||
|
|
||
|
const buttonFn = e => {
|
||
|
e.target.removeEventListener('click', buttonFn)
|
||
|
e.target.remove()
|
||
|
btf.setLoading.add(item)
|
||
|
appendItem(ig.getGroups().length + 1, 10)
|
||
|
}
|
||
|
|
||
|
button.addEventListener('click', buttonFn)
|
||
|
item.insertAdjacentElement('afterend', button)
|
||
|
}
|
||
|
|
||
|
const appendItem = (nextGroupKey, count) => {
|
||
|
ig.append(getItems(nextGroupKey, count), nextGroupKey)
|
||
|
}
|
||
|
|
||
|
const maxGroupKey = Math.ceil(dataLength / 10)
|
||
|
|
||
|
const completeFn = e => {
|
||
|
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)
|
||
|
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('justifiedGallery', () => { ig.destroy() })
|
||
|
}
|
||
|
|
||
|
const addJustifiedGallery = async (ele, tabs = false) => {
|
||
|
const init = async () => {
|
||
|
for (const item of ele) {
|
||
|
if (btf.isHidden(item)) continue
|
||
|
if (tabs && item.classList.contains('loaded')) {
|
||
|
item.querySelector('.gallery-items').innerHTML = ''
|
||
|
const button = item.querySelector(':scope > button')
|
||
|
const loadingContainer = item.querySelector(':scope > .loading-container')
|
||
|
button && button.remove()
|
||
|
loadingContainer && loadingContainer.remove()
|
||
|
}
|
||
|
|
||
|
const isButton = item.getAttribute('data-button') === 'true'
|
||
|
const text = item.firstElementChild.textContent
|
||
|
item.classList.add('loaded')
|
||
|
const content = item.getAttribute('data-type') === 'url' ? await fetchUrl(text) : JSON.parse(text)
|
||
|
runJustifiedGallery(item.lastElementChild, content, isButton, tabs)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (typeof InfiniteGrid === 'function') {
|
||
|
init()
|
||
|
} else {
|
||
|
await 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
|
||
|
|
||
|
// 當滾動條小于 56 的時候
|
||
|
if (document.body.scrollHeight <= innerHeight) {
|
||
|
$rightside.classList.add('rightside-show')
|
||
|
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)
|
||
|
|
||
|
if (document.body.scrollHeight <= innerHeight) {
|
||
|
$rightside.classList.add('rightside-show')
|
||
|
}
|
||
|
}, 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 activePosition = item.getBoundingClientRect().top
|
||
|
const sidebarScrollTop = $cardToc.scrollTop
|
||
|
if (activePosition > (document.documentElement.clientHeight - 100)) {
|
||
|
$cardToc.scrollTop = sidebarScrollTop + 150
|
||
|
}
|
||
|
if (activePosition < 100) {
|
||
|
$cardToc.scrollTop = sidebarScrollTop - 150
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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 = ''
|
||
|
|
||
|
$articleList.forEach((ele, index) => {
|
||
|
if (top > btf.getEleTop(ele) - 80) {
|
||
|
const id = ele.id
|
||
|
currentId = id ? '#' + encodeURI(id) : ''
|
||
|
currentIndex = index
|
||
|
}
|
||
|
})
|
||
|
|
||
|
if (detectItem === currentIndex) return
|
||
|
|
||
|
if (isAnchor) btf.updateAnchor(currentId)
|
||
|
|
||
|
detectItem = currentIndex
|
||
|
|
||
|
if (isToc) {
|
||
|
$cardToc.querySelectorAll('.active').forEach(i => { i.classList.remove('active') })
|
||
|
|
||
|
if (currentId === '') {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
const currentActive = $tocLink[currentIndex]
|
||
|
currentActive.classList.add('active')
|
||
|
|
||
|
setTimeout(() => {
|
||
|
autoScrollToc(currentActive)
|
||
|
}, 0)
|
||
|
|
||
|
if (isExpand) return
|
||
|
let parent = currentActive.parentNode
|
||
|
|
||
|
for (; !parent.matches('.toc'); parent = parent.parentNode) {
|
||
|
if (parent.matches('li')) parent.classList.add('active')
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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') {
|
||
|
activateDarkMode()
|
||
|
GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow(GLOBAL_CONFIG.Snackbar.day_to_night)
|
||
|
} else {
|
||
|
activateLightMode()
|
||
|
GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow(GLOBAL_CONFIG.Snackbar.night_to_day)
|
||
|
}
|
||
|
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'
|
||
|
saveToLocal.set('aside-status', saveStatus, 2)
|
||
|
$htmlDom.toggle('hide-aside')
|
||
|
},
|
||
|
'mobile-toc-button': item => { // Show mobile toc
|
||
|
const tocEle = document.getElementById('card-toc')
|
||
|
tocEle.style.transition = 'transform 0.3s ease-in-out'
|
||
|
tocEle.classList.toggle('open')
|
||
|
tocEle.addEventListener('transitionend', () => {
|
||
|
tocEle.style.transition = ''
|
||
|
}, { once: true })
|
||
|
},
|
||
|
'chat-btn': () => { // Show chat
|
||
|
window.chatBtnFn()
|
||
|
},
|
||
|
translateLink: () => { // switch between traditional and simplified chinese
|
||
|
window.translateFn.translatePage()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
document.getElementById('rightside').addEventListener('click', function (e) {
|
||
|
const $target = e.target.closest('[id]')
|
||
|
if ($target && rightSideFn[$target.id]) {
|
||
|
rightSideFn[$target.id](this)
|
||
|
}
|
||
|
})
|
||
|
|
||
|
/**
|
||
|
* menu
|
||
|
* 側邊欄sub-menu 展開/收縮
|
||
|
*/
|
||
|
const clickFnOfSubMenu = () => {
|
||
|
const handleClickOfSubMenu = e => {
|
||
|
const target = e.target.closest('.site-page.group')
|
||
|
if (!target) return
|
||
|
target.classList.toggle('hide')
|
||
|
}
|
||
|
|
||
|
document.querySelector('#sidebar-menus .menus_items').addEventListener('click', handleClickOfSubMenu)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 手机端目录点击
|
||
|
*/
|
||
|
const openMobileMenu = () => {
|
||
|
const handleClick = () => { sidebarFn.open() }
|
||
|
btf.addEventListenerPjax(document.getElementById('toggle-menu'), 'click', handleClick)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 複製時加上版權信息
|
||
|
*/
|
||
|
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
|
||
|
const handleClick = function (e) {
|
||
|
const $this = this
|
||
|
$this.classList.add('open')
|
||
|
const $fjGallery = $this.nextElementSibling.querySelectorAll('.gallery-container')
|
||
|
$fjGallery.length && addJustifiedGallery($fjGallery)
|
||
|
}
|
||
|
|
||
|
hideButtons.forEach(item => {
|
||
|
item.addEventListener('click', handleClick, { once: true })
|
||
|
})
|
||
|
}
|
||
|
|
||
|
const tabsFn = () => {
|
||
|
const navTabsElement = document.querySelectorAll('#article-container .tabs')
|
||
|
if (!navTabsElement.length) return
|
||
|
|
||
|
const removeAndAddActiveClass = (elements, detect) => {
|
||
|
Array.from(elements).forEach(element => {
|
||
|
element.classList.remove('active')
|
||
|
if (element === detect || element.id === detect) {
|
||
|
element.classList.add('active')
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
const addTabNavEventListener = (item, isJustifiedGallery) => {
|
||
|
const navClickHandler = function (e) {
|
||
|
const target = e.target.closest('button')
|
||
|
if (target.classList.contains('active')) return
|
||
|
removeAndAddActiveClass(this.children, target)
|
||
|
this.classList.remove('no-default')
|
||
|
const tabId = target.getAttribute('data-href')
|
||
|
const tabContent = this.nextElementSibling
|
||
|
removeAndAddActiveClass(tabContent.children, tabId)
|
||
|
if (isJustifiedGallery) {
|
||
|
btf.removeGlobalFnEvent('igOfTabs', this)
|
||
|
const justifiedGalleryItems = tabContent.querySelectorAll(`:scope > #${tabId} .gallery-container`)
|
||
|
justifiedGalleryItems.length && addJustifiedGallery(justifiedGalleryItems, this)
|
||
|
}
|
||
|
}
|
||
|
btf.addEventListenerPjax(item.firstElementChild, 'click', navClickHandler)
|
||
|
}
|
||
|
|
||
|
const addTabToTopEventListener = item => {
|
||
|
const btnClickHandler = (e) => {
|
||
|
const target = e.target.closest('button')
|
||
|
if (!target) return
|
||
|
btf.scrollToDest(btf.getEleTop(item), 300)
|
||
|
}
|
||
|
btf.addEventListenerPjax(item.lastElementChild, 'click', btnClickHandler)
|
||
|
}
|
||
|
|
||
|
navTabsElement.forEach(item => {
|
||
|
const isJustifiedGallery = !!item.querySelectorAll('.gallery-container')
|
||
|
addTabNavEventListener(item, isJustifiedGallery)
|
||
|
addTabToTopEventListener(item)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
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 commentContainer = document.getElementById('post-comment')
|
||
|
const handleSwitchBtn = () => {
|
||
|
commentContainer.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'
|
||
|
})
|
||
|
}
|
||
|
|
||
|
const relativeDate = function (selector) {
|
||
|
selector.forEach(item => {
|
||
|
const timeVal = item.getAttribute('datetime')
|
||
|
item.textContent = btf.diffDate(timeVal, true)
|
||
|
item.style.display = 'inline'
|
||
|
})
|
||
|
}
|
||
|
|
||
|
const unRefreshFn = function () {
|
||
|
window.addEventListener('resize', () => {
|
||
|
adjustMenu(false)
|
||
|
mobileSidebarOpen && btf.isHidden(document.getElementById('toggle-menu')) && sidebarFn.close()
|
||
|
})
|
||
|
|
||
|
document.getElementById('menu-mask').addEventListener('click', e => { 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 (saveToLocal.get('theme') !== undefined) return
|
||
|
e.matches ? handleThemeChange('dark') : handleThemeChange('light')
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
window.refreshFn = function () {
|
||
|
initAdjust()
|
||
|
|
||
|
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()
|
||
|
}
|
||
|
|
||
|
scrollFnToDo()
|
||
|
GLOBAL_CONFIG_SITE.isHome && scrollDownInIndex()
|
||
|
addHighlightTool()
|
||
|
GLOBAL_CONFIG.isPhotoFigcaption && addPhotoFigcaption()
|
||
|
scrollFn()
|
||
|
|
||
|
btf.removeGlobalFnEvent('justifiedGallery')
|
||
|
const galleryContainer = document.querySelectorAll('#article-container .gallery-container')
|
||
|
galleryContainer.length && addJustifiedGallery(galleryContainer)
|
||
|
|
||
|
runLightbox()
|
||
|
addTableWrap()
|
||
|
clickFnOfTagHide()
|
||
|
tabsFn()
|
||
|
switchComments()
|
||
|
openMobileMenu()
|
||
|
}
|
||
|
|
||
|
refreshFn()
|
||
|
unRefreshFn()
|
||
|
})
|