Repository navigation
Website listings add another update handler on every URL hash change #15206
Description
Each listing on a website page adds a list.on("updated", ...) handler every time the URL hash changes, and pages with several listings get extra handlers from the start. The handlers pile up, so a single listing update reruns the same work more and more times.
window["quarto-listing-loaded"] does two things: it applies the URL hash (category filter, page of each listing), then it attaches the updated handler to every listing registered so far.
quarto-cli/src/resources/projects/website/listing/quarto-listing.js
Lines 13 to 57 in 4bebcb4
| window["quarto-listing-loaded"] = () => { | |
| // Process any existing hash | |
| const hash = getHash(); | |
| if (hash) { | |
| // If there is a category, switch to that | |
| if (hash.category) { | |
| // category hash are URI encoded so we need to decode it before processing | |
| // so that we can match it with the category element processed in JS | |
| activateCategory(decodeURIComponent(hash.category)); | |
| } | |
| // Paginate a specific listing | |
| const listingIds = Object.keys(window["quarto-listings"]); | |
| for (const listingId of listingIds) { | |
| const page = hash[getListingPageKey(listingId)]; | |
| if (page) { | |
| showPage(listingId, page); | |
| } | |
| } | |
| } | |
| const listingIds = Object.keys(window["quarto-listings"]); | |
| for (const listingId of listingIds) { | |
| // The actual list | |
| const list = window["quarto-listings"][listingId]; | |
| // Update the handlers for pagination events | |
| refreshPaginationHandlers(listingId); | |
| // Render any visible items that need it | |
| renderVisibleProgressiveImages(list); | |
| // Whenever the list is updated, we also need to | |
| // attach handlers to the new pagination elements | |
| // and refresh any newly visible items. | |
| list.on("updated", function () { | |
| renderVisibleProgressiveImages(list); | |
| window.quartoDecorateExternalLinks?.(list.list); | |
| setTimeout(() => refreshPaginationHandlers(listingId)); | |
| // Show or hide the no matching message | |
| toggleNoMatchingMessage(list); | |
| }); | |
| } | |
| }; |
The script emitted for each listing calls this function from DOMContentLoaded, and also from a hashchange listener on window:
quarto-cli/src/project/types/website/listing/website-listing-template.ts
Lines 557 to 584 in 4bebcb4
| const jsScript = ` | |
| window.document.addEventListener("DOMContentLoaded", function (_event) { | |
| const listingTargetEl = window.document.querySelector('#listing-${id} .list'); | |
| if (!listingTargetEl) { | |
| // No listing discovered, do not attach. | |
| return; | |
| } | |
| const options = { | |
| valueNames: ${rowJs}, | |
| ${pageOptions} | |
| ${filterOptions} | |
| }; | |
| window['quarto-listings'] = window['quarto-listings'] || {}; | |
| window['quarto-listings']['listing-${id}'] = new List('listing-${id}', options); | |
| if (window['quarto-listing-loaded']) { | |
| window['quarto-listing-loaded'](); | |
| } | |
| }); | |
| window.addEventListener('hashchange',() => { | |
| if (window['quarto-listing-loaded']) { | |
| window['quarto-listing-loaded'](); | |
| } | |
| }) | |
| `; |
So the first listing of a page with two listings gets two handlers on load, and each hash change adds one handler per listing on the page to every listing. A hash change happens with back/forward after clicking a category or a page, or with an in-page # link.
With two paginated listings, counting how many times the handler runs for one list.update(), in Chromium:
| first listing | second listing | |
|---|---|---|
| After load | 2 | 1 |
| After 3 hash changes | 8 | 7 |
Nothing is duplicated on the page today, as every step of the handler can run several times without side effect (the external link icon step was made safe for this in #9469). It is only repeated work, but any non idempotent step added to this handler would show it.
The hashchange listener was added in 962d83d for #1139, and only the hash part is needed there. We could attach the handler once per listing, and use a single hashchange listener in quarto-listing.js that only applies the hash. Back/forward between categories also relies on this listener, so this behavior needs to stay.
Activity
Fix website listings stacking update handlers on hash change (quarto-…