blob: 81b063dd430c09c790711ae8eef3a35a40a0d170 [file] [edit]
/*!
* Bootstrap carousel.js v6.0.0-alpha1 (https://getbootstrap.com/)
* Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
import BaseComponent from "./base-component.js";
import EventHandler from "./dom/event-handler.js";
import Manipulator from "./dom/manipulator.js";
import SelectorEngine from "./dom/selector-engine.js";
import { isRTL, isVisible } from "./util/index.js";
//#region js/src/carousel.ts
/**
* --------------------------------------------------------------------------
* Bootstrap carousel.ts
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
/**
* Constants
*/
const NAME = "carousel";
const EVENT_KEY = `.bs.carousel`;
const DATA_API_KEY = ".data-api";
const ARROW_LEFT_KEY = "ArrowLeft";
const ARROW_RIGHT_KEY = "ArrowRight";
const DIRECTION_LEFT = "left";
const DIRECTION_RIGHT = "right";
const EVENT_SLIDE = `slide${EVENT_KEY}`;
const EVENT_SLID = `slid${EVENT_KEY}`;
const EVENT_KEYDOWN = `keydown${EVENT_KEY}`;
const EVENT_MOUSEENTER = `mouseenter${EVENT_KEY}`;
const EVENT_MOUSELEAVE = `mouseleave${EVENT_KEY}`;
const EVENT_POINTERDOWN = `pointerdown${EVENT_KEY}`;
const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`;
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`;
const CLASS_NAME_CAROUSEL = "carousel";
const CLASS_NAME_ACTIVE = "active";
const CLASS_NAME_FADE = "carousel-fade";
const CLASS_NAME_CENTER = "carousel-center";
const CLASS_NAME_AUTO = "carousel-auto";
const CLASS_NAME_CLONE = "carousel-item-clone";
const CLASS_NAME_PAUSED = "paused";
const CLASS_NAME_PLAYING = "carousel-playing";
const PROPERTY_INTERVAL = "--bs-carousel-interval";
const SCROLL_DURATION = 300;
const ACTIVE_RATIO_TOLERANCE = .05;
const SELECTOR_ACTIVE = ".active";
const SELECTOR_ITEM = `.carousel-item:not(.${CLASS_NAME_CLONE})`;
const SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM;
const SELECTOR_INNER = ".carousel-inner";
const SELECTOR_INDICATORS = ".carousel-indicators";
const SELECTOR_PLAY_PAUSE = ".carousel-control-play-pause";
const SELECTOR_DATA_SLIDE = "[data-bs-slide], [data-bs-slide-to]";
const SELECTOR_DATA_SLIDE_PREV = "[data-bs-slide=\"prev\"]";
const SELECTOR_DATA_SLIDE_NEXT = "[data-bs-slide=\"next\"]";
const SELECTOR_DATA_AUTOPLAY = "[data-bs-autoplay=\"true\"]";
const KEY_TO_DIRECTION = {
[ARROW_LEFT_KEY]: DIRECTION_RIGHT,
[ARROW_RIGHT_KEY]: DIRECTION_LEFT
};
const ENDS_STOP = "stop";
const ENDS_WRAP = "wrap";
const ENDS_LOOP = "loop";
const Default = {
autoplay: false,
ends: ENDS_LOOP,
interval: 5e3,
keyboard: true,
pause: "hover"
};
const DefaultType = {
autoplay: "boolean",
ends: "string",
interval: "number",
keyboard: "boolean",
pause: "(string|boolean)"
};
const easeInOutCubic = (progress) => progress < .5 ? 4 * progress * progress * progress : 1 - (-2 * progress + 2) ** 3 / 2;
/**
* Class definition
*/
var Carousel = class extends BaseComponent {
constructor(element, config) {
super(element, config);
this._viewport = SelectorEngine.findOne(SELECTOR_INNER, this._element) || this._element;
this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element);
this._playPauseElement = SelectorEngine.findOne(SELECTOR_PLAY_PAUSE, this._element);
this._prevControls = SelectorEngine.find(SELECTOR_DATA_SLIDE_PREV, this._element);
this._nextControls = SelectorEngine.find(SELECTOR_DATA_SLIDE_NEXT, this._element);
this._interval = null;
this._observer = null;
this._scrollFrame = null;
this._looping = false;
this._visibility = /* @__PURE__ */ new Map();
this._playing = this._config.autoplay;
this._activeIndex = this._initialActiveIndex();
this._addEventListeners();
this._observeItems();
this._refreshActiveState();
if (this._playing) this.cycle();
this._updatePlayPauseControl();
}
static get Default() {
return Default;
}
static get DefaultType() {
return DefaultType;
}
static get NAME() {
return NAME;
}
next() {
this.to(this._navIndex() + 1);
}
nextWhenVisible() {
if (document.visibilityState === "visible" && isVisible(this._element)) this.next();
}
prev() {
this.to(this._navIndex() - 1);
}
pause() {
this._clearInterval();
this._element.classList.remove(CLASS_NAME_PLAYING);
}
cycle() {
this._clearInterval();
this._scheduleAutoplay();
this._element.classList.add(CLASS_NAME_PLAYING);
}
to(index) {
if (this._looping) return;
const items = this._getItems();
const rawIndex = Number.parseInt(index, 10);
if (this._config.ends === ENDS_LOOP && !this._prefersReducedMotion() && this._canLoop()) {
if (rawIndex > items.length - 1) {
this._loopTransition(true);
return;
}
if (rawIndex < 0) {
this._loopTransition(false);
return;
}
}
const targetIndex = this._normalizeIndex(rawIndex, items.length);
const currentIndex = this._navIndex();
if (targetIndex === null || targetIndex === currentIndex) return;
if (EventHandler.trigger(this._element, EVENT_SLIDE, {
relatedTarget: items[targetIndex],
direction: this._direction(currentIndex, targetIndex),
from: currentIndex,
to: targetIndex
}).defaultPrevented) return;
if (this._isFade()) {
this._fadeTo(targetIndex);
return;
}
this._scrollToIndex(targetIndex);
}
dispose() {
this._clearInterval();
if (this._observer) this._observer.disconnect();
if (this._scrollFrame !== null) cancelAnimationFrame(this._scrollFrame);
for (const clone of SelectorEngine.find(`.${CLASS_NAME_CLONE}`, this._viewport)) clone.remove();
this._viewport.style.scrollSnapType = "";
EventHandler.off(this._viewport, EVENT_KEY);
super.dispose();
}
_configAfterMerge(config) {
if (![
ENDS_STOP,
ENDS_WRAP,
ENDS_LOOP
].includes(config.ends)) config.ends = Default.ends;
return config;
}
_initialActiveIndex() {
const active = SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element);
const index = active ? this._getItems().indexOf(active) : 0;
return Math.max(index, 0);
}
_addEventListeners() {
if (this._config.keyboard) EventHandler.on(this._element, EVENT_KEYDOWN, (event) => this._keydown(event));
if (this._config.pause === "hover") {
EventHandler.on(this._element, EVENT_MOUSEENTER, () => this.pause());
EventHandler.on(this._element, EVENT_MOUSELEAVE, () => this._maybeEnableCycle());
}
EventHandler.on(this._viewport, EVENT_POINTERDOWN, () => this._pauseFromInteraction());
}
_keydown(event) {
if (/input|textarea/i.test(event.target.tagName)) return;
const direction = KEY_TO_DIRECTION[event.key];
if (direction) {
event.preventDefault();
this._pauseFromInteraction();
if (direction === DIRECTION_RIGHT) this.prev();
else this.next();
}
}
_observeItems() {
if (this._isFade() || typeof IntersectionObserver === "undefined") return;
this._observer = new IntersectionObserver((entries) => this._handleIntersection(entries), {
root: this._viewport,
threshold: [
0,
.25,
.5,
.75,
1
]
});
for (const item of this._getItems()) this._observer.observe(item);
}
_handleIntersection(entries) {
if (this._looping) return;
for (const entry of entries) this._visibility.set(entry.target, entry.isIntersecting ? entry.intersectionRatio : 0);
const ratios = this._getItems().map((item) => this._visibility.get(item) ?? 0);
const maxRatio = Math.max(...ratios);
let bestIndex = this._activeIndex;
if (maxRatio > 0) bestIndex = ratios.findIndex((ratio) => ratio >= maxRatio - ACTIVE_RATIO_TOLERANCE);
this._setActive(bestIndex);
this._updateEndControls();
}
_navIndex() {
if (this._isFade() || this._viewport.scrollWidth - this._viewport.clientWidth <= 0) return this._activeIndex;
let index = this._activeIndex;
let smallestDelta = Number.POSITIVE_INFINITY;
for (const [itemIndex, item] of this._getItems().entries()) {
const delta = Math.abs(this._scrollDelta(item));
if (delta < smallestDelta) {
smallestDelta = delta;
index = itemIndex;
}
}
return index;
}
_scrollToIndex(index) {
const item = this._getItems()[index];
if (!item) return;
const left = this._scrollDelta(item);
if (Math.abs(left) < 1) return;
const targetLeft = this._viewport.scrollLeft + left;
this._viewport.style.scrollSnapType = "none";
this._animateScroll(targetLeft, () => {
this._viewport.style.scrollSnapType = "";
if (!this._observer) this._setActive(index);
this._updateEndControls();
});
}
_animateScroll(targetLeft, onComplete) {
if (this._scrollFrame !== null) {
cancelAnimationFrame(this._scrollFrame);
this._scrollFrame = null;
}
const startLeft = this._viewport.scrollLeft;
const distance = targetLeft - startLeft;
if (this._prefersReducedMotion() || typeof requestAnimationFrame === "undefined") {
this._viewport.scrollTo({
left: targetLeft,
behavior: "instant"
});
onComplete();
return;
}
let startTime = null;
const step = (now) => {
if (startTime === null) startTime = now;
const progress = Math.min((now - startTime) / SCROLL_DURATION, 1);
this._viewport.scrollTo({
left: startLeft + distance * easeInOutCubic(progress),
behavior: "instant"
});
if (progress < 1) {
this._scrollFrame = requestAnimationFrame(step);
return;
}
this._viewport.scrollTo({
left: targetLeft,
behavior: "instant"
});
this._scrollFrame = null;
onComplete();
};
this._scrollFrame = requestAnimationFrame(step);
}
_scrollDelta(element) {
const viewportRect = this._viewport.getBoundingClientRect();
const rect = element.getBoundingClientRect();
if (this._element.classList.contains(CLASS_NAME_CENTER)) return rect.left + rect.width / 2 - (viewportRect.left + viewportRect.width / 2);
const padStart = Number.parseFloat(getComputedStyle(this._viewport).scrollPaddingInlineStart) || 0;
return isRTL() ? rect.right - (viewportRect.right - padStart) : rect.left - (viewportRect.left + padStart);
}
_loopTransition(isNext) {
const items = this._getItems();
const last = items.length - 1;
const fromIndex = this._activeIndex;
const toIndex = isNext ? 0 : last;
const direction = this._loopDirection(isNext);
if (EventHandler.trigger(this._element, EVENT_SLIDE, {
relatedTarget: items[toIndex],
direction,
from: fromIndex,
to: toIndex
}).defaultPrevented) return;
this._looping = true;
const clone = (isNext ? items[0] : items[last]).cloneNode(true);
clone.classList.add(CLASS_NAME_CLONE);
clone.classList.remove(CLASS_NAME_ACTIVE);
clone.removeAttribute("id");
for (const node of SelectorEngine.find("[id]", clone)) node.removeAttribute("id");
clone.setAttribute("aria-hidden", "true");
clone.inert = true;
this._viewport.style.scrollSnapType = "none";
if (isNext) this._viewport.append(clone);
else {
this._viewport.prepend(clone);
this._jumpScroll(this._scrollDelta(items[fromIndex]));
}
this._animateScroll(this._viewport.scrollLeft + this._scrollDelta(clone), () => {
clone.remove();
this._jumpScroll(this._scrollDelta(items[toIndex]));
this._activeIndex = toIndex;
this._refreshActiveState();
EventHandler.trigger(this._element, EVENT_SLID, {
relatedTarget: items[toIndex],
direction,
from: fromIndex,
to: toIndex
});
this._viewport.style.scrollSnapType = "";
this._looping = false;
});
}
_loopDirection(isNext) {
if (isRTL()) return isNext ? DIRECTION_RIGHT : DIRECTION_LEFT;
return isNext ? DIRECTION_LEFT : DIRECTION_RIGHT;
}
_jumpScroll(delta) {
this._viewport.style.scrollSnapType = "none";
this._viewport.scrollBy({
left: delta,
top: 0,
behavior: "instant"
});
}
_fadeTo(index) {
this._setActive(index);
}
_setActive(index) {
const items = this._getItems();
if (index === this._activeIndex || !items[index]) return;
const from = this._activeIndex;
this._activeIndex = index;
this._refreshActiveState();
EventHandler.trigger(this._element, EVENT_SLID, {
relatedTarget: items[index],
direction: this._direction(from, index),
from,
to: index
});
}
_refreshActiveState() {
const items = this._getItems();
for (const [index, item] of items.entries()) item.classList.toggle(CLASS_NAME_ACTIVE, index === this._activeIndex);
this._setActiveIndicatorElement(this._activeIndex);
this._updateEndControls();
}
_updateEndControls() {
if (this._config.ends !== ENDS_STOP) return;
const viewport = this._viewport;
const maxScroll = viewport.scrollWidth - viewport.clientWidth;
let atStart;
let atEnd;
if (maxScroll > 0) {
const progress = Math.abs(viewport.scrollLeft);
atStart = progress <= 1;
atEnd = progress >= maxScroll - 1;
} else {
const last = this._getItems().length - 1;
atStart = this._activeIndex <= 0;
atEnd = this._activeIndex >= last;
}
this._setControlsDisabled(this._prevControls, atStart);
this._setControlsDisabled(this._nextControls, atEnd);
}
_setControlsDisabled(controls, disabled) {
for (const control of controls) {
if (disabled && control === document.activeElement) ((controls === this._prevControls ? this._nextControls : this._prevControls)[0] ?? this._viewport).focus({ preventScroll: true });
control.disabled = disabled;
}
}
_setActiveIndicatorElement(index) {
if (!this._indicatorsElement) return;
const active = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement);
if (active) {
active.classList.remove(CLASS_NAME_ACTIVE);
active.removeAttribute("aria-current");
}
const newActive = SelectorEngine.findOne(`[data-bs-slide-to="${index}"]`, this._indicatorsElement);
if (newActive) {
newActive.classList.add(CLASS_NAME_ACTIVE);
newActive.setAttribute("aria-current", "true");
}
}
_normalizeIndex(index, length) {
if (Number.isNaN(index) || length === 0) return null;
if (index < 0) return this._wrapsAround() ? length - 1 : null;
if (index > length - 1) return this._wrapsAround() ? 0 : null;
return index;
}
_wrapsAround() {
return this._config.ends === ENDS_WRAP || this._config.ends === ENDS_LOOP;
}
_canLoop() {
if (this._isFade() || this._getItems().length < 2) return false;
const styles = getComputedStyle(this._element);
const num = (name) => Number.parseFloat(styles.getPropertyValue(name)) || 0;
return (num("--bs-carousel-items") || 1) === 1 && num("--bs-carousel-items-peek") === 0 && !this._element.classList.contains(CLASS_NAME_CENTER) && !this._element.classList.contains(CLASS_NAME_AUTO);
}
_direction(from, to) {
const isNext = to > from;
if (isRTL()) return isNext ? DIRECTION_RIGHT : DIRECTION_LEFT;
return isNext ? DIRECTION_LEFT : DIRECTION_RIGHT;
}
_scheduleAutoplay(index = this._activeIndex) {
const interval = this._itemInterval(index);
this._element.style.setProperty(PROPERTY_INTERVAL, `${interval}ms`);
this._interval = setTimeout(() => {
const upcoming = this._upcomingIndex();
this.nextWhenVisible();
if (upcoming === null) {
this.pause();
return;
}
this._scheduleAutoplay(upcoming);
}, interval);
}
_upcomingIndex() {
return this._normalizeIndex(this._navIndex() + 1, this._getItems().length);
}
_itemInterval(index = this._activeIndex) {
const item = this._getItems()[index];
const interval = item ? Number.parseInt(item.getAttribute("data-bs-interval"), 10) : NaN;
return Number.isNaN(interval) ? this._config.interval : interval;
}
_maybeEnableCycle() {
if (!this._playing) return;
this.cycle();
}
_pauseFromInteraction() {
this._playing = false;
this.pause();
this._updatePlayPauseControl();
}
_togglePlayPause() {
if (this._playing) {
this._pauseFromInteraction();
return;
}
this._playing = true;
this.cycle();
this._updatePlayPauseControl();
}
_updatePlayPauseControl() {
if (!this._playPauseElement) return;
this._playPauseElement.classList.toggle(CLASS_NAME_PAUSED, !this._playing);
const label = this._playPauseElement.getAttribute(this._playing ? "data-bs-pause-label" : "data-bs-play-label");
if (label) this._playPauseElement.setAttribute("aria-label", label);
}
_isFade() {
return this._element.classList.contains(CLASS_NAME_FADE);
}
_prefersReducedMotion() {
return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}
_getItems() {
return SelectorEngine.find(SELECTOR_ITEM, this._element);
}
_clearInterval() {
if (this._interval) {
clearTimeout(this._interval);
this._interval = null;
}
}
};
/**
* Data API implementation
*/
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, function(event) {
const target = SelectorEngine.getElementFromSelector(this);
if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) return;
event.preventDefault();
const carousel = Carousel.getOrCreateInstance(target);
carousel._pauseFromInteraction();
const slideIndex = this.getAttribute("data-bs-slide-to");
if (slideIndex) {
carousel.to(slideIndex);
return;
}
if (Manipulator.getDataAttribute(this, "slide") === "next") {
carousel.next();
return;
}
carousel.prev();
});
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_PLAY_PAUSE, function(event) {
const target = SelectorEngine.getElementFromSelector(this);
if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) return;
event.preventDefault();
Carousel.getOrCreateInstance(target)._togglePlayPause();
});
EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
const carousels = SelectorEngine.find(SELECTOR_DATA_AUTOPLAY);
for (const carousel of carousels) Carousel.getOrCreateInstance(carousel);
});
//#endregion
export { Carousel as default };
//# sourceMappingURL=carousel.js.map