|
| 1 | +/* |
| 2 | +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. |
| 3 | +
|
| 4 | +Licensed under the Apache License, Version 2.0 (the "License"). |
| 5 | +You may not use this file except in compliance with the License. |
| 6 | +You may obtain a copy of the License at |
| 7 | +
|
| 8 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +
|
| 10 | +Unless required by applicable law or agreed to in writing, software |
| 11 | +distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | +See the License for the specific language governing permissions and |
| 14 | +limitations under the License. |
| 15 | +*/ |
| 16 | +const nonResourceSubHeadings = [ |
| 17 | + 'client', |
| 18 | + 'waiters', |
| 19 | + 'paginators', |
| 20 | + 'resources', |
| 21 | + 'examples' |
| 22 | +]; |
| 23 | +// Checks if an html doc name matches a service class name. |
| 24 | +function isValidServiceName(serviceClassName) { |
| 25 | + const pageTitle = document.getElementsByTagName('h1')[0]; |
| 26 | + const newDocName = pageTitle.innerText.replace('#', ''); |
| 27 | + return newDocName.toLowerCase() === serviceClassName; |
| 28 | +} |
| 29 | +// Checks if all elements of the split fragment are valid. |
| 30 | +// Fragment items should only contain alphanumerics, hyphens, & underscores. |
| 31 | +// A fragment should also only be redirected if it contain 3-5 items. |
| 32 | +function isValidFragment(splitFragment) { |
| 33 | + const regex = /^[a-z0-9-_]+$/i; |
| 34 | + for (index in splitFragment) { |
| 35 | + if (!regex.test(splitFragment[index])) { |
| 36 | + return false; |
| 37 | + } |
| 38 | + } |
| 39 | + return splitFragment.length >= 1 && splitFragment.length < 5; |
| 40 | +} |
| 41 | +// Checks if a name is a possible resource name. |
| 42 | +function isValidResource(name, serviceDocName) { |
| 43 | + return name !== serviceDocName.replaceAll('-', '') && !nonResourceSubHeadings.includes(name); |
| 44 | +} |
| 45 | +// Reroutes previously existing links to the new path. |
| 46 | +// Old: <root_url>/reference/services/s3.html#S3.Client.delete_bucket |
| 47 | +// New: <root_url>/reference/services/s3/client/delete_bucket.html |
| 48 | +// This must be done client side since the fragment (#S3.Client.delete_bucket) is never |
| 49 | +// passed to the server. |
| 50 | +(function () { |
| 51 | + const currentPath = window.location.pathname.split('/'); |
| 52 | + const fragment = window.location.hash.substring(1); |
| 53 | + const splitFragment = fragment.split('.').map(part => part.replace(/serviceresource/i, 'service-resource')); |
| 54 | + // Only redirect when viewing a top-level service page. |
| 55 | + if (isValidFragment(splitFragment) && currentPath[currentPath.length - 2] === 'services') { |
| 56 | + const serviceDocName = currentPath[currentPath.length - 1].replace('.html', ''); |
| 57 | + if (splitFragment.length > 1) { |
| 58 | + splitFragment[0] = splitFragment[0].toLowerCase(); |
| 59 | + splitFragment[1] = splitFragment[1].toLowerCase(); |
| 60 | + } |
| 61 | + let newPath; |
| 62 | + if (splitFragment.length >= 3 && isValidServiceName(splitFragment[0])) { |
| 63 | + splitFragment[0] = serviceDocName; |
| 64 | + newPath = `${ splitFragment.slice(0, 3).join('/') }.html#${ splitFragment.length > 3 ? fragment : '' }`; |
| 65 | + } else if (splitFragment.length == 2 && isValidResource(splitFragment[1].toLowerCase(), serviceDocName)) { |
| 66 | + newPath = `${ splitFragment.join('/') }/index.html#${ fragment }`; |
| 67 | + } else if (splitFragment.length == 1 && isValidResource(splitFragment[0], serviceDocName)) { |
| 68 | + newPath = `${ serviceDocName }/${ splitFragment.join('/') }/index.html`; |
| 69 | + } else { |
| 70 | + return; |
| 71 | + } |
| 72 | + window.location.assign(newPath); |
| 73 | + } |
| 74 | +}()); |
| 75 | +// Given a service name, we apply the html classes which indicate a current page to the corresponsing list item. |
| 76 | +// Before: <li class="toctree-l2"><a class="reference internal" href="../../acm.html">ACM</a></li> |
| 77 | +// After: <li class="toctree-l2 current current-page"><a class="reference internal" href="../../acm.html">ACM</a></li> |
| 78 | +function makeServiceLinkCurrent(serviceName) { |
| 79 | + const servicesSection = [...document.querySelectorAll('a')].find( |
| 80 | + e => e.innerHTML.includes('Available Services') |
| 81 | + ).parentElement; |
| 82 | + var linkElement = servicesSection.querySelectorAll(`a[href*="../${ serviceName }.html"]`); |
| 83 | + if (linkElement.length === 0) { |
| 84 | + linkElement = servicesSection.querySelectorAll(`a[href="#"]`)[0]; |
| 85 | + } else { |
| 86 | + linkElement = linkElement[0]; |
| 87 | + } |
| 88 | + let linkParent = linkElement.parentElement; |
| 89 | + linkParent.classList.add('current'); |
| 90 | + linkParent.classList.add('current-page'); |
| 91 | +} |
| 92 | +const currentPagePath = window.location.pathname.split('/'); |
| 93 | +const codeBlockSelector = 'div.highlight pre'; |
| 94 | +// Expands the "Available Services" sub-menu in the side-bar when viewing |
| 95 | +// nested doc pages and highlights the corresponding service list item. |
| 96 | +function expandSubMenu() { |
| 97 | + if (currentPagePath.includes('services')) { |
| 98 | + document.getElementById('toctree-checkbox-11').checked = true; |
| 99 | + // Example Nested Path: /reference/services/<service_name>/client/<operation_name>.html |
| 100 | + const serviceNameIndex = currentPagePath.indexOf('services') + 1; |
| 101 | + const serviceName = currentPagePath[serviceNameIndex]; |
| 102 | + makeServiceLinkCurrent(serviceName); |
| 103 | + } |
| 104 | +} |
| 105 | +// Allows code blocks to be scrollable by keyboard only users. |
| 106 | +function makeCodeBlocksScrollable() { |
| 107 | + const codeCells = document.querySelectorAll(codeBlockSelector); |
| 108 | + codeCells.forEach(codeCell => { |
| 109 | + codeCell.tabIndex = 0; |
| 110 | + }); |
| 111 | +} |
| 112 | +// Determines which of the two table-of-contents menu labels is visible. |
| 113 | +function determineVisibleTocOpenMenu() { |
| 114 | + const mediaQuery = window.matchMedia('(max-width: 67em)'); |
| 115 | + return mediaQuery.matches ? 'toc-menu-open-sm' : 'toc-menu-open-md'; |
| 116 | +} |
| 117 | + |
| 118 | +// A mapping of current to next focus id's. For example, We want a corresponsing |
| 119 | +// menu's close button to be highlighted after a menu is opened with a keyboard. |
| 120 | +const NEXT_FOCUS_ID_MAP = { |
| 121 | + 'nav-menu-open': 'nav-menu-close', |
| 122 | + 'nav-menu-close': 'nav-menu-open', |
| 123 | + 'toc-menu-open-sm': 'toc-menu-close', |
| 124 | + 'toc-menu-open-md': 'toc-menu-close', |
| 125 | + 'toc-menu-close': determineVisibleTocOpenMenu(), |
| 126 | +}; |
| 127 | + |
| 128 | +// Toggles the visibility of a sidebar menu to prevent keyboard focus on hidden elements. |
| 129 | +function toggleSidebarMenuVisibility(elementQuery, inputQuery) { |
| 130 | + const sidebarElement = document.querySelector(elementQuery); |
| 131 | + const sidebarInput = document.querySelector(inputQuery); |
| 132 | + sidebarInput.addEventListener('change', () => { |
| 133 | + setTimeout( |
| 134 | + () => { |
| 135 | + sidebarElement.classList.toggle('hide-sidebar', !sidebarInput.checked); |
| 136 | + }, |
| 137 | + sidebarInput.checked ? 0 : 250, |
| 138 | + ); |
| 139 | + }); |
| 140 | + window.matchMedia('(max-width: 67em)').addEventListener('change', (event) => { |
| 141 | + NEXT_FOCUS_ID_MAP['toc-menu-close'] = determineVisibleTocOpenMenu(); |
| 142 | + if (!event.matches) { |
| 143 | + document |
| 144 | + .querySelector('.sidebar-drawer') |
| 145 | + .classList.remove('hide-sidebar'); |
| 146 | + } |
| 147 | + }); |
| 148 | + window.matchMedia('(max-width: 82em)').addEventListener('change', (event) => { |
| 149 | + if (!event.matches) { |
| 150 | + document.querySelector('.toc-drawer').classList.remove('hide-sidebar'); |
| 151 | + } |
| 152 | + }); |
| 153 | +} |
| 154 | + |
| 155 | +// Activates labels when a user focuses on them and clicks "Enter". |
| 156 | +// Also highlights the next appropriate input label. |
| 157 | +function activateLabelOnEnter() { |
| 158 | + const labels = document.querySelectorAll('label'); |
| 159 | + labels.forEach((element) => { |
| 160 | + element.setAttribute('tabindex', '0'); |
| 161 | + element.addEventListener('keypress', (event) => { |
| 162 | + if (event.key === 'Enter') { |
| 163 | + const targetId = element.getAttribute('for'); |
| 164 | + document.getElementById(targetId).click(); |
| 165 | + const nextFocusId = NEXT_FOCUS_ID_MAP[element.id]; |
| 166 | + if (nextFocusId) { |
| 167 | + // Timeout is needed to let the label become visible. |
| 168 | + setTimeout(() => { |
| 169 | + document.getElementById(nextFocusId).focus(); |
| 170 | + }, 250); |
| 171 | + } |
| 172 | + } |
| 173 | + }); |
| 174 | + }); |
| 175 | +} |
| 176 | + |
| 177 | +// Improves accessibility for keyboard-only users. |
| 178 | +function setupKeyboardFriendlyNavigation() { |
| 179 | + activateLabelOnEnter(); |
| 180 | + toggleSidebarMenuVisibility('.toc-drawer', '#__toc'); |
| 181 | + toggleSidebarMenuVisibility('.sidebar-drawer', '#__navigation'); |
| 182 | +} |
| 183 | +// Functions to run after the DOM loads. |
| 184 | +function runAfterDOMLoads() { |
| 185 | + expandSubMenu(); |
| 186 | + makeCodeBlocksScrollable(); |
| 187 | + setupKeyboardFriendlyNavigation(); |
| 188 | +} |
| 189 | +// Run a function after the DOM loads. |
| 190 | +function ready(fn) { |
| 191 | + if (document.readyState !== 'loading') { |
| 192 | + fn(); |
| 193 | + } else { |
| 194 | + document.addEventListener('DOMContentLoaded', fn); |
| 195 | + } |
| 196 | +} |
| 197 | +ready(runAfterDOMLoads); |
0 commit comments