vuematerial/vue-material

View on GitHub
src/components/MdMenu/MdMenuContent.vue

Summary

Maintainability
Test Coverage
F
5%
<template>
  <md-popover :md-settings="popperSettings" :md-active="shouldRender">
    <transition name="md-menu-content" :css="didMount" v-if="shouldRender" v-on="$listeners">
      <div
        :class="[menuClasses, mdContentClass, $mdActiveTheme]"
        :style="menuStyles"
        ref="menu">
        <div class="md-menu-content-container md-scrollbar" :class="$mdActiveTheme" ref="container">
          <md-list :class="listClasses" v-bind="filteredAttrs">
            <slot />
          </md-list>
        </div>
      </div>
    </transition>
  </md-popover>
</template>

<script>
  import MdComponent from 'core/MdComponent'
  import MdObserveEvent from 'core/utils/MdObserveEvent'
  import MdResizeObserver from 'core/utils/MdResizeObserver'
  import MdPopover from 'components/MdPopover/MdPopover'
  import MdFocusTrap from 'components/MdFocusTrap/MdFocusTrap'
  import MdList from 'components/MdList/MdList'
  import MdContains from 'core/utils/MdContains'

  export default new MdComponent({
    name: 'MdMenuContent',
    components: {
      MdPopover,
      MdFocusTrap,
      MdList
    },
    props: {
      mdListClass: [String, Boolean],
      mdContentClass: [String, Boolean]
    },
    inject: ['MdMenu'],
    data: () => ({
      highlightIndex: -1,
      didMount: false,
      highlightItems: [],
      popperSettings: null,
      menuStyles: ''
    }),
    computed: {
      filteredAttrs () {
        const attrs = this.$attrs
        delete attrs.id
        return attrs
      },
      highlightedItem () {
        return this.highlightItems[this.highlightIndex]
      },
      shouldRender () {
        return this.MdMenu.active
      },
      menuClasses () {
        const prefix = 'md-menu-content-'

        return {
          [prefix + this.MdMenu.direction]: true,
          [prefix + this.MdMenu.size]: true,
          'md-menu-content': this.didMount,
          'md-shallow': !this.didMount
        }
      },
      listClasses () {
        return {
          'md-dense': this.MdMenu.dense,
          ...this.mdListClass
        }
      }
    },
    watch: {
      shouldRender (shouldRender) {
        if (shouldRender) {
          this.setPopperSettings()
          setTimeout(() => {
            this.setInitialHighlightIndex()
            this.createClickEventObserver()
            this.createResizeObserver()
            this.createKeydownListener()
          }, 0)
        }
      }
    },
    methods: {
      setPopperSettings () {
        const { direction, alignTrigger } = this.MdMenu

        let { offsetX, offsetY } = this.getOffsets()

        if (!this.hasCustomOffsets()) {
          if (this.MdMenu.instance.$el && this.MdMenu.instance.$el.offsetHeight) {
            offsetY = -this.MdMenu.instance.$el.offsetHeight - 11
          }

          if (direction.includes('start')) {
            offsetX = -8
          } else if (direction.includes('end')) {
            offsetX = 8
          }
        }

        this.popperSettings = {
          placement: direction,
          modifiers: {
            keepTogether: {
              enabled: true
            },
            flip: {
              enabled: false
            },
            offset: {
              offset: `${offsetX}, ${offsetY}`
            }
          }
        }
      },
      setInitialHighlightIndex () {
        this.setHighlightItems()
        this.highlightItems.forEach((item, index) => {
          if (item.classList.contains('md-selected')) {
            this.highlightIndex = index - 1
          }
        })
      },
      setHighlightItems () {
        if (this.$el.querySelectorAll) {
          const items = this.$el.querySelectorAll('.md-list-item-container:not(.md-list-item-default):not([disabled])')

          this.highlightItems = Array.from(items)
        }
      },
      setHighlight (direction) {
        this.setHighlightItems()

        if (this.highlightItems.length) {
          if (direction === 'down') {
            if (this.highlightIndex === this.highlightItems.length - 1) {
              this.highlightIndex = 0
            } else {
              this.highlightIndex++
            }
          } else {
            if (this.highlightIndex === 0) {
              this.highlightIndex = this.highlightItems.length - 1
            } else {
              this.highlightIndex--
            }
          }

          this.clearAllHighlights()
          this.setItemHighlight()
        }
      },
      clearAllHighlights () {
        this.highlightItems.forEach(item => {
          item.parentNode.__vue__.highlighted = false
        })
      },
      setItemHighlight () {
        if (this.highlightedItem) {
          this.highlightedItem.parentNode.__vue__.highlighted = true
          if (this.$parent.$parent.setOffsets) {
            this.$parent.$parent.setOffsets(this.highlightedItem.parentNode)
          }
        }
      },
      setSelection () {
        if (this.highlightedItem) {
          this.highlightedItem.parentNode.click()
        }
      },
      onEsc () {
        this.MdMenu.active = false
        this.destroyKeyDownListener()
      },
      getOffsets () {
        const relativePosition = this.getBodyPosition()

        const offsetX = this.MdMenu.offsetX || 0
        const offsetY = this.MdMenu.offsetY || 0

        return {
          offsetX: offsetX - relativePosition.x,
          offsetY: offsetY - relativePosition.y
        }
      },
      hasCustomOffsets () {
        const { offsetX, offsetY, alignTrigger } = this.MdMenu

        return Boolean(alignTrigger || offsetY || offsetX)
      },
      isMenu ({ target }) {
        return this.MdMenu.$el ? MdContains(this.MdMenu.$el, target) : false
      },
      isMenuContentEl ({ target }) {
        return this.$refs.menu ? MdContains(this.$refs.menu, target) : false
      },
      createClickEventObserver () {
        if (document) {
          this.MdMenu.bodyClickObserver = new MdObserveEvent(document.body, 'click', $event => {
            $event.stopPropagation()
            if (!this.isMenu($event) && (this.MdMenu.closeOnClick || !this.isMenuContentEl($event))) {
              this.MdMenu.active = false
              this.MdMenu.bodyClickObserver.destroy()
              this.MdMenu.windowResizeObserver.destroy()
              this.destroyKeyDownListener()
            }
          })
        }
      },
      createKeydownListener () {
        window.addEventListener('keydown', this.keyNavigation)
      },
      destroyKeyDownListener () {
        window.removeEventListener('keydown', this.keyNavigation)
      },
      keyNavigation (event) {
        switch (event.key) {
        case 'ArrowUp':
          event.preventDefault()
          this.setHighlight('up')
          break

        case 'ArrowDown':
          event.preventDefault()
          this.setHighlight('down')
          break

        case 'Enter':
          this.setSelection()
          break

        case 'Space':
          this.setSelection()
          break

        case 'Escape':
          this.onEsc()
        }
      },
      createResizeObserver () {
        this.MdMenu.windowResizeObserver = new MdResizeObserver(window, this.setStyles)
      },
      setupWatchers () {
        this.$watch('MdMenu.direction', this.setPopperSettings)
        this.$watch('MdMenu.alignTrigger', this.setPopperSettings)
        this.$watch('MdMenu.offsetX', this.setPopperSettings)
        this.$watch('MdMenu.offsetY', this.setPopperSettings)
      },
      setStyles () {
        if (this.MdMenu.fullWidth) {
          this.menuStyles = `
            width: ${this.MdMenu.instance.$el.offsetWidth}px;
            max-width: ${this.MdMenu.instance.$el.offsetWidth}px
          `
        }
      },
      getBodyPosition () {
        const body = document.body
        const { top, left } = body.getBoundingClientRect()

        const scrollLeft = window.pageXOffset !== undefined ? window.pageXOffset : body.scrollLeft
        const scrollTop = window.pageYOffset !== undefined ? window.pageYOffset : body.scrollTop

        return { x: left + scrollLeft, y: top + scrollTop }
      }
    },
    mounted () {
      this.$nextTick().then(() => {
        this.setHighlightItems()
        this.setupWatchers()
        this.setStyles()
        this.didMount = true
      })
    },
    beforeDestroy () {
      if (this.MdMenu.bodyClickObserver) {
        this.MdMenu.bodyClickObserver.destroy()
      }

      if (this.MdMenu.windowResizeObserver) {
        this.MdMenu.windowResizeObserver.destroy()
      }
      this.destroyKeyDownListener()
    }
  })
