/*! * Native JavaScript for Bootstrap v4.2.0 (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} */ const EventRegistry = {}; /** * The global event listener. * * @type {EventListener} * @this {EventTarget} */ function globalListener(e) { const that = this; const { type } = e; [...EventRegistry[type]].forEach((elementsMap) => { const [element, listenersMap] = elementsMap; /* istanbul ignore else */ if (element === that) { [...listenersMap].forEach((listenerMap) => { 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. * * @type {Listener.ListenerAction} */ 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 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. * * @type {Listener.ListenerAction} */ 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 /* istanbul ignore else */ 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 Listener = { 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 target * @param {string} property the css property * @return {string} the css property value */ function getElementStyle(element, property) { const computedStyle = getComputedStyle(element); // must use camelcase strings, // or non-camelcase strings with `getPropertyValue` return property.includes('--') ? computedStyle.getPropertyValue(property) : computedStyle[property]; } /** * Utility to get the computed `transitionDelay` * from Element in miliseconds. * * @param {HTMLElement} 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') ? /* istanbul ignore next */1 : 1000; const duration = propertyValue && propertyValue !== 'none' ? parseFloat(delayValue) * delayScale : 0; return !Number.isNaN(duration) ? duration : /* istanbul ignore next */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 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') ? /* istanbul ignore next */1 : 1000; const duration = propertyValue && propertyValue !== 'none' ? parseFloat(durationValue) * durationScale : 0; return !Number.isNaN(duration) ? duration : /* istanbul ignore next */0; } /** * Shortcut for the `Element.dispatchEvent(Event)` method. * * @param {HTMLElement} element is the target * @param {Event} event is the `Event` object */ const dispatchEvent = (element, event) => element.dispatchEvent(event); /** * Utility to make sure callbacks are consistently * called when transition ends. * * @param {HTMLElement} 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) => { /* istanbul ignore else */ if (e.target === element) { handler.apply(element, [e]); element.removeEventListener(transitionEndEvent, transitionEndWrapper); called = 1; } }; element.addEventListener(transitionEndEvent, transitionEndWrapper); setTimeout(() => { /* istanbul ignore next */ if (!called) dispatchEvent(element, endEvent); }, duration + delay + 17); } else { handler.apply(element, [endEvent]); } } /** * Checks if an object is a `Node`. * * @param {any} node the target object * @returns {boolean} the query result */ const isNode = (element) => (element && [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] .some((x) => +element.nodeType === x)) || false; /** * Check if a target object is `Window`. * => equivalent to `object instanceof Window` * * @param {any} object the target object * @returns {boolean} the query result */ const isWindow = (object) => (object && object.constructor.name === 'Window') || false; /** * Checks if an object is a `Document`. * @see https://dom.spec.whatwg.org/#node * * @param {any} object the target object * @returns {boolean} the query result */ const isDocument = (object) => (object && object.nodeType === 9) || false; /** * Returns the `document` or the `#document` element. * @see https://github.com/floating-ui/floating-ui * @param {(Node | Window)=} node * @returns {Document} */ function getDocument(node) { // node instanceof Document if (isDocument(node)) return node; // node instanceof Node if (isNode(node)) return node.ownerDocument; // node instanceof Window if (isWindow(node)) return node.document; // node is undefined | NULL return window.document; } /** * Utility to check if target is typeof `HTMLElement`, `Element`, `Node` * or find one that matches a selector. * * @param {Node | string} selector the input selector or target element * @param {ParentNode=} parent optional node to look into * @return {HTMLElement?} the `HTMLElement` or `querySelector` result */ function querySelector(selector, parent) { if (isNode(selector)) { return selector; } const lookUp = isNode(parent) ? parent : getDocument(); return 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 to look into * @param {string} selector the selector name * @return {HTMLElement?} the query result */ function closest(element, selector) { return element ? (element.closest(selector) // break out of `ShadowRoot` || closest(element.getRootNode().host, selector)) : null; } /** * Shortcut for `Object.assign()` static method. * @param {Record} obj a target object * @param {Record} source a source object */ const ObjectAssign = (obj, source) => Object.assign(obj, source); /** * Check class in `HTMLElement.classList`. * * @param {HTMLElement} 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 target * @param {string} classNAME to remove * @returns {void} */ function removeClass(element, classNAME) { element.classList.remove(classNAME); } /** * Checks if an element is an `HTMLElement`. * @see https://dom.spec.whatwg.org/#node * * @param {any} element the target object * @returns {boolean} the query result */ const isHTMLElement = (element) => (element && element.nodeType === 1) || false; /** @type {Map>>} */ 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 target element * @param {string} component the component's name or a unique key * @param {Record} instance the component instance */ set: (element, component, instance) => { if (!isHTMLElement(element)) return; /* istanbul ignore else */ if (!componentData.has(component)) { componentData.set(component, new Map()); } const instanceMap = componentData.get(component); // 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>?} all the component instances */ getAllFor: (component) => { const instanceMap = componentData.get(component); return instanceMap || null; }, /** * Returns the instance associated with the target. * @param {HTMLElement} element target element * @param {string} component the component's name or a unique key * @returns {Record?} the instance */ get: (element, component) => { if (!isHTMLElement(element) || !component) return null; const allForC = Data.getAllFor(component); const instance = element && allForC && allForC.get(element); return instance || null; }, /** * Removes web components data. * @param {HTMLElement} element target element * @param {string} component the component's name or a unique key */ remove: (element, component) => { const instanceMap = componentData.get(component); if (!instanceMap || !isHTMLElement(element)) return; instanceMap.delete(element); /* istanbul ignore else */ if (instanceMap.size === 0) { componentData.delete(component); } }, }; /** * An alias for `Data.get()`. * @type {SHORTY.getInstance} */ const getInstance = (target, component) => Data.get(target, component); /** * Checks if an object is an `Object`. * * @param {any} obj the target object * @returns {boolean} the query result */ const isObject = (obj) => (typeof obj === 'object') || false; /** * Returns a namespaced `CustomEvent` specific to each component. * @param {string} EventType Event.type * @param {Record=} config Event.options | Event.properties * @returns {SHORTY.OriginalEvent} a new namespaced event */ function OriginalEvent(EventType, config) { const OriginalCustomEvent = new CustomEvent(EventType, { cancelable: true, bubbles: true, }); /* istanbul ignore else */ if (isObject(config)) { 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 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 (['true', true].includes(value)) { // boolean // if ('true' === value) { // boolean return true; } if (['false', false].includes(value)) { // boolean // if ('false' === value) { // boolean return false; } if (value === '' || value === 'null') { // null return null; } if (value !== '' && !Number.isNaN(+value)) { // number return +value; } // string / function / HTMLElement / object return value; } /** * Shortcut for `Object.keys()` static method. * @param {Record} 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 target * @param {Record} defaultOps component default options * @param {Record} inputOps component instance options * @param {string=} ns component namespace * @return {Record} normalized component options object */ function normalizeOptions(element, defaultOps, inputOps, ns) { const data = { ...element.dataset }; /** @type {Record} */ const normalOps = {}; /** @type {Record} */ 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) => { /* istanbul ignore else */ 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.2.0"; const Version = version; /* Native JavaScript for Bootstrap 5 | Base Component ----------------------------------------------------- */ /** Returns a new `BaseComponent` instance. */ class BaseComponent { /** * @param {HTMLElement | 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} */ self.element = element; /* istanbul ignore else */ if (self.defaults && ObjectKeys(self.defaults).length) { self.options = normalizeOptions(element, self.defaults, (config || {}), 'bs'); } Data.set(element, self.name, self); } /* eslint-disable */ /* istanbul ignore next */ /** @static */ get version() { return Version; } /* eslint-enable */ /* istanbul ignore next */ /** @static */ get name() { return this.constructor.name; } /* istanbul ignore next */ /** @static */ get defaults() { return this.constructor.defaults; } /** * Removes component from target element; */ dispose() { const self = this; Data.remove(self.element, self.name); 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} */ const getAlertInstance = (element) => getInstance(element, alertComponent); /** * An `Alert` initialization callback. * @type {BSN.InitCallback} */ 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; /* istanbul ignore else */ if (dismiss) action(dismiss, mouseclickEvent, self.close); } // ALERT DEFINITION // ================ /** Creates a new Alert instance. */ class Alert extends BaseComponent { /** @param {HTMLElement | string} target element or selector */ constructor(target) { super(target); // bind const self = this; // initialization element const { element } = self; // the dismiss button /** @static @type {HTMLElement?} */ self.dismiss = querySelector(alertDismissSelector, element); // add event listener toggleAlertHandler(self, true); } /* eslint-disable */ /** * Returns component name string. */ 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) { const self = e ? getAlertInstance(closest(this, alertSelector)) : this; const { element } = self; /* istanbul ignore else */ if (element && 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 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 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