diff --git a/website/templates/site.js b/website/templates/site.js index 090e90cc..d639c7fb 100644 --- a/website/templates/site.js +++ b/website/templates/site.js @@ -28,6 +28,169 @@ location.replace('#/notes/' + encodeURIComponent(route) + (hash.includes('?') ? '?' + hash.split('?').slice(1).join('?') : '')); } } + var stopSpy = null; + // 高频小节:构建网站时把标题和目录中的 🔥 换成了空的 span(hot-1 为一个火焰,hot-2 为两个),这里画出小火焰,侧边栏中也显示同样的标记 + var FLAME = ''; + function hotLevel(node) { + return node.classList.contains('hot-2') ? 2 : 1; + } + function fillHot(node, level) { + node.title = level === 2 ? '面试中最常问' : '面试中常被问到'; + node.setAttribute('role', 'img'); + node.setAttribute('aria-label', level === 2 ? '高频' : '常问'); + node.innerHTML = level === 2 ? FLAME + FLAME : FLAME; + } + function hotBadge(level) { + var badge = document.createElement('span'); + badge.className = 'hot hot-' + level; + fillHot(badge, level); + return badge; + } + function drawHot(section) { + section.querySelectorAll('.hot:empty').forEach(function (node) { fillHot(node, hotLevel(node)); }); + } + + // 步骤动图:GitHub 上直接播放 GIF;网站上用同一个 GIF 的各帧(构建时拆出)做播放器,可以暂停、逐步查看 + function stepAnimations(section) { + var map = window.CS_NOTES_ANIMATIONS || {}; + section.querySelectorAll('img').forEach(function (img) { + if (img.dataset.anim) return; + var key; + try { key = decodeURI(new URL(img.getAttribute('src'), location.href).pathname); } catch (error) { return; } + var anim = map[key]; + if (!anim) return; + img.dataset.anim = '1'; + anim.frames.forEach(function (src) { new Image().src = src; }); + var index = 0, timer = null, playing = false, visible = false; + var bar = document.createElement('div'); + bar.className = 'anim-bar'; + bar.innerHTML = '' + + '' + + ''; + var playButton = bar.querySelector('[data-play]'), count = bar.querySelector('.anim-count'); + function show(i) { + index = (i + anim.frames.length) % anim.frames.length; + img.src = anim.frames[index]; + count.textContent = (index + 1) + ' / ' + anim.frames.length; + } + function schedule() { + clearTimeout(timer); + if (playing && visible) timer = setTimeout(function () { show(index + 1); schedule(); }, anim.durations[index]); + } + function setPlaying(value) { + playing = value; + playButton.textContent = value ? '暂停' : '播放'; + playButton.setAttribute('aria-label', playButton.textContent); + schedule(); + } + bar.addEventListener('click', function (event) { + var button = event.target.closest('button'); + if (!button) return; + if (button.hasAttribute('data-play')) { setPlaying(!playing); return; } + setPlaying(false); + show(index + Number(button.dataset.step)); + }); + img.after(bar); + show(0); + setPlaying(!window.matchMedia('(prefers-reduced-motion: reduce)').matches); + if ('IntersectionObserver' in window) { + new IntersectionObserver(function (entries) { + visible = entries[entries.length - 1].isIntersecting; + schedule(); + }, { threshold: 0.25 }).observe(img); + } else { + visible = true; + schedule(); + } + }); + } + + function articleToc(vm) { + // 侧边栏中显示当前文章的二、三级标题:文章在侧边栏中时挂在它下面,否则放在侧边栏顶部 + var nav = document.querySelector('.sidebar-nav'); + var section = document.querySelector('.markdown-section'); + if (stopSpy) { stopSpy(); stopSpy = null; } + if (!nav || !section) return; + nav.querySelectorAll('.page-toc, .page-toc-block').forEach(function (node) { node.remove(); }); + // Docsify 按前缀匹配当前项:“首页”的 #/ 会匹配所有文章,地址带 ?id= 时又匹配不到,这里按完整路径重新标记 + var target = decode('#' + vm.route.path), active = null; + nav.querySelectorAll('li > a').forEach(function (link) { + var same = !active && decode((link.getAttribute('href') || '').split('?')[0]) === target; + link.parentNode.classList.toggle('active', same); + if (same) active = link.parentNode; + }); + if (vm.route.path === '/') return; + drawHot(section); + var heads = Array.from(section.querySelectorAll('h2[id], h3[id]')); + if (heads.length < 2) return; + var route = '#' + encodedRoute(vm.route.path) + '?id='; + var root = document.createElement('ul'); + root.className = 'page-toc'; + var group = null, flat = false, links = {}; + heads.forEach(function (heading) { + var title = heading.textContent.trim(); + if (heading.tagName === 'H3' && (flat || !group)) return; + var li = document.createElement('li'); + var a = document.createElement('a'); + a.href = route + encodeURIComponent(heading.id); + a.textContent = a.title = title; + li.appendChild(a); + var mark = heading.querySelector('.hot'); + if (mark) a.appendChild(hotBadge(hotLevel(mark))); + links[heading.id] = li; + if (heading.tagName === 'H2') { + root.appendChild(li); + group = li; + flat = /^(附录|参考资料)/.test(title); + } else { + var sub = group.querySelector(':scope > ul'); + if (!sub) { + sub = document.createElement('ul'); + group.appendChild(sub); + group.classList.add('has-sub'); + } + sub.appendChild(li); + } + }); + if (active) { + active.appendChild(root); + } else { + var block = document.createElement('div'); + block.className = 'page-toc-block'; + var name = document.createElement('p'); + name.className = 'page-toc-title'; + name.textContent = (section.querySelector('h1') || {}).textContent || decode(vm.route.path.split('/').pop()); + block.appendChild(name); + block.appendChild(root); + nav.insertBefore(block, nav.firstChild); + } + var sidebar = document.querySelector('.sidebar'); + var pending = false; + function update() { + pending = false; + var current = null; + for (var i = 0; i < heads.length; i++) { + if (heads[i].getBoundingClientRect().top > 90) break; + if (links[heads[i].id]) current = links[heads[i].id]; + } + root.querySelectorAll('li.current, li.open').forEach(function (li) { li.classList.remove('current', 'open'); }); + if (!current) return; + current.classList.add('current'); + var top = current.parentNode === root ? current : current.parentNode.parentNode; + top.classList.add('open'); + if (sidebar) { + var offset = current.getBoundingClientRect().top - sidebar.getBoundingClientRect().top; + if (offset < 60 || offset > sidebar.clientHeight - 60) sidebar.scrollTop += offset - sidebar.clientHeight / 3; + } + } + function onScroll() { + if (!pending) { pending = true; requestAnimationFrame(update); } + } + window.addEventListener('scroll', onScroll, { passive: true }); + stopSpy = function () { window.removeEventListener('scroll', onScroll); }; + update(); + } preserveLegacyLinks(); window.addEventListener('hashchange', preserveLegacyLinks); window.$docsify = { @@ -61,6 +224,8 @@ var heading = document.querySelector('.markdown-section h1, .markdown-section h2'); document.title = (vm.route.path === '/' || !heading ? 'CS-Notes' : heading.textContent.trim() + ' · CS-Notes') + ' · CyC2018'; configureImages(document.querySelector('.markdown-section'), vm.route.path === '/'); + stepAnimations(document.querySelector('.markdown-section')); + articleToc(vm); document.querySelectorAll('.article-toc').forEach(function (toc) { toc.open = window.matchMedia('(min-width: 769px)').matches; });