| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334 |
- import {
- BaseTransition,
- BaseTransitionProps,
- h,
- warn,
- FunctionalComponent,
- getCurrentInstance,
- callWithAsyncErrorHandling
- } from '@vue/runtime-core'
- import { isObject, toNumber } from '@vue/shared'
- import { ErrorCodes } from 'packages/runtime-core/src/errorHandling'
- const TRANSITION = 'transition'
- const ANIMATION = 'animation'
- export interface TransitionProps extends BaseTransitionProps<Element> {
- name?: string
- type?: typeof TRANSITION | typeof ANIMATION
- css?: boolean
- duration?: number | { enter: number; leave: number }
- // custom transition classes
- enterFromClass?: string
- enterActiveClass?: string
- enterToClass?: string
- appearFromClass?: string
- appearActiveClass?: string
- appearToClass?: string
- leaveFromClass?: string
- leaveActiveClass?: string
- leaveToClass?: string
- }
- // DOM Transition is a higher-order-component based on the platform-agnostic
- // base Transition component, with DOM-specific logic.
- export const Transition: FunctionalComponent<TransitionProps> = (
- props,
- { slots }
- ) => h(BaseTransition, resolveTransitionProps(props), slots)
- export const TransitionPropsValidators = (Transition.props = {
- ...(BaseTransition as any).props,
- name: String,
- type: String,
- css: {
- type: Boolean,
- default: true
- },
- duration: [String, Number, Object],
- enterFromClass: String,
- enterActiveClass: String,
- enterToClass: String,
- appearFromClass: String,
- appearActiveClass: String,
- appearToClass: String,
- leaveFromClass: String,
- leaveActiveClass: String,
- leaveToClass: String
- })
- export function resolveTransitionProps({
- name = 'v',
- type,
- css = true,
- duration,
- enterFromClass = `${name}-enter-from`,
- enterActiveClass = `${name}-enter-active`,
- enterToClass = `${name}-enter-to`,
- appearFromClass = enterFromClass,
- appearActiveClass = enterActiveClass,
- appearToClass = enterToClass,
- leaveFromClass = `${name}-leave-from`,
- leaveActiveClass = `${name}-leave-active`,
- leaveToClass = `${name}-leave-to`,
- ...baseProps
- }: TransitionProps): BaseTransitionProps<Element> {
- if (!css) {
- return baseProps
- }
- const originEnterClass = [enterFromClass, enterActiveClass, enterToClass]
- const instance = getCurrentInstance()!
- const durations = normalizeDuration(duration)
- const enterDuration = durations && durations[0]
- const leaveDuration = durations && durations[1]
- const { appear, onBeforeEnter, onEnter, onLeave } = baseProps
- // is appearing
- if (appear && !instance.isMounted) {
- enterFromClass = appearFromClass
- enterActiveClass = appearActiveClass
- enterToClass = appearToClass
- }
- type Hook = (el: Element, done?: () => void) => void
- const finishEnter: Hook = (el, done) => {
- removeTransitionClass(el, enterToClass)
- removeTransitionClass(el, enterActiveClass)
- done && done()
- // reset enter class
- if (appear) {
- ;[enterFromClass, enterActiveClass, enterToClass] = originEnterClass
- }
- }
- const finishLeave: Hook = (el, done) => {
- removeTransitionClass(el, leaveToClass)
- removeTransitionClass(el, leaveActiveClass)
- done && done()
- }
- // only needed for user hooks called in nextFrame
- // sync errors are already handled by BaseTransition
- function callHookWithErrorHandling(hook: Hook, args: any[]) {
- callWithAsyncErrorHandling(hook, instance, ErrorCodes.TRANSITION_HOOK, args)
- }
- return {
- ...baseProps,
- onBeforeEnter(el) {
- onBeforeEnter && onBeforeEnter(el)
- addTransitionClass(el, enterActiveClass)
- addTransitionClass(el, enterFromClass)
- },
- onEnter(el, done) {
- nextFrame(() => {
- const resolve = () => finishEnter(el, done)
- onEnter && callHookWithErrorHandling(onEnter as Hook, [el, resolve])
- removeTransitionClass(el, enterFromClass)
- addTransitionClass(el, enterToClass)
- if (!(onEnter && onEnter.length > 1)) {
- if (enterDuration) {
- setTimeout(resolve, enterDuration)
- } else {
- whenTransitionEnds(el, type, resolve)
- }
- }
- })
- },
- onLeave(el, done) {
- addTransitionClass(el, leaveActiveClass)
- addTransitionClass(el, leaveFromClass)
- nextFrame(() => {
- const resolve = () => finishLeave(el, done)
- onLeave && callHookWithErrorHandling(onLeave as Hook, [el, resolve])
- removeTransitionClass(el, leaveFromClass)
- addTransitionClass(el, leaveToClass)
- if (!(onLeave && onLeave.length > 1)) {
- if (leaveDuration) {
- setTimeout(resolve, leaveDuration)
- } else {
- whenTransitionEnds(el, type, resolve)
- }
- }
- })
- },
- onEnterCancelled: finishEnter,
- onLeaveCancelled: finishLeave
- }
- }
- function normalizeDuration(
- duration: TransitionProps['duration']
- ): [number, number] | null {
- if (duration == null) {
- return null
- } else if (isObject(duration)) {
- return [NumberOf(duration.enter), NumberOf(duration.leave)]
- } else {
- const n = NumberOf(duration)
- return [n, n]
- }
- }
- function NumberOf(val: unknown): number {
- const res = toNumber(val)
- 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.'
- )
- }
- }
- 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
- // during the transition.
- _vtc?: Set<string>
- }
- export function addTransitionClass(el: Element, cls: string) {
- cls.split(/\s+/).forEach(c => c && el.classList.add(c))
- ;(
- (el as ElementWithTransition)._vtc ||
- ((el as ElementWithTransition)._vtc = new Set())
- ).add(cls)
- }
- export function removeTransitionClass(el: Element, cls: string) {
- cls.split(/\s+/).forEach(c => c && el.classList.remove(c))
- const { _vtc } = el as ElementWithTransition
- if (_vtc) {
- _vtc.delete(cls)
- if (!_vtc!.size) {
- ;(el as ElementWithTransition)._vtc = undefined
- }
- }
- }
- function nextFrame(cb: () => void) {
- requestAnimationFrame(() => {
- requestAnimationFrame(cb)
- })
- }
- function whenTransitionEnds(
- el: Element,
- expectedType: TransitionProps['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
- hasTransform: boolean
- }
- export function getTransitionInfo(
- el: Element,
- expectedType?: TransitionProps['type']
- ): CSSTransitionInfo {
- const styles: any = window.getComputedStyle(el)
- // JSDOM may return undefined for transition properties
- const getStyleProperties = (key: string) => (styles[key] || '').split(', ')
- const transitionDelays = getStyleProperties(TRANSITION + 'Delay')
- const transitionDurations = getStyleProperties(TRANSITION + 'Duration')
- const transitionTimeout = getTimeout(transitionDelays, transitionDurations)
- const animationDelays = getStyleProperties(ANIMATION + 'Delay')
- const animationDurations = getStyleProperties(ANIMATION + 'Duration')
- const animationTimeout = 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
- }
- const hasTransform =
- type === TRANSITION &&
- /\b(transform|all)(,|$)/.test(styles[TRANSITION + 'Property'])
- return {
- type,
- timeout,
- propCount,
- hasTransform
- }
- }
- function getTimeout(delays: string[], durations: string[]): number {
- while (delays.length < durations.length) {
- delays = delays.concat(delays)
- }
- return Math.max(...durations.map((d, i) => 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
- }
|