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