Transition.ts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. import {
  2. BaseTransition,
  3. BaseTransitionProps,
  4. h,
  5. warn,
  6. FunctionalComponent,
  7. getCurrentInstance,
  8. callWithAsyncErrorHandling
  9. } from '@vue/runtime-core'
  10. import { isObject, toNumber } from '@vue/shared'
  11. import { ErrorCodes } from 'packages/runtime-core/src/errorHandling'
  12. const TRANSITION = 'transition'
  13. const ANIMATION = 'animation'
  14. export interface TransitionProps extends BaseTransitionProps<Element> {
  15. name?: string
  16. type?: typeof TRANSITION | typeof ANIMATION
  17. css?: boolean
  18. duration?: number | { enter: number; leave: number }
  19. // custom transition classes
  20. enterFromClass?: string
  21. enterActiveClass?: string
  22. enterToClass?: string
  23. appearFromClass?: string
  24. appearActiveClass?: string
  25. appearToClass?: string
  26. leaveFromClass?: string
  27. leaveActiveClass?: string
  28. leaveToClass?: string
  29. }
  30. // DOM Transition is a higher-order-component based on the platform-agnostic
  31. // base Transition component, with DOM-specific logic.
  32. export const Transition: FunctionalComponent<TransitionProps> = (
  33. props,
  34. { slots }
  35. ) => h(BaseTransition, resolveTransitionProps(props), slots)
  36. export const TransitionPropsValidators = (Transition.props = {
  37. ...(BaseTransition as any).props,
  38. name: String,
  39. type: String,
  40. css: {
  41. type: Boolean,
  42. default: true
  43. },
  44. duration: [String, Number, Object],
  45. enterFromClass: String,
  46. enterActiveClass: String,
  47. enterToClass: String,
  48. appearFromClass: String,
  49. appearActiveClass: String,
  50. appearToClass: String,
  51. leaveFromClass: String,
  52. leaveActiveClass: String,
  53. leaveToClass: String
  54. })
  55. export function resolveTransitionProps({
  56. name = 'v',
  57. type,
  58. css = true,
  59. duration,
  60. enterFromClass = `${name}-enter-from`,
  61. enterActiveClass = `${name}-enter-active`,
  62. enterToClass = `${name}-enter-to`,
  63. appearFromClass = enterFromClass,
  64. appearActiveClass = enterActiveClass,
  65. appearToClass = enterToClass,
  66. leaveFromClass = `${name}-leave-from`,
  67. leaveActiveClass = `${name}-leave-active`,
  68. leaveToClass = `${name}-leave-to`,
  69. ...baseProps
  70. }: TransitionProps): BaseTransitionProps<Element> {
  71. if (!css) {
  72. return baseProps
  73. }
  74. const originEnterClass = [enterFromClass, enterActiveClass, enterToClass]
  75. const instance = getCurrentInstance()!
  76. const durations = normalizeDuration(duration)
  77. const enterDuration = durations && durations[0]
  78. const leaveDuration = durations && durations[1]
  79. const { appear, onBeforeEnter, onEnter, onLeave } = baseProps
  80. // is appearing
  81. if (appear && !instance.isMounted) {
  82. enterFromClass = appearFromClass
  83. enterActiveClass = appearActiveClass
  84. enterToClass = appearToClass
  85. }
  86. type Hook = (el: Element, done?: () => void) => void
  87. const finishEnter: Hook = (el, done) => {
  88. removeTransitionClass(el, enterToClass)
  89. removeTransitionClass(el, enterActiveClass)
  90. done && done()
  91. // reset enter class
  92. if (appear) {
  93. ;[enterFromClass, enterActiveClass, enterToClass] = originEnterClass
  94. }
  95. }
  96. const finishLeave: Hook = (el, done) => {
  97. removeTransitionClass(el, leaveToClass)
  98. removeTransitionClass(el, leaveActiveClass)
  99. done && done()
  100. }
  101. // only needed for user hooks called in nextFrame
  102. // sync errors are already handled by BaseTransition
  103. function callHookWithErrorHandling(hook: Hook, args: any[]) {
  104. callWithAsyncErrorHandling(hook, instance, ErrorCodes.TRANSITION_HOOK, args)
  105. }
  106. return {
  107. ...baseProps,
  108. onBeforeEnter(el) {
  109. onBeforeEnter && onBeforeEnter(el)
  110. addTransitionClass(el, enterActiveClass)
  111. addTransitionClass(el, enterFromClass)
  112. },
  113. onEnter(el, done) {
  114. nextFrame(() => {
  115. const resolve = () => finishEnter(el, done)
  116. onEnter && callHookWithErrorHandling(onEnter as Hook, [el, resolve])
  117. removeTransitionClass(el, enterFromClass)
  118. addTransitionClass(el, enterToClass)
  119. if (!(onEnter && onEnter.length > 1)) {
  120. if (enterDuration) {
  121. setTimeout(resolve, enterDuration)
  122. } else {
  123. whenTransitionEnds(el, type, resolve)
  124. }
  125. }
  126. })
  127. },
  128. onLeave(el, done) {
  129. addTransitionClass(el, leaveActiveClass)
  130. addTransitionClass(el, leaveFromClass)
  131. nextFrame(() => {
  132. const resolve = () => finishLeave(el, done)
  133. onLeave && callHookWithErrorHandling(onLeave as Hook, [el, resolve])
  134. removeTransitionClass(el, leaveFromClass)
  135. addTransitionClass(el, leaveToClass)
  136. if (!(onLeave && onLeave.length > 1)) {
  137. if (leaveDuration) {
  138. setTimeout(resolve, leaveDuration)
  139. } else {
  140. whenTransitionEnds(el, type, resolve)
  141. }
  142. }
  143. })
  144. },
  145. onEnterCancelled: finishEnter,
  146. onLeaveCancelled: finishLeave
  147. }
  148. }
  149. function normalizeDuration(
  150. duration: TransitionProps['duration']
  151. ): [number, number] | null {
  152. if (duration == null) {
  153. return null
  154. } else if (isObject(duration)) {
  155. return [NumberOf(duration.enter), NumberOf(duration.leave)]
  156. } else {
  157. const n = NumberOf(duration)
  158. return [n, n]
  159. }
  160. }
  161. function NumberOf(val: unknown): number {
  162. const res = toNumber(val)
  163. if (__DEV__) validateDuration(res)
  164. return res
  165. }
  166. function validateDuration(val: unknown) {
  167. if (typeof val !== 'number') {
  168. warn(
  169. `<transition> explicit duration is not a valid number - ` +
  170. `got ${JSON.stringify(val)}.`
  171. )
  172. } else if (isNaN(val)) {
  173. warn(
  174. `<transition> explicit duration is NaN - ` +
  175. 'the duration expression might be incorrect.'
  176. )
  177. }
  178. }
  179. export interface ElementWithTransition extends HTMLElement {
  180. // _vtc = Vue Transition Classes.
  181. // Store the temporarily-added transition classes on the element
  182. // so that we can avoid overwriting them if the element's class is patched
  183. // during the transition.
  184. _vtc?: Set<string>
  185. }
  186. export function addTransitionClass(el: Element, cls: string) {
  187. cls.split(/\s+/).forEach(c => c && el.classList.add(c))
  188. ;(
  189. (el as ElementWithTransition)._vtc ||
  190. ((el as ElementWithTransition)._vtc = new Set())
  191. ).add(cls)
  192. }
  193. export function removeTransitionClass(el: Element, cls: string) {
  194. cls.split(/\s+/).forEach(c => c && el.classList.remove(c))
  195. const { _vtc } = el as ElementWithTransition
  196. if (_vtc) {
  197. _vtc.delete(cls)
  198. if (!_vtc!.size) {
  199. ;(el as ElementWithTransition)._vtc = undefined
  200. }
  201. }
  202. }
  203. function nextFrame(cb: () => void) {
  204. requestAnimationFrame(() => {
  205. requestAnimationFrame(cb)
  206. })
  207. }
  208. function whenTransitionEnds(
  209. el: Element,
  210. expectedType: TransitionProps['type'] | undefined,
  211. cb: () => void
  212. ) {
  213. const { type, timeout, propCount } = getTransitionInfo(el, expectedType)
  214. if (!type) {
  215. return cb()
  216. }
  217. const endEvent = type + 'end'
  218. let ended = 0
  219. const end = () => {
  220. el.removeEventListener(endEvent, onEnd)
  221. cb()
  222. }
  223. const onEnd = (e: Event) => {
  224. if (e.target === el) {
  225. if (++ended >= propCount) {
  226. end()
  227. }
  228. }
  229. }
  230. setTimeout(() => {
  231. if (ended < propCount) {
  232. end()
  233. }
  234. }, timeout + 1)
  235. el.addEventListener(endEvent, onEnd)
  236. }
  237. interface CSSTransitionInfo {
  238. type: typeof TRANSITION | typeof ANIMATION | null
  239. propCount: number
  240. timeout: number
  241. hasTransform: boolean
  242. }
  243. export function getTransitionInfo(
  244. el: Element,
  245. expectedType?: TransitionProps['type']
  246. ): CSSTransitionInfo {
  247. const styles: any = window.getComputedStyle(el)
  248. // JSDOM may return undefined for transition properties
  249. const getStyleProperties = (key: string) => (styles[key] || '').split(', ')
  250. const transitionDelays = getStyleProperties(TRANSITION + 'Delay')
  251. const transitionDurations = getStyleProperties(TRANSITION + 'Duration')
  252. const transitionTimeout = getTimeout(transitionDelays, transitionDurations)
  253. const animationDelays = getStyleProperties(ANIMATION + 'Delay')
  254. const animationDurations = getStyleProperties(ANIMATION + 'Duration')
  255. const animationTimeout = getTimeout(animationDelays, animationDurations)
  256. let type: CSSTransitionInfo['type'] = null
  257. let timeout = 0
  258. let propCount = 0
  259. /* istanbul ignore if */
  260. if (expectedType === TRANSITION) {
  261. if (transitionTimeout > 0) {
  262. type = TRANSITION
  263. timeout = transitionTimeout
  264. propCount = transitionDurations.length
  265. }
  266. } else if (expectedType === ANIMATION) {
  267. if (animationTimeout > 0) {
  268. type = ANIMATION
  269. timeout = animationTimeout
  270. propCount = animationDurations.length
  271. }
  272. } else {
  273. timeout = Math.max(transitionTimeout, animationTimeout)
  274. type =
  275. timeout > 0
  276. ? transitionTimeout > animationTimeout
  277. ? TRANSITION
  278. : ANIMATION
  279. : null
  280. propCount = type
  281. ? type === TRANSITION
  282. ? transitionDurations.length
  283. : animationDurations.length
  284. : 0
  285. }
  286. const hasTransform =
  287. type === TRANSITION &&
  288. /\b(transform|all)(,|$)/.test(styles[TRANSITION + 'Property'])
  289. return {
  290. type,
  291. timeout,
  292. propCount,
  293. hasTransform
  294. }
  295. }
  296. function getTimeout(delays: string[], durations: string[]): number {
  297. while (delays.length < durations.length) {
  298. delays = delays.concat(delays)
  299. }
  300. return Math.max(...durations.map((d, i) => toMs(d) + toMs(delays[i])))
  301. }
  302. // Old versions of Chromium (below 61.0.3163.100) formats floating pointer
  303. // numbers in a locale-dependent way, using a comma instead of a dot.
  304. // If comma is not replaced with a dot, the input will be rounded down
  305. // (i.e. acting as a floor function) causing unexpected behaviors
  306. function toMs(s: string): number {
  307. return Number(s.slice(0, -1).replace(',', '.')) * 1000
  308. }