| /*! |
| * Bootstrap v5.3.8 (https://getbootstrap.com/) |
| * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) |
| * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) |
| * |
| * -------------------------------------------------------------------------- |
| * Modified to remove code unused by the Zephyr Dashboard. |
| * -------------------------------------------------------------------------- |
| */ |
| (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.bootstrap = factory()); |
| })(this, (function () { 'use strict'; |
| |
| /** |
| * -------------------------------------------------------------------------- |
| * Bootstrap dom/data.js |
| * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) |
| * -------------------------------------------------------------------------- |
| */ |
| |
| /** |
| * Constants |
| */ |
| |
| const elementMap = new Map(); |
| const Data = { |
| set(element, key, instance) { |
| if (!elementMap.has(element)) { |
| elementMap.set(element, new Map()); |
| } |
| const instanceMap = elementMap.get(element); |
| |
| // make it clear we only want one instance per element |
| // can be removed later when multiple key/instances are fine to be used |
| if (!instanceMap.has(key) && instanceMap.size !== 0) { |
| // eslint-disable-next-line no-console |
| console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(instanceMap.keys())[0]}.`); |
| return; |
| } |
| instanceMap.set(key, instance); |
| }, |
| get(element, key) { |
| if (elementMap.has(element)) { |
| return elementMap.get(element).get(key) || null; |
| } |
| return null; |
| }, |
| remove(element, key) { |
| if (!elementMap.has(element)) { |
| return; |
| } |
| const instanceMap = elementMap.get(element); |
| instanceMap.delete(key); |
| |
| // free up element references if there are no instances left for an element |
| if (instanceMap.size === 0) { |
| elementMap.delete(element); |
| } |
| } |
| }; |
| |
| /** |
| * -------------------------------------------------------------------------- |
| * Bootstrap util/index.js |
| * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) |
| * -------------------------------------------------------------------------- |
| */ |
| |
| const MAX_UID = 1000000; |
| const MILLISECONDS_MULTIPLIER = 1000; |
| const TRANSITION_END = 'transitionend'; |
| |
| /** |
| * Properly escape IDs selectors to handle weird IDs |
| * @param {string} selector |
| * @returns {string} |
| */ |
| const parseSelector = selector => { |
| if (selector && window.CSS && window.CSS.escape) { |
| // document.querySelector needs escaping to handle IDs (html5+) containing for instance / |
| selector = selector.replace(/#([^\s"#']+)/g, (match, id) => `#${CSS.escape(id)}`); |
| } |
| return selector; |
| }; |
| |
| // Shout-out Angus Croll (https://goo.gl/pxwQGp) |
| const toType = object => { |
| if (object === null || object === undefined) { |
| return `${object}`; |
| } |
| return Object.prototype.toString.call(object).match(/\s([a-z]+)/i)[1].toLowerCase(); |
| }; |
| |
| /** |
| * Public Util API |
| */ |
| |
| const getUID = prefix => { |
| do { |
| prefix += Math.floor(Math.random() * MAX_UID); |
| } while (document.getElementById(prefix)); |
| return prefix; |
| }; |
| const getTransitionDurationFromElement = element => { |
| if (!element) { |
| return 0; |
| } |
| |
| // Get transition-duration of the element |
| let { |
| transitionDuration, |
| transitionDelay |
| } = window.getComputedStyle(element); |
| const floatTransitionDuration = Number.parseFloat(transitionDuration); |
| const floatTransitionDelay = Number.parseFloat(transitionDelay); |
| |
| // Return 0 if element or transition duration is not found |
| if (!floatTransitionDuration && !floatTransitionDelay) { |
| return 0; |
| } |
| |
| // If multiple durations are defined, take the first |
| transitionDuration = transitionDuration.split(',')[0]; |
| transitionDelay = transitionDelay.split(',')[0]; |
| return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER; |
| }; |
| const triggerTransitionEnd = element => { |
| element.dispatchEvent(new Event(TRANSITION_END)); |
| }; |
| const isElement = object => { |
| if (!object || typeof object !== 'object') { |
| return false; |
| } |
| if (typeof object.jquery !== 'undefined') { |
| object = object[0]; |
| } |
| return typeof object.nodeType !== 'undefined'; |
| }; |
| const getElement = object => { |
| // it's a jQuery object or a node element |
| if (isElement(object)) { |
| return object.jquery ? object[0] : object; |
| } |
| if (typeof object === 'string' && object.length > 0) { |
| return document.querySelector(parseSelector(object)); |
| } |
| return null; |
| }; |
| const isVisible = element => { |
| if (!isElement(element) || element.getClientRects().length === 0) { |
| return false; |
| } |
| const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible'; |
| // Handle `details` element as its content may falsie appear visible when it is closed |
| const closedDetails = element.closest('details:not([open])'); |
| if (!closedDetails) { |
| return elementIsVisible; |
| } |
| if (closedDetails !== element) { |
| const summary = element.closest('summary'); |
| if (summary && summary.parentNode !== closedDetails) { |
| return false; |
| } |
| if (summary === null) { |
| return false; |
| } |
| } |
| return elementIsVisible; |
| }; |
| const isDisabled = element => { |
| if (!element || element.nodeType !== Node.ELEMENT_NODE) { |
| return true; |
| } |
| if (element.classList.contains('disabled')) { |
| return true; |
| } |
| if (typeof element.disabled !== 'undefined') { |
| return element.disabled; |
| } |
| return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false'; |
| }; |
| const findShadowRoot = element => { |
| if (!document.documentElement.attachShadow) { |
| return null; |
| } |
| |
| // Can find the shadow root otherwise it'll return the document |
| if (typeof element.getRootNode === 'function') { |
| const root = element.getRootNode(); |
| return root instanceof ShadowRoot ? root : null; |
| } |
| if (element instanceof ShadowRoot) { |
| return element; |
| } |
| |
| // when we don't find a shadow root |
| if (!element.parentNode) { |
| return null; |
| } |
| return findShadowRoot(element.parentNode); |
| }; |
| const noop = () => {}; |
| |
| /** |
| * Trick to restart an element's animation |
| * |
| * @param {HTMLElement} element |
| * @return void |
| * |
| * @see https://www.harrytheo.com/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation |
| */ |
| const reflow = element => { |
| element.offsetHeight; // eslint-disable-line no-unused-expressions |
| }; |
| const getjQuery = () => { |
| if (window.jQuery && !document.body.hasAttribute('data-bs-no-jquery')) { |
| return window.jQuery; |
| } |
| return null; |
| }; |
| const DOMContentLoadedCallbacks = []; |
| const onDOMContentLoaded = callback => { |
| if (document.readyState === 'loading') { |
| // add listener on the first call when the document is in loading state |
| if (!DOMContentLoadedCallbacks.length) { |
| document.addEventListener('DOMContentLoaded', () => { |
| for (const callback of DOMContentLoadedCallbacks) { |
| callback(); |
| } |
| }); |
| } |
| DOMContentLoadedCallbacks.push(callback); |
| } else { |
| callback(); |
| } |
| }; |
| const isRTL = () => document.documentElement.dir === 'rtl'; |
| const execute = (possibleCallback, args = [], defaultValue = possibleCallback) => { |
| return typeof possibleCallback === 'function' ? possibleCallback.call(...args) : defaultValue; |
| }; |
| const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => { |
| if (!waitForTransition) { |
| execute(callback); |
| return; |
| } |
| const durationPadding = 5; |
| const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding; |
| let called = false; |
| const handler = ({ |
| target |
| }) => { |
| if (target !== transitionElement) { |
| return; |
| } |
| called = true; |
| transitionElement.removeEventListener(TRANSITION_END, handler); |
| execute(callback); |
| }; |
| transitionElement.addEventListener(TRANSITION_END, handler); |
| setTimeout(() => { |
| if (!called) { |
| triggerTransitionEnd(transitionElement); |
| } |
| }, emulatedDuration); |
| }; |
| |
| /** |
| * Return the previous/next element of a list. |
| * |
| * @param {array} list The list of elements |
| * @param activeElement The active element |
| * @param shouldGetNext Choose to get next or previous element |
| * @param isCycleAllowed |
| * @return {Element|elem} The proper element |
| */ |
| const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => { |
| const listLength = list.length; |
| let index = list.indexOf(activeElement); |
| |
| // if the element does not exist in the list return an element |
| // depending on the direction and if cycle is allowed |
| if (index === -1) { |
| return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0]; |
| } |
| index += shouldGetNext ? 1 : -1; |
| if (isCycleAllowed) { |
| index = (index + listLength) % listLength; |
| } |
| return list[Math.max(0, Math.min(index, listLength - 1))]; |
| }; |
| |
| /** |
| * -------------------------------------------------------------------------- |
| * Bootstrap dom/event-handler.js |
| * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) |
| * -------------------------------------------------------------------------- |
| */ |
| |
| |
| /** |
| * Constants |
| */ |
| |
| const namespaceRegex = /[^.]*(?=\..*)\.|.*/; |
| const stripNameRegex = /\..*/; |
| const stripUidRegex = /::\d+$/; |
| const eventRegistry = {}; // Events storage |
| let uidEvent = 1; |
| const customEvents = { |
| mouseenter: 'mouseover', |
| mouseleave: 'mouseout' |
| }; |
| const nativeEvents = new Set(['click', 'dblclick', 'mouseup', 'mousedown', 'contextmenu', 'mousewheel', 'DOMMouseScroll', 'mouseover', 'mouseout', 'mousemove', 'selectstart', 'selectend', 'keydown', 'keypress', 'keyup', 'orientationchange', 'touchstart', 'touchmove', 'touchend', 'touchcancel', 'pointerdown', 'pointermove', 'pointerup', 'pointerleave', 'pointercancel', 'gesturestart', 'gesturechange', 'gestureend', 'focus', 'blur', 'change', 'reset', 'select', 'submit', 'focusin', 'focusout', 'load', 'unload', 'beforeunload', 'resize', 'move', 'DOMContentLoaded', 'readystatechange', 'error', 'abort', 'scroll']); |
| |
| /** |
| * Private methods |
| */ |
| |
| function makeEventUid(element, uid) { |
| return uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++; |
| } |
| function getElementEvents(element) { |
| const uid = makeEventUid(element); |
| element.uidEvent = uid; |
| eventRegistry[uid] = eventRegistry[uid] || {}; |
| return eventRegistry[uid]; |
| } |
| function bootstrapHandler(element, fn) { |
| return function handler(event) { |
| hydrateObj(event, { |
| delegateTarget: element |
| }); |
| if (handler.oneOff) { |
| EventHandler.off(element, event.type, fn); |
| } |
| return fn.apply(element, [event]); |
| }; |
| } |
| function bootstrapDelegationHandler(element, selector, fn) { |
| return function handler(event) { |
| const domElements = element.querySelectorAll(selector); |
| for (let { |
| target |
| } = event; target && target !== this; target = target.parentNode) { |
| for (const domElement of domElements) { |
| if (domElement !== target) { |
| continue; |
| } |
| hydrateObj(event, { |
| delegateTarget: target |
| }); |
| if (handler.oneOff) { |
| EventHandler.off(element, event.type, selector, fn); |
| } |
| return fn.apply(target, [event]); |
| } |
| } |
| }; |
| } |
| function findHandler(events, callable, delegationSelector = null) { |
| return Object.values(events).find(event => event.callable === callable && event.delegationSelector === delegationSelector); |
| } |
| function normalizeParameters(originalTypeEvent, handler, delegationFunction) { |
| const isDelegated = typeof handler === 'string'; |
| // TODO: tooltip passes `false` instead of selector, so we need to check |
| const callable = isDelegated ? delegationFunction : handler || delegationFunction; |
| let typeEvent = getTypeEvent(originalTypeEvent); |
| if (!nativeEvents.has(typeEvent)) { |
| typeEvent = originalTypeEvent; |
| } |
| return [isDelegated, callable, typeEvent]; |
| } |
| function addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) { |
| if (typeof originalTypeEvent !== 'string' || !element) { |
| return; |
| } |
| let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction); |
| |
| // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position |
| // this prevents the handler from being dispatched the same way as mouseover or mouseout does |
| if (originalTypeEvent in customEvents) { |
| const wrapFunction = fn => { |
| return function (event) { |
| if (!event.relatedTarget || event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget)) { |
| return fn.call(this, event); |
| } |
| }; |
| }; |
| callable = wrapFunction(callable); |
| } |
| const events = getElementEvents(element); |
| const handlers = events[typeEvent] || (events[typeEvent] = {}); |
| const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null); |
| if (previousFunction) { |
| previousFunction.oneOff = previousFunction.oneOff && oneOff; |
| return; |
| } |
| const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, '')); |
| const fn = isDelegated ? bootstrapDelegationHandler(element, handler, callable) : bootstrapHandler(element, callable); |
| fn.delegationSelector = isDelegated ? handler : null; |
| fn.callable = callable; |
| fn.oneOff = oneOff; |
| fn.uidEvent = uid; |
| handlers[uid] = fn; |
| element.addEventListener(typeEvent, fn, isDelegated); |
| } |
| function removeHandler(element, events, typeEvent, handler, delegationSelector) { |
| const fn = findHandler(events[typeEvent], handler, delegationSelector); |
| if (!fn) { |
| return; |
| } |
| element.removeEventListener(typeEvent, fn, Boolean(delegationSelector)); |
| delete events[typeEvent][fn.uidEvent]; |
| } |
| function removeNamespacedHandlers(element, events, typeEvent, namespace) { |
| const storeElementEvent = events[typeEvent] || {}; |
| for (const [handlerKey, event] of Object.entries(storeElementEvent)) { |
| if (handlerKey.includes(namespace)) { |
| removeHandler(element, events, typeEvent, event.callable, event.delegationSelector); |
| } |
| } |
| } |
| function getTypeEvent(event) { |
| // allow to get the native events from namespaced events ('click.bs.button' --> 'click') |
| event = event.replace(stripNameRegex, ''); |
| return customEvents[event] || event; |
| } |
| const EventHandler = { |
| on(element, event, handler, delegationFunction) { |
| addHandler(element, event, handler, delegationFunction, false); |
| }, |
| one(element, event, handler, delegationFunction) { |
| addHandler(element, event, handler, delegationFunction, true); |
| }, |
| off(element, originalTypeEvent, handler, delegationFunction) { |
| if (typeof originalTypeEvent !== 'string' || !element) { |
| return; |
| } |
| const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction); |
| const inNamespace = typeEvent !== originalTypeEvent; |
| const events = getElementEvents(element); |
| const storeElementEvent = events[typeEvent] || {}; |
| const isNamespace = originalTypeEvent.startsWith('.'); |
| if (typeof callable !== 'undefined') { |
| // Simplest case: handler is passed, remove that listener ONLY. |
| if (!Object.keys(storeElementEvent).length) { |
| return; |
| } |
| removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null); |
| return; |
| } |
| if (isNamespace) { |
| for (const elementEvent of Object.keys(events)) { |
| removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1)); |
| } |
| } |
| for (const [keyHandlers, event] of Object.entries(storeElementEvent)) { |
| const handlerKey = keyHandlers.replace(stripUidRegex, ''); |
| if (!inNamespace || originalTypeEvent.includes(handlerKey)) { |
| removeHandler(element, events, typeEvent, event.callable, event.delegationSelector); |
| } |
| } |
| }, |
| trigger(element, event, args) { |
| if (typeof event !== 'string' || !element) { |
| return null; |
| } |
| const $ = getjQuery(); |
| const typeEvent = getTypeEvent(event); |
| const inNamespace = event !== typeEvent; |
| let jQueryEvent = null; |
| let bubbles = true; |
| let nativeDispatch = true; |
| let defaultPrevented = false; |
| if (inNamespace && $) { |
| jQueryEvent = $.Event(event, args); |
| $(element).trigger(jQueryEvent); |
| bubbles = !jQueryEvent.isPropagationStopped(); |
| nativeDispatch = !jQueryEvent.isImmediatePropagationStopped(); |
| defaultPrevented = jQueryEvent.isDefaultPrevented(); |
| } |
| const evt = hydrateObj(new Event(event, { |
| bubbles, |
| cancelable: true |
| }), args); |
| if (defaultPrevented) { |
| evt.preventDefault(); |
| } |
| if (nativeDispatch) { |
| element.dispatchEvent(evt); |
| } |
| if (evt.defaultPrevented && jQueryEvent) { |
| jQueryEvent.preventDefault(); |
| } |
| return evt; |
| } |
| }; |
| function hydrateObj(obj, meta = {}) { |
| for (const [key, value] of Object.entries(meta)) { |
| try { |
| obj[key] = value; |
| } catch (_unused) { |
| Object.defineProperty(obj, key, { |
| configurable: true, |
| get() { |
| return value; |
| } |
| }); |
| } |
| } |
| return obj; |
| } |
| |
| /** |
| * -------------------------------------------------------------------------- |
| * Bootstrap dom/manipulator.js |
| * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) |
| * -------------------------------------------------------------------------- |
| */ |
| |
| function normalizeData(value) { |
| if (value === 'true') { |
| return true; |
| } |
| if (value === 'false') { |
| return false; |
| } |
| if (value === Number(value).toString()) { |
| return Number(value); |
| } |
| if (value === '' || value === 'null') { |
| return null; |
| } |
| if (typeof value !== 'string') { |
| return value; |
| } |
| try { |
| return JSON.parse(decodeURIComponent(value)); |
| } catch (_unused) { |
| return value; |
| } |
| } |
| function normalizeDataKey(key) { |
| return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`); |
| } |
| const Manipulator = { |
| setDataAttribute(element, key, value) { |
| element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value); |
| }, |
| removeDataAttribute(element, key) { |
| element.removeAttribute(`data-bs-${normalizeDataKey(key)}`); |
| }, |
| getDataAttributes(element) { |
| if (!element) { |
| return {}; |
| } |
| const attributes = {}; |
| const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig')); |
| for (const key of bsKeys) { |
| let pureKey = key.replace(/^bs/, ''); |
| pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1); |
| attributes[pureKey] = normalizeData(element.dataset[key]); |
| } |
| return attributes; |
| }, |
| getDataAttribute(element, key) { |
| return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`)); |
| } |
| }; |
| |
| /** |
| * -------------------------------------------------------------------------- |
| * Bootstrap util/config.js |
| * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) |
| * -------------------------------------------------------------------------- |
| */ |
| |
| |
| /** |
| * Class definition |
| */ |
| |
| class Config { |
| // Getters |
| static get Default() { |
| return {}; |
| } |
| static get DefaultType() { |
| return {}; |
| } |
| static get NAME() { |
| throw new Error('You have to implement the static method "NAME", for each component!'); |
| } |
| _getConfig(config) { |
| config = this._mergeConfigObj(config); |
| config = this._configAfterMerge(config); |
| this._typeCheckConfig(config); |
| return config; |
| } |
| _configAfterMerge(config) { |
| return config; |
| } |
| _mergeConfigObj(config, element) { |
| const jsonConfig = isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {}; // try to parse |
| |
| return { |
| ...this.constructor.Default, |
| ...(typeof jsonConfig === 'object' ? jsonConfig : {}), |
| ...(isElement(element) ? Manipulator.getDataAttributes(element) : {}), |
| ...(typeof config === 'object' ? config : {}) |
| }; |
| } |
| _typeCheckConfig(config, configTypes = this.constructor.DefaultType) { |
| for (const [property, expectedTypes] of Object.entries(configTypes)) { |
| const value = config[property]; |
| const valueType = isElement(value) ? 'element' : toType(value); |
| if (!new RegExp(expectedTypes).test(valueType)) { |
| throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${property}" provided type "${valueType}" but expected type "${expectedTypes}".`); |
| } |
| } |
| } |
| } |
| |
| /** |
| * -------------------------------------------------------------------------- |
| * Bootstrap base-component.js |
| * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) |
| * -------------------------------------------------------------------------- |
| */ |
| |
| |
| /** |
| * Constants |
| */ |
| |
| const VERSION = '5.3.8'; |
| |
| /** |
| * Class definition |
| */ |
| |
| class BaseComponent extends Config { |
| constructor(element, config) { |
| super(); |
| element = getElement(element); |
| if (!element) { |
| return; |
| } |
| this._element = element; |
| this._config = this._getConfig(config); |
| Data.set(this._element, this.constructor.DATA_KEY, this); |
| } |
| |
| // Public |
| dispose() { |
| Data.remove(this._element, this.constructor.DATA_KEY); |
| EventHandler.off(this._element, this.constructor.EVENT_KEY); |
| for (const propertyName of Object.getOwnPropertyNames(this)) { |
| this[propertyName] = null; |
| } |
| } |
| |
| // Private |
| _queueCallback(callback, element, isAnimated = true) { |
| executeAfterTransition(callback, element, isAnimated); |
| } |
| _getConfig(config) { |
| config = this._mergeConfigObj(config, this._element); |
| config = this._configAfterMerge(config); |
| this._typeCheckConfig(config); |
| return config; |
| } |
| |
| // Static |
| static getInstance(element) { |
| return Data.get(getElement(element), this.DATA_KEY); |
| } |
| static getOrCreateInstance(element, config = {}) { |
| return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null); |
| } |
| static get VERSION() { |
| return VERSION; |
| } |
| static get DATA_KEY() { |
| return `bs.${this.NAME}`; |
| } |
| static get EVENT_KEY() { |
| return `.${this.DATA_KEY}`; |
| } |
| static eventName(name) { |
| return `${name}${this.EVENT_KEY}`; |
| } |
| } |
| |
| /** |
| * -------------------------------------------------------------------------- |
| * Bootstrap dom/selector-engine.js |
| * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) |
| * -------------------------------------------------------------------------- |
| */ |
| |
| const getSelector = element => { |
| let selector = element.getAttribute('data-bs-target'); |
| if (!selector || selector === '#') { |
| let hrefAttribute = element.getAttribute('href'); |
| |
| // The only valid content that could double as a selector are IDs or classes, |
| // so everything starting with `#` or `.`. If a "real" URL is used as the selector, |
| // `document.querySelector` will rightfully complain it is invalid. |
| // See https://github.com/twbs/bootstrap/issues/32273 |
| if (!hrefAttribute || !hrefAttribute.includes('#') && !hrefAttribute.startsWith('.')) { |
| return null; |
| } |
| |
| // Just in case some CMS puts out a full URL with the anchor appended |
| if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) { |
| hrefAttribute = `#${hrefAttribute.split('#')[1]}`; |
| } |
| selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null; |
| } |
| return selector ? selector.split(',').map(sel => parseSelector(sel)).join(',') : null; |
| }; |
| const SelectorEngine = { |
| find(selector, element = document.documentElement) { |
| return [].concat(...Element.prototype.querySelectorAll.call(element, selector)); |
| }, |
| findOne(selector, element = document.documentElement) { |
| return Element.prototype.querySelector.call(element, selector); |
| }, |
| children(element, selector) { |
| return [].concat(...element.children).filter(child => child.matches(selector)); |
| }, |
| parents(element, selector) { |
| const parents = []; |
| let ancestor = element.parentNode.closest(selector); |
| while (ancestor) { |
| parents.push(ancestor); |
| ancestor = ancestor.parentNode.closest(selector); |
| } |
| return parents; |
| }, |
| prev(element, selector) { |
| let previous = element.previousElementSibling; |
| while (previous) { |
| if (previous.matches(selector)) { |
| return [previous]; |
| } |
| previous = previous.previousElementSibling; |
| } |
| return []; |
| }, |
| // TODO: this is now unused; remove later along with prev() |
| next(element, selector) { |
| let next = element.nextElementSibling; |
| while (next) { |
| if (next.matches(selector)) { |
| return [next]; |
| } |
| next = next.nextElementSibling; |
| } |
| return []; |
| }, |
| focusableChildren(element) { |
| const focusables = ['a', 'button', 'input', 'textarea', 'select', 'details', '[tabindex]', '[contenteditable="true"]'].map(selector => `${selector}:not([tabindex^="-"])`).join(','); |
| return this.find(focusables, element).filter(el => !isDisabled(el) && isVisible(el)); |
| }, |
| getSelectorFromElement(element) { |
| const selector = getSelector(element); |
| if (selector) { |
| return SelectorEngine.findOne(selector) ? selector : null; |
| } |
| return null; |
| }, |
| getElementFromSelector(element) { |
| const selector = getSelector(element); |
| return selector ? SelectorEngine.findOne(selector) : null; |
| }, |
| getMultipleElementsFromSelector(element) { |
| const selector = getSelector(element); |
| return selector ? SelectorEngine.find(selector) : []; |
| } |
| }; |
| |
| /** |
| * -------------------------------------------------------------------------- |
| * Bootstrap util/component-functions.js |
| * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) |
| * -------------------------------------------------------------------------- |
| */ |
| |
| const enableDismissTrigger = (component, method = 'hide') => { |
| const clickEvent = `click.dismiss${component.EVENT_KEY}`; |
| const name = component.NAME; |
| EventHandler.on(document, clickEvent, `[data-bs-dismiss="${name}"]`, function (event) { |
| if (['A', 'AREA'].includes(this.tagName)) { |
| event.preventDefault(); |
| } |
| if (isDisabled(this)) { |
| return; |
| } |
| const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`); |
| const instance = component.getOrCreateInstance(target); |
| |
| // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method |
| instance[method](); |
| }); |
| }; |
| |
| /** |
| * -------------------------------------------------------------------------- |
| * Bootstrap tab.js |
| * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) |
| * -------------------------------------------------------------------------- |
| */ |
| |
| |
| /** |
| * Constants |
| */ |
| |
| const NAME$1 = 'tab'; |
| const DATA_KEY$1 = 'bs.tab'; |
| const EVENT_KEY$1 = `.${DATA_KEY$1}`; |
| const EVENT_HIDE$1 = `hide${EVENT_KEY$1}`; |
| const EVENT_HIDDEN$1 = `hidden${EVENT_KEY$1}`; |
| const EVENT_SHOW$1 = `show${EVENT_KEY$1}`; |
| const EVENT_SHOWN$1 = `shown${EVENT_KEY$1}`; |
| const EVENT_CLICK_DATA_API = `click${EVENT_KEY$1}`; |
| const EVENT_KEYDOWN = `keydown${EVENT_KEY$1}`; |
| const EVENT_LOAD_DATA_API = `load${EVENT_KEY$1}`; |
| const ARROW_LEFT_KEY = 'ArrowLeft'; |
| const ARROW_RIGHT_KEY = 'ArrowRight'; |
| const ARROW_UP_KEY = 'ArrowUp'; |
| const ARROW_DOWN_KEY = 'ArrowDown'; |
| const HOME_KEY = 'Home'; |
| const END_KEY = 'End'; |
| const CLASS_NAME_ACTIVE = 'active'; |
| const CLASS_NAME_FADE$1 = 'fade'; |
| const CLASS_NAME_SHOW$1 = 'show'; |
| const CLASS_DROPDOWN = 'dropdown'; |
| const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'; |
| const SELECTOR_DROPDOWN_MENU = '.dropdown-menu'; |
| const NOT_SELECTOR_DROPDOWN_TOGGLE = `:not(${SELECTOR_DROPDOWN_TOGGLE})`; |
| const SELECTOR_TAB_PANEL = '.list-group, .nav, [role="tablist"]'; |
| const SELECTOR_OUTER = '.nav-item, .list-group-item'; |
| const SELECTOR_INNER = `.nav-link${NOT_SELECTOR_DROPDOWN_TOGGLE}, .list-group-item${NOT_SELECTOR_DROPDOWN_TOGGLE}, [role="tab"]${NOT_SELECTOR_DROPDOWN_TOGGLE}`; |
| const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]'; // TODO: could only be `tab` in v6 |
| const SELECTOR_INNER_ELEM = `${SELECTOR_INNER}, ${SELECTOR_DATA_TOGGLE}`; |
| const SELECTOR_DATA_TOGGLE_ACTIVE = `.${CLASS_NAME_ACTIVE}[data-bs-toggle="tab"], .${CLASS_NAME_ACTIVE}[data-bs-toggle="pill"], .${CLASS_NAME_ACTIVE}[data-bs-toggle="list"]`; |
| |
| /** |
| * Class definition |
| */ |
| |
| class Tab extends BaseComponent { |
| constructor(element) { |
| super(element); |
| this._parent = this._element.closest(SELECTOR_TAB_PANEL); |
| if (!this._parent) { |
| return; |
| // TODO: should throw exception in v6 |
| // throw new TypeError(`${element.outerHTML} has not a valid parent ${SELECTOR_INNER_ELEM}`) |
| } |
| |
| // Set up initial aria attributes |
| this._setInitialAttributes(this._parent, this._getChildren()); |
| EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event)); |
| } |
| |
| // Getters |
| static get NAME() { |
| return NAME$1; |
| } |
| |
| // Public |
| show() { |
| // Shows this elem and deactivate the active sibling if exists |
| const innerElem = this._element; |
| if (this._elemIsActive(innerElem)) { |
| return; |
| } |
| |
| // Search for active tab on same parent to deactivate it |
| const active = this._getActiveElem(); |
| const hideEvent = active ? EventHandler.trigger(active, EVENT_HIDE$1, { |
| relatedTarget: innerElem |
| }) : null; |
| const showEvent = EventHandler.trigger(innerElem, EVENT_SHOW$1, { |
| relatedTarget: active |
| }); |
| if (showEvent.defaultPrevented || hideEvent && hideEvent.defaultPrevented) { |
| return; |
| } |
| this._deactivate(active, innerElem); |
| this._activate(innerElem, active); |
| } |
| |
| // Private |
| _activate(element, relatedElem) { |
| if (!element) { |
| return; |
| } |
| element.classList.add(CLASS_NAME_ACTIVE); |
| this._activate(SelectorEngine.getElementFromSelector(element)); // Search and activate/show the proper section |
| |
| const complete = () => { |
| if (element.getAttribute('role') !== 'tab') { |
| element.classList.add(CLASS_NAME_SHOW$1); |
| return; |
| } |
| element.removeAttribute('tabindex'); |
| element.setAttribute('aria-selected', true); |
| this._toggleDropDown(element, true); |
| EventHandler.trigger(element, EVENT_SHOWN$1, { |
| relatedTarget: relatedElem |
| }); |
| }; |
| this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE$1)); |
| } |
| _deactivate(element, relatedElem) { |
| if (!element) { |
| return; |
| } |
| element.classList.remove(CLASS_NAME_ACTIVE); |
| element.blur(); |
| this._deactivate(SelectorEngine.getElementFromSelector(element)); // Search and deactivate the shown section too |
| |
| const complete = () => { |
| if (element.getAttribute('role') !== 'tab') { |
| element.classList.remove(CLASS_NAME_SHOW$1); |
| return; |
| } |
| element.setAttribute('aria-selected', false); |
| element.setAttribute('tabindex', '-1'); |
| this._toggleDropDown(element, false); |
| EventHandler.trigger(element, EVENT_HIDDEN$1, { |
| relatedTarget: relatedElem |
| }); |
| }; |
| this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE$1)); |
| } |
| _keydown(event) { |
| if (![ARROW_LEFT_KEY, ARROW_RIGHT_KEY, ARROW_UP_KEY, ARROW_DOWN_KEY, HOME_KEY, END_KEY].includes(event.key)) { |
| return; |
| } |
| event.stopPropagation(); // stopPropagation/preventDefault both added to support up/down keys without scrolling the page |
| event.preventDefault(); |
| const children = this._getChildren().filter(element => !isDisabled(element)); |
| let nextActiveElement; |
| if ([HOME_KEY, END_KEY].includes(event.key)) { |
| nextActiveElement = children[event.key === HOME_KEY ? 0 : children.length - 1]; |
| } else { |
| const isNext = [ARROW_RIGHT_KEY, ARROW_DOWN_KEY].includes(event.key); |
| nextActiveElement = getNextActiveElement(children, event.target, isNext, true); |
| } |
| if (nextActiveElement) { |
| nextActiveElement.focus({ |
| preventScroll: true |
| }); |
| Tab.getOrCreateInstance(nextActiveElement).show(); |
| } |
| } |
| _getChildren() { |
| // collection of inner elements |
| return SelectorEngine.find(SELECTOR_INNER_ELEM, this._parent); |
| } |
| _getActiveElem() { |
| return this._getChildren().find(child => this._elemIsActive(child)) || null; |
| } |
| _setInitialAttributes(parent, children) { |
| this._setAttributeIfNotExists(parent, 'role', 'tablist'); |
| for (const child of children) { |
| this._setInitialAttributesOnChild(child); |
| } |
| } |
| _setInitialAttributesOnChild(child) { |
| child = this._getInnerElement(child); |
| const isActive = this._elemIsActive(child); |
| const outerElem = this._getOuterElement(child); |
| child.setAttribute('aria-selected', isActive); |
| if (outerElem !== child) { |
| this._setAttributeIfNotExists(outerElem, 'role', 'presentation'); |
| } |
| if (!isActive) { |
| child.setAttribute('tabindex', '-1'); |
| } |
| this._setAttributeIfNotExists(child, 'role', 'tab'); |
| |
| // set attributes to the related panel too |
| this._setInitialAttributesOnTargetPanel(child); |
| } |
| _setInitialAttributesOnTargetPanel(child) { |
| const target = SelectorEngine.getElementFromSelector(child); |
| if (!target) { |
| return; |
| } |
| this._setAttributeIfNotExists(target, 'role', 'tabpanel'); |
| if (child.id) { |
| this._setAttributeIfNotExists(target, 'aria-labelledby', `${child.id}`); |
| } |
| } |
| _toggleDropDown(element, open) { |
| const outerElem = this._getOuterElement(element); |
| if (!outerElem.classList.contains(CLASS_DROPDOWN)) { |
| return; |
| } |
| const toggle = (selector, className) => { |
| const element = SelectorEngine.findOne(selector, outerElem); |
| if (element) { |
| element.classList.toggle(className, open); |
| } |
| }; |
| toggle(SELECTOR_DROPDOWN_TOGGLE, CLASS_NAME_ACTIVE); |
| toggle(SELECTOR_DROPDOWN_MENU, CLASS_NAME_SHOW$1); |
| outerElem.setAttribute('aria-expanded', open); |
| } |
| _setAttributeIfNotExists(element, attribute, value) { |
| if (!element.hasAttribute(attribute)) { |
| element.setAttribute(attribute, value); |
| } |
| } |
| _elemIsActive(elem) { |
| return elem.classList.contains(CLASS_NAME_ACTIVE); |
| } |
| |
| // Try to get the inner element (usually the .nav-link) |
| _getInnerElement(elem) { |
| return elem.matches(SELECTOR_INNER_ELEM) ? elem : SelectorEngine.findOne(SELECTOR_INNER_ELEM, elem); |
| } |
| |
| // Try to get the outer element (usually the .nav-item) |
| _getOuterElement(elem) { |
| return elem.closest(SELECTOR_OUTER) || elem; |
| } |
| |
| // Static |
| static jQueryInterface(config) { |
| return this.each(function () { |
| const data = Tab.getOrCreateInstance(this); |
| if (typeof config !== 'string') { |
| return; |
| } |
| if (data[config] === undefined || config.startsWith('_') || config === 'constructor') { |
| throw new TypeError(`No method named "${config}"`); |
| } |
| data[config](); |
| }); |
| } |
| } |
| |
| /** |
| * Data API implementation |
| */ |
| |
| EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { |
| if (['A', 'AREA'].includes(this.tagName)) { |
| event.preventDefault(); |
| } |
| if (isDisabled(this)) { |
| return; |
| } |
| Tab.getOrCreateInstance(this).show(); |
| }); |
| |
| /** |
| * Initialize on focus |
| */ |
| EventHandler.on(window, EVENT_LOAD_DATA_API, () => { |
| for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE_ACTIVE)) { |
| Tab.getOrCreateInstance(element); |
| } |
| }); |
| |
| /** |
| * -------------------------------------------------------------------------- |
| * Bootstrap index.umd.js |
| * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) |
| * -------------------------------------------------------------------------- |
| */ |
| |
| const index_umd = { |
| Tab |
| }; |
| |
| return index_umd; |
| |
| })); |