UNPKG

quasar-framework

Version:

Build responsive SPA, SSR, PWA, Hybrid Mobile Apps and Electron apps, all simultaneously using the same codebase

506 lines (473 loc) 13 kB
import TouchPan from '../../directives/touch-pan.js' import { between } from '../../utils/format.js' import ModelToggleMixin from '../../mixins/model-toggle.js' import preventScroll from '../../utils/prevent-scroll.js' const duration = 150 export default { name: 'QLayoutDrawer', 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) }, width: { type: Number, default: process.env.THEME === 'mat' ? 300 : 280 }, mini: Boolean, miniWidth: { type: Number, default: 60 }, breakpoint: { type: Number, default: 992 }, behavior: { type: String, validator: v => ['default', 'desktop', 'mobile'].includes(v), default: 'default' }, showIfAbove: Boolean, contentStyle: Object, contentClass: [String, Object, Array], noHideOnRouteChange: Boolean, noSwipeOpen: Boolean, noSwipeClose: Boolean }, data () { const largeScreenState = this.showIfAbove || ( 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 } }, watch: { belowBreakpoint (val) { 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(false) } else if (!this.overlay) { // from xs to lg this[this.largeScreenState ? 'show' : 'hide'](false) } }, side (_, oldSide) { this.layout[oldSide].space = false this.layout[oldSide].offset = 0 }, 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) )) }, 'layout.scrollbarWidth' () { this.applyPosition(this.showing ? 0 : void 0) }, offset (val) { this.__update('offset', val) }, onLayout (val) { this.$emit('on-layout', val) this.__update('space', val) }, $route () { if (this.noHideOnRouteChange) { return } if (this.mobileOpened || this.onScreenOverlay) { this.hide() } }, rightSide () { this.applyPosition() }, size (val) { this.applyPosition() this.__update('size', val) }, '$q.i18n.rtl' () { this.applyPosition() }, mini () { if (this.value) { this.layout.__animate() } } }, computed: { rightSide () { return this.side === 'right' }, offset () { return this.showing && !this.mobileOpened && !this.overlay ? this.size : 0 }, size () { return this.isMini ? this.miniWidth : this.width }, 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 { 'no-pointer-events': !this.showing || !this.mobileView } }, 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' ) }, belowClass () { return { 'fixed': true, 'on-top': true, 'q-layout-drawer-delimiter': this.fixed && this.showing, 'q-layout-drawer-mobile': true, 'top-padding': true } }, aboveClass () { return { 'fixed': this.fixed || !this.onLayout, 'q-layout-drawer-mini': this.isMini, 'q-layout-drawer-normal': !this.isMini, 'q-layout-drawer-delimiter': this.fixed && this.showing, '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, { width: `${this.size}px` }, this.mobileView ? '' : this.aboveStyle ] }, computedClass () { return [ `q-layout-drawer-${this.side}`, this.layout.container ? 'overflow-auto' : 'scroll', this.contentClass, this.mobileView ? this.belowClass : this.aboveClass ] }, stateDirection () { return (this.$q.i18n.rtl ? -1 : 1) * (this.rightSide ? 1 : -1) }, isMini () { return this.mini && !this.mobileView }, onNativeEvents () { if (!this.mobileView) { return { '!click': e => { this.$emit('click', e) }, mouseover: e => { this.$emit('mouseover', e) }, mouseout: e => { this.$emit('mouseout', e) } } } } }, methods: { applyPosition (position) { if (position === void 0) { this.$nextTick(() => { position = this.showing ? 0 : this.size this.applyPosition(this.stateDirection * position) }) } else if (this.$refs.content) { if (this.layout.container && this.rightSide && (this.mobileView || Math.abs(position) === this.size)) { position += this.stateDirection * this.layout.scrollbarWidth } this.$refs.content.style.transform = `translateX(${position}px)` } }, applyBackdrop (x) { if (this.$refs.backdrop) { this.$refs.backdrop.style.backgroundColor = `rgba(0,0,0,${x * 0.4})` } }, __setScrollable (v) { if (!this.layout.container) { document.body.classList[v ? 'add' : 'remove']('q-body-drawer-toggle') } }, __openByTouch (evt) { if (!this.belowBreakpoint) { return } const width = this.size, position = between(evt.distance.x, 0, width) if (evt.isFinal) { const el = this.$refs.content, opened = position >= Math.min(75, width) el.classList.remove('no-transition') if (opened) { this.show() } else { this.layout.__animate() this.applyBackdrop(0) this.applyPosition(this.stateDirection * width) el.classList.remove('q-layout-drawer-delimiter') } return } this.applyPosition( (this.$q.i18n.rtl ? !this.rightSide : this.rightSide) ? Math.max(width - position, 0) : Math.min(0, position - width) ) this.applyBackdrop( between(position / width, 0, 1) ) if (evt.isFirst) { const el = this.$refs.content el.classList.add('no-transition') el.classList.add('q-layout-drawer-delimiter') } }, __closeByTouch (evt) { if (!this.mobileOpened) { return } const width = this.size, dir = evt.direction === this.side, position = (this.$q.i18n.rtl ? !dir : dir) ? between(evt.distance.x, 0, width) : 0 if (evt.isFinal) { const opened = Math.abs(position) < Math.min(75, width) this.$refs.content.classList.remove('no-transition') if (opened) { 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) { this.$refs.content.classList.add('no-transition') } }, __show (animate = true) { animate && this.layout.__animate() this.applyPosition(0) const otherSide = this.layout.instances[this.rightSide ? 'left' : 'right'] if (otherSide && otherSide.mobileOpened) { otherSide.hide() } if (this.belowBreakpoint) { this.mobileOpened = true this.applyBackdrop(1) if (!this.layout.container) { this.preventedScroll = true preventScroll(true) } } else { this.__setScrollable(true) } clearTimeout(this.timer) this.timer = setTimeout(() => { if (this.showPromise) { this.showPromise.then(() => { this.__setScrollable(false) }) this.showPromiseResolve() } }, duration) }, __hide (animate = true) { animate && this.layout.__animate() if (this.mobileOpened) { this.mobileOpened = false } this.applyPosition(this.stateDirection * this.size) this.applyBackdrop(0) this.__cleanup() clearTimeout(this.timer) this.timer = setTimeout(() => { this.hidePromise && this.hidePromiseResolve() }, duration) }, __cleanup () { if (this.preventedScroll) { this.preventedScroll = false preventScroll(false) } this.__setScrollable(false) }, __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 } } }, created () { this.layout.instances[this.side] = this this.__update('size', this.size) this.__update('space', this.onLayout) this.__update('offset', this.offset) }, mounted () { this.applyPosition(this.showing ? 0 : void 0) }, beforeDestroy () { clearTimeout(this.timer) this.showing && this.__cleanup() if (this.layout.instances[this.side] === this) { this.layout.instances[this.side] = null this.__update('size', 0) this.__update('offset', 0) this.__update('space', false) } }, render (h) { const child = [ this.mobileView && !this.noSwipeOpen ? h('div', { staticClass: `q-layout-drawer-opener fixed-${this.side}`, directives: [{ name: 'touch-pan', modifiers: { horizontal: true }, value: this.__openByTouch }] }) : null, h('div', { ref: 'backdrop', staticClass: 'fullscreen q-layout-backdrop q-layout-transition', 'class': this.backdropClass, 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', { ref: 'content', staticClass: `q-layout-drawer q-layout-transition`, 'class': this.computedClass, style: this.computedStyle, attrs: this.$attrs, on: this.onNativeEvents, directives: this.mobileView && !this.noSwipeClose ? [{ name: 'touch-pan', modifiers: { horizontal: true }, value: this.__closeByTouch }] : null }, this.isMini && this.$slots.mini ? [ this.$slots.mini ] : this.$slots.default ) ])) } }