import { Controller } from '@hotwired/stimulus'; /* * The docked "Chapter notes" panel on a storyline chapter page. * *
* ...chapter... *
*
* * ... *
*
* * Drag the handle to resize; click the header to collapse. Both the pixel * height and the collapsed flag persist per browser (localStorage). */ const H_KEY = 'dmtools.chapterNotesHeight'; const C_KEY = 'dmtools.chapterNotesCollapsed'; const MIN_PX = 44; const MIN_TOP_PX = 140; export default class extends Controller { static targets = ['dock', 'handle', 'toggle']; connect() { this.onMove = this.onMove.bind(this); this.endResize = this.endResize.bind(this); let stored = null; try { stored = localStorage.getItem(H_KEY); } catch (e) { /* private mode */ } if (stored) { this.element.style.setProperty('--notes-h', `${this.clamp(parseInt(stored, 10))}px`); } let collapsed = false; try { collapsed = localStorage.getItem(C_KEY) === '1'; } catch (e) { /* ignore */ } this.setCollapsed(collapsed); } disconnect() { this.endResize(); } toggle() { this.setCollapsed(!this.dockTarget.classList.contains('is-collapsed')); } setCollapsed(on) { this.dockTarget.classList.toggle('is-collapsed', on); if (this.hasToggleTarget) { this.toggleTarget.setAttribute('aria-expanded', on ? 'false' : 'true'); } try { localStorage.setItem(C_KEY, on ? '1' : '0'); } catch (e) { /* ignore */ } } startResize(event) { if (this.dockTarget.classList.contains('is-collapsed')) return; event.preventDefault(); this.resizing = true; this.handleTarget.setPointerCapture?.(event.pointerId); document.body.style.userSelect = 'none'; document.body.style.cursor = 'row-resize'; window.addEventListener('pointermove', this.onMove); window.addEventListener('pointerup', this.endResize); window.addEventListener('pointercancel', this.endResize); } onMove(event) { if (!this.resizing) return; const rect = this.element.getBoundingClientRect(); const px = this.clamp(rect.bottom - event.clientY); this.element.style.setProperty('--notes-h', `${px}px`); } endResize() { window.removeEventListener('pointermove', this.onMove); window.removeEventListener('pointerup', this.endResize); window.removeEventListener('pointercancel', this.endResize); if (!this.resizing) return; this.resizing = false; document.body.style.userSelect = ''; document.body.style.cursor = ''; const current = this.element.style.getPropertyValue('--notes-h'); if (current.endsWith('px')) { try { localStorage.setItem(H_KEY, parseInt(current, 10)); } catch (e) { /* ignore */ } } } clamp(px) { if (!Number.isFinite(px)) return MIN_PX; const max = Math.max(MIN_PX, this.element.getBoundingClientRect().height - MIN_TOP_PX); return Math.min(Math.max(px, MIN_PX), max); } }