PageSourceSearch

https://fufffh.github.io/js/main.js?v=5.6.0

js fufffh.github.io collected 2026-10-03 09:13:05 UTC 33,133 bytes, 1,015 lines download raw bytes

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

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.