Skip to content
Snippets Groups Projects
Select Git revision
  • ebbc6545de3d39f176a7d86064a7d30d4ac2d22a
  • master default protected
  • 1.31
  • 4.24.3
  • 4.24.2
  • 4.24.1
  • 4.24.0
  • 4.23.6
  • 4.23.5
  • 4.23.4
  • 4.23.3
  • 4.23.2
  • 4.23.1
  • 4.23.0
  • 4.22.3
  • 4.22.2
  • 4.22.1
  • 4.22.0
  • 4.21.0
  • 4.20.1
  • 4.20.0
  • 4.19.0
  • 4.18.0
23 results

puppeteer.mjs

Blame
  • slider.mjs 19.15 KiB
    /**
     * Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved.
     * Node module: @schukai/monster
     *
     * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
     * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
     *
     * For those who do not wish to adhere to the AGPLv3, a commercial license is available.
     * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
     * For more information about purchasing a commercial license, please contact schukai GmbH.
     */
    
    import { instanceSymbol } from "../../constants.mjs";
    import { ATTRIBUTE_PREFIX, ATTRIBUTE_ROLE } from "../../dom/constants.mjs";
    import { CustomElement, getSlottedElements } from "../../dom/customelement.mjs";
    import {
    	assembleMethodSymbol,
    	registerCustomElement,
    } from "../../dom/customelement.mjs";
    import { SliderStyleSheet } from "./stylesheet/slider.mjs";
    import { fireCustomEvent } from "../../dom/events.mjs";
    
    import { getWindow } from "../../dom/util.mjs";
    import { isObject, isInteger } from "../../types/is.mjs";
    
    export { Slider };
    
    /**
     * @private
     * @type {symbol}
     */
    const sliderElementSymbol = Symbol("sliderElement");
    
    /**
     * @private
     * @type {symbol}
     */
    const controlElementSymbol = Symbol("controlElement");
    
    /**
     * @private
     * @type {symbol}
     */
    const prevElementSymbol = Symbol("prevElement");
    
    /**
     * @private
     * @type {symbol}
     */
    const nextElementSymbol = Symbol("nextElement");
    
    /**
     * @private
     * @type {symbol}
     */
    const thumbnailElementSymbol = Symbol("thumbnailElement");
    
    /**
     * @private
     * @type {symbol}
     */
    const configSymbol = Symbol("config");
    
    /**
     * A Slider
     *
     * @fragments /fragments/components/layout/slider/
     *
     * @example /examples/components/layout/slider-simple
     * @example /examples/components/layout/slider-carousel
     * @example /examples/components/layout/slider-multiple
     *
     * @since 3.74.0
     * @copyright schukai GmbH
     * @summary A beautiful Slider that can make your life easier and also looks good.
     * @fires monster-slider-resized
     * @fires monster-slider-moved
     */
    class Slider extends CustomElement {
    	/**
    	 * This method is called by the `instanceof` operator.
    	 * @return {symbol}
    	 */
    	static get [instanceSymbol]() {
    		return Symbol.for("@schukai/monster/components/layout/slider@@instance");
    	}
    
    	/**
    	 *
    	 * @return {Components.Layout.Slider
    	 */
    	[assembleMethodSymbol]() {
    		super[assembleMethodSymbol]();
    
    		this[configSymbol] = {
    			currentIndex: 0,
    
    			isDragging: false,
    			draggingPos: 0,
    			startPos: 0,
    			autoPlayInterval: null,
    
    			eventHandler: {
    				mouseOverPause: null,
    				mouseout: null,
    				touchstart : null,
    				touchend: null
    			}
    
    		};
    
    		// set --monster-slides-width
    		const slides = this.shadowRoot.querySelector(
    			`[${ATTRIBUTE_ROLE}="slider"]`,
    		);
    
    		const slidesVisible = getVisibleSlidesFromContainerWidth.call(this);
    		slides.style.setProperty(
    			"--monster-slides-width",
    			`${100 / slidesVisible}%`,
    		);
    
    		initControlReferences.call(this);
    		initEventHandler.call(this);
    		initStructure.call(this);
    
    		return this;
    	}
    
    	/**
    	 * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
    	 * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
    	 *
    	 * The individual configuration values can be found in the table.
    	 *
    	 * @property {Object} templates Template definitions
    	 * @property {string} templates.main Main template
    	 * @property {string} actions.click Callback when clicked
    	 * @property {Object} features Features
    	 * @property {boolean} features.carousel Carousel feature
    	 * @property {boolean} features.autoPlay Auto play feature
    	 * @property {boolean} features.thumbnails Thumbnails feature
    	 * @property {boolean} features.drag Drag feature
    	 * @property {Object} slides Slides configuration, an object with breakpoints and the number of slides to show
    	 * @property {Object} slides.0 Number of slides to show at 0px
    	 * @property {Object} slides.600 Number of slides to show at 600px @since 3.109.0
    	 * @property {Object} slides.1200 Number of slides to show at 1200px @since 3.109.0
    	 * @property {Object} slides.1800 Number of slides to show at 1800px @since 3.109.0
    	 * @property {Object} carousel Carousel configuration
    	 * @property {number} carousel.transition Transition time between a full rotation of the carousel
    	 * @property {Object} autoPlay Auto play configuration
    	 * @property {number} autoPlay.delay Delay between slides
    	 * @property {number} autoPlay.startDelay Start delay
    	 * @property {string} autoPlay.direction Direction of the autoplay
    	 * @property {boolean} autoPlay.mouseOverPause Pause on mouse over
    	 * @property {boolean} autoPlay.touchPause Pause on touch
    	 * @property {Object} classes CSS classes
    	 * @property {boolean} disabled Disabled state
    	 */
    	get defaults() {
    		return Object.assign({}, super.defaults, {
    			templates: {
    				main: getTemplate(),
    			},
    
    			classes: {},
    			disabled: false,
    
    			features: {
    				carousel: true,
    				autoPlay: true,
    				thumbnails: true,
    				drag: true,
    			},
    
    			slides: {
    				0: 1,
    				600: 2,
    				1200: 3,
    				1800: 4,
    			},
    
    			carousel: {
    				transition: 250,
    			},
    
    			autoPlay: {
    				delay: 1500,
    				startDelay: 1000,
    				direction: "next",
    				mouseOverPause: true,
    				touchPause: true,
    			},
    		});
    	}
    
    	/**
    	 * @return {string}
    	 */
    	static getTag() {
    		return "monster-slider";
    	}
    
    	/**
    	 * @return {CSSStyleSheet[]}
    	 */
    	static getCSSStyleSheet() {
    		return [SliderStyleSheet];
    	}
    
    	/**
    	 * moves the slider to the given index
    	 *
    	 * @param index
    	 * @return {void}
    	 */
    	moveTo(index) {
    		return moveTo.call(this, index);
    	}
    
    	/**
    	 * shows the previous slide
    	 *
    	 * @return {void}
    	 */
    	previous() {
    		return prev.call(this);
    	}
    
    	/**
    	 * shows the next slide
    	 *
    	 * @return {void}
    	 */
    	next() {
    		return next.call(this);
    	}
    
    	/**
    	 * stops the auto play
    	 *
    	 * @return {void}
    	 */
    	stopAutoPlay() {
    		if (this[configSymbol].autoPlayInterval) {
    			clearInterval(this[configSymbol].autoPlayInterval);
    		}
    	}
    
    	/**
    	 * starts the auto play
    	 *
    	 * @return {void}
    	 */
    	startAutoPlay() {
    		initAutoPlay.call(this);
    	}
    }
    
    /**
     * @private
     * @param name
     */
    //function initNavigation(name) {
    //const element = this.shadowRoot.querySelector("." + name + "");
    //const elementHeight = element.offsetHeight;
    //element.style.top = `calc(50% - ${elementHeight / 2}px)`;
    //}
    
    /**
     * @private
     */
    function initStructure() {
    	//initNavigation.call(this, "next");
    	//initNavigation.call(this, "prev");
    
    	if (this.getOption("features.thumbnails")) {
    		initThumbnails.call(this);
    	}
    
    	initShadows.call(this);
    
    	if (this.getOption("features.autoPlay")) {
    		initAutoPlay.call(this);
    	}
    }
    
    /**
     * @private
     */
    function initThumbnails() {
    	const self = this;
    	const thumbnails = this.shadowRoot.querySelector(
    		"[data-monster-role='thumbnails']",
    	);
    
    	// remove all thumbnails
    	while (thumbnails.firstChild) {
    		thumbnails.removeChild(thumbnails.firstChild);
    	}
    
    	const { originSlides } = getSlidesAndTotal.call(this);
    
    	originSlides.forEach((x, index) => {
    		const thumbnail = document.createElement("div");
    		thumbnail.classList.add("thumbnail");
    		thumbnail.addEventListener("click", () => {
    			this.moveTo(index);
    		});
    
    		thumbnails.appendChild(thumbnail);
    	});
    
    	this.addEventListener("monster-slider-moved", (e) => {
    		const index = e.detail.index;
    		const thumbnail = thumbnails.children[index];
    
    		if (!thumbnail) {
    			return;
    		}
    
    		Array.from(thumbnails.children).forEach((thumb) => {
    			thumb.classList.remove("current");
    		});
    
    		thumbnail.classList.add("current");
    	});
    }
    
    /**
     * @private
     */
    function initAutoPlay() {
    	const self = this;
    	const autoPlay = this.getOption("autoPlay");
    	const delay = autoPlay.delay;
    	const startDelay = autoPlay.startDelay;
    	const direction = autoPlay.direction;
    
    	function start() {
    		if (self[configSymbol].autoPlayInterval) {
    			clearInterval(self[configSymbol].autoPlayInterval);
    		}
    
    		self[configSymbol].autoPlayInterval = setInterval(() => {
    			const { totalOriginSlides } = getSlidesAndTotal.call(self);
    
    			if (direction === "next") {
    				if (
    					!self.getOption("features.carousel") &&
    					self[configSymbol].currentIndex >= totalOriginSlides - 1
    				) {
    					self[configSymbol].currentIndex = -1;
    				}
    				self.next();
    			} else {
    				if (
    					!self.getOption("features.carousel") &&
    					self[configSymbol].currentIndex <= 0
    				) {
    					self[configSymbol].currentIndex = totalOriginSlides;
    				}
    				self.previous();
    			}
    		}, delay);
    	}
    
    	setTimeout(() => {
    		start();
    	}, startDelay);
    
    	if (autoPlay.mouseOverPause) {
    
    		if(this[configSymbol].eventHandler.mouseOverPause===null) {
    			this[configSymbol].eventHandler.mouseOverPause =  () => {
    				clearInterval(this[configSymbol].autoPlayInterval);
    			}
    
    			this.addEventListener("mouseover",this[configSymbol].eventHandler.mouseOverPause);
    		}
    
    		if(this[configSymbol].eventHandler.mouseout===null) {
    
    			this[configSymbol].eventHandler.mouseout = () => {
    				if (this[configSymbol].isDragging) {
    					return;
    				}
    				start();
    			}
    
    			this.addEventListener("mouseout", this[configSymbol].eventHandler.mouseout);
    		}
    
    	}
    
    	if (autoPlay.touchPause) {
    		if(this[configSymbol].eventHandler.touchstart===null) {
    			this[configSymbol].eventHandler.touchstart =  () => {
    				clearInterval(this[configSymbol].autoPlayInterval);
    			}
    
    			this.addEventListener("touchstart",this[configSymbol].eventHandler.touchstart);
    		}
    
    		if(this[configSymbol].eventHandler.touchend===null) {
    
    			this[configSymbol].eventHandler.touchend = () => {
    				if (this[configSymbol].isDragging) {
    					return;
    				}
    				start();
    			}
    
    			this.addEventListener("touchend", this[configSymbol].eventHandler.touchend);
    		}
    	}
    }
    
    function getVisibleSlidesFromContainerWidth() {
    	const containerWidth = this.shadowRoot.querySelector(
    		`[${ATTRIBUTE_ROLE}="slider"]`,
    	).offsetWidth;
    	const slides = this.getOption("slides");
    	let visibleSlides = 1;
    
    	if (!isObject(slides)) {
    		return visibleSlides;
    	}
    
    	for (const key in slides) {
    		if (containerWidth >= key) {
    			visibleSlides = slides[key];
    		}
    	}
    
    	const { originSlides } = getSlidesAndTotal.call(this);
    	if (visibleSlides > originSlides.length) {
    		visibleSlides = originSlides.length - 1;
    	}
    
    	return visibleSlides;
    }
    
    /**
     * @private
     */
    function initShadows() {
    	const { slides, totalSlides } = getSlidesAndTotal.call(this);
    	const slidesVisible = getVisibleSlidesFromContainerWidth.call(this);
    
    	if (totalSlides > slidesVisible) {
    		let current = slides[0];
    		let last = slides[totalSlides - 1];
    		for (let i = 0; i < slidesVisible; i++) {
    			const clone = current.cloneNode(true);
    			clone.setAttribute("data-monster-clone-from", i);
    			last.insertAdjacentElement("afterend", clone);
    			current = current.nextElementSibling;
    			last = clone;
    		}
    
    		current = slides[totalSlides - 1];
    		let first = slides[0];
    		for (let i = 0; i < slidesVisible; i++) {
    			const clone = current.cloneNode(true);
    			clone.setAttribute("data-monster-clone-from", totalSlides - i);
    			first.insertAdjacentElement("beforebegin", clone);
    			current = current.previousElementSibling;
    			first = clone;
    		}
    
    		moveTo.call(this, 0);
    	}
    }
    
    /**
     * @private
     * @return {{slides: unknown[], totalSlides: number}}
     */
    function getSlidesAndTotal() {
    	const originSlides = Array.from(
    		getSlottedElements.call(
    			this,
    			":scope:not([data-monster-clone-from])",
    			null,
    		),
    	);
    	const totalOriginSlides = originSlides.length;
    
    	const slides = Array.from(getSlottedElements.call(this, ":scope", null));
    	const totalSlides = slides.length;
    
    	return { originSlides, totalOriginSlides, slides, totalSlides };
    }
    
    /**
     * @private
     * @return {number}
     */
    function next() {
    	const nextIndex = this[configSymbol].currentIndex + 1;
    
    	queueMicrotask(() => {
    		getWindow().requestAnimationFrame(() => {
    			getWindow().requestAnimationFrame(() => {
    				moveTo.call(this, nextIndex);
    			});
    		});
    	});
    
    	return 0;
    }
    
    /**
     * @private
     * @return {number}
     */
    function prev() {
    	const prevIndex = this[configSymbol].currentIndex - 1;
    
    	queueMicrotask(() => {
    		getWindow().requestAnimationFrame(() => {
    			getWindow().requestAnimationFrame(() => {
    				moveTo.call(this, prevIndex);
    			});
    		});
    	});
    
    	return 0;
    }
    
    /**
     * @private
     * @param slides
     * @param index
     */
    function setMoveProperties(slides, index) {
    	slides.forEach((slide) => {
    		slide.classList.remove("current");
    	});
    
    	let offset = -(index * 100);
    	const slidesVisible = getVisibleSlidesFromContainerWidth.call(this);
    
    	offset = offset / slidesVisible;
    
    	if (offset !== 0) {
    		offset += "%";
    	}
    
    	this[sliderElementSymbol].style.transform =
    		`translateX(calc(${offset} + ${this[configSymbol].draggingPos}px))`;
    
    	if (slides[index]) {
    		slides[index].classList.add("current");
    	}
    	this[configSymbol].lastOffset = offset;
    }
    
    /**
     * @private
     * @param {number} index
     * @param {boolean} animation
     * @fires monster-slider-moved
     */
    function moveTo(index, animation) {
    	const { slides, totalSlides, originSlides, totalOriginSlides } =
    		getSlidesAndTotal.call(this);
    
    	if (animation === false) {
    		this[sliderElementSymbol].classList.remove("animate");
    	} else {
    		this[sliderElementSymbol].classList.add("animate");
    	}
    
    	if (this.getOption("features.carousel") === true) {
    		if (index < 0) {
    			index = -1;
    		}
    
    		if (index > totalOriginSlides) {
    			index = totalOriginSlides;
    		}
    	} else {
    		if (index < 0) {
    			index = 0;
    		}
    
    		if (index >= totalOriginSlides) {
    			index = totalOriginSlides - 1;
    		}
    	}
    
    	if (!isInteger(index)) {
    		return;
    	}
    
    	const visibleSlides = getVisibleSlidesFromContainerWidth.call(this);
    	if (totalOriginSlides <= visibleSlides) {
    		this[prevElementSymbol].classList.add("hidden");
    		this[nextElementSymbol].classList.add("hidden");
    		this[thumbnailElementSymbol].classList.add("hidden");
    		return;
    	}
    	this[prevElementSymbol].classList.remove("hidden");
    	this[nextElementSymbol].classList.remove("hidden");
    	this[thumbnailElementSymbol].classList.remove("hidden");
    
    	let slidesIndex = index + visibleSlides;
    	this[configSymbol].currentIndex = index;
    
    	if (slidesIndex < 0) {
    		slidesIndex = totalSlides - 1 - visibleSlides;
    		this[configSymbol].currentIndex = totalOriginSlides - 1;
    	} else if (index > totalOriginSlides) {
    		slidesIndex = 0;
    		this[configSymbol].currentIndex = 0;
    	}
    
    	setMoveProperties.call(this, slides, slidesIndex);
    
    	if (index === totalOriginSlides) {
    		setTimeout(() => {
    			getWindow().requestAnimationFrame(() => {
    				moveTo.call(this, 0, false);
    			});
    		}, this.getOption("carousel.transition"));
    	} else if (index === -1) {
    		setTimeout(() => {
    			getWindow().requestAnimationFrame(() => {
    				moveTo.call(this, totalOriginSlides - 1, false);
    			});
    		}, this.getOption("carousel.transition"));
    	}
    
    	fireCustomEvent(this, "monster-slider-moved", {
    		index: index,
    	});
    }
    
    /**
     * @private
     * @return {initEventHandler}
     * @fires monster-slider-resized
     */
    function initEventHandler() {
    	const self = this;
    
    	const nextElements = this[nextElementSymbol];
    
    	if (nextElements) {
    		nextElements.addEventListener("click", () => {
    			self.next();
    		});
    	}
    
    	const prevElements = this[prevElementSymbol];
    
    	if (prevElements) {
    		prevElements.addEventListener("click", () => {
    			self.previous();
    		});
    	}
    
    	if (this.getOption("features.drag")) {
    		this[sliderElementSymbol].addEventListener("mousedown", (e) =>
    			startDragging.call(this, e, "mouse"),
    		);
    
    		this[sliderElementSymbol].addEventListener("touchstart", (e) =>
    			startDragging.call(this, e, "touch"),
    		);
    	}
    
    
    	const initialSize = {
    		width: this[sliderElementSymbol]?.offsetWidth || 0,
    		height: this[sliderElementSymbol]?.offsetHeight || 0
    	};
    
    	const resizeObserver = new ResizeObserver(entries => {
    		for (let entry of entries) {
    			const {width, height} = entry.contentRect;
    			if (width !== initialSize.width || height !== initialSize.height) {
    
    				self.stopAutoPlay();
    
    				if (this.getOption("features.thumbnails")) {
    					initThumbnails.call(this);
    				}
    
    				const slidesVisible = getVisibleSlidesFromContainerWidth.call(this);
    				this[sliderElementSymbol].style.setProperty(
    					"--monster-slides-width",
    					`${100 / slidesVisible}%`,
    				);
    
    				moveTo.call(self,0,false)
    				self.startAutoPlay();
    
    				fireCustomEvent(self, "monster-slider-resized", {
    					width: width,
    					height: height
    				});
    			}
    		}
    	});
    
    	resizeObserver.observe(this[sliderElementSymbol]);
    
    	
    
    	return this;
    }
    
    /**
     * @private
     * @param e
     * @param type
     */
    function startDragging(e, type) {
    	const { slides } = getSlidesAndTotal.call(this);
    
    	const widthOfSlider = slides[this[configSymbol].currentIndex]?.offsetWidth;
    
    	this[configSymbol].isDragging = true;
    	this[configSymbol].startPos = getPositionX(e, type);
    	this[sliderElementSymbol].classList.add("grabbing");
    	this[sliderElementSymbol].style.transitionProperty = "none";
    
    	const callbackMousemove = (x) => {
    		dragging.call(this, x, type);
    	};
    
    	const callbackMouseUp = () => {
    		const endEvent = type === "mouse" ? "mouseup" : "touchend";
    		const moveEvent = type === "mouse" ? "mousemove" : "touchmove";
    
    		document.body.removeEventListener(endEvent, callbackMouseUp);
    		document.body.removeEventListener(moveEvent, callbackMousemove);
    
    		this[configSymbol].isDragging = false;
    		this[configSymbol].startPos = 0;
    		this[sliderElementSymbol].classList.remove("grabbing");
    		this[sliderElementSymbol].style.transitionProperty = "";
    
    		const lastPos = this[configSymbol].draggingPos;
    		this[configSymbol].draggingPos = 0;
    
    		let newIndex = this[configSymbol].currentIndex;
    		const shift = lastPos / widthOfSlider;
    		const shiftIndex = Math.round(shift);
    
    		newIndex += shiftIndex * -1;
    		this.moveTo(newIndex);
    	};
    
    	document.body.addEventListener("mouseup", callbackMouseUp);
    	document.body.addEventListener("mousemove", callbackMousemove);
    }
    
    /**
     * @private
     * @param e
     * @param type
     * @return {*|number|number}
     */
    function getPositionX(e, type) {
    	return type === "mouse" ? e.pageX : e.touches[0].clientX;
    }
    
    /**
     * @private
     * @param e
     * @param type
     */
    function dragging(e, type) {
    	if (!this[configSymbol].isDragging) return;
    	this[configSymbol].draggingPos =
    		getPositionX(e, type) - this[configSymbol].startPos;
    
    	this[sliderElementSymbol].style.transform =
    		`translateX(calc(${this[configSymbol].lastOffset} + ${this[configSymbol].draggingPos}px))`;
    }
    
    /**
     * @private
     * @return {void}
     */
    function initControlReferences() {
    	this[controlElementSymbol] = this.shadowRoot.querySelector(
    		`[${ATTRIBUTE_ROLE}="control"]`,
    	);
    
    	this[sliderElementSymbol] = this.shadowRoot.querySelector(
    		`[${ATTRIBUTE_ROLE}="slider"]`,
    	);
    
    	this[prevElementSymbol] = this.shadowRoot.querySelector(
    		`[${ATTRIBUTE_ROLE}="prev"]`,
    	);
    
    	this[nextElementSymbol] = this.shadowRoot.querySelector(
    		`[${ATTRIBUTE_ROLE}="next"]`,
    	);
    
    	this[thumbnailElementSymbol] = this.shadowRoot.querySelector(
    		`[${ATTRIBUTE_ROLE}="thumbnails"]`,
    	);
    
    }
    
    /**
     * @private
     * @return {string}
     */
    function getTemplate() {
    	// language=HTML
    	return `
            <div data-monster-role="control" part="control">
                <div class="prev" data-monster-role="prev" part="prev" part="prev">
                    <slot name="prev"></slot>
                </div>
                <div data-monster-role="slider" part="slides">
                    <slot></slot>
                </div>
                <div data-monster-role="thumbnails" part="thumbnails"></div>
                <div class="next" data-monster-role="next" part="next" part="next">
                    <slot name="next"></slot>
                </div>
            </div>`;
    }
    
    registerCustomElement(Slider);