bootstrap-vue
Version:
BootstrapVue provides one of the most comprehensive implementations of Bootstrap 4 components and grid system for Vue.js and with extensive and automated WAI-ARIA accessibility markup.
741 lines (718 loc) • 22.4 kB
JavaScript
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 bBtn from '../button/button';
import bBtnClose from '../button/button-close';
import idMixin from '../../mixins/id';
import listenOnRootMixin from '../../mixins/listen-on-root';
import observeDom from '../../utils/observe-dom';
import warn from '../../utils/warn';
import KeyCodes from '../../utils/key-codes';
import BvEvent from '../../utils/bv-event.class';
import { isVisible, selectAll, select, getBCR, addClass, removeClass, hasClass, setAttr, removeAttr, getAttr, hasAttr, eventOn, eventOff } from '../../utils/dom';
// Selectors for padding/margin adjustments
var Selector = {
FIXED_CONTENT: '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top',
STICKY_CONTENT: '.sticky-top',
NAVBAR_TOGGLER: '.navbar-toggler'
// ObserveDom config
};var OBSERVER_CONFIG = {
subtree: true,
childList: true,
characterData: true,
attributes: true,
attributeFilter: ['style', 'class']
};
export default {
mixins: [idMixin, listenOnRootMixin],
components: { bBtn: bBtn, bBtnClose: bBtnClose },
render: function render(h) {
var _this = this;
var t = this;
var $slots = t.$slots;
// Modal Header
var header = h(false);
if (!t.hideHeader) {
var modalHeader = $slots['modal-header'];
if (!modalHeader) {
var closeButton = h(false);
if (!t.hideHeaderClose) {
closeButton = h('b-btn-close', {
props: {
disabled: t.is_transitioning,
ariaLabel: t.headerCloseLabel,
textVariant: t.headerTextVariant
},
on: {
click: function click(evt) {
t.hide('header-close');
}
}
}, [$slots['modal-header-close']]);
}
modalHeader = [h(t.titleTag, { class: ['modal-title'] }, [$slots['modal-title'] || t.title]), closeButton];
}
header = h('header', {
ref: 'header',
class: t.headerClasses,
attrs: { id: t.safeId('__BV_modal_header_') }
}, [modalHeader]);
}
// Modal Body
var body = h('div', {
ref: 'body',
class: t.bodyClasses,
attrs: { id: t.safeId('__BV_modal_body_') }
}, [$slots.default]);
// Modal Footer
var footer = h(false);
if (!t.hideFooter) {
var modalFooter = $slots['modal-footer'];
if (!modalFooter) {
var okButton = h(false);
if (!t.okOnly) {
okButton = h('b-btn', {
props: {
variant: t.cancelVariant,
size: t.buttonSize,
disabled: t.cancelDisabled || t.busy || t.is_transitioning
},
on: {
click: function click(evt) {
t.hide('cancel');
}
}
}, [$slots['modal-cancel'] || t.cancelTitle]);
}
var cancelButton = h('b-btn', {
props: {
variant: t.okVariant,
size: t.buttonSize,
disabled: t.okDisabled || t.busy || t.is_transitioning
},
on: {
click: function click(evt) {
t.hide('ok');
}
}
}, [$slots['modal-ok'] || t.okTitle]);
modalFooter = [cancelButton, okButton];
}
footer = h('footer', {
ref: 'footer',
class: t.footerClasses,
attrs: { id: t.safeId('__BV_modal_footer_') }
}, [modalFooter]);
}
// Assemble Modal Content
var modalContent = h('div', {
ref: 'content',
class: ['modal-content'],
attrs: {
tabindex: '-1',
role: 'document',
'aria-labelledby': t.hideHeader ? null : t.safeId('__BV_modal_header_'),
'aria-describedby': t.safeId('__BV_modal_body_')
},
on: {
focusout: t.onFocusout,
click: function click(evt) {
evt.stopPropagation();
// https://github.com/bootstrap-vue/bootstrap-vue/issues/1528
_this.$root.$emit('bv::dropdown::shown');
}
}
}, [header, body, footer]);
// Modal Dialog wrapper
var modalDialog = h('div', { class: t.dialogClasses }, [modalContent]);
// Modal
var modal = h('div', {
ref: 'modal',
class: t.modalClasses,
directives: [{
name: 'show',
rawName: 'v-show',
value: t.is_visible,
expression: 'is_visible'
}],
attrs: {
id: t.safeId(),
role: 'dialog',
'aria-hidden': t.is_visible ? null : 'true'
},
on: {
click: t.onClickOut,
keydown: t.onEsc
}
}, [modalDialog]);
// Wrap modal in transition
modal = h('transition', {
props: {
enterClass: '',
enterToClass: '',
enterActiveClass: '',
leaveClass: '',
leaveActiveClass: '',
leaveToClass: ''
},
on: {
'before-enter': t.onBeforeEnter,
enter: t.onEnter,
'after-enter': t.onAfterEnter,
'before-leave': t.onBeforeLeave,
leave: t.onLeave,
'after-leave': t.onAfterLeave
}
}, [modal]);
// Modal Backdrop
var backdrop = h(false);
if (!t.hideBackdrop && (t.is_visible || t.is_transitioning)) {
backdrop = h('div', {
class: t.backdropClasses,
attrs: { id: t.safeId('__BV_modal_backdrop_') }
});
}
// Assemble modal and backdrop
var outer = h(false);
if (!t.is_hidden) {
outer = h('div', { attrs: { id: t.safeId('__BV_modal_outer_') } }, [modal, backdrop]);
}
// Wrap in DIV to maintain thi.$el reference for hide/show method aceess
return h('div', {}, [outer]);
},
data: function data() {
return {
is_hidden: this.lazy || false,
is_visible: false,
is_transitioning: false,
is_show: false,
is_block: false,
scrollbarWidth: 0,
isBodyOverflowing: false,
return_focus: this.returnFocus || null
};
},
model: {
prop: 'visible',
event: 'change'
},
props: {
title: {
type: String,
default: ''
},
titleTag: {
type: String,
default: 'h5'
},
size: {
type: String,
default: 'md'
},
centered: {
type: Boolean,
default: false
},
buttonSize: {
type: String,
default: ''
},
noFade: {
type: Boolean,
default: false
},
noCloseOnBackdrop: {
type: Boolean,
default: false
},
noCloseOnEsc: {
type: Boolean,
default: false
},
noEnforceFocus: {
type: Boolean,
default: false
},
headerBgVariant: {
type: String,
default: null
},
headerBorderVariant: {
type: String,
default: null
},
headerTextVariant: {
type: String,
default: null
},
headerClass: {
type: [String, Array],
default: null
},
bodyBgVariant: {
type: String,
default: null
},
bodyTextVariant: {
type: String,
default: null
},
bodyClass: {
type: [String, Array],
default: null
},
footerBgVariant: {
type: String,
default: null
},
footerBorderVariant: {
type: String,
default: null
},
footerTextVariant: {
type: String,
default: null
},
footerClass: {
type: [String, Array],
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: {
default: null
},
headerCloseLabel: {
type: String,
default: 'Close'
},
cancelTitle: {
type: String,
default: 'Cancel'
},
okTitle: {
type: String,
default: 'OK'
},
cancelVariant: {
type: String,
default: 'secondary'
},
okVariant: {
type: String,
default: 'primary'
},
lazy: {
type: Boolean,
default: false
},
busy: {
type: Boolean,
default: false
}
},
computed: {
modalClasses: function modalClasses() {
return ['modal', {
fade: !this.noFade,
show: this.is_show,
'd-block': this.is_block
}];
},
dialogClasses: function dialogClasses() {
var _ref;
return ['modal-dialog', (_ref = {}, _defineProperty(_ref, 'modal-' + this.size, Boolean(this.size)), _defineProperty(_ref, 'modal-dialog-centered', this.centered), _ref)];
},
backdropClasses: function backdropClasses() {
return ['modal-backdrop', {
fade: !this.noFade,
show: this.is_show || this.noFade
}];
},
headerClasses: function headerClasses() {
var _ref2;
return ['modal-header', (_ref2 = {}, _defineProperty(_ref2, 'bg-' + this.headerBgVariant, Boolean(this.headerBgVariant)), _defineProperty(_ref2, 'text-' + this.headerTextVariant, Boolean(this.headerTextVariant)), _defineProperty(_ref2, 'border-' + this.headerBorderVariant, Boolean(this.headerBorderVariant)), _ref2), this.headerClass];
},
bodyClasses: function bodyClasses() {
var _ref3;
return ['modal-body', (_ref3 = {}, _defineProperty(_ref3, 'bg-' + this.bodyBgVariant, Boolean(this.bodyBgVariant)), _defineProperty(_ref3, 'text-' + this.bodyTextVariant, Boolean(this.bodyTextVariant)), _ref3), this.bodyClass];
},
footerClasses: function footerClasses() {
var _ref4;
return ['modal-footer', (_ref4 = {}, _defineProperty(_ref4, 'bg-' + this.footerBgVariant, Boolean(this.footerBgVariant)), _defineProperty(_ref4, 'text-' + this.footerTextVariant, Boolean(this.footerTextVariant)), _defineProperty(_ref4, 'border-' + this.footerBorderVariant, Boolean(this.footerBorderVariant)), _ref4), this.footerClass];
}
},
watch: {
visible: function visible(newVal, oldVal) {
if (newVal === oldVal) {
return;
}
this[newVal ? 'show' : 'hide']();
}
},
methods: {
// Public Methods
show: function show() {
if (this.is_visible) {
return;
}
var showEvt = new BvEvent('show', {
cancelable: true,
vueTarget: this,
target: this.$refs.modal,
relatedTarget: null
});
this.emitEvent(showEvt);
if (showEvt.defaultPrevented || this.is_visible) {
// Don't show if canceled
return;
}
if (hasClass(document.body, 'modal-open')) {
// If another modal is already open, wait for it to close
this.$root.$once('bv::modal::hidden', this.doShow);
} else {
// Show the modal
this.doShow();
}
},
hide: function hide(trigger) {
if (!this.is_visible) {
return;
}
var hideEvt = new BvEvent('hide', {
cancelable: true,
vueTarget: this,
target: this.$refs.modal,
// this could be the trigger element/component reference
relatedTarget: null,
isOK: trigger || null,
trigger: trigger || null,
cancel: function cancel() {
// Backwards compatibility
warn('b-modal: evt.cancel() is deprecated. Please use evt.preventDefault().');
this.preventDefault();
}
});
if (trigger === 'ok') {
this.$emit('ok', hideEvt);
} else if (trigger === 'cancel') {
this.$emit('cancel', hideEvt);
}
this.emitEvent(hideEvt);
// Hide if not canceled
if (hideEvt.defaultPrevented || !this.is_visible) {
return;
}
// stop observing for content changes
if (this._observer) {
this._observer.disconnect();
this._observer = null;
}
this.is_visible = false;
this.$emit('change', false);
},
// Private method to finish showing modal
doShow: function doShow() {
var _this2 = this;
// Plce modal in DOM if lazy
this.is_hidden = false;
this.$nextTick(function () {
// We do this in nextTick to ensure the modal is in DOM first before we show it
_this2.is_visible = true;
_this2.$emit('change', true);
// Observe changes in modal content and adjust if necessary
_this2._observer = observeDom(_this2.$refs.content, _this2.adjustDialog.bind(_this2), OBSERVER_CONFIG);
});
},
// Transition Handlers
onBeforeEnter: function onBeforeEnter() {
this.is_transitioning = true;
this.checkScrollbar();
this.setScrollbar();
this.adjustDialog();
addClass(document.body, 'modal-open');
this.setResizeEvent(true);
},
onEnter: function onEnter() {
this.is_block = true;
this.$refs.modal.scrollTop = 0;
},
onAfterEnter: function onAfterEnter() {
var _this3 = this;
this.is_show = true;
this.is_transitioning = false;
this.$nextTick(function () {
_this3.focusFirst();
var shownEvt = new BvEvent('shown', {
cancelable: false,
vueTarget: _this3,
target: _this3.$refs.modal,
relatedTarget: null
});
_this3.emitEvent(shownEvt);
});
},
onBeforeLeave: function onBeforeLeave() {
this.is_transitioning = true;
this.setResizeEvent(false);
},
onLeave: function onLeave() {
// Remove the 'show' class
this.is_show = false;
},
onAfterLeave: function onAfterLeave() {
var _this4 = this;
this.is_block = false;
this.resetAdjustments();
this.resetScrollbar();
this.is_transitioning = false;
removeClass(document.body, 'modal-open');
this.$nextTick(function () {
_this4.is_hidden = _this4.lazy || false;
_this4.returnFocusTo();
var hiddenEvt = new BvEvent('hidden', {
cancelable: false,
vueTarget: _this4,
target: _this4.lazy ? null : _this4.$refs.modal,
relatedTarget: null
});
_this4.emitEvent(hiddenEvt);
});
},
// Event emitter
emitEvent: function emitEvent(bvEvt) {
var type = bvEvt.type;
this.$emit(type, bvEvt);
this.$root.$emit('bv::modal::' + type, bvEvt);
},
// UI Event Handlers
onClickOut: function onClickOut(evt) {
// If backdrop clicked, hide modal
if (this.is_visible && !this.noCloseOnBackdrop) {
this.hide('backdrop');
}
},
onEsc: function onEsc(evt) {
// If ESC pressed, hide modal
if (evt.keyCode === KeyCodes.ESC && this.is_visible && !this.noCloseOnEsc) {
this.hide('esc');
}
},
onFocusout: function onFocusout(evt) {
// If focus leaves modal, bring it back
// 'focusout' Event Listener bound on content
var content = this.$refs.content;
if (!this.noEnforceFocus && this.is_visible && content && !content.contains(evt.relatedTarget)) {
content.focus();
}
},
// Resize Listener
setResizeEvent: function setResizeEvent(on) {
var _this5 = this;
;['resize', 'orientationchange'].forEach(function (evtName) {
if (on) {
eventOn(window, evtName, _this5.adjustDialog);
} else {
eventOff(window, evtName, _this5.adjustDialog);
}
});
},
// Root Listener handlers
showHandler: function showHandler(id, triggerEl) {
if (id === this.id) {
this.return_focus = triggerEl || null;
this.show();
}
},
hideHandler: function hideHandler(id) {
if (id === this.id) {
this.hide();
}
},
modalListener: function modalListener(bvEvt) {
// If another modal opens, close this one
if (bvEvt.vueTarget !== this) {
this.hide();
}
},
// Focus control handlers
focusFirst: function focusFirst() {
// Don't try and focus if we are SSR
if (typeof document === 'undefined') {
return;
}
var content = this.$refs.content;
var modal = this.$refs.modal;
var activeElement = document.activeElement;
if (activeElement && content && content.contains(activeElement)) {
// If activeElement is child of content, no need to change focus
} else if (content) {
if (modal) {
modal.scrollTop = 0;
}
// Focus the modal content wrapper
content.focus();
}
},
returnFocusTo: function returnFocusTo() {
// Prefer returnFocus prop over event specified return_focus value
var el = this.returnFocus || this.return_focus || null;
if (typeof el === 'string') {
// CSS Selector
el = select(el);
}
if (el) {
el = el.$el || el;
if (isVisible(el)) {
el.focus();
}
}
},
// Utility methods
getScrollbarWidth: function getScrollbarWidth() {
var scrollDiv = document.createElement('div');
scrollDiv.className = 'modal-scrollbar-measure';
document.body.appendChild(scrollDiv);
this.scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth;
document.body.removeChild(scrollDiv);
},
adjustDialog: function adjustDialog() {
if (!this.is_visible) {
return;
}
var modal = this.$refs.modal;
var isModalOverflowing = modal.scrollHeight > document.documentElement.clientHeight;
if (!this.isBodyOverflowing && isModalOverflowing) {
modal.style.paddingLeft = this.scrollbarWidth + 'px';
}
if (this.isBodyOverflowing && !isModalOverflowing) {
modal.style.paddingRight = this.scrollbarWidth + 'px';
}
},
resetAdjustments: function resetAdjustments() {
var modal = this.$refs.modal;
if (modal) {
modal.style.paddingLeft = '';
modal.style.paddingRight = '';
}
},
checkScrollbar: function checkScrollbar() {
var rect = getBCR(document.body);
this.isBodyOverflowing = rect.left + rect.right < window.innerWidth;
},
setScrollbar: function setScrollbar() {
if (this.isBodyOverflowing) {
// Note: DOMNode.style.paddingRight returns the actual value or '' if not set
// while $(DOMNode).css('padding-right') returns the calculated value or 0 if not set
var computedStyle = window.getComputedStyle;
var body = document.body;
var scrollbarWidth = this.scrollbarWidth;
// Adjust fixed content padding
selectAll(Selector.FIXED_CONTENT).forEach(function (el) {
var actualPadding = el.style.paddingRight;
var calculatedPadding = computedStyle(el).paddingRight || 0;
setAttr(el, 'data-padding-right', actualPadding);
el.style.paddingRight = parseFloat(calculatedPadding) + scrollbarWidth + 'px';
});
// Adjust sticky content margin
selectAll(Selector.STICKY_CONTENT).forEach(function (el) {
var actualMargin = el.style.marginRight;
var calculatedMargin = computedStyle(el).marginRight || 0;
setAttr(el, 'data-margin-right', actualMargin);
el.style.marginRight = parseFloat(calculatedMargin) - scrollbarWidth + 'px';
});
// Adjust navbar-toggler margin
selectAll(Selector.NAVBAR_TOGGLER).forEach(function (el) {
var actualMargin = el.style.marginRight;
var calculatedMargin = computedStyle(el).marginRight || 0;
setAttr(el, 'data-margin-right', actualMargin);
el.style.marginRight = parseFloat(calculatedMargin) + scrollbarWidth + 'px';
});
// Adjust body padding
var actualPadding = body.style.paddingRight;
var calculatedPadding = computedStyle(body).paddingRight;
setAttr(body, 'data-padding-right', actualPadding);
body.style.paddingRight = parseFloat(calculatedPadding) + scrollbarWidth + 'px';
}
},
resetScrollbar: function resetScrollbar() {
// Restore fixed content padding
selectAll(Selector.FIXED_CONTENT).forEach(function (el) {
if (hasAttr(el, 'data-padding-right')) {
el.style.paddingRight = getAttr(el, 'data-padding-right') || '';
removeAttr(el, 'data-padding-right');
}
});
// Restore sticky content and navbar-toggler margin
selectAll(Selector.STICKY_CONTENT + ', ' + Selector.NAVBAR_TOGGLER).forEach(function (el) {
if (hasAttr(el, 'data-margin-right')) {
el.style.marginRight = getAttr(el, 'data-margin-right') || '';
removeAttr(el, 'data-margin-right');
}
});
// Restore body padding
var body = document.body;
if (hasAttr(body, 'data-padding-right')) {
body.style.paddingRight = getAttr(body, 'data-padding-right') || '';
removeAttr(body, 'data-padding-right');
}
}
},
created: function created() {
// create non-reactive property
this._observer = null;
},
mounted: function mounted() {
// Measure scrollbar
this.getScrollbarWidth();
// Listen for events from others to either open or close ourselves
this.listenOnRoot('bv::show::modal', this.showHandler);
this.listenOnRoot('bv::hide::modal', this.hideHandler);
// 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.show();
}
},
beforeDestroy: function beforeDestroy() {
// Ensure everything is back to normal
if (this._observer) {
this._observer.disconnect();
this._observer = null;
}
this.setResizeEvent(false);
// Re-adjust body/navbar/fixed padding/margins (if needed)
removeClass(document.body, 'modal-open');
this.resetAdjustments();
this.resetScrollbar();
}
};