Something went wrong on our end
Select Git revision
template.js
-
Volker Schukai authoredVolker Schukai authored
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);