utils.js 14 KB

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