Files

373 lines
11 KiB
JavaScript

/*!
* Bootstrap Native Tab v5.1.10 (https://thednp.github.io/bootstrap.native/)
* Copyright 2026 © thednp
* Licensed under MIT (https://github.com/thednp/bootstrap.native/blob/master/LICENSE)
*/
"use strict";
import { Data, ObjectKeys, Timer, addClass, ariaSelected, closest, createCustomEvent, dispatchEvent, emulateTransitionEnd, getAttribute, getDocument, getElementsByClassName, getInstance, hasClass, isElement, isHTMLElement, isString, mouseclickEvent, normalizeOptions, querySelector, reflow, removeClass, setAttribute } from "@thednp/shorty";
import { addListener, removeListener } from "@thednp/event-listener";
//#region src/strings/collapsingClass.ts
/**
* Global namespace for most components `collapsing` class.
* As used by `Collapse` / `Tab`.
*/
const collapsingClass = "collapsing";
//#endregion
//#region src/strings/activeClass.ts
/**
* Global namespace for most components active class.
*/
const activeClass = "active";
//#endregion
//#region src/strings/showClass.ts
/**
* Global namespace for most components `show` class.
*/
const showClass = "show";
//#endregion
//#region src/strings/dropdownClasses.ts
/**
* Global namespace for `Dropdown` types / classes.
*/
const dropdownMenuClasses = [
"dropdown",
"dropup",
"dropstart",
"dropend"
];
//#endregion
//#region src/strings/dataBsToggle.ts
/**
* Global namespace for most components `toggle` option.
*/
const dataBsToggle = "data-bs-toggle";
//#endregion
//#region src/strings/dataBsTarget.ts
/**
* Global namespace for most components `target` option.
*/
const dataBsTarget = "data-bs-target";
//#endregion
//#region src/strings/dataBsParent.ts
/**
* Global namespace for most components `parent` option.
*/
const dataBsParent = "data-bs-parent";
//#endregion
//#region src/strings/dataBsContainer.ts
/**
* Global namespace for most components `container` option.
*/
const dataBsContainer = "data-bs-container";
//#endregion
//#region src/util/getTargetElement.ts
/**
* Returns the `Element` that THIS one targets
* via `data-bs-target`, `href`, `data-bs-parent` or `data-bs-container`.
*
* @param element the target element
* @returns the query result
*/
const getTargetElement = (element) => {
const targetAttr = [
dataBsTarget,
dataBsParent,
dataBsContainer,
"href"
];
const doc = getDocument(element);
return targetAttr.map((att) => {
const attValue = getAttribute(element, att);
if (attValue) return att === "data-bs-parent" ? closest(element, attValue) : querySelector(attValue, doc);
return null;
}).filter((x) => x)[0];
};
//#endregion
//#region src/version.ts
const Version = "5.1.10";
//#endregion
//#region src/components/base-component.ts
/** Returns a new `BaseComponent` instance. */
var BaseComponent = class {
/**
* @param target `Element` or selector string
* @param config component instance options
*/
constructor(target, config) {
let element;
try {
if (isElement(target)) element = target;
else if (isString(target)) {
element = querySelector(target);
if (!element) throw Error(`"${target}" is not a valid selector.`);
} else throw Error(`your target is not an instance of HTMLElement.`);
} catch (e) {
throw Error(`${this.name} Error: ${e.message}`);
}
const prevInstance = Data.get(element, this.name);
if (prevInstance) prevInstance._toggleEventListeners();
this.element = element;
this.options = this.defaults && ObjectKeys(this.defaults).length ? normalizeOptions(element, this.defaults, config || {}, "bs") : {};
Data.set(element, this.name, this);
}
get version() {
return Version;
}
get name() {
return "BaseComponent";
}
get defaults() {
return {};
}
/** just to have something to extend from */
_toggleEventListeners = () => {};
/** Removes component from target element. */
dispose() {
Data.remove(this.element, this.name);
ObjectKeys(this).forEach((prop) => {
delete this[prop];
});
}
};
//#endregion
//#region src/components/tab.ts
const tabSelector = `[${dataBsToggle}="tab"]`;
/**
* Static method which returns an existing `Tab` instance associated
* to a target `Element`.
*/
const getTabInstance = (element) => getInstance(element, "Tab");
/** A `Tab` initialization callback. */
const tabInitCallback = (element) => new Tab(element);
const showTabEvent = createCustomEvent(`show.bs.tab`);
const shownTabEvent = createCustomEvent(`shown.bs.tab`);
const hideTabEvent = createCustomEvent(`hide.bs.tab`);
const hiddenTabEvent = createCustomEvent(`hidden.bs.tab`);
/**
* Stores the current active tab and its content
* for a given `.nav` element.
*/
const tabPrivate = /* @__PURE__ */ new Map();
/**
* Executes after tab transition has finished.
*
* @param self the `Tab` instance
*/
const triggerTabEnd = (self) => {
const { tabContent, nav } = self;
if (tabContent && hasClass(tabContent, "collapsing")) {
tabContent.style.height = "";
removeClass(tabContent, collapsingClass);
}
if (nav) Timer.clear(nav);
};
/**
* Executes before showing the tab content.
*
* @param self the `Tab` instance
*/
const triggerTabShow = (self) => {
const { element, tabContent, content: nextContent, nav } = self;
const { tab } = isHTMLElement(nav) && tabPrivate.get(nav) || { tab: null };
if (tabContent && nextContent && hasClass(nextContent, "fade")) {
const { currentHeight, nextHeight } = tabPrivate.get(element) || {
currentHeight: 0,
nextHeight: 0
};
if (currentHeight !== nextHeight) setTimeout(() => {
tabContent.style.height = `${nextHeight}px`;
reflow(tabContent);
emulateTransitionEnd(tabContent, () => triggerTabEnd(self));
}, 50);
else triggerTabEnd(self);
} else if (nav) Timer.clear(nav);
shownTabEvent.relatedTarget = tab;
dispatchEvent(element, shownTabEvent);
};
/**
* Executes before hiding the tab.
*
* @param self the `Tab` instance
*/
const triggerTabHide = (self) => {
const { element, content: nextContent, tabContent, nav } = self;
const { tab, content } = nav && tabPrivate.get(nav) || {
tab: null,
content: null
};
let currentHeight = 0;
if (tabContent && nextContent && hasClass(nextContent, "fade")) {
[content, nextContent].forEach((c) => {
if (c) addClass(c, "overflow-hidden");
});
currentHeight = content ? content.scrollHeight : 0;
}
showTabEvent.relatedTarget = tab;
hiddenTabEvent.relatedTarget = element;
dispatchEvent(element, showTabEvent);
if (showTabEvent.defaultPrevented) return;
if (nextContent) addClass(nextContent, activeClass);
if (content) removeClass(content, activeClass);
if (tabContent && nextContent && hasClass(nextContent, "fade")) {
const nextHeight = nextContent.scrollHeight;
tabPrivate.set(element, {
currentHeight,
nextHeight,
tab: null,
content: null
});
addClass(tabContent, collapsingClass);
tabContent.style.height = `${currentHeight}px`;
reflow(tabContent);
[content, nextContent].forEach((c) => {
if (c) removeClass(c, "overflow-hidden");
});
}
if (nextContent && nextContent && hasClass(nextContent, "fade")) setTimeout(() => {
addClass(nextContent, showClass);
emulateTransitionEnd(nextContent, () => {
triggerTabShow(self);
});
}, 1);
else {
if (nextContent) addClass(nextContent, showClass);
triggerTabShow(self);
}
if (tab) dispatchEvent(tab, hiddenTabEvent);
};
/**
* Returns the current active tab and its target content.
*
* @param self the `Tab` instance
* @returns the query result
*/
const getActiveTab = (self) => {
const { nav } = self;
if (!isHTMLElement(nav)) return {
tab: null,
content: null
};
const activeTabs = getElementsByClassName(activeClass, nav);
let tab = null;
if (activeTabs.length === 1 && !dropdownMenuClasses.some((c) => hasClass(activeTabs[0].parentElement, c))) [tab] = activeTabs;
else if (activeTabs.length > 1) tab = activeTabs[activeTabs.length - 1];
const content = isHTMLElement(tab) ? getTargetElement(tab) : null;
return {
tab,
content
};
};
/**
* Returns a parent dropdown.
*
* @param element the `Tab` element
* @returns the parent dropdown
*/
const getParentDropdown = (element) => {
if (!isHTMLElement(element)) return null;
const dropdown = closest(element, `.${dropdownMenuClasses.join(",.")}`);
return dropdown ? querySelector(`.${dropdownMenuClasses[0]}-toggle`, dropdown) : null;
};
/**
* Handles the `click` event listener.
*
* @param e the `Event` object
*/
const tabClickHandler = (e) => {
const element = closest(e.target, tabSelector);
const self = element && getTabInstance(element);
if (!self) return;
e.preventDefault();
self.show();
};
/** Creates a new `Tab` instance. */
var Tab = class extends BaseComponent {
static selector = tabSelector;
static init = tabInitCallback;
static getInstance = getTabInstance;
/** @param target the target element */
constructor(target) {
super(target);
const { element } = this;
const content = getTargetElement(element);
if (!content) return;
const nav = closest(element, ".nav");
const container = closest(content, ".tab-content");
this.nav = nav;
this.content = content;
this.tabContent = container;
this.dropdown = getParentDropdown(element);
const { tab } = getActiveTab(this);
if (nav && !tab) {
const firstTab = querySelector(tabSelector, nav);
const firstTabContent = firstTab && getTargetElement(firstTab);
if (firstTabContent) {
addClass(firstTab, activeClass);
addClass(firstTabContent, showClass);
addClass(firstTabContent, activeClass);
setAttribute(element, ariaSelected, "true");
}
}
this._toggleEventListeners(true);
}
/**
* Returns component name string.
*/
get name() {
return "Tab";
}
/** Shows the tab to the user. */
show() {
const { element, content: nextContent, nav, dropdown } = this;
if (nav && Timer.get(nav) || hasClass(element, "active")) return;
const { tab, content } = getActiveTab(this);
if (nav && tab) tabPrivate.set(nav, {
tab,
content,
currentHeight: 0,
nextHeight: 0
});
hideTabEvent.relatedTarget = element;
if (!isHTMLElement(tab)) return;
dispatchEvent(tab, hideTabEvent);
if (hideTabEvent.defaultPrevented) return;
addClass(element, activeClass);
setAttribute(element, ariaSelected, "true");
const activeDropdown = isHTMLElement(tab) && getParentDropdown(tab);
if (activeDropdown && hasClass(activeDropdown, "active")) removeClass(activeDropdown, activeClass);
if (nav) {
const toggleTab = () => {
if (tab) {
removeClass(tab, activeClass);
setAttribute(tab, ariaSelected, "false");
}
if (dropdown && !hasClass(dropdown, "active")) addClass(dropdown, activeClass);
};
if (content && (hasClass(content, "fade") || nextContent && hasClass(nextContent, "fade"))) Timer.set(nav, toggleTab, 1);
else toggleTab();
}
if (content) {
removeClass(content, showClass);
if (hasClass(content, "fade")) emulateTransitionEnd(content, () => triggerTabHide(this));
else triggerTabHide(this);
}
}
/**
* Toggles on/off the `click` event listener.
*
* @param add when `true`, event listener is added
*/
_toggleEventListeners = (add) => {
(add ? addListener : removeListener)(this.element, mouseclickEvent, tabClickHandler);
};
/** Removes the `Tab` component from the target element. */
dispose() {
this._toggleEventListeners();
super.dispose();
}
};
//#endregion
export { Tab as default };
//# sourceMappingURL=tab.mjs.map