index.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. import baseComponent from '../helpers/baseComponent'
  2. import styleToCssString from '../helpers/styleToCssString'
  3. const ENTER = 'enter'
  4. const ENTERING = 'entering'
  5. const ENTERED = 'entered'
  6. const EXIT = 'exit'
  7. const EXITING = 'exiting'
  8. const EXITED = 'exited'
  9. const UNMOUNTED = 'unmounted'
  10. const TRANSITION = 'transition'
  11. const ANIMATION = 'animation'
  12. const TIMEOUT = 1000 / 60
  13. const defaultClassNames = {
  14. enter: '', // 进入过渡的开始状态,在过渡过程完成之后移除
  15. enterActive: '', // 进入过渡的结束状态,在过渡过程完成之后移除
  16. enterDone: '', // 进入过渡的完成状态
  17. exit: '', // 离开过渡的开始状态,在过渡过程完成之后移除
  18. exitActive: '', // 离开过渡的结束状态,在过渡过程完成之后移除
  19. exitDone: '', // 离开过渡的完成状态
  20. }
  21. baseComponent({
  22. properties: {
  23. // 触发组件进入或离开过渡的状态
  24. in: {
  25. type: Boolean,
  26. value: false,
  27. observer(newVal) {
  28. if (this.data.isMounting) {
  29. this.updated(newVal)
  30. }
  31. },
  32. },
  33. // 过渡的类名
  34. classNames: {
  35. type: null,
  36. value: defaultClassNames,
  37. },
  38. // 过渡持续时间
  39. duration: {
  40. type: null,
  41. value: null,
  42. },
  43. // 过渡动效的类型
  44. type: {
  45. type: String,
  46. value: TRANSITION,
  47. },
  48. // 首次挂载时是否触发进入过渡
  49. appear: {
  50. type: Boolean,
  51. value: false,
  52. },
  53. // 是否启用进入过渡
  54. enter: {
  55. type: Boolean,
  56. value: true,
  57. },
  58. // 是否启用离开过渡
  59. exit: {
  60. type: Boolean,
  61. value: true,
  62. },
  63. // 首次进入过渡时是否懒挂载组件
  64. mountOnEnter: {
  65. type: Boolean,
  66. value: true,
  67. },
  68. // 离开过渡完成时是否卸载组件
  69. unmountOnExit: {
  70. type: Boolean,
  71. value: true,
  72. },
  73. // 自定义类名
  74. wrapCls: {
  75. type: String,
  76. value: '',
  77. },
  78. // 自定义样式
  79. wrapStyle: {
  80. type: [String, Object],
  81. value: '',
  82. observer(newVal) {
  83. this.setData({
  84. extStyle: styleToCssString(newVal),
  85. })
  86. },
  87. },
  88. },
  89. data: {
  90. animateCss: '', // 动画样式
  91. animateStatus: EXITED, // 动画状态,可选值 entering、entered、exiting、exited
  92. isMounting: false, // 是否首次挂载
  93. extStyle: '', // 组件样式
  94. },
  95. methods: {
  96. /**
  97. * 监听过渡或动画的回调函数
  98. */
  99. addEventListener() {
  100. const { animateStatus } = this.data
  101. const { enter, exit } = this.getTimeouts()
  102. if (animateStatus === ENTERING && !enter && this.data.enter) {
  103. this.performEntered()
  104. }
  105. if (animateStatus === EXITING && !exit && this.data.exit) {
  106. this.performExited()
  107. }
  108. },
  109. /**
  110. * 会在 WXSS transition 或 wx.createAnimation 动画结束后触发
  111. */
  112. onTransitionEnd() {
  113. if (this.data.type === TRANSITION) {
  114. this.addEventListener()
  115. }
  116. },
  117. /**
  118. * 会在一个 WXSS animation 动画完成时触发
  119. */
  120. onAnimationEnd() {
  121. if (this.data.type === ANIMATION) {
  122. this.addEventListener()
  123. }
  124. },
  125. /**
  126. * 更新组件状态
  127. * @param {String} nextStatus 下一状态,ENTERING 或 EXITING
  128. * @param {Boolean} mounting 是否首次挂载
  129. */
  130. updateStatus(nextStatus, mounting = false) {
  131. if (nextStatus !== null) {
  132. this.cancelNextCallback()
  133. this.isAppearing = mounting
  134. if (nextStatus === ENTERING) {
  135. this.performEnter()
  136. } else {
  137. this.performExit()
  138. }
  139. }
  140. },
  141. /**
  142. * 进入过渡
  143. */
  144. performEnter() {
  145. const { className, activeClassName } = this.getClassNames(ENTER)
  146. const { enter } = this.getTimeouts()
  147. const enterParams = {
  148. animateStatus: ENTER,
  149. animateCss: className,
  150. }
  151. const enteringParams = {
  152. animateStatus: ENTERING,
  153. animateCss: `${className} ${activeClassName}`,
  154. }
  155. // 若已禁用进入过渡,则更新状态至 ENTERED
  156. if (!this.isAppearing && !this.data.enter) {
  157. return this.performEntered()
  158. }
  159. // 第一阶段:设置进入过渡的开始状态,并触发 ENTER 事件
  160. // 第二阶段:延迟一帧后,设置进入过渡的结束状态,并触发 ENTERING 事件
  161. // 第三阶段:若已设置过渡的持续时间,则延迟指定时间后触发进入过渡完成 performEntered,否则等待触发 onTransitionEnd 或 onAnimationEnd
  162. this.safeSetData(enterParams, () => {
  163. this.triggerEvent('change', { animateStatus: ENTER })
  164. this.triggerEvent(ENTER, { isAppearing: this.isAppearing })
  165. // 由于有些时候不能正确的触发动画完成的回调,具体原因未知
  166. // 所以采用延迟一帧的方式来确保可以触发回调
  167. this.delayHandler(TIMEOUT, () => {
  168. this.safeSetData(enteringParams, () => {
  169. this.triggerEvent('change', { animateStatus: ENTERING })
  170. this.triggerEvent(ENTERING, { isAppearing: this.isAppearing })
  171. if (enter) {
  172. this.delayHandler(enter, this.performEntered)
  173. }
  174. })
  175. })
  176. })
  177. },
  178. /**
  179. * 进入过渡完成
  180. */
  181. performEntered() {
  182. const { doneClassName } = this.getClassNames(ENTER)
  183. const enteredParams = {
  184. animateStatus: ENTERED,
  185. animateCss: doneClassName,
  186. }
  187. // 第三阶段:设置进入过渡的完成状态,并触发 ENTERED 事件
  188. this.safeSetData(enteredParams, () => {
  189. this.triggerEvent('change', { animateStatus: ENTERED })
  190. this.triggerEvent(ENTERED, { isAppearing: this.isAppearing })
  191. })
  192. },
  193. /**
  194. * 离开过渡
  195. */
  196. performExit() {
  197. const { className, activeClassName } = this.getClassNames(EXIT)
  198. const { exit } = this.getTimeouts()
  199. const exitParams = {
  200. animateStatus: EXIT,
  201. animateCss: className,
  202. }
  203. const exitingParams = {
  204. animateStatus: EXITING,
  205. animateCss: `${className} ${activeClassName}`,
  206. }
  207. // 若已禁用离开过渡,则更新状态至 EXITED
  208. if (!this.data.exit) {
  209. return this.performExited()
  210. }
  211. // 第一阶段:设置离开过渡的开始状态,并触发 EXIT 事件
  212. // 第二阶段:延迟一帧后,设置离开过渡的结束状态,并触发 EXITING 事件
  213. // 第三阶段:若已设置过渡的持续时间,则延迟指定时间后触发离开过渡完成 performExited,否则等待触发 onTransitionEnd 或 onAnimationEnd
  214. this.safeSetData(exitParams, () => {
  215. this.triggerEvent('change', { animateStatus: EXIT })
  216. this.triggerEvent(EXIT)
  217. this.delayHandler(TIMEOUT, () => {
  218. this.safeSetData(exitingParams, () => {
  219. this.triggerEvent('change', { animateStatus: EXITING })
  220. this.triggerEvent(EXITING)
  221. if (exit) {
  222. this.delayHandler(exit, this.performExited)
  223. }
  224. })
  225. })
  226. })
  227. },
  228. /**
  229. * 离开过渡完成
  230. */
  231. performExited() {
  232. const { doneClassName } = this.getClassNames(EXIT)
  233. const exitedParams = {
  234. animateStatus: EXITED,
  235. animateCss: doneClassName,
  236. }
  237. // 第三阶段:设置离开过渡的完成状态,并触发 EXITED 事件
  238. this.safeSetData(exitedParams, () => {
  239. this.triggerEvent('change', { animateStatus: EXITED })
  240. this.triggerEvent(EXITED)
  241. // 判断离开过渡完成时是否卸载组件
  242. if (this.data.unmountOnExit) {
  243. this.setData({ animateStatus: UNMOUNTED }, () => {
  244. this.triggerEvent('change', { animateStatus: UNMOUNTED })
  245. })
  246. }
  247. })
  248. },
  249. /**
  250. * 获取指定状态下的类名
  251. * @param {String} type 过渡类型,enter 或 exit
  252. */
  253. getClassNames(type) {
  254. const { classNames } = this.data
  255. const className = typeof classNames !== 'string' ? classNames[type] : `${classNames}-${type}`
  256. const activeClassName = typeof classNames !== 'string' ? classNames[`${type}Active`] : `${classNames}-${type}-active`
  257. const doneClassName = typeof classNames !== 'string' ? classNames[`${type}Done`] : `${classNames}-${type}-done`
  258. return {
  259. className,
  260. activeClassName,
  261. doneClassName,
  262. }
  263. },
  264. /**
  265. * 获取过渡持续时间
  266. */
  267. getTimeouts() {
  268. const { duration } = this.data
  269. if (duration !== null && typeof duration === 'object') {
  270. return {
  271. enter: duration.enter,
  272. exit: duration.exit,
  273. }
  274. } else if (typeof duration === 'number') {
  275. return {
  276. enter: duration,
  277. exit: duration,
  278. }
  279. }
  280. return {}
  281. },
  282. /**
  283. * 属性值 in 被更改时的响应函数
  284. * @param {Boolean} newVal 触发组件进入或离开过渡的状态
  285. */
  286. updated(newVal) {
  287. let { animateStatus } = this.pendingData || this.data
  288. let nextStatus = null
  289. if (newVal) {
  290. if (animateStatus === UNMOUNTED) {
  291. animateStatus = EXITED
  292. this.setData({ animateStatus: EXITED }, () => {
  293. this.triggerEvent('change', { animateStatus: EXITED })
  294. })
  295. }
  296. if (animateStatus !== ENTER && animateStatus !== ENTERING && animateStatus !== ENTERED) {
  297. nextStatus = ENTERING
  298. }
  299. } else {
  300. if (animateStatus === ENTER || animateStatus === ENTERING || animateStatus === ENTERED) {
  301. nextStatus = EXITING
  302. }
  303. }
  304. this.updateStatus(nextStatus)
  305. },
  306. /**
  307. * 延迟一段时间触发回调
  308. * @param {Number} timeout 延迟时间
  309. * @param {Function} handler 回调函数
  310. */
  311. delayHandler(timeout, handler) {
  312. if (timeout) {
  313. this.setNextCallback(handler)
  314. setTimeout(this.nextCallback, timeout)
  315. }
  316. },
  317. /**
  318. * 点击事件
  319. */
  320. onTap() {
  321. this.triggerEvent('click')
  322. },
  323. },
  324. attached() {
  325. let animateStatus = null
  326. let appearStatus = null
  327. if (this.data.in) {
  328. if (this.data.appear) {
  329. animateStatus = EXITED
  330. appearStatus = ENTERING
  331. } else {
  332. animateStatus = ENTERED
  333. }
  334. } else {
  335. if (this.data.unmountOnExit || this.data.mountOnEnter) {
  336. animateStatus = UNMOUNTED
  337. } else {
  338. animateStatus = EXITED
  339. }
  340. }
  341. // 由于小程序组件首次挂载时 observer 事件总是优先于 attached 事件
  342. // 所以使用 isMounting 来强制优先触发 attached 事件
  343. this.safeSetData({ animateStatus, isMounting: true }, () => {
  344. this.triggerEvent('change', { animateStatus })
  345. this.updateStatus(appearStatus, true)
  346. })
  347. },
  348. })