import Vue from "vue"; import { Component, Prop, Watch } from "vue-property-decorator"; import Popper, { Placement } from "popper.js"; import { defaultBreakpoints } from "../../filters"; /** * How popup will be presented to user: * - "modal" - modal window * - "popup" - simple popup */ export type DialogBehaviour = "modal" | "popup"; let openModalCounter: number = 0; function computeZIndexOfElement(element: HTMLElement): number { let current = element; while (true) { const zIndex = window.getComputedStyle(current).zIndex; if (zIndex !== "auto") { return parseInt(zIndex); } if (!current.parentElement) { break; } current = current.parentElement; } return 0; } @Component({ inheritAttrs: false, template: require('../../../components/ui/dialog.html'), }) export default class UiDialog extends Vue { @Prop({ type: String, default: "popup" }) private behaviour: DialogBehaviour; @Prop({ type: String }) private mobileBehaviour: DialogBehaviour; @Prop([String, HTMLElement]) public reference: string | HTMLElement; @Prop(Object) public refs: string; @Prop({ type: String, default: "auto" }) public placement: Placement; @Prop(Boolean) public arrow: boolean; @Prop({ type: Boolean, default: true }) public responsive: boolean; @Prop(String) public title: string; private isMobile: boolean = false; /** Inherited class hack */ private staticClass: string[] = []; private zIndex: number = 1000; private _focusOutEvent; private _resizeEvent; private _popper; get attrs() { return { ...this.$attrs, "class": this.staticClass } } get currentBehaviour(): DialogBehaviour { if (!this.mobileBehaviour) { return this.behaviour; } return this.isMobile ? this.mobileBehaviour : this.behaviour; } get hasFooter() { return this.$hasSlot('footer') } get hasHeader() { return this.$hasSlot('header') } private getReferenceElement() { const isInWrapper = this.$parent.$options.name == 'portalTarget'; if (typeof this.reference === 'string') { if (this.refs) { return this.refs[this.reference]; } if (isInWrapper) { return this.$parent.$parent.$refs[this.reference]; } return this.$parent.$refs[this.reference]; } if (this.reference instanceof HTMLElement) { return this.reference; } return isInWrapper ? this.$parent.$el : this.$el.parentElement; } focusOut(event: MouseEvent) { if (this.$el.contains(event.target as Node)) { return; } this.$emit('leave', event); } mounted() { this.zIndex = computeZIndexOfElement(this.getReferenceElement()) + 100; this.handleWindowResize(); if (this.behaviour === 'popup') { this.mountPopper(); } this.staticClass = Array.from(this.$el.classList).filter(cls => ["ui-backdrop", "ui-popup", "ui-popup--arrow"].indexOf(cls) === -1); window.addEventListener('resize', this._resizeEvent = this.handleWindowResize.bind(this)); this._activated(); } private _activated() { if (this.behaviour === 'modal') { this.mountModal(); } } private _deactivated() { if (this.behaviour === 'modal') { this.dismountModal(); } } private mountModal() { if (openModalCounter === 0) { document.body.style.paddingRight = `${window.screen.width - document.body.clientWidth}px` document.body.classList.add('contains-modal'); } openModalCounter++; } private dismountModal() { openModalCounter--; if (openModalCounter === 0) { document.body.style.paddingRight = ""; document.body.classList.remove('contains-modal'); } } activated() { this._activated(); } deactivated() { this._deactivated(); } private mountPopper() { const reference = this.getReferenceElement(); this._popper = new Popper(reference, this.$el, { placement: this.placement, modifiers: { arrow: { enabled: this.arrow, element: this.$refs['arrow'] as Element }, responsive: { enabled: this.responsive, order: 890, fn(data) { if (window.innerWidth < 560) { data.instance.options.placement = 'top'; data.styles.transform = `translate3d(0, ${ data.offsets.popper.top }px, 0)`; data.styles.right = '0'; data.styles.left = '0'; data.styles.width = 'auto'; data.arrowStyles.left = `${ data.offsets.popper.left + data.offsets.arrow.left }px`; } return data; } } } }); this.$nextTick(() => { this._popper && this._popper.update(); document.addEventListener('click', this._focusOutEvent = this.focusOut.bind(this), { capture: true }); }); } private removePopper() { this._popper.destroy() this._popper = null; } updated() { if (this._popper) { this._popper.update(); } } beforeDestroy() { this._focusOutEvent && document.removeEventListener('click', this._focusOutEvent, { capture: true }); this._deactivated() } removed() { if (this._popper) { this.removePopper(); } } private handleBackdropClick(ev: Event) { const target = ev.target as HTMLElement; if (target.classList.contains("ui-backdrop")) { this.$emit('leave'); } } private handleCloseClick() { this.$emit('leave'); this.$emit('close'); } private handleWindowResize() { this.isMobile = screen.width < defaultBreakpoints.md; } @Watch('currentBehaviour') private handleBehaviourChange(newBehaviour: DialogBehaviour, oldBehaviour: DialogBehaviour) { if (oldBehaviour === 'popup') { this.removePopper(); } if (newBehaviour === 'popup') { this.$nextTick(() => this.mountPopper()); } if (newBehaviour === 'modal') { this.mountModal(); } if (oldBehaviour === 'modal') { this.dismountModal(); } } } Vue.component("ui-dialog", UiDialog);