PageSourceSearch

https://www.dcbib.dk/themes/custom/novel/assets/dpl-design-system/js/content-list-month-separator.js?tlu26c

js dcbib.dk collected 2026-09-25 05:53:37 UTC 5,482 bytes, 142 lines download raw bytes

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.