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:
@@ -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):
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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"> happens</span>
|
||||||
|
<span class="scroll-buffer-text terUser"> next</span>
|
||||||
|
<span class="scroll-buffer-dots">
|
||||||
|
<span></span><span></span><span></span><span></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user