import { html, PropertyValueMap, render, unsafeCSS, svg, nothing } from "lit"; import { property, query } from "lit/decorators.js"; import { FRoot } from "./../../mixins/components/f-root/f-root"; import eleStyle from "./f-carousel.scss?inline"; import globalStyle from "./f-carousel-global.scss?inline"; import { FDiv } from "../f-div/f-div"; import { FCarouselContent } from "../f-carousel-content/f-carousel-content"; import { FIcon } from "../f-icon/f-icon"; import { flowElement } from "./../../utils"; import { injectCss } from "@cldcvr/flow-core-config"; injectCss("f-carousel", globalStyle); @flowElement("f-carousel") export class FCarousel extends FRoot { /** * css loaded from scss file */ static styles = [unsafeCSS(eleStyle), unsafeCSS(globalStyle), ...FDiv.styles]; /** * @attribute provide `f-corousel-content` content-id for activation */ @property({ type: String, attribute: "active-content-id" }) activeContentId?: string; /** * @attribute it will auto-play all content in loop */ @property({ type: Boolean, attribute: "auto-play" }) autoPlay?: boolean; /** * @attribute it will auto-play all content in loop */ @property({ type: Number, attribute: "auto-play-interval" }) autoPlayInterval?: number; @query(".slides") slides!: HTMLElement; @query(".slider") slider!: HTMLElement; @query("#dots") dots!: HTMLElement; @query(".next") nextArrow!: FIcon; @query(".progress") progress?: FDiv; @query(".prev") prevArrow!: FIcon; activeSlide?: HTMLElement | null; slideElements?: NodeListOf; handleNavigation(type: "next" | "prev" = "next") { if (this.activeSlide) { let nextActive = this.activeSlide.nextElementSibling; if (type === "prev") { nextActive = this.activeSlide.previousElementSibling; } if (nextActive) { if ((nextActive as HTMLElement).id === "firstNode") { this.slideTransition((this.slideElements?.length as number) + 1); } else if ((nextActive as HTMLElement).id === "lastNode") { this.slideTransition(0); } else if (nextActive) { this.slideTransition(this.getSlideIndex(nextActive as HTMLElement) + 1); } this.activeSlide = nextActive as HTMLElement; this.updateDots(); this.emitNavigationEvent(type, (this.activeSlide as FCarouselContent).contentId); this.checkAutoPlay(); } } } handleTransitionEnd() { if (this.activeSlide?.id === "firstNode") { this.slideTransition(1, 0); this.activeSlide = this.slideElements?.item(0) as HTMLElement; } if (this.activeSlide?.id === "lastNode") { this.slideTransition(this.slideElements?.length as number, 0); this.activeSlide = this.slideElements?.item(this.slideElements.length - 1) as HTMLElement; } this.updateDots(); } render() { const progressBar = () => { return nothing; // return this.autoPlay // ? html` // // // // // ` // : nothing; }; return html` this.handleNavigation("prev")} > this.handleNavigation("next")} > ${progressBar()} `; } protected async updated( changedProperties: PropertyValueMap | Map ): Promise { super.updated(changedProperties); await this.updateComplete; /** * remove clonned elements if any */ this.removeClonnedSlides(); /** * get all slide elements */ this.slideElements = this.querySelectorAll(`f-carousel-content`); /** * set active slide if any */ if (this.activeContentId) { this.activeSlide = this.querySelector(`[content-id='${this.activeContentId}']`); } else { this.activeSlide = this.slideElements.item(0); } /** clone last slide for looping */ const lastNode = this.slideElements ?.item(this.slideElements.length - 1) .cloneNode(true) as HTMLElement; lastNode.id = "lastNode"; this.prepend(lastNode); /** clone first slide for looping */ const firstNode = this.slideElements?.item(0).cloneNode(true) as HTMLElement; firstNode.id = "firstNode"; this.append(firstNode); this.slideElements.forEach(n => { n.style.width = this.slider.offsetWidth + "px"; }); lastNode.style.width = this.slider.offsetWidth + "px"; firstNode.style.width = this.slider.offsetWidth + "px"; this.renderDots(); if (this.activeSlide) { this.slideTransition(this.getSlideIndex(this.activeSlide) + 1, 0); this.emitNavigationEvent("next", (this.activeSlide as FCarouselContent).contentId); } this.checkAutoPlay(); } removeClonnedSlides() { const clonnedNodes = this.querySelectorAll(`#firstNode,#lastNode`); clonnedNodes.forEach(el => { el.remove(); }); } checkAutoPlay() { if (this.autoPlay) { setTimeout(() => { this.nextArrow.click(); }, this.autoPlayInterval ?? 5000); } // if (this.autoPlay) { // if (this.progress) { // this.progress.style.transitionDuration = `${ // this.autoPlayInterval ? this.autoPlayInterval / 1000 : 5 // }s`; // this.progress.width = "100%"; // } // } } handleProgressEnd() { if (this.progress) { this.progress.style.transitionDuration = `0s`; this.progress.width = "2px"; } this.nextArrow.click(); setTimeout(() => { this.checkAutoPlay(); }); } emitNavigationEvent(type: "next" | "prev", contentId: string) { const event = new CustomEvent(type, { detail: { contentId: contentId }, bubbles: true, composed: true }); this.dispatchEvent(event); } jumpTo(contentId: string) { this.activeSlide = this.querySelector(`[content-id='${contentId}']`); if (this.activeSlide) { this.slideTransition(this.getSlideIndex(this.activeSlide) + 1); } } getSlideIndex(slide: HTMLElement) { if (this.slideElements) { return Array.from(this.slideElements).findIndex(el => el === slide); } return -1; } slideTransition(slideIndex: number, duration = 0.3) { this.slides.style.transition = `transform ${duration}s ease-in-out`; this.slides.style.transform = "translateX(" + -this.slider.offsetWidth * slideIndex + "px)"; } renderDots() { if (this.slideElements) { const activeIndex = Array.from(this.slideElements).findIndex(el => el === this.activeSlide); render( html`${Array.from(this.slideElements).map((el, index) => { let dotClass = "dot tertiary"; if (activeIndex === index) { dotClass = "dot active"; } else if (Math.abs(activeIndex - index) === 1) { dotClass = "dot secondary"; } return html` { this.updateDots(index); this.jumpTo(el.getAttribute("content-id") as string); }} > ${svg` `}`; })}`, this.dots ); } } updateDots(activeIndex: number | null = null) { if (this.slideElements) { if (!activeIndex) { activeIndex = Array.from(this.slideElements).findIndex(el => el === this.activeSlide); } Array.from(this.slideElements).forEach((el, index) => { let dotClass = "tertiary"; if (activeIndex === index) { dotClass = "active"; } else if (activeIndex && Math.abs(activeIndex - index) === 1) { dotClass = "secondary"; } const svgElement = this.shadowRoot?.querySelector( `[content-id='${el.getAttribute("content-id")}']` ); if (svgElement) { svgElement.classList.remove("tertiary"); svgElement.classList.remove("active"); svgElement.classList.remove("secondary"); svgElement.classList.add(dotClass); } }); } } } /** * Required for typescript */ declare global { export interface HTMLElementTagNameMap { "f-carousel": FCarousel; } }