@rettangoli/ui 1.0.10 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,978 @@
1
+ import { css, dimensionWithUnit } from "../common.js";
2
+ import {
3
+ clampCarouselIndex,
4
+ resolveCarouselBooleanAttribute,
5
+ resolveCarouselSnapType,
6
+ resolveCarouselScrollLeft,
7
+ resolveCarouselSlideWidthCss,
8
+ resolveCarouselViewportPaddingCss,
9
+ } from "../common/carousel.js";
10
+
11
+ const INTERACTIVE_SELECTOR = [
12
+ "a",
13
+ "button",
14
+ "input",
15
+ "select",
16
+ "textarea",
17
+ "label",
18
+ "summary",
19
+ "[contenteditable]",
20
+ ].join(", ");
21
+
22
+ const normalizeRawCssValue = (value) => {
23
+ if (value === undefined || value === null) {
24
+ return null;
25
+ }
26
+
27
+ const normalizedValue = `${value}`.trim();
28
+ return normalizedValue.length > 0 ? normalizedValue : null;
29
+ };
30
+
31
+ const easeInOutQuad = (progress) => {
32
+ if (progress < 0.5) {
33
+ return 2 * progress * progress;
34
+ }
35
+
36
+ return 1 - (Math.pow(-2 * progress + 2, 2) / 2);
37
+ };
38
+
39
+ const createChevronIcon = (direction) => {
40
+ const svgNamespace = "http://www.w3.org/2000/svg";
41
+ const svg = document.createElementNS(svgNamespace, "svg");
42
+ svg.setAttribute("viewBox", "0 0 24 24");
43
+ svg.setAttribute("aria-hidden", "true");
44
+ svg.setAttribute("focusable", "false");
45
+
46
+ const path = document.createElementNS(svgNamespace, "path");
47
+ path.setAttribute(
48
+ "d",
49
+ direction === "left" ? "M15 18 9 12l6-6" : "M9 18l6-6-6-6",
50
+ );
51
+ path.setAttribute("fill", "none");
52
+ path.setAttribute("stroke", "currentColor");
53
+ path.setAttribute("stroke-width", "2");
54
+ path.setAttribute("stroke-linecap", "round");
55
+ path.setAttribute("stroke-linejoin", "round");
56
+
57
+ svg.appendChild(path);
58
+ return svg;
59
+ };
60
+
61
+ class RettangoliCarouselElement extends HTMLElement {
62
+ static styleSheet = null;
63
+
64
+ static initializeStyleSheet() {
65
+ if (!RettangoliCarouselElement.styleSheet) {
66
+ RettangoliCarouselElement.styleSheet = new CSSStyleSheet();
67
+ RettangoliCarouselElement.styleSheet.replaceSync(css`
68
+ :host {
69
+ display: block;
70
+ width: 100%;
71
+ min-width: 0;
72
+ box-sizing: border-box;
73
+ --rtgl-carousel-gap: var(--spacing-md);
74
+ --rtgl-carousel-slide-width: 100%;
75
+ --rtgl-carousel-scroll-snap-type: x mandatory;
76
+ --rtgl-carousel-scroll-padding-inline: 0px;
77
+ --rtgl-carousel-edge-padding-inline: 0px;
78
+ --rtgl-carousel-scroll-behavior: smooth;
79
+ --rtgl-carousel-snap-align: center;
80
+ }
81
+
82
+ :host([dragging]) {
83
+ cursor: grabbing;
84
+ }
85
+
86
+ #root {
87
+ display: grid;
88
+ gap: var(--spacing-md);
89
+ width: 100%;
90
+ min-width: 0;
91
+ }
92
+
93
+ #viewport-shell {
94
+ position: relative;
95
+ display: grid;
96
+ width: 100%;
97
+ min-width: 0;
98
+ }
99
+
100
+ #viewport {
101
+ display: flex;
102
+ gap: var(--rtgl-carousel-gap);
103
+ width: 100%;
104
+ min-width: 0;
105
+ box-sizing: border-box;
106
+ grid-area: 1 / 1;
107
+ padding-inline: var(--rtgl-carousel-edge-padding-inline);
108
+ overflow-x: auto;
109
+ overflow-y: hidden;
110
+ scroll-snap-type: var(--rtgl-carousel-scroll-snap-type);
111
+ scroll-behavior: var(--rtgl-carousel-scroll-behavior);
112
+ scroll-padding-inline: var(--rtgl-carousel-scroll-padding-inline);
113
+ -ms-overflow-style: none;
114
+ scrollbar-width: none;
115
+ cursor: grab;
116
+ touch-action: pan-x pan-y pinch-zoom;
117
+ overscroll-behavior-x: contain;
118
+ }
119
+
120
+ :host([dragging]) #viewport {
121
+ cursor: grabbing;
122
+ user-select: none;
123
+ }
124
+
125
+ #viewport::-webkit-scrollbar {
126
+ display: none;
127
+ }
128
+
129
+ slot {
130
+ display: contents;
131
+ }
132
+
133
+ #controls {
134
+ display: flex;
135
+ align-items: center;
136
+ justify-content: center;
137
+ gap: var(--spacing-sm);
138
+ width: 100%;
139
+ min-width: 0;
140
+ }
141
+
142
+ #prev-button,
143
+ #next-button {
144
+ appearance: none;
145
+ border: 1px solid var(--border);
146
+ background: var(--background);
147
+ color: var(--foreground);
148
+ border-radius: 999px;
149
+ width: 44px;
150
+ height: 44px;
151
+ min-width: 44px;
152
+ min-height: 44px;
153
+ padding: 0;
154
+ font: inherit;
155
+ line-height: 1;
156
+ cursor: pointer;
157
+ display: inline-flex;
158
+ align-items: center;
159
+ justify-content: center;
160
+ position: absolute;
161
+ top: 50%;
162
+ z-index: 1;
163
+ transition:
164
+ transform 160ms ease,
165
+ background-color 160ms ease,
166
+ color 160ms ease,
167
+ border-color 160ms ease,
168
+ box-shadow 160ms ease,
169
+ opacity 160ms ease;
170
+ }
171
+
172
+ #prev-button {
173
+ left: var(--spacing-sm);
174
+ transform: translateY(-50%);
175
+ }
176
+
177
+ #next-button {
178
+ right: var(--spacing-sm);
179
+ transform: translateY(-50%);
180
+ }
181
+
182
+ #prev-button svg,
183
+ #next-button svg {
184
+ width: 18px;
185
+ height: 18px;
186
+ pointer-events: none;
187
+ }
188
+
189
+ #pager {
190
+ display: flex;
191
+ align-items: center;
192
+ justify-content: center;
193
+ gap: var(--spacing-md);
194
+ flex-wrap: wrap;
195
+ }
196
+
197
+ #pager button {
198
+ appearance: none;
199
+ border: 1px solid color-mix(in srgb, var(--foreground) 18%, transparent);
200
+ background: color-mix(in srgb, var(--foreground) 22%, transparent);
201
+ color: var(--foreground);
202
+ border-radius: 999px;
203
+ width: 14px;
204
+ height: 14px;
205
+ min-width: 14px;
206
+ min-height: 14px;
207
+ padding: 0;
208
+ cursor: pointer;
209
+ transition:
210
+ transform 160ms ease,
211
+ background-color 160ms ease,
212
+ color 160ms ease,
213
+ border-color 160ms ease,
214
+ box-shadow 160ms ease,
215
+ opacity 160ms ease;
216
+ }
217
+
218
+ #prev-button:hover:not(:disabled),
219
+ #next-button:hover:not(:disabled),
220
+ #pager button:hover:not(:disabled),
221
+ #prev-button:focus-visible,
222
+ #next-button:focus-visible,
223
+ #pager button:focus-visible {
224
+ box-shadow: var(--shadow-sm);
225
+ outline: none;
226
+ }
227
+
228
+ #prev-button:hover:not(:disabled),
229
+ #next-button:hover:not(:disabled),
230
+ #prev-button:focus-visible,
231
+ #next-button:focus-visible {
232
+ transform: translateY(calc(-50% - 1px));
233
+ }
234
+
235
+ #pager button:hover:not(:disabled),
236
+ #pager button:focus-visible {
237
+ transform: scale(1.1);
238
+ }
239
+
240
+ #prev-button:disabled,
241
+ #next-button:disabled,
242
+ #pager button:disabled {
243
+ opacity: 0.45;
244
+ cursor: default;
245
+ box-shadow: none;
246
+ }
247
+
248
+ #prev-button:disabled,
249
+ #next-button:disabled {
250
+ transform: translateY(-50%);
251
+ }
252
+
253
+ #pager button:disabled {
254
+ transform: none;
255
+ }
256
+
257
+ #pager button.is-active {
258
+ background: var(--foreground);
259
+ border-color: var(--foreground);
260
+ transform: scale(1.2);
261
+ }
262
+ `);
263
+ }
264
+ }
265
+
266
+ static get observedAttributes() {
267
+ return ["index", "sw", "sna", "g", "spi", "sbh", "snap", "nav", "pager"];
268
+ }
269
+
270
+ constructor() {
271
+ super();
272
+ RettangoliCarouselElement.initializeStyleSheet();
273
+
274
+ this._slides = [];
275
+ this._pagerButtons = [];
276
+ this._currentIndex = 0;
277
+ this._clickSuppressUntil = 0;
278
+ this._dragState = null;
279
+ this._scrollFrame = null;
280
+ this._resizeFrame = null;
281
+ this._scrollAnimationFrame = null;
282
+ this._isReflectingIndex = false;
283
+ this._slideStyleCache = new WeakMap();
284
+
285
+ this.shadow = this.attachShadow({ mode: "open" });
286
+ this.shadow.adoptedStyleSheets = [RettangoliCarouselElement.styleSheet];
287
+
288
+ this._rootElement = document.createElement("div");
289
+ this._rootElement.id = "root";
290
+
291
+ this._viewportShellElement = document.createElement("div");
292
+ this._viewportShellElement.id = "viewport-shell";
293
+ this._viewportShellElement.setAttribute("part", "viewport-shell");
294
+
295
+ this._viewportElement = document.createElement("div");
296
+ this._viewportElement.id = "viewport";
297
+ this._viewportElement.tabIndex = 0;
298
+ this._viewportElement.setAttribute("part", "viewport");
299
+
300
+ this._slotElement = document.createElement("slot");
301
+ this._viewportElement.appendChild(this._slotElement);
302
+
303
+ this._controlsElement = document.createElement("div");
304
+ this._controlsElement.id = "controls";
305
+ this._controlsElement.setAttribute("part", "controls");
306
+ this._controlsElement.hidden = true;
307
+
308
+ this._prevButton = document.createElement("button");
309
+ this._prevButton.id = "prev-button";
310
+ this._prevButton.type = "button";
311
+ this._prevButton.hidden = true;
312
+ this._prevButton.append(createChevronIcon("left"));
313
+ this._prevButton.setAttribute("part", "nav-button prev-button");
314
+ this._prevButton.setAttribute("aria-label", "Previous slide");
315
+
316
+ this._pagerElement = document.createElement("div");
317
+ this._pagerElement.id = "pager";
318
+ this._pagerElement.setAttribute("part", "pager");
319
+ this._pagerElement.hidden = true;
320
+
321
+ this._nextButton = document.createElement("button");
322
+ this._nextButton.id = "next-button";
323
+ this._nextButton.type = "button";
324
+ this._nextButton.hidden = true;
325
+ this._nextButton.append(createChevronIcon("right"));
326
+ this._nextButton.setAttribute("part", "nav-button next-button");
327
+ this._nextButton.setAttribute("aria-label", "Next slide");
328
+
329
+ this._viewportShellElement.append(
330
+ this._viewportElement,
331
+ this._prevButton,
332
+ this._nextButton,
333
+ );
334
+ this._controlsElement.append(this._pagerElement);
335
+ this._rootElement.append(this._viewportShellElement, this._controlsElement);
336
+ this.shadow.append(this._rootElement);
337
+
338
+ this._handleSlotChange = this._handleSlotChange.bind(this);
339
+ this._handleClickPrev = this._handleClickPrev.bind(this);
340
+ this._handleClickNext = this._handleClickNext.bind(this);
341
+ this._handleViewportKeydown = this._handleViewportKeydown.bind(this);
342
+ this._handleViewportScroll = this._handleViewportScroll.bind(this);
343
+ this._handleViewportPointerDown = this._handleViewportPointerDown.bind(this);
344
+ this._handlePointerMove = this._handlePointerMove.bind(this);
345
+ this._handlePointerUp = this._handlePointerUp.bind(this);
346
+ this._handleViewportClickCapture = this._handleViewportClickCapture.bind(this);
347
+
348
+ this._resizeObserver = typeof ResizeObserver !== "undefined"
349
+ ? new ResizeObserver(() => {
350
+ if (this._resizeFrame !== null) {
351
+ cancelAnimationFrame(this._resizeFrame);
352
+ }
353
+
354
+ this._resizeFrame = requestAnimationFrame(() => {
355
+ this._resizeFrame = null;
356
+
357
+ if (this._dragState?.dragging || !this._slides.length) {
358
+ return;
359
+ }
360
+
361
+ this.goTo(this._currentIndex, { behavior: "auto" });
362
+ });
363
+ })
364
+ : null;
365
+ }
366
+
367
+ get index() {
368
+ return this._currentIndex;
369
+ }
370
+
371
+ set index(value) {
372
+ const numericValue = Number(value);
373
+ this.setAttribute("index", Number.isFinite(numericValue) ? `${numericValue}` : "0");
374
+ }
375
+
376
+ get snap() {
377
+ return resolveCarouselSnapType(this.getAttribute("snap")) !== "none";
378
+ }
379
+
380
+ set snap(value) {
381
+ if (value === false || `${value}`.trim().toLowerCase() === "false") {
382
+ this.setAttribute("snap", "false");
383
+ return;
384
+ }
385
+
386
+ this.setAttribute("snap", "true");
387
+ }
388
+
389
+ connectedCallback() {
390
+ this._slotElement.addEventListener("slotchange", this._handleSlotChange);
391
+ this._prevButton.addEventListener("click", this._handleClickPrev);
392
+ this._nextButton.addEventListener("click", this._handleClickNext);
393
+ this._viewportElement.addEventListener("keydown", this._handleViewportKeydown);
394
+ this._viewportElement.addEventListener("scroll", this._handleViewportScroll, { passive: true });
395
+ this._viewportElement.addEventListener("pointerdown", this._handleViewportPointerDown);
396
+ this._viewportElement.addEventListener("click", this._handleViewportClickCapture, true);
397
+ this._resizeObserver?.observe(this._viewportElement);
398
+
399
+ this._syncSlides();
400
+ this._updateLayoutStyles();
401
+
402
+ const initialIndex = this.hasAttribute("index")
403
+ ? Number(this.getAttribute("index"))
404
+ : 0;
405
+ this.goTo(initialIndex, { behavior: "auto" });
406
+ }
407
+
408
+ disconnectedCallback() {
409
+ this._slotElement.removeEventListener("slotchange", this._handleSlotChange);
410
+ this._prevButton.removeEventListener("click", this._handleClickPrev);
411
+ this._nextButton.removeEventListener("click", this._handleClickNext);
412
+ this._viewportElement.removeEventListener("keydown", this._handleViewportKeydown);
413
+ this._viewportElement.removeEventListener("scroll", this._handleViewportScroll);
414
+ this._viewportElement.removeEventListener("pointerdown", this._handleViewportPointerDown);
415
+ this._viewportElement.removeEventListener("click", this._handleViewportClickCapture, true);
416
+ this._removePointerListeners();
417
+ this._resizeObserver?.disconnect();
418
+
419
+ if (this._scrollFrame !== null) {
420
+ cancelAnimationFrame(this._scrollFrame);
421
+ this._scrollFrame = null;
422
+ }
423
+
424
+ if (this._resizeFrame !== null) {
425
+ cancelAnimationFrame(this._resizeFrame);
426
+ this._resizeFrame = null;
427
+ }
428
+
429
+ this._cancelScrollAnimation();
430
+
431
+ this._setDraggingState(false);
432
+
433
+ this._slides.forEach((slide) => {
434
+ this._restoreSlideStyles(slide);
435
+ slide.removeAttribute("data-rtgl-carousel-active");
436
+ });
437
+ }
438
+
439
+ attributeChangedCallback(name, oldValue, newValue) {
440
+ if (oldValue === newValue) {
441
+ return;
442
+ }
443
+
444
+ if (name === "index") {
445
+ if (this._isReflectingIndex) {
446
+ this._isReflectingIndex = false;
447
+ return;
448
+ }
449
+
450
+ this.goTo(Number(newValue), { behavior: "smooth" });
451
+ return;
452
+ }
453
+
454
+ if (name === "nav" || name === "pager") {
455
+ this._updateControls();
456
+ return;
457
+ }
458
+
459
+ this._updateLayoutStyles();
460
+ }
461
+
462
+ next() {
463
+ this.goTo(this._currentIndex + 1);
464
+ }
465
+
466
+ prev() {
467
+ this.goTo(this._currentIndex - 1);
468
+ }
469
+
470
+ _showsNavControls() {
471
+ return resolveCarouselBooleanAttribute({
472
+ value: this.getAttribute("nav"),
473
+ defaultValue: true,
474
+ });
475
+ }
476
+
477
+ _showsPagerControls() {
478
+ return resolveCarouselBooleanAttribute({
479
+ value: this.getAttribute("pager"),
480
+ defaultValue: false,
481
+ });
482
+ }
483
+
484
+ goTo(index, options = {}) {
485
+ if (!this._slides.length) {
486
+ this._setCurrentIndex(0, { emit: false, reflect: true });
487
+ return;
488
+ }
489
+
490
+ const targetIndex = clampCarouselIndex({
491
+ index: Number(index),
492
+ maxIndex: this._slides.length - 1,
493
+ });
494
+ const targetSlide = this._slides[targetIndex];
495
+ const behavior = options.behavior ?? "smooth";
496
+
497
+ if (!targetSlide) {
498
+ return;
499
+ }
500
+
501
+ this._scrollViewportTo(this._getSlideTargetScrollLeft(targetSlide), {
502
+ behavior,
503
+ });
504
+
505
+ this._setCurrentIndex(targetIndex);
506
+ }
507
+
508
+ _handleSlotChange() {
509
+ this._syncSlides();
510
+ this._updateLayoutStyles();
511
+ this.goTo(this._currentIndex, { behavior: "auto" });
512
+ }
513
+
514
+ _handleClickPrev() {
515
+ this.prev();
516
+ }
517
+
518
+ _handleClickNext() {
519
+ this.next();
520
+ }
521
+
522
+ _handleViewportKeydown(event) {
523
+ if (event.key === "ArrowRight") {
524
+ event.preventDefault();
525
+ this.next();
526
+ } else if (event.key === "ArrowLeft") {
527
+ event.preventDefault();
528
+ this.prev();
529
+ }
530
+ }
531
+
532
+ _handleViewportScroll() {
533
+ if (this._scrollAnimationFrame !== null) {
534
+ return;
535
+ }
536
+
537
+ if (this._dragState?.dragging) {
538
+ return;
539
+ }
540
+
541
+ this._requestSyncFromScroll();
542
+ }
543
+
544
+ _handleViewportPointerDown(event) {
545
+ if (event.pointerType !== "mouse") {
546
+ return;
547
+ }
548
+
549
+ if (event.button !== 0 || !this._slides.length) {
550
+ return;
551
+ }
552
+
553
+ if (this._eventTargetsInteractiveElement(event)) {
554
+ return;
555
+ }
556
+
557
+ this._cancelScrollAnimation();
558
+
559
+ this._dragState = {
560
+ pointerId: event.pointerId,
561
+ startX: event.clientX,
562
+ startScrollLeft: this._viewportElement.scrollLeft,
563
+ dragging: false,
564
+ };
565
+
566
+ this._viewportElement.setPointerCapture(event.pointerId);
567
+ this._viewportElement.addEventListener("pointermove", this._handlePointerMove);
568
+ this._viewportElement.addEventListener("pointerup", this._handlePointerUp);
569
+ this._viewportElement.addEventListener("pointercancel", this._handlePointerUp);
570
+ }
571
+
572
+ _handlePointerMove(event) {
573
+ if (!this._dragState || event.pointerId !== this._dragState.pointerId) {
574
+ return;
575
+ }
576
+
577
+ const deltaX = event.clientX - this._dragState.startX;
578
+ if (!this._dragState.dragging && Math.abs(deltaX) > 6) {
579
+ this._dragState.dragging = true;
580
+ this._setDraggingState(true);
581
+ }
582
+
583
+ if (!this._dragState.dragging) {
584
+ return;
585
+ }
586
+
587
+ event.preventDefault();
588
+ this._viewportElement.scrollLeft = this._dragState.startScrollLeft - deltaX;
589
+ }
590
+
591
+ _handlePointerUp(event) {
592
+ if (!this._dragState || event.pointerId !== this._dragState.pointerId) {
593
+ return;
594
+ }
595
+
596
+ const wasDragging = this._dragState.dragging;
597
+ const nearestIndex = wasDragging ? this._findNearestSlideIndex() : this._currentIndex;
598
+
599
+ if (this._viewportElement.hasPointerCapture(event.pointerId)) {
600
+ this._viewportElement.releasePointerCapture(event.pointerId);
601
+ }
602
+
603
+ this._dragState = null;
604
+ this._setDraggingState(false);
605
+ this._removePointerListeners();
606
+
607
+ if (wasDragging) {
608
+ this._clickSuppressUntil = performance.now() + 250;
609
+ if (this.snap) {
610
+ this.goTo(nearestIndex, { behavior: "smooth" });
611
+ } else {
612
+ this._requestSyncFromScroll();
613
+ }
614
+ }
615
+ }
616
+
617
+ _handleViewportClickCapture(event) {
618
+ if (performance.now() < this._clickSuppressUntil) {
619
+ event.preventDefault();
620
+ event.stopPropagation();
621
+ }
622
+ }
623
+
624
+ _removePointerListeners() {
625
+ this._viewportElement.removeEventListener("pointermove", this._handlePointerMove);
626
+ this._viewportElement.removeEventListener("pointerup", this._handlePointerUp);
627
+ this._viewportElement.removeEventListener("pointercancel", this._handlePointerUp);
628
+ }
629
+
630
+ _eventTargetsInteractiveElement(event) {
631
+ return event.composedPath().some((node) => {
632
+ return node instanceof Element && node.matches(INTERACTIVE_SELECTOR);
633
+ });
634
+ }
635
+
636
+ _requestSyncFromScroll() {
637
+ if (this._scrollFrame !== null) {
638
+ cancelAnimationFrame(this._scrollFrame);
639
+ }
640
+
641
+ this._scrollFrame = requestAnimationFrame(() => {
642
+ this._scrollFrame = null;
643
+ const nearestIndex = this._findNearestSlideIndex();
644
+ this._setCurrentIndex(nearestIndex);
645
+ });
646
+ }
647
+
648
+ _findNearestSlideIndex() {
649
+ if (!this._slides.length) {
650
+ return 0;
651
+ }
652
+
653
+ const currentScrollLeft = this._viewportElement.scrollLeft;
654
+
655
+ let nearestIndex = this._currentIndex;
656
+ let nearestDistance = Number.POSITIVE_INFINITY;
657
+
658
+ this._slides.forEach((slide, index) => {
659
+ const distance = Math.abs(
660
+ this._getSlideTargetScrollLeft(slide) - currentScrollLeft,
661
+ );
662
+
663
+ if (distance < nearestDistance) {
664
+ nearestIndex = index;
665
+ nearestDistance = distance;
666
+ }
667
+ });
668
+
669
+ return nearestIndex;
670
+ }
671
+
672
+ _syncSlides() {
673
+ const previousSlides = new Set(this._slides);
674
+ const nextSlides = this._slotElement
675
+ .assignedElements({ flatten: true })
676
+ .filter((element) => !element.hasAttribute("hidden"));
677
+
678
+ previousSlides.forEach((slide) => {
679
+ if (!nextSlides.includes(slide)) {
680
+ this._restoreSlideStyles(slide);
681
+ slide.removeAttribute("data-rtgl-carousel-active");
682
+ }
683
+ });
684
+
685
+ this._slides = nextSlides;
686
+ this._slides.forEach((slide) => {
687
+ if (!this._slideStyleCache.has(slide)) {
688
+ this._slideStyleCache.set(slide, {
689
+ flex: slide.style.flex,
690
+ width: slide.style.width,
691
+ minWidth: slide.style.minWidth,
692
+ maxWidth: slide.style.maxWidth,
693
+ boxSizing: slide.style.boxSizing,
694
+ scrollSnapAlign: slide.style.scrollSnapAlign,
695
+ scrollSnapStop: slide.style.scrollSnapStop,
696
+ });
697
+ }
698
+ });
699
+
700
+ this._buildPager();
701
+ this._setCurrentIndex(
702
+ clampCarouselIndex({
703
+ index: this._currentIndex,
704
+ maxIndex: this._slides.length - 1,
705
+ }),
706
+ { emit: false, reflect: true },
707
+ );
708
+ }
709
+
710
+ _restoreSlideStyles(slide) {
711
+ const cachedStyles = this._slideStyleCache.get(slide);
712
+ if (!cachedStyles) {
713
+ return;
714
+ }
715
+
716
+ slide.style.flex = cachedStyles.flex;
717
+ slide.style.width = cachedStyles.width;
718
+ slide.style.minWidth = cachedStyles.minWidth;
719
+ slide.style.maxWidth = cachedStyles.maxWidth;
720
+ slide.style.boxSizing = cachedStyles.boxSizing;
721
+ slide.style.scrollSnapAlign = cachedStyles.scrollSnapAlign;
722
+ slide.style.scrollSnapStop = cachedStyles.scrollSnapStop;
723
+ }
724
+
725
+ _updateLayoutStyles() {
726
+ const slideWidthCss = resolveCarouselSlideWidthCss({
727
+ slideWidth: this.getAttribute("sw"),
728
+ });
729
+ const snapAlign = normalizeRawCssValue(this.getAttribute("sna")) ?? "center";
730
+ const snapType = resolveCarouselSnapType(this.getAttribute("snap"));
731
+ const gap = dimensionWithUnit(this.getAttribute("g")) ?? "var(--spacing-md)";
732
+ const scrollPaddingInline =
733
+ dimensionWithUnit(this.getAttribute("spi")) ?? "0px";
734
+ const scrollBehavior =
735
+ normalizeRawCssValue(this.getAttribute("sbh")) ?? "smooth";
736
+ const edgePaddingInline = resolveCarouselViewportPaddingCss({
737
+ slideWidthCss,
738
+ snapAlign,
739
+ });
740
+
741
+ this.style.setProperty("--rtgl-carousel-slide-width", slideWidthCss);
742
+ this.style.setProperty("--rtgl-carousel-gap", gap);
743
+ this.style.setProperty("--rtgl-carousel-scroll-snap-type", snapType);
744
+ this.style.setProperty("--rtgl-carousel-scroll-padding-inline", scrollPaddingInline);
745
+ this.style.setProperty("--rtgl-carousel-edge-padding-inline", edgePaddingInline);
746
+ this.style.setProperty("--rtgl-carousel-scroll-behavior", scrollBehavior);
747
+ this.style.setProperty("--rtgl-carousel-snap-align", snapAlign);
748
+
749
+ this._slides.forEach((slide) => {
750
+ slide.style.flex = `0 0 ${slideWidthCss}`;
751
+ slide.style.width = slideWidthCss;
752
+ slide.style.minWidth = "0";
753
+ slide.style.maxWidth = "unset";
754
+ slide.style.boxSizing = "border-box";
755
+ slide.style.scrollSnapAlign = snapAlign;
756
+ slide.style.scrollSnapStop = "always";
757
+ });
758
+ }
759
+
760
+ _setDraggingState(isDragging) {
761
+ if (isDragging) {
762
+ this.setAttribute("dragging", "");
763
+ this._viewportElement.style.scrollSnapType = "none";
764
+ this._viewportElement.style.scrollBehavior = "auto";
765
+ return;
766
+ }
767
+
768
+ this.removeAttribute("dragging");
769
+ this._viewportElement.style.scrollSnapType = "";
770
+ this._viewportElement.style.scrollBehavior = "";
771
+ }
772
+
773
+ _scrollViewportTo(targetScrollLeft, options = {}) {
774
+ const behavior = options.behavior ?? "smooth";
775
+ const clampedTargetScrollLeft = Math.min(
776
+ Math.max(targetScrollLeft, 0),
777
+ Math.max(
778
+ this._viewportElement.scrollWidth - this._viewportElement.clientWidth,
779
+ 0,
780
+ ),
781
+ );
782
+
783
+ if (
784
+ behavior !== "smooth" ||
785
+ Math.abs(clampedTargetScrollLeft - this._viewportElement.scrollLeft) < 1
786
+ ) {
787
+ this._cancelScrollAnimation();
788
+ this._viewportElement.scrollLeft = clampedTargetScrollLeft;
789
+ return;
790
+ }
791
+
792
+ this._cancelScrollAnimation();
793
+
794
+ const startScrollLeft = this._viewportElement.scrollLeft;
795
+ const deltaScrollLeft = clampedTargetScrollLeft - startScrollLeft;
796
+ const animationDurationMs = Math.min(
797
+ 720,
798
+ Math.max(420, Math.abs(deltaScrollLeft) * 0.35),
799
+ );
800
+ const animationStartTime = performance.now();
801
+
802
+ this._setAnimatingScrollState(true);
803
+
804
+ const step = (frameTime) => {
805
+ const elapsedMs = frameTime - animationStartTime;
806
+ const progress = Math.min(elapsedMs / animationDurationMs, 1);
807
+ const easedProgress = easeInOutQuad(progress);
808
+
809
+ this._viewportElement.scrollLeft =
810
+ startScrollLeft + (deltaScrollLeft * easedProgress);
811
+
812
+ if (progress < 1) {
813
+ this._scrollAnimationFrame = requestAnimationFrame(step);
814
+ return;
815
+ }
816
+
817
+ this._viewportElement.scrollLeft = clampedTargetScrollLeft;
818
+ this._scrollAnimationFrame = null;
819
+ this._setAnimatingScrollState(false);
820
+ };
821
+
822
+ this._scrollAnimationFrame = requestAnimationFrame(step);
823
+ }
824
+
825
+ _cancelScrollAnimation() {
826
+ if (this._scrollAnimationFrame !== null) {
827
+ cancelAnimationFrame(this._scrollAnimationFrame);
828
+ this._scrollAnimationFrame = null;
829
+ this._setAnimatingScrollState(false);
830
+ }
831
+ }
832
+
833
+ _buildPager() {
834
+ this._pagerElement.replaceChildren();
835
+ this._pagerButtons = [];
836
+
837
+ this._slides.forEach((slide, index) => {
838
+ const button = document.createElement("button");
839
+ button.type = "button";
840
+ button.setAttribute("part", "pager-button");
841
+ button.setAttribute("aria-label", `Go to slide ${index + 1}`);
842
+ button.setAttribute("title", `Go to slide ${index + 1}`);
843
+ button.addEventListener("click", () => {
844
+ this.goTo(index);
845
+ });
846
+ this._pagerElement.appendChild(button);
847
+ this._pagerButtons.push(button);
848
+ });
849
+
850
+ this._updateControls();
851
+ }
852
+
853
+ _updateControls() {
854
+ const hasSlides = this._slides.length > 0;
855
+ const hasMultipleSlides = this._slides.length > 1;
856
+ const isAtStart = this._currentIndex <= 0;
857
+ const isAtEnd = this._currentIndex >= this._slides.length - 1;
858
+ const showNavControls = hasMultipleSlides && this._showsNavControls();
859
+ const showPagerControls = hasMultipleSlides && this._showsPagerControls();
860
+
861
+ this._prevButton.hidden = !showNavControls;
862
+ this._nextButton.hidden = !showNavControls;
863
+ this._pagerElement.hidden = !showPagerControls;
864
+ this._controlsElement.hidden = !showPagerControls;
865
+ this._prevButton.disabled = !hasSlides || isAtStart;
866
+ this._nextButton.disabled = !hasSlides || isAtEnd;
867
+
868
+ this._pagerButtons.forEach((button, index) => {
869
+ const isActive = index === this._currentIndex;
870
+ button.classList.toggle("is-active", isActive);
871
+ button.disabled = isActive;
872
+ button.setAttribute("part", isActive ? "pager-button pager-button-active" : "pager-button");
873
+ if (isActive) {
874
+ button.setAttribute("aria-current", "true");
875
+ } else {
876
+ button.removeAttribute("aria-current");
877
+ }
878
+ });
879
+ }
880
+
881
+ _setCurrentIndex(index, options = {}) {
882
+ const targetIndex = clampCarouselIndex({
883
+ index,
884
+ maxIndex: this._slides.length - 1,
885
+ });
886
+ const emit = options.emit ?? true;
887
+ const reflect = options.reflect ?? true;
888
+ const previousIndex = this._currentIndex;
889
+
890
+ this._currentIndex = targetIndex;
891
+
892
+ this._slides.forEach((slide, slideIndex) => {
893
+ if (slideIndex === targetIndex) {
894
+ slide.setAttribute("data-rtgl-carousel-active", "true");
895
+ } else {
896
+ slide.removeAttribute("data-rtgl-carousel-active");
897
+ }
898
+ });
899
+
900
+ this._updateControls();
901
+
902
+ if (reflect && this.getAttribute("index") !== `${targetIndex}`) {
903
+ this._isReflectingIndex = true;
904
+ this.setAttribute("index", `${targetIndex}`);
905
+ }
906
+
907
+ if (emit && previousIndex !== targetIndex) {
908
+ this.dispatchEvent(
909
+ new CustomEvent("index-change", {
910
+ detail: { index: targetIndex },
911
+ bubbles: true,
912
+ }),
913
+ );
914
+ }
915
+ }
916
+
917
+ _resolveScrollInlinePosition() {
918
+ const snapAlign = normalizeRawCssValue(this.getAttribute("sna")) ?? "center";
919
+ if (snapAlign === "start" || snapAlign === "end" || snapAlign === "center") {
920
+ return snapAlign;
921
+ }
922
+
923
+ return "center";
924
+ }
925
+
926
+ _getSlideTargetScrollLeft(slide) {
927
+ const viewportRect = this._viewportElement.getBoundingClientRect();
928
+ const slideRect = slide.getBoundingClientRect();
929
+
930
+ if (viewportRect.width <= 0 || slideRect.width <= 0) {
931
+ return this._viewportElement.scrollLeft;
932
+ }
933
+
934
+ const viewportStyles = getComputedStyle(this._viewportElement);
935
+ const currentScrollLeft = this._viewportElement.scrollLeft;
936
+ const slideLeft =
937
+ (slideRect.left - viewportRect.left) + currentScrollLeft;
938
+
939
+ return resolveCarouselScrollLeft({
940
+ slideLeft,
941
+ slideWidth: slideRect.width,
942
+ viewportWidth: viewportRect.width,
943
+ scrollPaddingInlineStart: this._parsePx(
944
+ viewportStyles.scrollPaddingInlineStart,
945
+ ),
946
+ scrollPaddingInlineEnd: this._parsePx(
947
+ viewportStyles.scrollPaddingInlineEnd,
948
+ ),
949
+ snapAlign: this._resolveScrollInlinePosition(),
950
+ maxScrollLeft:
951
+ this._viewportElement.scrollWidth - this._viewportElement.clientWidth,
952
+ });
953
+ }
954
+
955
+ _parsePx(value) {
956
+ const parsedValue = Number.parseFloat(value);
957
+ return Number.isFinite(parsedValue) ? parsedValue : 0;
958
+ }
959
+
960
+ _setAnimatingScrollState(isAnimating) {
961
+ if (isAnimating) {
962
+ this._viewportElement.style.scrollSnapType = "none";
963
+ this._viewportElement.style.scrollBehavior = "auto";
964
+ return;
965
+ }
966
+
967
+ if (this._dragState?.dragging) {
968
+ return;
969
+ }
970
+
971
+ this._viewportElement.style.scrollSnapType = "";
972
+ this._viewportElement.style.scrollBehavior = "";
973
+ }
974
+ }
975
+
976
+ export default () => {
977
+ return RettangoliCarouselElement;
978
+ };