/*! * 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} */ 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} 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} 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>>} */ 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} 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>?} 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?} 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} */ const getInstance = (target, component) => Data.get(target, component); /** * Returns a namespaced `CustomEvent` specific to each component. * @param {string} EventType Event.type * @param {Record=} 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} 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} 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) { // @ts-ignore -- our targets are always `HTMLElement` 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) => { 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} */ 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; 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