PageSourceSearch

https://www.paxlovid.com/lib/blocks/accordion/accordion.js

js paxlovid.com collected 2026-10-03 23:27:03 UTC 13,972 bytes, 370 lines download raw bytes

1import { FranklinBlock, blockColorThemeStructure, platformFetchPage, decorateSections, decorateBlocks, loadSections } from '../../scripts/lib-franklin.js';
2
3const ANIMATION_DELAY_SHORT = 10;
4const ANIMATION_DELAY_LONG = 100;
5
6/**
7 * Creates a promise that resolves after the specified timeout
8 * @param {number} timeout - Time in milliseconds to wait
9 * @returns {Promise} Promise that resolves after timeout
10 */
11const sleep = (timeout) =>
12  new Promise((resolve) => {
13    setTimeout(resolve, timeout);
14  });
15
16/**
17 * Checks if accordion item is expanded
18 * @param {HTMLElement} summary - The summary element to check
19 * @returns {boolean} True if the item is expanded
20 */
21const isExpanded = (summary) => summary.classList.contains('is-open');
22
23/**
24 * Expands an accordion item
25 * @param {HTMLElement} summary - The summary element to expand
26 * @param {boolean} singleOpen - Whether to close other open accordions
27 */
28async function expand(summary, singleOpen) {
29  const openSummary = summary.closest('.core-accordion').querySelector('summary.is-open');
30  const details = summary.closest('details');
31  details.setAttribute('open', '');
32  // waiting for the animation
33  await sleep(ANIMATION_DELAY_SHORT);
34  summary.classList.add('is-open');
35  summary.setAttribute('aria-expanded', 'true');
36
37  // Dispatch custom event with details element
38  const event = new CustomEvent('accordion:expand', {
39    detail: details,
40    bubbles: true,
41    composed: true,
42  });
43
44  if (singleOpen && openSummary && openSummary !== summary) {
45    // eslint-disable-next-line no-use-before-define
46    collapse(openSummary);
47  }
48
49  details.dispatchEvent(event);
50}
51
52/**
53 * Collapses an accordion item
54 * @param {HTMLElement} summary - The summary element to collapse
55 */
56async function collapse(summary) {
57  summary.classList.remove('is-open');
58  summary.setAttribute('aria-expanded', 'false');
59  // waiting for the animation
60  await sleep(ANIMATION_DELAY_LONG);
61  const details = summary.closest('details');
62  details.removeAttribute('open');
63}
64
65/**
66 * Adds click event listeners to all accordion summary elements
67 * @param {HTMLElement} block - The accordion block element
68 * @param {boolean} singleOpen - Whether only one accordion can be open at a time
69 * @param {boolean} autoOpenNext - Whether to auto-open next accordion on close
70 */
71function addListeners(block, singleOpen = false, autoOpenNext = false) {
72  const summaries = block.querySelectorAll('summary');
73
74  summaries.forEach((summary) =>
75    summary.addEventListener('click', async (event) => {
76      event.preventDefault();
77
78      if (isExpanded(summary)) {
79        // Closing current accordion
80        await collapse(summary);
81
82        // If autoOpenNext is enabled, open the next accordion
83        if (autoOpenNext) {
84          const currentIndex = Array.from(summaries).indexOf(summary);
85          const nextIndex = currentIndex + 1 < summaries.length ? currentIndex + 1 : 0;
86          const nextSummary = summaries[nextIndex];
87
88          if (nextSummary && !isExpanded(nextSummary)) {
89            await expand(nextSummary, singleOpen);
90          }
91        }
92      } else {
93        // Opening current accordion
94        await expand(summary, singleOpen);
95      }
96
97      summary.blur();
98    })
99  );
100
101  // handles the case when the browser toggles the details element on hash navigation
102  block.querySelectorAll('details').forEach((details) => {
103    details.addEventListener('toggle', () => {
104      if (details.open) {
105        details.querySelector('summary').classList.add('is-open');
106      } else {
107        details.querySelector('summary').classList.remove('is-open');
108      }
109    });
110  });
111}
112
113export default class Accordion extends FranklinBlock {
114  beforeBlockRender() {
115    this.inputData.accordionCollection = this.inputData.accordionCollection.map((item, index) => ({
116      index: index + 1,
117      ...item,
118    }));
119  }
120
121  /**
122   * Prepares block structure before reading data
123   */
124  async beforeBlockDataRead() {
125    Array.from(this.block.children).forEach((div) => {
126      const firstChild = div.firstElementChild;
127      if (firstChild?.textContent.trim().toLowerCase() !== 'subheader') {
128        div.classList.add('accordion-div-target');
129      }
130    });
131  }
132
133  /**
134   * Adds an image subcaption to accordion item if one exists
135   * @param {HTMLElement} accordionItem - The accordion item element
136   */
137  static addImageSubCaption(accordionItem) {
138    const rawCaptionEl = accordionItem.querySelector('picture + em, picture + p:has(em), p:has(picture) + em, p:has(picture) + p:has(em)');
139    if (rawCaptionEl) {
140      const captionText = rawCaptionEl.textContent.trim();
141      if (captionText) {
142        const caption = document.createElement('div');
143        caption.classList.add('accordion-image-sub-caption');
144        caption.innerHTML = captionText;
145        rawCaptionEl.parentElement.replaceChild(caption, rawCaptionEl);
146      }
147    }
148  }
149
150  /**
151   * Adds a disclaimer to accordion item if one exists
152   * Processes trailing paragraphs that are ENTIRELY wrapped in em (disclaimer pattern)
153   * and merges them into a single disclaimer container.
154   * @param {HTMLElement} accordionItem - The accordion item element
155   */
156  static addDisclaimer(accordionItem) {
157    // Only creates disclaimers for trailing paragraphs that are fully wrapped in em.
158    const paragraphs = Array.from(accordionItem.querySelectorAll(':scope > div:not(.core-accordion-media) > p, :scope > p'));
159    if (paragraphs.length === 0) return;
160
161    const trailingDisclaimerParagraphs = [];
162
163    for (let index = paragraphs.length - 1; index >= 0; index -= 1) {
164      const paragraph = paragraphs[index];
165      const emTag = paragraph.querySelector(':scope > em');
166      if (!emTag) break;
167
168      // Double-check that the em tag contains ALL the text content of the paragraph
169      const paragraphText = paragraph.textContent.trim();
170      const emText = emTag.textContent.trim();
171
172      // Stop once a non-disclaimer paragraph is found so only trailing disclaimers are converted
173      if (paragraphText !== emText) break;
174
175      trailingDisclaimerParagraphs.unshift(paragraph);
176    }
177
178    if (trailingDisclaimerParagraphs.length === 0) return;
179
180    const disclaimer = document.createElement('div');
181    disclaimer.classList.add('atoms-disclaimer');
182    disclaimer.innerHTML = trailingDisclaimerParagraphs
183      .map((paragraph) => paragraph.querySelector(':scope > em').innerHTML)
184      .join('<br><br>');
185
186    const firstDisclaimerParagraph = trailingDisclaimerParagraphs[0];
187    firstDisclaimerParagraph.textContent = '';
188    firstDisclaimerParagraph.appendChild(disclaimer);
189
190    trailingDisclaimerParagraphs.slice(1).forEach((paragraph) => paragraph.remove());
191  }
192
193  static onHashChange() {
194    if (!window.location.hash) return;
195    const hash = window.location.hash.substring(1);
196    const bookmarkedElement = document.getElementById(hash);
197    if (bookmarkedElement) {
198      const details = bookmarkedElement.closest('details');
199      if (!details) return;
200      details.setAttribute('open', '');
201      const summary = details.querySelector('summary');
202      if (summary) {
203        summary.classList.add('is-open');
204        setTimeout(() => {
205          bookmarkedElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
206        }, 100);
207      }
208    }
209  }
210
211  static listenHashChange() {
212    window.addEventListener('hashchange', Accordion.onHashChange);
213    Accordion.onHashChange();
214  }
215
216  afterBlockRender() {
217    // Check if accordion is inside accordion-2, accordion-3, or accordion-4 container
218    const parentContainer = this.block.closest('.core-accordion-container');
219    const isInSpecialContainer =
220      parentContainer &&
221      (parentContainer.classList.contains('accordion-2') || parentContainer.classList.contains('accordion-3') || parentContainer.classList.contains('accordion-4'));
222
223    // Enable single-open mode and auto-open-next only for accordion-2, accordion-3, accordion-4
224    const singleOpen = isInSpecialContainer;
225    const autoOpenNext = isInSpecialContainer;
226    addListeners(this.block, singleOpen, autoOpenNext);
227
228    this.block.querySelectorAll('.core-accordion-item-body').forEach((accordionItem) => {
229      Accordion.addImageSubCaption(accordionItem);
230      Accordion.addDisclaimer(accordionItem);
231    });
232
233    // Listen for accordion expand events
234    this.block.addEventListener('accordion:expand', (event) => {
235      Accordion.handleAccordionExpand(event);
236    });
237
238    // Open the first accordion by default only for accordion-2, accordion-3, accordion-4
239    if (isInSpecialContainer) {
240      const firstSummary = this.block.querySelector('summary');
241      if (firstSummary && !isExpanded(firstSummary)) {
242        expand(firstSummary, singleOpen);
243      }
244    }
245
246    this.block.classList.add('block-focus-visible');
247
248    // global standard rules for block padding ///////////////////////
249    this.block.classList.add('block-padding-standard');
250
251    const isEditorialWidth = this.block.classList.contains('editorial-width') || this.block.classList.contains('inline');
252
253    if (isEditorialWidth) {
254      this.block.classList.add('block-padding-desktop-x-190');
255    }
256
257    // Respect the heading level (h2/h3/h4...) the author selected for the subheader.
258    // The template always wraps the subheader in an <h2>. When an author sets the
259    // text to a heading in the docx, the browser cannot nest headings, so it hoists
260    // that heading out, leaving an empty <h2 class="core-accordion-subheader"></h2>
261    // followed by a sibling heading. Normal text instead stays wrapped in a <p>
262    // inside the <h2>. Normalise both cases into a single subheader element that
263    // keeps the authored heading level and any inline formatting.
264    const subHeading = this.block.querySelector('.core-accordion-subheader');
265    if (subHeading) {
266      const sibling = subHeading.nextElementSibling;
267      if (subHeading.textContent.trim() === '' && sibling && /^H[1-6]$/.test(sibling.tagName)) {
268        sibling.classList.add('core-accordion-subheader');
269        subHeading.remove();
270      } else if (subHeading.children.length === 1 && subHeading.firstElementChild.tagName === 'P') {
271        subHeading.innerHTML = subHeading.firstElementChild.innerHTML;
272      }
273    }
274    this.afterBlockRenderColorTheming();
275  }
276
277  afterBlockRenderColorTheming() {
278    // Skip theming if inside accordion-4 container
279    const parentContainer = this.block.closest('.core-accordion-container');
280    if (parentContainer?.classList.contains('accordion-4')) {
281      return;
282    }
283
284    const themeTargets = [
285      { target: '.block', classToAdd: 'surface' },
286      { target: '.block .core-accordion-subheader', classToAdd: 'body-copy' },
287      { target: '.block .core-accordion-item-label h3, .block .core-accordion-item-label h4', classToAdd: 'body-copy' },
288      { target: '.block .core-accordion-item-body.atoms-content p', classToAdd: 'body-copy' },
289      { target: '.block .core-accordion-item-body.atoms-content p a', classToAdd: 'text-link', multiple: true },
290      { target: '.block .core-accordion details', classToAdd: 'border-neutral' },
291      { target: '.block summary span.icon', classToAdd: 'themed-icon' },
292      { target: '.block .accordion-image-sub-caption', classToAdd: 'disclaimer' },
293    ];
294
295    const themeConfiguration = {
296      'color-theme-1-white': {
297        isDefaultVariant: true,
298        colorThemeCollection: 'data-color-theme-one',
299        colorModeValue: 'white',
300        domTargets: [...themeTargets],
301        isMatch: this.block.classList.contains('color-theme-1-white'),
302      },
303      'color-theme-1-core-tinted': {
304        colorThemeCollection: 'data-color-theme-one',
305        colorModeValue: 'core-tinted',
306        domTargets: [...themeTargets],
307        isMatch: this.block.classList.contains('color-theme-1-core-tinted'),
308      },
309    };
310
311    blockColorThemeStructure(this.block, themeConfiguration);
312
313    const icons = this.block.querySelectorAll('.block summary span.icon');
314    icons.forEach((icon) => {
315      icon.classList.add('themed-icon');
316    });
317
318    const detailsElements = this.block.querySelectorAll('.block .core-accordion details');
319    detailsElements.forEach((details) => {
320      details.classList.add('border-neutral');
321    });
322  }
323
324  /**
325   * Handles accordion expand event (for accordion-4 mobile fragment loading)
326   * @param {CustomEvent} event - The accordion expand event
327   */
328  static async handleAccordionExpand(event) {
329    if (!Accordion.isMobile()) return;
330    const accordionItem = event.detail;
331    const container = accordionItem.querySelector('.accordion-item-fragment');
332    if (container) return;
333    const fragmentContainer = Accordion.createFragmentContainer(accordionItem);
334    await Accordion.loadFragment(accordionItem, fragmentContainer);
335    accordionItem.querySelector('.core-accordion-media').appendChild(fragmentContainer);
336  }
337
338  static isMobile = () => !!window.matchMedia('(max-width: 1024px)').matches;
339
340  static createFragmentContainer() {
341    const container = document.createElement('div');
342    container.classList.add('accordion-item-fragment');
343    return container;
344  }
345
346  static async loadFragment(accordionItem, fragmentContainer) {
347    const fragmentLink = accordionItem.querySelector('a[href*="loadcontent=true"]');
348    if (fragmentLink) {
349      const fragmentUrl = new URL(fragmentLink.href);
350
351      if (fragmentUrl.origin !== window.location.origin) {
352        console.error('Cannot load overlay content from different origin:', fragmentUrl.origin);
353        return;
354      }
355      const contentHTML = await platformFetchPage('accordion-fragment', fragmentUrl.pathname);
356
357      if (!contentHTML) {
358        console.error('No content found in accordion');
359        return;
360      }
361
362      fragmentContainer.innerHTML = contentHTML; // sanitize?
363      decorateSections(fragmentContainer);
364      decorateBlocks(fragmentContainer);
365      loadSections(fragmentContainer);
366    }
367  }
368}
369
370setTimeout(Accordion.listenHashChange, 1500);

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.