/* global NexT, CONFIG */ HTMLElement.prototype.wrap = function(wrapper) { this.parentNode.insertBefore(wrapper, this); this.parentNode.removeChild(this); wrapper.appendChild(this); }; (function() { const onPageLoaded = () => document.dispatchEvent( new Event('page:loaded', { bubbles: true }) ); if (document.readyState === 'loading') { document.addEventListener('readystatechange', onPageLoaded, { once: true }); } else { onPageLoaded(); } document.addEventListener('pjax:success', onPageLoaded); })(); NexT.utils = { registerExtURL: function() { document.querySelectorAll('span.exturl').forEach(element => { const link = document.createElement('a'); // https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings link.href = decodeURIComponent(atob(element.dataset.url).split('').map(c => { return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); }).join('')); link.rel = 'noopener external nofollow noreferrer'; link.target = '_blank'; link.className = element.className; link.title = element.title; link.innerHTML = element.innerHTML; element.parentNode.replaceChild(link, element); }); }, /** * One-click copy code support. */ registerCopyCode: function() { let figure = document.querySelectorAll('figure.highlight'); if (figure.length === 0) figure = document.querySelectorAll('pre:not(.mermaid)'); figure.forEach(element => { element.querySelectorAll('.code .line span').forEach(span => { span.classList.forEach(name => { span.classList.replace(name, `hljs-${name}`); }); }); if (!CONFIG.copycode.enable) return; let target = element; if (CONFIG.copycode.style !== 'mac') target = element.querySelector('.table-container') || element; target.insertAdjacentHTML('beforeend', '
'); const button = element.querySelector('.copy-btn'); button.addEventListener('click', () => { const lines = element.querySelector('.code') || element.querySelector('code'); const code = lines.innerText; if (navigator.clipboard) { // https://caniuse.com/mdn-api_clipboard_writetext navigator.clipboard.writeText(code).then(() => { button.querySelector('i').className = 'fa fa-check-circle fa-fw'; }, () => { button.querySelector('i').className = 'fa fa-times-circle fa-fw'; }); } else { const ta = document.createElement('textarea'); ta.style.top = window.scrollY + 'px'; // Prevent page scrolling ta.style.position = 'absolute'; ta.style.opacity = '0'; ta.readOnly = true; ta.value = code; document.body.append(ta); ta.select(); ta.setSelectionRange(0, code.length); ta.readOnly = false; const result = document.execCommand('copy'); button.querySelector('i').className = result ? 'fa fa-check-circle fa-fw' : 'fa fa-times-circle fa-fw'; ta.blur(); // For iOS button.blur(); document.body.removeChild(ta); } }); element.addEventListener('mouseleave', () => { setTimeout(() => { button.querySelector('i').className = 'fa fa-copy fa-fw'; }, 300); }); }); }, wrapTableWithBox: function() { document.querySelectorAll('table').forEach(element => { const box = document.createElement('div'); box.className = 'table-container'; element.wrap(box); }); }, registerVideoIframe: function() { document.querySelectorAll('iframe').forEach(element => { const supported = [ 'www.youtube.com', 'player.vimeo.com', 'player.youku.com', 'player.bilibili.com', 'www.tudou.com' ].some(host => element.src.includes(host)); if (supported && !element.parentNode.matches('.video-container')) { const box = document.createElement('div'); box.className = 'video-container'; element.wrap(box); const width = Number(element.width); const height = Number(element.height); if (width && height) { box.style.paddingTop = (height / width * 100) + '%'; } } }); }, updateActiveNav: function() { if (!Array.isArray(NexT.utils.sections)) return; let index = NexT.utils.sections.findIndex(element => { return element && element.getBoundingClientRect().top > 10; }); if (index === -1) { index = NexT.utils.sections.length - 1; } else if (index > 0) { index--; } this.activateNavByIndex(index); }, registerScrollPercent: function() { const backToTop = document.querySelector('.back-to-top'); const readingProgressBar = document.querySelector('.reading-progress-bar'); // For init back to top in sidebar if page was scrolled after page refresh. window.addEventListener('scroll', () => { if (backToTop || readingProgressBar) { const contentHeight = document.body.scrollHeight - window.innerHeight; const scrollPercent = contentHeight > 0 ? Math.min(100 * window.scrollY / contentHeight, 100) : 0; if (backToTop) { backToTop.classList.toggle('back-to-top-on', Math.round(scrollPercent) >= 5); backToTop.querySelector('span').innerText = Math.round(scrollPercent) + '%'; } if (readingProgressBar) { readingProgressBar.style.setProperty('--progress', scrollPercent.toFixed(2) + '%'); } } this.updateActiveNav(); }, { passive: true }); backToTop && backToTop.addEventListener('click', () => { window.anime({ targets : document.scrollingElement, duration : 500, easing : 'linear', scrollTop: 0 }); }); }, /** * Tabs tag listener (without twitter bootstrap). */ registerTabsTag: function() { // Binding `nav-tabs` & `tab-content` by real time permalink changing. document.querySelectorAll('.tabs ul.nav-tabs .tab').forEach(element => { element.addEventListener('click', event => { event.preventDefault(); // Prevent selected tab to select again. if (element.classList.contains('active')) return; const nav = element.parentNode; // Get the height of `tab-pane` which is activated before, and set it as the height of `tab-content` with extra margin / paddings. const tabContent = nav.nextElementSibling; tabContent.style.overflow = 'hidden'; tabContent.style.transition = 'height 1s'; // Comment system selection tab does not contain .active class. const activeTab = tabContent.querySelector('.active') || tabContent.firstElementChild; // Hight might be `auto`. const prevHeight = parseInt(window.getComputedStyle(activeTab).height.replace('px', ''), 10) || 0; const paddingTop = parseInt(window.getComputedStyle(activeTab).paddingTop.replace('px', ''), 10); const marginBottom = parseInt(window.getComputedStyle(activeTab.firstElementChild).marginBottom.replace('px', ''), 10); tabContent.style.height = prevHeight + paddingTop + marginBottom + 'px'; // Add & Remove active class on `nav-tabs` & `tab-content`. [...nav.children].forEach(target => { target.classList.toggle('active', target === element); }); // https://stackoverflow.com/questions/20306204/using-queryselector-with-ids-that-are-numbers const tActive = document.getElementById(element.querySelector('a').getAttribute('href').replace('#', '')); [...tActive.parentNode.children].forEach(target => { target.classList.toggle('active', target === tActive); }); // Trigger event tActive.dispatchEvent(new Event('tabs:click', { bubbles: true })); // Get the height of `tab-pane` which is activated now. const hasScrollBar = document.body.scrollHeight > (window.innerHeight || document.documentElement.clientHeight); const currHeight = parseInt(window.getComputedStyle(tabContent.querySelector('.active')).height.replace('px', ''), 10); // Reset the height of `tab-content` and see the animation. tabContent.style.height = currHeight + paddingTop + marginBottom + 'px'; // Change the height of `tab-content` may cause scrollbar show / disappear, which may result in the change of the `tab-pane`'s height setTimeout(() => { if ((document.body.scrollHeight > (window.innerHeight || document.documentElement.clientHeight)) !== hasScrollBar) { tabContent.style.transition = 'height 0.3s linear'; // After the animation, we need reset the height of `tab-content` again. const currHeightAfterScrollBarChange = parseInt(window.getComputedStyle(tabContent.querySelector('.active')).height.replace('px', ''), 10); tabContent.style.height = currHeightAfterScrollBarChange + paddingTop + marginBottom + 'px'; } // Remove all the inline styles, and let the height be adaptive again. setTimeout(() => { tabContent.style.transition = ''; tabContent.style.height = ''; }, 250); }, 1000); if (!CONFIG.stickytabs) return; const offset = nav.parentNode.getBoundingClientRect().top + window.scrollY + 10; window.anime({ targets : document.scrollingElement, duration : 500, easing : 'linear', scrollTop: offset }); }); }); window.dispatchEvent(new Event('tabs:register')); }, registerCanIUseTag: function() { // Get responsive height passed from iframe. window.addEventListener('message', ({ data }) => { if (typeof data === 'string' && data.includes('ciu_embed')) { const featureID = data.split(':')[1]; const height = data.split(':')[2]; document.querySelector(`iframe[data-feature=${featureID}]`).style.height = parseInt(height, 10) + 5 + 'px'; } }, false); }, registerActiveMenuItem: function() { document.querySelectorAll('.menu-item a[href]').forEach(target => { const isSamePath = target.pathname === location.pathname || target.pathname === location.pathname.replace('index.html', ''); const isSubPath = !CONFIG.root.startsWith(target.pathname) && location.pathname.startsWith(target.pathname); target.classList.toggle('menu-item-active', target.hostname === location.hostname && (isSamePath || isSubPath)); }); }, registerLangSelect: function() { const selects = document.querySelectorAll('.lang-select'); selects.forEach(sel => { sel.value = CONFIG.page.lang; sel.addEventListener('change', () => { const target = sel.options[sel.selectedIndex]; document.querySelectorAll('.lang-select-label span').forEach(span => { span.innerText = target.text; }); // Disable Pjax to force refresh translation of menu item window.location.href = target.dataset.href; }); }); }, registerSidebarTOC: function() { this.sections = [...document.querySelectorAll('.post-toc:not(.placeholder-toc) li a.nav-link')].map(element => { const target = document.getElementById(decodeURI(element.getAttribute('href')).replace('#', '')); // TOC item animation navigate. element.addEventListener('click', event => { event.preventDefault(); const offset = target.getBoundingClientRect().top + window.scrollY; window.anime({ targets : document.scrollingElement, duration : 500, easing : 'linear', scrollTop: offset, complete : () => { history.pushState(null, document.title, element.href); } }); }); return target; }); this.updateActiveNav(); }, registerPostReward: function() { const button = document.querySelector('.reward-container button'); if (!button) return; button.addEventListener('click', () => { document.querySelector('.post-reward').classList.toggle('active'); }); }, activateNavByIndex: function(index) { const nav = document.querySelector('.post-toc:not(.placeholder-toc) .nav'); if (!nav) return; const navItemList = nav.querySelectorAll('.nav-item'); const target = navItemList[index]; if (!target || target.classList.contains('active-current')) return; const singleHeight = navItemList[navItemList.length - 1].offsetHeight; nav.querySelectorAll('.active').forEach(navItem => { navItem.classList.remove('active', 'active-current'); }); target.classList.add('active', 'active-current'); let activateEle = target.querySelector('.nav-child') || target.parentElement; let navChildHeight = 0; while (nav.contains(activateEle)) { if (activateEle.classList.contains('nav-item')) { activateEle.classList.add('active'); } else { // .nav-child or .nav // scrollHeight isn't reliable for transitioning child items. // The last nav-item in a list has a margin-bottom of 5px. navChildHeight += (singleHeight * activateEle.childElementCount) + 5; activateEle.style.setProperty('--height', `${navChildHeight}px`); } activateEle = activateEle.parentElement; } // Scrolling to center active TOC element if TOC content is taller then viewport. const tocElement = document.querySelector(CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini' ? '.sidebar-panel-container' : '.sidebar'); if (!document.querySelector('.sidebar-toc-active')) return; window.anime({ targets : tocElement, duration : 200, easing : 'linear', scrollTop: tocElement.scrollTop - (tocElement.offsetHeight / 2) + target.getBoundingClientRect().top - tocElement.getBoundingClientRect().top }); }, updateSidebarPosition: function() { if (window.innerWidth < 1200 || CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini') return; // Expand sidebar on post detail page by default, when post has a toc. const hasTOC = document.querySelector('.post-toc:not(.placeholder-toc)'); let display = CONFIG.page.sidebar; if (typeof display !== 'boolean') { // There's no definition sidebar in the page front-matter. display = CONFIG.sidebar.display === 'always' || (CONFIG.sidebar.display === 'post' && hasTOC); } if (display) { window.dispatchEvent(new Event('sidebar:show')); } }, activateSidebarPanel: function(index) { const sidebar = document.querySelector('.sidebar-inner'); const activeClassNames = ['sidebar-toc-active', 'sidebar-overview-active']; if (sidebar.classList.contains(activeClassNames[index])) return; const panelContainer = sidebar.querySelector('.sidebar-panel-container'); const tocPanel = panelContainer.firstElementChild; const overviewPanel = panelContainer.lastElementChild; let postTOCHeight = tocPanel.scrollHeight; // For TOC activation, try to use the animated TOC height if (index === 0) { const nav = tocPanel.querySelector('.nav'); if (nav) { postTOCHeight = parseInt(nav.style.getPropertyValue('--height'), 10); } } const panelHeights = [ postTOCHeight, overviewPanel.scrollHeight ]; panelContainer.style.setProperty('--inactive-panel-height', `${panelHeights[1 - index]}px`); panelContainer.style.setProperty('--active-panel-height', `${panelHeights[index]}px`); sidebar.classList.replace(activeClassNames[1 - index], activeClassNames[index]); }, getScript: function(src, options = {}, legacyCondition) { if (typeof options === 'function') { return this.getScript(src, { condition: legacyCondition }).then(options); } const { condition = false, attributes: { id = '', async = false, defer = false, crossOrigin = '', dataset = {}, ...otherAttributes } = {}, parentNode = null } = options; return new Promise((resolve, reject) => { if (condition) { resolve(); } else { const script = document.createElement('script'); if (id) script.id = id; if (crossOrigin) script.crossOrigin = crossOrigin; script.async = async; script.defer = defer; Object.assign(script.dataset, dataset); Object.entries(otherAttributes).forEach(([name, value]) => { script.setAttribute(name, String(value)); }); script.onload = resolve; script.onerror = reject; if (typeof src === 'object') { const { url, integrity } = src; script.src = url; if (integrity) { script.integrity = integrity; script.crossOrigin = 'anonymous'; } } else { script.src = src; } (parentNode || document.head).appendChild(script); } }); }, loadComments: function(selector, legacyCallback) { if (legacyCallback) { return this.loadComments(selector).then(legacyCallback); } return new Promise(resolve => { const element = document.querySelector(selector); if (!CONFIG.comments.lazyload || !element) { resolve(); return; } const intersectionObserver = new IntersectionObserver((entries, observer) => { const entry = entries[0]; if (!entry.isIntersecting) return; resolve(); observer.disconnect(); }); intersectionObserver.observe(element); }); } };