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