oak-general-business/lib/miniprogram_npm/wuxui/animation-group/index.js

383 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import baseComponent from '../helpers/baseComponent'
import styleToCssString from '../helpers/styleToCssString'
const ENTER = 'enter'
const ENTERING = 'entering'
const ENTERED = 'entered'
const EXIT = 'exit'
const EXITING = 'exiting'
const EXITED = 'exited'
const UNMOUNTED = 'unmounted'
const TRANSITION = 'transition'
const ANIMATION = 'animation'
const TIMEOUT = 1000 / 60
const defaultClassNames = {
enter: '', // 进入过渡的开始状态,在过渡过程完成之后移除
enterActive: '', // 进入过渡的结束状态,在过渡过程完成之后移除
enterDone: '', // 进入过渡的完成状态
exit: '', // 离开过渡的开始状态,在过渡过程完成之后移除
exitActive: '', // 离开过渡的结束状态,在过渡过程完成之后移除
exitDone: '', // 离开过渡的完成状态
}
baseComponent({
properties: {
// 触发组件进入或离开过渡的状态
in: {
type: Boolean,
value: false,
observer(newVal) {
if (this.data.isMounting) {
this.updated(newVal)
}
},
},
// 过渡的类名
classNames: {
type: null,
value: defaultClassNames,
},
// 过渡持续时间
duration: {
type: null,
value: null,
},
// 过渡动效的类型
type: {
type: String,
value: TRANSITION,
},
// 首次挂载时是否触发进入过渡
appear: {
type: Boolean,
value: false,
},
// 是否启用进入过渡
enter: {
type: Boolean,
value: true,
},
// 是否启用离开过渡
exit: {
type: Boolean,
value: true,
},
// 首次进入过渡时是否懒挂载组件
mountOnEnter: {
type: Boolean,
value: true,
},
// 离开过渡完成时是否卸载组件
unmountOnExit: {
type: Boolean,
value: true,
},
// 自定义类名
wrapCls: {
type: String,
value: '',
},
// 自定义样式
wrapStyle: {
type: [String, Object],
value: '',
observer(newVal) {
this.setData({
extStyle: styleToCssString(newVal),
})
},
},
disableScroll: {
type: Boolean,
value: false,
},
},
data: {
animateCss: '', // 动画样式
animateStatus: EXITED, // 动画状态,可选值 entering、entered、exiting、exited
isMounting: false, // 是否首次挂载
extStyle: '', // 组件样式
},
methods: {
/**
* 监听过渡或动画的回调函数
*/
addEventListener() {
const { animateStatus } = this.data
const { enter, exit } = this.getTimeouts()
if (animateStatus === ENTERING && !enter && this.data.enter) {
this.performEntered()
}
if (animateStatus === EXITING && !exit && this.data.exit) {
this.performExited()
}
},
/**
* 会在 WXSS transition 或 wx.createAnimation 动画结束后触发
*/
onTransitionEnd() {
if (this.data.type === TRANSITION) {
this.addEventListener()
}
},
/**
* 会在一个 WXSS animation 动画完成时触发
*/
onAnimationEnd() {
if (this.data.type === ANIMATION) {
this.addEventListener()
}
},
/**
* 更新组件状态
* @param {String} nextStatus 下一状态ENTERING 或 EXITING
* @param {Boolean} mounting 是否首次挂载
*/
updateStatus(nextStatus, mounting = false) {
if (nextStatus !== null) {
this.cancelNextCallback()
this.isAppearing = mounting
if (nextStatus === ENTERING) {
this.performEnter()
} else {
this.performExit()
}
}
},
/**
* 进入过渡
*/
performEnter() {
const { className, activeClassName } = this.getClassNames(ENTER)
const { enter } = this.getTimeouts()
const enterParams = {
animateStatus: ENTER,
animateCss: className,
}
const enteringParams = {
animateStatus: ENTERING,
animateCss: `${className} ${activeClassName}`,
}
// 若已禁用进入过渡,则更新状态至 ENTERED
if (!this.isAppearing && !this.data.enter) {
return this.performEntered()
}
// 第一阶段:设置进入过渡的开始状态,并触发 ENTER 事件
// 第二阶段:延迟一帧后,设置进入过渡的结束状态,并触发 ENTERING 事件
// 第三阶段:若已设置过渡的持续时间,则延迟指定时间后触发进入过渡完成 performEntered否则等待触发 onTransitionEnd 或 onAnimationEnd
this.safeSetData(enterParams, () => {
this.triggerEvent('change', { animateStatus: ENTER })
this.triggerEvent(ENTER, { isAppearing: this.isAppearing })
// 由于有些时候不能正确的触发动画完成的回调,具体原因未知
// 所以采用延迟一帧的方式来确保可以触发回调
this.delayHandler(TIMEOUT, () => {
this.safeSetData(enteringParams, () => {
this.triggerEvent('change', { animateStatus: ENTERING })
this.triggerEvent(ENTERING, { isAppearing: this.isAppearing })
if (enter) {
this.delayHandler(enter, this.performEntered)
}
})
})
})
},
/**
* 进入过渡完成
*/
performEntered() {
const { doneClassName } = this.getClassNames(ENTER)
const enteredParams = {
animateStatus: ENTERED,
animateCss: doneClassName,
}
// 第三阶段:设置进入过渡的完成状态,并触发 ENTERED 事件
this.safeSetData(enteredParams, () => {
this.triggerEvent('change', { animateStatus: ENTERED })
this.triggerEvent(ENTERED, { isAppearing: this.isAppearing })
})
},
/**
* 离开过渡
*/
performExit() {
const { className, activeClassName } = this.getClassNames(EXIT)
const { exit } = this.getTimeouts()
const exitParams = {
animateStatus: EXIT,
animateCss: className,
}
const exitingParams = {
animateStatus: EXITING,
animateCss: `${className} ${activeClassName}`,
}
// 若已禁用离开过渡,则更新状态至 EXITED
if (!this.data.exit) {
return this.performExited()
}
// 第一阶段:设置离开过渡的开始状态,并触发 EXIT 事件
// 第二阶段:延迟一帧后,设置离开过渡的结束状态,并触发 EXITING 事件
// 第三阶段:若已设置过渡的持续时间,则延迟指定时间后触发离开过渡完成 performExited否则等待触发 onTransitionEnd 或 onAnimationEnd
this.safeSetData(exitParams, () => {
this.triggerEvent('change', { animateStatus: EXIT })
this.triggerEvent(EXIT)
this.delayHandler(TIMEOUT, () => {
this.safeSetData(exitingParams, () => {
this.triggerEvent('change', { animateStatus: EXITING })
this.triggerEvent(EXITING)
if (exit) {
this.delayHandler(exit, this.performExited)
}
})
})
})
},
/**
* 离开过渡完成
*/
performExited() {
const { doneClassName } = this.getClassNames(EXIT)
const exitedParams = {
animateStatus: EXITED,
animateCss: doneClassName,
}
// 第三阶段:设置离开过渡的完成状态,并触发 EXITED 事件
this.safeSetData(exitedParams, () => {
this.triggerEvent('change', { animateStatus: EXITED })
this.triggerEvent(EXITED)
// 判断离开过渡完成时是否卸载组件
if (this.data.unmountOnExit) {
this.setData({ animateStatus: UNMOUNTED }, () => {
this.triggerEvent('change', { animateStatus: UNMOUNTED })
})
}
})
},
/**
* 获取指定状态下的类名
* @param {String} type 过渡类型enter 或 exit
*/
getClassNames(type) {
const { classNames } = this.data
const className = typeof classNames !== 'string' ? classNames[type] : `${classNames}-${type}`
const activeClassName = typeof classNames !== 'string' ? classNames[`${type}Active`] : `${classNames}-${type}-active`
const doneClassName = typeof classNames !== 'string' ? classNames[`${type}Done`] : `${classNames}-${type}-done`
return {
className,
activeClassName,
doneClassName,
}
},
/**
* 获取过渡持续时间
*/
getTimeouts() {
const { duration } = this.data
if (duration !== null && typeof duration === 'object') {
return {
enter: duration.enter,
exit: duration.exit,
}
} else if (typeof duration === 'number') {
return {
enter: duration,
exit: duration,
}
}
return {}
},
/**
* 属性值 in 被更改时的响应函数
* @param {Boolean} newVal 触发组件进入或离开过渡的状态
*/
updated(newVal) {
let { animateStatus } = this.pendingData || this.data
let nextStatus = null
if (newVal) {
if (animateStatus === UNMOUNTED) {
animateStatus = EXITED
this.setData({ animateStatus: EXITED }, () => {
this.triggerEvent('change', { animateStatus: EXITED })
})
}
if (animateStatus !== ENTER && animateStatus !== ENTERING && animateStatus !== ENTERED) {
nextStatus = ENTERING
}
} else {
if (animateStatus === ENTER || animateStatus === ENTERING || animateStatus === ENTERED) {
nextStatus = EXITING
}
}
this.updateStatus(nextStatus)
},
/**
* 延迟一段时间触发回调
* @param {Number} timeout 延迟时间
* @param {Function} handler 回调函数
*/
delayHandler(timeout, handler) {
if (timeout) {
this.setNextCallback(handler)
setTimeout(this.nextCallback, timeout)
}
},
/**
* 点击事件
*/
onTap() {
this.triggerEvent('click')
},
/**
* 阻止移动触摸
*/
noop() {},
},
attached() {
let animateStatus = null
let appearStatus = null
if (this.data.in) {
if (this.data.appear) {
animateStatus = EXITED
appearStatus = ENTERING
} else {
animateStatus = ENTERED
}
} else {
if (this.data.unmountOnExit || this.data.mountOnEnter) {
animateStatus = UNMOUNTED
} else {
animateStatus = EXITED
}
}
// 由于小程序组件首次挂载时 observer 事件总是优先于 attached 事件
// 所以使用 isMounting 来强制优先触发 attached 事件
this.safeSetData({ animateStatus, isMounting: true }, () => {
this.triggerEvent('change', { animateStatus })
this.updateStatus(appearStatus, true)
})
},
})