scanfile/server/web/templui/components/carousel/carousel.templ
2025-06-03 15:44:56 +02:00

394 lines
9 KiB
Text

// templui component carousel - version: main installed by templui v0.71.0
package carousel
import (
"fmt"
"git.jmbit.de/jmb/scanfile/server/web/templui/components/icon"
"git.jmbit.de/jmb/scanfile/server/web/templui/utils"
"strconv"
)
type Props struct {
ID string
Class string
Attributes templ.Attributes
Autoplay bool
Interval int
Loop bool
}
type ContentProps struct {
ID string
Class string
Attributes templ.Attributes
}
type ItemProps struct {
ID string
Class string
Attributes templ.Attributes
}
type PreviousProps struct {
ID string
Class string
Attributes templ.Attributes
}
type NextProps struct {
ID string
Class string
Attributes templ.Attributes
}
type IndicatorsProps struct {
ID string
Class string
Attributes templ.Attributes
Count int
}
templ Carousel(props ...Props) {
@Script()
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"carousel-component relative overflow-hidden w-full",
p.Class,
),
}
data-autoplay={ strconv.FormatBool(p.Autoplay) }
data-interval={ fmt.Sprintf("%d", func() int {
if p.Interval == 0 {
return 5000
}
return p.Interval
}()) }
data-loop={ strconv.FormatBool(p.Loop) }
{ p.Attributes... }
>
{ children... }
</div>
}
templ Content(props ...ContentProps) {
{{ var p ContentProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"carousel-track flex h-full w-full transition-transform duration-500 ease-in-out",
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
</div>
}
templ Item(props ...ItemProps) {
{{ var p ItemProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"carousel-item flex-shrink-0 w-full h-full relative",
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
</div>
}
templ Previous(props ...PreviousProps) {
{{ var p PreviousProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<button
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"carousel-prev absolute left-2 top-1/2 transform -translate-y-1/2 p-2 rounded-full bg-black/20 text-white hover:bg-black/40 focus:outline-none",
p.Class,
),
}
aria-label="Previous slide"
type="button"
{ p.Attributes... }
>
@icon.ChevronLeft()
</button>
}
templ Next(props ...NextProps) {
{{ var p NextProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<button
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"carousel-next absolute right-2 top-1/2 transform -translate-y-1/2 p-2 rounded-full bg-black/20 text-white hover:bg-black/40 focus:outline-none",
p.Class,
),
}
aria-label="Next slide"
type="button"
{ p.Attributes... }
>
@icon.ChevronRight()
</button>
}
templ Indicators(props ...IndicatorsProps) {
{{ var p IndicatorsProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"absolute bottom-4 left-1/2 transform -translate-x-1/2 flex gap-2",
p.Class,
),
}
{ p.Attributes... }
>
for i := 0; i < p.Count; i++ {
<button
class={
utils.TwMerge(
"carousel-indicator w-3 h-3 rounded-full bg-white/50 hover:bg-white/80 focus:outline-none transition-colors",
utils.If(i == 0, "bg-white"),
),
}
aria-label={ fmt.Sprintf("Go to slide %d", i+1) }
type="button"
></button>
}
</div>
}
var handle = templ.NewOnceHandle()
templ Script() {
@handle.Once() {
<script defer nonce={ templ.GetNonce(ctx) }>
(function() { // IIFE
function initCarousel(carousel) {
const track = carousel.querySelector('.carousel-track');
const items = Array.from(track?.querySelectorAll('.carousel-item') || []);
if (items.length === 0) return;
const indicators = Array.from(carousel.querySelectorAll('.carousel-indicator'));
const prevBtn = carousel.querySelector('.carousel-prev');
const nextBtn = carousel.querySelector('.carousel-next');
const state = {
currentIndex: 0,
slideCount: items.length,
autoplay: carousel.dataset.autoplay === 'true',
interval: parseInt(carousel.dataset.interval || 5000),
loop: carousel.dataset.loop === 'true',
autoplayInterval: null,
isHovering: false,
touchStartX: 0
};
function updateTrackPosition() {
track.style.transform = `translateX(-${state.currentIndex * 100}%)`;
}
function updateIndicators() {
indicators.forEach((indicator, i) => {
if (i < state.slideCount) {
if (i === state.currentIndex) {
indicator.classList.add('bg-white');
indicator.classList.remove('bg-white/50');
} else {
indicator.classList.remove('bg-white');
indicator.classList.add('bg-white/50');
}
indicator.style.display = '';
} else {
indicator.style.display = 'none';
}
});
}
function updateButtons() {
if (prevBtn) {
prevBtn.disabled = !state.loop && state.currentIndex === 0;
prevBtn.classList.toggle('opacity-50', prevBtn.disabled);
prevBtn.classList.toggle('cursor-not-allowed', prevBtn.disabled);
}
if (nextBtn) {
nextBtn.disabled = !state.loop && state.currentIndex === state.slideCount - 1;
nextBtn.classList.toggle('opacity-50', nextBtn.disabled);
nextBtn.classList.toggle('cursor-not-allowed', nextBtn.disabled);
}
}
function startAutoplay() {
if (state.autoplayInterval) {
clearInterval(state.autoplayInterval);
}
if (state.autoplay) {
state.autoplayInterval = setInterval(() => {
if (!state.isHovering) {
goToNext();
}
}, state.interval);
}
}
function stopAutoplay() {
if (state.autoplayInterval) {
clearInterval(state.autoplayInterval);
state.autoplayInterval = null;
}
}
function goToNext() {
let nextIndex = state.currentIndex + 1;
if (nextIndex >= state.slideCount) {
if (state.loop) {
nextIndex = 0;
} else {
return;
}
}
goToSlide(nextIndex);
}
function goToPrev() {
let prevIndex = state.currentIndex - 1;
if (prevIndex < 0) {
if (state.loop) {
prevIndex = state.slideCount - 1;
} else {
return;
}
}
goToSlide(prevIndex);
}
function goToSlide(index) {
if (index < 0 || index >= state.slideCount) {
if (state.loop) {
index = (index + state.slideCount) % state.slideCount;
} else {
return;
}
}
if (index === state.currentIndex) return;
state.currentIndex = index;
updateTrackPosition();
updateIndicators();
updateButtons();
if (state.autoplay && !state.isHovering) {
stopAutoplay();
startAutoplay();
}
}
if (track) {
track.addEventListener('touchstart', (e) => {
state.touchStartX = e.touches[0].clientX;
}, { passive: true });
track.addEventListener('touchend', (e) => {
const touchEndX = e.changedTouches[0].clientX;
const diff = state.touchStartX - touchEndX;
const sensitivity = 50;
if (Math.abs(diff) > sensitivity) {
diff > 0 ? goToNext() : goToPrev();
}
}, { passive: true });
}
indicators.forEach((indicator, index) => {
if (index < state.slideCount) {
indicator.addEventListener('click', () => goToSlide(index));
}
});
if (prevBtn) prevBtn.addEventListener('click', goToPrev);
if (nextBtn) nextBtn.addEventListener('click', goToNext);
carousel.addEventListener('mouseenter', () => {
state.isHovering = true;
if (state.autoplay) stopAutoplay();
});
carousel.addEventListener('mouseleave', () => {
state.isHovering = false;
if (state.autoplay) startAutoplay();
});
updateTrackPosition();
updateIndicators();
updateButtons();
if (state.autoplay) startAutoplay();
}
function initAllComponents(root = document) {
if (root instanceof Element && root.matches('.carousel-component')) {
initCarousel(root);
}
for (const carousel of root.querySelectorAll('.carousel-component')) {
initCarousel(carousel);
}
}
const handleHtmxSwap = (event) => {
const target = event.detail.target || event.detail.elt;
if (target instanceof Element) {
requestAnimationFrame(() => initAllComponents(target));
}
};
initAllComponents();
document.addEventListener('DOMContentLoaded', () => initAllComponents());
document.body.addEventListener('htmx:afterSwap', handleHtmxSwap);
document.body.addEventListener('htmx:oobAfterSwap', handleHtmxSwap);
})(); // End of IIFE
</script>
}
}