PageSourceSearch

https://meilbn.com/js/utils.js

js meilbn.com collected 2026-10-02 16:02:49 UTC 14,578 bytes, 401 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: function() {
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  /**
43   * One-click copy code support.
44   */
45  registerCopyCode: function() {
46    let figure = document.querySelectorAll('figure.highlight');
47    if (figure.length === 0) figure = document.querySelectorAll('pre:not(.mermaid)');
48    figure.forEach(element => {
49      element.querySelectorAll('.code .line span').forEach(span => {
50        span.classList.forEach(name => {
51          span.classList.replace(name, `hljs-${name}`);
52        });
53      });
54      if (!CONFIG.copycode.enable) return;
55      let target = element;
56      if (CONFIG.copycode.style !== 'mac') target = element.querySelector('.table-container') || element;
57      target.insertAdjacentHTML('beforeend', '<div class="copy-btn"><i class="fa fa-copy fa-fw"></i></div>');
58      const button = element.querySelector('.copy-btn');
59      button.addEventListener('click', () => {
60        const lines = element.querySelector('.code') || element.querySelector('code');
61        const code = lines.innerText;
62        if (navigator.clipboard) {
63          // https://caniuse.com/mdn-api_clipboard_writetext
64          navigator.clipboard.writeText(code).then(() => {
65            button.querySelector('i').className = 'fa fa-check-circle fa-fw';
66          }, () => {
67            button.querySelector('i').className = 'fa fa-times-circle fa-fw';
68          });
69        } else {
70          const ta = document.createElement('textarea');
71          ta.style.top = window.scrollY + 'px'; // Prevent page scrolling
72          ta.style.position = 'absolute';
73          ta.style.opacity = '0';
74          ta.readOnly = true;
75          ta.value = code;
76          document.body.append(ta);
77          ta.select();
78          ta.setSelectionRange(0, code.length);
79          ta.readOnly = false;
80          const result = document.execCommand('copy');
81          button.querySelector('i').className = result ? 'fa fa-check-circle fa-fw' : 'fa fa-times-circle fa-fw';
82          ta.blur(); // For iOS
83          button.blur();
84          document.body.removeChild(ta);
85        }
86      });
87      element.addEventListener('mouseleave', () => {
88        setTimeout(() => {
89          button.querySelector('i').className = 'fa fa-copy fa-fw';
90        }, 300);
91      });
92    });
93  },
94
95  wrapTableWithBox: function() {
96    document.querySelectorAll('table').forEach(element => {
97      const box = document.createElement('div');
98      box.className = 'table-container';
99      element.wrap(box);
100    });
101  },
102
103  registerVideoIframe: function() {
104    document.querySelectorAll('iframe').forEach(element => {
105      const supported = [
106        'www.youtube.com',
107        'player.vimeo.com',
108        'player.youku.com',
109        'player.bilibili.com',
110        'www.tudou.com'
111      ].some(host => element.src.includes(host));
112      if (supported && !element.parentNode.matches('.video-container')) {
113        const box = document.createElement('div');
114        box.className = 'video-container';
115        element.wrap(box);
116        const width = Number(element.width);
117        const height = Number(element.height);
118        if (width && height) {
119          box.style.paddingTop = (height / width * 100) + '%';
120        }
121      }
122    });
123  },
124
125  registerScrollPercent: function() {
126    const backToTop = document.querySelector('.back-to-top');
127    const readingProgressBar = document.querySelector('.reading-progress-bar');
128    // For init back to top in sidebar if page was scrolled after page refresh.
129    window.addEventListener('scroll', () => {
130      if (backToTop || readingProgressBar) {
131        const contentHeight = document.body.scrollHeight - window.innerHeight;
132        const scrollPercent = contentHeight > 0 ? Math.min(100 * window.scrollY / contentHeight, 100) : 0;
133        if (backToTop) {
134          backToTop.classList.toggle('back-to-top-on', Math.round(scrollPercent) >= 5);
135          backToTop.querySelector('span').innerText = Math.round(scrollPercent) + '%';
136        }
137        if (readingProgressBar) {
138          readingProgressBar.style.setProperty('--progress', scrollPercent.toFixed(2) + '%');
139        }
140      }
141      if (!Array.isArray(NexT.utils.sections)) return;
142      let index = NexT.utils.sections.findIndex(element => {
143        return element && element.getBoundingClientRect().top > 10;
144      });
145      if (index === -1) {
146        index = NexT.utils.sections.length - 1;
147      } else if (index > 0) {
148        index--;
149      }
150      this.activateNavByIndex(index);
151    }, { passive: true });
152
153    backToTop && backToTop.addEventListener('click', () => {
154      window.anime({
155        targets  : document.scrollingElement,
156        duration : 500,
157        easing   : 'linear',
158        scrollTop: 0
159      });
160    });
161  },
162
163  /**
164   * Tabs tag listener (without twitter bootstrap).
165   */
166  registerTabsTag: function() {
167    // Binding `nav-tabs` & `tab-content` by real time permalink changing.
168    document.querySelectorAll('.tabs ul.nav-tabs .tab').forEach(element => {
169      element.addEventListener('click', event => {
170        event.preventDefault();
171        // Prevent selected tab to select again.
172        if (element.classList.contains('active')) return;
173        const nav = element.parentNode;
174        // Add & Remove active class on `nav-tabs` & `tab-content`.
175        [...nav.children].forEach(target => {
176          target.classList.toggle('active', target === element);
177        });
178        // https://stackoverflow.com/questions/20306204/using-queryselector-with-ids-that-are-numbers
179        const tActive = document.getElementById(element.querySelector('a').getAttribute('href').replace('#', ''));
180        [...tActive.parentNode.children].forEach(target => {
181          target.classList.toggle('active', target === tActive);
182        });
183        // Trigger event
184        tActive.dispatchEvent(new Event('tabs:click', {
185          bubbles: true
186        }));
187        if (!CONFIG.stickytabs) return;
188        const offset = nav.parentNode.getBoundingClientRect().top + window.scrollY + 10;
189        window.anime({
190          targets  : document.scrollingElement,
191          duration : 500,
192          easing   : 'linear',
193          scrollTop: offset
194        });
195      });
196    });
197
198    window.dispatchEvent(new Event('tabs:register'));
199  },
200
201  registerCanIUseTag: function() {
202    // Get responsive height passed from iframe.
203    window.addEventListener('message', ({ data }) => {
204      if (typeof data === 'string' && data.includes('ciu_embed')) {
205        const featureID = data.split(':')[1];
206        const height = data.split(':')[2];
207        document.querySelector(`iframe[data-feature=${featureID}]`).style.height = parseInt(height, 10) + 5 + 'px';
208      }
209    }, false);
210  },
211
212  registerActiveMenuItem: function() {
213    document.querySelectorAll('.menu-item a[href]').forEach(target => {
214      const isSamePath = target.pathname === location.pathname || target.pathname === location.pathname.replace('index.html', '');
215      const isSubPath = !CONFIG.root.startsWith(target.pathname) && location.pathname.startsWith(target.pathname);
216      target.classList.toggle('menu-item-active', target.hostname === location.hostname && (isSamePath || isSubPath));
217    });
218  },
219
220  registerLangSelect: function() {
221    const selects = document.querySelectorAll('.lang-select');
222    selects.forEach(sel => {
223      sel.value = CONFIG.page.lang;
224      sel.addEventListener('change', () => {
225        const target = sel.options[sel.selectedIndex];
226        document.querySelectorAll('.lang-select-label span').forEach(span => {
227          span.innerText = target.text;
228        });
229        // Disable Pjax to force refresh translation of menu item
230        window.location.href = target.dataset.href;
231      });
232    });
233  },
234
235  registerSidebarTOC: function() {
236    this.sections = [...document.querySelectorAll('.post-toc li a.nav-link')].map(element => {
237      const target = document.getElementById(decodeURI(element.getAttribute('href')).replace('#', ''));
238      // TOC item animation navigate.
239      element.addEventListener('click', event => {
240        event.preventDefault();
241        const offset = target.getBoundingClientRect().top + window.scrollY;
242        window.anime({
243          targets  : document.scrollingElement,
244          duration : 500,
245          easing   : 'linear',
246          scrollTop: offset,
247          complete : () => {
248            history.pushState(null, document.title, element.href);
249          }
250        });
251      });
252      return target;
253    });
254  },
255
256  registerPostReward: function() {
257    const button = document.querySelector('.reward-container button');
258    if (!button) return;
259    button.addEventListener('click', () => {
260      document.querySelector('.post-reward').classList.toggle('active');
261    });
262  },
263
264  activateNavByIndex: function(index) {
265    const target = document.querySelectorAll('.post-toc li a.nav-link')[index];
266    if (!target || target.classList.contains('active-current')) return;
267
268    document.querySelectorAll('.post-toc .active').forEach(element => {
269      element.classList.remove('active', 'active-current');
270    });
271    target.classList.add('active', 'active-current');
272    let parent = target.parentNode;
273    while (!parent.matches('.post-toc')) {
274      if (parent.matches('li')) parent.classList.add('active');
275      parent = parent.parentNode;
276    }
277    // Scrolling to center active TOC element if TOC content is taller then viewport.
278    const tocElement = document.querySelector(CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini' ? '.sidebar-panel-container' : '.sidebar');
279    if (!document.querySelector('.sidebar-toc-active')) return;
280    window.anime({
281      targets  : tocElement,
282      duration : 200,
283      easing   : 'linear',
284      scrollTop: tocElement.scrollTop - (tocElement.offsetHeight / 2) + target.getBoundingClientRect().top - tocElement.getBoundingClientRect().top
285    });
286  },
287
288  updateSidebarPosition: function() {
289    if (window.innerWidth < 1200 || CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini') return;
290    // Expand sidebar on post detail page by default, when post has a toc.
291    const hasTOC = document.querySelector('.post-toc');
292    let display = CONFIG.page.sidebar;
293    if (typeof display !== 'boolean') {
294      // There's no definition sidebar in the page front-matter.
295      display = CONFIG.sidebar.display === 'always' || (CONFIG.sidebar.display === 'post' && hasTOC);
296    }
297    if (display) {
298      window.dispatchEvent(new Event('sidebar:show'));
299    }
300  },
301
302  activateSidebarPanel: function(index) {
303    const duration = 200;
304    const sidebar = document.querySelector('.sidebar-inner');
305    const panel = document.querySelector('.sidebar-panel-container');
306    const activeClassName = ['sidebar-toc-active', 'sidebar-overview-active'];
307
308    if (sidebar.classList.contains(activeClassName[index])) return;
309
310    window.anime({
311      duration,
312      targets   : panel,
313      easing    : 'linear',
314      opacity   : 0,
315      translateY: [0, -20],
316      complete  : () => {
317        // Prevent adding TOC to Overview if Overview was selected when close & open sidebar.
318        sidebar.classList.replace(activeClassName[1 - index], activeClassName[index]);
319        window.anime({
320          duration,
321          targets   : panel,
322          easing    : 'linear',
323          opacity   : [0, 1],
324          translateY: [-20, 0]
325        });
326      }
327    });
328  },
329
330  getScript: function(src, options = {}, legacyCondition) {
331    if (typeof options === 'function') {
332      return this.getScript(src, {
333        condition: legacyCondition
334      }).then(options);
335    }
336    const {
337      condition = false,
338      attributes: {
339        id = '',
340        async = false,
341        defer = false,
342        crossOrigin = '',
343        dataset = {},
344        ...otherAttributes
345      } = {},
346      parentNode = null
347    } = options;
348    return new Promise((resolve, reject) => {
349      if (condition) {
350        resolve();
351      } else {
352        const script = document.createElement('script');
353
354        if (id) script.id = id;
355        if (crossOrigin) script.crossOrigin = crossOrigin;
356        script.async = async;
357        script.defer = defer;
358        Object.assign(script.dataset, dataset);
359        Object.entries(otherAttributes).forEach(([name, value]) => {
360          script.setAttribute(name, String(value));
361        });
362
363        script.onload = resolve;
364        script.onerror = reject;
365
366        if (typeof src === 'object') {
367          const { url, integrity } = src;
368          script.src = url;
369          if (integrity) {
370            script.integrity = integrity;
371            script.crossOrigin = 'anonymous';
372          }
373        } else {
374          script.src = src;
375        }
376        (parentNode || document.head).appendChild(script);
377      }
378    });
379  },
380
381  loadComments: function(selector, legacyCallback) {
382    if (legacyCallback) {
383      return this.loadComments(selector).then(legacyCallback);
384    }
385    return new Promise(resolve => {
386      const element = document.querySelector(selector);
387      if (!CONFIG.comments.lazyload || !element) {
388        resolve();
389        return;
390      }
391      const intersectionObserver = new IntersectionObserver((entries, observer) => {
392        const entry = entries[0];
393        if (!entry.isIntersecting) return;
394
395        resolve();
396        observer.disconnect();
397      });
398      intersectionObserver.observe(element);
399    });
400  }
401};

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.