index.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  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. disableScroll: {
  89. type: Boolean,
  90. value: false,
  91. },
  92. },
  93. data: {
  94. animateCss: '', // 动画样式
  95. animateStatus: EXITED, // 动画状态,可选值 entering、entered、exiting、exited
  96. isMounting: false, // 是否首次挂载
  97. extStyle: '', // 组件样式
  98. },
  99. methods: {
  100. /**
  101. * 监听过渡或动画的回调函数
  102. */
  103. addEventListener() {
  104. const { animateStatus } = this.data
  105. const { enter, exit } = this.getTimeouts()
  106. if (animateStatus === ENTERING && !enter && this.data.enter) {
  107. this.performEntered()
  108. }
  109. if (animateStatus === EXITING && !exit && this.data.exit) {
  110. this.performExited()
  111. }
  112. },
  113. /**
  114. * 会在 WXSS transition 或 wx.createAnimation 动画结束后触发
  115. */
  116. onTransitionEnd() {
  117. if (this.data.type === TRANSITION) {
  118. this.addEventListener()
  119. }
  120. },
  121. /**
  122. * 会在一个 WXSS animation 动画完成时触发
  123. */
  124. onAnimationEnd() {
  125. if (this.data.type === ANIMATION) {
  126. this.addEventListener()
  127. }
  128. },
  129. /**
  130. * 更新组件状态
  131. * @param {String} nextStatus 下一状态,ENTERING 或 EXITING
  132. * @param {Boolean} mounting 是否首次挂载
  133. */
  134. updateStatus(nextStatus, mounting = false) {
  135. if (nextStatus !== null) {
  136. this.cancelNextCallback()
  137. this.isAppearing = mounting
  138. if (nextStatus === ENTERING) {
  139. this.performEnter()
  140. } else {
  141. this.performExit()
  142. }
  143. }
  144. },
  145. /**
  146. * 进入过渡
  147. */
  148. performEnter() {
  149. const { className, activeClassName } = this.getClassNames(ENTER)
  150. const { enter } = this.getTimeouts()
  151. const enterParams = {
  152. animateStatus: ENTER,
  153. animateCss: className,
  154. }
  155. const enteringParams = {
  156. animateStatus: ENTERING,
  157. animateCss: `${className} ${activeClassName}`,
  158. }
  159. // 若已禁用进入过渡,则更新状态至 ENTERED
  160. if (!this.isAppearing && !this.data.enter) {
  161. return this.performEntered()
  162. }
  163. // 第一阶段:设置进入过渡的开始状态,并触发 ENTER 事件
  164. // 第二阶段:延迟一帧后,设置进入过渡的结束状态,并触发 ENTERING 事件
  165. // 第三阶段:若已设置过渡的持续时间,则延迟指定时间后触发进入过渡完成 performEntered,否则等待触发 onTransitionEnd 或 onAnimationEnd
  166. this.safeSetData(enterParams, () => {
  167. this.triggerEvent('change', { animateStatus: ENTER })
  168. this.triggerEvent(ENTER, { isAppearing: this.isAppearing })
  169. // 由于有些时候不能正确的触发动画完成的回调,具体原因未知
  170. // 所以采用延迟一帧的方式来确保可以触发回调
  171. this.delayHandler(TIMEOUT, () => {
  172. this.safeSetData(enteringParams, () => {
  173. this.triggerEvent('change', { animateStatus: ENTERING })
  174. this.triggerEvent(ENTERING, { isAppearing: this.isAppearing })
  175. if (enter) {
  176. this.delayHandler(enter, this.performEntered)
  177. }
  178. })
  179. })
  180. })
  181. },
  182. /**
  183. * 进入过渡完成
  184. */
  185. performEntered() {
  186. const { doneClassName } = this.getClassNames(ENTER)
  187. const enteredParams = {
  188. animateStatus: ENTERED,
  189. animateCss: doneClassName,
  190. }
  191. // 第三阶段:设置进入过渡的完成状态,并触发 ENTERED 事件
  192. this.safeSetData(enteredParams, () => {
  193. this.triggerEvent('change', { animateStatus: ENTERED })
  194. this.triggerEvent(ENTERED, { isAppearing: this.isAppearing })
  195. })
  196. },
  197. /**
  198. * 离开过渡
  199. */
  200. performExit() {
  201. const { className, activeClassName } = this.getClassNames(EXIT)
  202. const { exit } = this.getTimeouts()
  203. const exitParams = {
  204. animateStatus: EXIT,
  205. animateCss: className,
  206. }
  207. const exitingParams = {
  208. animateStatus: EXITING,
  209. animateCss: `${className} ${activeClassName}`,
  210. }
  211. // 若已禁用离开过渡,则更新状态至 EXITED
  212. if (!this.data.exit) {
  213. return this.performExited()
  214. }
  215. // 第一阶段:设置离开过渡的开始状态,并触发 EXIT 事件
  216. // 第二阶段:延迟一帧后,设置离开过渡的结束状态,并触发 EXITING 事件
  217. // 第三阶段:若已设置过渡的持续时间,则延迟指定时间后触发离开过渡完成 performExited,否则等待触发 onTransitionEnd 或 onAnimationEnd
  218. this.safeSetData(exitParams, () => {
  219. this.triggerEvent('change', { animateStatus: EXIT })
  220. this.triggerEvent(EXIT)
  221. this.delayHandler(TIMEOUT, () => {
  222. this.safeSetData(exitingParams, () => {
  223. this.triggerEvent('change', { animateStatus: EXITING })
  224. this.triggerEvent(EXITING)
  225. if (exit) {
  226. this.delayHandler(exit, this.performExited)
  227. }
  228. })
  229. })
  230. })
  231. },
  232. /**
  233. * 离开过渡完成
  234. */
  235. performExited() {
  236. const { doneClassName } = this.getClassNames(EXIT)
  237. const exitedParams = {
  238. animateStatus: EXITED,
  239. animateCss: doneClassName,
  240. }
  241. // 第三阶段:设置离开过渡的完成状态,并触发 EXITED 事件
  242. this.safeSetData(exitedParams, () => {
  243. this.triggerEvent('change', { animateStatus: EXITED })
  244. this.triggerEvent(EXITED)
  245. // 判断离开过渡完成时是否卸载组件
  246. if (this.data.unmountOnExit) {
  247. this.setData({ animateStatus: UNMOUNTED }, () => {
  248. this.triggerEvent('change', { animateStatus: UNMOUNTED })
  249. })
  250. }
  251. })
  252. },
  253. /**
  254. * 获取指定状态下的类名
  255. * @param {String} type 过渡类型,enter 或 exit
  256. */
  257. getClassNames(type) {
  258. const { classNames } = this.data
  259. const className = typeof classNames !== 'string' ? classNames[type] : `${classNames}-${type}`
  260. const activeClassName = typeof classNames !== 'string' ? classNames[`${type}Active`] : `${classNames}-${type}-active`
  261. const doneClassName = typeof classNames !== 'string' ? classNames[`${type}Done`] : `${classNames}-${type}-done`
  262. return {
  263. className,
  264. activeClassName,
  265. doneClassName,
  266. }
  267. },
  268. /**
  269. * 获取过渡持续时间
  270. */
  271. getTimeouts() {
  272. const { duration } = this.data
  273. if (duration !== null && typeof duration === 'object') {
  274. return {
  275. enter: duration.enter,
  276. exit: duration.exit,
  277. }
  278. } else if (typeof duration === 'number') {
  279. return {
  280. enter: duration,
  281. exit: duration,
  282. }
  283. }
  284. return {}
  285. },
  286. /**
  287. * 属性值 in 被更改时的响应函数
  288. * @param {Boolean} newVal 触发组件进入或离开过渡的状态
  289. */
  290. updated(newVal) {
  291. let { animateStatus } = this.pendingData || this.data
  292. let nextStatus = null
  293. if (newVal) {
  294. if (animateStatus === UNMOUNTED) {
  295. animateStatus = EXITED
  296. this.setData({ animateStatus: EXITED }, () => {
  297. this.triggerEvent('change', { animateStatus: EXITED })
  298. })
  299. }
  300. if (animateStatus !== ENTER && animateStatus !== ENTERING && animateStatus !== ENTERED) {
  301. nextStatus = ENTERING
  302. }
  303. } else {
  304. if (animateStatus === ENTER || animateStatus === ENTERING || animateStatus === ENTERED) {
  305. nextStatus = EXITING
  306. }
  307. }
  308. this.updateStatus(nextStatus)
  309. },
  310. /**
  311. * 延迟一段时间触发回调
  312. * @param {Number} timeout 延迟时间
  313. * @param {Function} handler 回调函数
  314. */
  315. delayHandler(timeout, handler) {
  316. if (timeout) {
  317. this.setNextCallback(handler)
  318. setTimeout(this.nextCallback, timeout)
  319. }
  320. },
  321. /**
  322. * 点击事件
  323. */
  324. onTap() {
  325. this.triggerEvent('click')
  326. },
  327. /**
  328. * 阻止移动触摸
  329. */
  330. noop() {},
  331. },
  332. attached() {
  333. let animateStatus = null
  334. let appearStatus = null
  335. if (this.data.in) {
  336. if (this.data.appear) {
  337. animateStatus = EXITED
  338. appearStatus = ENTERING
  339. } else {
  340. animateStatus = ENTERED
  341. }
  342. } else {
  343. if (this.data.unmountOnExit || this.data.mountOnEnter) {
  344. animateStatus = UNMOUNTED
  345. } else {
  346. animateStatus = EXITED
  347. }
  348. }
  349. // 由于小程序组件首次挂载时 observer 事件总是优先于 attached 事件
  350. // 所以使用 isMounting 来强制优先触发 attached 事件
  351. this.safeSetData({ animateStatus, isMounting: true }, () => {
  352. this.triggerEvent('change', { animateStatus })
  353. this.updateStatus(appearStatus, true)
  354. })
  355. },
  356. })