2020-06-01 18:58:38 +02:00
|
|
|
/*!
|
2021-06-19 19:22:19 +02:00
|
|
|
* Native JavaScript for Bootstrap v4.0.2 (https://thednp.github.io/bootstrap.native/)
|
2021-01-19 17:55:21 +01:00
|
|
|
* Copyright 2015-2021 © dnp_theme
|
2020-06-01 18:58:38 +02:00
|
|
|
* Licensed under MIT (https://github.com/thednp/bootstrap.native/blob/master/LICENSE)
|
|
|
|
*/
|
2021-06-19 19:22:19 +02:00
|
|
|
(function (global, factory) {
|
2020-06-01 18:58:38 +02:00
|
|
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
|
|
|
typeof define === 'function' && define.amd ? define(factory) :
|
2021-06-19 19:22:19 +02:00
|
|
|
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.BSN = factory());
|
2020-06-01 18:58:38 +02:00
|
|
|
}(this, (function () { 'use strict';
|
|
|
|
|
2021-06-19 19:22:19 +02:00
|
|
|
const transitionEndEvent = 'webkitTransition' in document.head.style ? 'webkitTransitionEnd' : 'transitionend';
|
2020-06-01 18:58:38 +02:00
|
|
|
|
2021-06-19 19:22:19 +02:00
|
|
|
const supportTransition = 'webkitTransition' in document.head.style || 'transition' in document.head.style;
|
2020-06-01 18:58:38 +02:00
|
|
|
|
2021-06-19 19:22:19 +02:00
|
|
|
const transitionDuration = 'webkitTransition' in document.head.style ? 'webkitTransitionDuration' : 'transitionDuration';
|
2020-06-01 18:58:38 +02:00
|
|
|
|
2021-06-19 19:22:19 +02:00
|
|
|
const transitionProperty = 'webkitTransition' in document.head.style ? 'webkitTransitionProperty' : 'transitionProperty';
|
2021-01-19 17:55:21 +01:00
|
|
|
|
2020-08-31 16:40:21 +02:00
|
|
|
function getElementTransitionDuration(element) {
|
2021-06-19 19:22:19 +02:00
|
|
|
const computedStyle = getComputedStyle(element);
|
|
|
|
const propertyValue = computedStyle[transitionProperty];
|
|
|
|
const durationValue = computedStyle[transitionDuration];
|
|
|
|
const durationScale = durationValue.includes('ms') ? 1 : 1000;
|
|
|
|
const duration = supportTransition && propertyValue && propertyValue !== 'none'
|
|
|
|
? parseFloat(durationValue) * durationScale : 0;
|
|
|
|
|
|
|
|
return !Number.isNaN(duration) ? duration : 0;
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
|
|
|
|
2021-06-19 19:22:19 +02:00
|
|
|
function emulateTransitionEnd(element, handler) {
|
|
|
|
let called = 0;
|
|
|
|
const endEvent = new Event(transitionEndEvent);
|
|
|
|
const duration = getElementTransitionDuration(element);
|
|
|
|
|
|
|
|
if (duration) {
|
|
|
|
element.addEventListener(transitionEndEvent, function transitionEndWrapper(e) {
|
|
|
|
if (e.target === element) {
|
|
|
|
handler.apply(element, [e]);
|
|
|
|
element.removeEventListener(transitionEndEvent, transitionEndWrapper);
|
|
|
|
called = 1;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
setTimeout(() => {
|
|
|
|
if (!called) element.dispatchEvent(endEvent);
|
|
|
|
}, duration + 17);
|
|
|
|
} else {
|
|
|
|
handler.apply(element, [endEvent]);
|
|
|
|
}
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
|
|
|
|
2020-08-31 16:40:21 +02:00
|
|
|
function queryElement(selector, parent) {
|
2021-06-19 19:22:19 +02:00
|
|
|
const lookUp = parent && parent instanceof Element ? parent : document;
|
2020-06-01 18:58:38 +02:00
|
|
|
return selector instanceof Element ? selector : lookUp.querySelector(selector);
|
|
|
|
}
|
|
|
|
|
2021-06-19 19:22:19 +02:00
|
|
|
function hasClass(element, classNAME) {
|
|
|
|
return element.classList.contains(classNAME);
|
|
|
|
}
|
|
|
|
|
|
|
|
function removeClass(element, classNAME) {
|
|
|
|
element.classList.remove(classNAME);
|
|
|
|
}
|
|
|
|
|
|
|
|
const addEventListener = 'addEventListener';
|
|
|
|
|
|
|
|
const removeEventListener = 'removeEventListener';
|
|
|
|
|
|
|
|
const fadeClass = 'fade';
|
|
|
|
|
|
|
|
const showClass = 'show';
|
|
|
|
|
|
|
|
const dataBsDismiss = 'data-bs-dismiss';
|
|
|
|
|
|
|
|
function bootstrapCustomEvent(namespacedEventType, eventProperties) {
|
|
|
|
const OriginalCustomEvent = new CustomEvent(namespacedEventType, { cancelable: true });
|
|
|
|
|
|
|
|
if (eventProperties instanceof Object) {
|
|
|
|
Object.keys(eventProperties).forEach((key) => {
|
2021-01-19 17:55:21 +01:00
|
|
|
Object.defineProperty(OriginalCustomEvent, key, {
|
2021-06-19 19:22:19 +02:00
|
|
|
value: eventProperties[key],
|
2021-01-19 17:55:21 +01:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
2020-06-01 18:58:38 +02:00
|
|
|
return OriginalCustomEvent;
|
|
|
|
}
|
2020-08-31 16:40:21 +02:00
|
|
|
|
2021-06-19 19:22:19 +02:00
|
|
|
function normalizeValue(value) {
|
|
|
|
if (value === 'true') {
|
|
|
|
return true;
|
2020-08-31 16:40:21 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
|
|
|
|
if (value === 'false') {
|
|
|
|
return false;
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
|
|
|
|
if (!Number.isNaN(+value)) {
|
|
|
|
return +value;
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
|
|
|
|
if (value === '' || value === 'null') {
|
|
|
|
return null;
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
|
|
|
|
// string / function / Element / Object
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
|
|
|
function normalizeOptions(element, defaultOps, inputOps, ns) {
|
|
|
|
const normalOps = {};
|
|
|
|
const dataOps = {};
|
|
|
|
const data = { ...element.dataset };
|
|
|
|
|
|
|
|
Object.keys(data)
|
|
|
|
.forEach((k) => {
|
|
|
|
const key = k.includes(ns)
|
|
|
|
? k.replace(ns, '').replace(/[A-Z]/, (match) => match.toLowerCase())
|
|
|
|
: k;
|
|
|
|
|
|
|
|
dataOps[key] = normalizeValue(data[k]);
|
|
|
|
});
|
|
|
|
|
|
|
|
Object.keys(inputOps)
|
|
|
|
.forEach((k) => {
|
|
|
|
inputOps[k] = normalizeValue(inputOps[k]);
|
|
|
|
});
|
|
|
|
|
|
|
|
Object.keys(defaultOps)
|
|
|
|
.forEach((k) => {
|
|
|
|
if (k in inputOps) {
|
|
|
|
normalOps[k] = inputOps[k];
|
|
|
|
} else if (k in dataOps) {
|
|
|
|
normalOps[k] = dataOps[k];
|
2019-08-31 17:47:52 +02:00
|
|
|
} else {
|
2021-06-19 19:22:19 +02:00
|
|
|
normalOps[k] = defaultOps[k];
|
2019-08-31 17:47:52 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
return normalOps;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Native JavaScript for Bootstrap 5 | Base Component
|
|
|
|
----------------------------------------------------- */
|
|
|
|
|
|
|
|
class BaseComponent {
|
|
|
|
constructor(name, target, defaults, config) {
|
|
|
|
const self = this;
|
|
|
|
const element = queryElement(target);
|
|
|
|
|
|
|
|
if (element[name]) element[name].dispose();
|
|
|
|
self.element = element;
|
|
|
|
|
|
|
|
if (defaults && Object.keys(defaults).length) {
|
|
|
|
self.options = normalizeOptions(element, defaults, (config || {}), 'bs');
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
element[name] = self;
|
|
|
|
}
|
|
|
|
|
|
|
|
dispose(name) {
|
|
|
|
const self = this;
|
|
|
|
self.element[name] = null;
|
|
|
|
Object.keys(self).forEach((prop) => { self[prop] = null; });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Native JavaScript for Bootstrap 5 | Alert
|
|
|
|
-------------------------------------------- */
|
|
|
|
|
|
|
|
// ALERT PRIVATE GC
|
|
|
|
// ================
|
|
|
|
const alertString = 'alert';
|
|
|
|
const alertComponent = 'Alert';
|
|
|
|
const alertSelector = `.${alertString}`;
|
|
|
|
const alertDismissSelector = `[${dataBsDismiss}="${alertString}"]`;
|
|
|
|
|
|
|
|
// ALERT CUSTOM EVENTS
|
|
|
|
// ===================
|
|
|
|
const closeAlertEvent = bootstrapCustomEvent(`close.bs.${alertString}`);
|
|
|
|
const closedAlertEvent = bootstrapCustomEvent(`closed.bs.${alertString}`);
|
|
|
|
|
|
|
|
// ALERT EVENT HANDLERS
|
|
|
|
// ====================
|
|
|
|
function alertTransitionEnd(self) {
|
|
|
|
const { element, relatedTarget } = self;
|
|
|
|
toggleAlertHandler(self);
|
|
|
|
|
|
|
|
if (relatedTarget) closedAlertEvent.relatedTarget = relatedTarget;
|
|
|
|
element.dispatchEvent(closedAlertEvent);
|
|
|
|
|
|
|
|
self.dispose();
|
|
|
|
element.parentNode.removeChild(element);
|
|
|
|
}
|
|
|
|
|
|
|
|
// ALERT PRIVATE METHOD
|
|
|
|
// ====================
|
|
|
|
function toggleAlertHandler(self, add) {
|
|
|
|
const action = add ? addEventListener : removeEventListener;
|
|
|
|
if (self.dismiss) self.dismiss[action]('click', self.close);
|
|
|
|
}
|
|
|
|
|
|
|
|
// ALERT DEFINITION
|
|
|
|
// ================
|
|
|
|
class Alert extends BaseComponent {
|
|
|
|
constructor(target) {
|
|
|
|
super(alertComponent, target);
|
|
|
|
// bind
|
|
|
|
const self = this;
|
|
|
|
|
|
|
|
// initialization element
|
|
|
|
const { element } = self;
|
|
|
|
|
|
|
|
// the dismiss button
|
|
|
|
self.dismiss = queryElement(alertDismissSelector, element);
|
|
|
|
self.relatedTarget = null;
|
|
|
|
|
|
|
|
// add event listener
|
|
|
|
toggleAlertHandler(self, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
// ALERT PUBLIC METHODS
|
|
|
|
// ====================
|
|
|
|
close(e) {
|
|
|
|
const target = e ? e.target : null;
|
|
|
|
const self = e
|
|
|
|
? e.target.closest(alertSelector)[alertComponent]
|
|
|
|
: this;
|
|
|
|
const { element } = self;
|
|
|
|
|
|
|
|
if (self && element && hasClass(element, showClass)) {
|
|
|
|
if (target) {
|
|
|
|
closeAlertEvent.relatedTarget = target;
|
|
|
|
self.relatedTarget = target;
|
2019-08-31 17:47:52 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
element.dispatchEvent(closeAlertEvent);
|
|
|
|
if (closeAlertEvent.defaultPrevented) return;
|
|
|
|
|
|
|
|
removeClass(element, showClass);
|
|
|
|
|
|
|
|
if (hasClass(element, fadeClass)) {
|
|
|
|
emulateTransitionEnd(element, () => alertTransitionEnd(self));
|
|
|
|
} else alertTransitionEnd(self);
|
2019-08-31 17:47:52 +02:00
|
|
|
}
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
|
|
|
|
dispose() {
|
|
|
|
toggleAlertHandler(this);
|
|
|
|
super.dispose(alertComponent);
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
Alert.init = {
|
|
|
|
component: alertComponent,
|
|
|
|
selector: alertSelector,
|
|
|
|
constructor: Alert,
|
|
|
|
};
|
|
|
|
|
|
|
|
function addClass(element, classNAME) {
|
|
|
|
element.classList.add(classNAME);
|
|
|
|
}
|
|
|
|
|
|
|
|
const activeClass = 'active';
|
|
|
|
|
|
|
|
const dataBsToggle = 'data-bs-toggle';
|
|
|
|
|
|
|
|
/* Native JavaScript for Bootstrap 5 | Button
|
|
|
|
---------------------------------------------*/
|
|
|
|
|
|
|
|
// BUTTON PRIVATE GC
|
|
|
|
// =================
|
|
|
|
const buttonString = 'button';
|
|
|
|
const buttonComponent = 'Button';
|
|
|
|
const buttonSelector = `[${dataBsToggle}="${buttonString}"]`;
|
|
|
|
const ariaPressed = 'aria-pressed';
|
|
|
|
|
|
|
|
// BUTTON PRIVATE METHOD
|
|
|
|
// =====================
|
|
|
|
function toggleButtonHandler(self, add) {
|
|
|
|
const action = add ? addEventListener : removeEventListener;
|
|
|
|
self.element[action]('click', self.toggle);
|
|
|
|
}
|
|
|
|
|
|
|
|
// BUTTON DEFINITION
|
|
|
|
// =================
|
|
|
|
class Button extends BaseComponent {
|
|
|
|
constructor(target) {
|
|
|
|
super(buttonComponent, target);
|
|
|
|
const self = this;
|
|
|
|
|
|
|
|
// initialization element
|
|
|
|
const { element } = self;
|
|
|
|
|
|
|
|
// set initial state
|
|
|
|
self.isActive = hasClass(element, activeClass);
|
|
|
|
element.setAttribute(ariaPressed, !!self.isActive);
|
|
|
|
|
|
|
|
// add event listener
|
|
|
|
toggleButtonHandler(self, 1);
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
|
|
|
|
// BUTTON PUBLIC METHODS
|
|
|
|
// =====================
|
|
|
|
toggle(e) {
|
|
|
|
if (e) e.preventDefault();
|
|
|
|
const self = e ? this[buttonComponent] : this;
|
|
|
|
const { element } = self;
|
|
|
|
|
|
|
|
if (hasClass(element, 'disabled')) return;
|
|
|
|
|
|
|
|
self.isActive = hasClass(element, activeClass);
|
|
|
|
const { isActive } = self;
|
|
|
|
|
|
|
|
const action = isActive ? removeClass : addClass;
|
|
|
|
const ariaValue = isActive ? 'false' : 'true';
|
|
|
|
|
|
|
|
action(element, activeClass);
|
|
|
|
element.setAttribute(ariaPressed, ariaValue);
|
2019-08-31 17:47:52 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
|
|
|
|
dispose() {
|
|
|
|
toggleButtonHandler(this);
|
|
|
|
super.dispose(buttonComponent);
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-19 19:22:19 +02:00
|
|
|
Button.init = {
|
|
|
|
component: buttonComponent,
|
|
|
|
selector: buttonSelector,
|
|
|
|
constructor: Button,
|
|
|
|
};
|
2020-06-01 18:58:38 +02:00
|
|
|
|
2021-06-19 19:22:19 +02:00
|
|
|
const supportPassive = (() => {
|
|
|
|
let result = false;
|
2020-06-01 18:58:38 +02:00
|
|
|
try {
|
2021-06-19 19:22:19 +02:00
|
|
|
const opts = Object.defineProperty({}, 'passive', {
|
|
|
|
get() {
|
2020-06-01 18:58:38 +02:00
|
|
|
result = true;
|
2021-06-19 19:22:19 +02:00
|
|
|
return result;
|
|
|
|
},
|
2020-06-01 18:58:38 +02:00
|
|
|
});
|
2021-06-19 19:22:19 +02:00
|
|
|
document[addEventListener]('DOMContentLoaded', function wrap() {
|
|
|
|
document[removeEventListener]('DOMContentLoaded', wrap, opts);
|
2020-08-31 16:40:21 +02:00
|
|
|
}, opts);
|
2021-06-19 19:22:19 +02:00
|
|
|
} catch (e) {
|
|
|
|
throw Error('Passive events are not supported');
|
|
|
|
}
|
|
|
|
|
2020-06-01 18:58:38 +02:00
|
|
|
return result;
|
|
|
|
})();
|
|
|
|
|
2021-06-19 19:22:19 +02:00
|
|
|
// general event options
|
|
|
|
|
2020-06-01 18:58:38 +02:00
|
|
|
var passiveHandler = supportPassive ? { passive: true } : false;
|
|
|
|
|
2021-06-19 19:22:19 +02:00
|
|
|
function reflow(element) {
|
|
|
|
return element.offsetHeight;
|
|
|
|
}
|
|
|
|
|
2020-06-01 18:58:38 +02:00
|
|
|
function isElementInScrollRange(element) {
|
2021-06-19 19:22:19 +02:00
|
|
|
const bcr = element.getBoundingClientRect();
|
|
|
|
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
|
|
|
|
return bcr.top <= viewportHeight && bcr.bottom >= 0; // bottom && top
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Native JavaScript for Bootstrap 5 | Carousel
|
|
|
|
----------------------------------------------- */
|
|
|
|
|
|
|
|
// CAROUSEL PRIVATE GC
|
|
|
|
// ===================
|
|
|
|
const carouselString = 'carousel';
|
|
|
|
const carouselComponent = 'Carousel';
|
|
|
|
const carouselSelector = `[data-bs-ride="${carouselString}"]`;
|
|
|
|
const carouselControl = `${carouselString}-control`;
|
|
|
|
const carouselItem = `${carouselString}-item`;
|
|
|
|
const dataBsSlideTo = 'data-bs-slide-to';
|
|
|
|
const pausedClass = 'paused';
|
|
|
|
const defaultCarouselOptions = {
|
|
|
|
pause: 'hover', // 'boolean|string'
|
|
|
|
keyboard: false, // 'boolean'
|
|
|
|
touch: true, // 'boolean'
|
|
|
|
interval: 5000, // 'boolean|number'
|
|
|
|
};
|
|
|
|
let startX = 0;
|
|
|
|
let currentX = 0;
|
|
|
|
let endX = 0;
|
|
|
|
|
|
|
|
// CAROUSEL CUSTOM EVENTS
|
|
|
|
// ======================
|
|
|
|
const carouselSlideEvent = bootstrapCustomEvent(`slide.bs.${carouselString}`);
|
|
|
|
const carouselSlidEvent = bootstrapCustomEvent(`slid.bs.${carouselString}`);
|
|
|
|
|
|
|
|
// CAROUSEL EVENT HANDLERS
|
|
|
|
// =======================
|
|
|
|
function carouselTransitionEndHandler(self) {
|
|
|
|
const {
|
|
|
|
index, direction, element, slides, options, isAnimating,
|
|
|
|
} = self;
|
|
|
|
|
|
|
|
// discontinue disposed instances
|
|
|
|
if (isAnimating && element[carouselComponent]) {
|
|
|
|
const activeItem = getActiveIndex(self);
|
|
|
|
const orientation = direction === 'left' ? 'next' : 'prev';
|
|
|
|
const directionClass = direction === 'left' ? 'start' : 'end';
|
|
|
|
self.isAnimating = false;
|
|
|
|
|
|
|
|
addClass(slides[index], activeClass);
|
|
|
|
removeClass(slides[activeItem], activeClass);
|
|
|
|
|
|
|
|
removeClass(slides[index], `${carouselItem}-${orientation}`);
|
|
|
|
removeClass(slides[index], `${carouselItem}-${directionClass}`);
|
|
|
|
removeClass(slides[activeItem], `${carouselItem}-${directionClass}`);
|
|
|
|
|
|
|
|
element.dispatchEvent(carouselSlidEvent);
|
|
|
|
|
|
|
|
// check for element, might have been disposed
|
|
|
|
if (!document.hidden && options.interval
|
|
|
|
&& !hasClass(element, pausedClass)) {
|
|
|
|
self.cycle();
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function carouselPauseHandler(e) {
|
|
|
|
const eventTarget = e.target;
|
|
|
|
const self = eventTarget.closest(carouselSelector)[carouselComponent];
|
|
|
|
const { element, isAnimating } = self;
|
|
|
|
|
|
|
|
if (!hasClass(element, pausedClass)) {
|
|
|
|
addClass(element, pausedClass);
|
|
|
|
if (!isAnimating) {
|
|
|
|
clearInterval(self.timer);
|
|
|
|
self.timer = null;
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function carouselResumeHandler(e) {
|
|
|
|
const eventTarget = e.target;
|
|
|
|
const self = eventTarget.closest(carouselSelector)[carouselComponent];
|
|
|
|
const { isPaused, isAnimating, element } = self;
|
|
|
|
|
|
|
|
if (!isPaused && hasClass(element, pausedClass)) {
|
|
|
|
removeClass(element, pausedClass);
|
|
|
|
|
|
|
|
if (!isAnimating) {
|
|
|
|
clearInterval(self.timer);
|
|
|
|
self.timer = null;
|
|
|
|
self.cycle();
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function carouselIndicatorHandler(e) {
|
|
|
|
e.preventDefault();
|
|
|
|
const { target } = e;
|
|
|
|
const self = target.closest(carouselSelector)[carouselComponent];
|
|
|
|
|
|
|
|
if (self.isAnimating) return;
|
|
|
|
|
|
|
|
const newIndex = target.getAttribute(dataBsSlideTo);
|
|
|
|
|
|
|
|
if (target && !hasClass(target, activeClass) // event target is not active
|
|
|
|
&& newIndex) { // AND has the specific attribute
|
|
|
|
self.to(+newIndex); // do the slide
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function carouselControlsHandler(e) {
|
|
|
|
e.preventDefault();
|
|
|
|
const that = this;
|
|
|
|
const self = that.closest(carouselSelector)[carouselComponent];
|
|
|
|
const { controls } = self;
|
|
|
|
|
|
|
|
if (controls[1] && that === controls[1]) {
|
|
|
|
self.next();
|
|
|
|
} else if (controls[1] && that === controls[0]) {
|
|
|
|
self.prev();
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function carouselKeyHandler({ which }) {
|
|
|
|
const [element] = Array.from(document.querySelectorAll(carouselSelector))
|
|
|
|
.filter((x) => isElementInScrollRange(x));
|
|
|
|
|
|
|
|
if (!element) return;
|
|
|
|
const self = element[carouselComponent];
|
|
|
|
|
|
|
|
switch (which) {
|
|
|
|
case 39:
|
|
|
|
self.next();
|
|
|
|
break;
|
|
|
|
case 37:
|
|
|
|
self.prev();
|
|
|
|
break;
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// CAROUSEL TOUCH HANDLERS
|
|
|
|
// =======================
|
|
|
|
function carouselTouchDownHandler(e) {
|
|
|
|
const element = this;
|
|
|
|
const self = element[carouselComponent];
|
|
|
|
|
|
|
|
if (!self || self.isTouch) { return; }
|
|
|
|
|
|
|
|
startX = e.changedTouches[0].pageX;
|
|
|
|
|
|
|
|
if (element.contains(e.target)) {
|
|
|
|
self.isTouch = true;
|
|
|
|
toggleCarouselTouchHandlers(self, 1);
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function carouselTouchMoveHandler(e) {
|
|
|
|
const { changedTouches, type } = e;
|
|
|
|
const self = this[carouselComponent];
|
|
|
|
|
|
|
|
if (!self || !self.isTouch) { return; }
|
|
|
|
|
|
|
|
currentX = changedTouches[0].pageX;
|
|
|
|
|
|
|
|
// cancel touch if more than one changedTouches detected
|
|
|
|
if (type === 'touchmove' && changedTouches.length > 1) {
|
|
|
|
e.preventDefault();
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function carouselTouchEndHandler(e) {
|
|
|
|
const element = this;
|
|
|
|
const self = element[carouselComponent];
|
|
|
|
|
|
|
|
if (!self || !self.isTouch) { return; }
|
|
|
|
|
|
|
|
endX = currentX || e.changedTouches[0].pageX;
|
|
|
|
|
|
|
|
if (self.isTouch) {
|
|
|
|
// the event target is outside the carousel OR carousel doens't include the related target
|
|
|
|
if ((!element.contains(e.target) || !element.contains(e.relatedTarget))
|
|
|
|
&& Math.abs(startX - endX) < 75) { // AND swipe distance is less than 75px
|
|
|
|
// when the above conditions are satisfied, no need to continue
|
2020-06-01 18:58:38 +02:00
|
|
|
return;
|
2021-06-19 19:22:19 +02:00
|
|
|
} // OR determine next index to slide to
|
|
|
|
if (currentX < startX) {
|
|
|
|
self.index += 1;
|
|
|
|
} else if (currentX > startX) {
|
|
|
|
self.index -= 1;
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
|
|
|
|
self.isTouch = false;
|
|
|
|
self.to(self.index); // do the slide
|
|
|
|
|
|
|
|
toggleCarouselTouchHandlers(self); // remove touch events handlers
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// CAROUSEL PRIVATE METHODS
|
|
|
|
// ========================
|
|
|
|
function activateCarouselIndicator(self, pageIndex) { // indicators
|
|
|
|
const { indicators } = self;
|
|
|
|
Array.from(indicators).forEach((x) => removeClass(x, activeClass));
|
|
|
|
if (self.indicators[pageIndex]) addClass(indicators[pageIndex], activeClass);
|
|
|
|
}
|
|
|
|
|
|
|
|
function toggleCarouselTouchHandlers(self, add) {
|
|
|
|
const { element } = self;
|
|
|
|
const action = add ? addEventListener : removeEventListener;
|
|
|
|
element[action]('touchmove', carouselTouchMoveHandler, passiveHandler);
|
|
|
|
element[action]('touchend', carouselTouchEndHandler, passiveHandler);
|
|
|
|
}
|
|
|
|
|
|
|
|
function toggleCarouselHandlers(self, add) {
|
|
|
|
const {
|
|
|
|
element, options, slides, controls, indicator,
|
|
|
|
} = self;
|
|
|
|
const {
|
|
|
|
touch, pause, interval, keyboard,
|
|
|
|
} = options;
|
|
|
|
const action = add ? addEventListener : removeEventListener;
|
|
|
|
|
|
|
|
if (pause && interval) {
|
|
|
|
element[action]('mouseenter', carouselPauseHandler);
|
|
|
|
element[action]('mouseleave', carouselResumeHandler);
|
|
|
|
element[action]('touchstart', carouselPauseHandler, passiveHandler);
|
|
|
|
element[action]('touchend', carouselResumeHandler, passiveHandler);
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
|
|
|
|
if (touch && slides.length > 1) {
|
|
|
|
element[action]('touchstart', carouselTouchDownHandler, passiveHandler);
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
|
|
|
|
controls.forEach((arrow) => {
|
|
|
|
if (arrow) arrow[action]('click', carouselControlsHandler);
|
|
|
|
});
|
|
|
|
|
|
|
|
if (indicator) indicator[action]('click', carouselIndicatorHandler);
|
|
|
|
if (keyboard) window[action]('keydown', carouselKeyHandler);
|
|
|
|
}
|
|
|
|
|
|
|
|
function getActiveIndex(self) {
|
|
|
|
const { slides, element } = self;
|
|
|
|
return Array.from(slides)
|
|
|
|
.indexOf(element.getElementsByClassName(`${carouselItem} ${activeClass}`)[0]) || 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// CAROUSEL DEFINITION
|
|
|
|
// ===================
|
|
|
|
class Carousel extends BaseComponent {
|
|
|
|
constructor(target, config) {
|
|
|
|
super(carouselComponent, target, defaultCarouselOptions, config);
|
|
|
|
// bind
|
|
|
|
const self = this;
|
|
|
|
|
|
|
|
// additional properties
|
|
|
|
self.timer = null;
|
|
|
|
self.direction = 'left';
|
|
|
|
self.isPaused = false;
|
|
|
|
self.isAnimating = false;
|
|
|
|
self.index = 0;
|
|
|
|
self.timer = null;
|
|
|
|
self.isTouch = false;
|
|
|
|
|
|
|
|
// initialization element
|
|
|
|
const { element } = self;
|
|
|
|
// carousel elements
|
|
|
|
// a LIVE collection is prefferable
|
|
|
|
self.slides = element.getElementsByClassName(carouselItem);
|
|
|
|
const { slides } = self;
|
|
|
|
|
|
|
|
// invalidate when not enough items
|
|
|
|
// no need to go further
|
|
|
|
if (slides.length < 2) { return; }
|
|
|
|
|
|
|
|
self.controls = [
|
|
|
|
queryElement(`.${carouselControl}-prev`, element),
|
|
|
|
queryElement(`.${carouselControl}-next`, element),
|
|
|
|
];
|
|
|
|
|
|
|
|
// a LIVE collection is prefferable
|
|
|
|
self.indicator = queryElement('.carousel-indicators', element);
|
|
|
|
self.indicators = (self.indicator && self.indicator.querySelectorAll(`[${dataBsSlideTo}]`)) || [];
|
|
|
|
|
|
|
|
// set JavaScript and DATA API options
|
|
|
|
const { options } = self;
|
|
|
|
|
|
|
|
// don't use TRUE as interval, it's actually 0, use the default 5000ms better
|
|
|
|
self.options.interval = options.interval === true
|
|
|
|
? defaultCarouselOptions.interval
|
|
|
|
: options.interval;
|
|
|
|
|
|
|
|
// set first slide active if none
|
|
|
|
if (getActiveIndex(self) < 0) {
|
|
|
|
if (slides.length) addClass(slides[0], activeClass);
|
|
|
|
if (self.indicators.length) activateCarouselIndicator(self, 0);
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
|
|
|
|
// attach event handlers
|
|
|
|
toggleCarouselHandlers(self, 1);
|
|
|
|
|
|
|
|
// start to cycle if interval is set
|
|
|
|
if (options.interval) self.cycle();
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
|
|
|
|
// CAROUSEL PUBLIC METHODS
|
|
|
|
// =======================
|
|
|
|
cycle() {
|
|
|
|
const self = this;
|
|
|
|
const { isPaused, element, options } = self;
|
|
|
|
if (self.timer) {
|
|
|
|
clearInterval(self.timer);
|
|
|
|
self.timer = null;
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
|
|
|
|
if (isPaused) {
|
|
|
|
removeClass(element, pausedClass);
|
|
|
|
self.isPaused = !isPaused;
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
|
|
|
|
self.timer = setInterval(() => {
|
|
|
|
if (isElementInScrollRange(element)) {
|
|
|
|
self.index += 1;
|
|
|
|
self.to(self.index);
|
|
|
|
}
|
|
|
|
}, options.interval);
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
|
|
|
|
pause() {
|
|
|
|
const self = this;
|
|
|
|
const { element, options, isPaused } = self;
|
|
|
|
if (options.interval && !isPaused) {
|
|
|
|
clearInterval(self.timer);
|
|
|
|
self.timer = null;
|
|
|
|
addClass(element, pausedClass);
|
|
|
|
self.isPaused = !isPaused;
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
|
|
|
|
next() {
|
|
|
|
const self = this;
|
|
|
|
if (!self.isAnimating) { self.index += 1; self.to(self.index); }
|
2020-08-31 16:40:21 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
|
|
|
|
prev() {
|
|
|
|
const self = this;
|
|
|
|
if (!self.isAnimating) { self.index -= 1; self.to(self.index); }
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
|
|
|
|
to(idx) {
|
|
|
|
const self = this;
|
|
|
|
const {
|
|
|
|
element, isAnimating, slides, options,
|
|
|
|
} = self;
|
|
|
|
const activeItem = getActiveIndex(self);
|
|
|
|
let next = idx;
|
|
|
|
|
|
|
|
// when controled via methods, make sure to check again
|
|
|
|
// first return if we're on the same item #227
|
|
|
|
if (isAnimating || activeItem === next) return;
|
|
|
|
|
|
|
|
// determine transition direction
|
|
|
|
if ((activeItem < next) || (activeItem === 0 && next === slides.length - 1)) {
|
|
|
|
self.direction = 'left'; // next
|
|
|
|
} else if ((activeItem > next) || (activeItem === slides.length - 1 && next === 0)) {
|
|
|
|
self.direction = 'right'; // prev
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
const { direction } = self;
|
|
|
|
|
|
|
|
// find the right next index
|
|
|
|
if (next < 0) { next = slides.length - 1; } else if (next >= slides.length) { next = 0; }
|
|
|
|
|
|
|
|
// orientation, class name, eventProperties
|
|
|
|
const orientation = direction === 'left' ? 'next' : 'prev';
|
|
|
|
const directionClass = direction === 'left' ? 'start' : 'end';
|
|
|
|
const eventProperties = {
|
|
|
|
relatedTarget: slides[next], direction, from: activeItem, to: next,
|
|
|
|
};
|
|
|
|
|
|
|
|
// update event properties
|
|
|
|
Object.keys(eventProperties).forEach((k) => {
|
|
|
|
carouselSlideEvent[k] = eventProperties[k];
|
|
|
|
carouselSlidEvent[k] = eventProperties[k];
|
|
|
|
});
|
|
|
|
|
|
|
|
// discontinue when prevented
|
|
|
|
element.dispatchEvent(carouselSlideEvent);
|
|
|
|
if (carouselSlideEvent.defaultPrevented) return;
|
|
|
|
|
|
|
|
// update index
|
|
|
|
self.index = next;
|
|
|
|
|
|
|
|
clearInterval(self.timer);
|
|
|
|
self.timer = null;
|
|
|
|
|
|
|
|
self.isAnimating = true;
|
|
|
|
activateCarouselIndicator(self, next);
|
|
|
|
|
|
|
|
if (getElementTransitionDuration(slides[next]) && hasClass(element, 'slide')) {
|
|
|
|
addClass(slides[next], `${carouselItem}-${orientation}`);
|
|
|
|
reflow(slides[next]);
|
|
|
|
addClass(slides[next], `${carouselItem}-${directionClass}`);
|
|
|
|
addClass(slides[activeItem], `${carouselItem}-${directionClass}`);
|
|
|
|
|
|
|
|
emulateTransitionEnd(slides[next], () => carouselTransitionEndHandler(self));
|
|
|
|
} else {
|
|
|
|
addClass(slides[next], activeClass);
|
|
|
|
removeClass(slides[activeItem], activeClass);
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
self.isAnimating = false;
|
|
|
|
|
|
|
|
// check for element, might have been disposed
|
|
|
|
if (element && options.interval && !hasClass(element, pausedClass)) {
|
|
|
|
self.cycle();
|
|
|
|
}
|
|
|
|
|
|
|
|
element.dispatchEvent(carouselSlidEvent);
|
|
|
|
}, 100);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
dispose() {
|
|
|
|
const self = this;
|
|
|
|
const { slides } = self;
|
|
|
|
const itemClasses = ['start', 'end', 'prev', 'next'];
|
|
|
|
|
|
|
|
Array.from(slides).forEach((slide, idx) => {
|
|
|
|
if (hasClass(slide, activeClass)) activateCarouselIndicator(self, idx);
|
|
|
|
itemClasses.forEach((c) => removeClass(slide, `${carouselItem}-${c}`));
|
|
|
|
});
|
|
|
|
|
|
|
|
toggleCarouselHandlers(self);
|
|
|
|
clearInterval(self.timer);
|
|
|
|
super.dispose(carouselComponent);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Carousel.init = {
|
|
|
|
component: carouselComponent,
|
|
|
|
selector: carouselSelector,
|
|
|
|
constructor: Carousel,
|
|
|
|
};
|
|
|
|
|
|
|
|
const ariaExpanded = 'aria-expanded';
|
|
|
|
|
|
|
|
// collapse / tab
|
|
|
|
const collapsingClass = 'collapsing';
|
|
|
|
|
|
|
|
const dataBsTarget = 'data-bs-target';
|
|
|
|
|
|
|
|
const dataBsParent = 'data-bs-parent';
|
|
|
|
|
|
|
|
const dataBsContainer = 'data-bs-container';
|
|
|
|
|
|
|
|
function getTargetElement(element) {
|
|
|
|
return queryElement(element.getAttribute(dataBsTarget) || element.getAttribute('href'))
|
|
|
|
|| element.closest(element.getAttribute(dataBsParent))
|
|
|
|
|| queryElement(element.getAttribute(dataBsContainer));
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Native JavaScript for Bootstrap 5 | Collapse
|
|
|
|
----------------------------------------------- */
|
|
|
|
|
|
|
|
// COLLAPSE GC
|
|
|
|
// ===========
|
|
|
|
const collapseString = 'collapse';
|
|
|
|
const collapseComponent = 'Collapse';
|
|
|
|
const collapseSelector = `.${collapseString}`;
|
|
|
|
const collapseToggleSelector = `[${dataBsToggle}="${collapseString}"]`;
|
|
|
|
|
|
|
|
// COLLAPSE CUSTOM EVENTS
|
|
|
|
// ======================
|
|
|
|
const showCollapseEvent = bootstrapCustomEvent(`show.bs.${collapseString}`);
|
|
|
|
const shownCollapseEvent = bootstrapCustomEvent(`shown.bs.${collapseString}`);
|
|
|
|
const hideCollapseEvent = bootstrapCustomEvent(`hide.bs.${collapseString}`);
|
|
|
|
const hiddenCollapseEvent = bootstrapCustomEvent(`hidden.bs.${collapseString}`);
|
|
|
|
|
|
|
|
// COLLAPSE PRIVATE METHODS
|
|
|
|
// ========================
|
|
|
|
function expandCollapse(self) {
|
|
|
|
const {
|
|
|
|
element, parent, triggers,
|
|
|
|
} = self;
|
|
|
|
|
|
|
|
element.dispatchEvent(showCollapseEvent);
|
|
|
|
if (showCollapseEvent.defaultPrevented) return;
|
|
|
|
|
|
|
|
self.isAnimating = true;
|
|
|
|
if (parent) parent.isAnimating = true;
|
|
|
|
|
|
|
|
addClass(element, collapsingClass);
|
|
|
|
removeClass(element, collapseString);
|
|
|
|
|
|
|
|
element.style.height = `${element.scrollHeight}px`;
|
|
|
|
|
|
|
|
emulateTransitionEnd(element, () => {
|
|
|
|
self.isAnimating = false;
|
|
|
|
if (parent) parent.isAnimating = false;
|
|
|
|
|
|
|
|
triggers.forEach((btn) => btn.setAttribute(ariaExpanded, 'true'));
|
|
|
|
|
|
|
|
removeClass(element, collapsingClass);
|
|
|
|
addClass(element, collapseString);
|
|
|
|
addClass(element, showClass);
|
|
|
|
|
|
|
|
element.style.height = '';
|
|
|
|
|
|
|
|
element.dispatchEvent(shownCollapseEvent);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function collapseContent(self) {
|
|
|
|
const {
|
|
|
|
element, parent, triggers,
|
|
|
|
} = self;
|
|
|
|
|
|
|
|
element.dispatchEvent(hideCollapseEvent);
|
|
|
|
|
|
|
|
if (hideCollapseEvent.defaultPrevented) return;
|
|
|
|
|
|
|
|
self.isAnimating = true;
|
|
|
|
if (parent) parent.isAnimating = true;
|
|
|
|
|
|
|
|
element.style.height = `${element.scrollHeight}px`;
|
|
|
|
|
|
|
|
removeClass(element, collapseString);
|
|
|
|
removeClass(element, showClass);
|
|
|
|
addClass(element, collapsingClass);
|
|
|
|
|
|
|
|
reflow(element);
|
|
|
|
element.style.height = '0px';
|
|
|
|
|
|
|
|
emulateTransitionEnd(element, () => {
|
|
|
|
self.isAnimating = false;
|
|
|
|
if (parent) parent.isAnimating = false;
|
|
|
|
|
|
|
|
triggers.forEach((btn) => btn.setAttribute(ariaExpanded, 'false'));
|
|
|
|
|
|
|
|
removeClass(element, collapsingClass);
|
|
|
|
addClass(element, collapseString);
|
|
|
|
|
|
|
|
element.style.height = '';
|
|
|
|
|
|
|
|
element.dispatchEvent(hiddenCollapseEvent);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function toggleCollapseHandler(self, add) {
|
|
|
|
const action = add ? addEventListener : removeEventListener;
|
|
|
|
const { triggers } = self;
|
|
|
|
|
|
|
|
if (triggers.length) {
|
|
|
|
triggers.forEach((btn) => btn[action]('click', collapseClickHandler));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// COLLAPSE EVENT HANDLER
|
|
|
|
// ======================
|
|
|
|
function collapseClickHandler(e) {
|
|
|
|
const { target } = e;
|
|
|
|
const trigger = target.closest(collapseToggleSelector);
|
|
|
|
const element = getTargetElement(trigger);
|
|
|
|
const self = element && element[collapseComponent];
|
|
|
|
if (self) self.toggle(target);
|
|
|
|
|
|
|
|
// event target is anchor link #398
|
|
|
|
if (trigger && trigger.tagName === 'A') e.preventDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
// COLLAPSE DEFINITION
|
|
|
|
// ===================
|
|
|
|
class Collapse extends BaseComponent {
|
|
|
|
constructor(target, config) {
|
|
|
|
super(collapseComponent, target, { parent: null }, config);
|
|
|
|
// bind
|
|
|
|
const self = this;
|
|
|
|
|
|
|
|
// initialization element
|
|
|
|
const { element } = self;
|
|
|
|
|
|
|
|
// set triggering elements
|
|
|
|
self.triggers = Array.from(document.querySelectorAll(collapseToggleSelector))
|
|
|
|
.filter((btn) => getTargetElement(btn) === element);
|
|
|
|
|
|
|
|
// set parent accordion
|
|
|
|
self.parent = queryElement(self.options.parent);
|
|
|
|
const { parent } = self;
|
|
|
|
|
|
|
|
// set initial state
|
|
|
|
self.isAnimating = false;
|
|
|
|
if (parent) parent.isAnimating = false;
|
|
|
|
|
|
|
|
// add event listeners
|
|
|
|
toggleCollapseHandler(self, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
// COLLAPSE PUBLIC METHODS
|
|
|
|
// =======================
|
|
|
|
toggle(related) {
|
|
|
|
const self = this;
|
|
|
|
if (!hasClass(self.element, showClass)) self.show(related);
|
|
|
|
else self.hide(related);
|
|
|
|
}
|
|
|
|
|
|
|
|
hide() {
|
|
|
|
const self = this;
|
|
|
|
const { triggers, isAnimating } = self;
|
|
|
|
if (isAnimating) return;
|
|
|
|
|
|
|
|
collapseContent(self);
|
|
|
|
if (triggers.length) {
|
|
|
|
triggers.forEach((btn) => addClass(btn, `${collapseString}d`));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
show() {
|
|
|
|
const self = this;
|
|
|
|
const {
|
|
|
|
element, parent, triggers, isAnimating,
|
|
|
|
} = self;
|
|
|
|
let activeCollapse;
|
|
|
|
let activeCollapseInstance;
|
|
|
|
|
|
|
|
if (parent) {
|
|
|
|
activeCollapse = Array.from(parent.querySelectorAll(`.${collapseString}.${showClass}`))
|
|
|
|
.find((i) => i[collapseComponent]);
|
|
|
|
activeCollapseInstance = activeCollapse && activeCollapse[collapseComponent];
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((!parent || (parent && !parent.isAnimating)) && !isAnimating) {
|
|
|
|
if (activeCollapseInstance && activeCollapse !== element) {
|
|
|
|
collapseContent(activeCollapseInstance);
|
|
|
|
activeCollapseInstance.triggers.forEach((btn) => {
|
|
|
|
addClass(btn, `${collapseString}d`);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
expandCollapse(self);
|
|
|
|
if (triggers.length) {
|
|
|
|
triggers.forEach((btn) => removeClass(btn, `${collapseString}d`));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
dispose() {
|
|
|
|
const self = this;
|
|
|
|
const { parent } = self;
|
|
|
|
toggleCollapseHandler(self);
|
|
|
|
|
|
|
|
if (parent) delete parent.isAnimating;
|
|
|
|
super.dispose(collapseComponent);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Collapse.init = {
|
|
|
|
component: collapseComponent,
|
|
|
|
selector: collapseSelector,
|
|
|
|
constructor: Collapse,
|
|
|
|
};
|
|
|
|
|
|
|
|
const dropdownMenuClasses = ['dropdown', 'dropup', 'dropstart', 'dropend'];
|
|
|
|
|
|
|
|
const dropdownMenuClass = 'dropdown-menu';
|
|
|
|
|
|
|
|
function isEmptyAnchor(elem) {
|
|
|
|
const parentAnchor = elem.closest('A');
|
|
|
|
// anchor href starts with #
|
|
|
|
return elem && ((elem.href && elem.href.slice(-1) === '#')
|
|
|
|
// OR a child of an anchor with href starts with #
|
|
|
|
|| (parentAnchor && parentAnchor.href && parentAnchor.href.slice(-1) === '#'));
|
|
|
|
}
|
|
|
|
|
|
|
|
function setFocus(element) {
|
|
|
|
element.focus();
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Native JavaScript for Bootstrap 5 | Dropdown
|
|
|
|
----------------------------------------------- */
|
|
|
|
|
|
|
|
// DROPDOWN PRIVATE GC
|
|
|
|
// ===================
|
|
|
|
const [dropdownString] = dropdownMenuClasses;
|
|
|
|
const dropdownComponent = 'Dropdown';
|
|
|
|
const dropdownSelector = `[${dataBsToggle}="${dropdownString}"]`;
|
|
|
|
|
|
|
|
// DROPDOWN PRIVATE GC
|
|
|
|
// ===================
|
|
|
|
const dropupString = dropdownMenuClasses[1];
|
|
|
|
const dropstartString = dropdownMenuClasses[2];
|
|
|
|
const dropendString = dropdownMenuClasses[3];
|
|
|
|
const dropdownMenuEndClass = `${dropdownMenuClass}-end`;
|
|
|
|
const hideMenuClass = ['d-block', 'invisible'];
|
|
|
|
const verticalClass = [dropdownString, dropupString];
|
|
|
|
const horizontalClass = [dropstartString, dropendString];
|
|
|
|
const defaultDropdownOptions = {
|
|
|
|
offset: 5, // [number] 5(px)
|
|
|
|
display: 'dynamic', // [dynamic|static]
|
|
|
|
};
|
|
|
|
|
|
|
|
// DROPDOWN CUSTOM EVENTS
|
|
|
|
// ========================
|
|
|
|
const showDropdownEvent = bootstrapCustomEvent(`show.bs.${dropdownString}`);
|
|
|
|
const shownDropdownEvent = bootstrapCustomEvent(`shown.bs.${dropdownString}`);
|
|
|
|
const hideDropdownEvent = bootstrapCustomEvent(`hide.bs.${dropdownString}`);
|
|
|
|
const hiddenDropdownEvent = bootstrapCustomEvent(`hidden.bs.${dropdownString}`);
|
|
|
|
|
|
|
|
// DROPDOWN PRIVATE METHODS
|
|
|
|
// ========================
|
|
|
|
function styleDropdown(self, show) {
|
|
|
|
const {
|
|
|
|
element, menu, originalClass, menuEnd, options,
|
|
|
|
} = self;
|
|
|
|
const parent = element.parentElement;
|
|
|
|
|
|
|
|
// reset menu offset and position
|
|
|
|
const resetProps = ['margin', 'top', 'bottom', 'left', 'right'];
|
|
|
|
resetProps.forEach((p) => { menu.style[p] = ''; });
|
|
|
|
removeClass(parent, 'position-static');
|
|
|
|
|
|
|
|
if (!show) {
|
|
|
|
parent.className = originalClass.join(' ');
|
|
|
|
const menuAction = menuEnd && !hasClass(menu, dropdownMenuEndClass) ? addClass : removeClass;
|
|
|
|
menuAction(menu, dropdownMenuEndClass);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const { offset } = options;
|
|
|
|
let positionClass = dropdownMenuClasses.find((c) => originalClass.includes(c));
|
|
|
|
|
|
|
|
let dropdownMargin = {
|
|
|
|
dropdown: [offset, 0, 0],
|
|
|
|
dropup: [0, 0, offset],
|
|
|
|
dropstart: [-1, offset, 0],
|
|
|
|
dropend: [-1, 0, 0, offset],
|
|
|
|
};
|
|
|
|
|
|
|
|
const dropdownPosition = {
|
|
|
|
dropdown: { top: '100%' },
|
|
|
|
dropup: { top: 'auto', bottom: '100%' },
|
|
|
|
dropstart: { left: 'auto', right: '100%' },
|
|
|
|
dropend: { left: '100%', right: 'auto' },
|
|
|
|
menuEnd: { right: 0, left: 'auto' },
|
|
|
|
};
|
|
|
|
|
|
|
|
// force showing the menu to calculate its size
|
|
|
|
hideMenuClass.forEach((c) => addClass(menu, c));
|
|
|
|
|
|
|
|
const dropdownRegex = new RegExp(`\\b(${dropdownString}|${dropupString}|${dropstartString}|${dropendString})+`);
|
|
|
|
const elementDimensions = { w: element.offsetWidth, h: element.offsetHeight };
|
|
|
|
const menuDimensions = { w: menu.offsetWidth, h: menu.offsetHeight };
|
|
|
|
const HTML = document.documentElement;
|
|
|
|
const BD = document.body;
|
|
|
|
const windowWidth = (HTML.clientWidth || BD.clientWidth);
|
|
|
|
const windowHeight = (HTML.clientHeight || BD.clientHeight);
|
|
|
|
const targetBCR = element.getBoundingClientRect();
|
|
|
|
// dropdownMenuEnd && [ dropdown | dropup ]
|
|
|
|
const leftExceed = targetBCR.left + elementDimensions.w - menuDimensions.w < 0;
|
|
|
|
// dropstart
|
|
|
|
const leftFullExceed = targetBCR.left - menuDimensions.w < 0;
|
|
|
|
// !dropdownMenuEnd && [ dropdown | dropup ]
|
|
|
|
const rightExceed = targetBCR.left + menuDimensions.w >= windowWidth;
|
|
|
|
// dropend
|
|
|
|
const rightFullExceed = targetBCR.left + menuDimensions.w + elementDimensions.w >= windowWidth;
|
|
|
|
// dropstart | dropend
|
|
|
|
const bottomExceed = targetBCR.top + menuDimensions.h >= windowHeight;
|
|
|
|
// dropdown
|
|
|
|
const bottomFullExceed = targetBCR.top + menuDimensions.h + elementDimensions.h >= windowHeight;
|
|
|
|
// dropup
|
|
|
|
const topExceed = targetBCR.top - menuDimensions.h < 0;
|
|
|
|
|
|
|
|
const btnGroup = parent.parentNode.closest('.btn-group,.btn-group-vertical');
|
|
|
|
|
|
|
|
// recompute position
|
|
|
|
if (horizontalClass.includes(positionClass) && leftFullExceed && rightFullExceed) {
|
|
|
|
positionClass = dropdownString;
|
|
|
|
}
|
|
|
|
if (horizontalClass.includes(positionClass) && bottomExceed) {
|
|
|
|
positionClass = dropupString;
|
|
|
|
}
|
|
|
|
if (positionClass === dropstartString && leftFullExceed && !bottomExceed) {
|
|
|
|
positionClass = dropendString;
|
|
|
|
}
|
|
|
|
if (positionClass === dropendString && rightFullExceed && !bottomExceed) {
|
|
|
|
positionClass = dropstartString;
|
|
|
|
}
|
|
|
|
if (positionClass === dropupString && topExceed && !bottomFullExceed) {
|
|
|
|
positionClass = dropdownString;
|
|
|
|
}
|
|
|
|
if (positionClass === dropdownString && bottomFullExceed && !topExceed) {
|
|
|
|
positionClass = dropupString;
|
|
|
|
}
|
|
|
|
|
|
|
|
// set spacing
|
|
|
|
dropdownMargin = dropdownMargin[positionClass];
|
|
|
|
menu.style.margin = `${dropdownMargin.map((x) => (x ? `${x}px` : x)).join(' ')}`;
|
|
|
|
Object.keys(dropdownPosition[positionClass]).forEach((position) => {
|
|
|
|
menu.style[position] = dropdownPosition[positionClass][position];
|
|
|
|
});
|
|
|
|
|
|
|
|
// update dropdown position class
|
|
|
|
if (!hasClass(parent, positionClass)) {
|
|
|
|
parent.className = parent.className.replace(dropdownRegex, positionClass);
|
|
|
|
}
|
|
|
|
|
|
|
|
// update dropdown / dropup to handle parent btn-group element
|
|
|
|
// as well as the dropdown-menu-end utility class
|
|
|
|
if (verticalClass.includes(positionClass)) {
|
|
|
|
const menuEndAction = rightExceed ? addClass : removeClass;
|
|
|
|
|
|
|
|
if (!btnGroup) menuEndAction(menu, dropdownMenuEndClass);
|
|
|
|
else if (leftExceed) addClass(parent, 'position-static');
|
|
|
|
|
|
|
|
if (hasClass(menu, dropdownMenuEndClass)) {
|
|
|
|
Object.keys(dropdownPosition.menuEnd).forEach((p) => {
|
|
|
|
menu.style[p] = dropdownPosition.menuEnd[p];
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// remove util classes from the menu, we have its size
|
|
|
|
hideMenuClass.forEach((c) => removeClass(menu, c));
|
|
|
|
}
|
|
|
|
|
|
|
|
function toggleDropdownDismiss(self) {
|
|
|
|
const action = self.open ? addEventListener : removeEventListener;
|
|
|
|
|
|
|
|
document[action]('click', dropdownDismissHandler);
|
|
|
|
document[action]('focus', dropdownDismissHandler);
|
|
|
|
document[action]('keydown', dropdownPreventScroll);
|
|
|
|
document[action]('keyup', dropdownKeyHandler);
|
|
|
|
if (self.options.display === 'dynamic') {
|
|
|
|
window[action]('scroll', dropdownLayoutHandler, passiveHandler);
|
|
|
|
window[action]('resize', dropdownLayoutHandler, passiveHandler);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function toggleDropdownHandler(self, add) {
|
|
|
|
const action = add ? addEventListener : removeEventListener;
|
|
|
|
self.element[action]('click', dropdownClickHandler);
|
|
|
|
}
|
|
|
|
|
|
|
|
function getCurrentOpenDropdown() {
|
|
|
|
const currentParent = dropdownMenuClasses
|
|
|
|
.map((c) => document.getElementsByClassName(`${c} ${showClass}`))
|
|
|
|
.find((x) => x.length);
|
|
|
|
|
|
|
|
if (currentParent && currentParent.length) {
|
|
|
|
return Array.from(currentParent[0].children).find((x) => x.hasAttribute(dataBsToggle));
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// DROPDOWN EVENT HANDLERS
|
|
|
|
// =======================
|
|
|
|
function dropdownDismissHandler(e) {
|
|
|
|
const { target, type } = e;
|
|
|
|
if (!target.closest) return; // some weird FF bug #409
|
|
|
|
|
|
|
|
const element = getCurrentOpenDropdown();
|
|
|
|
const parent = element && element.parentNode;
|
|
|
|
const self = element && element[dropdownComponent];
|
|
|
|
const menu = self && self.menu;
|
|
|
|
|
|
|
|
const hasData = target.closest(dropdownSelector) !== null;
|
|
|
|
const isForm = parent && parent.contains(target)
|
|
|
|
&& (target.tagName === 'form' || target.closest('form') !== null);
|
|
|
|
|
|
|
|
if (type === 'click' && isEmptyAnchor(target)) {
|
|
|
|
e.preventDefault();
|
|
|
|
}
|
|
|
|
if (type === 'focus'
|
|
|
|
&& (target === element || target === menu || menu.contains(target))) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isForm || hasData) ; else if (self) {
|
|
|
|
self.hide(element);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function dropdownClickHandler(e) {
|
|
|
|
const element = this;
|
|
|
|
const self = element[dropdownComponent];
|
|
|
|
self.toggle(element);
|
|
|
|
|
|
|
|
if (isEmptyAnchor(e.target)) e.preventDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
function dropdownPreventScroll(e) {
|
|
|
|
if (e.which === 38 || e.which === 40) e.preventDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
function dropdownKeyHandler({ which }) {
|
|
|
|
const element = getCurrentOpenDropdown();
|
|
|
|
const self = element[dropdownComponent];
|
|
|
|
const { menu, menuItems, open } = self;
|
|
|
|
const activeItem = document.activeElement;
|
|
|
|
const isSameElement = activeItem === element;
|
|
|
|
const isInsideMenu = menu.contains(activeItem);
|
|
|
|
const isMenuItem = activeItem.parentNode === menu || activeItem.parentNode.parentNode === menu;
|
|
|
|
|
|
|
|
let idx = menuItems.indexOf(activeItem);
|
|
|
|
|
|
|
|
if (isMenuItem) { // navigate up | down
|
|
|
|
if (isSameElement) {
|
|
|
|
idx = 0;
|
|
|
|
} else if (which === 38) {
|
|
|
|
idx = idx > 1 ? idx - 1 : 0;
|
|
|
|
} else if (which === 40) {
|
|
|
|
idx = idx < menuItems.length - 1 ? idx + 1 : idx;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (menuItems[idx]) setFocus(menuItems[idx]);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (((menuItems.length && isMenuItem) // menu has items
|
|
|
|
|| (!menuItems.length && (isInsideMenu || isSameElement)) // menu might be a form
|
|
|
|
|| !isInsideMenu) // or the focused element is not in the menu at all
|
|
|
|
&& open && which === 27 // menu must be open
|
|
|
|
) {
|
|
|
|
self.toggle();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function dropdownLayoutHandler() {
|
|
|
|
const element = getCurrentOpenDropdown();
|
|
|
|
const self = element && element[dropdownComponent];
|
|
|
|
|
|
|
|
if (self && self.open) styleDropdown(self, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
// DROPDOWN DEFINITION
|
|
|
|
// ===================
|
|
|
|
class Dropdown extends BaseComponent {
|
|
|
|
constructor(target, config) {
|
|
|
|
super(dropdownComponent, target, defaultDropdownOptions, config);
|
|
|
|
// bind
|
|
|
|
const self = this;
|
|
|
|
|
|
|
|
// initialization element
|
|
|
|
const { element } = self;
|
|
|
|
|
|
|
|
// set targets
|
|
|
|
const parent = element.parentElement;
|
|
|
|
self.menu = queryElement(`.${dropdownMenuClass}`, parent);
|
|
|
|
const { menu } = self;
|
|
|
|
|
|
|
|
self.originalClass = Array.from(parent.classList);
|
|
|
|
|
|
|
|
// set original position
|
|
|
|
self.menuEnd = hasClass(menu, dropdownMenuEndClass);
|
|
|
|
|
|
|
|
self.menuItems = [];
|
|
|
|
|
|
|
|
Array.from(menu.children).forEach((child) => {
|
|
|
|
if (child.children.length && (child.children[0].tagName === 'A')) self.menuItems.push(child.children[0]);
|
|
|
|
if (child.tagName === 'A') self.menuItems.push(child);
|
|
|
|
});
|
|
|
|
|
|
|
|
// set initial state to closed
|
|
|
|
self.open = false;
|
|
|
|
|
|
|
|
// add event listener
|
|
|
|
toggleDropdownHandler(self, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
// DROPDOWN PUBLIC METHODS
|
|
|
|
// =======================
|
|
|
|
toggle(related) {
|
|
|
|
const self = this;
|
|
|
|
const { open } = self;
|
|
|
|
|
|
|
|
if (open) self.hide(related);
|
|
|
|
else self.show(related);
|
|
|
|
}
|
|
|
|
|
|
|
|
show(related) {
|
|
|
|
const self = this;
|
|
|
|
const currentParent = queryElement(dropdownMenuClasses.map((c) => `.${c}.${showClass}`).join(','));
|
|
|
|
const currentElement = currentParent && queryElement(dropdownSelector, currentParent);
|
|
|
|
|
|
|
|
if (currentElement) currentElement[dropdownComponent].hide();
|
|
|
|
|
|
|
|
const { element, menu, open } = self;
|
|
|
|
const parent = element.parentNode;
|
|
|
|
|
|
|
|
// update relatedTarget and dispatch
|
|
|
|
showDropdownEvent.relatedTarget = related || null;
|
|
|
|
parent.dispatchEvent(showDropdownEvent);
|
|
|
|
if (showDropdownEvent.defaultPrevented) return;
|
|
|
|
|
|
|
|
// change menu position
|
|
|
|
styleDropdown(self, 1);
|
|
|
|
|
|
|
|
addClass(menu, showClass);
|
|
|
|
addClass(parent, showClass);
|
|
|
|
|
|
|
|
element.setAttribute(ariaExpanded, true);
|
|
|
|
self.open = !open;
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
setFocus(menu.getElementsByTagName('INPUT')[0] || element); // focus the first input item | element
|
|
|
|
toggleDropdownDismiss(self);
|
|
|
|
|
|
|
|
shownDropdownEvent.relatedTarget = related || null;
|
|
|
|
parent.dispatchEvent(shownDropdownEvent);
|
|
|
|
}, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
hide(related) {
|
|
|
|
const self = this;
|
|
|
|
const { element, menu, open } = self;
|
|
|
|
const parent = element.parentNode;
|
|
|
|
hideDropdownEvent.relatedTarget = related || null;
|
|
|
|
parent.dispatchEvent(hideDropdownEvent);
|
|
|
|
if (hideDropdownEvent.defaultPrevented) return;
|
|
|
|
|
|
|
|
removeClass(menu, showClass);
|
|
|
|
removeClass(parent, showClass);
|
|
|
|
|
|
|
|
// revert to original position
|
|
|
|
styleDropdown(self);
|
|
|
|
|
|
|
|
element.setAttribute(ariaExpanded, false);
|
|
|
|
self.open = !open;
|
|
|
|
|
|
|
|
setFocus(element);
|
|
|
|
|
|
|
|
// only re-attach handler if the instance is not disposed
|
|
|
|
setTimeout(() => toggleDropdownDismiss(self), 1);
|
|
|
|
|
|
|
|
// update relatedTarget and dispatch
|
|
|
|
hiddenDropdownEvent.relatedTarget = related || null;
|
|
|
|
parent.dispatchEvent(hiddenDropdownEvent);
|
|
|
|
}
|
|
|
|
|
|
|
|
dispose() {
|
|
|
|
const self = this;
|
|
|
|
const { element } = self;
|
|
|
|
|
|
|
|
if (hasClass(element.parentNode, showClass) && self.open) self.hide();
|
|
|
|
|
|
|
|
toggleDropdownHandler(self);
|
|
|
|
|
|
|
|
super.dispose(dropdownComponent);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Dropdown.init = {
|
|
|
|
component: dropdownComponent,
|
|
|
|
selector: dropdownSelector,
|
|
|
|
constructor: Dropdown,
|
|
|
|
};
|
|
|
|
|
|
|
|
const ariaHidden = 'aria-hidden';
|
|
|
|
|
|
|
|
const ariaModal = 'aria-modal';
|
|
|
|
|
|
|
|
const fixedTopClass = 'fixed-top';
|
|
|
|
|
|
|
|
const fixedBottomClass = 'fixed-bottom';
|
|
|
|
|
|
|
|
const stickyTopClass = 'sticky-top';
|
|
|
|
|
|
|
|
const fixedItems = Array.from(document.getElementsByClassName(fixedTopClass))
|
|
|
|
.concat(Array.from(document.getElementsByClassName(fixedBottomClass)))
|
|
|
|
.concat(Array.from(document.getElementsByClassName(stickyTopClass)))
|
|
|
|
.concat(Array.from(document.getElementsByClassName('is-fixed')));
|
|
|
|
|
|
|
|
function resetScrollbar() {
|
|
|
|
const bd = document.body;
|
|
|
|
bd.style.paddingRight = '';
|
|
|
|
bd.style.overflow = '';
|
|
|
|
|
|
|
|
if (fixedItems.length) {
|
|
|
|
fixedItems.forEach((fixed) => {
|
|
|
|
fixed.style.paddingRight = '';
|
|
|
|
fixed.style.marginRight = '';
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function measureScrollbar() {
|
|
|
|
const windowWidth = document.documentElement.clientWidth;
|
|
|
|
return Math.abs(window.innerWidth - windowWidth);
|
|
|
|
}
|
|
|
|
|
|
|
|
function setScrollbar(scrollbarWidth, overflow) {
|
|
|
|
const bd = document.body;
|
|
|
|
const bdStyle = getComputedStyle(bd);
|
|
|
|
const bodyPad = parseInt(bdStyle.paddingRight, 10);
|
|
|
|
const isOpen = bdStyle.overflow === 'hidden';
|
|
|
|
const sbWidth = isOpen && bodyPad ? 0 : scrollbarWidth;
|
|
|
|
|
|
|
|
if (overflow) {
|
|
|
|
bd.style.overflow = 'hidden';
|
|
|
|
bd.style.paddingRight = `${bodyPad + sbWidth}px`;
|
|
|
|
|
|
|
|
if (fixedItems.length) {
|
|
|
|
fixedItems.forEach((fixed) => {
|
|
|
|
const isSticky = hasClass(fixed, stickyTopClass);
|
|
|
|
const itemPadValue = getComputedStyle(fixed).paddingRight;
|
|
|
|
fixed.style.paddingRight = `${parseInt(itemPadValue, 10) + sbWidth}px`;
|
|
|
|
if (isSticky) {
|
|
|
|
const itemMValue = getComputedStyle(fixed).marginRight;
|
|
|
|
fixed.style.marginRight = `${parseInt(itemMValue, 10) - sbWidth}px`;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const modalOpenClass = 'modal-open';
|
|
|
|
const modalBackdropClass = 'modal-backdrop';
|
|
|
|
const modalActiveSelector = `.modal.${showClass}`;
|
|
|
|
const offcanvasActiveSelector = `.offcanvas.${showClass}`;
|
|
|
|
|
|
|
|
const overlay = document.createElement('div');
|
|
|
|
overlay.setAttribute('class', `${modalBackdropClass}`);
|
|
|
|
|
|
|
|
function getCurrentOpen() {
|
|
|
|
return queryElement(`${modalActiveSelector},${offcanvasActiveSelector}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
function appendOverlay(hasFade) {
|
|
|
|
document.body.appendChild(overlay);
|
|
|
|
if (hasFade) addClass(overlay, fadeClass);
|
|
|
|
}
|
|
|
|
|
|
|
|
function showOverlay() {
|
|
|
|
addClass(overlay, showClass);
|
|
|
|
reflow(overlay);
|
|
|
|
}
|
|
|
|
|
|
|
|
function hideOverlay() {
|
|
|
|
removeClass(overlay, showClass);
|
|
|
|
}
|
|
|
|
|
|
|
|
function removeOverlay() {
|
|
|
|
const bd = document.body;
|
|
|
|
const currentOpen = getCurrentOpen();
|
|
|
|
|
|
|
|
if (!currentOpen) {
|
|
|
|
removeClass(overlay, fadeClass);
|
|
|
|
removeClass(bd, modalOpenClass);
|
|
|
|
bd.removeChild(overlay);
|
|
|
|
resetScrollbar();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function isVisible(element) {
|
|
|
|
return getComputedStyle(element).visibility !== 'hidden'
|
|
|
|
&& element.offsetParent !== null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Native JavaScript for Bootstrap 5 | Modal
|
|
|
|
-------------------------------------------- */
|
|
|
|
|
|
|
|
// MODAL PRIVATE GC
|
|
|
|
// ================
|
|
|
|
const modalString = 'modal';
|
|
|
|
const modalComponent = 'Modal';
|
|
|
|
const modalSelector = `.${modalString}`;
|
|
|
|
// const modalActiveSelector = `.${modalString}.${showClass}`;
|
|
|
|
const modalToggleSelector = `[${dataBsToggle}="${modalString}"]`;
|
|
|
|
const modalDismissSelector = `[${dataBsDismiss}="${modalString}"]`;
|
|
|
|
const modalStaticClass = `${modalString}-static`;
|
|
|
|
const modalDefaultOptions = {
|
|
|
|
backdrop: true, // boolean|string
|
|
|
|
keyboard: true, // boolean
|
|
|
|
};
|
|
|
|
|
|
|
|
// MODAL CUSTOM EVENTS
|
|
|
|
// ===================
|
|
|
|
const showModalEvent = bootstrapCustomEvent(`show.bs.${modalString}`);
|
|
|
|
const shownModalEvent = bootstrapCustomEvent(`shown.bs.${modalString}`);
|
|
|
|
const hideModalEvent = bootstrapCustomEvent(`hide.bs.${modalString}`);
|
|
|
|
const hiddenModalEvent = bootstrapCustomEvent(`hidden.bs.${modalString}`);
|
|
|
|
|
|
|
|
// MODAL PRIVATE METHODS
|
|
|
|
// =====================
|
|
|
|
function setModalScrollbar(self) {
|
|
|
|
const { element, scrollbarWidth } = self;
|
|
|
|
const bd = document.body;
|
|
|
|
const html = document.documentElement;
|
|
|
|
const bodyOverflow = html.clientHeight !== html.scrollHeight
|
|
|
|
|| bd.clientHeight !== bd.scrollHeight;
|
|
|
|
const modalOverflow = element.clientHeight !== element.scrollHeight;
|
|
|
|
|
|
|
|
if (!modalOverflow && scrollbarWidth) {
|
|
|
|
element.style.paddingRight = `${scrollbarWidth}px`;
|
|
|
|
}
|
|
|
|
setScrollbar(scrollbarWidth, (modalOverflow || bodyOverflow));
|
|
|
|
}
|
|
|
|
|
|
|
|
function toggleModalDismiss(self, add) {
|
|
|
|
const action = add ? addEventListener : removeEventListener;
|
|
|
|
window[action]('resize', self.update, passiveHandler);
|
|
|
|
self.element[action]('click', modalDismissHandler);
|
|
|
|
document[action]('keydown', modalKeyHandler);
|
|
|
|
}
|
|
|
|
|
|
|
|
function toggleModalHandler(self, add) {
|
|
|
|
const action = add ? addEventListener : removeEventListener;
|
|
|
|
const { triggers } = self;
|
|
|
|
|
|
|
|
if (triggers.length) {
|
|
|
|
triggers.forEach((btn) => btn[action]('click', modalClickHandler));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function afterModalHide(self) {
|
|
|
|
const { triggers } = self;
|
|
|
|
removeOverlay();
|
|
|
|
self.element.style.paddingRight = '';
|
|
|
|
self.isAnimating = false;
|
|
|
|
|
|
|
|
if (triggers.length) {
|
|
|
|
const visibleTrigger = triggers.find((x) => isVisible(x));
|
|
|
|
if (visibleTrigger) setFocus(visibleTrigger);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function afterModalShow(self) {
|
|
|
|
const { element, relatedTarget } = self;
|
|
|
|
setFocus(element);
|
|
|
|
self.isAnimating = false;
|
|
|
|
|
|
|
|
toggleModalDismiss(self, 1);
|
|
|
|
|
|
|
|
shownModalEvent.relatedTarget = relatedTarget;
|
|
|
|
element.dispatchEvent(shownModalEvent);
|
|
|
|
}
|
|
|
|
|
|
|
|
function beforeModalShow(self) {
|
|
|
|
const { element, hasFade } = self;
|
|
|
|
element.style.display = 'block';
|
|
|
|
|
|
|
|
setModalScrollbar(self);
|
|
|
|
if (!queryElement(modalActiveSelector)) {
|
|
|
|
document.body.style.overflow = 'hidden';
|
|
|
|
addClass(document.body, modalOpenClass);
|
|
|
|
}
|
|
|
|
|
|
|
|
addClass(element, showClass);
|
|
|
|
element.removeAttribute(ariaHidden);
|
|
|
|
element.setAttribute(ariaModal, true);
|
|
|
|
|
|
|
|
if (hasFade) emulateTransitionEnd(element, () => afterModalShow(self));
|
|
|
|
else afterModalShow(self);
|
|
|
|
}
|
|
|
|
|
|
|
|
function beforeModalHide(self, force) {
|
|
|
|
const {
|
|
|
|
element, relatedTarget, hasFade,
|
|
|
|
} = self;
|
|
|
|
const currentOpen = getCurrentOpen();
|
|
|
|
|
|
|
|
element.style.display = '';
|
|
|
|
|
|
|
|
// force can also be the transitionEvent object, we wanna make sure it's not
|
|
|
|
// call is not forced and overlay is visible
|
|
|
|
if (!force && hasFade && hasClass(overlay, showClass)
|
|
|
|
&& !currentOpen) { // AND no modal is visible
|
|
|
|
hideOverlay();
|
|
|
|
emulateTransitionEnd(overlay, () => afterModalHide(self));
|
|
|
|
} else {
|
|
|
|
afterModalHide(self);
|
|
|
|
}
|
|
|
|
|
|
|
|
toggleModalDismiss(self);
|
|
|
|
|
|
|
|
hiddenModalEvent.relatedTarget = relatedTarget;
|
|
|
|
element.dispatchEvent(hiddenModalEvent);
|
|
|
|
}
|
|
|
|
|
|
|
|
// MODAL EVENT HANDLERS
|
|
|
|
// ====================
|
|
|
|
function modalClickHandler(e) {
|
|
|
|
const { target } = e;
|
|
|
|
const trigger = target.closest(modalToggleSelector);
|
|
|
|
const element = getTargetElement(trigger);
|
|
|
|
const self = element && element[modalComponent];
|
|
|
|
|
|
|
|
if (trigger.tagName === 'A') e.preventDefault();
|
|
|
|
|
|
|
|
if (self.isAnimating) return;
|
|
|
|
|
|
|
|
self.relatedTarget = trigger;
|
|
|
|
|
|
|
|
self.toggle();
|
|
|
|
}
|
|
|
|
|
|
|
|
function modalKeyHandler({ which }) {
|
|
|
|
const element = queryElement(modalActiveSelector);
|
|
|
|
const self = element[modalComponent];
|
|
|
|
const { options, isAnimating } = self;
|
|
|
|
if (!isAnimating // modal has no animations running
|
|
|
|
&& options.keyboard && which === 27 // the keyboard option is enabled and the key is 27
|
|
|
|
&& hasClass(element, showClass)) { // the modal is not visible
|
|
|
|
self.relatedTarget = null;
|
|
|
|
self.hide();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function modalDismissHandler(e) {
|
|
|
|
const element = this;
|
|
|
|
const self = element[modalComponent];
|
|
|
|
|
|
|
|
if (self.isAnimating) return;
|
|
|
|
|
|
|
|
const { isStatic, modalDialog } = self;
|
|
|
|
const { target } = e;
|
|
|
|
const selectedText = document.getSelection().toString().length;
|
|
|
|
const targetInsideDialog = modalDialog.contains(target);
|
|
|
|
const dismiss = target.closest(modalDismissSelector);
|
|
|
|
|
|
|
|
if (isStatic && !targetInsideDialog) {
|
|
|
|
addClass(element, modalStaticClass);
|
|
|
|
self.isAnimating = true;
|
|
|
|
emulateTransitionEnd(modalDialog, () => staticTransitionEnd(self));
|
|
|
|
} else if (dismiss || (!selectedText && !isStatic && !targetInsideDialog)) {
|
|
|
|
self.relatedTarget = dismiss || null;
|
|
|
|
self.hide();
|
|
|
|
e.preventDefault();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function staticTransitionEnd(self) {
|
|
|
|
const duration = getElementTransitionDuration(self.modalDialog) + 17;
|
|
|
|
removeClass(self.element, modalStaticClass);
|
|
|
|
// user must wait for zoom out transition
|
|
|
|
setTimeout(() => { self.isAnimating = false; }, duration);
|
|
|
|
}
|
|
|
|
|
|
|
|
// MODAL DEFINITION
|
|
|
|
// ================
|
|
|
|
class Modal extends BaseComponent {
|
|
|
|
constructor(target, config) {
|
|
|
|
super(modalComponent, target, modalDefaultOptions, config);
|
|
|
|
|
|
|
|
// bind
|
|
|
|
const self = this;
|
|
|
|
|
|
|
|
// the modal
|
|
|
|
const { element } = self;
|
|
|
|
|
|
|
|
// the modal-dialog
|
|
|
|
self.modalDialog = queryElement(`.${modalString}-dialog`, element);
|
|
|
|
|
|
|
|
// modal can have multiple triggering elements
|
|
|
|
self.triggers = Array.from(document.querySelectorAll(modalToggleSelector))
|
|
|
|
.filter((btn) => getTargetElement(btn) === element);
|
|
|
|
|
|
|
|
// additional internals
|
|
|
|
self.isStatic = self.options.backdrop === 'static';
|
|
|
|
self.hasFade = hasClass(element, fadeClass);
|
|
|
|
self.isAnimating = false;
|
|
|
|
self.scrollbarWidth = measureScrollbar();
|
|
|
|
self.relatedTarget = null;
|
|
|
|
|
|
|
|
// attach event listeners
|
|
|
|
toggleModalHandler(self, 1);
|
|
|
|
|
|
|
|
// bind
|
|
|
|
self.update = self.update.bind(self);
|
|
|
|
}
|
|
|
|
|
|
|
|
// MODAL PUBLIC METHODS
|
|
|
|
// ====================
|
|
|
|
toggle() {
|
|
|
|
const self = this;
|
|
|
|
if (hasClass(self.element, showClass)) self.hide();
|
|
|
|
else self.show();
|
|
|
|
}
|
|
|
|
|
|
|
|
show() {
|
|
|
|
const self = this;
|
|
|
|
const {
|
|
|
|
element, isAnimating, hasFade, relatedTarget,
|
|
|
|
} = self;
|
|
|
|
let overlayDelay = 0;
|
|
|
|
|
|
|
|
if (hasClass(element, showClass) && !isAnimating) return;
|
|
|
|
|
|
|
|
showModalEvent.relatedTarget = relatedTarget || null;
|
|
|
|
element.dispatchEvent(showModalEvent);
|
|
|
|
if (showModalEvent.defaultPrevented) return;
|
|
|
|
|
|
|
|
self.isAnimating = true;
|
|
|
|
|
|
|
|
// we elegantly hide any opened modal/offcanvas
|
|
|
|
const currentOpen = getCurrentOpen();
|
|
|
|
if (currentOpen && currentOpen !== element) {
|
|
|
|
const that = currentOpen[modalComponent]
|
|
|
|
? currentOpen[modalComponent]
|
|
|
|
: currentOpen.Offcanvas;
|
|
|
|
that.hide();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!queryElement(`.${modalBackdropClass}`)) {
|
|
|
|
appendOverlay(hasFade);
|
|
|
|
}
|
|
|
|
overlayDelay = getElementTransitionDuration(overlay);
|
|
|
|
|
|
|
|
if (!hasClass(overlay, showClass)) {
|
|
|
|
showOverlay();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!currentOpen) {
|
|
|
|
setTimeout(() => beforeModalShow(self), overlayDelay);
|
|
|
|
} else beforeModalShow(self);
|
|
|
|
}
|
|
|
|
|
|
|
|
hide(force) {
|
|
|
|
const self = this;
|
|
|
|
const {
|
|
|
|
element, isAnimating, hasFade, relatedTarget,
|
|
|
|
} = self;
|
|
|
|
if (!hasClass(element, showClass) && !isAnimating) return;
|
|
|
|
|
|
|
|
hideModalEvent.relatedTarget = relatedTarget || null;
|
|
|
|
element.dispatchEvent(hideModalEvent);
|
|
|
|
if (hideModalEvent.defaultPrevented) return;
|
|
|
|
|
|
|
|
self.isAnimating = true;
|
|
|
|
removeClass(element, showClass);
|
|
|
|
element.setAttribute(ariaHidden, true);
|
|
|
|
element.removeAttribute(ariaModal);
|
|
|
|
|
|
|
|
if (hasFade && force !== 1) {
|
|
|
|
emulateTransitionEnd(element, () => beforeModalHide(self));
|
|
|
|
} else {
|
|
|
|
beforeModalHide(self, force);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
update() {
|
|
|
|
const self = this;
|
|
|
|
|
|
|
|
if (hasClass(self.element, showClass)) setModalScrollbar(self);
|
|
|
|
}
|
|
|
|
|
|
|
|
dispose() {
|
|
|
|
const self = this;
|
|
|
|
self.hide(1); // forced call
|
|
|
|
|
|
|
|
toggleModalHandler(self);
|
|
|
|
|
|
|
|
super.dispose(modalComponent);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Modal.init = {
|
|
|
|
component: modalComponent,
|
|
|
|
selector: modalSelector,
|
|
|
|
constructor: Modal,
|
|
|
|
};
|
|
|
|
|
|
|
|
/* Native JavaScript for Bootstrap 5 | OffCanvas
|
|
|
|
------------------------------------------------ */
|
|
|
|
|
|
|
|
// OFFCANVAS PRIVATE GC
|
|
|
|
// ====================
|
|
|
|
const offcanvasString = 'offcanvas';
|
|
|
|
const offcanvasComponent = 'Offcanvas';
|
|
|
|
const OffcanvasSelector = `.${offcanvasString}`;
|
|
|
|
const offcanvasToggleSelector = `[${dataBsToggle}="${offcanvasString}"]`;
|
|
|
|
const offcanvasDismissSelector = `[${dataBsDismiss}="${offcanvasString}"]`;
|
|
|
|
const offcanvasTogglingClass = `${offcanvasString}-toggling`;
|
|
|
|
const offcanvasDefaultOptions = {
|
|
|
|
backdrop: true, // boolean
|
|
|
|
keyboard: true, // boolean
|
|
|
|
scroll: false, // boolean
|
|
|
|
};
|
|
|
|
|
|
|
|
// OFFCANVAS CUSTOM EVENTS
|
|
|
|
// =======================
|
|
|
|
const showOffcanvasEvent = bootstrapCustomEvent(`show.bs.${offcanvasString}`);
|
|
|
|
const shownOffcanvasEvent = bootstrapCustomEvent(`shown.bs.${offcanvasString}`);
|
|
|
|
const hideOffcanvasEvent = bootstrapCustomEvent(`hide.bs.${offcanvasString}`);
|
|
|
|
const hiddenOffcanvasEvent = bootstrapCustomEvent(`hidden.bs.${offcanvasString}`);
|
|
|
|
|
|
|
|
// OFFCANVAS PRIVATE METHODS
|
|
|
|
// =========================
|
|
|
|
function setOffCanvasScrollbar(self) {
|
|
|
|
const bd = document.body;
|
|
|
|
const html = document.documentElement;
|
|
|
|
const bodyOverflow = html.clientHeight !== html.scrollHeight
|
|
|
|
|| bd.clientHeight !== bd.scrollHeight;
|
|
|
|
setScrollbar(self.scrollbarWidth, bodyOverflow);
|
|
|
|
}
|
|
|
|
|
|
|
|
function toggleOffcanvasEvents(self, add) {
|
|
|
|
const action = add ? addEventListener : removeEventListener;
|
|
|
|
self.triggers.forEach((btn) => btn[action]('click', offcanvasTriggerHandler));
|
|
|
|
}
|
|
|
|
|
|
|
|
function toggleOffCanvasDismiss(add) {
|
|
|
|
const action = add ? addEventListener : removeEventListener;
|
|
|
|
document[action]('keydown', offcanvasKeyDismissHandler);
|
|
|
|
document[action]('click', offcanvasDismissHandler);
|
|
|
|
}
|
|
|
|
|
|
|
|
function beforeOffcanvasShow(self) {
|
|
|
|
const { element, options } = self;
|
|
|
|
|
|
|
|
if (!options.scroll) {
|
|
|
|
addClass(document.body, modalOpenClass);
|
|
|
|
document.body.style.overflow = 'hidden';
|
|
|
|
setOffCanvasScrollbar(self);
|
|
|
|
}
|
|
|
|
|
|
|
|
addClass(element, offcanvasTogglingClass);
|
|
|
|
addClass(element, showClass);
|
|
|
|
element.style.visibility = 'visible';
|
|
|
|
|
|
|
|
emulateTransitionEnd(element, () => showOffcanvasComplete(self));
|
|
|
|
}
|
|
|
|
|
|
|
|
function beforeOffcanvasHide(self) {
|
|
|
|
const { element, options } = self;
|
|
|
|
const currentOpen = getCurrentOpen();
|
|
|
|
|
|
|
|
element.blur();
|
|
|
|
|
|
|
|
if (!currentOpen && options.backdrop && hasClass(overlay, showClass)) {
|
|
|
|
hideOverlay();
|
|
|
|
emulateTransitionEnd(overlay, () => hideOffcanvasComplete(self));
|
|
|
|
} else hideOffcanvasComplete(self);
|
|
|
|
}
|
|
|
|
|
|
|
|
// OFFCANVAS EVENT HANDLERS
|
|
|
|
// ========================
|
|
|
|
function offcanvasTriggerHandler(e) {
|
|
|
|
const trigger = this.closest(offcanvasToggleSelector);
|
|
|
|
const element = getTargetElement(trigger);
|
|
|
|
const self = element && element[offcanvasComponent];
|
|
|
|
|
|
|
|
if (trigger.tagName === 'A') e.preventDefault();
|
|
|
|
if (self) {
|
|
|
|
self.relatedTarget = trigger;
|
|
|
|
self.toggle();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function offcanvasDismissHandler(e) {
|
|
|
|
const element = queryElement(offcanvasActiveSelector);
|
|
|
|
if (!element) return;
|
|
|
|
|
|
|
|
const offCanvasDismiss = queryElement(offcanvasDismissSelector, element);
|
|
|
|
const self = element[offcanvasComponent];
|
|
|
|
if (!self) return;
|
|
|
|
|
|
|
|
const { options, open, triggers } = self;
|
|
|
|
const { target } = e;
|
|
|
|
const trigger = target.closest(offcanvasToggleSelector);
|
|
|
|
|
|
|
|
if (trigger && trigger.tagName === 'A') e.preventDefault();
|
|
|
|
|
|
|
|
if (open && ((!element.contains(target) && options.backdrop
|
|
|
|
&& (!trigger || (trigger && !triggers.includes(trigger))))
|
|
|
|
|| offCanvasDismiss.contains(target))) {
|
|
|
|
self.relatedTarget = target === offCanvasDismiss ? offCanvasDismiss : null;
|
|
|
|
self.hide();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function offcanvasKeyDismissHandler({ which }) {
|
|
|
|
const element = queryElement(offcanvasActiveSelector);
|
|
|
|
if (!element) return;
|
|
|
|
|
|
|
|
const self = element[offcanvasComponent];
|
|
|
|
|
|
|
|
if (self && self.options.keyboard && which === 27) {
|
|
|
|
self.relatedTarget = null;
|
|
|
|
self.hide();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function showOffcanvasComplete(self) {
|
|
|
|
const { element, triggers, relatedTarget } = self;
|
|
|
|
removeClass(element, offcanvasTogglingClass);
|
|
|
|
|
|
|
|
element.removeAttribute(ariaHidden);
|
|
|
|
element.setAttribute(ariaModal, true);
|
|
|
|
element.setAttribute('role', 'dialog');
|
|
|
|
self.isAnimating = false;
|
|
|
|
|
|
|
|
if (triggers.length) {
|
|
|
|
triggers.forEach((btn) => btn.setAttribute(ariaExpanded, true));
|
|
|
|
}
|
|
|
|
|
|
|
|
shownOffcanvasEvent.relatedTarget = relatedTarget || null;
|
|
|
|
element.dispatchEvent(shownOffcanvasEvent);
|
|
|
|
|
|
|
|
toggleOffCanvasDismiss(1);
|
|
|
|
setFocus(element);
|
|
|
|
}
|
|
|
|
|
|
|
|
function hideOffcanvasComplete(self) {
|
|
|
|
const {
|
|
|
|
element, options, relatedTarget, triggers,
|
|
|
|
} = self;
|
|
|
|
const currentOpen = getCurrentOpen();
|
|
|
|
|
|
|
|
element.setAttribute(ariaHidden, true);
|
|
|
|
element.removeAttribute(ariaModal);
|
|
|
|
element.removeAttribute('role');
|
|
|
|
element.style.visibility = '';
|
|
|
|
self.open = false;
|
|
|
|
self.isAnimating = false;
|
|
|
|
|
|
|
|
if (triggers.length) {
|
|
|
|
triggers.forEach((btn) => btn.setAttribute(ariaExpanded, false));
|
|
|
|
const visibleTrigger = triggers.find((x) => isVisible(x));
|
|
|
|
if (visibleTrigger) setFocus(visibleTrigger);
|
|
|
|
}
|
|
|
|
|
|
|
|
// handle new offcanvas showing up
|
|
|
|
if (!currentOpen) {
|
|
|
|
if (options.backdrop) removeOverlay();
|
|
|
|
if (!options.scroll) {
|
|
|
|
resetScrollbar();
|
|
|
|
removeClass(document.body, modalOpenClass);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
hiddenOffcanvasEvent.relatedTarget = relatedTarget || null;
|
|
|
|
element.dispatchEvent(hiddenOffcanvasEvent);
|
|
|
|
removeClass(element, offcanvasTogglingClass);
|
|
|
|
|
|
|
|
toggleOffCanvasDismiss();
|
|
|
|
}
|
|
|
|
|
|
|
|
// OFFCANVAS DEFINITION
|
|
|
|
// ====================
|
|
|
|
class Offcanvas extends BaseComponent {
|
|
|
|
constructor(target, config) {
|
|
|
|
super(offcanvasComponent, target, offcanvasDefaultOptions, config);
|
|
|
|
const self = this;
|
|
|
|
|
|
|
|
// instance element
|
|
|
|
const { element } = self;
|
|
|
|
|
|
|
|
// all the triggering buttons
|
|
|
|
self.triggers = Array.from(document.querySelectorAll(offcanvasToggleSelector))
|
|
|
|
.filter((btn) => getTargetElement(btn) === element);
|
|
|
|
|
|
|
|
// additional instance property
|
|
|
|
self.open = false;
|
|
|
|
self.isAnimating = false;
|
|
|
|
self.scrollbarWidth = measureScrollbar();
|
|
|
|
|
|
|
|
// attach event listeners
|
|
|
|
toggleOffcanvasEvents(self, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
// OFFCANVAS PUBLIC METHODS
|
|
|
|
// ========================
|
|
|
|
toggle() {
|
|
|
|
const self = this;
|
|
|
|
return self.open ? self.hide() : self.show();
|
|
|
|
}
|
|
|
|
|
|
|
|
show() {
|
|
|
|
const self = this[offcanvasComponent] ? this[offcanvasComponent] : this;
|
|
|
|
const {
|
|
|
|
element, options, isAnimating, relatedTarget,
|
|
|
|
} = self;
|
|
|
|
let overlayDelay = 0;
|
|
|
|
|
|
|
|
if (self.open || isAnimating) return;
|
|
|
|
|
|
|
|
showOffcanvasEvent.relatedTarget = relatedTarget || null;
|
|
|
|
element.dispatchEvent(showOffcanvasEvent);
|
|
|
|
|
|
|
|
if (showOffcanvasEvent.defaultPrevented) return;
|
|
|
|
|
|
|
|
// we elegantly hide any opened modal/offcanvas
|
|
|
|
const currentOpen = getCurrentOpen();
|
|
|
|
if (currentOpen && currentOpen !== element) {
|
|
|
|
const that = currentOpen[offcanvasComponent]
|
|
|
|
? currentOpen[offcanvasComponent]
|
|
|
|
: currentOpen.Modal;
|
|
|
|
that.hide();
|
|
|
|
}
|
|
|
|
|
|
|
|
self.open = true;
|
|
|
|
self.isAnimating = true;
|
|
|
|
|
|
|
|
if (options.backdrop) {
|
|
|
|
if (!queryElement(`.${modalBackdropClass}`)) {
|
|
|
|
appendOverlay(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
overlayDelay = getElementTransitionDuration(overlay);
|
|
|
|
|
|
|
|
if (!hasClass(overlay, showClass)) showOverlay();
|
|
|
|
|
|
|
|
setTimeout(() => beforeOffcanvasShow(self), overlayDelay);
|
|
|
|
} else beforeOffcanvasShow(self);
|
|
|
|
}
|
|
|
|
|
|
|
|
hide(force) {
|
|
|
|
const self = this;
|
|
|
|
const { element, isAnimating, relatedTarget } = self;
|
|
|
|
|
|
|
|
if (!self.open || isAnimating) return;
|
|
|
|
|
|
|
|
hideOffcanvasEvent.relatedTarget = relatedTarget || null;
|
|
|
|
element.dispatchEvent(hideOffcanvasEvent);
|
|
|
|
if (hideOffcanvasEvent.defaultPrevented) return;
|
|
|
|
|
|
|
|
self.isAnimating = true;
|
|
|
|
addClass(element, offcanvasTogglingClass);
|
|
|
|
removeClass(element, showClass);
|
|
|
|
|
|
|
|
if (!force) {
|
|
|
|
emulateTransitionEnd(element, () => beforeOffcanvasHide(self));
|
|
|
|
} else beforeOffcanvasHide(self);
|
|
|
|
}
|
|
|
|
|
|
|
|
dispose() {
|
|
|
|
const self = this;
|
|
|
|
self.hide(1);
|
|
|
|
toggleOffcanvasEvents(self);
|
|
|
|
super.dispose(offcanvasComponent);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Offcanvas.init = {
|
|
|
|
component: offcanvasComponent,
|
|
|
|
selector: OffcanvasSelector,
|
|
|
|
constructor: Offcanvas,
|
|
|
|
};
|
|
|
|
|
|
|
|
const ariaDescribedBy = 'aria-describedby';
|
|
|
|
|
|
|
|
var tipClassPositions = {
|
|
|
|
top: 'top', bottom: 'bottom', left: 'start', right: 'end',
|
|
|
|
};
|
|
|
|
|
|
|
|
function isVisibleTip(tip, container) {
|
|
|
|
return container.contains(tip);
|
|
|
|
}
|
|
|
|
|
|
|
|
function isMedia(element) {
|
|
|
|
return [SVGElement, HTMLImageElement, HTMLVideoElement]
|
|
|
|
.some((mediaType) => element instanceof mediaType);
|
|
|
|
}
|
|
|
|
|
|
|
|
function closestRelative(element) {
|
|
|
|
let retval = null;
|
|
|
|
let el = element;
|
|
|
|
while (el !== document.body) {
|
|
|
|
el = el.parentElement;
|
|
|
|
if (getComputedStyle(el).position === 'relative') {
|
|
|
|
retval = el;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return retval;
|
|
|
|
}
|
|
|
|
|
|
|
|
// both popovers and tooltips (this, event)
|
|
|
|
function styleTip(self, e) {
|
|
|
|
const tipClasses = /\b(top|bottom|start|end)+/;
|
|
|
|
const tip = self.tooltip || self.popover;
|
|
|
|
// reset tip style
|
|
|
|
tip.style.top = '';
|
|
|
|
tip.style.left = '';
|
|
|
|
tip.style.right = '';
|
|
|
|
// continue with metrics
|
|
|
|
const isPopover = !!self.popover;
|
|
|
|
let tipDimensions = { w: tip.offsetWidth, h: tip.offsetHeight };
|
|
|
|
const windowWidth = (document.documentElement.clientWidth || document.body.clientWidth);
|
|
|
|
const windowHeight = (document.documentElement.clientHeight || document.body.clientHeight);
|
|
|
|
const { element, options, arrow } = self;
|
|
|
|
let { container, placement } = options;
|
|
|
|
let parentIsBody = container === document.body;
|
|
|
|
const targetPosition = getComputedStyle(element).position;
|
|
|
|
const parentPosition = getComputedStyle(container).position;
|
|
|
|
const staticParent = !parentIsBody && parentPosition === 'static';
|
|
|
|
let relativeParent = !parentIsBody && parentPosition === 'relative';
|
|
|
|
const relContainer = staticParent && closestRelative(container);
|
|
|
|
// static containers should refer to another relative container or the body
|
|
|
|
container = relContainer || container;
|
|
|
|
relativeParent = staticParent && relContainer ? 1 : relativeParent;
|
|
|
|
parentIsBody = container === document.body;
|
|
|
|
const parentRect = container.getBoundingClientRect();
|
|
|
|
const leftBoundry = relativeParent ? parentRect.left : 0;
|
|
|
|
const rightBoundry = relativeParent ? parentRect.right : windowWidth;
|
|
|
|
// this case should not be possible
|
|
|
|
// absoluteParent = !parentIsBody && parentPosition === 'absolute',
|
|
|
|
// this case requires a container with placement: relative
|
|
|
|
const absoluteTarget = targetPosition === 'absolute';
|
|
|
|
const targetRect = element.getBoundingClientRect();
|
|
|
|
const scroll = parentIsBody
|
|
|
|
? { x: window.pageXOffset, y: window.pageYOffset }
|
|
|
|
: { x: container.scrollLeft, y: container.scrollTop };
|
|
|
|
const elemDimensions = { w: element.offsetWidth, h: element.offsetHeight };
|
|
|
|
const top = relativeParent ? element.offsetTop : targetRect.top;
|
|
|
|
const left = relativeParent ? element.offsetLeft : targetRect.left;
|
|
|
|
// reset arrow style
|
|
|
|
arrow.style.top = '';
|
|
|
|
arrow.style.left = '';
|
|
|
|
arrow.style.right = '';
|
|
|
|
let topPosition;
|
|
|
|
let leftPosition;
|
|
|
|
let rightPosition;
|
|
|
|
let arrowTop;
|
|
|
|
let arrowLeft;
|
|
|
|
let arrowRight;
|
|
|
|
|
|
|
|
// check placement
|
|
|
|
let topExceed = targetRect.top - tipDimensions.h < 0;
|
|
|
|
let bottomExceed = targetRect.top + tipDimensions.h + elemDimensions.h >= windowHeight;
|
|
|
|
let leftExceed = targetRect.left - tipDimensions.w < leftBoundry;
|
|
|
|
let rightExceed = targetRect.left + tipDimensions.w + elemDimensions.w >= rightBoundry;
|
|
|
|
|
|
|
|
topExceed = ['left', 'right'].includes(placement)
|
|
|
|
? targetRect.top + elemDimensions.h / 2 - tipDimensions.h / 2 < 0
|
|
|
|
: topExceed;
|
|
|
|
bottomExceed = ['left', 'right'].includes(placement)
|
|
|
|
? targetRect.top + tipDimensions.h / 2 + elemDimensions.h / 2 >= windowHeight
|
|
|
|
: bottomExceed;
|
|
|
|
leftExceed = ['top', 'bottom'].includes(placement)
|
|
|
|
? targetRect.left + elemDimensions.w / 2 - tipDimensions.w / 2 < leftBoundry
|
|
|
|
: leftExceed;
|
|
|
|
rightExceed = ['top', 'bottom'].includes(placement)
|
|
|
|
? targetRect.left + tipDimensions.w / 2 + elemDimensions.w / 2 >= rightBoundry
|
|
|
|
: rightExceed;
|
|
|
|
|
|
|
|
// recompute placement
|
|
|
|
// first, when both left and right limits are exceeded, we fall back to top|bottom
|
|
|
|
placement = (['left', 'right'].includes(placement)) && leftExceed && rightExceed ? 'top' : placement;
|
|
|
|
placement = placement === 'top' && topExceed ? 'bottom' : placement;
|
|
|
|
placement = placement === 'bottom' && bottomExceed ? 'top' : placement;
|
|
|
|
placement = placement === 'left' && leftExceed ? 'right' : placement;
|
|
|
|
placement = placement === 'right' && rightExceed ? 'left' : placement;
|
|
|
|
|
|
|
|
// update tooltip/popover class
|
|
|
|
if (!tip.className.includes(placement)) {
|
|
|
|
tip.className = tip.className.replace(tipClasses, tipClassPositions[placement]);
|
|
|
|
}
|
|
|
|
// if position has changed, update tip dimensions
|
|
|
|
tipDimensions = { w: tip.offsetWidth, h: tip.offsetHeight };
|
|
|
|
|
|
|
|
// we check the computed width & height and update here
|
|
|
|
const arrowWidth = arrow.offsetWidth || 0;
|
|
|
|
const arrowHeight = arrow.offsetHeight || 0;
|
|
|
|
const arrowAdjust = arrowWidth / 2;
|
|
|
|
|
|
|
|
// compute tooltip / popover coordinates
|
|
|
|
if (['left', 'right'].includes(placement)) { // secondary|side positions
|
|
|
|
if (placement === 'left') { // LEFT
|
|
|
|
leftPosition = left + scroll.x - tipDimensions.w - (isPopover ? arrowWidth : 0);
|
|
|
|
} else { // RIGHT
|
|
|
|
leftPosition = left + scroll.x + elemDimensions.w + (isPopover ? arrowWidth : 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
// adjust top and arrow
|
|
|
|
if (topExceed) {
|
|
|
|
topPosition = top + scroll.y;
|
|
|
|
arrowTop = elemDimensions.h / 2 - arrowWidth;
|
|
|
|
} else if (bottomExceed) {
|
|
|
|
topPosition = top + scroll.y - tipDimensions.h + elemDimensions.h;
|
|
|
|
arrowTop = tipDimensions.h - elemDimensions.h / 2 - arrowWidth;
|
|
|
|
} else {
|
|
|
|
topPosition = top + scroll.y - tipDimensions.h / 2 + elemDimensions.h / 2;
|
|
|
|
arrowTop = tipDimensions.h / 2 - arrowHeight / 2;
|
|
|
|
}
|
|
|
|
} else if (['top', 'bottom'].includes(placement)) {
|
|
|
|
if (e && isMedia(element)) {
|
|
|
|
const eX = !relativeParent ? e.pageX : e.layerX + (absoluteTarget ? element.offsetLeft : 0);
|
|
|
|
const eY = !relativeParent ? e.pageY : e.layerY + (absoluteTarget ? element.offsetTop : 0);
|
|
|
|
|
|
|
|
if (placement === 'top') {
|
|
|
|
topPosition = eY - tipDimensions.h - (isPopover ? arrowWidth : arrowHeight);
|
|
|
|
} else {
|
|
|
|
topPosition = eY + arrowHeight;
|
|
|
|
}
|
|
|
|
|
|
|
|
// adjust left | right and also the arrow
|
|
|
|
if (e.clientX - tipDimensions.w / 2 < leftBoundry) { // when exceeds left
|
|
|
|
leftPosition = 0;
|
|
|
|
arrowLeft = eX - arrowAdjust;
|
|
|
|
} else if (e.clientX + tipDimensions.w * 0.51 >= rightBoundry) { // when exceeds right
|
|
|
|
leftPosition = 'auto';
|
|
|
|
rightPosition = 0;
|
|
|
|
arrowLeft = tipDimensions.w - (rightBoundry - eX) - arrowAdjust;
|
|
|
|
} else { // normal top/bottom
|
|
|
|
leftPosition = eX - tipDimensions.w / 2;
|
|
|
|
arrowLeft = tipDimensions.w / 2 - arrowAdjust;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (placement === 'top') {
|
|
|
|
topPosition = top + scroll.y - tipDimensions.h - (isPopover ? arrowHeight : 0);
|
|
|
|
} else { // BOTTOM
|
|
|
|
topPosition = top + scroll.y + elemDimensions.h + (isPopover ? arrowHeight : 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
// adjust left | right and also the arrow
|
|
|
|
if (leftExceed) {
|
|
|
|
leftPosition = 0;
|
|
|
|
arrowLeft = left + elemDimensions.w / 2 - arrowAdjust;
|
|
|
|
} else if (rightExceed) {
|
|
|
|
leftPosition = 'auto';
|
|
|
|
rightPosition = 0;
|
|
|
|
arrowRight = elemDimensions.w / 2 + (parentRect.right - targetRect.right) - arrowAdjust;
|
|
|
|
} else {
|
|
|
|
leftPosition = left + scroll.x - tipDimensions.w / 2 + elemDimensions.w / 2;
|
|
|
|
arrowLeft = tipDimensions.w / 2 - arrowAdjust;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// apply style to tooltip/popover and its arrow
|
|
|
|
tip.style.top = `${topPosition}px`;
|
|
|
|
tip.style.left = leftPosition === 'auto' ? leftPosition : `${leftPosition}px`;
|
|
|
|
tip.style.right = rightPosition !== undefined ? `${rightPosition}px` : '';
|
|
|
|
// update arrow placement or clear side
|
|
|
|
if (arrowTop !== undefined) {
|
|
|
|
arrow.style.top = `${arrowTop}px`;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (arrowLeft !== undefined) {
|
|
|
|
arrow.style.left = `${arrowLeft}px`;
|
|
|
|
} else if (arrowRight !== undefined) {
|
|
|
|
arrow.style.right = `${arrowRight}px`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let bsnUID = 1;
|
|
|
|
|
|
|
|
// popover, tooltip, scrollspy need a unique id
|
|
|
|
function getUID(element, key) {
|
|
|
|
bsnUID += 1;
|
|
|
|
return element[key] || bsnUID;
|
|
|
|
}
|
|
|
|
|
|
|
|
function getTipContainer(element) {
|
|
|
|
// maybe the element is inside a modal
|
|
|
|
const modal = element.closest('.modal');
|
|
|
|
|
|
|
|
// OR maybe the element is inside a fixed navbar
|
|
|
|
const navbarFixed = element.closest(`.${fixedTopClass},.${fixedBottomClass}`);
|
|
|
|
|
|
|
|
// set default container option appropriate for the context
|
|
|
|
return modal || navbarFixed || document.body;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Native JavaScript for Bootstrap 5 | Popover
|
|
|
|
---------------------------------------------- */
|
|
|
|
|
|
|
|
// POPOVER PRIVATE GC
|
|
|
|
// ==================
|
|
|
|
const popoverString = 'popover';
|
|
|
|
const popoverComponent = 'Popover';
|
|
|
|
const popoverSelector = `[${dataBsToggle}="${popoverString}"],[data-tip="${popoverString}"]`;
|
|
|
|
const popoverDefaultOptions = {
|
|
|
|
template: '<div class="popover" role="tooltip"><div class="popover-arrow"></div><h3 class="popover-header"></h3><div class="popover-body"></div></div>', // string
|
|
|
|
title: null, // string
|
|
|
|
content: null, // string
|
|
|
|
sanitizeFn: null, // function
|
|
|
|
customClass: null, // string
|
|
|
|
dismissible: false, // boolean
|
|
|
|
animation: true, // boolean
|
|
|
|
trigger: 'hover', // string
|
|
|
|
placement: 'top', // string
|
|
|
|
delay: 200, // number
|
|
|
|
};
|
|
|
|
|
|
|
|
// POPOVER PRIVATE GC
|
|
|
|
// ==================
|
|
|
|
const appleBrands = /(iPhone|iPod|iPad)/;
|
|
|
|
const isIphone = navigator.userAgentData
|
|
|
|
? navigator.userAgentData.brands.some((x) => appleBrands.test(x.brand))
|
|
|
|
: appleBrands.test(navigator.userAgent);
|
|
|
|
// popoverArrowClass = `${popoverString}-arrow`,
|
|
|
|
const popoverHeaderClass = `${popoverString}-header`;
|
|
|
|
const popoverBodyClass = `${popoverString}-body`;
|
|
|
|
// close btn for dissmissible popover
|
|
|
|
let popoverCloseButton = '<button type="button" class="btn-close"></button>';
|
|
|
|
|
|
|
|
// POPOVER CUSTOM EVENTS
|
|
|
|
// =====================
|
|
|
|
const showPopoverEvent = bootstrapCustomEvent(`show.bs.${popoverString}`);
|
|
|
|
const shownPopoverEvent = bootstrapCustomEvent(`shown.bs.${popoverString}`);
|
|
|
|
const hidePopoverEvent = bootstrapCustomEvent(`hide.bs.${popoverString}`);
|
|
|
|
const hiddenPopoverEvent = bootstrapCustomEvent(`hidden.bs.${popoverString}`);
|
|
|
|
|
|
|
|
// POPOVER EVENT HANDLERS
|
|
|
|
// ======================
|
|
|
|
function popoverForceFocus() {
|
|
|
|
setFocus(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
function popoverTouchHandler({ target }) {
|
|
|
|
const self = this;
|
|
|
|
const { popover, element } = self;
|
|
|
|
|
|
|
|
if ((popover && popover.contains(target)) // popover includes touch target
|
|
|
|
|| target === element // OR touch target is element
|
|
|
|
|| element.contains(target)) ; else {
|
|
|
|
self.hide();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// POPOVER PRIVATE METHODS
|
|
|
|
// =======================
|
|
|
|
function createPopover(self) {
|
|
|
|
const { id, options } = self;
|
|
|
|
const {
|
|
|
|
animation, customClass, sanitizeFn, placement, dismissible,
|
|
|
|
} = options;
|
|
|
|
let { title, content, template } = options;
|
|
|
|
|
|
|
|
// set initial popover class
|
|
|
|
const placementClass = `bs-${popoverString}-${tipClassPositions[placement]}`;
|
|
|
|
|
|
|
|
// fixing #233
|
|
|
|
title = title ? title.trim() : null;
|
|
|
|
content = content ? content.trim() : null;
|
|
|
|
|
|
|
|
// sanitize title && content
|
|
|
|
if (sanitizeFn) {
|
|
|
|
title = title ? sanitizeFn(title) : null;
|
|
|
|
content = content ? sanitizeFn(content) : null;
|
|
|
|
template = template ? sanitizeFn(template) : null;
|
|
|
|
popoverCloseButton = sanitizeFn(popoverCloseButton);
|
|
|
|
}
|
|
|
|
|
|
|
|
self.popover = document.createElement('div');
|
|
|
|
const { popover } = self;
|
|
|
|
|
|
|
|
// set id and aria-describedby
|
|
|
|
popover.setAttribute('id', id);
|
|
|
|
popover.setAttribute('role', 'tooltip');
|
|
|
|
|
|
|
|
// load template
|
|
|
|
const popoverTemplate = document.createElement('div');
|
|
|
|
popoverTemplate.innerHTML = template.trim();
|
|
|
|
popover.className = popoverTemplate.firstChild.className;
|
|
|
|
popover.innerHTML = popoverTemplate.firstChild.innerHTML;
|
|
|
|
|
|
|
|
const popoverHeader = queryElement(`.${popoverHeaderClass}`, popover);
|
|
|
|
const popoverBody = queryElement(`.${popoverBodyClass}`, popover);
|
|
|
|
|
|
|
|
// set arrow
|
|
|
|
self.arrow = queryElement(`.${popoverString}-arrow`, popover);
|
|
|
|
|
|
|
|
// set dismissible button
|
|
|
|
if (dismissible) {
|
|
|
|
title = title ? title + popoverCloseButton : title;
|
|
|
|
content = title === null ? +popoverCloseButton : content;
|
|
|
|
}
|
|
|
|
|
|
|
|
// fill the template with content from data attributes
|
|
|
|
if (title && popoverHeader) popoverHeader.innerHTML = title.trim();
|
|
|
|
if (content && popoverBody) popoverBody.innerHTML = content.trim();
|
|
|
|
|
|
|
|
// set popover animation and placement
|
|
|
|
if (!hasClass(popover, popoverString)) addClass(popover, popoverString);
|
|
|
|
if (animation && !hasClass(popover, fadeClass)) addClass(popover, fadeClass);
|
|
|
|
if (customClass && !hasClass(popover, customClass)) {
|
|
|
|
addClass(popover, customClass);
|
|
|
|
}
|
|
|
|
if (!hasClass(popover, placementClass)) addClass(popover, placementClass);
|
|
|
|
}
|
|
|
|
|
|
|
|
function removePopover(self) {
|
|
|
|
const { element, popover, options } = self;
|
|
|
|
element.removeAttribute(ariaDescribedBy);
|
|
|
|
options.container.removeChild(popover);
|
|
|
|
self.timer = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
function togglePopoverHandlers(self, add) {
|
|
|
|
const action = add ? addEventListener : removeEventListener;
|
|
|
|
const { element, options } = self;
|
|
|
|
const { trigger, dismissible } = options;
|
|
|
|
self.enabled = !!add;
|
|
|
|
|
|
|
|
if (trigger === 'hover') {
|
|
|
|
element[action]('mousedown', self.show);
|
|
|
|
element[action]('mouseenter', self.show);
|
|
|
|
if (isMedia(element)) element[action]('mousemove', self.update, passiveHandler);
|
|
|
|
if (!dismissible) element[action]('mouseleave', self.hide);
|
|
|
|
} else if (trigger === 'click') {
|
|
|
|
element[action](trigger, self.toggle);
|
|
|
|
} else if (trigger === 'focus') {
|
|
|
|
if (isIphone) element[action]('click', popoverForceFocus);
|
|
|
|
element[action]('focusin', self.show);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function dismissHandlerToggle(self, add) {
|
|
|
|
const action = add ? addEventListener : removeEventListener;
|
|
|
|
const { options, element, popover } = self;
|
|
|
|
const { trigger, dismissible } = options;
|
|
|
|
|
|
|
|
if (dismissible) {
|
|
|
|
const [btnClose] = popover.getElementsByClassName('btn-close');
|
|
|
|
if (btnClose) btnClose[action]('click', self.hide);
|
|
|
|
} else {
|
|
|
|
if (trigger === 'focus') element[action]('focusout', self.hide);
|
|
|
|
if (trigger === 'hover') document[action]('touchstart', popoverTouchHandler, passiveHandler);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!isMedia(element)) {
|
|
|
|
window[action]('scroll', self.update, passiveHandler);
|
|
|
|
window[action]('resize', self.update, passiveHandler);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function popoverShowTrigger(self) {
|
|
|
|
dismissHandlerToggle(self, 1);
|
|
|
|
self.element.dispatchEvent(shownPopoverEvent);
|
|
|
|
}
|
|
|
|
|
|
|
|
function popoverHideTrigger(self) {
|
|
|
|
dismissHandlerToggle(self);
|
|
|
|
removePopover(self);
|
|
|
|
self.element.dispatchEvent(hiddenPopoverEvent);
|
|
|
|
}
|
|
|
|
|
|
|
|
// POPOVER DEFINITION
|
|
|
|
// ==================
|
|
|
|
class Popover extends BaseComponent {
|
|
|
|
constructor(target, config) {
|
|
|
|
popoverDefaultOptions.container = getTipContainer(queryElement(target));
|
|
|
|
super(popoverComponent, target, popoverDefaultOptions, config);
|
|
|
|
|
|
|
|
// bind
|
|
|
|
const self = this;
|
|
|
|
|
|
|
|
// initialization element
|
|
|
|
const { element } = self;
|
|
|
|
// additional instance properties
|
|
|
|
self.timer = null;
|
|
|
|
self.popover = null;
|
|
|
|
self.arrow = null;
|
|
|
|
self.enabled = false;
|
|
|
|
// set unique ID for aria-describedby
|
|
|
|
self.id = `${popoverString}-${getUID(element)}`;
|
|
|
|
|
|
|
|
// set instance options
|
|
|
|
const { options } = self;
|
|
|
|
|
|
|
|
// media elements only work with body as a container
|
|
|
|
self.options.container = isMedia(element)
|
|
|
|
? popoverDefaultOptions.container
|
|
|
|
: queryElement(options.container);
|
|
|
|
|
|
|
|
// reset default container
|
|
|
|
popoverDefaultOptions.container = null;
|
|
|
|
|
|
|
|
// invalidate when no content is set
|
|
|
|
if (!options.content) return;
|
|
|
|
|
|
|
|
// crate popover
|
|
|
|
createPopover(self);
|
|
|
|
|
|
|
|
// bind
|
|
|
|
self.update = self.update.bind(self);
|
|
|
|
|
|
|
|
// attach event listeners
|
|
|
|
togglePopoverHandlers(self, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
update(e) {
|
|
|
|
styleTip(this, e);
|
|
|
|
}
|
|
|
|
|
|
|
|
// POPOVER PUBLIC METHODS
|
|
|
|
// ======================
|
|
|
|
toggle(e) {
|
|
|
|
const self = e ? this[popoverComponent] : this;
|
|
|
|
const { popover, options } = self;
|
|
|
|
if (!isVisibleTip(popover, options.container)) self.show();
|
|
|
|
else self.hide();
|
|
|
|
}
|
|
|
|
|
|
|
|
show(e) {
|
|
|
|
const self = e ? this[popoverComponent] : this;
|
|
|
|
const {
|
|
|
|
element, popover, options, id,
|
|
|
|
} = self;
|
|
|
|
const { container } = options;
|
|
|
|
|
|
|
|
clearTimeout(self.timer);
|
|
|
|
|
|
|
|
self.timer = setTimeout(() => {
|
|
|
|
if (!isVisibleTip(popover, container)) {
|
|
|
|
element.dispatchEvent(showPopoverEvent);
|
|
|
|
if (showPopoverEvent.defaultPrevented) return;
|
|
|
|
|
|
|
|
// append to the container
|
|
|
|
container.appendChild(popover);
|
|
|
|
element.setAttribute(ariaDescribedBy, id);
|
|
|
|
|
|
|
|
self.update(e);
|
|
|
|
if (!hasClass(popover, showClass)) addClass(popover, showClass);
|
|
|
|
|
|
|
|
if (options.animation) emulateTransitionEnd(popover, () => popoverShowTrigger(self));
|
|
|
|
else popoverShowTrigger(self);
|
|
|
|
}
|
|
|
|
}, 17);
|
|
|
|
}
|
|
|
|
|
|
|
|
hide(e) {
|
|
|
|
let self;
|
|
|
|
if (e && this[popoverComponent]) {
|
|
|
|
self = this[popoverComponent];
|
|
|
|
} else if (e) { // dismissible popover
|
|
|
|
const dPopover = this.closest(`.${popoverString}`);
|
|
|
|
const dEl = dPopover && queryElement(`[${ariaDescribedBy}="${dPopover.id}"]`);
|
|
|
|
self = dEl[popoverComponent];
|
|
|
|
} else {
|
|
|
|
self = this;
|
|
|
|
}
|
|
|
|
const { element, popover, options } = self;
|
|
|
|
|
|
|
|
clearTimeout(self.timer);
|
|
|
|
|
|
|
|
self.timer = setTimeout(() => {
|
|
|
|
if (isVisibleTip(popover, options.container)) {
|
|
|
|
element.dispatchEvent(hidePopoverEvent);
|
|
|
|
if (hidePopoverEvent.defaultPrevented) return;
|
|
|
|
|
|
|
|
removeClass(popover, showClass);
|
|
|
|
|
|
|
|
if (options.animation) emulateTransitionEnd(popover, () => popoverHideTrigger(self));
|
|
|
|
else popoverHideTrigger(self);
|
|
|
|
}
|
|
|
|
}, options.delay + 17);
|
|
|
|
}
|
|
|
|
|
|
|
|
enable() {
|
|
|
|
const self = this;
|
|
|
|
const { enabled } = self;
|
|
|
|
if (!enabled) {
|
|
|
|
togglePopoverHandlers(self, 1);
|
|
|
|
self.enabled = !enabled;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
disable() {
|
|
|
|
const self = this;
|
|
|
|
const { enabled, popover, options } = self;
|
|
|
|
if (enabled) {
|
|
|
|
if (isVisibleTip(popover, options.container) && options.animation) {
|
|
|
|
self.hide();
|
|
|
|
|
|
|
|
setTimeout(
|
|
|
|
() => togglePopoverHandlers(self),
|
|
|
|
getElementTransitionDuration(popover) + options.delay + 17,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
togglePopoverHandlers(self);
|
|
|
|
}
|
|
|
|
self.enabled = !enabled;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
toggleEnabled() {
|
|
|
|
const self = this;
|
|
|
|
if (!self.enabled) self.enable();
|
|
|
|
else self.disable();
|
|
|
|
}
|
|
|
|
|
|
|
|
dispose() {
|
|
|
|
const self = this;
|
|
|
|
const { popover, options } = self;
|
|
|
|
const { container, animation } = options;
|
|
|
|
if (animation && isVisibleTip(popover, container)) {
|
|
|
|
options.delay = 0; // reset delay
|
|
|
|
self.hide();
|
|
|
|
emulateTransitionEnd(popover, () => togglePopoverHandlers(self));
|
|
|
|
} else {
|
|
|
|
togglePopoverHandlers(self);
|
|
|
|
}
|
|
|
|
super.dispose(popoverComponent);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Popover.init = {
|
|
|
|
component: popoverComponent,
|
|
|
|
selector: popoverSelector,
|
|
|
|
constructor: Popover,
|
|
|
|
};
|
|
|
|
|
|
|
|
/* Native JavaScript for Bootstrap 5 | ScrollSpy
|
|
|
|
------------------------------------------------ */
|
|
|
|
|
|
|
|
// SCROLLSPY PRIVATE GC
|
|
|
|
// ====================
|
|
|
|
const scrollspyString = 'scrollspy';
|
|
|
|
const scrollspyComponent = 'ScrollSpy';
|
|
|
|
const scrollspySelector = '[data-bs-spy="scroll"]';
|
|
|
|
const scrollSpyDefaultOptions = {
|
|
|
|
offset: 10,
|
|
|
|
target: null,
|
|
|
|
};
|
|
|
|
|
|
|
|
// SCROLLSPY CUSTOM EVENT
|
|
|
|
// ======================
|
|
|
|
const activateScrollSpy = bootstrapCustomEvent(`activate.bs.${scrollspyString}`);
|
|
|
|
|
|
|
|
// SCROLLSPY PRIVATE METHODS
|
|
|
|
// =========================
|
|
|
|
function updateSpyTargets(self) {
|
|
|
|
const {
|
|
|
|
target, scrollTarget, isWindow, options, itemsLength, scrollHeight,
|
|
|
|
} = self;
|
|
|
|
const { offset } = options;
|
|
|
|
const links = target.getElementsByTagName('A');
|
|
|
|
|
|
|
|
self.scrollTop = isWindow
|
|
|
|
? scrollTarget.pageYOffset
|
|
|
|
: scrollTarget.scrollTop;
|
|
|
|
|
|
|
|
// only update items/offsets once or with each mutation
|
|
|
|
if (itemsLength !== links.length || getScrollHeight(scrollTarget) !== scrollHeight) {
|
|
|
|
let href;
|
|
|
|
let targetItem;
|
|
|
|
let rect;
|
|
|
|
|
|
|
|
// reset arrays & update
|
|
|
|
self.items = [];
|
|
|
|
self.offsets = [];
|
|
|
|
self.scrollHeight = getScrollHeight(scrollTarget);
|
|
|
|
self.maxScroll = self.scrollHeight - getOffsetHeight(self);
|
|
|
|
|
|
|
|
Array.from(links).forEach((link) => {
|
|
|
|
href = link.getAttribute('href');
|
|
|
|
targetItem = href && href.charAt(0) === '#' && href.slice(-1) !== '#' && queryElement(href);
|
|
|
|
|
|
|
|
if (targetItem) {
|
|
|
|
self.items.push(link);
|
|
|
|
rect = targetItem.getBoundingClientRect();
|
|
|
|
self.offsets.push((isWindow ? rect.top + self.scrollTop : targetItem.offsetTop) - offset);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
self.itemsLength = self.items.length;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function getScrollHeight(scrollTarget) {
|
|
|
|
return scrollTarget.scrollHeight || Math.max(
|
|
|
|
document.body.scrollHeight,
|
|
|
|
document.documentElement.scrollHeight,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function getOffsetHeight({ element, isWindow }) {
|
|
|
|
if (!isWindow) return element.getBoundingClientRect().height;
|
|
|
|
return window.innerHeight;
|
|
|
|
}
|
|
|
|
|
|
|
|
function clear(target) {
|
|
|
|
Array.from(target.getElementsByTagName('A')).forEach((item) => {
|
|
|
|
if (hasClass(item, activeClass)) removeClass(item, activeClass);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function activate(self, item) {
|
|
|
|
const { target, element } = self;
|
|
|
|
clear(target);
|
|
|
|
self.activeItem = item;
|
|
|
|
addClass(item, activeClass);
|
|
|
|
|
|
|
|
// activate all parents
|
|
|
|
const parents = [];
|
|
|
|
let parentItem = item;
|
|
|
|
while (parentItem !== document.body) {
|
|
|
|
parentItem = parentItem.parentNode;
|
|
|
|
if (hasClass(parentItem, 'nav') || hasClass(parentItem, 'dropdown-menu')) parents.push(parentItem);
|
|
|
|
}
|
|
|
|
|
|
|
|
parents.forEach((menuItem) => {
|
|
|
|
const parentLink = menuItem.previousElementSibling;
|
|
|
|
|
|
|
|
if (parentLink && !hasClass(parentLink, activeClass)) {
|
|
|
|
addClass(parentLink, activeClass);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// update relatedTarget and dispatch
|
|
|
|
activateScrollSpy.relatedTarget = item;
|
|
|
|
element.dispatchEvent(activateScrollSpy);
|
|
|
|
}
|
|
|
|
|
|
|
|
function toggleSpyHandlers(self, add) {
|
|
|
|
const action = add ? addEventListener : removeEventListener;
|
|
|
|
self.scrollTarget[action]('scroll', self.refresh, passiveHandler);
|
|
|
|
}
|
|
|
|
|
|
|
|
// SCROLLSPY DEFINITION
|
|
|
|
// ====================
|
|
|
|
class ScrollSpy extends BaseComponent {
|
|
|
|
constructor(target, config) {
|
|
|
|
super(scrollspyComponent, target, scrollSpyDefaultOptions, config);
|
|
|
|
// bind
|
|
|
|
const self = this;
|
|
|
|
|
|
|
|
// initialization element & options
|
|
|
|
const { element, options } = self;
|
|
|
|
|
|
|
|
// additional properties
|
|
|
|
self.target = queryElement(options.target);
|
|
|
|
|
|
|
|
// invalidate
|
|
|
|
if (!self.target) return;
|
|
|
|
|
|
|
|
// set initial state
|
|
|
|
self.scrollTarget = element.clientHeight < element.scrollHeight ? element : window;
|
|
|
|
self.isWindow = self.scrollTarget === window;
|
|
|
|
self.scrollTop = 0;
|
|
|
|
self.maxScroll = 0;
|
|
|
|
self.scrollHeight = 0;
|
|
|
|
self.activeItem = null;
|
|
|
|
self.items = [];
|
|
|
|
self.offsets = [];
|
|
|
|
|
|
|
|
// bind events
|
|
|
|
self.refresh = self.refresh.bind(self);
|
|
|
|
|
|
|
|
// add event handlers
|
|
|
|
toggleSpyHandlers(self, 1);
|
|
|
|
|
|
|
|
self.refresh();
|
|
|
|
}
|
|
|
|
|
|
|
|
// SCROLLSPY PUBLIC METHODS
|
|
|
|
// ========================
|
|
|
|
refresh() {
|
|
|
|
const self = this;
|
|
|
|
const { target } = self;
|
|
|
|
|
|
|
|
// check if target is visible and invalidate
|
|
|
|
if (target.offsetHeight === 0) return;
|
|
|
|
|
|
|
|
updateSpyTargets(self);
|
|
|
|
|
|
|
|
const {
|
|
|
|
scrollTop, maxScroll, itemsLength, items, activeItem,
|
|
|
|
} = self;
|
|
|
|
|
|
|
|
if (scrollTop >= maxScroll) {
|
|
|
|
const newActiveItem = items[itemsLength - 1];
|
|
|
|
|
|
|
|
if (activeItem !== newActiveItem) {
|
|
|
|
activate(self, newActiveItem);
|
2019-08-31 17:47:52 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
return;
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
|
|
|
|
const { offsets } = self;
|
|
|
|
|
|
|
|
if (activeItem && scrollTop < offsets[0] && offsets[0] > 0) {
|
|
|
|
self.activeItem = null;
|
|
|
|
clear(target);
|
|
|
|
return;
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
|
|
|
|
items.forEach((item, i) => {
|
|
|
|
if (activeItem !== item && scrollTop >= offsets[i]
|
|
|
|
&& (typeof offsets[i + 1] === 'undefined' || scrollTop < offsets[i + 1])) {
|
|
|
|
activate(self, item);
|
|
|
|
}
|
|
|
|
});
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
|
|
|
|
dispose() {
|
|
|
|
toggleSpyHandlers(this);
|
|
|
|
super.dispose(scrollspyComponent);
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
ScrollSpy.init = {
|
|
|
|
component: scrollspyComponent,
|
|
|
|
selector: scrollspySelector,
|
|
|
|
constructor: ScrollSpy,
|
|
|
|
};
|
|
|
|
|
|
|
|
const ariaSelected = 'aria-selected';
|
|
|
|
|
|
|
|
/* Native JavaScript for Bootstrap 5 | Tab
|
|
|
|
------------------------------------------ */
|
|
|
|
|
|
|
|
// TAB PRIVATE GC
|
|
|
|
// ================
|
|
|
|
const tabString = 'tab';
|
|
|
|
const tabComponent = 'Tab';
|
|
|
|
const tabSelector = `[${dataBsToggle}="${tabString}"]`;
|
|
|
|
|
|
|
|
// TAB CUSTOM EVENTS
|
|
|
|
// =================
|
|
|
|
const showTabEvent = bootstrapCustomEvent(`show.bs.${tabString}`);
|
|
|
|
const shownTabEvent = bootstrapCustomEvent(`shown.bs.${tabString}`);
|
|
|
|
const hideTabEvent = bootstrapCustomEvent(`hide.bs.${tabString}`);
|
|
|
|
const hiddenTabEvent = bootstrapCustomEvent(`hidden.bs.${tabString}`);
|
|
|
|
|
|
|
|
let nextTab;
|
|
|
|
let nextTabContent;
|
|
|
|
let nextTabHeight;
|
|
|
|
let activeTab;
|
|
|
|
let activeTabContent;
|
|
|
|
let tabContainerHeight;
|
|
|
|
let tabEqualContents;
|
|
|
|
|
|
|
|
// TAB PRIVATE METHODS
|
|
|
|
// ===================
|
|
|
|
function triggerTabEnd(self) {
|
|
|
|
const { tabContent, nav } = self;
|
|
|
|
tabContent.style.height = '';
|
|
|
|
removeClass(tabContent, collapsingClass);
|
|
|
|
nav.isAnimating = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
function triggerTabShow(self) {
|
|
|
|
const { tabContent, nav } = self;
|
|
|
|
|
|
|
|
if (tabContent) { // height animation
|
|
|
|
if (tabEqualContents) {
|
|
|
|
triggerTabEnd(self);
|
2020-06-01 18:58:38 +02:00
|
|
|
} else {
|
2021-06-19 19:22:19 +02:00
|
|
|
setTimeout(() => { // enables height animation
|
|
|
|
tabContent.style.height = `${nextTabHeight}px`; // height animation
|
|
|
|
reflow(tabContent);
|
|
|
|
emulateTransitionEnd(tabContent, () => triggerTabEnd(self));
|
|
|
|
}, 50);
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
} else {
|
|
|
|
nav.isAnimating = false;
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
shownTabEvent.relatedTarget = activeTab;
|
|
|
|
nextTab.dispatchEvent(shownTabEvent);
|
|
|
|
}
|
|
|
|
|
|
|
|
function triggerTabHide(self) {
|
|
|
|
const { tabContent } = self;
|
|
|
|
if (tabContent) {
|
|
|
|
activeTabContent.style.float = 'left';
|
|
|
|
nextTabContent.style.float = 'left';
|
|
|
|
tabContainerHeight = activeTabContent.scrollHeight;
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
|
|
|
|
// update relatedTarget and dispatch event
|
|
|
|
showTabEvent.relatedTarget = activeTab;
|
|
|
|
hiddenTabEvent.relatedTarget = nextTab;
|
|
|
|
nextTab.dispatchEvent(showTabEvent);
|
|
|
|
if (showTabEvent.defaultPrevented) return;
|
|
|
|
|
|
|
|
addClass(nextTabContent, activeClass);
|
|
|
|
removeClass(activeTabContent, activeClass);
|
|
|
|
|
|
|
|
if (tabContent) {
|
|
|
|
nextTabHeight = nextTabContent.scrollHeight;
|
|
|
|
tabEqualContents = nextTabHeight === tabContainerHeight;
|
|
|
|
addClass(tabContent, collapsingClass);
|
|
|
|
tabContent.style.height = `${tabContainerHeight}px`; // height animation
|
|
|
|
reflow(tabContent);
|
|
|
|
activeTabContent.style.float = '';
|
|
|
|
nextTabContent.style.float = '';
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
|
|
|
|
if (hasClass(nextTabContent, fadeClass)) {
|
|
|
|
setTimeout(() => {
|
|
|
|
addClass(nextTabContent, showClass);
|
|
|
|
emulateTransitionEnd(nextTabContent, () => {
|
|
|
|
triggerTabShow(self);
|
2020-06-01 18:58:38 +02:00
|
|
|
});
|
2021-06-19 19:22:19 +02:00
|
|
|
}, 20);
|
|
|
|
} else { triggerTabShow(self); }
|
|
|
|
|
|
|
|
activeTab.dispatchEvent(hiddenTabEvent);
|
|
|
|
}
|
|
|
|
|
|
|
|
function getActiveTab({ nav }) {
|
|
|
|
const activeTabs = nav.getElementsByClassName(activeClass);
|
|
|
|
|
|
|
|
if (activeTabs.length === 1
|
|
|
|
&& !dropdownMenuClasses.some((c) => hasClass(activeTabs[0].parentNode, c))) {
|
|
|
|
[activeTab] = activeTabs;
|
|
|
|
} else if (activeTabs.length > 1) {
|
|
|
|
activeTab = activeTabs[activeTabs.length - 1];
|
|
|
|
}
|
|
|
|
return activeTab;
|
|
|
|
}
|
|
|
|
|
|
|
|
function getActiveTabContent(self) {
|
|
|
|
return queryElement(getActiveTab(self).getAttribute('href'));
|
|
|
|
}
|
|
|
|
|
|
|
|
function toggleTabHandler(self, add) {
|
|
|
|
const action = add ? addEventListener : removeEventListener;
|
|
|
|
self.element[action]('click', tabClickHandler);
|
|
|
|
}
|
|
|
|
|
|
|
|
// TAB EVENT HANDLER
|
|
|
|
// =================
|
|
|
|
function tabClickHandler(e) {
|
|
|
|
const self = this[tabComponent];
|
|
|
|
e.preventDefault();
|
|
|
|
if (!self.nav.isAnimating) self.show();
|
|
|
|
}
|
|
|
|
|
|
|
|
// TAB DEFINITION
|
|
|
|
// ==============
|
|
|
|
class Tab extends BaseComponent {
|
|
|
|
constructor(target) {
|
|
|
|
super(tabComponent, target);
|
|
|
|
// bind
|
|
|
|
const self = this;
|
|
|
|
|
|
|
|
// initialization element
|
|
|
|
const { element } = self;
|
|
|
|
|
|
|
|
// event targets
|
|
|
|
self.nav = element.closest('.nav');
|
|
|
|
const { nav } = self;
|
|
|
|
self.dropdown = nav && queryElement(`.${dropdownMenuClasses[0]}-toggle`, nav);
|
|
|
|
activeTabContent = getActiveTabContent(self);
|
|
|
|
self.tabContent = supportTransition && activeTabContent.closest('.tab-content');
|
|
|
|
tabContainerHeight = activeTabContent.scrollHeight;
|
|
|
|
|
|
|
|
// set default animation state
|
|
|
|
nav.isAnimating = false;
|
|
|
|
|
|
|
|
// add event listener
|
|
|
|
toggleTabHandler(self, 1);
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
|
|
|
|
// TAB PUBLIC METHODS
|
|
|
|
// ==================
|
|
|
|
show() { // the tab we clicked is now the nextTab tab
|
|
|
|
const self = this;
|
|
|
|
const { element, nav, dropdown } = self;
|
|
|
|
nextTab = element;
|
|
|
|
if (!hasClass(nextTab, activeClass)) {
|
|
|
|
// this is the actual object, the nextTab tab content to activate
|
|
|
|
nextTabContent = queryElement(nextTab.getAttribute('href'));
|
|
|
|
activeTab = getActiveTab({ nav });
|
|
|
|
activeTabContent = getActiveTabContent({ nav });
|
|
|
|
|
|
|
|
// update relatedTarget and dispatch
|
|
|
|
hideTabEvent.relatedTarget = nextTab;
|
|
|
|
activeTab.dispatchEvent(hideTabEvent);
|
|
|
|
if (hideTabEvent.defaultPrevented) return;
|
|
|
|
|
|
|
|
nav.isAnimating = true;
|
|
|
|
removeClass(activeTab, activeClass);
|
|
|
|
activeTab.setAttribute(ariaSelected, 'false');
|
|
|
|
addClass(nextTab, activeClass);
|
|
|
|
nextTab.setAttribute(ariaSelected, 'true');
|
|
|
|
|
|
|
|
if (dropdown) {
|
|
|
|
if (!hasClass(element.parentNode, dropdownMenuClass)) {
|
|
|
|
if (hasClass(dropdown, activeClass)) removeClass(dropdown, activeClass);
|
|
|
|
} else if (!hasClass(dropdown, activeClass)) addClass(dropdown, activeClass);
|
2019-08-31 17:47:52 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
|
|
|
|
if (hasClass(activeTabContent, fadeClass)) {
|
|
|
|
removeClass(activeTabContent, showClass);
|
|
|
|
emulateTransitionEnd(activeTabContent, () => triggerTabHide(self));
|
|
|
|
} else {
|
|
|
|
triggerTabHide(self);
|
2019-08-31 17:47:52 +02:00
|
|
|
}
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2019-08-31 17:47:52 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
|
|
|
|
dispose() {
|
|
|
|
toggleTabHandler(this);
|
|
|
|
super.dispose(tabComponent);
|
2019-08-31 17:47:52 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
Tab.init = {
|
|
|
|
component: tabComponent,
|
|
|
|
selector: tabSelector,
|
|
|
|
constructor: Tab,
|
|
|
|
};
|
|
|
|
|
|
|
|
/* Native JavaScript for Bootstrap 5 | Toast
|
|
|
|
-------------------------------------------- */
|
|
|
|
|
|
|
|
// TOAST PRIVATE GC
|
|
|
|
// ================
|
|
|
|
const toastString = 'toast';
|
|
|
|
const toastComponent = 'Toast';
|
|
|
|
const toastSelector = `.${toastString}`;
|
|
|
|
const toastDismissSelector = `[${dataBsDismiss}="${toastString}"]`;
|
|
|
|
const showingClass = 'showing';
|
|
|
|
const hideClass = 'hide';
|
|
|
|
const toastDefaultOptions = {
|
|
|
|
animation: true,
|
|
|
|
autohide: true,
|
|
|
|
delay: 500,
|
|
|
|
};
|
|
|
|
|
|
|
|
// TOAST CUSTOM EVENTS
|
|
|
|
// ===================
|
|
|
|
const showToastEvent = bootstrapCustomEvent(`show.bs.${toastString}`);
|
|
|
|
const hideToastEvent = bootstrapCustomEvent(`hide.bs.${toastString}`);
|
|
|
|
const shownToastEvent = bootstrapCustomEvent(`shown.bs.${toastString}`);
|
|
|
|
const hiddenToastEvent = bootstrapCustomEvent(`hidden.bs.${toastString}`);
|
|
|
|
|
|
|
|
// TOAST PRIVATE METHODS
|
|
|
|
// =====================
|
|
|
|
function showToastComplete(self) {
|
|
|
|
const { element, options } = self;
|
|
|
|
if (!options.animation) {
|
|
|
|
removeClass(element, showingClass);
|
|
|
|
addClass(element, showClass);
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
|
|
|
|
element.dispatchEvent(shownToastEvent);
|
|
|
|
if (options.autohide) self.hide();
|
|
|
|
}
|
|
|
|
|
|
|
|
function hideToastComplete(self) {
|
|
|
|
const { element } = self;
|
|
|
|
addClass(element, hideClass);
|
|
|
|
element.dispatchEvent(hiddenToastEvent);
|
|
|
|
}
|
|
|
|
|
|
|
|
function closeToast(self) {
|
|
|
|
const { element, options } = self;
|
|
|
|
removeClass(element, showClass);
|
|
|
|
|
|
|
|
if (options.animation) {
|
|
|
|
reflow(element);
|
|
|
|
emulateTransitionEnd(element, () => hideToastComplete(self));
|
|
|
|
} else {
|
|
|
|
hideToastComplete(self);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function openToast(self) {
|
|
|
|
const { element, options } = self;
|
|
|
|
removeClass(element, hideClass);
|
|
|
|
|
|
|
|
if (options.animation) {
|
|
|
|
reflow(element);
|
|
|
|
addClass(element, showingClass);
|
|
|
|
addClass(element, showClass);
|
|
|
|
|
|
|
|
emulateTransitionEnd(element, () => showToastComplete(self));
|
|
|
|
} else {
|
|
|
|
showToastComplete(self);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function toggleToastHandler(self, add) {
|
|
|
|
const action = add ? addEventListener : removeEventListener;
|
|
|
|
if (self.dismiss) {
|
|
|
|
self.dismiss[action]('click', self.hide);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TOAST EVENT HANDLERS
|
|
|
|
// ====================
|
|
|
|
function completeDisposeToast(self) {
|
|
|
|
clearTimeout(self.timer);
|
|
|
|
toggleToastHandler(self);
|
|
|
|
}
|
|
|
|
|
|
|
|
// TOAST DEFINITION
|
|
|
|
// ================
|
|
|
|
class Toast extends BaseComponent {
|
|
|
|
constructor(target, config) {
|
|
|
|
super(toastComponent, target, toastDefaultOptions, config);
|
|
|
|
// bind
|
|
|
|
const self = this;
|
|
|
|
|
|
|
|
// dismiss button
|
|
|
|
self.dismiss = queryElement(toastDismissSelector, self.element);
|
|
|
|
|
|
|
|
// bind
|
|
|
|
self.show = self.show.bind(self);
|
|
|
|
self.hide = self.hide.bind(self);
|
|
|
|
|
|
|
|
// add event listener
|
|
|
|
toggleToastHandler(self, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
// TOAST PUBLIC METHODS
|
|
|
|
// ====================
|
|
|
|
show() {
|
|
|
|
const self = this;
|
|
|
|
const { element } = self;
|
|
|
|
if (element && hasClass(element, hideClass)) {
|
|
|
|
element.dispatchEvent(showToastEvent);
|
|
|
|
if (showToastEvent.defaultPrevented) return;
|
|
|
|
|
|
|
|
addClass(element, fadeClass);
|
|
|
|
clearTimeout(self.timer);
|
|
|
|
self.timer = setTimeout(() => openToast(self), 10);
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
hide(noTimer) {
|
|
|
|
const self = this;
|
|
|
|
const { element, options } = self;
|
|
|
|
|
|
|
|
if (element && hasClass(element, showClass)) {
|
|
|
|
element.dispatchEvent(hideToastEvent);
|
|
|
|
if (hideToastEvent.defaultPrevented) return;
|
|
|
|
|
|
|
|
clearTimeout(self.timer);
|
|
|
|
self.timer = setTimeout(() => closeToast(self), // Bugfix by BlackDex to get autohide with a delay working.
|
|
|
|
noTimer ? 10 : options.delay,
|
|
|
|
);
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
|
|
|
|
dispose() {
|
|
|
|
const self = this;
|
|
|
|
const { element, options } = self;
|
|
|
|
self.hide();
|
|
|
|
|
|
|
|
if (options.animation) emulateTransitionEnd(element, () => completeDisposeToast(self));
|
|
|
|
else completeDisposeToast(self);
|
|
|
|
|
|
|
|
super.dispose(toastComponent);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Toast.init = {
|
|
|
|
component: toastComponent,
|
|
|
|
selector: toastSelector,
|
|
|
|
constructor: Toast,
|
|
|
|
};
|
|
|
|
|
|
|
|
const dataOriginalTitle = 'data-original-title';
|
|
|
|
|
|
|
|
/* Native JavaScript for Bootstrap 5 | Tooltip
|
|
|
|
---------------------------------------------- */
|
|
|
|
|
|
|
|
// TOOLTIP PRIVATE GC
|
|
|
|
// ==================
|
|
|
|
const tooltipString = 'tooltip';
|
|
|
|
const tooltipComponent = 'Tooltip';
|
|
|
|
const tooltipSelector = `[${dataBsToggle}="${tooltipString}"],[data-tip="${tooltipString}"]`;
|
|
|
|
|
|
|
|
const titleAttr = 'title';
|
|
|
|
const tooltipInnerClass = `${tooltipString}-inner`;
|
|
|
|
const tooltipDefaultOptions = {
|
|
|
|
title: null,
|
|
|
|
template: '<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
|
|
|
|
placement: 'top',
|
|
|
|
animation: true,
|
|
|
|
customClass: null,
|
|
|
|
delay: 200,
|
|
|
|
sanitizeFn: null,
|
|
|
|
};
|
|
|
|
|
|
|
|
// TOOLTIP CUSTOM EVENTS
|
|
|
|
// =====================
|
|
|
|
const showTooltipEvent = bootstrapCustomEvent(`show.bs.${tooltipString}`);
|
|
|
|
const shownTooltipEvent = bootstrapCustomEvent(`shown.bs.${tooltipString}`);
|
|
|
|
const hideTooltipEvent = bootstrapCustomEvent(`hide.bs.${tooltipString}`);
|
|
|
|
const hiddenTooltipEvent = bootstrapCustomEvent(`hidden.bs.${tooltipString}`);
|
|
|
|
|
|
|
|
// TOOLTIP PRIVATE METHODS
|
|
|
|
// =======================
|
|
|
|
function createTooltip(self) {
|
|
|
|
const { options, id } = self;
|
|
|
|
const placementClass = `bs-${tooltipString}-${tipClassPositions[options.placement]}`;
|
|
|
|
let titleString = options.title.trim();
|
|
|
|
|
|
|
|
// sanitize stuff
|
|
|
|
if (options.sanitizeFn) {
|
|
|
|
titleString = options.sanitizeFn(titleString);
|
|
|
|
options.template = options.sanitizeFn(options.template);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!titleString) return;
|
|
|
|
|
|
|
|
// create tooltip
|
|
|
|
self.tooltip = document.createElement('div');
|
|
|
|
const { tooltip } = self;
|
|
|
|
|
|
|
|
// set aria
|
|
|
|
tooltip.setAttribute('id', id);
|
|
|
|
|
|
|
|
// set markup
|
|
|
|
const tooltipMarkup = document.createElement('div');
|
|
|
|
tooltipMarkup.innerHTML = options.template.trim();
|
|
|
|
|
|
|
|
tooltip.className = tooltipMarkup.firstChild.className;
|
|
|
|
tooltip.innerHTML = tooltipMarkup.firstChild.innerHTML;
|
|
|
|
|
|
|
|
queryElement(`.${tooltipInnerClass}`, tooltip).innerHTML = titleString;
|
|
|
|
|
|
|
|
// set arrow
|
|
|
|
self.arrow = queryElement(`.${tooltipString}-arrow`, tooltip);
|
|
|
|
|
|
|
|
// set class and role attribute
|
|
|
|
tooltip.setAttribute('role', tooltipString);
|
|
|
|
// set classes
|
|
|
|
if (!hasClass(tooltip, tooltipString)) addClass(tooltip, tooltipString);
|
|
|
|
if (options.animation && !hasClass(tooltip, fadeClass)) addClass(tooltip, fadeClass);
|
|
|
|
if (options.customClass && !hasClass(tooltip, options.customClass)) {
|
|
|
|
addClass(tooltip, options.customClass);
|
|
|
|
}
|
|
|
|
if (!hasClass(tooltip, placementClass)) addClass(tooltip, placementClass);
|
|
|
|
}
|
|
|
|
|
|
|
|
function removeTooltip(self) {
|
|
|
|
const { element, options, tooltip } = self;
|
|
|
|
element.removeAttribute(ariaDescribedBy);
|
|
|
|
options.container.removeChild(tooltip);
|
|
|
|
self.timer = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
function disposeTooltipComplete(self) {
|
|
|
|
const { element } = self;
|
|
|
|
toggleTooltipHandlers(self);
|
|
|
|
if (element.hasAttribute(dataOriginalTitle)) toggleTooltipTitle(self);
|
|
|
|
}
|
|
|
|
function toggleTooltipAction(self, add) {
|
|
|
|
const action = add ? addEventListener : removeEventListener;
|
|
|
|
|
|
|
|
document[action]('touchstart', tooltipTouchHandler, passiveHandler);
|
|
|
|
|
|
|
|
if (!isMedia(self.element)) {
|
|
|
|
window[action]('scroll', self.update, passiveHandler);
|
|
|
|
window[action]('resize', self.update, passiveHandler);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function tooltipShownAction(self) {
|
|
|
|
toggleTooltipAction(self, 1);
|
|
|
|
self.element.dispatchEvent(shownTooltipEvent);
|
|
|
|
}
|
|
|
|
function tooltipHiddenAction(self) {
|
|
|
|
toggleTooltipAction(self);
|
|
|
|
removeTooltip(self);
|
|
|
|
self.element.dispatchEvent(hiddenTooltipEvent);
|
|
|
|
}
|
|
|
|
function toggleTooltipHandlers(self, add) {
|
|
|
|
const action = add ? addEventListener : removeEventListener;
|
|
|
|
const { element } = self;
|
|
|
|
|
|
|
|
if (isMedia(element)) element[action]('mousemove', self.update, passiveHandler);
|
|
|
|
element[action]('mousedown', self.show);
|
|
|
|
element[action]('mouseenter', self.show);
|
|
|
|
element[action]('mouseleave', self.hide);
|
|
|
|
}
|
|
|
|
|
|
|
|
function toggleTooltipTitle(self, content) {
|
|
|
|
// [0 - add, 1 - remove] | [0 - remove, 1 - add]
|
|
|
|
const titleAtt = [dataOriginalTitle, titleAttr];
|
|
|
|
const { element } = self;
|
|
|
|
|
|
|
|
element.setAttribute(titleAtt[content ? 0 : 1],
|
|
|
|
(content || element.getAttribute(titleAtt[0])));
|
|
|
|
element.removeAttribute(titleAtt[content ? 1 : 0]);
|
|
|
|
}
|
|
|
|
|
|
|
|
// TOOLTIP EVENT HANDLERS
|
|
|
|
// ======================
|
|
|
|
function tooltipTouchHandler({ target }) {
|
|
|
|
const { tooltip, element } = this;
|
|
|
|
if (tooltip.contains(target) || target === element || element.contains(target)) ; else {
|
|
|
|
this.hide();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TOOLTIP DEFINITION
|
|
|
|
// ==================
|
|
|
|
class Tooltip extends BaseComponent {
|
|
|
|
constructor(target, config) {
|
|
|
|
// initialization element
|
|
|
|
const element = queryElement(target);
|
|
|
|
tooltipDefaultOptions.title = element.getAttribute(titleAttr);
|
|
|
|
tooltipDefaultOptions.container = getTipContainer(element);
|
|
|
|
super(tooltipComponent, element, tooltipDefaultOptions, config);
|
|
|
|
|
|
|
|
// bind
|
|
|
|
const self = this;
|
|
|
|
|
|
|
|
// additional properties
|
|
|
|
self.tooltip = null;
|
|
|
|
self.arrow = null;
|
|
|
|
self.timer = null;
|
|
|
|
self.enabled = false;
|
|
|
|
|
|
|
|
// instance options
|
|
|
|
const { options } = self;
|
|
|
|
|
|
|
|
// media elements only work with body as a container
|
|
|
|
self.options.container = isMedia(element)
|
|
|
|
? tooltipDefaultOptions.container
|
|
|
|
: queryElement(options.container);
|
|
|
|
|
|
|
|
// reset default options
|
|
|
|
tooltipDefaultOptions.container = null;
|
|
|
|
tooltipDefaultOptions[titleAttr] = null;
|
|
|
|
|
|
|
|
// invalidate
|
|
|
|
if (!options.title) return;
|
|
|
|
|
|
|
|
// all functions bind
|
|
|
|
tooltipTouchHandler.bind(self);
|
|
|
|
self.update = self.update.bind(self);
|
|
|
|
|
|
|
|
// set title attributes and add event listeners
|
|
|
|
if (element.hasAttribute(titleAttr)) toggleTooltipTitle(self, options.title);
|
|
|
|
|
|
|
|
// create tooltip here
|
|
|
|
self.id = `${tooltipString}-${getUID(element)}`;
|
|
|
|
createTooltip(self);
|
|
|
|
|
|
|
|
// attach events
|
|
|
|
toggleTooltipHandlers(self, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
// TOOLTIP PUBLIC METHODS
|
|
|
|
// ======================
|
|
|
|
show(e) {
|
|
|
|
const self = e ? this[tooltipComponent] : this;
|
|
|
|
const {
|
|
|
|
options, tooltip, element, id,
|
|
|
|
} = self;
|
|
|
|
clearTimeout(self.timer);
|
|
|
|
self.timer = setTimeout(() => {
|
|
|
|
if (!isVisibleTip(tooltip, options.container)) {
|
|
|
|
element.dispatchEvent(showTooltipEvent);
|
|
|
|
if (showTooltipEvent.defaultPrevented) return;
|
|
|
|
|
|
|
|
// append to container
|
|
|
|
options.container.appendChild(tooltip);
|
|
|
|
element.setAttribute(ariaDescribedBy, id);
|
|
|
|
|
|
|
|
self.update(e);
|
|
|
|
if (!hasClass(tooltip, showClass)) addClass(tooltip, showClass);
|
|
|
|
if (options.animation) emulateTransitionEnd(tooltip, () => tooltipShownAction(self));
|
|
|
|
else tooltipShownAction(self);
|
2019-08-31 17:47:52 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
}, 20);
|
|
|
|
}
|
|
|
|
|
|
|
|
hide(e) {
|
|
|
|
const self = e ? this[tooltipComponent] : this;
|
|
|
|
const { options, tooltip, element } = self;
|
|
|
|
|
|
|
|
clearTimeout(self.timer);
|
|
|
|
self.timer = setTimeout(() => {
|
|
|
|
if (isVisibleTip(tooltip, options.container)) {
|
|
|
|
element.dispatchEvent(hideTooltipEvent);
|
|
|
|
if (hideTooltipEvent.defaultPrevented) return;
|
|
|
|
|
|
|
|
removeClass(tooltip, showClass);
|
|
|
|
if (options.animation) emulateTransitionEnd(tooltip, () => tooltipHiddenAction(self));
|
|
|
|
else tooltipHiddenAction(self);
|
|
|
|
}
|
|
|
|
}, options.delay);
|
|
|
|
}
|
|
|
|
|
|
|
|
update(e) {
|
|
|
|
styleTip(this, e);
|
|
|
|
}
|
|
|
|
|
|
|
|
toggle() {
|
|
|
|
const self = this;
|
|
|
|
const { tooltip, options } = self;
|
|
|
|
if (!isVisibleTip(tooltip, options.container)) self.show();
|
|
|
|
else self.hide();
|
|
|
|
}
|
|
|
|
|
|
|
|
enable() {
|
|
|
|
const self = this;
|
|
|
|
const { enabled } = self;
|
|
|
|
if (!enabled) {
|
|
|
|
toggleTooltipHandlers(self, 1);
|
|
|
|
self.enabled = !enabled;
|
2019-08-31 17:47:52 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
disable() {
|
|
|
|
const self = this;
|
|
|
|
const { tooltip, options, enabled } = self;
|
|
|
|
if (enabled) {
|
|
|
|
if (!isVisibleTip(tooltip, options.container) && options.animation) {
|
|
|
|
self.hide();
|
|
|
|
|
|
|
|
setTimeout(
|
|
|
|
() => toggleTooltipHandlers(self),
|
|
|
|
getElementTransitionDuration(tooltip) + options.delay + 17,
|
|
|
|
);
|
2020-06-01 18:58:38 +02:00
|
|
|
} else {
|
2021-06-19 19:22:19 +02:00
|
|
|
toggleTooltipHandlers(self);
|
2019-08-31 17:47:52 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
self.enabled = !enabled;
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
|
|
|
|
toggleEnabled() {
|
|
|
|
const self = this;
|
|
|
|
if (!self.enabled) self.enable();
|
|
|
|
else self.disable();
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
|
|
|
|
dispose() {
|
|
|
|
const self = this;
|
|
|
|
const { tooltip, options } = self;
|
|
|
|
|
|
|
|
if (options.animation && isVisibleTip(tooltip, options.container)) {
|
|
|
|
options.delay = 0; // reset delay
|
2020-06-01 18:58:38 +02:00
|
|
|
self.hide();
|
2021-06-19 19:22:19 +02:00
|
|
|
emulateTransitionEnd(tooltip, () => disposeTooltipComplete(self));
|
|
|
|
} else {
|
|
|
|
disposeTooltipComplete(self);
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
super.dispose(tooltipComponent);
|
2020-06-01 18:58:38 +02:00
|
|
|
}
|
2021-06-19 19:22:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
Tooltip.init = {
|
|
|
|
component: tooltipComponent,
|
|
|
|
selector: tooltipSelector,
|
|
|
|
constructor: Tooltip,
|
|
|
|
};
|
|
|
|
|
|
|
|
var version = "4.0.2";
|
|
|
|
|
|
|
|
// import { alertInit } from '../components/alert-native.js';
|
|
|
|
// import { buttonInit } from '../components/button-native.js';
|
|
|
|
// import { carouselInit } from '../components/carousel-native.js';
|
|
|
|
// import { collapseInit } from '../components/collapse-native.js';
|
|
|
|
// import { dropdownInit } from '../components/dropdown-native.js';
|
|
|
|
// import { modalInit } from '../components/modal-native.js';
|
|
|
|
// import { offcanvasInit } from '../components/offcanvas-native.js';
|
|
|
|
// import { popoverInit } from '../components/popover-native.js';
|
|
|
|
// import { scrollSpyInit } from '../components/scrollspy-native.js';
|
|
|
|
// import { tabInit } from '../components/tab-native.js';
|
|
|
|
// import { toastInit } from '../components/toast-native.js';
|
|
|
|
// import { tooltipInit } from '../components/tooltip-native.js';
|
|
|
|
|
|
|
|
const componentsInit = {
|
|
|
|
Alert: Alert.init,
|
|
|
|
Button: Button.init,
|
|
|
|
Carousel: Carousel.init,
|
|
|
|
Collapse: Collapse.init,
|
|
|
|
Dropdown: Dropdown.init,
|
|
|
|
Modal: Modal.init,
|
|
|
|
Offcanvas: Offcanvas.init,
|
|
|
|
Popover: Popover.init,
|
|
|
|
ScrollSpy: ScrollSpy.init,
|
|
|
|
Tab: Tab.init,
|
|
|
|
Toast: Toast.init,
|
|
|
|
Tooltip: Tooltip.init,
|
|
|
|
};
|
|
|
|
|
|
|
|
function initializeDataAPI(Konstructor, collection) {
|
|
|
|
Array.from(collection).forEach((x) => new Konstructor(x));
|
|
|
|
}
|
|
|
|
|
|
|
|
function initCallback(context) {
|
|
|
|
const lookUp = context instanceof Element ? context : document;
|
|
|
|
|
|
|
|
Object.keys(componentsInit).forEach((comp) => {
|
|
|
|
const { constructor, selector } = componentsInit[comp];
|
|
|
|
initializeDataAPI(constructor, lookUp.querySelectorAll(selector));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// bulk initialize all components
|
|
|
|
if (document.body) initCallback();
|
|
|
|
else {
|
|
|
|
document.addEventListener('DOMContentLoaded', () => initCallback(), { once: true });
|
|
|
|
}
|
2020-06-01 18:58:38 +02:00
|
|
|
|
|
|
|
var index = {
|
2021-06-19 19:22:19 +02:00
|
|
|
Alert,
|
|
|
|
Button,
|
|
|
|
Carousel,
|
|
|
|
Collapse,
|
|
|
|
Dropdown,
|
|
|
|
Modal,
|
|
|
|
Offcanvas,
|
|
|
|
Popover,
|
|
|
|
ScrollSpy,
|
|
|
|
Tab,
|
|
|
|
Toast,
|
|
|
|
Tooltip,
|
|
|
|
|
|
|
|
initCallback,
|
|
|
|
Version: version,
|
2019-08-31 17:47:52 +02:00
|
|
|
};
|
2020-06-01 18:58:38 +02:00
|
|
|
|
|
|
|
return index;
|
|
|
|
|
|
|
|
})));
|