@moni-labs/moni-ui 0.3.0 → 0.3.1
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.
package/custom-elements.json
CHANGED
|
@@ -2,18 +2,6 @@
|
|
|
2
2
|
"schemaVersion": "1.0.0",
|
|
3
3
|
"readme": "",
|
|
4
4
|
"modules": [
|
|
5
|
-
{
|
|
6
|
-
"kind": "javascript-module",
|
|
7
|
-
"path": "scripts/add-headers.js",
|
|
8
|
-
"declarations": [],
|
|
9
|
-
"exports": []
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
"kind": "javascript-module",
|
|
13
|
-
"path": "scripts/build.js",
|
|
14
|
-
"declarations": [],
|
|
15
|
-
"exports": []
|
|
16
|
-
},
|
|
17
5
|
{
|
|
18
6
|
"kind": "javascript-module",
|
|
19
7
|
"path": "dist/index.js",
|
|
@@ -412,6 +400,18 @@
|
|
|
412
400
|
}
|
|
413
401
|
]
|
|
414
402
|
},
|
|
403
|
+
{
|
|
404
|
+
"kind": "javascript-module",
|
|
405
|
+
"path": "scripts/add-headers.js",
|
|
406
|
+
"declarations": [],
|
|
407
|
+
"exports": []
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
"kind": "javascript-module",
|
|
411
|
+
"path": "scripts/build.js",
|
|
412
|
+
"declarations": [],
|
|
413
|
+
"exports": []
|
|
414
|
+
},
|
|
415
415
|
{
|
|
416
416
|
"kind": "javascript-module",
|
|
417
417
|
"path": "src/index.ts",
|
|
@@ -872,12 +872,6 @@
|
|
|
872
872
|
"declarations": [],
|
|
873
873
|
"exports": []
|
|
874
874
|
},
|
|
875
|
-
{
|
|
876
|
-
"kind": "javascript-module",
|
|
877
|
-
"path": "src/actions/index.ts",
|
|
878
|
-
"declarations": [],
|
|
879
|
-
"exports": []
|
|
880
|
-
},
|
|
881
875
|
{
|
|
882
876
|
"kind": "javascript-module",
|
|
883
877
|
"path": "dist/components/index.js",
|
|
@@ -1455,7 +1449,7 @@
|
|
|
1455
1449
|
{
|
|
1456
1450
|
"kind": "variable",
|
|
1457
1451
|
"name": "MoniCarousel",
|
|
1458
|
-
"default": "class MoniCarousel extends MoniElement { constructor() { super(...arguments); this.items = []; this.layout = 'multi-browse'; this.auto = true; this.largeWidth = 220; this.mediumWidth = 96; this.smallWidth = 48; this.gap = 8; this.padding = 16; this.borderRadius = 28; this.showAll = false; this.showAllText = 'Show all'; this.headerText = ''; this.hideNav = false; this._containerWidth = 0; this._slottedItems = []; this._hasSlottedShowAll = false; this._resizeObserver = null; this._isDown = false; this._startX = 0; this._scrollLeftStart = 0; this._velocity = 0; this._lastTime = 0; this._lastX = 0; this._draggedDistance = 0; this._rafId = null; this._isMobile = false; // Scroll interpolation state this._tickerTarget = 0; this._tickerCurrent = 0; this._isTicking = false; // quickSetter caches — avoid gsap.set() overhead per frame this._cardSetX = []; this._cardSetWidth = []; this._imgSetX = []; this._titleSetOpacity = []; /** * RAF-gated version of updateLayout for scroll events. * Prevents multiple layout recalculations per frame. */ this._tick = () => { const dt = gsap.ticker.deltaRatio(); // Smoother lerp on mobile to avoid abrupt changes (\"brusco\") const ease = this._isMobile ? 0.08 : 0.15; this._tickerCurrent += (this._tickerTarget - this._tickerCurrent) * ease * dt; if (Math.abs(this._tickerTarget - this._tickerCurrent) < 0.0005) { this._tickerCurrent = this._tickerTarget; gsap.ticker.remove(this._tick); this._isTicking = false; } this._applyLayout(this._tickerCurrent); }; this._scheduleLayout = () => { if (!this._scrollContainer) return; this._tickerTarget = this._scrollContainer.scrollLeft / this.itemSize; if (!this._isTicking) { this._isTicking = true; gsap.ticker.add(this._tick); } }; this.updateLayout = (instant = false) => { if (!this._scrollContainer || !this._cards || this._cards.length === 0) return; // Rebuild quickSetters if card count changed (e.g. after items update) if (this._cardSetX.length !== this._cards.length) { this._buildQuickSetters(); } this._tickerTarget = this._scrollContainer.scrollLeft / this.itemSize; if (instant || (!this._isTicking && this._tickerCurrent === 0)) { this._tickerCurrent = this._tickerTarget; this._applyLayout(this._tickerCurrent); } else { if (!this._isTicking) { this._isTicking = true; gsap.ticker.add(this._tick); } } }; } get effectiveItems() { return this.items && this.items.length > 0 ? this.items : this._slottedItems; } static { this.styles = [ sharedStyles, css ` :host { display: block; width: 100%; user-select: none; -webkit-user-select: none; } .carousel-container { position: relative; width: 100%; display: flex; flex-direction: column; gap: 12px; } .carousel-header { display: flex; justify-content: space-between; align-items: flex-end; padding: 0 var(--carousel-padding, 16px); } .carousel-header h3 { margin: 0; font-family: var(--font-title, serif); font-size: 1.5rem; font-weight: 500; color: var(--on-surface); } .show-all-link { color: var(--primary); font-weight: 500; font-size: 0.875rem; text-decoration: none; cursor: pointer; transition: opacity var(--speed2); } .show-all-link:hover { opacity: 0.8; text-decoration: underline; } .carousel-viewport { position: relative; width: 100%; overflow: hidden; } .scroll-container { width: 100%; height: 296px; overflow-x: auto; overflow-y: hidden; scroll-snap-type: x mandatory; scrollbar-width: none; -webkit-overflow-scrolling: touch; cursor: grab; } .scroll-container:active { cursor: grabbing; } .scroll-container::-webkit-scrollbar { display: none; } .snap-track { display: flex; width: calc(var(--carousel-track-width, 0px) + var(--carousel-right-padding, 128px)); gap: var(--carousel-gap, 8px); padding: 8px 0 8px var(--carousel-padding, 16px); height: 100%; pointer-events: none; } .snap-item { flex: 0 0 var(--carousel-snap-width, 220px); height: 100%; scroll-snap-align: start; pointer-events: none; } .visual-track { position: absolute; left: 0; top: 8px; width: 0; height: 280px; overflow: visible; pointer-events: none; } .card { position: absolute; top: 0; height: 280px; border-radius: var(--carousel-border-radius, 28px); overflow: hidden; background-color: var(--surface-container-high); pointer-events: auto; box-shadow: var(--elevate1); will-change: transform; cursor: pointer; transition: box-shadow var(--speed2); } .card:hover { box-shadow: var(--elevate2); } .card::after { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(to bottom, transparent 40%, rgba(0, 0, 0, 0.7) 100%); pointer-events: none; z-index: 1; } .img-parallax-container { width: 100%; height: 100%; position: relative; overflow: hidden; } .card img { width: calc(100% + 50px); height: 100%; object-fit: cover; position: absolute; left: -25px; pointer-events: none; user-select: none; -webkit-user-drag: none; will-change: transform; } .card-title { position: absolute; bottom: 20px; left: 20px; right: 20px; color: #ffffff; margin: 0; font-size: 1.1rem; font-weight: 500; pointer-events: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; z-index: 2; } /* Arrow navigation overlays */ .nav-button { position: absolute; top: 50%; transform: translateY(-50%); z-index: 10; width: 48px; height: 48px; border-radius: 50%; background-color: var(--surface-container-highest); color: var(--on-surface); border: none; box-shadow: var(--elevate2); display: flex; justify-content: center; align-items: center; cursor: pointer; opacity: 0; pointer-events: none; transition: opacity var(--speed2), background-color var(--speed2); } .nav-button:hover { background-color: var(--primary-container); color: var(--on-primary-container); } .nav-button.prev { left: 24px; } .nav-button.next { right: 24px; } .carousel-viewport:hover .nav-button.visible { opacity: 1; pointer-events: auto; } @media (max-width: 600px) { .nav-button { display: none !important; } } /* Utility icon style inside shadow dom */ .material-symbols { font-family: var(--font-icon); font-weight: normal; font-style: normal; font-size: 24px; line-height: 1; letter-spacing: normal; text-transform: none; display: inline-block; white-space: nowrap; word-wrap: normal; direction: ltr; -webkit-font-smoothing: antialiased; } ` ]; } connectedCallback() { super.connectedCallback(); if (typeof ResizeObserver !== 'undefined') { this._resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { this._containerWidth = entry.contentRect.width; this.updateLayout(true); // Synchronous update for instant resizing! this.requestUpdate(); } }); this._resizeObserver.observe(this); } // Detect mobile for parallax toggle if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') { const mql = window.matchMedia('(max-width: 600px)'); this._isMobile = mql.matches; const handler = (e) => { this._isMobile = e.matches; }; mql.addEventListener('change', handler); // Store for cleanup this.__mqlCleanup = () => mql.removeEventListener('change', handler); } } disconnectedCallback() { if (this._resizeObserver) { this._resizeObserver.disconnect(); } // Cancel any pending RAF or ticker if (this._rafId !== null) { cancelAnimationFrame(this._rafId); this._rafId = null; } gsap.ticker.remove(this._tick); this._isTicking = false; // Clean up matchMedia listener if (this.__mqlCleanup) { this.__mqlCleanup(); delete this.__mqlCleanup; } super.disconnectedCallback(); } firstUpdated() { const defaultSlot = this.shadowRoot?.querySelector('slot:not([name])'); if (defaultSlot) { this._handleSlotChange({ target: defaultSlot }); } const showAllSlot = this.shadowRoot?.querySelector('slot[name=\"show-all\"]'); if (showAllSlot) { this._handleShowAllSlotChange({ target: showAllSlot }); } this._buildQuickSetters(); this.updateLayout(true); } updated(changedProperties) { super.updated(changedProperties); if (changedProperties.has('layout') || changedProperties.has('largeWidth') || changedProperties.has('mediumWidth') || changedProperties.has('smallWidth') || changedProperties.has('gap') || changedProperties.has('padding') || changedProperties.has('items') || changedProperties.has('hideNav') || changedProperties.has('_containerWidth') || changedProperties.has('_slottedItems') || changedProperties.has('_hasSlottedShowAll')) { if (changedProperties.has('layout') && this._scrollContainer) { this._scrollContainer.scrollLeft = 0; this._scrollContainer.style.scrollSnapType = this.layout === 'uncontained' ? 'none' : 'x mandatory'; } this.updateLayout(true); } } get computedLayout() { const W_c = this._containerWidth || this.getBoundingClientRect().width || 360; const gap = this.gap; const padding = this.padding; const W_a = W_c - 2 * padding; let L = this.largeWidth; let M = this.mediumWidth; let S = this.smallWidth; let N = 1; if (this.auto) { if (this.layout === 'uncontained') { // Fits as many L items as possible, letting them bleed off the right edge. // We target large width around 260px. L = Math.min(W_a, 260); } else if (this.layout === 'hero') { // Hero has N large items and 1 small peeking item. // W_a = N * L + S + N * gap S = 48; N = Math.max(1, Math.floor((W_a - S) / (250 + gap))); L = (W_a - S - N * gap) / N; if (L < 150) { N = 1; L = W_a - S - gap; } } else { // multi-browse: N large items, 1 medium, 1 small // W_a = N * L + M + S + (N + 1) * gap S = 48; let bestN = 1; let bestL = 220; let bestDiff = Infinity; for (let n = 1; n <= 10; n++) { const l = (W_a - S - (n + 1) * gap) / (n + 0.45); if (l >= 140 && l <= 340) { const diff = Math.abs(l - 220); if (diff < bestDiff) { bestDiff = diff; bestN = n; bestL = l; } } } if (bestDiff === Infinity) { N = 1; L = (W_a - S - 2 * gap) / 1.45; } else { N = bestN; L = bestL; } M = 0.45 * L; } } else { // Non-auto/manual sizing mode L = this.largeWidth; M = this.mediumWidth; S = this.smallWidth; if (this.layout === 'hero') { N = 1; } else if (this.layout === 'uncontained') { N = Math.max(1, Math.floor(W_a / (L + gap))); } else { N = 1; } } return { L, M, S, N }; } get itemSize() { return this.computedLayout.L + this.gap; } _getCardLayout(p) { const { L, M, S, N } = this.computedLayout; const gap = this.gap; const padding = this.padding; let width = L; let x = padding; let opacity = 1; if (this.layout === 'uncontained') { width = L; x = padding + p * (L + gap); opacity = 1; } else if (this.layout === 'hero') { const X_N = padding + N * (L + gap); const X_M1 = padding - gap - S; if (p <= -1) { width = S; x = X_M1 + (p - -1) * (S + gap); opacity = 0; } else if (p <= 0) { width = gsap.utils.interpolate(S, L, p + 1); x = gsap.utils.interpolate(X_M1, padding, p + 1); opacity = gsap.utils.interpolate(0, 1, p + 1); } else if (p <= N - 1) { width = L; x = padding + p * (L + gap); opacity = 1; } else if (p <= N) { const u = p - (N - 1); const X_last = padding + (N - 1) * (L + gap); width = gsap.utils.interpolate(L, S, u); x = gsap.utils.interpolate(X_last, X_N, u); opacity = gsap.utils.interpolate(1, 0, u); } else { width = S; x = X_N + (p - N) * (S + gap); opacity = 0; } } else { // multi-browse const X_N = padding + N * (L + gap); const X_N1 = X_N + M + gap; const X_M1 = padding - gap - S; if (p <= -1) { width = S; x = X_M1 + (p - -1) * (S + gap); opacity = 0; } else if (p <= 0) { width = gsap.utils.interpolate(S, L, p + 1); x = gsap.utils.interpolate(X_M1, padding, p + 1); opacity = gsap.utils.interpolate(0, 1, p + 1); } else if (p <= N - 1) { width = L; x = padding + p * (L + gap); opacity = 1; } else if (p <= N) { const u = p - (N - 1); const X_last = padding + (N - 1) * (L + gap); width = gsap.utils.interpolate(L, M, u); x = gsap.utils.interpolate(X_last, X_N, u); opacity = gsap.utils.interpolate(1, 0, u); } else if (p <= N + 1) { const u = p - N; width = gsap.utils.interpolate(M, S, u); x = gsap.utils.interpolate(X_N, X_N1, u); opacity = 0; } else { width = S; x = X_N1 + (p - (N + 1)) * (S + gap); opacity = 0; } } return { width, x, opacity }; } /** * Rebuild quickSetter caches after cards change. * quickSetter avoids gsap.set() overhead for properties updated every frame. */ _buildQuickSetters() { if (!this._cards || this._cards.length === 0) return; this._cardSetX = []; this._cardSetWidth = []; this._imgSetX = []; this._titleSetOpacity = []; this._cards.forEach((card) => { this._cardSetX.push(gsap.quickSetter(card, 'x', 'px')); this._cardSetWidth.push(gsap.quickSetter(card, 'width', 'px')); const img = card.querySelector('img'); this._imgSetX.push(img ? gsap.quickSetter(img, 'x', 'px') : (() => { })); const title = card.querySelector('.card-title'); this._titleSetOpacity.push(title ? gsap.quickSetter(title, 'opacity') : (() => { })); }); } _applyLayout(t) { const isMobile = this._isMobile; const cardCount = this._cards.length; for (let i = 0; i < cardCount; i++) { const p = i - t; const layout = this._getCardLayout(p); // Use quickSetters — ~3x faster than gsap.set() per call this._cardSetX[i](layout.x); this._cardSetWidth[i](layout.width); // Skip parallax on mobile to save per-frame work if (!isMobile) { const clampedP = Math.max(-1, Math.min(1, p)); this._imgSetX[i](clampedP * -25); } this._titleSetOpacity[i](layout.opacity); } } _handleMouseDown(e) { gsap.killTweensOf(this._scrollContainer); this._isDown = true; this._draggedDistance = 0; this._scrollContainer.style.scrollBehavior = 'auto'; this._scrollContainer.style.scrollSnapType = 'none'; this._startX = e.pageX - this._scrollContainer.offsetLeft; this._scrollLeftStart = this._scrollContainer.scrollLeft; this._velocity = 0; this._lastX = e.pageX; this._lastTime = performance.now(); } _handleMouseMove(e) { if (!this._isDown) return; e.preventDefault(); const currentX = e.pageX; const x = currentX - this._scrollContainer.offsetLeft; this._scrollContainer.scrollLeft = this._scrollLeftStart + (this._startX - x) * 1.25; const currentTime = performance.now(); const dt = currentTime - this._lastTime; if (dt > 0) { this._velocity = (currentX - this._lastX) / dt; } this._draggedDistance += Math.abs(currentX - this._lastX); this._lastX = currentX; this._lastTime = currentTime; } _handleMouseUp() { if (!this._isDown) return; this._isDown = false; const currentTime = performance.now(); if (currentTime - this._lastTime > 100) { this._velocity = 0; } // Dynamic momentum: stronger swipes have a much higher multiplier const momentumMultiplier = 350 + Math.abs(this._velocity) * 300; const projectedScrollLeft = this._scrollContainer.scrollLeft - this._velocity * momentumMultiplier; if (this.layout !== 'uncontained') { this._scrollContainer.style.scrollSnapType = 'none'; let targetIndex = Math.round(projectedScrollLeft / this.itemSize); targetIndex = Math.max(0, Math.min(this.effectiveItems.length - 1, targetIndex)); const snapPoint = targetIndex * this.itemSize; // Dynamic duration: longer distance = more time, max 0.85s for faster feeling const currentIndex = Math.round(this._scrollContainer.scrollLeft / this.itemSize); const cardsSkipped = Math.abs(targetIndex - currentIndex); const duration = Math.min(0.85, 0.25 + cardsSkipped * 0.04); this._scrollContainer.style.scrollBehavior = 'auto'; gsap.to(this._scrollContainer, { scrollLeft: snapPoint, duration: duration, ease: 'power3.out', onComplete: () => { if (!this._isDown && this._scrollContainer) { this._scrollContainer.style.scrollSnapType = 'x mandatory'; } } }); } } _handleTouchStart(e) { gsap.killTweensOf(this._scrollContainer); this._isDown = true; this._draggedDistance = 0; this._scrollContainer.style.scrollBehavior = 'auto'; this._scrollContainer.style.scrollSnapType = 'none'; const touch = e.touches[0]; this._startX = touch.pageX - this._scrollContainer.offsetLeft; this._scrollLeftStart = this._scrollContainer.scrollLeft; this._velocity = 0; this._lastX = touch.pageX; this._lastTime = performance.now(); } _handleTouchMove(e) { if (!this._isDown) return; const touch = e.touches[0]; const currentX = touch.pageX; const x = currentX - this._scrollContainer.offsetLeft; this._scrollContainer.scrollLeft = this._scrollLeftStart + (this._startX - x); const currentTime = performance.now(); const dt = currentTime - this._lastTime; if (dt > 0) { this._velocity = (currentX - this._lastX) / dt; } this._draggedDistance += Math.abs(currentX - this._lastX); this._lastX = currentX; this._lastTime = currentTime; } _handleTouchEnd() { this._handleMouseUp(); } _handleWheel(e) { // Solo interceptar scroll vertical puro (rueda de mouse típica) if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) { const maxScroll = this._scrollContainer.scrollWidth - this._scrollContainer.clientWidth; const currentScroll = this._scrollContainer.scrollLeft; // Comprobar si estamos en los bordes para permitir el scroll natural de la página const isAtStart = currentScroll <= 0; const isAtEnd = currentScroll >= maxScroll - 1; // Margen de 1px por redondeos decimales if ((e.deltaY < 0 && isAtStart) || (e.deltaY > 0 && isAtEnd)) { return; // Dejar que la página haga scroll vertical } e.preventDefault(); // Prevenir el scroll de la página // Desactivar el snap momentáneamente para que el movimiento de rueda sea fluido this._scrollContainer.style.scrollSnapType = 'none'; // Multiplicador si el deltaMode no es pixel (0) let delta = e.deltaY; if (e.deltaMode === 1) delta *= 40; // Líneas else if (e.deltaMode === 2) delta *= 800; // Páginas this._scrollContainer.scrollLeft += delta; // Restablecer el snap después de un pequeño retraso if (this._wheelTimeout) clearTimeout(this._wheelTimeout); this._wheelTimeout = setTimeout(() => { if (this.layout !== 'uncontained') { this._scrollContainer.style.scrollSnapType = 'x mandatory'; } }, 150); } } _handleCardClick(e, item, index) { if (this._draggedDistance > 8) { e.preventDefault(); e.stopPropagation(); return; } this.dispatchEvent(new CustomEvent('item-click', { detail: { item, index }, bubbles: true, composed: true })); } _scrollPrevious() { const target = Math.max(0, this._scrollContainer.scrollLeft - this.itemSize); this._scrollContainer.style.scrollBehavior = 'smooth'; this._scrollContainer.scrollTo({ left: target, behavior: 'smooth' }); } _scrollNext() { const maxScroll = this._scrollContainer.scrollWidth - this._scrollContainer.clientWidth; const target = Math.min(maxScroll, this._scrollContainer.scrollLeft + this.itemSize); this._scrollContainer.style.scrollBehavior = 'smooth'; this._scrollContainer.scrollTo({ left: target, behavior: 'smooth' }); } _handleShowAllClick(e) { const target = e.target; if (target.tagName.toLowerCase() === 'a' && target.getAttribute('href') === '#') { e.preventDefault(); } this.dispatchEvent(new CustomEvent('show-all-click', { bubbles: true, composed: true })); } _handleShowAllSlotChange(e) { const slot = e.target; let nodes = typeof slot.assignedElements === 'function' ? slot.assignedElements({ flatten: true }) : []; if (nodes.length === 0) { nodes = slot.assignedNodes().filter(n => n.nodeType === 1); } if (nodes.length === 0) { nodes = Array.from(this.querySelectorAll('[slot=\"show-all\"]')); // Fallback for JSDOM } this._hasSlottedShowAll = nodes.length > 0; } _handleSlotChange(e) { const slot = e.target; let nodes = typeof slot.assignedElements === 'function' ? slot.assignedElements({ flatten: true }) : []; if (nodes.length === 0) { nodes = slot.assignedNodes().filter(n => n.nodeType === 1); } if (nodes.length === 0) { nodes = Array.from(this.children).filter(child => child.getAttribute('slot') !== 'show-all'); // Fallback for JSDOM, ignore show-all slot } const items = []; for (const node of nodes) { if (node.tagName.toLowerCase() === 'img') { const img = node; items.push({ title: img.getAttribute('title') || img.getAttribute('alt') || '', img: img.src }); } else if (node.tagName.toLowerCase() === 'a') { const anchor = node; const img = anchor.querySelector('img'); if (img) { items.push({ title: img.getAttribute('title') || img.getAttribute('alt') || anchor.getAttribute('title') || '', img: img.src, href: anchor.href, target: anchor.target }); } } } this._slottedItems = items; // Check if slotted show-all button presence changed const hasSlottedShowAll = !!this.querySelector('[slot=\"show-all\"]'); if (hasSlottedShowAll !== this._hasSlottedShowAll) { this._hasSlottedShowAll = hasSlottedShowAll; } void this.updateComplete.then(() => { this.updateLayout(true); }); } render() { const { L, M, S } = this.computedLayout; const hasItems = this.effectiveItems && this.effectiveItems.length > 0; const hasSlottedShowAll = this._hasSlottedShowAll || !!this.querySelector('[slot=\"show-all\"]'); const showHeader = this.headerText || this.showAll || hasSlottedShowAll; let rightPadding = 128; if (this.layout === 'hero') { rightPadding = S + this.gap; } else if (this.layout === 'uncontained') { rightPadding = 16; } else { rightPadding = M + S + 2 * this.gap; } // Ensure the scroll track is long enough to allow the last item to scroll to the start (p = 0 focus). // Safety margin of 400px avoids fractional rounding/zoom-scale snapping blockages in Firefox. const W_c = this._containerWidth || this.getBoundingClientRect().width || 360; const minRightPaddingNeeded = W_c - L - this.padding + 400; if (rightPadding < minRightPaddingNeeded) { rightPadding = minRightPaddingNeeded; } const trackWidth = hasItems ? this.effectiveItems.length * L + (this.effectiveItems.length - 1) * this.gap : 0; const isScrollable = hasItems && this._scrollContainer && (this._scrollContainer.scrollWidth > this._scrollContainer.clientWidth); const showPrevArrow = isScrollable && this._scrollContainer.scrollLeft > 5; const showNextArrow = isScrollable && (this._scrollContainer.scrollWidth - this._scrollContainer.scrollLeft - this._scrollContainer.clientWidth > 5); return html ` <div class=\"carousel-container\" style=\" --carousel-snap-width: ${L}px; --carousel-gap: ${this.gap}px; --carousel-padding: ${this.padding}px; --carousel-right-padding: ${rightPadding}px; --carousel-border-radius: ${this.borderRadius}px; --carousel-track-width: ${trackWidth}px; \"> <slot @slotchange=${this._handleSlotChange} style=\"display: none;\"></slot> ${showHeader ? html ` <div class=\"carousel-header\"> ${this.headerText ? html `<h3>${this.headerText}</h3>` : ''} <slot name=\"show-all\" @slotchange=${this._handleShowAllSlotChange} @click=${this._handleShowAllClick}> ${this.showAll ? html ` <a href=\"#\" class=\"show-all-link\"> ${this.showAllText} </a> ` : ''} </slot> </div> ` : ''} <div class=\"carousel-viewport\"> ${!this.hideNav ? html ` <button class=\"nav-button prev ${showPrevArrow ? 'visible' : ''}\" @click=${this._scrollPrevious} aria-label=\"Previous items\" > <span class=\"material-symbols\">chevron_left</span> </button> ` : ''} <div class=\"scroll-container\" @scroll=${this._scheduleLayout} @mousedown=${this._handleMouseDown} @mousemove=${this._handleMouseMove} @mouseup=${this._handleMouseUp} @mouseleave=${this._handleMouseUp} @touchstart=${this._handleTouchStart} @touchmove=${this._handleTouchMove} @touchend=${this._handleTouchEnd} @wheel=${this._handleWheel} > <div class=\"snap-track\"> ${hasItems ? this.effectiveItems.map(() => html `<div class=\"snap-item\"></div>`) : ''} </div> <div class=\"visual-track\"> ${hasItems ? this.effectiveItems.map((item, idx) => html ` <div class=\"card\" @click=${(e) => this._handleCardClick(e, item, idx)} > ${item.href ? html ` <a href=\"${item.href}\" target=\"${item.target || '_self'}\" style=\"text-decoration:none; color:inherit;\"> <div class=\"img-parallax-container\"> <img src=\"${item.img}\" alt=\"${item.title}\" draggable=\"false\" /> </div> <h2 class=\"card-title\">${item.title}</h2> </a> ` : html ` <div class=\"img-parallax-container\"> <img src=\"${item.img}\" alt=\"${item.title}\" draggable=\"false\" /> </div> <h2 class=\"card-title\">${item.title}</h2> `} </div> `) : ''} </div> </div> ${!this.hideNav ? html ` <button class=\"nav-button next ${showNextArrow ? 'visible' : ''}\" @click=${this._scrollNext} aria-label=\"Next items\" > <span class=\"material-symbols\">chevron_right</span> </button> ` : ''} </div> </div> `; } }",
|
|
1452
|
+
"default": "class MoniCarousel extends MoniElement { constructor() { super(...arguments); this.items = []; this.layout = 'multi-browse'; this.auto = true; this.largeWidth = 220; this.mediumWidth = 96; this.smallWidth = 48; this.gap = 8; this.padding = 16; this.borderRadius = 28; this.showAll = false; this.showAllText = 'Show all'; this.headerText = ''; this.hideNav = false; this.infinite = false; this.autoplay = false; this.autoplayInterval = 3000; this._containerWidth = 0; this._slottedItems = []; this._hasSlottedShowAll = false; this._resizeObserver = null; this._isDown = false; this._startX = 0; this._scrollLeftStart = 0; this._velocity = 0; this._lastTime = 0; this._lastX = 0; this._draggedDistance = 0; this._rafId = null; this._isMobile = false; // Scroll interpolation state this._tickerTarget = 0; this._tickerCurrent = 0; this._isTicking = false; // Infinite & Autoplay state this._infiniteInitialized = false; this._autoplayTimer = null; // quickSetter caches — avoid gsap.set() overhead per frame this._cardSetX = []; this._cardSetWidth = []; this._imgSetX = []; this._titleSetOpacity = []; /** * RAF-gated version of updateLayout for scroll events. * Prevents multiple layout recalculations per frame. */ this._tick = () => { const dt = gsap.ticker.deltaRatio(); // Smoother lerp on mobile to avoid abrupt changes (\"brusco\") const ease = this._isMobile ? 0.08 : 0.15; this._tickerCurrent += (this._tickerTarget - this._tickerCurrent) * ease * dt; if (Math.abs(this._tickerTarget - this._tickerCurrent) < 0.0005) { this._tickerCurrent = this._tickerTarget; gsap.ticker.remove(this._tick); this._isTicking = false; this._checkInfiniteWrap(); } this._applyLayout(this._tickerCurrent); }; this._scheduleLayout = () => { if (!this._scrollContainer) return; this._tickerTarget = this._scrollContainer.scrollLeft / this.itemSize; if (!this._isTicking) { this._isTicking = true; gsap.ticker.add(this._tick); } }; this.updateLayout = (instant = false) => { if (!this._scrollContainer || !this._cards || this._cards.length === 0) return; // Rebuild quickSetters if card count changed (e.g. after items update) if (this._cardSetX.length !== this._cards.length) { this._buildQuickSetters(); } if (this.infinite && !this._infiniteInitialized && this.effectiveItems.length > 0) { const C = this.effectiveItems.length; const V = C * this._visualCardsMultiplier; const K = this._snapSetsCount; const centerSetIndex = Math.floor(K / 2); // Wait for the browser to render the new items so scrollWidth is updated // A double-rAF or setTimeout ensures the layout engine has processed the new snap-items requestAnimationFrame(() => { setTimeout(() => { if (this._scrollContainer) { // Force layout calculation this._scrollContainer.scrollWidth; this._scrollContainer.scrollLeft = centerSetIndex * V * this.itemSize; // If the browser still clamped it, it means it's not ready. // But with setTimeout it should be fully ready. // Force the visual ticker to match instantly to avoid an animation zoom from 0 this._tickerTarget = this._scrollContainer.scrollLeft / this.itemSize; this._tickerCurrent = this._tickerTarget; this._applyLayout(this._tickerCurrent); } }, 20); }); this._infiniteInitialized = true; } this._tickerTarget = this._scrollContainer.scrollLeft / this.itemSize; if (instant || (!this._isTicking && this._tickerCurrent === 0)) { this._tickerCurrent = this._tickerTarget; this._applyLayout(this._tickerCurrent); } else { if (!this._isTicking) { this._isTicking = true; gsap.ticker.add(this._tick); } } }; } get effectiveItems() { return this.items && this.items.length > 0 ? this.items : this._slottedItems; } static { this.styles = [ sharedStyles, css ` :host { display: block; width: 100%; user-select: none; -webkit-user-select: none; } .carousel-container { position: relative; width: 100%; display: flex; flex-direction: column; gap: 12px; } .carousel-header { display: flex; justify-content: space-between; align-items: flex-end; padding: 0 var(--carousel-padding, 16px); } .carousel-header h3 { margin: 0; font-family: var(--font-title, serif); font-size: 1.5rem; font-weight: 500; color: var(--on-surface); } .show-all-link { color: var(--primary); font-weight: 500; font-size: 0.875rem; text-decoration: none; cursor: pointer; transition: opacity var(--speed2); } .show-all-link:hover { opacity: 0.8; text-decoration: underline; } .carousel-viewport { position: relative; width: 100%; overflow: hidden; } .scroll-container { width: 100%; height: 296px; overflow-x: auto; overflow-y: hidden; scroll-snap-type: x mandatory; scrollbar-width: none; -webkit-overflow-scrolling: touch; cursor: grab; } .scroll-container:active { cursor: grabbing; } .scroll-container::-webkit-scrollbar { display: none; } .snap-track { display: flex; width: calc(var(--carousel-track-width, 0px) + var(--carousel-right-padding, 128px)); gap: var(--carousel-gap, 8px); padding: 8px 0 8px var(--carousel-padding, 16px); height: 100%; pointer-events: none; } .snap-item { flex: 0 0 var(--carousel-snap-width, 220px); height: 100%; scroll-snap-align: start; pointer-events: none; } .visual-track { position: absolute; left: 0; top: 8px; width: 0; height: 280px; overflow: visible; pointer-events: none; } .card { position: absolute; top: 0; height: 280px; border-radius: var(--carousel-border-radius, 28px); overflow: hidden; background-color: var(--surface-container-high); pointer-events: auto; box-shadow: var(--elevate1); will-change: transform; cursor: pointer; transition: box-shadow var(--speed2); } .card:hover { box-shadow: var(--elevate2); } .card::after { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(to bottom, transparent 40%, rgba(0, 0, 0, 0.7) 100%); pointer-events: none; z-index: 1; } .img-parallax-container { width: 100%; height: 100%; position: relative; overflow: hidden; } .card img { width: calc(100% + 50px); height: 100%; object-fit: cover; position: absolute; left: -25px; pointer-events: none; user-select: none; -webkit-user-drag: none; will-change: transform; } .card-title { position: absolute; bottom: 20px; left: 20px; right: 20px; color: #ffffff; margin: 0; font-size: 1.1rem; font-weight: 500; pointer-events: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; z-index: 2; } /* Arrow navigation overlays */ .nav-button { position: absolute; top: 50%; transform: translateY(-50%); z-index: 10; width: 48px; height: 48px; border-radius: 50%; background-color: var(--surface-container-highest); color: var(--on-surface); border: none; box-shadow: var(--elevate2); display: flex; justify-content: center; align-items: center; cursor: pointer; opacity: 0; pointer-events: none; transition: opacity var(--speed2), background-color var(--speed2); } .nav-button:hover { background-color: var(--primary-container); color: var(--on-primary-container); } .nav-button.prev { left: 24px; } .nav-button.next { right: 24px; } .carousel-viewport:hover .nav-button.visible { opacity: 1; pointer-events: auto; } @media (max-width: 600px) { .nav-button { display: none !important; } } /* Utility icon style inside shadow dom */ .material-symbols { font-family: var(--font-icon); font-weight: normal; font-style: normal; font-size: 24px; line-height: 1; letter-spacing: normal; text-transform: none; display: inline-block; white-space: nowrap; word-wrap: normal; direction: ltr; -webkit-font-smoothing: antialiased; } ` ]; } connectedCallback() { super.connectedCallback(); if (typeof ResizeObserver !== 'undefined') { this._resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { this._containerWidth = entry.contentRect.width; this.updateLayout(true); // Synchronous update for instant resizing! this.requestUpdate(); } }); this._resizeObserver.observe(this); } // Detect mobile for parallax toggle if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') { const mql = window.matchMedia('(max-width: 600px)'); this._isMobile = mql.matches; const handler = (e) => { this._isMobile = e.matches; }; mql.addEventListener('change', handler); // Store for cleanup this.__mqlCleanup = () => mql.removeEventListener('change', handler); } } disconnectedCallback() { if (this._resizeObserver) { this._resizeObserver.disconnect(); } // Cancel any pending RAF or ticker if (this._rafId !== null) { cancelAnimationFrame(this._rafId); this._rafId = null; } gsap.ticker.remove(this._tick); this._isTicking = false; // Clean up matchMedia listener if (this.__mqlCleanup) { this.__mqlCleanup(); delete this.__mqlCleanup; } super.disconnectedCallback(); } firstUpdated() { const defaultSlot = this.shadowRoot?.querySelector('slot:not([name])'); if (defaultSlot) { this._handleSlotChange({ target: defaultSlot }); } const showAllSlot = this.shadowRoot?.querySelector('slot[name=\"show-all\"]'); if (showAllSlot) { this._handleShowAllSlotChange({ target: showAllSlot }); } this._buildQuickSetters(); this.updateLayout(true); if (this.autoplay) { this._startAutoplay(); } } updated(changedProperties) { super.updated(changedProperties); if (changedProperties.has('layout') || changedProperties.has('largeWidth') || changedProperties.has('mediumWidth') || changedProperties.has('smallWidth') || changedProperties.has('gap') || changedProperties.has('padding') || changedProperties.has('items') || changedProperties.has('hideNav') || changedProperties.has('infinite') || changedProperties.has('autoplay') || changedProperties.has('autoplayInterval') || changedProperties.has('_containerWidth') || changedProperties.has('_slottedItems') || changedProperties.has('_hasSlottedShowAll')) { if (changedProperties.has('infinite') || changedProperties.has('items') || changedProperties.has('_slottedItems')) { this._infiniteInitialized = false; } if (changedProperties.has('layout') && this._scrollContainer) { this._scrollContainer.scrollLeft = 0; this._infiniteInitialized = false; this._scrollContainer.style.scrollSnapType = this.layout === 'uncontained' ? 'none' : 'x mandatory'; } this.updateLayout(true); if (changedProperties.has('autoplay') || changedProperties.has('autoplayInterval') || changedProperties.has('items') || changedProperties.has('_slottedItems')) { if (this.autoplay) { this._startAutoplay(); } else { this._stopAutoplay(); } } } } get computedLayout() { const W_c = this._containerWidth || this.getBoundingClientRect().width || 360; const gap = this.gap; const padding = this.padding; const W_a = W_c - 2 * padding; let L = this.largeWidth; let M = this.mediumWidth; let S = this.smallWidth; let N = 1; if (this.auto) { if (this.layout === 'uncontained') { // Fits as many L items as possible, letting them bleed off the right edge. // We target large width around 260px. L = Math.min(W_a, 260); } else if (this.layout === 'hero') { // Hero has N large items and 1 small peeking item. // W_a = N * L + S + N * gap S = 48; N = Math.max(1, Math.floor((W_a - S) / (250 + gap))); L = (W_a - S - N * gap) / N; if (L < 150) { N = 1; L = W_a - S - gap; } } else { // multi-browse: N large items, 1 medium, 1 small // W_a = N * L + M + S + (N + 1) * gap S = 48; let bestN = 1; let bestL = 220; let bestDiff = Infinity; for (let n = 1; n <= 10; n++) { const l = (W_a - S - (n + 1) * gap) / (n + 0.45); if (l >= 140 && l <= 340) { const diff = Math.abs(l - 220); if (diff < bestDiff) { bestDiff = diff; bestN = n; bestL = l; } } } if (bestDiff === Infinity) { N = 1; L = (W_a - S - 2 * gap) / 1.45; } else { N = bestN; L = bestL; } M = 0.45 * L; } } else { // Non-auto/manual sizing mode L = this.largeWidth; M = this.mediumWidth; S = this.smallWidth; if (this.layout === 'hero') { N = 1; } else if (this.layout === 'uncontained') { N = Math.max(1, Math.floor(W_a / (L + gap))); } else { N = 1; } } return { L, M, S, N }; } get itemSize() { return this.computedLayout.L + this.gap; } _getCardLayout(p) { const { L, M, S, N } = this.computedLayout; const gap = this.gap; const padding = this.padding; let width = L; let x = padding; let opacity = 1; if (this.layout === 'uncontained') { width = L; x = padding + p * (L + gap); opacity = 1; } else if (this.layout === 'hero') { const X_N = padding + N * (L + gap); const X_M1 = padding - gap - S; if (p <= -1) { width = S; x = X_M1 + (p - -1) * (S + gap); opacity = 0; } else if (p <= 0) { width = gsap.utils.interpolate(S, L, p + 1); x = gsap.utils.interpolate(X_M1, padding, p + 1); opacity = gsap.utils.interpolate(0, 1, p + 1); } else if (p <= N - 1) { width = L; x = padding + p * (L + gap); opacity = 1; } else if (p <= N) { const u = p - (N - 1); const X_last = padding + (N - 1) * (L + gap); width = gsap.utils.interpolate(L, S, u); x = gsap.utils.interpolate(X_last, X_N, u); opacity = gsap.utils.interpolate(1, 0, u); } else { width = S; x = X_N + (p - N) * (S + gap); opacity = 0; } } else { // multi-browse const X_N = padding + N * (L + gap); const X_N1 = X_N + M + gap; const X_M1 = padding - gap - S; if (p <= -1) { width = S; x = X_M1 + (p - -1) * (S + gap); opacity = 0; } else if (p <= 0) { width = gsap.utils.interpolate(S, L, p + 1); x = gsap.utils.interpolate(X_M1, padding, p + 1); opacity = gsap.utils.interpolate(0, 1, p + 1); } else if (p <= N - 1) { width = L; x = padding + p * (L + gap); opacity = 1; } else if (p <= N) { const u = p - (N - 1); const X_last = padding + (N - 1) * (L + gap); width = gsap.utils.interpolate(L, M, u); x = gsap.utils.interpolate(X_last, X_N, u); opacity = gsap.utils.interpolate(1, 0, u); } else if (p <= N + 1) { const u = p - N; width = gsap.utils.interpolate(M, S, u); x = gsap.utils.interpolate(X_N, X_N1, u); opacity = 0; } else { width = S; x = X_N1 + (p - (N + 1)) * (S + gap); opacity = 0; } } return { width, x, opacity }; } /** * Rebuild quickSetter caches after cards change. * quickSetter avoids gsap.set() overhead for properties updated every frame. */ _buildQuickSetters() { if (!this._cards || this._cards.length === 0) return; this._cardSetX = []; this._cardSetWidth = []; this._imgSetX = []; this._titleSetOpacity = []; this._cards.forEach((card) => { this._cardSetX.push(gsap.quickSetter(card, 'x', 'px')); this._cardSetWidth.push(gsap.quickSetter(card, 'width', 'px')); const img = card.querySelector('img'); this._imgSetX.push(img ? gsap.quickSetter(img, 'x', 'px') : (() => { })); const title = card.querySelector('.card-title'); this._titleSetOpacity.push(title ? gsap.quickSetter(title, 'opacity') : (() => { })); }); } _applyLayout(t) { const isMobile = this._isMobile; const cardCount = this._cards.length; for (let i = 0; i < cardCount; i++) { let p = i - t; if (this.infinite) { // Modulo wrap so the cards perfectly loop around the center p = i - (t % cardCount); if (p > cardCount / 2) p -= cardCount; if (p < -cardCount / 2) p += cardCount; } // Optimization: if the card is far off-screen, hide it to save rendering work if (p < -15 || p > 15) { this._cardSetX[i](-9999); this._titleSetOpacity[i](0); continue; } const layout = this._getCardLayout(p); // Use quickSetters — ~3x faster than gsap.set() per call this._cardSetX[i](layout.x); this._cardSetWidth[i](layout.width); // Skip parallax on mobile to save per-frame work if (!isMobile) { const clampedP = Math.max(-1, Math.min(1, p)); this._imgSetX[i](clampedP * -25); } this._titleSetOpacity[i](layout.opacity); } } get _visualCardsMultiplier() { if (!this.infinite || this.effectiveItems.length === 0) return 1; const C = this.effectiveItems.length; // We need at least ~15 cards to fill the screen seamlessly for small datasets return Math.max(1, Math.ceil(15 / C)); } get _snapSetsCount() { if (!this.infinite || this.effectiveItems.length === 0) return 1; const C = this.effectiveItems.length; const V = C * this._visualCardsMultiplier; // We want enough snap items so the user NEVER reaches the end in a single fast swipe. // A total of ~1000 items is extremely cheap as empty divs, and provides a massive buffer. let K = Math.ceil(1000 / V); if (K % 2 === 0) K += 1; // Keep it odd for a perfect center return Math.max(3, K); } get _snapItemsCount() { if (this.infinite) { const C = this.effectiveItems.length; const V_mult = this._visualCardsMultiplier; const K = this._snapSetsCount; return K * (C * V_mult); } return this.effectiveItems.length; } _checkInfiniteWrap() { if (!this.infinite || this.effectiveItems.length === 0 || !this._scrollContainer) return; const C = this.effectiveItems.length; const V = C * this._visualCardsMultiplier; const K = this._snapSetsCount; const centerSetIndex = Math.floor(K / 2); // The scroll track is divided into K sets, each containing V items. const setWidth = V * this.itemSize; const currentScroll = this._scrollContainer.scrollLeft; const currentSet = Math.floor(currentScroll / setWidth); if (currentSet !== centerSetIndex) { const delta = (centerSetIndex - currentSet) * setWidth; const oldSnap = this._scrollContainer.style.scrollSnapType; const oldBehavior = this._scrollContainer.style.scrollBehavior; // Temporarily disable snap and smooth behavior so the wrap is instantaneous this._scrollContainer.style.scrollSnapType = 'none'; this._scrollContainer.style.scrollBehavior = 'auto'; this._scrollContainer.scrollLeft += delta; // INSTANTLY update the GSAP visual ticker so it doesn't animate the jump this._tickerTarget = this._scrollContainer.scrollLeft / this.itemSize; this._tickerCurrent = this._tickerTarget; this._applyLayout(this._tickerCurrent); // Restore original scroll behavior this._scrollContainer.style.scrollBehavior = oldBehavior; if (this._isDown) { this._scrollLeftStart += delta; } if (oldSnap && oldSnap !== 'none') { // Re-enable snap on the next frame to avoid jumping requestAnimationFrame(() => { if (this._scrollContainer && !this._isDown) { this._scrollContainer.style.scrollSnapType = oldSnap; } }); } } } _startAutoplay() { this._stopAutoplay(); if (this.autoplay && this.effectiveItems.length > 0) { this._autoplayTimer = setInterval(() => { // We removed _isInteracting to make autoplay more robust. It only pauses on active drag. if (!this._isDown) { this._scrollNext(); } }, this.autoplayInterval); } } _stopAutoplay() { if (this._autoplayTimer) { clearInterval(this._autoplayTimer); this._autoplayTimer = null; } } _handleMouseEnter() { } _handleMouseLeave() { this._handleMouseUp(); } _handleMouseDown(e) { gsap.killTweensOf(this._scrollContainer); this._checkInfiniteWrap(); this._isDown = true; this._draggedDistance = 0; this._scrollContainer.style.scrollBehavior = 'auto'; this._scrollContainer.style.scrollSnapType = 'none'; this._startX = e.pageX - this._scrollContainer.offsetLeft; this._scrollLeftStart = this._scrollContainer.scrollLeft; this._velocity = 0; this._lastX = e.pageX; this._lastTime = performance.now(); } _handleMouseMove(e) { if (!this._isDown) return; e.preventDefault(); const currentX = e.pageX; const x = currentX - this._scrollContainer.offsetLeft; this._scrollContainer.scrollLeft = this._scrollLeftStart + (this._startX - x) * 1.25; const currentTime = performance.now(); const dt = currentTime - this._lastTime; if (dt > 0) { this._velocity = (currentX - this._lastX) / dt; } this._draggedDistance += Math.abs(currentX - this._lastX); this._lastX = currentX; this._lastTime = currentTime; } _handleMouseUp() { if (!this._isDown) return; this._isDown = false; const currentTime = performance.now(); if (currentTime - this._lastTime > 100) { this._velocity = 0; } // Dynamic momentum: stronger swipes have a much higher multiplier const momentumMultiplier = 350 + Math.abs(this._velocity) * 300; let projectedScrollLeft = this._scrollContainer.scrollLeft - this._velocity * momentumMultiplier; // Cap momentum throw to prevent hitting infinite wrap edges on fast swipes const maxThrowDist = 15 * this.itemSize; if (projectedScrollLeft < this._scrollContainer.scrollLeft - maxThrowDist) { projectedScrollLeft = this._scrollContainer.scrollLeft - maxThrowDist; } else if (projectedScrollLeft > this._scrollContainer.scrollLeft + maxThrowDist) { projectedScrollLeft = this._scrollContainer.scrollLeft + maxThrowDist; } if (this.layout !== 'uncontained') { this._scrollContainer.style.scrollSnapType = 'none'; let targetIndex = Math.round(projectedScrollLeft / this.itemSize); const maxIndex = this.effectiveItems.length > 0 ? this._snapItemsCount - 1 : 0; targetIndex = Math.max(0, Math.min(maxIndex, targetIndex)); const snapPoint = targetIndex * this.itemSize; // Dynamic duration: longer distance = more time, max 0.85s for faster feeling const currentIndex = Math.round(this._scrollContainer.scrollLeft / this.itemSize); const cardsSkipped = Math.abs(targetIndex - currentIndex); const duration = Math.min(0.85, 0.25 + cardsSkipped * 0.04); this._scrollContainer.style.scrollBehavior = 'auto'; gsap.to(this._scrollContainer, { scrollLeft: snapPoint, duration: duration, ease: 'power3.out', onComplete: () => { if (!this._isDown && this._scrollContainer) { this._scrollContainer.style.scrollSnapType = 'x mandatory'; } } }); } } _handleTouchStart(e) { gsap.killTweensOf(this._scrollContainer); this._checkInfiniteWrap(); this._isDown = true; this._draggedDistance = 0; this._scrollContainer.style.scrollBehavior = 'auto'; this._scrollContainer.style.scrollSnapType = 'none'; const touch = e.touches[0]; this._startX = touch.pageX - this._scrollContainer.offsetLeft; this._scrollLeftStart = this._scrollContainer.scrollLeft; this._velocity = 0; this._lastX = touch.pageX; this._lastTime = performance.now(); } _handleTouchMove(e) { if (!this._isDown) return; const touch = e.touches[0]; const currentX = touch.pageX; const x = currentX - this._scrollContainer.offsetLeft; this._scrollContainer.scrollLeft = this._scrollLeftStart + (this._startX - x); const currentTime = performance.now(); const dt = currentTime - this._lastTime; if (dt > 0) { this._velocity = (currentX - this._lastX) / dt; } this._draggedDistance += Math.abs(currentX - this._lastX); this._lastX = currentX; this._lastTime = currentTime; } _handleTouchEnd() { this._handleMouseUp(); } _handleWheel(e) { // Solo interceptar scroll vertical puro (rueda de mouse típica) if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) { const maxScroll = this._scrollContainer.scrollWidth - this._scrollContainer.clientWidth; const currentScroll = this._scrollContainer.scrollLeft; // Comprobar si estamos en los bordes para permitir el scroll natural de la página const isAtStart = currentScroll <= 0; const isAtEnd = currentScroll >= maxScroll - 1; // Margen de 1px por redondeos decimales if ((e.deltaY < 0 && isAtStart) || (e.deltaY > 0 && isAtEnd)) { return; // Dejar que la página haga scroll vertical } e.preventDefault(); // Prevenir el scroll de la página // Desactivar el snap momentáneamente para que el movimiento de rueda sea fluido this._scrollContainer.style.scrollSnapType = 'none'; // Multiplicador si el deltaMode no es pixel (0) let delta = e.deltaY; if (e.deltaMode === 1) delta *= 40; // Líneas else if (e.deltaMode === 2) delta *= 800; // Páginas this._scrollContainer.scrollLeft += delta; // Restablecer el snap después de un pequeño retraso if (this._wheelTimeout) clearTimeout(this._wheelTimeout); this._wheelTimeout = setTimeout(() => { if (this.layout !== 'uncontained' && !this._isDown) { this._scrollContainer.style.scrollSnapType = 'x mandatory'; } }, 150); } } _handleCardClick(e, item, index) { if (this._draggedDistance > 8) { e.preventDefault(); e.stopPropagation(); return; } const originalIndex = index % this.effectiveItems.length; this.dispatchEvent(new CustomEvent('item-click', { detail: { item, index: originalIndex }, bubbles: true, composed: true })); } _scrollPrevious() { const target = this._scrollContainer.scrollLeft - this.itemSize; this._scrollContainer.style.scrollBehavior = 'smooth'; this._scrollContainer.scrollTo({ left: target, behavior: 'smooth' }); } _scrollNext() { const target = this._scrollContainer.scrollLeft + this.itemSize; this._scrollContainer.style.scrollBehavior = 'smooth'; this._scrollContainer.scrollTo({ left: target, behavior: 'smooth' }); } _handleShowAllClick(e) { const target = e.target; if (target.tagName.toLowerCase() === 'a' && target.getAttribute('href') === '#') { e.preventDefault(); } this.dispatchEvent(new CustomEvent('show-all-click', { bubbles: true, composed: true })); } _handleShowAllSlotChange(e) { const slot = e.target; let nodes = typeof slot.assignedElements === 'function' ? slot.assignedElements({ flatten: true }) : []; if (nodes.length === 0) { nodes = slot.assignedNodes().filter(n => n.nodeType === 1); } if (nodes.length === 0) { nodes = Array.from(this.querySelectorAll('[slot=\"show-all\"]')); // Fallback for JSDOM } this._hasSlottedShowAll = nodes.length > 0; } _handleSlotChange(e) { const slot = e.target; let nodes = typeof slot.assignedElements === 'function' ? slot.assignedElements({ flatten: true }) : []; if (nodes.length === 0) { nodes = slot.assignedNodes().filter(n => n.nodeType === 1); } if (nodes.length === 0) { nodes = Array.from(this.children).filter(child => child.getAttribute('slot') !== 'show-all'); // Fallback for JSDOM, ignore show-all slot } const items = []; for (const node of nodes) { if (node.tagName.toLowerCase() === 'img') { const img = node; items.push({ title: img.getAttribute('title') || img.getAttribute('alt') || '', img: img.src }); } else if (node.tagName.toLowerCase() === 'a') { const anchor = node; const img = anchor.querySelector('img'); if (img) { items.push({ title: img.getAttribute('title') || img.getAttribute('alt') || anchor.getAttribute('title') || '', img: img.src, href: anchor.href, target: anchor.target }); } } } this._slottedItems = items; // Check if slotted show-all button presence changed const hasSlottedShowAll = !!this.querySelector('[slot=\"show-all\"]'); if (hasSlottedShowAll !== this._hasSlottedShowAll) { this._hasSlottedShowAll = hasSlottedShowAll; } void this.updateComplete.then(() => { this.updateLayout(true); }); } render() { const { L, M, S } = this.computedLayout; const hasItems = this.effectiveItems && this.effectiveItems.length > 0; const hasSlottedShowAll = this._hasSlottedShowAll || !!this.querySelector('[slot=\"show-all\"]'); const showHeader = this.headerText || this.showAll || hasSlottedShowAll; let rightPadding = 128; if (this.layout === 'hero') { rightPadding = S + this.gap; } else if (this.layout === 'uncontained') { rightPadding = 16; } else { rightPadding = M + S + 2 * this.gap; } // Ensure the scroll track is long enough to allow the last item to scroll to the start (p = 0 focus). // Safety margin of 400px avoids fractional rounding/zoom-scale snapping blockages in Firefox. const W_c = this._containerWidth || this.getBoundingClientRect().width || 360; const minRightPaddingNeeded = W_c - L - this.padding + 400; if (rightPadding < minRightPaddingNeeded) { rightPadding = minRightPaddingNeeded; } let snapItemsCount = hasItems ? this._snapItemsCount : 0; let visualItems = this.effectiveItems; if (this.infinite && hasItems) { const V_mult = this._visualCardsMultiplier; visualItems = []; for (let i = 0; i < V_mult; i++) { visualItems = visualItems.concat(this.effectiveItems); } } const trackWidth = hasItems ? snapItemsCount * L + (snapItemsCount - 1) * this.gap : 0; const isScrollable = hasItems && this._scrollContainer && (this._scrollContainer.scrollWidth > this._scrollContainer.clientWidth); const showPrevArrow = this.infinite || (isScrollable && this._scrollContainer.scrollLeft > 5); const showNextArrow = this.infinite || (isScrollable && (this._scrollContainer.scrollWidth - this._scrollContainer.scrollLeft - this._scrollContainer.clientWidth > 5)); return html ` <div class=\"carousel-container\" style=\" --carousel-snap-width: ${L}px; --carousel-gap: ${this.gap}px; --carousel-padding: ${this.padding}px; --carousel-right-padding: ${rightPadding}px; --carousel-border-radius: ${this.borderRadius}px; --carousel-track-width: ${trackWidth}px; \"> <slot @slotchange=${this._handleSlotChange} style=\"display: none;\"></slot> ${showHeader ? html ` <div class=\"carousel-header\"> ${this.headerText ? html `<h3>${this.headerText}</h3>` : ''} <slot name=\"show-all\" @slotchange=${this._handleShowAllSlotChange} @click=${this._handleShowAllClick}> ${this.showAll ? html ` <a href=\"#\" class=\"show-all-link\"> ${this.showAllText} </a> ` : ''} </slot> </div> ` : ''} <div class=\"carousel-viewport\"> ${!this.hideNav ? html ` <button class=\"nav-button prev ${showPrevArrow ? 'visible' : ''}\" @click=${this._scrollPrevious} aria-label=\"Previous items\" > <span class=\"material-symbols\">chevron_left</span> </button> ` : ''} <div class=\"scroll-container\" @scroll=${this._scheduleLayout} @mousedown=${this._handleMouseDown} @mousemove=${this._handleMouseMove} @mouseup=${this._handleMouseUp} @mouseenter=${this._handleMouseEnter} @mouseleave=${this._handleMouseLeave} @touchstart=${this._handleTouchStart} @touchmove=${this._handleTouchMove} @touchend=${this._handleTouchEnd} @wheel=${this._handleWheel} > <div class=\"snap-track\"> ${hasItems ? Array.from({ length: snapItemsCount }).map(() => html `<div class=\"snap-item\"></div>`) : ''} </div> <div class=\"visual-track\"> ${hasItems ? visualItems.map((item, idx) => html ` <div class=\"card\" @click=${(e) => this._handleCardClick(e, item, idx)} > ${item.href ? html ` <a href=\"${item.href}\" target=\"${item.target || '_self'}\" style=\"text-decoration:none; color:inherit;\" draggable=\"false\"> <div class=\"img-parallax-container\"> <img src=\"${item.img}\" alt=\"${item.title}\" draggable=\"false\" loading=\"lazy\" /> </div> <h2 class=\"card-title\">${item.title}</h2> </a> ` : html ` <div class=\"img-parallax-container\"> <img src=\"${item.img}\" alt=\"${item.title}\" draggable=\"false\" loading=\"lazy\" /> </div> <h2 class=\"card-title\">${item.title}</h2> `} </div> `) : ''} </div> </div> ${!this.hideNav ? html ` <button class=\"nav-button next ${showNextArrow ? 'visible' : ''}\" @click=${this._scrollNext} aria-label=\"Next items\" > <span class=\"material-symbols\">chevron_right</span> </button> ` : ''} </div> </div> `; } }",
|
|
1459
1453
|
"description": "Material Design 3 Expressive Carousel component.\n\nCarousels display a collection of related items in a scrollable, horizontal list.\nThey allow users to quickly browse through items like images, cards, or products.\n\n**M3 spec reference:** `m3-docs/components/carousel/specs.md`\n\n**Layout variants:**\n- `multi-browse` (default) — Shows a mix of large, medium, and small (peeking)\n items. Best for exploring a large number of items.\n- `hero` — Focuses on one large primary item while showing a sliver of the\n next item. Best for featuring important content.\n- `uncontained` — Standard layout where all items have the same width and\n bleed off the edges of the container.\n\n**Animation & Gestures:**\nThis component uses GSAP for smooth drag, flick, and snap animations,\nmirroring the high-fidelity M3 Expressive motion specs. It handles touch\ngestures for mobile and mouse-drag for desktop.\n\n**Auto-sizing (`auto` mode):**\nWhen `auto=true` (default), the carousel measures its own width and\ndynamically calculates the optimal sizes for large, medium, and small items\nbased on the active `layout` to ensure they fit perfectly without awkward gaps\nor clipping at the edges."
|
|
1460
1454
|
}
|
|
1461
1455
|
],
|
|
@@ -3054,6 +3048,12 @@
|
|
|
3054
3048
|
}
|
|
3055
3049
|
]
|
|
3056
3050
|
},
|
|
3051
|
+
{
|
|
3052
|
+
"kind": "javascript-module",
|
|
3053
|
+
"path": "src/actions/index.ts",
|
|
3054
|
+
"declarations": [],
|
|
3055
|
+
"exports": []
|
|
3056
|
+
},
|
|
3057
3057
|
{
|
|
3058
3058
|
"kind": "javascript-module",
|
|
3059
3059
|
"path": "src/components/index.ts",
|
|
@@ -4858,6 +4858,30 @@
|
|
|
4858
4858
|
},
|
|
4859
4859
|
"default": "false"
|
|
4860
4860
|
},
|
|
4861
|
+
{
|
|
4862
|
+
"kind": "field",
|
|
4863
|
+
"name": "infinite",
|
|
4864
|
+
"type": {
|
|
4865
|
+
"text": "boolean"
|
|
4866
|
+
},
|
|
4867
|
+
"default": "false"
|
|
4868
|
+
},
|
|
4869
|
+
{
|
|
4870
|
+
"kind": "field",
|
|
4871
|
+
"name": "autoplay",
|
|
4872
|
+
"type": {
|
|
4873
|
+
"text": "boolean"
|
|
4874
|
+
},
|
|
4875
|
+
"default": "false"
|
|
4876
|
+
},
|
|
4877
|
+
{
|
|
4878
|
+
"kind": "field",
|
|
4879
|
+
"name": "autoplayInterval",
|
|
4880
|
+
"type": {
|
|
4881
|
+
"text": "number"
|
|
4882
|
+
},
|
|
4883
|
+
"default": "3000"
|
|
4884
|
+
},
|
|
4861
4885
|
{
|
|
4862
4886
|
"kind": "field",
|
|
4863
4887
|
"name": "_containerWidth",
|
|
@@ -5026,6 +5050,24 @@
|
|
|
5026
5050
|
"privacy": "private",
|
|
5027
5051
|
"default": "false"
|
|
5028
5052
|
},
|
|
5053
|
+
{
|
|
5054
|
+
"kind": "field",
|
|
5055
|
+
"name": "_infiniteInitialized",
|
|
5056
|
+
"type": {
|
|
5057
|
+
"text": "boolean"
|
|
5058
|
+
},
|
|
5059
|
+
"privacy": "private",
|
|
5060
|
+
"default": "false"
|
|
5061
|
+
},
|
|
5062
|
+
{
|
|
5063
|
+
"kind": "field",
|
|
5064
|
+
"name": "_autoplayTimer",
|
|
5065
|
+
"type": {
|
|
5066
|
+
"text": "any"
|
|
5067
|
+
},
|
|
5068
|
+
"privacy": "private",
|
|
5069
|
+
"default": "null"
|
|
5070
|
+
},
|
|
5029
5071
|
{
|
|
5030
5072
|
"kind": "field",
|
|
5031
5073
|
"name": "_cardSetX",
|
|
@@ -5144,6 +5186,49 @@
|
|
|
5144
5186
|
}
|
|
5145
5187
|
]
|
|
5146
5188
|
},
|
|
5189
|
+
{
|
|
5190
|
+
"kind": "field",
|
|
5191
|
+
"name": "_visualCardsMultiplier",
|
|
5192
|
+
"privacy": "private",
|
|
5193
|
+
"readonly": true
|
|
5194
|
+
},
|
|
5195
|
+
{
|
|
5196
|
+
"kind": "field",
|
|
5197
|
+
"name": "_snapSetsCount",
|
|
5198
|
+
"privacy": "private",
|
|
5199
|
+
"readonly": true
|
|
5200
|
+
},
|
|
5201
|
+
{
|
|
5202
|
+
"kind": "field",
|
|
5203
|
+
"name": "_snapItemsCount",
|
|
5204
|
+
"privacy": "private",
|
|
5205
|
+
"readonly": true
|
|
5206
|
+
},
|
|
5207
|
+
{
|
|
5208
|
+
"kind": "method",
|
|
5209
|
+
"name": "_checkInfiniteWrap",
|
|
5210
|
+
"privacy": "private"
|
|
5211
|
+
},
|
|
5212
|
+
{
|
|
5213
|
+
"kind": "method",
|
|
5214
|
+
"name": "_startAutoplay",
|
|
5215
|
+
"privacy": "private"
|
|
5216
|
+
},
|
|
5217
|
+
{
|
|
5218
|
+
"kind": "method",
|
|
5219
|
+
"name": "_stopAutoplay",
|
|
5220
|
+
"privacy": "private"
|
|
5221
|
+
},
|
|
5222
|
+
{
|
|
5223
|
+
"kind": "method",
|
|
5224
|
+
"name": "_handleMouseEnter",
|
|
5225
|
+
"privacy": "private"
|
|
5226
|
+
},
|
|
5227
|
+
{
|
|
5228
|
+
"kind": "method",
|
|
5229
|
+
"name": "_handleMouseLeave",
|
|
5230
|
+
"privacy": "private"
|
|
5231
|
+
},
|
|
5147
5232
|
{
|
|
5148
5233
|
"kind": "method",
|
|
5149
5234
|
"name": "_handleMouseDown",
|
|
@@ -89,6 +89,9 @@ export declare class MoniCarousel extends MoniElement {
|
|
|
89
89
|
showAllText: string;
|
|
90
90
|
headerText: string;
|
|
91
91
|
hideNav: boolean;
|
|
92
|
+
infinite: boolean;
|
|
93
|
+
autoplay: boolean;
|
|
94
|
+
autoplayInterval: number;
|
|
92
95
|
private _containerWidth;
|
|
93
96
|
private _slottedItems;
|
|
94
97
|
private _hasSlottedShowAll;
|
|
@@ -108,6 +111,8 @@ export declare class MoniCarousel extends MoniElement {
|
|
|
108
111
|
private _tickerTarget;
|
|
109
112
|
private _tickerCurrent;
|
|
110
113
|
private _isTicking;
|
|
114
|
+
private _infiniteInitialized;
|
|
115
|
+
private _autoplayTimer;
|
|
111
116
|
private _cardSetX;
|
|
112
117
|
private _cardSetWidth;
|
|
113
118
|
private _imgSetX;
|
|
@@ -138,6 +143,14 @@ export declare class MoniCarousel extends MoniElement {
|
|
|
138
143
|
private _scheduleLayout;
|
|
139
144
|
updateLayout: (instant?: boolean) => void;
|
|
140
145
|
private _applyLayout;
|
|
146
|
+
private get _visualCardsMultiplier();
|
|
147
|
+
private get _snapSetsCount();
|
|
148
|
+
private get _snapItemsCount();
|
|
149
|
+
private _checkInfiniteWrap;
|
|
150
|
+
private _startAutoplay;
|
|
151
|
+
private _stopAutoplay;
|
|
152
|
+
private _handleMouseEnter;
|
|
153
|
+
private _handleMouseLeave;
|
|
141
154
|
private _handleMouseDown;
|
|
142
155
|
private _handleMouseMove;
|
|
143
156
|
private _handleMouseUp;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"moni-carousel.d.ts","sourceRoot":"","sources":["../../src/components/moni-carousel.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAAE,WAAW,EAAgB,MAAM,kBAAkB,CAAC;AAG7D;;GAEG;AACH,MAAM,WAAW,YAAY;IAC5B,0CAA0C;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,+EAA+E;IAC/E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,qBACa,YAAa,SAAQ,WAAW;IACjB,KAAK,EAAE,YAAY,EAAE,CAAM;IACzB,MAAM,EAAE,cAAc,GAAG,MAAM,GAAG,aAAa,CAAkB;IAClD,IAAI,UAAQ;IACF,UAAU,SAAO;IAChB,WAAW,SAAM;IAClB,UAAU,SAAM;IAC1C,GAAG,SAAK;IACR,OAAO,SAAM;IACe,YAAY,SAAM;IACtB,OAAO,UAAS;IAC1B,WAAW,SAAc;IAC3B,UAAU,SAAM;IACJ,OAAO,UAAS;
|
|
1
|
+
{"version":3,"file":"moni-carousel.d.ts","sourceRoot":"","sources":["../../src/components/moni-carousel.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAAE,WAAW,EAAgB,MAAM,kBAAkB,CAAC;AAG7D;;GAEG;AACH,MAAM,WAAW,YAAY;IAC5B,0CAA0C;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,+EAA+E;IAC/E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,qBACa,YAAa,SAAQ,WAAW;IACjB,KAAK,EAAE,YAAY,EAAE,CAAM;IACzB,MAAM,EAAE,cAAc,GAAG,MAAM,GAAG,aAAa,CAAkB;IAClD,IAAI,UAAQ;IACF,UAAU,SAAO;IAChB,WAAW,SAAM;IAClB,UAAU,SAAM;IAC1C,GAAG,SAAK;IACR,OAAO,SAAM;IACe,YAAY,SAAM;IACtB,OAAO,UAAS;IAC1B,WAAW,SAAc;IAC3B,UAAU,SAAM;IACJ,OAAO,UAAS;IACxB,QAAQ,UAAS;IACjB,QAAQ,UAAS;IACD,gBAAgB,SAAQ;IAE3E,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,aAAa,CAAsB;IAC3C,OAAO,CAAC,kBAAkB,CAAS;IAE5C,IAAI,cAAc,IAAI,YAAY,EAAE,CAEnC;IAE2B,OAAO,CAAC,gBAAgB,CAAkB;IACnD,OAAO,CAAC,MAAM,CAA8B;IAE/D,OAAO,CAAC,eAAe,CAA+B;IACtD,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,SAAS,CAAS;IAG1B,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,UAAU,CAAS;IAG3B,OAAO,CAAC,oBAAoB,CAAS;IACrC,OAAO,CAAC,cAAc,CAAa;IAGnC,OAAO,CAAC,SAAS,CAAmC;IACpD,OAAO,CAAC,aAAa,CAAmC;IACxD,OAAO,CAAC,QAAQ,CAAmC;IACnD,OAAO,CAAC,gBAAgB,CAAmC;IAE3D,OAAgB,MAAM,4BAiOpB;IAEO,iBAAiB;IAwBjB,oBAAoB;IAoBpB,YAAY;IAiBZ,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC;IAwCpD,IAAI,cAAc;;;;;MAwEjB;IAED,IAAI,QAAQ,WAEX;IAED,OAAO,CAAC,cAAc;IA+EtB;;;OAGG;IACH,OAAO,CAAC,kBAAkB;IAwB1B;;;OAGG;IACH,OAAO,CAAC,KAAK,CAcX;IAEF,OAAO,CAAC,eAAe,CAOrB;IAEF,YAAY,GAAI,iBAAe,UA+C7B;IAEF,OAAO,CAAC,YAAY;IAmCpB,OAAO,KAAK,sBAAsB,GAKjC;IAED,OAAO,KAAK,cAAc,GASzB;IAED,OAAO,KAAK,eAAe,GAQ1B;IAED,OAAO,CAAC,kBAAkB;IAiD1B,OAAO,CAAC,cAAc;IAYtB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,iBAAiB;IAGzB,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,gBAAgB;IAexB,OAAO,CAAC,gBAAgB;IAkBxB,OAAO,CAAC,cAAc;IAiDtB,OAAO,CAAC,iBAAiB;IAgBzB,OAAO,CAAC,gBAAgB;IAkBxB,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,YAAY;IAoCpB,OAAO,CAAC,gBAAgB;IAiBxB,OAAO,CAAC,eAAe;IAMvB,OAAO,CAAC,WAAW;IAMnB,OAAO,CAAC,mBAAmB;IAa3B,OAAO,CAAC,wBAAwB;IAYhC,OAAO,CAAC,iBAAiB;IA4ChB,MAAM;CAkJf;AAED,OAAO,CAAC,MAAM,CAAC;IACd,UAAU,qBAAqB;QAC9B,eAAe,EAAE,YAAY,CAAC;KAC9B;CACD"}
|
|
@@ -87,6 +87,9 @@ let MoniCarousel = class MoniCarousel extends MoniElement {
|
|
|
87
87
|
this.showAllText = 'Show all';
|
|
88
88
|
this.headerText = '';
|
|
89
89
|
this.hideNav = false;
|
|
90
|
+
this.infinite = false;
|
|
91
|
+
this.autoplay = false;
|
|
92
|
+
this.autoplayInterval = 3000;
|
|
90
93
|
this._containerWidth = 0;
|
|
91
94
|
this._slottedItems = [];
|
|
92
95
|
this._hasSlottedShowAll = false;
|
|
@@ -104,6 +107,9 @@ let MoniCarousel = class MoniCarousel extends MoniElement {
|
|
|
104
107
|
this._tickerTarget = 0;
|
|
105
108
|
this._tickerCurrent = 0;
|
|
106
109
|
this._isTicking = false;
|
|
110
|
+
// Infinite & Autoplay state
|
|
111
|
+
this._infiniteInitialized = false;
|
|
112
|
+
this._autoplayTimer = null;
|
|
107
113
|
// quickSetter caches — avoid gsap.set() overhead per frame
|
|
108
114
|
this._cardSetX = [];
|
|
109
115
|
this._cardSetWidth = [];
|
|
@@ -122,6 +128,7 @@ let MoniCarousel = class MoniCarousel extends MoniElement {
|
|
|
122
128
|
this._tickerCurrent = this._tickerTarget;
|
|
123
129
|
gsap.ticker.remove(this._tick);
|
|
124
130
|
this._isTicking = false;
|
|
131
|
+
this._checkInfiniteWrap();
|
|
125
132
|
}
|
|
126
133
|
this._applyLayout(this._tickerCurrent);
|
|
127
134
|
};
|
|
@@ -141,6 +148,30 @@ let MoniCarousel = class MoniCarousel extends MoniElement {
|
|
|
141
148
|
if (this._cardSetX.length !== this._cards.length) {
|
|
142
149
|
this._buildQuickSetters();
|
|
143
150
|
}
|
|
151
|
+
if (this.infinite && !this._infiniteInitialized && this.effectiveItems.length > 0) {
|
|
152
|
+
const C = this.effectiveItems.length;
|
|
153
|
+
const V = C * this._visualCardsMultiplier;
|
|
154
|
+
const K = this._snapSetsCount;
|
|
155
|
+
const centerSetIndex = Math.floor(K / 2);
|
|
156
|
+
// Wait for the browser to render the new items so scrollWidth is updated
|
|
157
|
+
// A double-rAF or setTimeout ensures the layout engine has processed the new snap-items
|
|
158
|
+
requestAnimationFrame(() => {
|
|
159
|
+
setTimeout(() => {
|
|
160
|
+
if (this._scrollContainer) {
|
|
161
|
+
// Force layout calculation
|
|
162
|
+
this._scrollContainer.scrollWidth;
|
|
163
|
+
this._scrollContainer.scrollLeft = centerSetIndex * V * this.itemSize;
|
|
164
|
+
// If the browser still clamped it, it means it's not ready.
|
|
165
|
+
// But with setTimeout it should be fully ready.
|
|
166
|
+
// Force the visual ticker to match instantly to avoid an animation zoom from 0
|
|
167
|
+
this._tickerTarget = this._scrollContainer.scrollLeft / this.itemSize;
|
|
168
|
+
this._tickerCurrent = this._tickerTarget;
|
|
169
|
+
this._applyLayout(this._tickerCurrent);
|
|
170
|
+
}
|
|
171
|
+
}, 20);
|
|
172
|
+
});
|
|
173
|
+
this._infiniteInitialized = true;
|
|
174
|
+
}
|
|
144
175
|
this._tickerTarget = this._scrollContainer.scrollLeft / this.itemSize;
|
|
145
176
|
if (instant || (!this._isTicking && this._tickerCurrent === 0)) {
|
|
146
177
|
this._tickerCurrent = this._tickerTarget;
|
|
@@ -434,6 +465,9 @@ let MoniCarousel = class MoniCarousel extends MoniElement {
|
|
|
434
465
|
}
|
|
435
466
|
this._buildQuickSetters();
|
|
436
467
|
this.updateLayout(true);
|
|
468
|
+
if (this.autoplay) {
|
|
469
|
+
this._startAutoplay();
|
|
470
|
+
}
|
|
437
471
|
}
|
|
438
472
|
updated(changedProperties) {
|
|
439
473
|
super.updated(changedProperties);
|
|
@@ -445,15 +479,30 @@ let MoniCarousel = class MoniCarousel extends MoniElement {
|
|
|
445
479
|
changedProperties.has('padding') ||
|
|
446
480
|
changedProperties.has('items') ||
|
|
447
481
|
changedProperties.has('hideNav') ||
|
|
482
|
+
changedProperties.has('infinite') ||
|
|
483
|
+
changedProperties.has('autoplay') ||
|
|
484
|
+
changedProperties.has('autoplayInterval') ||
|
|
448
485
|
changedProperties.has('_containerWidth') ||
|
|
449
486
|
changedProperties.has('_slottedItems') ||
|
|
450
487
|
changedProperties.has('_hasSlottedShowAll')) {
|
|
488
|
+
if (changedProperties.has('infinite') || changedProperties.has('items') || changedProperties.has('_slottedItems')) {
|
|
489
|
+
this._infiniteInitialized = false;
|
|
490
|
+
}
|
|
451
491
|
if (changedProperties.has('layout') && this._scrollContainer) {
|
|
452
492
|
this._scrollContainer.scrollLeft = 0;
|
|
493
|
+
this._infiniteInitialized = false;
|
|
453
494
|
this._scrollContainer.style.scrollSnapType =
|
|
454
495
|
this.layout === 'uncontained' ? 'none' : 'x mandatory';
|
|
455
496
|
}
|
|
456
497
|
this.updateLayout(true);
|
|
498
|
+
if (changedProperties.has('autoplay') || changedProperties.has('autoplayInterval') || changedProperties.has('items') || changedProperties.has('_slottedItems')) {
|
|
499
|
+
if (this.autoplay) {
|
|
500
|
+
this._startAutoplay();
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
this._stopAutoplay();
|
|
504
|
+
}
|
|
505
|
+
}
|
|
457
506
|
}
|
|
458
507
|
}
|
|
459
508
|
get computedLayout() {
|
|
@@ -639,7 +688,21 @@ let MoniCarousel = class MoniCarousel extends MoniElement {
|
|
|
639
688
|
const isMobile = this._isMobile;
|
|
640
689
|
const cardCount = this._cards.length;
|
|
641
690
|
for (let i = 0; i < cardCount; i++) {
|
|
642
|
-
|
|
691
|
+
let p = i - t;
|
|
692
|
+
if (this.infinite) {
|
|
693
|
+
// Modulo wrap so the cards perfectly loop around the center
|
|
694
|
+
p = i - (t % cardCount);
|
|
695
|
+
if (p > cardCount / 2)
|
|
696
|
+
p -= cardCount;
|
|
697
|
+
if (p < -cardCount / 2)
|
|
698
|
+
p += cardCount;
|
|
699
|
+
}
|
|
700
|
+
// Optimization: if the card is far off-screen, hide it to save rendering work
|
|
701
|
+
if (p < -15 || p > 15) {
|
|
702
|
+
this._cardSetX[i](-9999);
|
|
703
|
+
this._titleSetOpacity[i](0);
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
643
706
|
const layout = this._getCardLayout(p);
|
|
644
707
|
// Use quickSetters — ~3x faster than gsap.set() per call
|
|
645
708
|
this._cardSetX[i](layout.x);
|
|
@@ -652,8 +715,97 @@ let MoniCarousel = class MoniCarousel extends MoniElement {
|
|
|
652
715
|
this._titleSetOpacity[i](layout.opacity);
|
|
653
716
|
}
|
|
654
717
|
}
|
|
718
|
+
get _visualCardsMultiplier() {
|
|
719
|
+
if (!this.infinite || this.effectiveItems.length === 0)
|
|
720
|
+
return 1;
|
|
721
|
+
const C = this.effectiveItems.length;
|
|
722
|
+
// We need at least ~15 cards to fill the screen seamlessly for small datasets
|
|
723
|
+
return Math.max(1, Math.ceil(15 / C));
|
|
724
|
+
}
|
|
725
|
+
get _snapSetsCount() {
|
|
726
|
+
if (!this.infinite || this.effectiveItems.length === 0)
|
|
727
|
+
return 1;
|
|
728
|
+
const C = this.effectiveItems.length;
|
|
729
|
+
const V = C * this._visualCardsMultiplier;
|
|
730
|
+
// We want enough snap items so the user NEVER reaches the end in a single fast swipe.
|
|
731
|
+
// A total of ~1000 items is extremely cheap as empty divs, and provides a massive buffer.
|
|
732
|
+
let K = Math.ceil(1000 / V);
|
|
733
|
+
if (K % 2 === 0)
|
|
734
|
+
K += 1; // Keep it odd for a perfect center
|
|
735
|
+
return Math.max(3, K);
|
|
736
|
+
}
|
|
737
|
+
get _snapItemsCount() {
|
|
738
|
+
if (this.infinite) {
|
|
739
|
+
const C = this.effectiveItems.length;
|
|
740
|
+
const V_mult = this._visualCardsMultiplier;
|
|
741
|
+
const K = this._snapSetsCount;
|
|
742
|
+
return K * (C * V_mult);
|
|
743
|
+
}
|
|
744
|
+
return this.effectiveItems.length;
|
|
745
|
+
}
|
|
746
|
+
_checkInfiniteWrap() {
|
|
747
|
+
if (!this.infinite || this.effectiveItems.length === 0 || !this._scrollContainer)
|
|
748
|
+
return;
|
|
749
|
+
const C = this.effectiveItems.length;
|
|
750
|
+
const V = C * this._visualCardsMultiplier;
|
|
751
|
+
const K = this._snapSetsCount;
|
|
752
|
+
const centerSetIndex = Math.floor(K / 2);
|
|
753
|
+
// The scroll track is divided into K sets, each containing V items.
|
|
754
|
+
const setWidth = V * this.itemSize;
|
|
755
|
+
const currentScroll = this._scrollContainer.scrollLeft;
|
|
756
|
+
const currentSet = Math.floor(currentScroll / setWidth);
|
|
757
|
+
if (currentSet !== centerSetIndex) {
|
|
758
|
+
const delta = (centerSetIndex - currentSet) * setWidth;
|
|
759
|
+
const oldSnap = this._scrollContainer.style.scrollSnapType;
|
|
760
|
+
const oldBehavior = this._scrollContainer.style.scrollBehavior;
|
|
761
|
+
// Temporarily disable snap and smooth behavior so the wrap is instantaneous
|
|
762
|
+
this._scrollContainer.style.scrollSnapType = 'none';
|
|
763
|
+
this._scrollContainer.style.scrollBehavior = 'auto';
|
|
764
|
+
this._scrollContainer.scrollLeft += delta;
|
|
765
|
+
// INSTANTLY update the GSAP visual ticker so it doesn't animate the jump
|
|
766
|
+
this._tickerTarget = this._scrollContainer.scrollLeft / this.itemSize;
|
|
767
|
+
this._tickerCurrent = this._tickerTarget;
|
|
768
|
+
this._applyLayout(this._tickerCurrent);
|
|
769
|
+
// Restore original scroll behavior
|
|
770
|
+
this._scrollContainer.style.scrollBehavior = oldBehavior;
|
|
771
|
+
if (this._isDown) {
|
|
772
|
+
this._scrollLeftStart += delta;
|
|
773
|
+
}
|
|
774
|
+
if (oldSnap && oldSnap !== 'none') {
|
|
775
|
+
// Re-enable snap on the next frame to avoid jumping
|
|
776
|
+
requestAnimationFrame(() => {
|
|
777
|
+
if (this._scrollContainer && !this._isDown) {
|
|
778
|
+
this._scrollContainer.style.scrollSnapType = oldSnap;
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
_startAutoplay() {
|
|
785
|
+
this._stopAutoplay();
|
|
786
|
+
if (this.autoplay && this.effectiveItems.length > 0) {
|
|
787
|
+
this._autoplayTimer = setInterval(() => {
|
|
788
|
+
// We removed _isInteracting to make autoplay more robust. It only pauses on active drag.
|
|
789
|
+
if (!this._isDown) {
|
|
790
|
+
this._scrollNext();
|
|
791
|
+
}
|
|
792
|
+
}, this.autoplayInterval);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
_stopAutoplay() {
|
|
796
|
+
if (this._autoplayTimer) {
|
|
797
|
+
clearInterval(this._autoplayTimer);
|
|
798
|
+
this._autoplayTimer = null;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
_handleMouseEnter() {
|
|
802
|
+
}
|
|
803
|
+
_handleMouseLeave() {
|
|
804
|
+
this._handleMouseUp();
|
|
805
|
+
}
|
|
655
806
|
_handleMouseDown(e) {
|
|
656
807
|
gsap.killTweensOf(this._scrollContainer);
|
|
808
|
+
this._checkInfiniteWrap();
|
|
657
809
|
this._isDown = true;
|
|
658
810
|
this._draggedDistance = 0;
|
|
659
811
|
this._scrollContainer.style.scrollBehavior = 'auto';
|
|
@@ -690,11 +842,20 @@ let MoniCarousel = class MoniCarousel extends MoniElement {
|
|
|
690
842
|
}
|
|
691
843
|
// Dynamic momentum: stronger swipes have a much higher multiplier
|
|
692
844
|
const momentumMultiplier = 350 + Math.abs(this._velocity) * 300;
|
|
693
|
-
|
|
845
|
+
let projectedScrollLeft = this._scrollContainer.scrollLeft - this._velocity * momentumMultiplier;
|
|
846
|
+
// Cap momentum throw to prevent hitting infinite wrap edges on fast swipes
|
|
847
|
+
const maxThrowDist = 15 * this.itemSize;
|
|
848
|
+
if (projectedScrollLeft < this._scrollContainer.scrollLeft - maxThrowDist) {
|
|
849
|
+
projectedScrollLeft = this._scrollContainer.scrollLeft - maxThrowDist;
|
|
850
|
+
}
|
|
851
|
+
else if (projectedScrollLeft > this._scrollContainer.scrollLeft + maxThrowDist) {
|
|
852
|
+
projectedScrollLeft = this._scrollContainer.scrollLeft + maxThrowDist;
|
|
853
|
+
}
|
|
694
854
|
if (this.layout !== 'uncontained') {
|
|
695
855
|
this._scrollContainer.style.scrollSnapType = 'none';
|
|
696
856
|
let targetIndex = Math.round(projectedScrollLeft / this.itemSize);
|
|
697
|
-
|
|
857
|
+
const maxIndex = this.effectiveItems.length > 0 ? this._snapItemsCount - 1 : 0;
|
|
858
|
+
targetIndex = Math.max(0, Math.min(maxIndex, targetIndex));
|
|
698
859
|
const snapPoint = targetIndex * this.itemSize;
|
|
699
860
|
// Dynamic duration: longer distance = more time, max 0.85s for faster feeling
|
|
700
861
|
const currentIndex = Math.round(this._scrollContainer.scrollLeft / this.itemSize);
|
|
@@ -715,6 +876,7 @@ let MoniCarousel = class MoniCarousel extends MoniElement {
|
|
|
715
876
|
}
|
|
716
877
|
_handleTouchStart(e) {
|
|
717
878
|
gsap.killTweensOf(this._scrollContainer);
|
|
879
|
+
this._checkInfiniteWrap();
|
|
718
880
|
this._isDown = true;
|
|
719
881
|
this._draggedDistance = 0;
|
|
720
882
|
this._scrollContainer.style.scrollBehavior = 'auto';
|
|
@@ -770,7 +932,7 @@ let MoniCarousel = class MoniCarousel extends MoniElement {
|
|
|
770
932
|
if (this._wheelTimeout)
|
|
771
933
|
clearTimeout(this._wheelTimeout);
|
|
772
934
|
this._wheelTimeout = setTimeout(() => {
|
|
773
|
-
if (this.layout !== 'uncontained') {
|
|
935
|
+
if (this.layout !== 'uncontained' && !this._isDown) {
|
|
774
936
|
this._scrollContainer.style.scrollSnapType = 'x mandatory';
|
|
775
937
|
}
|
|
776
938
|
}, 150);
|
|
@@ -782,20 +944,20 @@ let MoniCarousel = class MoniCarousel extends MoniElement {
|
|
|
782
944
|
e.stopPropagation();
|
|
783
945
|
return;
|
|
784
946
|
}
|
|
947
|
+
const originalIndex = index % this.effectiveItems.length;
|
|
785
948
|
this.dispatchEvent(new CustomEvent('item-click', {
|
|
786
|
-
detail: { item, index },
|
|
949
|
+
detail: { item, index: originalIndex },
|
|
787
950
|
bubbles: true,
|
|
788
951
|
composed: true
|
|
789
952
|
}));
|
|
790
953
|
}
|
|
791
954
|
_scrollPrevious() {
|
|
792
|
-
const target =
|
|
955
|
+
const target = this._scrollContainer.scrollLeft - this.itemSize;
|
|
793
956
|
this._scrollContainer.style.scrollBehavior = 'smooth';
|
|
794
957
|
this._scrollContainer.scrollTo({ left: target, behavior: 'smooth' });
|
|
795
958
|
}
|
|
796
959
|
_scrollNext() {
|
|
797
|
-
const
|
|
798
|
-
const target = Math.min(maxScroll, this._scrollContainer.scrollLeft + this.itemSize);
|
|
960
|
+
const target = this._scrollContainer.scrollLeft + this.itemSize;
|
|
799
961
|
this._scrollContainer.style.scrollBehavior = 'smooth';
|
|
800
962
|
this._scrollContainer.scrollTo({ left: target, behavior: 'smooth' });
|
|
801
963
|
}
|
|
@@ -883,14 +1045,23 @@ let MoniCarousel = class MoniCarousel extends MoniElement {
|
|
|
883
1045
|
if (rightPadding < minRightPaddingNeeded) {
|
|
884
1046
|
rightPadding = minRightPaddingNeeded;
|
|
885
1047
|
}
|
|
1048
|
+
let snapItemsCount = hasItems ? this._snapItemsCount : 0;
|
|
1049
|
+
let visualItems = this.effectiveItems;
|
|
1050
|
+
if (this.infinite && hasItems) {
|
|
1051
|
+
const V_mult = this._visualCardsMultiplier;
|
|
1052
|
+
visualItems = [];
|
|
1053
|
+
for (let i = 0; i < V_mult; i++) {
|
|
1054
|
+
visualItems = visualItems.concat(this.effectiveItems);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
886
1057
|
const trackWidth = hasItems
|
|
887
|
-
?
|
|
1058
|
+
? snapItemsCount * L + (snapItemsCount - 1) * this.gap
|
|
888
1059
|
: 0;
|
|
889
1060
|
const isScrollable = hasItems && this._scrollContainer &&
|
|
890
1061
|
(this._scrollContainer.scrollWidth > this._scrollContainer.clientWidth);
|
|
891
|
-
const showPrevArrow = isScrollable && this._scrollContainer.scrollLeft > 5;
|
|
892
|
-
const showNextArrow = isScrollable &&
|
|
893
|
-
(this._scrollContainer.scrollWidth - this._scrollContainer.scrollLeft - this._scrollContainer.clientWidth > 5);
|
|
1062
|
+
const showPrevArrow = this.infinite || (isScrollable && this._scrollContainer.scrollLeft > 5);
|
|
1063
|
+
const showNextArrow = this.infinite || (isScrollable &&
|
|
1064
|
+
(this._scrollContainer.scrollWidth - this._scrollContainer.scrollLeft - this._scrollContainer.clientWidth > 5));
|
|
894
1065
|
return html `
|
|
895
1066
|
<div class="carousel-container" style="
|
|
896
1067
|
--carousel-snap-width: ${L}px;
|
|
@@ -937,34 +1108,35 @@ let MoniCarousel = class MoniCarousel extends MoniElement {
|
|
|
937
1108
|
@mousedown=${this._handleMouseDown}
|
|
938
1109
|
@mousemove=${this._handleMouseMove}
|
|
939
1110
|
@mouseup=${this._handleMouseUp}
|
|
940
|
-
@
|
|
1111
|
+
@mouseenter=${this._handleMouseEnter}
|
|
1112
|
+
@mouseleave=${this._handleMouseLeave}
|
|
941
1113
|
@touchstart=${this._handleTouchStart}
|
|
942
1114
|
@touchmove=${this._handleTouchMove}
|
|
943
1115
|
@touchend=${this._handleTouchEnd}
|
|
944
1116
|
@wheel=${this._handleWheel}
|
|
945
1117
|
>
|
|
946
1118
|
<div class="snap-track">
|
|
947
|
-
${hasItems ?
|
|
1119
|
+
${hasItems ? Array.from({ length: snapItemsCount }).map(() => html `<div class="snap-item"></div>`) : ''}
|
|
948
1120
|
</div>
|
|
949
1121
|
<div class="visual-track">
|
|
950
1122
|
${hasItems
|
|
951
|
-
?
|
|
1123
|
+
? visualItems.map((item, idx) => html `
|
|
952
1124
|
<div
|
|
953
1125
|
class="card"
|
|
954
1126
|
@click=${(e) => this._handleCardClick(e, item, idx)}
|
|
955
1127
|
>
|
|
956
1128
|
${item.href
|
|
957
1129
|
? html `
|
|
958
|
-
<a href="${item.href}" target="${item.target || '_self'}" style="text-decoration:none; color:inherit;">
|
|
1130
|
+
<a href="${item.href}" target="${item.target || '_self'}" style="text-decoration:none; color:inherit;" draggable="false">
|
|
959
1131
|
<div class="img-parallax-container">
|
|
960
|
-
<img src="${item.img}" alt="${item.title}" draggable="false" />
|
|
1132
|
+
<img src="${item.img}" alt="${item.title}" draggable="false" loading="lazy" />
|
|
961
1133
|
</div>
|
|
962
1134
|
<h2 class="card-title">${item.title}</h2>
|
|
963
1135
|
</a>
|
|
964
1136
|
`
|
|
965
1137
|
: html `
|
|
966
1138
|
<div class="img-parallax-container">
|
|
967
|
-
<img src="${item.img}" alt="${item.title}" draggable="false" />
|
|
1139
|
+
<img src="${item.img}" alt="${item.title}" draggable="false" loading="lazy" />
|
|
968
1140
|
</div>
|
|
969
1141
|
<h2 class="card-title">${item.title}</h2>
|
|
970
1142
|
`}
|
|
@@ -1029,6 +1201,15 @@ __decorate([
|
|
|
1029
1201
|
__decorate([
|
|
1030
1202
|
property({ type: Boolean, attribute: 'hide-nav' })
|
|
1031
1203
|
], MoniCarousel.prototype, "hideNav", void 0);
|
|
1204
|
+
__decorate([
|
|
1205
|
+
property({ type: Boolean, reflect: true })
|
|
1206
|
+
], MoniCarousel.prototype, "infinite", void 0);
|
|
1207
|
+
__decorate([
|
|
1208
|
+
property({ type: Boolean, reflect: true })
|
|
1209
|
+
], MoniCarousel.prototype, "autoplay", void 0);
|
|
1210
|
+
__decorate([
|
|
1211
|
+
property({ type: Number, attribute: 'autoplay-interval' })
|
|
1212
|
+
], MoniCarousel.prototype, "autoplayInterval", void 0);
|
|
1032
1213
|
__decorate([
|
|
1033
1214
|
state()
|
|
1034
1215
|
], MoniCarousel.prototype, "_containerWidth", void 0);
|
package/package.json
CHANGED
|
@@ -96,6 +96,9 @@ export class MoniCarousel extends MoniElement {
|
|
|
96
96
|
@property({ attribute: 'show-all-text' }) showAllText = 'Show all';
|
|
97
97
|
@property({ attribute: 'header-text' }) headerText = '';
|
|
98
98
|
@property({ type: Boolean, attribute: 'hide-nav' }) hideNav = false;
|
|
99
|
+
@property({ type: Boolean, reflect: true }) infinite = false;
|
|
100
|
+
@property({ type: Boolean, reflect: true }) autoplay = false;
|
|
101
|
+
@property({ type: Number, attribute: 'autoplay-interval' }) autoplayInterval = 3000;
|
|
99
102
|
|
|
100
103
|
@state() private _containerWidth = 0;
|
|
101
104
|
@state() private _slottedItems: CarouselItem[] = [];
|
|
@@ -124,6 +127,10 @@ export class MoniCarousel extends MoniElement {
|
|
|
124
127
|
private _tickerCurrent = 0;
|
|
125
128
|
private _isTicking = false;
|
|
126
129
|
|
|
130
|
+
// Infinite & Autoplay state
|
|
131
|
+
private _infiniteInitialized = false;
|
|
132
|
+
private _autoplayTimer: any = null;
|
|
133
|
+
|
|
127
134
|
// quickSetter caches — avoid gsap.set() overhead per frame
|
|
128
135
|
private _cardSetX: ((value: number) => void)[] = [];
|
|
129
136
|
private _cardSetWidth: ((value: number) => void)[] = [];
|
|
@@ -412,6 +419,10 @@ export class MoniCarousel extends MoniElement {
|
|
|
412
419
|
}
|
|
413
420
|
this._buildQuickSetters();
|
|
414
421
|
this.updateLayout(true);
|
|
422
|
+
|
|
423
|
+
if (this.autoplay) {
|
|
424
|
+
this._startAutoplay();
|
|
425
|
+
}
|
|
415
426
|
}
|
|
416
427
|
|
|
417
428
|
override updated(changedProperties: Map<string, any>) {
|
|
@@ -425,16 +436,32 @@ export class MoniCarousel extends MoniElement {
|
|
|
425
436
|
changedProperties.has('padding') ||
|
|
426
437
|
changedProperties.has('items') ||
|
|
427
438
|
changedProperties.has('hideNav') ||
|
|
439
|
+
changedProperties.has('infinite') ||
|
|
440
|
+
changedProperties.has('autoplay') ||
|
|
441
|
+
changedProperties.has('autoplayInterval') ||
|
|
428
442
|
changedProperties.has('_containerWidth') ||
|
|
429
443
|
changedProperties.has('_slottedItems') ||
|
|
430
444
|
changedProperties.has('_hasSlottedShowAll')
|
|
431
445
|
) {
|
|
446
|
+
if (changedProperties.has('infinite') || changedProperties.has('items') || changedProperties.has('_slottedItems')) {
|
|
447
|
+
this._infiniteInitialized = false;
|
|
448
|
+
}
|
|
449
|
+
|
|
432
450
|
if (changedProperties.has('layout') && this._scrollContainer) {
|
|
433
451
|
this._scrollContainer.scrollLeft = 0;
|
|
452
|
+
this._infiniteInitialized = false;
|
|
434
453
|
this._scrollContainer.style.scrollSnapType =
|
|
435
454
|
this.layout === 'uncontained' ? 'none' : 'x mandatory';
|
|
436
455
|
}
|
|
437
456
|
this.updateLayout(true);
|
|
457
|
+
|
|
458
|
+
if (changedProperties.has('autoplay') || changedProperties.has('autoplayInterval') || changedProperties.has('items') || changedProperties.has('_slottedItems')) {
|
|
459
|
+
if (this.autoplay) {
|
|
460
|
+
this._startAutoplay();
|
|
461
|
+
} else {
|
|
462
|
+
this._stopAutoplay();
|
|
463
|
+
}
|
|
464
|
+
}
|
|
438
465
|
}
|
|
439
466
|
}
|
|
440
467
|
|
|
@@ -637,6 +664,7 @@ export class MoniCarousel extends MoniElement {
|
|
|
637
664
|
this._tickerCurrent = this._tickerTarget;
|
|
638
665
|
gsap.ticker.remove(this._tick);
|
|
639
666
|
this._isTicking = false;
|
|
667
|
+
this._checkInfiniteWrap();
|
|
640
668
|
}
|
|
641
669
|
|
|
642
670
|
this._applyLayout(this._tickerCurrent);
|
|
@@ -659,6 +687,34 @@ export class MoniCarousel extends MoniElement {
|
|
|
659
687
|
this._buildQuickSetters();
|
|
660
688
|
}
|
|
661
689
|
|
|
690
|
+
if (this.infinite && !this._infiniteInitialized && this.effectiveItems.length > 0) {
|
|
691
|
+
const C = this.effectiveItems.length;
|
|
692
|
+
const V = C * this._visualCardsMultiplier;
|
|
693
|
+
const K = this._snapSetsCount;
|
|
694
|
+
const centerSetIndex = Math.floor(K / 2);
|
|
695
|
+
|
|
696
|
+
// Wait for the browser to render the new items so scrollWidth is updated
|
|
697
|
+
// A double-rAF or setTimeout ensures the layout engine has processed the new snap-items
|
|
698
|
+
requestAnimationFrame(() => {
|
|
699
|
+
setTimeout(() => {
|
|
700
|
+
if (this._scrollContainer) {
|
|
701
|
+
// Force layout calculation
|
|
702
|
+
this._scrollContainer.scrollWidth;
|
|
703
|
+
this._scrollContainer.scrollLeft = centerSetIndex * V * this.itemSize;
|
|
704
|
+
|
|
705
|
+
// If the browser still clamped it, it means it's not ready.
|
|
706
|
+
// But with setTimeout it should be fully ready.
|
|
707
|
+
|
|
708
|
+
// Force the visual ticker to match instantly to avoid an animation zoom from 0
|
|
709
|
+
this._tickerTarget = this._scrollContainer.scrollLeft / this.itemSize;
|
|
710
|
+
this._tickerCurrent = this._tickerTarget;
|
|
711
|
+
this._applyLayout(this._tickerCurrent);
|
|
712
|
+
}
|
|
713
|
+
}, 20);
|
|
714
|
+
});
|
|
715
|
+
this._infiniteInitialized = true;
|
|
716
|
+
}
|
|
717
|
+
|
|
662
718
|
this._tickerTarget = this._scrollContainer.scrollLeft / this.itemSize;
|
|
663
719
|
|
|
664
720
|
if (instant || (!this._isTicking && this._tickerCurrent === 0)) {
|
|
@@ -676,7 +732,21 @@ export class MoniCarousel extends MoniElement {
|
|
|
676
732
|
const isMobile = this._isMobile;
|
|
677
733
|
const cardCount = this._cards.length;
|
|
678
734
|
for (let i = 0; i < cardCount; i++) {
|
|
679
|
-
|
|
735
|
+
let p = i - t;
|
|
736
|
+
if (this.infinite) {
|
|
737
|
+
// Modulo wrap so the cards perfectly loop around the center
|
|
738
|
+
p = i - (t % cardCount);
|
|
739
|
+
if (p > cardCount / 2) p -= cardCount;
|
|
740
|
+
if (p < -cardCount / 2) p += cardCount;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Optimization: if the card is far off-screen, hide it to save rendering work
|
|
744
|
+
if (p < -15 || p > 15) {
|
|
745
|
+
this._cardSetX[i](-9999);
|
|
746
|
+
this._titleSetOpacity[i](0);
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
|
|
680
750
|
const layout = this._getCardLayout(p);
|
|
681
751
|
|
|
682
752
|
// Use quickSetters — ~3x faster than gsap.set() per call
|
|
@@ -693,8 +763,112 @@ export class MoniCarousel extends MoniElement {
|
|
|
693
763
|
}
|
|
694
764
|
}
|
|
695
765
|
|
|
766
|
+
private get _visualCardsMultiplier() {
|
|
767
|
+
if (!this.infinite || this.effectiveItems.length === 0) return 1;
|
|
768
|
+
const C = this.effectiveItems.length;
|
|
769
|
+
// We need at least ~15 cards to fill the screen seamlessly for small datasets
|
|
770
|
+
return Math.max(1, Math.ceil(15 / C));
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
private get _snapSetsCount() {
|
|
774
|
+
if (!this.infinite || this.effectiveItems.length === 0) return 1;
|
|
775
|
+
const C = this.effectiveItems.length;
|
|
776
|
+
const V = C * this._visualCardsMultiplier;
|
|
777
|
+
// We want enough snap items so the user NEVER reaches the end in a single fast swipe.
|
|
778
|
+
// A total of ~1000 items is extremely cheap as empty divs, and provides a massive buffer.
|
|
779
|
+
let K = Math.ceil(1000 / V);
|
|
780
|
+
if (K % 2 === 0) K += 1; // Keep it odd for a perfect center
|
|
781
|
+
return Math.max(3, K);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
private get _snapItemsCount() {
|
|
785
|
+
if (this.infinite) {
|
|
786
|
+
const C = this.effectiveItems.length;
|
|
787
|
+
const V_mult = this._visualCardsMultiplier;
|
|
788
|
+
const K = this._snapSetsCount;
|
|
789
|
+
return K * (C * V_mult);
|
|
790
|
+
}
|
|
791
|
+
return this.effectiveItems.length;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
private _checkInfiniteWrap() {
|
|
795
|
+
if (!this.infinite || this.effectiveItems.length === 0 || !this._scrollContainer) return;
|
|
796
|
+
|
|
797
|
+
const C = this.effectiveItems.length;
|
|
798
|
+
const V = C * this._visualCardsMultiplier;
|
|
799
|
+
const K = this._snapSetsCount;
|
|
800
|
+
const centerSetIndex = Math.floor(K / 2);
|
|
801
|
+
|
|
802
|
+
// The scroll track is divided into K sets, each containing V items.
|
|
803
|
+
const setWidth = V * this.itemSize;
|
|
804
|
+
|
|
805
|
+
const currentScroll = this._scrollContainer.scrollLeft;
|
|
806
|
+
const currentSet = Math.floor(currentScroll / setWidth);
|
|
807
|
+
|
|
808
|
+
if (currentSet !== centerSetIndex) {
|
|
809
|
+
const delta = (centerSetIndex - currentSet) * setWidth;
|
|
810
|
+
|
|
811
|
+
const oldSnap = this._scrollContainer.style.scrollSnapType;
|
|
812
|
+
const oldBehavior = this._scrollContainer.style.scrollBehavior;
|
|
813
|
+
|
|
814
|
+
// Temporarily disable snap and smooth behavior so the wrap is instantaneous
|
|
815
|
+
this._scrollContainer.style.scrollSnapType = 'none';
|
|
816
|
+
this._scrollContainer.style.scrollBehavior = 'auto';
|
|
817
|
+
|
|
818
|
+
this._scrollContainer.scrollLeft += delta;
|
|
819
|
+
|
|
820
|
+
// INSTANTLY update the GSAP visual ticker so it doesn't animate the jump
|
|
821
|
+
this._tickerTarget = this._scrollContainer.scrollLeft / this.itemSize;
|
|
822
|
+
this._tickerCurrent = this._tickerTarget;
|
|
823
|
+
this._applyLayout(this._tickerCurrent);
|
|
824
|
+
|
|
825
|
+
// Restore original scroll behavior
|
|
826
|
+
this._scrollContainer.style.scrollBehavior = oldBehavior;
|
|
827
|
+
|
|
828
|
+
if (this._isDown) {
|
|
829
|
+
this._scrollLeftStart += delta;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (oldSnap && oldSnap !== 'none') {
|
|
833
|
+
// Re-enable snap on the next frame to avoid jumping
|
|
834
|
+
requestAnimationFrame(() => {
|
|
835
|
+
if (this._scrollContainer && !this._isDown) {
|
|
836
|
+
this._scrollContainer.style.scrollSnapType = oldSnap;
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
private _startAutoplay() {
|
|
844
|
+
this._stopAutoplay();
|
|
845
|
+
if (this.autoplay && this.effectiveItems.length > 0) {
|
|
846
|
+
this._autoplayTimer = setInterval(() => {
|
|
847
|
+
// We removed _isInteracting to make autoplay more robust. It only pauses on active drag.
|
|
848
|
+
if (!this._isDown) {
|
|
849
|
+
this._scrollNext();
|
|
850
|
+
}
|
|
851
|
+
}, this.autoplayInterval) as unknown as number;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
private _stopAutoplay() {
|
|
856
|
+
if (this._autoplayTimer) {
|
|
857
|
+
clearInterval(this._autoplayTimer);
|
|
858
|
+
this._autoplayTimer = null;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
private _handleMouseEnter() {
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
private _handleMouseLeave() {
|
|
866
|
+
this._handleMouseUp();
|
|
867
|
+
}
|
|
868
|
+
|
|
696
869
|
private _handleMouseDown(e: MouseEvent) {
|
|
697
870
|
gsap.killTweensOf(this._scrollContainer);
|
|
871
|
+
this._checkInfiniteWrap();
|
|
698
872
|
this._isDown = true;
|
|
699
873
|
this._draggedDistance = 0;
|
|
700
874
|
this._scrollContainer.style.scrollBehavior = 'auto';
|
|
@@ -736,13 +910,22 @@ export class MoniCarousel extends MoniElement {
|
|
|
736
910
|
|
|
737
911
|
// Dynamic momentum: stronger swipes have a much higher multiplier
|
|
738
912
|
const momentumMultiplier = 350 + Math.abs(this._velocity) * 300;
|
|
739
|
-
|
|
913
|
+
let projectedScrollLeft = this._scrollContainer.scrollLeft - this._velocity * momentumMultiplier;
|
|
914
|
+
|
|
915
|
+
// Cap momentum throw to prevent hitting infinite wrap edges on fast swipes
|
|
916
|
+
const maxThrowDist = 15 * this.itemSize;
|
|
917
|
+
if (projectedScrollLeft < this._scrollContainer.scrollLeft - maxThrowDist) {
|
|
918
|
+
projectedScrollLeft = this._scrollContainer.scrollLeft - maxThrowDist;
|
|
919
|
+
} else if (projectedScrollLeft > this._scrollContainer.scrollLeft + maxThrowDist) {
|
|
920
|
+
projectedScrollLeft = this._scrollContainer.scrollLeft + maxThrowDist;
|
|
921
|
+
}
|
|
740
922
|
|
|
741
923
|
if (this.layout !== 'uncontained') {
|
|
742
924
|
this._scrollContainer.style.scrollSnapType = 'none';
|
|
743
925
|
|
|
744
926
|
let targetIndex = Math.round(projectedScrollLeft / this.itemSize);
|
|
745
|
-
|
|
927
|
+
const maxIndex = this.effectiveItems.length > 0 ? this._snapItemsCount - 1 : 0;
|
|
928
|
+
targetIndex = Math.max(0, Math.min(maxIndex, targetIndex));
|
|
746
929
|
const snapPoint = targetIndex * this.itemSize;
|
|
747
930
|
|
|
748
931
|
// Dynamic duration: longer distance = more time, max 0.85s for faster feeling
|
|
@@ -767,6 +950,7 @@ export class MoniCarousel extends MoniElement {
|
|
|
767
950
|
|
|
768
951
|
private _handleTouchStart(e: TouchEvent) {
|
|
769
952
|
gsap.killTweensOf(this._scrollContainer);
|
|
953
|
+
this._checkInfiniteWrap();
|
|
770
954
|
this._isDown = true;
|
|
771
955
|
this._draggedDistance = 0;
|
|
772
956
|
this._scrollContainer.style.scrollBehavior = 'auto';
|
|
@@ -831,7 +1015,7 @@ export class MoniCarousel extends MoniElement {
|
|
|
831
1015
|
// Restablecer el snap después de un pequeño retraso
|
|
832
1016
|
if ((this as any)._wheelTimeout) clearTimeout((this as any)._wheelTimeout);
|
|
833
1017
|
(this as any)._wheelTimeout = setTimeout(() => {
|
|
834
|
-
if (this.layout !== 'uncontained') {
|
|
1018
|
+
if (this.layout !== 'uncontained' && !this._isDown) {
|
|
835
1019
|
this._scrollContainer.style.scrollSnapType = 'x mandatory';
|
|
836
1020
|
}
|
|
837
1021
|
}, 150);
|
|
@@ -845,9 +1029,10 @@ export class MoniCarousel extends MoniElement {
|
|
|
845
1029
|
return;
|
|
846
1030
|
}
|
|
847
1031
|
|
|
1032
|
+
const originalIndex = index % this.effectiveItems.length;
|
|
848
1033
|
this.dispatchEvent(
|
|
849
1034
|
new CustomEvent('item-click', {
|
|
850
|
-
detail: { item, index },
|
|
1035
|
+
detail: { item, index: originalIndex },
|
|
851
1036
|
bubbles: true,
|
|
852
1037
|
composed: true
|
|
853
1038
|
})
|
|
@@ -855,14 +1040,13 @@ export class MoniCarousel extends MoniElement {
|
|
|
855
1040
|
}
|
|
856
1041
|
|
|
857
1042
|
private _scrollPrevious() {
|
|
858
|
-
const target =
|
|
1043
|
+
const target = this._scrollContainer.scrollLeft - this.itemSize;
|
|
859
1044
|
this._scrollContainer.style.scrollBehavior = 'smooth';
|
|
860
1045
|
this._scrollContainer.scrollTo({ left: target, behavior: 'smooth' });
|
|
861
1046
|
}
|
|
862
1047
|
|
|
863
1048
|
private _scrollNext() {
|
|
864
|
-
const
|
|
865
|
-
const target = Math.min(maxScroll, this._scrollContainer.scrollLeft + this.itemSize);
|
|
1049
|
+
const target = this._scrollContainer.scrollLeft + this.itemSize;
|
|
866
1050
|
this._scrollContainer.style.scrollBehavior = 'smooth';
|
|
867
1051
|
this._scrollContainer.scrollTo({ left: target, behavior: 'smooth' });
|
|
868
1052
|
}
|
|
@@ -959,15 +1143,27 @@ export class MoniCarousel extends MoniElement {
|
|
|
959
1143
|
rightPadding = minRightPaddingNeeded;
|
|
960
1144
|
}
|
|
961
1145
|
|
|
1146
|
+
let snapItemsCount = hasItems ? this._snapItemsCount : 0;
|
|
1147
|
+
let visualItems = this.effectiveItems;
|
|
1148
|
+
|
|
1149
|
+
if (this.infinite && hasItems) {
|
|
1150
|
+
const V_mult = this._visualCardsMultiplier;
|
|
1151
|
+
|
|
1152
|
+
visualItems = [];
|
|
1153
|
+
for (let i = 0; i < V_mult; i++) {
|
|
1154
|
+
visualItems = visualItems.concat(this.effectiveItems);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
962
1158
|
const trackWidth = hasItems
|
|
963
|
-
?
|
|
1159
|
+
? snapItemsCount * L + (snapItemsCount - 1) * this.gap
|
|
964
1160
|
: 0;
|
|
965
1161
|
|
|
966
1162
|
const isScrollable = hasItems && this._scrollContainer &&
|
|
967
1163
|
(this._scrollContainer.scrollWidth > this._scrollContainer.clientWidth);
|
|
968
|
-
const showPrevArrow = isScrollable && this._scrollContainer.scrollLeft > 5;
|
|
969
|
-
const showNextArrow = isScrollable &&
|
|
970
|
-
(this._scrollContainer.scrollWidth - this._scrollContainer.scrollLeft - this._scrollContainer.clientWidth > 5);
|
|
1164
|
+
const showPrevArrow = this.infinite || (isScrollable && this._scrollContainer.scrollLeft > 5);
|
|
1165
|
+
const showNextArrow = this.infinite || (isScrollable &&
|
|
1166
|
+
(this._scrollContainer.scrollWidth - this._scrollContainer.scrollLeft - this._scrollContainer.clientWidth > 5));
|
|
971
1167
|
|
|
972
1168
|
return html`
|
|
973
1169
|
<div class="carousel-container" style="
|
|
@@ -1015,18 +1211,19 @@ export class MoniCarousel extends MoniElement {
|
|
|
1015
1211
|
@mousedown=${this._handleMouseDown}
|
|
1016
1212
|
@mousemove=${this._handleMouseMove}
|
|
1017
1213
|
@mouseup=${this._handleMouseUp}
|
|
1018
|
-
@
|
|
1214
|
+
@mouseenter=${this._handleMouseEnter}
|
|
1215
|
+
@mouseleave=${this._handleMouseLeave}
|
|
1019
1216
|
@touchstart=${this._handleTouchStart}
|
|
1020
1217
|
@touchmove=${this._handleTouchMove}
|
|
1021
1218
|
@touchend=${this._handleTouchEnd}
|
|
1022
1219
|
@wheel=${this._handleWheel}
|
|
1023
1220
|
>
|
|
1024
1221
|
<div class="snap-track">
|
|
1025
|
-
${hasItems ?
|
|
1222
|
+
${hasItems ? Array.from({ length: snapItemsCount }).map(() => html`<div class="snap-item"></div>`) : ''}
|
|
1026
1223
|
</div>
|
|
1027
1224
|
<div class="visual-track">
|
|
1028
1225
|
${hasItems
|
|
1029
|
-
?
|
|
1226
|
+
? visualItems.map(
|
|
1030
1227
|
(item, idx) => html`
|
|
1031
1228
|
<div
|
|
1032
1229
|
class="card"
|
|
@@ -1034,16 +1231,16 @@ export class MoniCarousel extends MoniElement {
|
|
|
1034
1231
|
>
|
|
1035
1232
|
${item.href
|
|
1036
1233
|
? html`
|
|
1037
|
-
<a href="${item.href}" target="${item.target || '_self'}" style="text-decoration:none; color:inherit;">
|
|
1234
|
+
<a href="${item.href}" target="${item.target || '_self'}" style="text-decoration:none; color:inherit;" draggable="false">
|
|
1038
1235
|
<div class="img-parallax-container">
|
|
1039
|
-
<img src="${item.img}" alt="${item.title}" draggable="false" />
|
|
1236
|
+
<img src="${item.img}" alt="${item.title}" draggable="false" loading="lazy" />
|
|
1040
1237
|
</div>
|
|
1041
1238
|
<h2 class="card-title">${item.title}</h2>
|
|
1042
1239
|
</a>
|
|
1043
1240
|
`
|
|
1044
1241
|
: html`
|
|
1045
1242
|
<div class="img-parallax-container">
|
|
1046
|
-
<img src="${item.img}" alt="${item.title}" draggable="false" />
|
|
1243
|
+
<img src="${item.img}" alt="${item.title}" draggable="false" loading="lazy" />
|
|
1047
1244
|
</div>
|
|
1048
1245
|
<h2 class="card-title">${item.title}</h2>
|
|
1049
1246
|
`}
|