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: function() { 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 /** 43 * One-click copy code support. 44 */ 45 registerCopyCode: function() { 46 let figure = document.querySelectorAll('figure.highlight'); 47 if (figure.length === 0) figure = document.querySelectorAll('pre:not(.mermaid)'); 48 figure.forEach(element => { 49 element.querySelectorAll('.code .line span').forEach(span => { 50 span.classList.forEach(name => { 51 span.classList.replace(name, `hljs-${name}`); 52 }); 53 }); 54 if (!CONFIG.copycode.enable) return; 55 let target = element; 56 if (CONFIG.copycode.style !== 'mac') target = element.querySelector('.table-container') || element; 57 target.insertAdjacentHTML('beforeend', '<div class="copy-btn"><i class="fa fa-copy fa-fw"></i></div>'); 58 const button = element.querySelector('.copy-btn'); 59 button.addEventListener('click', () => { 60 const lines = element.querySelector('.code') || element.querySelector('code'); 61 const code = lines.innerText; 62 if (navigator.clipboard) { 63 // https://caniuse.com/mdn-api_clipboard_writetext 64 navigator.clipboard.writeText(code).then(() => { 65 button.querySelector('i').className = 'fa fa-check-circle fa-fw'; 66 }, () => { 67 button.querySelector('i').className = 'fa fa-times-circle fa-fw'; 68 }); 69 } else { 70 const ta = document.createElement('textarea'); 71 ta.style.top = window.scrollY + 'px'; // Prevent page scrolling 72 ta.style.position = 'absolute'; 73 ta.style.opacity = '0'; 74 ta.readOnly = true; 75 ta.value = code; 76 document.body.append(ta); 77 ta.select(); 78 ta.setSelectionRange(0, code.length); 79 ta.readOnly = false; 80 const result = document.execCommand('copy'); 81 button.querySelector('i').className = result ? 'fa fa-check-circle fa-fw' : 'fa fa-times-circle fa-fw'; 82 ta.blur(); // For iOS 83 button.blur(); 84 document.body.removeChild(ta); 85 } 86 }); 87 element.addEventListener('mouseleave', () => { 88 setTimeout(() => { 89 button.querySelector('i').className = 'fa fa-copy fa-fw'; 90 }, 300); 91 }); 92 }); 93 }, 94 95 wrapTableWithBox: function() { 96 document.querySelectorAll('table').forEach(element => { 97 const box = document.createElement('div'); 98 box.className = 'table-container'; 99 element.wrap(box); 100 }); 101 }, 102 103 registerVideoIframe: function() { 104 document.querySelectorAll('iframe').forEach(element => { 105 const supported = [ 106 'www.youtube.com', 107 'player.vimeo.com', 108 'player.youku.com', 109 'player.bilibili.com', 110 'www.tudou.com' 111 ].some(host => element.src.includes(host)); 112 if (supported && !element.parentNode.matches('.video-container')) { 113 const box = document.createElement('div'); 114 box.className = 'video-container'; 115 element.wrap(box); 116 const width = Number(element.width); 117 const height = Number(element.height); 118 if (width && height) { 119 box.style.paddingTop = (height / width * 100) + '%'; 120 } 121 } 122 }); 123 }, 124 125 registerScrollPercent: function() { 126 const backToTop = document.querySelector('.back-to-top'); 127 const readingProgressBar = document.querySelector('.reading-progress-bar'); 128 // For init back to top in sidebar if page was scrolled after page refresh. 129 window.addEventListener('scroll', () => { 130 if (backToTop || readingProgressBar) { 131 const contentHeight = document.body.scrollHeight - window.innerHeight; 132 const scrollPercent = contentHeight > 0 ? Math.min(100 * window.scrollY / contentHeight, 100) : 0; 133 if (backToTop) { 134 backToTop.classList.toggle('back-to-top-on', Math.round(scrollPercent) >= 5); 135 backToTop.querySelector('span').innerText = Math.round(scrollPercent) + '%'; 136 } 137 if (readingProgressBar) { 138 readingProgressBar.style.setProperty('--progress', scrollPercent.toFixed(2) + '%'); 139 } 140 } 141 if (!Array.isArray(NexT.utils.sections)) return; 142 let index = NexT.utils.sections.findIndex(element => { 143 return element && element.getBoundingClientRect().top > 10; 144 }); 145 if (index === -1) { 146 index = NexT.utils.sections.length - 1; 147 } else if (index > 0) { 148 index--; 149 } 150 this.activateNavByIndex(index); 151 }, { passive: true }); 152 153 backToTop && backToTop.addEventListener('click', () => { 154 window.anime({ 155 targets : document.scrollingElement, 156 duration : 500, 157 easing : 'linear', 158 scrollTop: 0 159 }); 160 }); 161 }, 162 163 /** 164 * Tabs tag listener (without twitter bootstrap). 165 */ 166 registerTabsTag: function() {
167 // Binding `nav-tabs` & `tab-content` by real time permalink changing. 168 document.querySelectorAll('.tabs ul.nav-tabs .tab').forEach(element => { 169 element.addEventListener('click', event => { 170 event.preventDefault(); 171 // Prevent selected tab to select again. 172 if (element.classList.contains('active')) return; 173 const nav = element.parentNode; 174 // Add & Remove active class on `nav-tabs` & `tab-content`. 175 [...nav.children].forEach(target => { 176 target.classList.toggle('active', target === element); 177 }); 178 // https://stackoverflow.com/questions/20306204/using-queryselector-with-ids-that-are-numbers 179 const tActive = document.getElementById(element.querySelector('a').getAttribute('href').replace('#', '')); 180 [...tActive.parentNode.children].forEach(target => { 181 target.classList.toggle('active', target === tActive); 182 }); 183 // Trigger event 184 tActive.dispatchEvent(new Event('tabs:click', { 185 bubbles: true 186 })); 187 if (!CONFIG.stickytabs) return; 188 const offset = nav.parentNode.getBoundingClientRect().top + window.scrollY + 10; 189 window.anime({ 190 targets : document.scrollingElement, 191 duration : 500, 192 easing : 'linear', 193 scrollTop: offset 194 }); 195 }); 196 }); 197 198 window.dispatchEvent(new Event('tabs:register')); 199 }, 200 201 registerCanIUseTag: function() { 202 // Get responsive height passed from iframe. 203 window.addEventListener('message', ({ data }) => { 204 if (typeof data === 'string' && data.includes('ciu_embed')) { 205 const featureID = data.split(':')[1]; 206 const height = data.split(':')[2]; 207 document.querySelector(`iframe[data-feature=${featureID}]`).style.height = parseInt(height, 10) + 5 + 'px'; 208 } 209 }, false); 210 }, 211 212 registerActiveMenuItem: function() { 213 document.querySelectorAll('.menu-item a[href]').forEach(target => { 214 const isSamePath = target.pathname === location.pathname || target.pathname === location.pathname.replace('index.html', ''); 215 const isSubPath = !CONFIG.root.startsWith(target.pathname) && location.pathname.startsWith(target.pathname); 216 target.classList.toggle('menu-item-active', target.hostname === location.hostname && (isSamePath || isSubPath)); 217 }); 218 }, 219 220 registerLangSelect: function() { 221 const selects = document.querySelectorAll('.lang-select'); 222 selects.forEach(sel => { 223 sel.value = CONFIG.page.lang; 224 sel.addEventListener('change', () => { 225 const target = sel.options[sel.selectedIndex]; 226 document.querySelectorAll('.lang-select-label span').forEach(span => { 227 span.innerText = target.text; 228 }); 229 // Disable Pjax to force refresh translation of menu item 230 window.location.href = target.dataset.href; 231 }); 232 }); 233 }, 234 235 registerSidebarTOC: function() { 236 this.sections = [...document.querySelectorAll('.post-toc li a.nav-link')].map(element => { 237 const target = document.getElementById(decodeURI(element.getAttribute('href')).replace('#', '')); 238 // TOC item animation navigate. 239 element.addEventListener('click', event => { 240 event.preventDefault(); 241 const offset = target.getBoundingClientRect().top + window.scrollY; 242 window.anime({ 243 targets : document.scrollingElement, 244 duration : 500, 245 easing : 'linear', 246 scrollTop: offset, 247 complete : () => { 248 history.pushState(null, document.title, element.href); 249 } 250 }); 251 }); 252 return target; 253 }); 254 }, 255 256 registerPostReward: function() { 257 const button = document.querySelector('.reward-container button'); 258 if (!button) return; 259 button.addEventListener('click', () => { 260 document.querySelector('.post-reward').classList.toggle('active'); 261 }); 262 }, 263 264 activateNavByIndex: function(index) { 265 const target = document.querySelectorAll('.post-toc li a.nav-link')[index]; 266 if (!target || target.classList.contains('active-current')) return; 267 268 document.querySelectorAll('.post-toc .active').forEach(element => { 269 element.classList.remove('active', 'active-current'); 270 }); 271 target.classList.add('active', 'active-current'); 272 let parent = target.parentNode; 273 while (!parent.matches('.post-toc')) { 274 if (parent.matches('li')) parent.classList.add('active'); 275 parent = parent.parentNode; 276 } 277 // Scrolling to center active TOC element if TOC content is taller then viewport. 278 const tocElement = document.querySelector(CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini' ? '.sidebar-panel-container' : '.sidebar'); 279 if (!document.querySelector('.sidebar-toc-active')) return; 280 window.anime({ 281 targets : tocElement, 282 duration : 200, 283 easing : 'linear', 284 scrollTop: tocElement.scrollTop - (tocElement.offsetHeight / 2) + target.getBoundingClientRect().top - tocElement.getBoundingClientRect().top 285 }); 286 }, 287 288 updateSidebarPosition: function() { 289 if (window.innerWidth < 1200 || CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini') return;
290 // Expand sidebar on post detail page by default, when post has a toc. 291 const hasTOC = document.querySelector('.post-toc'); 292 let display = CONFIG.page.sidebar; 293 if (typeof display !== 'boolean') { 294 // There's no definition sidebar in the page front-matter. 295 display = CONFIG.sidebar.display === 'always' || (CONFIG.sidebar.display === 'post' && hasTOC); 296 } 297 if (display) { 298 window.dispatchEvent(new Event('sidebar:show')); 299 } 300 }, 301 302 activateSidebarPanel: function(index) { 303 const duration = 200; 304 const sidebar = document.querySelector('.sidebar-inner'); 305 const panel = document.querySelector('.sidebar-panel-container'); 306 const activeClassName = ['sidebar-toc-active', 'sidebar-overview-active']; 307 308 if (sidebar.classList.contains(activeClassName[index])) return; 309 310 window.anime({ 311 duration, 312 targets : panel, 313 easing : 'linear', 314 opacity : 0, 315 translateY: [0, -20], 316 complete : () => { 317 // Prevent adding TOC to Overview if Overview was selected when close & open sidebar. 318 sidebar.classList.replace(activeClassName[1 - index], activeClassName[index]); 319 window.anime({ 320 duration, 321 targets : panel, 322 easing : 'linear', 323 opacity : [0, 1], 324 translateY: [-20, 0] 325 }); 326 } 327 }); 328 }, 329 330 getScript: function(src, options = {}, legacyCondition) { 331 if (typeof options === 'function') { 332 return this.getScript(src, { 333 condition: legacyCondition 334 }).then(options); 335 } 336 const { 337 condition = false, 338 attributes: { 339 id = '', 340 async = false, 341 defer = false, 342 crossOrigin = '', 343 dataset = {}, 344 ...otherAttributes 345 } = {}, 346 parentNode = null 347 } = options; 348 return new Promise((resolve, reject) => { 349 if (condition) { 350 resolve(); 351 } else { 352 const script = document.createElement('script'); 353 354 if (id) script.id = id; 355 if (crossOrigin) script.crossOrigin = crossOrigin; 356 script.async = async; 357 script.defer = defer; 358 Object.assign(script.dataset, dataset); 359 Object.entries(otherAttributes).forEach(([name, value]) => { 360 script.setAttribute(name, String(value)); 361 }); 362 363 script.onload = resolve; 364 script.onerror = reject; 365 366 if (typeof src === 'object') { 367 const { url, integrity } = src; 368 script.src = url; 369 if (integrity) { 370 script.integrity = integrity; 371 script.crossOrigin = 'anonymous'; 372 } 373 } else { 374 script.src = src; 375 } 376 (parentNode || document.head).appendChild(script); 377 } 378 }); 379 }, 380 381 loadComments: function(selector, legacyCallback) { 382 if (legacyCallback) { 383 return this.loadComments(selector).then(legacyCallback); 384 } 385 return new Promise(resolve => { 386 const element = document.querySelector(selector); 387 if (!CONFIG.comments.lazyload || !element) { 388 resolve(); 389 return; 390 } 391 const intersectionObserver = new IntersectionObserver((entries, observer) => { 392 const entry = entries[0]; 393 if (!entry.isIntersecting) return; 394 395 resolve(); 396 observer.disconnect(); 397 }); 398 intersectionObserver.observe(element); 399 }); 400 } 401};
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.