UNPKG

@xiee/utils

Version:

Miscellaneous tools and utilities to manipulate HTML pages

194 lines (192 loc) 7.3 kB
(function(d) { let p = d.body; // container of slides; assume <body> for now const s1 = ':scope > hr:not([class])', s2 = ':scope > h2'; // find a container that has at least n "slides" function findContainer(s, n = 1) { if (p.querySelectorAll(s).length >= n) return true; // if body doesn't contain headings or <hr>s, look into children for (let i = 0; i < p.children.length; i++) { if (p.children[i].querySelectorAll(s).length >= n) { p = p.children[i]; break; } } return false; } function newEl(tag, cls) { const el = d.createElement(tag); if (cls) el.className = cls; return el; } if (!findContainer(s1, 3)) { // if not enough <hr>s found in children; look for <h2> instead if (p.tagName === 'BODY') { // not enough h2 found, this page is not appropriate for slides if (!findContainer(s2) && p.tagName === 'BODY') return; p.querySelectorAll(s2).forEach(h2 => h2.before(newEl('hr'))); } } p.classList.add('slide-container'); // add 'slide' class to the frontmatter/abstract divs and toc ['.frontmatter', '.abstract', '#TOC'].forEach(sel => { const el = d.body.querySelector(sel); if (!el) return; if (sel === '.frontmatter') { el.classList.add('slide'); } else { const s = newSlide(); el.before(s); s.append(el); } }); function newSlide(s) { return (s?.innerHTML === '') ? s : newEl('div', 'slide'); } function isSep(el) { return el.tagName === 'HR' && el.attributes.length === 0; } let el = p.firstElementChild; isSep(el) && el.remove(); el = p.firstElementChild; if (!el) return; let s = newSlide(); el.before(s); while (true) { let el = s.nextSibling; if (!el) break; // remove slide separators (<hr>) and create new slide if (isSep(el)) { s = newSlide(s); el.before(s); el.remove(); } else if (el.classList?.contains('slide')) { s = newSlide(s); el.after(s); } else { s.append(el); } } function setAttr(el, attr) { const m = newEl('div'); m.innerHTML = `<div ${attr}></div>`; const attrs = m.firstElementChild.attributes; for (const a of attrs) { el.setAttribute(a.name, a.value); } m.remove(); } function reveal(el) { setTimeout(() => el?.scrollIntoView(), 100); } const dE = d.documentElement, dC = d.body.classList; const slides = d.querySelectorAll('div.slide'), N = slides.length, tm = d.querySelector('span.timer'), fn = d.querySelector('.footnotes'); slides.forEach((s, i) => { // slide header, main body, and footer const header = newEl('div', 'header'), main = newEl('div', 'main'), footer = newEl('div', 'footer'); main.append(...s.childNodes); s.append(main); s.insertAdjacentElement('afterbegin', header); s.insertAdjacentElement('beforeend', footer); // append footnotes if (fn) s.querySelectorAll('.footnote-ref > a[href^="#fn"]').forEach(a => { const li = fn.querySelector('li' + a.getAttribute('href')); if (!li) return; let f = s.querySelector('section.footnotes'); if (!f || f.contains(li)) { f = newEl('section', 'footnotes'); footer.before(f); } f.append(li); li.firstElementChild?.insertAdjacentHTML('afterbegin', `[${a.innerHTML}] `); li.outerHTML = li.innerHTML; }); // add a timer footer.append(tm ? tm.cloneNode() : newEl('span', 'timer')); // add page numbers const n = newEl('span', 'page-number'); n.innerText = i + 1 + '/' + N; n.onclick = e => location.hash = i + 1; footer.append(n); // apply slide attributes in <!--# --> for (const node of main.childNodes) { if (node.nodeType !== Node.COMMENT_NODE) continue; let t = node.textContent; if (!/^#/.test(t)) continue; t = t.replace(/^#/, ''); const r = /[\s\n]class="([^"]+)"/, m = t.match(r); if (m) { t = t.replace(r, '').trim(); s.className += ' ' + m[1]; } if (t) setAttr(s, t); break; } s.addEventListener('click', e => { (e.altKey || e.ctrlKey) && (toggleView(e), reveal(e.target)); }); }); [...d.querySelectorAll('a.footnote-backref'), fn, tm].forEach(el => el?.remove()); const tms = d.querySelectorAll('span.timer'), t1 = 1000 * tms[0].dataset.total; let t0; function startTimers() { t0 = new Date(); setInterval(setTimers, 1000); } function setTimers() { if (!dC.contains('slide-mode')) return; // set timer only in slide mode let t = (new Date() - t0); if (t1) t = t1 - t; const t2 = new Date(Math.abs(t)).toISOString().substr(11, 8).replace(/^00:/, ''); tms.forEach(el => { el.innerText = t2; if (t < 0) el.style.opacity = Math.ceiling(t/1000) % 2; }); } function toggleView(e) { (dC.toggle('overview') ? dC.remove('slide-mode') : setScale(e)); } // press f for fullscreen mode d.addEventListener('keyup', e => { if (e.target !== d.body) return; e.key === 'f' && dE.requestFullscreen(); e.key === 'o' && toggleView(e); e.key === 'm' && dC.toggle('mirrored'); sessionStorage.setItem('body-class', d.body.className); }); // start timer and set scale on fullscreen d.onfullscreenchange = e => { d.fullscreenElement && (!t0 && startTimers(), setScale(e)); } tms.forEach(el => el.addEventListener('click', e => startTimers())); // measure the height of an empty slide let H = -1; function slideHeight() { if (H >= 0) return H; const s = newEl('div', 'slide'); p.querySelector('.slide:last-of-type').after(s); H = s.offsetHeight; s.remove() return H; } // scale slides according to window height (baseline: 900px) const sty = newEl('style'); sty.setAttribute('type', 'text/css'); d.head.append(sty); // default height/width ratio from screen size sty.innerHTML = `:root{--slide-ratio:${screen.height / screen.width}}`; // read --slide-ratio in case users have set it in their CSS const ratio = +getComputedStyle(dE).getPropertyValue('--slide-ratio'); function setScale(e) { // navigate to a slide indicated by the hash if provided e.type === 'load' && reveal(slides[location.hash.replace(/^#/, '') - 1]); if (dC.contains('overview')) return; let h = window.innerHeight, p = h / 900, r2 = h / window.innerWidth, full = d.fullscreenElement; // add slide mode if there's enough window width, and remove it in case of scrollbar dC.toggle('slide-mode', full || r2 <= ratio) && (!full && (dE.scrollWidth > dE.offsetWidth)) && dC.remove('slide-mode'); sty.innerHTML = `:root{--slide-ratio:${ratio};--slide-scale:${p};--slide-top:${(p - 1)/2 * d.body.scrollHeight + 'px'};}`; // add spacers with enough height on load !d.querySelector('.spacer.fade') && dC.contains('slide-mode') && slides.forEach(s => { const sp = newEl('div', 'spacer fade'), h = s.offsetHeight, h2 = slideHeight(); s.append(sp); if (h <= h2) return; sp.style.height = (h2 - h % h2) * p + 'px'; }); } ['load', 'resize'].forEach(evt => window.addEventListener(evt, setScale)); // restore previously saved body class const bc = sessionStorage.getItem('body-class'); if (bc) d.body.className += ' ' + bc; })(document);