This commit is contained in:
2024-07-30 21:41:51 +08:00
commit 192ef21b12
574 changed files with 70686 additions and 0 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+154
View File
@@ -0,0 +1,154 @@
<!DOCTYPE html><html lang="zh-Hans" data-theme="light"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0,viewport-fit=cover"><title>Page Not Found | QuickReference</title><meta name="author" content="shenjianZ"><meta name="copyright" content="shenjianZ"><meta name="format-detection" content="telephone=no"><meta name="theme-color" content="#ffffff"><meta name="description" content="一份快捷简便的文档,便于查阅编程的细节">
<meta property="og:type" content="website">
<meta property="og:title" content="QuickReference">
<meta property="og:url" content="http://rq.shenjianl.cn/404.html">
<meta property="og:site_name" content="QuickReference">
<meta property="og:description" content="一份快捷简便的文档,便于查阅编程的细节">
<meta property="og:locale">
<meta property="og:image" content="http://rq.shenjianl.cn/img/avatar.jpg">
<meta property="article:author" content="shenjianZ">
<meta property="article:tag" content="doucment">
<meta name="twitter:card" content="summary">
<meta name="twitter:image" content="http://rq.shenjianl.cn/img/avatar.jpg"><link rel="shortcut icon" href="/img/favicon.png"><link rel="canonical" href="http://rq.shenjianl.cn/404.html"><link rel="preconnect" href="//cdn.jsdelivr.net"/><link rel="preconnect" href="//busuanzi.ibruce.info"/><link rel="stylesheet" href="/css/index.css?v=4.13.0"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.5.1/css/all.min.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fancyapps/ui@5.0.33/dist/fancybox/fancybox.min.css" media="print" onload="this.media='all'"><script>const GLOBAL_CONFIG = {
root: '/',
algolia: undefined,
localSearch: undefined,
translate: undefined,
noticeOutdate: undefined,
highlight: {"plugin":"highlight.js","highlightCopy":true,"highlightLang":true,"highlightHeightLimit":false},
copy: {
success: 'Copy Successful',
error: 'Copy Error',
noSupport: 'Browser Not Supported'
},
relativeDate: {
homepage: false,
post: false
},
runtime: 'days',
dateSuffix: {
just: 'Just now',
min: 'minutes ago',
hour: 'hours ago',
day: 'days ago',
month: 'months ago'
},
copyright: undefined,
lightbox: 'fancybox',
Snackbar: undefined,
infinitegrid: {
js: 'https://cdn.jsdelivr.net/npm/@egjs/infinitegrid@4.11.1/dist/infinitegrid.min.js',
buttonText: 'Load More'
},
isPhotoFigcaption: false,
islazyload: false,
isAnchor: false,
percent: {
toc: true,
rightside: false,
},
autoDarkmode: false
}</script><script id="config-diff">var GLOBAL_CONFIG_SITE = {
title: 'Page Not Found',
isPost: false,
isHome: false,
isHighlightShrink: false,
isToc: false,
postUpdate: '2024-07-30 21:38:06'
}</script><script>(win=>{
win.saveToLocal = {
set: (key, value, ttl) => {
if (ttl === 0) return
const now = Date.now()
const expiry = now + ttl * 86400000
const item = {
value,
expiry
}
localStorage.setItem(key, JSON.stringify(item))
},
get: key => {
const itemStr = localStorage.getItem(key)
if (!itemStr) {
return undefined
}
const item = JSON.parse(itemStr)
const now = Date.now()
if (now > item.expiry) {
localStorage.removeItem(key)
return undefined
}
return item.value
}
}
win.getScript = (url, attr = {}) => new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = url
script.async = true
script.onerror = reject
script.onload = script.onreadystatechange = function() {
const loadState = this.readyState
if (loadState && loadState !== 'loaded' && loadState !== 'complete') return
script.onload = script.onreadystatechange = null
resolve()
}
Object.keys(attr).forEach(key => {
script.setAttribute(key, attr[key])
})
document.head.appendChild(script)
})
win.getCSS = (url, id = false) => new Promise((resolve, reject) => {
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = url
if (id) link.id = id
link.onerror = reject
link.onload = link.onreadystatechange = function() {
const loadState = this.readyState
if (loadState && loadState !== 'loaded' && loadState !== 'complete') return
link.onload = link.onreadystatechange = null
resolve()
}
document.head.appendChild(link)
})
win.activateDarkMode = () => {
document.documentElement.setAttribute('data-theme', 'dark')
if (document.querySelector('meta[name="theme-color"]') !== null) {
document.querySelector('meta[name="theme-color"]').setAttribute('content', '#0d0d0d')
}
}
win.activateLightMode = () => {
document.documentElement.setAttribute('data-theme', 'light')
if (document.querySelector('meta[name="theme-color"]') !== null) {
document.querySelector('meta[name="theme-color"]').setAttribute('content', '#ffffff')
}
}
const t = saveToLocal.get('theme')
if (t === 'dark') activateDarkMode()
else if (t === 'light') activateLightMode()
const asideStatus = saveToLocal.get('aside-status')
if (asideStatus !== undefined) {
if (asideStatus === 'hide') {
document.documentElement.classList.add('hide-aside')
} else {
document.documentElement.classList.remove('hide-aside')
}
}
const detectApple = () => {
if(/iPad|iPhone|iPod|Macintosh/.test(navigator.userAgent)){
document.documentElement.classList.add('apple')
}
}
detectApple()
})(window)</script><meta name="generator" content="Hexo 7.3.0"></head><body><div id="sidebar"><div id="menu-mask"></div><div id="sidebar-menus"><div class="avatar-img is-center"><img src="/img/avatar.jpg" onerror="onerror=null;src='/img/friend_404.gif'" alt="avatar"/></div><div class="sidebar-site-data site-data is-center"><a href="/archives/"><div class="headline">Articles</div><div class="length-num">2</div></a><a href="/tags/"><div class="headline">Tags</div><div class="length-num">0</div></a><a href="/categories/"><div class="headline">Categories</div><div class="length-num">0</div></a></div><hr class="custom-hr"/><div class="menus_items"><div class="menus_item"><a class="site-page" href="/"><i class="fa-fw fas fa-home"></i><span> Home</span></a></div><div class="menus_item"><a class="site-page" href="/archives/"><i class="fa-fw fas fa-archive"></i><span> Archives</span></a></div><div class="menus_item"><a class="site-page" href="/tags/"><i class="fa-fw fas fa-tags"></i><span> Tags</span></a></div><div class="menus_item"><a class="site-page" href="/categories/"><i class="fa-fw fas fa-folder-open"></i><span> Categories</span></a></div><div class="menus_item"><a class="site-page group" href="javascript:void(0);"><i class="fa-fw fas fa-list"></i><span> List</span><i class="fas fa-chevron-down"></i></a><ul class="menus_item_child"><li><a class="site-page child" href="/music/"><i class="fa-fw fas fa-music"></i><span> Music</span></a></li><li><a class="site-page child" href="/movies/"><i class="fa-fw fas fa-video"></i><span> Movie</span></a></li></ul></div><div class="menus_item"><a class="site-page" href="/link/"><i class="fa-fw fas fa-link"></i><span> Link</span></a></div><div class="menus_item"><a class="site-page" href="/about/"><i class="fa-fw fas fa-heart"></i><span> About</span></a></div></div></div></div><div class="error404" id="body-wrap"><header class="not-top-img fixed" id="page-header"><nav id="nav"><span id="blog-info"><a href="/" title="QuickReference"><span class="site-name">QuickReference</span></a></span><div id="menus"><div class="menus_items"><div class="menus_item"><a class="site-page" href="/"><i class="fa-fw fas fa-home"></i><span> Home</span></a></div><div class="menus_item"><a class="site-page" href="/archives/"><i class="fa-fw fas fa-archive"></i><span> Archives</span></a></div><div class="menus_item"><a class="site-page" href="/tags/"><i class="fa-fw fas fa-tags"></i><span> Tags</span></a></div><div class="menus_item"><a class="site-page" href="/categories/"><i class="fa-fw fas fa-folder-open"></i><span> Categories</span></a></div><div class="menus_item"><a class="site-page group" href="javascript:void(0);"><i class="fa-fw fas fa-list"></i><span> List</span><i class="fas fa-chevron-down"></i></a><ul class="menus_item_child"><li><a class="site-page child" href="/music/"><i class="fa-fw fas fa-music"></i><span> Music</span></a></li><li><a class="site-page child" href="/movies/"><i class="fa-fw fas fa-video"></i><span> Movie</span></a></li></ul></div><div class="menus_item"><a class="site-page" href="/link/"><i class="fa-fw fas fa-link"></i><span> Link</span></a></div><div class="menus_item"><a class="site-page" href="/about/"><i class="fa-fw fas fa-heart"></i><span> About</span></a></div></div><div id="toggle-menu"><a class="site-page" href="javascript:void(0);"><i class="fas fa-bars fa-fw"></i></a></div></div></nav></header><div id="error-wrap"><div class="error-content"><div class="error-img"><img src="https://i.loli.net/2020/05/19/aKOcLiyPl2JQdFD.png" alt="Page not found"></div><div class="error-info"><h1 class="error_title">404</h1><div class="error_subtitle">Page Not Found</div></div></div></div></div><div id="rightside"><div id="rightside-config-hide"><button id="darkmode" type="button" title="Toggle Between Light And Dark Mode"><i class="fas fa-adjust"></i></button><button id="hide-aside-btn" type="button" title="Toggle between Single-column and Double-column"><i class="fas fa-arrows-alt-h"></i></button></div><div id="rightside-config-show"><button id="rightside-config" type="button" title="Setting"><i class="fas fa-cog fa-spin"></i></button><button id="go-up" type="button" title="Back To Top"><span class="scroll-percent"></span><i class="fas fa-arrow-up"></i></button></div></div><div><script src="/js/utils.js?v=4.13.0"></script><script src="/js/main.js?v=4.13.0"></script><script src="https://cdn.jsdelivr.net/npm/@fancyapps/ui@5.0.33/dist/fancybox/fancybox.umd.min.js"></script><div class="js-pjax"></div><script async data-pjax src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script></div></body></html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+6284
View File
File diff suppressed because it is too large Load Diff
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

+167
View File
File diff suppressed because one or more lines are too long
+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
File diff suppressed because one or more lines are too long
+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]
}
}