PageSourceSearch

https://woodenrobot.me/js/utils.js

js woodenrobot.me collected 2026-10-09 22:09:20 UTC 15,976 bytes, 415 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
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.