scroll position save fix attempt no. 1 feat. 'What happens next…?' text at bottom of scroll; buffer added to scroll, accounter for in FTs

This commit is contained in:
Disco DeDisco
2026-03-24 19:02:29 -04:00
parent bc78d2c470
commit 8bab26e003
4 changed files with 71 additions and 8 deletions

View File

@@ -154,15 +154,17 @@ class BillscrollPositionTest(FunctionalTest):
) )
# 2. Force the element scrollable (CSS not served by StaticLiveServerTestCase), # 2. Force the element scrollable (CSS not served by StaticLiveServerTestCase),
# set position, and dispatch scroll event to trigger the debounced save # set position, and dispatch scroll event to trigger the debounced save.
target = 100 # JS saves scrollTop + clientHeight (bottom-of-viewport); forced height is 150px.
scroll_top = 100
forced_height = 150
self.browser.execute_script(""" self.browser.execute_script("""
var el = arguments[0]; var el = arguments[0];
el.style.overflow = 'auto'; el.style.overflow = 'auto';
el.style.height = '150px'; el.style.height = '150px';
el.scrollTop = arguments[1]; el.scrollTop = arguments[1];
el.dispatchEvent(new Event('scroll')); el.dispatchEvent(new Event('scroll'));
""", scroll_el, target) """, scroll_el, scroll_top)
# 3. Wait for debounce (800ms) + fetch to complete # 3. Wait for debounce (800ms) + fetch to complete
time.sleep(3) time.sleep(3)
@@ -178,8 +180,11 @@ class BillscrollPositionTest(FunctionalTest):
scroll_el = self.wait_for( scroll_el = self.wait_for(
lambda: self.browser.find_element(By.ID, "id_drama_scroll") 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")) 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): class BillboardAppletsTest(FunctionalTest):

View File

@@ -57,6 +57,31 @@ body.page-billscroll {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
overflow-y: auto; 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;
}
}
}
} }
} }
} }

View File

@@ -7,23 +7,48 @@
var scroll = document.getElementById('id_drama_scroll'); var scroll = document.getElementById('id_drama_scroll');
if (!scroll) return; if (!scroll) return;
// Restore saved position // Push buffer so its top aligns with the bottom of the aperture when all
scroll.scrollTop = {{ scroll_position }}; // 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; var saveTimer;
scroll.addEventListener('scroll', function() { scroll.addEventListener('scroll', function() {
clearTimeout(saveTimer); clearTimeout(saveTimer);
saveTimer = setTimeout(function() { saveTimer = setTimeout(function() {
var csrfToken = document.querySelector('[name=csrfmiddlewaretoken]'); var csrfToken = document.querySelector('[name=csrfmiddlewaretoken]');
var token = csrfToken ? csrfToken.value : ''; var token = csrfToken ? csrfToken.value : '';
var remPx = parseFloat(getComputedStyle(document.documentElement).fontSize);
fetch("{% url 'billboard:save_scroll_position' room.id %}", { fetch("{% url 'billboard:save_scroll_position' room.id %}", {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRFToken': token, 'X-CSRFToken': token,
}, },
body: 'position=' + Math.round(scroll.scrollTop), body: 'position=' + Math.round(scroll.scrollTop + scroll.clientHeight + remPx * 2.5),
}); });
}, 800); }, 800);
}); });

View File

@@ -13,4 +13,12 @@
{% empty %} {% empty %}
<p class="event-empty"><small>No events yet.</small></p> <p class="event-empty"><small>No events yet.</small></p>
{% endfor %} {% endfor %}
<div class="scroll-buffer" aria-hidden="true">
<span class="scroll-buffer-text">What</span>
<span class="scroll-buffer-text quaUser">&nbsp;happens</span>
<span class="scroll-buffer-text terUser">&nbsp;next</span>
<span class="scroll-buffer-dots">
<span></span><span></span><span></span><span></span>
</span>
</div>
</section> </section>