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