UNPKG

nly-adminlte-vue

Version:
1,165 lines (1,146 loc) 35.5 kB
import Vue from "../../utils/vue"; import NlyToastTransition from "../../utils/nly-toast-transition"; import KeyCodes from "../../utils/key-codes"; import identity from "../../utils/identity"; import observeDom from "../../utils/observe-dom"; import { arrayIncludes, concat } from "../../utils/array"; import { getComponentConfig } from "../../utils/config"; import { closest, contains, isVisible, requestAF, select, selectAll } from "../../utils/dom"; import { isBrowser } from "../../utils/env"; import { EVENT_OPTIONS_NO_CAPTURE, eventOn, eventOff } from "../../utils/events"; import { stripTags } from "../../utils/html"; import { isString, isUndefinedOrNull } from "../../utils/inspect"; import { HTMLElement } from "../../utils/safe-types"; import { NlyTransporterSingle } from "../../utils/transporter"; import idMixin from "../../mixins/id"; import listenOnDocumentMixin from "../../mixins/listen-on-document"; import listenOnRootMixin from "../../mixins/listen-on-root"; import listenOnWindowMixin from "../../mixins/listen-on-window"; import normalizeSlotMixin from "../../mixins/normalize-slot"; import scopedStyleAttrsMixin from "../../mixins/scoped-style-attrs"; import { NlyButton } from "../button/button"; import { NlyButtonClose } from "../button/button-close"; import { modalManager } from "./helpers/modal-manager"; import { NlyaModalEvent } from "./helpers/nly-modal-event.class"; // --- Constants --- const NAME = "NlyModal"; const OBSERVER_CONFIG = { subtree: true, childList: true, characterData: true, attributes: true, attributeFilter: ["style", "class"] }; // Query selector to find all tabbable elements // (includes tabindex="-1", which we filter out after) const TABABLE_SELECTOR = [ "button", "[href]:not(.disabled)", "input", "select", "textarea", "[tabindex]", "[contenteditable]" ] .map(s => `${s}:not(:disabled):not([disabled])`) .join(", "); // --- Utility methods --- // Attempt to focus an element, and return true if successful const attemptFocus = el => { if (el && isVisible(el) && el.focus) { try { el.focus(); // eslint-disable-next-line no-empty } catch {} } // If the element has focus, then return true return document.activeElement === el; }; // --- Props --- export const props = { size: { type: String, default: () => getComponentConfig(NAME, "size") }, centered: { type: Boolean, default: false }, scrollable: { type: Boolean, default: false }, buttonSize: { type: String // default: '' }, noStacking: { type: Boolean, default: false }, noFade: { type: Boolean, default: false }, noCloseOnBackdrop: { type: Boolean, default: false }, noCloseOnEsc: { type: Boolean, default: false }, noEnforceFocus: { type: Boolean, default: false }, ignoreEnforceFocusSelector: { type: [Array, String], default: "" }, title: { type: String, default: "" }, titleHtml: { type: String }, titleTag: { type: String, default: () => getComponentConfig(NAME, "titleTag") }, titleClass: { type: [String, Array, Object] // default: null }, titleSrOnly: { type: Boolean, default: false }, ariaLabel: { type: String // default: null }, headerBgVariant: { type: String, default: () => getComponentConfig(NAME, "headerBgVariant") }, headerBorderVariant: { type: String, default: () => getComponentConfig(NAME, "headerBorderVariant") }, headerTextVariant: { type: String, default: () => getComponentConfig(NAME, "headerTextVariant") }, headerCloseVariant: { type: String, default: () => getComponentConfig(NAME, "headerCloseVariant") }, headerClass: { type: [String, Array, Object] // default: null }, bodyBgVariant: { type: String, default: () => getComponentConfig(NAME, "bodyBgVariant") }, bodyTextVariant: { type: String, default: () => getComponentConfig(NAME, "bodyTextVariant") }, modalClass: { type: [String, Array, Object] // default: null }, dialogClass: { type: [String, Array, Object] // default: null }, contentClass: { type: [String, Array, Object] // default: null }, bodyClass: { type: [String, Array, Object] // default: null }, footerBgVariant: { type: String, default: () => getComponentConfig(NAME, "footerBgVariant") }, footerBorderVariant: { type: String, default: () => getComponentConfig(NAME, "footerBorderVariant") }, footerTextVariant: { type: String, default: () => getComponentConfig(NAME, "footerTextVariant") }, footerClass: { type: [String, Array, Object] // default: null }, // TODO: Rename to `noHeader` and deprecate `hideHeader` hideHeader: { type: Boolean, default: false }, // TODO: Rename to `noFooter` and deprecate `hideFooter` hideFooter: { type: Boolean, default: false }, // TODO: Rename to `noHeaderClose` and deprecate `hideHeaderClose` hideHeaderClose: { type: Boolean, default: false }, // TODO: Rename to `noBackdrop` and deprecate `hideBackdrop` hideBackdrop: { type: Boolean, default: false }, okOnly: { type: Boolean, default: false }, okDisabled: { type: Boolean, default: false }, cancelDisabled: { type: Boolean, default: false }, visible: { type: Boolean, default: false }, returnFocus: { // HTML Element, CSS selector string or Vue component instance type: [HTMLElement, String, Object], default: null }, headerCloseContent: { type: String, default: () => getComponentConfig(NAME, "headerCloseContent") }, headerCloseLabel: { type: String, default: () => getComponentConfig(NAME, "headerCloseLabel") }, cancelTitle: { type: String, default: () => getComponentConfig(NAME, "cancelTitle") }, cancelTitleHtml: { type: String }, okTitle: { type: String, default: () => getComponentConfig(NAME, "okTitle") }, okTitleHtml: { type: String }, cancelVariant: { type: String, default: () => getComponentConfig(NAME, "cancelVariant") }, okVariant: { type: String, default: () => getComponentConfig(NAME, "okVariant") }, lazy: { type: Boolean, default: false }, busy: { type: Boolean, default: false }, static: { type: Boolean, default: false }, autoFocusButton: { type: String, default: null, validator: val => { /* istanbul ignore next */ return ( isUndefinedOrNull(val) || arrayIncludes(["ok", "cancel", "close"], val) ); } } }; // @vue/component export const NlyModal = /*#__PURE__*/ Vue.extend({ name: NAME, mixins: [ idMixin, listenOnDocumentMixin, listenOnRootMixin, listenOnWindowMixin, normalizeSlotMixin, scopedStyleAttrsMixin ], inheritAttrs: false, model: { prop: "visible", event: "change" }, props, data() { return { isHidden: true, // If modal should not be in document isVisible: false, // Controls modal visible state isTransitioning: false, // Used for style control isShow: false, // Used for style control isBlock: false, // Used for style control isOpening: false, // To signal that the modal is in the process of opening isClosing: false, // To signal that the modal is in the process of closing ignoreBackdropClick: false, // Used to signify if click out listener should ignore the click isModalOverflowing: false, return_focus: this.returnFocus || null, // The following items are controlled by the modalManager instance scrollbarWidth: 0, zIndex: modalManager.getBaseZIndex(), isTop: true, isBodyOverflowing: false }; }, computed: { modalClasses() { return [ { fade: !this.noFade, show: this.isShow }, this.modalClass ]; }, modalStyles() { const sbWidth = `${this.scrollbarWidth}px`; return { paddingLeft: !this.isBodyOverflowing && this.isModalOverflowing ? sbWidth : "", paddingRight: this.isBodyOverflowing && !this.isModalOverflowing ? sbWidth : "", // Needed to fix issue https://github.com/bootstrap-vue/bootstrap-vue/issues/3457 // Even though we are using v-show, we must ensure 'none' is restored in the styles display: this.isBlock ? "block" : "none" }; }, dialogClasses() { return [ { [`modal-${this.size}`]: this.size, "modal-dialog-centered": this.centered, "modal-dialog-scrollable": this.scrollable }, this.dialogClass ]; }, headerClasses() { return [ { [`bg-${this.headerBgVariant}`]: this.headerBgVariant, [`text-${this.headerTextVariant}`]: this.headerTextVariant, [`border-${this.headerBorderVariant}`]: this.headerBorderVariant }, this.headerClass ]; }, titleClasses() { return [{ "sr-only": this.titleSrOnly }, this.titleClass]; }, bodyClasses() { return [ { [`bg-${this.bodyBgVariant}`]: this.bodyBgVariant, [`text-${this.bodyTextVariant}`]: this.bodyTextVariant }, this.bodyClass ]; }, footerClasses() { return [ { [`bg-${this.footerBgVariant}`]: this.footerBgVariant, [`text-${this.footerTextVariant}`]: this.footerTextVariant, [`border-${this.footerBorderVariant}`]: this.footerBorderVariant }, this.footerClass ]; }, modalOuterStyle() { // Styles needed for proper stacking of modals return { position: "absolute", zIndex: this.zIndex }; }, slotScope() { return { ok: this.onOk, cancel: this.onCancel, close: this.onClose, hide: this.hide, visible: this.isVisible }; }, computeIgnoreEnforceFocusSelector() { // Normalize to an single selector with selectors separated by `,` return concat(this.ignoreEnforceFocusSelector) .filter(identity) .join(",") .trim(); } }, watch: { visible(newVal, oldVal) { if (newVal !== oldVal) { this[newVal ? "show" : "hide"](); } } }, created() { // Define non-reactive properties this._observer = null; }, mounted() { // Set initial z-index as queried from the DOM this.zIndex = modalManager.getBaseZIndex(); // Listen for events from others to either open or close ourselves // and listen to all modals to enable/disable enforce focus this.listenOnRoot("nlya::show::modal", this.showHandler); this.listenOnRoot("nlya::hide::modal", this.hideHandler); this.listenOnRoot("nlya::toggle::modal", this.toggleHandler); // Listen for `nlya:modal::show events`, and close ourselves if the // opening modal not us this.listenOnRoot("nlya::modal::show", this.modalListener); // Initially show modal? if (this.visible === true) { this.$nextTick(this.show); } }, beforeDestroy() { // Ensure everything is back to normal if (this._observer) { this._observer.disconnect(); this._observer = null; } if (this.isVisible) { this.isVisible = false; this.isShow = false; this.isTransitioning = false; } }, methods: { // Private method to update the v-model updateModel(val) { if (val !== this.visible) { this.$emit("change", val); } }, // Private method to create a nlyaModalEvent object buildEvent(type, options = {}) { return new NlyaModalEvent(type, { // Default options cancelable: false, target: this.$refs.modal || this.$el || null, relatedTarget: null, trigger: null, // Supplied options ...options, // Options that can't be overridden vueTarget: this, componentId: this.safeId() }); }, // Public method to show modal show() { if (this.isVisible || this.isOpening) { // If already open, or in the process of opening, do nothing /* istanbul ignore next */ return; } /* istanbul ignore next */ if (this.isClosing) { // If we are in the process of closing, wait until hidden before re-opening /* istanbul ignore next */ this.$once("hidden", this.show); /* istanbul ignore next */ return; } this.isOpening = true; // Set the element to return focus to when closed this.return_focus = this.return_focus || this.getActiveElement(); const showEvt = this.buildEvent("show", { cancelable: true }); this.emitEvent(showEvt); // Don't show if canceled if (showEvt.defaultPrevented || this.isVisible) { this.isOpening = false; // Ensure the v-model reflects the current state this.updateModel(false); return; } // Show the modal this.doShow(); }, // Public method to hide modal hide(trigger = "") { if (!this.isVisible || this.isClosing) { /* istanbul ignore next */ return; } this.isClosing = true; const hideEvt = this.buildEvent("hide", { cancelable: trigger !== "FORCE", trigger: trigger || null }); // We emit specific event for one of the three built-in buttons if (trigger === "ok") { this.$emit("ok", hideEvt); } else if (trigger === "cancel") { this.$emit("cancel", hideEvt); } else if (trigger === "headerclose") { this.$emit("close", hideEvt); } this.emitEvent(hideEvt); // Hide if not canceled if (hideEvt.defaultPrevented || !this.isVisible) { this.isClosing = false; // Ensure v-model reflects current state this.updateModel(true); return; } // Stop observing for content changes if (this._observer) { this._observer.disconnect(); this._observer = null; } // Trigger the hide transition this.isVisible = false; // Update the v-model this.updateModel(false); }, // Public method to toggle modal visibility toggle(triggerEl) { if (triggerEl) { this.return_focus = triggerEl; } if (this.isVisible) { this.hide("toggle"); } else { this.show(); } }, // Private method to get the current document active element getActiveElement() { if (isBrowser) { const activeElement = document.activeElement; // Note: On IE 11, `document.activeElement` may be null. // So we test it for truthiness first. // https://github.com/bootstrap-vue/bootstrap-vue/issues/3206 // Returning focus to document.body may cause unwanted scrolls, so we // exclude setting focus on body if ( activeElement && activeElement !== document.body && activeElement.focus ) { // Preset the fallback return focus value if it is not set // `document.activeElement` should be the trigger element that was clicked or // in the case of using the v-model, which ever element has current focus // Will be overridden by some commands such as toggle, etc. return activeElement; } } return null; }, // Private method to get a list of all tabable elements within modal content getTabables() { // Find all tabable elements in the modal content // Assumes users have not used tabindex > 0 on elements! return selectAll(TABABLE_SELECTOR, this.$refs.content) .filter(isVisible) .filter(i => i.tabIndex > -1 && !i.disabled); }, // Private method to finish showing modal doShow() { /* istanbul ignore next: commenting out for now until we can test stacking */ if (modalManager.modalsAreOpen && this.noStacking) { // If another modal(s) is already open, wait for it(them) to close this.listenOnRootOnce("nlya::modal::hidden", this.doShow); return; } modalManager.registerModal(this); // Place modal in DOM this.isHidden = false; this.$nextTick(() => { // We do this in `$nextTick()` to ensure the modal is in DOM first // before we show it this.isVisible = true; this.isOpening = false; // Update the v-model this.updateModel(true); this.$nextTick(() => { // In a nextTick in case modal content is lazy // Observe changes in modal content and adjust if necessary this._observer = observeDom( this.$refs.content, this.checkModalOverflow.bind(this), OBSERVER_CONFIG ); }); }); }, // Transition handlers onBeforeEnter() { this.isTransitioning = true; this.setResizeEvent(true); }, onEnter() { this.isBlock = true; // We add the `show` class 1 frame later // `requestAF()` runs the callback before the next repaint, so we need // two calls to guarantee the next frame has been rendered requestAF(() => { requestAF(() => { this.isShow = true; }); }); }, onAfterEnter() { this.checkModalOverflow(); this.isTransitioning = false; // We use `requestAF()` to allow transition hooks to complete // before passing control over to the other handlers // This will allow users to not have to use `$nextTick()` or `requestAF()` // when trying to pre-focus an element requestAF(() => { this.emitEvent(this.buildEvent("shown")); this.setEnforceFocus(true); this.$nextTick(() => { // Delayed in a `$nextTick()` to allow users time to pre-focus // an element if the wish this.focusFirst(); }); }); }, onBeforeLeave() { this.isTransitioning = true; this.setResizeEvent(false); this.setEnforceFocus(false); }, onLeave() { // Remove the 'show' class this.isShow = false; }, onAfterLeave() { this.isBlock = false; this.isTransitioning = false; this.isModalOverflowing = false; this.isHidden = true; this.$nextTick(() => { this.isClosing = false; modalManager.unregisterModal(this); this.returnFocusTo(); // TODO: Need to find a way to pass the `trigger` property // to the `hidden` event, not just only the `hide` event this.emitEvent(this.buildEvent("hidden")); }); }, // Event emitter emitEvent(nlyaModalEvt) { const type = nlyaModalEvt.type; // We emit on root first incase a global listener wants to cancel // the event first before the instance emits its event this.emitOnRoot( `nlya::modal::${type}`, nlyaModalEvt, nlyaModalEvt.componentId ); this.$emit(type, nlyaModalEvt); }, // UI event handlers onDialogMousedown() { // Watch to see if the matching mouseup event occurs outside the dialog // And if it does, cancel the clickOut handler const modal = this.$refs.modal; const onceModalMouseup = evt => { eventOff(modal, "mouseup", onceModalMouseup, EVENT_OPTIONS_NO_CAPTURE); if (evt.target === modal) { this.ignoreBackdropClick = true; } }; eventOn(modal, "mouseup", onceModalMouseup, EVENT_OPTIONS_NO_CAPTURE); }, onClickOut(evt) { if (this.ignoreBackdropClick) { // Click was initiated inside the modal content, but finished outside. // Set by the above onDialogMousedown handler this.ignoreBackdropClick = false; return; } // Do nothing if not visible, backdrop click disabled, or element // that generated click event is no longer in document body if ( !this.isVisible || this.noCloseOnBackdrop || !contains(document.body, evt.target) ) { return; } // If backdrop clicked, hide modal if (!contains(this.$refs.content, evt.target)) { this.hide("backdrop"); } }, onOk() { this.hide("ok"); }, onCancel() { this.hide("cancel"); }, onClose() { this.hide("headerclose"); }, onEsc(evt) { // If ESC pressed, hide modal if ( evt.keyCode === KeyCodes.ESC && this.isVisible && !this.noCloseOnEsc ) { this.hide("esc"); } }, // Document focusin listener focusHandler(evt) { // If focus leaves modal content, bring it back const content = this.$refs.content; const { target } = evt; if ( this.noEnforceFocus || !this.isTop || !this.isVisible || !content || document === target || contains(content, target) || (this.computeIgnoreEnforceFocusSelector && closest(this.computeIgnoreEnforceFocusSelector, target, true)) ) { return; } const tabables = this.getTabables(); const { bottomTrap, topTrap } = this.$refs; if (bottomTrap && target === bottomTrap) { // If user pressed TAB out of modal into our bottom trab trap element // Find the first tabable element in the modal content and focus it if (attemptFocus(tabables[0])) { // Focus was successful return; } } else if (topTrap && target === topTrap) { // If user pressed CTRL-TAB out of modal and into our top tab trap element // Find the last tabable element in the modal content and focus it if (attemptFocus(tabables[tabables.length - 1])) { // Focus was successful return; } } // Otherwise focus the modal content container content.focus({ preventScroll: true }); }, // Turn on/off focusin listener setEnforceFocus(on) { this.listenDocument(on, "focusin", this.focusHandler); }, // Resize listener setResizeEvent(on) { this.listenWindow(on, "resize", this.checkModalOverflow); this.listenWindow(on, "orientationchange", this.checkModalOverflow); }, // Root listener handlers showHandler(id, triggerEl) { if (id === this.safeId()) { this.return_focus = triggerEl || this.getActiveElement(); this.show(); } }, hideHandler(id) { if (id === this.safeId()) { this.hide("event"); } }, toggleHandler(id, triggerEl) { if (id === this.safeId()) { this.toggle(triggerEl); } }, modalListener(nlyaEvt) { // If another modal opens, close this one if stacking not permitted if (this.noStacking && nlyaEvt.vueTarget !== this) { this.hide(); } }, // Focus control handlers focusFirst() { // Don't try and focus if we are SSR if (isBrowser) { requestAF(() => { const modal = this.$refs.modal; const content = this.$refs.content; const activeElement = this.getActiveElement(); // If the modal contains the activeElement, we don't do anything if ( modal && content && !(activeElement && contains(content, activeElement)) ) { const ok = this.$refs["ok-button"]; const cancel = this.$refs["cancel-button"]; const close = this.$refs["close-button"]; // Focus the appropriate button or modal content wrapper const autoFocus = this.autoFocusButton; const el = autoFocus === "ok" && ok ? ok.$el || ok : autoFocus === "cancel" && cancel ? cancel.$el || cancel : autoFocus === "close" && close ? close.$el || close : content; // Focus the element attemptFocus(el); if (el === content) { // Make sure top of modal is showing (if longer than the viewport) this.$nextTick(() => { modal.scrollTop = 0; }); } } }); } }, returnFocusTo() { // Prefer `returnFocus` prop over event specified // `return_focus` value let el = this.returnFocus || this.return_focus || null; this.return_focus = null; this.$nextTick(() => { // Is el a string CSS selector? el = isString(el) ? select(el) : el; if (el) { // Possibly could be a component reference el = el.$el || el; attemptFocus(el); } }); }, checkModalOverflow() { if (this.isVisible) { const modal = this.$refs.modal; this.isModalOverflowing = modal.scrollHeight > document.documentElement.clientHeight; } }, makeModal(h) { // Modal header let header = h(); if (!this.hideHeader) { // TODO: Rename slot to `header` and deprecate `modal-header` let modalHeader = this.normalizeSlot("modal-header", this.slotScope); if (!modalHeader) { let closeButton = h(); if (!this.hideHeaderClose) { closeButton = h( NlyButtonClose, { ref: "close-button", props: { content: this.headerCloseContent, disabled: this.isTransitioning, ariaLabel: this.headerCloseLabel, textVariant: this.headerCloseVariant || this.headerTextVariant }, on: { click: this.onClose } }, // TODO: Rename slot to `header-close` and deprecate `modal-header-close` [this.normalizeSlot("modal-header-close")] ); } const domProps = // TODO: Rename slot to `title` and deprecate `modal-title` !this.hasNormalizedSlot("modal-title") && this.titleHtml ? { innerHTML: this.titleHtml } : {}; modalHeader = [ h( this.titleTag, { staticClass: "modal-title", class: this.titleClasses, attrs: { id: this.safeId("__nlya_modal_title_") }, domProps }, // TODO: Rename slot to `title` and deprecate `modal-title` [ this.normalizeSlot("modal-title", this.slotScope) || stripTags(this.title) ] ), closeButton ]; } header = h( "header", { ref: "header", staticClass: "modal-header", class: this.headerClasses, attrs: { id: this.safeId("__nlya_modal_header_") } }, [modalHeader] ); } // Modal body const body = h( "div", { ref: "body", staticClass: "modal-body", class: this.bodyClasses, attrs: { id: this.safeId("__nlya_modal_body_") } }, this.normalizeSlot("default", this.slotScope) ); // Modal footer let footer = h(); if (!this.hideFooter) { // TODO: Rename slot to `footer` and deprecate `modal-footer` let modalFooter = this.normalizeSlot("modal-footer", this.slotScope); if (!modalFooter) { let cancelButton = h(); if (!this.okOnly) { const cancelHtml = this.cancelTitleHtml ? { innerHTML: this.cancelTitleHtml } : null; cancelButton = h( NlyButton, { ref: "cancel-button", props: { variant: this.cancelVariant, size: this.buttonSize, disabled: this.cancelDisabled || this.busy || this.isTransitioning }, on: { click: this.onCancel } }, [ // TODO: Rename slot to `cancel-button` and deprecate `modal-cancel` this.normalizeSlot("modal-cancel") || (cancelHtml ? h("span", { domProps: cancelHtml }) : stripTags(this.cancelTitle)) ] ); } const okHtml = this.okTitleHtml ? { innerHTML: this.okTitleHtml } : null; const okButton = h( NlyButton, { ref: "ok-button", props: { variant: this.okVariant, size: this.buttonSize, disabled: this.okDisabled || this.busy || this.isTransitioning }, on: { click: this.onOk } }, [ // TODO: Rename slot to `ok-button` and deprecate `modal-ok` this.normalizeSlot("modal-ok") || (okHtml ? h("span", { domProps: okHtml }) : stripTags(this.okTitle)) ] ); modalFooter = [cancelButton, okButton]; } footer = h( "footer", { ref: "footer", staticClass: "modal-footer", class: this.footerClasses, attrs: { id: this.safeId("__nlya_modal_footer_") } }, [modalFooter] ); } // Assemble modal content const modalContent = h( "div", { ref: "content", staticClass: "modal-content", class: this.contentClass, attrs: { role: "document", id: this.safeId("__nlya_modal_content_"), tabindex: "-1" } }, [header, body, footer] ); // Tab trap to prevent page from scrolling to next element in // tab index during enforce focus tab cycle let tabTrapTop = h(); let tabTrapBottom = h(); if (this.isVisible && !this.noEnforceFocus) { tabTrapTop = h("span", { ref: "topTrap", attrs: { tabindex: "0" } }); tabTrapBottom = h("span", { ref: "bottomTrap", attrs: { tabindex: "0" } }); } // Modal dialog wrapper const modalDialog = h( "div", { ref: "dialog", staticClass: "modal-dialog", class: this.dialogClasses, on: { mousedown: this.onDialogMousedown } }, [tabTrapTop, modalContent, tabTrapBottom] ); // Modal let modal = h( "div", { ref: "modal", staticClass: "modal", class: this.modalClasses, style: this.modalStyles, directives: [ { name: "show", rawName: "v-show", value: this.isVisible, expression: "isVisible" } ], attrs: { id: this.safeId(), role: "dialog", "aria-hidden": this.isVisible ? null : "true", "aria-modal": this.isVisible ? "true" : null, "aria-label": this.ariaLabel, "aria-labelledby": this.hideHeader || this.ariaLabel || // TODO: Rename slot to `title` and deprecate `modal-title` !( this.hasNormalizedSlot("modal-title") || this.titleHtml || this.title ) ? null : this.safeId("__nlya_modal_title_"), "aria-describedby": this.safeId("__nlya_modal_body_") }, on: { keydown: this.onEsc, click: this.onClickOut } }, [modalDialog] ); // Wrap modal in transition // Sadly, we can't use nlyaTransition here due to the differences in // transition durations for .modal and .modal-dialog. Not until // issue https://github.com/vuejs/vue/issues/9986 is resolved modal = h( "transition", { props: { enterClass: "", enterToClass: "", enterActiveClass: "", leaveClass: "", leaveActiveClass: "", leaveToClass: "" }, on: { beforeEnter: this.onBeforeEnter, enter: this.onEnter, afterEnter: this.onAfterEnter, beforeLeave: this.onBeforeLeave, leave: this.onLeave, afterLeave: this.onAfterLeave } }, [modal] ); // Modal backdrop let backdrop = h(); if (!this.hideBackdrop && this.isVisible) { backdrop = h( "div", { staticClass: "modal-backdrop", attrs: { id: this.safeId("__nlya_modal_backdrop_") } }, // TODO: Rename slot to `backdrop` and deprecate `modal-backdrop` [this.normalizeSlot("modal-backdrop")] ); } backdrop = h(NlyToastTransition, { props: { noFade: this.noFade } }, [ backdrop ]); // If the parent has a scoped style attribute, and the modal // is portalled, add the scoped attribute to the modal wrapper const scopedStyleAttrs = !this.static ? this.scopedStyleAttrs : {}; // Assemble modal and backdrop in an outer <div> return h( "div", { key: `modal-outer-${this._uid}`, style: this.modalOuterStyle, attrs: { ...scopedStyleAttrs, ...this.$attrs, id: this.safeId("__nlya_modal_outer_") } }, [modal, backdrop] ); } }, render(h) { if (this.static) { return this.lazy && this.isHidden ? h() : this.makeModal(h); } else { return this.isHidden ? h() : h(NlyTransporterSingle, [this.makeModal(h)]); } } });