x-transition.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. import { releaseNextTicks, holdNextTicks } from '../nextTick'
  2. import { setClasses } from '../utils/classes'
  3. import { setStyles } from '../utils/styles'
  4. import { directive } from '../directives'
  5. import { mutateDom } from '../mutation'
  6. import { once } from '../utils/once'
  7. directive('transition', (el, { value, modifiers, expression }, { evaluate }) => {
  8. if (typeof expression === 'function') expression = evaluate(expression)
  9. if (expression === false) return
  10. if (!expression || typeof expression === 'boolean') {
  11. registerTransitionsFromHelper(el, modifiers, value)
  12. } else {
  13. registerTransitionsFromClassString(el, expression, value)
  14. }
  15. })
  16. function registerTransitionsFromClassString(el, classString, stage) {
  17. registerTransitionObject(el, setClasses, '')
  18. let directiveStorageMap = {
  19. 'enter': (classes) => { el._x_transition.enter.during = classes },
  20. 'enter-start': (classes) => { el._x_transition.enter.start = classes },
  21. 'enter-end': (classes) => { el._x_transition.enter.end = classes },
  22. 'leave': (classes) => { el._x_transition.leave.during = classes },
  23. 'leave-start': (classes) => { el._x_transition.leave.start = classes },
  24. 'leave-end': (classes) => { el._x_transition.leave.end = classes },
  25. }
  26. directiveStorageMap[stage](classString)
  27. }
  28. function registerTransitionsFromHelper(el, modifiers, stage) {
  29. registerTransitionObject(el, setStyles)
  30. let doesntSpecify = (! modifiers.includes('in') && ! modifiers.includes('out')) && ! stage
  31. let transitioningIn = doesntSpecify || modifiers.includes('in') || ['enter'].includes(stage)
  32. let transitioningOut = doesntSpecify || modifiers.includes('out') || ['leave'].includes(stage)
  33. if (modifiers.includes('in') && ! doesntSpecify) {
  34. modifiers = modifiers.filter((i, index) => index < modifiers.indexOf('out'))
  35. }
  36. if (modifiers.includes('out') && ! doesntSpecify) {
  37. modifiers = modifiers.filter((i, index) => index > modifiers.indexOf('out'))
  38. }
  39. let wantsAll = ! modifiers.includes('opacity') && ! modifiers.includes('scale')
  40. let wantsOpacity = wantsAll || modifiers.includes('opacity')
  41. let wantsScale = wantsAll || modifiers.includes('scale')
  42. let opacityValue = wantsOpacity ? 0 : 1
  43. let scaleValue = wantsScale ? modifierValue(modifiers, 'scale', 95) / 100 : 1
  44. let delay = modifierValue(modifiers, 'delay', 0) / 1000
  45. let origin = modifierValue(modifiers, 'origin', 'center')
  46. let property = 'opacity, transform'
  47. let durationIn = modifierValue(modifiers, 'duration', 150) / 1000
  48. let durationOut = modifierValue(modifiers, 'duration', 75) / 1000
  49. let easing = `cubic-bezier(0.4, 0.0, 0.2, 1)`
  50. if (transitioningIn) {
  51. el._x_transition.enter.during = {
  52. transformOrigin: origin,
  53. transitionDelay: `${delay}s`,
  54. transitionProperty: property,
  55. transitionDuration: `${durationIn}s`,
  56. transitionTimingFunction: easing,
  57. }
  58. el._x_transition.enter.start = {
  59. opacity: opacityValue,
  60. transform: `scale(${scaleValue})`,
  61. }
  62. el._x_transition.enter.end = {
  63. opacity: 1,
  64. transform: `scale(1)`,
  65. }
  66. }
  67. if (transitioningOut) {
  68. el._x_transition.leave.during = {
  69. transformOrigin: origin,
  70. transitionDelay: `${delay}s`,
  71. transitionProperty: property,
  72. transitionDuration: `${durationOut}s`,
  73. transitionTimingFunction: easing,
  74. }
  75. el._x_transition.leave.start = {
  76. opacity: 1,
  77. transform: `scale(1)`,
  78. }
  79. el._x_transition.leave.end = {
  80. opacity: opacityValue,
  81. transform: `scale(${scaleValue})`,
  82. }
  83. }
  84. }
  85. function registerTransitionObject(el, setFunction, defaultValue = {}) {
  86. if (! el._x_transition) el._x_transition = {
  87. enter: { during: defaultValue, start: defaultValue, end: defaultValue },
  88. leave: { during: defaultValue, start: defaultValue, end: defaultValue },
  89. in(before = () => {}, after = () => {}) {
  90. transition(el, setFunction, {
  91. during: this.enter.during,
  92. start: this.enter.start,
  93. end: this.enter.end,
  94. }, before, after)
  95. },
  96. out(before = () => {}, after = () => {}) {
  97. transition(el, setFunction, {
  98. during: this.leave.during,
  99. start: this.leave.start,
  100. end: this.leave.end,
  101. }, before, after)
  102. },
  103. }
  104. }
  105. window.Element.prototype._x_toggleAndCascadeWithTransitions = function (el, value, show, hide) {
  106. // We are running this function after one tick to prevent
  107. // a race condition from happening where elements that have a
  108. // @click.away always view themselves as shown on the page.
  109. // If the tab is active, we prioritise requestAnimationFrame which plays
  110. // nicely with nested animations otherwise we use setTimeout to make sure
  111. // it keeps running in background. setTimeout has a lower priority in the
  112. // event loop so it would skip nested transitions but when the tab is
  113. // hidden, it's not relevant.
  114. const nextTick = document.visibilityState === 'visible' ? requestAnimationFrame : setTimeout;
  115. let clickAwayCompatibleShow = () => nextTick(show);
  116. if (value) {
  117. if (el._x_transition && (el._x_transition.enter || el._x_transition.leave)) {
  118. // This fixes a bug where if you are only transitioning OUT and you are also using @click.outside
  119. // the element when shown immediately starts transitioning out. There is a test in the manual
  120. // transition test file for this: /tests/cypress/manual-transition-test.html
  121. (el._x_transition.enter && (Object.entries(el._x_transition.enter.during).length || Object.entries(el._x_transition.enter.start).length || Object.entries(el._x_transition.enter.end).length))
  122. ? el._x_transition.in(show)
  123. : clickAwayCompatibleShow()
  124. } else {
  125. el._x_transition
  126. ? el._x_transition.in(show)
  127. : clickAwayCompatibleShow()
  128. }
  129. return
  130. }
  131. // Livewire depends on el._x_hidePromise.
  132. el._x_hidePromise = el._x_transition
  133. ? new Promise((resolve, reject) => {
  134. el._x_transition.out(() => {}, () => resolve(hide))
  135. el._x_transitioning.beforeCancel(() => reject({ isFromCancelledTransition: true }))
  136. })
  137. : Promise.resolve(hide)
  138. queueMicrotask(() => {
  139. let closest = closestHide(el)
  140. if (closest) {
  141. if (! closest._x_hideChildren) closest._x_hideChildren = []
  142. closest._x_hideChildren.push(el)
  143. } else {
  144. nextTick(() => {
  145. let hideAfterChildren = el => {
  146. let carry = Promise.all([
  147. el._x_hidePromise,
  148. ...(el._x_hideChildren || []).map(hideAfterChildren),
  149. ]).then(([i]) => i())
  150. delete el._x_hidePromise
  151. delete el._x_hideChildren
  152. return carry
  153. }
  154. hideAfterChildren(el).catch((e) => {
  155. if (! e.isFromCancelledTransition) throw e
  156. })
  157. })
  158. }
  159. })
  160. }
  161. function closestHide(el) {
  162. let parent = el.parentNode
  163. if (! parent) return
  164. return parent._x_hidePromise ? parent : closestHide(parent)
  165. }
  166. export function transition(el, setFunction, { during, start, end } = {}, before = () => {}, after = () => {}) {
  167. if (el._x_transitioning) el._x_transitioning.cancel()
  168. if (Object.keys(during).length === 0 && Object.keys(start).length === 0 && Object.keys(end).length === 0) {
  169. // Execute right away if there is no transition.
  170. before(); after()
  171. return
  172. }
  173. let undoStart, undoDuring, undoEnd
  174. performTransition(el, {
  175. start() {
  176. undoStart = setFunction(el, start)
  177. },
  178. during() {
  179. undoDuring = setFunction(el, during)
  180. },
  181. before,
  182. end() {
  183. undoStart()
  184. undoEnd = setFunction(el, end)
  185. },
  186. after,
  187. cleanup() {
  188. undoDuring()
  189. undoEnd()
  190. },
  191. })
  192. }
  193. export function performTransition(el, stages) {
  194. // All transitions need to be truly "cancellable". Meaning we need to
  195. // account for interruptions at ALL stages of the transitions and
  196. // immediately run the rest of the transition.
  197. let interrupted, reachedBefore, reachedEnd
  198. let finish = once(() => {
  199. mutateDom(() => {
  200. interrupted = true
  201. if (! reachedBefore) stages.before()
  202. if (! reachedEnd) {
  203. stages.end()
  204. releaseNextTicks()
  205. }
  206. stages.after()
  207. // Adding an "isConnected" check, in case the callback removed the element from the DOM.
  208. if (el.isConnected) stages.cleanup()
  209. delete el._x_transitioning
  210. })
  211. })
  212. el._x_transitioning = {
  213. beforeCancels: [],
  214. beforeCancel(callback) { this.beforeCancels.push(callback) },
  215. cancel: once(function () { while (this.beforeCancels.length) { this.beforeCancels.shift()() }; finish(); }),
  216. finish,
  217. }
  218. mutateDom(() => {
  219. stages.start()
  220. stages.during()
  221. })
  222. holdNextTicks()
  223. requestAnimationFrame(() => {
  224. if (interrupted) return
  225. // Note: Safari's transitionDuration property will list out comma separated transition durations
  226. // for every single transition property. Let's grab the first one and call it a day.
  227. let duration = Number(getComputedStyle(el).transitionDuration.replace(/,.*/, '').replace('s', '')) * 1000
  228. let delay = Number(getComputedStyle(el).transitionDelay.replace(/,.*/, '').replace('s', '')) * 1000
  229. if (duration === 0) duration = Number(getComputedStyle(el).animationDuration.replace('s', '')) * 1000
  230. mutateDom(() => {
  231. stages.before()
  232. })
  233. reachedBefore = true
  234. requestAnimationFrame(() => {
  235. if (interrupted) return
  236. mutateDom(() => {
  237. stages.end()
  238. })
  239. releaseNextTicks()
  240. setTimeout(el._x_transitioning.finish, duration + delay)
  241. reachedEnd = true
  242. })
  243. })
  244. }
  245. export function modifierValue(modifiers, key, fallback) {
  246. // If the modifier isn't present, use the default.
  247. if (modifiers.indexOf(key) === -1) return fallback
  248. // If it IS present, grab the value after it: x-show.transition.duration.500ms
  249. const rawValue = modifiers[modifiers.indexOf(key) + 1]
  250. if (! rawValue) return fallback
  251. if (key === 'scale') {
  252. // Check if the very next value is NOT a number and return the fallback.
  253. // If x-show.transition.scale, we'll use the default scale value.
  254. // That is how a user opts out of the opacity transition.
  255. if (isNaN(rawValue)) return fallback
  256. }
  257. if (key === 'duration' || key === 'delay') {
  258. // Support x-transition.duration.500ms && duration.500
  259. let match = rawValue.match(/([0-9]+)ms/)
  260. if (match) return match[1]
  261. }
  262. if (key === 'origin') {
  263. // Support chaining origin directions: x-show.transition.top.right
  264. if (['top', 'right', 'left', 'center', 'bottom'].includes(modifiers[modifiers.indexOf(key) + 2])) {
  265. return [rawValue, modifiers[modifiers.indexOf(key) + 2]].join(' ')
  266. }
  267. }
  268. return rawValue
  269. }