This commit is contained in:
shenjianZ committed 2024-07-30 21:41:51 +08:00
commit 192ef21b12
574 files changed
+70686

No files matched your search

+878
View File
@@ -0,0 +1,878 @@
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, '&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.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()
})
+177
View File
@@ -0,0 +1,177 @@
window.addEventListener('load', () => {
const $searchMask = document.getElementById('search-mask')
const $searchDialog = document.querySelector('#algolia-search .search-dialog')
const openSearch = () => {
const bodyStyle = document.body.style
bodyStyle.width = '100%'
bodyStyle.overflow = 'hidden'
btf.animateIn($searchMask, 'to_show 0.5s')
btf.animateIn($searchDialog, 'titleScale 0.5s')
setTimeout(() => { document.querySelector('#algolia-search .ais-SearchBox-input').focus() }, 100)
// shortcut: ESC
document.addEventListener('keydown', function f (event) {
if (event.code === 'Escape') {
closeSearch()
document.removeEventListener('keydown', f)
}
})
fixSafariHeight()
window.addEventListener('resize', fixSafariHeight)
}
const closeSearch = () => {
const bodyStyle = document.body.style
bodyStyle.width = ''
bodyStyle.overflow = ''
btf.animateOut($searchDialog, 'search_close .5s')
btf.animateOut($searchMask, 'to_hide 0.5s')
window.removeEventListener('resize', fixSafariHeight)
}
// fix safari
const fixSafariHeight = () => {
if (window.innerWidth < 768) {
$searchDialog.style.setProperty('--search-height', window.innerHeight + 'px')
}
}
const searchClickFn = () => {
btf.addEventListenerPjax(document.querySelector('#search-button > .search'), 'click', openSearch)
}
const searchFnOnce = () => {
$searchMask.addEventListener('click', closeSearch)
document.querySelector('#algolia-search .search-close-button').addEventListener('click', closeSearch)
}
const cutContent = content => {
if (content === '') return ''
const firstOccur = content.indexOf('<mark>')
let start = firstOccur - 30
let end = firstOccur + 120
let pre = ''
let post = ''
if (start <= 0) {
start = 0
end = 140
} else {
pre = '...'
}
if (end > content.length) {
end = content.length
} else {
post = '...'
}
const matchContent = pre + content.substring(start, end) + post
return matchContent
}
const algolia = GLOBAL_CONFIG.algolia
const isAlgoliaValid = algolia.appId && algolia.apiKey && algolia.indexName
if (!isAlgoliaValid) {
return console.error('Algolia setting is invalid!')
}
const search = instantsearch({
indexName: algolia.indexName,
/* global algoliasearch */
searchClient: algoliasearch(algolia.appId, algolia.apiKey),
searchFunction (helper) {
helper.state.query && helper.search()
}
})
const configure = instantsearch.widgets.configure({
hitsPerPage: 5
})
const searchBox = instantsearch.widgets.searchBox({
container: '#algolia-search-input',
showReset: false,
showSubmit: false,
placeholder: GLOBAL_CONFIG.algolia.languages.input_placeholder,
showLoadingIndicator: true
})
const hits = instantsearch.widgets.hits({
container: '#algolia-hits',
templates: {
item (data) {
const link = data.permalink ? data.permalink : (GLOBAL_CONFIG.root + data.path)
const result = data._highlightResult
const content = result.contentStripTruncate
? cutContent(result.contentStripTruncate.value)
: result.contentStrip
? cutContent(result.contentStrip.value)
: result.content
? cutContent(result.content.value)
: ''
return `
<a href="${link}" class="algolia-hit-item-link">
<span class="algolia-hits-item-title">${result.title.value || 'no-title'}</span>
<p class="algolia-hit-item-content">${content}</p>
</a>`
},
empty: function (data) {
return (
'<div id="algolia-hits-empty">' +
GLOBAL_CONFIG.algolia.languages.hits_empty.replace(/\$\{query}/, data.query) +
'</div>'
)
}
}
})
const stats = instantsearch.widgets.stats({
container: '#algolia-info > .algolia-stats',
templates: {
text: function (data) {
const stats = GLOBAL_CONFIG.algolia.languages.hits_stats
.replace(/\$\{hits}/, data.nbHits)
.replace(/\$\{time}/, data.processingTimeMS)
return (
`<hr>${stats}`
)
}
}
})
const powerBy = instantsearch.widgets.poweredBy({
container: '#algolia-info > .algolia-poweredBy'
})
const pagination = instantsearch.widgets.pagination({
container: '#algolia-pagination',
totalPages: 5,
templates: {
first: '<i class="fas fa-angle-double-left"></i>',
last: '<i class="fas fa-angle-double-right"></i>',
previous: '<i class="fas fa-angle-left"></i>',
next: '<i class="fas fa-angle-right"></i>'
}
})
search.addWidgets([configure, searchBox, hits, stats, powerBy, pagination]) // add the widgets to the instantsearch instance
search.start()
searchClickFn()
searchFnOnce()
window.addEventListener('pjax:complete', () => {
!btf.isHidden($searchMask) && closeSearch()
searchClickFn()
})
window.pjax && search.on('render', () => {
window.pjax.refresh(document.getElementById('algolia-hits'))
})
})
+364
View File
@@ -0,0 +1,364 @@
/**
* Refer to hexo-generator-searchdb
* https://github.com/next-theme/hexo-generator-searchdb/blob/main/dist/search.js
* Modified by hexo-theme-butterfly
*/
class LocalSearch {
constructor ({
path = '',
unescape = false,
top_n_per_article = 1
}) {
this.path = path
this.unescape = unescape
this.top_n_per_article = top_n_per_article
this.isfetched = false
this.datas = null
}
getIndexByWord (words, text, caseSensitive = false) {
const index = []
const included = new Set()
if (!caseSensitive) {
text = text.toLowerCase()
}
words.forEach(word => {
if (this.unescape) {
const div = document.createElement('div')
div.innerText = word
word = div.innerHTML
}
const wordLen = word.length
if (wordLen === 0) return
let startPosition = 0
let position = -1
if (!caseSensitive) {
word = word.toLowerCase()
}
while ((position = text.indexOf(word, startPosition)) > -1) {
index.push({ position, word })
included.add(word)
startPosition = position + wordLen
}
})
// Sort index by position of keyword
index.sort((left, right) => {
if (left.position !== right.position) {
return left.position - right.position
}
return right.word.length - left.word.length
})
return [index, included]
}
// Merge hits into slices
mergeIntoSlice (start, end, index) {
let item = index[0]
let { position, word } = item
const hits = []
const count = new Set()
while (position + word.length <= end && index.length !== 0) {
count.add(word)
hits.push({
position,
length: word.length
})
const wordEnd = position + word.length
// Move to next position of hit
index.shift()
while (index.length !== 0) {
item = index[0]
position = item.position
word = item.word
if (wordEnd > position) {
index.shift()
} else {
break
}
}
}
return {
hits,
start,
end,
count: count.size
}
}
// Highlight title and content
highlightKeyword (val, slice) {
let result = ''
let index = slice.start
for (const { position, length } of slice.hits) {
result += val.substring(index, position)
index = position + length
result += `<mark class="search-keyword">${val.substr(position, length)}</mark>`
}
result += val.substring(index, slice.end)
return result
}
getResultItems (keywords) {
const resultItems = []
this.datas.forEach(({ title, content, url }) => {
// The number of different keywords included in the article.
const [indexOfTitle, keysOfTitle] = this.getIndexByWord(keywords, title)
const [indexOfContent, keysOfContent] = this.getIndexByWord(keywords, content)
const includedCount = new Set([...keysOfTitle, ...keysOfContent]).size
// Show search results
const hitCount = indexOfTitle.length + indexOfContent.length
if (hitCount === 0) return
const slicesOfTitle = []
if (indexOfTitle.length !== 0) {
slicesOfTitle.push(this.mergeIntoSlice(0, title.length, indexOfTitle))
}
let slicesOfContent = []
while (indexOfContent.length !== 0) {
const item = indexOfContent[0]
const { position } = item
// Cut out 120 characters. The maxlength of .search-input is 80.
const start = Math.max(0, position - 20)
const end = Math.min(content.length, position + 100)
slicesOfContent.push(this.mergeIntoSlice(start, end, indexOfContent))
}
// Sort slices in content by included keywords' count and hits' count
slicesOfContent.sort((left, right) => {
if (left.count !== right.count) {
return right.count - left.count
} else if (left.hits.length !== right.hits.length) {
return right.hits.length - left.hits.length
}
return left.start - right.start
})
// Select top N slices in content
const upperBound = parseInt(this.top_n_per_article, 10)
if (upperBound >= 0) {
slicesOfContent = slicesOfContent.slice(0, upperBound)
}
let resultItem = ''
url = new URL(url, location.origin)
url.searchParams.append('highlight', keywords.join(' '))
if (slicesOfTitle.length !== 0) {
resultItem += `<div class="local-search-hit-item"><a href="${url.href}"><span class="search-result-title">${this.highlightKeyword(title, slicesOfTitle[0])}</span>`
} else {
resultItem += `<div class="local-search-hit-item"><a href="${url.href}"><span class="search-result-title">${title}</span>`
}
slicesOfContent.forEach(slice => {
resultItem += `<p class="search-result">${this.highlightKeyword(content, slice)}...</p></a>`
})
resultItem += '</div>'
resultItems.push({
item: resultItem,
id: resultItems.length,
hitCount,
includedCount
})
})
return resultItems
}
fetchData () {
const isXml = !this.path.endsWith('json')
fetch(this.path)
.then(response => response.text())
.then(res => {
// Get the contents from search data
this.isfetched = true
this.datas = isXml
? [...new DOMParser().parseFromString(res, 'text/xml').querySelectorAll('entry')].map(element => ({
title: element.querySelector('title').textContent,
content: element.querySelector('content').textContent,
url: element.querySelector('url').textContent
}))
: JSON.parse(res)
// Only match articles with non-empty titles
this.datas = this.datas.filter(data => data.title).map(data => {
data.title = data.title.trim()
data.content = data.content ? data.content.trim().replace(/<[^>]+>/g, '') : ''
data.url = decodeURIComponent(data.url).replace(/\/{2,}/g, '/')
return data
})
// Remove loading animation
window.dispatchEvent(new Event('search:loaded'))
})
}
// Highlight by wrapping node in mark elements with the given class name
highlightText (node, slice, className) {
const val = node.nodeValue
let index = slice.start
const children = []
for (const { position, length } of slice.hits) {
const text = document.createTextNode(val.substring(index, position))
index = position + length
const mark = document.createElement('mark')
mark.className = className
mark.appendChild(document.createTextNode(val.substr(position, length)))
children.push(text, mark)
}
node.nodeValue = val.substring(index, slice.end)
children.forEach(element => {
node.parentNode.insertBefore(element, node)
})
}
// Highlight the search words provided in the url in the text
highlightSearchWords (body) {
const params = new URL(location.href).searchParams.get('highlight')
const keywords = params ? params.split(' ') : []
if (!keywords.length || !body) return
const walk = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, null)
const allNodes = []
while (walk.nextNode()) {
if (!walk.currentNode.parentNode.matches('button, select, textarea, .mermaid')) allNodes.push(walk.currentNode)
}
allNodes.forEach(node => {
const [indexOfNode] = this.getIndexByWord(keywords, node.nodeValue)
if (!indexOfNode.length) return
const slice = this.mergeIntoSlice(0, node.nodeValue.length, indexOfNode)
this.highlightText(node, slice, 'search-keyword')
})
}
}
window.addEventListener('load', () => {
// Search
const { path, top_n_per_article, unescape, languages } = GLOBAL_CONFIG.localSearch
const localSearch = new LocalSearch({
path,
top_n_per_article,
unescape
})
const input = document.querySelector('#local-search-input input')
const statsItem = document.getElementById('local-search-stats-wrap')
const $loadingStatus = document.getElementById('loading-status')
const isXml = !path.endsWith('json')
const inputEventFunction = () => {
if (!localSearch.isfetched) return
let searchText = input.value.trim().toLowerCase()
isXml && (searchText = searchText.replace(/</g, '&lt;').replace(/>/g, '&gt;'))
if (searchText !== '') $loadingStatus.innerHTML = '<i class="fas fa-spinner fa-pulse"></i>'
const keywords = searchText.split(/[-\s]+/)
const container = document.getElementById('local-search-results')
let resultItems = []
if (searchText.length > 0) {
// Perform local searching
resultItems = localSearch.getResultItems(keywords)
}
if (keywords.length === 1 && keywords[0] === '') {
container.textContent = ''
statsItem.textContent = ''
} else if (resultItems.length === 0) {
container.textContent = ''
const statsDiv = document.createElement('div')
statsDiv.className = 'search-result-stats'
statsDiv.textContent = languages.hits_empty.replace(/\$\{query}/, searchText)
statsItem.innerHTML = statsDiv.outerHTML
} else {
resultItems.sort((left, right) => {
if (left.includedCount !== right.includedCount) {
return right.includedCount - left.includedCount
} else if (left.hitCount !== right.hitCount) {
return right.hitCount - left.hitCount
}
return right.id - left.id
})
const stats = languages.hits_stats.replace(/\$\{hits}/, resultItems.length)
container.innerHTML = `<div class="search-result-list">${resultItems.map(result => result.item).join('')}</div>`
statsItem.innerHTML = `<hr><div class="search-result-stats">${stats}</div>`
window.pjax && window.pjax.refresh(container)
}
$loadingStatus.textContent = ''
}
let loadFlag = false
const $searchMask = document.getElementById('search-mask')
const $searchDialog = document.querySelector('#local-search .search-dialog')
// fix safari
const fixSafariHeight = () => {
if (window.innerWidth < 768) {
$searchDialog.style.setProperty('--search-height', window.innerHeight + 'px')
}
}
const openSearch = () => {
const bodyStyle = document.body.style
bodyStyle.width = '100%'
bodyStyle.overflow = 'hidden'
btf.animateIn($searchMask, 'to_show 0.5s')
btf.animateIn($searchDialog, 'titleScale 0.5s')
setTimeout(() => { input.focus() }, 300)
if (!loadFlag) {
!localSearch.isfetched && localSearch.fetchData()
input.addEventListener('input', inputEventFunction)
loadFlag = true
}
// shortcut: ESC
document.addEventListener('keydown', function f (event) {
if (event.code === 'Escape') {
closeSearch()
document.removeEventListener('keydown', f)
}
})
fixSafariHeight()
window.addEventListener('resize', fixSafariHeight)
}
const closeSearch = () => {
const bodyStyle = document.body.style
bodyStyle.width = ''
bodyStyle.overflow = ''
btf.animateOut($searchDialog, 'search_close .5s')
btf.animateOut($searchMask, 'to_hide 0.5s')
window.removeEventListener('resize', fixSafariHeight)
}
const searchClickFn = () => {
btf.addEventListenerPjax(document.querySelector('#search-button > .search'), 'click', openSearch)
}
const searchFnOnce = () => {
document.querySelector('#local-search .search-close-button').addEventListener('click', closeSearch)
$searchMask.addEventListener('click', closeSearch)
if (GLOBAL_CONFIG.localSearch.preload) {
localSearch.fetchData()
}
localSearch.highlightSearchWords(document.getElementById('article-container'))
}
window.addEventListener('search:loaded', () => {
const $loadDataItem = document.getElementById('loading-database')
$loadDataItem.nextElementSibling.style.display = 'block'
$loadDataItem.remove()
})
searchClickFn()
searchFnOnce()
// pjax
window.addEventListener('pjax:complete', () => {
!btf.isHidden($searchMask) && closeSearch()
localSearch.highlightSearchWords(document.getElementById('article-container'))
searchClickFn()
})
})
+122
View File
@@ -0,0 +1,122 @@
document.addEventListener('DOMContentLoaded', function () {
const { defaultEncoding, translateDelay, msgToTraditionalChinese, msgToSimplifiedChinese } = GLOBAL_CONFIG.translate
const snackbarData = GLOBAL_CONFIG.Snackbar
let currentEncoding = defaultEncoding
const targetEncodingCookie = 'translate-chn-cht'
let targetEncoding =
saveToLocal.get(targetEncodingCookie) === undefined
? defaultEncoding
: Number(saveToLocal.get('translate-chn-cht'))
let translateButtonObject
const isSnackbar = snackbarData !== undefined
function setLang () {
document.documentElement.lang = targetEncoding === 1 ? 'zh-TW' : 'zh-CN'
}
function translateText (txt) {
if (txt === '' || txt == null) return ''
if (currentEncoding === 1 && targetEncoding === 2) return Simplized(txt)
else if (currentEncoding === 2 && targetEncoding === 1) {
return Traditionalized(txt)
} else return txt
}
function translateBody (fobj) {
let objs
if (typeof fobj === 'object') objs = fobj.childNodes
else objs = document.body.childNodes
for (let i = 0; i < objs.length; i++) {
const obj = objs.item(i)
if (
'||BR|HR|'.indexOf('|' + obj.tagName + '|') > 0 ||
obj === translateButtonObject
) {
continue
}
if (obj.title !== '' && obj.title != null) {
obj.title = translateText(obj.title)
}
if (obj.alt !== '' && obj.alt != null) obj.alt = translateText(obj.alt)
if (obj.placeholder !== '' && obj.placeholder != null) { obj.placeholder = translateText(obj.placeholder) }
if (
obj.tagName === 'INPUT' &&
obj.value !== '' &&
obj.type !== 'text' &&
obj.type !== 'hidden'
) {
obj.value = translateText(obj.value)
}
if (obj.nodeType === 3) obj.data = translateText(obj.data)
else translateBody(obj)
}
}
function translatePage () {
if (targetEncoding === 1) {
currentEncoding = 1
targetEncoding = 2
translateButtonObject.textContent = msgToTraditionalChinese
isSnackbar && btf.snackbarShow(snackbarData.cht_to_chs)
} else if (targetEncoding === 2) {
currentEncoding = 2
targetEncoding = 1
translateButtonObject.textContent = msgToSimplifiedChinese
isSnackbar && btf.snackbarShow(snackbarData.chs_to_cht)
}
saveToLocal.set(targetEncodingCookie, targetEncoding, 2)
setLang()
translateBody()
}
function JTPYStr () {
return '万与丑专业丛东丝丢两严丧个丬丰临为丽举么义乌乐乔习乡书买乱争于亏云亘亚产亩亲亵亸亿仅从仑仓仪们价众优伙会伛伞伟传伤伥伦伧伪伫体余佣佥侠侣侥侦侧侨侩侪侬俣俦俨俩俪俭债倾偬偻偾偿傥傧储傩儿兑兖党兰关兴兹养兽冁内冈册写军农冢冯冲决况冻净凄凉凌减凑凛几凤凫凭凯击凼凿刍划刘则刚创删别刬刭刽刿剀剂剐剑剥剧劝办务劢动励劲劳势勋勐勚匀匦匮区医华协单卖卢卤卧卫却卺厂厅历厉压厌厍厕厢厣厦厨厩厮县参叆叇双发变叙叠叶号叹叽吁后吓吕吗吣吨听启吴呒呓呕呖呗员呙呛呜咏咔咙咛咝咤咴咸哌响哑哒哓哔哕哗哙哜哝哟唛唝唠唡唢唣唤唿啧啬啭啮啰啴啸喷喽喾嗫呵嗳嘘嘤嘱噜噼嚣嚯团园囱围囵国图圆圣圹场坂坏块坚坛坜坝坞坟坠垄垅垆垒垦垧垩垫垭垯垱垲垴埘埙埚埝埯堑堕塆墙壮声壳壶壸处备复够头夸夹夺奁奂奋奖奥妆妇妈妩妪妫姗姜娄娅娆娇娈娱娲娴婳婴婵婶媪嫒嫔嫱嬷孙学孪宁宝实宠审宪宫宽宾寝对寻导寿将尔尘尧尴尸尽层屃屉届属屡屦屿岁岂岖岗岘岙岚岛岭岳岽岿峃峄峡峣峤峥峦崂崃崄崭嵘嵚嵛嵝嵴巅巩巯币帅师帏帐帘帜带帧帮帱帻帼幂幞干并广庄庆庐庑库应庙庞废庼廪开异弃张弥弪弯弹强归当录彟彦彻径徕御忆忏忧忾怀态怂怃怄怅怆怜总怼怿恋恳恶恸恹恺恻恼恽悦悫悬悭悯惊惧惨惩惫惬惭惮惯愍愠愤愦愿慑慭憷懑懒懔戆戋戏戗战戬户扎扑扦执扩扪扫扬扰抚抛抟抠抡抢护报担拟拢拣拥拦拧拨择挂挚挛挜挝挞挟挠挡挢挣挤挥挦捞损捡换捣据捻掳掴掷掸掺掼揸揽揿搀搁搂搅携摄摅摆摇摈摊撄撑撵撷撸撺擞攒敌敛数斋斓斗斩断无旧时旷旸昙昼昽显晋晒晓晔晕晖暂暧札术朴机杀杂权条来杨杩杰极构枞枢枣枥枧枨枪枫枭柜柠柽栀栅标栈栉栊栋栌栎栏树栖样栾桊桠桡桢档桤桥桦桧桨桩梦梼梾检棂椁椟椠椤椭楼榄榇榈榉槚槛槟槠横樯樱橥橱橹橼檐檩欢欤欧歼殁殇残殒殓殚殡殴毁毂毕毙毡毵氇气氢氩氲汇汉污汤汹沓沟没沣沤沥沦沧沨沩沪沵泞泪泶泷泸泺泻泼泽泾洁洒洼浃浅浆浇浈浉浊测浍济浏浐浑浒浓浔浕涂涌涛涝涞涟涠涡涢涣涤润涧涨涩淀渊渌渍渎渐渑渔渖渗温游湾湿溃溅溆溇滗滚滞滟滠满滢滤滥滦滨滩滪漤潆潇潋潍潜潴澜濑濒灏灭灯灵灾灿炀炉炖炜炝点炼炽烁烂烃烛烟烦烧烨烩烫烬热焕焖焘煅煳熘爱爷牍牦牵牺犊犟状犷犸犹狈狍狝狞独狭狮狯狰狱狲猃猎猕猡猪猫猬献獭玑玙玚玛玮环现玱玺珉珏珐珑珰珲琎琏琐琼瑶瑷璇璎瓒瓮瓯电画畅畲畴疖疗疟疠疡疬疮疯疱疴痈痉痒痖痨痪痫痴瘅瘆瘗瘘瘪瘫瘾瘿癞癣癫癯皑皱皲盏盐监盖盗盘眍眦眬着睁睐睑瞒瞩矫矶矾矿砀码砖砗砚砜砺砻砾础硁硅硕硖硗硙硚确硷碍碛碜碱碹磙礼祎祢祯祷祸禀禄禅离秃秆种积称秽秾稆税稣稳穑穷窃窍窑窜窝窥窦窭竖竞笃笋笔笕笺笼笾筑筚筛筜筝筹签简箓箦箧箨箩箪箫篑篓篮篱簖籁籴类籼粜粝粤粪粮糁糇紧絷纟纠纡红纣纤纥约级纨纩纪纫纬纭纮纯纰纱纲纳纴纵纶纷纸纹纺纻纼纽纾线绀绁绂练组绅细织终绉绊绋绌绍绎经绐绑绒结绔绕绖绗绘给绚绛络绝绞统绠绡绢绣绤绥绦继绨绩绪绫绬续绮绯绰绱绲绳维绵绶绷绸绹绺绻综绽绾绿缀缁缂缃缄缅缆缇缈缉缊缋缌缍缎缏缐缑缒缓缔缕编缗缘缙缚缛缜缝缞缟缠缡缢缣缤缥缦缧缨缩缪缫缬缭缮缯缰缱缲缳缴缵罂网罗罚罢罴羁羟羡翘翙翚耢耧耸耻聂聋职聍联聩聪肃肠肤肷肾肿胀胁胆胜胧胨胪胫胶脉脍脏脐脑脓脔脚脱脶脸腊腌腘腭腻腼腽腾膑臜舆舣舰舱舻艰艳艹艺节芈芗芜芦苁苇苈苋苌苍苎苏苘苹茎茏茑茔茕茧荆荐荙荚荛荜荞荟荠荡荣荤荥荦荧荨荩荪荫荬荭荮药莅莜莱莲莳莴莶获莸莹莺莼萚萝萤营萦萧萨葱蒇蒉蒋蒌蓝蓟蓠蓣蓥蓦蔷蔹蔺蔼蕲蕴薮藁藓虏虑虚虫虬虮虽虾虿蚀蚁蚂蚕蚝蚬蛊蛎蛏蛮蛰蛱蛲蛳蛴蜕蜗蜡蝇蝈蝉蝎蝼蝾螀螨蟏衅衔补衬衮袄袅袆袜袭袯装裆裈裢裣裤裥褛褴襁襕见观觃规觅视觇览觉觊觋觌觍觎觏觐觑觞触觯詟誉誊讠计订讣认讥讦讧讨让讪讫训议讯记讱讲讳讴讵讶讷许讹论讻讼讽设访诀证诂诃评诅识诇诈诉诊诋诌词诎诏诐译诒诓诔试诖诗诘诙诚诛诜话诞诟诠诡询诣诤该详诧诨诩诪诫诬语诮误诰诱诲诳说诵诶请诸诹诺读诼诽课诿谀谁谂调谄谅谆谇谈谊谋谌谍谎谏谐谑谒谓谔谕谖谗谘谙谚谛谜谝谞谟谠谡谢谣谤谥谦谧谨谩谪谫谬谭谮谯谰谱谲谳谴谵谶谷豮贝贞负贠贡财责贤败账货质贩贪贫贬购贮贯贰贱贲贳贴贵贶贷贸费贺贻贼贽贾贿赀赁赂赃资赅赆赇赈赉赊赋赌赍赎赏赐赑赒赓赔赕赖Line truncated
}
function FTPYStr () {
return '萬與醜專業叢東絲丟兩嚴喪個爿豐臨為麗舉麼義烏樂喬習鄉書買亂爭於虧雲亙亞產畝親褻嚲億僅從侖倉儀們價眾優夥會傴傘偉傳傷倀倫傖偽佇體餘傭僉俠侶僥偵側僑儈儕儂俁儔儼倆儷儉債傾傯僂僨償儻儐儲儺兒兌兗黨蘭關興茲養獸囅內岡冊寫軍農塚馮衝決況凍淨淒涼淩減湊凜幾鳳鳧憑凱擊氹鑿芻劃劉則剛創刪別剗剄劊劌剴劑剮劍剝劇勸辦務勱動勵勁勞勢勳猛勩勻匭匱區醫華協單賣盧鹵臥衛卻巹廠廳曆厲壓厭厙廁廂厴廈廚廄廝縣參靉靆雙發變敘疊葉號歎嘰籲後嚇呂嗎唚噸聽啟吳嘸囈嘔嚦唄員咼嗆嗚詠哢嚨嚀噝吒噅鹹呱響啞噠嘵嗶噦嘩噲嚌噥喲嘜嗊嘮啢嗩唕喚呼嘖嗇囀齧囉嘽嘯噴嘍嚳囁嗬噯噓嚶囑嚕劈囂謔團園囪圍圇國圖圓聖壙場阪壞塊堅壇壢壩塢墳墜壟壟壚壘墾坰堊墊埡墶壋塏堖塒塤堝墊垵塹墮壪牆壯聲殼壺壼處備複夠頭誇夾奪奩奐奮獎奧妝婦媽嫵嫗媯姍薑婁婭嬈嬌孌娛媧嫻嫿嬰嬋嬸媼嬡嬪嬙嬤孫學孿寧寶實寵審憲宮寬賓寢對尋導壽將爾塵堯尷屍盡層屭屜屆屬屢屨嶼歲豈嶇崗峴嶴嵐島嶺嶽崠巋嶨嶧峽嶢嶠崢巒嶗崍嶮嶄嶸嶔崳嶁脊巔鞏巰幣帥師幃帳簾幟帶幀幫幬幘幗冪襆幹並廣莊慶廬廡庫應廟龐廢廎廩開異棄張彌弳彎彈強歸當錄彠彥徹徑徠禦憶懺憂愾懷態慫憮慪悵愴憐總懟懌戀懇惡慟懨愷惻惱惲悅愨懸慳憫驚懼慘懲憊愜慚憚慣湣慍憤憒願懾憖怵懣懶懍戇戔戲戧戰戩戶紮撲扡執擴捫掃揚擾撫拋摶摳掄搶護報擔擬攏揀擁攔擰撥擇掛摯攣掗撾撻挾撓擋撟掙擠揮撏撈損撿換搗據撚擄摑擲撣摻摜摣攬撳攙擱摟攪攜攝攄擺搖擯攤攖撐攆擷擼攛擻攢敵斂數齋斕鬥斬斷無舊時曠暘曇晝曨顯晉曬曉曄暈暉暫曖劄術樸機殺雜權條來楊榪傑極構樅樞棗櫪梘棖槍楓梟櫃檸檉梔柵標棧櫛櫳棟櫨櫟欄樹棲樣欒棬椏橈楨檔榿橋樺檜槳樁夢檮棶檢欞槨櫝槧欏橢樓欖櫬櫚櫸檟檻檳櫧橫檣櫻櫫櫥櫓櫞簷檁歡歟歐殲歿殤殘殞殮殫殯毆毀轂畢斃氈毿氌氣氫氬氳彙漢汙湯洶遝溝沒灃漚瀝淪滄渢溈滬濔濘淚澩瀧瀘濼瀉潑澤涇潔灑窪浹淺漿澆湞溮濁測澮濟瀏滻渾滸濃潯濜塗湧濤澇淶漣潿渦溳渙滌潤澗漲澀澱淵淥漬瀆漸澠漁瀋滲溫遊灣濕潰濺漵漊潷滾滯灩灄滿瀅濾濫灤濱灘澦濫瀠瀟瀲濰潛瀦瀾瀨瀕灝滅燈靈災燦煬爐燉煒熗點煉熾爍爛烴燭煙煩燒燁燴燙燼熱煥燜燾煆糊溜愛爺牘犛牽犧犢強狀獷獁猶狽麅獮獰獨狹獅獪猙獄猻獫獵獼玀豬貓蝟獻獺璣璵瑒瑪瑋環現瑲璽瑉玨琺瓏璫琿璡璉瑣瓊瑤璦璿瓔瓚甕甌電畫暢佘疇癤療瘧癘瘍鬁瘡瘋皰屙癰痙癢瘂癆瘓癇癡癉瘮瘞瘺癟癱癮癭癩癬癲臒皚皺皸盞鹽監蓋盜盤瞘眥矓著睜睞瞼瞞矚矯磯礬礦碭碼磚硨硯碸礪礱礫礎硜矽碩硤磽磑礄確鹼礙磧磣堿镟滾禮禕禰禎禱禍稟祿禪離禿稈種積稱穢穠穭稅穌穩穡窮竊竅窯竄窩窺竇窶豎競篤筍筆筧箋籠籩築篳篩簹箏籌簽簡籙簀篋籜籮簞簫簣簍籃籬籪籟糴類秈糶糲粵糞糧糝餱緊縶糸糾紆紅紂纖紇約級紈纊紀紉緯紜紘純紕紗綱納紝縱綸紛紙紋紡紵紖紐紓線紺絏紱練組紳細織終縐絆紼絀紹繹經紿綁絨結絝繞絰絎繪給絢絳絡絕絞統綆綃絹繡綌綏絛繼綈績緒綾緓續綺緋綽緔緄繩維綿綬繃綢綯綹綣綜綻綰綠綴緇緙緗緘緬纜緹緲緝縕繢緦綞緞緶線緱縋緩締縷編緡緣縉縛縟縝縫縗縞纏縭縊縑繽縹縵縲纓縮繆繅纈繚繕繒韁繾繰繯繳纘罌網羅罰罷羆羈羥羨翹翽翬耮耬聳恥聶聾職聹聯聵聰肅腸膚膁腎腫脹脅膽勝朧腖臚脛膠脈膾髒臍腦膿臠腳脫腡臉臘醃膕齶膩靦膃騰臏臢輿艤艦艙艫艱豔艸藝節羋薌蕪蘆蓯葦藶莧萇蒼苧蘇檾蘋莖蘢蔦塋煢繭荊薦薘莢蕘蓽蕎薈薺蕩榮葷滎犖熒蕁藎蓀蔭蕒葒葤藥蒞蓧萊蓮蒔萵薟獲蕕瑩鶯蓴蘀蘿螢營縈蕭薩蔥蕆蕢蔣蔞藍薊蘺蕷鎣驀薔蘞藺藹蘄蘊藪槁蘚虜慮虛蟲虯蟣雖蝦蠆蝕蟻螞蠶蠔蜆蠱蠣蟶蠻蟄蛺蟯螄蠐蛻蝸蠟蠅蟈蟬蠍螻蠑螿蟎蠨釁銜補襯袞襖嫋褘襪襲襏裝襠褌褳襝褲襇褸襤繈襴見觀覎規覓視覘覽覺覬覡覿覥覦覯覲覷觴觸觶讋譽謄訁計訂訃認譏訐訌討讓訕訖訓議訊記訒講諱謳詎訝訥許訛論訩訟諷設訪訣證詁訶評詛識詗詐訴診詆謅詞詘詔詖譯詒誆誄試詿詩詰詼誠誅詵話誕詬詮詭詢詣諍該詳詫諢詡譸誡誣語誚誤誥誘誨誑說誦誒請諸諏諾讀諑誹課諉諛誰諗調諂諒諄誶談誼謀諶諜謊諫諧謔謁謂諤諭諼讒諮諳諺諦謎諞諝謨讜謖謝謠謗諡謙謐謹謾謫譾謬譚譖譙讕譜譎讞譴譫讖穀豶貝貞負貟貢財責賢敗賬貨質販貪貧貶購貯貫貳賤賁貰貼貴貺貸貿費賀貽賊贄賈賄貲賃賂贓資賅贐賕賑賚賒賦賭齎贖賞賜贔賙賡賠賧賴Line truncated
}
function Traditionalized (cc) {
let str = ''
const ss = JTPYStr()
const tt = FTPYStr()
for (let i = 0; i < cc.length; i++) {
if (cc.charCodeAt(i) > 10000 && ss.indexOf(cc.charAt(i)) !== -1) {
str += tt.charAt(ss.indexOf(cc.charAt(i)))
} else str += cc.charAt(i)
}
return str
}
function Simplized (cc) {
let str = ''
const ss = JTPYStr()
const tt = FTPYStr()
for (let i = 0; i < cc.length; i++) {
if (cc.charCodeAt(i) > 10000 && tt.indexOf(cc.charAt(i)) !== -1) {
str += ss.charAt(tt.indexOf(cc.charAt(i)))
} else str += cc.charAt(i)
}
return str
}
function translateInitialization () {
translateButtonObject = document.getElementById('translateLink')
if (translateButtonObject) {
if (currentEncoding !== targetEncoding) {
translateButtonObject.textContent =
targetEncoding === 1
? msgToSimplifiedChinese
: msgToTraditionalChinese
setLang()
setTimeout(translateBody, translateDelay)
}
}
}
window.translateFn = {
translatePage,
Traditionalized,
Simplized
}
translateInitialization()
document.addEventListener('pjax:complete', translateInitialization)
})
+296
View File
@@ -0,0 +1,296 @@
const btf = {
debounce: (func, wait = 0, immediate = false) => {
let timeout
return (...args) => {
const later = () => {
timeout = null
if (!immediate) func(...args)
}
const callNow = immediate && !timeout
clearTimeout(timeout)
timeout = setTimeout(later, wait)
if (callNow) func(...args)
}
},
throttle: function (func, wait, options = {}) {
let timeout, context, args
let previous = 0
const later = () => {
previous = options.leading === false ? 0 : new Date().getTime()
timeout = null
func.apply(context, args)
if (!timeout) context = args = null
}
const throttled = (...params) => {
const now = new Date().getTime()
if (!previous && options.leading === false) previous = now
const remaining = wait - (now - previous)
context = this
args = params
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
func.apply(context, args)
if (!timeout) context = args = null
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining)
}
}
return throttled
},
sidebarPaddingR: () => {
const innerWidth = window.innerWidth
const clientWidth = document.body.clientWidth
const paddingRight = innerWidth - clientWidth
if (innerWidth !== clientWidth) {
document.body.style.paddingRight = paddingRight + 'px'
}
},
snackbarShow: (text, showAction = false, duration = 2000) => {
const { position, bgLight, bgDark } = GLOBAL_CONFIG.Snackbar
const bg = document.documentElement.getAttribute('data-theme') === 'light' ? bgLight : bgDark
Snackbar.show({
text,
backgroundColor: bg,
showAction,
duration,
pos: position,
customClass: 'snackbar-css'
})
},
diffDate: (d, more = false) => {
const dateNow = new Date()
const datePost = new Date(d)
const dateDiff = dateNow.getTime() - datePost.getTime()
const minute = 1000 * 60
const hour = minute * 60
const day = hour * 24
const month = day * 30
const { dateSuffix } = GLOBAL_CONFIG
if (!more) return parseInt(dateDiff / day)
const monthCount = dateDiff / month
const dayCount = dateDiff / day
const hourCount = dateDiff / hour
const minuteCount = dateDiff / minute
if (monthCount > 12) return datePost.toISOString().slice(0, 10)
if (monthCount >= 1) return `${parseInt(monthCount)} ${dateSuffix.month}`
if (dayCount >= 1) return `${parseInt(dayCount)} ${dateSuffix.day}`
if (hourCount >= 1) return `${parseInt(hourCount)} ${dateSuffix.hour}`
if (minuteCount >= 1) return `${parseInt(minuteCount)} ${dateSuffix.min}`
return dateSuffix.just
},
loadComment: (dom, callback) => {
if ('IntersectionObserver' in window) {
const observerItem = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
callback()
observerItem.disconnect()
}
}, { threshold: [0] })
observerItem.observe(dom)
} else {
callback()
}
},
scrollToDest: (pos, time = 500) => {
const currentPos = window.pageYOffset
const isNavFixed = document.getElementById('page-header').classList.contains('fixed')
if (currentPos > pos || isNavFixed) pos = pos - 70
if ('scrollBehavior' in document.documentElement.style) {
window.scrollTo({
top: pos,
behavior: 'smooth'
})
return
}
let start = null
pos = +pos
window.requestAnimationFrame(function step (currentTime) {
start = !start ? currentTime : start
const progress = currentTime - start
if (currentPos < pos) {
window.scrollTo(0, ((pos - currentPos) * progress / time) + currentPos)
} else {
window.scrollTo(0, currentPos - ((currentPos - pos) * progress / time))
}
if (progress < time) {
window.requestAnimationFrame(step)
} else {
window.scrollTo(0, pos)
}
})
},
animateIn: (ele, text) => {
ele.style.display = 'block'
ele.style.animation = text
},
animateOut: (ele, text) => {
ele.addEventListener('animationend', function f () {
ele.style.display = ''
ele.style.animation = ''
ele.removeEventListener('animationend', f)
})
ele.style.animation = text
},
wrap: (selector, eleType, options) => {
const createEle = document.createElement(eleType)
for (const [key, value] of Object.entries(options)) {
createEle.setAttribute(key, value)
}
selector.parentNode.insertBefore(createEle, selector)
createEle.appendChild(selector)
},
isHidden: ele => ele.offsetHeight === 0 && ele.offsetWidth === 0,
getEleTop: ele => {
let actualTop = ele.offsetTop
let current = ele.offsetParent
while (current !== null) {
actualTop += current.offsetTop
current = current.offsetParent
}
return actualTop
},
loadLightbox: ele => {
const service = GLOBAL_CONFIG.lightbox
if (service === 'mediumZoom') {
mediumZoom(ele, { background: 'var(--zoom-bg)' })
}
if (service === 'fancybox') {
Array.from(ele).forEach(i => {
if (i.parentNode.tagName !== 'A') {
const dataSrc = i.dataset.lazySrc || i.src
const dataCaption = i.title || i.alt || ''
btf.wrap(i, 'a', { href: dataSrc, 'data-fancybox': 'gallery', 'data-caption': dataCaption, 'data-thumb': dataSrc })
}
})
if (!window.fancyboxRun) {
Fancybox.bind('[data-fancybox]', {
Hash: false,
Thumbs: {
showOnStart: false
},
Images: {
Panzoom: {
maxScale: 4
}
},
Carousel: {
transition: 'slide'
},
Toolbar: {
display: {
left: ['infobar'],
middle: [
'zoomIn',
'zoomOut',
'toggle1to1',
'rotateCCW',
'rotateCW',
'flipX',
'flipY'
],
right: ['slideshow', 'thumbs', 'close']
}
}
})
window.fancyboxRun = true
}
}
},
setLoading: {
add: ele => {
const html = `
<div class="loading-container">
<div class="loading-item">
<div></div><div></div><div></div><div></div><div></div>
</div>
</div>
`
ele.insertAdjacentHTML('afterend', html)
},
remove: ele => {
ele.nextElementSibling.remove()
}
},
updateAnchor: (anchor) => {
if (anchor !== window.location.hash) {
if (!anchor) anchor = location.pathname
const title = GLOBAL_CONFIG_SITE.title
window.history.replaceState({
url: location.href,
title
}, title, anchor)
}
},
getScrollPercent: (currentTop, ele) => {
const docHeight = ele.clientHeight
const winHeight = document.documentElement.clientHeight
const headerHeight = ele.offsetTop
const contentMath = (docHeight > winHeight) ? (docHeight - winHeight) : (document.documentElement.scrollHeight - winHeight)
const scrollPercent = (currentTop - headerHeight) / (contentMath)
const scrollPercentRounded = Math.round(scrollPercent * 100)
const percentage = (scrollPercentRounded > 100) ? 100 : (scrollPercentRounded <= 0) ? 0 : scrollPercentRounded
return percentage
},
addGlobalFn: (key, fn, name = false, parent = window) => {
const globalFn = parent.globalFn || {}
const keyObj = globalFn[key] || {}
if (name && keyObj[name]) return
name = name || Object.keys(keyObj).length
keyObj[name] = fn
globalFn[key] = keyObj
parent.globalFn = globalFn
},
addEventListenerPjax: (ele, event, fn, option = false) => {
ele.addEventListener(event, fn, option)
btf.addGlobalFn('pjax', () => {
ele.removeEventListener(event, fn, option)
})
},
removeGlobalFnEvent: (key, parent = window) => {
const { globalFn = {} } = parent
const keyObj = globalFn[key] || {}
const keyArr = Object.keys(keyObj)
if (!keyArr.length) return
keyArr.forEach(i => {
keyObj[i]()
})
delete parent.globalFn[key]
}
}