const smCarousel = document.createElement('template') smCarousel.innerHTML = ` `; customElements.define('sm-carousel', class extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }).append(smCarousel.content.cloneNode(true)) this.isAutoPlaying = false this.autoPlayInterval = 5000 this.autoPlayTimeout this.initialTimeout this.activeSlideNum = 0 this.carouselItems this.indicators this.showIndicator = false this.carousel = this.shadowRoot.querySelector('.carousel') this.carouselContainer = this.shadowRoot.querySelector('.carousel-container') this.carouselSlot = this.shadowRoot.querySelector('slot') this.navButtonRight = this.shadowRoot.querySelector('.carousel__button--right') this.navButtonLeft = this.shadowRoot.querySelector('.carousel__button--left') this.indicatorsContainer = this.shadowRoot.querySelector('.indicators') this.scrollLeft = this.scrollLeft.bind(this) this.scrollRight = this.scrollRight.bind(this) this.handleIndicatorClick = this.handleIndicatorClick.bind(this) this.showSlide = this.showSlide.bind(this) this.nextSlide = this.nextSlide.bind(this) this.autoPlay = this.autoPlay.bind(this) this.startAutoPlay = this.startAutoPlay.bind(this) this.stopAutoPlay = this.stopAutoPlay.bind(this) } static get observedAttributes() { return ['indicator', 'autoplay', 'interval'] } scrollLeft() { this.carousel.scrollBy({ left: -this.scrollDistance, behavior: 'smooth' }) } scrollRight() { this.carousel.scrollBy({ left: this.scrollDistance, behavior: 'smooth' }) } showSlide(slideNum) { this.carousel.scrollTo({ left: (this.carouselItems[slideNum].getBoundingClientRect().left - this.carousel.getBoundingClientRect().left + this.carousel.scrollLeft), behavior: 'smooth' }) } nextSlide() { if (!this.carouselItems) return let showSlideNo = (this.activeSlideNum + 1) < this.carouselItems.length ? this.activeSlideNum + 1 : 0 this.showSlide(showSlideNo) } autoPlay() { this.nextSlide() if (this.isAutoPlaying) { this.autoPlayTimeout = setTimeout(() => { this.autoPlay() }, this.autoPlayInterval); } } startAutoPlay() { this.setAttribute('autoplay', '') } stopAutoPlay() { this.removeAttribute('autoplay') } createIndicator(index) { let indicator = document.createElement('div') indicator.classList.add('indicator') indicator.dataset.rank = index return indicator } handleIndicatorClick(e) { if (e.target.closest('.indicator')) { const slideNum = parseInt(e.target.closest('.indicator').dataset.rank) if (this.activeSlideNum !== slideNum) { this.showSlide(slideNum) } } } handleKeyDown(e) { if (e.code === 'ArrowLeft') this.scrollRight() else if (e.code === 'ArrowRight') this.scrollRight() } connectedCallback() { let frag = document.createDocumentFragment(); this.carouselSlot.addEventListener('slotchange', e => { this.carouselItems = this.carouselSlot.assignedElements() this.carouselItems.forEach(item => allElementsObserver.observe(item)) if (this.carouselItems.length > 0) { firstOptionObserver.observe(this.carouselItems[0]) lastOptionObserver.observe(this.carouselItems[this.carouselItems.length - 1]) } else { navButtonLeft.classList.add('hide') navButtonRight.classList.add('hide') firstOptionObserver.disconnect() lastOptionObserver.disconnect() } if (this.showIndicator) { this.indicatorsContainer.innerHTML = `` this.carouselItems.forEach((item, index) => { frag.append( this.createIndicator(index) ) item.dataset.rank = index }) this.indicatorsContainer.append(frag) this.indicators = this.indicatorsContainer.children } }) const IOOoptions = { threshold: 0.9, root: this } const allElementsObserver = new IntersectionObserver(entries => { entries.forEach(entry => { if (this.showIndicator) { const activeRank = parseInt(entry.target.dataset.rank) if (entry.isIntersecting) { this.indicators[activeRank].classList.add('active') this.activeSlideNum = activeRank } else this.indicators[activeRank].classList.remove('active') } }) }, IOOoptions) const firstOptionObserver = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting) { this.navButtonLeft.classList.add('hide') } else { this.navButtonLeft.classList.remove('hide') } }) }, IOOoptions ) const lastOptionObserver = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting) { this.navButtonRight.classList.add('hide') } else { this.navButtonRight.classList.remove('hide') } }) }, IOOoptions ) const resObs = new ResizeObserver(entries => { entries.forEach(entry => { if (entry.contentBoxSize) { // Firefox implements `contentBoxSize` as a single content rect, rather than an array const contentBoxSize = Array.isArray(entry.contentBoxSize) ? entry.contentBoxSize[0] : entry.contentBoxSize; this.scrollDistance = contentBoxSize.inlineSize * 0.6 } else { this.scrollDistance = entry.contentRect.width * 0.6 } }) }) resObs.observe(this) this.addEventListener('keydown', this.handleKeyDown) this.navButtonRight.addEventListener('click', this.scrollRight) this.navButtonLeft.addEventListener('click', this.scrollLeft) this.indicatorsContainer.addEventListener('click', this.handleIndicatorClick) } attributeChangedCallback(name, oldValue, newValue) { if (oldValue !== newValue) { if (name === 'indicator') { this.showIndicator = this.hasAttribute('indicator') } if (name === 'autoplay') { if (this.hasAttribute('autoplay')) { this.initialTimeout = setTimeout(() => { this.isAutoPlaying = true this.autoPlay() }, this.autoPlayInterval); } else { this.isAutoPlaying = false clearTimeout(this.autoPlayTimeout) clearTimeout(this.initialTimeout) } } if (name === 'interval') { if (this.hasAttribute('interval') && this.getAttribute('interval').trim() !== '') { this.autoPlayInterval = Math.abs(parseInt(this.getAttribute('interval').trim())) } else { this.autoPlayInterval = 5000 } } } } disconnectedCallback() { this.navButtonRight.removeEventListener('click', this.scrollRight) this.navButtonLeft.removeEventListener('click', this.scrollLeft) this.indicatorsContainer.removeEventListener('click', this.handleIndicatorClick) } })