import { ChangeDetectorRef, Directive, ElementRef, EventEmitter, HostBinding, HostListener, Inject, Input, OnDestroy, OnInit, Optional, Output } from '@angular/core'; import { AbsoluteScrollStrategy } from '../../services/overlay/scroll/absolute-scroll-strategy'; import { CancelableBrowserEventArgs, IBaseEventArgs } from '../../core/utils'; import { ConnectedPositioningStrategy } from '../../services/overlay/position/connected-positioning-strategy'; import { filter, first, takeUntil } from 'rxjs/operators'; import { IgxNavigationService, IToggleView } from '../../core/navigation'; import { IgxOverlayService } from '../../services/overlay/overlay'; import { IPositionStrategy } from '../../services/overlay/position/IPositionStrategy'; import { OverlayClosingEventArgs, OverlayEventArgs, OverlaySettings } from '../../services/overlay/utilities'; import { Subscription, Subject, MonoTypeOperatorFunction } from 'rxjs'; export interface ToggleViewEventArgs extends IBaseEventArgs { /** Id of the toggle view */ id: string; event?: Event; } export interface ToggleViewCancelableEventArgs extends ToggleViewEventArgs, CancelableBrowserEventArgs { } @Directive({ exportAs: 'toggle', selector: '[igxToggle]', standalone: true }) export class IgxToggleDirective implements IToggleView, OnInit, OnDestroy { /** * Emits an event after the toggle container is opened. * * ```typescript * onToggleOpened(event) { * alert("Toggle opened!"); * } * ``` * * ```html *
*
* ``` */ @Output() public opened = new EventEmitter(); /** * Emits an event before the toggle container is opened. * * ```typescript * onToggleOpening(event) { * alert("Toggle opening!"); * } * ``` * * ```html *
*
* ``` */ @Output() public opening = new EventEmitter(); /** * Emits an event after the toggle container is closed. * * ```typescript * onToggleClosed(event) { * alert("Toggle closed!"); * } * ``` * * ```html *
*
* ``` */ @Output() public closed = new EventEmitter(); /** * Emits an event before the toggle container is closed. * * ```typescript * onToggleClosing(event) { * alert("Toggle closing!"); * } * ``` * * ```html *
*
* ``` */ @Output() public closing = new EventEmitter(); /** * Emits an event after the toggle element is appended to the overlay container. * * ```typescript * onAppended() { * alert("Content appended!"); * } * ``` * * ```html *
*
* ``` */ @Output() public appended = new EventEmitter(); /** * @hidden */ public get collapsed(): boolean { return this._collapsed; } /** * Identifier which is registered into `IgxNavigationService` * * ```typescript * let myToggleId = this.toggle.id; * ``` */ @Input() public id: string; /** * @hidden */ public get element(): HTMLElement { return this.elementRef.nativeElement; } /** * @hidden */ @HostBinding('class.igx-toggle--hidden') @HostBinding('attr.aria-hidden') public get hiddenClass() { return this.collapsed; } /** * @hidden */ @HostBinding('class.igx-toggle') public get defaultClass() { return !this.collapsed; } protected _overlayId: string; private _collapsed = true; protected destroy$ = new Subject(); private _overlaySubFilter: [MonoTypeOperatorFunction, MonoTypeOperatorFunction] = [ filter(x => x.id === this._overlayId), takeUntil(this.destroy$) ]; private _overlayOpenedSub: Subscription; private _overlayClosingSub: Subscription; private _overlayClosedSub: Subscription; private _overlayContentAppendedSub: Subscription; /** * @hidden */ constructor( private elementRef: ElementRef, private cdr: ChangeDetectorRef, @Inject(IgxOverlayService) protected overlayService: IgxOverlayService, @Optional() private navigationService: IgxNavigationService) { } /** * Opens the toggle. * * ```typescript * this.myToggle.open(); * ``` */ public open(overlaySettings?: OverlaySettings) { // if there is open animation do nothing // if toggle is not collapsed and there is no close animation do nothing const info = this.overlayService.getOverlayById(this._overlayId); const openAnimationStarted = info?.openAnimationPlayer?.hasStarted() ?? false; const closeAnimationStarted = info?.closeAnimationPlayer?.hasStarted() ?? false; if (openAnimationStarted || !(this._collapsed || closeAnimationStarted)) { return; } this._collapsed = false; this.cdr.detectChanges(); if (!info) { this.unsubscribe(); this.subscribe(); this._overlayId = this.overlayService.attach(this.elementRef, overlaySettings); } const args: ToggleViewCancelableEventArgs = { cancel: false, owner: this, id: this._overlayId }; this.opening.emit(args); if (args.cancel) { this.unsubscribe(); this.overlayService.detach(this._overlayId); this._collapsed = true; delete this._overlayId; this.cdr.detectChanges(); return; } this.overlayService.show(this._overlayId, overlaySettings); } /** * Closes the toggle. * * ```typescript * this.myToggle.close(); * ``` */ public close(event?: Event) { // if toggle is collapsed do nothing // if there is close animation do nothing, toggle will close anyway const info = this.overlayService.getOverlayById(this._overlayId); const closeAnimationStarted = info?.closeAnimationPlayer?.hasStarted() || false; if (this._collapsed || closeAnimationStarted) { return; } this.overlayService.hide(this._overlayId, event); } /** * Opens or closes the toggle, depending on its current state. * * ```typescript * this.myToggle.toggle(); * ``` */ public toggle(overlaySettings?: OverlaySettings) { // if toggle is collapsed call open // if there is running close animation call open if (this.collapsed || this.isClosing) { this.open(overlaySettings); } else { this.close(); } } /** @hidden @internal */ public get isClosing() { const info = this.overlayService.getOverlayById(this._overlayId); return info ? info.closeAnimationPlayer?.hasStarted() : false; } /** * Returns the id of the overlay the content is rendered in. * ```typescript * this.myToggle.overlayId; * ``` */ public get overlayId() { return this._overlayId; } /** * Repositions the toggle. * ```typescript * this.myToggle.reposition(); * ``` */ public reposition() { this.overlayService.reposition(this._overlayId); } /** * Offsets the content along the corresponding axis by the provided amount */ public setOffset(deltaX: number, deltaY: number) { this.overlayService.setOffset(this._overlayId, deltaX, deltaY); } /** * @hidden */ public ngOnInit() { if (this.navigationService && this.id) { this.navigationService.add(this.id, this); } } /** * @hidden */ public ngOnDestroy() { if (this.navigationService && this.id) { this.navigationService.remove(this.id); } if (this._overlayId) { this.overlayService.detach(this._overlayId); } this.unsubscribe(); this.destroy$.next(true); this.destroy$.complete(); } private overlayClosed = (e) => { this._collapsed = true; this.cdr.detectChanges(); this.unsubscribe(); this.overlayService.detach(this.overlayId); const args: ToggleViewEventArgs = { owner: this, id: this._overlayId, event: e.event }; delete this._overlayId; this.closed.emit(args); this.cdr.markForCheck(); }; private subscribe() { this._overlayContentAppendedSub = this.overlayService .contentAppended .pipe(first(), takeUntil(this.destroy$)) .subscribe(() => { const args: ToggleViewEventArgs = { owner: this, id: this._overlayId }; this.appended.emit(args); }); this._overlayOpenedSub = this.overlayService .opened .pipe(...this._overlaySubFilter) .subscribe(() => { const args: ToggleViewEventArgs = { owner: this, id: this._overlayId }; this.opened.emit(args); }); this._overlayClosingSub = this.overlayService .closing .pipe(...this._overlaySubFilter) .subscribe((e: OverlayClosingEventArgs) => { const args: ToggleViewCancelableEventArgs = { cancel: false, event: e.event, owner: this, id: this._overlayId }; this.closing.emit(args); e.cancel = args.cancel; // in case event is not canceled this will close the toggle and we need to unsubscribe. // Otherwise if for some reason, e.g. close on outside click, close() gets called before // onClosed was fired we will end with calling onClosing more than once if (!e.cancel) { this.clearSubscription(this._overlayClosingSub); } }); this._overlayClosedSub = this.overlayService .closed .pipe(...this._overlaySubFilter) .subscribe(this.overlayClosed); } private unsubscribe() { this.clearSubscription(this._overlayOpenedSub); this.clearSubscription(this._overlayClosingSub); this.clearSubscription(this._overlayClosedSub); this.clearSubscription(this._overlayContentAppendedSub); } private clearSubscription(subscription: Subscription) { if (subscription && !subscription.closed) { subscription.unsubscribe(); } } } @Directive({ exportAs: 'toggle-action', selector: '[igxToggleAction]', standalone: true }) export class IgxToggleActionDirective implements OnInit { /** * Provide settings that control the toggle overlay positioning, interaction and scroll behavior. * ```typescript * const settings: OverlaySettings = { * closeOnOutsideClick: false, * modal: false * } * ``` * --- * ```html * *
* ``` */ @Input() public overlaySettings: OverlaySettings; /** * Determines where the toggle element overlay should be attached. * * ```html * *
* ``` * Where `outlet` in an instance of `IgxOverlayOutletDirective` or an `ElementRef` */ @Input('igxToggleOutlet') public outlet: IgxOverlayOutletDirective | ElementRef; /** * @hidden */ @Input('igxToggleAction') public set target(target: any) { if (target !== null && target !== '') { this._target = target; } } /** * @hidden */ public get target(): any { if (typeof this._target === 'string') { return this.navigationService.get(this._target); } return this._target; } protected _overlayDefaults: OverlaySettings; protected _target: IToggleView | string; constructor(private element: ElementRef, @Optional() private navigationService: IgxNavigationService) { } /** * @hidden */ @HostListener('click') public onClick() { if (this.outlet) { this._overlayDefaults.outlet = this.outlet; } const clonedSettings = Object.assign({}, this._overlayDefaults, this.overlaySettings); this.updateOverlaySettings(clonedSettings); this.target.toggle(clonedSettings); } /** * @hidden */ public ngOnInit() { const targetElement = this.element.nativeElement; this._overlayDefaults = { target: targetElement, positionStrategy: new ConnectedPositioningStrategy(), scrollStrategy: new AbsoluteScrollStrategy(), closeOnOutsideClick: true, modal: false, excludeFromOutsideClick: [targetElement as HTMLElement] }; } /** * Updates provided overlay settings * * @param settings settings to update * @returns returns updated copy of provided overlay settings */ protected updateOverlaySettings(settings: OverlaySettings): OverlaySettings { if (settings && settings.positionStrategy) { const positionStrategyClone: IPositionStrategy = settings.positionStrategy.clone(); settings.target = this.element.nativeElement; settings.positionStrategy = positionStrategyClone; } return settings; } } /** * Mark an element as an igxOverlay outlet container. * Directive instance is exported as `overlay-outlet` to be assigned to templates variables: * ```html *
* ``` */ @Directive({ exportAs: 'overlay-outlet', selector: '[igxOverlayOutlet]', standalone: true }) export class IgxOverlayOutletDirective { constructor(public element: ElementRef) { } /** @hidden */ public get nativeElement() { return this.element.nativeElement; } }