UNPKG

@coreui/vue

Version:

UI Components Library for Vue.js

322 lines (302 loc) 8.75 kB
import { defineComponent, h, PropType, provide, ref, RendererElement, Transition, vShow, watch, withDirectives, } from 'vue' import { CBackdrop } from '../backdrop/CBackdrop' import { CConditionalTeleport } from '../conditional-teleport' import { executeAfterTransition } from '../../utils/transition' const CModal = defineComponent({ name: 'CModal', inheritAttrs: false, props: { /** * Align the modal in the center or top of the screen. * * @values 'top', 'center' */ alignment: { default: 'top', validator: (value: string) => { return ['top', 'center'].includes(value) }, }, /** * Apply a backdrop on body while offcanvas is open. * * @values boolean | 'static' */ backdrop: { type: [Boolean, String], default: true, validator: (value: boolean | string) => { if (typeof value == 'string') { return ['static'].includes(value) } if (typeof value == 'boolean') { return true } return false }, }, /** * Appends the vue popover to a specific element. You can pass an HTML element or function that returns a single element. By default `document.body`. * * @since 5.3.0 */ container: { type: [Object, String] as PropType<HTMLElement | (() => HTMLElement) | string>, default: 'body', }, /** * A string of all className you want applied to the modal content component. */ contentClassName: String, /** * Puts the focus on the modal when shown. * * @since 5.0.0 */ focus: { type: Boolean, default: true, }, /** * Set modal to covers the entire user viewport * * @values boolean, 'sm', 'md', 'lg', 'xl', 'xxl' */ fullscreen: { type: [Boolean, String], validator: (value: boolean | string) => { if (typeof value == 'string') { return ['sm', 'md', 'lg', 'xl', 'xxl'].includes(value) } if (typeof value == 'boolean') { return true } return false }, }, /** * Closes the modal when escape key is pressed. */ keyboard: { type: Boolean, default: true, }, /** * Create a scrollable modal that allows scrolling the modal body. */ scrollable: Boolean, /** * Size the component small, large, or extra large. * * @values 'sm', 'lg', 'xl' */ size: { type: String, validator: (value: string) => { return ['sm', 'lg', 'xl'].includes(value) }, }, /** * Generates modal using Teleport. * * @since 5.3.0 */ teleport: { type: Boolean, default: false, }, /** * Remove animation to create modal that simply appear rather than fade in to view. */ transition: { type: Boolean, default: true, }, /** * By default the component is unmounted after close animation, if you want to keep the component mounted set this property to false. */ unmountOnClose: { type: Boolean, default: true, }, /** * Toggle the visibility of alert component. */ visible: Boolean, }, emits: [ /** * Callback fired when the component requests to be closed. */ 'close', /** * Callback fired when the component requests to be closed. */ 'close-prevented', /** * Callback fired when the modal is shown, its backdrop is static and a click outside the modal or an escape key press is performed with the keyboard option set to false. */ 'show', ], setup(props, { slots, attrs, emit }) { const activeElementRef = ref() const modalRef = ref() const modalContentRef = ref() const visible = ref(props.visible) watch( () => props.visible, () => { visible.value = props.visible }, ) const handleEnter = (el: RendererElement, done: () => void) => { activeElementRef.value = document.activeElement as HTMLElement | null executeAfterTransition(() => done(), el as HTMLElement) document.body.classList.add('modal-open') document.body.style.overflow = 'hidden' document.body.style.paddingRight = '0px' el.style.display = 'block' setTimeout(() => { el.classList.add('show') }, 1) emit('show') } const handleAfterEnter = () => { props.focus && modalRef.value?.focus() window.addEventListener('mousedown', handleMouseDown) window.addEventListener('keyup', handleKeyUp) } // eslint-disable-next-line unicorn/consistent-function-scoping const handleLeave = (el: RendererElement, done: () => void) => { executeAfterTransition(() => done(), el as HTMLElement) document.body.classList.remove('modal-open') document.body.style.removeProperty('overflow') document.body.style.removeProperty('padding-right') if (document.body.className === '') { document.body.removeAttribute('class') } el.classList.remove('show') } const handleAfterLeave = (el: RendererElement) => { activeElementRef.value?.focus() window.removeEventListener('mousedown', handleMouseDown) window.removeEventListener('keyup', handleKeyUp) el.style.display = 'none' } const handleDismiss = () => { emit('close') visible.value = false } const handleKeyUp = (event: KeyboardEvent) => { if (modalContentRef.value && !modalContentRef.value.contains(event.target as HTMLElement)) { if (props.backdrop !== 'static' && event.key === 'Escape' && props.keyboard) { handleDismiss() } if (props.backdrop === 'static') { modalRef.value.classList.add('modal-static') emit('close-prevented') setTimeout(() => { modalRef.value.classList.remove('modal-static') }, 300) } } } const handleMouseDown = (event: Event) => { window.addEventListener('mouseup', () => handleMouseUp(event), { once: true }) } const handleMouseUp = (event: Event) => { if (modalContentRef.value && !modalContentRef.value.contains(event.target as HTMLElement)) { if (props.backdrop !== 'static') { handleDismiss() } if (props.backdrop === 'static') { modalRef.value.classList.add('modal-static') setTimeout(() => { modalRef.value.classList.remove('modal-static') }, 300) } } } provide('handleDismiss', handleDismiss) const modal = () => h( 'div', { ...attrs, class: [ 'modal', { ['fade']: props.transition, }, attrs.class, ], ...(visible.value ? { 'aria-modal': true, role: 'dialog' } : { 'aria-hidden': 'true' }), ref: modalRef, }, h( 'div', { class: [ 'modal-dialog', { 'modal-dialog-centered': props.alignment === 'center', [`modal-fullscreen-${props.fullscreen}-down`]: props.fullscreen && typeof props.fullscreen === 'string', 'modal-fullscreen': props.fullscreen && typeof props.fullscreen === 'boolean', ['modal-dialog-scrollable']: props.scrollable, [`modal-${props.size}`]: props.size, }, ], }, h( 'div', { class: ['modal-content', props.contentClassName], ref: modalContentRef }, slots.default && slots.default(), ), ), ) return () => h( CConditionalTeleport, { container: props.container, teleport: props.teleport, }, { default: () => [ h( Transition, { css: false, onEnter: (el, done) => handleEnter(el, done), onAfterEnter: () => handleAfterEnter(), onLeave: (el, done) => handleLeave(el, done), onAfterLeave: (el) => handleAfterLeave(el), }, () => props.unmountOnClose ? visible.value && modal() : withDirectives(modal(), [[vShow, visible.value]]), ), props.backdrop && h(CBackdrop, { class: 'modal-backdrop', visible: visible.value, }), ], }, ) }, }) export { CModal }