394 lines
9 KiB
Text
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>
|
|
}
|
|
}
|