UNPKG

ngx-bootstrap

Version:
352 lines 12.7 kB
// 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