PageSourceSearch

https://fika.ink/js/main.js?v=4.12.0

js fika.ink collected 2026-09-25 21:09:34 UTC 27,842 bytes, 878 lines download raw bytes

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

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.