vuetify
Version:
Vue Material Component Framework
472 lines (426 loc) • 12 kB
text/typescript
// Styles
import './VNavigationDrawer.sass'
// Components
import VImg, { srcObject } from '../VImg/VImg'
// Mixins
import Applicationable from '../../mixins/applicationable'
import Colorable from '../../mixins/colorable'
import Dependent from '../../mixins/dependent'
import Mobile from '../../mixins/mobile'
import Overlayable from '../../mixins/overlayable'
import SSRBootable from '../../mixins/ssr-bootable'
import Themeable from '../../mixins/themeable'
// Directives
import ClickOutside from '../../directives/click-outside'
import Resize from '../../directives/resize'
import Touch from '../../directives/touch'
// Utilities
import { convertToUnit, getSlot } from '../../util/helpers'
import mixins from '../../util/mixins'
// Types
import { VNode, VNodeDirective, PropType } from 'vue'
import { TouchWrapper } from 'vuetify/types'
const baseMixins = mixins(
Applicationable('left', [
'isActive',
'isMobile',
'miniVariant',
'expandOnHover',
'permanent',
'right',
'temporary',
'width',
]),
Colorable,
Dependent,
Mobile,
Overlayable,
SSRBootable,
Themeable
)
/* @vue/component */
export default baseMixins.extend({
name: 'v-navigation-drawer',
directives: {
ClickOutside,
Resize,
Touch,
},
provide (): object {
return {
isInNav: this.tag === 'nav',
}
},
props: {
bottom: Boolean,
clipped: Boolean,
disableResizeWatcher: Boolean,
disableRouteWatcher: Boolean,
expandOnHover: Boolean,
floating: Boolean,
height: {
type: [Number, String],
default (): string {
return this.app ? '100vh' : '100%'
},
},
miniVariant: Boolean,
miniVariantWidth: {
type: [Number, String],
default: 56,
},
permanent: Boolean,
right: Boolean,
src: {
type: [String, Object] as PropType<string | srcObject>,
default: '',
},
stateless: Boolean,
tag: {
type: String,
default (): string {
return this.app ? 'nav' : 'aside'
},
},
temporary: Boolean,
touchless: Boolean,
width: {
type: [Number, String],
default: 256,
},
value: null as unknown as PropType<any>,
},
data: () => ({
isMouseover: false,
touchArea: {
left: 0,
right: 0,
},
stackMinZIndex: 6,
}),
computed: {
/**
* Used for setting an app value from a dynamic
* property. Called from applicationable.js
*/
applicationProperty (): string {
return this.right ? 'right' : 'left'
},
classes (): object {
return {
'v-navigation-drawer': true,
'v-navigation-drawer--absolute': this.absolute,
'v-navigation-drawer--bottom': this.bottom,
'v-navigation-drawer--clipped': this.clipped,
'v-navigation-drawer--close': !this.isActive,
'v-navigation-drawer--fixed': !this.absolute && (this.app || this.fixed),
'v-navigation-drawer--floating': this.floating,
'v-navigation-drawer--is-mobile': this.isMobile,
'v-navigation-drawer--is-mouseover': this.isMouseover,
'v-navigation-drawer--mini-variant': this.isMiniVariant,
'v-navigation-drawer--custom-mini-variant': Number(this.miniVariantWidth) !== 56,
'v-navigation-drawer--open': this.isActive,
'v-navigation-drawer--open-on-hover': this.expandOnHover,
'v-navigation-drawer--right': this.right,
'v-navigation-drawer--temporary': this.temporary,
...this.themeClasses,
}
},
computedMaxHeight (): number | null {
if (!this.hasApp) return null
const computedMaxHeight = (
this.$vuetify.application.bottom +
this.$vuetify.application.footer +
this.$vuetify.application.bar
)
if (!this.clipped) return computedMaxHeight
return computedMaxHeight + this.$vuetify.application.top
},
computedTop (): number {
if (!this.hasApp) return 0
let computedTop = this.$vuetify.application.bar
computedTop += this.clipped
? this.$vuetify.application.top
: 0
return computedTop
},
computedTransform (): number {
if (this.isActive) return 0
if (this.isBottom) return 100
return this.right ? 100 : -100
},
computedWidth (): string | number {
return this.isMiniVariant ? this.miniVariantWidth : this.width
},
hasApp (): boolean {
return (
this.app &&
(!this.isMobile && !this.temporary)
)
},
isBottom (): boolean {
return this.bottom && this.isMobile
},
isMiniVariant (): boolean {
return (
!this.expandOnHover &&
this.miniVariant
) || (
this.expandOnHover &&
!this.isMouseover
)
},
isMobile (): boolean {
return (
!this.stateless &&
!this.permanent &&
Mobile.options.computed.isMobile.call(this)
)
},
reactsToClick (): boolean {
return (
!this.stateless &&
!this.permanent &&
(this.isMobile || this.temporary)
)
},
reactsToMobile (): boolean {
return (
this.app &&
!this.disableResizeWatcher &&
!this.permanent &&
!this.stateless &&
!this.temporary
)
},
reactsToResize (): boolean {
return !this.disableResizeWatcher && !this.stateless
},
reactsToRoute (): boolean {
return (
!this.disableRouteWatcher &&
!this.stateless &&
(this.temporary || this.isMobile)
)
},
showOverlay (): boolean {
return (
!this.hideOverlay &&
this.isActive &&
(this.isMobile || this.temporary)
)
},
styles (): object {
const translate = this.isBottom ? 'translateY' : 'translateX'
return {
height: convertToUnit(this.height),
top: !this.isBottom ? convertToUnit(this.computedTop) : 'auto',
maxHeight: this.computedMaxHeight != null
? `calc(100% - ${convertToUnit(this.computedMaxHeight)})`
: undefined,
transform: `${translate}(${convertToUnit(this.computedTransform, '%')})`,
width: convertToUnit(this.computedWidth),
}
},
},
watch: {
$route: 'onRouteChange',
isActive (val) {
this.$emit('input', val)
},
/**
* When mobile changes, adjust the active state
* only when there has been a previous value
*/
isMobile (val, prev) {
!val &&
this.isActive &&
!this.temporary &&
this.removeOverlay()
if (prev == null ||
!this.reactsToResize ||
!this.reactsToMobile
) return
this.isActive = !val
},
permanent (val) {
// If enabling prop enable the drawer
if (val) this.isActive = true
},
showOverlay (val) {
if (val) this.genOverlay()
else this.removeOverlay()
},
value (val) {
if (this.permanent) return
if (val == null) {
this.init()
return
}
if (val !== this.isActive) this.isActive = val
},
expandOnHover: 'updateMiniVariant',
isMouseover (val) {
this.updateMiniVariant(!val)
},
},
beforeMount () {
this.init()
},
methods: {
calculateTouchArea () {
const parent = this.$el.parentNode as Element
if (!parent) return
const parentRect = parent.getBoundingClientRect()
this.touchArea = {
left: parentRect.left + 50,
right: parentRect.right - 50,
}
},
closeConditional () {
return this.isActive && !this._isDestroyed && this.reactsToClick
},
genAppend () {
return this.genPosition('append')
},
genBackground () {
const props = {
height: '100%',
width: '100%',
src: this.src,
}
const image = this.$scopedSlots.img
? this.$scopedSlots.img(props)
: this.$createElement(VImg, { props })
return this.$createElement('div', {
staticClass: 'v-navigation-drawer__image',
}, [image])
},
genDirectives (): VNodeDirective[] {
const directives = [{
name: 'click-outside',
value: {
handler: () => { this.isActive = false },
closeConditional: this.closeConditional,
include: this.getOpenDependentElements,
},
}]
if (!this.touchless && !this.stateless) {
directives.push({
name: 'touch',
value: {
parent: true,
left: this.swipeLeft,
right: this.swipeRight,
},
} as any)
}
return directives
},
genListeners () {
const on: Record<string, (e: Event) => void> = {
transitionend: (e: Event) => {
if (e.target !== e.currentTarget) return
this.$emit('transitionend', e)
// IE11 does not support new Event('resize')
const resizeEvent = document.createEvent('UIEvents')
resizeEvent.initUIEvent('resize', true, false, window, 0)
window.dispatchEvent(resizeEvent)
},
}
if (this.miniVariant) {
on.click = () => this.$emit('update:mini-variant', false)
}
if (this.expandOnHover) {
on.mouseenter = () => (this.isMouseover = true)
on.mouseleave = () => (this.isMouseover = false)
}
return on
},
genPosition (name: 'prepend' | 'append') {
const slot = getSlot(this, name)
if (!slot) return slot
return this.$createElement('div', {
staticClass: `v-navigation-drawer__${name}`,
}, slot)
},
genPrepend () {
return this.genPosition('prepend')
},
genContent () {
return this.$createElement('div', {
staticClass: 'v-navigation-drawer__content',
}, this.$slots.default)
},
genBorder () {
return this.$createElement('div', {
staticClass: 'v-navigation-drawer__border',
})
},
init () {
if (this.permanent) {
this.isActive = true
} else if (this.stateless ||
this.value != null
) {
this.isActive = this.value
} else if (!this.temporary) {
this.isActive = !this.isMobile
}
},
onRouteChange () {
if (this.reactsToRoute && this.closeConditional()) {
this.isActive = false
}
},
swipeLeft (e: TouchWrapper) {
if (this.isActive && this.right) return
this.calculateTouchArea()
if (Math.abs(e.touchendX - e.touchstartX) < 100) return
if (this.right &&
e.touchstartX >= this.touchArea.right
) this.isActive = true
else if (!this.right && this.isActive) this.isActive = false
},
swipeRight (e: TouchWrapper) {
if (this.isActive && !this.right) return
this.calculateTouchArea()
if (Math.abs(e.touchendX - e.touchstartX) < 100) return
if (!this.right &&
e.touchstartX <= this.touchArea.left
) this.isActive = true
else if (this.right && this.isActive) this.isActive = false
},
/**
* Update the application layout
*/
updateApplication () {
if (
!this.isActive ||
this.isMobile ||
this.temporary ||
!this.$el
) return 0
const width = Number(this.computedWidth)
return isNaN(width) ? this.$el.clientWidth : width
},
updateMiniVariant (val: boolean) {
if (this.miniVariant !== val) this.$emit('update:mini-variant', val)
},
},
render (h): VNode {
const children = [
this.genPrepend(),
this.genContent(),
this.genAppend(),
this.genBorder(),
]
if (this.src || getSlot(this, 'img')) children.unshift(this.genBackground())
return h(this.tag, this.setBackgroundColor(this.color, {
class: this.classes,
style: this.styles,
directives: this.genDirectives(),
on: this.genListeners(),
}), children)
},
})