1(function (Drupal, once) { 2 'use strict'; 3 4 Drupal.behaviors.tkBaseFrontpage = { 5 attach(context) { 6 once('tk-base-frontpage', '[data-tk-frontpage]', context).forEach((root) => { 7 const focus = root.querySelector('.frontpage-focus'); 8 const results = root.querySelector('[data-tk-frontpage-results]'); 9 const error = root.querySelector('[data-tk-frontpage-error]'); 10 const sortControls = Array.from(root.querySelectorAll('[data-tk-frontpage-sort]')); 11 const typeControls = Array.from(root.querySelectorAll('[data-tk-frontpage-type]')); 12 const filterMenu = root.querySelector('.frontpage-filter-menu'); 13 const filterPanel = filterMenu.querySelector('.frontpage-filter-menu__panel'); 14 const reset = root.querySelector('[data-tk-frontpage-reset]'); 15 const links = Array.from(root.querySelectorAll('[data-tk-frontpage-channel][data-tk-frontpage-fragment]')); 16 const initialHtml = results.innerHTML; 17 let activeLink = reset; 18 let requestController = null; 19 let transition = null; 20 21 const header = document.getElementById('region_header'); 22 const navigation = root.querySelector('.frontpage-focus__navigation'); 23 const updateOffsets = () => { 24 const headerBottom = header ? header.offsetTop + header.offsetHeight : 0; 25 const hideHeader = activeLink !== reset 26 && window.matchMedia('(min-height: 361px)').matches 27 && focus.getBoundingClientRect().top <= headerBottom; 28 if (header) { 29 header.classList.toggle('frontpage-header--hidden', hideHeader); 30 header.inert = hideHeader; 31 } 32 focus.style.setProperty('--frontpage-header-height', `${hideHeader ? 0 : headerBottom}px`); 33 focus.style.setProperty('--frontpage-viewport-width', `${document.documentElement.clientWidth}px`); 34 if (filterMenu.open) { 35 focus.style.setProperty('--frontpage-filter-top', `${filterPanel.getBoundingClientRect().top}px`); 36 } 37 }; 38 updateOffsets(); 39 const resizeObserver = new ResizeObserver(updateOffsets); 40 resizeObserver.observe(navigation); 41 if (header) { 42 resizeObserver.observe(header); 43 } 44 let offsetFrame = 0; 45 const scheduleOffsets = () => { 46 if (!offsetFrame) { 47 offsetFrame = window.requestAnimationFrame(() => { 48 offsetFrame = 0; 49 updateOffsets(); 50 }); 51 } 52 }; 53 window.addEventListener('scroll', scheduleOffsets, { passive: true }); 54 window.addEventListener('resize', scheduleOffsets); 55 filterMenu.addEventListener('toggle', updateOffsets); 56 navigation.addEventListener('transitionend', updateOffsets); 57 58 const load = async (link, resetOverview = false, focusResult = false) => { 59 requestController?.abort(); 60 transition?.cancel(); 61 requestController = new AbortController(); 62 const controller = requestController; 63 let timedOut = false; 64 const timeout = window.setTimeout(() => { 65 timedOut = true; 66 controller.abort(); 67 if (requestController === controller) { 68 transition?.cancel(); 69 } 70 }, 10000); 71 const endpoint = new URL(link.dataset.tkFrontpageFragment, window.location.origin); 72 endpoint.searchParams.set('sort', sortControls.find((control) => control.checked).value); 73 endpoint.searchParams.set('type', typeControls.find((control) => control.checked).value); 74 focus.classList.add('is-loading'); 75 results.setAttribute('aria-busy', 'true'); 76 error.hidden = true; 77 78 try { 79 let html = initialHtml; 80 if (!resetOverview) { 81 const response = await fetch(endpoint, { 82 headers: { 83 Accept: 'application/json', 84 'X-Requested-With': 'XMLHttpRequest', 85 }, 86 signal: controller.signal, 87 }); 88 if (!response.ok) { 89 throw new Error(`Frontpage fragment failed with ${response.status}.`); 90 } 91 const payload = await response.json(); 92 html = payload.html; 93 } 94 controller.signal.throwIfAborted(); 95 const duration = window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : 180; 96 transition = results.animate([{ opacity: 1 }, { opacity: 0 }], { duration, fill: 'forwards' }); 97 await transition.finished; 98 controller.signal.throwIfAborted(); 99 results.innerHTML = html; 100 activeLink = link; 101 reset.classList.toggle('is-active', link === reset); 102 links.forEach((item) => item.classList.toggle('is-active', item === link));
103 [reset, ...links].forEach((item) => item.setAttribute('aria-current', item === link ? 'true' : 'false')); 104 if (focusResult) { 105 results.querySelector('.frontpage-section__title a')?.focus({ preventScroll: true }); 106 } 107 root.querySelector('[data-tk-frontpage-status]').textContent = [ 108 link.textContent.trim(), 109 typeControls.find((control) => control.checked).parentElement.textContent.trim(), 110 sortControls.find((control) => control.checked).parentElement.textContent.trim(), 111 ].join(' · '); 112 updateOffsets(); 113 focus.scrollIntoView({ behavior: 'instant', block: 'start' }); 114 updateOffsets(); 115 transition.cancel(); 116 transition = results.animate([{ opacity: 0 }, { opacity: 1 }], { duration, fill: 'forwards' }); 117 await transition.finished; 118 } 119 catch (requestError) { 120 if (requestController === controller && (requestError.name !== 'AbortError' || timedOut)) { 121 error.hidden = false; 122 } 123 } 124 finally { 125 window.clearTimeout(timeout); 126 if (requestController === controller) { 127 transition?.cancel(); 128 transition = null; 129 focus.classList.remove('is-loading'); 130 results.setAttribute('aria-busy', 'false'); 131 } 132 } 133 }; 134 135 root.addEventListener('click', (event) => { 136 if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) { 137 return; 138 } 139 const trigger = event.target.closest('[data-tk-frontpage-channel]'); 140 const link = links.find((item) => item.dataset.tkFrontpageChannel === trigger?.dataset.tkFrontpageChannel); 141 if (!link) { 142 return; 143 } 144 event.preventDefault(); 145 if (trigger !== link && navigation.contains(link)) { 146 link.focus({ preventScroll: true }); 147 link.parentElement.scrollLeft = link.offsetLeft - link.parentElement.offsetLeft; 148 } 149 load(link, false, !navigation.contains(link)); 150 }); 151 reset.addEventListener('click', (event) => { 152 if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) { 153 return; 154 } 155 event.preventDefault(); 156 activeLink = reset; 157 sortControls.find((control) => control.value === 'newest').checked = true; 158 typeControls.find((control) => control.value === 'all').checked = true; 159 load(reset, true); 160 }); 161 [...sortControls, ...typeControls].forEach((control) => control.addEventListener('change', () => { 162 filterMenu.open = false; 163 filterMenu.querySelector('summary').focus({ preventScroll: true }); 164 load(activeLink); 165 })); 166 filterMenu.addEventListener('keydown', (event) => { 167 if (event.key === 'Escape' && filterMenu.open) { 168 filterMenu.open = false; 169 filterMenu.querySelector('summary').focus({ preventScroll: true }); 170 } 171 }); 172 document.addEventListener('click', (event) => { 173 if (filterMenu.open && !filterMenu.contains(event.target)) { 174 filterMenu.open = false; 175 } 176 }); 177 }); 178 }, 179 }; 180})(Drupal, once);
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.