1
// Styles
2
import './VNavigationDrawer.sass'
3

4
// Components
5
import VImg, { srcObject } from '../VImg/VImg'
6

7
// Mixins
8
import Applicationable from '../../mixins/applicationable'
9
import Colorable from '../../mixins/colorable'
10
import Dependent from '../../mixins/dependent'
11
import Mobile from '../../mixins/mobile'
12
import Overlayable from '../../mixins/overlayable'
13
import SSRBootable from '../../mixins/ssr-bootable'
14
import Themeable from '../../mixins/themeable'
15

16
// Directives
17
import ClickOutside from '../../directives/click-outside'
18
import Resize from '../../directives/resize'
19
import Touch from '../../directives/touch'
20

21
// Utilities
22
import { convertToUnit, getSlot } from '../../util/helpers'
23
import mixins from '../../util/mixins'
24

25
// Types
26
import { VNode, VNodeDirective, PropType } from 'vue'
27
import { TouchWrapper } from 'vuetify/types'
28

29 1
const baseMixins = mixins(
30
  Applicationable('left', [
31
    'isActive',
32
    'isMobile',
33
    'miniVariant',
34
    'expandOnHover',
35
    'permanent',
36
    'right',
37
    'temporary',
38
    'width',
39
  ]),
40
  Colorable,
41
  Dependent,
42
  Mobile,
43
  Overlayable,
44
  SSRBootable,
45
  Themeable
46
)
47

