1(function (jtd, undefined) { 2 3// Event handling 4 5jtd.addEvent = function(el, type, handler) { 6 if (el.attachEvent) el.attachEvent('on'+type, handler); else el.addEventListener(type, handler); 7} 8jtd.removeEvent = function(el, type, handler) { 9 if (el.detachEvent) el.detachEvent('on'+type, handler); else el.removeEventListener(type, handler); 10} 11jtd.onReady = function(ready) { 12 // in case the document is already rendered 13 if (document.readyState!='loading') ready(); 14 // modern browsers 15 else if (document.addEventListener) document.addEventListener('DOMContentLoaded', ready); 16 // IE <= 8 17 else document.attachEvent('onreadystatechange', function(){ 18 if (document.readyState=='complete') ready(); 19 }); 20} 21 22// Show/hide mobile menu 23 24function initNav() { 25 jtd.addEvent(document, 'click', function(e){ 26 var target = e.target; 27 while (target && !(target.classList && target.classList.contains('nav-list-expander'))) { 28 target = target.parentNode; 29 } 30 if (target) { 31 e.preventDefault(); 32 target.ariaExpanded = target.parentNode.classList.toggle('active'); 33 } 34 }); 35 36 const siteNav = document.getElementById('site-nav'); 37 const mainHeader = document.getElementById('main-header'); 38 const menuButton = document.getElementById('menu-button'); 39 40 disableHeadStyleSheets(); 41 42 jtd.addEvent(menuButton, 'click', function(e){ 43 e.preventDefault(); 44 45 if (menuButton.classList.toggle('nav-open')) { 46 siteNav.classList.add('nav-open'); 47 mainHeader.classList.add('nav-open'); 48 menuButton.ariaExpanded = true; 49 } else { 50 siteNav.classList.remove('nav-open'); 51 mainHeader.classList.remove('nav-open'); 52 menuButton.ariaExpanded = false; 53 } 54 }); 55} 56 57// The <head> element is assumed to include the following stylesheets: 58// - a <link> to /assets/css/just-the-docs-head-nav.css, 59// with id 'jtd-head-nav-stylesheet' 60// - a <style> containing the result of _includes/css/activation.scss.liquid. 61// To avoid relying on the order of stylesheets (which can change with HTML 62// compression, user-added JavaScript, and other side effects), stylesheets 63// are only interacted with via ID 64 65function disableHeadStyleSheets() { 66 const headNav = document.getElementById('jtd-head-nav-stylesheet'); 67 if (headNav) { 68 headNav.disabled = true; 69 } 70 71 const activation = document.getElementById('jtd-nav-activation'); 72 if (activation) { 73 activation.disabled = true; 74 } 75} 76// Site search 77 78function initSearch() { 79 var request = new XMLHttpRequest(); 80 request.open('GET', '/assets/js/search-data.json', true); 81 82 request.onload = function(){ 83 if (request.status >= 200 && request.status < 400) { 84 var docs = JSON.parse(request.responseText); 85 86 lunr.tokenizer.separator = /[\s\-/]+/ 87 88 var index = lunr(function(){ 89 this.ref('id'); 90 this.field('title', { boost: 200 }); 91 this.field('content', { boost: 2 }); 92 this.field('relUrl'); 93 this.metadataWhitelist = ['position'] 94 95 for (var i in docs) { 96 97 this.add({ 98 id: i, 99 title: docs[i].title, 100 content: docs[i].content, 101 relUrl: docs[i].relUrl 102 }); 103 } 104 }); 105 106 searchLoaded(index, docs); 107 } else { 108 console.log('Error loading ajax request. Request status:' + request.status); 109 } 110 }; 111 112 request.onerror = function(){ 113 console.log('There was a connection error'); 114 }; 115 116 request.send(); 117} 118 119function searchLoaded(index, docs) { 120 var index = index; 121 var docs = docs; 122 var searchInput = document.getElementById('search-input'); 123 var searchResults = document.getElementById('search-results'); 124 var mainHeader = document.getElementById('main-header'); 125 var currentInput; 126 var currentSearchIndex = 0; 127 128 function showSearch() { 129 document.documentElement.classList.add('search-active'); 130 } 131 132 function hideSearch() { 133 document.documentElement.classList.remove('search-active'); 134 } 135 136 function update() { 137 currentSearchIndex++; 138 139 var input = searchInput.value; 140 if (input === '') { 141 hideSearch(); 142 } else { 143 showSearch(); 144 // scroll search input into view, workaround for iOS Safari 145 window.scroll(0, -1); 146 setTimeout(function(){ window.scroll(0, 0); }, 0); 147 } 148 if (input === currentInput) { 149 return; 150 } 151 currentInput = input; 152 searchResults.innerHTML = ''; 153 if (input === '') { 154 return; 155 } 156 157 var results = index.query(function (query) { 158 var tokens = lunr.tokenizer(input) 159 query.term(tokens, { 160 boost: 10 161 }); 162 query.term(tokens, { 163 wildcard: lunr.Query.wildcard.TRAILING 164 }); 165 }); 166 167 if ((results.length == 0) && (input.length > 2)) { 168 var tokens = lunr.tokenizer(input).filter(function(token, i) { 169 return token.str.length < 20; 170 }) 171 if (tokens.length > 0) { 172 results = index.query(function (query) { 173 query.term(tokens, { 174 editDistance: Math.round(Math.sqrt(input.length / 2 - 1)) 175 }); 176 }); 177 } 178 } 179 180 if (results.length == 0) { 181 var noResultsDiv = document.createElement('div'); 182 noResultsDiv.classList.add('search-no-result'); 183 noResultsDiv.innerText = 'No results found'; 184 searchResults.appendChild(noResultsDiv); 185 186 } else { 187 var resultsList = document.createElement('ul'); 188 resultsList.classList.add('search-results-list'); 189 searchResults.appendChild(resultsList); 190 191 addResults(resultsList, results, 0, 10, 100, currentSearchIndex); 192 } 193 194 function addResults(resultsList, results, start, batchSize, batchMillis, searchIndex) { 195 if (searchIndex != currentSearchIndex) { 196 return; 197 } 198 for (var i = start; i < (start + batchSize); i++) { 199 if (i == results.length) { 200 return; 201 } 202 addResult(resultsList, results[i]); 203 } 204 setTimeout(function() { 205 addResults(resultsList, results, start + batchSize, batchSize, batchMillis, searchIndex); 206 }, batchMillis); 207 } 208 209 function addResult(resultsList, result) { 210 var doc = docs[result.ref]; 211 212 var resultsListItem = document.createElement('li'); 213 resultsListItem.classList.add('search-results-list-item'); 214 resultsList.appendChild(resultsListItem); 215 216 var resultLink = document.createElement('a'); 217 resultLink.classList.add('search-result'); 218 resultLink.setAttribute('href', doc.url); 219 resultsListItem.appendChild(resultLink); 220 221 var resultTitle = document.createElement('div'); 222 resultTitle.classList.add('search-result-title'); 223 resultLink.appendChild(resultTitle); 224 225 // note: the SVG svg-doc is only loaded as a Jekyll include if site.search_enabled is true; see _includes/icons/icons.html 226 var resultDoc = document.createElement('div');
227 resultDoc.classList.add('search-result-doc'); 228 resultDoc.innerHTML = '<svg viewBox="0 0 24 24" class="search-result-icon" aria-hidden="true"><use xlink:href="#svg-doc"></use></svg>'; 229 resultTitle.appendChild(resultDoc); 230 231 var resultDocTitle = document.createElement('div'); 232 resultDocTitle.classList.add('search-result-doc-title'); 233 resultDocTitle.innerHTML = doc.doc; 234 resultDoc.appendChild(resultDocTitle); 235 var resultDocOrSection = resultDocTitle; 236 237 if (doc.doc != doc.title) { 238 resultDoc.classList.add('search-result-doc-parent'); 239 var resultSection = document.createElement('div'); 240 resultSection.classList.add('search-result-section'); 241 resultSection.innerHTML = doc.title; 242 resultTitle.appendChild(resultSection); 243 resultDocOrSection = resultSection; 244 } 245 246 var metadata = result.matchData.metadata; 247 var titlePositions = []; 248 var contentPositions = []; 249 for (var j in metadata) { 250 var meta = metadata[j]; 251 if (meta.title) { 252 var positions = meta.title.position; 253 for (var k in positions) { 254 titlePositions.push(positions[k]); 255 } 256 } 257 if (meta.content) { 258 var positions = meta.content.position; 259 for (var k in positions) { 260 var position = positions[k]; 261 var previewStart = position[0]; 262 var previewEnd = position[0] + position[1]; 263 var ellipsesBefore = true; 264 var ellipsesAfter = true; 265 for (var k = 0; k < 5; k++) { 266 var nextSpace = doc.content.lastIndexOf(' ', previewStart - 2); 267 var nextDot = doc.content.lastIndexOf('. ', previewStart - 2); 268 if ((nextDot >= 0) && (nextDot > nextSpace)) { 269 previewStart = nextDot + 1; 270 ellipsesBefore = false; 271 break; 272 } 273 if (nextSpace < 0) { 274 previewStart = 0; 275 ellipsesBefore = false; 276 break; 277 } 278 previewStart = nextSpace + 1; 279 } 280 for (var k = 0; k < 10; k++) { 281 var nextSpace = doc.content.indexOf(' ', previewEnd + 1); 282 var nextDot = doc.content.indexOf('. ', previewEnd + 1); 283 if ((nextDot >= 0) && (nextDot < nextSpace)) { 284 previewEnd = nextDot; 285 ellipsesAfter = false; 286 break; 287 } 288 if (nextSpace < 0) { 289 previewEnd = doc.content.length; 290 ellipsesAfter = false; 291 break; 292 } 293 previewEnd = nextSpace; 294 } 295 contentPositions.push({ 296 highlight: position, 297 previewStart: previewStart, previewEnd: previewEnd, 298 ellipsesBefore: ellipsesBefore, ellipsesAfter: ellipsesAfter 299 }); 300 } 301 } 302 } 303 304 if (titlePositions.length > 0) { 305 titlePositions.sort(function(p1, p2){ return p1[0] - p2[0] }); 306 resultDocOrSection.innerHTML = ''; 307 addHighlightedText(resultDocOrSection, doc.title, 0, doc.title.length, titlePositions); 308 } 309 310 if (contentPositions.length > 0) { 311 contentPositions.sort(function(p1, p2){ return p1.highlight[0] - p2.highlight[0] }); 312 var contentPosition = contentPositions[0]; 313 var previewPosition = { 314 highlight: [contentPosition.highlight], 315 previewStart: contentPosition.previewStart, previewEnd: contentPosition.previewEnd, 316 ellipsesBefore: contentPosition.ellipsesBefore, ellipsesAfter: contentPosition.ellipsesAfter 317 }; 318 var previewPositions = [previewPosition]; 319 for (var j = 1; j < contentPositions.length; j++) { 320 contentPosition = contentPositions[j]; 321 if (previewPosition.previewEnd < contentPosition.previewStart) { 322 previewPosition = { 323 highlight: [contentPosition.highlight], 324 previewStart: contentPosition.previewStart, previewEnd: contentPosition.previewEnd, 325 ellipsesBefore: contentPosition.ellipsesBefore, ellipsesAfter: contentPosition.ellipsesAfter 326 } 327 previewPositions.push(previewPosition); 328 } else { 329 previewPosition.highlight.push(contentPosition.highlight); 330 previewPosition.previewEnd = contentPosition.previewEnd; 331 previewPosition.ellipsesAfter = contentPosition.ellipsesAfter; 332 } 333 } 334 335 var resultPreviews = document.createElement('div'); 336 resultPreviews.classList.add('search-result-previews'); 337 resultLink.appendChild(resultPreviews); 338 339 var content = doc.content; 340 for (var j = 0; j < Math.min(previewPositions.length, 3); j++) { 341 var position = previewPositions[j]; 342 343 var resultPreview = document.createElement('div'); 344 resultPreview.classList.add('search-result-preview'); 345 resultPreviews.appendChild(resultPreview); 346 347 if (position.ellipsesBefore) { 348 resultPreview.appendChild(document.createTextNode('... ')); 349 } 350 addHighlightedText(resultPreview, content, position.previewStart, position.previewEnd, position.highlight); 351 if (position.ellipsesAfter) { 352 resultPreview.appendChild(document.createTextNode(' ...')); 353 } 354 } 355 } 356 var resultRelUrl = document.createElement('span'); 357 resultRelUrl.classList.add('search-result-rel-url'); 358 resultRelUrl.innerText = doc.relUrl; 359 resultTitle.appendChild(resultRelUrl); 360 } 361 362 function addHighlightedText(parent, text, start, end, positions) { 363 var index = start; 364 for (var i in positions) { 365 var position = positions[i]; 366 var span = document.createElement('span'); 367 span.innerHTML = text.substring(index, position[0]); 368 parent.appendChild(span); 369 index = position[0] + position[1]; 370 var highlight = document.createElement('span'); 371 highlight.classList.add('search-result-highlight'); 372 highlight.innerHTML = text.substring(position[0], index); 373 parent.appendChild(highlight); 374 } 375 var span = document.createElement('span'); 376 span.innerHTML = text.substring(index, end); 377 parent.appendChild(span); 378 } 379 } 380 381 jtd.addEvent(searchInput, 'focus', function(){ 382 setTimeout(update, 0); 383 }); 384 385 // When the search bar is *not* focused, it should be hidden. This code 386 // manages that - which is a bit tricky given that we can't just rely on 387 // focusout, since we could be re-focusing within the search itself. 388 const updateSearchFocus = function(evt) { 389 const nextFocusedElement = evt.relatedTarget; 390 391 // Re-focusing on search bar - "keep focus" 392 if (nextFocusedElement.id === 'search-input') return; 393 394 // Re-focusing on the next search result element - "keep focus" 395 if (nextFocusedElement.classList.contains('search-result')) return; 396 397 // Otherwise, we're not focused on the search bar anymore. Hide! 398 hideSearch(); 399 } 400 401 searchInput.addEventListener('focusout', updateSearchFocus); 402 searchResults.addEventListener('focusout', updateSearchFocus); 403 404 jtd.addEvent(searchInput, 'keyup', function(e){ 405 switch (e.keyCode) { 406 case 27: // When esc key is pressed, hide the results and clear the field 407 searchInput.value = ''; 408 break; 409 case 38: // arrow up 410 case 40: // arrow down 411 case 13: // enter 412 e.preventDefault(); 413 return; 414 } 415 update(); 416 }); 417 418 jtd.addEvent(searchInput, 'keydown', function(e){ 419 switch (e.keyCode) { 420 case 38: // arrow up 421 e.preventDefault(); 422 var active = document.querySelector('.search-result.active'); 423 if (active) { 424 active.classList.remove('active'); 425 if (active.parentElement.previousSibling) { 426 var previous = active.parentElement.previousSibling.querySelector('.search-result'); 427 previous.classList.add('active'); 428 } 429 } 430 return; 431 case 40: // arrow down 432 e.preventDefault(); 433 var active = document.querySelector('.search-result.active'); 434 if (active) { 435 if (active.parentElement.nextSibling) { 436 var next = active.parentElement.nextSibling.querySelector('.search-result'); 437 active.classList.remove('active'); 438 next.classList.add('active'); 439 } 440 } else { 441 var next = document.querySelector('.search-result'); 442 if (next) { 443 next.classList.add('active'); 444 } 445 } 446 return; 447 case 13: // enter 448 e.preventDefault(); 449 var active = document.querySelector('.search-result.active'); 450 if (active) { 451 active.click(); 452 } else { 453 var first = document.querySelector('.search-result'); 454 if (first) { 455 first.click(); 456 } 457 } 458 return; 459 } 460 }); 461 462 jtd.addEvent(document, 'click', function(e){ 463 if (e.target != searchInput) { 464 hideSearch(); 465 } 466 }); 467} 468 469// Switch theme 470 471jtd.getTheme = function() { 472 var cssFileHref = document.querySelector('[rel="stylesheet"]').getAttribute('href'); 473 return cssFileHref.substring(cssFileHref.lastIndexOf('-') + 1, cssFileHref.length - 4); 474} 475 476jtd.setTheme = function(theme) { 477 var cssFile = document.querySelector('[rel="stylesheet"]'); 478 cssFile.setAttribute('href', '/assets/css/just-the-docs-' + theme + '.css'); 479} 480 481// Note: pathname can have a trailing slash on a local jekyll server 482// and not have the slash on GitHub Pages 483 484function navLink() { 485 var pathname = document.location.pathname; 486 487 var navLink = document.getElementById('site-nav').querySelector('a[href="' + pathname + '"]'); 488 if (navLink) { 489 return navLink; 490 } 491 492 // The `permalink` setting may produce navigation links whose `href` ends with `/` or `.html`. 493 // To find these links when `/` is omitted from or added to pathname, or `.html` is omitted: 494 495 if (pathname.endsWith('/') && pathname != '/') { 496 pathname = pathname.slice(0, -1); 497 } 498 499 if (pathname != '/') { 500 navLink = document.getElementById('site-nav').querySelector('a[href="' + pathname + '"], a[href="' + pathname + '/"], a[href="' + pathname + '.html"]'); 501 if (navLink) { 502 return navLink; 503 } 504 } 505 506 return null; // avoids `undefined` 507} 508 509// Scroll site-nav to ensure the link to the current page is visible 510 511function scrollNav() { 512 const targetLink = navLink(); 513 if (targetLink) { 514 targetLink.scrollIntoView({ block: "center" }); 515 targetLink.removeAttribute('href'); 516 } 517} 518 519// Find the nav-list-link that refers to the current page 520// then make it and all enclosing nav-list-item elements active. 521 522function activateNav() { 523 var target = navLink(); 524 if (target) { 525 target.classList.toggle('active', true); 526 } 527 while (target) { 528 while (target && !(target.classList && target.classList.contains('nav-list-item'))) { 529 target = target.parentNode; 530 } 531 if (target) { 532 target.classList.toggle('active', true); 533 target = target.parentNode; 534 } 535 } 536} 537 538// Document ready 539 540jtd.onReady(function(){ 541 if (document.getElementById('site-nav')) { 542 initNav(); 543 activateNav(); 544 scrollNav(); 545 } 546 initSearch(); 547}); 548 549// Accessibility: set tabindex=0 on each code highlight block, so screenreaders 550// can focus over (particularly important if there's horizontal scroll) 551// see: https://dequeuniversity.com/rules/axe/4.9/scrollable-region-focusable?application=a
551xeAPI 552 553jtd.onReady(() => { 554 document 555 .querySelectorAll("div.highlight") 556 .forEach(codeBlock => codeBlock.setAttribute("tabindex", "0")); 557}); 558 559// Copy button on code 560 561jtd.onReady(function(){ 562 563 if (!window.isSecureContext) { 564 console.log('Window does not have a secure context, therefore code clipboard copy functionality will not be available. For more details see https://web.dev/async-clipboard/#security-and-permissions'); 565 return; 566 } 567 568 var codeBlocks = document.querySelectorAll('div.highlighter-rouge, div.listingblock > div.content, figure.highlight'); 569 570 // note: the SVG svg-copied and svg-copy is only loaded as a Jekyll include if site.enable_copy_code_button is true; see _includes/icons/icons.html 571 var svgCopied = '<svg viewBox="0 0 24 24" class="copy-icon"><use xlink:href="#svg-copied"></use></svg>'; 572 var svgCopy = '<svg viewBox="0 0 24 24" class="copy-icon"><use xlink:href="#svg-copy"></use></svg>'; 573 574 codeBlocks.forEach(codeBlock => { 575 var copyButton = document.createElement('button'); 576 var timeout = null; 577 copyButton.type = 'button'; 578 copyButton.ariaLabel = 'Copy code to clipboard'; 579 copyButton.innerHTML = svgCopy; 580 codeBlock.append(copyButton); 581 582 copyButton.addEventListener('click', function () { 583 if(timeout === null) { 584 var code = (codeBlock.querySelector('pre:not(.lineno, .highlight)') || codeBlock.querySelector('code')).innerText; 585 window.navigator.clipboard.writeText(code); 586 587 copyButton.innerHTML = svgCopied; 588 589 var timeoutSetting = 4000; 590 591 timeout = setTimeout(function () { 592 copyButton.innerHTML = svgCopy; 593 timeout = null; 594 }, timeoutSetting); 595 } 596 }); 597 }); 598 599}); 600 601})(window.jtd = window.jtd || {}); 602 603
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.