UNPKG

gd-bs

Version:

Bootstrap JavaScript, TypeScript and Web Components library.

408 lines (337 loc) 14.3 kB
import { Base } from "../base"; import { ICarousel, ICarouselProps } from "./types"; import { CarouselItem } from "./item"; import { HTML } from "./templates"; /** * Carousel * @param props - The carousel properties. */ class _Carousel extends Base<ICarouselProps> implements ICarousel { private _eventId = null; private _indicators: HTMLElement[] = null; private _pauseFlag = false; private _slides: CarouselItem[] = null; private _toggle: boolean = false; // Constructor constructor(props: ICarouselProps, template: string = HTML, slideTemplate?: string) { super(template, props); // Configure the carousel this.configure(slideTemplate); // Configure the events this.configureEvents(); // Configure the parent this.configureParent(); } // Configure the card group private configure(slideTemplate: string) { // Set the attributes this.el.id = this.props.id == null ? "carousel" : this.props.id; this.props.enableCrossfade ? this.el.classList.add("carousel-fade") : null; // Render the indicators this.renderIndicators(); // Render the controls this.renderControls(); // Render the slides this.renderSlides(slideTemplate); // Set the dark theme this.props.isDark ? this.setTheme(true) : null; // Get the options let options = this.props.options; if (options) { // See if the interval is set if (options.interval) { this.start(options.interval); } // See if the starting slide is set if (options.slide) { this.nextWhenVisible(options.slide); } } // Call the event if it exists this.props.onRendered ? this.props.onRendered(this.el, this.props) : null; } // Configures the events private configureEvents() { let el: HTMLElement = this.el; // Get the options let options = this.props.options; if (options) { // See if the keyboard option is set if (options.keyboard) { // Add a keydown event el.addEventListener("keydown", (ev) => { // See if the left arrow was pressed if (ev.key == "ArrowLeft" || ev.keyCode == 37) { // Move to the previous slide this.previous(); } // Else, see if the right arrow was pressed else if (ev.key == "ArrowRight" || ev.keyCode == 39) { // Move tot he next slide this.next(); } }); } // See if the pause option is set if (options.pause) { // Set the mouse enter event el.addEventListener("mouseenter", () => { // See if automation exists if (this._eventId) { // Pause the automation this.pause(); } }); // Set the mouse exit event el.addEventListener("mouseenter", () => { // See if automation exists if (this._eventId) { // Unpause the automation this.unpause(); } }); } } } // Moves to the another slides private moveToSlide(current: CarouselItem, next: CarouselItem, slideRight: boolean = true) { // Do nothing if the toggle flag is set if (this._toggle) { return; } // Set the flag this._toggle = true; // Ensure the slides exist if (current && next) { // Animate the current slide out next.el.classList.add(slideRight ? "carousel-item-next" : "carousel-item-prev"); current.el.classList.add(slideRight ? "carousel-item-start" : "carousel-item-end"); // Wait for the animation to complete setTimeout(() => { // Animate the next slide in next.el.classList.add(slideRight ? "carousel-item-start" : "carousel-item-end"); // Wait for the animation to complete setTimeout(() => { // Update the classes next.el.classList.add("active"); current.el.classList.remove("active", "carousel-item-start", "carousel-item-end"); next.el.classList.remove("carousel-item-next", "carousel-item-prev", "carousel-item-start", "carousel-item-end"); // Set the flag this._toggle = false; }, 600); }, 10); } } // Renders the controls private renderControls() { // Get the controls let nextControl = this.el.querySelector(".carousel-control-next") as HTMLAnchorElement; let prevControl = this.el.querySelector(".carousel-control-prev") as HTMLAnchorElement; // See if we are rendering controls if (this.props.enableControls) { // Configure the controls nextControl ? nextControl.setAttribute("data-bs-target", "#" + this.el.id) : null; prevControl ? prevControl.setAttribute("data-bs-target", "#" + this.el.id) : null; // Set the click event nextControl.addEventListener("click", ev => { ev.preventDefault(); this.next(); }) prevControl.addEventListener("click", ev => { ev.preventDefault(); this.previous(); }) } else { // Remove the controls nextControl ? this.el.removeChild(nextControl) : null; prevControl ? this.el.removeChild(prevControl) : null; } } // Renders the indicators private renderIndicators() { // Clear the indicators this._indicators = []; // Get the indicators let indicators = this.el.querySelector(".carousel-indicators"); if (indicators) { // See if we are enabling indicators if (this.props.enableIndicators) { // Parse the items let items = this.props.items || []; for (let i = 0; i < items.length; i++) { let item = items[i]; // Create the item let elItem = document.createElement("button"); elItem.setAttribute("data-bs-target", "#" + this.el.id); elItem.setAttribute("aria-label", "Slide " + (i + 1)); elItem.setAttribute("data-bs-slide-to", i.toString()); item.isActive ? elItem.classList.add("active") : null; elItem.addEventListener("click", ev => { let elSlide = ev.currentTarget as HTMLElement; // Prevent the page from moving to the top ev.preventDefault(); // Go to the slide this.nextWhenVisible(elSlide.getAttribute("data-bs-slide-to")); }); // Add the item indicators.appendChild(elItem); this._indicators.push(elItem); } } else { // Remove the indicators this.el.removeChild(indicators); } } } // Renders the slides private renderSlides(slideTemplate: string) { // Clear the slides this._slides = []; // Get the indicators let slides = this.el.querySelector(".carousel-inner"); if (slides) { let hasActiveItem = false; // Parse the items let items = this.props.items || []; for (let i = 0; i < items.length; i++) { let slide = new CarouselItem(items[i], slideTemplate); this._slides.push(slide); // See if this is active slide.isActive ? hasActiveItem = true : null; // Create the item element slides.appendChild(slide.el); // Call the event this.props.onSlideRendered ? this.props.onSlideRendered(slide.el, items[i]) : null; } // See if it doesn't have an active item if (!hasActiveItem) { // Set the first as active let firstSlide = this._slides[0]; firstSlide ? firstSlide.el.classList.add("active") : null; } } } // Starts to move automatically private start(timeToWait = 5000) { // Do nothing if the event already exists if (this._eventId) { return; } // Validate the time if (timeToWait < 1000) { timeToWait = 1000; } // Start the event this._eventId = setInterval(() => { // Do nothing if we have paused it if (this._pauseFlag) { return; } // Move to the next slide this.next(); }, timeToWait); } /** * Public Interface */ // Cycle the carousel cycle() { // Start the event this.start(this.props.options && this.props.options.interval as any) } // Goes to the next slide next() { let currentSlide: CarouselItem = null; let nextSlide: CarouselItem = null; let options = this.props.options || {}; // Ensure there are multiple slides if (this._slides.length < 2) { return; } // Parse the slides for (let i = 0; i < this._slides.length; i++) { let slide = this._slides[i]; if (slide.isActive) { // See if we are at the end and wrapping is disabled if (i + 1 == this._slides.length && options.wrap == false) { // Do nothing return; } // Set the slides currentSlide = slide; nextSlide = this._slides[i + 1] || this._slides[0]; // Update the indicators let indicator = this._indicators[i]; indicator ? indicator.classList.remove("active") : null; let nextIndicator = this._indicators[i + 1] || this._indicators[0]; nextIndicator ? nextIndicator.classList.add("active") : null; break; } } // Move to the next slide this.moveToSlide(currentSlide, nextSlide); } // Cycles the carousel to a particular frame nextWhenVisible(idx) { let currentSlide: CarouselItem = null; let nextSlide: CarouselItem = this._slides[idx]; let slideRight = true; // Ensure there are multiple slides if (this._slides.length < 2) { return; } // Parse the slides for (let i = 0; i < this._slides.length; i++) { let slide = this._slides[i]; // See if this slide is active if (slide.isActive) { // Do nothing if we selected the same slide if (idx == i) { return; } // Set the flag slideRight = idx > i; // Set the current slide currentSlide = slide; // Update the indicators let indicator = this._indicators[i]; indicator ? indicator.classList.remove("active") : null; let nextIndicator = this._indicators[idx]; nextIndicator ? nextIndicator.classList.add("active") : null; break; } } // Move to the next slide this.moveToSlide(currentSlide, nextSlide, slideRight); } // Pauses the slide pause() { // Set the flag this._pauseFlag = true; } // Goes to the previous slide previous() { let currentSlide: CarouselItem = null; let options = this.props.options || {}; let prevSlide: CarouselItem = null; // Ensure there are multiple slides if (this._slides.length < 2) { return; } // Parse the slides for (let i = 0; i < this._slides.length; i++) { let slide = this._slides[i]; if (slide.isActive) { // See if we are at the end and wrapping is disabled if (i - 1 < 0 && options.wrap == false) { // Do nothing return; } // Set the slides currentSlide = slide; prevSlide = this._slides[i - 1] || this._slides[this._slides.length - 1]; // Update the indicators this._indicators[i].classList.remove("active"); (this._indicators[i - 1] || this._indicators[this._indicators.length - 1]).classList.add("active"); break; } } // Move to the next slide this.moveToSlide(currentSlide, prevSlide, false); } // Enables/Disables the dark theme setTheme(isDark: boolean) { // See if we are setting the dark theme if (isDark) { // Set the theme this.el.classList.add("carousel-dark"); } else { // Set the theme this.el.classList.remove("carousel-dark"); } } // Unpauses the carousel unpause() { // Set the flag this._pauseFlag = false; } } export const Carousel = (props: ICarouselProps, template?: string, slideTemplate?: string): ICarousel => { return new _Carousel(props, template, slideTemplate); }