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// https://caniuse.com/#feat=mdn-api_element_classlist_replace 10if (typeof DOMTokenList.prototype.replace !== 'function') { 11 DOMTokenList.prototype.replace = function(remove, add) { 12 this.remove(remove); 13 this.add(add); 14 }; 15} 16 17NexT.utils = { 18 19 /** 20 * Wrap images with fancybox. 21 */ 22 wrapImageWithFancyBox: function() { 23 document.querySelectorAll('.post-body :not(a) > img, .post-body > img').forEach(element => { 24 const $image = $(element); 25 const imageLink = $image.attr('data-src') || $image.attr('src'); 26 const $imageWrapLink = $image.wrap(`<a class="fancybox fancybox.image" href="${imageLink}" itemscope itemtype="http://schema.org/ImageObject" itemprop="url"></a>`).parent('a'); 27 if ($image.is('.post-gallery img')) { 28 $imageWrapLink.attr('data-fancybox', 'gallery').attr('rel', 'gallery'); 29 } else if ($image.is('.group-picture img')) { 30 $imageWrapLink.attr('data-fancybox', 'group').attr('rel', 'group'); 31 } else { 32 $imageWrapLink.attr('data-fancybox', 'default').attr('rel', 'default'); 33 } 34 35 const imageTitle = $image.attr('title') || $image.attr('alt'); 36 if (imageTitle) { 37 $imageWrapLink.append(`<p class="image-caption">${imageTitle}</p>`); 38 // Make sure img title tag will show correctly in fancybox 39 $imageWrapLink.attr('title', imageTitle).attr('data-caption', imageTitle); 40 } 41 }); 42 43 $.fancybox.defaults.hash = false; 44 $('.fancybox').fancybox({ 45 loop : true, 46 helpers: { 47 overlay: { 48 locked: false 49 } 50 } 51 }); 52 }, 53 54 registerExtURL: function() { 55 document.querySelectorAll('span.exturl').forEach(element => { 56 const link = document.createElement('a'); 57 // https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings 58 link.href = decodeURIComponent(atob(element.dataset.url).split('').map(c => { 59 return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); 60 }).join('')); 61 link.rel = 'noopener external nofollow noreferrer'; 62 link.target = '_blank'; 63 link.className = element.className; 64 link.title = element.title; 65 link.innerHTML = element.innerHTML; 66 element.parentNode.replaceChild(link, element); 67 }); 68 }, 69 70 /** 71 * One-click copy code support. 72 */ 73 registerCopyCode: function() { 74 document.querySelectorAll('figure.highlight').forEach(element => { 75 element.querySelectorAll('.code .line span').forEach(span => { 76 span.classList.forEach(name => { 77 span.classList.replace(name, `hljs-${name}`); 78 }); 79 }); 80 if (!CONFIG.copycode) return; 81 element.insertAdjacentHTML('beforeend', '<div class="copy-btn"><i class="fa fa-clipboard fa-fw"></i></div>'); 82 const button = element.querySelector('.copy-btn'); 83 button.addEventListener('click', () => { 84 const code = [...button.parentNode.querySelectorAll('.code .line')].map(line => line.innerText).join('\n'); 85 const ta = document.createElement('textarea'); 86 ta.style.top = window.scrollY + 'px'; // Prevent page scrolling 87 ta.style.position = 'absolute'; 88 ta.style.opacity = '0'; 89 ta.readOnly = true; 90 ta.value = code; 91 document.body.append(ta); 92 ta.select(); 93 ta.setSelectionRange(0, code.length); 94 ta.readOnly = false; 95 const result = document.execCommand('copy'); 96 button.querySelector('i').className = result ? 'fa fa-check-circle fa-fw' : 'fa fa-times-circle fa-fw'; 97 ta.blur(); // For iOS 98 button.blur(); 99 document.body.removeChild(ta); 100 }); 101 element.addEventListener('mouseleave', () => { 102 setTimeout(() => { 103 button.querySelector('i').className = 'fa fa-clipboard fa-fw'; 104 }, 300); 105 }); 106 }); 107 }, 108 109 wrapTableWithBox: function() { 110 document.querySelectorAll('table').forEach(element => { 111 const box = document.createElement('div'); 112 box.className = 'table-container'; 113 element.wrap(box); 114 }); 115 }, 116 117 registerVideoIframe: function() { 118 document.querySelectorAll('iframe').forEach(element => { 119 const supported = [ 120 'www.youtube.com', 121 'player.vimeo.com', 122 'player.youku.com', 123 'player.bilibili.com', 124 'www.tudou.com' 125 ].some(host => element.src.includes(host)); 126 if (supported && !element.parentNode.matches('.video-container')) { 127 const box = document.createElement('div'); 128 box.className = 'video-container'; 129 element.wrap(box); 130 const width = Number(element.width); 131 const height = Number(element.height); 132 if (width && height) { 133 box.style.paddingTop = (height / width * 100) + '%'; 134 } 135 } 136 }); 137 }, 138 139 registerScrollPercent: function() { 140 const backToTop = document.querySelector('.back-to-top'); 141 const readingProgressBar = document.querySelector('.reading-progress-bar'); 142 // For init back to top in sidebar if page was scrolled after page refresh. 143 window.addEventListener('scroll', () => { 144 if (backToTop || readingProgressBar) { 145 const docHeight = document.querySelector('.container').offsetHeight; 146 const winHeight = window.innerHeight; 147 const contentHeight = docHeight > winHeight ? docHeight - winHeight : document.body.scrollHeight - winHeight; 148 const scrollPercent = contentHeight > 0 ? Math.min(100 * window.scrollY / contentHeight, 100) : 0; 149 if (backToTop) { 150 backToTop.classList.toggle('back-to-top-on', Math.round(scrollPercent) >= 5); 151 backToTop.querySelector('span').innerText = Math.round(scrollPercent) + '%'; 152 } 153 if (readingProgressBar) { 154 readingProgressBar.style.width = scrollPercent.toFixed(2) + '%'; 155 } 156 } 157 if (!Array.isArray(NexT.utils.sections)) return; 158 let index = NexT.utils.sections.findIndex(element => { 159 return element && element.getBoundingClientRect().top > 0; 160 }); 161 if (index === -1) { 162 index = NexT.utils.sections.length - 1; 163 } else if (index > 0) { 164 index--; 165 } 166 this.activateNavByIndex(index); 167 }); 168 169 backToTop && backToTop.addEventListener('click', () => { 170 window.anime({ 171 targets : document.scrollingElement, 172 duration : 500, 173 easing : 'linear', 174 scrollTop: 0 175 }); 176 }); 177 }, 178 179 /** 180 * Tabs tag listener (without twitter bootstrap). 181 */ 182 registerTabsTag: function() {
183 // Binding `nav-tabs` & `tab-content` by real time permalink changing. 184 document.querySelectorAll('.tabs ul.nav-tabs .tab').forEach(element => { 185 element.addEventListener('click', event => { 186 event.preventDefault(); 187 // Prevent selected tab to select again. 188 if (element.classList.contains('active')) return; 189 // Add & Remove active class on `nav-tabs` & `tab-content`. 190 [...element.parentNode.children].forEach(target => { 191 target.classList.toggle('active', target === element); 192 }); 193 // https://stackoverflow.com/questions/20306204/using-queryselector-with-ids-that-are-numbers 194 const tActive = document.getElementById(element.querySelector('a').getAttribute('href').replace('#', '')); 195 [...tActive.parentNode.children].forEach(target => { 196 target.classList.toggle('active', target === tActive); 197 }); 198 // Trigger event 199 tActive.dispatchEvent(new Event('tabs:click', { 200 bubbles: true 201 })); 202 }); 203 }); 204 205 window.dispatchEvent(new Event('tabs:register')); 206 }, 207 208 registerCanIUseTag: function() { 209 // Get responsive height passed from iframe. 210 window.addEventListener('message', ({ data }) => { 211 if (typeof data === 'string' && data.includes('ciu_embed')) { 212 const featureID = data.split(':')[1]; 213 const height = data.split(':')[2]; 214 document.querySelector(`iframe[data-feature=${featureID}]`).style.height = parseInt(height, 10) + 5 + 'px'; 215 } 216 }, false); 217 }, 218 219 registerActiveMenuItem: function() { 220 document.querySelectorAll('.menu-item a[href]').forEach(target => { 221 const isSamePath = target.pathname === location.pathname || target.pathname === location.pathname.replace('index.html', ''); 222 const isSubPath = !CONFIG.root.startsWith(target.pathname) && location.pathname.startsWith(target.pathname); 223 target.classList.toggle('menu-item-active', target.hostname === location.hostname && (isSamePath || isSubPath)); 224 }); 225 }, 226 227 registerLangSelect: function() { 228 const selects = document.querySelectorAll('.lang-select'); 229 selects.forEach(sel => { 230 sel.value = CONFIG.page.lang; 231 sel.addEventListener('change', () => { 232 const target = sel.options[sel.selectedIndex]; 233 document.querySelectorAll('.lang-select-label span').forEach(span => { 234 span.innerText = target.text; 235 }); 236 // Disable Pjax to force refresh translation of menu item 237 window.location.href = target.dataset.href; 238 }); 239 }); 240 }, 241 242 registerSidebarTOC: function() { 243 this.sections = [...document.querySelectorAll('.post-toc li a.nav-link')].map(element => { 244 const target = document.getElementById(decodeURI(element.getAttribute('href')).replace('#', '')); 245 // TOC item animation navigate. 246 element.addEventListener('click', event => { 247 event.preventDefault(); 248 const offset = target.getBoundingClientRect().top + window.scrollY; 249 window.anime({ 250 targets : document.scrollingElement, 251 duration : 500, 252 easing : 'linear', 253 scrollTop: offset + 10 254 }); 255 }); 256 return target; 257 }); 258 }, 259 260 activateNavByIndex: function(index) { 261 const target = document.querySelectorAll('.post-toc li a.nav-link')[index]; 262 if (!target || target.classList.contains('active-current')) return; 263 264 document.querySelectorAll('.post-toc .active').forEach(element => { 265 element.classList.remove('active', 'active-current'); 266 }); 267 target.classList.add('active', 'active-current'); 268 let parent = target.parentNode; 269 while (!parent.matches('.post-toc')) { 270 if (parent.matches('li')) parent.classList.add('active'); 271 parent = parent.parentNode; 272 } 273 // Scrolling to center active TOC element if TOC content is taller then viewport. 274 const tocElement = document.querySelector('.post-toc-wrap'); 275 window.anime({ 276 targets : tocElement, 277 duration : 200, 278 easing : 'linear', 279 scrollTop: tocElement.scrollTop - (tocElement.offsetHeight / 2) + target.getBoundingClientRect().top - tocElement.getBoundingClientRect().top 280 }); 281 }, 282 283 supportsPDFs: function() { 284 const ua = navigator.userAgent; 285 const isFirefoxWithPDFJS = ua.includes('irefox') && parseInt(ua.split('rv:')[1].split('.')[0], 10) > 18; 286 const supportsPdfMimeType = typeof navigator.mimeTypes['application/pdf'] !== 'undefined'; 287 const isIOS = /iphone|ipad|ipod/i.test(ua.toLowerCase()); 288 return isFirefoxWithPDFJS || (supportsPdfMimeType && !isIOS); 289 }, 290 291 getComputedStyle: function(element) { 292 const clone = element.cloneNode(true); 293 clone.style.position = 'absolute'; 294 clone.style.visibility = 'hidden'; 295 clone.style.display = 'block'; 296 element.parentNode.appendChild(clone); 297 const height = clone.clientHeight; 298 element.parentNode.removeChild(clone); 299 return height; 300 }, 301 302 /** 303 * Init Sidebar & TOC inner dimensions on all pages and for all schemes. 304 * Need for Sidebar/TOC inner scrolling if content taller then viewport. 305 */ 306 initSidebarDimension: function() { 307 const sidebarNav = document.querySelector('.sidebar-nav'); 308 const sidebarb2t = document.querySelector('.sidebar-inner .back-to-top'); 309 const sidebarNavHeight = sidebarNav ? sidebarNav.offsetHeight : 0; 310 const sidebarb2tHeight = sidebarb2t ? sidebarb2t.offsetHeight : 0; 311 const sidebarOffset = CONFIG.sidebar.offset || 12; 312 let sidebarSchemePadding = (CONFIG.sidebar.padding * 2) + sidebarNavHeight + sidebarb2tHeight; 313 if (CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini') sidebarSchemePadding += sidebarOffset * 2; 314 // Initialize Sidebar & TOC Height. 315 const sidebarWrapperHeight = document.body.offsetHeight - sidebarSchemePadding + 'px'; 316 document.documentElement.style.setProperty('--sidebar-wrapper-height', sidebarWrapperHeight); 317 }, 318 319 updateSidebarPosition: function() { 320 NexT.utils.initSidebarDimension(); 321 if (window.screen.width < 992 || CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini') return;
322 // Expand sidebar on post detail page by default, when post has a toc. 323 const hasTOC = document.querySelector('.post-toc'); 324 let display = CONFIG.page.sidebar; 325 if (typeof display !== 'boolean') { 326 // There's no definition sidebar in the page front-matter. 327 display = CONFIG.sidebar.display === 'always' || (CONFIG.sidebar.display === 'post' && hasTOC); 328 } 329 if (display) { 330 window.dispatchEvent(new Event('sidebar:show')); 331 } 332 }, 333 334 getScript: function(url, callback, condition) { 335 if (condition) { 336 callback(); 337 } else { 338 let script = document.createElement('script'); 339 script.onload = script.onreadystatechange = function(_, isAbort) { 340 if (isAbort || !script.readyState || /loaded|complete/.test(script.readyState)) { 341 script.onload = script.onreadystatechange = null; 342 script = undefined; 343 if (!isAbort && callback) setTimeout(callback, 0); 344 } 345 }; 346 script.src = url; 347 document.head.appendChild(script); 348 } 349 }, 350 351 loadComments: function(selector, callback) { 352 const element = document.querySelector(selector); 353 if (!CONFIG.comments.lazyload || !element) { 354 callback(); 355 return; 356 } 357 const intersectionObserver = new IntersectionObserver((entries, observer) => { 358 const entry = entries[0]; 359 if (entry.isIntersecting) { 360 callback(); 361 observer.disconnect(); 362 } 363 }); 364 intersectionObserver.observe(element); 365 return intersectionObserver; 366 } 367};
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.