vaultwarden/src/static/scripts/bootstrap-native.js

5751 lines
168 KiB
JavaScript

/*!
* Native JavaScript for Bootstrap v4.1.2 (https://thednp.github.io/bootstrap.native/)
* Copyright 2015-2022 © dnp_theme
* Licensed under MIT (https://github.com/thednp/bootstrap.native/blob/master/LICENSE)
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.BSN = factory());
})(this, (function () { 'use strict';
/** @type {Record<string, any>} */
const EventRegistry = {};
/**
* The global event listener.
*
* @this {Element | HTMLElement | Window | Document}
* @param {Event} e
* @returns {void}
*/
function globalListener(e) {
const that = this;
const { type } = e;
const oneEvMap = EventRegistry[type] ? [...EventRegistry[type]] : [];
oneEvMap.forEach((elementsMap) => {
const [element, listenersMap] = elementsMap;
[...listenersMap].forEach((listenerMap) => {
if (element === that) {
const [listener, options] = listenerMap;
listener.apply(element, [e]);
if (options && options.once) {
removeListener(element, type, listener, options);
}
}
});
});
}
/**
* Register a new listener with its options and attach the `globalListener`
* to the target if this is the first listener.
*
* @param {Element | HTMLElement | Window | Document} element
* @param {string} eventType
* @param {EventListenerObject['handleEvent']} listener
* @param {AddEventListenerOptions=} options
*/
const addListener = (element, eventType, listener, options) => {
// get element listeners first
if (!EventRegistry[eventType]) {
EventRegistry[eventType] = new Map();
}
const oneEventMap = EventRegistry[eventType];
if (!oneEventMap.has(element)) {
oneEventMap.set(element, new Map());
}
const oneElementMap = oneEventMap.get(element);
// get listeners size
const { size } = oneElementMap;
// register listener with its options
if (oneElementMap) {
oneElementMap.set(listener, options);
}
// add listener last
if (!size) {
element.addEventListener(eventType, globalListener, options);
}
};
/**
* Remove a listener from registry and detach the `globalListener`
* if no listeners are found in the registry.
*
* @param {Element | HTMLElement | Window | Document} element
* @param {string} eventType
* @param {EventListenerObject['handleEvent']} listener
* @param {AddEventListenerOptions=} options
*/
const removeListener = (element, eventType, listener, options) => {
// get listener first
const oneEventMap = EventRegistry[eventType];
const oneElementMap = oneEventMap && oneEventMap.get(element);
const savedOptions = oneElementMap && oneElementMap.get(listener);
// also recover initial options
const { options: eventOptions } = savedOptions !== undefined
? savedOptions
: { options };
// unsubscribe second, remove from registry
if (oneElementMap && oneElementMap.has(listener)) oneElementMap.delete(listener);
if (oneEventMap && (!oneElementMap || !oneElementMap.size)) oneEventMap.delete(element);
if (!oneEventMap || !oneEventMap.size) delete EventRegistry[eventType];
// remove listener last
if (!oneElementMap || !oneElementMap.size) {
element.removeEventListener(eventType, globalListener, eventOptions);
}
};
/**
* Advanced event listener based on subscribe / publish pattern.
* @see https://www.patterns.dev/posts/classic-design-patterns/#observerpatternjavascript
* @see https://gist.github.com/shystruk/d16c0ee7ac7d194da9644e5d740c8338#file-subpub-js
* @see https://hackernoon.com/do-you-still-register-window-event-listeners-in-each-component-react-in-example-31a4b1f6f1c8
*/
const EventListener = {
on: addListener,
off: removeListener,
globalListener,
registry: EventRegistry,
};
/**
* A global namespace for `click` event.
* @type {string}
*/
const mouseclickEvent = 'click';
/**
* A global namespace for 'transitionend' string.
* @type {string}
*/
const transitionEndEvent = 'transitionend';
/**
* A global namespace for 'transitionDelay' string.
* @type {string}
*/
const transitionDelay = 'transitionDelay';
/**
* A global namespace for `transitionProperty` string for modern browsers.
*
* @type {string}
*/
const transitionProperty = 'transitionProperty';
/**
* Shortcut for `window.getComputedStyle(element).propertyName`
* static method.
*
* * If `element` parameter is not an `HTMLElement`, `getComputedStyle`
* throws a `ReferenceError`.
*
* @param {HTMLElement | Element} element target
* @param {string} property the css property
* @return {string} the css property value
*/
function getElementStyle(element, property) {
const computedStyle = getComputedStyle(element);
// @ts-ignore -- must use camelcase strings,
// or non-camelcase strings with `getPropertyValue`
return property in computedStyle ? computedStyle[property] : '';
}
/**
* Utility to get the computed `transitionDelay`
* from Element in miliseconds.
*
* @param {HTMLElement | Element} element target
* @return {number} the value in miliseconds
*/
function getElementTransitionDelay(element) {
const propertyValue = getElementStyle(element, transitionProperty);
const delayValue = getElementStyle(element, transitionDelay);
const delayScale = delayValue.includes('ms') ? 1 : 1000;
const duration = propertyValue && propertyValue !== 'none'
? parseFloat(delayValue) * delayScale : 0;
return !Number.isNaN(duration) ? duration : 0;
}
/**
* A global namespace for 'transitionDuration' string.
* @type {string}
*/
const transitionDuration = 'transitionDuration';
/**
* Utility to get the computed `transitionDuration`
* from Element in miliseconds.
*
* @param {HTMLElement | Element} element target
* @return {number} the value in miliseconds
*/
function getElementTransitionDuration(element) {
const propertyValue = getElementStyle(element, transitionProperty);
const durationValue = getElementStyle(element, transitionDuration);
const durationScale = durationValue.includes('ms') ? 1 : 1000;
const duration = propertyValue && propertyValue !== 'none'
? parseFloat(durationValue) * durationScale : 0;
return !Number.isNaN(duration) ? duration : 0;
}
/**
* Utility to make sure callbacks are consistently
* called when transition ends.
*
* @param {HTMLElement | Element} element target
* @param {EventListener} handler `transitionend` callback
*/
function emulateTransitionEnd(element, handler) {
let called = 0;
const endEvent = new Event(transitionEndEvent);
const duration = getElementTransitionDuration(element);
const delay = getElementTransitionDelay(element);
if (duration) {
/**
* Wrap the handler in on -> off callback
* @type {EventListener} e Event object
*/
const transitionEndWrapper = (e) => {
if (e.target === element) {
handler.apply(element, [e]);
element.removeEventListener(transitionEndEvent, transitionEndWrapper);
called = 1;
}
};
element.addEventListener(transitionEndEvent, transitionEndWrapper);
setTimeout(() => {
if (!called) element.dispatchEvent(endEvent);
}, duration + delay + 17);
} else {
handler.apply(element, [endEvent]);
}
}
/**
* Returns the `document` or the `#document` element.
* @see https://github.com/floating-ui/floating-ui
* @param {(Node | HTMLElement | Element | globalThis)=} node
* @returns {Document}
*/
function getDocument(node) {
if (node instanceof HTMLElement) return node.ownerDocument;
if (node instanceof Window) return node.document;
return window.document;
}
/**
* A global array of possible `ParentNode`.
*/
const parentNodes = [Document, Element, HTMLElement];
/**
* A global array with `Element` | `HTMLElement`.
*/
const elementNodes = [Element, HTMLElement];
/**
* Utility to check if target is typeof `HTMLElement`, `Element`, `Node`
* or find one that matches a selector.
*
* @param {HTMLElement | Element | string} selector the input selector or target element
* @param {(HTMLElement | Element | Document)=} parent optional node to look into
* @return {(HTMLElement | Element)?} the `HTMLElement` or `querySelector` result
*/
function querySelector(selector, parent) {
const lookUp = parentNodes.some((x) => parent instanceof x)
? parent : getDocument();
// @ts-ignore
return elementNodes.some((x) => selector instanceof x)
// @ts-ignore
? selector : lookUp.querySelector(selector);
}
/**
* Shortcut for `HTMLElement.closest` method which also works
* with children of `ShadowRoot`. The order of the parameters
* is intentional since they're both required.
*
* @see https://stackoverflow.com/q/54520554/803358
*
* @param {HTMLElement | Element} element Element to look into
* @param {string} selector the selector name
* @return {(HTMLElement | Element)?} the query result
*/
function closest(element, selector) {
return element ? (element.closest(selector)
// @ts-ignore -- break out of `ShadowRoot`
|| closest(element.getRootNode().host, selector)) : null;
}
/**
* Shortcut for `Object.assign()` static method.
* @param {Record<string, any>} obj a target object
* @param {Record<string, any>} source a source object
*/
const ObjectAssign = (obj, source) => Object.assign(obj, source);
/**
* Check class in `HTMLElement.classList`.
*
* @param {HTMLElement | Element} element target
* @param {string} classNAME to check
* @returns {boolean}
*/
function hasClass(element, classNAME) {
return element.classList.contains(classNAME);
}
/**
* Remove class from `HTMLElement.classList`.
*
* @param {HTMLElement | Element} element target
* @param {string} classNAME to remove
* @returns {void}
*/
function removeClass(element, classNAME) {
element.classList.remove(classNAME);
}
/**
* Shortcut for the `Element.dispatchEvent(Event)` method.
*
* @param {HTMLElement | Element} element is the target
* @param {Event} event is the `Event` object
*/
const dispatchEvent = (element, event) => element.dispatchEvent(event);
/** @type {Map<string, Map<HTMLElement | Element, Record<string, any>>>} */
const componentData = new Map();
/**
* An interface for web components background data.
* @see https://github.com/thednp/bootstrap.native/blob/master/src/components/base-component.js
*/
const Data = {
/**
* Sets web components data.
* @param {HTMLElement | Element | string} target target element
* @param {string} component the component's name or a unique key
* @param {Record<string, any>} instance the component instance
*/
set: (target, component, instance) => {
const element = querySelector(target);
if (!element) return;
if (!componentData.has(component)) {
componentData.set(component, new Map());
}
const instanceMap = componentData.get(component);
// @ts-ignore - not undefined, but defined right above
instanceMap.set(element, instance);
},
/**
* Returns all instances for specified component.
* @param {string} component the component's name or a unique key
* @returns {Map<HTMLElement | Element, Record<string, any>>?} all the component instances
*/
getAllFor: (component) => {
const instanceMap = componentData.get(component);
return instanceMap || null;
},
/**
* Returns the instance associated with the target.
* @param {HTMLElement | Element | string} target target element
* @param {string} component the component's name or a unique key
* @returns {Record<string, any>?} the instance
*/
get: (target, component) => {
const element = querySelector(target);
const allForC = Data.getAllFor(component);
const instance = element && allForC && allForC.get(element);
return instance || null;
},
/**
* Removes web components data.
* @param {HTMLElement | Element | string} target target element
* @param {string} component the component's name or a unique key
*/
remove: (target, component) => {
const element = querySelector(target);
const instanceMap = componentData.get(component);
if (!instanceMap || !element) return;
instanceMap.delete(element);
if (instanceMap.size === 0) {
componentData.delete(component);
}
},
};
/**
* An alias for `Data.get()`.
* @type {SHORTER.getInstance<any>}
*/
const getInstance = (target, component) => Data.get(target, component);
/**
* Returns a namespaced `CustomEvent` specific to each component.
* @param {string} EventType Event.type
* @param {Record<string, any>=} config Event.options | Event.properties
* @returns {SHORTER.OriginalEvent} a new namespaced event
*/
function OriginalEvent(EventType, config) {
const OriginalCustomEvent = new CustomEvent(EventType, {
cancelable: true, bubbles: true,
});
if (config instanceof Object) {
ObjectAssign(OriginalCustomEvent, config);
}
return OriginalCustomEvent;
}
/**
* Global namespace for most components `fade` class.
*/
const fadeClass = 'fade';
/**
* Global namespace for most components `show` class.
*/
const showClass = 'show';
/**
* Global namespace for most components `dismiss` option.
*/
const dataBsDismiss = 'data-bs-dismiss';
/** @type {string} */
const alertString = 'alert';
/** @type {string} */
const alertComponent = 'Alert';
/**
* Shortcut for `HTMLElement.getAttribute()` method.
* @param {HTMLElement | Element} element target element
* @param {string} attribute attribute name
* @returns {string?} attribute value
*/
const getAttribute = (element, attribute) => element.getAttribute(attribute);
/**
* The raw value or a given component option.
*
* @typedef {string | HTMLElement | Function | number | boolean | null} niceValue
*/
/**
* Utility to normalize component options
*
* @param {any} value the input value
* @return {niceValue} the normalized value
*/
function normalizeValue(value) {
if (value === 'true') { // boolean
return true;
}
if (value === 'false') { // boolean
return false;
}
if (!Number.isNaN(+value)) { // number
return +value;
}
if (value === '' || value === 'null') { // null
return null;
}
// string / function / HTMLElement / object
return value;
}
/**
* Shortcut for `Object.keys()` static method.
* @param {Record<string, any>} obj a target object
* @returns {string[]}
*/
const ObjectKeys = (obj) => Object.keys(obj);
/**
* Shortcut for `String.toLowerCase()`.
*
* @param {string} source input string
* @returns {string} lowercase output string
*/
const toLowerCase = (source) => source.toLowerCase();
/**
* Utility to normalize component options.
*
* @param {HTMLElement | Element} element target
* @param {Record<string, any>} defaultOps component default options
* @param {Record<string, any>} inputOps component instance options
* @param {string=} ns component namespace
* @return {Record<string, any>} normalized component options object
*/
function normalizeOptions(element, defaultOps, inputOps, ns) {
// @ts-ignore -- our targets are always `HTMLElement`
const data = { ...element.dataset };
/** @type {Record<string, any>} */
const normalOps = {};
/** @type {Record<string, any>} */
const dataOps = {};
const title = 'title';
ObjectKeys(data).forEach((k) => {
const key = ns && k.includes(ns)
? k.replace(ns, '').replace(/[A-Z]/, (match) => toLowerCase(match))
: k;
dataOps[key] = normalizeValue(data[k]);
});
ObjectKeys(inputOps).forEach((k) => {
inputOps[k] = normalizeValue(inputOps[k]);
});
ObjectKeys(defaultOps).forEach((k) => {
if (k in inputOps) {
normalOps[k] = inputOps[k];
} else if (k in dataOps) {
normalOps[k] = dataOps[k];
} else {
normalOps[k] = k === title
? getAttribute(element, title)
: defaultOps[k];
}
});
return normalOps;
}
var version = "4.1.2";
const Version = version;
/* Native JavaScript for Bootstrap 5 | Base Component
----------------------------------------------------- */
/** Returns a new `BaseComponent` instance. */
class BaseComponent {
/**
* @param {HTMLElement | Element | string} target `Element` or selector string
* @param {BSN.ComponentOptions=} config component instance options
*/
constructor(target, config) {
const self = this;
const element = querySelector(target);
if (!element) {
throw Error(`${self.name} Error: "${target}" is not a valid selector.`);
}
/** @static @type {BSN.ComponentOptions} */
self.options = {};
const prevInstance = Data.get(element, self.name);
if (prevInstance) prevInstance.dispose();
/** @type {HTMLElement | Element} */
self.element = element;
if (self.defaults && Object.keys(self.defaults).length) {
self.options = normalizeOptions(element, self.defaults, (config || {}), 'bs');
}
Data.set(element, self.name, self);
}
/* eslint-disable */
/** @static */
get version() { return Version; }
/* eslint-enable */
/** @static */
get name() { return this.constructor.name; }
/** @static */
// @ts-ignore
get defaults() { return this.constructor.defaults; }
/**
* Removes component from target element;
*/
dispose() {
const self = this;
Data.remove(self.element, self.name);
// @ts-ignore
ObjectKeys(self).forEach((prop) => { self[prop] = null; });
}
}
/* Native JavaScript for Bootstrap 5 | Alert
-------------------------------------------- */
// ALERT PRIVATE GC
// ================
const alertSelector = `.${alertString}`;
const alertDismissSelector = `[${dataBsDismiss}="${alertString}"]`;
/**
* Static method which returns an existing `Alert` instance associated
* to a target `Element`.
*
* @type {BSN.GetInstance<Alert>}
*/
const getAlertInstance = (element) => getInstance(element, alertComponent);
/**
* An `Alert` initialization callback.
* @type {BSN.InitCallback<Alert>}
*/
const alertInitCallback = (element) => new Alert(element);
// ALERT CUSTOM EVENTS
// ===================
const closeAlertEvent = OriginalEvent(`close.bs.${alertString}`);
const closedAlertEvent = OriginalEvent(`closed.bs.${alertString}`);
// ALERT EVENT HANDLER
// ===================
/**
* Alert `transitionend` callback.
* @param {Alert} self target Alert instance
*/
function alertTransitionEnd(self) {
const { element } = self;
toggleAlertHandler(self);
dispatchEvent(element, closedAlertEvent);
self.dispose();
element.remove();
}
// ALERT PRIVATE METHOD
// ====================
/**
* Toggle on / off the `click` event listener.
* @param {Alert} self the target alert instance
* @param {boolean=} add when `true`, event listener is added
*/
function toggleAlertHandler(self, add) {
const action = add ? addListener : removeListener;
const { dismiss } = self;
if (dismiss) action(dismiss, mouseclickEvent, self.close);
}
// ALERT DEFINITION
// ================
/** Creates a new Alert instance. */
class Alert extends BaseComponent {
/** @param {HTMLElement | Element | string} target element or selector */
constructor(target) {
super(target);
// bind
const self = this;
// initialization element
const { element } = self;
// the dismiss button
/** @static @type {(HTMLElement | Element)?} */
self.dismiss = querySelector(alertDismissSelector, element);
// add event listener
toggleAlertHandler(self, true);
}
/* eslint-disable */
/**
* Returns component name string.
* @readonly @static
*/
get name() { return alertComponent; }
/* eslint-enable */
// ALERT PUBLIC METHODS
// ====================
/**
* Public method that hides the `.alert` element from the user,
* disposes the instance once animation is complete, then
* removes the element from the DOM.
*
* @param {Event=} e most likely the `click` event
* @this {Alert} the `Alert` instance or `EventTarget`
*/
close(e) {
// @ts-ignore
const self = e ? getAlertInstance(closest(this, alertSelector)) : this;
if (!self) return;
const { element } = self;
if (hasClass(element, showClass)) {
dispatchEvent(element, closeAlertEvent);
if (closeAlertEvent.defaultPrevented) return;
removeClass(element, showClass);
if (hasClass(element, fadeClass)) {
emulateTransitionEnd(element, () => alertTransitionEnd(self));
} else alertTransitionEnd(self);
}
}
/** Remove the component from target element. */
dispose() {
toggleAlertHandler(this);
super.dispose();
}
}
ObjectAssign(Alert, {
selector: alertSelector,
init: alertInitCallback,
getInstance: getAlertInstance,
});
/**
* A global namespace for aria-pressed.
* @type {string}
*/
const ariaPressed = 'aria-pressed';
/**
* Shortcut for `HTMLElement.setAttribute()` method.
* @param {HTMLElement | Element} element target element
* @param {string} attribute attribute name
* @param {string} value attribute value
* @returns {void}
*/
const setAttribute = (element, attribute, value) => element.setAttribute(attribute, value);
/**
* Add class to `HTMLElement.classList`.
*
* @param {HTMLElement | Element} element target
* @param {string} classNAME to add
* @returns {void}
*/
function addClass(element, classNAME) {
element.classList.add(classNAME);
}
/**
* Global namespace for most components active class.
*/
const activeClass = 'active';
/**
* Global namespace for most components `toggle` option.
*/
const dataBsToggle = 'data-bs-toggle';
/** @type {string} */
const buttonString = 'button';
/** @type {string} */
const buttonComponent = 'Button';
/* Native JavaScript for Bootstrap 5 | Button
---------------------------------------------*/
// BUTTON PRIVATE GC
// =================
const buttonSelector = `[${dataBsToggle}="${buttonString}"]`;
/**
* Static method which returns an existing `Button` instance associated
* to a target `Element`.
*
* @type {BSN.GetInstance<Button>}
*/
const getButtonInstance = (element) => getInstance(element, buttonComponent);
/**
* A `Button` initialization callback.
* @type {BSN.InitCallback<Button>}
*/
const buttonInitCallback = (element) => new Button(element);
// BUTTON PRIVATE METHOD
// =====================
/**
* Toggles on/off the `click` event listener.
* @param {Button} self the `Button` instance
* @param {boolean=} add when `true`, event listener is added
*/
function toggleButtonHandler(self, add) {
const action = add ? addListener : removeListener;
action(self.element, mouseclickEvent, self.toggle);
}
// BUTTON DEFINITION
// =================
/** Creates a new `Button` instance. */
class Button extends BaseComponent {
/**
* @param {HTMLElement | Element | string} target usually a `.btn` element
*/
constructor(target) {
super(target);
const self = this;
// initialization element
const { element } = self;
// set initial state
/** @type {boolean} */
self.isActive = hasClass(element, activeClass);
setAttribute(element, ariaPressed, `${!!self.isActive}`);
// add event listener
toggleButtonHandler(self, true);
}
/* eslint-disable */
/**
* Returns component name string.
* @readonly @static
*/
get name() { return buttonComponent; }
/* eslint-enable */
// BUTTON PUBLIC METHODS
// =====================
/**
* Toggles the state of the target button.
* @param {MouseEvent} e usually `click` Event object
*/
toggle(e) {
if (e) e.preventDefault();
// @ts-ignore
const self = e ? getButtonInstance(this) : this;
if (!self) return;
const { element } = self;
if (hasClass(element, 'disabled')) return;
self.isActive = hasClass(element, activeClass);
const { isActive } = self;
const action = isActive ? removeClass : addClass;
action(element, activeClass);
setAttribute(element, ariaPressed, isActive ? 'false' : 'true');
}
/** Removes the `Button` component from the target element. */
dispose() {
toggleButtonHandler(this);
super.dispose();
}
}
ObjectAssign(Button, {
selector: buttonSelector,
init: buttonInitCallback,
getInstance: getButtonInstance,
});
/**
* A global namespace for `mouseenter` event.
* @type {string}
*/
const mouseenterEvent = 'mouseenter';
/**
* A global namespace for `mouseleave` event.
* @type {string}
*/
const mouseleaveEvent = 'mouseleave';
/**
* A global namespace for `keydown` event.
* @type {string}
*/
const keydownEvent = 'keydown';
/**
* A global namespace for `touchmove` event.
* @type {string}
*/
const touchmoveEvent = 'touchmove';
/**
* A global namespace for `touchend` event.
* @type {string}
*/
const touchendEvent = 'touchend';
/**
* A global namespace for `touchstart` event.
* @type {string}
*/
const touchstartEvent = 'touchstart';
/**
* A global namespace for `ArrowLeft` key.
* @type {string} e.which = 37 equivalent
*/
const keyArrowLeft = 'ArrowLeft';
/**
* A global namespace for `ArrowRight` key.
* @type {string} e.which = 39 equivalent
*/
const keyArrowRight = 'ArrowRight';
/**
* Returns the `Window` object of a target node.
* @see https://github.com/floating-ui/floating-ui
*
* @param {(Node | HTMLElement | Element | Window)=} node target node
* @returns {globalThis}
*/
function getWindow(node) {
if (node == null) {
return window;
}
if (!(node instanceof Window)) {
const { ownerDocument } = node;
return ownerDocument ? ownerDocument.defaultView || window : window;
}
// @ts-ignore
return node;
}
/**
* Returns the bounding client rect of a target `HTMLElement`.
*
* @see https://github.com/floating-ui/floating-ui
*
* @param {HTMLElement | Element} element event.target
* @param {boolean=} includeScale when *true*, the target scale is also computed
* @returns {SHORTER.BoundingClientRect} the bounding client rect object
*/
function getBoundingClientRect(element, includeScale) {
const {
width, height, top, right, bottom, left,
} = element.getBoundingClientRect();
let scaleX = 1;
let scaleY = 1;
if (includeScale && element instanceof HTMLElement) {
const { offsetWidth, offsetHeight } = element;
scaleX = offsetWidth > 0 ? Math.round(width) / offsetWidth || 1 : 1;
scaleY = offsetHeight > 0 ? Math.round(height) / offsetHeight || 1 : 1;
}
return {
width: width / scaleX,
height: height / scaleY,
top: top / scaleY,
right: right / scaleX,
bottom: bottom / scaleY,
left: left / scaleX,
x: left / scaleX,
y: top / scaleY,
};
}
/**
* Returns the `document.documentElement` or the `<html>` element.
*
* @param {(Node | HTMLElement | Element | globalThis)=} node
* @returns {HTMLElement | HTMLHtmlElement}
*/
function getDocumentElement(node) {
return getDocument(node).documentElement;
}
/**
* Utility to determine if an `HTMLElement`
* is partially visible in viewport.
*
* @param {HTMLElement | Element} element target
* @return {boolean} the query result
*/
const isElementInScrollRange = (element) => {
const { top, bottom } = getBoundingClientRect(element);
const { clientHeight } = getDocumentElement(element);
// checks bottom && top
return top <= clientHeight && bottom >= 0;
};
/**
* Checks if a page is Right To Left.
* @param {(HTMLElement | Element)=} node the target
* @returns {boolean} the query result
*/
const isRTL = (node) => getDocumentElement(node).dir === 'rtl';
/**
* A shortcut for `(document|Element).querySelectorAll`.
*
* @param {string} selector the input selector
* @param {(HTMLElement | Element | Document | Node)=} parent optional node to look into
* @return {NodeListOf<HTMLElement | Element>} the query result
*/
function querySelectorAll(selector, parent) {
const lookUp = parent && parentNodes
.some((x) => parent instanceof x) ? parent : getDocument();
// @ts-ignore -- `ShadowRoot` is also a node
return lookUp.querySelectorAll(selector);
}
/**
* Shortcut for `HTMLElement.getElementsByClassName` method. Some `Node` elements
* like `ShadowRoot` do not support `getElementsByClassName`.
*
* @param {string} selector the class name
* @param {(HTMLElement | Element | Document)=} parent optional Element to look into
* @return {HTMLCollectionOf<HTMLElement | Element>} the 'HTMLCollection'
*/
function getElementsByClassName(selector, parent) {
const lookUp = parent && parentNodes.some((x) => parent instanceof x)
? parent : getDocument();
return lookUp.getElementsByClassName(selector);
}
/** @type {Map<HTMLElement | Element, any>} */
const TimeCache = new Map();
/**
* An interface for one or more `TimerHandler`s per `Element`.
* @see https://github.com/thednp/navbar.js/
*/
const Timer = {
/**
* Sets a new timeout timer for an element, or element -> key association.
* @param {HTMLElement | Element | string} target target element
* @param {ReturnType<TimerHandler>} callback the callback
* @param {number} delay the execution delay
* @param {string=} key a unique key
*/
set: (target, callback, delay, key) => {
const element = querySelector(target);
if (!element) return;
if (key && key.length) {
if (!TimeCache.has(element)) {
TimeCache.set(element, new Map());
}
const keyTimers = TimeCache.get(element);
keyTimers.set(key, setTimeout(callback, delay));
} else {
TimeCache.set(element, setTimeout(callback, delay));
}
},
/**
* Returns the timer associated with the target.
* @param {HTMLElement | Element | string} target target element
* @param {string=} key a unique
* @returns {number?} the timer
*/
get: (target, key) => {
const element = querySelector(target);
if (!element) return null;
const keyTimers = TimeCache.get(element);
if (key && key.length && keyTimers && keyTimers.get) {
return keyTimers.get(key) || null;
}
return keyTimers || null;
},
/**
* Clears the element's timer.
* @param {HTMLElement | Element | string} target target element
* @param {string=} key a unique key
*/
clear: (target, key) => {
const element = querySelector(target);
if (!element) return;
if (key && key.length) {
const keyTimers = TimeCache.get(element);
if (keyTimers && keyTimers.get) {
clearTimeout(keyTimers.get(key));
keyTimers.delete(key);
if (keyTimers.size === 0) {
TimeCache.delete(element);
}
}
} else {
clearTimeout(TimeCache.get(element));
TimeCache.delete(element);
}
},
};
/**
* Utility to force re-paint of an `HTMLElement` target.
*
* @param {HTMLElement | Element} element is the target
* @return {number} the `Element.offsetHeight` value
*/
// @ts-ignore
const reflow = (element) => element.offsetHeight;
/**
* A global namespace for most scroll event listeners.
* @type {Partial<AddEventListenerOptions>}
*/
const passiveHandler = { passive: true };
/**
* Global namespace for most components `target` option.
*/
const dataBsTarget = 'data-bs-target';
/** @type {string} */
const carouselString = 'carousel';
/** @type {string} */
const carouselComponent = 'Carousel';
/**
* Global namespace for most components `parent` option.
*/
const dataBsParent = 'data-bs-parent';
/**
* Global namespace for most components `container` option.
*/
const dataBsContainer = 'data-bs-container';
/**
* Returns the `Element` that THIS one targets
* via `data-bs-target`, `href`, `data-bs-parent` or `data-bs-container`.
*
* @param {HTMLElement | Element} element the target element
* @returns {(HTMLElement | Element)?} the query result
*/
function getTargetElement(element) {
const targetAttr = [dataBsTarget, dataBsParent, dataBsContainer, 'href'];
const doc = getDocument(element);
return targetAttr.map((att) => {
const attValue = getAttribute(element, att);
if (attValue) {
return att === dataBsParent ? closest(element, attValue) : querySelector(attValue, doc);
}
return null;
}).filter((x) => x)[0];
}
/* Native JavaScript for Bootstrap 5 | Carousel
----------------------------------------------- */
// CAROUSEL PRIVATE GC
// ===================
const carouselSelector = `[data-bs-ride="${carouselString}"]`;
const carouselItem = `${carouselString}-item`;
const dataBsSlideTo = 'data-bs-slide-to';
const dataBsSlide = 'data-bs-slide';
const pausedClass = 'paused';
const carouselDefaults = {
pause: 'hover',
keyboard: false,
touch: true,
interval: 5000,
};
/**
* Static method which returns an existing `Carousel` instance associated
* to a target `Element`.
*
* @type {BSN.GetInstance<Carousel>}
*/
const getCarouselInstance = (element) => getInstance(element, carouselComponent);
/**
* A `Carousel` initialization callback.
* @type {BSN.InitCallback<Carousel>}
*/
const carouselInitCallback = (element) => new Carousel(element);
let startX = 0;
let currentX = 0;
let endX = 0;
// CAROUSEL CUSTOM EVENTS
// ======================
const carouselSlideEvent = OriginalEvent(`slide.bs.${carouselString}`);
const carouselSlidEvent = OriginalEvent(`slid.bs.${carouselString}`);
// CAROUSEL EVENT HANDLERS
// =======================
/**
* The `transitionend` event listener of the `Carousel`.
* @param {Carousel} self the `Carousel` instance
*/
function carouselTransitionEndHandler(self) {
const {
index, direction, element, slides, options,
} = self;
// discontinue disposed instances
if (self.isAnimating && getCarouselInstance(element)) {
const activeItem = getActiveIndex(self);
const orientation = direction === 'left' ? 'next' : 'prev';
const directionClass = direction === 'left' ? 'start' : 'end';
addClass(slides[index], activeClass);
removeClass(slides[index], `${carouselItem}-${orientation}`);
removeClass(slides[index], `${carouselItem}-${directionClass}`);
removeClass(slides[activeItem], activeClass);
removeClass(slides[activeItem], `${carouselItem}-${directionClass}`);
dispatchEvent(element, carouselSlidEvent);
Timer.clear(element, dataBsSlide);
// check for element, might have been disposed
if (!getDocument(element).hidden && options.interval
&& !self.isPaused) {
self.cycle();
}
}
}
/**
* Handles the `mouseenter` / `touchstart` events when *options.pause*
* is set to `hover`.
*
* @this {HTMLElement | Element}
*/
function carouselPauseHandler() {
const element = this;
const self = getCarouselInstance(element);
if (self && !self.isPaused && !Timer.get(element, pausedClass)) {
addClass(element, pausedClass);
}
}
/**
* Handles the `mouseleave` / `touchend` events when *options.pause*
* is set to `hover`.
*
* @this {HTMLElement | Element}
*/
function carouselResumeHandler() {
const element = this;
const self = getCarouselInstance(element);
if (self && self.isPaused && !Timer.get(element, pausedClass)) {
self.cycle();
}
}
/**
* Handles the `click` event for the `Carousel` indicators.
*
* @this {HTMLElement}
* @param {MouseEvent} e the `Event` object
*/
function carouselIndicatorHandler(e) {
e.preventDefault();
const indicator = this;
const element = closest(indicator, carouselSelector) || getTargetElement(indicator);
if (!element) return;
const self = getCarouselInstance(element);
if (!self || self.isAnimating) return;
// @ts-ignore
const newIndex = +getAttribute(indicator, dataBsSlideTo);
if (indicator && !hasClass(indicator, activeClass) // event target is not active
&& !Number.isNaN(newIndex)) { // AND has the specific attribute
self.to(newIndex); // do the slide
}
}
/**
* Handles the `click` event for the `Carousel` arrows.
*
* @this {HTMLElement}
* @param {MouseEvent} e the `Event` object
*/
function carouselControlsHandler(e) {
e.preventDefault();
const control = this;
const element = closest(control, carouselSelector) || getTargetElement(control);
const self = element && getCarouselInstance(element);
if (!self || self.isAnimating) return;
const orientation = getAttribute(control, dataBsSlide);
if (orientation === 'next') {
self.next();
} else if (orientation === 'prev') {
self.prev();
}
}
/**
* Handles the keyboard `keydown` event for the visible `Carousel` elements.
*
* @param {KeyboardEvent} e the `Event` object
*/
function carouselKeyHandler({ code }) {
const [element] = [...querySelectorAll(carouselSelector)]
.filter((x) => isElementInScrollRange(x));
const self = getCarouselInstance(element);
if (!self) return;
const RTL = isRTL();
const arrowKeyNext = !RTL ? keyArrowRight : keyArrowLeft;
const arrowKeyPrev = !RTL ? keyArrowLeft : keyArrowRight;
if (code === arrowKeyPrev) self.prev();
else if (code === arrowKeyNext) self.next();
}
// CAROUSEL TOUCH HANDLERS
// =======================
/**
* Handles the `touchdown` event for the `Carousel` element.
*
* @this {HTMLElement | Element}
* @param {TouchEvent} e the `Event` object
*/
function carouselTouchDownHandler(e) {
const element = this;
const self = getCarouselInstance(element);
if (!self || self.isTouch) { return; }
startX = e.changedTouches[0].pageX;
// @ts-ignore
if (element.contains(e.target)) {
self.isTouch = true;
toggleCarouselTouchHandlers(self, true);
}
}
/**
* Handles the `touchmove` event for the `Carousel` element.
*
* @this {HTMLElement | Element}
* @param {TouchEvent} e
*/
function carouselTouchMoveHandler(e) {
const { changedTouches, type } = e;
const self = getCarouselInstance(this);
if (!self || !self.isTouch) { return; }
currentX = changedTouches[0].pageX;
// cancel touch if more than one changedTouches detected
if (type === touchmoveEvent && changedTouches.length > 1) {
e.preventDefault();
}
}
/**
* Handles the `touchend` event for the `Carousel` element.
*
* @this {HTMLElement | Element}
* @param {TouchEvent} e
*/
function carouselTouchEndHandler(e) {
const element = this;
const self = getCarouselInstance(element);
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
// @ts-ignore
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
return;
} // OR determine next index to slide to
if (currentX < startX) {
self.index += 1;
} else if (currentX > startX) {
self.index -= 1;
}
self.isTouch = false;
self.to(self.index); // do the slide
toggleCarouselTouchHandlers(self); // remove touch events handlers
}
}
// CAROUSEL PRIVATE METHODS
// ========================
/**
* Sets active indicator for the `Carousel` instance.
* @param {Carousel} self the `Carousel` instance
* @param {number} pageIndex the index of the new active indicator
*/
function activateCarouselIndicator(self, pageIndex) {
const { indicators } = self;
[...indicators].forEach((x) => removeClass(x, activeClass));
if (self.indicators[pageIndex]) addClass(indicators[pageIndex], activeClass);
}
/**
* Toggles the touch event listeners for a given `Carousel` instance.
* @param {Carousel} self the `Carousel` instance
* @param {boolean=} add when `TRUE` event listeners are added
*/
function toggleCarouselTouchHandlers(self, add) {
const { element } = self;
const action = add ? addListener : removeListener;
action(element, touchmoveEvent, carouselTouchMoveHandler, passiveHandler);
action(element, touchendEvent, carouselTouchEndHandler, passiveHandler);
}
/**
* Toggles all event listeners for a given `Carousel` instance.
* @param {Carousel} self the `Carousel` instance
* @param {boolean=} add when `TRUE` event listeners are added
*/
function toggleCarouselHandlers(self, add) {
const {
element, options, slides, controls, indicators,
} = self;
const {
touch, pause, interval, keyboard,
} = options;
const action = add ? addListener : removeListener;
if (pause && interval) {
action(element, mouseenterEvent, carouselPauseHandler);
action(element, mouseleaveEvent, carouselResumeHandler);
action(element, touchstartEvent, carouselPauseHandler, passiveHandler);
action(element, touchendEvent, carouselResumeHandler, passiveHandler);
}
if (touch && slides.length > 1) {
action(element, touchstartEvent, carouselTouchDownHandler, passiveHandler);
}
if (controls.length) {
controls.forEach((arrow) => {
if (arrow) action(arrow, mouseclickEvent, carouselControlsHandler);
});
}
if (indicators.length) {
indicators.forEach((indicator) => {
action(indicator, mouseclickEvent, carouselIndicatorHandler);
});
}
// @ts-ignore
if (keyboard) action(getWindow(element), keydownEvent, carouselKeyHandler);
}
/**
* Returns the index of the current active item.
* @param {Carousel} self the `Carousel` instance
* @returns {number} the query result
*/
function getActiveIndex(self) {
const { slides, element } = self;
const activeItem = querySelector(`.${carouselItem}.${activeClass}`, element);
// @ts-ignore
return [...slides].indexOf(activeItem);
}
// CAROUSEL DEFINITION
// ===================
/** Creates a new `Carousel` instance. */
class Carousel extends BaseComponent {
/**
* @param {HTMLElement | Element | string} target mostly a `.carousel` element
* @param {BSN.Options.Carousel=} config instance options
*/
constructor(target, config) {
super(target, config);
// bind
const self = this;
// additional properties
/** @type {string} */
self.direction = isRTL() ? 'right' : 'left';
/** @type {number} */
self.index = 0;
/** @type {boolean} */
self.isTouch = false;
// initialization element
const { element } = self;
// carousel elements
// a LIVE collection is prefferable
self.slides = getElementsByClassName(carouselItem, element);
const { slides } = self;
// invalidate when not enough items
// no need to go further
if (slides.length < 2) { return; }
self.controls = [
...querySelectorAll(`[${dataBsSlide}]`, element),
...querySelectorAll(`[${dataBsSlide}][${dataBsTarget}="#${element.id}"]`),
];
/** @type {(HTMLElement | Element)?} */
self.indicator = querySelector(`.${carouselString}-indicators`, element);
// a LIVE collection is prefferable
/** @type {(HTMLElement | Element)[]} */
self.indicators = [
...(self.indicator ? querySelectorAll(`[${dataBsSlideTo}]`, self.indicator) : []),
...querySelectorAll(`[${dataBsSlideTo}][${dataBsTarget}="#${element.id}"]`),
];
// 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
? carouselDefaults.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);
}
// attach event handlers
toggleCarouselHandlers(self, true);
// start to cycle if interval is set
if (options.interval) self.cycle();
}
/* eslint-disable */
/**
* Returns component name string.
* @readonly @static
*/
get name() { return carouselComponent; }
/**
* Returns component default options.
* @readonly @static
*/
get defaults() { return carouselDefaults; }
/* eslint-enable */
/**
* Check if instance is paused.
* @returns {boolean}
*/
get isPaused() {
return hasClass(this.element, pausedClass);
}
/**
* Check if instance is animating.
* @returns {boolean}
*/
get isAnimating() {
return querySelector(`.${carouselItem}-next,.${carouselItem}-prev`, this.element) !== null;
}
// CAROUSEL PUBLIC METHODS
// =======================
/** Slide automatically through items. */
cycle() {
const self = this;
const { element, options, isPaused } = self;
Timer.clear(element, carouselString);
if (isPaused) {
Timer.clear(element, pausedClass);
removeClass(element, pausedClass);
}
Timer.set(element, () => {
if (!self.isPaused && isElementInScrollRange(element)) {
self.index += 1;
self.to(self.index);
}
}, options.interval, carouselString);
}
/** Pause the automatic cycle. */
pause() {
const self = this;
const { element, options } = self;
if (!self.isPaused && options.interval) {
addClass(element, pausedClass);
Timer.set(element, () => {}, 1, pausedClass);
}
}
/** Slide to the next item. */
next() {
const self = this;
if (!self.isAnimating) { self.index += 1; self.to(self.index); }
}
/** Slide to the previous item. */
prev() {
const self = this;
if (!self.isAnimating) { self.index -= 1; self.to(self.index); }
}
/**
* Jump to the item with the `idx` index.
* @param {number} idx the index of the item to jump to
*/
to(idx) {
const self = this;
const {
element, slides, options,
} = self;
const activeItem = getActiveIndex(self);
const RTL = isRTL();
let next = idx;
// when controled via methods, make sure to check again
// first return if we're on the same item #227
if (self.isAnimating || activeItem === next) return;
// determine transition direction
if ((activeItem < next) || (activeItem === 0 && next === slides.length - 1)) {
self.direction = RTL ? 'right' : 'left'; // next
} else if ((activeItem > next) || (activeItem === slides.length - 1 && next === 0)) {
self.direction = RTL ? 'left' : 'right'; // prev
}
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],
from: activeItem,
to: next,
direction,
};
// update event properties
ObjectAssign(carouselSlideEvent, eventProperties);
ObjectAssign(carouselSlidEvent, eventProperties);
// discontinue when prevented
dispatchEvent(element, carouselSlideEvent);
if (carouselSlideEvent.defaultPrevented) return;
// update index
self.index = next;
activateCarouselIndicator(self, next);
if (getElementTransitionDuration(slides[next]) && hasClass(element, 'slide')) {
Timer.set(element, () => {
addClass(slides[next], `${carouselItem}-${orientation}`);
reflow(slides[next]);
addClass(slides[next], `${carouselItem}-${directionClass}`);
addClass(slides[activeItem], `${carouselItem}-${directionClass}`);
emulateTransitionEnd(slides[next], () => carouselTransitionEndHandler(self));
}, 17, dataBsSlide);
} else {
addClass(slides[next], activeClass);
removeClass(slides[activeItem], activeClass);
Timer.set(element, () => {
Timer.clear(element, dataBsSlide);
// check for element, might have been disposed
if (element && options.interval && !self.isPaused) {
self.cycle();
}
dispatchEvent(element, carouselSlidEvent);
}, 17, dataBsSlide);
}
}
/** Remove `Carousel` component from target. */
dispose() {
const self = this;
const { slides } = self;
const itemClasses = ['start', 'end', 'prev', 'next'];
[...slides].forEach((slide, idx) => {
if (hasClass(slide, activeClass)) activateCarouselIndicator(self, idx);
itemClasses.forEach((c) => removeClass(slide, `${carouselItem}-${c}`));
});
toggleCarouselHandlers(self);
super.dispose();
}
}
ObjectAssign(Carousel, {
selector: carouselSelector,
init: carouselInitCallback,
getInstance: getCarouselInstance,
});
/**
* A global namespace for aria-expanded.
* @type {string}
*/
const ariaExpanded = 'aria-expanded';
/**
* Global namespace for most components `collapsing` class.
* As used by `Collapse` / `Tab`.
*/
const collapsingClass = 'collapsing';
/** @type {string} */
const collapseString = 'collapse';
/** @type {string} */
const collapseComponent = 'Collapse';
/* Native JavaScript for Bootstrap 5 | Collapse
----------------------------------------------- */
// COLLAPSE GC
// ===========
const collapseSelector = `.${collapseString}`;
const collapseToggleSelector = `[${dataBsToggle}="${collapseString}"]`;
const collapseDefaults = { parent: null };
/**
* Static method which returns an existing `Collapse` instance associated
* to a target `Element`.
*
* @type {BSN.GetInstance<Collapse>}
*/
const getCollapseInstance = (element) => getInstance(element, collapseComponent);
/**
* A `Collapse` initialization callback.
* @type {BSN.InitCallback<Collapse>}
*/
const collapseInitCallback = (element) => new Collapse(element);
// COLLAPSE CUSTOM EVENTS
// ======================
const showCollapseEvent = OriginalEvent(`show.bs.${collapseString}`);
const shownCollapseEvent = OriginalEvent(`shown.bs.${collapseString}`);
const hideCollapseEvent = OriginalEvent(`hide.bs.${collapseString}`);
const hiddenCollapseEvent = OriginalEvent(`hidden.bs.${collapseString}`);
// COLLAPSE PRIVATE METHODS
// ========================
/**
* Expand the designated `Element`.
* @param {Collapse} self the `Collapse` instance
*/
function expandCollapse(self) {
const {
element, parent, triggers,
} = self;
dispatchEvent(element, showCollapseEvent);
if (showCollapseEvent.defaultPrevented) return;
Timer.set(element, () => {}, 17);
if (parent) Timer.set(parent, () => {}, 17);
addClass(element, collapsingClass);
removeClass(element, collapseString);
// @ts-ignore
element.style.height = `${element.scrollHeight}px`;
emulateTransitionEnd(element, () => {
Timer.clear(element);
if (parent) Timer.clear(parent);
triggers.forEach((btn) => setAttribute(btn, ariaExpanded, 'true'));
removeClass(element, collapsingClass);
addClass(element, collapseString);
addClass(element, showClass);
// @ts-ignore
element.style.height = '';
dispatchEvent(element, shownCollapseEvent);
});
}
/**
* Collapse the designated `Element`.
* @param {Collapse} self the `Collapse` instance
*/
function collapseContent(self) {
const {
// @ts-ignore
element, parent, triggers,
} = self;
dispatchEvent(element, hideCollapseEvent);
if (hideCollapseEvent.defaultPrevented) return;
Timer.set(element, () => {}, 17);
if (parent) Timer.set(parent, () => {}, 17);
// @ts-ignore
element.style.height = `${element.scrollHeight}px`;
removeClass(element, collapseString);
removeClass(element, showClass);
addClass(element, collapsingClass);
reflow(element);
// @ts-ignore
element.style.height = '0px';
emulateTransitionEnd(element, () => {
Timer.clear(element);
if (parent) Timer.clear(parent);
triggers.forEach((btn) => setAttribute(btn, ariaExpanded, 'false'));
removeClass(element, collapsingClass);
addClass(element, collapseString);
// @ts-ignore
element.style.height = '';
dispatchEvent(element, hiddenCollapseEvent);
});
}
/**
* Toggles on/off the event listener(s) of the `Collapse` instance.
* @param {Collapse} self the `Collapse` instance
* @param {boolean=} add when `true`, the event listener is added
*/
function toggleCollapseHandler(self, add) {
const action = add ? addListener : removeListener;
const { triggers } = self;
if (triggers.length) {
triggers.forEach((btn) => action(btn, mouseclickEvent, collapseClickHandler));
}
}
// COLLAPSE EVENT HANDLER
// ======================
/**
* Handles the `click` event for the `Collapse` instance.
* @param {MouseEvent} e the `Event` object
*/
function collapseClickHandler(e) {
const { target } = e; // @ts-ignore - our target is `HTMLElement`
const trigger = target && closest(target, collapseToggleSelector);
const element = trigger && getTargetElement(trigger);
const self = element && getCollapseInstance(element);
if (self) self.toggle();
// event target is anchor link #398
if (trigger && trigger.tagName === 'A') e.preventDefault();
}
// COLLAPSE DEFINITION
// ===================
/** Returns a new `Colapse` instance. */
class Collapse extends BaseComponent {
/**
* @param {HTMLElement | Element | string} target and `Element` that matches the selector
* @param {BSN.Options.Collapse=} config instance options
*/
constructor(target, config) {
super(target, config);
// bind
const self = this;
// initialization element
const { element, options } = self;
// set triggering elements
/** @type {(HTMLElement | Element)[]} */
self.triggers = [...querySelectorAll(collapseToggleSelector)]
.filter((btn) => getTargetElement(btn) === element);
// set parent accordion
/** @type {(HTMLElement | Element)?} */
self.parent = querySelector(options.parent);
// add event listeners
toggleCollapseHandler(self, true);
}
/* eslint-disable */
/**
* Returns component name string.
* @readonly @static
*/
get name() { return collapseComponent; }
/**
* Returns component default options.
* @readonly @static
*/
get defaults() { return collapseDefaults; }
/* eslint-enable */
// COLLAPSE PUBLIC METHODS
// =======================
/** Toggles the visibility of the collapse. */
toggle() {
const self = this;
if (!hasClass(self.element, showClass)) self.show();
else self.hide();
}
/** Hides the collapse. */
hide() {
const self = this;
const { triggers, element } = self;
if (Timer.get(element)) return;
collapseContent(self);
if (triggers.length) {
triggers.forEach((btn) => addClass(btn, `${collapseString}d`));
}
}
/** Shows the collapse. */
show() {
const self = this;
const {
element, parent, triggers,
} = self;
let activeCollapse;
let activeCollapseInstance;
if (parent) {
activeCollapse = [...querySelectorAll(`.${collapseString}.${showClass}`, parent)]
.find((i) => getCollapseInstance(i));
activeCollapseInstance = activeCollapse && getCollapseInstance(activeCollapse);
}
if ((!parent || (parent && !Timer.get(parent))) && !Timer.get(element)) {
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`));
}
}
}
/** Remove the `Collapse` component from the target `Element`. */
dispose() {
const self = this;
toggleCollapseHandler(self);
super.dispose();
}
}
ObjectAssign(Collapse, {
selector: collapseSelector,
init: collapseInitCallback,
getInstance: getCollapseInstance,
});
/**
* A global namespace for `focus` event.
* @type {string}
*/
const focusEvent = 'focus';
/**
* A global namespace for `keyup` event.
* @type {string}
*/
const keyupEvent = 'keyup';
/**
* A global namespace for `scroll` event.
* @type {string}
*/
const scrollEvent = 'scroll';
/**
* A global namespace for `resize` event.
* @type {string}
*/
const resizeEvent = 'resize';
/**
* A global namespace for `ArrowUp` key.
* @type {string} e.which = 38 equivalent
*/
const keyArrowUp = 'ArrowUp';
/**
* A global namespace for `ArrowDown` key.
* @type {string} e.which = 40 equivalent
*/
const keyArrowDown = 'ArrowDown';
/**
* A global namespace for `Escape` key.
* @type {string} e.which = 27 equivalent
*/
const keyEscape = 'Escape';
/**
* Shortcut for `HTMLElement.hasAttribute()` method.
* @param {HTMLElement | Element} element target element
* @param {string} attribute attribute name
* @returns {boolean} the query result
*/
const hasAttribute = (element, attribute) => element.hasAttribute(attribute);
/**
* Shortcut for multiple uses of `HTMLElement.style.propertyName` method.
* @param {HTMLElement | Element} element target element
* @param {Partial<CSSStyleDeclaration>} styles attribute value
*/
// @ts-ignore
const setElementStyle = (element, styles) => { ObjectAssign(element.style, styles); };
/**
* Utility to focus an `HTMLElement` target.
*
* @param {HTMLElement | Element} element is the target
*/
// @ts-ignore -- `Element`s resulted from querySelector can focus too
const focus = (element) => element.focus();
/**
* Global namespace for `Dropdown` types / classes.
*/
const dropdownMenuClasses = ['dropdown', 'dropup', 'dropstart', 'dropend'];
/** @type {string} */
const dropdownComponent = 'Dropdown';
/**
* Global namespace for `.dropdown-menu`.
*/
const dropdownMenuClass = 'dropdown-menu';
/**
* Checks if an *event.target* or its parent has an `href="#"` value.
* We need to prevent jumping around onclick, don't we?
*
* @param {HTMLElement | HTMLAnchorElement | EventTarget} element the target element
* @returns {boolean} the query result
*/
function isEmptyAnchor(element) {
// @ts-ignore -- `EventTarget` must be `HTMLElement`
const parentAnchor = closest(element, 'A');
// @ts-ignore -- anchor href starts with #
return element && ((hasAttribute(element, 'href') && element.href.slice(-1) === '#')
// @ts-ignore -- OR a child of an anchor with href starts with #
|| (parentAnchor && hasAttribute(parentAnchor, 'href') && parentAnchor.href.slice(-1) === '#'));
}
/* Native JavaScript for Bootstrap 5 | Dropdown
----------------------------------------------- */
// DROPDOWN PRIVATE GC
// ===================
const [
dropdownString,
dropupString,
dropstartString,
dropendString,
] = dropdownMenuClasses;
const dropdownSelector = `[${dataBsToggle}="${dropdownString}"]`;
/**
* Static method which returns an existing `Dropdown` instance associated
* to a target `Element`.
*
* @type {BSN.GetInstance<Dropdown>}
*/
const getDropdownInstance = (element) => getInstance(element, dropdownComponent);
/**
* A `Dropdown` initialization callback.
* @type {BSN.InitCallback<Dropdown>}
*/
const dropdownInitCallback = (element) => new Dropdown(element);
// DROPDOWN PRIVATE GC
// ===================
const dropdownMenuEndClass = `${dropdownMenuClass}-end`;
const verticalClass = [dropdownString, dropupString];
const horizontalClass = [dropstartString, dropendString];
const menuFocusTags = ['A', 'BUTTON'];
const dropdownDefaults = {
offset: 5, // [number] 5(px)
display: 'dynamic', // [dynamic|static]
};
// DROPDOWN CUSTOM EVENTS
// ======================
const showDropdownEvent = OriginalEvent(`show.bs.${dropdownString}`);
const shownDropdownEvent = OriginalEvent(`shown.bs.${dropdownString}`);
const hideDropdownEvent = OriginalEvent(`hide.bs.${dropdownString}`);
const hiddenDropdownEvent = OriginalEvent(`hidden.bs.${dropdownString}`);
// DROPDOWN PRIVATE METHODS
// ========================
/**
* Apply specific style or class names to a `.dropdown-menu` to automatically
* accomodate the layout and the page scroll.
*
* @param {Dropdown} self the `Dropdown` instance
*/
function styleDropdown(self) {
const {
element, menu, parentElement, options,
} = self;
const { offset } = options;
// don't apply any style on mobile view
if (getElementStyle(menu, 'position') === 'static') return;
const RTL = isRTL(element);
const menuEnd = hasClass(parentElement, dropdownMenuEndClass);
// reset menu offset and position
const resetProps = ['margin', 'top', 'bottom', 'left', 'right'];
// @ts-ignore
resetProps.forEach((p) => { menu.style[p] = ''; });
// set initial position class
// take into account .btn-group parent as .dropdown
let positionClass = dropdownMenuClasses.find((c) => hasClass(parentElement, c)) || dropdownString;
/** @type {Record<string, Record<string, any>>} */
let dropdownMargin = {
dropdown: [offset, 0, 0],
dropup: [0, 0, offset],
dropstart: RTL ? [-1, 0, 0, offset] : [-1, offset, 0],
dropend: RTL ? [-1, offset, 0] : [-1, 0, 0, offset],
};
/** @type {Record<string, Record<string, any>>} */
const dropdownPosition = {
dropdown: { top: '100%' },
dropup: { top: 'auto', bottom: '100%' },
dropstart: RTL ? { left: '100%', right: 'auto' } : { left: 'auto', right: '100%' },
dropend: RTL ? { left: 'auto', right: '100%' } : { left: '100%', right: 'auto' },
menuEnd: RTL ? { right: 'auto', left: 0 } : { right: 0, left: 'auto' },
};
// @ts-ignore
const { offsetWidth: menuWidth, offsetHeight: menuHeight } = menu;
const { clientWidth, clientHeight } = getDocumentElement(element);
const {
left: targetLeft, top: targetTop,
width: targetWidth, height: targetHeight,
} = getBoundingClientRect(element);
// dropstart | dropend
const leftFullExceed = targetLeft - menuWidth - offset < 0;
// dropend
const rightFullExceed = targetLeft + menuWidth + targetWidth + offset >= clientWidth;
// dropstart | dropend
const bottomExceed = targetTop + menuHeight + offset >= clientHeight;
// dropdown
const bottomFullExceed = targetTop + menuHeight + targetHeight + offset >= clientHeight;
// dropup
const topExceed = targetTop - menuHeight - offset < 0;
// dropdown / dropup
const leftExceed = ((!RTL && menuEnd) || (RTL && !menuEnd))
&& targetLeft + targetWidth - menuWidth < 0;
const rightExceed = ((RTL && menuEnd) || (!RTL && !menuEnd))
&& targetLeft + menuWidth >= clientWidth;
// recompute position
// handle RTL as well
if (horizontalClass.includes(positionClass) && leftFullExceed && rightFullExceed) {
positionClass = dropdownString;
}
if (positionClass === dropstartString && (!RTL ? leftFullExceed : rightFullExceed)) {
positionClass = dropendString;
}
if (positionClass === dropendString && (RTL ? leftFullExceed : rightFullExceed)) {
positionClass = dropstartString;
}
if (positionClass === dropupString && topExceed && !bottomFullExceed) {
positionClass = dropdownString;
}
if (positionClass === dropdownString && bottomFullExceed && !topExceed) {
positionClass = dropupString;
}
// override position for horizontal classes
if (horizontalClass.includes(positionClass) && bottomExceed) {
ObjectAssign(dropdownPosition[positionClass], {
top: 'auto', bottom: 0,
});
}
// override position for vertical classes
if (verticalClass.includes(positionClass) && (leftExceed || rightExceed)) {
// don't realign when menu is wider than window
// in both RTL and non-RTL readability is KING
if (targetLeft + targetWidth + Math.abs(menuWidth - targetWidth) + offset < clientWidth) {
ObjectAssign(dropdownPosition[positionClass],
leftExceed ? { left: 0, right: 'auto' } : { left: 'auto', right: 0 });
}
}
dropdownMargin = dropdownMargin[positionClass];
// @ts-ignore
menu.style.margin = `${dropdownMargin.map((x) => (x ? `${x}px` : x)).join(' ')}`;
setElementStyle(menu, dropdownPosition[positionClass]);
// update dropdown-menu-end
if (hasClass(menu, dropdownMenuEndClass)) {
setElementStyle(menu, dropdownPosition.menuEnd);
}
}
/**
* Returns an `Array` of focusable items in the given dropdown-menu.
* @param {HTMLElement | Element} menu
* @returns {(HTMLElement | Element)[]}
*/
function getMenuItems(menu) {
// @ts-ignore
return [...menu.children].map((c) => {
if (c && menuFocusTags.includes(c.tagName)) return c;
const { firstElementChild } = c;
if (firstElementChild && menuFocusTags.includes(firstElementChild.tagName)) {
return firstElementChild;
}
return null;
}).filter((c) => c);
}
/**
* Toggles on/off the listeners for the events that close the dropdown
* as well as event that request a new position for the dropdown.
*
* @param {Dropdown} self the `Dropdown` instance
*/
function toggleDropdownDismiss(self) {
const { element } = self;
const action = self.open ? addListener : removeListener;
const doc = getDocument(element);
action(doc, mouseclickEvent, dropdownDismissHandler);
action(doc, focusEvent, dropdownDismissHandler);
action(doc, keydownEvent, dropdownPreventScroll);
action(doc, keyupEvent, dropdownKeyHandler);
if (self.options.display === 'dynamic') {
[scrollEvent, resizeEvent].forEach((ev) => {
// @ts-ignore
action(getWindow(element), ev, dropdownLayoutHandler, passiveHandler);
});
}
}
/**
* Toggles on/off the `click` event listener of the `Dropdown`.
*
* @param {Dropdown} self the `Dropdown` instance
* @param {boolean=} add when `true`, it will add the event listener
*/
function toggleDropdownHandler(self, add) {
const action = add ? addListener : removeListener;
action(self.element, mouseclickEvent, dropdownClickHandler);
}
/**
* Returns the currently open `.dropdown` element.
*
* @param {(Document | HTMLElement | Element | globalThis)=} element target
* @returns {HTMLElement?} the query result
*/
function getCurrentOpenDropdown(element) {
const currentParent = [...dropdownMenuClasses, 'btn-group', 'input-group']
.map((c) => getElementsByClassName(`${c} ${showClass}`), getDocument(element))
.find((x) => x.length);
if (currentParent && currentParent.length) {
// @ts-ignore -- HTMLElement is also Element
return [...currentParent[0].children]
.find((x) => hasAttribute(x, dataBsToggle));
}
return null;
}
// DROPDOWN EVENT HANDLERS
// =======================
/**
* Handles the `click` event for the `Dropdown` instance.
*
* @param {MouseEvent} e event object
* @this {Document}
*/
function dropdownDismissHandler(e) {
const { target, type } = e;
// @ts-ignore
if (!target || !target.closest) return; // some weird FF bug #409
// @ts-ignore
const element = getCurrentOpenDropdown(target);
if (!element) return;
const self = getDropdownInstance(element);
if (!self) return;
const { parentElement, menu } = self;
// @ts-ignore
const hasData = closest(target, dropdownSelector) !== null;
// @ts-ignore
const isForm = parentElement && parentElement.contains(target)
// @ts-ignore
&& (target.tagName === 'form' || closest(target, 'form') !== null);
// @ts-ignore
if (type === mouseclickEvent && isEmptyAnchor(target)) {
e.preventDefault();
}
if (type === focusEvent // @ts-ignore
&& (target === element || target === menu || menu.contains(target))) {
return;
}
if (isForm || hasData) ; else if (self) {
self.hide();
}
}
/**
* Handles `click` event listener for `Dropdown`.
* @this {HTMLElement | Element}
* @param {MouseEvent} e event object
*/
function dropdownClickHandler(e) {
const element = this;
const { target } = e;
const self = getDropdownInstance(element);
if (self) {
self.toggle();
if (target && isEmptyAnchor(target)) e.preventDefault();
}
}
/**
* Prevents scroll when dropdown-menu is visible.
* @param {KeyboardEvent} e event object
*/
function dropdownPreventScroll(e) {
if ([keyArrowDown, keyArrowUp].includes(e.code)) e.preventDefault();
}
/**
* Handles keyboard `keydown` events for `Dropdown`.
* @param {KeyboardEvent} e keyboard key
* @this {Document}
*/
function dropdownKeyHandler(e) {
const { code } = e;
const element = getCurrentOpenDropdown(this);
const self = element && getDropdownInstance(element);
const activeItem = element && getDocument(element).activeElement;
if (!self || !activeItem) return;
const { menu, open } = self;
const menuItems = getMenuItems(menu);
// arrow up & down
if (menuItems && menuItems.length && [keyArrowDown, keyArrowUp].includes(code)) {
let idx = menuItems.indexOf(activeItem);
if (activeItem === element) {
idx = 0;
} else if (code === keyArrowUp) {
idx = idx > 1 ? idx - 1 : 0;
} else if (code === keyArrowDown) {
idx = idx < menuItems.length - 1 ? idx + 1 : idx;
}
if (menuItems[idx]) focus(menuItems[idx]);
}
if (keyEscape === code && open) {
self.toggle();
focus(element);
}
}
/**
* @this {globalThis}
* @returns {void}
*/
function dropdownLayoutHandler() {
const element = getCurrentOpenDropdown(this);
const self = element && getDropdownInstance(element);
if (self && self.open) styleDropdown(self);
}
// DROPDOWN DEFINITION
// ===================
/** Returns a new Dropdown instance. */
class Dropdown extends BaseComponent {
/**
* @param {HTMLElement | Element | string} target Element or string selector
* @param {BSN.Options.Dropdown=} config the instance options
*/
constructor(target, config) {
super(target, config);
// bind
const self = this;
// initialization element
const { element } = self;
const { parentElement } = element;
// set targets
/** @type {(Element | HTMLElement)} */
// @ts-ignore
self.parentElement = parentElement;
/** @type {(Element | HTMLElement)} */
// @ts-ignore
self.menu = querySelector(`.${dropdownMenuClass}`, parentElement);
// set initial state to closed
/** @type {boolean} */
self.open = false;
// add event listener
toggleDropdownHandler(self, true);
}
/* eslint-disable */
/**
* Returns component name string.
* @readonly @static
*/
get name() { return dropdownComponent; }
/**
* Returns component default options.
* @readonly @static
*/
get defaults() { return dropdownDefaults; }
/* eslint-enable */
// DROPDOWN PUBLIC METHODS
// =======================
/** Shows/hides the dropdown menu to the user. */
toggle() {
const self = this;
if (self.open) self.hide();
else self.show();
}
/** Shows the dropdown menu to the user. */
show() {
const self = this;
const {
element, open, menu, parentElement,
} = self;
const currentElement = getCurrentOpenDropdown(element);
const currentInstance = currentElement && getDropdownInstance(currentElement);
if (currentInstance) currentInstance.hide();
// dispatch
[showDropdownEvent, shownDropdownEvent].forEach((e) => { e.relatedTarget = element; });
dispatchEvent(parentElement, showDropdownEvent);
if (showDropdownEvent.defaultPrevented) return;
addClass(menu, showClass);
addClass(parentElement, showClass);
setAttribute(element, ariaExpanded, 'true');
// change menu position
styleDropdown(self);
self.open = !open;
setTimeout(() => {
focus(element); // focus the element
toggleDropdownDismiss(self);
dispatchEvent(parentElement, shownDropdownEvent);
}, 1);
}
/** Hides the dropdown menu from the user. */
hide() {
const self = this;
const {
element, open, menu, parentElement,
} = self;
[hideDropdownEvent, hiddenDropdownEvent].forEach((e) => { e.relatedTarget = element; });
dispatchEvent(parentElement, hideDropdownEvent);
if (hideDropdownEvent.defaultPrevented) return;
removeClass(menu, showClass);
removeClass(parentElement, showClass);
setAttribute(element, ariaExpanded, 'false');
self.open = !open;
// only re-attach handler if the instance is not disposed
setTimeout(() => toggleDropdownDismiss(self), 1);
dispatchEvent(parentElement, hiddenDropdownEvent);
}
/** Removes the `Dropdown` component from the target element. */
dispose() {
const self = this;
const { parentElement } = self;
if (hasClass(parentElement, showClass) && self.open) self.hide();
toggleDropdownHandler(self);
super.dispose();
}
}
ObjectAssign(Dropdown, {
selector: dropdownSelector,
init: dropdownInitCallback,
getInstance: getDropdownInstance,
});
/**
* A global namespace for aria-hidden.
* @type {string}
*/
const ariaHidden = 'aria-hidden';
/**
* A global namespace for aria-modal.
* @type {string}
*/
const ariaModal = 'aria-modal';
/**
* Shortcut for `HTMLElement.removeAttribute()` method.
* @param {HTMLElement | Element} element target element
* @param {string} attribute attribute name
* @returns {void}
*/
const removeAttribute = (element, attribute) => element.removeAttribute(attribute);
/**
* Returns the `document.body` or the `<body>` element.
*
* @param {(Node | HTMLElement | Element | globalThis)=} node
* @returns {HTMLElement | HTMLBodyElement}
*/
function getDocumentBody(node) {
return getDocument(node).body;
}
/** @type {string} */
const modalString = 'modal';
/** @type {string} */
const modalComponent = 'Modal';
/**
* Check if target is a `ShadowRoot`.
*
* @param {any} element target
* @returns {boolean} the query result
*/
const isShadowRoot = (element) => {
const OwnElement = getWindow(element).ShadowRoot;
return element instanceof OwnElement || element instanceof ShadowRoot;
};
/**
* Returns the `parentNode` also going through `ShadowRoot`.
* @see https://github.com/floating-ui/floating-ui
*
* @param {Node | HTMLElement | Element} node the target node
* @returns {Node | HTMLElement | Element} the apropriate parent node
*/
function getParentNode(node) {
if (node.nodeName === 'HTML') {
return node;
}
// this is a quicker (but less type safe) way to save quite some bytes from the bundle
return (
// @ts-ignore
node.assignedSlot // step into the shadow DOM of the parent of a slotted node
|| node.parentNode // @ts-ignore DOM Element detected
|| (isShadowRoot(node) ? node.host : null) // ShadowRoot detected
|| getDocumentElement(node) // fallback
);
}
/**
* Check if a target element is a `<table>`, `<td>` or `<th>`.
* @param {any} element the target element
* @returns {boolean} the query result
*/
const isTableElement = (element) => ['TABLE', 'TD', 'TH'].includes(element.tagName);
/**
* Checks if an element is an `HTMLElement`.
*
* @param {any} element the target object
* @returns {boolean} the query result
*/
const isHTMLElement = (element) => element instanceof HTMLElement;
/**
* Returns an `HTMLElement` to be used as default value for *options.container*
* for `Tooltip` / `Popover` components.
*
* When `getOffset` is *true*, it returns the `offsetParent` for tooltip/popover
* offsets computation similar to **floating-ui**.
* @see https://github.com/floating-ui/floating-ui
*
* @param {HTMLElement | Element} element the target
* @param {boolean=} getOffset when *true* it will return an `offsetParent`
* @returns {HTMLElement | HTMLBodyElement | Window | globalThis} the query result
*/
function getElementContainer(element, getOffset) {
const majorBlockTags = ['HTML', 'BODY'];
if (getOffset) {
/** @type {any} */
let { offsetParent } = element;
const win = getWindow(element);
// const { innerWidth } = getDocumentElement(element);
while (offsetParent && (isTableElement(offsetParent)
|| (isHTMLElement(offsetParent)
// we must count for both fixed & sticky
&& !['sticky', 'fixed'].includes(getElementStyle(offsetParent, 'position'))))) {
offsetParent = offsetParent.offsetParent;
}
if (!offsetParent || (offsetParent
&& (majorBlockTags.includes(offsetParent.tagName)
|| getElementStyle(offsetParent, 'position') === 'static'))) {
offsetParent = win;
}
return offsetParent;
}
/** @type {(HTMLElement)[]} */
const containers = [];
/** @type {any} */
let { parentNode } = element;
while (parentNode && !majorBlockTags.includes(parentNode.nodeName)) {
parentNode = getParentNode(parentNode);
if (!(isShadowRoot(parentNode) || !!parentNode.shadowRoot
|| isTableElement(parentNode))) {
containers.push(parentNode);
}
}
return containers.find((c, i) => {
if (getElementStyle(c, 'position') !== 'relative'
&& containers.slice(i + 1).every((r) => getElementStyle(r, 'position') === 'static')) {
return c;
}
return null;
}) || getDocumentBody(element);
}
/**
* Global namespace for components `fixed-top` class.
*/
const fixedTopClass = 'fixed-top';
/**
* Global namespace for components `fixed-bottom` class.
*/
const fixedBottomClass = 'fixed-bottom';
/**
* Global namespace for components `sticky-top` class.
*/
const stickyTopClass = 'sticky-top';
/**
* Global namespace for components `position-sticky` class.
*/
const positionStickyClass = 'position-sticky';
/** @param {(HTMLElement | Element | Document)=} parent */
const getFixedItems = (parent) => [
...getElementsByClassName(fixedTopClass, parent),
...getElementsByClassName(fixedBottomClass, parent),
...getElementsByClassName(stickyTopClass, parent),
...getElementsByClassName(positionStickyClass, parent),
...getElementsByClassName('is-fixed', parent),
];
/**
* Removes *padding* and *overflow* from the `<body>`
* and all spacing from fixed items.
* @param {(HTMLElement | Element)=} element the target modal/offcanvas
*/
function resetScrollbar(element) {
const bd = getDocumentBody(element);
setElementStyle(bd, {
paddingRight: '',
overflow: '',
});
const fixedItems = getFixedItems(bd);
if (fixedItems.length) {
fixedItems.forEach((fixed) => {
setElementStyle(fixed, {
paddingRight: '',
marginRight: '',
});
});
}
}
/**
* Returns the scrollbar width if the body does overflow
* the window.
* @param {(HTMLElement | Element)=} element
* @returns {number} the value
*/
function measureScrollbar(element) {
const { clientWidth } = getDocumentElement(element);
const { innerWidth } = getWindow(element);
return Math.abs(innerWidth - clientWidth);
}
/**
* Sets the `<body>` and fixed items style when modal / offcanvas
* is shown to the user.
*
* @param {HTMLElement | Element} element the target modal/offcanvas
* @param {boolean=} overflow body does overflow or not
*/
function setScrollbar(element, overflow) {
const bd = getDocumentBody(element);
const bodyPad = parseInt(getElementStyle(bd, 'paddingRight'), 10);
const isOpen = getElementStyle(bd, 'overflow') === 'hidden';
const sbWidth = isOpen && bodyPad ? 0 : measureScrollbar(element);
const fixedItems = getFixedItems(bd);
if (overflow) {
setElementStyle(bd, {
overflow: 'hidden',
paddingRight: `${bodyPad + sbWidth}px`,
});
if (fixedItems.length) {
fixedItems.forEach((fixed) => {
const itemPadValue = getElementStyle(fixed, 'paddingRight');
// @ts-ignore
fixed.style.paddingRight = `${parseInt(itemPadValue, 10) + sbWidth}px`;
if ([stickyTopClass, positionStickyClass].some((c) => hasClass(fixed, c))) {
const itemMValue = getElementStyle(fixed, 'marginRight');
// @ts-ignore
fixed.style.marginRight = `${parseInt(itemMValue, 10) - sbWidth}px`;
}
});
}
}
}
/**
* This is a shortie for `document.createElement` method
* which allows you to create a new `HTMLElement` for a given `tagName`
* or based on an object with specific non-readonly attributes:
* `id`, `className`, `textContent`, `style`, etc.
* @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement
*
* @param {Record<string, string> | string} param `tagName` or object
* @return {HTMLElement | Element} a new `HTMLElement` or `Element`
*/
function createElement(param) {
if (typeof param === 'string') {
return getDocument().createElement(param);
}
const { tagName } = param;
const attr = { ...param };
const newElement = createElement(tagName);
delete attr.tagName;
ObjectAssign(newElement, attr);
return newElement;
}
/** @type {string} */
const offcanvasString = 'offcanvas';
const backdropString = 'backdrop';
const modalBackdropClass = `${modalString}-${backdropString}`;
const offcanvasBackdropClass = `${offcanvasString}-${backdropString}`;
const modalActiveSelector = `.${modalString}.${showClass}`;
const offcanvasActiveSelector = `.${offcanvasString}.${showClass}`;
// any document would suffice
const overlay = createElement('div');
/**
* Returns the current active modal / offcancas element.
* @param {(HTMLElement | Element)=} element the context element
* @returns {(HTMLElement | Element)?} the requested element
*/
function getCurrentOpen(element) {
return querySelector(`${modalActiveSelector},${offcanvasActiveSelector}`, getDocument(element));
}
/**
* Toogles from a Modal overlay to an Offcanvas, or vice-versa.
* @param {boolean=} isModal
*/
function toggleOverlayType(isModal) {
const targetClass = isModal ? modalBackdropClass : offcanvasBackdropClass;
[modalBackdropClass, offcanvasBackdropClass].forEach((c) => {
removeClass(overlay, c);
});
addClass(overlay, targetClass);
}
/**
* Append the overlay to DOM.
* @param {HTMLElement | Element} container
* @param {boolean} hasFade
* @param {boolean=} isModal
*/
function appendOverlay(container, hasFade, isModal) {
toggleOverlayType(isModal);
container.append(overlay);
if (hasFade) addClass(overlay, fadeClass);
}
/**
* Shows the overlay to the user.
*/
function showOverlay() {
if (!hasClass(overlay, showClass)) {
addClass(overlay, showClass);
reflow(overlay);
}
}
/**
* Hides the overlay from the user.
*/
function hideOverlay() {
removeClass(overlay, showClass);
}
/**
* Removes the overlay from DOM.
* @param {(HTMLElement | Element)=} element
*/
function removeOverlay(element) {
if (!getCurrentOpen(element)) {
removeClass(overlay, fadeClass);
overlay.remove();
resetScrollbar(element);
}
}
/**
* @param {HTMLElement | Element} element target
* @returns {boolean}
*/
function isVisible(element) {
return element && getElementStyle(element, 'visibility') !== 'hidden'
// @ts-ignore
&& element.offsetParent !== null;
}
/* Native JavaScript for Bootstrap 5 | Modal
-------------------------------------------- */
// MODAL PRIVATE GC
// ================
const modalSelector = `.${modalString}`;
const modalToggleSelector = `[${dataBsToggle}="${modalString}"]`;
const modalDismissSelector = `[${dataBsDismiss}="${modalString}"]`;
const modalStaticClass = `${modalString}-static`;
const modalDefaults = {
backdrop: true, // boolean|string
keyboard: true, // boolean
};
/**
* Static method which returns an existing `Modal` instance associated
* to a target `Element`.
*
* @type {BSN.GetInstance<Modal>}
*/
const getModalInstance = (element) => getInstance(element, modalComponent);
/**
* A `Modal` initialization callback.
* @type {BSN.InitCallback<Modal>}
*/
const modalInitCallback = (element) => new Modal(element);
// MODAL CUSTOM EVENTS
// ===================
const showModalEvent = OriginalEvent(`show.bs.${modalString}`);
const shownModalEvent = OriginalEvent(`shown.bs.${modalString}`);
const hideModalEvent = OriginalEvent(`hide.bs.${modalString}`);
const hiddenModalEvent = OriginalEvent(`hidden.bs.${modalString}`);
// MODAL PRIVATE METHODS
// =====================
/**
* Applies special style for the `<body>` and fixed elements
* when a modal instance is shown to the user.
*
* @param {Modal} self the `Modal` instance
*/
function setModalScrollbar(self) {
const { element } = self;
const scrollbarWidth = measureScrollbar(element);
const { clientHeight, scrollHeight } = getDocumentElement(element);
const { clientHeight: modalHeight, scrollHeight: modalScrollHeight } = element;
const modalOverflow = modalHeight !== modalScrollHeight;
if (!modalOverflow && scrollbarWidth) {
const pad = isRTL(element) ? 'paddingLeft' : 'paddingRight';
// @ts-ignore -- cannot use `setElementStyle`
element.style[pad] = `${scrollbarWidth}px`;
}
setScrollbar(element, (modalOverflow || clientHeight !== scrollHeight));
}
/**
* Toggles on/off the listeners of events that close the modal.
*
* @param {Modal} self the `Modal` instance
* @param {boolean=} add when `true`, event listeners are added
*/
function toggleModalDismiss(self, add) {
const action = add ? addListener : removeListener;
const { element } = self;
action(element, mouseclickEvent, modalDismissHandler);
// @ts-ignore
action(getWindow(element), resizeEvent, self.update, passiveHandler);
action(getDocument(element), keydownEvent, modalKeyHandler);
}
/**
* Toggles on/off the `click` event listener of the `Modal` instance.
* @param {Modal} self the `Modal` instance
* @param {boolean=} add when `true`, event listener is added
*/
function toggleModalHandler(self, add) {
const action = add ? addListener : removeListener;
const { triggers } = self;
if (triggers.length) {
triggers.forEach((btn) => action(btn, mouseclickEvent, modalClickHandler));
}
}
/**
* Executes after a modal is hidden to the user.
* @param {Modal} self the `Modal` instance
*/
function afterModalHide(self) {
const { triggers, element, relatedTarget } = self;
removeOverlay(element);
setElementStyle(element, { paddingRight: '' });
toggleModalDismiss(self);
const focusElement = showModalEvent.relatedTarget || triggers.find(isVisible);
if (focusElement) focus(focusElement);
hiddenModalEvent.relatedTarget = relatedTarget;
dispatchEvent(element, hiddenModalEvent);
}
/**
* Executes after a modal is shown to the user.
* @param {Modal} self the `Modal` instance
*/
function afterModalShow(self) {
const { element, relatedTarget } = self;
focus(element);
toggleModalDismiss(self, true);
shownModalEvent.relatedTarget = relatedTarget;
dispatchEvent(element, shownModalEvent);
}
/**
* Executes before a modal is shown to the user.
* @param {Modal} self the `Modal` instance
*/
function beforeModalShow(self) {
const { element, hasFade } = self;
setElementStyle(element, { display: 'block' });
setModalScrollbar(self);
if (!getCurrentOpen(element)) {
setElementStyle(getDocumentBody(element), { overflow: 'hidden' });
}
addClass(element, showClass);
removeAttribute(element, ariaHidden);
setAttribute(element, ariaModal, 'true');
if (hasFade) emulateTransitionEnd(element, () => afterModalShow(self));
else afterModalShow(self);
}
/**
* Executes before a modal is hidden to the user.
* @param {Modal} self the `Modal` instance
* @param {boolean=} force when `true` skip animation
*/
function beforeModalHide(self, force) {
const {
element, options, hasFade,
} = self;
setElementStyle(element, { display: '' });
// force can also be the transitionEvent object, we wanna make sure it's not
// call is not forced and overlay is visible
if (options.backdrop && !force && hasFade && hasClass(overlay, showClass)
&& !getCurrentOpen(element)) { // AND no modal is visible
hideOverlay();
emulateTransitionEnd(overlay, () => afterModalHide(self));
} else {
afterModalHide(self);
}
}
// MODAL EVENT HANDLERS
// ====================
/**
* Handles the `click` event listener for modal.
* @param {MouseEvent} e the `Event` object
* @this {HTMLElement | Element}
*/
function modalClickHandler(e) {
const { target } = e;
const trigger = target && closest(this, modalToggleSelector);
const element = trigger && getTargetElement(trigger);
const self = element && getModalInstance(element);
if (!self) return;
if (trigger && trigger.tagName === 'A') e.preventDefault();
self.relatedTarget = trigger;
self.toggle();
}
/**
* Handles the `keydown` event listener for modal
* to hide the modal when user type the `ESC` key.
*
* @param {KeyboardEvent} e the `Event` object
*/
function modalKeyHandler({ code }) {
const element = querySelector(modalActiveSelector);
const self = element && getModalInstance(element);
if (!self) return;
const { options } = self;
if (options.keyboard && code === keyEscape // the keyboard option is enabled and the key is 27
&& hasClass(element, showClass)) { // the modal is not visible
self.relatedTarget = null;
self.hide();
}
}
/**
* Handles the `click` event listeners that hide the modal.
*
* @this {HTMLElement | Element}
* @param {MouseEvent} e the `Event` object
*/
function modalDismissHandler(e) {
const element = this;
const self = getModalInstance(element);
// this timer is needed
if (!self || Timer.get(element)) return;
const { options, isStatic, modalDialog } = self;
const { backdrop } = options;
const { target } = e;
// @ts-ignore
const selectedText = getDocument(element).getSelection().toString().length;
// @ts-ignore
const targetInsideDialog = modalDialog.contains(target);
// @ts-ignore
const dismiss = target && closest(target, modalDismissSelector);
if (isStatic && !targetInsideDialog) {
Timer.set(element, () => {
addClass(element, modalStaticClass);
emulateTransitionEnd(modalDialog, () => staticTransitionEnd(self));
}, 17);
} else if (dismiss || (!selectedText && !isStatic && !targetInsideDialog && backdrop)) {
self.relatedTarget = dismiss || null;
self.hide();
e.preventDefault();
}
}
/**
* Handles the `transitionend` event listeners for `Modal`.
*
* @param {Modal} self the `Modal` instance
*/
function staticTransitionEnd(self) {
const { element, modalDialog } = self;
const duration = getElementTransitionDuration(modalDialog) + 17;
removeClass(element, modalStaticClass);
// user must wait for zoom out transition
Timer.set(element, () => Timer.clear(element), duration);
}
// MODAL DEFINITION
// ================
/** Returns a new `Modal` instance. */
class Modal extends BaseComponent {
/**
* @param {HTMLElement | Element | string} target usually the `.modal` element
* @param {BSN.Options.Modal=} config instance options
*/
constructor(target, config) {
super(target, config);
// bind
const self = this;
// the modal
const { element } = self;
// the modal-dialog
/** @type {(HTMLElement | Element)} */
// @ts-ignore
self.modalDialog = querySelector(`.${modalString}-dialog`, element);
// modal can have multiple triggering elements
/** @type {(HTMLElement | Element)[]} */
self.triggers = [...querySelectorAll(modalToggleSelector)]
.filter((btn) => getTargetElement(btn) === element);
// additional internals
/** @type {boolean} */
self.isStatic = self.options.backdrop === 'static';
/** @type {boolean} */
self.hasFade = hasClass(element, fadeClass);
/** @type {(HTMLElement | Element)?} */
self.relatedTarget = null;
/** @type {HTMLBodyElement | HTMLElement | Element} */
// @ts-ignore
self.container = getElementContainer(element);
// attach event listeners
toggleModalHandler(self, true);
// bind
self.update = self.update.bind(self);
}
/* eslint-disable */
/**
* Returns component name string.
* @readonly @static
*/
get name() { return modalComponent; }
/**
* Returns component default options.
* @readonly @static
*/
get defaults() { return modalDefaults; }
/* eslint-enable */
// MODAL PUBLIC METHODS
// ====================
/** Toggles the visibility of the modal. */
toggle() {
const self = this;
if (hasClass(self.element, showClass)) self.hide();
else self.show();
}
/** Shows the modal to the user. */
show() {
const self = this;
const {
element, options, hasFade, relatedTarget, container,
} = self;
const { backdrop } = options;
let overlayDelay = 0;
if (hasClass(element, showClass)) return;
showModalEvent.relatedTarget = relatedTarget || null;
dispatchEvent(element, showModalEvent);
if (showModalEvent.defaultPrevented) return;
// we elegantly hide any opened modal/offcanvas
const currentOpen = getCurrentOpen(element);
if (currentOpen && currentOpen !== element) {
const this1 = getModalInstance(currentOpen);
const that1 = this1 || getInstance(currentOpen, 'Offcanvas');
that1.hide();
}
if (backdrop) {
if (!container.contains(overlay)) {
appendOverlay(container, hasFade, true);
} else {
toggleOverlayType(true);
}
overlayDelay = getElementTransitionDuration(overlay);
showOverlay();
setTimeout(() => beforeModalShow(self), overlayDelay);
} else {
beforeModalShow(self);
if (currentOpen && hasClass(overlay, showClass)) {
hideOverlay();
}
}
}
/**
* Hide the modal from the user.
* @param {boolean=} force when `true` it will skip animation
*/
hide(force) {
const self = this;
const {
element, hasFade, relatedTarget,
} = self;
if (!hasClass(element, showClass)) return;
hideModalEvent.relatedTarget = relatedTarget || null;
dispatchEvent(element, hideModalEvent);
if (hideModalEvent.defaultPrevented) return;
removeClass(element, showClass);
setAttribute(element, ariaHidden, 'true');
removeAttribute(element, ariaModal);
if (hasFade && force !== false) {
emulateTransitionEnd(element, () => beforeModalHide(self));
} else {
beforeModalHide(self, force);
}
}
/** Updates the modal layout. */
update() {
const self = this;
if (hasClass(self.element, showClass)) setModalScrollbar(self);
}
/** Removes the `Modal` component from target element. */
dispose() {
const self = this;
self.hide(true); // forced call
toggleModalHandler(self);
super.dispose();
}
}
ObjectAssign(Modal, {
selector: modalSelector,
init: modalInitCallback,
getInstance: getModalInstance,
});
/** @type {string} */
const offcanvasComponent = 'Offcanvas';
/* Native JavaScript for Bootstrap 5 | OffCanvas
------------------------------------------------ */
// OFFCANVAS PRIVATE GC
// ====================
const offcanvasSelector = `.${offcanvasString}`;
const offcanvasToggleSelector = `[${dataBsToggle}="${offcanvasString}"]`;
const offcanvasDismissSelector = `[${dataBsDismiss}="${offcanvasString}"]`;
const offcanvasTogglingClass = `${offcanvasString}-toggling`;
const offcanvasDefaults = {
backdrop: true, // boolean
keyboard: true, // boolean
scroll: false, // boolean
};
/**
* Static method which returns an existing `Offcanvas` instance associated
* to a target `Element`.
*
* @type {BSN.GetInstance<Offcanvas>}
*/
const getOffcanvasInstance = (element) => getInstance(element, offcanvasComponent);
/**
* An `Offcanvas` initialization callback.
* @type {BSN.InitCallback<Offcanvas>}
*/
const offcanvasInitCallback = (element) => new Offcanvas(element);
// OFFCANVAS CUSTOM EVENTS
// =======================
const showOffcanvasEvent = OriginalEvent(`show.bs.${offcanvasString}`);
const shownOffcanvasEvent = OriginalEvent(`shown.bs.${offcanvasString}`);
const hideOffcanvasEvent = OriginalEvent(`hide.bs.${offcanvasString}`);
const hiddenOffcanvasEvent = OriginalEvent(`hidden.bs.${offcanvasString}`);
// OFFCANVAS PRIVATE METHODS
// =========================
/**
* Sets additional style for the `<body>` and other elements
* when showing an offcanvas to the user.
*
* @param {Offcanvas} self the `Offcanvas` instance
*/
function setOffCanvasScrollbar(self) {
const { element } = self;
const { clientHeight, scrollHeight } = getDocumentElement(element);
setScrollbar(element, clientHeight !== scrollHeight);
}
/**
* Toggles on/off the `click` event listeners.
*
* @param {Offcanvas} self the `Offcanvas` instance
* @param {boolean=} add when *true*, listeners are added
*/
function toggleOffcanvasEvents(self, add) {
const action = add ? addListener : removeListener;
self.triggers.forEach((btn) => action(btn, mouseclickEvent, offcanvasTriggerHandler));
}
/**
* Toggles on/off the listeners of the events that close the offcanvas.
*
* @param {Offcanvas} self the `Offcanvas` instance
* @param {boolean=} add when *true* listeners are added
*/
function toggleOffCanvasDismiss(self, add) {
const action = add ? addListener : removeListener;
const doc = getDocument(self.element);
action(doc, keydownEvent, offcanvasKeyDismissHandler);
action(doc, mouseclickEvent, offcanvasDismissHandler);
}
/**
* Executes before showing the offcanvas.
*
* @param {Offcanvas} self the `Offcanvas` instance
*/
function beforeOffcanvasShow(self) {
const { element, options } = self;
if (!options.scroll) {
setOffCanvasScrollbar(self);
setElementStyle(getDocumentBody(element), { overflow: 'hidden' });
}
addClass(element, offcanvasTogglingClass);
addClass(element, showClass);
setElementStyle(element, { visibility: 'visible' });
emulateTransitionEnd(element, () => showOffcanvasComplete(self));
}
/**
* Executes before hiding the offcanvas.
*
* @param {Offcanvas} self the `Offcanvas` instance
*/
function beforeOffcanvasHide(self) {
const { element, options } = self;
const currentOpen = getCurrentOpen(element);
// @ts-ignore
element.blur();
if (!currentOpen && options.backdrop && hasClass(overlay, showClass)) {
hideOverlay();
emulateTransitionEnd(overlay, () => hideOffcanvasComplete(self));
} else hideOffcanvasComplete(self);
}
// OFFCANVAS EVENT HANDLERS
// ========================
/**
* Handles the `click` event listeners.
*
* @this {HTMLElement | Element}
* @param {MouseEvent} e the `Event` object
*/
function offcanvasTriggerHandler(e) {
const trigger = closest(this, offcanvasToggleSelector);
const element = trigger && getTargetElement(trigger);
const self = element && getOffcanvasInstance(element);
if (self) {
self.relatedTarget = trigger;
self.toggle();
if (trigger && trigger.tagName === 'A') {
e.preventDefault();
}
}
}
/**
* Handles the event listeners that close the offcanvas.
*
* @this {Document}
* @param {MouseEvent} e the `Event` object
*/
function offcanvasDismissHandler(e) {
const element = querySelector(offcanvasActiveSelector, this);
if (!element) return;
const offCanvasDismiss = querySelector(offcanvasDismissSelector, element);
const self = getOffcanvasInstance(element);
if (!self) return;
const { options, triggers } = self;
const { target } = e;
// @ts-ignore -- `EventTarget` is `HTMLElement`
const trigger = closest(target, offcanvasToggleSelector);
const selection = getDocument(element).getSelection();
if (!(selection && selection.toString().length)
// @ts-ignore
&& ((!element.contains(target) && options.backdrop
&& (!trigger || (trigger && !triggers.includes(trigger))))
// @ts-ignore
|| (offCanvasDismiss && offCanvasDismiss.contains(target)))) {
// @ts-ignore
self.relatedTarget = offCanvasDismiss && offCanvasDismiss.contains(target)
? offCanvasDismiss : null;
self.hide();
}
if (trigger && trigger.tagName === 'A') e.preventDefault();
}
/**
* Handles the `keydown` event listener for offcanvas
* to hide it when user type the `ESC` key.
*
* @param {KeyboardEvent} e the `Event` object
* @this {Document}
*/
function offcanvasKeyDismissHandler({ code }) {
const element = querySelector(offcanvasActiveSelector, this);
if (!element) return;
const self = getOffcanvasInstance(element);
if (self && self.options.keyboard && code === keyEscape) {
self.relatedTarget = null;
self.hide();
}
}
/**
* Handles the `transitionend` when showing the offcanvas.
*
* @param {Offcanvas} self the `Offcanvas` instance
*/
function showOffcanvasComplete(self) {
const { element } = self;
removeClass(element, offcanvasTogglingClass);
removeAttribute(element, ariaHidden);
setAttribute(element, ariaModal, 'true');
setAttribute(element, 'role', 'dialog');
dispatchEvent(element, shownOffcanvasEvent);
toggleOffCanvasDismiss(self, true);
focus(element);
}
/**
* Handles the `transitionend` when hiding the offcanvas.
*
* @param {Offcanvas} self the `Offcanvas` instance
*/
function hideOffcanvasComplete(self) {
const { element, triggers } = self;
setAttribute(element, ariaHidden, 'true');
removeAttribute(element, ariaModal);
removeAttribute(element, 'role');
setElementStyle(element, { visibility: '' });
const visibleTrigger = showOffcanvasEvent.relatedTarget || triggers.find((x) => isVisible(x));
if (visibleTrigger) focus(visibleTrigger);
removeOverlay(element);
dispatchEvent(element, hiddenOffcanvasEvent);
removeClass(element, offcanvasTogglingClass);
// must check for open instances
if (!getCurrentOpen(element)) {
toggleOffCanvasDismiss(self);
}
}
// OFFCANVAS DEFINITION
// ====================
/** Returns a new `Offcanvas` instance. */
class Offcanvas extends BaseComponent {
/**
* @param {HTMLElement | Element | string} target usually an `.offcanvas` element
* @param {BSN.Options.Offcanvas=} config instance options
*/
constructor(target, config) {
super(target, config);
const self = this;
// instance element
const { element } = self;
// all the triggering buttons
/** @type {(HTMLElement | Element)[]} */
self.triggers = [...querySelectorAll(offcanvasToggleSelector)]
.filter((btn) => getTargetElement(btn) === element);
// additional instance property
/** @type {HTMLBodyElement | HTMLElement | Element} */
// @ts-ignore
self.container = getElementContainer(element);
/** @type {(HTMLElement | Element)?} */
self.relatedTarget = null;
// attach event listeners
toggleOffcanvasEvents(self, true);
}
/* eslint-disable */
/**
* Returns component name string.
* @readonly @static
*/
get name() { return offcanvasComponent; }
/**
* Returns component default options.
* @readonly @static
*/
get defaults() { return offcanvasDefaults; }
/* eslint-enable */
// OFFCANVAS PUBLIC METHODS
// ========================
/** Shows or hides the offcanvas from the user. */
toggle() {
const self = this;
if (hasClass(self.element, showClass)) self.hide();
else self.show();
}
/** Shows the offcanvas to the user. */
show() {
const self = this;
const {
element, options, container, relatedTarget,
} = self;
let overlayDelay = 0;
if (hasClass(element, showClass)) return;
showOffcanvasEvent.relatedTarget = relatedTarget;
shownOffcanvasEvent.relatedTarget = relatedTarget;
dispatchEvent(element, showOffcanvasEvent);
if (showOffcanvasEvent.defaultPrevented) return;
// we elegantly hide any opened modal/offcanvas
const currentOpen = getCurrentOpen(element);
if (currentOpen && currentOpen !== element) {
const this1 = getOffcanvasInstance(currentOpen);
const that1 = this1 || getInstance(currentOpen, 'Modal');
that1.hide();
}
if (options.backdrop) {
if (!container.contains(overlay)) {
appendOverlay(container, true);
} else {
toggleOverlayType();
}
overlayDelay = getElementTransitionDuration(overlay);
showOverlay();
setTimeout(() => beforeOffcanvasShow(self), overlayDelay);
} else {
beforeOffcanvasShow(self);
if (currentOpen && hasClass(overlay, showClass)) {
hideOverlay();
}
}
}
/**
* Hides the offcanvas from the user.
* @param {boolean=} force when `true` it will skip animation
*/
hide(force) {
const self = this;
const { element, relatedTarget } = self;
if (!hasClass(element, showClass)) return;
hideOffcanvasEvent.relatedTarget = relatedTarget;
hiddenOffcanvasEvent.relatedTarget = relatedTarget;
dispatchEvent(element, hideOffcanvasEvent);
if (hideOffcanvasEvent.defaultPrevented) return;
addClass(element, offcanvasTogglingClass);
removeClass(element, showClass);
if (!force) {
emulateTransitionEnd(element, () => beforeOffcanvasHide(self));
} else beforeOffcanvasHide(self);
}
/** Removes the `Offcanvas` from the target element. */
dispose() {
const self = this;
self.hide(true);
toggleOffcanvasEvents(self);
super.dispose();
}
}
ObjectAssign(Offcanvas, {
selector: offcanvasSelector,
init: offcanvasInitCallback,
getInstance: getOffcanvasInstance,
});
/** @type {string} */
const popoverString = 'popover';
/** @type {string} */
const popoverComponent = 'Popover';
/** @type {string} */
const tooltipString = 'tooltip';
/**
* Returns a template for Popover / Tooltip.
*
* @param {string} tipType the expected markup type
* @returns {string} the template markup
*/
function getTipTemplate(tipType) {
const isTooltip = tipType === tooltipString;
const bodyClass = isTooltip ? `${tipType}-inner` : `${tipType}-body`;
const header = !isTooltip ? `<h3 class="${tipType}-header"></h3>` : '';
const arrow = `<div class="${tipType}-arrow"></div>`;
const body = `<div class="${bodyClass}"></div>`;
return `<div class="${tipType}" role="${tooltipString}">${header + arrow + body}</div>`;
}
/**
* Checks if an element is an `<svg>` (or any type of SVG element),
* `<img>` or `<video>`.
*
* *Tooltip* / *Popover* works different with media elements.
* @param {any} element the target element
* @returns {boolean} the query result
*/
const isMedia = (element) => element
&& [SVGElement, HTMLImageElement, HTMLVideoElement]
.some((mediaType) => element instanceof mediaType);
/**
* Returns an `{x,y}` object with the target
* `HTMLElement` / `Node` scroll position.
*
* @see https://github.com/floating-ui/floating-ui
*
* @param {HTMLElement | Element | Window} element target node / element
* @returns {{x: number, y: number}} the scroll tuple
*/
function getNodeScroll(element) {
const isWin = 'scrollX' in element;
const x = isWin ? element.scrollX : element.scrollLeft;
const y = isWin ? element.scrollY : element.scrollTop;
return { x, y };
}
/**
* Checks if a target `HTMLElement` is affected by scale.
* @see https://github.com/floating-ui/floating-ui
*
* @param {HTMLElement} element target
* @returns {boolean} the query result
*/
function isScaledElement(element) {
const { width, height } = getBoundingClientRect(element);
const { offsetWidth, offsetHeight } = element;
return Math.round(width) !== offsetWidth
|| Math.round(height) !== offsetHeight;
}
/**
* Returns the rect relative to an offset parent.
* @see https://github.com/floating-ui/floating-ui
*
* @param {HTMLElement | Element} element target
* @param {HTMLElement | Element | Window} offsetParent the container / offset parent
* @param {{x: number, y: number}} scroll
* @returns {SHORTER.OffsetRect}
*/
function getRectRelativeToOffsetParent(element, offsetParent, scroll) {
const isParentAnElement = offsetParent instanceof HTMLElement;
const rect = getBoundingClientRect(element, isParentAnElement && isScaledElement(offsetParent));
const offsets = { x: 0, y: 0 };
if (isParentAnElement) {
const offsetRect = getBoundingClientRect(offsetParent, true);
offsets.x = offsetRect.x + offsetParent.clientLeft;
offsets.y = offsetRect.y + offsetParent.clientTop;
}
return {
x: rect.left + scroll.x - offsets.x,
y: rect.top + scroll.y - offsets.y,
width: rect.width,
height: rect.height,
};
}
/** @type {Record<string, string>} */
var tipClassPositions = {
top: 'top',
bottom: 'bottom',
left: 'start',
right: 'end',
};
/**
* Style popovers and tooltips.
* @param {BSN.Tooltip | BSN.Popover} self the `Popover` / `Tooltip` instance
* @param {PointerEvent=} e event object
*/
function styleTip(self, e) {
const tipClasses = /\b(top|bottom|start|end)+/;
const {
element, tooltip, options, arrow, offsetParent,
} = self;
const tipPositions = { ...tipClassPositions };
// reset tooltip style (top: 0, left: 0 works best)
setElementStyle(tooltip, { top: '0px', left: '0px', right: '' });
// @ts-ignore
const isPopover = self.name === popoverComponent;
const tipWidth = tooltip.offsetWidth;
const tipHeight = tooltip.offsetHeight;
const RTL = isRTL(element);
if (RTL) {
tipPositions.left = 'end';
tipPositions.right = 'start';
}
const documentElement = getDocumentElement(element);
const windowWidth = documentElement.clientWidth;
const windowHeight = documentElement.clientHeight;
const { container } = options;
let { placement } = options;
const {
left: parentLeft, right: parentRight, top: parentTop,
} = getBoundingClientRect(container, true);
const parentWidth = container.clientWidth;
const scrollbarWidth = Math.abs(parentWidth - container.offsetWidth);
const parentPosition = getElementStyle(container, 'position');
// const absoluteParent = parentPosition === 'absolute';
const fixedParent = parentPosition === 'fixed';
const staticParent = parentPosition === 'static';
const stickyParent = parentPosition === 'sticky';
const isSticky = stickyParent && parentTop === parseFloat(getElementStyle(container, 'top'));
// const absoluteTarget = getElementStyle(element, 'position') === 'absolute';
// const stickyFixedParent = ['sticky', 'fixed'].includes(parentPosition);
const leftBoundry = RTL && fixedParent ? scrollbarWidth : 0;
const rightBoundry = fixedParent ? parentWidth + parentLeft + (RTL ? scrollbarWidth : 0)
: parentWidth + parentLeft + (windowWidth - parentRight) - 1;
const {
width: elemWidth,
height: elemHeight,
left: elemRectLeft,
right: elemRectRight,
top: elemRectTop,
} = getBoundingClientRect(element, true);
const scroll = getNodeScroll(offsetParent);
const { x, y } = getRectRelativeToOffsetParent(element, offsetParent, scroll);
// reset arrow style
setElementStyle(arrow, { top: '', left: '', right: '' });
let topPosition;
let leftPosition;
let rightPosition;
let arrowTop;
let arrowLeft;
let arrowRight;
const arrowWidth = arrow.offsetWidth || 0;
const arrowHeight = arrow.offsetHeight || 0;
const arrowAdjust = arrowWidth / 2;
// check placement
let topExceed = elemRectTop - tipHeight - arrowHeight < 0;
let bottomExceed = elemRectTop + tipHeight + elemHeight
+ arrowHeight >= windowHeight;
let leftExceed = elemRectLeft - tipWidth - arrowWidth < leftBoundry;
let rightExceed = elemRectLeft + tipWidth + elemWidth
+ arrowWidth >= rightBoundry;
const horizontal = ['left', 'right'];
const vertical = ['top', 'bottom'];
topExceed = horizontal.includes(placement)
? elemRectTop + elemHeight / 2 - tipHeight / 2 - arrowHeight < 0
: topExceed;
bottomExceed = horizontal.includes(placement)
? elemRectTop + tipHeight / 2 + elemHeight / 2 + arrowHeight >= windowHeight
: bottomExceed;
leftExceed = vertical.includes(placement)
? elemRectLeft + elemWidth / 2 - tipWidth / 2 < leftBoundry
: leftExceed;
rightExceed = vertical.includes(placement)
? elemRectLeft + tipWidth / 2 + elemWidth / 2 >= rightBoundry
: rightExceed;
// recompute placement
// first, when both left and right limits are exceeded, we fall back to top|bottom
placement = (horizontal.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 (!tooltip.className.includes(placement)) {
tooltip.className = tooltip.className.replace(tipClasses, tipPositions[placement]);
}
// compute tooltip / popover coordinates
if (horizontal.includes(placement)) { // secondary|side positions
if (placement === 'left') { // LEFT
leftPosition = x - tipWidth - (isPopover ? arrowWidth : 0);
} else { // RIGHT
leftPosition = x + elemWidth + (isPopover ? arrowWidth : 0);
}
// adjust top and arrow
if (topExceed) {
topPosition = y;
topPosition += (isSticky ? -parentTop - scroll.y : 0);
arrowTop = elemHeight / 2 - arrowWidth;
} else if (bottomExceed) {
topPosition = y - tipHeight + elemHeight;
topPosition += (isSticky ? -parentTop - scroll.y : 0);
arrowTop = tipHeight - elemHeight / 2 - arrowWidth;
} else {
topPosition = y - tipHeight / 2 + elemHeight / 2;
topPosition += (isSticky ? -parentTop - scroll.y : 0);
arrowTop = tipHeight / 2 - arrowHeight / 2;
}
} else if (vertical.includes(placement)) {
if (e && isMedia(element)) {
let eX = 0;
let eY = 0;
if (staticParent) {
eX = e.pageX;
eY = e.pageY;
} else { // fixedParent | stickyParent
eX = e.clientX - parentLeft + (fixedParent ? scroll.x : 0);
eY = e.clientY - parentTop + (fixedParent ? scroll.y : 0);
}
// some weird RTL bug
eX -= RTL && fixedParent && scrollbarWidth ? scrollbarWidth : 0;
if (placement === 'top') {
topPosition = eY - tipHeight - arrowWidth;
} else {
topPosition = eY + arrowWidth;
}
// adjust (left | right) and also the arrow
if (e.clientX - tipWidth / 2 < leftBoundry) {
leftPosition = 0;
arrowLeft = eX - arrowAdjust;
} else if (e.clientX + tipWidth / 2 > rightBoundry) {
leftPosition = 'auto';
rightPosition = 0;
arrowRight = rightBoundry - eX - arrowAdjust;
arrowRight -= fixedParent ? parentLeft + (RTL ? scrollbarWidth : 0) : 0;
// normal top/bottom
} else {
leftPosition = eX - tipWidth / 2;
arrowLeft = tipWidth / 2 - arrowAdjust;
}
} else {
if (placement === 'top') {
topPosition = y - tipHeight - (isPopover ? arrowHeight : 0);
} else { // BOTTOM
topPosition = y + elemHeight + (isPopover ? arrowHeight : 0);
}
// adjust left | right and also the arrow
if (leftExceed) {
leftPosition = 0;
arrowLeft = x + elemWidth / 2 - arrowAdjust;
} else if (rightExceed) {
leftPosition = 'auto';
rightPosition = 0;
arrowRight = elemWidth / 2 + rightBoundry - elemRectRight - arrowAdjust;
} else {
leftPosition = x - tipWidth / 2 + elemWidth / 2;
arrowLeft = tipWidth / 2 - arrowAdjust;
}
}
}
// apply style to tooltip/popover
setElementStyle(tooltip, {
top: `${topPosition}px`,
left: leftPosition === 'auto' ? leftPosition : `${leftPosition}px`,
right: rightPosition !== undefined ? `${rightPosition}px` : '',
});
// update arrow placement
if (arrow instanceof HTMLElement) {
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`;
}
}
}
const tooltipDefaults = {
/** @type {string} */
template: getTipTemplate(tooltipString),
/** @type {string?} */
title: null, // string
/** @type {string?} */
customClass: null, // string | null
/** @type {string} */
trigger: 'hover focus',
/** @type {string?} */
placement: 'top', // string
/** @type {((c:string)=>string)?} */
sanitizeFn: null, // function
/** @type {boolean} */
animation: true, // bool
/** @type {number} */
delay: 200, // number
/** @type {(HTMLElement | Element)?} */
container: null,
};
/**
* A global namespace for aria-describedby.
* @type {string}
*/
const ariaDescribedBy = 'aria-describedby';
/**
* A global namespace for `mousedown` event.
* @type {string}
*/
const mousedownEvent = 'mousedown';
/**
* A global namespace for `mousemove` event.
* @type {string}
*/
const mousemoveEvent = 'mousemove';
/**
* A global namespace for `focusin` event.
* @type {string}
*/
const focusinEvent = 'focusin';
/**
* A global namespace for `focusout` event.
* @type {string}
*/
const focusoutEvent = 'focusout';
/**
* A global namespace for `hover` event.
* @type {string}
*/
const mousehoverEvent = 'hover';
let elementUID = 0;
let elementMapUID = 0;
const elementIDMap = new Map();
/**
* Returns a unique identifier for popover, tooltip, scrollspy.
*
* @param {HTMLElement | Element} element target element
* @param {string=} key predefined key
* @returns {number} an existing or new unique ID
*/
function getUID(element, key) {
let result = key ? elementUID : elementMapUID;
if (key) {
const elID = getUID(element);
const elMap = elementIDMap.get(elID) || new Map();
if (!elementIDMap.has(elID)) {
elementIDMap.set(elID, elMap);
}
if (!elMap.has(key)) {
elMap.set(key, result);
elementUID += 1;
} else result = elMap.get(key);
} else {
const elkey = element.id || element;
if (!elementIDMap.has(elkey)) {
elementIDMap.set(elkey, result);
elementMapUID += 1;
} else result = elementIDMap.get(elkey);
}
return result;
}
// @ts-ignore
const { userAgentData: uaDATA } = navigator;
/**
* A global namespace for `userAgentData` object.
*/
const userAgentData = uaDATA;
const { userAgent: userAgentString } = navigator;
/**
* A global namespace for `navigator.userAgent` string.
*/
const userAgent = userAgentString;
const appleBrands = /(iPhone|iPod|iPad)/;
/**
* A global `boolean` for Apple browsers.
* @type {boolean}
*/
const isApple = !userAgentData ? appleBrands.test(userAgent)
: userAgentData.brands.some((/** @type {Record<string, any>} */x) => appleBrands.test(x.brand));
/**
* Global namespace for `data-bs-title` attribute.
*/
const dataOriginalTitle = 'data-original-title';
/** @type {string} */
const tooltipComponent = 'Tooltip';
/**
* Append an existing `Element` to Popover / Tooltip component or HTML
* markup string to be parsed & sanitized to be used as popover / tooltip content.
*
* @param {HTMLElement | Element} element target
* @param {HTMLElement | Element | string} content the `Element` to append / string
* @param {ReturnType<any>} sanitizeFn a function to sanitize string content
*/
function setHtml(element, content, sanitizeFn) {
if (typeof content === 'string' && !content.length) return;
if (typeof content === 'string') {
let dirty = content.trim(); // fixing #233
if (typeof sanitizeFn === 'function') dirty = sanitizeFn(dirty);
const domParser = new DOMParser();
const tempDocument = domParser.parseFromString(dirty, 'text/html');
const { body } = tempDocument;
const method = body.children.length ? 'innerHTML' : 'innerText';
// @ts-ignore
element[method] = body[method];
} else if (content instanceof HTMLElement) {
element.append(content);
}
}
/**
* Creates a new tooltip / popover.
*
* @param {BSN.Popover | BSN.Tooltip} self the `Popover` instance
*/
function createTip(self) {
const { id, element, options } = self;
const {
animation, customClass, sanitizeFn, placement, dismissible,
} = options;
let { title, content } = options;
const isTooltip = self.name === tooltipComponent;
const tipString = isTooltip ? tooltipString : popoverString;
const { template, btnClose } = options;
const tipPositions = { ...tipClassPositions };
if (isRTL(element)) {
tipPositions.left = 'end';
tipPositions.right = 'start';
}
// set initial popover class
const placementClass = `bs-${tipString}-${tipPositions[placement]}`;
// load template
/** @type {(HTMLElement | Element)?} */
let popoverTemplate;
if ([Element, HTMLElement].some((x) => template instanceof x)) {
popoverTemplate = template;
} else {
const htmlMarkup = getDocument(element).createElement('div');
setHtml(htmlMarkup, template, sanitizeFn);
popoverTemplate = htmlMarkup.firstElementChild;
}
// set popover markup
self.tooltip = popoverTemplate && popoverTemplate.cloneNode(true);
const { tooltip } = self;
// set id and role attributes
setAttribute(tooltip, 'id', id);
setAttribute(tooltip, 'role', tooltipString);
const bodyClass = isTooltip ? `${tooltipString}-inner` : `${popoverString}-body`;
const tooltipHeader = isTooltip ? null : querySelector(`.${popoverString}-header`, tooltip);
const tooltipBody = querySelector(`.${bodyClass}`, tooltip);
// set arrow and enable access for styleTip
self.arrow = querySelector(`.${tipString}-arrow`, tooltip);
// set dismissible button
if (dismissible) {
if (title) {
if (title instanceof HTMLElement) setHtml(title, btnClose, sanitizeFn);
else title += btnClose;
} else {
if (tooltipHeader) tooltipHeader.remove();
if (content instanceof HTMLElement) setHtml(content, btnClose, sanitizeFn);
else content += btnClose;
}
}
// fill the template with content from options / data attributes
// also sanitize title && content
if (!isTooltip) {
if (title && tooltipHeader) setHtml(tooltipHeader, title, sanitizeFn);
if (content && tooltipBody) setHtml(tooltipBody, content, sanitizeFn);
// @ts-ignore -- set btn
self.btn = querySelector('.btn-close', tooltip);
} else if (title && tooltipBody) setHtml(tooltipBody, title, sanitizeFn);
// set popover animation and placement
if (!hasClass(tooltip, tipString)) addClass(tooltip, tipString);
if (animation && !hasClass(tooltip, fadeClass)) addClass(tooltip, fadeClass);
if (customClass && !hasClass(tooltip, customClass)) {
addClass(tooltip, customClass);
}
if (!hasClass(tooltip, placementClass)) addClass(tooltip, placementClass);
}
/**
* @param {(HTMLElement | Element)?} tip target
* @param {HTMLElement | ParentNode} container parent container
* @returns {boolean}
*/
function isVisibleTip(tip, container) {
return tip instanceof HTMLElement && container.contains(tip);
}
/* Native JavaScript for Bootstrap 5 | Tooltip
---------------------------------------------- */
// TOOLTIP PRIVATE GC
// ==================
const tooltipSelector = `[${dataBsToggle}="${tooltipString}"],[data-tip="${tooltipString}"]`;
const titleAttr = 'title';
/**
* Static method which returns an existing `Tooltip` instance associated
* to a target `Element`.
*
* @type {BSN.GetInstance<Tooltip>}
*/
let getTooltipInstance = (element) => getInstance(element, tooltipComponent);
/**
* A `Tooltip` initialization callback.
* @type {BSN.InitCallback<Tooltip>}
*/
const tooltipInitCallback = (element) => new Tooltip(element);
// TOOLTIP PRIVATE METHODS
// =======================
/**
* Removes the tooltip from the DOM.
*
* @param {Tooltip} self the `Tooltip` instance
*/
function removeTooltip(self) {
const { element, tooltip } = self;
removeAttribute(element, ariaDescribedBy);
tooltip.remove();
}
/**
* Executes after the instance has been disposed.
*
* @param {Tooltip} self the `Tooltip` instance
*/
function disposeTooltipComplete(self) {
const { element } = self;
toggleTooltipHandlers(self);
if (element.hasAttribute(dataOriginalTitle) && self.name === tooltipString) {
toggleTooltipTitle(self);
}
}
/**
* Toggles on/off the special `Tooltip` event listeners.
*
* @param {Tooltip} self the `Tooltip` instance
* @param {boolean=} add when `true`, event listeners are added
*/
function toggleTooltipAction(self, add) {
const action = add ? addListener : removeListener;
const { element } = self;
action(getDocument(element), touchstartEvent, self.handleTouch, passiveHandler);
if (!isMedia(element)) {
[scrollEvent, resizeEvent].forEach((ev) => {
// @ts-ignore
action(getWindow(element), ev, self.update, passiveHandler);
});
}
}
/**
* Executes after the tooltip was shown to the user.
*
* @param {Tooltip} self the `Tooltip` instance
*/
function tooltipShownAction(self) {
const { element } = self;
const shownTooltipEvent = OriginalEvent(`shown.bs.${toLowerCase(self.name)}`);
toggleTooltipAction(self, true);
dispatchEvent(element, shownTooltipEvent);
Timer.clear(element, 'in');
}
/**
* Executes after the tooltip was hidden to the user.
*
* @param {Tooltip} self the `Tooltip` instance
*/
function tooltipHiddenAction(self) {
const { element } = self;
const hiddenTooltipEvent = OriginalEvent(`hidden.bs.${toLowerCase(self.name)}`);
toggleTooltipAction(self);
removeTooltip(self);
dispatchEvent(element, hiddenTooltipEvent);
Timer.clear(element, 'out');
}
/**
* Toggles on/off the `Tooltip` event listeners.
*
* @param {Tooltip} self the `Tooltip` instance
* @param {boolean=} add when `true`, event listeners are added
*/
function toggleTooltipHandlers(self, add) {
const action = add ? addListener : removeListener;
// @ts-ignore -- btn is only for dismissible popover
const { element, options, btn } = self;
const { trigger, dismissible } = options;
if (trigger.includes('manual')) return;
self.enabled = !!add;
/** @type {string[]} */
const triggerOptions = trigger.split(' ');
const elemIsMedia = isMedia(element);
if (elemIsMedia) {
action(element, mousemoveEvent, self.update, passiveHandler);
}
triggerOptions.forEach((tr) => {
if (elemIsMedia || tr === mousehoverEvent) {
action(element, mousedownEvent, self.show);
action(element, mouseenterEvent, self.show);
if (dismissible && btn) {
action(btn, mouseclickEvent, self.hide);
} else {
action(element, mouseleaveEvent, self.hide);
action(getDocument(element), touchstartEvent, self.handleTouch, passiveHandler);
}
} else if (tr === mouseclickEvent) {
action(element, tr, (!dismissible ? self.toggle : self.show));
} else if (tr === focusEvent) {
action(element, focusinEvent, self.show);
if (!dismissible) action(element, focusoutEvent, self.hide);
if (isApple) action(element, mouseclickEvent, () => focus(element));
}
});
}
/**
* Toggles on/off the `Tooltip` event listeners that hide/update the tooltip.
*
* @param {Tooltip} self the `Tooltip` instance
* @param {boolean=} add when `true`, event listeners are added
*/
function toggleTooltipOpenHandlers(self, add) {
const action = add ? addListener : removeListener;
const { element, options, offsetParent } = self;
const { container } = options;
const { offsetHeight, scrollHeight } = container;
const parentModal = closest(element, `.${modalString}`);
const parentOffcanvas = closest(element, `.${offcanvasString}`);
if (!isMedia(element)) {
const win = getWindow(element);
const overflow = offsetHeight !== scrollHeight;
const scrollTarget = overflow || offsetParent !== win ? container : win;
// @ts-ignore
action(win, resizeEvent, self.update, passiveHandler);
action(scrollTarget, scrollEvent, self.update, passiveHandler);
}
// dismiss tooltips inside modal / offcanvas
if (parentModal) action(parentModal, `hide.bs.${modalString}`, self.hide);
if (parentOffcanvas) action(parentOffcanvas, `hide.bs.${offcanvasString}`, self.hide);
}
/**
* Toggles the `title` and `data-original-title` attributes.
*
* @param {Tooltip} self the `Tooltip` instance
* @param {string=} content when `true`, event listeners are added
*/
function toggleTooltipTitle(self, content) {
// [0 - add, 1 - remove] | [0 - remove, 1 - add]
const titleAtt = [dataOriginalTitle, titleAttr];
const { element } = self;
setAttribute(element, titleAtt[content ? 0 : 1],
// @ts-ignore
(content || getAttribute(element, titleAtt[0])));
removeAttribute(element, titleAtt[content ? 1 : 0]);
}
// TOOLTIP DEFINITION
// ==================
/** Creates a new `Tooltip` instance. */
class Tooltip extends BaseComponent {
/**
* @param {HTMLElement | Element | string} target the target element
* @param {BSN.Options.Tooltip=} config the instance options
*/
constructor(target, config) {
super(target, config);
// bind
const self = this;
const { element } = self;
const isTooltip = self.name === tooltipComponent;
const tipString = isTooltip ? tooltipString : popoverString;
const tipComponent = isTooltip ? tooltipComponent : popoverComponent;
getTooltipInstance = (elem) => getInstance(elem, tipComponent);
// additional properties
/** @type {any} */
self.tooltip = {};
if (!isTooltip) {
/** @type {any?} */
// @ts-ignore
self.btn = null;
}
/** @type {any} */
self.arrow = {};
/** @type {any} */
self.offsetParent = {};
/** @type {boolean} */
self.enabled = true;
/** @type {string} Set unique ID for `aria-describedby`. */
self.id = `${tipString}-${getUID(element, tipString)}`;
// instance options
const { options } = self;
// invalidate
if ((!options.title && isTooltip) || (!isTooltip && !options.content)) return;
const container = querySelector(options.container);
const idealContainer = getElementContainer(element);
// bypass container option when its position is static/relative
self.options.container = !container || (container
&& ['static', 'relative'].includes(getElementStyle(container, 'position')))
? idealContainer
: container || getDocumentBody(element);
// reset default options
tooltipDefaults[titleAttr] = null;
// all functions bind
self.handleTouch = self.handleTouch.bind(self);
self.update = self.update.bind(self);
self.show = self.show.bind(self);
self.hide = self.hide.bind(self);
self.toggle = self.toggle.bind(self);
// set title attributes and add event listeners
if (element.hasAttribute(titleAttr) && isTooltip) {
toggleTooltipTitle(self, options.title);
}
// create tooltip here
createTip(self);
// attach events
toggleTooltipHandlers(self, true);
}
/* eslint-disable */
/**
* Returns component name string.
* @readonly @static
*/
get name() { return tooltipComponent; }
/**
* Returns component default options.
* @readonly @static
*/
get defaults() { return tooltipDefaults; }
/* eslint-enable */
// TOOLTIP PUBLIC METHODS
// ======================
/**
* Shows the tooltip.
*
* @param {Event=} e the `Event` object
* @this {Tooltip}
*/
show(e) {
const self = this;
const {
options, tooltip, element, id,
} = self;
const { container, animation } = options;
const outTimer = Timer.get(element, 'out');
Timer.clear(element, 'out');
if (tooltip && !outTimer && !isVisibleTip(tooltip, container)) {
Timer.set(element, () => {
const showTooltipEvent = OriginalEvent(`show.bs.${toLowerCase(self.name)}`);
dispatchEvent(element, showTooltipEvent);
if (showTooltipEvent.defaultPrevented) return;
// append to container
container.append(tooltip);
setAttribute(element, ariaDescribedBy, `#${id}`);
// set offsetParent
self.offsetParent = getElementContainer(tooltip, true);
self.update(e);
toggleTooltipOpenHandlers(self, true);
if (!hasClass(tooltip, showClass)) addClass(tooltip, showClass);
if (animation) emulateTransitionEnd(tooltip, () => tooltipShownAction(self));
else tooltipShownAction(self);
}, 17, 'in');
}
}
/**
* Hides the tooltip.
*
* @this {Tooltip}
*/
hide() {
const self = this;
const { options, tooltip, element } = self;
const { container, animation, delay } = options;
Timer.clear(element, 'in');
if (tooltip && isVisibleTip(tooltip, container)) {
Timer.set(element, () => {
const hideTooltipEvent = OriginalEvent(`hide.bs.${toLowerCase(self.name)}`);
dispatchEvent(element, hideTooltipEvent);
if (hideTooltipEvent.defaultPrevented) return;
// @ts-ignore
removeClass(tooltip, showClass);
toggleTooltipOpenHandlers(self);
if (animation) emulateTransitionEnd(tooltip, () => tooltipHiddenAction(self));
else tooltipHiddenAction(self);
}, delay + 17, 'out');
}
}
/**
* Updates the tooltip position.
*
* @param {Event=} e the `Event` object
* @this {Tooltip} the `Tooltip` instance
*/
update(e) {
// @ts-ignore
styleTip(this, e);
}
/**
* Toggles the tooltip visibility.
*
* @param {Event=} e the `Event` object
* @this {Tooltip} the instance
*/
toggle(e) {
const self = this;
const { tooltip, options } = self;
if (!isVisibleTip(tooltip, options.container)) self.show(e);
else self.hide();
}
/** Enables the tooltip. */
enable() {
const self = this;
const { enabled } = self;
if (!enabled) {
toggleTooltipHandlers(self, true);
self.enabled = !enabled;
}
}
/** Disables the tooltip. */
disable() {
const self = this;
const {
element, tooltip, options, enabled,
} = self;
const { animation, container, delay } = options;
if (enabled) {
if (isVisibleTip(tooltip, container) && animation) {
self.hide();
Timer.set(element, () => {
toggleTooltipHandlers(self);
Timer.clear(element, tooltipString);
}, getElementTransitionDuration(tooltip) + delay + 17, tooltipString);
} else {
toggleTooltipHandlers(self);
}
self.enabled = !enabled;
}
}
/** Toggles the `disabled` property. */
toggleEnabled() {
const self = this;
if (!self.enabled) self.enable();
else self.disable();
}
/**
* Handles the `touchstart` event listener for `Tooltip`
* @this {Tooltip}
* @param {TouchEvent} e the `Event` object
*/
handleTouch({ target }) {
const { tooltip, element } = this;
if (tooltip.contains(target) || target === element
// @ts-ignore
|| (target && element.contains(target))) ; else {
this.hide();
}
}
/** Removes the `Tooltip` from the target element. */
dispose() {
const self = this;
const { tooltip, options } = self;
if (options.animation && isVisibleTip(tooltip, options.container)) {
options.delay = 0; // reset delay
self.hide();
emulateTransitionEnd(tooltip, () => disposeTooltipComplete(self));
} else {
disposeTooltipComplete(self);
}
super.dispose();
}
}
ObjectAssign(Tooltip, {
selector: tooltipSelector,
init: tooltipInitCallback,
getInstance: getTooltipInstance,
styleTip,
});
/* Native JavaScript for Bootstrap 5 | Popover
---------------------------------------------- */
// POPOVER PRIVATE GC
// ==================
const popoverSelector = `[${dataBsToggle}="${popoverString}"],[data-tip="${popoverString}"]`;
const popoverDefaults = {
...tooltipDefaults,
/** @type {string} */
template: getTipTemplate(popoverString),
/** @type {string} */
btnClose: '<button class="btn-close" aria-label="Close"></button>',
/** @type {boolean} */
dismissible: false,
/** @type {string?} */
content: null,
};
// POPOVER DEFINITION
// ==================
/** Returns a new `Popover` instance. */
class Popover extends Tooltip {
/* eslint-disable -- we want to specify Popover Options */
/**
* @param {HTMLElement | Element | string} target the target element
* @param {BSN.Options.Popover=} config the instance options
*/
constructor(target, config) {
super(target, config);
}
/**
* Returns component name string.
* @readonly @static
*/
get name() { return popoverComponent; }
/**
* Returns component default options.
* @readonly @static
*/
get defaults() { return popoverDefaults; }
/* eslint-enable */
/* extend original `show()` */
show() {
super.show();
// @ts-ignore -- btn only exists within dismissible popover
const { options, btn } = this;
if (options.dismissible && btn) setTimeout(() => focus(btn), 17);
}
}
/**
* Static method which returns an existing `Popover` instance associated
* to a target `Element`.
*
* @type {BSN.GetInstance<Popover>}
*/
const getPopoverInstance = (element) => getInstance(element, popoverComponent);
/**
* A `Popover` initialization callback.
* @type {BSN.InitCallback<Popover>}
*/
const popoverInitCallback = (element) => new Popover(element);
ObjectAssign(Popover, {
selector: popoverSelector,
init: popoverInitCallback,
getInstance: getPopoverInstance,
styleTip,
});
/**
* Shortcut for `HTMLElement.getElementsByTagName` method. Some `Node` elements
* like `ShadowRoot` do not support `getElementsByTagName`.
*
* @param {string} selector the tag name
* @param {(HTMLElement | Element | Document)=} parent optional Element to look into
* @return {HTMLCollectionOf<HTMLElement | Element>} the 'HTMLCollection'
*/
function getElementsByTagName(selector, parent) {
const lookUp = parent && parentNodes
.some((x) => parent instanceof x) ? parent : getDocument();
return lookUp.getElementsByTagName(selector);
}
/** @type {string} */
const scrollspyString = 'scrollspy';
/** @type {string} */
const scrollspyComponent = 'ScrollSpy';
/* Native JavaScript for Bootstrap 5 | ScrollSpy
------------------------------------------------ */
// console.log(typeof addEventListener)
// SCROLLSPY PRIVATE GC
// ====================
const scrollspySelector = '[data-bs-spy="scroll"]';
const scrollspyDefaults = {
offset: 10,
target: null,
};
/**
* Static method which returns an existing `ScrollSpy` instance associated
* to a target `Element`.
*
* @type {BSN.GetInstance<ScrollSpy>}
*/
const getScrollSpyInstance = (element) => getInstance(element, scrollspyComponent);
/**
* A `ScrollSpy` initialization callback.
* @type {BSN.InitCallback<ScrollSpy>}
*/
const scrollspyInitCallback = (element) => new ScrollSpy(element);
// SCROLLSPY CUSTOM EVENT
// ======================
const activateScrollSpy = OriginalEvent(`activate.bs.${scrollspyString}`);
// SCROLLSPY PRIVATE METHODS
// =========================
/**
* Update the state of all items.
* @param {ScrollSpy} self the `ScrollSpy` instance
*/
function updateSpyTargets(self) {
const {
target, scrollTarget, options, itemsLength, scrollHeight, element,
} = self;
const { offset } = options;
const isWin = scrollTarget instanceof Window;
const links = target && getElementsByTagName('A', target);
const scrollHEIGHT = scrollTarget && getScrollHeight(scrollTarget);
// @ts-ignore
self.scrollTop = isWin ? scrollTarget.scrollY : scrollTarget.scrollTop;
// only update items/offsets once or with each mutation
if (links && (itemsLength !== links.length || scrollHEIGHT !== scrollHeight)) {
let href;
let targetItem;
let rect;
// reset arrays & update
self.items = [];
self.offsets = [];
self.scrollHeight = scrollHEIGHT;
self.maxScroll = self.scrollHeight - getOffsetHeight(self);
[...links].forEach((link) => {
href = getAttribute(link, 'href');
targetItem = href && href.charAt(0) === '#' && href.slice(-1) !== '#'
&& querySelector(href, getDocument(element));
if (targetItem) {
self.items.push(link);
rect = getBoundingClientRect(targetItem);
// @ts-ignore
self.offsets.push((isWin ? rect.top + self.scrollTop : targetItem.offsetTop) - offset);
}
});
self.itemsLength = self.items.length;
}
}
/**
* Returns the `scrollHeight` property of the scrolling element.
* @param {HTMLElement | Element | Window | globalThis} scrollTarget the `ScrollSpy` instance
* @return {number} `scrollTarget` height
*/
function getScrollHeight(scrollTarget) {
return scrollTarget instanceof HTMLElement
? scrollTarget.scrollHeight // @ts-ignore
: getDocumentElement(scrollTarget).scrollHeight;
}
/**
* Returns the height property of the scrolling element.
* @param {ScrollSpy} params the `ScrollSpy` instance
* @returns {number}
*/
function getOffsetHeight({ element, scrollTarget }) {
return (scrollTarget instanceof Window)
? scrollTarget.innerHeight
: getBoundingClientRect(element).height;
}
/**
* Clear all items of the target.
* @param {HTMLElement | Element} target a single item
*/
function clear(target) {
[...getElementsByTagName('A', target)].forEach((item) => {
if (hasClass(item, activeClass)) removeClass(item, activeClass);
});
}
/**
* Activates a new item.
* @param {ScrollSpy} self the `ScrollSpy` instance
* @param {HTMLElement | Element} item a single item
*/
function activate(self, item) {
const { target, element } = self;
// @ts-ignore
clear(target);
// @ts-ignore
self.activeItem = item;
addClass(item, activeClass);
// activate all parents
const parents = [];
let parentItem = item;
while (parentItem !== getDocumentBody(element)) {
// @ts-ignore
parentItem = parentItem.parentElement;
if (hasClass(parentItem, 'nav') || hasClass(parentItem, 'dropdown-menu')) parents.push(parentItem);
}
parents.forEach((menuItem) => {
/** @type {(HTMLElement | Element)?} */
const parentLink = menuItem.previousElementSibling;
if (parentLink && !hasClass(parentLink, activeClass)) {
addClass(parentLink, activeClass);
}
});
// dispatch
activateScrollSpy.relatedTarget = item;
dispatchEvent(element, activateScrollSpy);
}
/**
* Toggles on/off the component event listener.
* @param {ScrollSpy} self the `ScrollSpy` instance
* @param {boolean=} add when `true`, listener is added
*/
function toggleSpyHandlers(self, add) {
const action = add ? addListener : removeListener;
// @ts-ignore
action(self.scrollTarget, scrollEvent, self.refresh, passiveHandler);
}
// SCROLLSPY DEFINITION
// ====================
/** Returns a new `ScrollSpy` instance. */
class ScrollSpy extends BaseComponent {
/**
* @param {HTMLElement | Element | string} target the target element
* @param {BSN.Options.ScrollSpy=} config the instance options
*/
constructor(target, config) {
super(target, config);
// bind
const self = this;
// initialization element & options
const { element, options } = self;
// additional properties
/** @type {(HTMLElement | Element)?} */
self.target = querySelector(options.target, getDocument(element));
// invalidate
if (!self.target) return;
const win = getWindow(element);
// set initial state
/** @type {HTMLElement | Element | Window | globalThis} */
self.scrollTarget = element.clientHeight < element.scrollHeight ? element : win;
/** @type {number} */
self.scrollTop = 0;
/** @type {number} */
self.maxScroll = 0;
/** @type {number} */
self.scrollHeight = 0;
/** @type {(HTMLElement | Element)?} */
self.activeItem = null;
/** @type {(HTMLElement | Element)[]} */
self.items = [];
/** @type {number} */
self.itemsLength = 0;
/** @type {number[]} */
self.offsets = [];
// bind events
self.refresh = self.refresh.bind(self);
// add event handlers
toggleSpyHandlers(self, true);
self.refresh();
}
/* eslint-disable */
/**
* Returns component name string.
* @readonly @static
*/
get name() { return scrollspyComponent; }
/**
* Returns component default options.
* @readonly @static
*/
get defaults() { return scrollspyDefaults; }
/* eslint-enable */
// SCROLLSPY PUBLIC METHODS
// ========================
/** Updates all items. */
refresh() {
const self = this;
const { target } = self;
// check if target is visible and invalidate
// @ts-ignore
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);
}
return;
}
const { offsets } = self;
if (activeItem && scrollTop < offsets[0] && offsets[0] > 0) {
self.activeItem = null;
// @ts-ignore
clear(target);
return;
}
items.forEach((item, i) => {
if (activeItem !== item && scrollTop >= offsets[i]
&& (typeof offsets[i + 1] === 'undefined' || scrollTop < offsets[i + 1])) {
activate(self, item);
}
});
}
/** Removes `ScrollSpy` from the target element. */
dispose() {
toggleSpyHandlers(this);
super.dispose();
}
}
ObjectAssign(ScrollSpy, {
selector: scrollspySelector,
init: scrollspyInitCallback,
getInstance: getScrollSpyInstance,
});
/**
* A global namespace for aria-selected.
* @type {string}
*/
const ariaSelected = 'aria-selected';
/** @type {string} */
const tabString = 'tab';
/** @type {string} */
const tabComponent = 'Tab';
/* Native JavaScript for Bootstrap 5 | Tab
------------------------------------------ */
// TAB PRIVATE GC
// ================
const tabSelector = `[${dataBsToggle}="${tabString}"]`;
/**
* Static method which returns an existing `Tab` instance associated
* to a target `Element`.
*
* @type {BSN.GetInstance<Tab>}
*/
const getTabInstance = (element) => getInstance(element, tabComponent);
/**
* A `Tab` initialization callback.
* @type {BSN.InitCallback<Tab>}
*/
const tabInitCallback = (element) => new Tab(element);
// TAB CUSTOM EVENTS
// =================
const showTabEvent = OriginalEvent(`show.bs.${tabString}`);
const shownTabEvent = OriginalEvent(`shown.bs.${tabString}`);
const hideTabEvent = OriginalEvent(`hide.bs.${tabString}`);
const hiddenTabEvent = OriginalEvent(`hidden.bs.${tabString}`);
/**
* Stores the current active tab and its content
* for a given `.nav` element.
* @type {Map<(HTMLElement | Element), any>}
*/
const tabPrivate = new Map();
// TAB PRIVATE METHODS
// ===================
/**
* Executes after tab transition has finished.
* @param {Tab} self the `Tab` instance
*/
function triggerTabEnd(self) {
const { tabContent, nav } = self;
if (tabContent && hasClass(tabContent, collapsingClass)) {
// @ts-ignore
tabContent.style.height = '';
removeClass(tabContent, collapsingClass);
}
if (nav) Timer.clear(nav);
}
/**
* Executes before showing the tab content.
* @param {Tab} self the `Tab` instance
*/
function triggerTabShow(self) {
const {
element, tabContent, content: nextContent, nav,
} = self;
const { tab } = nav && tabPrivate.get(nav);
if (tabContent && hasClass(nextContent, fadeClass)) { // height animation
const { currentHeight, nextHeight } = tabPrivate.get(element);
if (currentHeight === nextHeight) {
triggerTabEnd(self);
} else {
setTimeout(() => { // enables height animation
// @ts-ignore
tabContent.style.height = `${nextHeight}px`; // height animation
reflow(tabContent);
emulateTransitionEnd(tabContent, () => triggerTabEnd(self));
}, 50);
}
} else if (nav) Timer.clear(nav);
shownTabEvent.relatedTarget = tab;
dispatchEvent(element, shownTabEvent);
}
/**
* Executes before hiding the tab.
* @param {Tab} self the `Tab` instance
*/
function triggerTabHide(self) {
const {
element, content: nextContent, tabContent, nav,
} = self;
const { tab, content } = nav && tabPrivate.get(nav);
let currentHeight = 0;
if (tabContent && hasClass(nextContent, fadeClass)) {
[content, nextContent].forEach((c) => {
addClass(c, 'overflow-hidden');
});
currentHeight = content.scrollHeight || 0;
}
// update relatedTarget and dispatch event
showTabEvent.relatedTarget = tab;
hiddenTabEvent.relatedTarget = element;
dispatchEvent(element, showTabEvent);
if (showTabEvent.defaultPrevented) return;
addClass(nextContent, activeClass);
removeClass(content, activeClass);
if (tabContent && hasClass(nextContent, fadeClass)) {
const nextHeight = nextContent.scrollHeight;
tabPrivate.set(element, { currentHeight, nextHeight });
addClass(tabContent, collapsingClass);
// @ts-ignore -- height animation
tabContent.style.height = `${currentHeight}px`;
reflow(tabContent);
[content, nextContent].forEach((c) => {
removeClass(c, 'overflow-hidden');
});
}
if (nextContent && hasClass(nextContent, fadeClass)) {
setTimeout(() => {
addClass(nextContent, showClass);
emulateTransitionEnd(nextContent, () => {
triggerTabShow(self);
});
}, 1);
} else {
addClass(nextContent, showClass);
triggerTabShow(self);
}
dispatchEvent(tab, hiddenTabEvent);
}
/**
* Returns the current active tab and its target content.
* @param {Tab} self the `Tab` instance
* @returns {Record<string, any>} the query result
*/
function getActiveTab(self) {
const { nav } = self;
// @ts-ignore
const activeTabs = getElementsByClassName(activeClass, nav);
/** @type {(HTMLElement | Element)=} */
let tab;
if (activeTabs.length === 1
// @ts-ignore
&& !dropdownMenuClasses.some((c) => hasClass(activeTabs[0].parentElement, c))) {
[tab] = activeTabs;
} else if (activeTabs.length > 1) {
tab = activeTabs[activeTabs.length - 1];
}
const content = tab ? getTargetElement(tab) : null;
// @ts-ignore
return { tab, content };
}
/**
* Returns a parent dropdown.
* @param {HTMLElement | Element} element the `Tab` element
* @returns {(HTMLElement | Element)?} the parent dropdown
*/
function getParentDropdown(element) {
const dropdown = closest(element, `.${dropdownMenuClasses.join(',.')}`);
return dropdown ? querySelector(`.${dropdownMenuClasses[0]}-toggle`, dropdown) : null;
}
/**
* Toggles on/off the `click` event listener.
* @param {Tab} self the `Tab` instance
* @param {boolean=} add when `true`, event listener is added
*/
function toggleTabHandler(self, add) {
const action = add ? addListener : removeListener;
action(self.element, mouseclickEvent, tabClickHandler);
}
// TAB EVENT HANDLER
// =================
/**
* Handles the `click` event listener.
* @this {HTMLElement | Element}
* @param {MouseEvent} e the `Event` object
*/
function tabClickHandler(e) {
const self = getTabInstance(this);
if (!self) return;
e.preventDefault();
self.show();
}
// TAB DEFINITION
// ==============
/** Creates a new `Tab` instance. */
class Tab extends BaseComponent {
/**
* @param {HTMLElement | Element | string} target the target element
*/
constructor(target) {
super(target);
// bind
const self = this;
// initialization element
const { element } = self;
const content = getTargetElement(element);
// no point initializing a tab without a corresponding content
if (!content) return;
const nav = closest(element, '.nav');
const container = closest(content, '.tab-content');
/** @type {(HTMLElement | Element)?} */
self.nav = nav;
/** @type {HTMLElement | Element} */
self.content = content;
/** @type {(HTMLElement | Element)?} */
self.tabContent = container;
// event targets
/** @type {(HTMLElement | Element)?} */
self.dropdown = getParentDropdown(element);
// show first Tab instance of none is shown
// suggested on #432
const { tab } = getActiveTab(self);
if (nav && !tab) {
const firstTab = querySelector(tabSelector, nav);
const firstTabContent = firstTab && getTargetElement(firstTab);
if (firstTabContent) {
addClass(firstTab, activeClass);
addClass(firstTabContent, showClass);
addClass(firstTabContent, activeClass);
setAttribute(element, ariaSelected, 'true');
}
}
// add event listener
toggleTabHandler(self, true);
}
/* eslint-disable */
/**
* Returns component name string.
* @readonly @static
*/
get name() { return tabComponent; }
/* eslint-enable */
// TAB PUBLIC METHODS
// ==================
/** Shows the tab to the user. */
show() {
const self = this;
const { element, nav, dropdown } = self;
if (!(nav && Timer.get(nav)) && !hasClass(element, activeClass)) {
const { tab, content } = getActiveTab(self);
if (nav) tabPrivate.set(nav, { tab, content });
// update relatedTarget and dispatch
hideTabEvent.relatedTarget = element;
dispatchEvent(tab, hideTabEvent);
if (hideTabEvent.defaultPrevented) return;
addClass(element, activeClass);
setAttribute(element, ariaSelected, 'true');
const activeDropdown = getParentDropdown(tab);
if (activeDropdown && hasClass(activeDropdown, activeClass)) {
removeClass(activeDropdown, activeClass);
}
if (nav) {
Timer.set(nav, () => {
removeClass(tab, activeClass);
setAttribute(tab, ariaSelected, 'false');
if (dropdown && !hasClass(dropdown, activeClass)) addClass(dropdown, activeClass);
}, 1);
}
if (hasClass(content, fadeClass)) {
removeClass(content, showClass);
emulateTransitionEnd(content, () => triggerTabHide(self));
} else {
triggerTabHide(self);
}
}
}
/** Removes the `Tab` component from the target element. */
dispose() {
toggleTabHandler(this);
super.dispose();
}
}
ObjectAssign(Tab, {
selector: tabSelector,
init: tabInitCallback,
getInstance: getTabInstance,
});
/** @type {string} */
const toastString = 'toast';
/** @type {string} */
const toastComponent = 'Toast';
/* Native JavaScript for Bootstrap 5 | Toast
-------------------------------------------- */
// TOAST PRIVATE GC
// ================
const toastSelector = `.${toastString}`;
const toastDismissSelector = `[${dataBsDismiss}="${toastString}"]`;
const showingClass = 'showing';
/** @deprecated */
const hideClass = 'hide';
const toastDefaults = {
animation: true,
autohide: true,
delay: 5000,
};
/**
* Static method which returns an existing `Toast` instance associated
* to a target `Element`.
*
* @type {BSN.GetInstance<Toast>}
*/
const getToastInstance = (element) => getInstance(element, toastComponent);
/**
* A `Toast` initialization callback.
* @type {BSN.InitCallback<Toast>}
*/
const toastInitCallback = (element) => new Toast(element);
// TOAST CUSTOM EVENTS
// ===================
const showToastEvent = OriginalEvent(`show.bs.${toastString}`);
const shownToastEvent = OriginalEvent(`shown.bs.${toastString}`);
const hideToastEvent = OriginalEvent(`hide.bs.${toastString}`);
const hiddenToastEvent = OriginalEvent(`hidden.bs.${toastString}`);
// TOAST PRIVATE METHODS
// =====================
/**
* Executes after the toast is shown to the user.
* @param {Toast} self the `Toast` instance
*/
function showToastComplete(self) {
const { element, options } = self;
removeClass(element, showingClass);
Timer.clear(element, showingClass);
dispatchEvent(element, shownToastEvent);
if (options.autohide) {
Timer.set(element, () => self.hide(), options.delay, toastString);
}
}
/**
* Executes after the toast is hidden to the user.
* @param {Toast} self the `Toast` instance
*/
function hideToastComplete(self) {
const { element } = self;
removeClass(element, showingClass);
removeClass(element, showClass);
addClass(element, hideClass); // B/C
Timer.clear(element, toastString);
dispatchEvent(element, hiddenToastEvent);
}
/**
* Executes before hiding the toast.
* @param {Toast} self the `Toast` instance
*/
function hideToast(self) {
const { element, options } = self;
addClass(element, showingClass);
if (options.animation) {
reflow(element);
emulateTransitionEnd(element, () => hideToastComplete(self));
} else {
hideToastComplete(self);
}
}
/**
* Executes before showing the toast.
* @param {Toast} self the `Toast` instance
*/
function showToast(self) {
const { element, options } = self;
Timer.set(element, () => {
removeClass(element, hideClass); // B/C
reflow(element);
addClass(element, showClass);
addClass(element, showingClass);
if (options.animation) {
emulateTransitionEnd(element, () => showToastComplete(self));
} else {
showToastComplete(self);
}
}, 17, showingClass);
}
/**
* Toggles on/off the `click` event listener.
* @param {Toast} self the `Toast` instance
* @param {boolean=} add when `true`, it will add the listener
*/
function toggleToastHandlers(self, add) {
const action = add ? addListener : removeListener;
const { element, dismiss, options } = self;
if (dismiss) {
action(dismiss, mouseclickEvent, self.hide);
}
if (options.autohide) {
[focusinEvent, focusoutEvent, mouseenterEvent, mouseleaveEvent]
.forEach((e) => action(element, e, interactiveToastHandler));
}
}
// TOAST EVENT HANDLERS
// ====================
/**
* Executes after the instance has been disposed.
* @param {Toast} self the `Toast` instance
*/
function completeDisposeToast(self) {
Timer.clear(self.element, toastString);
toggleToastHandlers(self);
}
/**
* Executes when user interacts with the toast without closing it,
* usually by hovering or focusing it.
*
* @this {HTMLElement | Element}
* @param {MouseEvent} e the `Toast` instance
*/
function interactiveToastHandler(e) {
const element = this;
const self = getToastInstance(element);
const { type, relatedTarget } = e;
// @ts-ignore
if (!self || (element === relatedTarget || element.contains(relatedTarget))) return;
if ([mouseenterEvent, focusinEvent].includes(type)) {
Timer.clear(element, toastString);
} else {
Timer.set(element, () => self.hide(), self.options.delay, toastString);
}
}
// TOAST DEFINITION
// ================
/** Creates a new `Toast` instance. */
class Toast extends BaseComponent {
/**
* @param {HTMLElement | Element | string} target the target `.toast` element
* @param {BSN.Options.Toast=} config the instance options
*/
constructor(target, config) {
super(target, config);
// bind
const self = this;
const { element, options } = self;
// set fadeClass, the options.animation will override the markup
if (options.animation && !hasClass(element, fadeClass)) addClass(element, fadeClass);
else if (!options.animation && hasClass(element, fadeClass)) removeClass(element, fadeClass);
// dismiss button
/** @type {(HTMLElement | Element)?} */
self.dismiss = querySelector(toastDismissSelector, element);
// bind
self.show = self.show.bind(self);
self.hide = self.hide.bind(self);
// add event listener
toggleToastHandlers(self, true);
}
/* eslint-disable */
/**
* Returns component name string.
* @readonly @static
*/
get name() { return toastComponent; }
/**
* Returns component default options.
* @readonly @static
*/
get defaults() { return toastDefaults; }
/* eslint-enable */
// TOAST PUBLIC METHODS
// ====================
/** Shows the toast. */
show() {
const self = this;
const { element } = self;
if (element && !hasClass(element, showClass)) {
dispatchEvent(element, showToastEvent);
if (showToastEvent.defaultPrevented) return;
showToast(self);
}
}
/** Hides the toast. */
hide() {
const self = this;
const { element } = self;
if (element && hasClass(element, showClass)) {
dispatchEvent(element, hideToastEvent);
if (hideToastEvent.defaultPrevented) return;
hideToast(self);
}
}
/** Removes the `Toast` component from the target element. */
dispose() {
const self = this;
const { element } = self;
if (hasClass(element, showClass)) {
removeClass(element, showClass);
}
completeDisposeToast(self);
super.dispose();
}
}
ObjectAssign(Toast, {
selector: toastSelector,
init: toastInitCallback,
getInstance: getToastInstance,
});
/**
* Check if element matches a CSS selector.
*
* @param {HTMLElement | Element} target
* @param {string} selector
* @returns {boolean}
*/
function matches(target, selector) {
return target.matches(selector);
}
/** @type {Record<string, any>} */
const componentsList = {
Alert,
Button,
Carousel,
Collapse,
Dropdown,
Modal,
Offcanvas,
Popover,
ScrollSpy,
Tab,
Toast,
Tooltip,
};
/**
* Initialize all matched `Element`s for one component.
* @param {BSN.InitCallback<any>} callback
* @param {NodeListOf<HTMLElement | Element> | (HTMLElement | Element)[]} collection
*/
function initComponentDataAPI(callback, collection) {
[...collection].forEach((x) => callback(x));
}
/**
* Remove one component from a target container element or all in the page.
* @param {string} component the component name
* @param {(Element | HTMLElement | Document)=} context parent `Element`
*/
function removeComponentDataAPI(component, context) {
const compData = Data.getAllFor(component);
if (compData) {
[...compData].forEach((x) => {
const [element, instance] = x;
if (context && context.contains(element)) instance.dispose();
});
}
}
/**
* Initialize all BSN components for a target container.
* @param {(Element | HTMLElement | Document)=} context parent `Element`
*/
function initCallback(context) {
const lookUp = context && parentNodes.some((x) => context instanceof x)
? context : undefined;
const elemCollection = [...getElementsByTagName('*', lookUp)];
ObjectKeys(componentsList).forEach((comp) => {
const { init, selector } = componentsList[comp];
initComponentDataAPI(init, elemCollection.filter((item) => matches(item, selector)));
});
}
/**
* Remove all BSN components for a target container.
* @param {(Element | HTMLElement | Document)=} context parent `Element`
*/
function removeDataAPI(context) {
const lookUp = context && parentNodes.some((x) => context instanceof x)
? context : undefined;
ObjectKeys(componentsList).forEach((comp) => {
removeComponentDataAPI(comp, lookUp);
});
}
// bulk initialize all components
if (document.body) initCallback();
else {
addListener(document, 'DOMContentLoaded', () => initCallback(), { once: true });
}
const BSN = {
Alert,
Button,
Carousel,
Collapse,
Dropdown,
Modal,
Offcanvas,
Popover,
ScrollSpy,
Tab,
Toast,
Tooltip,
initCallback,
removeDataAPI,
Version,
EventListener,
};
return BSN;
}));