UNPKG

quasar

Version:

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

415 lines (343 loc) 11.4 kB
import { h, ref, computed, watch, onBeforeUnmount, nextTick, Transition, getCurrentInstance } from 'vue' import useHistory from '../../composables/private/use-history.js' import useTimeout from '../../composables/private/use-timeout.js' import useTick from '../../composables/private/use-tick.js' import useModelToggle, { useModelToggleProps, useModelToggleEmits } from '../../composables/private/use-model-toggle.js' import { useTransitionProps } from '../../composables/private/use-transition.js' import usePortal from '../../composables/private/use-portal.js' import usePreventScroll from '../../composables/private/use-prevent-scroll.js' import { createComponent } from '../../utils/private/create.js' import { childHasFocus } from '../../utils/dom.js' import { hSlot } from '../../utils/private/render.js' import { addEscapeKey, removeEscapeKey } from '../../utils/private/escape-key.js' import { addFocusout, removeFocusout } from '../../utils/private/focusout.js' import { addFocusFn } from '../../utils/private/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 transitions = { 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, transitionHide: String, 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, position: { type: String, default: 'standard', validator: val => val === 'standard' || [ 'top', 'bottom', 'left', 'right' ].includes(val) } }, emits: [ ...useModelToggleEmits, 'shake', 'click', 'escape-key' ], setup (props, { slots, emit, attrs }) { const vm = getCurrentInstance() const innerRef = ref(null) const showing = ref(false) const transitionState = ref(false) const animating = ref(false) let shakeTimeout, refocusTarget = null, isMaximized, avoidAutoClose const hideOnRouteChange = computed(() => props.persistent !== true && props.noRouteDismiss !== true && props.seamless !== true ) const { preventBodyScroll } = usePreventScroll() const { registerTimeout, removeTimeout } = useTimeout() const { registerTick, removeTick } = useTick() const { showPortal, hidePortal, portalIsAccessible, renderPortal } = usePortal( vm, innerRef, renderPortalContent, /* pls do check if on a global dialog */ true ) 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 transitionShow = computed(() => 'q-transition--' + (props.transitionShow === void 0 ? transitions[ props.position ][ 0 ] : props.transitionShow) ) const transitionHide = computed(() => 'q-transition--' + (props.transitionHide === void 0 ? transitions[ props.position ][ 1 ] : props.transitionHide) ) const transition = computed(() => ( transitionState.value === true ? transitionHide.value : transitionShow.value )) const transitionStyle = computed( () => `--q-transition-duration: ${ props.transitionDuration }ms` ) 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(showing, val => { nextTick(() => { transitionState.value = val }) }) 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) { removeTimeout() removeTick() 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) } 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) { removeTimeout() removeTick() removeFromHistory() cleanup(true) animating.value = true hidePortal() if (refocusTarget !== null) { refocusTarget.focus() refocusTarget = null } 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 || node.contains(document.activeElement) === true) { return } node = node.querySelector(selector || '[autofocus], [data-autofocus]') || node node.focus({ preventScroll: true }) }) } function shake () { focus() emit('shake') const node = innerRef.value if (node !== null) { node.classList.remove('q-animate--scale') node.classList.add('q-animate--scale') clearTimeout(shakeTimeout) shakeTimeout = setTimeout(() => { 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('escape-key') hide() } } } function cleanup (hiding) { clearTimeout(shakeTimeout) 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', { ...attrs, class: rootClasses.value }, [ h(Transition, { name: 'q-transition--fade', appear: true }, () => ( useBackdrop.value === true ? h('div', { class: 'q-dialog__backdrop fixed-full', style: transitionStyle.value, 'aria-hidden': 'true', onMousedown: onBackdropClick }) : null )), h( Transition, { name: transition.value, appear: true }, () => ( showing.value === true ? h('div', { ref: innerRef, class: classes.value, style: transitionStyle.value, tabindex: -1, ...onEvents.value }, hSlot(slots.default)) : null ) ) ]) } return renderPortal } })