PageSourceSearch

https://www.eventeny.com/js/Components/Carousel.js?v=1790189618

js eventeny.com collected 2026-09-24 08:54:49 UTC 11,278 bytes, 385 lines download raw bytes

1/**
2 * Custom Web Component: <carousel-element> — A horizontal scrolling carousel container with navigation controls.
3 *
4 * Description:
5 * This component provides a horizontally scrollable container that displays slotted child elements
6 * (typically card components) in a row. It includes:
7 *  - Previous/Next navigation buttons that auto-disable at scroll boundaries.
8 *  - Slotted heading and description for a customizable header area.
9 *  - Responsive behavior: hides nav buttons on mobile (<600px) and enables touch scroll with snap.
10 *  - ResizeObserver-based state updates for dynamic content.
11 *
12 * Shadow DOM Structure (default):
13 *  <div class="carousel-container">
14 *    <div class="carousel-header">
15 *      <div class="header-content">
16 *        <slot name="heading"></slot>
17 *        <slot name="description"></slot>
18 *      </div>
19 *      <div class="nav-buttons">
20 *        <button class="nav-button prev">...</button>
21 *        <button class="nav-button next">...</button>
22 *      </div>
23 *    </div>
24 *    <div class="scroll-container">
25 *      <slot></slot>   <!-- Default slot for card children -->
26 *    </div>
27 *  </div>
28 *
29 * Shadow DOM Structure (side-arrows attribute present):
30 *  <div class="carousel-container">
31 *    <div class="carousel-header">
32 *      <div class="header-content">
33 *        <slot name="heading"></slot>
34 *        <slot name="description"></slot>
35 *      </div>
36 *    </div>
37 *    <div class="carousel-body">                    <!-- position:relative anchor for the buttons -->
38 *      <button class="nav-button prev">...</button>  <!-- absolutely positioned, vertically centered, left edge -->
39 *      <div class="scroll-container">
40 *        <slot></slot>
41 *      </div>
42 *      <button class="nav-button next">...</button>  <!-- absolutely positioned, vertically centered, right edge -->
43 *    </div>
44 *  </div>
45 *
46 * Attributes:
47 *  - gap (string): Currently observed but not yet applied via CSS variable. Reserved for future gap customization.
48 *  - side-arrows (boolean): Opt-in layout variant -- nav buttons flank the scroll area itself (one pinned
49 *    left, one pinned right, vertically centered) instead of sitting in the header row next to the
50 *    heading/description. Default (attribute absent) is the original header placement, so every existing
51 *    carousel on the site keeps its current look; only pages that explicitly want the side layout opt in.
52 *    Either layout auto-disables the prev/next button (existing .disabled toggling in _updateState()) when
53 *    there's nothing further to scroll in that direction -- prev disabled at the start, next at the end.
54 *
55 * Slots:
56 *  - "heading": Heading element (e.g., <a> or <span>) displayed above the carousel.
57 *  - "description": Description text displayed below the heading.
58 *  - default (unnamed): Card elements to scroll through (e.g., <carousel-image-card>, <carousel-text-list-card>).
59 *
60 * Responsive Breakpoints padding on left most and right most:
61 *  - Expanded (840px+): 48px horizontal padding.
62 *  - Medium (600-839px): 24px horizontal padding.
63 *  - Compact (<600px): 16px horizontal padding, nav buttons hidden, scroll-snap enabled.
64 *
65 * Usage:
66 *  <carousel-element>
67 *    <a slot="heading" href="#">Section Title</a>
68 *    <span slot="description">Description text</span>
69 *    <carousel-image-card ...></carousel-image-card>
70 *    <carousel-image-card ...></carousel-image-card>
71 *  </carousel-element>
72 *
73 *  <carousel-element side-arrows>
74 *    <carousel-image-card ...></carousel-image-card>
75 *    <carousel-image-card ...></carousel-image-card>
76 *  </carousel-element>
77 */
78import { WebComponent } from './Base/WebComponent.js';
79
80class Carousel extends WebComponent {
81	constructor() {
82		super();
83		this._isMobile = false;
84
85		// Placeholder for styling
86        const style = document.createElement('style');
87        style.textContent = ``;
88	}
89
90	static get observedAttributes() {
91		return ['gap', 'side-arrows'];
92	}
93
94	render() {
95		const sideArrows = this.hasAttribute('side-arrows');
96
97		const prevButton = `
98			<button class="nav-button prev disabled" aria-label="Previous">
99				<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
100					<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/>
101				</svg>
102			</button>
103		`;
104		const nextButton = `
105			<button class="nav-button next" aria-label="Next">
106				<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
107					<path d="M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z"/>
108				</svg>
109			</button>
110		`;
111
112		this.loadTemplate(`
113			<style>
114				:host {
115					display: block;
116				}
117				.carousel-header {
118					display: flex;
119					flex-direction: row;
120					align-items: flex-end;
121					gap: 24px;
122					margin-bottom: 24px;
123				}
124				.header-content {
125					display: flex;
126					flex-direction: column;
127					flex: 1;
128				}
129				.nav-buttons {
130					display: flex;
131					align-items: center;
132					gap: var(--spacing-space-8, 8px);
133					flex-shrink: 0;
134				}
135				.nav-button {
136					border-radius: 50%;
137					background: var(--color-bg-on-brand-primary, #038580);
138					display: flex;
139					padding: var(--spacing-space-6, 6px);
140					justify-content: center;
141					align-items: center;
142					gap: 10px;
143					border: none;
144					cursor: pointer;
145					transition: opacity 0.2s;
146				}
147				.nav-button:hover {
148					opacity: 0.85;
149				}
150				.nav-button svg {
151					width: 20px;
152					height: 20px;
153					fill: #fff;
154				}
155
156				.nav-button.disabled svg {
157					width: 20px;
158					height: 20px;
159					fill: #9DA6A5;
160				}
161				.nav-button.disabled {
162					cursor: default;
163					pointer-events: none;
164					border-radius: 50%;
165					background: #D8DBDB;
166				}
167				.scroll-container {
168					display: flex;
169					gap: 24px;
170					overflow-x: auto;
171					scrollbar-width: none;
172					scroll-behavior: smooth;
173				}
174				.scroll-container::-webkit-scrollbar {
175					display: none;
176				}
177				.carousel-container {
178					padding: 0 48px;
179				}
180				::slotted(:not([slot])) {
181					flex-shrink: 0;
182					align-self: stretch;
183				}
184				.dot {
185					width: 8px;
186					height: 8px;
187					border-radius: 50%;
188					background: #D1D5DB;
189					border: none;
190					padding: 0;
191					cursor: pointer;
192					transition: background 0.2s;
193				}
194				.dot.active {
195					background: #1a1a1a;
196				}
197
198				::slotted([slot="heading"]) {
199					font-size: 24px;
200					font-weight: 600;
201					line-height: 32px;
202					color: #026E80;
203					cursor: pointer;
204					text-decoration: none;
205				}
206				::slotted([slot="description"]) {
207					font-size: 16px;
208					font-weight: 400;
209					line-height: 24px;
210					color: var(--color-text-secondary, #5E6463);
211					display: block;
212					margin-top: 10px !important;
213				}
214
215				/* side-arrows variant only: buttons flank the scroll area itself (one left, one
216				   right, vertically centered) instead of sitting in the header row. Scoped so
217				   the default carousel used elsewhere on the site is completely unaffected. */
218				${sideArrows ? `
219				.carousel-body {
220					position: relative;
221				}
222				.nav-button {
223					position: absolute;
224					top: 50%;
225					transform: translateY(-50%);
226					z-index: 2;
227				}
228				.nav-button.prev {
229					left: 8px;
230				}
231				.nav-button.next {
232					right: 8px;
233				}
234				` : ''}
235
236				/* Expanded: 840px+ */
237				@media (min-width: 840px) {
238					.carousel-container {
239						padding: 0 48px;
240					}
241				}
242				/* Medium: 600-839px */
243				@media (min-width: 600px) and (max-width: 839px) {
244					.carousel-container {
245						padding: 0 24px;
246					}
247				}
248				/* Compact: under 600px — hide buttons, touch scroll */
249				@media (max-width: 599px) {
250					.carousel-container {
251						padding: 0 16px;
252					}
253					::slotted(:not([slot])) {
254						scroll-snap-align: start;
255					}
256					.nav-buttons {
257						display: none;
258					}
259					${sideArrows ? '.nav-button { display: none; }' : ''}
260				}
261
262			</style>
263			<div class="carousel-container">
264				<div class="carousel-header">
265					<div class="header-content">
266						<slot name="heading"></slot>
267						<slot name="description"></slot>
268					</div>
269					${sideArrows ? '' : `<div class="nav-buttons">${prevButton}${nextButton}</div>`}
270				</div>
271				${sideArrows ? `
272				<div class="carousel-body">
273					${prevButton}
274					<div class="scroll-container">
275						<slot></slot>
276					</div>
277					${nextButton}
278				</div>
279				` : `
280				<div class="scroll-container">
281					<slot></slot>
282				</div>
283				`}
284			</div>
285		`);
286	}
287
288	setEventListeners() {
289		this._scrollContainer = this.shadowRoot.querySelector('.scroll-container');
290		this._prevBtn = this.shadowRoot.querySelector('.nav-button.prev');
291		this._nextBtn = this.shadowRoot.querySelector('.nav-button.next');
292		this._slot = this.shadowRoot.querySelector('slot:not([name])');
293
294		this._prevBtn.addEventListener('click', () => this._scroll(-1));
295		this._nextBtn.addEventListener('click', () => this._scroll(1));
296		this._scrollContainer.addEventListener('scroll', () => this._updateState());
297
298		this._slot.addEventListener('slotchange', () => {
299			this._updateState();
300		});
301
302		this._setupResizeObserver();
303		this._setupMediaQuery();
304
305		requestAnimationFrame(() => {
306			this._updateState();
307		});
308	}
309
310	disconnectedCallback() {
311		if (this._resizeObserver) {
312			this._resizeObserver.disconnect();
313		}
314		if (this._mqMobile && this._mqMobileHandler) {
315			this._mqMobile.removeEventListener('change', this._mqMobileHandler);
316		}
317	}
318
319	attributeChangedCallback(name, oldVal, newVal) {
320		if (name === 'gap') {
321			this._updateState();
322		} else if (name === 'side-arrows' && this.shadowRoot.childNodes.length) {
323			this.render();
324		}
325	}
326
327	_scroll(direction) {
328		const items = this._slot.assignedElements();
329		if (items.length === 0) return;
330
331		// Scroll by one "page" worth of items
332		const scrollAmount = this._scrollContainer.clientWidth * direction;
333		this._scrollContainer.scrollBy({ left: scrollAmount, behavior: 'smooth' });
334	}
335
336	_scrollToPage(pageIndex) {
337		const pageWidth = this._scrollContainer.clientWidth;
338		this._scrollContainer.scrollTo({ left: pageWidth * pageIndex, behavior: 'smooth' });
339	}
340
341	_getPageCount() {
342		const { scrollWidth, clientWidth } = this._scrollContainer;
343		if (clientWidth === 0) return 1;
344		return Math.max(1, Math.ceil(scrollWidth / clientWidth));
345	}
346
347	_getCurrentPage() {
348		const { scrollLeft, clientWidth } = this._scrollContainer;
349		if (clientWidth === 0) return 0;
350		return Math.round(scrollLeft / clientWidth);
351	}
352
353	_updateState() {
354		const { scrollLeft, scrollWidth, clientWidth } = this._scrollContainer;
355		const allFit = scrollWidth <= clientWidth;
356
357		// Update button states
358		const atStart = scrollLeft <= 0;
359		const atEnd = Math.ceil(scrollLeft) >= scrollWidth - clientWidth;
360
361		this._prevBtn.classList.toggle('disabled', atStart || allFit);
362		this._nextBtn.classList.toggle('disabled', atEnd || allFit);
363	}
364
365	_setupResizeObserver() {
366		this._resizeObserver = new ResizeObserver(() => {
367			this._updateState();
368		});
369		this._resizeObserver.observe(this._scrollContainer);
370	}
371
372	_setupMediaQuery() {
373		this._mqMobile = window.matchMedia('(max-width: 599px)');
374		this._mqMobileHandler = (e) => {
375			this._isMobile = e.matches;
376			this._updateState();
377		};
378		this._mqMobile.addEventListener('change', this._mqMobileHandler);
379		this._isMobile = this._mqMobile.matches;
380	}
381}
382
383if (!customElements.get('carousel-element')) {
384	customElements.define('carousel-element', Carousel);
385}

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.