vuematerial/vue-material

View on GitHub
src/components/MdPortal/MdPortal.js

Summary

Maintainability
A
35 mins
Test Coverage
import Vue from 'vue'
import raf from 'raf'

export default {
  name: 'MdPortal',
  abstract: true,
  props: {
    mdAttachToParent: Boolean,
    mdTarget: {
      type: null,
      validator (value) {
        if (HTMLElement && value && value instanceof HTMLElement) {
          return true
        }

        Vue.util.warn('The md-target-el prop is invalid. You should pass a valid HTMLElement.', this)

        return false
      }
    }
  },
  data: () => ({
    leaveTimeout: null,
    originalParentEl: null
  }),
  computed: {
    transitionName () {
      const childrenComponent = this._vnode.componentOptions.children[0]

      if (childrenComponent) {
        const transition = childrenComponent.data.transition

        if (transition) {
          return transition.name
        } else {
          const transition = childrenComponent.componentOptions.propsData.name

          if (transition) {
            return transition
          }
        }
      }

      return 'v'
    },
    leaveClass () {
      return this.transitionName + '-leave'
    },
    leaveActiveClass () {
      return this.transitionName + '-leave-active'
    },
    leaveToClass () {
      return this.transitionName + '-leave-to'
    }
  },
  watch: {
    mdTarget (newTarget, oldTarget) {
      this.changeParentEl(newTarget)

      if (oldTarget) {
        this.$forceUpdate()
      }
    }
  },
  methods: {
    getTransitionDuration (el) {
      const duration = window.getComputedStyle(el).transitionDuration
      const num = parseFloat(duration, 10)
      let unit = duration.match(/m?s/)

      if (unit) {
        unit = unit[0]
      }

      if (unit === 's') {
        return num * 1000
      }

      if (unit === 'ms') {
        return num
      }

      return 0
    },
    killGhostElement (el) {
      if (el.parentNode) {
        this.changeParentEl(this.originalParentEl)
        this.$options._parentElm = this.originalParentEl
        el.parentNode.removeChild(el)
      }
    },
    initDestroy (manualCall) {
      let el = this.$el

      if (manualCall && this.$el.nodeType === Node.COMMENT_NODE) {
        el = this.$vnode.elm
      }

      el.classList.add(this.leaveClass)
      el.classList.add(this.leaveActiveClass)

      this.$nextTick().then(() => {
        el.classList.add(this.leaveToClass)

        clearTimeout(this.leaveTimeout)
        this.leaveTimeout = setTimeout(() => {
          this.destroyElement(el)
        }, this.getTransitionDuration(el))
      })
    },
    destroyElement (el) {
      raf(() => {
        el.classList.remove(this.leaveClass)
        el.classList.remove(this.leaveActiveClass)
        el.classList.remove(this.leaveToClass)
        this.$emit('md-destroy')
        this.killGhostElement(el)
      })
    },
    changeParentEl (newTarget) {
      newTarget && newTarget.appendChild(this.$el)
    }
  },
  mounted () {
    if (!this.originalParentEl) {
      this.originalParentEl = this.$el.parentNode
      this.$emit('md-initial-parent', this.$el.parentNode)
    }

    if (this.mdAttachToParent && this.$el.parentNode.parentNode) {
      this.changeParentEl(this.$el.parentNode.parentNode)
    } else if (document) {
      this.changeParentEl(this.mdTarget || document.body)
    }
  },
  beforeDestroy () {
    if (this.$el.classList) {
      this.initDestroy()
    } else {
      this.killGhostElement(this.$el)
    }
  },
  render (createElement) {
    const defaultSlot = this.$slots.default

    if (defaultSlot && defaultSlot[0]) {
      return defaultSlot[0]
    }
  }
}