diff --git a/cigaradvisor/blocks/carousel/carousel.css b/cigaradvisor/blocks/carousel/carousel.css new file mode 100644 index 00000000..51ebe4cb --- /dev/null +++ b/cigaradvisor/blocks/carousel/carousel.css @@ -0,0 +1,100 @@ +.carousel-wrapper { + position: relative; + padding-left: 0; + padding-right: 0; + width: 100%; + margin-left: auto; + margin-right: auto; + max-width: 1080px; + overflow: hidden; +} + +.carousel.block>div { + scroll-margin: 0; + scroll-snap-type: x mandatory; + scroll-behavior: smooth; + display: flex; + scrollbar-width: none; + transition: transform 3000ms ease 0ms; +} + +.carousel.block a picture { + display: flex; +} + +.carousel.block a picture > img{ + display: block; + flex-grow: 1; +} + +.carousel.block>div::-webkit-scrollbar { + display: none; +} + +.carousel.block .slide { + scroll-snap-align: start; + display: block; + flex: 1 0 100%; + scroll-margin: unset; + padding: 0 2px; +} + + +.carousel.block>button { + position: absolute; + top: 40%; + background: unset; + padding: 0; + margin: 0; + border: none; +} + +.carousel.block>button.prev { + left: 0; +} + +.carousel.block>button.next { + right: 0; + padding: 0 30px 0 0; +} + + +.carousel.block .carousel-arrow { + display: inline-block; + cursor: pointer; + position: absolute; + z-index: 9; + padding: 0; + margin: 0; + background-color: #000000B3; + text-align: center; + width: 30px; + height: 50px; +} + +.carousel.block .carousel-arrow .prev-icon, +.carousel.block .carousel-arrow .next-icon { + display: inline-block; + font-size: 45px; + color: #fff; +} + +.carousel.block .carousel-arrow .next-icon { + transform: rotate(180deg); + margin: 10%; +} + +.carousel.block .carousel-arrow .prev-icon { + margin: -10%; +} + +.carousel.block .arrow-prev { + display: none; +} + + +@media screen and (min-width: 600px) { + .carousel.block .slide { + flex: 1 0 50%; + } +} \ No newline at end of file diff --git a/cigaradvisor/blocks/carousel/carousel.js b/cigaradvisor/blocks/carousel/carousel.js new file mode 100644 index 00000000..47443159 --- /dev/null +++ b/cigaradvisor/blocks/carousel/carousel.js @@ -0,0 +1,106 @@ +import { isExternal } from '../../scripts/scripts.js'; + +function setAutoScroll(moveSlides, block) { + let interval; + setTimeout(() => { + if (interval === undefined) { + interval = setInterval(() => { + moveSlides('next'); + }, 6000); + } + }, 3000); + + // Stop auto-scroll on user interaction + block.addEventListener('mouseenter', () => { + clearInterval(interval); + interval = undefined; + }); + + block.addEventListener('mouseleave', () => { + if (interval === undefined) { + interval = setInterval(() => { + moveSlides('next'); + }, 6000); + } + }); +} + +function createButtons(moveSlides) { + return ['prev', 'next'].map((direction) => { + const button = document.createElement('button'); + button.ariaLabel = `show ${direction} slide`; + button.classList.add(direction); + const iconDiv = document.createElement('div'); + iconDiv.classList.add(`arrow-${direction}`); + iconDiv.classList.add('carousel-arrow'); + const iconSpan = document.createElement('span'); + iconSpan.classList.add(`${direction}-icon`); + iconSpan.innerHTML = '‹'; + iconDiv.append(iconSpan); + button.appendChild(iconDiv); + button.addEventListener('click', () => moveSlides(direction)); + return button; + }); +} + +/** + * Generic carousel block, which can be used for any content or blocks. + * Each row is a slide. + * left column is image and right column is the link. + * @param block + */ +export default async function decorate(block) { + const mobile = (window.screen.width < 600); + const offset = mobile ? 100 : 50; + const itemsToShow = mobile ? 1 : 2; + const slidesWrapper = document.createElement('div'); + slidesWrapper.classList.add('slides-wrappper'); + [...block.children].forEach((row) => { + const slide = document.createElement('div'); + slide.classList.add('slide'); + let pic; + let anchor; + [...row.children].forEach((col) => { + if (col.querySelector('picture')) { + pic = col.querySelector('picture'); + } + if (col.querySelector('a')) { + anchor = col.querySelector('a'); + } + }); + const img = pic.querySelector('img'); + const link = anchor.getAttribute('href'); + anchor.setAttribute('target', isExternal(link) ? '_blank' : '_self'); + anchor.setAttribute('title', img.alt); + anchor.replaceChildren(pic); + slide.append(anchor); + slidesWrapper.append(slide); + }); + + block.replaceChildren(slidesWrapper); + + let currentIndex = 0; + const items = slidesWrapper.querySelectorAll('.slide'); + function moveSlides(prevOrNext) { + if (prevOrNext === 'next') { + if (currentIndex < (items.length - itemsToShow)) { + currentIndex += 1; + slidesWrapper.style.transform = `translate3d(-${currentIndex * offset}%, 0, 0)`; + block.querySelector('.arrow-prev').style.display = 'inline-block'; + if (currentIndex === (items.length - itemsToShow)) { + block.querySelector('.arrow-next').style.display = 'none'; + } + } + } else if (currentIndex >= 1) { + currentIndex -= 1; + slidesWrapper.style.transform = `translate3d(-${currentIndex * offset}%, 0, 0)`; + block.querySelector('.arrow-next').style.display = 'inline-block'; + if (currentIndex < 1) { + block.querySelector('.arrow-prev').style.display = 'none'; + } + } + } + + block.append(...createButtons(moveSlides)); + setAutoScroll(moveSlides, block); +}