quasar
Version:
Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time
711 lines (587 loc) • 19 kB
JavaScript
import { h, withDirectives, ref, computed, watch, onMounted, onBeforeUnmount, nextTick, inject, getCurrentInstance } from 'vue'
import useHistory from '../../composables/private/use-history.js'
import useModelToggle, { useModelToggleProps, useModelToggleEmits } from '../../composables/private/use-model-toggle.js'
import usePreventScroll from '../../composables/private/use-prevent-scroll.js'
import useTimeout from '../../composables/private/use-timeout.js'
import useDark, { useDarkProps } from '../../composables/private/use-dark.js'
import TouchPan from '../../directives/TouchPan.js'
import { createComponent } from '../../utils/private/create.js'
import { between } from '../../utils/format.js'
import { hSlot, hDir } from '../../utils/private/render.js'
import { layoutKey, emptyRenderFn } from '../../utils/private/symbols.js'
const duration = 150
export default createComponent({
name: 'QDrawer',
inheritAttrs: false,
props: {
...useModelToggleProps,
...useDarkProps,
side: {
type: String,
default: 'left',
validator: v => [ 'left', 'right' ].includes(v)
},
width: {
type: Number,
default: 300
},
mini: Boolean,
miniToOverlay: Boolean,
miniWidth: {
type: Number,
default: 57
},
breakpoint: {
type: Number,
default: 1023
},
showIfAbove: Boolean,
behavior: {
type: String,
validator: v => [ 'default', 'desktop', 'mobile' ].includes(v),
default: 'default'
},
bordered: Boolean,
elevated: Boolean,
overlay: Boolean,
persistent: Boolean,
noSwipeOpen: Boolean,
noSwipeClose: Boolean,
noSwipeBackdrop: Boolean
},
emits: [
...useModelToggleEmits,
'onLayout', 'miniState'
],
setup (props, { slots, emit, attrs }) {
const vm = getCurrentInstance()
const { proxy: { $q } } = vm
const isDark = useDark(props, $q)
const { preventBodyScroll } = usePreventScroll()
const { registerTimeout, removeTimeout } = useTimeout()
const $layout = inject(layoutKey, emptyRenderFn)
if ($layout === emptyRenderFn) {
console.error('QDrawer needs to be child of QLayout')
return emptyRenderFn
}
let lastDesktopState, timerMini = null, layoutTotalWidthWatcher
const belowBreakpoint = ref(
props.behavior === 'mobile'
|| (props.behavior !== 'desktop' && $layout.totalWidth.value <= props.breakpoint)
)
const isMini = computed(() =>
props.mini === true && belowBreakpoint.value !== true
)
const size = computed(() => (
isMini.value === true
? props.miniWidth
: props.width
))
const showing = ref(
props.showIfAbove === true && belowBreakpoint.value === false
? true
: props.modelValue === true
)
const hideOnRouteChange = computed(() =>
props.persistent !== true
&& (belowBreakpoint.value === true || onScreenOverlay.value === true)
)
function handleShow (evt, noEvent) {
addToHistory()
evt !== false && $layout.animate()
applyPosition(0)
if (belowBreakpoint.value === true) {
const otherInstance = $layout.instances[ otherSide.value ]
if (otherInstance !== void 0 && otherInstance.belowBreakpoint === true) {
otherInstance.hide(false)
}
applyBackdrop(1)
$layout.isContainer.value !== true && preventBodyScroll(true)
}
else {
applyBackdrop(0)
evt !== false && setScrollable(false)
}
registerTimeout(() => {
evt !== false && setScrollable(true)
noEvent !== true && emit('show', evt)
}, duration)
}
function handleHide (evt, noEvent) {
removeFromHistory()
evt !== false && $layout.animate()
applyBackdrop(0)
applyPosition(stateDirection.value * size.value)
cleanup()
if (noEvent !== true) {
registerTimeout(() => { emit('hide', evt) }, duration)
}
else {
removeTimeout()
}
}
const { show, hide } = useModelToggle({
showing,
hideOnRouteChange,
handleShow,
handleHide
})
const { addToHistory, removeFromHistory } = useHistory(showing, hide, hideOnRouteChange)
const instance = {
belowBreakpoint,
hide
}
const rightSide = computed(() => props.side === 'right')
const stateDirection = computed(() =>
($q.lang.rtl === true ? -1 : 1) * (rightSide.value === true ? 1 : -1)
)
const flagBackdropBg = ref(0)
const flagPanning = ref(false)
const flagMiniAnimate = ref(false)
const flagContentPosition = ref( // starting with "hidden" for SSR
size.value * stateDirection.value
)
const otherSide = computed(() => (rightSide.value === true ? 'left' : 'right'))
const offset = computed(() => (
showing.value === true && belowBreakpoint.value === false && props.overlay === false
? (props.miniToOverlay === true ? props.miniWidth : size.value)
: 0
))
const fixed = computed(() =>
props.overlay === true
|| props.miniToOverlay === true
|| $layout.view.value.indexOf(rightSide.value ? 'R' : 'L') > -1
|| ($q.platform.is.ios === true && $layout.isContainer.value === true)
)
const onLayout = computed(() =>
props.overlay === false
&& showing.value === true
&& belowBreakpoint.value === false
)
const onScreenOverlay = computed(() =>
props.overlay === true
&& showing.value === true
&& belowBreakpoint.value === false
)
const backdropClass = computed(() =>
'fullscreen q-drawer__backdrop'
+ (showing.value === false && flagPanning.value === false ? ' hidden' : '')
)
const backdropStyle = computed(() => ({
backgroundColor: `rgba(0,0,0,${ flagBackdropBg.value * 0.4 })`
}))
const headerSlot = computed(() => (
rightSide.value === true
? $layout.rows.value.top[ 2 ] === 'r'
: $layout.rows.value.top[ 0 ] === 'l'
))
const footerSlot = computed(() => (
rightSide.value === true
? $layout.rows.value.bottom[ 2 ] === 'r'
: $layout.rows.value.bottom[ 0 ] === 'l'
))
const aboveStyle = computed(() => {
const css = {}
if ($layout.header.space === true && headerSlot.value === false) {
if (fixed.value === true) {
css.top = `${ $layout.header.offset }px`
}
else if ($layout.header.space === true) {
css.top = `${ $layout.header.size }px`
}
}
if ($layout.footer.space === true && footerSlot.value === false) {
if (fixed.value === true) {
css.bottom = `${ $layout.footer.offset }px`
}
else if ($layout.footer.space === true) {
css.bottom = `${ $layout.footer.size }px`
}
}
return css
})
const style = computed(() => {
const style = {
width: `${ size.value }px`,
transform: `translateX(${ flagContentPosition.value }px)`
}
return belowBreakpoint.value === true
? style
: Object.assign(style, aboveStyle.value)
})
const contentClass = computed(() =>
'q-drawer__content fit '
+ ($layout.isContainer.value !== true ? 'scroll' : 'overflow-auto')
)
const classes = computed(() =>
`q-drawer q-drawer--${ props.side }`
+ (flagMiniAnimate.value === true ? ' q-drawer--mini-animate' : '')
+ (props.bordered === true ? ' q-drawer--bordered' : '')
+ (isDark.value === true ? ' q-drawer--dark q-dark' : '')
+ (
flagPanning.value === true
? ' no-transition'
: (showing.value === true ? '' : ' q-layout--prevent-focus')
)
+ (
belowBreakpoint.value === true
? ' fixed q-drawer--on-top q-drawer--mobile q-drawer--top-padding'
: ` q-drawer--${ isMini.value === true ? 'mini' : 'standard' }`
+ (fixed.value === true || onLayout.value !== true ? ' fixed' : '')
+ (props.overlay === true || props.miniToOverlay === true ? ' q-drawer--on-top' : '')
+ (headerSlot.value === true ? ' q-drawer--top-padding' : '')
)
)
const openDirective = computed(() => {
// if props.noSwipeOpen !== true
const dir = $q.lang.rtl === true ? props.side : otherSide.value
return [ [
TouchPan,
onOpenPan,
void 0,
{
[ dir ]: true,
mouse: true
}
] ]
})
const contentCloseDirective = computed(() => {
// if belowBreakpoint.value === true && props.noSwipeClose !== true
const dir = $q.lang.rtl === true ? otherSide.value : props.side
return [ [
TouchPan,
onClosePan,
void 0,
{
[ dir ]: true,
mouse: true
}
] ]
})
const backdropCloseDirective = computed(() => {
// if showing.value === true && props.noSwipeBackdrop !== true
const dir = $q.lang.rtl === true ? otherSide.value : props.side
return [ [
TouchPan,
onClosePan,
void 0,
{
[ dir ]: true,
mouse: true,
mouseAllDir: true
}
] ]
})
function updateBelowBreakpoint () {
updateLocal(belowBreakpoint, (
props.behavior === 'mobile'
|| (props.behavior !== 'desktop' && $layout.totalWidth.value <= props.breakpoint)
))
}
watch(belowBreakpoint, val => {
if (val === true) { // from lg to xs
lastDesktopState = showing.value
showing.value === true && hide(false)
}
else if (
props.overlay === false
&& props.behavior !== 'mobile'
&& lastDesktopState !== false
) { // from xs to lg
if (showing.value === true) {
applyPosition(0)
applyBackdrop(0)
cleanup()
}
else {
show(false)
}
}
})
watch(() => props.side, (newSide, oldSide) => {
if ($layout.instances[ oldSide ] === instance) {
$layout.instances[ oldSide ] = void 0
$layout[ oldSide ].space = false
$layout[ oldSide ].offset = 0
}
$layout.instances[ newSide ] = instance
$layout[ newSide ].size = size.value
$layout[ newSide ].space = onLayout.value
$layout[ newSide ].offset = offset.value
})
watch($layout.totalWidth, () => {
if ($layout.isContainer.value === true || document.qScrollPrevented !== true) {
updateBelowBreakpoint()
}
})
watch(
() => props.behavior + props.breakpoint,
updateBelowBreakpoint
)
watch($layout.isContainer, val => {
showing.value === true && preventBodyScroll(val !== true)
val === true && updateBelowBreakpoint()
})
watch($layout.scrollbarWidth, () => {
applyPosition(showing.value === true ? 0 : void 0)
})
watch(offset, val => { updateLayout('offset', val) })
watch(onLayout, val => {
emit('onLayout', val)
updateLayout('space', val)
})
watch(rightSide, () => { applyPosition() })
watch(size, val => {
applyPosition()
updateSizeOnLayout(props.miniToOverlay, val)
})
watch(() => props.miniToOverlay, val => {
updateSizeOnLayout(val, size.value)
})
watch(() => $q.lang.rtl, () => { applyPosition() })
watch(() => props.mini, () => {
if (props.modelValue === true) {
animateMini()
$layout.animate()
}
})
watch(isMini, val => { emit('miniState', val) })
function applyPosition (position) {
if (position === void 0) {
nextTick(() => {
position = showing.value === true ? 0 : size.value
applyPosition(stateDirection.value * position)
})
}
else {
if (
$layout.isContainer.value === true
&& rightSide.value === true
&& (belowBreakpoint.value === true || Math.abs(position) === size.value)
) {
position += stateDirection.value * $layout.scrollbarWidth.value
}
flagContentPosition.value = position
}
}
function applyBackdrop (x) {
flagBackdropBg.value = x
}
function setScrollable (v) {
const action = v === true
? 'remove'
: ($layout.isContainer.value !== true ? 'add' : '')
action !== '' && document.body.classList[ action ]('q-body--drawer-toggle')
}
function animateMini () {
timerMini !== null && clearTimeout(timerMini)
if (vm.proxy && vm.proxy.$el) {
// need to speed it up and apply it immediately,
// even faster than Vue's nextTick!
vm.proxy.$el.classList.add('q-drawer--mini-animate')
}
flagMiniAnimate.value = true
timerMini = setTimeout(() => {
timerMini = null
flagMiniAnimate.value = false
if (vm && vm.proxy && vm.proxy.$el) {
vm.proxy.$el.classList.remove('q-drawer--mini-animate')
}
}, 150)
}
function onOpenPan (evt) {
if (showing.value !== false) {
// some browsers might capture and trigger this
// even if Drawer has just been opened (but animation is still pending)
return
}
const
width = size.value,
position = between(evt.distance.x, 0, width)
if (evt.isFinal === true) {
const opened = position >= Math.min(75, width)
if (opened === true) {
show()
}
else {
$layout.animate()
applyBackdrop(0)
applyPosition(stateDirection.value * width)
}
flagPanning.value = false
return
}
applyPosition(
($q.lang.rtl === true ? rightSide.value !== true : rightSide.value)
? Math.max(width - position, 0)
: Math.min(0, position - width)
)
applyBackdrop(
between(position / width, 0, 1)
)
if (evt.isFirst === true) {
flagPanning.value = true
}
}
function onClosePan (evt) {
if (showing.value !== true) {
// some browsers might capture and trigger this
// even if Drawer has just been closed (but animation is still pending)
return
}
const
width = size.value,
dir = evt.direction === props.side,
position = ($q.lang.rtl === true ? dir !== true : dir)
? between(evt.distance.x, 0, width)
: 0
if (evt.isFinal === true) {
const opened = Math.abs(position) < Math.min(75, width)
if (opened === true) {
$layout.animate()
applyBackdrop(1)
applyPosition(0)
}
else {
hide()
}
flagPanning.value = false
return
}
applyPosition(stateDirection.value * position)
applyBackdrop(between(1 - position / width, 0, 1))
if (evt.isFirst === true) {
flagPanning.value = true
}
}
function cleanup () {
preventBodyScroll(false)
setScrollable(true)
}
function updateLayout (prop, val) {
$layout.update(props.side, prop, val)
}
function updateLocal (prop, val) {
if (prop.value !== val) {
prop.value = val
}
}
function updateSizeOnLayout (miniToOverlay, size) {
updateLayout('size', miniToOverlay === true ? props.miniWidth : size)
}
$layout.instances[ props.side ] = instance
updateSizeOnLayout(props.miniToOverlay, size.value)
updateLayout('space', onLayout.value)
updateLayout('offset', offset.value)
if (
props.showIfAbove === true
&& props.modelValue !== true
&& showing.value === true
&& props[ 'onUpdate:modelValue' ] !== void 0
) {
emit('update:modelValue', true)
}
onMounted(() => {
emit('onLayout', onLayout.value)
emit('miniState', isMini.value)
lastDesktopState = props.showIfAbove === true
const fn = () => {
const action = showing.value === true ? handleShow : handleHide
action(false, true)
}
if ($layout.totalWidth.value !== 0) {
// make sure that all computed properties
// have been updated before calling handleShow/handleHide()
nextTick(fn)
return
}
layoutTotalWidthWatcher = watch($layout.totalWidth, () => {
layoutTotalWidthWatcher()
layoutTotalWidthWatcher = void 0
if (showing.value === false && props.showIfAbove === true && belowBreakpoint.value === false) {
show(false)
}
else {
fn()
}
})
})
onBeforeUnmount(() => {
layoutTotalWidthWatcher !== void 0 && layoutTotalWidthWatcher()
if (timerMini !== null) {
clearTimeout(timerMini)
timerMini = null
}
showing.value === true && cleanup()
if ($layout.instances[ props.side ] === instance) {
$layout.instances[ props.side ] = void 0
updateLayout('size', 0)
updateLayout('offset', 0)
updateLayout('space', false)
}
})
return () => {
const child = []
if (belowBreakpoint.value === true) {
props.noSwipeOpen === false && child.push(
withDirectives(
h('div', {
key: 'open',
class: `q-drawer__opener fixed-${ props.side }`,
'aria-hidden': 'true'
}),
openDirective.value
)
)
child.push(
hDir(
'div',
{
ref: 'backdrop',
class: backdropClass.value,
style: backdropStyle.value,
'aria-hidden': 'true',
onClick: hide
},
void 0,
'backdrop',
props.noSwipeBackdrop !== true && showing.value === true,
() => backdropCloseDirective.value
)
)
}
const mini = isMini.value === true && slots.mini !== void 0
const content = [
h('div', {
...attrs,
key: '' + mini, // required otherwise Vue will not diff correctly
class: [
contentClass.value,
attrs.class
]
}, mini === true
? slots.mini()
: hSlot(slots.default)
)
]
if (props.elevated === true && showing.value === true) {
content.push(
h('div', {
class: 'q-layout__shadow absolute-full overflow-hidden no-pointer-events'
})
)
}
child.push(
hDir(
'aside',
{ ref: 'content', class: classes.value, style: style.value },
content,
'contentclose',
props.noSwipeClose !== true && belowBreakpoint.value === true,
() => contentCloseDirective.value
)
)
return h('div', { class: 'q-drawer-container' }, child)
}
}
})