ngx-bootstrap
Version:
Angular Bootstrap
243 lines (237 loc) • 12.3 kB
JavaScript
import * as i0 from '@angular/core';
import { output, input, effect, HostBinding, Directive, NgModule } from '@angular/core';
import { onTransitionFinished } from 'ngx-bootstrap/utils';
const COLLAPSE_ANIMATION_DURATION_MS = 400;
const COLLAPSE_ANIMATION_TIMING = `${COLLAPSE_ANIMATION_DURATION_MS}ms cubic-bezier(0.4,0.0,0.2,1)`;
class CollapseDirective {
constructor(_el, _renderer) {
this._el = _el;
this._renderer = _renderer;
/** This event fires as soon as content collapses */
this.collapsed = output();
/** This event fires when collapsing is started */
this.collapses = output();
/** This event fires as soon as content becomes visible */
this.expanded = output();
/** This event fires when expansion is started */
this.expands = output();
// shown
this.isExpanded = true;
this.collapseNewValue = true;
// hidden
this.isCollapsed = false;
// stale state
this.isCollapse = true;
// animation state
this.isCollapsing = false;
this.display = input('block', ...(ngDevMode ? [{ debugName: "display" }] : []));
/** turn on/off animation */
this.isAnimated = input(false, ...(ngDevMode ? [{ debugName: "isAnimated" }] : []));
/** A flag indicating visibility of content (shown or hidden) */
this.collapse = input(false, ...(ngDevMode ? [{ debugName: "collapse" }] : []));
this._display = 'block';
this._stylesLoaded = false;
this._isTransitionRunning = false;
this._COLLAPSE_ACTION_NAME = 'collapse';
this._EXPAND_ACTION_NAME = 'expand';
// Watch for display changes
effect(() => {
const displayValue = this.display();
this._display = displayValue;
if (displayValue === 'none') {
this.hide();
return;
}
this.isAnimated() ? this.toggle() : this.show();
});
// Watch for collapse changes
effect(() => {
const collapseValue = this.collapse();
this.collapseNewValue = collapseValue;
if (!this._isTransitionRunning || this._isAnimationDone) {
this.isExpanded = collapseValue;
this.toggle();
}
});
}
ngAfterViewChecked() {
this._stylesLoaded = true;
}
/** allows to manually toggle content visibility */
toggle() {
if (this.isExpanded) {
this.hide();
}
else {
this.show();
}
}
/** allows to manually hide content */
hide() {
this.isCollapsing = true;
this.isExpanded = false;
this.isCollapsed = true;
this.isCollapsing = false;
this.collapses.emit(this);
this._isAnimationDone = false;
this.animationRun(this.isAnimated(), this._COLLAPSE_ACTION_NAME)(() => {
this._isAnimationDone = true;
if (this.collapseNewValue !== this.isCollapsed && this.isAnimated()) {
this.show();
return;
}
this.collapsed.emit(this);
this._renderer.setStyle(this._el.nativeElement, 'display', 'none');
});
}
/** allows to manually show collapsed content */
show() {
this._renderer.setStyle(this._el.nativeElement, 'display', this._display);
this.isCollapsing = true;
this.isExpanded = true;
this.isCollapsed = false;
this.isCollapsing = false;
this.expands.emit(this);
this._isAnimationDone = false;
this.animationRun(this.isAnimated(), this._EXPAND_ACTION_NAME)(() => {
this._isAnimationDone = true;
if (this.collapseNewValue !== this.isCollapsed && this.isAnimated()) {
this.hide();
return;
}
this.expanded.emit(this);
});
}
animationRun(isAnimated, action) {
if (!isAnimated || !this._stylesLoaded) {
return (callback) => callback();
}
const el = this._el.nativeElement;
const isExpand = action === this._EXPAND_ACTION_NAME;
// True when a CSS transition is already mid-flight; we can reverse it by
// snapshotting the current rendered height and flipping the target.
const wasRunning = !!this._pendingFinish;
this._cancelPending();
// Ensure the element is visible before measuring scrollHeight — Bootstrap's
// .collapse:not(.show) { display:none } can win over a missing inline style.
this._renderer.setStyle(el, 'display', this._display);
this._renderer.setStyle(el, 'overflow', 'hidden');
this._renderer.setStyle(el, 'transition', `height ${COLLAPSE_ANIMATION_TIMING}`);
this._isTransitionRunning = true;
return (callback) => {
const finish = () => {
this._cancelPending();
this._isTransitionRunning = false;
if (isExpand) {
// Remove the inline display so Bootstrap classes resume display control.
this._renderer.removeStyle(el, 'display');
}
else {
// Set display:none before removing the height style so the element
// doesn't flash at its natural height for one frame.
this._renderer.setStyle(el, 'display', 'none');
}
this._renderer.removeStyle(el, 'height');
this._renderer.removeStyle(el, 'transition');
this._renderer.removeStyle(el, 'overflow');
callback();
};
// Guards against bubbled child events; falls back to a timeout if
// transitionend never fires (e.g. start == end value).
this._pendingFinish = onTransitionFinished(el, 'height', COLLAPSE_ANIMATION_DURATION_MS, finish);
if (wasRunning) {
// Mid-animation reversal: snapshot the current rendered height, force
// a reflow so the browser registers it as the start state, then flip.
const currentHeight = el.getBoundingClientRect().height;
this._renderer.setStyle(el, 'height', `${currentHeight}px`);
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
el.offsetHeight;
this._renderer.setStyle(el, 'height', isExpand ? `${el.scrollHeight}px` : '0');
}
else if (isExpand) {
// Pin at 0px and force a synchronous layout so the browser registers it
// as the CSS "before-change" style, then defer the target height to the
// next animation frame so Chrome paints the 0px start state first.
this._renderer.setStyle(el, 'height', '0');
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
el.offsetHeight;
this._rafId = requestAnimationFrame(() => {
this._rafId = undefined;
this._renderer.setStyle(el, 'height', `${el.scrollHeight}px`);
});
}
else {
// Two-rAF pattern mirrors the expand direction: the first frame pins
// the element at its natural height so the browser records a concrete
// painted "before" value, then the second frame sets height:0 and the
// browser transitions from the painted state. A single rAF with an
// offsetHeight reflow is not reliable across browsers because the forced
// reflow inside a rAF callback is not always treated as a transition
// "before-change" checkpoint.
this._rafId = requestAnimationFrame(() => {
this._renderer.setStyle(el, 'height', `${el.scrollHeight}px`);
this._rafId = requestAnimationFrame(() => {
this._rafId = undefined;
this._renderer.setStyle(el, 'height', '0');
});
});
}
};
}
ngOnDestroy() {
this._cancelPending();
}
_cancelPending() {
this._pendingFinish?.cancel();
this._pendingFinish = undefined;
if (this._rafId !== undefined) {
cancelAnimationFrame(this._rafId);
this._rafId = undefined;
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CollapseDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.0", type: CollapseDirective, isStandalone: true, selector: "[collapse]", inputs: { display: { classPropertyName: "display", publicName: "display", isSignal: true, isRequired: false, transformFunction: null }, isAnimated: { classPropertyName: "isAnimated", publicName: "isAnimated", isSignal: true, isRequired: false, transformFunction: null }, collapse: { classPropertyName: "collapse", publicName: "collapse", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { collapsed: "collapsed", collapses: "collapses", expanded: "expanded", expands: "expands" }, host: { properties: { "class.collapse": "this.isCollapse", "class.in": "this.isExpanded", "class.show": "this.isExpanded", "attr.aria-hidden": "this.isCollapsed", "class.collapsing": "this.isCollapsing" } }, exportAs: ["bs-collapse"], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CollapseDirective, decorators: [{
type: Directive,
args: [{
selector: '[collapse]',
exportAs: 'bs-collapse',
host: {
'[class.collapse]': 'true'
},
standalone: true
}]
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }], propDecorators: { collapsed: [{ type: i0.Output, args: ["collapsed"] }], collapses: [{ type: i0.Output, args: ["collapses"] }], expanded: [{ type: i0.Output, args: ["expanded"] }], expands: [{ type: i0.Output, args: ["expands"] }], isExpanded: [{
type: HostBinding,
args: ['class.in']
}, {
type: HostBinding,
args: ['class.show']
}], isCollapsed: [{
type: HostBinding,
args: ['attr.aria-hidden']
}], isCollapse: [{
type: HostBinding,
args: ['class.collapse']
}], isCollapsing: [{
type: HostBinding,
args: ['class.collapsing']
}], display: [{ type: i0.Input, args: [{ isSignal: true, alias: "display", required: false }] }], isAnimated: [{ type: i0.Input, args: [{ isSignal: true, alias: "isAnimated", required: false }] }], collapse: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapse", required: false }] }] } });
class CollapseModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CollapseModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.0", ngImport: i0, type: CollapseModule, imports: [CollapseDirective], exports: [CollapseDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CollapseModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CollapseModule, decorators: [{
type: NgModule,
args: [{
imports: [CollapseDirective],
exports: [CollapseDirective]
}]
}] });
/**
* Generated bundle index. Do not edit.
*/
export { CollapseDirective, CollapseModule };
//# sourceMappingURL=ngx-bootstrap-collapse.mjs.map