quasar
Version:
Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time
367 lines (303 loc) • 9.52 kB
JavaScript
import Vue from 'vue'
import HistoryMixin from '../../mixins/history.js'
import ModelToggleMixin from '../../mixins/model-toggle.js'
import PortalMixin from '../../mixins/portal.js'
import PreventScrollMixin from '../../mixins/prevent-scroll.js'
import AttrsMixin, { ariaHidden } from '../../mixins/attrs.js'
import { childHasFocus } from '../../utils/dom.js'
import EscapeKey from '../../utils/escape-key.js'
import { slot } from '../../utils/slot.js'
import { create, stop } from '../../utils/event.js'
import cache from '../../utils/cache.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 Vue.extend({
name: 'QDialog',
mixins: [
AttrsMixin,
HistoryMixin,
ModelToggleMixin,
PortalMixin,
PreventScrollMixin
],
props: {
persistent: Boolean,
autoClose: Boolean,
noEscDismiss: Boolean,
noBackdropDismiss: Boolean,
noRouteDismiss: Boolean,
noRefocus: Boolean,
noFocus: 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)
},
transitionShow: String,
transitionHide: String
},
data () {
return {
transitionState: this.showing
}
},
watch: {
showing (val) {
if (this.transitionShowComputed !== this.transitionHideComputed) {
this.$nextTick(() => {
this.transitionState = val
})
}
},
maximized (state) {
this.showing === true && this.__updateMaximized(state)
},
useBackdrop (v) {
this.__preventScroll(v)
this.__preventFocusout(v)
}
},
computed: {
classes () {
return `q-dialog__inner--${this.maximized === true ? 'maximized' : 'minimized'} ` +
`q-dialog__inner--${this.position} ${positionClass[this.position]}` +
(this.fullWidth === true ? ' q-dialog__inner--fullwidth' : '') +
(this.fullHeight === true ? ' q-dialog__inner--fullheight' : '') +
(this.square === true ? ' q-dialog__inner--square' : '')
},
transitionShowComputed () {
return 'q-transition--' + (this.transitionShow === void 0 ? transitions[this.position][0] : this.transitionShow)
},
transitionHideComputed () {
return 'q-transition--' + (this.transitionHide === void 0 ? transitions[this.position][1] : this.transitionHide)
},
transition () {
return this.transitionState === true
? this.transitionHideComputed
: this.transitionShowComputed
},
useBackdrop () {
return this.showing === true && this.seamless !== true
},
hideOnRouteChange () {
return this.persistent !== true &&
this.noRouteDismiss !== true &&
this.seamless !== true
},
onEvents () {
const on = {
...this.qListeners,
// stop propagating these events from children
input: stop,
'popup-show': stop,
'popup-hide': stop
}
if (this.autoClose === true) {
on.click = this.__onAutoClose
}
return on
}
},
methods: {
focus () {
let node = this.__getInnerNode()
if (node === void 0 || node.contains(document.activeElement) === true) {
return
}
node = node.querySelector('[autofocus], [data-autofocus]') || node
node.focus()
},
shake () {
this.focus()
this.$emit('shake')
const node = this.__getInnerNode()
if (node !== void 0) {
node.classList.remove('q-animate--scale')
node.classList.add('q-animate--scale')
clearTimeout(this.shakeTimeout)
this.shakeTimeout = setTimeout(() => {
node.classList.remove('q-animate--scale')
}, 170)
}
},
__getInnerNode () {
return this.__portal !== void 0 && this.__portal.$refs !== void 0
? this.__portal.$refs.inner
: void 0
},
__show (evt) {
this.__addHistory()
// IE can have null document.activeElement
this.__refocusTarget = this.noRefocus === false && document.activeElement !== null
? document.activeElement
: void 0
this.$el.dispatchEvent(create('popup-show', { bubbles: true }))
this.__updateMaximized(this.maximized)
EscapeKey.register(this, () => {
if (this.seamless !== true) {
if (this.persistent === true || this.noEscDismiss === true) {
this.maximized !== true && this.shake()
}
else {
this.$emit('escape-key')
this.hide()
}
}
})
this.__showPortal()
if (this.noFocus !== true) {
// IE can have null document.activeElement
document.activeElement !== null && document.activeElement.blur()
this.__nextTick(this.focus)
}
this.__setTimeout(() => {
if (this.$q.platform.is.ios === true) {
if (this.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
this.__portal.$el.click()
}
this.$emit('show', evt)
}, 300)
},
__hide (evt) {
this.__removeHistory()
this.__cleanup(true)
// check null for IE
if (this.__refocusTarget !== void 0 && this.__refocusTarget !== null) {
this.__refocusTarget.focus()
}
this.$el.dispatchEvent(create('popup-hide', { bubbles: true }))
this.__setTimeout(() => {
this.__hidePortal()
this.$emit('hide', evt)
}, 300)
},
__cleanup (hiding) {
clearTimeout(this.shakeTimeout)
if (hiding === true || this.showing === true) {
EscapeKey.pop(this)
this.__updateMaximized(false)
if (this.seamless !== true) {
this.__preventScroll(false)
this.__preventFocusout(false)
}
}
},
__updateMaximized (active) {
if (active === true) {
if (this.isMaximized !== true) {
maximizedModals < 1 && document.body.classList.add('q-body--dialog')
maximizedModals++
this.isMaximized = true
}
}
else if (this.isMaximized === true) {
if (maximizedModals < 2) {
document.body.classList.remove('q-body--dialog')
}
maximizedModals--
this.isMaximized = false
}
},
__preventFocusout (state) {
if (this.$q.platform.is.desktop === true) {
const action = `${state === true ? 'add' : 'remove'}EventListener`
document.body[action]('focusin', this.__onFocusChange)
}
},
__onAutoClose (e) {
this.hide(e)
this.qListeners.click !== void 0 && this.$emit('click', e)
},
__onBackdropClick (e) {
if (this.persistent !== true && this.noBackdropDismiss !== true) {
this.hide(e)
}
else {
this.shake()
}
},
__onFocusChange (e) {
// the focus is not in a vue child component
if (
this.showing === true &&
this.__portal !== void 0 &&
childHasFocus(this.__portal.$el, e.target) !== true
) {
this.focus()
}
},
__renderPortal (h) {
return h('div', {
staticClass: `q-dialog fullscreen no-pointer-events q-dialog--${this.useBackdrop === true ? 'modal' : 'seamless'}`,
class: this.contentClass,
style: this.contentStyle,
attrs: this.qAttrs
}, [
h('transition', {
props: { name: 'q-transition--fade' }
}, this.useBackdrop === true ? [
h('div', {
staticClass: 'q-dialog__backdrop fixed-full',
attrs: ariaHidden,
on: cache(this, 'bkdrop', {
click: this.__onBackdropClick
})
})
] : null),
h('transition', {
props: { name: this.transition }
}, [
this.showing === true ? h('div', {
ref: 'inner',
staticClass: 'q-dialog__inner flex no-pointer-events',
class: this.classes,
attrs: { tabindex: -1 },
on: this.onEvents
}, slot(this, 'default')) : null
])
])
}
},
mounted () {
this.__processModelChange(this.value)
},
beforeDestroy () {
this.__cleanup()
}
})