1/** 2 * Rail arrows. 3 * 4 * The rail itself is CSS â `overflow-x: auto` plus scroll snapping, in 5 * sass/components/_rail.scss. Touch, trackpad, keyboard and the scrollbar all 6 * work with no script at all. This file exists only so the two arrow buttons 7 * the templates already render keep working, and so they can grey out at the 8 * ends the way Swiper's did. 9 * 10 * It replaces swiper-bundle.min.js: 147 KB of minified JavaScript, for three 11 * carousels that all wanted the same strip of cards. 12 * 13 * The markup is not uniform, because three modules wrote it independently: 14 * 15 * .swiper.swiper-article arrows INSIDE the rail's own container 16 * .stories-onpage-wrap arrows are siblings of the rail 17 * .swiper-podcast + .podcast-nav arrows in a sibling block further out 18 * 19 * so the controls are found by walking up from the scroller until an ancestor 20 * holds both of them. `[class*="swiper-button-next"]` matches the plain 21 * `swiper-button-next`, the stories' `story-swiper-button-next` and the 22 * podcast's `podcast-button-next swiper-button-next` in one selector. 23 */ 24(function () { 25 'use strict'; 26 27 // How far up to look for the arrows. Four is enough for the deepest of the 28 // three layouts and stops a rail without arrows from stealing another 29 // rail's on a page that holds several. 30 var MAX_DEPTH = 4; 31 32 // scrollLeft is fractional and the snap positions rarely land on a whole 33 // pixel, so the ends are compared with a tolerance rather than for equality. 34 var EDGE_TOLERANCE = 2; 35
36 function findControls(scroller) { 37 var node = scroller.parentElement; 38 39 for (var depth = 0; depth < MAX_DEPTH && node; depth++) { 40 var next = node.querySelector('[class*="swiper-button-next"]'); 41 var prev = node.querySelector('[class*="swiper-button-prev"]'); 42 43 if (next && prev) { 44 return { next: next, prev: prev }; 45 } 46 47 node = node.parentElement; 48 } 49 50 return null; 51 } 52 53 /** One card plus its gutter â the distance one press should travel. */ 54 function step(scroller) { 55 var slide = scroller.querySelector('.swiper-slide'); 56 57 if (!slide) { 58 return scroller.clientWidth; 59 } 60 61 var gap = parseFloat(getComputedStyle(scroller).columnGap); 62 63 return slide.getBoundingClientRect().width + (isNaN(gap) ? 0 : gap); 64 } 65 66 function setUp(scroller) { 67 var controls = findControls(scroller); 68 69 if (!controls) { 70 return; 71 } 72 73 function refresh() { 74 // scrollWidth includes the overflowing cards; when the two are equal 75 // everything already fits and neither arrow has anywhere to go. 76 var max = scroller.scrollWidth - scroller.clientWidth; 77 var atStart = scroller.scrollLeft <= EDGE_TOLERANCE; 78 var atEnd = scroller.scrollLeft >= max - EDGE_TOLERANCE; 79 80 controls.prev.classList.toggle('swiper-button-disabled', atStart); 81 controls.next.classList.toggle('swiper-button-disabled', atEnd || max <= 0); 82 controls.prev.setAttribute('aria-disabled', atStart ? 'true' : 'false'); 83 controls.next.setAttribute('aria-disabled', (atEnd || max <= 0) ? 'true' : 'false'); 84 } 85 86 // Where the rail is heading while a smooth scroll is still running. 87 // Without this, a second press computes its step from the position the 88 // animation happens to be passing through, so pressing next three times 89 // quickly advances by less than three cards â measured: three presses 90 // moved two cards and left the last one unreachable. 91 var pending = null; 92 93 function move(direction) { 94 var max = scroller.scrollWidth - scroller.clientWidth; 95 var base = pending === null ? scroller.scrollLeft : pending; 96 var target = Math.max(0, Math.min(max, base + direction * step(scroller))); 97 98 pending = target; 99 // `behavior: 'auto'` means "whatever scroll-behavior says", which is 100 // smooth for most readers and instant for anyone who asked for 101 // reduced motion â see the media query in _rail.scss. 102 scroller.scrollTo({ left: target, behavior: 'auto' }); 103 } 104 105 function clearPending() { 106 pending = null; 107 } 108 109 // The templates render these as <div>, which is neither focusable nor 110 // announced. Giving them a role and a tab stop here keeps the change out 111 // of four PHP files that would each have to be edited identically. 112 [controls.prev, controls.next].forEach(function (button, index) { 113 if (!button.hasAttribute('role')) { 114 button.setAttribute('role', 'button'); 115 } 116 117 if (!button.hasAttribute('tabindex')) { 118 button.setAttribute('tabindex', '0'); 119 } 120 121 if (!button.hasAttribute('aria-label')) { 122 button.setAttribute('aria-label', index === 0 ? 'Zurück' : 'Weiter'); 123 } 124 125 function activate(event) { 126 if (button.classList.contains('swiper-button-disabled')) { 127 return; 128 } 129 130 event.preventDefault(); 131 move(index === 0 ? -1 : 1); 132 } 133 134 button.addEventListener('click', activate); 135 button.addEventListener('keydown', function (event) { 136 if (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') { 137 activate(event); 138 } 139 }); 140 }); 141 142 scroller.addEventListener('scroll', refresh, { passive: true }); 143 144 // The moment the reader takes over â a flick, a wheel, a drag â the 145 // queued target is stale and the next arrow press must start from where 146 // they actually left the rail. 147 ['pointerdown', 'wheel', 'touchstart', 'keydown'].forEach(function (type) { 148 scroller.addEventListener(type, clearPending, { passive: true }); 149 }); 150 151 if ('onscrollend' in scroller) { 152 scroller.addEventListener('scrollend', clearPending); 153 } else { 154 // Safari before 17.4 has no scrollend: settle it on a timer instead, 155 // restarted on every scroll tick. 156 var settle = null; 157 158 scroller.addEventListener('scroll', function () { 159 window.clearTimeout(settle); 160 settle = window.setTimeout(clearPending, 250); 161 }, { passive: true }); 162 } 163 164 // The card width is a percentage of the rail, so a resize moves the ends. 165 if (typeof ResizeObserver === 'function') { 166 new ResizeObserver(refresh).observe(scroller); 167 } else { 168 window.addEventListener('resize', refresh); 169 } 170 171 // Cards hold lazily loaded images; scrollWidth is only final once they 172 // have taken their space. 173 window.addEventListener('load', refresh); 174 175 refresh(); 176 } 177
178 function init() { 179 var scrollers = document.querySelectorAll('.swiper-wrapper'); 180 181 Array.prototype.forEach.call(scrollers, setUp); 182 } 183 184 if (document.readyState === 'loading') { 185 document.addEventListener('DOMContentLoaded', init); 186 } else { 187 init(); 188 } 189})();
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.