PageSourceSearch

https://ville-boisemont.fr/modules/contrib/toc_js/assets/js/tocjs.js?tm9rsb

js ville-boisemont.fr collected 2026-10-02 21:48:19 UTC 27,881 bytes, 872 lines download raw bytes

1/*!
2 * Javascript code based on the original toc.js library.
3 * Original code has been heavily modified and refactored.
4 *
5 * toc - jQuery Table of Contents Plugin
6 * http://projects.jga.me/toc/
7 * copyright Greg Allen 2014
8 * MIT License
9 */
10(($) => {
11  const PADDING_TOP_UPDATE_EVENT = 'toc_js-padding-top-update';
12  const TOC_UPDATE_EVENT = 'toc_js-toc-update';
13  const DATA_FOCUSED_ELEMENT_ID = 'focusedElementId';
14
15  /**
16   * The TocObject struct definition.
17   * @typedef {Object} TocObject
18   * @property {Object} opts - TODO.
19   * @property {Node} element - TODO.
20   * @property {Object} $element - TODO.
21   * @property {Node} tocContainer - TODO.
22   * @property {Object} $tocContainer - TODO.
23   * @property {NodeList} containers - TODO.
24   * @property {Object} $headings - TODO.
25   * @property {number} bodyPaddingTop - TODO.
26   * @property {number|null} highlightTimeout - Highlight on scroll timeout
27   * @property {number|null} scrollToTimeout - TODO.
28   * @property {function} highlightOnScroll - TODO.
29   * @property {function} scrollTo - TODO.
30   * @property {function|undefined} scrollListener - TODO.
31   * @property {boolean} updating - TODO.
32   * @property {Node} observableContainer - TODO.
33   */
34
35  /**
36   * Get the new body padding-top and update the sticky positioning.
37   *
38   * @param {TocObject} tocObj - The tocObj object.
39   */
40  function refreshBodyPaddingTop(tocObj) {
41    const bodyCompStyle = window.getComputedStyle(document.body);
42    tocObj.bodyPaddingTop = parseInt(
43      bodyCompStyle.getPropertyValue('padding-top'),
44      10,
45    );
46    document.dispatchEvent(new Event(PADDING_TOP_UPDATE_EVENT));
47  }
48
49  /**
50   * Handle body padding-top changes for correct sticky positioning.
51   *
52   * The Drupal admin toolbar adds a padding-top to the page body.
53   * We try to handle it as properly as possible.
54   *
55   * @param {TocObject} tocObj - The tocObj object.
56   */
57  function initBodyPaddingTopObserver(tocObj) {
58    const mutObserver = new MutationObserver((mutations) => {
59      mutations.forEach((mutationRecord) => {
60        if (
61          mutationRecord.type === 'attributes' &&
62          mutationRecord.attributeName === 'style' &&
63          mutationRecord.target === document.body
64        ) {
65          refreshBodyPaddingTop(tocObj);
66        }
67      });
68    });
69    mutObserver.observe(document.body, {
70      attributes: true,
71      attributeFilter: ['style'],
72    });
73  }
74
75  /**
76   * Creates a Range from a NodeList.
77   *
78   * It will be used to get the common ancestor of several containers.
79   *
80   * @param {NodeList} nodeList - The node list to use to create the range.
81   * @return {Range} - The corresponding range.
82   */
83  function createRangeFromNodeList(nodeList) {
84    if (nodeList.length === 0) return null;
85    const range = document.createRange();
86    range.setStartBefore(nodeList[0]);
87    range.setEndAfter(nodeList[nodeList.length - 1]);
88    return range;
89  }
90
91  /**
92   * Starts observing the specified container for mutations.
93   *
94   * @param {MutationObserver} mutObserver - The Ajax MutationObserver.
95   * @param {Node} container - The Node to observer for mutation.
96   */
97  function mutationObserve(mutObserver, container) {
98    mutObserver.observe(container, {
99      childList: true,
100      subtree: true,
101    });
102  }
103
104  /**
105   * @param {TocObject} tocObj - The tocObj object.
106   */
107  function initContainerObserver(tocObj) {
108    const mutObserver = new MutationObserver((mutations) => {
109      let updateNeeded = false;
110      for (let k = 0; k < mutations.length; k++) {
111        if (updateNeeded) {
112          break;
113        }
114        const mutation = mutations[k];
115        if (mutation.type === 'childList') {
116          const { addedNodes, removedNodes } = mutation;
117          if (addedNodes.length > 0) {
118            let containerUpdated = false;
119            for (let i = 0; i < addedNodes.length; i++) {
120              if (containerUpdated) {
121                break;
122              }
123              const addedNode = addedNodes[i];
124              if (
125                addedNode.nodeType === Node.ELEMENT_NODE &&
126                (addedNode.querySelectorAll(tocObj.opts.selectors).length > 0 ||
127                  addedNode.closest(tocObj.opts.selectors))
128              ) {
129                for (let j = 0; j < tocObj.containers.length; j++) {
130                  if (containerUpdated) {
131                    break;
132                  }
133                  const container = tocObj.containers[j];
134                  if (container.contains(addedNode)) {
135                    containerUpdated = true;
136                  }
137                }
138              }
139            }
140            updateNeeded = updateNeeded || containerUpdated;
141          }
142          if (updateNeeded) {
143            break;
144          }
145          if (removedNodes.length > 0) {
146            let containerUpdated = false;
147            for (let i = 0; i < removedNodes.length; i++) {
148              if (containerUpdated) {
149                break;
150              }
151              const removedNode = removedNodes[i];
152              if (removedNode.nodeType === Node.ELEMENT_NODE) {
153                for (let j = 0; j < tocObj.containers.length; j++) {
154                  if (containerUpdated) {
155                    break;
156                  }
157                  const container = tocObj.containers[j];
158                  if (removedNode.contains(container)) {
159                    containerUpdated = true;
160                  } else if (container.contains(mutation.target)) {
161                    containerUpdated =
162                      removedNode.querySelectorAll(tocObj.opts.selectors)
163                        .length > 0;
164                  }
165                }
166              }
167            }
168            updateNeeded = updateNeeded || containerUpdated;
169          }
170        }
171      }
172      if (updateNeeded) {
173        document.dispatchEvent(new Event(TOC_UPDATE_EVENT));
174      }
175    });
176    if (tocObj.observableContainer) {
177      mutationObserve(mutObserver, tocObj.observableContainer);
178    } else if (tocObj.containers.length === 1) {
179      mutationObserve(mutObserver, tocObj.containers[0]);
180    } else {
181      const containersRange = createRangeFromNodeList(tocObj.containers);
182      if (containersRange) {
183        mutationObserve(mutObserver, containersRange.commonAncestorContainer);
184      } else {
185        // Could be problematic performance wise, but should not happen.
186        mutationObserve(mutObserver, document);
187      }
188    }
189  }
190
191  /**
192   * @param {TocObject} tocObj - The tocObj object.
193   */
194  function removeActiveStatus(tocObj) {
195    const { element, opts } = tocObj;
196    element.querySelectorAll(`li.${opts.activeClass}`).forEach((li) => {
197      li.classList.remove(opts.activeClass);
198      li.querySelector('a[aria-current="true"]').removeAttribute(
199        'aria-current',
200      );
201    });
202  }
203
204  /**
205   * Add class and aria-current to the active link.
206   *
207   * @param {TocObject} tocObj - The tocObj object.
208   * @param {Element} activeLink - The link to set active.
209   */
210  function setActiveStatus(tocObj, activeLink) {
211    const { opts } = tocObj;
212    activeLink.closest('li').classList.add(opts.activeClass);
213    activeLink.setAttribute('aria-current', 'true');
214  }
215
216  /**
217   * @param {TocObject} tocObj - The tocObj object.
218   */
219  function refreshHighlight(tocObj) {
220    const scrollHeight = window.innerHeight - tocObj.bodyPaddingTop;
221    let closest = Number.MAX_VALUE;
222    let index = -1;
223    let prevIndex = -1;
224    for (let i = 0; i < tocObj.$headings.length; i += 1) {
225      const headingRect = tocObj.$headings[i].getBoundingClientRect();
226      // Process visible headings only.
227      if (headingRect.height > 0) {
228        prevIndex = index;
229        const headingScrollTop = headingRect.top - tocObj.bodyPaddingTop;
230        const currentClosest = Math.abs(headingScrollTop);
231        if (currentClosest < closest) {
232          closest = currentClosest;
233          index = i;
234          if (headingScrollTop > scrollHeight / 2 && prevIndex !== -1) {
235            // Go back to the previous index if not high enough on screen.
236            index = prevIndex;
237          }
238        } else {
239          break;
240        }
241      }
242    }
243    if (index >= 0) {
244      removeActiveStatus(tocObj);
245      const headingId = tocObj.$headings[index].id;
246      const highlighted = tocObj.element.querySelector(
247        `li a[href="#${headingId}"]`,
248      );
249      setActiveStatus(tocObj, highlighted);
250      tocObj.opts.onHighlight($(highlighted).closest('li'));
251    }
252  }
253
254  function highlightOnScroll() {
255    /** @type {TocObject} */
256    const tocObj = this;
257    if (tocObj.highlightTimeout) {
258      clearTimeout(tocObj.highlightTimeout);
259    }
260    tocObj.highlightTimeout = setTimeout(() => {
261      refreshHighlight(tocObj);
262    }, 50);
263  }
264
265  /**
266   * @param {TocObject} tocObj - The tocObj object.
267   */
268  function addHighlightScrollListener(tocObj) {
269    $(window).on(
270      'scroll',
271      ((to) => {
272        to.scrollListener = () => {
273          to.highlightOnScroll();
274        };
275        return to.scrollListener;
276      })(tocObj),
277    );
278  }
279
280  function scrollTo(e, anchor) {
281    /** @type {TocObject} */
282    const tocObj = this;
283    if (tocObj.opts.smoothScrolling) {
284      e.preventDefault();
285      if (tocObj.opts.highlightOnScroll) {
286        // If currently waiting for highlight re-enabling... Check below.
287        if (tocObj.scrollToTimeout) {
288          clearTimeout(tocObj.scrollToTimeout);
289        }
290        // Disable highlight before using scrollIntoView for performance reason.
291        $(window).off('scroll', tocObj.scrollListener);
292      }
293      anchor.scrollIntoView({ behavior: 'smooth' });
294      window.history.replaceState(undefined, undefined, `#${anchor.id}`);
295      if (tocObj.opts.highlightOnScroll) {
296        // Re-enable highlight after waiting for one second.
297        tocObj.scrollToTimeout = setTimeout(() => {
298          addHighlightScrollListener(tocObj);
299        }, 1000);
300      }
301    }
302  }
303
304  /**
305   * Get the heading level from the related DOM element.
306   *
307   * @param {Object} heading - The heading element object.
308   * @return {number} - The heading level if the element is an HTML heading,
309   * 1 otherwise.
310   */
311  function getHeadingLevel(heading) {
312    if (/^H[1-6]$/.test(heading.tagName)) {
313      return parseInt(heading.tagName[1], 10);
314    }
315    return 1;
316  }
317
318  /**
319   * Returns a cleaned copy of the heading element, removing unwanted child elements.
320   *
321   * @param {Element} heading - The heading DOM element to clean.
322   * @param {Object} opts - The configuration options object.
323   * @return {Element} - The cleaned heading element.
324   */
325  function cleanupHeading(heading, opts) {
326    let headingElt = heading;
327    if (opts.headingCleanupSelector > '') {
328      headingElt = heading.cloneNode(true);
329      headingElt.querySelectorAll(opts.headingCleanupSelector).forEach((el) => {
330        el.remove();
331      });
332    }
333    return headingElt;
334  }
335
336  /**
337   * @param {TocObject} tocObj - The tocObj object.
338   */
339  function processHeadings(tocObj) {
340    const $tocEl = tocObj.$element;
341    const { $headings, element, opts } = tocObj;
342
343    let currentLevel = 0;
344    let currentList;
345    const $tocNav = $tocEl.find('nav');
346    $tocNav.find('ul').remove();
347    let lastLi = $tocNav[0];
348
349    $headings.each((i, heading) => {
350      // Skip invisible headings if specified in configuration.
351      if (opts.skipInvisibleHeadings) {
352        if (!heading.checkVisibility()) {
353          return;
354        }
355      }
356
357      // Cleanup heading content.
358      const cleanHeading = cleanupHeading(heading, opts);
359
360      // Generate heading ID from the clean heading content for anchor use.
361      heading.id = opts.generateHeadingId(cleanHeading, opts.prefix);
362
363      // If specified, allow heading to take focus.
364      if (opts.headingFocus) {
365        heading.tabIndex = -1;
366      }
367
368      // Apply scroll offset.
369      if (opts.scrollToOffset) {
370        heading.style.scrollMarginTop = opts.scrollToOffset;
371      }
372
373      // build TOC item
374      const a = document.createElement('a');
375      if (opts.useHeadingHtml) {
376        a.innerHTML = opts.headingHtml(cleanHeading);
377      } else {
378        a.textContent = opts.headingText(cleanHeading);
379      }
380      a.href = `#${heading.id}`;
381      a.addEventListener('click', (event) => {
382        if (opts.collapsibleItems) {
383          event.stopPropagation(); // To prevent toggling of subitems.
384        }
385        tocObj.scrollTo(event, heading);
386        if (opts.headingFocus) {
387          heading.focus({ preventScroll: true });
388        }
389        removeActiveStatus(tocObj);
390        setActiveStatus(tocObj, event.target);
391        $tocEl.trigger('selected', event.target.href);
392      });
393      if (opts.collapsibleItems) {
394        a.addEventListener('keydown', function noToggleCollapse(e) {
395          if (e.key === 'Enter' || e.key === ' ') {
396            e.stopPropagation(); // To prevent toggling of subitems.
397          }
398        });
399      }
400
401      const li = document.createElement('li');
402      li.classList.add(opts.itemClass(heading, opts.prefix));
403      $(li).addClass(opts.liClasses);
404      if (opts.inheritableClasses.length > 0) {
405        heading.classList.forEach((className) => {
406          if (opts.inheritableClasses.includes(className)) {
407            li.classList.add(className);
408          }
409        });
410      }
411      li.append(a);
412
413      // Add custom classes to heading after adding inheritable classes to toc.
414      $(heading).addClass(opts.headingClasses);
415
416      const level = getHeadingLevel(heading);
417
418      if (level === currentLevel) {
419        currentList.appendChild(li);
420        lastLi = li;
421      } else if (level > currentLevel) {
422        const ul = document.createElement(opts.listType);
423        if (opts.listClasses.length > 0) {
424          ul.classList.add(...opts.listClasses);
425        }
426        if (currentLevel > 0 && opts.collapsibleItems) {
427          ul.classList.add('collapsible');
428          const expanded = opts.collapsibleExpanded;
429          lastLi.role = 'button';
430          lastLi.tabIndex = 0;
431          lastLi.setAttribute('aria-label', expanded ? 'Collapse' : 'Expand');
432          lastLi.setAttribute('aria-expanded', expanded ? 'true' : 'false');
433          lastLi.addEventListener('click', function toggleCollapse(e) {
434            if (e.target === this) {
435              const isExpanded = this.getAttribute('aria-expanded') === 'true';
436              this.setAttribute('aria-expanded', !isExpanded);
437            }
438          });
439          lastLi.addEventListener('keydown', function toggleCollapse(e) {
440            if (e.key === 'Enter' || e.key === ' ') {
441              e.preventDefault(); // Prevent space from scrolling the page
442              this.click();
443            }
444          });
445        }
446        currentLevel = level;
447        ul.level = level;
448        ul.appendChild(li);
449        lastLi.appendChild(ul);
450        currentList = ul;
451        lastLi = li;
452      } else if (level < currentLevel) {
453        while (level < currentLevel) {
454          // Special case where the parent element is the root nav element.
455          // Can occur when the first heading is not of the highest level.
456          // See: https://www.drupal.org/project/toc_js/issues/3474535
457          if (currentList.parentNode.nodeName === 'NAV') {
458            if (level < currentList.level) {
459              currentList.level = level;
460            }
461            break;
462          } else {
463            currentList = currentList.parentNode.parentNode;
464            currentLevel = currentList.level;
465          }
466        }
467        currentList.appendChild(li);
468        lastLi = li;
469        currentLevel = level;
470      }
471    });
472  }
473
474  /**
475   * Build "back to top" and "back to toc" links.
476   *
477   * @param {TocObject} tocObj - The tocObj object.
478   */
479  function buildTocAndTopLinks(tocObj) {
480    const { element, $element, $headings, opts } = tocObj;
481    const tocId = element.id;
482    const firstHeadingId = $headings[0].id;
483    const firstLinkSelector = `a[href="#${firstHeadingId}"]`;
484    const firstLink = element.querySelector(firstLinkSelector);
485    let currentLinkIndex = 0;
486    $element.find('a').each(function updateTocLinks() {
487      const link = this;
488      const linkHash = link.hash;
489      const anchor = linkHash.replace(/^#/, '');
490      const heading = document.getElementById(anchor);
491      if (!heading) {
492        // Can happen when the "Use heading HTML" option is activated.
493        // Some headings can have links in their HTML.
494        // These are not valid toc links, obviously.
495        return;
496      }
497      if (
498        opts.backToTop &&
499        currentLinkIndex > 0 &&
500        (opts.backToTopSelector === '' ||
501          heading.matches(opts.backToTopSelector))
502      ) {
503        const a = document.createElement('a');
504        a.innerHTML = opts.backToTopLabel;
505        a.href = `#${firstHeadingId}`;
506        a.dataset.tocId = tocId;
507        a.addEventListener('click', (e) => {
508          const firstHeading = document.getElementById(firstHeadingId);
509          tocObj.scrollTo(e, firstHeading);
510          if (opts.headingFocus) {
511            firstHeading.focus({ preventScroll: true });
512          }
513          removeActiveStatus(tocObj);
514          setActiveStatus(tocObj, firstLink);
515          $element.trigger('selected', linkHash);
516        });
517        const $a = $(a);
518        $a.addClass(opts.backToTopClass);
519        heading.parentNode.insertBefore(a, heading);
520      }
521      if (opts.backToToc) {
522        const anchorNameToToc = `${tocId}-${anchor}-to-toc`;
523        $(link).attr('id', anchorNameToToc);
524        const a = document.createElement('a');
525        a.innerHTML = opts.backToTocLabel;
526        a.href = `#${anchorNameToToc}`;
527        a.dataset.tocId = tocId;
528        a.addEventListener('click', (e) => {
529          tocObj.scrollTo(e, link);
530          link.focus({ preventScroll: true });
531          removeActiveStatus(tocObj);
532          setActiveStatus(tocObj, link);
533          $element.trigger('selected', linkHash);
534        });
535        const $a = $(a);
536        $a.addClass(opts.backToTocClass);
537        $a.addClass(opts.backToTocClasses);
538        heading.parentNode.insertBefore(a, heading);
539      }
540      currentLinkIndex += 1;
541    });
542  }
543
544  /**
545   * @param {TocObject} tocObj - The tocObj object.
546   * @return {string} - The focused element id.
547   */
548  function getFocusedElementId(tocObj) {
549    return tocObj.$element.data(DATA_FOCUSED_ELEMENT_ID);
550  }
551
552  /**
553   * @param {TocObject} tocObj - The tocObj object.
554   * @param {string} id - The focused element id.
555   */
556  function setFocusedElementId(tocObj, id) {
557    tocObj.$element.data(DATA_FOCUSED_ELEMENT_ID, id);
558  }
559
560  /**
561   * @param {TocObject} tocObj - The tocObj object.
562   */
563  function unsetFocusedElementId(tocObj) {
564    tocObj.$element.data(DATA_FOCUSED_ELEMENT_ID, null);
565  }
566
567  /**
568   * @param {TocObject} tocObj - The tocObj object.
569   * @return {Object} - The focusable element if found.
570   */
571  function getFirstFocusableElement(tocObj) {
572    return tocObj.$element.find('button, a').first();
573  }
574
575  /**
576   * Restore focus on previously focused element when toc is updated.
577   *
578   * @param {TocObject} tocObj - The tocObj object.
579   */
580  function restoreFocus(tocObj) {
581    setTimeout(() => {
582      // Get the ID of the previously focused element.
583      const focusedElementId = getFocusedElementId(tocObj);
584      if (focusedElementId) {
585        // Unset the previously focused element.
586        unsetFocusedElementId(tocObj);
587        // Get element to focus using the stored focused element id.
588        let focusedElement = tocObj.$element.find(`#${focusedElementId}`);
589        if (focusedElement.length === 0) {
590          // Use first focusable element if stored focused element not found.
591          focusedElement = getFirstFocusableElement(tocObj);
592        }
593        if (focusedElement) {
594          focusedElement.focus();
595        }
596      }
597    }, 0);
598  }
599
600  function isFocusingOut(event, element) {
601    return (
602      event.relatedTarget === null || !element.contains(event.relatedTarget)
603    );
604  }
605
606  /**
607   * @param {TocObject} tocObj - The tocObj object.
608   */
609  function manageFocus(tocObj) {
610    // A11y: store the currently focused toc element to be able to restore the
611    // focus on this element after a toc refresh caused by an Ajax page update.
612    tocObj.$element
613      .on('focusin', function focusIn(event) {
614        // We store the currently focused element id in the toc.
615        setFocusedElementId(tocObj, event.target.id);
616      })
617      .on('focusout', function focusOut(event) {
618        if (!tocObj.updating && isFocusingOut(event, this)) {
619          // Delete the stored focused element id when focus leaves the toc.
620          unsetFocusedElementId(tocObj);
621        }
622      });
623  }
624
625  /**
626   * Stick the toc.
627   *
628   * @param {TocObject} tocObj - The tocObj object.
629   */
630  function stickTheToc(tocObj) {
631    const topArray = [
632      tocObj.bodyPaddingTop > 0 ? `${tocObj.bodyPaddingTop}px` : null,
633      tocObj.opts.stickyOffset,
634    ].filter(Boolean);
635    tocObj.tocContainer.style.top =
636      topArray.length === 2
637        ? `calc(${topArray.join(' + ')})`
638        : topArray[0] || '0';
639  }
640
641  /**
642   * Remove eventual existing back to top links.
643   *
644   * @param {TocObject} tocObj - The tocObj object.
645   */
646  function cleanupBackToTopAndTocLinks(tocObj) {
647    const tocId = tocObj.element.id;
648    const topClass = tocObj.opts.backToTopClass;
649    const tocClass = tocObj.opts.backToTocClass;
650    const selector = `a.${topClass}[data-toc-id="${tocId}"], a.${tocClass}[data-toc-id="${tocId}"]`;
651    tocObj.containers.forEach((container) => {
652      container.querySelectorAll(selector).forEach((el) => {
653        el.remove();
654      });
655    });
656  }
657
658  /**
659   * Adds the is-visible class to back to top links.
660   *
661   * @param {TocObject} tocObj - The tocObj object.
662   */
663  function showBackToTopLinks(tocObj) {
664    const tocId = tocObj.element.id;
665    const selector = `a.${tocObj.opts.backToTopClass}[data-toc-id="${tocId}"]`;
666    tocObj.containers.forEach((container) => {
667      container.querySelectorAll(selector).forEach((el) => {
668        el.classList.add('is-visible');
669      });
670    });
671  }
672
673  /**
674   * Build ToC.
675   *
676   * @param {TocObject} tocObj - The tocObj object.
677   */
678  function buildToc(tocObj) {
679    tocObj.updating = true;
680    tocObj.$tocContainer.addClass('toc-js-container');
681    processHeadings(tocObj);
682    const tocLength = tocObj.$element.find('li').length;
683    const minimum = tocObj.opts.selectorsMinimum;
684    if (tocLength >= minimum) {
685      if (
686        (tocObj.opts.backToTop || tocObj.opts.backToToc) &&
687        tocObj.$headings.length > 1
688      ) {
689        buildTocAndTopLinks(tocObj);
690      }
691
692      if (tocObj.opts.sticky) {
693        tocObj.$tocContainer.addClass('sticky');
694        document.addEventListener(PADDING_TOP_UPDATE_EVENT, () => {
695          stickTheToc(tocObj);
696        });
697        refreshBodyPaddingTop(tocObj);
698      }
699
700      tocObj.tocContainer.classList.add('is-visible');
701      tocObj.tocContainer.classList.remove('is-hidden');
702      if (tocObj.element !== tocObj.tocContainer) {
703        tocObj.element.classList.add('is-visible');
704      }
705      showBackToTopLinks(tocObj);
706    } else {
707      tocObj.tocContainer.classList.remove('is-visible');
708      tocObj.tocContainer.classList.add('is-hidden');
709      if (tocObj.element !== tocObj.tocContainer) {
710        tocObj.element.classList.remove('is-visible');
711      }
712    }
713    tocObj.updating = false;
714  }
715
716  /**
717   * Build a TocObject instance for a specific ToC.
718   *
719   * @param {Object} element - TODO.
720   * @param {Object} opts - TODO.
721   * @return {TocObject} - The TocObject instance.
722   */
723  function initTocObject(element, opts) {
724    const tocContainer = opts.getTocContainer(element);
725    return {
726      opts,
727      element,
728      $element: $(element),
729      tocContainer,
730      $tocContainer: $(tocContainer),
731      containers: opts.getContainers(),
732      $headings: $(opts.getHeadings()),
733      highlightTimeout: null,
734      scrollToTimeout: null,
735      bodyPaddingTop: 0,
736      highlightOnScroll,
737      scrollTo,
738      scrollListener: undefined,
739      updating: false,
740      observableContainer: opts.getObservableContainer(),
741    };
742  }
743
744  /**
745   * @param {TocObject} tocObj - The tocObj object.
746   */
747  function updateTocObject(tocObj) {
748    tocObj.containers = tocObj.opts.getContainers();
749    tocObj.$headings = $(tocObj.opts.getHeadings());
750  }
751
752  /**
753   * @param {TocObject} tocObj - The tocObj object.
754   */
755  function addTocUpdateListener(tocObj) {
756    document.addEventListener(TOC_UPDATE_EVENT, () => {
757      updateTocObject(tocObj);
758      cleanupBackToTopAndTocLinks(tocObj);
759      buildToc(tocObj);
760      restoreFocus(tocObj);
761      if (tocObj.opts.highlightOnScroll) {
762        refreshHighlight(tocObj);
763      }
764    });
765  }
766
767  $.fn.toc_js = function fnTocJS(options) {
768    const opts = $.extend({}, jQuery.fn.toc_js.defaults, options);
769    return this.each(function eachToc() {
770      const tocObj = initTocObject(this, opts);
771      initBodyPaddingTopObserver(tocObj);
772      buildToc(tocObj);
773      if (tocObj.opts.highlightOnScroll) {
774        addHighlightScrollListener(tocObj);
775        refreshHighlight(tocObj);
776      }
777      if (tocObj.opts.ajaxPageUpdates) {
778        manageFocus(tocObj);
779        initContainerObserver(tocObj);
780        addTocUpdateListener(tocObj);
781      }
782    });
783  };
784
785  jQuery.fn.toc_js.defaults = {
786    container: 'body',
787    selectorsMinimum: 0,
788    listType: 'ul',
789    selectors: 'h1,h2,h3',
790    smoothScrolling: true,
791    scrollToOffset: null,
792    sticky: false,
793    stickyOffset: null,
794    prefix: 'toc',
795    activeClass: 'toc-active',
796    onHighlight() {},
797    highlightOnScroll: true,
798    highlightOffset: 0,
799    skipInvisibleHeadings: false,
800    useHeadingHtml: false,
801    collapsibleItems: false,
802    collapsibleExpanded: true,
803    backToTop: false,
804    backToTopLabel: 'Back to top',
805    headingFocus: false,
806    backToToc: false,
807    tocContainer: '',
808    ajaxPageUpdates: false,
809    observableSelector: '',
810    generateHeadingId(heading, prefix) {
811      if (heading.id.length) {
812        // Use existing id if not empty.
813        return heading.id;
814      }
815      // Source: https://www.30secondsofcode.org/js/s/remove-accents/
816      let headingId = heading.textContent
817        .toLowerCase()
818        .normalize('NFD')
819        .replace(/[\u0300-\u036f]/g, '')
820        .replace(/[^a-z0-9]/gi, ' ')
821        .replace(/\s+/g, '-')
822        .replace(/^-+|-+$/g, '');
823      headingId = prefix ? `${prefix}-${headingId}` : headingId;
824      // Handle potential headingId collision.
825      if (document.getElementById(headingId)) {
826        let newHeadingId;
827        let j = 2;
828        do {
829          newHeadingId = `${headingId}-${j}`;
830          j += 1;
831        } while (document.getElementById(newHeadingId));
832        headingId = newHeadingId;
833      }
834      return headingId;
835    },
836    headingHtml(heading) {
837      return heading.innerHTML;
838    },
839    headingText(heading) {
840      return heading.textContent;
841    },
842    itemClass(heading, prefix) {
843      const tagName = heading.tagName.toLowerCase();
844      return prefix ? `${prefix}-${tagName}` : tagName;
845    },
846    getContainers() {
847      return document.querySelectorAll(this.container || 'body');
848    },
849    getHeadings() {
850      const containers = this.getContainers();
851      const headings = [];
852      containers.forEach((container) => {
853        headings.push(
854          ...Array.from(container.querySelectorAll(this.selectors)),
855        );
856      });
857      return headings;
858    },
859    getTocContainer(element) {
860      if (this.tocContainer > '') {
861        return element.closest(this.tocContainer) || element;
862      }
863      return element;
864    },
865    getObservableContainer() {
866      if (this.observableSelector > '') {
867        return document.querySelector(this.observableSelector);
868      }
869      return null;
870    },
871  };
872})(jQuery);

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.