PageSourceSearch

https://z-bosh.github.io/js/main.js

js z-bosh.github.io collected 2026-10-03 10:14:07 UTC 30,353 bytes, 943 lines download raw bytes

1document.addEventListener('DOMContentLoaded', () => {
2  let headerContentWidth, $nav
3  let mobileSidebarOpen = false
4
5  const adjustMenu = init => {
6    const getAllWidth = ele => Array.from(ele).reduce((width, i) => width + i.offsetWidth, 0)
7
8    if (init) {
9      const blogInfoWidth = getAllWidth(document.querySelector('#blog-info > a').children)
10      const menusWidth = getAllWidth(document.getElementById('menus').children)
11      headerContentWidth = blogInfoWidth + menusWidth
12      $nav = document.getElementById('nav')
13    }
14
15    const hideMenuIndex = window.innerWidth <= 768 || headerContentWidth > $nav.offsetWidth - 120
16    $nav.classList.toggle('hide-menu', hideMenuIndex)
17  }
18
19  // 初始化header
20  const initAdjust = () => {
21    adjustMenu(true)
22    $nav.classList.add('show')
23  }
24
25  // sidebar menus
26  const sidebarFn = {
27    open: () => {
28      btf.overflowPaddingR.add()
29      btf.animateIn(document.getElementById('menu-mask'), 'to_show 0.5s')
30      document.getElementById('sidebar-menus').classList.add('open')
31      mobileSidebarOpen = true
32    },
33    close: () => {
34      btf.overflowPaddingR.remove()
35      btf.animateOut(document.getElementById('menu-mask'), 'to_hide 0.5s')
36      document.getElementById('sidebar-menus').classList.remove('open')
37      mobileSidebarOpen = false
38    }
39  }
40
41  /**
42   * 首頁top_img底下的箭頭
43   */
44  const scrollDownInIndex = () => {
45    const handleScrollToDest = () => {
46      btf.scrollToDest(document.getElementById('content-inner').offsetTop, 300)
47    }
48
49    const $scrollDownEle = document.getElementById('scroll-down')
50    $scrollDownEle && btf.addEventListenerPjax($scrollDownEle, 'click', handleScrollToDest)
51  }
52
53  /**
54   * 代碼
55   * 只適用於Hexo默認的代碼渲染
56   */
57  const addHighlightTool = () => {
58    const highLight = GLOBAL_CONFIG.highlight
59    if (!highLight) return
60
61    const { highlightCopy, highlightLang, highlightHeightLimit, highlightFullpage, highlightMacStyle, plugin } = highLight
62    const isHighlightShrink = GLOBAL_CONFIG_SITE.isHighlightShrink
63    const isShowTool = highlightCopy || highlightLang || isHighlightShrink !== undefined || highlightFullpage || highlightMacStyle
64    const $figureHighlight = plugin === 'highlight.js' ? document.querySelectorAll('figure.highlight') : document.querySelectorAll('pre[class*="language-"]')
65
66    if (!((isShowTool || highlightHeightLimit) && $figureHighlight.length)) return
67
68    const isPrismjs = plugin === 'prismjs'
69    const highlightShrinkClass = isHighlightShrink === true ? 'closed' : ''
70    const highlightShrinkEle = isHighlightShrink !== undefined ? '<i class="fas fa-angle-down expand"></i>' : ''
71    const highlightCopyEle = highlightCopy ? '<div class="copy-notice"></div><i class="fas fa-paste copy-button"></i>' : ''
72    const highlightMacStyleEle = '<div class="macStyle"><div class="mac-close"></div><div class="mac-minimize"></div><div class="mac-maximize"></div></div>'
73    const highlightFullpageEle = highlightFullpage ? '<i class="fa-solid fa-up-right-and-down-left-from-center fullpage-button"></i>' : ''
74
75    const alertInfo = (ele, text) => {
76      if (GLOBAL_CONFIG.Snackbar !== undefined) {
77        btf.snackbarShow(text)
78      } else {
79        ele.textContent = text
80        ele.style.opacity = 1
81        setTimeout(() => { ele.style.opacity = 0 }, 800)
82      }
83    }
84
85    const copy = async (text, ctx) => {
86      try {
87        await navigator.clipboard.writeText(text)
88        alertInfo(ctx, GLOBAL_CONFIG.copy.success)
89      } catch (err) {
90        console.error('Failed to copy: ', err)
91        alertInfo(ctx, GLOBAL_CONFIG.copy.noSupport)
92      }
93    }
94
95    // click events
96    const highlightCopyFn = (ele, clickEle) => {
97      const $buttonParent = ele.parentNode
98      $buttonParent.classList.add('copy-true')
99      const preCodeSelector = isPrismjs ? 'pre code' : 'table .code pre'
100      const codeElement = $buttonParent.querySelector(preCodeSelector)
101      if (!codeElement) return
102      copy(codeElement.innerText, clickEle.previousElementSibling)
103      $buttonParent.classList.remove('copy-true')
104    }
105
106    const highlightShrinkFn = ele => ele.classList.toggle('closed')
107
108    const codeFullpage = (item, clickEle) => {
109      const wrapEle = item.closest('figure.highlight')
110      const isFullpage = wrapEle.classList.toggle('code-fullpage')
111
112      document.body.style.overflow = isFullpage ? 'hidden' : ''
113      clickEle.classList.toggle('fa-down-left-and-up-right-to-center', isFullpage)
114      clickEle.classList.toggle('fa-up-right-and-down-left-from-center', !isFullpage)
115    }
116
117    const highlightToolsFn = e => {
118      const $target = e.target.classList
119      const currentElement = e.currentTarget
120      if ($target.contains('expand')) highlightShrinkFn(currentElement)
121      else if ($target.contains('copy-button')) highlightCopyFn(currentElement, e.target)
122      else if ($target.contains('fullpage-button')) codeFullpage(currentElement, e.target)
123    }
124
125    const expandCode = e => e.currentTarget.classList.toggle('expand-done')
126
127    // 獲取隱藏狀態下元素的真實高度
128    const getActualHeight = item => {
129      const hiddenElements = new Map()
130
131      const fix = () => {
132        let current = item
133        while (current !== document.body && current != null) {
134          if (window.getComputedStyle(current).display === 'none') {
135            hiddenElements.set(current, current.getAttribute('style') || '')
136          }
137          current = current.parentNode
138        }
139
140        const style = 'visibility: hidden !important; display: block !important;'
141        hiddenElements.forEach((originalStyle, elem) => {
142          elem.setAttribute('style', originalStyle ? originalStyle + ';' + style : style)
143        })
144      }
145
146      const restore = () => {
147        hiddenElements.forEach((originalStyle, elem) => {
148          if (originalStyle === '') elem.removeAttribute('style')
149          else elem.setAttribute('style', originalStyle)
150        })
151      }
152
153      fix()
154      const height = item.offsetHeight
155      restore()
156      return height
157    }
158
159    const createEle = (lang, item) => {
160      const fragment = document.createDocumentFragment()
161
162      if (isShowTool) {
163        const hlTools = document.createElement('div')
164        hlTools.className = `highlight-tools ${highlightShrinkClass}`
165        hlTools.innerHTML = highlightMacStyleEle + highlightShrinkEle + lang + highlightCopyEle + highlightFullpageEle
166        btf.addEventListenerPjax(hlTools, 'click', highlightToolsFn)
167        fragment.appendChild(hlTools)
168      }
169
170      if (highlightHeightLimit && getActualHeight(item) > highlightHeightLimit + 30) {
171        const ele = document.createElement('div')
172        ele.className = 'code-expand-btn'
173        ele.innerHTML = '<i class="fas fa-angle-double-down"></i>'
174        btf.addEventListenerPjax(ele, 'click', expandCode)
175        fragment.appendChild(ele)
176      }
177
178      isPrismjs ? item.parentNode.insertBefore(fragment, item) : item.insertBefore(fragment, item.firstChild)
179    }
180
181    $figureHighlight.forEach(item => {
182      let langName = ''
183      if (isPrismjs) btf.wrap(item, 'figure', { class: 'highlight' })
184
185      if (!highlightLang) {
186        createEle('', item)
187        return
188      }
189
190      if (isPrismjs) {
191        langName = item.getAttribute('data-language') || 'Code'
192      } else {
193        langName = item.getAttribute('class').split(' ')[1]
194        if (langName === 'plain' || langName === undefined) langName = 'Code'
195      }
196      createEle(`<div class="code-lang">${langName}</div>`, item)
197    })
198  }
199
200  /**
201   * PhotoFigcaption
202   */
203  const addPhotoFigcaption = () => {
204    if (!GLOBAL_CONFIG.isPhotoFigcaption) return
205    document.querySelectorAll('#article-container img').forEach(item => {
206      const altValue = item.title || item.alt
207      if (!altValue) return
208      const ele = document.createElement('div')
209      ele.className = 'img-alt is-center'
210      ele.textContent = altValue
211      item.insertAdjacentElement('afterend', ele)
212    })
213  }
214
215  /**
216   * Lightbox
217   */
218  const runLightbox = () => {
219    btf.loadLightbox(document.querySelectorAll('#article-container img:not(.no-lightbox)'))
220  }
221
222  /**
223   * justified-gallery 圖庫排版
224   */
225
226  const fetchUrl = async url => {
227    const response = await fetch(url)
228    return await response.json()
229  }
230
231  const runJustifiedGallery = (item, data, isButton = false, tabs) => {
232    const dataLength = data.length
233
234    const ig = new InfiniteGrid.JustifiedInfiniteGrid(item, {
235      gap: 5,
236      isConstantSize: true,
237      sizeRange: [150, 600],
238      // useResizeObserver: true,
239      // observeChildren: true,
240      useTransform: true
241      // useRecycle: false
242    })
243
244    const replaceDq = str => str.replace(/"/g, '&quot;') // replace double quotes to &quot;
245
246    const getItems = (nextGroupKey, count) => {
247      const nextItems = []
248      const startCount = (nextGroupKey - 1) * count
249
250      for (let i = 0; i < count; ++i) {
251        const num = startCount + i
252        if (num >= dataLength) {
253          break
254        }
255
256        const item = data[num]
257        const alt = item.alt ? `alt="${replaceDq(item.alt)}"` : ''
258        const title = item.title ? `title="${replaceDq(item.title)}"` : ''
259
260        nextItems.push(`<div class="item">
261            <img src="${item.url}" data-grid-maintained-target="true" ${alt + title} />
262          </div>`)
263      }
264      return nextItems
265    }
266
267    const buttonText = GLOBAL_CONFIG.infinitegrid.buttonText
268    const addButton = item => {
269      const button = document.createElement('button')
270      button.innerHTML = buttonText + '<i class="fa-solid fa-arrow-down"></i>'
271
272      button.addEventListener('click', e => {
273        e.target.closest('button').remove()
274        btf.setLoading.add(item)
275        appendItem(ig.getGroups().length + 1, 10)
276      }, { once: true })
277
278      item.insertAdjacentElement('afterend', button)
279    }
280
281    const appendItem = (nextGroupKey, count) => {
282      ig.append(getItems(nextGroupKey, count), nextGroupKey)
283    }
284
285    const maxGroupKey = Math.ceil(dataLength / 10)
286    let isLayoutHidden = false
287
288    const completeFn = e => {
289      if (tabs) {
290        const parentNode = item.parentNode
291
292        if (isLayoutHidden) {
293          parentNode.style.visibility = 'visible'
294        }
295
296        if (item.offsetHeight === 0) {
297          parentNode.style.visibility = 'hidden'
298          isLayoutHidden = true
299        }
300      }
301
302      const { updated, isResize, mounted } = e
303      if (!updated.length || !mounted.length || isResize) {
304        return
305      }
306
307      btf.loadLightbox(item.querySelectorAll('img:not(.medium-zoom-image)'))
308
309      if (ig.getGroups().length === maxGroupKey) {
310        btf.setLoading.remove(item)
311        !tabs && ig.off('renderComplete', completeFn)
312        return
313      }
314
315      if (isButton) {
316        btf.setLoading.remove(item)
317        addButton(item)
318      }
319    }
320
321    const requestAppendFn = btf.debounce(e => {
322      const nextGroupKey = (+e.groupKey || 0) + 1
323      appendItem(nextGroupKey, 10)
324
325      if (nextGroupKey === maxGroupKey) {
326        ig.off('requestAppend', requestAppendFn)
327      }
328    }, 300)
329
330    btf.setLoading.add(item)
331    ig.on('renderComplete', completeFn)
332
333    if (isButton) {
334      appendItem(1, 10)
335    } else {
336      ig.on('requestAppend', requestAppendFn)
337      ig.renderItems()
338    }
339
340    btf.addGlobalFn('pjaxSendOnce', () =>
340 { ig.destroy() })
341  }
342
343  const addJustifiedGallery = async (ele, tabs = false) => {
344    if (!ele.length) return
345    const init = async () => {
346      for (const item of ele) {
347        if (btf.isHidden(item) || item.classList.contains('loaded')) continue
348
349        const isButton = item.getAttribute('data-button') === 'true'
350        const children = item.firstElementChild
351        const text = children.textContent
352        children.textContent = ''
353        item.classList.add('loaded')
354        try {
355          const content = item.getAttribute('data-type') === 'url' ? await fetchUrl(text) : JSON.parse(text)
356          runJustifiedGallery(children, content, isButton, tabs)
357        } catch (e) {
358          console.error('Gallery data parsing failed:', e)
359        }
360      }
361    }
362
363    if (typeof InfiniteGrid === 'function') {
364      init()
365    } else {
366      await btf.getScript(`${GLOBAL_CONFIG.infinitegrid.js}`)
367      init()
368    }
369  }
370
371  /**
372   * rightside scroll percent
373   */
374  const rightsideScrollPercent = currentTop => {
375    const scrollPercent = btf.getScrollPercent(currentTop, document.body)
376    const goUpElement = document.getElementById('go-up')
377
378    if (scrollPercent < 95) {
379      goUpElement.classList.add('show-percent')
380      goUpElement.querySelector('.scroll-percent').textContent = scrollPercent
381    } else {
382      goUpElement.classList.remove('show-percent')
383    }
384  }
385
386  /**
387   * 滾動處理
388   */
389  const scrollFn = () => {
390    const $rightside = document.getElementById('rightside')
391    const innerHeight = window.innerHeight + 56
392    let initTop = 0
393    const $header = document.getElementById('page-header')
394    const isChatBtn = typeof chatBtn !== 'undefined'
395    const isShowPercent = GLOBAL_CONFIG.percent.rightside
396
397    // 檢查文檔高度是否小於視窗高度
398    const checkDocumentHeight = () => {
399      if (document.body.scrollHeight <= innerHeight) {
400        $rightside.classList.add('rightside-show')
401        return true
402      }
403      return false
404    }
405
406    // 如果文檔高度小於視窗高度,直接返回
407    if (checkDocumentHeight()) return
408
409    // find the scroll direction
410    const scrollDirection = currentTop => {
411      const result = currentTop > initTop // true is down & false is up
412      initTop = currentTop
413      return result
414    }
415
416    let flag = ''
417    const scrollTask = btf.throttle(() => {
418      const currentTop = window.scrollY || document.documentElement.scrollTop
419      const isDown = scrollDirection(currentTop)
420      if (currentTop > 56) {
421        if (flag === '') {
422          $header.classList.add('nav-fixed')
423          $rightside.classList.add('rightside-show')
424        }
425
426        if (isDown) {
427          if (flag !== 'down') {
428            $header.classList.remove('nav-visible')
429            isChatBtn && window.chatBtn.hide()
430            flag = 'down'
431          }
432        } else {
433          if (flag !== 'up') {
434            $header.classList.add('nav-visible')
435            isChatBtn && window.chatBtn.show()
436            flag = 'up'
437          }
438        }
439      } else {
440        flag = ''
441        if (currentTop === 0) {
442          $header.classList.remove('nav-fixed', 'nav-visible')
443        }
444        $rightside.classList.remove('rightside-show')
445      }
446
447      isShowPercent && rightsideScrollPercent(currentTop)
448      checkDocumentHeight()
449    }, 300)
450
451    btf.addEventListenerPjax(window, 'scroll', scrollTask, { passive: true })
452  }
453
454  /**
455  * toc,anchor
456  */
457  const scrollFnToDo = () => {
458    const isToc = GLOBAL_CONFIG_SITE.isToc
459    const isAnchor = GLOBAL_CONFIG.isAnchor
460    const $article = document.getElementById('article-container')
461
462    if (!($article && (isToc || isAnchor))) return
463
464    let $tocLink, $cardToc, autoScrollToc, $tocPercentage, isExpand
465
466    if (isToc) {
467      const $cardTocLayout = document.getElementById('card-toc')
468      $cardToc = $cardTocLayout.querySelector('.toc-content')
469      $tocLink = $cardToc.querySelectorAll('.toc-link')
470      $tocPercentage = $cardTocLayout.querySelector('.toc-percentage')
471      isExpand = $cardToc.classList.contains('is-expand')
472
473      // toc元素點擊
474      const tocItemClickFn = e => {
475        const target = e.target.closest('.toc-link')
476        if (!target) return
477
478        e.preventDefault()
479        btf.scrollToDest(btf.getEleTop(document.getElementById(decodeURI(target.getAttribute('href')).replace('#', ''))), 300)
480        if (window.innerWidth < 900) {
481          $cardTocLayout.classList.remove('open')
482        }
483      }
484
485      btf.addEventListenerPjax($cardToc, 'click', tocItemClickFn)
486
487      autoScrollToc = item => {
488        const sidebarHeight = $cardToc.clientHeight
489        const itemOffsetTop = item.offsetTop
490        const itemHeight = item.clientHeight
491        const scrollTop = $cardToc.scrollTop
492        const offset = itemOffsetTop - scrollTop
493        const middlePosition = (sidebarHeight - itemHeight) / 2
494
495        if (offset !== middlePosition) {
496          $cardToc.scrollTop = scrollTop + (offset - middlePosition)
497        }
498      }
499
500      // 處理 hexo-blog-encrypt 事件
501      $cardToc.style.display = 'block'
502    }
503
504    // find head position & add active class
505    const $articleList = $article.querySelectorAll('h1,h2,h3,h4,h5,h6')
506    let detectItem = ''
507
508    const findHeadPosition = top => {
509      if (top === 0) return false
510
511      let currentId = ''
512      let currentIndex = ''
513
514      for (let i = 0; i < $articleList.length; i++) {
515        const ele = $articleList[i]
516        if (top > btf.getEleTop(ele) - 80) {
517          const id = ele.id
518          currentId = id ? '#' + encodeURI(id) : ''
519          currentIndex = i
520        } else {
521          break
522        }
523      }
524
525      if (detectItem === currentIndex) return
526
527      if (isAnchor) btf.updateAnchor(currentId)
528
529      detectItem = currentIndex
530
531      if (isToc) {
532        $cardToc.querySelectorAll('.active').forEach(i => i.classList.remove('active'))
533
534        if (currentId) {
535          const currentActive = $tocLink[currentIndex]
536          currentActive.classList.add('active')
537
538          setTimeout(() => autoScrollToc(currentActive), 0)
539
540          if (!isExpand) {
541            let parent = currentActive.parentNode
542            while (!parent.matches('.toc')) {
543              if (parent.matches('li')) parent.classList.add('active')
544              parent = parent.parentNode
545            }
546          }
547        }
548      }
549    }
550
551    // main of scroll
552    const tocScrollFn = btf.throttle(() => {
553      const currentTop = window.scrollY || document.documentElement.scrollTop
554      if (isToc && GLOBAL_CONFIG.percent.toc) {
555        $tocPercentage.textContent = btf.getScrollPercent(currentTop, $article)
556      }
557      findHeadPosition(currentTop)
558    }, 100)
559
560    btf.addEventListenerPjax(window, 'scroll', tocScrollFn, { passive: true })
561  }
562
563  const handleThemeChange = mode => {
564    const globalFn = window.globalFn || {}
565    const themeChange = globalFn.themeChange || {}
566    if (!themeChange) {
567      return
568    }
569
570    Object.keys(themeChange).forEach(key => {
571      const themeChangeFn = themeChange[key]
572      if (['disqus', 'disqusjs'].includes(key)) {
573        setTimeout(() => themeChangeFn(mode), 300)
574      } else {
575        themeChangeFn(mode)
576      }
577    })
578  }
579
580  /**
581   * Rightside
582   */
583  const rightSideFn = {
584    readmode: () => { // read mode
585      const $body = document.body
586      const newEle = document.createElement('button')
587
588      const exitReadMode = () => {
589        $body.classList.remove('read-mode')
590        newEle.remove()
591        newEle.removeEventListener('click', exitReadMode)
592      }
593
594      $body.classList.add('read-mode')
595      newEle.type = 'button'
596      newEle.className = 'fas fa-sign-out-alt exit-readmode'
597      newEle.addEventListener('click', exitReadMode)
598      $body.appendChild(newEle)
599    },
600    darkmode: () => { // switch between light and dark mode
601      const willChangeMode = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'
602      if (willChangeMode === 'dark') {
603        btf.activateDarkMode()
604        GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow(GLOBAL_CONFIG.Snackbar.day_to_night)
605      } else {
606        btf.activateLightMode()
607        GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow(GLOBAL_CONFIG.Snackbar.night_to_day)
608      }
609      btf.saveToLocal.set('theme', willChangeMode, 2)
610      handleThemeChange(willChangeMode)
611    },
612    'rightside-config': item => { // Show or hide rightside-hide-btn
613      const hideLayout = item.firstElementChild
614      if (hideLayout.classList.contains('show')) {
615        hideLayout.classList.add('status')
616        setTimeout(() => {
617          hideLayout.classList.remove('status')
618        }, 300)
619      }
620
621      hideLayout.classList.toggle('show')
622    },
623    'go-up': () => { // Back to top
624      btf.scrollToDest(0, 500)
625    },
626    'hide-aside-btn': () => { // Hide aside
627      const $htmlDom = document.documentElement.classList
628      const saveStatus = $htmlDom.contains('hide-aside') ? 'show' : 'hide'
629      btf.saveToLocal.set('aside-status', saveStatus, 2)
630      $htmlDom.toggle('hide-aside')
631    },
632    'mobile-toc-button': (p, item) => { // Show mobile toc
633      const tocEle = document.getElementById('card-toc')
634      tocEle.style.transition = 'transform 0.3s ease-in-out'
635
636      const tocEleHeight = tocEle.clientHeight
637      const btData = item.getBoundingClientRect()
638
639      const tocEleBottom = window.innerHeight - btData.bottom - 30
640      if (tocEleHeight > tocEleBottom) {
641        tocEle.style.transformOrigin = `right ${tocEleHeight - tocEleBottom - btData.height / 2}px`
642      }
643
644      tocEle.classList.toggle('open')
645      tocEle.addEventListener('transitionend', () => {
646        tocEle.style.cssText = ''
647      }, { once: true })
648    },
649    'chat-btn': () => { // Show chat
650      window.chatBtnFn()
651    },
652    translateLink: () => { // switch between traditional and simplified chinese
653      window.translateFn.translatePage()
654    }
655  }
656
657  document.getElementById('rightside').addEventListener('click', e => {
658    const $target = e.target.closest('[id]')
659    if ($target && rightSideFn[$target.id]) {
660      rightSideFn[$target.id](e.currentTarget, $target)
661    }
662  })
663
664  /**
665   * menu
666   * 側邊欄sub-menu 展開/收縮
667   */
668  const clickFnOfSubMenu = () => {
669    const handleClickOfSubMenu = e => {
670      const target = e.target.closest('.site-page.group')
671      if (!target) return
672      target.classList.toggle('hide')
673    }
674
675    const menusItems = document.querySelector('#sidebar-menus .menus_items')
676    menusItems && menusItems.addEventListener('click', handleClickOfSubMenu)
677  }
678
679  /**
680   * 手机端目录点击
681   */
682  const openMobileMenu = () => {
683    const toggleMenu = document.getElementById('toggle-menu')
684    if (!toggleMenu) return
685    btf.addEventListenerPjax(toggleMenu, 'click', () => { sidebarFn.open() })
686  }
687
688  /**
689 * 複製時加上版權信息
690 */
691  const addCopyright = () => {
692    const { limitCount, languages } = GLOBAL_CONFIG.copyright
693
694    const handleCopy = (e) => {
695      e.preventDefault()
696      const copyFont = window.getSelection(0).toString()
697      let textFont = copyFont
698      if (copyFont.length > limitCount) {
699        textFont = `${copyFont}\n\n\n${languages.author}\n${languages.link}${window.location.href}\n${languages.source}\n${languages.info}`
700      }
701      if (e.clipboardData) {
702        return e.clipboardData.setData('text', textFont)
703      } else {
704        return window.clipboardData.setData('text', textFont)
705      }
706    }
707
708    document.body.addEventListener('copy', handleCopy)
709  }
710
711  /**
712   * 網頁運行時間
713   */
714  const addRuntime = () => {
715    const $runtimeCount = document.getElementById('runtimeshow')
716    if ($runtimeCount) {
717      const publishDate = $runtimeCount.getAttribute('data-publishDate')
718      $runtimeCount.textContent = `${btf.diffDate(publishDate)} ${GLOBAL_CONFIG.runtime}`
719    }
720  }
721
722  /**
723   * 最後一次更新時間
724   */
725  const addLastPushDate = () => {
726    const $lastPushDateItem = document.getElementById('last-push-date')
727    if ($lastPushDateItem) {
728      const lastPushDate = $lastPushDateItem.getAttribute('data-lastPushDate')
729      $lastPushDateItem.textContent = btf.diffDate(lastPushDate, true)
730    }
731  }
732
733  /**
734   * table overflow
735   */
736  const addTableWrap = () => {
737    const $table = document.querySelectorAll('#article-container table')
738    if (!$table.length) return
739
740    $table.forEach(item => {
741      if (!item.closest('.highlight')) {
742        btf.wrap(item, 'div', { class: 'table-wrap' })
743      }
744    })
745  }
746
747  /**
748   * tag-hide
749   */
750  const clickFnOfTagHide = () => {
751    const hideButtons = document.querySelectorAll('#article-container .hide-button')
752    if (!hideButtons.length) return
753    hideButtons.forEach(item => item.addEventListener('click', e => {
754      const currentTarget = e.currentTarget
755      currentTarget.classList.add('open')
756      addJustifiedGallery(currentTarget.nextElementSibling.querySelectorAll('.gallery-container'))
757    }, { once: true }))
758  }
759
760  const tabsFn = () => {
761    const navTabsElements = document.querySelectorAll('#article-container .tabs')
762    if (!navTabsElements.length) return
763
764    const setActiveClass = (elements, activeIndex) => {
765      elements.forEach((el, index) => {
766        el.classList.toggle('active', index === activeIndex)
767      })
768    }
769
770    const handleNavClick = e => {
771      const target = e.target.closest('button')
772      if (!target || target.classList.contains('active')) return
773
774      const navItems = [...e.currentTarget.children]
775      const tabContents = [...e.currentTarget.nextElementSibling.children]
776      const indexOfButton = navItems.indexOf(target)
777      setActiveClass(navItems, indexOfButton)
778      e.currentTarget.classList.remove('no-default')
779      setActiveClass(tabContents, indexOfButton)
780      addJustifiedGallery(tabContents[indexOfButton].querySelectorAll('.gallery-container'), true)
781    }
782
783    const handleToTopClick = tabElement => e => {
784      if (e.target.closest('button')) {
785        btf.scrollToDest(btf.getEleTop(tabElement), 300)
786      }
787    }
788
789    navTabsElements.forEach(tabElement => {
790      btf.addEventListenerPjax(tabElement.firstElementChild, 'click', handleNavClick)
791      btf.addEventListenerPjax(tabElement.lastElementChild, 'click', handleToTopClick(tabElement))
792    })
793  }
794
795  const toggleCardCategory = () => {
796    const cardCategory = document.querySelector('#aside-cat-list.expandBtn')
797    if (!cardCategory) return
798
799    const handleToggleBtn = e => {
800      const target = e.target
801      if (target.nodeName === 'I') {
802        e.preventDefault()
803        target.parentNode.classList.toggle('expand')
804      }
805    }
806    btf.addEventListenerPjax(cardCategory, 'click', handleToggleBtn, true)
807  }
808
809  const switchComments = () => {
810    const switchBtn = document.getElementById('switch-btn')
811    if (!switchBtn) return
812
813    let switchDone = false
814    const postComment = document.getElementById('post-comment')
815    const handleSwitchBtn = () => {
816      postComment.classList.toggle('move')
817      if (!switchDone && typeof loadOtherComment === 'function') {
818        switchDone = true
819        loadOtherComment()
820      }
821    }
822    btf.addEventListenerPjax(switchBtn, 'click', handleSwitchBtn)
823  }
824
825  const addPostOutdateNotice = () => {
826    const { limitDay, messagePrev, messageNext, position } = GLOBAL_CONFIG.noticeOutdate
827    const diffDay = btf.diffDate(GLOBAL_CONFIG_SITE.postUpdate)
828    if (diffDay >= limitDay) {
829      const ele = document.createElement('div')
830      ele.className = 'post-outdate-notice'
831      ele.textContent = `${messagePrev} ${diffDay} ${messageNext}`
832      const $targetEle = document.getElementById('article-container')
833      if (position === 'top') {
834        $targetEle.insertBefore(ele, $targetEle.firstChild)
835      } else {
836        $targetEle.appendChild(ele)
837      }
838    }
839  }
840
841  const lazyloadImg = () => {
842    window.lazyLoadInstance = new LazyLoad({
843      elements_selector: 'img',
844      threshold: 0,
845      data_src: 'lazy-src'
846    })
847
848    btf.addGlobalFn('pjaxComplete', () => {
849      window.lazyLoadInstance.update()
850    }, 'lazyload')
851  }
852
853  const relativeDate = selector => {
854    selector.forEach(item => {
855      item.textContent = btf.diffDate(item.getAttribute('datetime'), true)
856      item.style.display = 'inline'
857    })
858  }
859
860  const justifiedIndexPostUI = () => {
861    const recentPostsElement = document.getElementById('recent-posts')
862    if (!(recentPostsElement && recentPostsElement.classList.contains('masonry'))) return
863
864    const init = () => {
865      const masonryItem = new InfiniteGrid.MasonryInfiniteGrid('.recent-post-items', {
866        gap: { horizontal: 10, vertical: 20 },
867        useTransform: true,
868        useResizeObserver: true
869      })
870      masonryItem.renderItems()
871      btf.addGlobalFn('pjaxCompleteOnce', () => { masonryItem.destroy() }, 'removeJustifiedIndexPostUI')
872    }
873
874    typeof InfiniteGrid === 'function' ? init() : btf.getScript(`${GLOBAL_CONFIG.infinitegrid.js}`).then(init)
875  }
876
877  const unRefreshFn = () => {
878    window.addEventListener('resize', () => {
879      adjustMenu(false)
880      mobileSidebarOpen && btf.isHidden(document.getElementById('toggle-menu')) && sidebarFn.close()
881    })
882
883    const menuMask = document.getElementById('menu-mask')
884    menuMask && menuMask.addEventListener('click', () => { sidebarFn.close() })
885
886    clickFnOfSubMenu()
887    GLOBAL_CONFIG.islazyload && lazyloadImg()
888    GLOBAL_CONFIG.copyright !== undefined && addCopyright()
889
890    if (GLOBAL_CONFIG.autoDarkmode) {
891      window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
892        if (btf.saveToLocal.get('theme') !== undefined) return
893        e.matches ? handleThemeChange('dark') : handleThemeChange('light')
894      })
895    }
896  }
897
898  const forPostFn = () => {
899    addHighlightTool()
900    addPhotoFigcaption()
901    addJustifiedGallery(document.querySelectorAll('#article-container .gallery-container'))
902    runLightbox()
903    scrollFnToDo()
904    addTableWrap()
905    clickFnOfTagHide()
906    tabsFn()
907  }
908
909  const refreshFn = () => {
910    initAdjust()
911    justifiedIndexPostUI()
912
913    if (GLOBAL_CONFIG_SITE.isPost) {
914      GLOBAL_CONFIG.noticeOutdate !== undefined && addPostOutdateNotice()
915      GLOBAL_CONFIG.relativeDate.post && relativeDate(document.querySelectorAll('#post-meta time'))
916    } else {
917      GLOBAL_CONFIG.relativeDate.homepage && relativeDate(document.querySelectorAll('#recent-posts time'))
918      GLOBAL_CONFIG.runtime && addRuntime()
919      addLastPushDate()
920      toggleCardCategory()
921    }
922
923    GLOBAL_CONFIG_SITE.isHome && scrollDownInIndex()
924    scrollFn()
925
926    forPostFn()
927    switchComments()
928    openMobileMenu()
929  }
930
931  btf.addGlobalFn('pjaxComplete', refreshFn, 'refreshFn')
932  refreshFn()
933  unRefreshFn()
934
935  // 處理 hexo-blog-encrypt 事件
936  window.addEventListener('hexo-blog-decrypt', e => {
937    forPostFn()
938    window.translateFn.translateInitialization()
939    Object.values(window.globalFn.encrypt).forEach(fn => {
940      fn()
941    })
942  })
943})

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.