1/** 2 * Keep the URL, the heading and the open section saying the same thing. 3 * 4 * Two directions. Arriving on a category URL opens the section it names, by triggering the same 5 * control a visitor would click â the accordion and the nav are the theme's, and driving them any 6 * other way would reproduce their behaviour rather than use it, which is how the two drift apart. 7 * Opening a section by hand writes that section's URL into the address bar, and its name into the 8 * heading where the route asks for that, so the page can be shared from wherever the visitor got 9 * to. 10 * 11 * Two modes, one per kind of layout: 12 * 13 * accordion the section's opener is clicked until the theme's accordion shows it open, then the 14 * opener of the media type inside it. 15 * anchor the nav link to the section is clicked, so the page scrolls, marks or switches 16 * exactly as it does for a visitor. A section no link leads to is scrolled into view. 17 * A `#fragment` link keeps doing what it does; only the address it leaves changes. 18 * 19 * Where each part is in the markup comes from the route's selectors. DEFAULTS is the markup of 20 * Telly-2023/rm-categories/telly.php; rm_categories_route_defaults() in 21 * inc/class-rm-categories-router.php holds the same values and sends only what a site changes, 22 * and tests/route-js-test.js holds the two copies together. 23 * 24 * replaceState rather than pushState: the list is one page, and pushing an entry per click 25 * makes Back walk the list instead of leaving the page. 26 * 27 * Build: 2026.09.13.03 28 */ 29(function (window, document) { 30 'use strict'; 31 32 var config = window.rmCategoriesRoute || {}; 33 34 if (!config.base) { 35 return; 36 } 37 38 /* The route's defaults, as rm_categories_route_defaults() encodes them. */ 39 var DEFAULTS = { 40 "level": "group", 41 "mode": "accordion", 42 "heading": "replace", 43 "selectors": { 44 "group": "li[data-rm-group]", 45 "group_opener": "a.opener", 46 "group_label": "a.opener span", 47 "media": "li[data-rm-media]", 48 "media_opener": "a.inner-opener", 49 "media_label": "a.inner-opener", 50 "active": "active", 51 "media_active": "active", 52 "heading": ".hero-area h1", 53 "list": [".category-holder", "ul.category"], 54 "nav": "" 55 } 56 }; 57 58 var level = config.level || DEFAULTS.level; 59 var mode = config.mode || DEFAULTS.mode; 60 var headingMode = config.heading || DEFAULTS.heading; 61 var sel = withDefaults(DEFAULTS.selectors, config.selectors); 62 63 /** The theme animates its accordion over 300ms; read the result once it has settled. */ 64 var SETTLE_MS = 360; 65 66 /** How often to retry the opening click, and how many times, while the theme binds its accordion. */ 67 var READY_POLL_MS = 150; 68 var READY_ATTEMPTS = 40; 69 70 /** 71 * How many times a nav link is clicked before it is taken to have no handler at all. 72 * 73 * A page binds its nav when the document is ready, moments after this script starts. A link 74 * nothing binds is a plain `#fragment` link, and waiting on it as long as on an accordion would 75 * leave the page standing still for six seconds before it scrolled. 76 */ 77 var LINK_ATTEMPTS = 10; 78 79 /** 80 * @param {Object} defaults 81 * @param {Object|undefined} overrides 82 * @returns {Object} Every key of defaults, each taken from overrides where that names it. 83 */ 84 function withDefaults(defaults, overrides) { 85 var out = {}; 86 87 Object.keys(defaults).forEach(function (key) { 88 out[key] = overrides && Object.prototype.hasOwnProperty.call(overrides, key) ? overrides[key] : defaults[key]; 89 }); 90 91 return out; 92 } 93 94 /** 95 * The first element any of the selectors finds, trying them in order. 96 * 97 * @param {string|string[]} selectors 98 * @param {Document|Element} [scope] 99 * @returns {Element|null} 100 */ 101 function first(selectors, scope) { 102 var list = Array.isArray(selectors) ? selectors : [selectors]; 103 var root = scope || document; 104 105 for (var i = 0; i < list.length; i++) { 106 var found = list[i] ? root.querySelector(list[i]) : null; 107 108 if (found) { 109 return found; 110 } 111 } 112 113 return null; 114 } 115 116 /** 117 * @param {Element|null} element 118 * @param {string} which 'group' or 'media'. 119 * @returns {string} The slug the element carries, or ''. 120 */ 121 function slugOf(element, which) { 122 return element ? element.getAttribute('data-rm-' + which) || '' : ''; 123 } 124 125 /** 126 * The section the accordion is currently showing. 127 * 128 * Read from the DOM rather than tracked, so a click the page handles some other way cannot 129 * leave this out of step with what is on screen. At media level a media type is looked for 130 * across the page when no grouping is open, since a list with no grouping has none to open. 131 * 132 * @returns {{group: Element|null, media: Element|null}} 133 */ 134 function openSection() { 135 var group = document.querySelector(sel.group + '.' + sel.active); 136 var scope = group || ('media' === level ? document : null); 137 var media = scope ? scope.querySelector(sel.media + '.' + sel.media_active) : null; 138 139 return { group: group, media: media }; 140 } 141 142 /** 143 * @param {string} groupSlug 144 * @param {string} mediaSlug 145 * @returns {string} Path for that section, or the bare category page when it names none. 146 */ 147 function pathFor(groupSlug, mediaSlug) { 148 var path = '/' + config.base.replace(/^\/+|\/+$/g, '') + '/'; 149 var lead = 'media' === level ? mediaSlug : groupSlug; 150 151 if (!lead) { 152 return path; 153 } 154 155 path += encodeURIComponent(lead) + '/'; 156 157 if ('group' === level && mediaSlug) { 158 path += encodeURIComponent(mediaSlug) + '/'; 159 } 160 161 return path; 162 } 163 164 /** 165 * The name to show as the heading for a section. 166 * 167 * Taken from the opener the visitor sees, so the heading and the control they clicked always 168 * read the same. Falls back to the page's own title when nothing is open. 169 * 170 * @param {Element|null} group 171 * @param {Element|null} media 172 * @returns {string} 173 */ 174 function headingFor(group, media) { 175 var opener = null; 176 177 if (media) { 178 opener = first(sel.media_label, media); 179 } else if (group) { 180 opener = first([sel.group_label, sel.group_opener], group); 181 } 182 183 return textOf(opener); 184 } 185 186 /** 187 * @param {Element|null} element 188 * @returns {string} Its text on one line, or the page's own title when there is none. 189 */ 190 function textOf(element) { 191 if (!element) { 192 return config.defaultHeading || ''; 193 } 194 195 return (element.textContent || '').replace(/\s+/g, ' ').trim() || config.defaultHeading || ''; 196 } 197 198 /** 199 * Put a section's name in the heading, where the route asks for that. 200 * 201 * @param {string} text 202 * @returns {void} 203 */ 204 function setHeading(text) { 205 var node = 'replace' === headingMode ? first(sel.heading) : null; 206 207 if (node) { 208 node.textContent = text; 209 } 210 } 211
212 /** 213 * True while a section is being opened by this script rather than by a visitor. 214 * 215 * Opening one fires the page's own click handling, which would otherwise be read back as a 216 * choice the visitor made and rewrite the URL they arrived on. 217 */ 218 var opening = false; 219 220 /** 221 * Write the current section into the address bar and the heading. 222 * 223 * Opening a grouping opens its first media type as well â the theme's accordion does that on 224 * its own â so a grouping click records the grouping, not whichever media type came with it. 225 * Only choosing a media type puts one in the URL. Without that, arriving at a grouping whose 226 * first media type shares its name rewrites the address to `.../branded-content/branded-content/`. 227 * 228 * @param {string} chosen 'group' or 'media'. 229 * @returns {void} 230 */ 231 function sync(chosen) { 232 var open = openSection(); 233 var media = ('media' === chosen) ? open.media : null; 234 var path = pathFor(slugOf(open.group, 'group'), slugOf(media, 'media')); 235 236 if (window.history && window.history.replaceState && window.location.pathname !== path) { 237 window.history.replaceState(null, '', path + window.location.search + window.location.hash); 238 } 239 240 setHeading(headingFor(open.group, media)); 241 clearBareHash(); 242 } 243 244 /** 245 * Open the section the URL names. 246 * 247 * By clicking the opener, not by setting classes: the accordion holds its own state and 248 * setting the classes underneath it would leave the next click toggling the wrong way. 249 * 250 * @returns {void} 251 */ 252 function openFromUrl() { 253 if (!('media' === level ? config.media : config.group)) { 254 return; 255 } 256 257 opening = true; 258 restoreTo = window.pageYOffset || document.documentElement.scrollTop || 0; 259 openWhenReady(0); 260 } 261 262 /** 263 * Where the page was before anything was opened for the visitor. 264 * 265 * Opening a section means clicking an opener, and those openers are anchors with `href="#"`. 266 * A click on one moves the page and leaves a bare `#` on the address bar, neither of which the 267 * visitor asked for â they asked for a URL, and arriving on it should look like arriving on the 268 * page with that section already open. 269 */ 270 var restoreTo = 0; 271 272 /** 273 * Open the section, retrying until the click has an effect. 274 * 275 * An opener is only an opener once the theme has bound its accordion to it, and that happens in 276 * a script of the theme's own further down the page. A click before then does nothing at all, 277 * and the accordion afterwards opens its first item â which reads as success whenever the URL 278 * names the first grouping, and as nothing happening for every other one. 279 * 280 * The readiness test is whether clicking worked, rather than whether some element the theme 281 * happens to use today is present: this library serves ten sites and the accordion is each 282 * theme's own, so any check written against one theme's internals is a check that silently 283 * stops applying to the rest. 284 * 285 * @param {number} attempt 286 * @returns {void} 287 */ 288 function openWhenReady(attempt) { 289 var group = groupToOpen(); 290 291 if (!group) { 292 // Nothing to open. Released, or every later click reads as programmatic. 293 opening = false; 294 295 return; 296 } 297 298 if (group === document || group.classList.contains(sel.active)) { 299 openMediaThenSettle(group); 300 301 return; 302 } 303 304 if (attempt >= READY_ATTEMPTS) { 305 opening = false; 306 307 return; 308 } 309 310 var opener = group.querySelector(sel.group_opener); 311 312 if (opener) { 313 opener.click(); 314 } 315 316 window.setTimeout(function () { 317 openWhenReady(attempt + 1); 318 }, READY_POLL_MS); 319 } 320 321 /** 322 * The grouping holding the section the URL names. 323 * 324 * Named by the URL at group level. At media level it is the grouping around the media type, or 325 * the whole page where the list has no grouping to open. 326 * 327 * @returns {Element|Document|null} 328 */ 329 function groupToOpen() { 330 if ('media' !== level) { 331 return document.querySelector(sel.group + '[data-rm-group="' + selector(config.group) + '"]'); 332 } 333 334 var media = document.querySelector(sel.media + '[data-rm-media="' + selector(config.media) + '"]'); 335 336 if (!media) { 337 return null; 338 } 339 340 return (media.parentNode && media.parentNode.closest ? media.parentNode.closest(sel.group) : null) || document; 341 } 342 343 /** 344 * @param {string} value 345 * @returns {string} Safe to interpolate into an attribute selector. 346 */ 347 function selector(value) { 348 return String(value).replace(/["\\]/g, ''); 349 } 350 351 /** 352 * The grouping is open. Choose the media type inside it, then stand down. 353 * 354 * @param {Element|Document} group 355 * @returns {void} 356 */ 357 function openMediaThenSettle(group) { 358 if (config.media) { 359 var media = group.querySelector(sel.media + '[data-rm-media="' + selector(config.media) + '"]'); 360 var inner = media ? media.querySelector(sel.media_opener) : null; 361 362 /* 363 * Opening a grouping opens its first media type on its own, so clicking the one that 364 * is already open would close it again. 365 */ 366 if (inner && !media.classList.contains(sel.media_active)) { 367 inner.click(); 368 } 369 } 370 371 /* 372 * Once the accordion has finished animating, bring the top of the list into view. Scrolling 373 * to the opened grouping instead lands partway down it â a grouping near the foot of the 374 * list puts its own heading above the viewport â and the list only reads from its top. 375 */ 376 window.setTimeout(function () { 377 scrollListIntoView(); 378 clearBareHash(); 379 opening = false;
380 }, SETTLE_MS); 381 } 382 383 /** 384 * Put the top of the category list just below anything fixed to the top of the window. 385 * 386 * Measured rather than configured. These themes hang a navigation bar at the top once the page 387 * scrolls, so nothing is stuck there to measure until after the scroll has happened â which is 388 * why this scrolls, looks, and then corrects. A fixed offset per site would be a number that is 389 * right until someone changes the header. 390 * 391 * @returns {void} 392 */ 393 function scrollListIntoView() { 394 var list = first(sel.list); 395 396 if (!list) { 397 window.scrollTo(0, restoreTo); 398 399 return; 400 } 401 402 scrollIntoPlace(list); 403 } 404 405 /** 406 * Put the top of an element just below anything fixed to the top of the window. 407 * 408 * @param {Element} element 409 * @returns {void} 410 */ 411 function scrollIntoPlace(element) { 412 var allowance = parseInt((window.rmCategories || {}).scrollOffset, 10) || 0; 413 var top = element.getBoundingClientRect().top + (window.pageYOffset || 0) - allowance; 414 415 window.scrollTo(0, top < 0 ? 0 : top); 416 417 /* 418 * The site's own allowance covers furniture the theme pins only once the page has moved, 419 * which is not there to be measured beforehand. Anything that does turn out to be stuck is 420 * taken as a floor rather than added to it, or the two would be subtracted twice. 421 */ 422 window.setTimeout(function () { 423 var gap = element.getBoundingClientRect().top; 424 var stuck = stuckToTop(); 425 426 if (stuck > gap) { 427 window.scrollBy(0, -(stuck - gap)); 428 } 429 }, 60); 430 } 431 432 /** 433 * How far down the window the topmost fixed or sticky furniture reaches. 434 * 435 * @returns {number} Pixels, 0 when nothing is stuck to the top. 436 */ 437 function stuckToTop() { 438 var lowest = 0; 439 440 [].forEach.call(document.body.getElementsByTagName('*'), function (element) { 441 var style = window.getComputedStyle(element); 442 443 if ('fixed' !== style.position && 'sticky' !== style.position) { 444 return; 445 } 446 447 var box = element.getBoundingClientRect(); 448 449 // Only what actually covers the top of the window, and only full-width furniture. 450 if (box.top > 4 || box.bottom <= 0 || box.width < window.innerWidth * 0.5) { 451 return; 452 } 453 454 if (box.bottom > lowest) { 455 lowest = box.bottom; 456 } 457 }); 458 459 return Math.round(lowest); 460 } 461 462 /** 463 * Take the `#` an opener click leaves behind off the address bar. 464 * 465 * Only a bare one. A real fragment is something the visitor or another script put there and is 466 * none of this code's business. 467 * 468 * @returns {void} 469 */ 470 function clearBareHash() { 471 if ('#' !== window.location.hash || !window.history || !window.history.replaceState) { 472 return; 473 } 474 475 window.history.replaceState(null, '', window.location.pathname + window.location.search); 476 } 477 478 /** 479 * @returns {void} 480 */ 481 function startAccordion() { 482 document.addEventListener('click', function (event) { 483 if (opening || !event.target || !event.target.closest) { 484 return; 485 } 486 487 /* 488 * The markup nests the media-type openers inside the grouping's own anchor, so an 489 * inner click matches both selectors. The inner one is the specific answer. 490 */ 491 var inner = event.target.closest(sel.media_opener); 492 var outer = inner ? null : event.target.closest(sel.group_opener); 493 494 if (!inner && !outer) { 495 return; 496 } 497 498 window.setTimeout(function () { 499 sync(inner ? 'media' : 'group'); 500 }, SETTLE_MS); 501 }); 502 503 openFromUrl(); 504 } 505 506 /* 507 * --------------------------------------------------------------------------------------------- 508 * Anchor mode: the page's nav is the opener. 509 * --------------------------------------------------------------------------------------------- 510 */ 511 512 /** The nav link this script is clicking, and whether the page's own handler took the click. */ 513 var following = null; 514 var handled = false; 515 516 /** 517 * @returns {string} Selector for the links a visitor chooses sections with. 518 */ 519 function navSelector() { 520 return sel.nav || (window.rmCategories || {}).navLink || ''; 521 } 522 523 /** 524 * @param {Element} link 525 * @returns {string} The id a same-page link points at, or ''. 526 */ 527 function fragmentOf(link) { 528 var href = link.getAttribute('href') || ''; 529 530 return '#' === href.charAt(0) ? href.slice(1) : ''; 531 } 532 533 /** 534 * The section a URL names. 535 * 536 * A media type is looked for inside its grouping when the URL names both, since two groupings 537 * can each hold a media type of the same name. 538 * 539 * @param {string} groupSlug 540 * @param {string} mediaSlug 541 * @returns {Element|null} 542 */ 543 function sectionFor(groupSlug, mediaSlug) { 544 if (!mediaSlug) { 545 return groupSlug ? document.querySelector(sel.group + '[data-rm-group="' + selector(groupSlug) + '"]') : null; 546 } 547 548 var found = document.querySelectorAll(sel.media + '[data-rm-media="' + selector(mediaSlug) + '"]'); 549 550 for (var i = 0; i < found.length; i++) { 551 if (!groupSlug || found[i].closest('[data-rm-group="' + selector(groupSlug) + '"]')) { 552 return found[i]; 553 } 554 } 555 556 return null; 557 } 558 559 /** 560 * The nav link for a section: one the theme marked with its slug, else one pointing at the 561 * section's id, else one whose fragment is the slug itself, the form the page's own `#fragment` 562 * addresses take. 563 * 564 * @param {string} which 'group' or 'media'. 565 * @param {string} slug 566 * @param {Element|null} section 567 * @returns {Element|null} 568 */ 569 function linkFor(which, slug, section) { 570 var nav = navSelector(); 571 var links = nav ? document.querySelectorAll(nav) : []; 572 var i; 573 574 for (i = 0; i < links.length; i++) { 575 if (slugOf(links[i], which) === slug) { 576 return links[i]; 577 } 578 } 579 580 for (i = 0; i < links.length; i++) { 581 var fragment = fragmentOf(links[i]); 582 583 if (fragment && ((section && section.id === fragment) || fragment === slug)) { 584 return links[i]; 585 } 586 } 587 588 return null; 589 } 590 591 /** 592 * Whether following a link lands on the section. 593 * 594 * A link marked for the slug, or one pointing at the section itself, is the visitor's own way 595 * there. One pointing at another element â a grouping's first section, when the URL names a 596 * later media type inside the grouping â is marked rather than followed. 597 * 598 * @param {Element} link 599 * @param {string} which 600 * @param {string} slug 601 * @param {Element|null} section 602 * @returns {boolean} 603 */ 604 function leadsTo(link, which, slug, section) { 605 if (!section || slugOf(link, which) === slug) { 606 return true; 607 } 608 609 return document.getElementById(fragmentOf(link)) === section; 610 } 611 612 /** 613 * Click a nav link until something on the page handles the click. 614 * 615 * Handled means default-prevented by the time the click reaches the window, which the shared 616 * anchor nav and every theme nav do. Until then this prevents it instead, so the browser does 617 * not jump to the fragment while the page is still binding its nav. 618 * 619 * @param {Element} link 620 * @param {number} attempt 621 * @param {function(boolean)} done Told whether the page took the click. 622 * @returns {void} 623 */ 624 function follow(link, attempt, done) { 625 following = link; 626 handled = false;
627 link.click(); 628 following = null; 629 630 if (handled || attempt + 1 >= LINK_ATTEMPTS) { 631 done(handled); 632 633 return; 634 } 635 636 window.setTimeout(function () { 637 follow(link, attempt + 1, done); 638 }, READY_POLL_MS); 639 } 640 641 /** 642 * Bring a section into view and mark the nav item leading to it, the way the shared anchor nav 643 * does for a link a visitor follows. 644 * 645 * @param {Element|null} section 646 * @param {Element|null} link 647 * @returns {void} 648 */ 649 function reveal(section, link) { 650 var nav = window.rmCategoriesNav; 651 652 // A page that draws its own list loads no shared nav, and the section is scrolled to directly. 653 if (!nav) { 654 if (section) { 655 scrollIntoPlace(section); 656 } 657 658 return; 659 } 660 661 if (link && nav.setActive) { 662 nav.setActive(link); 663 } 664 665 if (section && nav.scrollTo) { 666 nav.scrollTo(section); 667 } 668 } 669 670 /** 671 * Open the section the URL names. 672 * 673 * @returns {void} 674 */ 675 function openAnchor() { 676 var slug = 'media' === level ? config.media : (config.media || config.group); 677 var which = config.media ? 'media' : 'group'; 678 679 if (!slug) { 680 return; 681 } 682 683 var section = sectionFor('media' === level ? '' : config.group, config.media); 684 var link = linkFor(which, slug, section); 685 686 if (!link && !section) { 687 return; 688 } 689 690 opening = true; 691 692 if (link && leadsTo(link, which, slug, section)) { 693 follow(link, 0, function (taken) { 694 if (!taken) { 695 reveal(section || document.getElementById(fragmentOf(link)), link); 696 } 697 698 settleAnchor(link); 699 }); 700 701 return; 702 } 703 704 reveal(section, link); 705 settleAnchor(link); 706 } 707 708 /** 709 * Once the page has moved, take the fragment the link left on the address bar off it again: 710 * the path names the section already. 711 * 712 * @param {Element|null} link 713 * @returns {void} 714 */ 715 function settleAnchor(link) { 716 window.setTimeout(function () { 717 clearFragment(link ? link.getAttribute('href') || '' : ''); 718 clearBareHash(); 719 opening = false; 720 }, SETTLE_MS); 721 } 722 723 /** 724 * Remove the fragment from the address bar when it is exactly the one a nav link wrote there. 725 * 726 * @param {string} href The link's own href. 727 * @returns {void} 728 */ 729 function clearFragment(href) { 730 if ('#' !== href.charAt(0) || window.location.hash !== href || !window.history || !window.history.replaceState) { 731 return; 732 } 733 734 window.history.replaceState(null, '', window.location.pathname + window.location.search); 735 } 736 737 /** 738 * Which section a nav link chose. 739 * 740 * Read from the link where the theme marked it, and otherwise from the element its fragment 741 * points at. At group level a media type goes into the URL only when the link names one itself: 742 * a grouping's link points at the grouping's first section, which carries that section's media 743 * type as well, and the visitor chose the grouping. 744 * 745 * @param {Element} link 746 * @returns {{group: string, media: string}} 747 */ 748 function chosenBy(link) { 749 var fragment = fragmentOf(link); 750 var target = fragment ? document.getElementById(fragment) : null; 751 var media = slugOf(link, 'media'); 752 var group = slugOf(link, 'group'); 753 754 if ('media' === level) { 755 return { group: '', media: media || slugOf(target && target.closest('[data-rm-media]'), 'media') }; 756 } 757 758 if (!group) { 759 var holder = target || (media ? sectionFor('', media) : null); 760 761 group = slugOf(holder && holder.closest('[data-rm-group]'), 'group'); 762 } 763 764 return { group: group, media: group ? media : '' }; 765 } 766 767 /** 768 * Write the section a nav link chose into the address bar, and its name into the heading. 769 * 770 * A link that names no section â one that shows every media type at once, say â writes the 771 * bare category page. The fragment the link itself put on the address bar comes off, since the 772 * path names the section; any other fragment, and the query, stay. 773 * 774 * @param {Element} link 775 * @returns {void} 776 */ 777 function syncAnchor(link) { 778 var chosen = chosenBy(link); 779 var path = pathFor(chosen.group, chosen.media); 780 var href = link.getAttribute('href') || ''; 781 var hash = window.location.hash === href ? '' : window.location.hash; 782 783 if (window.history && window.history.replaceState && window.location.pathname + window.location.hash !== path + hash) { 784 window.history.replaceState(null, '', path + window.location.search + hash); 785 } 786 787 setHeading(chosen.group || chosen.media ? textOf(link) : config.defaultHeading || ''); 788 clearBareHash(); 789 } 790 791 /** 792 * @returns {void} 793 */ 794 function startAnchor() { 795 /* 796 * Whether the page handled a click this script made. On the window, so it runs after every 797 * listener on the document, whenever the page bound it. 798 */ 799 window.addEventListener('click', function (event) { 800 if (!following || event.target !== following) { 801 return; 802 } 803 804 handled = event.defaultPrevented; 805 806 if (!handled) { 807 event.preventDefault(); 808 } 809 }); 810 811 document.addEventListener('click', function (event) { 812 // A click can land on a text node, which has no closest(). 813 var from = event.target && 1 === event.target.nodeType ? event.target : event.target && event.target.parentElement; 814 var nav = navSelector(); 815 var link = !opening && from && nav ? from.closest(nav) : null; 816 817 // Only a link that addresses a section on this page says which section was chosen. 818 if (!link || (!fragmentOf(link) && !slugOf(link, 'media') && !slugOf(link, 'group'))) { 819 return; 820 } 821 822 window.setTimeout(function () { 823 syncAnchor(link); 824 }, SETTLE_MS); 825 }); 826 827 // After the document's own ready handlers have run, which is where a page binds its nav. 828 window.setTimeout(openAnchor, 0); 829 } 830 831 /** 832 * @returns {void} 833 */ 834 function start() { 835 if ('anchor' === mode) { 836 startAnchor(); 837 838 return; 839 } 840 841 startAccordion(); 842 } 843 844 if (document.readyState === 'loading') { 845 document.addEventListener('DOMContentLoaded', start); 846 } else { 847 start(); 848 } 849})(window, document);
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.