Skip to content

Commit 51f3a7c

Browse files
committed
Fix scroll-restore batch tracking and podcast replay bugs
Three issues from automated review, all confirmed with a standalone Playwright harness against the real markup and HTMX build: - Track/list/history modal buttons inside each episode row also fire HTMX requests within #episodes-list, so the batch counter (keyed on htmx:beforeRequest) was counting those too. Restrict counting to the actual pagination trigger. - The "Show more" link is itself an a[href] inside the container, so clicking it (HTMX intercepts the navigation) was overwriting the saved scroll/batch state meant for a real departure. Exclude it from the save-state click handler. - Podcast page replay inserted fetched fragments as raw HTML next to the old trigger instead of replacing it, leaving a stale "revealed" listener that could re-fire and duplicate episodes, and leaving new hx-get attributes unprocessed by HTMX. Replace the old trigger and call htmx.process on the inserted markup. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZeEWvRjRr287HKjX6hM5g
1 parent 8d70574 commit 51f3a7c

1 file changed

Lines changed: 47 additions & 32 deletions

File tree

src/templates/app/components/_episode_scroll_restore.html

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@
3535
return null;
3636
}
3737

38+
function isPaginationTrigger(elt) {
39+
if (!elt) return false;
40+
if (elt.closest && elt.closest('[data-episode-load-more]')) return true;
41+
return !!(elt.classList && elt.classList.contains('episodes-load-more-trigger'));
42+
}
43+
3844
function bindBatchTracking(container) {
3945
if (container.dataset.scrollRestoreBound === 'true') {
4046
return;
@@ -46,10 +52,13 @@
4652
// outerHTML swap (TV/comic "Show more") is detached from the DOM by
4753
// the time afterSwap fires, so a containment check there always
4854
// misses. The requesting element is still attached at beforeRequest.
55+
// Only count the pagination trigger itself - the track/lists/history
56+
// buttons on each episode row also issue HTMX requests from within
57+
// this container and must not be counted as a loaded batch.
4958
document.body.addEventListener('htmx:beforeRequest', function(event) {
5059
var elt = event.detail.elt;
5160
if (!elt) return;
52-
if (container.contains(elt) || elt === container) {
61+
if ((container.contains(elt) || elt === container) && isPaginationTrigger(elt)) {
5362
var current = Number.parseInt(container.dataset.loadedBatches || '0', 10);
5463
container.dataset.loadedBatches = String((Number.isNaN(current) ? 0 : current) + 1);
5564
}
@@ -60,6 +69,11 @@
6069
if (!link || !container.contains(link)) {
6170
return;
6271
}
72+
// The "Show more" link is intercepted by HTMX (no real navigation),
73+
// so it must not overwrite the saved state for an actual departure.
74+
if (link.closest('[data-episode-load-more]')) {
75+
return;
76+
}
6377
var batches = Number.parseInt(container.dataset.loadedBatches || '0', 10);
6478
var state = {
6579
scrollY: window.scrollY,
@@ -100,43 +114,44 @@
100114
});
101115
}
102116

103-
function replayPodcastPages(container, batches) {
104-
if (batches <= 0) {
117+
function replayPodcastPages(container, remaining) {
118+
if (remaining <= 0) {
105119
return Promise.resolve();
106120
}
107121
var loadMore = getLoadMoreLink(container);
108122
if (!loadMore) {
109123
return Promise.resolve();
110124
}
111-
var url = new URL(loadMore.url, window.location.origin);
112-
var startPage = Number.parseInt(url.searchParams.get('page') || '2', 10);
113-
if (Number.isNaN(startPage)) {
114-
startPage = 2;
115-
}
116-
117-
var chain = Promise.resolve();
118-
for (var i = 0; i < batches; i++) {
119-
(function(page) {
120-
chain = chain.then(function() {
121-
var pageUrl = new URL(loadMore.url, window.location.origin);
122-
pageUrl.searchParams.set('page', page);
123-
return fetch(pageUrl.toString())
124-
.then(function(response) { return response.ok ? response.text() : ''; })
125-
.then(function(html) {
126-
if (html) {
127-
var trigger = container.querySelector('.episodes-load-more-trigger');
128-
if (trigger) {
129-
trigger.insertAdjacentHTML('beforebegin', html);
130-
} else {
131-
container.insertAdjacentHTML('beforeend', html);
132-
}
133-
}
134-
})
135-
.catch(function() {});
136-
});
137-
})(startPage + i);
138-
}
139-
return chain;
125+
return fetch(loadMore.url)
126+
.then(function(response) { return response.ok ? response.text() : ''; })
127+
.then(function(html) {
128+
if (!html) return;
129+
// Replace the old trigger (rather than inserting alongside it) so
130+
// its still-active "revealed" listener can't re-fire and
131+
// re-request the same page. htmx.process activates the hx-*
132+
// attributes on the freshly inserted markup (its own next-page
133+
// trigger and any per-episode buttons), since raw innerHTML
134+
// insertion bypasses HTMX's usual auto-processing on swap.
135+
var temp = document.createElement('div');
136+
temp.innerHTML = html;
137+
var fragment = document.createDocumentFragment();
138+
while (temp.firstChild) {
139+
fragment.appendChild(temp.firstChild);
140+
}
141+
var oldTrigger = container.querySelector('.episodes-load-more-trigger');
142+
if (oldTrigger) {
143+
container.replaceChild(fragment, oldTrigger);
144+
} else {
145+
container.appendChild(fragment);
146+
}
147+
if (window.htmx) {
148+
htmx.process(container);
149+
}
150+
})
151+
.catch(function() {})
152+
.then(function() {
153+
return replayPodcastPages(container, remaining - 1);
154+
});
140155
}
141156

142157
function restoreScroll() {

0 commit comments

Comments
 (0)