UNPKG

ngx-bootstrap

Version:
243 lines (237 loc) 12.3 kB
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