PageSourceSearch

https://www.hooforia.com/content/themes/jahr-media/js/rail.js?ver=1.0.0

js hooforia.com collected 2026-10-02 18:54:13 UTC 6,439 bytes, 189 lines download raw bytes

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.