ngx-bootstrap
Version:
Angular Bootstrap
352 lines • 12.7 kB
JavaScript
// todo: should we support enforce focus in?
// todo: in original bs there are was a way to prevent modal from showing
// todo: original modal had resize events
import { Directive, ElementRef, EventEmitter, HostListener, Input, Output, Renderer2, ViewContainerRef, Optional, Inject } from '@angular/core';
import { document, window, isBs3, Utils } from 'ngx-bootstrap/utils';
import { ModalBackdropComponent } from './modal-backdrop.component';
import { CLASS_NAME, DISMISS_REASONS, modalConfigDefaults, ModalOptions, MODAL_CONFIG_DEFAULT_OVERRIDE } from './modal-options.class';
import { ComponentLoaderFactory } from 'ngx-bootstrap/component-loader';
const TRANSITION_DURATION = 300;
const BACKDROP_TRANSITION_DURATION = 150;
/** Mark any code with directive to show it's content in modal */
export class ModalDirective {
constructor(_element, _viewContainerRef, _renderer, clf, modalDefaultOption) {
this._element = _element;
this._renderer = _renderer;
/** This event fires immediately when the `show` instance method is called. */
this.onShow = new EventEmitter();
/** This event is fired when the modal has been made visible to the user
* (will wait for CSS transitions to complete)
*/
this.onShown = new EventEmitter();
/** This event is fired immediately when
* the hide instance method has been called.
*/
this.onHide = new EventEmitter();
/** This event is fired when the modal has finished being
* hidden from the user (will wait for CSS transitions to complete).
*/
this.onHidden = new EventEmitter();
this._isShown = false;
this.isBodyOverflowing = false;
this.originalBodyPadding = 0;
this.scrollbarWidth = 0;
this.timerHideModal = 0;
this.timerRmBackDrop = 0;
this.isNested = false;
this.clickStartedInContent = false;
this._backdrop = clf.createLoader(_element, _viewContainerRef, _renderer);
this._config = modalDefaultOption || modalConfigDefaults;
}
/** allows to set modal configuration via element property */
set config(conf) {
this._config = this.getConfig(conf);
}
get config() {
return this._config;
}
get isShown() {
return this._isShown;
}
onClickStarted(event) {
this.clickStartedInContent = event.target !== this._element.nativeElement;
}
onClickStop(event) {
const clickedInBackdrop = event.target === this._element.nativeElement && !this.clickStartedInContent;
if (this.config.ignoreBackdropClick ||
this.config.backdrop === 'static' ||
!clickedInBackdrop) {
this.clickStartedInContent = false;
return;
}
this.dismissReason = DISMISS_REASONS.BACKRDOP;
this.hide(event);
}
// todo: consider preventing default and stopping propagation
onEsc(event) {
if (!this._isShown) {
return;
}
if (event.keyCode === 27 || event.key === 'Escape') {
event.preventDefault();
}
if (this.config.keyboard) {
this.dismissReason = DISMISS_REASONS.ESC;
this.hide();
}
}
ngOnDestroy() {
if (this._isShown) {
this._isShown = false;
this.hideModal();
this._backdrop.dispose();
}
}
ngOnInit() {
this._config = this._config || this.getConfig();
setTimeout(() => {
if (this._config.show) {
this.show();
}
}, 0);
}
/* Public methods */
/** Allows to manually toggle modal visibility */
toggle() {
return this._isShown ? this.hide() : this.show();
}
/** Allows to manually open modal */
show() {
this.dismissReason = void 0;
this.onShow.emit(this);
if (this._isShown) {
return;
}
clearTimeout(this.timerHideModal);
clearTimeout(this.timerRmBackDrop);
this._isShown = true;
this.checkScrollbar();
this.setScrollbar();
if (document && document.body) {
if (document.body.classList.contains(CLASS_NAME.OPEN)) {
this.isNested = true;
}
else {
this._renderer.addClass(document.body, CLASS_NAME.OPEN);
}
}
this.showBackdrop(() => {
this.showElement();
});
}
/** Check if we can close the modal */
hide(event) {
if (!this._isShown) {
return;
}
if (event) {
event.preventDefault();
}
if (this.config.closeInterceptor) {
this.config.closeInterceptor().then(() => this._hide(), () => undefined);
return;
}
this._hide();
}
/** Private methods @internal */
/**
* Manually close modal
* @internal
*/
_hide() {
this.onHide.emit(this);
window.clearTimeout(this.timerHideModal);
window.clearTimeout(this.timerRmBackDrop);
this._isShown = false;
this._renderer.removeClass(this._element.nativeElement, CLASS_NAME.IN);
if (!isBs3()) {
this._renderer.removeClass(this._element.nativeElement, CLASS_NAME.SHOW);
}
// this._addClassIn = false;
if (this._config.animated) {
this.timerHideModal = window.setTimeout(() => this.hideModal(), TRANSITION_DURATION);
}
else {
this.hideModal();
}
}
getConfig(config) {
return Object.assign({}, this._config, config);
}
/**
* Show dialog
* @internal
*/
showElement() {
// todo: replace this with component loader usage
if (!this._element.nativeElement.parentNode ||
this._element.nativeElement.parentNode.nodeType !== Node.ELEMENT_NODE) {
// don't move modals dom position
if (document && document.body) {
document.body.appendChild(this._element.nativeElement);
}
}
this._renderer.setAttribute(this._element.nativeElement, 'aria-hidden', 'false');
this._renderer.setAttribute(this._element.nativeElement, 'aria-modal', 'true');
this._renderer.setStyle(this._element.nativeElement, 'display', 'block');
this._renderer.setProperty(this._element.nativeElement, 'scrollTop', 0);
if (this._config.animated) {
Utils.reflow(this._element.nativeElement);
}
// this._addClassIn = true;
this._renderer.addClass(this._element.nativeElement, CLASS_NAME.IN);
if (!isBs3()) {
this._renderer.addClass(this._element.nativeElement, CLASS_NAME.SHOW);
}
const transitionComplete = () => {
if (this._config.focus) {
this._element.nativeElement.focus();
}
this.onShown.emit(this);
};
if (this._config.animated) {
setTimeout(transitionComplete, TRANSITION_DURATION);
}
else {
transitionComplete();
}
}
/** @internal */
hideModal() {
this._renderer.setAttribute(this._element.nativeElement, 'aria-hidden', 'true');
this._renderer.setStyle(this._element.nativeElement, 'display', 'none');
this.showBackdrop(() => {
if (!this.isNested) {
if (document && document.body) {
this._renderer.removeClass(document.body, CLASS_NAME.OPEN);
}
this.resetScrollbar();
}
this.resetAdjustments();
this.focusOtherModal();
this.onHidden.emit(this);
});
}
// todo: original show was calling a callback when done, but we can use
// promise
/** @internal */
showBackdrop(callback) {
if (this._isShown &&
this.config.backdrop &&
(!this.backdrop || !this.backdrop.instance.isShown)) {
this.removeBackdrop();
this._backdrop
.attach(ModalBackdropComponent)
.to('body')
.show({ isAnimated: this._config.animated });
this.backdrop = this._backdrop._componentRef;
if (!callback) {
return;
}
if (!this._config.animated) {
callback();
return;
}
setTimeout(callback, BACKDROP_TRANSITION_DURATION);
}
else if (!this._isShown && this.backdrop) {
this.backdrop.instance.isShown = false;
const callbackRemove = () => {
this.removeBackdrop();
if (callback) {
callback();
}
};
if (this.backdrop.instance.isAnimated) {
this.timerRmBackDrop = window.setTimeout(callbackRemove, BACKDROP_TRANSITION_DURATION);
}
else {
callbackRemove();
}
}
else if (callback) {
callback();
}
}
/** @internal */
removeBackdrop() {
this._backdrop.hide();
}
/** Events tricks */
// no need for it
// protected setEscapeEvent():void {
// if (this._isShown && this._config.keyboard) {
// $(this._element).on(Event.KEYDOWN_DISMISS, (event) => {
// if (event.which === 27) {
// this.hide()
// }
// })
//
// } else if (!this._isShown) {
// $(this._element).off(Event.KEYDOWN_DISMISS)
// }
// }
// protected setResizeEvent():void {
// console.log(this.renderer.listenGlobal('', Event.RESIZE));
// if (this._isShown) {
// $(window).on(Event.RESIZE, $.proxy(this._handleUpdate, this))
// } else {
// $(window).off(Event.RESIZE)
// }
// }
focusOtherModal() {
if (this._element.nativeElement.parentElement == null) {
return;
}
const otherOpenedModals = this._element.nativeElement.parentElement.querySelectorAll('.in[bsModal]');
if (!otherOpenedModals.length) {
return;
}
otherOpenedModals[otherOpenedModals.length - 1].focus();
}
/** @internal */
resetAdjustments() {
this._renderer.setStyle(this._element.nativeElement, 'paddingLeft', '');
this._renderer.setStyle(this._element.nativeElement, 'paddingRight', '');
}
/** Scroll bar tricks */
/** @internal */
checkScrollbar() {
this.isBodyOverflowing = document.body.clientWidth < window.innerWidth;
this.scrollbarWidth = this.getScrollbarWidth();
}
setScrollbar() {
if (!document) {
return;
}
this.originalBodyPadding = parseInt(window
.getComputedStyle(document.body)
.getPropertyValue('padding-right') || 0, 10);
if (this.isBodyOverflowing) {
document.body.style.paddingRight = `${this.originalBodyPadding +
this.scrollbarWidth}px`;
}
}
resetScrollbar() {
document.body.style.paddingRight = `${this.originalBodyPadding}px`;
}
// thx d.walsh
getScrollbarWidth() {
const scrollDiv = this._renderer.createElement('div');
this._renderer.addClass(scrollDiv, CLASS_NAME.SCROLLBAR_MEASURER);
this._renderer.appendChild(document.body, scrollDiv);
const scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
this._renderer.removeChild(document.body, scrollDiv);
return scrollbarWidth;
}
}
ModalDirective.decorators = [
{ type: Directive, args: [{
selector: '[bsModal]',
exportAs: 'bs-modal'
},] }
];
ModalDirective.ctorParameters = () => [
{ type: ElementRef },
{ type: ViewContainerRef },
{ type: Renderer2 },
{ type: ComponentLoaderFactory },
{ type: ModalOptions, decorators: [{ type: Optional }, { type: Inject, args: [MODAL_CONFIG_DEFAULT_OVERRIDE,] }] }
];
ModalDirective.propDecorators = {
config: [{ type: Input }],
closeInterceptor: [{ type: Input }],
onShow: [{ type: Output }],
onShown: [{ type: Output }],
onHide: [{ type: Output }],
onHidden: [{ type: Output }],
onClickStarted: [{ type: HostListener, args: ['mousedown', ['$event'],] }],
onClickStop: [{ type: HostListener, args: ['mouseup', ['$event'],] }],
onEsc: [{ type: HostListener, args: ['keydown.esc', ['$event'],] }]
};
//# sourceMappingURL=modal.directive.js.map