function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } import Vue from '../../utils/vue'; import BVTransition from '../../utils/bv-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 { BTransporterSingle } 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 { BButton } from '../button/button'; import { BButtonClose } from '../button/button-close'; import { modalManager } from './helpers/modal-manager'; import { BvModalEvent } from './helpers/bv-modal-event.class'; // --- Constants --- var NAME = 'BModal'; // ObserveDom config to detect changes in modal content // so that we can adjust the modal padding if needed var 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) var TABABLE_SELECTOR = ['button', '[href]:not(.disabled)', 'input', 'select', 'textarea', '[tabindex]', '[contenteditable]'].map(function (s) { return "".concat(s, ":not(:disabled):not([disabled])"); }).join(', '); // --- Utility methods --- // Attempt to focus an element, and return true if successful var attemptFocus = function attemptFocus(el) { if (el && isVisible(el) && el.focus) { try { el.focus(); } catch (_unused) {} } // If the element has focus, then return true return document.activeElement === el; }; // --- Props --- export var props = { size: { type: String, default: function _default() { return 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: function _default() { return getComponentConfig(NAME, 'titleTag'); } }, titleClass: { type: [String, Array, Object], default: null }, titleSrOnly: { type: Boolean, default: false }, ariaLabel: { type: String, default: null }, headerBgVariant: { type: String, default: function _default() { return getComponentConfig(NAME, 'headerBgVariant'); } }, headerBorderVariant: { type: String, default: function _default() { return getComponentConfig(NAME, 'headerBorderVariant'); } }, headerTextVariant: { type: String, default: function _default() { return getComponentConfig(NAME, 'headerTextVariant'); } }, headerCloseVariant: { type: String, default: function _default() { return getComponentConfig(NAME, 'headerCloseVariant'); } }, headerClass: { type: [String, Array, Object], default: null }, bodyBgVariant: { type: String, default: function _default() { return getComponentConfig(NAME, 'bodyBgVariant'); } }, bodyTextVariant: { type: String, default: function _default() { return 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: function _default() { return getComponentConfig(NAME, 'footerBgVariant'); } }, footerBorderVariant: { type: String, default: function _default() { return getComponentConfig(NAME, 'footerBorderVariant'); } }, footerTextVariant: { type: String, default: function _default() { return getComponentConfig(NAME, 'footerTextVariant'); } }, footerClass: { type: [String, Array, Object], default: null }, hideHeader: { type: Boolean, default: false }, hideFooter: { type: Boolean, default: false }, hideHeaderClose: { type: Boolean, default: false }, 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: function _default() { return getComponentConfig(NAME, 'headerCloseContent'); } }, headerCloseLabel: { type: String, default: function _default() { return getComponentConfig(NAME, 'headerCloseLabel'); } }, cancelTitle: { type: String, default: function _default() { return getComponentConfig(NAME, 'cancelTitle'); } }, cancelTitleHtml: { type: String }, okTitle: { type: String, default: function _default() { return getComponentConfig(NAME, 'okTitle'); } }, okTitleHtml: { type: String }, cancelVariant: { type: String, default: function _default() { return getComponentConfig(NAME, 'cancelVariant'); } }, okVariant: { type: String, default: function _default() { return getComponentConfig(NAME, 'okVariant'); } }, lazy: { type: Boolean, default: false }, busy: { type: Boolean, default: false }, static: { type: Boolean, default: false }, autoFocusButton: { type: String, default: null, validator: function validator(val) { /* istanbul ignore next */ return isUndefinedOrNull(val) || arrayIncludes(['ok', 'cancel', 'close'], val); } } }; // @vue/component export var BModal = /*#__PURE__*/ Vue.extend({ name: NAME, mixins: [idMixin, listenOnDocumentMixin, listenOnRootMixin, listenOnWindowMixin, normalizeSlotMixin, scopedStyleAttrsMixin], inheritAttrs: false, model: { prop: 'visible', event: 'change' }, props: props, data: function 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: function modalClasses() { return [{ fade: !this.noFade, show: this.isShow }, this.modalClass]; }, modalStyles: function modalStyles() { var sbWidth = "".concat(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: function dialogClasses() { var _ref; return [(_ref = {}, _defineProperty(_ref, "modal-".concat(this.size), this.size), _defineProperty(_ref, 'modal-dialog-centered', this.centered), _defineProperty(_ref, 'modal-dialog-scrollable', this.scrollable), _ref), this.dialogClass]; }, headerClasses: function headerClasses() { var _ref2; return [(_ref2 = {}, _defineProperty(_ref2, "bg-".concat(this.headerBgVariant), this.headerBgVariant), _defineProperty(_ref2, "text-".concat(this.headerTextVariant), this.headerTextVariant), _defineProperty(_ref2, "border-".concat(this.headerBorderVariant), this.headerBorderVariant), _ref2), this.headerClass]; }, titleClasses: function titleClasses() { return [{ 'sr-only': this.titleSrOnly }, this.titleClass]; }, bodyClasses: function bodyClasses() { var _ref3; return [(_ref3 = {}, _defineProperty(_ref3, "bg-".concat(this.bodyBgVariant), this.bodyBgVariant), _defineProperty(_ref3, "text-".concat(this.bodyTextVariant), this.bodyTextVariant), _ref3), this.bodyClass]; }, footerClasses: function footerClasses() { var _ref4; return [(_ref4 = {}, _defineProperty(_ref4, "bg-".concat(this.footerBgVariant), this.footerBgVariant), _defineProperty(_ref4, "text-".concat(this.footerTextVariant), this.footerTextVariant), _defineProperty(_ref4, "border-".concat(this.footerBorderVariant), this.footerBorderVariant), _ref4), this.footerClass]; }, modalOuterStyle: function modalOuterStyle() { // Styles needed for proper stacking of modals return { position: 'absolute', zIndex: this.zIndex }; }, slotScope: function slotScope() { return { ok: this.onOk, cancel: this.onCancel, close: this.onClose, hide: this.hide, visible: this.isVisible }; }, computeIgnoreEnforceFocusSelector: function computeIgnoreEnforceFocusSelector() { // Normalize to an single selector with selectors separated by `,` return concat(this.ignoreEnforceFocusSelector).filter(identity).join(',').trim(); } }, watch: { visible: function visible(newVal, oldVal) { if (newVal !== oldVal) { this[newVal ? 'show' : 'hide'](); } } }, created: function created() { // Define non-reactive properties this._observer = null; }, mounted: function 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('bv::show::modal', this.showHandler); this.listenOnRoot('bv::hide::modal', this.hideHandler); this.listenOnRoot('bv::toggle::modal', this.toggleHandler); // Listen for `bv:modal::show events`, and close ourselves if the // opening modal not us this.listenOnRoot('bv::modal::show', this.modalListener); // Initially show modal? if (this.visible === true) { this.$nextTick(this.show); } }, beforeDestroy: function 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: function updateModel(val) { if (val !== this.visible) { this.$emit('change', val); } }, // Private method to create a BvModalEvent object buildEvent: function buildEvent(type) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; return new BvModalEvent(type, _objectSpread({ // Default options cancelable: false, target: this.$refs.modal || this.$el || null, relatedTarget: null, trigger: null }, options, { // Options that can't be overridden vueTarget: this, componentId: this.safeId() })); }, // Public method to show modal show: function 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(); var 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: function hide() { var trigger = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; if (!this.isVisible || this.isClosing) { /* istanbul ignore next */ return; } this.isClosing = true; var 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: function 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: function getActiveElement() { if (isBrowser) { var 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: function 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(function (i) { return i.tabIndex > -1 && !i.disabled; }); }, // Private method to finish showing modal doShow: function doShow() { var _this = this; /* 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('bv::modal::hidden', this.doShow); return; } modalManager.registerModal(this); // Place modal in DOM this.isHidden = false; this.$nextTick(function () { // 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(function () { // 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: function onBeforeEnter() { this.isTransitioning = true; this.setResizeEvent(true); }, onEnter: function onEnter() { var _this2 = this; 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(function () { requestAF(function () { _this2.isShow = true; }); }); }, onAfterEnter: function onAfterEnter() { var _this3 = this; 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(function () { _this3.emitEvent(_this3.buildEvent('shown')); _this3.setEnforceFocus(true); _this3.$nextTick(function () { // Delayed in a `$nextTick()` to allow users time to pre-focus // an element if the wish _this3.focusFirst(); }); }); }, onBeforeLeave: function onBeforeLeave() { this.isTransitioning = true; this.setResizeEvent(false); this.setEnforceFocus(false); }, onLeave: function onLeave() { // Remove the 'show' class this.isShow = false; }, onAfterLeave: function onAfterLeave() { var _this4 = this; this.isBlock = false; this.isTransitioning = false; this.isModalOverflowing = false; this.isHidden = true; this.$nextTick(function () { _this4.isClosing = false; modalManager.unregisterModal(_this4); _this4.returnFocusTo(); // TODO: Need to find a way to pass the `trigger` property // to the `hidden` event, not just only the `hide` event _this4.emitEvent(_this4.buildEvent('hidden')); }); }, // Event emitter emitEvent: function emitEvent(bvModalEvt) { var type = bvModalEvt.type; // We emit on root first incase a global listener wants to cancel // the event first before the instance emits its event this.emitOnRoot("bv::modal::".concat(type), bvModalEvt, bvModalEvt.componentId); this.$emit(type, bvModalEvt); }, // UI event handlers onDialogMousedown: function onDialogMousedown() { var _this5 = this; // Watch to see if the matching mouseup event occurs outside the dialog // And if it does, cancel the clickOut handler var modal = this.$refs.modal; var onceModalMouseup = function onceModalMouseup(evt) { eventOff(modal, 'mouseup', onceModalMouseup, EVENT_OPTIONS_NO_CAPTURE); if (evt.target === modal) { _this5.ignoreBackdropClick = true; } }; eventOn(modal, 'mouseup', onceModalMouseup, EVENT_OPTIONS_NO_CAPTURE); }, onClickOut: function 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: function onOk() { this.hide('ok'); }, onCancel: function onCancel() { this.hide('cancel'); }, onClose: function onClose() { this.hide('headerclose'); }, onEsc: function onEsc(evt) { // If ESC pressed, hide modal if (evt.keyCode === KeyCodes.ESC && this.isVisible && !this.noCloseOnEsc) { this.hide('esc'); } }, // Document focusin listener focusHandler: function focusHandler(evt) { // If focus leaves modal content, bring it back var content = this.$refs.content; var target = evt.target; if (this.noEnforceFocus || !this.isTop || !this.isVisible || !content || document === target || contains(content, target) || this.computeIgnoreEnforceFocusSelector && closest(this.computeIgnoreEnforceFocusSelector, target, true)) { return; } var tabables = this.getTabables(); var _this$$refs = this.$refs, bottomTrap = _this$$refs.bottomTrap, topTrap = _this$$refs.topTrap; 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: function setEnforceFocus(on) { this.listenDocument(on, 'focusin', this.focusHandler); }, // Resize listener setResizeEvent: function setResizeEvent(on) { this.listenWindow(on, 'resize', this.checkModalOverflow); this.listenWindow(on, 'orientationchange', this.checkModalOverflow); }, // Root listener handlers showHandler: function showHandler(id, triggerEl) { if (id === this.safeId()) { this.return_focus = triggerEl || this.getActiveElement(); this.show(); } }, hideHandler: function hideHandler(id) { if (id === this.safeId()) { this.hide('event'); } }, toggleHandler: function toggleHandler(id, triggerEl) { if (id === this.safeId()) { this.toggle(triggerEl); } }, modalListener: function modalListener(bvEvt) { // If another modal opens, close this one if stacking not permitted if (this.noStacking && bvEvt.vueTarget !== this) { this.hide(); } }, // Focus control handlers focusFirst: function focusFirst() { var _this6 = this; // Don't try and focus if we are SSR if (isBrowser) { requestAF(function () { var modal = _this6.$refs.modal; var content = _this6.$refs.content; var activeElement = _this6.getActiveElement(); // If the modal contains the activeElement, we don't do anything if (modal && content && !(activeElement && contains(content, activeElement))) { var ok = _this6.$refs['ok-button']; var cancel = _this6.$refs['cancel-button']; var close = _this6.$refs['close-button']; // Focus the appropriate button or modal content wrapper var autoFocus = _this6.autoFocusButton; var 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) _this6.$nextTick(function () { modal.scrollTop = 0; }); } } }); } }, returnFocusTo: function returnFocusTo() { // Prefer `returnFocus` prop over event specified // `return_focus` value var el = this.returnFocus || this.return_focus || null; this.return_focus = null; this.$nextTick(function () { // 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: function checkModalOverflow() { if (this.isVisible) { var modal = this.$refs.modal; this.isModalOverflowing = modal.scrollHeight > document.documentElement.clientHeight; } }, makeModal: function makeModal(h) { // Modal header var header = h(); if (!this.hideHeader) { var modalHeader = this.normalizeSlot('modal-header', this.slotScope); if (!modalHeader) { var closeButton = h(); if (!this.hideHeaderClose) { closeButton = h(BButtonClose, { ref: 'close-button', props: { content: this.headerCloseContent, disabled: this.isTransitioning, ariaLabel: this.headerCloseLabel, textVariant: this.headerCloseVariant || this.headerTextVariant }, on: { click: this.onClose } }, [this.normalizeSlot('modal-header-close')]); } var domProps = !this.hasNormalizedSlot('modal-title') && this.titleHtml ? { innerHTML: this.titleHtml } : {}; modalHeader = [h(this.titleTag, { staticClass: 'modal-title', class: this.titleClasses, attrs: { id: this.safeId('__BV_modal_title_') }, domProps: domProps }, [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('__BV_modal_header_') } }, [modalHeader]); } // Modal body var body = h('div', { ref: 'body', staticClass: 'modal-body', class: this.bodyClasses, attrs: { id: this.safeId('__BV_modal_body_') } }, this.normalizeSlot('default', this.slotScope)); // Modal footer var footer = h(); if (!this.hideFooter) { var modalFooter = this.normalizeSlot('modal-footer', this.slotScope); if (!modalFooter) { var cancelButton = h(); if (!this.okOnly) { var cancelHtml = this.cancelTitleHtml ? { innerHTML: this.cancelTitleHtml } : null; cancelButton = h(BButton, { ref: 'cancel-button', props: { variant: this.cancelVariant, size: this.buttonSize, disabled: this.cancelDisabled || this.busy || this.isTransitioning }, on: { click: this.onCancel } }, [this.normalizeSlot('modal-cancel') || (cancelHtml ? h('span', { domProps: cancelHtml }) : stripTags(this.cancelTitle))]); } var okHtml = this.okTitleHtml ? { innerHTML: this.okTitleHtml } : null; var okButton = h(BButton, { ref: 'ok-button', props: { variant: this.okVariant, size: this.buttonSize, disabled: this.okDisabled || this.busy || this.isTransitioning }, on: { click: this.onOk } }, [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('__BV_modal_footer_') } }, [modalFooter]); } // Assemble modal content var modalContent = h('div', { ref: 'content', staticClass: 'modal-content', class: this.contentClass, attrs: { role: 'document', id: this.safeId('__BV_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 var tabTrapTop = h(); var 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 var modalDialog = h('div', { ref: 'dialog', staticClass: 'modal-dialog', class: this.dialogClasses, on: { mousedown: this.onDialogMousedown } }, [tabTrapTop, modalContent, tabTrapBottom]); // Modal var 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 || !(this.hasNormalizedSlot('modal-title') || this.titleHtml || this.title) ? null : this.safeId('__BV_modal_title_'), 'aria-describedby': this.safeId('__BV_modal_body_') }, on: { keydown: this.onEsc, click: this.onClickOut } }, [modalDialog]); // Wrap modal in transition // Sadly, we can't use BVTransition 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 var backdrop = h(); if (!this.hideBackdrop && this.isVisible) { backdrop = h('div', { staticClass: 'modal-backdrop', attrs: { id: this.safeId('__BV_modal_backdrop_') } }, [this.normalizeSlot('modal-backdrop')]); } backdrop = h(BVTransition, { 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 var scopedStyleAttrs = !this.static ? this.scopedStyleAttrs : {}; // Assemble modal and backdrop in an outer