StackGenVis: Alignment of Data, Algorithms, and Models for Stacking Ensemble Learning Using Performance Metrics
https://doi.org/10.1109/TVCG.2020.3030352
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1112 lines
34 KiB
1112 lines
34 KiB
4 years ago
|
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 <div>
|
||
|
|
||
|
return h('div', {
|
||
|
key: "modal-outer-".concat(this._uid),
|
||
|
style: this.modalOuterStyle,
|
||
|
attrs: _objectSpread({}, scopedStyleAttrs, {}, this.$attrs, {
|
||
|
id: this.safeId('__BV_modal_outer_')
|
||
|
})
|
||
|
}, [modal, backdrop]);
|
||
|
}
|
||
|
},
|
||
|
render: function render(h) {
|
||
|
if (this.static) {
|
||
|
return this.lazy && this.isHidden ? h() : this.makeModal(h);
|
||
|
} else {
|
||
|
return this.isHidden ? h() : h(BTransporterSingle, [this.makeModal(h)]);
|
||
|
}
|
||
|
}
|
||
|
});
|