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.