import { Controller } from '@hotwired/stimulus'; /* * Sidebar table of contents on a storyline chapter page. * * - the caret unfolds/folds a chapter's section list * - links for the chapter being read (target "link", href="#section-…") * jump inside the split view's scrolling pane * - the section currently at the top of the pane is highlighted */ export default class extends Controller { static targets = ['link']; connect() { this.pane = this.element.closest('.storyline-split-main'); this.onScroll = () => { if (this.frame) return; this.frame = requestAnimationFrame(() => { this.frame = null; this.highlight(); }); }; (this.pane || window).addEventListener('scroll', this.onScroll, { passive: true }); window.addEventListener('scroll', this.onScroll, { passive: true }); this.highlight(); } disconnect() { (this.pane || window).removeEventListener('scroll', this.onScroll); window.removeEventListener('scroll', this.onScroll); } fold(event) { const item = event.currentTarget.closest('.storyline-toc-item'); const open = item.classList.toggle('is-open'); event.currentTarget.setAttribute('aria-expanded', open ? 'true' : 'false'); } jump(event) { const id = event.currentTarget.getAttribute('href').slice(1); const el = document.getElementById(id); if (!el) return; event.preventDefault(); el.scrollIntoView({ block: 'start' }); history.replaceState(null, '', `#${id}`); } linkTargetConnected(link) { link.addEventListener('click', (e) => this.jump({ currentTarget: link, preventDefault: () => e.preventDefault() })); } highlight() { if (!this.hasLinkTarget) return; // In the split view the pane scrolls; on small screens the window does. const paneScrolls = this.pane && getComputedStyle(this.pane).overflowY !== 'visible'; const top = paneScrolls ? this.pane.getBoundingClientRect().top : 0; let current = null; for (const link of this.linkTargets) { const el = document.getElementById(link.getAttribute('href').slice(1)); if (el && el.getBoundingClientRect().top - top <= 90) current = link; } this.linkTargets.forEach((l) => l.classList.toggle('is-current', l === current)); if (current && this.pane) { const list = current.closest('.storyline-toc-sections'); if (list && list.scrollHeight > list.clientHeight) { const c = current.getBoundingClientRect(); const l = list.getBoundingClientRect(); if (c.top < l.top || c.bottom > l.bottom) current.scrollIntoView({ block: 'nearest' }); } } } }