PageSourceSearch

https://zhjgh.github.io/js/utils.js

js zhjgh.github.io collected 2026-10-03 10:14:47 UTC 14,359 bytes, 367 lines download raw bytes

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.