diff --git a/PlotLine/wwwroot/js/site.js b/PlotLine/wwwroot/js/site.js index bd65c66..e22835a 100644 --- a/PlotLine/wwwroot/js/site.js +++ b/PlotLine/wwwroot/js/site.js @@ -450,6 +450,7 @@ const scene = payload.scenes[points[0].index]; if (scene?.timelineUrl) { + window.PlotLineTimelineScroll?.save(); window.location.href = scene.timelineUrl; } }); @@ -534,6 +535,15 @@ const collapsedKey = "plotline.timeline.collapsed"; const densityKey = "plotline.timeline.density"; const zoomKey = "plotline.timeline.zoom"; + const scrollKey = "plotline.timeline.pending-scroll"; + const scrollContainerSelector = [ + ".timeline-shell", + ".plot-lane-board", + ".asset-lane-board", + ".character-lane-board", + ".metric-graph-stack", + "[data-timeline-minimap]" + ].join(","); const densityValues = ["compact", "comfortable", "expanded"]; const zoomValues = ["book", "chapter", "scene-detail"]; @@ -548,6 +558,62 @@ const hasCollapsedPreference = localStorage.getItem(collapsedKey) !== null; const collapsed = readSet(collapsedKey); + const getScrollContainers = () => [...root.querySelectorAll(scrollContainerSelector)]; + + const saveScrollPosition = () => { + try { + sessionStorage.setItem(scrollKey, JSON.stringify({ + path: window.location.pathname, + top: window.scrollY, + left: window.scrollX, + workspaceLeft: workspace.scrollLeft, + workspaceTop: workspace.scrollTop, + containers: getScrollContainers().map((container, index) => ({ + index, + left: container.scrollLeft, + top: container.scrollTop + })) + })); + } catch { + // Storage can be unavailable in private or restricted browser modes. + } + }; + + const restoreScrollPosition = () => { + let scrollPosition = null; + try { + scrollPosition = JSON.parse(sessionStorage.getItem(scrollKey) || "null"); + sessionStorage.removeItem(scrollKey); + } catch { + try { + sessionStorage.removeItem(scrollKey); + } catch { + // Ignore storage failures; normal navigation should continue. + } + return; + } + + if (!scrollPosition || scrollPosition.path !== window.location.pathname) { + return; + } + + window.requestAnimationFrame(() => { + workspace.scrollLeft = Number(scrollPosition.workspaceLeft) || 0; + workspace.scrollTop = Number(scrollPosition.workspaceTop) || 0; + const containers = getScrollContainers(); + (scrollPosition.containers || []).forEach((saved) => { + const container = containers[saved.index]; + if (container) { + container.scrollLeft = Number(saved.left) || 0; + container.scrollTop = Number(saved.top) || 0; + } + }); + window.scrollTo(Number(scrollPosition.left) || 0, Number(scrollPosition.top) || 0); + }); + }; + + window.PlotLineTimelineScroll = { save: saveScrollPosition }; + const applyMode = (kind, value, values) => { values.forEach((item) => root.classList.remove(`${kind}-${item}`)); root.classList.add(`${kind}-${value}`); @@ -638,7 +704,38 @@ zoomSelect?.addEventListener("change", () => applyZoom(zoomSelect.value)); applyDensity(localStorage.getItem(densityKey) || "comfortable"); applyZoom(localStorage.getItem(zoomKey) || "chapter"); - window.requestAnimationFrame(() => scrollActiveChapterIntoView("auto")); + const hadPendingScrollRestore = (() => { + try { + return sessionStorage.getItem(scrollKey) !== null; + } catch { + return false; + } + })(); + window.requestAnimationFrame(() => { + scrollActiveChapterIntoView("auto"); + if (hadPendingScrollRestore) { + restoreScrollPosition(); + } + }); + + const saveScrollForSceneSelection = (event) => { + if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) { + return; + } + + const target = event.target instanceof Element ? event.target : null; + const link = target?.closest("a[href]"); + if (!link || !root.contains(link)) { + return; + } + + const url = new URL(link.href, window.location.href); + if ([...url.searchParams.keys()].some((key) => key.toLowerCase() === "selectedsceneid")) { + saveScrollPosition(); + } + }; + + root.addEventListener("click", saveScrollForSceneSelection, { capture: true }); const runSearch = () => { const query = searchInput?.value.trim().toLowerCase() || "";