diff --git a/packages/runtime-core/src/components/Transition.ts b/packages/runtime-core/src/components/Transition.ts index 2dda75ae..501ffc3c 100644 --- a/packages/runtime-core/src/components/Transition.ts +++ b/packages/runtime-core/src/components/Transition.ts @@ -31,7 +31,7 @@ export interface TransitionProps { } const TransitionImpl = { - name: `Transition`, + name: `BaseTransition`, setup(props: TransitionProps, { slots }: SetupContext) { const instance = getCurrentInstance()! let isLeaving = false diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 4c241a57..bb7c4688 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -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 index 00000000..ae4107b5 --- /dev/null +++ b/packages/runtime-dom/src/components/CSSTransition.ts @@ -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( + ` explicit duration is not a valid number - ` + + `got ${JSON.stringify(val)}.` + ) + } else if (isNaN(val)) { + warn( + ` 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 = ( + styles[TRANSITION + 'Delay'] || '' + ).split(', ') + const transitionDurations: Array = ( + styles[TRANSITION + 'Duration'] || '' + ).split(', ') + const transitionTimeout: number = getTimeout( + transitionDelays, + transitionDurations + ) + const animationDelays: Array = ( + styles[ANIMATION + 'Delay'] || '' + ).split(', ') + const animationDurations: Array = ( + 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 index 70b786d1..00000000 --- a/packages/runtime-dom/src/components/Transition.ts +++ /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 index 70b786d1..00000000 --- a/packages/runtime-dom/src/components/TransitionGroup.ts +++ /dev/null @@ -1 +0,0 @@ -// TODO diff --git a/packages/runtime-dom/src/index.ts b/packages/runtime-dom/src/index.ts index 865553a2..f093ac50 100644 --- a/packages/runtime-dom/src/index.ts +++ b/packages/runtime-dom/src/index.ts @@ -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'