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, '"')) || '' 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.