From 8bab26e003e37dc9b81eeb63aacdd2da1439d12d Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Tue, 24 Mar 2026 19:02:29 -0400 Subject: [PATCH] =?UTF-8?q?scroll=20position=20save=20fix=20attempt=20no.?= =?UTF-8?q?=201=20feat.=20'What=20happens=20next=E2=80=A6=3F'=20text=20at?= =?UTF-8?q?=20bottom=20of=20scroll;=20buffer=20added=20to=20scroll,=20acco?= =?UTF-8?q?unter=20for=20in=20FTs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/functional_tests/test_billboard.py | 13 +++++--- src/static_src/scss/_billboard.scss | 25 ++++++++++++++ .../_partials/_applet-billboard-scroll.html | 33 ++++++++++++++++--- src/templates/core/_partials/_scroll.html | 8 +++++ 4 files changed, 71 insertions(+), 8 deletions(-) diff --git a/src/functional_tests/test_billboard.py b/src/functional_tests/test_billboard.py index 28331da..16fe0e3 100644 --- a/src/functional_tests/test_billboard.py +++ b/src/functional_tests/test_billboard.py @@ -154,15 +154,17 @@ class BillscrollPositionTest(FunctionalTest): ) # 2. Force the element scrollable (CSS not served by StaticLiveServerTestCase), - # set position, and dispatch scroll event to trigger the debounced save - target = 100 + # set position, and dispatch scroll event to trigger the debounced save. + # JS saves scrollTop + clientHeight (bottom-of-viewport); forced height is 150px. + scroll_top = 100 + forced_height = 150 self.browser.execute_script(""" var el = arguments[0]; el.style.overflow = 'auto'; el.style.height = '150px'; el.scrollTop = arguments[1]; el.dispatchEvent(new Event('scroll')); - """, scroll_el, target) + """, scroll_el, scroll_top) # 3. Wait for debounce (800ms) + fetch to complete time.sleep(3) @@ -178,8 +180,11 @@ class BillscrollPositionTest(FunctionalTest): scroll_el = self.wait_for( lambda: self.browser.find_element(By.ID, "id_drama_scroll") ) + buffer_px = self.browser.execute_script( + "return Math.round(parseFloat(getComputedStyle(document.documentElement).fontSize) * 2.5)" + ) restored = int(scroll_el.get_attribute("data-scroll-position")) - self.assertEqual(restored, target) + self.assertEqual(restored, scroll_top + forced_height + buffer_px) class BillboardAppletsTest(FunctionalTest): diff --git a/src/static_src/scss/_billboard.scss b/src/static_src/scss/_billboard.scss index c004b49..f3e406d 100644 --- a/src/static_src/scss/_billboard.scss +++ b/src/static_src/scss/_billboard.scss @@ -57,6 +57,31 @@ body.page-billscroll { flex: 1; min-height: 0; overflow-y: auto; + + .scroll-buffer { + display: flex; + justify-content: center; + align-items: baseline; + padding: 2rem 0 1rem; + opacity: 0.4; + font-size: 0.8rem; + text-transform: uppercase; + + .scroll-buffer-text { + letter-spacing: 0.33em; + } + + .scroll-buffer-dots { + display: inline-flex; + letter-spacing: 0; + + span { + display: inline-block; + width: 0.7em; + text-align: center; + } + } + } } } } diff --git a/src/templates/apps/billboard/_partials/_applet-billboard-scroll.html b/src/templates/apps/billboard/_partials/_applet-billboard-scroll.html index c36b6a8..c051372 100644 --- a/src/templates/apps/billboard/_partials/_applet-billboard-scroll.html +++ b/src/templates/apps/billboard/_partials/_applet-billboard-scroll.html @@ -7,23 +7,48 @@ var scroll = document.getElementById('id_drama_scroll'); if (!scroll) return; - // Restore saved position - scroll.scrollTop = {{ scroll_position }}; + // Push buffer so its top aligns with the bottom of the aperture when all + // events fit within the viewport (no natural scrolling). For longer scrolls + // the buffer top naturally appears at the aperture bottom when the last event + // clears the top of the visible area. + var buffer = scroll.querySelector('.scroll-buffer'); + if (buffer) { + var eventsHeight = scroll.scrollHeight - buffer.offsetHeight; + var gap = scroll.clientHeight - eventsHeight; + if (gap > 0) { + buffer.style.marginTop = gap + 'px'; + } + } - // Debounced save on scroll + // Restore: position stored is bottom-of-viewport; subtract clientHeight to align it + scroll.scrollTop = Math.max(0, {{ scroll_position }} - scroll.clientHeight); + + // Animate "What happens next. . . ?" buffer dots — 4th span shows '?' + var dotsWrap = scroll.querySelector('.scroll-buffer-dots'); + if (dotsWrap) { + var dots = dotsWrap.querySelectorAll('span'); + var n = 0; + setInterval(function() { + dots.forEach(function(d, i) { d.textContent = i < n ? (i === 3 ? '?' : '.') : ''; }); + n = (n + 1) % 5; + }, 400); + } + + // Debounced save on scroll — store bottom-of-viewport so the last-read line is restored var saveTimer; scroll.addEventListener('scroll', function() { clearTimeout(saveTimer); saveTimer = setTimeout(function() { var csrfToken = document.querySelector('[name=csrfmiddlewaretoken]'); var token = csrfToken ? csrfToken.value : ''; + var remPx = parseFloat(getComputedStyle(document.documentElement).fontSize); fetch("{% url 'billboard:save_scroll_position' room.id %}", { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRFToken': token, }, - body: 'position=' + Math.round(scroll.scrollTop), + body: 'position=' + Math.round(scroll.scrollTop + scroll.clientHeight + remPx * 2.5), }); }, 800); }); diff --git a/src/templates/core/_partials/_scroll.html b/src/templates/core/_partials/_scroll.html index 69c788f..38ad7b5 100644 --- a/src/templates/core/_partials/_scroll.html +++ b/src/templates/core/_partials/_scroll.html @@ -13,4 +13,12 @@ {% empty %}

No events yet.

{% endfor %} +