ngx-bootstrap
Version:
Angular Bootstrap
485 lines (476 loc) • 26.6 kB
JavaScript
import * as i0 from '@angular/core';
import { input, effect, Directive, Injectable, HostBinding, Component, output, NgModule } from '@angular/core';
import { NgClass, CommonModule } from '@angular/common';
class NgTranscludeDirective {
constructor(viewRef) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.ngTransclude = input(...(ngDevMode ? [undefined, { debugName: "ngTransclude" }] : []));
this.viewRef = viewRef;
effect(() => {
const templateRef = this.ngTransclude();
this._ngTransclude = templateRef;
if (templateRef) {
this.viewRef.createEmbeddedView(templateRef);
}
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: NgTranscludeDirective, deps: [{ token: i0.ViewContainerRef }], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.0", type: NgTranscludeDirective, isStandalone: true, selector: "[ngTransclude]", inputs: { ngTransclude: { classPropertyName: "ngTransclude", publicName: "ngTransclude", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: NgTranscludeDirective, decorators: [{
type: Directive,
args: [{
selector: '[ngTransclude]',
standalone: true
}]
}], ctorParameters: () => [{ type: i0.ViewContainerRef }], propDecorators: { ngTransclude: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngTransclude", required: false }] }] } });
class TabsetConfig {
constructor() {
/** provides default navigation context class: 'tabs' or 'pills' */
this.type = 'tabs';
/** provides possibility to set keyNavigations enable or disable, by default is enable */
this.isKeysAllowed = true;
/** aria label for tab list */
this.ariaLabel = 'Tabs';
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TabsetConfig, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TabsetConfig, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TabsetConfig, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}] });
// todo: add active event to tab
// todo: fix? mixing static and dynamic tabs position tabs in order of creation
class TabsetComponent {
get isKeysAllowed() {
return this._isKeysAllowed;
}
set isKeysAllowed(value) {
this._isKeysAllowed = value;
}
constructor(_config, renderer, elementRef) {
this._config = _config;
this.renderer = renderer;
this.elementRef = elementRef;
/** if true tabs will be placed vertically */
this.vertical = input(false, ...(ngDevMode ? [{ debugName: "vertical" }] : []));
/** if true tabs fill the container and have a consistent width */
this.justified = input(false, ...(ngDevMode ? [{ debugName: "justified" }] : []));
/** navigation context class: 'tabs' or 'pills' */
this.type = input(this._config.type, ...(ngDevMode ? [{ debugName: "type" }] : []));
this.clazz = true;
this.tabs = [];
this.classMap = {};
/** aria label for tab list */
this.ariaLabel = 'Tabs';
this.isDestroyed = false;
this._isKeysAllowed = true;
this.defaultActivationScheduled = false;
this._isKeysAllowed = _config.isKeysAllowed;
this.ariaLabel = _config.ariaLabel;
// Watch for input changes and update class map
effect(() => {
const _ = [this.vertical(), this.justified(), this.type()];
this.setClassMap();
});
}
ngOnDestroy() {
this.isDestroyed = true;
}
addTab(tab) {
// If tab has a tabOrder, insert it in the correct position
if (tab.tabOrder !== undefined) {
this.insertTabByOrder(tab);
}
else {
// Default behavior - add to end
this.tabs.push(tab);
}
// Activation logic
// - If the newly added tab is already active, set it active again to leverage
// TabDirective's setter which will deactivate others.
// - Otherwise, schedule a single deferred default-first activation if none active.
if (tab.active) {
tab.active = true;
return;
}
if (!this.defaultActivationScheduled) {
this.defaultActivationScheduled = true;
// Defer default activation to avoid racing template-driven [active] inputs
Promise.resolve().then(() => {
this.defaultActivationScheduled = false;
// Guard in case the tabset changed meanwhile
if (!this.tabs.length) {
return;
}
if (this.tabs.some((t) => !!t.active)) {
return;
}
const firstEnabled = this.tabs.find((t) => !t.disabled);
if (firstEnabled) {
firstEnabled.active = true;
}
});
}
}
insertTabByOrder(tab) {
let insertIndex = this.tabs.length; // Default to end
// Find the correct position to insert the ordered tab
for (let i = 0; i < this.tabs.length; i++) {
const existingTab = this.tabs[i];
// If the existing tab has an order and the new tab's order is less than it
if (existingTab.tabOrder !== undefined &&
tab.tabOrder !== undefined &&
tab.tabOrder < existingTab.tabOrder) {
insertIndex = i;
break;
}
// If we reach an unordered tab, we want to insert before it
// (ordered tabs should come before unordered tabs)
if (existingTab.tabOrder === undefined) {
insertIndex = i;
break;
}
}
// Insert at the found position
this.tabs.splice(insertIndex, 0, tab);
}
removeTab(tab, options = { reselect: true, emit: true }) {
const index = this.tabs.indexOf(tab);
if (index === -1 || this.isDestroyed) {
return;
}
// Select a new tab if the tab to be removed is selected and not destroyed
if (options.reselect && tab.active && this.hasAvailableTabs(index)) {
const newActiveIndex = this.getClosestTabIndex(index);
this.tabs[newActiveIndex].active = true;
}
if (options.emit) {
tab.removed.emit(tab);
}
this.tabs.splice(index, 1);
if (tab.elementRef.nativeElement.parentNode) {
this.renderer.removeChild(tab.elementRef.nativeElement.parentNode, tab.elementRef.nativeElement);
}
}
keyNavActions(event, index) {
if (!this.isKeysAllowed) {
return;
}
const list = Array.from(this.elementRef.nativeElement.querySelectorAll('.nav-link'));
// const activeElList = list.filter((el: HTMLElement) => !el.classList.contains('disabled'));
if (event.keyCode === 13 || event.key === 'Enter' || event.keyCode === 32 || event.key === 'Space') {
event.preventDefault();
const currentTab = list[(index) % list.length];
currentTab.click();
return;
}
if (event.keyCode === 39 || event.key === 'RightArrow') {
let nextTab;
let shift = 1;
do {
nextTab = list[(index + shift) % list.length];
shift++;
} while (nextTab.classList.contains('disabled'));
nextTab.focus();
return;
}
if (event.keyCode === 37 || event.key === 'LeftArrow') {
let previousTab;
let shift = 1;
let i = index;
do {
if ((i - shift) < 0) {
i = list.length - 1;
previousTab = list[i];
shift = 0;
}
else {
previousTab = list[i - shift];
}
shift++;
} while (previousTab.classList.contains('disabled'));
previousTab.focus();
return;
}
if (event.keyCode === 36 || event.key === 'Home') {
event.preventDefault();
let firstTab;
let shift = 0;
do {
firstTab = list[shift % list.length];
shift++;
} while (firstTab.classList.contains('disabled'));
firstTab.focus();
return;
}
if (event.keyCode === 35 || event.key === 'End') {
event.preventDefault();
let lastTab;
let shift = 1;
let i = index;
do {
if ((i - shift) < 0) {
i = list.length - 1;
lastTab = list[i];
shift = 0;
}
else {
lastTab = list[i - shift];
}
shift++;
} while (lastTab.classList.contains('disabled'));
lastTab.focus();
return;
}
if (event.keyCode === 46 || event.key === 'Delete') {
if (this.tabs[index].removable) {
this.removeTab(this.tabs[index]);
if (list[index + 1]) {
list[(index + 1) % list.length].focus();
return;
}
if (list[list.length - 1]) {
list[0].focus();
}
}
}
}
getClosestTabIndex(index) {
const tabsLength = this.tabs.length;
if (!tabsLength) {
return -1;
}
for (let step = 1; step <= tabsLength; step += 1) {
const prevIndex = index - step;
const nextIndex = index + step;
if (this.tabs[prevIndex] && !this.tabs[prevIndex].disabled) {
return prevIndex;
}
if (this.tabs[nextIndex] && !this.tabs[nextIndex].disabled) {
return nextIndex;
}
}
return -1;
}
hasAvailableTabs(index) {
const tabsLength = this.tabs.length;
if (!tabsLength) {
return false;
}
for (let i = 0; i < tabsLength; i += 1) {
if (!this.tabs[i].disabled && i !== index) {
return true;
}
}
return false;
}
setClassMap() {
this.classMap = {
'nav-stacked': this.vertical(),
'flex-column': this.vertical(),
'nav-justified': this.justified(),
[`nav-${this.type()}`]: true
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TabsetComponent, deps: [{ token: TabsetConfig }, { token: i0.Renderer2 }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: TabsetComponent, isStandalone: true, selector: "tabset", inputs: { vertical: { classPropertyName: "vertical", publicName: "vertical", isSignal: true, isRequired: false, transformFunction: null }, justified: { classPropertyName: "justified", publicName: "justified", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.tab-container": "this.clazz" } }, ngImport: i0, template: "<ul class=\"nav\" [ngClass]=\"classMap\"\n (click)=\"$event.preventDefault()\"\n [attr.aria-label]=\"ariaLabel\"\n role=\"tablist\">\n @for (tabz of tabs; track tabz; let i = $index) {\n <li [ngClass]=\"['nav-item', tabz._customClass || '']\"\n [class.active]=\"tabz.active\" [class.disabled]=\"tabz.disabled\" (keydown)=\"keyNavActions($event, i)\">\n <a href=\"javascript:void(0);\" class=\"nav-link\" role=\"tab\"\n [attr.aria-controls]=\"tabz.id ? tabz.id : ''\"\n [attr.aria-selected]=\"!!tabz.active\"\n [attr.id]=\"tabz.id ? tabz.id + '-link' : ''\"\n [class.active]=\"tabz.active\" [class.disabled]=\"tabz.disabled\"\n (click)=\"tabz.active = true\">\n <span [ngTransclude]=\"tabz.headingRef\">{{ tabz.heading() }}</span>\n @if (tabz.removable) {\n <span (click)=\"$event.preventDefault(); removeTab(tabz);\" class=\"bs-remove-tab\"> ❌</span>\n }\n </a>\n </li>\n }\n</ul>\n<div class=\"tab-content\">\n <ng-content></ng-content>\n</div>\n", styles: [":host .nav-tabs .nav-item.disabled a.disabled{cursor:default}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTranscludeDirective, selector: "[ngTransclude]", inputs: ["ngTransclude"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TabsetComponent, decorators: [{
type: Component,
args: [{ selector: 'tabset', standalone: true, imports: [NgClass, NgTranscludeDirective], template: "<ul class=\"nav\" [ngClass]=\"classMap\"\n (click)=\"$event.preventDefault()\"\n [attr.aria-label]=\"ariaLabel\"\n role=\"tablist\">\n @for (tabz of tabs; track tabz; let i = $index) {\n <li [ngClass]=\"['nav-item', tabz._customClass || '']\"\n [class.active]=\"tabz.active\" [class.disabled]=\"tabz.disabled\" (keydown)=\"keyNavActions($event, i)\">\n <a href=\"javascript:void(0);\" class=\"nav-link\" role=\"tab\"\n [attr.aria-controls]=\"tabz.id ? tabz.id : ''\"\n [attr.aria-selected]=\"!!tabz.active\"\n [attr.id]=\"tabz.id ? tabz.id + '-link' : ''\"\n [class.active]=\"tabz.active\" [class.disabled]=\"tabz.disabled\"\n (click)=\"tabz.active = true\">\n <span [ngTransclude]=\"tabz.headingRef\">{{ tabz.heading() }}</span>\n @if (tabz.removable) {\n <span (click)=\"$event.preventDefault(); removeTab(tabz);\" class=\"bs-remove-tab\"> ❌</span>\n }\n </a>\n </li>\n }\n</ul>\n<div class=\"tab-content\">\n <ng-content></ng-content>\n</div>\n", styles: [":host .nav-tabs .nav-item.disabled a.disabled{cursor:default}\n"] }]
}], ctorParameters: () => [{ type: TabsetConfig }, { type: i0.Renderer2 }, { type: i0.ElementRef }], propDecorators: { vertical: [{ type: i0.Input, args: [{ isSignal: true, alias: "vertical", required: false }] }], justified: [{ type: i0.Input, args: [{ isSignal: true, alias: "justified", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], clazz: [{
type: HostBinding,
args: ['class.tab-container']
}] } });
class TabDirective {
/** tab active state toggle */
get active() {
return this._active;
}
set active(active) {
if (this._active === active) {
return;
}
if ((this.disabled && active) || !active) {
if (this._active && !active) {
this.deselect.emit(this);
this._active = active;
this._cdr.markForCheck();
}
return;
}
this._active = active;
this._cdr.markForCheck();
this.selectTab.emit(this);
this.tabset.tabs.forEach((tab) => {
if (tab !== this) {
tab.active = false;
}
});
}
get ariaLabelledby() {
return this.id ? `${this.id}-link` : '';
}
constructor(tabset, elementRef, renderer, _cdr) {
this.elementRef = elementRef;
this.renderer = renderer;
this._cdr = _cdr;
/** tab header text */
this.heading = input(...(ngDevMode ? [undefined, { debugName: "heading" }] : []));
// eslint-disable-next-line @angular-eslint/no-input-rename
this.idInput = input(undefined, { ...(ngDevMode ? { debugName: "idInput" } : {}), alias: 'id' });
/** if true tab can not be activated */
this.disabled = false;
// eslint-disable-next-line @angular-eslint/no-input-rename
this.disabledInput = input(false, { ...(ngDevMode ? { debugName: "disabledInput" } : {}), alias: 'disabled' });
/** if true tab can be removable, additional button will appear */
this.removable = false;
// eslint-disable-next-line @angular-eslint/no-input-rename
this.removableInput = input(false, { ...(ngDevMode ? { debugName: "removableInput" } : {}), alias: 'removable' });
// eslint-disable-next-line @angular-eslint/no-input-rename
this.tabOrderInput = input(undefined, { ...(ngDevMode ? { debugName: "tabOrderInput" } : {}), alias: 'tabOrder' });
/** if set, will be added to the tab's class attribute. Multiple classes are supported. */
// eslint-disable-next-line @angular-eslint/no-input-rename
this.customClassInput = input(undefined, { ...(ngDevMode ? { debugName: "customClassInput" } : {}), alias: 'customClass' });
/** tab active state - can be set via input */
// eslint-disable-next-line @angular-eslint/no-input-rename
this.activeInput = input(undefined, { ...(ngDevMode ? { debugName: "activeInput" } : {}), alias: 'active' });
/** fired when tab became active, $event:Tab equals to selected instance of Tab component */
this.selectTab = output();
/** fired when tab became inactive, $event:Tab equals to deselected instance of Tab component */
this.deselect = output();
/** fired before tab will be removed, $event:Tab equals to instance of removed tab */
this.removed = output();
this.addClass = true;
this.role = 'tabpanel';
this._active = false;
this._customClass = '';
this.tabset = tabset;
// Watch for id input changes
effect(() => {
const idValue = this.idInput();
if (idValue !== undefined) {
this.id = idValue;
}
});
// Watch for disabled input changes
effect(() => {
this.disabled = this.disabledInput();
});
// Watch for removable input changes
effect(() => {
this.removable = this.removableInput();
});
// Watch for tabOrder input changes
effect(() => {
this.tabOrder = this.tabOrderInput();
});
// Watch for customClass input changes
effect(() => {
const customClass = this.customClassInput();
if (this._customClass) {
this._customClass.split(' ').forEach((cssClass) => {
this.renderer.removeClass(this.elementRef.nativeElement, cssClass);
});
}
this._customClass = customClass ? customClass.trim() : '';
if (this._customClass) {
this._customClass.split(' ').forEach((cssClass) => {
this.renderer.addClass(this.elementRef.nativeElement, cssClass);
});
}
});
// Watch for active input changes
effect(() => {
const activeValue = this.activeInput();
if (activeValue !== undefined) {
this.active = activeValue;
}
});
}
ngOnInit() {
this.removable = !!this.removableInput();
this.tabOrder = this.tabOrderInput();
this.disabled = this.disabledInput();
// Add tab to tabset after input properties are set
this.tabset.addTab(this);
}
ngOnDestroy() {
this.tabset.removeTab(this, { reselect: false, emit: false });
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TabDirective, deps: [{ token: TabsetComponent }, { token: i0.ElementRef }, { token: i0.Renderer2 }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.0", type: TabDirective, isStandalone: true, selector: "tab, [tab]", inputs: { heading: { classPropertyName: "heading", publicName: "heading", isSignal: true, isRequired: false, transformFunction: null }, idInput: { classPropertyName: "idInput", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, disabledInput: { classPropertyName: "disabledInput", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, removableInput: { classPropertyName: "removableInput", publicName: "removable", isSignal: true, isRequired: false, transformFunction: null }, tabOrderInput: { classPropertyName: "tabOrderInput", publicName: "tabOrder", isSignal: true, isRequired: false, transformFunction: null }, customClassInput: { classPropertyName: "customClassInput", publicName: "customClass", isSignal: true, isRequired: false, transformFunction: null }, activeInput: { classPropertyName: "activeInput", publicName: "active", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectTab: "selectTab", deselect: "deselect", removed: "removed" }, host: { properties: { "attr.id": "this.id", "class.active": "this.active", "class.tab-pane": "this.addClass", "attr.role": "this.role", "attr.aria-labelledby": "this.ariaLabelledby" } }, exportAs: ["tab"], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TabDirective, decorators: [{
type: Directive,
args: [{
selector: 'tab, [tab]', exportAs: 'tab',
standalone: true
}]
}], ctorParameters: () => [{ type: TabsetComponent }, { type: i0.ElementRef }, { type: i0.Renderer2 }, { type: i0.ChangeDetectorRef }], propDecorators: { heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: false }] }], id: [{
type: HostBinding,
args: ['attr.id']
}], idInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], disabledInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], removableInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "removable", required: false }] }], tabOrderInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabOrder", required: false }] }], customClassInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "customClass", required: false }] }], activeInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], active: [{
type: HostBinding,
args: ['class.active']
}], selectTab: [{ type: i0.Output, args: ["selectTab"] }], deselect: [{ type: i0.Output, args: ["deselect"] }], removed: [{ type: i0.Output, args: ["removed"] }], addClass: [{
type: HostBinding,
args: ['class.tab-pane']
}], role: [{
type: HostBinding,
args: ['attr.role']
}], ariaLabelledby: [{
type: HostBinding,
args: ['attr.aria-labelledby']
}] } });
/** Should be used to mark <ng-template> element as a template for tab heading */
class TabHeadingDirective {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(templateRef, tab) {
tab.headingRef = templateRef;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TabHeadingDirective, deps: [{ token: i0.TemplateRef }, { token: TabDirective }], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.0", type: TabHeadingDirective, isStandalone: true, selector: "[tabHeading]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TabHeadingDirective, decorators: [{
type: Directive,
args: [{
selector: '[tabHeading]',
standalone: true
}]
}], ctorParameters: () => [{ type: i0.TemplateRef }, { type: TabDirective }] });
class TabsModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TabsModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.0", ngImport: i0, type: TabsModule, imports: [CommonModule, NgTranscludeDirective,
TabDirective,
TabsetComponent,
TabHeadingDirective], exports: [TabDirective,
TabsetComponent,
TabHeadingDirective,
NgTranscludeDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TabsModule, imports: [CommonModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TabsModule, decorators: [{
type: NgModule,
args: [{
imports: [CommonModule, NgTranscludeDirective,
TabDirective,
TabsetComponent,
TabHeadingDirective],
exports: [
TabDirective,
TabsetComponent,
TabHeadingDirective,
NgTranscludeDirective
]
}]
}] });
/**
* Generated bundle index. Do not edit.
*/
export { NgTranscludeDirective, TabDirective, TabHeadingDirective, TabsModule, TabsetComponent, TabsetConfig };
//# sourceMappingURL=ngx-bootstrap-tabs.mjs.map