utils.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. /* global NexT, CONFIG */
  2. HTMLElement.prototype.wrap = function(wrapper) {
  3. this.parentNode.insertBefore(wrapper, this);
  4. this.parentNode.removeChild(this);
  5. wrapper.appendChild(this);
  6. };
  7. NexT.utils = {
  8. /**
  9. * Wrap images with fancybox.
  10. */
  11. wrapImageWithFancyBox: function() {
  12. document.querySelectorAll('.post-body :not(a) > img, .post-body > img').forEach(element => {
  13. var $image = $(element);
  14. var imageLink = $image.attr('data-src') || $image.attr('src');
  15. var $imageWrapLink = $image.wrap(`<a class="fancybox fancybox.image" href="${imageLink}" itemscope itemtype="http://schema.org/ImageObject" itemprop="url"></a>`).parent('a');
  16. if ($image.is('.post-gallery img')) {
  17. $imageWrapLink.attr('data-fancybox', 'gallery').attr('rel', 'gallery');
  18. } else if ($image.is('.group-picture img')) {
  19. $imageWrapLink.attr('data-fancybox', 'group').attr('rel', 'group');
  20. } else {
  21. $imageWrapLink.attr('data-fancybox', 'default').attr('rel', 'default');
  22. }
  23. var imageTitle = $image.attr('title') || $image.attr('alt');
  24. if (imageTitle) {
  25. $imageWrapLink.append(`<p class="image-caption">${imageTitle}</p>`);
  26. // Make sure img title tag will show correctly in fancybox
  27. $imageWrapLink.attr('title', imageTitle).attr('data-caption', imageTitle);
  28. }
  29. });
  30. $.fancybox.defaults.hash = false;
  31. $('.fancybox').fancybox({
  32. loop : true,
  33. helpers: {
  34. overlay: {
  35. locked: false
  36. }
  37. }
  38. });
  39. },
  40. registerExtURL: function() {
  41. document.querySelectorAll('span.exturl').forEach(element => {
  42. let link = document.createElement('a');
  43. // https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings
  44. link.href = decodeURIComponent(atob(element.dataset.url).split('').map(c => {
  45. return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
  46. }).join(''));
  47. link.rel = 'noopener external nofollow noreferrer';
  48. link.target = '_blank';
  49. link.className = element.className;
  50. link.title = element.title;
  51. link.innerHTML = element.innerHTML;
  52. element.parentNode.replaceChild(link, element);
  53. });
  54. },
  55. /**
  56. * One-click copy code support.
  57. */
  58. registerCopyCode: function() {
  59. document.querySelectorAll('figure.highlight').forEach(element => {
  60. const box = document.createElement('div');
  61. element.wrap(box);
  62. box.classList.add('highlight-container');
  63. box.insertAdjacentHTML('beforeend', '<div class="copy-btn"><i class="fa fa-clipboard fa-fw"></i></div>');
  64. var button = element.parentNode.querySelector('.copy-btn');
  65. button.addEventListener('click', event => {
  66. var target = event.currentTarget;
  67. var code = [...target.parentNode.querySelectorAll('.code .line')].map(line => line.innerText).join('\n');
  68. var ta = document.createElement('textarea');
  69. ta.style.top = window.scrollY + 'px'; // Prevent page scrolling
  70. ta.style.position = 'absolute';
  71. ta.style.opacity = '0';
  72. ta.readOnly = true;
  73. ta.value = code;
  74. document.body.append(ta);
  75. const selection = document.getSelection();
  76. const selected = selection.rangeCount > 0 ? selection.getRangeAt(0) : false;
  77. ta.select();
  78. ta.setSelectionRange(0, code.length);
  79. ta.readOnly = false;
  80. var result = document.execCommand('copy');
  81. if (CONFIG.copycode.show_result) {
  82. target.querySelector('i').className = result ? 'fa fa-check fa-fw' : 'fa fa-times fa-fw';
  83. }
  84. ta.blur(); // For iOS
  85. target.blur();
  86. if (selected) {
  87. selection.removeAllRanges();
  88. selection.addRange(selected);
  89. }
  90. document.body.removeChild(ta);
  91. });
  92. button.addEventListener('mouseleave', event => {
  93. setTimeout(() => {
  94. event.target.querySelector('i').className = 'fa fa-clipboard fa-fw';
  95. }, 300);
  96. });
  97. });
  98. },
  99. wrapTableWithBox: function() {
  100. document.querySelectorAll('table').forEach(element => {
  101. const box = document.createElement('div');
  102. box.className = 'table-container';
  103. element.wrap(box);
  104. });
  105. },
  106. registerVideoIframe: function() {
  107. document.querySelectorAll('iframe').forEach(element => {
  108. const supported = [
  109. 'www.youtube.com',
  110. 'player.vimeo.com',
  111. 'player.youku.com',
  112. 'player.bilibili.com',
  113. 'www.tudou.com'
  114. ].some(host => element.src.includes(host));
  115. if (supported && !element.parentNode.matches('.video-container')) {
  116. const box = document.createElement('div');
  117. box.className = 'video-container';
  118. element.wrap(box);
  119. let width = Number(element.width);
  120. let height = Number(element.height);
  121. if (width && height) {
  122. element.parentNode.style.paddingTop = (height / width * 100) + '%';
  123. }
  124. }
  125. });
  126. },
  127. registerScrollPercent: function() {
  128. var THRESHOLD = 50;
  129. var backToTop = document.querySelector('.back-to-top');
  130. var readingProgressBar = document.querySelector('.reading-progress-bar');
  131. // For init back to top in sidebar if page was scrolled after page refresh.
  132. window.addEventListener('scroll', () => {
  133. if (backToTop || readingProgressBar) {
  134. var docHeight = document.querySelector('.container').offsetHeight;
  135. var winHeight = window.innerHeight;
  136. var contentVisibilityHeight = docHeight > winHeight ? docHeight - winHeight : document.body.scrollHeight - winHeight;
  137. var scrollPercent = Math.min(100 * window.scrollY / contentVisibilityHeight, 100);
  138. if (backToTop) {
  139. backToTop.classList.toggle('back-to-top-on', window.scrollY > THRESHOLD);
  140. backToTop.querySelector('span').innerText = Math.round(scrollPercent) + '%';
  141. }
  142. if (readingProgressBar) {
  143. readingProgressBar.style.width = scrollPercent.toFixed(2) + '%';
  144. }
  145. }
  146. });
  147. backToTop && backToTop.addEventListener('click', () => {
  148. window.anime({
  149. targets : document.scrollingElement,
  150. duration : 500,
  151. easing : 'linear',
  152. scrollTop: 0
  153. });
  154. });
  155. },
  156. /**
  157. * Tabs tag listener (without twitter bootstrap).
  158. */
  159. registerTabsTag: function() {
  160. // Binding `nav-tabs` & `tab-content` by real time permalink changing.
  161. document.querySelectorAll('.tabs ul.nav-tabs .tab').forEach(element => {
  162. element.addEventListener('click', event => {
  163. event.preventDefault();
  164. var target = event.currentTarget;
  165. // Prevent selected tab to select again.
  166. if (!target.classList.contains('active')) {
  167. // Add & Remove active class on `nav-tabs` & `tab-content`.
  168. [...target.parentNode.children].forEach(element => {
  169. element.classList.remove('active');
  170. });
  171. target.classList.add('active');
  172. var tActive = document.getElementById(target.querySelector('a').getAttribute('href').replace('#', ''));
  173. [...tActive.parentNode.children].forEach(element => {
  174. element.classList.remove('active');
  175. });
  176. tActive.classList.add('active');
  177. // Trigger event
  178. tActive.dispatchEvent(new Event('tabs:click', {
  179. bubbles: true
  180. }));
  181. }
  182. });
  183. });
  184. window.dispatchEvent(new Event('tabs:register'));
  185. },
  186. registerCanIUseTag: function() {
  187. // Get responsive height passed from iframe.
  188. window.addEventListener('message', ({ data }) => {
  189. if ((typeof data === 'string') && data.includes('ciu_embed')) {
  190. var featureID = data.split(':')[1];
  191. var height = data.split(':')[2];
  192. document.querySelector(`iframe[data-feature=${featureID}]`).style.height = parseInt(height, 10) + 5 + 'px';
  193. }
  194. }, false);
  195. },
  196. registerActiveMenuItem: function() {
  197. document.querySelectorAll('.menu-item').forEach(element => {
  198. var target = element.querySelector('a[href]');
  199. if (!target) return;
  200. var isSamePath = target.pathname === location.pathname || target.pathname === location.pathname.replace('index.html', '');
  201. var isSubPath = !CONFIG.root.startsWith(target.pathname) && location.pathname.startsWith(target.pathname);
  202. element.classList.toggle('menu-item-active', target.hostname === location.hostname && (isSamePath || isSubPath));
  203. });
  204. },
  205. registerLangSelect: function() {
  206. let selects = document.querySelectorAll('.lang-select');
  207. selects.forEach(sel => {
  208. sel.value = CONFIG.page.lang;
  209. sel.addEventListener('change', () => {
  210. let target = sel.options[sel.selectedIndex];
  211. document.querySelectorAll('.lang-select-label span').forEach(span => span.innerText = target.text);
  212. let url = target.dataset.href;
  213. window.pjax ? window.pjax.loadUrl(url) : window.location.href = url;
  214. });
  215. });
  216. },
  217. registerSidebarTOC: function() {
  218. const navItems = document.querySelectorAll('.post-toc li');
  219. const sections = [...navItems].map(element => {
  220. var link = element.querySelector('a.nav-link');
  221. var target = document.getElementById(decodeURI(link.getAttribute('href')).replace('#', ''));
  222. // TOC item animation navigate.
  223. link.addEventListener('click', event => {
  224. event.preventDefault();
  225. var offset = target.getBoundingClientRect().top + window.scrollY;
  226. window.anime({
  227. targets : document.scrollingElement,
  228. duration : 500,
  229. easing : 'linear',
  230. scrollTop: offset + 10
  231. });
  232. });
  233. return target;
  234. });
  235. var tocElement = document.querySelector('.post-toc-wrap');
  236. function activateNavByIndex(target) {
  237. if (target.classList.contains('active-current')) return;
  238. document.querySelectorAll('.post-toc .active').forEach(element => {
  239. element.classList.remove('active', 'active-current');
  240. });
  241. target.classList.add('active', 'active-current');
  242. var parent = target.parentNode;
  243. while (!parent.matches('.post-toc')) {
  244. if (parent.matches('li')) parent.classList.add('active');
  245. parent = parent.parentNode;
  246. }
  247. // Scrolling to center active TOC element if TOC content is taller then viewport.
  248. window.anime({
  249. targets : tocElement,
  250. duration : 200,
  251. easing : 'linear',
  252. scrollTop: tocElement.scrollTop - (tocElement.offsetHeight / 2) + target.getBoundingClientRect().top - tocElement.getBoundingClientRect().top
  253. });
  254. }
  255. function findIndex(entries) {
  256. let index = 0;
  257. let entry = entries[index];
  258. if (entry.boundingClientRect.top > 0) {
  259. index = sections.indexOf(entry.target);
  260. return index === 0 ? 0 : index - 1;
  261. }
  262. for (; index < entries.length; index++) {
  263. if (entries[index].boundingClientRect.top <= 0) {
  264. entry = entries[index];
  265. } else {
  266. return sections.indexOf(entry.target);
  267. }
  268. }
  269. return sections.indexOf(entry.target);
  270. }
  271. function createIntersectionObserver(marginTop) {
  272. marginTop = Math.floor(marginTop + 10000);
  273. let intersectionObserver = new IntersectionObserver((entries, observe) => {
  274. let scrollHeight = document.documentElement.scrollHeight + 100;
  275. if (scrollHeight > marginTop) {
  276. observe.disconnect();
  277. createIntersectionObserver(scrollHeight);
  278. return;
  279. }
  280. let index = findIndex(entries);
  281. activateNavByIndex(navItems[index]);
  282. }, {
  283. rootMargin: marginTop + 'px 0px -100% 0px',
  284. threshold : 0
  285. });
  286. sections.forEach(element => {
  287. element && intersectionObserver.observe(element);
  288. });
  289. }
  290. createIntersectionObserver(document.documentElement.scrollHeight);
  291. },
  292. hasMobileUA: function() {
  293. let ua = navigator.userAgent;
  294. let pa = /iPad|iPhone|Android|Opera Mini|BlackBerry|webOS|UCWEB|Blazer|PSP|IEMobile|Symbian/g;
  295. return pa.test(ua);
  296. },
  297. isTablet: function() {
  298. return window.screen.width < 992 && window.screen.width > 767 && this.hasMobileUA();
  299. },
  300. isMobile: function() {
  301. return window.screen.width < 767 && this.hasMobileUA();
  302. },
  303. isDesktop: function() {
  304. return !this.isTablet() && !this.isMobile();
  305. },
  306. supportsPDFs: function() {
  307. let ua = navigator.userAgent;
  308. let isFirefoxWithPDFJS = ua.includes('irefox') && parseInt(ua.split('rv:')[1].split('.')[0], 10) > 18;
  309. let supportsPdfMimeType = typeof navigator.mimeTypes['application/pdf'] !== 'undefined';
  310. let isIOS = /iphone|ipad|ipod/i.test(ua.toLowerCase());
  311. return isFirefoxWithPDFJS || (supportsPdfMimeType && !isIOS);
  312. },
  313. /**
  314. * Init Sidebar & TOC inner dimensions on all pages and for all schemes.
  315. * Need for Sidebar/TOC inner scrolling if content taller then viewport.
  316. */
  317. initSidebarDimension: function() {
  318. var sidebarNav = document.querySelector('.sidebar-nav');
  319. var sidebarNavHeight = sidebarNav.style.display !== 'none' ? sidebarNav.offsetHeight : 0;
  320. var sidebarOffset = CONFIG.sidebar.offset || 12;
  321. var sidebarb2tHeight = CONFIG.back2top.enable && CONFIG.back2top.sidebar ? document.querySelector('.back-to-top').offsetHeight : 0;
  322. var sidebarSchemePadding = (CONFIG.sidebar.padding * 2) + sidebarNavHeight + sidebarb2tHeight;
  323. // Margin of sidebar b2t: -4px -10px -18px, brings a different of 22px.
  324. if (CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini') sidebarSchemePadding += (sidebarOffset * 2) - 22;
  325. // Initialize Sidebar & TOC Height.
  326. var sidebarWrapperHeight = document.body.offsetHeight - sidebarSchemePadding + 'px';
  327. document.querySelector('.site-overview-wrap').style.maxHeight = sidebarWrapperHeight;
  328. document.querySelector('.post-toc-wrap').style.maxHeight = sidebarWrapperHeight;
  329. },
  330. updateSidebarPosition: function() {
  331. var sidebarNav = document.querySelector('.sidebar-nav');
  332. var hasTOC = document.querySelector('.post-toc');
  333. if (hasTOC) {
  334. sidebarNav.style.display = '';
  335. sidebarNav.classList.add('motion-element');
  336. document.querySelector('.sidebar-nav-toc').click();
  337. } else {
  338. sidebarNav.style.display = 'none';
  339. sidebarNav.classList.remove('motion-element');
  340. document.querySelector('.sidebar-nav-overview').click();
  341. }
  342. NexT.utils.initSidebarDimension();
  343. if (!this.isDesktop() || CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini') return;
  344. // Expand sidebar on post detail page by default, when post has a toc.
  345. var display = CONFIG.page.sidebar;
  346. if (typeof display !== 'boolean') {
  347. // There's no definition sidebar in the page front-matter.
  348. display = CONFIG.sidebar.display === 'always' || (CONFIG.sidebar.display === 'post' && hasTOC);
  349. }
  350. if (display) {
  351. window.dispatchEvent(new Event('sidebar:show'));
  352. }
  353. },
  354. getScript: function(url, callback, condition) {
  355. if (condition) {
  356. callback();
  357. } else {
  358. var script = document.createElement('script');
  359. script.onload = script.onreadystatechange = function(_, isAbort) {
  360. if (isAbort || !script.readyState || /loaded|complete/.test(script.readyState)) {
  361. script.onload = script.onreadystatechange = null;
  362. script = undefined;
  363. if (!isAbort && callback) setTimeout(callback, 0);
  364. }
  365. };
  366. script.src = url;
  367. document.head.appendChild(script);
  368. }
  369. },
  370. loadComments: function(element, callback) {
  371. if (!CONFIG.comments.lazyload || !element) {
  372. callback();
  373. return;
  374. }
  375. let intersectionObserver = new IntersectionObserver((entries, observer) => {
  376. let entry = entries[0];
  377. if (entry.isIntersecting) {
  378. callback();
  379. observer.disconnect();
  380. }
  381. });
  382. intersectionObserver.observe(element);
  383. return intersectionObserver;
  384. }
  385. };