| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458 |
- import {
- BaseTransition,
- BaseTransitionProps,
- BaseTransitionPropsValidators,
- h,
- assertNumber,
- FunctionalComponent,
- compatUtils,
- DeprecationTypes
- } from '@vue/runtime-core'
- import { isObject, toNumber, extend, isArray } from '@vue/shared'
- const TRANSITION = 'transition'
- const ANIMATION = 'animation'
- type AnimationTypes = typeof TRANSITION | typeof ANIMATION
- export interface TransitionProps extends BaseTransitionProps<Element> {
- name?: string
- type?: AnimationTypes
- 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
- }
- export const vtcKey = Symbol('_vtc')
- 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.
- [vtcKey]?: Set<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)
- Transition.displayName = 'Transition'
- if (__COMPAT__) {
- Transition.__isBuiltIn = true
- }
- const DOMTransitionPropsValidators = {
- 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 const TransitionPropsValidators = (Transition.props =
- /*#__PURE__*/ extend(
- {},
- BaseTransitionPropsValidators as any,
- DOMTransitionPropsValidators
- ))
- /**
- * #3227 Incoming hooks may be merged into arrays when wrapping Transition
- * with custom HOCs.
- */
- const callHook = (
- hook: Function | Function[] | undefined,
- args: any[] = []
- ) => {
- if (isArray(hook)) {
- hook.forEach(h => h(...args))
- } else if (hook) {
- hook(...args)
- }
- }
- /**
- * Check if a hook expects a callback (2nd arg), which means the user
- * intends to explicitly control the end of the transition.
- */
- const hasExplicitCallback = (
- hook: Function | Function[] | undefined
- ): boolean => {
- return hook
- ? isArray(hook)
- ? hook.some(h => h.length > 1)
- : hook.length > 1
- : false
- }
- export function resolveTransitionProps(
- rawProps: TransitionProps
- ): BaseTransitionProps<Element> {
- const baseProps: BaseTransitionProps<Element> = {}
- for (const key in rawProps) {
- if (!(key in DOMTransitionPropsValidators)) {
- ;(baseProps as any)[key] = (rawProps as any)[key]
- }
- }
- if (rawProps.css === false) {
- return baseProps
- }
- const {
- name = 'v',
- type,
- 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`
- } = rawProps
- // legacy transition class compat
- const legacyClassEnabled =
- __COMPAT__ &&
- compatUtils.isCompatEnabled(DeprecationTypes.TRANSITION_CLASSES, null)
- let legacyEnterFromClass: string
- let legacyAppearFromClass: string
- let legacyLeaveFromClass: string
- if (__COMPAT__ && legacyClassEnabled) {
- const toLegacyClass = (cls: string) => cls.replace(/-from$/, '')
- if (!rawProps.enterFromClass) {
- legacyEnterFromClass = toLegacyClass(enterFromClass)
- }
- if (!rawProps.appearFromClass) {
- legacyAppearFromClass = toLegacyClass(appearFromClass)
- }
- if (!rawProps.leaveFromClass) {
- legacyLeaveFromClass = toLegacyClass(leaveFromClass)
- }
- }
- const durations = normalizeDuration(duration)
- const enterDuration = durations && durations[0]
- const leaveDuration = durations && durations[1]
- const {
- onBeforeEnter,
- onEnter,
- onEnterCancelled,
- onLeave,
- onLeaveCancelled,
- onBeforeAppear = onBeforeEnter,
- onAppear = onEnter,
- onAppearCancelled = onEnterCancelled
- } = baseProps
- const finishEnter = (el: Element, isAppear: boolean, done?: () => void) => {
- removeTransitionClass(el, isAppear ? appearToClass : enterToClass)
- removeTransitionClass(el, isAppear ? appearActiveClass : enterActiveClass)
- done && done()
- }
- const finishLeave = (
- el: Element & { _isLeaving?: boolean },
- done?: () => void
- ) => {
- el._isLeaving = false
- removeTransitionClass(el, leaveFromClass)
- removeTransitionClass(el, leaveToClass)
- removeTransitionClass(el, leaveActiveClass)
- done && done()
- }
- const makeEnterHook = (isAppear: boolean) => {
- return (el: Element, done: () => void) => {
- const hook = isAppear ? onAppear : onEnter
- const resolve = () => finishEnter(el, isAppear, done)
- callHook(hook, [el, resolve])
- nextFrame(() => {
- removeTransitionClass(el, isAppear ? appearFromClass : enterFromClass)
- if (__COMPAT__ && legacyClassEnabled) {
- const legacyClass = isAppear
- ? legacyAppearFromClass
- : legacyEnterFromClass
- if (legacyClass) {
- removeTransitionClass(el, legacyClass)
- }
- }
- addTransitionClass(el, isAppear ? appearToClass : enterToClass)
- if (!hasExplicitCallback(hook)) {
- whenTransitionEnds(el, type, enterDuration, resolve)
- }
- })
- }
- }
- return extend(baseProps, {
- onBeforeEnter(el) {
- callHook(onBeforeEnter, [el])
- addTransitionClass(el, enterFromClass)
- if (__COMPAT__ && legacyClassEnabled && legacyEnterFromClass) {
- addTransitionClass(el, legacyEnterFromClass)
- }
- addTransitionClass(el, enterActiveClass)
- },
- onBeforeAppear(el) {
- callHook(onBeforeAppear, [el])
- addTransitionClass(el, appearFromClass)
- if (__COMPAT__ && legacyClassEnabled && legacyAppearFromClass) {
- addTransitionClass(el, legacyAppearFromClass)
- }
- addTransitionClass(el, appearActiveClass)
- },
- onEnter: makeEnterHook(false),
- onAppear: makeEnterHook(true),
- onLeave(el: Element & { _isLeaving?: boolean }, done) {
- el._isLeaving = true
- const resolve = () => finishLeave(el, done)
- addTransitionClass(el, leaveFromClass)
- if (__COMPAT__ && legacyClassEnabled && legacyLeaveFromClass) {
- addTransitionClass(el, legacyLeaveFromClass)
- }
- // force reflow so *-leave-from classes immediately take effect (#2593)
- forceReflow()
- addTransitionClass(el, leaveActiveClass)
- nextFrame(() => {
- if (!el._isLeaving) {
- // cancelled
- return
- }
- removeTransitionClass(el, leaveFromClass)
- if (__COMPAT__ && legacyClassEnabled && legacyLeaveFromClass) {
- removeTransitionClass(el, legacyLeaveFromClass)
- }
- addTransitionClass(el, leaveToClass)
- if (!hasExplicitCallback(onLeave)) {
- whenTransitionEnds(el, type, leaveDuration, resolve)
- }
- })
- callHook(onLeave, [el, resolve])
- },
- onEnterCancelled(el) {
- finishEnter(el, false)
- callHook(onEnterCancelled, [el])
- },
- onAppearCancelled(el) {
- finishEnter(el, true)
- callHook(onAppearCancelled, [el])
- },
- onLeaveCancelled(el) {
- finishLeave(el)
- callHook(onLeaveCancelled, [el])
- }
- } as BaseTransitionProps<Element>)
- }
- 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__) {
- assertNumber(res, '<transition> explicit duration')
- }
- return res
- }
- export function addTransitionClass(el: Element, cls: string) {
- cls.split(/\s+/).forEach(c => c && el.classList.add(c))
- ;(
- (el as ElementWithTransition)[vtcKey] ||
- ((el as ElementWithTransition)[vtcKey] = 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)[vtcKey]
- if (_vtc) {
- _vtc.delete(cls)
- if (!_vtc!.size) {
- ;(el as ElementWithTransition)[vtcKey] = undefined
- }
- }
- }
- function nextFrame(cb: () => void) {
- requestAnimationFrame(() => {
- requestAnimationFrame(cb)
- })
- }
- let endId = 0
- function whenTransitionEnds(
- el: Element & { _endId?: number },
- expectedType: TransitionProps['type'] | undefined,
- explicitTimeout: number | null,
- resolve: () => void
- ) {
- const id = (el._endId = ++endId)
- const resolveIfNotStale = () => {
- if (id === el._endId) {
- resolve()
- }
- }
- if (explicitTimeout) {
- return setTimeout(resolveIfNotStale, explicitTimeout)
- }
- const { type, timeout, propCount } = getTransitionInfo(el, expectedType)
- if (!type) {
- return resolve()
- }
- const endEvent = type + 'end'
- let ended = 0
- const end = () => {
- el.removeEventListener(endEvent, onEnd)
- resolveIfNotStale()
- }
- const onEnd = (e: Event) => {
- if (e.target === el && ++ended >= propCount) {
- end()
- }
- }
- setTimeout(() => {
- if (ended < propCount) {
- end()
- }
- }, timeout + 1)
- el.addEventListener(endEvent, onEnd)
- }
- interface CSSTransitionInfo {
- type: AnimationTypes | null
- propCount: number
- timeout: number
- hasTransform: boolean
- }
- type AnimationProperties = 'Delay' | 'Duration'
- type StylePropertiesKey =
- | `${AnimationTypes}${AnimationProperties}`
- | `${typeof TRANSITION}Property`
- export function getTransitionInfo(
- el: Element,
- expectedType?: TransitionProps['type']
- ): CSSTransitionInfo {
- const styles = window.getComputedStyle(el) as Pick<
- CSSStyleDeclaration,
- StylePropertiesKey
- >
- // JSDOM may return undefined for transition properties
- const getStyleProperties = (key: StylePropertiesKey) =>
- (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(
- getStyleProperties(`${TRANSITION}Property`).toString()
- )
- 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 {
- // #8409 default value for CSS durations can be 'auto'
- if (s === 'auto') return 0
- return Number(s.slice(0, -1).replace(',', '.')) * 1000
- }
- // synchronously force layout to put elements into a certain state
- export function forceReflow() {
- return document.body.offsetHeight
- }
|