1 2const hasTransition = (element) => { 3 const {transition} = getComputedStyle(element); 4 return transition !== 'none 0s ease 0s'; 5} 6 7const waitUntilTransitioned = async (element) => { 8 9 return new Promise((resolve) => { 10 11 if(!hasTransition(element)) { 12 resolve(); 13 } 14 15 const cb = () => { 16 removeEventListener('transitionend', cb); 17 resolve(); 18 } 19 20 element.addEventListener('transitionend', cb); 21 }) 22 23} 24 25const trapFocus = (element) => { 26 27 const focusable = element.querySelectorAll('a[href]:not(.hidden a[href]), button:not(.hidden button)'); 28 const firstFocusable = focusable[0]; 29 const lastFocusable = focusable[focusable.length - 1]; 30 31 element.addEventListener('keydown', (e) => { 32 33 if (e.key === 'Tab') { 34 if (e.shiftKey) { 35 if (document.activeElement === firstFocusable) { 36 e.preventDefault(); 37 lastFocusable.focus(); 38 } 39 } else { 40 if (document.activeElement === lastFocusable) { 41 e.preventDefault(); 42 firstFocusable.focus(); 43 } 44 } 45 } 46 }); 47 48 firstFocusable.focus(); 49} 50 51 52const openPanelMenu = async () => { 53 const popover = document.getElementById('panel-menu-popover'); 54 55 const backdrop = document.querySelector('.panel-menu-backdrop'); 56 const menu = document.querySelector('.panel-menu'); 57 58 popover.classList.add('opening'); 59 await Promise.all([waitUntilTransitioned(backdrop), waitUntilTransitioned(menu)]); 60 popover.classList.replace('opening', 'open'); 61 trapFocus(menu); 62 63} 64 65const closePanelMenu = async () => { 66 const popover = document.getElementById('panel-menu-popover'); 67 68 const backdrop = document.querySelector('.panel-menu-backdrop'); 69 const menu = document.querySelector('.panel-menu'); 70 71 popover.classList.replace('open', 'closing'); 72 await Promise.all([waitUntilTransitioned(backdrop), waitUntilTransitioned(menu)]); 73 popover.classList.remove('closing'); 74 75 document.querySelector('.panel-menu-toggle')?.focus(); 76} 77 78const togglePanelMenu = () => isPanelMenuOpen() ? closePanelMenu() : openPanelMenu(); 79 80const isPanelMenuOpen = () => { 81 return document.getElementById('panel-menu-popover')?.classList.contains('open') ?? false; 82} 83 84 85const registerPanelMenu = () => { 86 87 document.querySelectorAll('.panel-menu-toggle').forEach((toggler) => { 88 toggler.addEventListener('click', togglePanelMenu); 89 }); 90 91 document.querySelector('.panel-menu-backdrop')?.addEventListener('click', closePanelMenu); 92 93 document.addEventListener('keyup', ({key}) => { 94 95 if(key === 'Escape') { 96 closePanelMenu(); 97 } 98 }) 99 100 101 document.querySelectorAll('[data-panel-menu-link-to]').forEach((menuLink) => { 102 103 menuLink.addEventListener('click', async (event) => { 104 event.preventDefault(); 105 event.stopPropagation(); 106 const {panelMenuLinkTo} = menuLink.dataset; 107 const currentPanel = menuLink.closest('.menu-panel'); 108 const nextPanel = document.getElementById(`menu-panel-${panelMenuLinkTo}`); 109 currentPanel.classList.add('hidden'); 110 nextPanel.classList.remove('hidden'); 111 112 await waitUntilTransitioned(nextPanel); 113 trapFocus(document.querySelector('.panel-menu')) 114 }) 115 116 }) 117} 118 119 120document.addEventListener('DOMContentLoaded', registerPanelMenu);
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.