igniteui-angular-sovn
Version:
Ignite UI for Angular is a dependency-free Angular toolkit for building modern web apps
1,007 lines (936 loc) • 40.7 kB
text/typescript
import { AnimationReferenceMetadata } from '@angular/animations';
import { DOCUMENT } from '@angular/common';
import {
ApplicationRef,
ComponentFactory,
ComponentFactoryResolver,
ComponentRef,
ElementRef,
EventEmitter,
Inject,
Injectable,
Injector,
NgZone,
OnDestroy,
Type,
ViewContainerRef
} from '@angular/core';
import { fromEvent, Subject, Subscription } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import {
fadeIn,
fadeOut,
IAnimationParams,
scaleInHorLeft,
scaleInHorRight,
scaleInVerBottom,
scaleInVerTop,
scaleOutHorLeft,
scaleOutHorRight,
scaleOutVerBottom,
scaleOutVerTop,
slideInBottom,
slideInTop,
slideOutBottom,
slideOutTop
} from '../../animations/main';
import { PlatformUtil } from '../../core/utils';
import { IgxOverlayOutletDirective } from '../../directives/toggle/toggle.directive';
import { IgxAngularAnimationService } from '../animation/angular-animation-service';
import { AnimationService } from '../animation/animation';
import { AutoPositionStrategy } from './position/auto-position-strategy';
import { ConnectedPositioningStrategy } from './position/connected-positioning-strategy';
import { ContainerPositionStrategy } from './position/container-position-strategy';
import { ElasticPositionStrategy } from './position/elastic-position-strategy';
import { GlobalPositionStrategy } from './position/global-position-strategy';
import { IPositionStrategy } from './position/IPositionStrategy';
import { NoOpScrollStrategy } from './scroll/NoOpScrollStrategy';
import {
AbsolutePosition,
HorizontalAlignment,
OverlayAnimationEventArgs,
OverlayCancelableEventArgs,
OverlayClosingEventArgs,
OverlayEventArgs,
OverlayInfo,
OverlaySettings,
Point,
PositionSettings,
RelativePosition,
RelativePositionStrategy,
VerticalAlignment
} from './utilities';
/**
* [Documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/overlay-main)
* The overlay service allows users to show components on overlay div above all other elements in the page.
*/
export class IgxOverlayService implements OnDestroy {
/**
* Emitted just before the overlay content starts to open.
* ```typescript
* opening(event: OverlayCancelableEventArgs){
* const opening = event;
* }
* ```
*/
public opening = new EventEmitter<OverlayCancelableEventArgs>();
/**
* Emitted after the overlay content is opened and all animations are finished.
* ```typescript
* opened(event: OverlayEventArgs){
* const opened = event;
* }
* ```
*/
public opened = new EventEmitter<OverlayEventArgs>();
/**
* Emitted just before the overlay content starts to close.
* ```typescript
* closing(event: OverlayCancelableEventArgs){
* const closing = event;
* }
* ```
*/
public closing = new EventEmitter<OverlayClosingEventArgs>();
/**
* Emitted after the overlay content is closed and all animations are finished.
* ```typescript
* closed(event: OverlayEventArgs){
* const closed = event;
* }
* ```
*/
public closed = new EventEmitter<OverlayEventArgs>();
/**
* Emitted before the content is appended to the overlay.
* ```typescript
* contentAppending(event: OverlayEventArgs){
* const contentAppending = event;
* }
* ```
*/
public contentAppending = new EventEmitter<OverlayEventArgs>();
/**
* Emitted after the content is appended to the overlay, and before animations are started.
* ```typescript
* contentAppended(event: OverlayEventArgs){
* const contentAppended = event;
* }
* ```
*/
public contentAppended = new EventEmitter<OverlayEventArgs>();
/**
* Emitted just before the overlay animation start.
* ```typescript
* animationStarting(event: OverlayAnimationEventArgs){
* const animationStarting = event;
* }
* ```
*/
public animationStarting = new EventEmitter<OverlayAnimationEventArgs>();
private _componentId = 0;
private _overlayInfos: OverlayInfo[] = [];
private _overlayElement: HTMLElement;
private _document: Document;
private _keyPressEventListener: Subscription;
private destroy$ = new Subject<boolean>();
private _cursorStyleIsSet = false;
private _cursorOriginalValue: string;
private _defaultSettings: OverlaySettings = {
excludeFromOutsideClick: [],
positionStrategy: new GlobalPositionStrategy(),
scrollStrategy: new NoOpScrollStrategy(),
modal: true,
closeOnOutsideClick: true,
closeOnEscape: false
};
constructor(
private _factoryResolver: ComponentFactoryResolver,
private _appRef: ApplicationRef,
private _injector: Injector,
private document: any,
private _zone: NgZone,
protected platformUtil: PlatformUtil,
private animationService: AnimationService) {
this._document = this.document;
}
/**
* Creates overlay settings with global or container position strategy and preset position settings
*
* @param position Preset position settings. Default position is 'center'
* @param outlet The outlet container to attach the overlay to
* @returns Non-modal overlay settings based on Global or Container position strategy and the provided position.
*/
public static createAbsoluteOverlaySettings(
position?: AbsolutePosition, outlet?: IgxOverlayOutletDirective | ElementRef): OverlaySettings {
const positionSettings = this.createAbsolutePositionSettings(position);
const strategy = outlet ? new ContainerPositionStrategy(positionSettings) : new GlobalPositionStrategy(positionSettings);
const overlaySettings: OverlaySettings = {
positionStrategy: strategy,
scrollStrategy: new NoOpScrollStrategy(),
modal: false,
closeOnOutsideClick: true,
outlet
};
return overlaySettings;
}
/**
* Creates overlay settings with auto, connected or elastic position strategy and preset position settings
*
* @param target Attaching target for the component to show
* @param strategy The relative position strategy to be applied to the overlay settings. Default is Auto positioning strategy.
* @param position Preset position settings. By default the element is positioned below the target, left aligned.
* @returns Non-modal overlay settings based on the provided target, strategy and position.
*/
public static createRelativeOverlaySettings(
target: Point | HTMLElement,
position?: RelativePosition,
strategy?: RelativePositionStrategy):
OverlaySettings {
const positionSettings = this.createRelativePositionSettings(position);
const overlaySettings: OverlaySettings = {
target,
positionStrategy: this.createPositionStrategy(strategy, positionSettings),
scrollStrategy: new NoOpScrollStrategy(),
modal: false,
closeOnOutsideClick: true
};
return overlaySettings;
}
private static createAbsolutePositionSettings(position: AbsolutePosition): PositionSettings {
let positionSettings: PositionSettings;
switch (position) {
case AbsolutePosition.Bottom:
positionSettings = {
horizontalDirection: HorizontalAlignment.Center,
verticalDirection: VerticalAlignment.Bottom,
openAnimation: slideInBottom,
closeAnimation: slideOutBottom
};
break;
case AbsolutePosition.Top:
positionSettings = {
horizontalDirection: HorizontalAlignment.Center,
verticalDirection: VerticalAlignment.Top,
openAnimation: slideInTop,
closeAnimation: slideOutTop
};
break;
case AbsolutePosition.Center:
default:
positionSettings = {
horizontalDirection: HorizontalAlignment.Center,
verticalDirection: VerticalAlignment.Middle,
openAnimation: fadeIn,
closeAnimation: fadeOut
};
}
return positionSettings;
}
private static createRelativePositionSettings(position: RelativePosition): PositionSettings {
let positionSettings: PositionSettings;
switch (position) {
case RelativePosition.Above:
positionSettings = {
horizontalStartPoint: HorizontalAlignment.Center,
verticalStartPoint: VerticalAlignment.Top,
horizontalDirection: HorizontalAlignment.Center,
verticalDirection: VerticalAlignment.Top,
openAnimation: scaleInVerBottom,
closeAnimation: scaleOutVerBottom,
};
break;
case RelativePosition.Below:
positionSettings = {
horizontalStartPoint: HorizontalAlignment.Center,
verticalStartPoint: VerticalAlignment.Bottom,
horizontalDirection: HorizontalAlignment.Center,
verticalDirection: VerticalAlignment.Bottom,
openAnimation: scaleInVerTop,
closeAnimation: scaleOutVerTop
};
break;
case RelativePosition.After:
positionSettings = {
horizontalStartPoint: HorizontalAlignment.Right,
verticalStartPoint: VerticalAlignment.Middle,
horizontalDirection: HorizontalAlignment.Right,
verticalDirection: VerticalAlignment.Middle,
openAnimation: scaleInHorLeft,
closeAnimation: scaleOutHorLeft
};
break;
case RelativePosition.Before:
positionSettings = {
horizontalStartPoint: HorizontalAlignment.Left,
verticalStartPoint: VerticalAlignment.Middle,
horizontalDirection: HorizontalAlignment.Left,
verticalDirection: VerticalAlignment.Middle,
openAnimation: scaleInHorRight,
closeAnimation: scaleOutHorRight
};
break;
case RelativePosition.Default:
default:
positionSettings = {
horizontalStartPoint: HorizontalAlignment.Left,
verticalStartPoint: VerticalAlignment.Bottom,
horizontalDirection: HorizontalAlignment.Right,
verticalDirection: VerticalAlignment.Bottom,
openAnimation: scaleInVerTop,
closeAnimation: scaleOutVerTop,
};
break;
}
return positionSettings;
}
private static createPositionStrategy(strategy: RelativePositionStrategy, positionSettings: PositionSettings): IPositionStrategy {
switch (strategy) {
case RelativePositionStrategy.Connected:
return new ConnectedPositioningStrategy(positionSettings);
case RelativePositionStrategy.Elastic:
return new ElasticPositionStrategy(positionSettings);
case RelativePositionStrategy.Auto:
default:
return new AutoPositionStrategy(positionSettings);
}
}
/**
* Generates Id. Provide this Id when call `show(id)` method
*
* @param component ElementRef to show in overlay
* @param settings Display settings for the overlay, such as positioning and scroll/close behavior.
* @returns Id of the created overlay. Valid until `detach` is called.
*/
public attach(element: ElementRef, settings?: OverlaySettings): string;
/**
* Generates Id. Provide this Id when call `show(id)` method
*
* @param component Component Type to show in overlay
* @param settings Display settings for the overlay, such as positioning and scroll/close behavior.
* @param moduleRef Optional reference to an object containing Injector and ComponentFactoryResolver
* that can resolve the component's factory
* @returns Id of the created overlay. Valid until `detach` is called.
* @deprecated deprecated in 14.0.0. Use the `attach(component, viewContainerRef, settings)` overload
*/
public attach(
component: Type<any>,
settings?: OverlaySettings,
moduleRef?: { injector: Injector, componentFactoryResolver: ComponentFactoryResolver }): string;
/**
* Generates an Id. Provide this Id when calling the `show(id)` method
*
* @param component Component Type to show in overlay
* @param viewContainerRef Reference to the container where created component's host view will be inserted
* @param settings Display settings for the overlay, such as positioning and scroll/close behavior.
*/
public attach(component: Type<any>, viewContainerRef: ViewContainerRef, settings?: OverlaySettings): string;
public attach(
componentOrElement: ElementRef | Type<any>,
viewContainerRefOrSettings?: ViewContainerRef | OverlaySettings,
moduleRefOrSettings?: { injector: Injector, componentFactoryResolver: ComponentFactoryResolver } | OverlaySettings): string {
const info: OverlayInfo = this.getOverlayInfo(componentOrElement, this.getUserViewContainerOrModuleRef(viewContainerRefOrSettings, moduleRefOrSettings));
if (!info) {
console.warn('Overlay was not able to attach provided component!');
return null;
}
info.id = (this._componentId++).toString();
info.visible = false;
const settings = Object.assign({}, this._defaultSettings, this.getUserOverlaySettings(viewContainerRefOrSettings, moduleRefOrSettings));
// Emit the contentAppending event before appending the content
const eventArgs = { id: info.id, elementRef: info.elementRef, componentRef: info.componentRef, settings };
this.contentAppending.emit(eventArgs);
// Append the content to the overlay
info.settings = eventArgs.settings;
this._overlayInfos.push(info);
info.hook = this.placeElementHook(info.elementRef.nativeElement);
const elementRect = info.elementRef.nativeElement.getBoundingClientRect();
info.initialSize = { width: elementRect.width, height: elementRect.height };
this.moveElementToOverlay(info);
this.contentAppended.emit({ id: info.id, componentRef: info.componentRef });
info.settings.scrollStrategy.initialize(this._document, this, info.id);
info.settings.scrollStrategy.attach();
this.addOutsideClickListener(info);
this.addResizeHandler();
this.addCloseOnEscapeListener(info);
this.buildAnimationPlayers(info);
return info.id;
}
/**
* Remove overlay with the provided id.
*
* @param id Id of the overlay to remove
* ```typescript
* this.overlay.detach(id);
* ```
*/
public detach(id: string) {
const info: OverlayInfo = this.getOverlayById(id);
if (!info) {
console.warn('igxOverlay.detach was called with wrong id: ', id);
return;
}
info.detached = true;
this.finishAnimations(info);
info.settings.scrollStrategy.detach();
this.removeOutsideClickListener(info);
this.removeResizeHandler();
this.cleanUp(info);
}
/**
* Remove all the overlays.
* ```typescript
* this.overlay.detachAll();
* ```
*/
public detachAll() {
for (let i = this._overlayInfos.length; i--;) {
this.detach(this._overlayInfos[i].id);
}
}
/**
* Shows the overlay for provided id.
*
* @param id Id to show overlay for
* @param settings Display settings for the overlay, such as positioning and scroll/close behavior.
*/
public show(id: string, settings?: OverlaySettings): void {
const info: OverlayInfo = this.getOverlayById(id);
if (!info) {
console.warn('igxOverlay.show was called with wrong id: ', id);
return;
}
const eventArgs: OverlayCancelableEventArgs = { id, componentRef: info.componentRef, cancel: false };
this.opening.emit(eventArgs);
if (eventArgs.cancel) {
return;
}
if (settings) {
// TODO: update attach
}
this.updateSize(info);
info.settings.positionStrategy.position(
info.elementRef.nativeElement.parentElement,
{ width: info.initialSize.width, height: info.initialSize.height },
document,
true,
info.settings.target);
this.addModalClasses(info);
if (info.settings.positionStrategy.settings.openAnimation) {
// TODO: should we build players again. This was already done in attach!!!
// this.buildAnimationPlayers(info);
this.playOpenAnimation(info);
} else {
// to eliminate flickering show the element just before opened fires
info.wrapperElement.style.visibility = '';
info.visible = true;
this.opened.emit({ id: info.id, componentRef: info.componentRef });
}
}
/**
* Hides the component with the ID provided as a parameter.
* ```typescript
* this.overlay.hide(id);
* ```
*/
public hide(id: string, event?: Event) {
this._hide(id, event);
}
/**
* Hides all the components and the overlay.
* ```typescript
* this.overlay.hideAll();
* ```
*/
public hideAll() {
for (let i = this._overlayInfos.length; i--;) {
this.hide(this._overlayInfos[i].id);
}
}
/**
* Repositions the component with ID provided as a parameter.
*
* @param id Id to reposition overlay for
* ```typescript
* this.overlay.reposition(id);
* ```
*/
public reposition(id: string) {
const overlayInfo = this.getOverlayById(id);
if (!overlayInfo || !overlayInfo.settings) {
console.error('Wrong id provided in overlay.reposition method. Id: ' + id);
return;
}
if (!overlayInfo.visible) {
return;
}
const contentElement = overlayInfo.elementRef.nativeElement.parentElement;
const contentElementRect = contentElement.getBoundingClientRect();
overlayInfo.settings.positionStrategy.position(
contentElement,
{
width: contentElementRect.width,
height: contentElementRect.height
},
this._document,
false,
overlayInfo.settings.target);
}
/**
* Offsets the content along the corresponding axis by the provided amount
*
* @param id Id to offset overlay for
* @param deltaX Amount of offset in horizontal direction
* @param deltaY Amount of offset in vertical direction
* ```typescript
* this.overlay.setOffset(id, deltaX, deltaY);
* ```
*/
public setOffset(id: string, deltaX: number, deltaY: number) {
const info: OverlayInfo = this.getOverlayById(id);
if (!info) {
return;
}
info.transformX += deltaX;
info.transformY += deltaY;
const transformX = info.transformX;
const transformY = info.transformY;
const translate = `translate(${transformX}px, ${transformY}px)`;
info.elementRef.nativeElement.parentElement.style.transform = translate;
}
/** @hidden */
public repositionAll = () => {
for (let i = this._overlayInfos.length; i--;) {
this.reposition(this._overlayInfos[i].id);
}
};
/** @hidden */
public ngOnDestroy(): void {
this.destroy$.next(true);
this.destroy$.complete();
}
/** @hidden @internal */
public getOverlayById(id: string): OverlayInfo {
if (!id) {
return null;
}
const info = this._overlayInfos.find(e => e.id === id);
return info;
}
private _hide(id: string, event?: Event) {
const info: OverlayInfo = this.getOverlayById(id);
if (!info) {
console.warn('igxOverlay.hide was called with wrong id: ', id);
return;
}
const eventArgs: OverlayClosingEventArgs = { id, componentRef: info.componentRef, cancel: false, event };
this.closing.emit(eventArgs);
if (eventArgs.cancel) {
return;
}
this.removeModalClasses(info);
if (info.settings.positionStrategy.settings.closeAnimation) {
this.playCloseAnimation(info, event);
} else {
this.closeDone(info);
}
}
private getUserOverlaySettings(
viewContainerRefOrSettings?: ViewContainerRef | OverlaySettings,
moduleRefOrSettings?: { injector: Injector, componentFactoryResolver: ComponentFactoryResolver } | OverlaySettings): OverlaySettings {
let result: OverlaySettings | undefined;
if (viewContainerRefOrSettings && !(viewContainerRefOrSettings instanceof ViewContainerRef)) {
result = viewContainerRefOrSettings;
return result;
}
if (moduleRefOrSettings && !('injector' in moduleRefOrSettings && 'componentFactoryResolver' in moduleRefOrSettings)) {
result = moduleRefOrSettings;
}
return result;
}
private getUserViewContainerOrModuleRef(
viewContainerRefOrSettings?: ViewContainerRef | OverlaySettings,
moduleRefOrSettings?: { injector: Injector, componentFactoryResolver: ComponentFactoryResolver } | OverlaySettings
): ViewContainerRef | { injector: Injector, componentFactoryResolver: ComponentFactoryResolver } | undefined {
let result: ViewContainerRef | { injector: Injector, componentFactoryResolver: ComponentFactoryResolver } | undefined;
if (viewContainerRefOrSettings instanceof ViewContainerRef) {
result = viewContainerRefOrSettings;
}
if (moduleRefOrSettings && 'injector' in moduleRefOrSettings && 'componentFactoryResolver' in moduleRefOrSettings) {
result = moduleRefOrSettings;
}
return result;
}
private getOverlayInfo(
component: ElementRef | Type<any>,
viewContainerRef?: { injector: Injector, componentFactoryResolver: ComponentFactoryResolver } | ViewContainerRef): OverlayInfo | null {
const info: OverlayInfo = { ngZone: this._zone, transformX: 0, transformY: 0 };
if (component instanceof ElementRef) {
info.elementRef = component;
} else {
let dynamicComponent: ComponentRef<any>;
if (viewContainerRef instanceof ViewContainerRef) {
dynamicComponent = viewContainerRef.createComponent(component);
} else {
let dynamicFactory: ComponentFactory<any>;
const factoryResolver = viewContainerRef ? viewContainerRef.componentFactoryResolver : this._factoryResolver;
try {
dynamicFactory = factoryResolver.resolveComponentFactory(component);
} catch (error) {
console.error(error);
return null;
}
const injector = viewContainerRef ? viewContainerRef.injector : this._injector;
dynamicComponent = dynamicFactory.create(injector);
this._appRef.attachView(dynamicComponent.hostView);
}
if (dynamicComponent.onDestroy) {
dynamicComponent.onDestroy(() => {
if (!info.detached && this._overlayInfos.indexOf(info) !== -1) {
this.detach(info.id);
}
})
}
// If the element is newly created from a Component, it is wrapped in 'ng-component' tag - we do not want that.
const element = dynamicComponent.location.nativeElement;
info.elementRef = { nativeElement: element };
info.componentRef = dynamicComponent;
}
return info;
}
private placeElementHook(element: HTMLElement): HTMLElement {
if (!element.parentElement) {
return null;
}
const hook = this._document.createElement('div');
hook.style.display = 'none';
element.parentElement.insertBefore(hook, element);
return hook;
}
private moveElementToOverlay(info: OverlayInfo) {
info.wrapperElement = this.getWrapperElement();
const contentElement = this.getContentElement(info.wrapperElement, info.settings.modal);
this.getOverlayElement(info).appendChild(info.wrapperElement);
contentElement.appendChild(info.elementRef.nativeElement);
}
private getWrapperElement(): HTMLElement {
const wrapper: HTMLElement = this._document.createElement('div');
wrapper.classList.add('igx-overlay__wrapper');
return wrapper;
}
private getContentElement(wrapperElement: HTMLElement, modal: boolean): HTMLElement {
const content: HTMLElement = this._document.createElement('div');
if (modal) {
content.classList.add('igx-overlay__content--modal');
content.addEventListener('click', (ev: Event) => {
ev.stopPropagation();
});
} else {
content.classList.add('igx-overlay__content');
}
content.addEventListener('scroll', (ev: Event) => {
ev.stopPropagation();
});
// hide element to eliminate flickering. Show the element exactly before animation starts
wrapperElement.style.visibility = 'hidden';
wrapperElement.appendChild(content);
return content;
}
private getOverlayElement(info: OverlayInfo): HTMLElement {
if (info.settings.outlet) {
return info.settings.outlet.nativeElement || info.settings.outlet;
}
if (!this._overlayElement) {
this._overlayElement = this._document.createElement('div');
this._overlayElement.classList.add('igx-overlay');
this._document.body.appendChild(this._overlayElement);
}
return this._overlayElement;
}
private updateSize(info: OverlayInfo) {
if (info.componentRef) {
// if we are positioning component this is first time it gets visible
// and we can finally get its size
info.componentRef.changeDetectorRef.detectChanges();
info.initialSize = info.elementRef.nativeElement.getBoundingClientRect();
}
// set content div width only if element to show has width
if (info.initialSize.width !== 0) {
info.elementRef.nativeElement.parentElement.style.width = info.initialSize.width + 'px';
}
}
private closeDone(info: OverlayInfo) {
info.visible = false;
if (info.wrapperElement) {
// to eliminate flickering show the element just before animation start
info.wrapperElement.style.visibility = 'hidden';
}
if (!info.closeAnimationDetaching) {
this.closed.emit({ id: info.id, componentRef: info.componentRef, event: info.event });
}
delete info.event;
}
private cleanUp(info: OverlayInfo) {
const child: HTMLElement = info.elementRef.nativeElement;
const outlet = this.getOverlayElement(info);
// if same element is shown in other overlay outlet will not contain
// the element and we should not remove it form outlet
if (outlet.contains(child)) {
outlet.removeChild(child.parentNode.parentNode);
}
if (info.componentRef) {
this._appRef.detachView(info.componentRef.hostView);
info.componentRef.destroy();
delete info.componentRef;
}
if (info.hook) {
info.hook.parentElement.insertBefore(info.elementRef.nativeElement, info.hook);
info.hook.parentElement.removeChild(info.hook);
delete info.hook;
}
const index = this._overlayInfos.indexOf(info);
this._overlayInfos.splice(index, 1);
// this._overlayElement.parentElement check just for tests that manually delete the element
if (this._overlayInfos.length === 0) {
if (this._overlayElement && this._overlayElement.parentElement) {
this._overlayElement.parentElement.removeChild(this._overlayElement);
this._overlayElement = null;
}
this.removeCloseOnEscapeListener();
}
// clean all the resources attached to info
delete info.elementRef;
delete info.settings;
delete info.initialSize;
info.openAnimationDetaching = true;
info.openAnimationPlayer?.destroy();
delete info.openAnimationPlayer;
info.closeAnimationDetaching = true;
info.closeAnimationPlayer?.destroy();
delete info.closeAnimationPlayer;
delete info.ngZone;
delete info.wrapperElement;
info = null;
}
private playOpenAnimation(info: OverlayInfo) {
// if there is opening animation already started do nothing
if (info.openAnimationPlayer?.hasStarted()) {
return;
}
if (info.closeAnimationPlayer?.hasStarted()) {
const position = info.closeAnimationPlayer.position;
info.closeAnimationPlayer.reset();
info.openAnimationPlayer.init();
info.openAnimationPlayer.position = 1 - position;
}
this.animationStarting.emit({ id: info.id, animationPlayer: info.openAnimationPlayer, animationType: 'open' });
// to eliminate flickering show the element just before animation start
info.wrapperElement.style.visibility = '';
info.visible = true;
info.openAnimationPlayer.play();
}
private playCloseAnimation(info: OverlayInfo, event?: Event) {
// if there is closing animation already started do nothing
if (info.closeAnimationPlayer?.hasStarted()) {
return;
}
if (info.openAnimationPlayer?.hasStarted()) {
const position = info.openAnimationPlayer.position;
info.openAnimationPlayer.reset();
info.closeAnimationPlayer.init();
info.closeAnimationPlayer.position = 1 - position;
}
this.animationStarting.emit({ id: info.id, animationPlayer: info.closeAnimationPlayer, animationType: 'close' });
info.event = event;
info.closeAnimationPlayer.play();
}
// TODO: check if applyAnimationParams will work with complex animations
private applyAnimationParams(wrapperElement: HTMLElement, animationOptions: AnimationReferenceMetadata) {
if (!animationOptions) {
wrapperElement.style.transitionDuration = '0ms';
return;
}
if (!animationOptions.options || !animationOptions.options.params) {
return;
}
const params = animationOptions.options.params as IAnimationParams;
if (params.duration) {
wrapperElement.style.transitionDuration = params.duration;
}
if (params.easing) {
wrapperElement.style.transitionTimingFunction = params.easing;
}
}
private documentClicked = (ev: MouseEvent) => {
// if we get to modal overlay just return - we should not close anything under it
// if we get to non-modal overlay do the next:
// 1. Check it has close on outside click. If not go on to next overlay;
// 2. If true check if click is on the element. If it is on the element we have closed
// already all previous non-modal with close on outside click elements, so we return. If
// not close the overlay and check next
for (let i = this._overlayInfos.length; i--;) {
const info = this._overlayInfos[i];
if (info.settings.modal) {
return;
}
if (info.settings.closeOnOutsideClick) {
const target = ev.composed ? ev.composedPath()[0] : ev.target;
const overlayElement = info.elementRef.nativeElement;
// check if the click is on the overlay element or on an element from the exclusion list, and if so do not close the overlay
const excludeElements = info.settings.excludeFromOutsideClick ?
[...info.settings.excludeFromOutsideClick, overlayElement] : [overlayElement];
const isInsideClick: boolean = excludeElements.some(e => e.contains(target as Node));
if (isInsideClick) {
return;
// if the click is outside click, but close animation has started do nothing
} else if (!(info.closeAnimationPlayer?.hasStarted())) {
this._hide(info.id, ev);
}
}
}
};
private addOutsideClickListener(info: OverlayInfo) {
if (info.settings.closeOnOutsideClick) {
if (info.settings.modal) {
fromEvent(info.elementRef.nativeElement.parentElement.parentElement, 'click')
.pipe(takeUntil(this.destroy$))
.subscribe((e: Event) => this._hide(info.id, e));
} else if (
// if all overlays minus closing overlays equals one add the handler
this._overlayInfos.filter(x => x.settings.closeOnOutsideClick && !x.settings.modal).length -
this._overlayInfos.filter(x => x.settings.closeOnOutsideClick && !x.settings.modal &&
x.closeAnimationPlayer?.hasStarted()).length === 1) {
// click event is not fired on iOS. To make element "clickable" we are
// setting the cursor to pointer
if (this.platformUtil.isIOS && !this._cursorStyleIsSet) {
this._cursorOriginalValue = this._document.body.style.cursor;
this._document.body.style.cursor = 'pointer';
this._cursorStyleIsSet = true;
}
this._document.addEventListener('click', this.documentClicked, true);
}
}
}
private removeOutsideClickListener(info: OverlayInfo) {
if (info.settings.modal === false) {
let shouldRemoveClickEventListener = true;
this._overlayInfos.forEach(o => {
if (o.settings.modal === false && o.id !== info.id) {
shouldRemoveClickEventListener = false;
}
});
if (shouldRemoveClickEventListener) {
if (this._cursorStyleIsSet) {
this._document.body.style.cursor = this._cursorOriginalValue;
this._cursorOriginalValue = '';
this._cursorStyleIsSet = false;
}
this._document.removeEventListener('click', this.documentClicked, true);
}
}
}
private addResizeHandler() {
const closingOverlaysCount =
this._overlayInfos
.filter(o => o.closeAnimationPlayer?.hasStarted())
.length;
if (this._overlayInfos.length - closingOverlaysCount === 1) {
this._document.defaultView.addEventListener('resize', this.repositionAll);
}
}
private removeResizeHandler() {
const closingOverlaysCount =
this._overlayInfos
.filter(o => o.closeAnimationPlayer?.hasStarted())
.length;
if (this._overlayInfos.length - closingOverlaysCount === 1) {
this._document.defaultView.removeEventListener('resize', this.repositionAll);
}
}
private addCloseOnEscapeListener(info: OverlayInfo) {
if (info.settings.closeOnEscape && !this._keyPressEventListener) {
this._keyPressEventListener = fromEvent(this._document, 'keydown').pipe(
filter((ev: KeyboardEvent) => ev.key === 'Escape' || ev.key === 'Esc')
).subscribe((ev) => {
const visibleOverlays = this._overlayInfos.filter(o => o.visible);
if (visibleOverlays.length < 1) {
return;
}
const targetOverlayInfo = visibleOverlays[visibleOverlays.length - 1];
if (targetOverlayInfo.visible && targetOverlayInfo.settings.closeOnEscape) {
this.hide(targetOverlayInfo.id, ev);
}
});
}
}
private removeCloseOnEscapeListener() {
if (this._keyPressEventListener) {
this._keyPressEventListener.unsubscribe();
this._keyPressEventListener = null;
}
}
private addModalClasses(info: OverlayInfo) {
if (info.settings.modal) {
const wrapperElement = info.elementRef.nativeElement.parentElement.parentElement;
wrapperElement.classList.remove('igx-overlay__wrapper');
this.applyAnimationParams(wrapperElement, info.settings.positionStrategy.settings.openAnimation);
requestAnimationFrame(() => {
wrapperElement.classList.add('igx-overlay__wrapper--modal');
});
}
}
private removeModalClasses(info: OverlayInfo) {
if (info.settings.modal) {
const wrapperElement = info.elementRef.nativeElement.parentElement.parentElement;
this.applyAnimationParams(wrapperElement, info.settings.positionStrategy.settings.closeAnimation);
wrapperElement.classList.remove('igx-overlay__wrapper--modal');
wrapperElement.classList.add('igx-overlay__wrapper');
}
}
private buildAnimationPlayers(info: OverlayInfo) {
if (info.settings.positionStrategy.settings.openAnimation) {
info.openAnimationPlayer = this.animationService
.buildAnimation(info.settings.positionStrategy.settings.openAnimation, info.elementRef.nativeElement);
info.openAnimationPlayer.animationEnd
.pipe(takeUntil(this.destroy$))
.subscribe(() => this.openAnimationDone(info));
}
if (info.settings.positionStrategy.settings.closeAnimation) {
info.closeAnimationPlayer = this.animationService
.buildAnimation(info.settings.positionStrategy.settings.closeAnimation, info.elementRef.nativeElement);
info.closeAnimationPlayer.animationEnd
.pipe(takeUntil(this.destroy$))
.subscribe(() => this.closeAnimationDone(info));
}
}
private openAnimationDone(info: OverlayInfo) {
if (!info.openAnimationDetaching) {
this.opened.emit({ id: info.id, componentRef: info.componentRef });
}
if (info.openAnimationPlayer) {
info.openAnimationPlayer.reset();
}
if (info.closeAnimationPlayer?.hasStarted()) {
info.closeAnimationPlayer.reset();
}
}
private closeAnimationDone(info: OverlayInfo) {
if (info.closeAnimationPlayer) {
info.closeAnimationPlayer.reset();
}
if (info.openAnimationPlayer?.hasStarted()) {
info.openAnimationPlayer.reset();
}
this.closeDone(info);
}
private finishAnimations(info: OverlayInfo) {
// // TODO: should we emit here opened or closed events
if (info.openAnimationPlayer?.hasStarted()) {
info.openAnimationPlayer.finish();
}
if (info.closeAnimationPlayer?.hasStarted()) {
info.closeAnimationPlayer.finish();
}
}
}