Repository URL to install this package:
|
Version:
1.0.0-next.10 ▾
|
/**
* @license
* FOURBURNER CONFIDENTIAL
* Unpublished Copyright (C) 2021 FourBurner Technologies, Inc. All Rights Reserved.
*
* NOTICE: All information contained herein is, and remains the property of FOURBURNER TECHNOLOGIES,
* INC. The intellectual and technical concepts contained herein are proprietary to FOURBURNER
* TECHNOLOGIES, INC. and may be covered by U.S. and Foreign Patents, patents in process, and are
* protected by trade secret or copyright law. Dissemination of this information or reproduction of
* this material is strictly forbidden unless prior written permission is obtained from FOURBURNER
* TECHNOLOGIES, INC. Access to the source code contained herein is hereby forbidden to anyone
* except current FOURBURNER TECHNOLOGIES, INC. employees, managers or contractors who have executed
* Confidentiality and Non-disclosure agreements explicitly covering such access.
*
* The copyright notice above does not evidence any actual or intended publication or disclosure of
* this source code, which includes information that is confidential and/or proprietary, and is a
* trade secret, of FOURBURNER TECHNOLOGIES, INC. ANY REPRODUCTION, MODIFICATION, DISTRIBUTION,
* PUBLIC PERFORMANCE, OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT THE EXPRESS
* WRITTEN CONSENT OF FOURBURNER TECHNOLOGIES, INC. IS STRICTLY PROHIBITED, AND IN VIOLATION OF
* APPLICABLE LAWS AND INTERNATIONAL TREATIES. THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR
* RELATED INFORMATION DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS
* CONTENTS, OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART.
*/
import {FocusOrigin} from '@angular/cdk/a11y';
import {ESCAPE, hasModifierKey} from '@angular/cdk/keycodes';
import {GlobalPositionStrategy, OverlayRef} from '@angular/cdk/overlay';
import {Observable, Subject} from 'rxjs';
import {filter, take} from 'rxjs/operators';
import {ModalPosition} from './modal.config';
import {_TWNDModalContainerBase} from './modal.container';
// Counter for unique modal ids.
let uniqueId = 0;
/**
* Possible states of the lifecycle of a modal.
*/
export const enum TWNDModalState {
OPEN,
CLOSING,
CLOSED,
}
/**
* Reference to a modal opened via the TWNDModal service.
*/
export class TWNDModalRef<T, R = any>
{
/**
* The instance of component opened into the modal.
*/
componentInstance: T;
/**
* Whether the user is allowed to close the modal.
*/
disableClose: boolean|undefined = this._containerInstance._config.disableClose;
/**
* Subject for notifying the user that the modal has finished opening.
*/
private readonly _afterOpened = new Subject<void>();
/**
* Subject for notifying the user that the modal has finished closing.
*/
private readonly _afterClosed = new Subject<R|undefined>();
/**
* Subject for notifying the user that the modal has started closing.
*/
private readonly _beforeClosed = new Subject<R|undefined>();
/**
* Result to be passed to afterClosed.
*/
private _result: R|undefined;
/**
* Handle to the timeout that's running as a fallback in case the exit animation doesn't fire.
*/
private _closeFallbackTimeout: ReturnType<typeof setTimeout>;
/**
* Current state of the modal.
*/
private _state = TWNDModalState.OPEN;
constructor(
private _overlayRef: OverlayRef,
public _containerInstance: _TWNDModalContainerBase,
/**
Id of the modal.
*/
readonly id: string = `twnd-modal-${uniqueId++}`,
)
{
// Pass the id along to the container.
_containerInstance._id = id;
// Emit when opening animation completes
_containerInstance._animationStateChanged
.pipe(
filter(event => event.state === 'opened'),
take(1),
)
.subscribe(() => {
this._afterOpened.next();
this._afterOpened.complete();
});
// Dispose overlay when closing animation is complete
_containerInstance._animationStateChanged
.pipe(
filter(event => event.state === 'closed'),
take(1),
)
.subscribe(() => {
clearTimeout(this._closeFallbackTimeout);
this._finishModalClose();
});
_overlayRef.detachments().subscribe(() => {
this._beforeClosed.next(this._result);
this._beforeClosed.complete();
this._afterClosed.next(this._result);
this._afterClosed.complete();
this.componentInstance = null!;
this._overlayRef.dispose();
});
_overlayRef.keydownEvents()
.pipe(
filter(event => {
return event.keyCode === ESCAPE && !this.disableClose && !hasModifierKey(event);
}),
)
.subscribe(event => {
event.preventDefault();
_closeModalVia(this, 'keyboard');
});
_overlayRef.backdropClick().subscribe(() => {
if (this.disableClose) {
this._containerInstance._recaptureFocus();
} else {
_closeModalVia(this, 'mouse');
}
});
}
/**
* Close the modal.
* @param modalResult Optional result to return to the modal opener.
*/
close(modalResult?: R): void
{
this._result = modalResult;
// Transition the backdrop in parallel to the modal.
this._containerInstance._animationStateChanged
.pipe(
filter(event => event.state === 'closing'),
take(1),
)
.subscribe(event => {
this._beforeClosed.next(modalResult);
this._beforeClosed.complete();
this._overlayRef.detachBackdrop();
// The logic that disposes of the overlay depends on the exit animation completing, however
// it isn't guaranteed if the parent view is destroyed while it's running. Add a fallback
// timeout which will clean everything up if the animation hasn't fired within the specified
// amount of time plus 100ms. We don't need to run this outside the NgZone, because for the
// vast majority of cases the timeout will have been cleared before it has the chance to
// fire.
this._closeFallbackTimeout = setTimeout(
() => this._finishModalClose(),
event.totalTime + 100,
);
});
this._state = TWNDModalState.CLOSING;
this._containerInstance._startExitAnimation();
}
/**
* Gets an observable that is notified when the modal is finished opening.
*/
afterOpened(): Observable<void>
{
return this._afterOpened;
}
/**
* Gets an observable that is notified when the modal is finished closing.
*/
afterClosed(): Observable<R|undefined>
{
return this._afterClosed;
}
/**
* Gets an observable that is notified when the modal has started closing.
*/
beforeClosed(): Observable<R|undefined>
{
return this._beforeClosed;
}
/**
* Gets an observable that emits when the overlay's backdrop has been clicked.
*/
backdropClick(): Observable<MouseEvent>
{
return this._overlayRef.backdropClick();
}
/**
* Gets an observable that emits when keydown events are targeted on the overlay.
*/
keydownEvents(): Observable<KeyboardEvent>
{
return this._overlayRef.keydownEvents();
}
/**
* Updates the modal's position.
* @param position New modal position.
*/
updatePosition(position?: ModalPosition): this
{
let strategy = this._getPositionStrategy();
if (position && (position.left || position.right)) {
position.left ? strategy.left(position.left) : strategy.right(position.right);
} else {
strategy.centerHorizontally();
}
if (position && (position.top || position.bottom)) {
position.top ? strategy.top(position.top) : strategy.bottom(position.bottom);
} else {
strategy.centerVertically();
}
this._overlayRef.updatePosition();
return this;
}
/**
* Updates the modal's width and height.
* @param width New width of the modal.
* @param height New height of the modal.
*/
updateSize(width: string = '', height: string = ''): this
{
this._overlayRef.updateSize({width, height});
this._overlayRef.updatePosition();
return this;
}
/**
* Add a CSS class or an array of classes to the overlay pane.
*/
addPanelClass(classes: string|string[]): this
{
this._overlayRef.addPanelClass(classes);
return this;
}
/**
* Remove a CSS class or an array of classes from the overlay pane.
*/
removePanelClass(classes: string|string[]): this
{
this._overlayRef.removePanelClass(classes);
return this;
}
/**
* Gets the current state of the modal's lifecycle.
*/
getState(): TWNDModalState
{
return this._state;
}
/**
* Finishes the modal close by updating the state of the modal
* and disposing the overlay.
*/
private _finishModalClose()
{
this._state = TWNDModalState.CLOSED;
this._overlayRef.dispose();
}
/**
* Fetches the position strategy object from the overlay ref.
*/
private _getPositionStrategy(): GlobalPositionStrategy
{
return this._overlayRef.getConfig().positionStrategy as GlobalPositionStrategy;
}
}
/**
* Closes the modal with the specified interaction type. This is currently not part of
* `TWNDModalRef` as that would conflict with custom modal ref mocks provided in tests.
*/
// TODO: TODO: Move this back into `TWNDModalRef` when we provide an official mock modal ref.
export function _closeModalVia<R>(ref: TWNDModalRef<R>, interactionType: FocusOrigin, result?: R)
{
// Some mock modal ref instances in tests do not have the `_containerInstance` property.
// For those, we keep the behavior as is and do not deal with the interaction type.
if (ref._containerInstance !== undefined) {
ref._containerInstance._closeInteractionType = interactionType;
}
return ref.close(result);
}