PageSourceSearch

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

js yc518suray.github.io collected 2026-10-03 10:12:41 UTC 20,252 bytes, 522 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(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() {
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  registerCopyButton(target, element, code = '') {
43    // One-click copy code support.
44    target.insertAdjacentHTML('beforeend', '<div class="copy-btn"><i class="fa fa-copy fa-fw"></i></div>');
45    const button = target.querySelector('.copy-btn');
46    button.addEventListener('click', () => {
47      if (!code) {
48        const lines = element.querySelector('.code') || element.querySelector('code');
49        code = lines.innerText;
50      }
51      if (navigator.clipboard) {
52        // https://caniuse.com/mdn-api_clipboard_writetext
53        navigator.clipboard.writeText(code).then(() => {
54          button.querySelector('i').className = 'fa fa-check-circle fa-fw';
55        }, () => {
56          button.querySelector('i').className = 'fa fa-times-circle fa-fw';
57        });
58      } else {
59        const ta = document.createElement('textarea');
60        ta.style.top = window.scrollY + 'px'; // Prevent page scrolling
61        ta.style.position = 'absolute';
62        ta.style.opacity = '0';
63        ta.readOnly = true;
64        ta.value = code;
65        document.body.append(ta);
66        ta.select();
67        ta.setSelectionRange(0, code.length);
68        ta.readOnly = false;
69        const result = document.execCommand('copy');
70        button.querySelector('i').className = result ? 'fa fa-check-circle fa-fw' : 'fa fa-times-circle fa-fw';
71        ta.blur(); // For iOS
72        button.blur();
73        document.body.removeChild(ta);
74      }
75    });
76    // If copycode.style is not mac, element is larger than target
77    // So we need to accept both of them as parameters
78    element.addEventListener('mouseleave', () => {
79      setTimeout(() => {
80        button.querySelector('i').className = 'fa fa-copy fa-fw';
81      }, 300);
82    });
83  },
84
85  registerCodeblock(element) {
86    const inited = !!element;
87    let figure;
88    if (CONFIG.hljswrap) {
89      figure = (inited ? element : document).querySelectorAll('figure.highlight');
90    } else {
91      figure = document.querySelectorAll('pre');
92    }
93    figure.forEach(element => {
94      // Skip pre > .mermaid for folding and copy button
95      if (element.querySelector('.mermaid')) return;
96      if (!inited) {
97        let span = element.querySelectorAll('.code .line span');
98        if (span.length === 0) {
99          // Hljs without line_number and wrap
100          span = element.querySelectorAll('code.highlight span');
101        }
102        span.forEach(s => {
103          s.classList.forEach(name => {
104            s.classList.replace(name, `hljs-${name}`);
105          });
106        });
107      }
108      const height = parseInt(window.getComputedStyle(element).height, 10);
109      const needFold = CONFIG.fold.enable && (height > CONFIG.fold.height);
110      if (!needFold && !CONFIG.copycode.enable) return;
111      let target;
112      if (CONFIG.hljswrap && CONFIG.copycode.style === 'mac') {
113        target = element;
114      } else {
115        let box = element.querySelector('.code-container');
116        if (!box) {
117          // https://github.com/next-theme/hexo-theme-next/issues/98
118          // https://github.com/next-theme/hexo-theme-next/pull/508
119          const container = element.querySelector('.table-container') || element;
120          box = document.createElement('div');
121          box.className = 'code-container';
122          container.wrap(box);
123
124          // add "notranslate" to prevent Google Translate from translating it, which also completely messes up the layout
125          box.classList.add('notranslate');
126        }
127        target = box;
128      }
129      if (needFold && !target.classList.contains('unfold')) {
130        target.classList.add('highlight-fold');
131        target.insertAdjacentHTML('beforeend', '<div class="fold-cover"></div><div class="expand-btn"><i class="fa fa-angle-down fa-fw"></i></div>');
132        target.querySelector('.expand-btn').addEventListener('click', () => {
133          target.classList.remove('highlight-fold');
134          target.classList.add('unfold');
135        });
136      }
137      if (!inited && CONFIG.copycode.enable) {
138        this.registerCopyButton(target, element);
139      }
140    });
141  },
142
143  wrapTableWithBox() {
144    document.querySelectorAll('table').forEach(element => {
145      const box = document.createElement('div');
146      box.className = 'table-container';
147      element.wrap(box);
148    });
149  },
150
151  registerVideoIframe() {
152    document.querySelectorAll('iframe').forEach(element => {
153      const supported = [
154        'www.youtube.com',
155        'player.vimeo.com',
156        'player.youku.com',
157        'player.bilibili.com',
158        'www.tudou.com'
159      ].some(host => element.src.includes(host));
160      if (supported && !element.parentNode.matches('.video-container')) {
161        const box = document.createElement('div');
162        box.className = 'video-container';
163        element.wrap(box);
164        const width = Number(element.width);
165        const height = Number(element.height);
166        if (width && height) {
167          box.style.paddingTop = (height / width * 100) + '%';
168        }
169      }
170    });
171  },
172
173  updateActiveNav() {
174    if (!Array.isArray(this.sections)) return;
175    let index = this.sections.findIndex(element => {
176      return element && element.getBoundingClientRect().top > 10;
177    });
178    if (index === -1) {
179      index = this.sections.length - 1;
180    } else if (index > 0) {
181      index--;
182    }
183    this.activateNavByIndex(index);
184  },
185
186  registerScrollPercent() {
187    const backToTop = document.querySelector('.back-to-top');
188    const readingProgressBar = document.querySelector('.reading-progress-bar');
189    // For init back to top in sidebar if page was scrolled after page refresh.
190    window.addEventListener('scroll', () => {
191      if (backToTop || readingProgressBar) {
192        const contentHeight = document.body.scrollHeight - window.innerHeight;
193        const scrollPercent = contentHeight > 0 ? Math.min(100 * window.scrollY / contentHeight, 100) : 0;
194        if (backToTop) {
195          backToTop.classList.toggle('back-to-top-on', Math.round(scrollPercent) >= 5);
196          backToTop.querySelector('span').innerText = Math.round(scrollPercent) + '%';
197        }
198        if (readingProgressBar) {
199          readingProgressBar.style.setProperty('--progress', scrollPercent.toFixed(2) + '%');
200        }
201      }
202      this.updateActiveNav();
203    }, { passive: true });
204
205    backToTop && backToTop.addEventListener('click', () => {
206      window.anime({
207        targets  : document.scrollingElement,
208        duration : 500,
209        easing   : 'linear',
210        scrollTop: 0
211      });
212    });
213  },
214
215  /**
216   * Tabs tag listener (without twitter bootstrap).
217   */
218  registerTabsTag() {
219    // Binding `nav-tabs` & `tab-content` by real time permalink changing.
220    document.querySelectorAll('.tabs ul.nav-tabs .tab').forEach(element => {
221      element.addEventListener('click', event => {
222        event.preventDefault();
223        // Prevent selected tab to select again.
224        if (element.classList.contains('active')) return;
225        const nav = element.parentNode;
226        // Get the height of `tab-pane` which is activated before, and set it as the height of `tab-content` with extra margin / paddings.
227        const tabContent = nav.nextElementSibling;
228        tabContent.style.overflow = 'hidden';
229        tabContent.style.transition = 'height 1s';
230        // Comment system selection tab does not contain .active class.
231        const activeTab = tabContent.querySelector('.active') || tabContent.firstElementChild;
232        // Hight might be `auto`.
233        const prevHeight = parseInt(window.getComputedStyle(activeTab).height, 10) || 0;
234        const paddingTop = parseInt(window.getComputedStyle(activeTab).paddingTop, 10);
235        const marginBottom = parseInt(window.getComputedStyle(activeTab.firstElementChild).marginBottom, 10);
236        tabContent.style.height = prevHeight + paddingTop + marginBottom + 'px';
237        // Add & Remove active class on `nav-tabs` & `tab-content`.
238        [...nav.children].forEach(target => {
239          target.classList.toggle('active', target === element);
240        });
241        // https://stackoverflow.com/questions/20306204/using-queryselector-with-ids-that-are-numbers
242        const tActive = document.getElementById(element.querySelector('a').getAttribute('href').replace('#', ''));
243        [...tActive.parentNode.children].forEach(target => {
244          target.classList.toggle('active', target === tActive);
245        });
246        // Trigger event
247        tActive.dispatchEvent(new Event('tabs:click', {
248          bubbles: true
249        }));
250        // Get the height of `tab-pane` which is activated now.
251        const hasScrollBar = document.body.scrollHeight > (window.innerHeight || document.documentElement.clientHeight);
252        const currHeight = parseInt(window.getComputedStyle(tabContent.querySelector('.active')).height, 10);
253        // Reset the height of `tab-content` and see the animation.
254        tabContent.style.height = currHeight + paddingTop + marginBottom + 'px';
255        // Change the height of `tab-content` may cause scrollbar show / disappear, which may result in the change of the `tab-pane`'s height
256        setTimeout(() => {
257          if ((document.body.scrollHeight > (window.innerHeight || document.documentElement.clientHeight)) !== hasScrollBar) {
258            tabContent.style.transition = 'height 0.3s linear';
259            // After the animation, we need reset the height of `tab-content` again.
260            const currHeightAfterScrollBarChange = parseInt(window.getComputedStyle(tabContent.querySelector('.active')).height, 10);
261            tabContent.style.height = currHeightAfterScrollBarChange + paddingTop + marginBottom + 'px';
262          }
263          // Remove all the inline styles, and let the height be adaptive again.
264          setTimeout(() => {
265            tabContent.style.transition = '';
266            tabContent.style.height = '';
267          }, 250);
268        }, 1000);
269        if (!CONFIG.stickytabs) return;
270        const offset = nav.parentNode.getBoundingClientRect().top + window.scrollY + 10;
271        window.anime({
272          targets  : document.scrollingElement,
273          duration : 500,
274          easing   : 'linear',
275          scrollTop: offset
276        });
277      });
278    });
279
280    window.dispatchEvent(new Event('tabs:register'));
281  },
282
283  registerCanIUseTag() {
284    // Get responsive height passed from iframe.
285    window.addEventListener('message', ({ data }) => {
286      if (typeof data === 'string' && data.includes('ciu_embed')) {
287        const featureID = data.split(':')[1];
288        const height = data.split(':')[2];
289        document.querySelector(`iframe[data-feature=${featureID}]`).style.height = parseInt(height, 10) + 5 + 'px';
290      }
291    }, false);
292  },
293
294  registerActiveMenuItem() {
295    document.querySelectorAll('.menu-item a[href]').forEach(target => {
296      const isSamePath = target.pathname === location.pathname || target.pathname === location.pathname.replace('index.html', '');
297      const isSubPath = !CONFIG.root.startsWith(target.pathname) && location.pathname.startsWith(target.pathname);
298      target.classList.toggle('menu-item-active', target.hostname === location.hostname && (isSamePath || isSubPath));
299    });
300  },
301
302  registerLangSelect() {
303    const selects = document.querySelectorAll('.lang-select');
304    selects.forEach(sel => {
305      sel.value = CONFIG.page.lang;
306      sel.addEventListener('change', () => {
307        const target = sel.options[sel.selectedIndex];
308        document.querySelectorAll('.lang-select-label span').forEach(span => {
309          span.innerText = target.text;
310        });
311        // Disable Pjax to force refresh translation of menu item
312        window.location.href = target.dataset.href;
313      });
314    });
315  },
316
317  registerSidebarTOC() {
318    this.sections = [...document.querySelectorAll('.post-toc:not(.placeholder-toc) li a.nav-link')].map(element => {
319      const target = document.getElementById(decodeURI(element.getAttribute('href')).replace('#', ''));
320      // TOC item animation navigate.
321      element.addEventListener('click', event => {
322        event.preventDefault();
323        const offset = target.getBoundingClientRect().top + window.scrollY;
324        window.anime({
325          targets  : document.scrollingElement,
326          duration : 500,
327          easing   : 'linear',
328          scrollTop: offset,
329          complete : () => {
330            history.pushState(null, document.title, element.href);
331          }
332        });
333      });
334      return target;
335    });
336    this.updateActiveNav();
337  },
338
339  registerPostReward() {
340    const button = document.querySelector('.reward-container button');
341    if (!button) return;
342    button.addEventListener('click', () => {
343      document.querySelector('.post-reward').classList.toggle('active');
344    });
345  },
346
347  activateNavByIndex(index) {
348    const nav = document.querySelector('.post-toc:not(.placeholder-toc) .nav');
349    if (!nav) return;
350
351    const navItemList = nav.querySelectorAll('.nav-item');
352    const target = navItemList[index];
353    if (!target || target.classList.contains('active-current')) return;
354
355    const singleHeight = navItemList[navItemList.length - 1].offsetHeight;
356
357    nav.querySelectorAll('.active').forEach(navItem => {
358      navItem.classList.remove('active', 'active-current');
359    });
360    target.classList.add('active', 'active-current');
361
362    let activateEle = target.querySelector('.nav-child') || target.parentElement;
363    let navChildHeight = 0;
364
365    while (nav.contains(activateEle)) {
366      if (activateEle.classList.contains('nav-item')) {
367        activateEle.classList.add('active');
368      } else { // .nav-child or .nav
369        // scrollHeight isn't reliable for transitioning child items.
370        // The last nav-item in a list has a margin-bottom of 5px.
371        navChildHeight += (singleHeight * activateEle.childElementCount) + 5;
372        activateEle.style.setProperty('--height', `${navChildHeight}px`);
373      }
374      activateEle = activateEle.parentElement;
375    }
376
377    // Scrolling to center active TOC element if TOC content is taller then viewport.
378    const tocElement = document.querySelector(CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini' ? '.sidebar-panel-container' : '.sidebar');
379    if (!document.querySelector('.sidebar-toc-active')) return;
380    window.anime({
381      targets  : tocElement,
382      duration : 200,
383      easing   : 'linear',
384      scrollTop: tocElement.scrollTop - (tocElement.offsetHeight / 2) + target.getBoundingClientRect().top - tocElement.getBoundingClientRect().top
385    });
386  },
387
388  updateSidebarPosition() {
389    if (window.innerWidth < 1200 || CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini') return;
390    // Expand sidebar on post detail page by default, when post has a toc.
391    const hasTOC = document.querySelector('.post-toc:not(.placeholder-toc)');
392    let display = CONFIG.page.sidebar;
393    if (typeof display !== 'boolean') {
394      // There's no definition sidebar in the page front-matter.
395      display = CONFIG.sidebar.display === 'always' || (CONFIG.sidebar.display === 'post' && hasTOC);
396    }
397    if (display) {
398      window.dispatchEvent(new Event('sidebar:show'));
399    }
400  },
401
402  activateSidebarPanel(index) {
403    const sidebar = document.querySelector('.sidebar-inner');
404    const activeClassNames = ['sidebar-toc-active', 'sidebar-overview-active'];
405    if (sidebar.classList.contains(activeClassNames[index])) return;
406
407    const panelContainer = sidebar.querySelector('.sidebar-panel-container');
408    const tocPanel = panelContainer.firstElementChild;
409    const overviewPanel = panelContainer.lastElementChild;
410
411    let postTOCHeight = tocPanel.scrollHeight;
412    // For TOC activation, try to use the animated TOC height
413    if (index === 0) {
414      const nav = tocPanel.querySelector('.nav');
415      if (nav) {
416        postTOCHeight = parseInt(nav.style.getPropertyValue('--height'), 10);
417      }
418    }
419    const panelHeights = [
420      postTOCHeight,
421      overviewPanel.scrollHeight
422    ];
423    panelContainer.style.setProperty('--inactive-panel-height', `${panelHeights[1 - index]}px`);
424    panelContainer.style.setProperty('--active-panel-height', `${panelHeights[index]}px`);
425
426    sidebar.classList.replace(activeClassNames[1 - index], activeClassNames[index]);
427  },
428
429  updateFooterPosition() {
430    if (CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini') return;
431    function updateFooterPosition() {
432      const footer = document.querySelector('.footer');
433      const containerHeight = document.querySelector('.main').offsetHeight + footer.offsetHeight;
434      footer.classList.toggle('footer-fixed', containerHeight <= window.innerHeight);
435    }
436
437    updateFooterPosition();
438    window.addEventListener('resize', updateFooterPosition);
439    window.addEventListener('scroll', updateFooterPosition, { passive: true });
440  },
441
442  getScript(src, options = {}, legacyCondition) {
443    if (typeof options === 'function') {
444      return this.getScript(src, {
445        condition: legacyCondition
446      }).then(options);
447    }
448    const {
449      condition = false,
450      attributes: {
451        id = '',
452        defer = false,
453        crossOrigin = '',
454        dataset = {},
455        ...otherAttributes
456      } = {},
457      parentNode = null
458    } = options;
459    const async = options.async ?? false;
460    return new Promise((resolve, reject) => {
461      if (condition) {
462        resolve();
463      } else {
464        const script = document.createElement('script');
465
466        if (id) script.id = id;
467        if (crossOrigin) script.crossOrigin = crossOrigin;
468        script.async = async;
469        script.defer = defer;
470        Object.assign(script.dataset, dataset);
471        Object.entries(otherAttributes).forEach(([name, value]) => {
472          script.setAttribute(name, String(value));
473        });
474
475        script.onload = resolve;
476        script.onerror = reject;
477
478        if (typeof src === 'object') {
479          const { url, integrity } = src;
480          script.src = url;
481          if (integrity) {
482            script.integrity = integrity;
483            script.crossOrigin = 'anonymous';
484          }
485        } else {
486          script.src = src;
487        }
488        (parentNode || document.head).appendChild(script);
489      }
490    });
491  },
492
493  loadComments(selector, legacyCallback) {
494    if (legacyCallback) {
495      return this.loadComments(selector).then(legacyCallback);
496    }
497    return new Promise(resolve => {
498      const element = document.querySelector(selector);
499      if (!CONFIG.comments.lazyload || !element) {
500        resolve();
501        return;
502      }
503      const intersectionObserver = new IntersectionObserver((entries, observer) => {
504        const entry = entries[0];
505        if (!entry.isIntersecting) return;
506
507        resolve();
508        observer.disconnect();
509      });
510      intersectionObserver.observe(element);
511    });
512  },
513
514  debounce(func, wait) {
515    let timeout;
516    return function(...args) {
517      const context = this;
518      clearTimeout(timeout);
519      timeout = setTimeout(() => func.apply(context, args), wait);
520    };
521  }
522};

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.