UNPKG

@ionic/core

Version:
770 lines (765 loc) • 24.8 kB
import { h } from '../ionic.core.js'; function transitionEnd(el, callback) { let unRegTrans; const opts = { passive: true }; function unregister() { if (unRegTrans) { unRegTrans(); } } function onTransitionEnd(ev) { if (el === ev.target) { unregister(); callback(ev); } } if (el) { el.addEventListener('webkitTransitionEnd', onTransitionEnd, opts); el.addEventListener('transitionend', onTransitionEnd, opts); unRegTrans = () => { el.removeEventListener('webkitTransitionEnd', onTransitionEnd, opts); el.removeEventListener('transitionend', onTransitionEnd, opts); }; } return unregister; } const CSS_VALUE_REGEX = /(^-?\d*\.?\d*)(.*)/; const DURATION_MIN = 32; const TRANSITION_END_FALLBACK_PADDING_MS = 400; const TRANSFORM_PROPS = { 'translateX': 1, 'translateY': 1, 'translateZ': 1, 'scale': 1, 'scaleX': 1, 'scaleY': 1, 'scaleZ': 1, 'rotate': 1, 'rotateX': 1, 'rotateY': 1, 'rotateZ': 1, 'skewX': 1, 'skewY': 1, 'perspective': 1 }; const raf = window.requestAnimationFrame ? window.requestAnimationFrame.bind(window) : (f) => f(Date.now()); class Animator { constructor() { this._hasDur = false; this._hasTweenEffect = false; this._isAsync = false; this._isReverse = false; this._destroyed = false; this.hasChildren = false; this.isPlaying = false; this.hasCompleted = false; } addElement(el) { if (el != null) { if (el.length > 0) { for (let i = 0; i < el.length; i++) { this._addEl(el[i]); } } else { this._addEl(el); } } return this; } _addEl(el) { if (el.nodeType === 1) { (this._elements = this._elements || []).push(el); } } add(childAnimation) { childAnimation.parent = this; this.hasChildren = true; (this._childAnimations = this._childAnimations || []).push(childAnimation); return this; } getDuration(opts) { if (opts && opts.duration !== undefined) { return opts.duration; } else if (this._duration !== undefined) { return this._duration; } else if (this.parent) { return this.parent.getDuration(); } return 0; } isRoot() { return !this.parent; } duration(milliseconds) { this._duration = milliseconds; return this; } getEasing() { if (this._isReverse && this._reversedEasingName !== undefined) { return this._reversedEasingName; } return this._easingName !== undefined ? this._easingName : (this.parent && this.parent.getEasing()) || null; } easing(name) { this._easingName = name; return this; } easingReverse(name) { this._reversedEasingName = name; return this; } from(prop, val) { this._addProp('from', prop, val); return this; } to(prop, val, clearProperyAfterTransition = false) { const fx = this._addProp('to', prop, val); if (clearProperyAfterTransition) { this.afterClearStyles([fx.trans ? 'transform' : prop]); } return this; } fromTo(prop, fromVal, toVal, clearProperyAfterTransition) { return this.from(prop, fromVal).to(prop, toVal, clearProperyAfterTransition); } _getProp(name) { if (this._fxProperties) { return this._fxProperties.find(prop => prop.effectName === name); } return undefined; } _addProp(state, prop, val) { let fxProp = this._getProp(prop); if (!fxProp) { const shouldTrans = (TRANSFORM_PROPS[prop] === 1); fxProp = { effectName: prop, trans: shouldTrans, wc: (shouldTrans ? 'transform' : prop) }; (this._fxProperties = this._fxProperties || []).push(fxProp); } const fxState = { val, num: 0, effectUnit: '', }; fxProp[state] = fxState; if (typeof val === 'string' && val.indexOf(' ') < 0) { const r = val.match(CSS_VALUE_REGEX); if (r) { const num = parseFloat(r[1]); if (!isNaN(num)) { fxState.num = num; } fxState.effectUnit = (r[0] !== r[2] ? r[2] : ''); } } else if (typeof val === 'number') { fxState.num = val; } return fxProp; } beforeAddClass(className) { (this._beforeAddClasses = this._beforeAddClasses || []).push(className); return this; } beforeRemoveClass(className) { (this._beforeRemoveClasses = this._beforeRemoveClasses || []).push(className); return this; } beforeStyles(styles) { this._beforeStyles = styles; return this; } beforeClearStyles(propertyNames) { this._beforeStyles = this._beforeStyles || {}; for (const prop of propertyNames) { this._beforeStyles[prop] = ''; } return this; } beforeAddRead(domReadFn) { (this._readCallbacks = this._readCallbacks || []).push(domReadFn); return this; } beforeAddWrite(domWriteFn) { (this._writeCallbacks = this._writeCallbacks || []).push(domWriteFn); return this; } afterAddClass(className) { (this._afterAddClasses = this._afterAddClasses || []).push(className); return this; } afterRemoveClass(className) { (this._afterRemoveClasses = this._afterRemoveClasses || []).push(className); return this; } afterStyles(styles) { this._afterStyles = styles; return this; } afterClearStyles(propertyNames) { this._afterStyles = this._afterStyles || {}; for (const prop of propertyNames) { this._afterStyles[prop] = ''; } return this; } play(opts) { if (this._destroyed) { return; } this._isAsync = this._hasDuration(opts); this._clearAsync(); this._playInit(opts); raf(() => { raf(() => { this._playDomInspect(opts); }); }); } playAsync(opts) { return new Promise(resolve => { this.onFinish(resolve, { oneTimeCallback: true, clearExistingCallbacks: true }); this.play(opts); return this; }); } playSync() { if (!this._destroyed) { const opts = { duration: 0 }; this._isAsync = false; this._clearAsync(); this._playInit(opts); this._playDomInspect(opts); } } _playInit(opts) { this._hasTweenEffect = false; this.isPlaying = true; this.hasCompleted = false; this._hasDur = (this.getDuration(opts) > DURATION_MIN); const children = this._childAnimations; if (children) { for (const child of children) { child._playInit(opts); } } if (this._hasDur) { this._progress(0); this._willChange(true); } } _playDomInspect(opts) { this._beforeAnimation(); const dur = this.getDuration(opts); if (this._isAsync) { this._asyncEnd(dur, true); } this._playProgress(opts); if (this._isAsync && !this._destroyed) { raf(() => { this._playToStep(1); }); } } _playProgress(opts) { const children = this._childAnimations; if (children) { for (const child of children) { child._playProgress(opts); } } if (this._hasDur) { this._setTrans(this.getDuration(opts), false); } else { this._progress(1); this._setAfterStyles(); this._didFinish(true); } } _playToStep(stepValue) { if (!this._destroyed) { const children = this._childAnimations; if (children) { for (const child of children) { child._playToStep(stepValue); } } if (this._hasDur) { this._progress(stepValue); } } } _asyncEnd(dur, shouldComplete) { const self = this; function onTransitionEnd() { self._clearAsync(); self._playEnd(); self._didFinishAll(shouldComplete, true, false); } function onTransitionFallback() { console.debug('Animation onTransitionFallback, CSS onTransitionEnd did not fire!'); self._timerId = undefined; self._clearAsync(); self._playEnd(shouldComplete ? 1 : 0); self._didFinishAll(shouldComplete, true, false); } self._unregisterTrnsEnd = transitionEnd(self._transEl(), onTransitionEnd); self._timerId = setTimeout(onTransitionFallback, (dur + TRANSITION_END_FALLBACK_PADDING_MS)); } _playEnd(stepValue) { const children = this._childAnimations; if (children) { for (const child of children) { child._playEnd(stepValue); } } if (this._hasDur) { if (stepValue !== undefined) { this._setTrans(0, true); this._progress(stepValue); } this._setAfterStyles(); this._willChange(false); } } _hasDuration(opts) { if (this.getDuration(opts) > DURATION_MIN) { return true; } const children = this._childAnimations; if (children) { for (const child of children) { if (child._hasDuration(opts)) { return true; } } } return false; } _hasDomReads() { if (this._readCallbacks && this._readCallbacks.length > 0) { return true; } const children = this._childAnimations; if (children) { for (const child of children) { if (child._hasDomReads()) { return true; } } } return false; } stop(stepValue = 1) { this._clearAsync(); this._hasDur = true; this._playEnd(stepValue); } _clearAsync() { if (this._unregisterTrnsEnd) { this._unregisterTrnsEnd(); } if (this._timerId) { clearTimeout(this._timerId); } this._timerId = this._unregisterTrnsEnd = undefined; } _progress(stepValue) { let val; const elements = this._elements; const effects = this._fxProperties; if (!elements || elements.length === 0 || !effects || this._destroyed) { return; } if (this._isReverse) { stepValue = 1 - stepValue; } let i = 0; let j = 0; let finalTransform = ''; let fx; for (i = 0; i < effects.length; i++) { fx = effects[i]; if (fx.from && fx.to) { const fromNum = fx.from.num; const toNum = fx.to.num; const tweenEffect = (fromNum !== toNum); if (tweenEffect) { this._hasTweenEffect = true; } if (stepValue === 0) { val = fx.from.val; } else if (stepValue === 1) { val = fx.to.val; } else if (tweenEffect) { const valNum = (((toNum - fromNum) * stepValue) + fromNum); const unit = fx.to.effectUnit; val = valNum + unit; } if (val !== null) { const prop = fx.effectName; if (fx.trans) { finalTransform += prop + '(' + val + ') '; } else { for (j = 0; j < elements.length; j++) { elements[j].style.setProperty(prop, val); } } } } } if (finalTransform.length > 0) { if (!this._isReverse && stepValue !== 1 || this._isReverse && stepValue !== 0) { finalTransform += 'translateZ(0px)'; } for (i = 0; i < elements.length; i++) { elements[i].style.setProperty('transform', finalTransform); } } } _setTrans(dur, forcedLinearEasing) { const elements = this._elements; if (!elements || elements.length === 0 || !this._fxProperties) { return; } const easing = (forcedLinearEasing ? 'linear' : this.getEasing()); const durString = dur + 'ms'; for (const { style } of elements) { if (dur > 0) { style.transitionDuration = durString; if (easing !== null) { style.transitionTimingFunction = easing; } } else { style.transitionDuration = '0'; } } } _beforeAnimation() { this._fireBeforeReadFunc(); this._fireBeforeWriteFunc(); this._setBeforeStyles(); } _setBeforeStyles() { const children = this._childAnimations; if (children) { for (const child of children) { child._setBeforeStyles(); } } const elements = this._elements; if (!elements || elements.length === 0 || this._isReverse) { return; } const addClasses = this._beforeAddClasses; const removeClasses = this._beforeRemoveClasses; for (const el of elements) { const elementClassList = el.classList; if (addClasses) { for (const c of addClasses) { elementClassList.add(c); } } if (removeClasses) { for (const c of removeClasses) { elementClassList.remove(c); } } if (this._beforeStyles) { for (const [key, value] of Object.entries(this._beforeStyles)) { el.style.setProperty(key, value); } } } } _fireBeforeReadFunc() { const children = this._childAnimations; if (children) { for (const child of children) { child._fireBeforeReadFunc(); } } const readFunctions = this._readCallbacks; if (readFunctions) { for (const callback of readFunctions) { callback(); } } } _fireBeforeWriteFunc() { const children = this._childAnimations; if (children) { for (const child of children) { child._fireBeforeWriteFunc(); } } const writeFunctions = this._writeCallbacks; if (writeFunctions) { for (const callback of writeFunctions) { callback(); } } } _setAfterStyles() { const elements = this._elements; if (!elements) { return; } for (const el of elements) { const elementClassList = el.classList; el.style.transitionDuration = el.style.transitionTimingFunction = ''; if (this._isReverse) { const beforeAddClasses = this._beforeAddClasses; if (beforeAddClasses) { for (const c of beforeAddClasses) { elementClassList.remove(c); } } const beforeRemoveClasses = this._beforeRemoveClasses; if (beforeRemoveClasses) { for (const c of beforeRemoveClasses) { elementClassList.add(c); } } const beforeStyles = this._beforeStyles; if (beforeStyles) { for (const propName of Object.keys(beforeStyles)) { el.style.removeProperty(propName); } } } else { const afterAddClasses = this._afterAddClasses; if (afterAddClasses) { for (const c of afterAddClasses) { elementClassList.add(c); } } const afterRemoveClasses = this._afterRemoveClasses; if (afterRemoveClasses) { for (const c of afterRemoveClasses) { elementClassList.remove(c); } } const afterStyles = this._afterStyles; if (afterStyles) { for (const [key, value] of Object.entries(afterStyles)) { el.style.setProperty(key, value); } } } } } _willChange(addWillChange) { let wc; const effects = this._fxProperties; let willChange; if (addWillChange && effects) { wc = []; for (const effect of effects) { const propWC = effect.wc; if (propWC === 'webkitTransform') { wc.push('transform', '-webkit-transform'); } else if (propWC !== undefined) { wc.push(propWC); } } willChange = wc.join(','); } else { willChange = ''; } const elements = this._elements; if (elements) { for (const el of elements) { el.style.setProperty('will-change', willChange); } } } progressStart() { this._clearAsync(); this._beforeAnimation(); this._progressStart(); } _progressStart() { const children = this._childAnimations; if (children) { for (const child of children) { child._progressStart(); } } this._setTrans(0, true); this._willChange(true); } progressStep(stepValue) { stepValue = Math.min(1, Math.max(0, stepValue)); const children = this._childAnimations; if (children) { for (const child of children) { child.progressStep(stepValue); } } this._progress(stepValue); } progressEnd(shouldComplete, currentStepValue, dur = -1) { if (this._isReverse) { currentStepValue = 1 - currentStepValue; } const stepValue = shouldComplete ? 1 : 0; const diff = Math.abs(currentStepValue - stepValue); if (dur < 0) { dur = this._duration || 0; } else if (diff < 0.05) { dur = 0; } this._isAsync = (dur > 30); this._progressEnd(shouldComplete, stepValue, dur, this._isAsync); if (this._isAsync) { this._asyncEnd(dur, shouldComplete); if (!this._destroyed) { raf(() => { this._playToStep(stepValue); }); } } } _progressEnd(shouldComplete, stepValue, dur, isAsync) { const children = this._childAnimations; if (children) { for (const child of children) { child._progressEnd(shouldComplete, stepValue, dur, isAsync); } } if (!isAsync) { this._progress(stepValue); this._willChange(false); this._setAfterStyles(); this._didFinish(shouldComplete); } else { this.isPlaying = true; this.hasCompleted = false; this._hasDur = true; this._willChange(true); this._setTrans(dur, false); } } onFinish(callback, opts) { if (opts && opts.clearExistingCallbacks) { this._onFinishCallbacks = this._onFinishOneTimeCallbacks = undefined; } if (opts && opts.oneTimeCallback) { this._onFinishOneTimeCallbacks = this._onFinishOneTimeCallbacks || []; this._onFinishOneTimeCallbacks.push(callback); } else { this._onFinishCallbacks = this._onFinishCallbacks || []; this._onFinishCallbacks.push(callback); } return this; } _didFinishAll(hasCompleted, finishAsyncAnimations, finishNoDurationAnimations) { const children = this._childAnimations; if (children) { for (const child of children) { child._didFinishAll(hasCompleted, finishAsyncAnimations, finishNoDurationAnimations); } } if (finishAsyncAnimations && this._isAsync || finishNoDurationAnimations && !this._isAsync) { this._didFinish(hasCompleted); } } _didFinish(hasCompleted) { this.isPlaying = false; this.hasCompleted = hasCompleted; if (this._onFinishCallbacks) { for (const callback of this._onFinishCallbacks) { callback(this); } } if (this._onFinishOneTimeCallbacks) { for (const callback of this._onFinishOneTimeCallbacks) { callback(this); } this._onFinishOneTimeCallbacks.length = 0; } } reverse(shouldReverse = true) { const children = this._childAnimations; if (children) { for (const child of children) { child.reverse(shouldReverse); } } this._isReverse = !!shouldReverse; return this; } destroy() { this._didFinish(false); this._destroyed = true; const children = this._childAnimations; if (children) { for (const child of children) { child.destroy(); } } this._clearAsync(); if (this._elements) { this._elements.length = 0; } if (this._readCallbacks) { this._readCallbacks.length = 0; } if (this._writeCallbacks) { this._writeCallbacks.length = 0; } this.parent = undefined; if (this._childAnimations) { this._childAnimations.length = 0; } if (this._onFinishCallbacks) { this._onFinishCallbacks.length = 0; } if (this._onFinishOneTimeCallbacks) { this._onFinishOneTimeCallbacks.length = 0; } } _transEl() { const children = this._childAnimations; if (children) { for (const child of children) { const targetEl = child._transEl(); if (targetEl) { return targetEl; } } } return (this._hasTweenEffect && this._hasDur && this._elements !== undefined && this._elements.length > 0 ? this._elements[0] : null); } } function create(animationBuilder, baseEl, opts) { if (animationBuilder) { return animationBuilder(Animator, baseEl, opts); } return Promise.resolve(new Animator()); } export { create };