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