PageSourceSearch

https://simple9656.github.io/js/main.js?v=5.5.3

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

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.