48
/* @vue/component */
49
export default baseMixins.extend({
50
  name: 'v-navigation-drawer',
51

52
  directives: {
53
    ClickOutside,
54
    Resize,
55
    Touch,
56
  },
57

58 1
  provide (): object {
59 1
    return {
60
      isInNav: this.tag === 'nav',
61
    }
62
  },
63

64
  props: {
65
    bottom: Boolean,
66
    clipped: Boolean,
67
    disableResizeWatcher: Boolean,
68
    disableRouteWatcher: Boolean,
69
    expandOnHover: Boolean,
70
    floating: Boolean,
71
    height: {
72
      type: [Number, String],
73 1
      default (): string {
74 1
        return this.app ? '100vh' : '100%'
75
      },
76
    },
77
    miniVariant: Boolean,
78
    miniVariantWidth: {
79
      type: [Number, String],
80
      default: 56,
81
    },
82
    permanent: Boolean,
83
    right: Boolean,
84
    src: {
85
      type: [String, Object] as PropType<string | srcObject>,
86
      default: '',
87
    },
88
    stateless: Boolean,
89
    tag: {
90
      type: String,
91 1
      default (): string {
92 1
        return this.app ? 'nav' : 'aside'
93
      },
94
    },
95
    temporary: Boolean,
96
    touchless: Boolean,
97
    width: {
98
      type: [Number, String],
99
      default: 256,
100
    },
101
    value: null as unknown as PropType<any>,
102
  },
103

104 1
  data: () => ({
105
    isMouseover: false,
106
    touchArea: {
107
      left: 0,
108
      right: 0,
109
    },
110
    stackMinZIndex: 6,
111
  }),
112

113
  computed: {
114
    /**
115
     * Used for setting an app value from a dynamic
116
     * property. Called from applicationable.js
117
     */
118 1
    applicationProperty (): string {
119 1
      return this.right ? 'right' : 'left'
120
    },
121 1
    classes (): object {
122 1
      return {
123
        'v-navigation-drawer': true,
124
        'v-navigation-drawer--absolute': this.absolute,
125
        'v-navigation-drawer--bottom': this.bottom,
126
        'v-navigation-drawer--clipped': this.clipped,
127
        'v-navigation-drawer--close': !this.isActive,
128 1
        'v-navigation-drawer--fixed': !this.absolute && (this.app || this.fixed),
129
        'v-navigation-drawer--floating': this.floating,
130
        'v-navigation-drawer--is-mobile': this.isMobile,
131
        'v-navigation-drawer--is-mouseover': this.isMouseover,
132
        'v-navigation-drawer--mini-variant': this.isMiniVariant,
133
        'v-navigation-drawer--custom-mini-variant': Number(this.miniVariantWidth) !== 56,
134
        'v-navigation-drawer--open': this.isActive,
135
        'v-navigation-drawer--open-on-hover': this.expandOnHover,
136
        'v-navigation-drawer--right': this.right,
137
        'v-navigation-drawer--temporary': this.temporary,
138
        ...this.themeClasses,
139
      }
140
    },
141 1
    computedMaxHeight (): number | null {
142 1
      if (!this.hasApp) return null
143

144
      const computedMaxHeight = (
145 1
        this.$vuetify.application.bottom +
146
        this.$vuetify.application.footer +
147
        this.$vuetify.application.bar
148
      )
149

150 1
      if (!this.clipped) return computedMaxHeight
151

152 1
      return computedMaxHeight + this.$vuetify.application.top
153
    },
154 1
    computedTop (): number {
155 1
      if (!this.hasApp) return 0
156

157 1
      let computedTop = this.$vuetify.application.bar
158

159 1
      computedTop += this.clipped
160 1
        ? this.$vuetify.application.top
161 1
        : 0
162

163 1
      return computedTop
164
    },
165 1
    computedTransform (): number {
166 1
      if (this.isActive) return 0
167 1
      if (this.isBottom) return 100
168 1
      return this.right ? 100 : -100
169
    },
170 1
    computedWidth (): string | number {
171 1
      return this.isMiniVariant ? this.miniVariantWidth : this.width
172
    },
173 1
    hasApp (): boolean {
174 1
      return (
175 1
        this.app &&
176 1
        (!this.isMobile && !this.temporary)
177
      )
178
    },
179 1
    isBottom (): boolean {
180 1
      return this.bottom && this.isMobile
181
    },
182 1
    isMiniVariant (): boolean {
183 1
      return (
184 1
        !this.expandOnHover &&
185 1
        this.miniVariant
186
      ) || (
187 1
        this.expandOnHover &&
188 1
        !this.isMouseover
189
      )
190
    },
191 1
    isMobile (): boolean {
192 1
      return (
193 1
        !this.stateless &&
194 1
        !this.permanent &&
195 1
        Mobile.options.computed.isMobile.call(this)
196
      )
197
    },
198 1
    reactsToClick (): boolean {
199 1
      return (
200 1
        !this.stateless &&
201 1
        !this.permanent &&
202 1
        (this.isMobile || this.temporary)
203
      )
204
    },
205 1
    reactsToMobile (): boolean {
206 1
      return (
207 1
        this.app &&
208 1
        !this.disableResizeWatcher &&
209 1
        !this.permanent &&
210 1
        !this.stateless &&
211 1
        !this.temporary
212
      )
213
    },
214 1
    reactsToResize (): boolean {
215 1
      return !this.disableResizeWatcher && !this.stateless
216
    },
217 1
    reactsToRoute (): boolean {
218 1
      return (
219 1
        !this.disableRouteWatcher &&
220 1
        !this.stateless &&
221 1
        (this.temporary || this.isMobile)
222
      )
223
    },
224 1
    showOverlay (): boolean {
225 1
      return (
226 1
        !this.hideOverlay &&
227 1
        this.isActive &&
228 1
        (this.isMobile || this.temporary)
229
      )
230
    },
231 1
    styles (): object {
232 1
      const translate = this.isBottom ? 'translateY' : 'translateX'
233 1
      return {
234
        height: convertToUnit(this.height),
235 1
        top: !this.isBottom ? convertToUnit(this.computedTop) : 'auto',
236 1
        maxHeight: this.computedMaxHeight != null
237 1
          ? `calc(100% - ${convertToUnit(this.computedMaxHeight)})`
238 1
          : undefined,
239
        transform: `${translate}(${convertToUnit(this.computedTransform, '%')})`,
240
        width: convertToUnit(this.computedWidth),
241
      }
242
    },
243
  },
244

245
  watch: {
246
    $route: 'onRouteChange',
247 1
    isActive (val) {
248 1
      this.$emit('input', val)
249
    },
250
    /**
251
     * When mobile changes, adjust the active state
252
     * only when there has been a previous value
253
     */
254 1
    isMobile (val, prev) {
255 1
      !val &&
256 1
        this.isActive &&
257 1
        !this.temporary &&
258 1
        this.removeOverlay()
259

260 1
      if (prev == null ||
261 1
        !this.reactsToResize ||
262 1
        !this.reactsToMobile
263 1
      ) return
264

265 1
      this.isActive = !val
266
    },
267 1
    permanent (val) {
268
      // If enabling prop enable the drawer
269 1
      if (val) this.isActive = true
270
    },
271 1
    showOverlay (val) {
272 1
      if (val) this.genOverlay()
273 1
      else this.removeOverlay()
274
    },
275 1
    value (val) {
276 1
      if (this.permanent) return
277

278 1
      if (val == null) {
279 0
        this.init()
280 0
        return
281
      }
282

283 1
      if (val !== this.isActive) this.isActive = val
284
    },
285
    expandOnHover: 'updateMiniVariant',
286 1
    isMouseover (val) {
287 1
      this.updateMiniVariant(!val)
288
    },
289
  },
290

291 1
  beforeMount () {
292 1
    this.init()
293
  },
294

295
  methods: {
296 1
    calculateTouchArea () {
297 1
      const parent = this.$el.parentNode as Element
298

299 1
      if (!parent) return
300

301 1
      const parentRect = parent.getBoundingClientRect()
302

303 1
      this.touchArea = {
304
        left: parentRect.left + 50,
305
        right: parentRect.right - 50,
306
      }
307
    },
308 1
    closeConditional () {
309 1
      return this.isActive && !this._isDestroyed && this.reactsToClick
310
    },
311 1
    genAppend () {
312 1
      return this.genPosition('append')
313
    },
314 0
    genBackground () {
315 0
      const props = {
316
        height: '100%',
317
        width: '100%',
318
        src: this.src,
319
      }
320

321 1
      const image = this.$scopedSlots.img
322 0
        ? this.$scopedSlots.img(props)
323 0
        : this.$createElement(VImg, { props })
324

325 0
      return this.$createElement('div', {
326
        staticClass: 'v-navigation-drawer__image',
327
      }, [image])
328
    },
329 1
    genDirectives (): VNodeDirective[] {
330 1
      const directives = [{
331
        name: 'click-outside',
332
        value: {
333 0
          handler: () => { this.isActive = false },
334
          closeConditional: this.closeConditional,
335
          include: this.getOpenDependentElements,
336
        },
337
      }]
338

339 1
      if (!this.touchless && !this.stateless) {
340 1
        directives.push({
341
          name: 'touch',
342
          value: {
343
            parent: true,
344
            left: this.swipeLeft,
345
            right: this.swipeRight,
346
          },
347
        } as any)
348
      }
349

350 1
      return directives
351
    },
352 1
    genListeners () {
353 1
      const on: Record<string, (e: Event) => void> = {
354 0
        transitionend: (e: Event) => {
355 1
          if (e.target !== e.currentTarget) return
356 0
          this.$emit('transitionend', e)
357

358
          // IE11 does not support new Event('resize')
359 0
          const resizeEvent = document.createEvent('UIEvents')
360 0
          resizeEvent.initUIEvent('resize', true, false, window, 0)
361 0
          window.dispatchEvent(resizeEvent)
362
        },
363
      }
364

365 1
      if (this.miniVariant) {
366 1
        on.click = () => this.$emit('update:mini-variant', false)
367
      }
368

369 1
      if (this.expandOnHover) {
370 1
        on.mouseenter = () => (this.isMouseover = true)
371 1
        on.mouseleave = () => (this.isMouseover = false)
372
      }
373

374 1
      return on
375
    },
376 1
    genPosition (name: 'prepend' | 'append') {
377 1
      const slot = getSlot(this, name)
378

379 1
      if (!slot) return slot
380

381 0
      return this.$createElement('div', {
382
        staticClass: `v-navigation-drawer__${name}`,
383
      }, slot)
384
    },
385 1
    genPrepend () {
386 1
      return this.genPosition('prepend')
387
    },
388 1
    genContent () {
389 1
      return this.$createElement('div', {
390
        staticClass: 'v-navigation-drawer__content',
391
      }, this.$slots.default)
392
    },
393 1
    genBorder () {
394 1
      return this.$createElement('div', {
395
        staticClass: 'v-navigation-drawer__border',
396
      })
397
    },
398 1
    init () {
399 1
      if (this.permanent) {
400 1
        this.isActive = true
401 1
      } else if (this.stateless ||
402 1
        this.value != null
403
      ) {
404 1
        this.isActive = this.value
405 1
      } else if (!this.temporary) {
406 1
        this.isActive = !this.isMobile
407
      }
408
    },
409 1
    onRouteChange () {
410 1
      if (this.reactsToRoute && this.closeConditional()) {
411 1
        this.isActive = false
412
      }
413
    },
414 1
    swipeLeft (e: TouchWrapper) {
415 1
      if (this.isActive && this.right) return
416 1
      this.calculateTouchArea()
417

418 1
      if (Math.abs(e.touchendX - e.touchstartX) < 100) return
419 1
      if (this.right &&
420 1
        e.touchstartX >= this.touchArea.right
421 1
      ) this.isActive = true
422 1
      else if (!this.right && this.isActive) this.isActive = false
423
    },
424 1
    swipeRight (e: TouchWrapper) {
425 1
      if (this.isActive && !this.right) return
426 1
      this.calculateTouchArea()
427

428 1
      if (Math.abs(e.touchendX - e.touchstartX) < 100) return
429 1
      if (!this.right &&
430 1
        e.touchstartX <= this.touchArea.left
431 1
      ) this.isActive = true
432 1
      else if (this.right && this.isActive) this.isActive = false
433
    },
434
    /**
435
     * Update the application layout
436
     */
437 1
    updateApplication () {
438 1
      if (
439 1
        !this.isActive ||
440 1
        this.isMobile ||
441 1
        this.temporary ||
442 1
        !this.$el
443 1
      ) return 0
444

445 1
      const width = Number(this.computedWidth)
446

447 1
      return isNaN(width) ? this.$el.clientWidth : width
448
    },
449 1
    updateMiniVariant (val: boolean) {
450 1
      if (this.miniVariant !== val) this.$emit('update:mini-variant', val)
451
    },
452
  },
453

454 1
  render (h): VNode {
455 1
    const children = [
456
      this.genPrepend(),
457
      this.genContent(),
458
      this.genAppend(),
459
      this.genBorder(),
460
    ]
461

462 1
    if (this.src || getSlot(this, 'img')) children.unshift(this.genBackground())
463

464 1
    return h(this.tag, this.setBackgroundColor(this.color, {
465
      class: this.classes,
466
      style: this.styles,
467
      directives: this.genDirectives(),
468
      on: this.genListeners(),
469
    }), children)
470
  },
471
})

Read our documentation on viewing source code .

Loading