1// Inserts a `.content-list__month-separator` before the first event of each 2// month in the events overview. Each item carries `data-content-list-month` 3// (the 'Y-m' grouping key) and `data-content-list-month-label` (the month name 4// to show). 5// 6// The infinite-scroll pager renders each page as its own `.content-list` <ul> 7// inside an opt-in `.content-list-month-wrapper`. We walk every <ul> in 8// document order, so a month spanning a page boundary gets no duplicate 9// heading. Keeping that heading pinned across the boundary is a CSS concern 10// (the <ul>s are dissolved with `display: contents`, see content-list.scss) - 11// this file never restructures the pager's DOM. A MutationObserver re-runs on 12// each new page, rebuilding separators from scratch. 13 14const SEPARATOR_CLASS = "content-list__month-separator"; 15const FIRST_SEPARATOR_CLASS = "content-list__month-separator--first"; 16const ITEM_SELECTOR = "li.content-list__item[data-content-list-month]"; 17const WRAPPER_SELECTOR = ".content-list-month-wrapper"; 18 19// Containers already wired up, so behaviour re-runs (Drupal AJAX) don't attach 20// a second observer to a container that already has one. 21const initializedContainers = new WeakSet(); 22 23const renderSeparators = (container) => { 24 // Rebuild from scratch so we never end up with duplicate or stale 25 // separators when the infinite scroll pager appends a new page. 26 container 27 .querySelectorAll(`.${SEPARATOR_CLASS}`) 28 .forEach((separator) => separator.remove()); 29 30 let lastMonth = null; 31 let isFirstSeparator = true; 32 33 // One pass in document order across every <ul>, so `lastMonth` de-duplicates 34 // a month that spans a page boundary. 35 container.querySelectorAll(ITEM_SELECTOR).forEach((item) => {
36 const month = item.getAttribute("data-content-list-month"); 37 38 if (!month || month === lastMonth) { 39 return; 40 } 41 42 // Fall back to the grouping key if no explicit label is set, so the 43 // separator always shows something. 44 const label = item.getAttribute("data-content-list-month-label") || month; 45 46 const separator = document.createElement("li"); 47 separator.className = SEPARATOR_CLASS; 48 49 // The separator is a visual scanning aid; every event already announces 50 // its month through its own date. Hiding it from assistive technology 51 // keeps it from being read as an item of the list. 52 separator.setAttribute("aria-hidden", "true"); 53 54 // The very first month heading sits at the top of the list; it shouldn't 55 // get the large top spacing that separates one month from the previous. 56 if (isFirstSeparator) { 57 separator.classList.add(FIRST_SEPARATOR_CLASS); 58 } 59 60 separator.textContent = label; 61 62 item.parentNode.insertBefore(separator, item); 63 lastMonth = month; 64 isFirstSeparator = false; 65 }); 66}; 67 68const affectsItems = (node) => 69 node.nodeType === Node.ELEMENT_NODE && 70 (node.matches(".content-list__item") || 71 !!node.querySelector(".content-list__item")); 72 73const setupMonthSeparators = (container) => { 74 // A behaviour re-run may re-visit a container we already handle; only wire 75 // it once. Also skip lists without month data (e.g. the article overview), 76 // which share the same markup but carry no month attributes. 77 if (initializedContainers.has(container)) { 78 return; 79 } 80 if (!container.querySelector(ITEM_SELECTOR)) { 81 return; 82 } 83 initializedContainers.add(container); 84 85 renderSeparators(container); 86 87 // Re-render when the infinite scroll pager appends a new page (a new <ul>) 88 // or when items are otherwise added/removed. 89 const observer = new MutationObserver((mutations) => { 90 const itemsChanged = mutations.some( 91 (mutation) => 92 Array.from(mutation.addedNodes).some(affectsItems) || 93 Array.from(mutation.removedNodes).some(affectsItems), 94 ); 95 96 if (!itemsChanged) { 97 return; 98 } 99 100 // Pause observing while we mutate the DOM ourselves, so our own 101 // separator changes don't trigger another pass. 102 observer.disconnect(); 103 renderSeparators(container); 104 observer.observe(container, { childList: true, subtree: true }); 105 }); 106 107 observer.observe(container, { childList: true, subtree: true }); 108}; 109 110const processScope = (root) => { 111 // Treat all pages under one month wrapper as a single list; a content list 112 // outside any wrapper stands alone. 113 const scope = root && root.querySelectorAll ? root : document; 114 115 const lists = [...scope.querySelectorAll(".content-list")]; 116 if (scope.matches && scope.matches(".content-list")) { 117 lists.push(scope); 118 } 119 120 const containers = new Set(); 121 lists.forEach((list) => { 122 containers.add(list.closest(WRAPPER_SELECTOR) || list); 123 }); 124 125 containers.forEach((container) => setupMonthSeparators(container)); 126}; 127 128// In the CMS the events view re-renders via AJAX (infinite scroll, exposed 129// filters, sort) without firing DOMContentLoaded, so we register a Drupal 130// behaviour that re-runs on every AJAX response. For non-Drupal consumers the 131// file stays framework-agnostic: one pass over the document, run immediately 132// if it has already loaded (e.g. when the file is lazily required) or on 133// DOMContentLoaded otherwise. 134if (window.Drupal && window.Drupal.behaviors) { 135 window.Drupal.behaviors.contentListMonthSeparator = { 136 attach: (context) => processScope(context), 137 }; 138} else if (document.readyState === "loading") { 139 document.addEventListener("DOMContentLoaded", () => processScope(document)); 140} else { 141 processScope(document); 142}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.