quasar
Version:
Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time
714 lines (597 loc) • 18.4 kB
JavaScript
import Vue from 'vue'
import HistoryMixin from '../../mixins/history.js'
import ModelToggleMixin from '../../mixins/model-toggle.js'
import PreventScrollMixin from '../../mixins/prevent-scroll.js'
import DarkMixin from '../../mixins/dark.js'
import TouchPan from '../../directives/TouchPan.js'
import { between } from '../../utils/format.js'
import { slot } from '../../utils/slot.js'
import cache from '../../utils/cache.js'
import { ariaHidden } from '../../mixins/attrs'
const duration = 150
const mouseEvents = [
'mouseover', 'mouseout', 'mouseenter', 'mouseleave'
]
export default Vue.extend({
name: 'QDrawer',
inject: {
layout: {
default () {
console.error('QDrawer needs to be child of QLayout')
}
}
},
mixins: [ DarkMixin, HistoryMixin, ModelToggleMixin, PreventScrollMixin ],
directives: {
TouchPan
},
props: {
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,
contentStyle: [String, Object, Array],
contentClass: [String, Object, Array],
overlay: Boolean,
persistent: Boolean,
noSwipeOpen: Boolean,
noSwipeClose: Boolean,
noSwipeBackdrop: Boolean
},
data () {
const belowBreakpoint = (
this.behavior === 'mobile' ||
(this.behavior !== 'desktop' && this.layout.totalWidth <= this.breakpoint)
)
return {
belowBreakpoint,
showing: this.showIfAbove === true && belowBreakpoint === false
? true
: this.value === true
}
},
watch: {
belowBreakpoint (val) {
if (val === true) { // from lg to xs
this.lastDesktopState = this.showing
this.showing === true && this.hide(false)
}
else if (
this.overlay === false &&
this.behavior !== 'mobile' &&
this.lastDesktopState !== false
) { // from xs to lg
if (this.showing === true) {
this.__applyPosition(0)
this.__applyBackdrop(0)
this.__cleanup()
}
else {
this.show(false)
}
}
},
'layout.totalWidth' (val) {
this.__updateLocal('belowBreakpoint', (
this.behavior === 'mobile' ||
(this.behavior !== 'desktop' && val <= this.breakpoint)
))
},
side (newSide, oldSide) {
if (this.layout.instances[oldSide] === this) {
this.layout.instances[oldSide] = void 0
this.layout[oldSide].space = false
this.layout[oldSide].offset = 0
}
this.layout.instances[newSide] = this
this.layout[newSide].size = this.size
this.layout[newSide].space = this.onLayout
this.layout[newSide].offset = this.offset
},
behavior (val) {
this.__updateLocal('belowBreakpoint', (
val === 'mobile' ||
(val !== 'desktop' && this.layout.totalWidth <= this.breakpoint)
))
},
breakpoint (val) {
this.__updateLocal('belowBreakpoint', (
this.behavior === 'mobile' ||
(this.behavior !== 'desktop' && this.layout.totalWidth <= val)
))
},
'layout.container' (val) {
this.showing === true && this.__preventScroll(val !== true)
},
'layout.scrollbarWidth' () {
this.__applyPosition(this.showing === true ? 0 : void 0)
},
offset (val) {
this.__update('offset', val)
},
onLayout (val) {
this.$emit('on-layout', val)
this.__update('space', val)
},
rightSide () {
this.__applyPosition()
},
size (val) {
this.__applyPosition()
this.__updateSizeOnLayout(this.miniToOverlay, val)
},
miniToOverlay (val) {
this.__updateSizeOnLayout(val, this.size)
},
'$q.lang.rtl' () {
this.__applyPosition()
},
mini () {
if (this.value === true) {
this.__animateMini()
this.layout.__animate()
}
},
isMini (val) {
this.$emit('mini-state', val)
}
},
computed: {
rightSide () {
return this.side === 'right'
},
otherSide () {
return this.rightSide === true ? 'left' : 'right'
},
offset () {
return this.showing === true && this.belowBreakpoint === false && this.overlay === false
? (this.miniToOverlay === true ? this.miniWidth : this.size)
: 0
},
size () {
return this.isMini === true
? this.miniWidth
: this.width
},
fixed () {
return this.overlay === true ||
this.miniToOverlay === true ||
this.layout.view.indexOf(this.rightSide ? 'R' : 'L') > -1 ||
(this.$q.platform.is.ios && this.layout.container === true)
},
onLayout () {
return this.showing === true && this.belowBreakpoint === false && this.overlay === false
},
onScreenOverlay () {
return this.showing === true && this.belowBreakpoint === false && this.overlay === true
},
backdropClass () {
return this.showing === false ? 'hidden' : null
},
headerSlot () {
return this.rightSide === true
? this.layout.rows.top[2] === 'r'
: this.layout.rows.top[0] === 'l'
},
footerSlot () {
return this.rightSide === true
? this.layout.rows.bottom[2] === 'r'
: this.layout.rows.bottom[0] === 'l'
},
aboveStyle () {
const css = {}
if (this.layout.header.space === true && this.headerSlot === false) {
if (this.fixed === true) {
css.top = `${this.layout.header.offset}px`
}
else if (this.layout.header.space === true) {
css.top = `${this.layout.header.size}px`
}
}
if (this.layout.footer.space === true && this.footerSlot === false) {
if (this.fixed === true) {
css.bottom = `${this.layout.footer.offset}px`
}
else if (this.layout.footer.space === true) {
css.bottom = `${this.layout.footer.size}px`
}
}
return css
},
style () {
const style = { width: `${this.size}px` }
return this.belowBreakpoint === true
? style
: Object.assign(style, this.aboveStyle)
},
classes () {
return `q-drawer--${this.side}` +
(this.bordered === true ? ' q-drawer--bordered' : '') +
(this.isDark === true ? ' q-drawer--dark q-dark' : '') +
(this.showing !== true ? ' q-layout--prevent-focus' : '') +
(
this.belowBreakpoint === true
? ' fixed q-drawer--on-top q-drawer--mobile q-drawer--top-padding'
: ` q-drawer--${this.isMini === true ? 'mini' : 'standard'}` +
(this.fixed === true || this.onLayout !== true ? ' fixed' : '') +
(this.overlay === true || this.miniToOverlay === true ? ' q-drawer--on-top' : '') +
(this.headerSlot === true ? ' q-drawer--top-padding' : '')
)
},
stateDirection () {
return (this.$q.lang.rtl === true ? -1 : 1) * (this.rightSide === true ? 1 : -1)
},
isMini () {
return this.mini === true && this.belowBreakpoint !== true
},
onNativeEvents () {
if (this.belowBreakpoint !== true) {
const evt = {
'!click': e => { this.$emit('click', e) }
}
mouseEvents.forEach(name => {
evt[name] = e => {
this.qListeners[name] !== void 0 && this.$emit(name, e)
}
})
return evt
}
},
hideOnRouteChange () {
return this.persistent !== true &&
(this.belowBreakpoint === true || this.onScreenOverlay === true)
},
openDirective () {
const dir = this.$q.lang.rtl === true ? this.side : this.otherSide
return [{
name: 'touch-pan',
value: this.__openByTouch,
modifiers: {
[ dir ]: true,
mouse: true
}
}]
},
contentCloseDirective () {
if (this.noSwipeClose !== true) {
const dir = this.$q.lang.rtl === true ? this.otherSide : this.side
return [{
name: 'touch-pan',
value: this.__closeByTouch,
modifiers: {
[ dir ]: true,
mouse: true
}
}]
}
},
backdropCloseDirective () {
if (this.noSwipeBackdrop !== true) {
const dir = this.$q.lang.rtl === true ? this.otherSide : this.side
return [{
name: 'touch-pan',
value: this.__closeByTouch,
modifiers: {
[ dir ]: true,
mouse: true,
mouseAllDir: true
}
}]
}
}
},
methods: {
__applyPosition (position) {
if (position === void 0) {
this.$nextTick(() => {
position = this.showing === true ? 0 : this.size
this.__applyPosition(this.stateDirection * position)
})
}
else if (this.$refs.content !== void 0) {
if (
this.layout.container === true &&
this.rightSide === true &&
(this.belowBreakpoint === true || Math.abs(position) === this.size)
) {
position += this.stateDirection * this.layout.scrollbarWidth
}
if (this.__lastPosition !== position) {
this.$refs.content.style.transform = `translateX(${position}px)`
this.__lastPosition = position
}
}
},
__applyBackdrop (x, retry) {
if (this.$refs.backdrop !== void 0) {
this.$refs.backdrop.style.backgroundColor =
this.lastBackdropBg = `rgba(0,0,0,${x * 0.4})`
}
else {
// rendered nodes might not have
// picked up this.showing change yet,
// so we need one retry
retry !== true && this.$nextTick(() => {
this.__applyBackdrop(x, true)
})
}
},
__setBackdropVisible (v) {
if (this.$refs.backdrop !== void 0) {
this.$refs.backdrop.classList[v === true ? 'remove' : 'add']('hidden')
}
},
__setScrollable (v) {
const action = v === true
? 'remove'
: (this.layout.container !== true ? 'add' : '')
action !== '' && document.body.classList[action]('q-body--drawer-toggle')
},
__animateMini () {
if (this.timerMini !== void 0) {
clearTimeout(this.timerMini)
}
else if (this.$el !== void 0) {
this.$el.classList.add('q-drawer--mini-animate')
}
this.timerMini = setTimeout(() => {
this.$el !== void 0 && this.$el.classList.remove('q-drawer--mini-animate')
this.timerMini = void 0
}, 150)
},
__openByTouch (evt) {
if (this.showing !== false) {
// some browsers might capture and trigger this
// even if Drawer has just been opened (but animation is still pending)
return
}
const
width = this.size,
position = between(evt.distance.x, 0, width)
if (evt.isFinal === true) {
const
el = this.$refs.content,
opened = position >= Math.min(75, width)
el.classList.remove('no-transition')
if (opened === true) {
this.show()
}
else {
this.layout.__animate()
this.__applyBackdrop(0)
this.__applyPosition(this.stateDirection * width)
el.classList.remove('q-drawer--delimiter')
el.classList.add('q-layout--prevent-focus')
this.__setBackdropVisible(false)
}
return
}
this.__applyPosition(
(this.$q.lang.rtl === true ? this.rightSide !== true : this.rightSide)
? Math.max(width - position, 0)
: Math.min(0, position - width)
)
this.__applyBackdrop(
between(position / width, 0, 1)
)
if (evt.isFirst === true) {
const el = this.$refs.content
el.classList.add('no-transition')
el.classList.add('q-drawer--delimiter')
el.classList.remove('q-layout--prevent-focus')
this.__setBackdropVisible(true)
}
},
__closeByTouch (evt) {
if (this.showing !== true) {
// some browsers might capture and trigger this
// even if Drawer has just been closed (but animation is still pending)
return
}
const
width = this.size,
dir = evt.direction === this.side,
position = (this.$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)
this.$refs.content.classList.remove('no-transition')
if (opened === true) {
this.layout.__animate()
this.__applyBackdrop(1)
this.__applyPosition(0)
}
else {
this.hide()
}
return
}
this.__applyPosition(this.stateDirection * position)
this.__applyBackdrop(between(1 - position / width, 0, 1))
if (evt.isFirst === true) {
this.$refs.content.classList.add('no-transition')
}
},
__show (evt, noEvent) {
this.__addHistory()
this.__setBackdropVisible(true)
evt !== false && this.layout.__animate()
this.__applyPosition(0)
if (this.belowBreakpoint === true) {
const otherSide = this.layout.instances[this.otherSide]
if (otherSide !== void 0 && otherSide.belowBreakpoint === true) {
otherSide.hide(false)
}
this.__applyBackdrop(1)
this.layout.container !== true && this.__preventScroll(true)
}
else {
this.__applyBackdrop(0)
evt !== false && this.__setScrollable(false)
}
this.__setTimeout(() => {
evt !== false && this.__setScrollable(true)
noEvent !== true && this.$emit('show', evt)
}, duration)
},
__hide (evt, noEvent) {
this.__removeHistory()
evt !== false && this.layout.__animate()
this.__applyBackdrop(0)
this.__applyPosition(this.stateDirection * this.size)
this.__setBackdropVisible(false)
this.__cleanup()
noEvent !== true && this.__setTimeout(() => {
this.$emit('hide', evt)
}, duration)
},
__cleanup () {
this.__preventScroll(false)
this.__setScrollable(true)
},
__update (prop, val) {
if (this.layout[this.side][prop] !== val) {
this.layout[this.side][prop] = val
}
},
__updateLocal (prop, val) {
if (this[prop] !== val) {
this[prop] = val
}
},
__updateSizeOnLayout (miniToOverlay, size) {
this.__update('size', miniToOverlay === true ? this.miniWidth : size)
}
},
created () {
this.layout.instances[this.side] = this
this.__updateSizeOnLayout(this.miniToOverlay, this.size)
this.__update('space', this.onLayout)
this.__update('offset', this.offset)
if (
this.showIfAbove === true &&
this.value !== true &&
this.showing === true &&
this.qListeners.input !== void 0
) {
this.$emit('input', true)
}
},
mounted () {
this.$emit('on-layout', this.onLayout)
this.$emit('mini-state', this.isMini)
this.lastDesktopState = this.showIfAbove === true
const fn = () => {
const action = this.showing === true ? 'show' : 'hide'
this[`__${action}`](false, true)
}
if (this.layout.totalWidth !== 0) {
// make sure that all computed properties
// have been updated before calling __show/__hide()
this.$nextTick(fn)
return
}
this.watcher = this.$watch('layout.totalWidth', () => {
this.watcher()
this.watcher = void 0
if (this.showing === false && this.showIfAbove === true && this.belowBreakpoint === false) {
this.show(false)
}
else {
fn()
}
})
},
beforeDestroy () {
this.watcher !== void 0 && this.watcher()
clearTimeout(this.timerMini)
this.showing === true && this.__cleanup()
if (this.layout.instances[this.side] === this) {
this.layout.instances[this.side] = void 0
this.__update('size', 0)
this.__update('offset', 0)
this.__update('space', false)
}
},
render (h) {
const child = []
if (this.belowBreakpoint === true) {
this.noSwipeOpen !== true && child.push(
h('div', {
staticClass: `q-drawer__opener fixed-${this.side}`,
attrs: ariaHidden,
directives: this.openDirective
})
)
child.push(
h('div', {
ref: 'backdrop',
staticClass: 'fullscreen q-drawer__backdrop',
class: this.backdropClass,
attrs: ariaHidden,
style: this.lastBackdropBg !== void 0
? { backgroundColor: this.lastBackdropBg }
: null,
on: cache(this, 'bkdrop', { click: this.hide }),
directives: this.showing === false
? void 0
: this.backdropCloseDirective
})
)
}
const content = [
h('div', {
staticClass: 'q-drawer__content fit ' + (this.layout.container === true ? 'overflow-auto' : 'scroll'),
class: this.contentClass,
style: this.contentStyle
}, this.isMini === true && this.$scopedSlots.mini !== void 0
? this.$scopedSlots.mini()
: slot(this, 'default')
)
]
if (this.elevated === true && this.showing === true) {
content.push(
h('div', {
staticClass: 'q-layout__shadow absolute-full overflow-hidden no-pointer-events'
})
)
}
child.push(
h('aside', {
ref: 'content',
staticClass: `q-drawer`,
class: this.classes,
style: this.style,
on: this.onNativeEvents,
directives: this.belowBreakpoint === true
? this.contentCloseDirective
: void 0
}, content)
)
return h('div', { staticClass: 'q-drawer-container' }, child)
}
})