UNPKG

quasar

Version:

Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time

438 lines (364 loc) 12.4 kB
import { h, ref, computed, watch, onBeforeUnmount, Transition, getCurrentInstance } from 'vue' import useHistory from '../../composables/private.use-history/use-history.js' import useTimeout from '../../composables/use-timeout/use-timeout.js' import useTick from '../../composables/use-tick/use-tick.js' import useModelToggle, { useModelToggleProps, useModelToggleEmits } from '../../composables/private.use-model-toggle/use-model-toggle.js' import useTransition, { useTransitionProps } from '../../composables/private.use-transition/use-transition.js' import usePortal from '../../composables/private.use-portal/use-portal.js' import usePreventScroll from '../../composables/private.use-prevent-scroll/use-prevent-scroll.js' import { createComponent } from '../../utils/private.create/create.js' import { childHasFocus } from '../../utils/dom/dom.js' import { hSlot } from '../../utils/private.render/render.js' import { addEscapeKey, removeEscapeKey } from '../../utils/private.keyboard/escape-key.js' import { addFocusout, removeFocusout } from '../../utils/private.focus/focusout.js' import { addFocusFn } from '../../utils/private.focus/focus-manager.js' let maximizedModals = 0 const positionClass = { standard: 'fixed-full flex-center', top: 'fixed-top justify-center', bottom: 'fixed-bottom justify-center', right: 'fixed-right items-center', left: 'fixed-left items-center' } const defaultTransitions = { standard: [ 'scale', 'scale' ], top: [ 'slide-down', 'slide-up' ], bottom: [ 'slide-up', 'slide-down' ], right: [ 'slide-left', 'slide-right' ], left: [ 'slide-right', 'slide-left' ] } export default createComponent({ name: 'QDialog', inheritAttrs: false, props: { ...useModelToggleProps, ...useTransitionProps, transitionShow: String, // override useTransitionProps transitionHide: String, // override useTransitionProps persistent: Boolean, autoClose: Boolean, allowFocusOutside: Boolean, noEscDismiss: Boolean, noBackdropDismiss: Boolean, noRouteDismiss: Boolean, noRefocus: Boolean, noFocus: Boolean, noShake: Boolean, seamless: Boolean, maximized: Boolean, fullWidth: Boolean, fullHeight: Boolean, square: Boolean, backdropFilter: String, position: { type: String, default: 'standard', validator: val => [ 'standard', 'top', 'bottom', 'left', 'right' ].includes(val) } }, emits: [ ...useModelToggleEmits, 'shake', 'click', 'escapeKey' ], setup (props, { slots, emit, attrs }) { const vm = getCurrentInstance() const innerRef = ref(null) const showing = ref(false) const animating = ref(false) let shakeTimeout = null, refocusTarget = null, isMaximized, avoidAutoClose const hideOnRouteChange = computed(() => props.persistent !== true && props.noRouteDismiss !== true && props.seamless !== true ) const { preventBodyScroll } = usePreventScroll() const { registerTimeout } = useTimeout() const { registerTick, removeTick } = useTick() const { transitionProps, transitionStyle } = useTransition( props, () => defaultTransitions[ props.position ][ 0 ], () => defaultTransitions[ props.position ][ 1 ] ) const backdropStyle = computed(() => ( transitionStyle.value + ( props.backdropFilter !== void 0 // Safari requires the -webkit prefix ? `;backdrop-filter:${ props.backdropFilter };-webkit-backdrop-filter:${ props.backdropFilter }` : '' ) )) const { showPortal, hidePortal, portalIsAccessible, renderPortal } = usePortal( vm, innerRef, renderPortalContent, 'dialog' ) const { hide } = useModelToggle({ showing, hideOnRouteChange, handleShow, handleHide, processOnMount: true }) const { addToHistory, removeFromHistory } = useHistory(showing, hide, hideOnRouteChange) const classes = computed(() => 'q-dialog__inner flex no-pointer-events' + ` q-dialog__inner--${ props.maximized === true ? 'maximized' : 'minimized' }` + ` q-dialog__inner--${ props.position } ${ positionClass[ props.position ] }` + (animating.value === true ? ' q-dialog__inner--animating' : '') + (props.fullWidth === true ? ' q-dialog__inner--fullwidth' : '') + (props.fullHeight === true ? ' q-dialog__inner--fullheight' : '') + (props.square === true ? ' q-dialog__inner--square' : '') ) const useBackdrop = computed(() => showing.value === true && props.seamless !== true) const onEvents = computed(() => ( props.autoClose === true ? { onClick: onAutoClose } : {} )) const rootClasses = computed(() => [ 'q-dialog fullscreen no-pointer-events ' + `q-dialog--${ useBackdrop.value === true ? 'modal' : 'seamless' }`, attrs.class ]) watch(() => props.maximized, state => { showing.value === true && updateMaximized(state) }) watch(useBackdrop, val => { preventBodyScroll(val) if (val === true) { addFocusout(onFocusChange) addEscapeKey(onEscapeKey) } else { removeFocusout(onFocusChange) removeEscapeKey(onEscapeKey) } }) function handleShow (evt) { addToHistory() refocusTarget = props.noRefocus === false && document.activeElement !== null ? document.activeElement : null updateMaximized(props.maximized) showPortal() animating.value = true if (props.noFocus !== true) { document.activeElement !== null && document.activeElement.blur() registerTick(focus) } else { removeTick() } // should removeTimeout() if this gets removed registerTimeout(() => { if (vm.proxy.$q.platform.is.ios === true) { if (props.seamless !== true && document.activeElement) { const { top, bottom } = document.activeElement.getBoundingClientRect(), { innerHeight } = window, height = window.visualViewport !== void 0 ? window.visualViewport.height : innerHeight if (top > 0 && bottom > height / 2) { document.scrollingElement.scrollTop = Math.min( document.scrollingElement.scrollHeight - height, bottom >= innerHeight ? Infinity : Math.ceil(document.scrollingElement.scrollTop + bottom - height / 2) ) } document.activeElement.scrollIntoView() } // required in order to avoid the "double-tap needed" issue avoidAutoClose = true innerRef.value.click() avoidAutoClose = false } showPortal(true) // done showing portal animating.value = false emit('show', evt) }, props.transitionDuration) } function handleHide (evt) { removeTick() removeFromHistory() cleanup(true) animating.value = true hidePortal() if (refocusTarget !== null) { ((evt && evt.type.indexOf('key') === 0 ? refocusTarget.closest('[tabindex]:not([tabindex^="-"])') : void 0 ) || refocusTarget).focus() refocusTarget = null } // should removeTimeout() if this gets removed registerTimeout(() => { hidePortal(true) // done hiding, now destroy animating.value = false emit('hide', evt) }, props.transitionDuration) } function focus (selector) { addFocusFn(() => { let node = innerRef.value if (node === null) return if (selector !== void 0) { const target = node.querySelector(selector) if (target !== null) { target.focus({ preventScroll: true }) return } } if (node.contains(document.activeElement) !== true) { node = ( node.querySelector('[autofocus][tabindex], [data-autofocus][tabindex]') || node.querySelector('[autofocus] [tabindex], [data-autofocus] [tabindex]') || node.querySelector('[autofocus], [data-autofocus]') || node ) node.focus({ preventScroll: true }) } }) } function shake (focusTarget) { if (focusTarget && typeof focusTarget.focus === 'function') { focusTarget.focus({ preventScroll: true }) } else { focus() } emit('shake') const node = innerRef.value if (node !== null) { node.classList.remove('q-animate--scale') node.classList.add('q-animate--scale') shakeTimeout !== null && clearTimeout(shakeTimeout) shakeTimeout = setTimeout(() => { shakeTimeout = null if (innerRef.value !== null) { node.classList.remove('q-animate--scale') // some platforms (like desktop Chrome) // require calling focus() again focus() } }, 170) } } function onEscapeKey () { if (props.seamless !== true) { if (props.persistent === true || props.noEscDismiss === true) { props.maximized !== true && props.noShake !== true && shake() } else { emit('escapeKey') hide() } } } function cleanup (hiding) { if (shakeTimeout !== null) { clearTimeout(shakeTimeout) shakeTimeout = null } if (hiding === true || showing.value === true) { updateMaximized(false) if (props.seamless !== true) { preventBodyScroll(false) removeFocusout(onFocusChange) removeEscapeKey(onEscapeKey) } } if (hiding !== true) { refocusTarget = null } } function updateMaximized (active) { if (active === true) { if (isMaximized !== true) { maximizedModals < 1 && document.body.classList.add('q-body--dialog') maximizedModals++ isMaximized = true } } else if (isMaximized === true) { if (maximizedModals < 2) { document.body.classList.remove('q-body--dialog') } maximizedModals-- isMaximized = false } } function onAutoClose (e) { if (avoidAutoClose !== true) { hide(e) emit('click', e) } } function onBackdropClick (e) { if (props.persistent !== true && props.noBackdropDismiss !== true) { hide(e) } else if (props.noShake !== true) { shake() } } function onFocusChange (evt) { // the focus is not in a vue child component if ( props.allowFocusOutside !== true && portalIsAccessible.value === true && childHasFocus(innerRef.value, evt.target) !== true ) { focus('[tabindex]:not([tabindex="-1"])') } } Object.assign(vm.proxy, { // expose public methods focus, shake, // private but needed by QSelect __updateRefocusTarget (target) { refocusTarget = target || null } }) onBeforeUnmount(cleanup) function renderPortalContent () { return h('div', { role: 'dialog', 'aria-modal': useBackdrop.value === true ? 'true' : 'false', ...attrs, class: rootClasses.value }, [ h(Transition, { name: 'q-transition--fade', appear: true }, () => ( useBackdrop.value === true ? h('div', { class: 'q-dialog__backdrop fixed-full', style: backdropStyle.value, 'aria-hidden': 'true', tabindex: -1, onClick: onBackdropClick }) : null )), h( Transition, transitionProps.value, () => ( showing.value === true ? h('div', { ref: innerRef, class: classes.value, style: transitionStyle.value, tabindex: -1, ...onEvents.value }, hSlot(slots.default)) : null ) ) ]) } return renderPortal } })