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 {Directionality} from '@angular/cdk/bidi';
import {
Overlay,
OverlayConfig,
OverlayContainer,
OverlayRef,
ScrollStrategy,
} from '@angular/cdk/overlay';
import {ComponentPortal, ComponentType, TemplatePortal} from '@angular/cdk/portal';
import {Location} from '@angular/common';
import {
Directive,
Inject,
Injectable,
InjectFlags,
InjectionToken,
Injector,
OnDestroy,
Optional,
SkipSelf,
StaticProvider,
TemplateRef,
Type,
} from '@angular/core';
import {defer, Observable, of as observableOf, Subject, Subscription} from 'rxjs';
import {startWith} from 'rxjs/operators';
import {ngDevMode} from '@twnd/ux/core';
import {TWNDModalConfig} from './modal.config';
import {TWNDModalContainer, _TWNDModalContainerBase} from './modal.container';
import {TWNDModalRef} from './modal.ref';
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
/**
* Injection token that can be used to access the data that was passed in to a modal.
*/
export const TWND_MODAL_DATA = new InjectionToken<any>('TWNDModalData');
/**
* Injection token that can be used to specify default modal options.
*/
export const TWND_MODAL_DEFAULT_OPTIONS = new InjectionToken<TWNDModalConfig>(
'twnd-modal-default-options',
);
/**
* Injection token that determines the scroll handling while the modal is open.
*/
export const TWND_MODAL_SCROLL_STRATEGY = new InjectionToken<() => ScrollStrategy>(
'twnd-modal-scroll-strategy',
);
/**
* @docs-private
*/
export function TWND_MODAL_SCROLL_STRATEGY_FACTORY(overlay: Overlay): () => ScrollStrategy
{
return () => overlay.scrollStrategies.block();
}
/**
* @docs-private
*/
export function TWND_MODAL_SCROLL_STRATEGY_PROVIDER_FACTORY(
overlay: Overlay,
): () => ScrollStrategy
{
return () => overlay.scrollStrategies.block();
}
/**
* @docs-private
*/
export const TWND_MODAL_SCROLL_STRATEGY_PROVIDER = {
provide : TWND_MODAL_SCROLL_STRATEGY,
deps : [ Overlay ],
useFactory : TWND_MODAL_SCROLL_STRATEGY_PROVIDER_FACTORY,
};
/**
* Base class for modal services. The base modal service allows
* for arbitrary modal refs and modal container components.
*/
@Directive()
export abstract class _TWNDModalBase<C extends _TWNDModalContainerBase> implements OnDestroy
{
private _openModalsAtThisLevel: TWNDModalRef<any>[] = [];
private readonly _afterAllClosedAtThisLevel = new Subject<void>();
private readonly _afterOpenedAtThisLevel = new Subject<TWNDModalRef<any>>();
private _ariaHiddenElements = new Map<Element, string|null>();
private _scrollStrategy: () => ScrollStrategy;
private _modalAnimatingOpen = false;
private _animationStateSubscriptions: Subscription;
private _lastModalRef: TWNDModalRef<any>;
/**
* Keeps track of the currently-open modals.
*/
get openModals(): TWNDModalRef<any>[]
{
return this._parentModal ? this._parentModal.openModals : this._openModalsAtThisLevel;
}
/**
* Stream that emits when a modal has been opened.
*/
get afterOpened(): Subject<TWNDModalRef<any>>
{
return this._parentModal ? this._parentModal.afterOpened : this._afterOpenedAtThisLevel;
}
_getAfterAllClosed(): Subject<void>
{
const parent = this._parentModal;
return parent ? parent._getAfterAllClosed() : this._afterAllClosedAtThisLevel;
}
/**
* Stream that emits when all open modal have finished closing.
* Will emit on subscribe if there are no open modals to begin with.
*/
readonly afterAllClosed:
Observable<void> = defer(
() => this.openModals.length ?
this._getAfterAllClosed() :
this._getAfterAllClosed().pipe(startWith(undefined)),
) as Observable<any>;
constructor(
private _overlay: Overlay,
private _injector: Injector,
private _defaultOptions: TWNDModalConfig|undefined,
private _parentModal: _TWNDModalBase<C>|undefined,
private _overlayContainer: OverlayContainer,
scrollStrategy: any,
private _modalRefConstructor: Type<TWNDModalRef<any>>,
private _modalContainerType: Type<C>,
private _modalDataToken: InjectionToken<any>,
private _animationMode?: 'NoopAnimations'|'BrowserAnimations',
)
{
this._scrollStrategy = scrollStrategy;
}
/**
* Opens a modal modal containing the given component.
* @param component Type of the component to load into the modal.
* @param config Extra configuration options.
* @returns Reference to the newly-opened modal.
*/
open<T, D = any, R = any>(
component: ComponentType<T>,
config?: TWNDModalConfig<D>,
): TWNDModalRef<T, R>;
/**
* Opens a modal modal containing the given template.
* @param template TemplateRef to instantiate as the modal content.
* @param config Extra configuration options.
* @returns Reference to the newly-opened modal.
*/
open<T, D = any, R = any>(
template: TemplateRef<T>,
config?: TWNDModalConfig<D>,
): TWNDModalRef<T, R>;
open<T, D = any, R = any>(
template: ComponentType<T>|TemplateRef<T>,
config?: TWNDModalConfig<D>,
): TWNDModalRef<T, R>;
open<T, D = any, R = any>(
componentOrTemplateRef: ComponentType<T>|TemplateRef<T>,
config?: TWNDModalConfig<D>,
): TWNDModalRef<T, R>
{
config = _applyConfigDefaults(config, this._defaultOptions || new TWNDModalConfig());
if (config.id && this.getModalById(config.id) &&
(typeof ngDevMode === 'undefined' || ngDevMode)) {
throw Error(`Modal with id "${config.id}" exists already. The modal id must be unique.`);
}
// If there is a modal that is currently animating open, return the TWNDModalRef of that modal
if (this._modalAnimatingOpen) {
return this._lastModalRef;
}
const overlayRef = this._createOverlay(config);
const modalContainer = this._attachModalContainer(overlayRef, config);
if (this._animationMode !== 'NoopAnimations') {
const animationStateSubscription = modalContainer._animationStateChanged.subscribe(
modalAnimationEvent => {
if (modalAnimationEvent.state === 'opening') {
this._modalAnimatingOpen = true;
}
if (modalAnimationEvent.state === 'opened') {
this._modalAnimatingOpen = false;
animationStateSubscription.unsubscribe();
}
},
);
if (!this._animationStateSubscriptions) {
this._animationStateSubscriptions = new Subscription();
}
this._animationStateSubscriptions.add(animationStateSubscription);
}
const modalRef = this._attachModalContent<T, R>(
componentOrTemplateRef,
modalContainer,
overlayRef,
config,
);
this._lastModalRef = modalRef;
// If this is the first modal that we're opening, hide all the non-overlay content.
if (!this.openModals.length) {
this._hideNonModalContentFromAssistiveTechnology();
}
this.openModals.push(modalRef);
modalRef.afterClosed().subscribe(() => this._removeOpenModal(modalRef));
this.afterOpened.next(modalRef);
// Notify the modal container that the content has been attached.
modalContainer._initializeWithAttachedContent();
return modalRef;
}
/**
* Closes all of the currently-open modals.
*/
closeAll(): void
{
this._closeModals(this.openModals);
}
/**
* Finds an open modal by its id.
* @param id ID to use when looking up the modal.
*/
getModalById(id: string): TWNDModalRef<any>|undefined
{
return this.openModals.find(modal => modal.id === id);
}
ngOnDestroy()
{
// Only close the modals at this level on destroy
// since the parent service may still be active.
this._closeModals(this._openModalsAtThisLevel);
this._afterAllClosedAtThisLevel.complete();
this._afterOpenedAtThisLevel.complete();
// Clean up any subscriptions to modals that never finished opening.
if (this._animationStateSubscriptions) {
this._animationStateSubscriptions.unsubscribe();
}
}
/**
* Creates the overlay into which the modal will be loaded.
* @param config The modal configuration.
* @returns A promise resolving to the OverlayRef for the created overlay.
*/
private _createOverlay(config: TWNDModalConfig): OverlayRef
{
const overlayConfig = this._getOverlayConfig(config);
return this._overlay.create(overlayConfig);
}
/**
* Creates an overlay config from a modal config.
* @param modalConfig The modal configuration.
* @returns The overlay configuration.
*/
private _getOverlayConfig(modalConfig: TWNDModalConfig): OverlayConfig
{
const state = new OverlayConfig({
positionStrategy : this._overlay.position().global(),
scrollStrategy : modalConfig.scrollStrategy || this._scrollStrategy(),
panelClass : modalConfig.panelClass,
hasBackdrop : modalConfig.hasBackdrop,
direction : modalConfig.direction,
minWidth : modalConfig.minWidth,
minHeight : modalConfig.minHeight,
maxWidth : modalConfig.maxWidth,
maxHeight : modalConfig.maxHeight,
disposeOnNavigation : modalConfig.closeOnNavigation,
});
if (modalConfig.backdropClass) {
state.backdropClass = modalConfig.backdropClass;
}
return state;
}
/**
* Attaches a modal container to a modal's already-created overlay.
* @param overlay Reference to the modal's underlying overlay.
* @param config The modal configuration.
* @returns A promise resolving to a ComponentRef for the attached container.
*/
private _attachModalContainer(overlay: OverlayRef, config: TWNDModalConfig): C
{
const userInjector = config && config.viewContainerRef && config.viewContainerRef.injector;
const injector = Injector.create({
parent : userInjector || this._injector,
providers : [ {provide : TWNDModalConfig, useValue : config} ],
});
const containerPortal = new ComponentPortal(
this._modalContainerType,
config.viewContainerRef,
injector,
config.componentFactoryResolver,
);
const containerRef = overlay.attach<C>(containerPortal);
return containerRef.instance;
}
/**
* Attaches the user-provided component to the already-created modal container.
* @param componentOrTemplateRef The type of component being loaded into the modal,
* or a TemplateRef to instantiate as the content.
* @param modalContainer Reference to the wrapping modal container.
* @param overlayRef Reference to the overlay in which the modal resides.
* @param config The modal configuration.
* @returns A promise resolving to the TWNDModalRef that should be returned to the user.
*/
private _attachModalContent<T, R>(
componentOrTemplateRef: ComponentType<T>|TemplateRef<T>,
modalContainer: C,
overlayRef: OverlayRef,
config: TWNDModalConfig,
): TWNDModalRef<T, R>
{
// Create a reference to the modal we're creating in order to give the user a handle
// to modify and close it.
const modalRef = new this._modalRefConstructor(overlayRef, modalContainer, config.id);
if (componentOrTemplateRef instanceof TemplateRef) {
modalContainer.attachTemplatePortal(
new TemplatePortal<T>(componentOrTemplateRef, null!, <any>{
$implicit : config.data,
modalRef,
}),
);
} else {
const injector = this._createInjector<T>(config, modalRef, modalContainer);
const contentRef = modalContainer.attachComponentPortal<T>(
new ComponentPortal(componentOrTemplateRef, config.viewContainerRef, injector),
);
modalRef.componentInstance = contentRef.instance;
}
modalRef.updateSize(config.width, config.height).updatePosition(config.position);
return modalRef;
}
/**
* Creates a custom injector to be used inside the modal. This allows a component loaded inside
* of a modal to close itself and, optionally, to return a value.
* @param config Config object that is used to construct the modal.
* @param modalRef Reference to the modal.
* @param modalContainer Modal container element that wraps all of the contents.
* @returns The custom injector that can be used inside the modal.
*/
private _createInjector<T>(
config: TWNDModalConfig,
modalRef: TWNDModalRef<T>,
modalContainer: C,
): Injector
{
const userInjector = config && config.viewContainerRef && config.viewContainerRef.injector;
// The modal container should be provided as the modal container and the modal's
// content are created out of the same `ViewContainerRef` and as such, are siblings
// for injector purposes. To allow the hierarchy that is expected, the modal
// container is explicitly provided in the injector.
const providers: StaticProvider[] = [
{provide : this._modalContainerType, useValue : modalContainer},
{provide : this._modalDataToken, useValue : config.data},
{provide : this._modalRefConstructor, useValue : modalRef},
];
if (config.direction &&
(!userInjector ||
!userInjector.get<Directionality|null>(Directionality, null, InjectFlags.Optional))) {
providers.push({
provide : Directionality,
useValue : {value : config.direction, change : observableOf()},
});
}
return Injector.create({parent : userInjector || this._injector, providers});
}
/**
* Removes a modal from the array of open modals.
* @param modalRef Modal to be removed.
*/
private _removeOpenModal(modalRef: TWNDModalRef<any>)
{
const index = this.openModals.indexOf(modalRef);
if (index > -1) {
this.openModals.splice(index, 1);
// If all the modals were closed, remove/restore the `aria-hidden`
// to a the siblings and emit to the `afterAllClosed` stream.
if (!this.openModals.length) {
this._ariaHiddenElements.forEach((previousValue, element) => {
if (previousValue) {
element.setAttribute('aria-hidden', previousValue);
} else {
element.removeAttribute('aria-hidden');
}
});
this._ariaHiddenElements.clear();
this._getAfterAllClosed().next();
}
}
}
/**
* Hides all of the content that isn't an overlay from assistive technology.
*/
private _hideNonModalContentFromAssistiveTechnology()
{
const overlayContainer = this._overlayContainer.getContainerElement();
// Ensure that the overlay container is attached to the DOM.
if (overlayContainer.parentElement) {
const siblings = overlayContainer.parentElement.children;
for (let i = siblings.length - 1; i > -1; i--) {
let sibling = siblings[i];
if (sibling !== overlayContainer && sibling.nodeName !== 'SCRIPT' &&
sibling.nodeName !== 'STYLE' && !sibling.hasAttribute('aria-live')) {
this._ariaHiddenElements.set(sibling, sibling.getAttribute('aria-hidden'));
sibling.setAttribute('aria-hidden', 'true');
}
}
}
}
/**
* Closes all of the modals in an array.
*/
private _closeModals(modals: TWNDModalRef<any>[])
{
let i = modals.length;
while (i--) {
// The `_openModals` property isn't updated after close until the rxjs subscription
// runs on the next microtask, in addition to modifying the array as we're going
// through it. We loop through all of them and call close without assuming that
// they'll be removed from the list instantaneously.
modals[i].close();
}
}
}
/**
* Service to open Tailwind UX modals.
*/
@Injectable() export class TWNDModal extends _TWNDModalBase<TWNDModalContainer>
{
constructor(
overlay: Overlay,
injector: Injector,
/**
* @deprecated `_location` parameter to be removed.
* @breaking-change 10.0.0
*/
@Optional() location: Location,
@Optional() @Inject(TWND_MODAL_DEFAULT_OPTIONS) defaultOptions: TWNDModalConfig,
@Inject(TWND_MODAL_SCROLL_STRATEGY) scrollStrategy: any,
@Optional() @SkipSelf() parentModal: TWNDModal,
overlayContainer: OverlayContainer,
@Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: 'NoopAnimations'|'BrowserAnimations',
)
{
super(
overlay,
injector,
defaultOptions,
parentModal,
overlayContainer,
scrollStrategy,
TWNDModalRef,
TWNDModalContainer,
TWND_MODAL_DATA,
animationMode,
);
}
}
/**
* Applies default options to the modal config.
* @param config Config to be modified.
* @param defaultOptions Default options provided.
* @returns The new configuration object.
*/
function _applyConfigDefaults(
config?: TWNDModalConfig,
defaultOptions?: TWNDModalConfig,
): TWNDModalConfig
{
return {...defaultOptions, ...config};
}