transition.js 7.3 KB
import { warn } from 'core/util/debug'
import { extend, once, noop } from 'shared/util'
import { activeInstance } from 'core/instance/lifecycle'
import { resolveTransition } from 'web/runtime/transition-util'

export default {
  create: enter,
  activate: enter,
  remove: leave
}

function enter (_, vnode) {
  const el = vnode.elm

  // call leave callback now
  if (el._leaveCb) {
    el._leaveCb.cancelled = true
    el._leaveCb()
  }

  const data = resolveTransition(vnode.data.transition)
  if (!data) {
    return
  }

  /* istanbul ignore if */
  if (el._enterCb) {
    return
  }

  const {
    enterClass,
    enterToClass,
    enterActiveClass,
    appearClass,
    appearToClass,
    appearActiveClass,
    beforeEnter,
    enter,
    afterEnter,
    enterCancelled,
    beforeAppear,
    appear,
    afterAppear,
    appearCancelled
  } = data

  let context = activeInstance
  let transitionNode = activeInstance.$vnode
  while (transitionNode && transitionNode.parent) {
    transitionNode = transitionNode.parent
    context = transitionNode.context
  }

  const isAppear = !context._isMounted || !vnode.isRootInsert

  if (isAppear && !appear && appear !== '') {
    return
  }

  const startClass = isAppear ? appearClass : enterClass
  const toClass = isAppear ? appearToClass : enterToClass
  const activeClass = isAppear ? appearActiveClass : enterActiveClass
  const beforeEnterHook = isAppear ? (beforeAppear || beforeEnter) : beforeEnter
  const enterHook = isAppear ? (typeof appear === 'function' ? appear : enter) : enter
  const afterEnterHook = isAppear ? (afterAppear || afterEnter) : afterEnter
  const enterCancelledHook = isAppear ? (appearCancelled || enterCancelled) : enterCancelled

  const userWantsControl =
    enterHook &&
    // enterHook may be a bound method which exposes
    // the length of original fn as _length
    (enterHook._length || enterHook.length) > 1

  const stylesheet = vnode.context.$options.style || {}
  const startState = stylesheet[startClass]
  const transitionProperties = (stylesheet['@TRANSITION'] && stylesheet['@TRANSITION'][activeClass]) || {}
  const endState = getEnterTargetState(el, stylesheet, startClass, toClass, activeClass)
  const needAnimation = Object.keys(endState).length > 0

  const cb = el._enterCb = once(() => {
    if (cb.cancelled) {
      enterCancelledHook && enterCancelledHook(el)
    } else {
      afterEnterHook && afterEnterHook(el)
    }
    el._enterCb = null
  })

  // We need to wait until the native element has been inserted, but currently
  // there's no API to do that. So we have to wait "one frame" - not entirely
  // sure if this is guaranteed to be enough (e.g. on slow devices?)
  setTimeout(() => {
    const parent = el.parentNode
    const pendingNode = parent && parent._pending && parent._pending[vnode.key]
    if (pendingNode &&
      pendingNode.context === vnode.context &&
      pendingNode.tag === vnode.tag &&
      pendingNode.elm._leaveCb
    ) {
      pendingNode.elm._leaveCb()
    }
    enterHook && enterHook(el, cb)

    if (needAnimation) {
      const animation = vnode.context.$requireWeexModule('animation')
      animation.transition(el.ref, {
        styles: endState,
        duration: transitionProperties.duration || 0,
        delay: transitionProperties.delay || 0,
        timingFunction: transitionProperties.timingFunction || 'linear'
      }, userWantsControl ? noop : cb)
    } else if (!userWantsControl) {
      cb()
    }
  }, 16)

  // start enter transition
  beforeEnterHook && beforeEnterHook(el)

  if (startState) {
    if (typeof el.setStyles === 'function') {
      el.setStyles(startState)
    } else {
      for (const key in startState) {
        el.setStyle(key, startState[key])
      }
    }
  }

  if (!needAnimation && !userWantsControl) {
    cb()
  }
}

function leave (vnode, rm) {
  const el = vnode.elm

  // call enter callback now
  if (el._enterCb) {
    el._enterCb.cancelled = true
    el._enterCb()
  }

  const data = resolveTransition(vnode.data.transition)
  if (!data) {
    return rm()
  }

  if (el._leaveCb) {
    return
  }

  const {
    leaveClass,
    leaveToClass,
    leaveActiveClass,
    beforeLeave,
    leave,
    afterLeave,
    leaveCancelled,
    delayLeave
  } = data

  const userWantsControl =
    leave &&
    // leave hook may be a bound method which exposes
    // the length of original fn as _length
    (leave._length || leave.length) > 1

  const stylesheet = vnode.context.$options.style || {}
  const startState = stylesheet[leaveClass]
  const endState = stylesheet[leaveToClass] || stylesheet[leaveActiveClass]
  const transitionProperties = (stylesheet['@TRANSITION'] && stylesheet['@TRANSITION'][leaveActiveClass]) || {}

  const cb = el._leaveCb = once(() => {
    if (el.parentNode && el.parentNode._pending) {
      el.parentNode._pending[vnode.key] = null
    }
    if (cb.cancelled) {
      leaveCancelled && leaveCancelled(el)
    } else {
      rm()
      afterLeave && afterLeave(el)
    }
    el._leaveCb = null
  })

  if (delayLeave) {
    delayLeave(performLeave)
  } else {
    performLeave()
  }

  function performLeave () {
    const animation = vnode.context.$requireWeexModule('animation')
    // the delayed leave may have already been cancelled
    if (cb.cancelled) {
      return
    }
    // record leaving element
    if (!vnode.data.show) {
      (el.parentNode._pending || (el.parentNode._pending = {}))[vnode.key] = vnode
    }
    beforeLeave && beforeLeave(el)

    if (startState) {
      animation.transition(el.ref, {
        styles: startState
      }, next)
    } else {
      next()
    }

    function next () {
      animation.transition(el.ref, {
        styles: endState,
        duration: transitionProperties.duration || 0,
        delay: transitionProperties.delay || 0,
        timingFunction: transitionProperties.timingFunction || 'linear'
      }, userWantsControl ? noop : cb)
    }

    leave && leave(el, cb)
    if (!endState && !userWantsControl) {
      cb()
    }
  }
}

// determine the target animation style for an entering transition.
function getEnterTargetState (el, stylesheet, startClass, endClass, activeClass) {
  const targetState = {}
  const startState = stylesheet[startClass]
  const endState = stylesheet[endClass]
  const activeState = stylesheet[activeClass]
  // 1. fallback to element's default styling
  if (startState) {
    for (const key in startState) {
      targetState[key] = el.style[key]
      if (
        process.env.NODE_ENV !== 'production' &&
        targetState[key] == null &&
        (!activeState || activeState[key] == null) &&
        (!endState || endState[key] == null)
      ) {
        warn(
          `transition property "${key}" is declared in enter starting class (.${startClass}), ` +
          `but not declared anywhere in enter ending class (.${endClass}), ` +
          `enter active cass (.${activeClass}) or the element's default styling. ` +
          `Note in Weex, CSS properties need explicit values to be transitionable.`
        )
      }
    }
  }
  // 2. if state is mixed in active state, extract them while excluding
  //    transition properties
  if (activeState) {
    for (const key in activeState) {
      if (key.indexOf('transition') !== 0) {
        targetState[key] = activeState[key]
      }
    }
  }
  // 3. explicit endState has highest priority
  if (endState) {
    extend(targetState, endState)
  }
  return targetState
}