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