]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(transition): CSS transition for runtime-dom
authorEvan You <yyx990803@gmail.com>
Fri, 22 Nov 2019 18:15:59 +0000 (13:15 -0500)
committerEvan You <yyx990803@gmail.com>
Fri, 22 Nov 2019 20:35:41 +0000 (15:35 -0500)
packages/runtime-core/src/components/Transition.ts
packages/runtime-core/src/index.ts
packages/runtime-dom/src/components/CSSTransition.ts [new file with mode: 0644]
packages/runtime-dom/src/components/Transition.ts [deleted file]
packages/runtime-dom/src/components/TransitionGroup.ts [deleted file]
packages/runtime-dom/src/index.ts

index 2dda75ae9ee1391f42eca43adbac51be6e78821a..501ffc3c33b8b2032a99a733da379c113e00c134 100644 (file)
@@ -31,7 +31,7 @@ export interface TransitionProps {
 }
 
 const TransitionImpl = {
-  name: `Transition`,
+  name: `BaseTransition`,
   setup(props: TransitionProps, { slots }: SetupContext) {
     const instance = getCurrentInstance()!
     let isLeaving = false
index 4c241a57a787777b1f2342dd90f6358ee4170c86..bb7c46885d38bec4b8371d7ba7aa924d610877ca 100644 (file)
@@ -88,7 +88,8 @@ export {
   Component,
   FunctionalComponent,
   ComponentInternalInstance,
-  RenderFunction
+  RenderFunction,
+  SetupContext
 } from './component'
 export {
   ComponentOptions,
diff --git a/packages/runtime-dom/src/components/CSSTransition.ts b/packages/runtime-dom/src/components/CSSTransition.ts
new file mode 100644 (file)
index 0000000..ae4107b
--- /dev/null
@@ -0,0 +1,268 @@
+import {
+  Transition as BaseTransition,
+  TransitionProps,
+  h,
+  SetupContext,
+  warn
+} from '@vue/runtime-core'
+import { isObject } from '@vue/shared'
+
+const TRANSITION = 'transition'
+const ANIMATION = 'animation'
+
+export interface CSSTransitionProps extends TransitionProps {
+  name?: string
+  type?: typeof TRANSITION | typeof ANIMATION
+  duration?: number | { enter: number; leave: number }
+
+  enterFromClass?: string
+  enterActiveClass?: string
+  enterToClass?: string
+  leaveFromClass?: string
+  leaveActiveClass?: string
+  leaveToClass?: string
+}
+
+export const CSSTransition = (
+  props: CSSTransitionProps,
+  { slots }: SetupContext
+) => h(BaseTransition, resolveCSSTransitionData(props), slots)
+
+if (__DEV__) {
+  CSSTransition.props = {
+    ...(BaseTransition as any).props,
+    name: String,
+    type: String,
+    enterClass: String,
+    enterActiveClass: String,
+    enterToClass: String,
+    leaveClass: String,
+    leaveActiveClass: String,
+    leaveToClass: String,
+    duration: Object
+  }
+}
+
+function resolveCSSTransitionData({
+  name = 'v',
+  type,
+  duration,
+  enterFromClass = `${name}-enter-from`,
+  enterActiveClass = `${name}-enter-active`,
+  enterToClass = `${name}-enter-to`,
+  leaveFromClass = `${name}-leave-from`,
+  leaveActiveClass = `${name}-leave-active`,
+  leaveToClass = `${name}-leave-to`,
+  ...baseProps
+}: CSSTransitionProps): TransitionProps {
+  const durations = normalizeDuration(duration)
+  const enterDuration = durations && durations[0]
+  const leaveDuration = durations && durations[1]
+  const { onBeforeEnter, onEnter, onLeave } = baseProps
+
+  return {
+    ...baseProps,
+    onBeforeEnter(el) {
+      onBeforeEnter && onBeforeEnter(el)
+      el.classList.add(enterActiveClass)
+      el.classList.add(enterFromClass)
+    },
+    onEnter(el, done) {
+      nextFrame(() => {
+        const resolve = () => {
+          el.classList.remove(enterToClass)
+          el.classList.remove(enterActiveClass)
+          done()
+        }
+        onEnter && onEnter(el, resolve)
+        el.classList.remove(enterFromClass)
+        el.classList.add(enterToClass)
+        if (!(onEnter && onEnter.length > 1)) {
+          if (enterDuration) {
+            setTimeout(resolve, enterDuration)
+          } else {
+            whenTransitionEnds(el, type, resolve)
+          }
+        }
+      })
+    },
+    onLeave(el, done) {
+      el.classList.add(leaveActiveClass)
+      el.classList.add(leaveFromClass)
+      nextFrame(() => {
+        const resolve = () => {
+          el.classList.remove(leaveToClass)
+          el.classList.remove(leaveActiveClass)
+          done()
+        }
+        onLeave && onLeave(el, resolve)
+        el.classList.remove(leaveFromClass)
+        el.classList.add(leaveToClass)
+        if (!(onLeave && onLeave.length > 1)) {
+          if (leaveDuration) {
+            setTimeout(resolve, leaveDuration)
+          } else {
+            whenTransitionEnds(el, type, resolve)
+          }
+        }
+      })
+    }
+  }
+}
+
+function normalizeDuration(
+  duration: CSSTransitionProps['duration']
+): [number, number] | null {
+  if (duration == null) {
+    return null
+  } else if (isObject(duration)) {
+    return [toNumber(duration.enter), toNumber(duration.leave)]
+  } else {
+    const n = toNumber(duration)
+    return [n, n]
+  }
+}
+
+function toNumber(val: unknown): number {
+  const res = Number(val || 0)
+  if (__DEV__) validateDuration(res)
+  return res
+}
+
+function validateDuration(val: unknown) {
+  if (typeof val !== 'number') {
+    warn(
+      `<transition> explicit duration is not a valid number - ` +
+        `got ${JSON.stringify(val)}.`
+    )
+  } else if (isNaN(val)) {
+    warn(
+      `<transition> explicit duration is NaN - ` +
+        'the duration expression might be incorrect.'
+    )
+  }
+}
+
+function nextFrame(cb: () => void) {
+  requestAnimationFrame(() => {
+    requestAnimationFrame(cb)
+  })
+}
+
+function whenTransitionEnds(
+  el: Element,
+  expectedType: CSSTransitionProps['type'] | undefined,
+  cb: () => void
+) {
+  const { type, timeout, propCount } = getTransitionInfo(el, expectedType)
+  if (!type) return cb()
+  const endEvent = type + 'end'
+  let ended = 0
+  const end = () => {
+    el.removeEventListener(endEvent, onEnd)
+    cb()
+  }
+  const onEnd = (e: Event) => {
+    if (e.target === el) {
+      if (++ended >= propCount) {
+        end()
+      }
+    }
+  }
+  setTimeout(() => {
+    if (ended < propCount) {
+      end()
+    }
+  }, timeout + 1)
+  el.addEventListener(endEvent, onEnd)
+}
+
+interface CSSTransitionInfo {
+  type: typeof TRANSITION | typeof ANIMATION | null
+  propCount: number
+  timeout: number
+}
+
+function getTransitionInfo(
+  el: Element,
+  expectedType?: CSSTransitionProps['type']
+): CSSTransitionInfo {
+  const styles: any = window.getComputedStyle(el)
+  // JSDOM may return undefined for transition properties
+  const transitionDelays: Array<string> = (
+    styles[TRANSITION + 'Delay'] || ''
+  ).split(', ')
+  const transitionDurations: Array<string> = (
+    styles[TRANSITION + 'Duration'] || ''
+  ).split(', ')
+  const transitionTimeout: number = getTimeout(
+    transitionDelays,
+    transitionDurations
+  )
+  const animationDelays: Array<string> = (
+    styles[ANIMATION + 'Delay'] || ''
+  ).split(', ')
+  const animationDurations: Array<string> = (
+    styles[ANIMATION + 'Duration'] || ''
+  ).split(', ')
+  const animationTimeout: number = getTimeout(
+    animationDelays,
+    animationDurations
+  )
+
+  let type: CSSTransitionInfo['type'] = null
+  let timeout = 0
+  let propCount = 0
+  /* istanbul ignore if */
+  if (expectedType === TRANSITION) {
+    if (transitionTimeout > 0) {
+      type = TRANSITION
+      timeout = transitionTimeout
+      propCount = transitionDurations.length
+    }
+  } else if (expectedType === ANIMATION) {
+    if (animationTimeout > 0) {
+      type = ANIMATION
+      timeout = animationTimeout
+      propCount = animationDurations.length
+    }
+  } else {
+    timeout = Math.max(transitionTimeout, animationTimeout)
+    type =
+      timeout > 0
+        ? transitionTimeout > animationTimeout
+          ? TRANSITION
+          : ANIMATION
+        : null
+    propCount = type
+      ? type === TRANSITION
+        ? transitionDurations.length
+        : animationDurations.length
+      : 0
+  }
+  return {
+    type,
+    timeout,
+    propCount
+  }
+}
+
+function getTimeout(delays: string[], durations: string[]): number {
+  while (delays.length < durations.length) {
+    delays = delays.concat(delays)
+  }
+  return Math.max.apply(
+    null,
+    durations.map((d, i) => {
+      return toMs(d) + toMs(delays[i])
+    })
+  )
+}
+
+// Old versions of Chromium (below 61.0.3163.100) formats floating pointer
+// numbers in a locale-dependent way, using a comma instead of a dot.
+// If comma is not replaced with a dot, the input will be rounded down
+// (i.e. acting as a floor function) causing unexpected behaviors
+function toMs(s: string): number {
+  return Number(s.slice(0, -1).replace(',', '.')) * 1000
+}
diff --git a/packages/runtime-dom/src/components/Transition.ts b/packages/runtime-dom/src/components/Transition.ts
deleted file mode 100644 (file)
index 70b786d..0000000
+++ /dev/null
@@ -1 +0,0 @@
-// TODO
diff --git a/packages/runtime-dom/src/components/TransitionGroup.ts b/packages/runtime-dom/src/components/TransitionGroup.ts
deleted file mode 100644 (file)
index 70b786d..0000000
+++ /dev/null
@@ -1 +0,0 @@
-// TODO
index 865553a28cd3dabd820e988f96581e1fcd4cfcf5..f093ac505972da2b4cc24661af20e925671a6146 100644 (file)
@@ -63,9 +63,11 @@ export {
   vModelSelect,
   vModelDynamic
 } from './directives/vModel'
-
 export { withModifiers, withKeys } from './directives/vOn'
 
+// DOM-only components
+export { CSSTransition } from './components/CSSTransition'
+
 // re-export everything from core
 // h, Component, reactivity API, nextTick, flags & types
 export * from '@vue/runtime-core'