| 1 | (function () { |
| 2 | const docnav = document.querySelector('header.doc-nav'); |
| 3 | const active = docnav.querySelector('li.active'); |
| 4 | active?.scrollIntoView({ block: 'center', inline: 'nearest' }); |
| 5 | setupMobileToggle(); |
| 6 | setupDarkMode(); |
| 7 | setupScrollSpy(); |
| 8 | setupSearch(); |
| 9 | setupCollapse(); |
| 10 | setupCodeCopy(); |
| 11 | })(); |
| 12 | |
| 13 | function setupScrollSpy() { |
| 14 | const mainContent = document.querySelector('#main-content'); |
| 15 | const toc = mainContent.querySelector('.doc-toc'); |
| 16 | if (!toc) { |
| 17 | return; |
| 18 | } |
| 19 | const sections = mainContent.querySelectorAll('section'); |
| 20 | const sectionPositions = Array.from(sections).map((section) => section.offsetTop); |
| 21 | let lastActive = null; |
| 22 | let clickedScroll = false; |
| 23 | const handleScroll = debounce(() => { |
| 24 | if (clickedScroll) { |
| 25 | clickedScroll = false; |
| 26 | return; |
| 27 | } |
| 28 | if (lastActive) { |
| 29 | lastActive.classList.remove('active'); |
| 30 | } |
| 31 | for (const [i, position] of sectionPositions.entries()) { |
| 32 | if (position >= mainContent.scrollTop) { |
| 33 | const link = toc.querySelector('a[href="#' + sections[i].id + '"]'); |
| 34 | if (link) { |
| 35 | // Set current menu link as active |
| 36 | link.classList.add('active'); |
| 37 | const tocStart = toc.getBoundingClientRect().top + window.scrollY; |
| 38 | if (link.offsetTop > toc.scrollTop + toc.clientHeight - tocStart - 16) { |
| 39 | // Scroll the toc down if the active links position is below the bottom of the toc |
| 40 | toc.scrollTop = link.clientHeight + link.offsetTop - toc.clientHeight + tocStart + 10; |
| 41 | } else if (toc.scrollTop < 32 + tocStart) { |
| 42 | // Scroll to the top of the toc if having scrolled up into the last bit |
| 43 | toc.scrollTop = 0; |
| 44 | } else if (link.offsetTop < toc.scrollTop) { |
| 45 | // Scroll the toc up if the active links position is above the top of the toc |
| 46 | toc.scrollTop = link.offsetTop - 10; |
| 47 | } |
| 48 | } |
| 49 | lastActive = link; |
| 50 | break; |
| 51 | } |
| 52 | } |
| 53 | }, 10); |
| 54 | mainContent.addEventListener('scroll', handleScroll); |
| 55 | toc.querySelectorAll('a').forEach((a) => |
| 56 | a.addEventListener('click', () => { |
| 57 | if (lastActive) { |
| 58 | lastActive.classList.remove('active'); |
| 59 | } |
| 60 | a.classList.add('active'); |
| 61 | lastActive = a; |
| 62 | clickedScroll = true; |
| 63 | }), |
| 64 | ); |
| 65 | } |
| 66 | |
| 67 | function setupMobileToggle() { |
| 68 | document.getElementById('toggle-menu').addEventListener('click', () => { |
| 69 | const docNav = document.querySelector('header.doc-nav'); |
| 70 | const isHidden = docNav.classList.contains('hidden'); |
| 71 | docNav.classList.toggle('hidden'); |
| 72 | const search = docNav.querySelector('.search'); |
| 73 | const searchHasResults = search.classList.contains('has-results'); |
| 74 | if (isHidden && searchHasResults) { |
| 75 | search.classList.remove('mobile-hidden'); |
| 76 | } else { |
| 77 | search.classList.add('mobile-hidden'); |
| 78 | } |
| 79 | const content = docNav.querySelector('.content'); |
| 80 | content.classList.toggle('hidden'); |
| 81 | content.classList.toggle('show'); |
| 82 | }); |
| 83 | } |
| 84 | |
| 85 | function setupDarkMode() { |
| 86 | const html = document.querySelector('html'); |
| 87 | const darkModeToggle = document.getElementById('dark-mode-toggle'); |
| 88 | darkModeToggle.addEventListener('click', () => { |
| 89 | html.classList.toggle('dark'); |
| 90 | const isDarkModeEnabled = html.classList.contains('dark'); |
| 91 | localStorage.setItem('dark-mode', isDarkModeEnabled); |
| 92 | darkModeToggle.setAttribute('aria-checked', isDarkModeEnabled); |
| 93 | }); |
| 94 | } |
| 95 | |
| 96 | function setupSearch() { |
| 97 | const onInputChange = debounce((e) => { |
| 98 | const searchValue = e.target.value.toLowerCase(); |
| 99 | const docNav = document.querySelector('header.doc-nav'); |
| 100 | const menu = docNav.querySelector('.content'); |
| 101 | const search = docNav.querySelector('.search'); |
| 102 | if (searchValue === '') { |
| 103 | // reset to default |
| 104 | menu.style.display = ''; |
| 105 | if (!search.classList.contains('hidden')) { |
| 106 | search.classList.add('hidden'); |
| 107 | search.classList.remove('has-results'); |
| 108 | } |
| 109 | } else if (searchValue.length >= 2) { |
| 110 | // search for less than 2 characters can display too much results |
| 111 | search.innerHTML = ''; |
| 112 | menu.style.display = 'none'; |
| 113 | if (search.classList.contains('hidden')) { |
| 114 | search.classList.remove('hidden'); |
| 115 | search.classList.remove('mobile-hidden'); |
| 116 | search.classList.add('has-results'); |
| 117 | } |
| 118 | // cache length for performance |
| 119 | let foundModule = false; |
| 120 | const ul = document.createElement('ul'); |
| 121 | search.appendChild(ul); |
| 122 | for (const [i, title] of searchModuleIndex.entries()) { |
| 123 | // no toLowerCase needed because modules are always lowercase |
| 124 | if (title.indexOf(searchValue) === -1) { |
| 125 | continue; |
| 126 | } |
| 127 | foundModule = true; |
| 128 | // [description, link] |
| 129 | const data = searchModuleData[i]; |
| 130 | const el = createSearchResult({ |
| 131 | badge: 'module', |
| 132 | description: data[0], |
| 133 | link: data[1], |
| 134 | title: title, |
| 135 | }); |
| 136 | ul.appendChild(el); |
| 137 | } |
| 138 | if (foundModule) { |
| 139 | const hr = document.createElement('hr'); |
| 140 | hr.classList.add('separator'); |
| 141 | search.appendChild(hr); |
| 142 | } |
| 143 | let results = []; |
| 144 | for (const [i, title] of searchIndex.entries()) { |
| 145 | if (title.toLowerCase().indexOf(searchValue) === -1) { |
| 146 | continue; |
| 147 | } |
| 148 | // [badge, description, link] |
| 149 | const data = searchData[i]; |
| 150 | results.push({ |
| 151 | badge: data[0], |
| 152 | description: data[1], |
| 153 | link: data[2], |
| 154 | title: data[3] + ' ' + title, |
| 155 | }); |
| 156 | } |
| 157 | results.sort((a, b) => (a.title < b.title ? -1 : a.title > b.title ? 1 : 0)); |
| 158 | const ul_ = document.createElement('ul'); |
| 159 | search.appendChild(ul_); |
| 160 | results.forEach((result) => { |
| 161 | const el = createSearchResult(result); |
| 162 | ul_.appendChild(el); |
| 163 | }); |
| 164 | } |
| 165 | }); |
| 166 | const searchInput = document.querySelector('#search input'); |
| 167 | const url = document.location.toString(); |
| 168 | if (url.includes('?')) { |
| 169 | const query = |
| 170 | url |
| 171 | .split('?') |
| 172 | .slice(1) |
| 173 | .filter((p) => p.startsWith('q=')) |
| 174 | .map((p) => p.replace(/^q=/, ''))[0] || ''; |
| 175 | if (query) { |
| 176 | searchInput.value = query; |
| 177 | searchInput.focus(); |
| 178 | onInputChange({ target: { value: query } }); |
| 179 | } |
| 180 | } |
| 181 | const searchInputDiv = document.getElementById('search'); |
| 182 | searchInputDiv.addEventListener('input', onInputChange); |
| 183 | setupSearchKeymaps(); |
| 184 | } |
| 185 | |
| 186 | function setupSearchKeymaps() { |
| 187 | const searchInput = document.querySelector('#search input'); |
| 188 | const mainContent = document.querySelector('#main-content'); |
| 189 | const docnav = document.querySelector('header.doc-nav'); |
| 190 | // Keyboard shortcut indicator |
| 191 | const searchKeys = document.createElement('div'); |
| 192 | const modifierKeyPrefix = navigator.platform.includes('Mac') ? '⌘' : 'Ctrl'; |
| 193 | searchKeys.setAttribute('id', 'search-keys'); |
| 194 | searchKeys.innerHTML = '<kbd>' + modifierKeyPrefix + '</kbd><kbd>k</kbd>'; |
| 195 | searchInput.parentElement?.appendChild(searchKeys); |
| 196 | searchInput.addEventListener('focus', () => searchKeys.classList.add('hide')); |
| 197 | searchInput.addEventListener('blur', () => searchKeys.classList.remove('hide')); |
| 198 | // Global shortcuts to focus searchInput |
| 199 | document.addEventListener('keydown', (ev) => { |
| 200 | if (ev.key === '/' || ((ev.ctrlKey || ev.metaKey) && ev.key === 'k')) { |
| 201 | ev.preventDefault(); |
| 202 | searchInput.focus(); |
| 203 | } |
| 204 | }); |
| 205 | // Shortcuts while searchInput is focused |
| 206 | let selectedIdx = -1; |
| 207 | function selectResult(results, newIdx) { |
| 208 | if (selectedIdx !== -1) { |
| 209 | results[selectedIdx].classList.remove('selected'); |
| 210 | } |
| 211 | results[newIdx].classList.add('selected'); |
| 212 | results[newIdx].scrollIntoView({ behavior: 'instant', block: 'nearest', inline: 'nearest' }); |
| 213 | selectedIdx = newIdx; |
| 214 | } |
| 215 | searchInput.addEventListener('keydown', (ev) => { |
| 216 | const searchResults = document.querySelectorAll('.search .result'); |
| 217 | switch (ev.key) { |
| 218 | case 'Escape': |
| 219 | searchInput.blur(); |
| 220 | mainContent.focus(); |
| 221 | break; |
| 222 | case 'Enter': |
| 223 | if (!searchResults.length || selectedIdx === -1) break; |
| 224 | searchResults[selectedIdx].querySelector('a').click(); |
| 225 | break; |
| 226 | case 'ArrowDown': |
| 227 | ev.preventDefault(); |
| 228 | if (!searchResults.length) break; |
| 229 | if (selectedIdx >= searchResults.length - 1) { |
| 230 | // Cycle to first if last is selected |
| 231 | selectResult(searchResults, 0); |
| 232 | } else { |
| 233 | // Select next |
| 234 | selectResult(searchResults, selectedIdx + 1); |
| 235 | } |
| 236 | break; |
| 237 | case 'ArrowUp': |
| 238 | ev.preventDefault(); |
| 239 | if (!searchResults.length) break; |
| 240 | if (selectedIdx <= 0) { |
| 241 | // Cycle to last if first is selected (or select it if none is selected yet) |
| 242 | selectResult(searchResults, searchResults.length - 1); |
| 243 | } else { |
| 244 | // Select previous |
| 245 | selectResult(searchResults, selectedIdx - 1); |
| 246 | } |
| 247 | break; |
| 248 | default: |
| 249 | docnav.scroll(0, 0); |
| 250 | selectedIdx = -1; |
| 251 | } |
| 252 | }); |
| 253 | // Ensure initial keyboard navigability |
| 254 | mainContent.focus(); |
| 255 | } |
| 256 | |
| 257 | function createSearchResult(data) { |
| 258 | const li = document.createElement('li'); |
| 259 | li.classList.add('result'); |
| 260 | const a = document.createElement('a'); |
| 261 | a.href = data.link; |
| 262 | a.classList.add('link'); |
| 263 | li.appendChild(a); |
| 264 | const definition = document.createElement('div'); |
| 265 | definition.classList.add('definition'); |
| 266 | a.appendChild(definition); |
| 267 | if (data.description) { |
| 268 | const description = document.createElement('div'); |
| 269 | description.classList.add('description'); |
| 270 | description.textContent = data.description; |
| 271 | a.appendChild(description); |
| 272 | } |
| 273 | const title = document.createElement('span'); |
| 274 | title.classList.add('title'); |
| 275 | title.textContent = data.title; |
| 276 | definition.appendChild(title); |
| 277 | const badge = document.createElement('badge'); |
| 278 | badge.classList.add('badge'); |
| 279 | badge.textContent = data.badge; |
| 280 | definition.appendChild(badge); |
| 281 | return li; |
| 282 | } |
| 283 | |
| 284 | function setupCollapse() { |
| 285 | const dropdownArrows = document.querySelectorAll('.dropdown-arrow'); |
| 286 | dropdownArrows.forEach((arrow) => { |
| 287 | arrow.addEventListener('click', (e) => { |
| 288 | const parent = e.target.parentElement.parentElement.parentElement; |
| 289 | parent.classList.toggle('open'); |
| 290 | }); |
| 291 | }); |
| 292 | } |
| 293 | |
| 294 | function debounce(func, timeout) { |
| 295 | let timer; |
| 296 | return (...args) => { |
| 297 | const next = () => func(...args); |
| 298 | if (timer) { |
| 299 | clearTimeout(timer); |
| 300 | } |
| 301 | timer = setTimeout(next, timeout > 0 ? timeout : 300); |
| 302 | }; |
| 303 | } |
| 304 | |
| 305 | function setupCodeCopy() { |
| 306 | const pres = document.querySelectorAll('pre:not(.signature)'); |
| 307 | pres.forEach((pre) => { |
| 308 | const tempDiv = document.createElement('button'); |
| 309 | tempDiv.className = 'copy'; |
| 310 | tempDiv.innerHTML = |
| 311 | '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="rgba(173,184,194,1)"><path d="M6.9998 6V3C6.9998 2.44772 7.44752 2 7.9998 2H19.9998C20.5521 2 20.9998 2.44772 20.9998 3V17C20.9998 17.5523 20.5521 18 19.9998 18H16.9998V20.9991C16.9998 21.5519 16.5499 22 15.993 22H4.00666C3.45059 22 3 21.5554 3 20.9991L3.0026 7.00087C3.0027 6.44811 3.45264 6 4.00942 6H6.9998ZM5.00242 8L5.00019 20H14.9998V8H5.00242ZM8.9998 6H16.9998V16H18.9998V4H8.9998V6Z"></path></svg>'; |
| 312 | tempDiv.addEventListener('click', (e) => { |
| 313 | const parent = e.target; |
| 314 | var code = tempDiv.parentElement.querySelector('code'); |
| 315 | let i = Array.from(code.childNodes) |
| 316 | .map((r) => r.textContent) |
| 317 | .join(''); |
| 318 | navigator.clipboard.writeText(i); |
| 319 | var tmp = tempDiv.innerHTML; |
| 320 | tempDiv.innerHTML = 'Copied'; |
| 321 | window.setTimeout(function () { |
| 322 | tempDiv.innerHTML = tmp; |
| 323 | }, 1000); |
| 324 | }); |
| 325 | pre.insertAdjacentElement('afterbegin', tempDiv); |
| 326 | }); |
| 327 | } |
| 328 | |