</script>

<style lang="scss">
  @import "~components/MdAnimation/variables";
  @import "~components/MdElevation/mixins";
  @import "~components/MdLayout/mixins";

  $md-menu-base-width: 56px;

  .md-menu-content {
    @include md-elevation(8);
    min-width: $md-menu-base-width * 2;
    max-width: $md-menu-base-width * 5;
    max-height: 35vh;
    display: flex;
    flex-direction: row;
    position: absolute;
    z-index: 60;
    border-radius: 2px;
    transition: transform .2s $md-transition-stand-timing,
                opacity .3s $md-transition-stand-timing;
    will-change: opacity, transform, top, left !important;

    &.md-shallow {
      position: fixed !important;
      top: -9999em !important;
      left: -9999em !important;
      pointer-events: none;
    }

    &.md-menu-content-enter-active {
      opacity: 1;
      transform: translate3d(0, 0, 0);
    }

    &.md-menu-content-leave-active {
      transition: opacity .4s $md-transition-default-timing;
      opacity: 0;
    }

    &.md-menu-content-enter {
      &.md-menu-content-top-start {
        transform-origin: bottom left;
        transform: translate3d(0, 8px, 0) scaleY(.95);
      }

      &.md-menu-content-top-end {
        transform-origin: bottom right;
        transform: translate3d(0, 8px, 0) scaleY(.95);
      }

      &.md-menu-content-right-start {
        transform-origin: left top;
        transform: translate3d(0, -8px, 0) scaleY(.95);
      }

      &.md-menu-content-right-end {
        transform-origin: left bottom;
        transform: translate3d(0, 8px, 0) scaleY(.95);
      }

      &.md-menu-content-bottom-start {
        transform-origin: top left;
        transform: translate3d(0, -8px, 0) scaleY(.95);
      }

      &.md-menu-content-bottom-end {
        transform-origin: top right;
        transform: translate3d(0, -8px, 0) scaleY(.95);
      }

      &.md-menu-content-left-start {
        transform-origin: right top;
        transform: translate3d(0, -8px, 0) scaleY(.95);
      }

      &.md-menu-content-left-end {
        transform-origin: right bottom;
        transform: translate3d(0, 8px, 0) scaleY(.95);
      }

      .md-list {
        opacity: 0;
      }
    }

    &.md-menu-content-medium {
      min-width: $md-menu-base-width * 3;
    }

    &.md-menu-content-big {
      min-width: $md-menu-base-width * 4;
    }

    &.md-menu-content-huge {
      min-width: $md-menu-base-width * 5;
    }
  }

  .md-menu-content-container {
    flex: 1;
    overflow: auto;

    .md-list {
      transition: opacity .3s $md-transition-stand-timing;
      will-change: opacity;
      font-family: 'Roboto', sans-serif;
      text-transform: none;
      white-space: nowrap;

      .md-list-item-container {
        height: 100%;
      }

      @include md-layout-small {
        font-size: 14px;
      }
    }
  }
</style>