(() => { const form = document.querySelector('[data-story-point-form]'); if (!form) return; const book = form.querySelector('[data-point-book]'); const chapter = form.querySelector('[data-point-chapter]'); const scene = form.querySelector('[data-point-scene]'); const chapters = [...chapter.options].map(x => x.cloneNode(true)); const scenes = [...scene.options].map(x => x.cloneNode(true)); const snapshot = document.querySelector('[data-point-snapshot]'); const feedback = document.querySelector('[data-point-feedback]'); let pending; let request = 0; function filter(select, options, key, value) { const previous = select.value; select.replaceChildren(...options.filter(x => x.dataset[key] === value).map(x => x.cloneNode(true))); if ([...select.options].some(x => x.value === previous)) select.value = previous; select.disabled = !select.options.length; } function filterScenes() { filter(scene, scenes, 'chapter', chapter.value); } function filterChapters() { filter(chapter, chapters, 'book', book.value); filterScenes(); } async function load(push = true) { const current = ++request; pending?.abort(); if (!scene.value) { snapshot.replaceChildren(); feedback.textContent = 'No scenes in this selection.'; return; } pending = new AbortController(); const url = new URL(form.action, location.href); url.search = new URLSearchParams(new FormData(form)).toString(); feedback.textContent = 'Loading story state…'; snapshot.replaceChildren(); snapshot.setAttribute('aria-busy', 'true'); try { const response = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' }, signal: pending.signal }); if (response.redirected) { location.assign(response.url); return; } if (!response.ok) throw new Error('load'); const html = await response.text(); if (current !== request) return; snapshot.innerHTML = html; if (push) history.pushState({}, '', url); feedback.textContent = 'Story state updated.'; } catch (error) { if (error.name !== 'AbortError' && current === request) { snapshot.replaceChildren(); feedback.textContent = 'Story state could not be loaded. Use Show state to retry.'; } } finally { if (current === request) snapshot.removeAttribute('aria-busy'); } } book.addEventListener('change', () => { filterChapters(); load(); }); chapter.addEventListener('change', () => { filterScenes(); load(); }); scene.addEventListener('change', () => load()); form.addEventListener('submit', event => { event.preventDefault(); load(); }); window.addEventListener('popstate', () => { const id = new URL(location.href).searchParams.get('sceneId'); const chosenScene = scenes.find(x => x.value === id); const chosenChapter = chapters.find(x => x.value === chosenScene?.dataset.chapter); if (!chosenScene || !chosenChapter) { location.reload(); return; } book.value = chosenChapter.dataset.book; filterChapters(); chapter.value = chosenChapter.value; filterScenes(); scene.value = id; load(false); }); filterChapters(); })();