UNPKG

quasar-framework

Version:

Simultaneously build desktop/mobile SPA websites & phone/tablet apps with VueJS

388 lines (362 loc) 9.97 kB
import TouchPan from '../../directives/touch-pan' import { cssTransform } from '../../utils/dom' import { between } from '../../utils/format' import { QResizeObservable } from '../observables' import ModelToggleMixin from '../../mixins/model-toggle' const bodyClassBelow = 'with-layout-drawer-opened', bodyClassAbove = 'with-layout-drawer-opened-above', duration = 150 export default { name: 'q-layout-drawer', inject: { layout: { default () { console.error('QLayoutDrawer needs to be child of QLayout') } } }, mixins: [ModelToggleMixin], directives: { TouchPan }, props: { overlay: Boolean, side: { type: String, default: 'left', validator: v => ['left', 'right'].includes(v) }, breakpoint: { type: Number, default: 992 }, behavior: { type: String, validator: v => ['default', 'desktop', 'mobile'].includes(v), default: 'default' }, contentStyle: Object, contentClass: [String, Object, Array], noSwipeOpen: Boolean, noSwipeClose: Boolean }, data () { const largeScreenState = this.value !== void 0 ? this.value : true, showing = this.behavior !== 'mobile' && this.breakpoint < this.layout.width && !this.overlay ? largeScreenState : false if (this.value !== void 0 && this.value !== showing) { this.$emit('input', showing) } return { showing, belowBreakpoint: ( this.behavior === 'mobile' || (this.behavior !== 'desktop' && this.breakpoint >= this.layout.width) ), largeScreenState, mobileOpened: false, size: 300, inTransit: false, position: 0, percentage: 0 } }, watch: { belowBreakpoint (val, old) { if (this.mobileOpened) { return } if (val) { // from lg to xs if (!this.overlay) { this.largeScreenState = this.showing } // ensure we close it for small screen this.hide() } else if (!this.overlay) { // from xs to lg this[this.largeScreenState ? 'show' : 'hide']() } }, behavior (val) { this.__updateLocal('belowBreakpoint', ( val === 'mobile' || (val !== 'desktop' && this.breakpoint >= this.layout.width) )) }, breakpoint (val) { this.__updateLocal('belowBreakpoint', ( this.behavior === 'mobile' || (this.behavior !== 'desktop' && val >= this.layout.width) )) }, 'layout.width' (val) { this.__updateLocal('belowBreakpoint', ( this.behavior === 'mobile' || (this.behavior !== 'desktop' && this.breakpoint >= val) )) }, offset (val) { this.__update('offset', val) }, onScreenOverlay () { if (this.animateOverlay) { this.layout.__animate() } }, onLayout (val) { this.__update('space', val) this.layout.__animate() }, $route () { if (this.mobileOpened || this.onScreenOverlay) { this.hide() } } }, computed: { rightSide () { return this.side === 'right' }, offset () { return this.showing && !this.mobileOpened ? this.size : 0 }, fixed () { return this.overlay || this.layout.view.indexOf(this.rightSide ? 'R' : 'L') > -1 }, onLayout () { return this.showing && !this.mobileView && !this.overlay }, onScreenOverlay () { return this.showing && !this.mobileView && this.overlay }, backdropClass () { return { 'q-layout-backdrop-transition': !this.inTransit, 'no-pointer-events': !this.inTransit && !this.showing } }, mobileView () { return this.belowBreakpoint || this.mobileOpened }, headerSlot () { return this.overlay ? false : (this.rightSide ? this.layout.rows.top[2] === 'r' : this.layout.rows.top[0] === 'l' ) }, footerSlot () { return this.overlay ? false : (this.rightSide ? this.layout.rows.bottom[2] === 'r' : this.layout.rows.bottom[0] === 'l' ) }, backdropStyle () { return { backgroundColor: `rgba(0,0,0,${this.percentage * 0.4})` } }, belowClass () { return { 'fixed': true, 'on-top': true, 'on-screen': this.showing, 'off-screen': !this.showing, 'transition-generic': !this.inTransit, 'top-padding': true } }, belowStyle () { if (this.inTransit) { return cssTransform(`translateX(${this.position}px)`) } }, aboveClass () { const onScreen = this.onLayout || this.onScreenOverlay return { 'off-screen': !onScreen, 'on-screen': onScreen, 'fixed': this.fixed || !this.onLayout, 'top-padding': this.headerSlot } }, aboveStyle () { const css = {} if (this.layout.header.space && !this.headerSlot) { if (this.fixed) { css.top = `${this.layout.header.offset}px` } else if (this.layout.header.space) { css.top = `${this.layout.header.size}px` } } if (this.layout.footer.space && !this.footerSlot) { if (this.fixed) { css.bottom = `${this.layout.footer.offset}px` } else if (this.layout.footer.space) { css.bottom = `${this.layout.footer.size}px` } } return css }, computedStyle () { return [this.contentStyle, this.mobileView ? this.belowStyle : this.aboveStyle] }, computedClass () { return [this.contentClass, this.mobileView ? this.belowClass : this.aboveClass] } }, render (h) { const child = [] if (this.mobileView) { if (!this.noSwipeOpen) { child.push(h('div', { staticClass: `q-layout-drawer-opener fixed-${this.side}`, directives: [{ name: 'touch-pan', modifiers: { horizontal: true }, value: this.__openByTouch }] })) } child.push(h('div', { staticClass: 'fullscreen q-layout-backdrop', 'class': this.backdropClass, style: this.backdropStyle, on: { click: this.hide }, directives: [{ name: 'touch-pan', modifiers: { horizontal: true }, value: this.__closeByTouch }] })) } return h('div', { staticClass: 'q-drawer-container' }, child.concat([ h('aside', { staticClass: `q-layout-drawer q-layout-drawer-${this.side} scroll q-layout-transition`, 'class': this.computedClass, style: this.computedStyle, attrs: this.$attrs, listeners: this.$listeners, directives: this.mobileView && !this.noSwipeClose ? [{ name: 'touch-pan', modifiers: { horizontal: true }, value: this.__closeByTouch }] : null }, [ this.$slots.default, h(QResizeObservable, { on: { resize: this.__onResize } }) ]) ])) }, created () { if (this.onLayout) { this.__update('space', true) this.__update('offset', this.offset) } this.$nextTick(() => { this.animateOverlay = true }) }, beforeDestroy () { clearTimeout(this.timer) this.__update('size', 0) this.__update('space', false) }, methods: { __openByTouch (evt) { if (!this.belowBreakpoint) { return } const width = this.size, position = between(evt.distance.x, 0, width) if (evt.isFinal) { const opened = position >= Math.min(75, width) this.inTransit = false if (opened) { this.show() } else { this.percentage = 0 } return } this.position = this.rightSide ? Math.max(width - position, 0) : Math.min(0, position - width) this.percentage = between(position / width, 0, 1) if (evt.isFirst) { document.body.classList.add(bodyClassBelow) this.inTransit = true } }, __closeByTouch (evt) { if (!this.mobileOpened) { return } const width = this.size, position = evt.direction === this.side ? between(evt.distance.x, 0, width) : 0 if (evt.isFinal) { const opened = Math.abs(position) < Math.min(75, width) this.inTransit = false if (opened) { this.percentage = 1 } else { this.hide() } return } this.position = (this.rightSide ? 1 : -1) * position this.percentage = between(1 - position / width, 0, 1) if (evt.isFirst) { this.inTransit = true } }, __show () { if (this.belowBreakpoint) { this.mobileOpened = true this.percentage = 1 } document.body.classList.add(this.belowBreakpoint ? bodyClassBelow : bodyClassAbove) clearTimeout(this.timer) this.timer = setTimeout(() => { if (this.showPromise) { this.showPromise.then(() => { document.body.classList.remove(bodyClassAbove) }) this.showPromiseResolve() } }, duration) }, __hide () { this.mobileOpened = false this.percentage = 0 document.body.classList.remove(bodyClassAbove) document.body.classList.remove(bodyClassBelow) clearTimeout(this.timer) this.timer = setTimeout(() => { this.hidePromise && this.hidePromiseResolve() }, duration) }, __onResize ({ width }) { this.__update('size', width) this.__updateLocal('size', width) }, __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 } } } }