]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(transition): TransitionGroup
authorEvan You <yyx990803@gmail.com>
Thu, 28 Nov 2019 23:41:01 +0000 (18:41 -0500)
committerEvan You <yyx990803@gmail.com>
Thu, 28 Nov 2019 23:41:01 +0000 (18:41 -0500)
packages/runtime-core/src/components/BaseTransition.ts
packages/runtime-core/src/index.ts
packages/runtime-dom/src/components/Transition.ts
packages/runtime-dom/src/components/TransitionGroup.ts [new file with mode: 0644]
packages/runtime-dom/src/index.ts

index 0513cdb1a144084d5e4986387f9e9d67f4025b07..7fa736aea5540bd1beaf0b1c7826a7267ba00c85 100644 (file)
@@ -1,7 +1,8 @@
 import {
   getCurrentInstance,
   SetupContext,
-  ComponentOptions
+  ComponentOptions,
+  ComponentInternalInstance
 } from '../component'
 import {
   cloneVNode,
@@ -61,9 +62,9 @@ type TransitionHookCaller = (
   args?: any[]
 ) => void
 
-type PendingCallback = (cancelled?: boolean) => void
+export type PendingCallback = (cancelled?: boolean) => void
 
-interface TransitionState {
+export interface TransitionState {
   isMounted: boolean
   isLeaving: boolean
   isUnmounting: boolean
@@ -72,7 +73,7 @@ interface TransitionState {
   leavingVNodes: Map<any, Record<string, VNode>>
 }
 
-interface TransitionElement {
+export interface TransitionElement {
   // in persisted mode (e.g. v-show), the same element is toggled, so the
   // pending enter/leave callbacks may need to cancalled if the state is toggled
   // before it finishes.
@@ -80,32 +81,27 @@ interface TransitionElement {
   _leaveCb?: PendingCallback
 }
 
+export function useTransitionState(): TransitionState {
+  const state: TransitionState = {
+    isMounted: false,
+    isLeaving: false,
+    isUnmounting: false,
+    leavingVNodes: new Map()
+  }
+  onMounted(() => {
+    state.isMounted = true
+  })
+  onBeforeUnmount(() => {
+    state.isUnmounting = true
+  })
+  return state
+}
+
 const BaseTransitionImpl = {
   name: `BaseTransition`,
   setup(props: BaseTransitionProps, { slots }: SetupContext) {
     const instance = getCurrentInstance()!
-    const state: TransitionState = {
-      isMounted: false,
-      isLeaving: false,
-      isUnmounting: false,
-      leavingVNodes: new Map()
-    }
-    onMounted(() => {
-      state.isMounted = true
-    })
-    onBeforeUnmount(() => {
-      state.isUnmounting = true
-    })
-
-    const callTransitionHook: TransitionHookCaller = (hook, args) => {
-      hook &&
-        callWithAsyncErrorHandling(
-          hook,
-          instance,
-          ErrorCodes.TRANSITION_HOOK,
-          args
-        )
-    }
+    const state = useTransitionState()
 
     return () => {
       const children = slots.default && slots.default()
@@ -147,7 +143,7 @@ const BaseTransitionImpl = {
         innerChild,
         rawProps,
         state,
-        callTransitionHook
+        instance
       ))
 
       const oldChild = instance.subTree
@@ -163,7 +159,7 @@ const BaseTransitionImpl = {
           oldInnerChild,
           rawProps,
           state,
-          callTransitionHook
+          instance
         )
         // update old tree's hooks in case of dynamic transition
         setTransitionHooks(oldInnerChild, leavingHooks)
@@ -245,7 +241,7 @@ function getLeavingNodesForType(
 
 // The transition hooks are attached to the vnode as vnode.transition
 // and will be called at appropriate timing in the renderer.
-function resolveTransitionHooks(
+export function resolveTransitionHooks(
   vnode: VNode,
   {
     appear,
@@ -260,11 +256,21 @@ function resolveTransitionHooks(
     onLeaveCancelled
   }: BaseTransitionProps,
   state: TransitionState,
-  callHook: TransitionHookCaller
+  instance: ComponentInternalInstance
 ): TransitionHooks {
   const key = String(vnode.key)
   const leavingVNodesCache = getLeavingNodesForType(state, vnode)
 
+  const callHook: TransitionHookCaller = (hook, args) => {
+    hook &&
+      callWithAsyncErrorHandling(
+        hook,
+        instance,
+        ErrorCodes.TRANSITION_HOOK,
+        args
+      )
+  }
+
   const hooks: TransitionHooks = {
     persisted,
     beforeEnter(el: TransitionElement) {
index 88831de37db07856d91cfc4c58b24b72f48bf6b3..019b081e89ecf683d831f7e53d802584a92f58e9 100644 (file)
@@ -58,6 +58,13 @@ export {
   callWithErrorHandling,
   callWithAsyncErrorHandling
 } from './errorHandling'
+export {
+  useTransitionState,
+  TransitionState,
+  resolveTransitionHooks,
+  setTransitionHooks,
+  TransitionHooks
+} from './components/BaseTransition'
 
 // Internal, for compiler generated code
 // should sync with '@vue/compiler-core/src/runtimeConstants.ts'
index 18f15ddcedb555b51e2a8836f134f4a11b9c6210..825802f061b7092860e3a85a9b8c7580d1b9371b 100644 (file)
@@ -58,7 +58,7 @@ if (__DEV__) {
   }
 }
 
-function resolveTransitionProps({
+export function resolveTransitionProps({
   name = 'v',
   type,
   css = true,
@@ -91,7 +91,7 @@ function resolveTransitionProps({
     enterToClass = appearToClass
   }
 
-  type Hook = (el: Element, done?: () => void) => void
+  type Hook = (el: HTMLElement, done?: () => void) => void
 
   const finishEnter: Hook = (el, done) => {
     removeTransitionClass(el, enterToClass)
@@ -188,7 +188,7 @@ function validateDuration(val: unknown) {
   }
 }
 
-export interface ElementWithTransition extends Element {
+export interface ElementWithTransition extends HTMLElement {
   // _vtc = Vue Transition Classes.
   // Store the temporarily-added transition classes on the element
   // so that we can avoid overwriting them if the element's class is patched
@@ -196,12 +196,12 @@ export interface ElementWithTransition extends Element {
   _vtc?: Set<string>
 }
 
-function addTransitionClass(el: ElementWithTransition, cls: string) {
+export function addTransitionClass(el: ElementWithTransition, cls: string) {
   el.classList.add(cls)
   ;(el._vtc || (el._vtc = new Set())).add(cls)
 }
 
-function removeTransitionClass(el: ElementWithTransition, cls: string) {
+export function removeTransitionClass(el: ElementWithTransition, cls: string) {
   el.classList.remove(cls)
   if (el._vtc) {
     el._vtc.delete(cls)
@@ -252,9 +252,10 @@ interface CSSTransitionInfo {
   type: typeof TRANSITION | typeof ANIMATION | null
   propCount: number
   timeout: number
+  hasTransform: boolean
 }
 
-function getTransitionInfo(
+export function getTransitionInfo(
   el: Element,
   expectedType?: TransitionProps['type']
 ): CSSTransitionInfo {
@@ -298,10 +299,14 @@ function getTransitionInfo(
         : animationDurations.length
       : 0
   }
+  const hasTransform =
+    type === TRANSITION &&
+    /\b(transform|all)(,|$)/.test(styles[TRANSITION + 'Property'])
   return {
     type,
     timeout,
-    propCount
+    propCount,
+    hasTransform
   }
 }
 
diff --git a/packages/runtime-dom/src/components/TransitionGroup.ts b/packages/runtime-dom/src/components/TransitionGroup.ts
new file mode 100644 (file)
index 0000000..cc5d8f2
--- /dev/null
@@ -0,0 +1,180 @@
+import {
+  TransitionProps,
+  addTransitionClass,
+  removeTransitionClass,
+  ElementWithTransition,
+  getTransitionInfo,
+  resolveTransitionProps
+} from './Transition'
+import {
+  Fragment,
+  VNode,
+  Slots,
+  warn,
+  resolveTransitionHooks,
+  toRaw,
+  useTransitionState,
+  getCurrentInstance,
+  setTransitionHooks,
+  createVNode,
+  onUpdated
+} from '@vue/runtime-core'
+
+interface Position {
+  top: number
+  left: number
+}
+
+const positionMap = new WeakMap<VNode, Position>()
+const newPositionMap = new WeakMap<VNode, Position>()
+
+export type TransitionGroupProps = Omit<TransitionProps, 'mode'> & {
+  tag?: string
+  moveClass?: string
+}
+
+export const TransitionGroup = {
+  setup(props: TransitionGroupProps, { slots }: { slots: Slots }) {
+    const instance = getCurrentInstance()!
+    const state = useTransitionState()
+    let prevChildren: VNode[]
+    let children: VNode[]
+    let hasMove: boolean | null = null
+
+    onUpdated(() => {
+      // children is guaranteed to exist after initial render
+      if (!prevChildren.length) {
+        return
+      }
+      const moveClass = props.moveClass || `${props.name || 'v'}-move`
+      // Check if move transition is needed. This check is cached per-instance.
+      hasMove =
+        hasMove === null
+          ? (hasMove = hasCSSTransform(
+              prevChildren[0].el,
+              instance.vnode.el,
+              moveClass
+            ))
+          : hasMove
+      if (!hasMove) {
+        return
+      }
+
+      // we divide the work into three loops to avoid mixing DOM reads and writes
+      // in each iteration - which helps prevent layout thrashing.
+      prevChildren.forEach(callPendingCbs)
+      prevChildren.forEach(recordPosition)
+      const movedChildren = prevChildren.filter(applyTranslation)
+
+      // force reflow to put everything in position
+      forceReflow()
+
+      movedChildren.forEach(c => {
+        const el = c.el
+        const style = el.style
+        addTransitionClass(el, moveClass)
+        style.transform = style.WebkitTransform = style.transitionDuration = ''
+        const cb = (el._moveCb = (e: TransitionEvent) => {
+          if (e && e.target !== el) {
+            return
+          }
+          if (!e || /transform$/.test(e.propertyName)) {
+            el.removeEventListener('transitionend', cb)
+            el._moveCb = null
+            removeTransitionClass(el, moveClass)
+          }
+        })
+        el.addEventListener('transitionend', cb)
+      })
+    })
+
+    return () => {
+      const rawProps = toRaw(props)
+      const cssTransitionProps = resolveTransitionProps(rawProps)
+      const tag = rawProps.tag || Fragment
+      prevChildren = children
+      children = slots.default ? slots.default() : []
+
+      for (let i = 0; i < children.length; i++) {
+        const child = children[i]
+        if (child.key != null) {
+          setTransitionHooks(
+            child,
+            resolveTransitionHooks(child, cssTransitionProps, state, instance)
+          )
+        } else if (__DEV__) {
+          warn(`<TransitionGroup> children must be keyed.`)
+        }
+      }
+
+      if (prevChildren) {
+        for (let i = 0; i < prevChildren.length; i++) {
+          const child = prevChildren[i]
+          setTransitionHooks(
+            child,
+            resolveTransitionHooks(child, cssTransitionProps, state, instance)
+          )
+          positionMap.set(child, child.el.getBoundingClientRect())
+        }
+      }
+
+      return createVNode(tag, null, children)
+    }
+  }
+}
+
+function callPendingCbs(c: VNode) {
+  if (c.el._moveCb) {
+    c.el._moveCb()
+  }
+  if (c.el._enterCb) {
+    c.el._enterCb()
+  }
+}
+
+function recordPosition(c: VNode) {
+  newPositionMap.set(c, c.el.getBoundingClientRect())
+}
+
+function applyTranslation(c: VNode): VNode | undefined {
+  const oldPos = positionMap.get(c)!
+  const newPos = newPositionMap.get(c)!
+  const dx = oldPos.left - newPos.left
+  const dy = oldPos.top - newPos.top
+  if (dx || dy) {
+    const s = c.el.style
+    s.transform = s.WebkitTransform = `translate(${dx}px,${dy}px)`
+    s.transitionDuration = '0s'
+    return c
+  }
+}
+
+// this is put in a dedicated function to avoid the line from being treeshaken
+function forceReflow() {
+  return document.body.offsetHeight
+}
+
+function hasCSSTransform(
+  el: ElementWithTransition,
+  root: Node,
+  moveClass: string
+): boolean {
+  // Detect whether an element with the move class applied has
+  // CSS transitions. Since the element may be inside an entering
+  // transition at this very moment, we make a clone of it and remove
+  // all other transition classes applied to ensure only the move class
+  // is applied.
+  const clone = el.cloneNode() as HTMLElement
+  if (el._vtc) {
+    el._vtc.forEach(cls => clone.classList.remove(cls))
+  }
+  clone.classList.add(moveClass)
+  clone.style.display = 'none'
+  const container = (root.nodeType === 1
+    ? root
+    : root.parentNode) as HTMLElement
+  container.appendChild(clone)
+  const { hasTransform } = getTransitionInfo(clone)
+  container.removeChild(clone)
+  return hasTransform
+}
index 7a0ea80d5ae994c734577668afab4c7a48db0043..62885cb2fb09a62a6f9a514b55f3d30e0c70d88b 100644 (file)
@@ -55,7 +55,7 @@ export const createApp = (): App<Element> => {
   return app
 }
 
-// DOM-only runtime helpers
+// DOM-only runtime directive helpers
 export {
   vModelText,
   vModelCheckbox,
@@ -64,11 +64,14 @@ export {
   vModelDynamic
 } from './directives/vModel'
 export { withModifiers, withKeys } from './directives/vOn'
+export { vShow } from './directives/vShow'
 
 // DOM-only components
 export { Transition, TransitionProps } from './components/Transition'
-
-export { vShow } from './directives/vShow'
+export {
+  TransitionGroup,
+  TransitionGroupProps
+} from './components/TransitionGroup'
 
 // re-export everything from core
 // h, Component, reactivity API, nextTick, flags & types