PageSourceSearch

https://roko-o.github.io/js/main.js

js roko-o.github.io collected 2026-10-03 09:51:45 UTC 30,338 bytes, 930 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 text-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    try {
228      const response = await fetch(url)
229      return await response.json()
230    } catch (error) {
231      console.error('Failed to fetch URL:', error)
232      return []
233    }
234  }
235
236  const runJustifiedGallery = (container, data, config) => {
237    const { isButton, limit, firstLimit, tabs } = config
238
239    const dataLength = data.length
240    const maxGroupKey = Math.ceil((dataLength - firstLimit) / limit + 1)
241
242    // Gallery configuration
243    const igConfig = {
244      gap: 5,
245      isConstantSize: true,
246      sizeRange: [150, 600],
247      // useResizeObserver: true,
248      // observeChildren: true,
249      useTransform: true
250      // useRecycle: false
251    }
252
253    const ig = new InfiniteGrid.JustifiedInfiniteGrid(container, igConfig)
254    let isLayoutHidden = false
255
256    // Utility functions
257    const sanitizeString = str => (str && str.replace(/"/g, '&quot;')) || ''
258
259    const createImageItem = item => {
260      const alt = item.alt ? `alt="${sanitizeString(item.alt)}"` : ''
261      const title = item.title ? `title="${sanitizeString(item.title)}"` : ''
262      return `<div class="item">
263        <img src="${item.url}" data-grid-maintained-target="true" ${alt} ${title} />
264      </div>`
265    }
266
267    const getItems = (nextGroupKey, count, isFirst = false) => {
268      const startIndex = isFirst ? (nextGroupKey - 1) * count : (nextGroupKey - 2) * count + firstLimit
269      return data.slice(startIndex, startIndex + count).map(createImageItem)
270    }
271
272    // Load more button
273    const addLoadMoreButton = container => {
274      const button = document.createElement('button')
275      button.innerHTML = `${GLOBAL_CONFIG.infinitegrid.buttonText}<i class="fa-solid fa-arrow-down"></i>`
276
277      button.addEventListener('click', () => {
278        button.remove()
279        btf.setLoading.add(container)
280        appendItems(ig.getGroups().length + 1, limit)
281      }, { once: true })
282
283      container.insertAdjacentElement('afterend', button)
284    }
285
286    const appendItems = (nextGroupKey, count, isFirst) => {
287      ig.append(getItems(nextGroupKey, count, isFirst), nextGroupKey)
288    }
289
290    // Event handlers
291    const handleRenderComplete = e => {
292      if (tabs) {
293        const parentNode = container.parentNode
294        if (isLayoutHidden) {
295          parentNode.style.visibility = 'visible'
296        }
297        if (container.offsetHeight === 0) {
298          parentNode.style.visibility = 'hidden'
299          isLayoutHidden = true
300        }
301      }
302
303      const { updated, isResize, mounted } = e
304      if (!updated.length || !mounted.length || isResize) return
305
306      btf.loadLightbox(container.querySelectorAll('img:not(.medium-zoom-image)'))
307
308      if (ig.getGroups().length === maxGroupKey) {
309        btf.setLoading.remove(container)
310        !tabs && ig.off('renderComplete', handleRenderComplete)
311        return
312      }
313
314      if (isButton) {
315        btf.setLoading.remove(container)
316        addLoadMoreButton(container)
317      }
318    }
319
320    const handleRequestAppend = btf.debounce(e => {
321      const nextGroupKey = (+e.groupKey || 0) + 1
322
323      if (nextGroupKey === 1) appendItems(nextGroupKey, firstLimit, true)
324      else appendItems(nextGroupKey, limit)
325
326      if (nextGroupKey === maxGroupKey) ig.off('requestAppend', handleRequestAppend)
327    }, 300)
328
329    btf.setLoading.add(container)
330    ig.on('renderComplete', handleRenderComplete)
331
332    if (isButton) {
333      appendItems(1, firstLimit, true)
334    } else {
335      ig.on('requestAppend', handleRequestAppend)
336      ig.renderItems()
337    }
338
339    btf.addGlobalFn('pjaxSendOnce', () => ig.destroy())
340  }
341
342  const addJustifiedGallery = async (elements, tabs = false) => {
343    if (!elements.length) return
344
345    const initGallery = async () => {
346      for (const element of elements) {
347        if (btf.isHidden(element) || element.classList.contains('loaded')) continue
348
349        const config = {
350          isButton: element.getAttribute('data-button') === 'true',
351          limit: parseInt(element.getAttribute('data-limit'), 10),
352          firstLimit: parseInt(element.getAttribute('data-first'), 10),
353          tabs
354        }
355
356        const container = element.firstElementChild
357        const content = container.textContent
358        container.textContent = ''
359        element.classList.add('loaded')
360
361        try {
362          const data = element.getAttribute('data-type') === 'url' ? await fetchUrl(content) : JSON.parse(content)
363          runJustifiedGallery(container, data, config)
364        } catch (error) {
365          console.error('Gallery data parsing failed:', error)
366        }
367      }
368    }
369
370    if (typeof InfiniteGrid === 'function') {
371      await initGallery()
372    } else {
373      await btf.getScript(GLOBAL_CONFIG.infinitegrid.js)
374      await initGallery()
375    }
376  }
377
378  /**
379   * rightside scroll percent
380   */
381  const rightsideScrollPercent = currentTop => {
382    const scrollPercent = btf.getScrollPercent(currentTop, document.body)
383    const goUpElement = document.getElementById('go-up')
384
385    if (scrollPercent < 95) {
386      goUpElement.classList.add('show-percent')
387      goUpElement.querySelector('.scroll-percent').textContent = scrollPercent
388    } else {
389      goUpElement.classList.remove('show-percent')
390    }
391  }
392
393  /**
394   * 滾動處理
395   */
396  const scrollFn = () => {
397    const $rightside = document.getElementById('rightside')
398    const innerHeight = window.innerHeight + 56
399    let initTop = 0
400    const $header = document.getElementById('page-header')
401    const isChatBtn = typeof chatBtn !== 'undefined'
402    const isShowPercent = GLOBAL_CONFIG.percent.rightside
403
404    // 檢查文檔高度是否小於視窗高度
405    const checkDocumentHeight = () => {
406      if (document.body.scrollHeight <= innerHeight) {
407        $rightside.classList.add('rightside-show')
408        return true
409      }
410      return false
411    }
412
413    // 如果文檔高度小於視窗高度,直接返回
414    if (checkDocumentHeight()) return
415
416    // find the scroll direction
417    const scrollDirection = currentTop => {
418      const result = currentTop > initTop // true is down & false is up
419      initTop = currentTop
420      return result
421    }
422
423    let flag = ''
424    const scrollTask = btf.throttle(() => {
425      const currentTop = window.scrollY || document.documentElement.scrollTop
426      const isDown = scrollDirection(currentTop)
427      if (currentTop > 56) {
428        if (flag === '') {
429          $header.classList.add('nav-fixed')
430          $rightside.classList.add('rightside-show')
431        }
432
433        if (isDown) {
434          if (flag !== 'down') {
435            $header.classList.remove('nav-visible')
436            isChatBtn && window.chatBtn.hide()
437            flag = 'down'
438          }
439        } else {
440          if (flag !== 'up') {
441            $header.classList.add('nav-visible')
442            isChatBtn && window.chatBtn.show()
443            flag = 'up'
444          }
445        }
446      } else {
447        flag = ''
448        if (currentTop === 0) {
449          $header.classList.remove('nav-fixed', 'nav-visible')
450        }
451        $rightside.classList.remove('rightside-show')
452      }
453
454      isShowPercent && rightsideScrollPercent(currentTop)
455      checkDocumentHeight()
456    }, 300)
457
458    btf.addEventListenerPjax(window, 'scroll', scrollTask, { passive: true })
459  }
460
461  /**
462  * toc,anchor
463  */
464  const scrollFnToDo = () => {
465    const isToc = GLOBAL_CONFIG_SITE.isToc
466    const isAnchor = GLOBAL_CONFIG.isAnchor
467    const $article = document.getElementById('article-container')
468
469    if (!($article && (isToc || isAnchor))) return
470
471    let $tocLink, $cardToc, autoScrollToc, $tocPercentage, isExpand
472
473    if (isToc) {
474      const $cardTocLayout = document.getElementById('card-toc')
475      $cardToc = $cardTocLayout.querySelector('.toc-content')
476      $tocLink = $cardToc.querySelectorAll('.toc-link')
477      $tocPercentage = $cardTocLayout.querySelector('.toc-percentage')
478      isExpand = $cardToc.classList.contains('is-expand')
479
480      // toc元素點擊
481      const tocItemClickFn = e => {
482        const target = e.target.closest('.toc-link')
483        if (!target) return
484
485        e.preventDefault()
486        btf.scrollToDest(btf.getEleTop(document.getElementById(decodeURI(target.getAttribute('href')).replace('#', ''))), 300)
487        if (window.innerWidth < 900) {
488          $cardTocLayout.classList.remove('open')
489        }
490      }
491
492      btf.addEventListenerPjax($cardToc, 'click', tocItemClickFn)
493
494      autoScrollToc = item => {
495        const sidebarHeight = $cardToc.clientHeight
496        const itemOffsetTop = item.offsetTop
497        const itemHeight = item.clientHeight
498        const scrollTop = $cardToc.scrollTop
499        const offset = itemOffsetTop - scrollTop
500        const middlePosition = (sidebarHeight - itemHeight) / 2
501
502        if (offset !== middlePosition) {
503          $cardToc.scrollTop = scrollTop + (offset - middlePosition)
504        }
505      }
506
507      // 處理 hexo-blog-encrypt 事件
508      $cardToc.style.display = 'block'
509    }
510
511    // find head position & add active class
512    const $articleList = $article.querySelectorAll('h1,h2,h3,h4,h5,h6')
513    let detectItem = ''
514
515    const findHeadPosition = top => {
516      if (top === 0) return false
517
518      let currentId = ''
519      let currentIndex = ''
520
521      for (let i = 0; i < $articleList.length; i++) {
522        const ele = $articleList[i]
523        if (top > btf.getEleTop(ele) - 80) {
524          const id = ele.id
525          currentId = id ? '#' + encodeURI(id) : ''
526          currentIndex = i
527        } else {
528          break
529        }
530      }
531
532      if (detectItem === currentIndex) return
533
534      if (isAnchor) btf.updateAnchor(currentId)
535
536      detectItem = currentIndex
537
538      if (isToc) {
539        $cardToc.querySelectorAll('.active').forEach(i => i.classList.remove('active'))
540
541        if (currentId) {
542          const currentActive = $tocLink[currentIndex]
543          currentActive.classList.add('active')
544
545          setTimeout(() => autoScrollToc(currentActive), 0)
546
547          if (!isExpand) {
548            let parent = currentActive.parentNode
549            while (!parent.matches('.toc')) {
550              if (parent.matches('li')) parent.classList.add('active')
551              parent = parent.parentNode
552            }
553          }
554        }
555      }
556    }
557
558    // main of scroll
559    const tocScrollFn = btf.throttle(() => {
560      const currentTop = window.scrollY || document.documentElement.scrollTop
561      if (isToc && GLOBAL_CONFIG.percent.toc) {
562        $tocPercentage.textContent = btf.getScrollPercent(currentTop, $article)
563      }
564      findHeadPosition(currentTop)
565    }, 100)
566
567    btf.addEventListenerPjax(window, 'scroll', tocScrollFn, { passive: true })
568  }
569
570  const handleThemeChange = mode => {
571    const globalFn = window.globalFn || {}
572    const themeChange = globalFn.themeChange || {}
573    if (!themeChange) {
574      return
575    }
576
577    Object.keys(themeChange).forEach(key => {
578      const themeChangeFn = themeChange[key]
579      if (['disqus', 'disqusjs'].includes(key)) {
580        setTimeout(() => themeChangeFn(mode), 300)
581      } else {
582        themeChangeFn(mode)
583      }
584    })
585  }
586
587  /**
588   * Rightside
589   */
590  const rightSideFn = {
591    readmode: () => { // read mode
592      const $body = document.body
593      const newEle = document.createElement('button')
594
595      const exitReadMode = () => {
596        $body.classList.remove('read-mode')
597        newEle.remove()
598        newEle.removeEventListener('click', exitReadMode)
599      }
600
601      $body.classList.add('read-mode')
602      newEle.type = 'button'
603      newEle.className = 'fas fa-sign-out-alt exit-readmode'
604      newEle.addEventListener('click', exitReadMode)
605      $body.appendChild(newEle)
606    },
607    darkmode: () => { // switch between light and dark mode
608      const willChangeMode = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'
609      if (willChangeMode === 'dark') {
610        btf.activateDarkMode()
611        GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow(GLOBAL_CONFIG.Snackbar.day_to_night)
612      } else {
613        btf.activateLightMode()
614        GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow(GLOBAL_CONFIG.Snackbar.night_to_day)
615      }
616      btf.saveToLocal.set('theme', willChangeMode, 2)
617      handleThemeChange(willChangeMode)
618    },
619    'rightside-config': item => { // Show or hide rightside-hide-btn
620      const hideLayout = item.firstElementChild
621      if (hideLayout.classList.contains('show')) {
622        hideLayout.classList.add('status')
623        setTimeout(() => {
624          hideLayout.classList.remove('status')
625        }, 300)
626      }
627
628      hideLayout.classList.toggle('show')
629    },
630    'go-up': () => { // Back to top
631      btf.scrollToDest(0, 500)
632    },
633    'hide-aside-btn': () => { // Hide aside
634      const $htmlDom = document.documentElement.classList
635      const saveStatus = $htmlDom.contains('hide-aside') ? 'show' : 'hide'
636      btf.saveToLocal.set('aside-status', saveStatus, 2)
637      $htmlDom.toggle('hide-aside')
638    },
639    'mobile-toc-button': (p, item) => { // Show mobile toc
640      const tocEle = document.getElementById('card-toc')
641      tocEle.style.transition = 'transform 0.3s ease-in-out'
642
643      const tocEleHeight = tocEle.clientHeight
644      const btData = item.getBoundingClientRect()
645
646      const tocEleBottom = window.innerHeight - btData.bottom - 30
647      if (tocEleHeight > tocEleBottom) {
648        tocEle.style.transformOrigin = `right ${tocEleHeight - tocEleBottom - btData.height / 2}px`
649      }
650
651      tocEle.classList.toggle('open')
652      tocEle.addEventListener('transitionend', () => {
653        tocEle.style.cssText = ''
654      }, { once: true })
655    },
656    'chat-btn': () => { // Show chat
657      window.chatBtnFn()
658    },
659    translateLink: () => { // switch between traditional and simplified chinese
660      window.translateFn.translatePage()
661    }
662  }
663
664  document.getElementById('rightside').addEventListener('click', e => {
665    const $target = e.target.closest('[id]')
666    if ($target && rightSideFn[$target.id]) {
667      rightSideFn[$target.id](e.currentTarget, $target)
668    }
669  })
670
671  /**
672   * menu
673   * 側邊欄sub-menu 展開/收縮
674   */
675  const clickFnOfSubMenu = () => {
676    const handleClickOfSubMenu = e => {
677      const target = e.target.closest('.site-page.group')
678      if (!target) return
679      target.classList.toggle('hide')
680    }
681
682    const menusItems = document.querySelector('#sidebar-menus .menus_items')
683    menusItems && menusItems.addEventListener('click', handleClickOfSubMenu)
684  }
685
686  /**
687   * 手机端目录点击
688   */
689  const openMobileMenu = () => {
690    const toggleMenu = document.getElementById('toggle-menu')
691    if (!toggleMenu) return
692    btf.addEventListenerPjax(toggleMenu, 'click', () => { sidebarFn.open() })
693  }
694
695  /**
696 * 複製時加上版權信息
697 */
698  const addCopyright = () => {
699    const { limitCount, languages } = GLOBAL_CONFIG.copyright
700
701    const handleCopy = (e) => {
702      e.preventDefault()
703      const copyFont = window.getSelection(0).toString()
704      let textFont = copyFont
705      if (copyFont.length > limitCount) {
706        textFont = `${copyFont}\n\n\n${languages.author}\n${languages.link}${window.location.href}\n${languages.source}\n${languages.info}`
707      }
708      if (e.clipboardData) {
709        return e.clipboardData.setData('text', textFont)
710      } else {
711        return window.clipboardData.setData('text', textFont)
712      }
713    }
714
715    document.body.addEventListener('copy', handleCopy)
716  }
717
718  /**
719   * 網頁運行時間
720   */
721  const addRuntime = () => {
722    const $runtimeCount = document.getElementById('runtimeshow')
723    if ($runtimeCount) {
724      const publishDate = $runtimeCount.getAttribute('data-publishDate')
725      $runtimeCount.textContent = `${btf.diffDate(publishDate)} ${GLOBAL_CONFIG.runtime}`
726    }
727  }
728
729  /**
730   * 最後一次更新時間
731   */
732  const addLastPushDate = () => {
733    const $lastPushDateItem = document.getElementById('last-push-date')
734    if ($lastPushDateItem) {
735      const lastPushDate = $lastPushDateItem.getAttribute('data-lastPushDate')
736      $lastPushDateItem.textContent = btf.diffDate(lastPushDate, true)
737    }
738  }
739
740  /**
741   * table overflow
742   */
743  const addTableWrap = () => {
744    const $table = document.querySelectorAll('#article-container table')
745    if (!$table.length) return
746
747    $table.forEach(item => {
748      if (!item.closest('.highlight')) {
749        btf.wrap(item, 'div', { class: 'table-wrap' })
750      }
751    })
752  }
753
754  /**
755   * tag-hide
756   */
757  const clickFnOfTagHide = () => {
758    const hideButtons = document.querySelectorAll('#article-container .hide-button')
759    if (!hideButtons.length) return
760    hideButtons.forEach(item => item.addEventListener('click', e => {
761      const currentTarget = e.currentTarget
762      currentTarget.classList.add('open')
763      addJustifiedGallery(currentTarget.nextElementSibling.querySelectorAll('.gallery-container'))
764    }, { once: true }))
765  }
766
767  const tabsFn = () => {
768    const navTabsElements = document.querySelectorAll('#article-container .tabs')
769    if (!navTabsElements.length) return
770
771    const setActiveClass = (elements, activeIndex) => {
772      elements.forEach((el, index) => {
773        el.classList.toggle('active', index === activeIndex)
774      })
775    }
776
777    const handleNavClick = e => {
778      const target = e.target.closest('button')
779      if (!target || target.classList.contains('active')) return
780
781      const navItems = [...e.currentTarget.children]
782      const tabContents = [...e.currentTarget.nextElementSibling.children]
783      const indexOfButton = navItems.indexOf(target)
784      setActiveClass(navItems, indexOfButton)
785      e.currentTarget.classList.remove('no-default')
786      setActiveClass(tabContents, indexOfButton)
787      addJustifiedGallery(tabContents[indexOfButton].querySelectorAll('.gallery-container'), true)
788    }
789
790    const handleToTopClick = tabElement => e => {
791      if (e.target.closest('button')) {
792        btf.scrollToDest(btf.getEleTop(tabElement), 300)
793      }
794    }
795
796    navTabsElements.forEach(tabElement => {
797      btf.addEventListenerPjax(tabElement.firstElementChild, 'click', handleNavClick)
798      btf.addEventListenerPjax(tabElement.lastElementChild, 'click', handleToTopClick(tabElement))
799    })
800  }
801
802  const toggleCardCategory = () => {
803    const cardCategory = document.querySelector('#aside-cat-list.expandBtn')
804    if (!cardCategory) return
805
806    const handleToggleBtn = e => {
807      const target = e.target
808      if (target.nodeName === 'I') {
809        e.preventDefault()
810        target.parentNode.classList.toggle('expand')
811      }
812    }
813    btf.addEventListenerPjax(cardCategory, 'click', handleToggleBtn, true)
814  }
815
816  const addPostOutdateNotice = () => {
817    const ele = document.getElementById('post-outdate-notice')
818    if (!ele) return
819
820    const { limitDay, messagePrev, messageNext, postUpdate } = JSON.parse(ele.getAttribute('data'))
821    const diffDay = btf.diffDate(postUpdate)
822    if (diffDay >= limitDay) {
823      ele.textContent = `${messagePrev} ${diffDay} ${messageNext}`
824      ele.hidden = false
825    }
826  }
827
828  const lazyloadImg = () => {
829    window.lazyLoadInstance = new LazyLoad({
830      elements_selector: 'img',
831      threshold: 0,
832      data_src: 'lazy-src'
833    })
834
835    btf.addGlobalFn('pjaxComplete', () => {
836      window.lazyLoadInstance.update()
837    }, 'lazyload')
838  }
839
840  const relativeDate = selector => {
841    selector.forEach(item => {
842      item.textContent = btf.diffDate(item.getAttribute('datetime'), true)
843      item.style.display = 'inline'
844    })
845  }
846
847  const justifiedIndexPostUI = () => {
848    const recentPostsElement = document.getElementById('recent-posts')
849    if (!(recentPostsElement && recentPostsElement.classList.contains('masonry'))) return
850
851    const init = () => {
852      const masonryItem = new InfiniteGrid.MasonryInfiniteGrid('.recent-post-items', {
853        gap: { horizontal: 10, vertical: 20 },
854        useTransform: true,
855        useResizeObserver: true
856      })
857      masonryItem.renderItems()
858      btf.addGlobalFn('pjaxCompleteOnce', () => { masonryItem.destroy() }, 'removeJustifiedIndexPostUI')
859    }
860
861    typeof InfiniteGrid === 'function' ? init() : btf.getScript(`${GLOBAL_CONFIG.infinitegrid.js}`).then(init)
862  }
863
864  const unRefreshFn = () => {
865    window.addEventListener('resize', () => {
866      adjustMenu(false)
867      mobileSidebarOpen && btf.isHidden(document.getElementById('toggle-menu')) && sidebarFn.close()
868    })
869
870    const menuMask = document.getElementById('menu-mask')
871    menuMask && menuMask.addEventListener('click', () => { sidebarFn.close() })
872
873    clickFnOfSubMenu()
874    GLOBAL_CONFIG.islazyloadPlugin && lazyloadImg()
875    GLOBAL_CONFIG.copyright !== undefined && addCopyright()
876
877    if (GLOBAL_CONFIG.autoDarkmode) {
878      window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
879        if (btf.saveToLocal.get('theme') !== undefined) return
880        e.matches ? handleThemeChange('dark') : handleThemeChange('light')
881      })
882    }
883  }
884
885  const forPostFn = () => {
886    addHighlightTool()
887    addPhotoFigcaption()
888    addJustifiedGallery(document.querySelectorAll('#article-container .gallery-container'))
889    runLightbox()
890    scrollFnToDo()
891    addTableWrap()
892    clickFnOfTagHide()
893    tabsFn()
894  }
895
896  const refreshFn = () => {
897    initAdjust()
898    justifiedIndexPostUI()
899
900    if (GLOBAL_CONFIG_SITE.pageType === 'post') {
901      addPostOutdateNotice()
902      GLOBAL_CONFIG.relativeDate.post && relativeDate(document.querySelectorAll('#post-meta time'))
903    } else {
904      GLOBAL_CONFIG.relativeDate.homepage && relativeDate(document.querySelectorAll('#recent-posts time'))
905      GLOBAL_CONFIG.runtime && addRuntime()
906      addLastPushDate()
907      toggleCardCategory()
908    }
909
910    GLOBAL_CONFIG_SITE.pageType === 'home' && scrollDownInIndex()
911    scrollFn()
912
913    forPostFn()
914    GLOBAL_CONFIG_SITE.pageType !== 'shuoshuo' && btf.switchComments(document)
915    openMobileMenu()
916  }
917
918  btf.addGlobalFn('pjaxComplete', refreshFn, 'refreshFn')
919  refreshFn()
920  unRefreshFn()
921
922  // 處理 hexo-blog-encrypt 事件
923  window.addEventListener('hexo-blog-decrypt', e => {
924    forPostFn()
925    window.translateFn.translateInitialization()
926    Object.values(window.globalFn.encrypt).forEach(fn => {
927      fn()
928    })
929  })
930})

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.