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 ? '' : '' const highlightCopyEle = highlightCopy ? '
' : '' 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 = '' 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 = `
${langName}
` 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 = `
${langName}
` createEle(highlightLangEle, item, 'hl') } else { createEle('', item, 'hl') } }) } } /** * PhotoFigcaption */ const addPhotoFigcaption = () => { document.querySelectorAll('#article-container img').forEach(item => { const altValue = item.title || item.alt if (!altValue) return const ele = document.createElement('div') ele.className = 'img-alt is-center' ele.textContent = altValue item.insertAdjacentElement('afterend', ele) }) } /** * Lightbox */ const runLightbox = () => { btf.loadLightbox(document.querySelectorAll('#article-container img:not(.no-lightbox)')) } /** * justified-gallery 圖庫排版 */ const fetchUrl = async (url) => { const response = await fetch(url) return await response.json() } const runJustifiedGallery = (item, data, isButton = false, tabs) => { const dataLength = data.length const ig = new InfiniteGrid.JustifiedInfiniteGrid(item, { gap: 5, isConstantSize: true, sizeRange: [150, 600], useResizeObserver: true, observeChildren: true, useTransform: true // useRecycle: false }) if (tabs) { btf.addGlobalFn('igOfTabs', () => { ig.destroy() }, false, tabs) } const replaceDq = str => str.replace(/"/g, '"') // replace double quotes to " const getItems = (nextGroupKey, count) => { const nextItems = [] const startCount = (nextGroupKey - 1) * count for (let i = 0; i < count; ++i) { const num = startCount + i if (num >= dataLength) { break } const item = data[num] const alt = item.alt ? `alt="${replaceDq(item.alt)}"` : '' const title = item.title ? `title="${replaceDq(item.title)}"` : '' nextItems.push(`
`) } 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() })