1/* global NexT, CONFIG */ 2 3HTMLElement.prototype.wrap = function(wrapper) { 4 this.parentNode.insertBefore(wrapper, this); 5 this.parentNode.removeChild(this); 6 wrapper.appendChild(this); 7}; 8 9(function() { 10 const onPageLoaded = () => document.dispatchEvent( 11 new Event('page:loaded', { 12 bubbles: true 13 }) 14 ); 15 16 if (document.readyState === 'loading') { 17 document.addEventListener('readystatechange', onPageLoaded, { once: true }); 18 } else { 19 onPageLoaded(); 20 } 21 document.addEventListener('pjax:success', onPageLoaded); 22})(); 23 24NexT.utils = { 25 26 registerExtURL() { 27 document.querySelectorAll('span.exturl').forEach(element => { 28 const link = document.createElement('a'); 29 // https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings 30 link.href = decodeURIComponent(atob(element.dataset.url).split('').map(c => { 31 return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); 32 }).join('')); 33 link.rel = 'noopener external nofollow noreferrer'; 34 link.target = '_blank'; 35 link.className = element.className; 36 link.title = element.title; 37 link.innerHTML = element.innerHTML; 38 element.parentNode.replaceChild(link, element); 39 }); 40 }, 41 42 registerCopyButton(target, element, code = '') { 43 // One-click copy code support. 44 target.insertAdjacentHTML('beforeend', '<div class="copy-btn"><i class="fa fa-copy fa-fw"></i></div>'); 45 const button = target.querySelector('.copy-btn'); 46 button.addEventListener('click', () => { 47 if (!code) { 48 const lines = element.querySelector('.code') || element.querySelector('code'); 49 code = lines.innerText; 50 } 51 if (navigator.clipboard) { 52 // https://caniuse.com/mdn-api_clipboard_writetext 53 navigator.clipboard.writeText(code).then(() => { 54 button.querySelector('i').className = 'fa fa-check-circle fa-fw'; 55 }, () => { 56 button.querySelector('i').className = 'fa fa-times-circle fa-fw'; 57 }); 58 } else { 59 const ta = document.createElement('textarea'); 60 ta.style.top = window.scrollY + 'px'; // Prevent page scrolling 61 ta.style.position = 'absolute'; 62 ta.style.opacity = '0'; 63 ta.readOnly = true; 64 ta.value = code; 65 document.body.append(ta); 66 ta.select(); 67 ta.setSelectionRange(0, code.length); 68 ta.readOnly = false; 69 const result = document.execCommand('copy'); 70 button.querySelector('i').className = result ? 'fa fa-check-circle fa-fw' : 'fa fa-times-circle fa-fw'; 71 ta.blur(); // For iOS 72 button.blur(); 73 document.body.removeChild(ta); 74 } 75 }); 76 // If copycode.style is not mac, element is larger than target 77 // So we need to accept both of them as parameters 78 element.addEventListener('mouseleave', () => { 79 setTimeout(() => { 80 button.querySelector('i').className = 'fa fa-copy fa-fw'; 81 }, 300); 82 }); 83 }, 84 85 registerCodeblock(element) { 86 const inited = !!element; 87 let figure; 88 if (CONFIG.hljswrap) { 89 figure = (inited ? element : document).querySelectorAll('figure.highlight'); 90 } else { 91 figure = document.querySelectorAll('pre'); 92 } 93 figure.forEach(element => { 94 // Skip pre > .mermaid for folding and copy button 95 if (element.querySelector('.mermaid')) return; 96 if (!inited) { 97 let span = element.querySelectorAll('.code .line span'); 98 if (span.length === 0) { 99 // Hljs without line_number and wrap 100 span = element.querySelectorAll('code.highlight span'); 101 } 102 span.forEach(s => { 103 s.classList.forEach(name => { 104 s.classList.replace(name, `hljs-${name}`); 105 }); 106 }); 107 } 108 const height = parseInt(window.getComputedStyle(element).height, 10); 109 const needFold = CONFIG.fold.enable && (height > CONFIG.fold.height); 110 if (!needFold && !CONFIG.copycode.enable) return; 111 let target; 112 if (CONFIG.hljswrap && CONFIG.copycode.style === 'mac') { 113 target = element; 114 } else { 115 let box = element.querySelector('.code-container'); 116 if (!box) { 117 // https://github.com/next-theme/hexo-theme-next/issues/98 118 // https://github.com/next-theme/hexo-theme-next/pull/508 119 const container = element.querySelector('.table-container') || element; 120 box = document.createElement('div'); 121 box.className = 'code-container'; 122 container.wrap(box); 123 124 // add "notranslate" to prevent Google Translate from translating it, which also completely messes up the layout 125 box.classList.add('notranslate'); 126 } 127 target = box; 128 } 129 if (needFold && !target.classList.contains('unfold')) { 130 target.classList.add('highlight-fold'); 131 target.insertAdjacentHTML('beforeend', '<div class="fold-cover"></div><div class="expand-btn"><i class="fa fa-angle-down fa-fw"></i></div>'); 132 target.querySelector('.expand-btn').addEventListener('click', () => { 133 target.classList.remove('highlight-fold'); 134 target.classList.add('unfold'); 135 }); 136 } 137 if (!inited && CONFIG.copycode.enable) { 138 this.registerCopyButton(target, element); 139 } 140 }); 141 }, 142 143 wrapTableWithBox() { 144 document.querySelectorAll('table').forEach(element => { 145 const box = document.createElement('div'); 146 box.className = 'table-container'; 147 element.wrap(box); 148 }); 149 }, 150 151 registerVideoIframe() { 152 document.querySelectorAll('iframe').forEach(element => { 153 const supported = [ 154 'www.youtube.com', 155 'player.vimeo.com', 156 'player.youku.com', 157 'player.bilibili.com', 158 'www.tudou.com' 159 ].some(host => element.src.includes(host)); 160 if (supported && !element.parentNode.matches('.video-container')) { 161 const box = document.createElement('div'); 162 box.className = 'video-container'; 163 element.wrap(box); 164 const width = Number(element.width); 165 const height = Number(element.height); 166 if (width && height) { 167 box.style.paddingTop = (height / width * 100) + '%'; 168 } 169 } 170 }); 171 }, 172 173 updateActiveNav() { 174 if (!Array.isArray(this.sections)) return; 175 let index = this.sections.findIndex(element => { 176 return element && element.getBoundingClientRect().top > 10; 177 }); 178 if (index === -1) { 179 index = this.sections.length - 1; 180 } else if (index > 0) { 181 index--; 182 } 183 this.activateNavByIndex(index); 184 }, 185 186 registerScrollPercent() { 187 const backToTop = document.querySelector('.back-to-top'); 188 const readingProgressBar = document.querySelector('.reading-progress-bar'); 189 // For init back to top in sidebar if page was scrolled after page refresh. 190 window.addEventListener('scroll', () => { 191 if (backToTop || readingProgressBar) { 192 const contentHeight = document.body.scrollHeight - window.innerHeight; 193 const scrollPercent = contentHeight > 0 ? Math.min(100 * window.scrollY / contentHeight, 100) : 0; 194 if (backToTop) { 195 backToTop.classList.toggle('back-to-top-on', Math.round(scrollPercent) >= 5); 196 backToTop.querySelector('span').innerText = Math.round(scrollPercent) + '%'; 197 } 198 if (readingProgressBar) { 199 readingProgressBar.style.setProperty('--progress', scrollPercent.toFixed(2) + '%'); 200 } 201 } 202 this.updateActiveNav(); 203 }, { passive: true }); 204 205 backToTop && backToTop.addEventListener('click', () => { 206 window.anime({ 207 targets : document.scrollingElement, 208 duration : 500, 209 easing : 'linear', 210 scrollTop: 0 211 }); 212 }); 213 }, 214 215 /** 216 * Tabs tag listener (without twitter bootstrap). 217 */ 218 registerTabsTag() {
219 // Binding `nav-tabs` & `tab-content` by real time permalink changing. 220 document.querySelectorAll('.tabs ul.nav-tabs .tab').forEach(element => { 221 element.addEventListener('click', event => { 222 event.preventDefault(); 223 // Prevent selected tab to select again. 224 if (element.classList.contains('active')) return; 225 const nav = element.parentNode; 226 // Get the height of `tab-pane` which is activated before, and set it as the height of `tab-content` with extra margin / paddings. 227 const tabContent = nav.nextElementSibling; 228 tabContent.style.overflow = 'hidden'; 229 tabContent.style.transition = 'height 1s'; 230 // Comment system selection tab does not contain .active class. 231 const activeTab = tabContent.querySelector('.active') || tabContent.firstElementChild; 232 // Hight might be `auto`. 233 const prevHeight = parseInt(window.getComputedStyle(activeTab).height, 10) || 0; 234 const paddingTop = parseInt(window.getComputedStyle(activeTab).paddingTop, 10); 235 const marginBottom = parseInt(window.getComputedStyle(activeTab.firstElementChild).marginBottom, 10); 236 tabContent.style.height = prevHeight + paddingTop + marginBottom + 'px'; 237 // Add & Remove active class on `nav-tabs` & `tab-content`. 238 [...nav.children].forEach(target => { 239 target.classList.toggle('active', target === element); 240 }); 241 // https://stackoverflow.com/questions/20306204/using-queryselector-with-ids-that-are-numbers 242 const tActive = document.getElementById(element.querySelector('a').getAttribute('href').replace('#', '')); 243 [...tActive.parentNode.children].forEach(target => { 244 target.classList.toggle('active', target === tActive); 245 }); 246 // Trigger event 247 tActive.dispatchEvent(new Event('tabs:click', { 248 bubbles: true 249 })); 250 // Get the height of `tab-pane` which is activated now. 251 const hasScrollBar = document.body.scrollHeight > (window.innerHeight || document.documentElement.clientHeight); 252 const currHeight = parseInt(window.getComputedStyle(tabContent.querySelector('.active')).height, 10); 253 // Reset the height of `tab-content` and see the animation. 254 tabContent.style.height = currHeight + paddingTop + marginBottom + 'px'; 255 // Change the height of `tab-content` may cause scrollbar show / disappear, which may result in the change of the `tab-pane`'s height 256 setTimeout(() => { 257 if ((document.body.scrollHeight > (window.innerHeight || document.documentElement.clientHeight)) !== hasScrollBar) { 258 tabContent.style.transition = 'height 0.3s linear'; 259 // After the animation, we need reset the height of `tab-content` again. 260 const currHeightAfterScrollBarChange = parseInt(window.getComputedStyle(tabContent.querySelector('.active')).height, 10); 261 tabContent.style.height = currHeightAfterScrollBarChange + paddingTop + marginBottom + 'px'; 262 } 263 // Remove all the inline styles, and let the height be adaptive again. 264 setTimeout(() => { 265 tabContent.style.transition = ''; 266 tabContent.style.height = ''; 267 }, 250); 268 }, 1000); 269 if (!CONFIG.stickytabs) return; 270 const offset = nav.parentNode.getBoundingClientRect().top + window.scrollY + 10; 271 window.anime({ 272 targets : document.scrollingElement, 273 duration : 500, 274 easing : 'linear', 275 scrollTop: offset 276 }); 277 }); 278 }); 279 280 window.dispatchEvent(new Event('tabs:register')); 281 }, 282 283 registerCanIUseTag() { 284 // Get responsive height passed from iframe. 285 window.addEventListener('message', ({ data }) => { 286 if (typeof data === 'string' && data.includes('ciu_embed')) { 287 const featureID = data.split(':')[1]; 288 const height = data.split(':')[2]; 289 document.querySelector(`iframe[data-feature=${featureID}]`).style.height = parseInt(height, 10) + 5 + 'px'; 290 } 291 }, false); 292 }, 293 294 registerActiveMenuItem() { 295 document.querySelectorAll('.menu-item a[href]').forEach(target => { 296 const isSamePath = target.pathname === location.pathname || target.pathname === location.pathname.replace('index.html', ''); 297 const isSubPath = !CONFIG.root.startsWith(target.pathname) && location.pathname.startsWith(target.pathname); 298 target.classList.toggle('menu-item-active', target.hostname === location.hostname && (isSamePath || isSubPath)); 299 }); 300 }, 301 302 registerLangSelect() { 303 const selects = document.querySelectorAll('.lang-select');
304 selects.forEach(sel => { 305 sel.value = CONFIG.page.lang; 306 sel.addEventListener('change', () => { 307 const target = sel.options[sel.selectedIndex]; 308 document.querySelectorAll('.lang-select-label span').forEach(span => { 309 span.innerText = target.text; 310 }); 311 // Disable Pjax to force refresh translation of menu item 312 window.location.href = target.dataset.href; 313 }); 314 }); 315 }, 316 317 registerSidebarTOC() { 318 this.sections = [...document.querySelectorAll('.post-toc:not(.placeholder-toc) li a.nav-link')].map(element => { 319 const target = document.getElementById(decodeURI(element.getAttribute('href')).replace('#', '')); 320 // TOC item animation navigate. 321 element.addEventListener('click', event => { 322 event.preventDefault(); 323 const offset = target.getBoundingClientRect().top + window.scrollY; 324 window.anime({ 325 targets : document.scrollingElement, 326 duration : 500, 327 easing : 'linear', 328 scrollTop: offset, 329 complete : () => { 330 history.pushState(null, document.title, element.href); 331 } 332 }); 333 }); 334 return target; 335 }); 336 this.updateActiveNav(); 337 }, 338 339 registerPostReward() { 340 const button = document.querySelector('.reward-container button'); 341 if (!button) return; 342 button.addEventListener('click', () => { 343 document.querySelector('.post-reward').classList.toggle('active'); 344 }); 345 }, 346 347 activateNavByIndex(index) { 348 const nav = document.querySelector('.post-toc:not(.placeholder-toc) .nav'); 349 if (!nav) return; 350 351 const navItemList = nav.querySelectorAll('.nav-item'); 352 const target = navItemList[index]; 353 if (!target || target.classList.contains('active-current')) return; 354 355 const singleHeight = navItemList[navItemList.length - 1].offsetHeight; 356 357 nav.querySelectorAll('.active').forEach(navItem => { 358 navItem.classList.remove('active', 'active-current'); 359 }); 360 target.classList.add('active', 'active-current'); 361 362 let activateEle = target.querySelector('.nav-child') || target.parentElement; 363 let navChildHeight = 0; 364 365 while (nav.contains(activateEle)) { 366 if (activateEle.classList.contains('nav-item')) { 367 activateEle.classList.add('active'); 368 } else { // .nav-child or .nav 369 // scrollHeight isn't reliable for transitioning child items. 370 // The last nav-item in a list has a margin-bottom of 5px. 371 navChildHeight += (singleHeight * activateEle.childElementCount) + 5; 372 activateEle.style.setProperty('--height', `${navChildHeight}px`); 373 } 374 activateEle = activateEle.parentElement; 375 } 376 377 // Scrolling to center active TOC element if TOC content is taller then viewport. 378 const tocElement = document.querySelector(CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini' ? '.sidebar-panel-container' : '.sidebar'); 379 if (!document.querySelector('.sidebar-toc-active')) return; 380 window.anime({ 381 targets : tocElement, 382 duration : 200, 383 easing : 'linear', 384 scrollTop: tocElement.scrollTop - (tocElement.offsetHeight / 2) + target.getBoundingClientRect().top - tocElement.getBoundingClientRect().top 385 }); 386 }, 387 388 updateSidebarPosition() { 389 if (window.innerWidth < 1200 || CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini') return; 390 // Expand sidebar on post detail page by default, when post has a toc. 391 const hasTOC = document.querySelector('.post-toc:not(.placeholder-toc)'); 392 let display = CONFIG.page.sidebar; 393 if (typeof display !== 'boolean') { 394 // There's no definition sidebar in the page front-matter. 395 display = CONFIG.sidebar.display === 'always' || (CONFIG.sidebar.display === 'post' && hasTOC); 396 } 397 if (display) { 398 window.dispatchEvent(new Event('sidebar:show')); 399 } 400 }, 401 402 activateSidebarPanel(index) { 403 const sidebar = document.querySelector('.sidebar-inner'); 404 const activeClassNames = ['sidebar-toc-active', 'sidebar-overview-active']; 405 if (sidebar.classList.contains(activeClassNames[index])) return; 406 407 const panelContainer = sidebar.querySelector('.sidebar-panel-container'); 408 const tocPanel = panelContainer.firstElementChild; 409 const overviewPanel = panelContainer.lastElementChild; 410 411 let postTOCHeight = tocPanel.scrollHeight; 412 // For TOC activation, try to use the animated TOC height 413 if (index === 0) { 414 const nav = tocPanel.querySelector('.nav'); 415 if (nav) { 416 postTOCHeight = parseInt(nav.style.getPropertyValue('--height'), 10); 417 } 418 } 419 const panelHeights = [ 420 postTOCHeight, 421 overviewPanel.scrollHeight 422 ]; 423 panelContainer.style.setProperty('--inactive-panel-height', `${panelHeights[1 - index]}px`); 424 panelContainer.style.setProperty('--active-panel-height', `${panelHeights[index]}px`); 425 426 sidebar.classList.replace(activeClassNames[1 - index], activeClassNames[index]); 427 }, 428 429 updateFooterPosition() { 430 if (CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini') return; 431 function updateFooterPosition() { 432 const footer = document.querySelector('.footer'); 433 const containerHeight = document.querySelector('.main').offsetHeight + footer.offsetHeight; 434 footer.classList.toggle('footer-fixed', containerHeight <= window.innerHeight); 435 } 436 437 updateFooterPosition(); 438 window.addEventListener('resize', updateFooterPosition); 439 window.addEventListener('scroll', updateFooterPosition, { passive: true }); 440 }, 441 442 getScript(src, options = {}, legacyCondition) { 443 if (typeof options === 'function') {
444 return this.getScript(src, { 445 condition: legacyCondition 446 }).then(options); 447 } 448 const { 449 condition = false, 450 attributes: { 451 id = '', 452 defer = false, 453 crossOrigin = '', 454 dataset = {}, 455 ...otherAttributes 456 } = {}, 457 parentNode = null 458 } = options; 459 const async = options.async ?? false; 460 return new Promise((resolve, reject) => { 461 if (condition) { 462 resolve(); 463 } else { 464 const script = document.createElement('script'); 465 466 if (id) script.id = id; 467 if (crossOrigin) script.crossOrigin = crossOrigin; 468 script.async = async; 469 script.defer = defer; 470 Object.assign(script.dataset, dataset); 471 Object.entries(otherAttributes).forEach(([name, value]) => { 472 script.setAttribute(name, String(value)); 473 }); 474 475 script.onload = resolve; 476 script.onerror = reject; 477 478 if (typeof src === 'object') { 479 const { url, integrity } = src; 480 script.src = url; 481 if (integrity) { 482 script.integrity = integrity; 483 script.crossOrigin = 'anonymous'; 484 } 485 } else { 486 script.src = src; 487 } 488 (parentNode || document.head).appendChild(script); 489 } 490 }); 491 }, 492 493 loadComments(selector, legacyCallback) { 494 if (legacyCallback) { 495 return this.loadComments(selector).then(legacyCallback); 496 } 497 return new Promise(resolve => { 498 const element = document.querySelector(selector); 499 if (!CONFIG.comments.lazyload || !element) { 500 resolve(); 501 return; 502 } 503 const intersectionObserver = new IntersectionObserver((entries, observer) => { 504 const entry = entries[0]; 505 if (!entry.isIntersecting) return; 506 507 resolve(); 508 observer.disconnect(); 509 }); 510 intersectionObserver.observe(element); 511 }); 512 }, 513 514 debounce(func, wait) { 515 let timeout; 516 return function(...args) { 517 const context = this; 518 clearTimeout(timeout); 519 timeout = setTimeout(() => func.apply(context, args), wait); 520 }; 521 } 522};
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.