diff --git a/src/animations/animation.ts b/src/animations/animation.ts index 5313d93ceb..19cbf263e1 100644 --- a/src/animations/animation.ts +++ b/src/animations/animation.ts @@ -1,81 +1,57 @@ -import { CSS, rafFrames, transitionEnd, nativeTimeout } from '../util/dom'; -import { assign, isDefined } from '../util/util'; +import { CSS, nativeRaf, transitionEnd, nativeTimeout } from '../util/dom'; +import { isDefined } from '../util/util'; /** * @private - * - * - play - * - Add before classes - DOM WRITE - * - Remove before classes - DOM WRITE - * - Add before inline styles - DOM WRITE - * - set inline FROM styles - DOM WRITE - * - RAF - * - run before functions that have dom reads - DOM READ - * - run before functions that have dom writes - DOM WRITE - * - set css transition duration/easing - DOM WRITE - * - RAF - * - set inline TO styles - DOM WRITE */ export class Animation { - private _parent: Animation; - private _c: Animation[] = []; - private _el: HTMLElement[] = []; - private _opts: AnimationOptions; - private _fx: {[key: string]: EffectProperty} = {}; + private _c: Animation[]; + private _cL: number; + private _e: HTMLElement[]; + private _eL: number; + private _fx: {[key: string]: EffectProperty}; private _dur: number = null; - private _easing: string = null; - private _bfSty: { [property: string]: any; } = {}; - private _bfAdd: string[] = []; - private _bfRmv: string[] = []; - private _afSty: { [property: string]: any; } = {}; - private _afAdd: string[] = []; - private _afRmv: string[] = []; - private _bfReadFns: Function[] = []; - private _bfWriteFns: Function[] = []; - private _fFns: Function[] = []; - private _fOnceFns: Function[] = []; - private _rv: boolean = false; - private _unregTrans: Function; - private _tmr: number; - private _lastUpd: number = 0; + private _es: string = null; + private _bfSty: { [property: string]: any; }; + private _bfAdd: string[]; + private _bfRm: string[]; + private _afSty: { [property: string]: any; }; + private _afAdd: string[]; + private _afRm: string[]; + private _rdFn: Function[]; + private _wrFn: Function[]; + private _fFn: Function[]; + private _fOneFn: Function[]; + private _rv: boolean; + private _unrgTrns: Function; + private _tm: number; + private _upd: number = 0; + private _hasDur: boolean; + private _isAsync: boolean; + private _twn: boolean; + private _raf: Function; - public isPlaying: boolean = false; - public hasTween: boolean = false; - public hasCompleted: boolean = false; + parent: Animation; + opts: AnimationOptions; + isPlaying: boolean = false; + hasCompleted: boolean = false; - constructor(ele?: any, opts: AnimationOptions = {}) { - this.element(ele); - - this._opts = assign({ - renderDelay: 24 - }, opts); - } - - /** - * NO DOM - */ - _reset() { - this._fx = {}; - this._bfSty = {}; - this._afSty = {}; - - this._el.length = this._c.length = this._bfAdd.length = this._bfRmv.length = this._afAdd.length = this._afRmv.length = this._fFns.length = this._bfReadFns.length = this._bfWriteFns.length = this._fOnceFns.length = 0; - this._easing = this._dur = null; + constructor(ele?: any, opts?: AnimationOptions, raf?: Function) { + this.element(ele).opts = opts; + this._raf = raf || nativeRaf; } element(ele: any): Animation { - var i: number; - if (ele) { if (typeof ele === 'string') { ele = document.querySelectorAll(ele); - for (i = 0; i < ele.length; i++) { + for (var i = 0; i < ele.length; i++) { this._addEle(ele[i]); } } else if (ele.length) { - for (i = 0; i < ele.length; i++) { + for (var i = 0; i < ele.length; i++) { this._addEle(ele[i]); } @@ -95,37 +71,30 @@ export class Animation { ele = ele.nativeElement; } - if (ele.nodeType === 1) { - this._el.push(ele); + if ((ele).nodeType === 1) { + this._eL = (this._e = this._e || []).push(ele); } } /** - * NO DOM - */ - parent(parentAnimation: Animation): Animation { - this._parent = parentAnimation; - return this; - } - - /** - * NO DOM + * Add a child animation to this animation. */ add(childAnimation: Animation): Animation { - childAnimation.parent(this); - this._c.push(childAnimation); + childAnimation.parent = this; + this._cL = (this._c = this._c || []).push(childAnimation); return this; } /** - * NO DOM + * Get the duration of this animation. If this animation does + * not have a duration, then it'll get the duration from its parent. */ - getDuration(): number { - return this._dur !== null ? this._dur : (this._parent && this._parent.getDuration()) || 0; + getDuration(opts?: PlayOptions): number { + return (opts && isDefined(opts.duration) ? opts.duration : this._dur !== null ? this._dur : (this.parent && this.parent.getDuration()) || 0); } /** - * NO DOM + * Set the duration for this animation. */ duration(milliseconds: number): Animation { this._dur = milliseconds; @@ -133,22 +102,23 @@ export class Animation { } /** - * NO DOM + * Get the easing of this animation. If this animation does + * not have an easing, then it'll get the easing from its parent. */ getEasing(): string { - return this._easing !== null ? this._easing : (this._parent && this._parent.getEasing()) || null; + return this._es !== null ? this._es : (this.parent && this.parent.getEasing()) || null; } /** - * NO DOM + * Set the easing for this animation. */ easing(name: string): Animation { - this._easing = name; + this._es = name; return this; } /** - * NO DOM + * Add the "from" value for a specific property. */ from(prop: string, val: any): Animation { this._addProp('from', prop, val); @@ -156,51 +126,47 @@ export class Animation { } /** - * NO DOM + * Add the "to" value for a specific property. */ to(prop: string, val: any, clearProperyAfterTransition?: boolean): Animation { - var fx: EffectProperty = this._addProp('to', prop, val); + const fx: EffectProperty = this._addProp('to', prop, val); if (clearProperyAfterTransition) { // if this effect is a transform then clear the transform effect // otherwise just clear the actual property - this.after.clearStyles([ fx.trans ? CSS.transform : prop]); + this.afterClearStyles([ fx.trans ? CSS.transform : prop]); } return this; } /** - * NO DOM + * Shortcut to add both the "from" and "to" for the same property. */ fromTo(prop: string, fromVal: any, toVal: any, clearProperyAfterTransition?: boolean): Animation { return this.from(prop, fromVal).to(prop, toVal, clearProperyAfterTransition); } /** + * @private * NO DOM */ private _addProp(state: string, prop: string, val: any): EffectProperty { - var fxProp: EffectProperty = this._fx[prop]; + this._fx = this._fx || {}; + let fxProp = this._fx[prop]; if (!fxProp) { // first time we've see this EffectProperty fxProp = this._fx[prop] = { - trans: (typeof TRANSFORMS[prop] !== 'undefined'), - wc: '' + trans: (TRANSFORMS[prop] === 1) }; // add the will-change property for transforms or opacity - if (fxProp.trans) { - fxProp.wc = CSS.transform; - - } else if (prop === 'opacity') { - fxProp.wc = prop; - } + fxProp.wc = (fxProp.trans ? CSS.transform : prop); } // add from/to EffectState to the EffectProperty - var fxState: EffectState = fxProp[state] = { + let fxState: EffectState = (fxProp)[state] = { val: val, num: null, unit: '', @@ -223,86 +189,108 @@ export class Animation { } /** - * NO DOM + * Add CSS class to this animation's elements + * before the animation begins. */ - get before() { - return { - addClass: (className: string): Animation => { - this._bfAdd.push(className); - return this; - }, - removeClass: (className: string): Animation => { - this._bfRmv.push(className); - return this; - }, - setStyles: (styles: { [property: string]: any; }): Animation => { - this._bfSty = styles; - return this; - }, - clearStyles: (propertyNames: string[]): Animation => { - for (var i = 0; i < propertyNames.length; i++) { - this._bfSty[propertyNames[i]] = ''; - } - return this; - }, - addDomReadFn: (domReadFn: Function): Animation => { - this._bfReadFns.push(domReadFn); - return this; - }, - addDomWriteFn: (domWriteFn: Function): Animation => { - this._bfWriteFns.push(domWriteFn); - return this; - } - }; + beforeAddClass(className: string): Animation { + (this._bfAdd = this._bfAdd || []).push(className); + return this; } /** - * NO DOM + * Remove CSS class from this animation's elements + * before the animation begins. */ - get after() { - return { - addClass: (className: string): Animation => { - this._afAdd.push(className); - return this; - }, - removeClass: (className: string): Animation => { - this._afRmv.push(className); - return this; - }, - setStyles: (styles: { [property: string]: any; }): Animation => { - this._afSty = styles; - return this; - }, - clearStyles: (propertyNames: string[]): Animation => { - for (var i = 0; i < propertyNames.length; i++) { - this._afSty[propertyNames[i]] = ''; - } - return this; - } - }; + beforeRemoveClass(className: string): Animation { + (this._bfRm = this._bfRm || []).push(className); + return this; } /** - * DOM WRITE + * Set CSS inline styles to this animation's elements + * before the animation begins. */ - play(opts: PlayOptions = {}) { - var self = this; - var i: number; + beforeStyles(styles: { [property: string]: any; }): Animation { + this._bfSty = styles; + return this; + } - let dur = this._dur; - if (isDefined(opts.duration)) { - dur = opts.duration; + /** + * Clear CSS inline styles from this animation's elements + * before the animation begins. + */ + beforeClearStyles(propertyNames: string[]): Animation { + this._bfSty = this._bfSty || {}; + for (var i = 0; i < propertyNames.length; i++) { + this._bfSty[propertyNames[i]] = ''; } + return this; + } - console.debug('Animation, play, duration', dur, 'easing', this._easing); + /** + * Add a function which contains DOM reads, which will run + * before the animation begins. + */ + beforeAddRead(domReadFn: Function): Animation { + (this._rdFn = this._rdFn || []).push(domReadFn); + return this; + } - // always default that an animation does not tween - // a tween requires that an Animation class has an element - // and that it has at least one FROM/TO effect - // and that the FROM/TO effect can tween numeric values - self.hasTween = false; - self.hasCompleted = false; - self.isPlaying = true; + /** + * Add a function which contains DOM writes, which will run + * before the animation begins. + */ + beforeAddWrite(domWriteFn: Function): Animation { + (this._wrFn = this._wrFn || []).push(domWriteFn); + return this; + } + + /** + * Add CSS class to this animation's elements + * after the animation finishes. + */ + afterAddClass(className: string): Animation { + (this._afAdd = this._afAdd || []).push(className); + return this; + } + + /** + * Remove CSS class from this animation's elements + * after the animation finishes. + */ + afterRemoveClass(className: string): Animation { + (this._afRm = this._afRm || []).push(className); + return this; + } + + /** + * Set CSS inline styles to this animation's elements + * after the animation finishes. + */ + afterStyles(styles: { [property: string]: any; }): Animation { + this._afSty = styles; + return this; + } + + /** + * Clear CSS inline styles from this animation's elements + * after the animation finishes. + */ + afterClearStyles(propertyNames: string[]): Animation { + this._afSty = this._afSty || {}; + for (var i = 0; i < propertyNames.length; i++) { + this._afSty[propertyNames[i]] = ''; + } + return this; + } + + /** + * Play the animation. + */ + play(opts?: PlayOptions) { + const dur = this.getDuration(opts); + + // console.debug('Animation, play, duration', dur, 'easing', this._es); // this is the top level animation and is in full control // of when the async play() should actually kick off @@ -310,250 +298,311 @@ export class Animation { // if there is a duration, then it'll stage all animations at the // FROM property and transition duration, wait a few frames, then // kick off the animation by setting the TO property for each animation - - // ensure all past transition end events have been cleared - self._clearAsync(); - - if (dur > 30) { - // this animation has a duration, so it should animate - // place all the elements with their FROM properties - - // set the FROM properties - // ******** DOM WRITE **************** - self._progress(0); - - // add the will-change or translateZ properties when applicable - // ******** DOM WRITE **************** - self._willChg(true); - - // set the async TRANSITION END event - // and run onFinishes when the transition ends - // ******** DOM WRITE **************** - self._asyncEnd(dur, true); - - // begin each animation when everything is rendered in their place - // and the transition duration/easing is ready to go - rafFrames(self._opts.renderDelay / 16, function() { - // there's been a moment and the elements are in place - - // fire off all the "before" function that have DOM READS in them - // elements will be in the DOM, however visibily hidden - // so we can read their dimensions if need be - // ******** DOM READ **************** - self._beforeReadFn(); - - // ******** DOM READS ABOVE / DOM WRITES BELOW **************** - - // fire off all the "before" function that have DOM WRITES in them - // ******** DOM WRITE **************** - self._beforeWriteFn(); - - // stage all of the before css classes and inline styles - // will recursively stage all child elements - // ******** DOM WRITE **************** - self._before(); - - // now set the TRANSITION duration/easing - // ******** DOM WRITE **************** - self._setTrans(self._dur, false); - - // wait a few moments again to wait for the transition - // info to take hold in the DOM - rafFrames(2, function() { - // browser had some time to render everything in place - // and the transition duration/easing is set - // now set the TO properties - // which will trigger the transition to begin - // ******** DOM WRITE **************** - self._progress(1); - }); - - }); - - } else { - // this animation does not have a duration - // but we still need to apply the styles and wait - // a frame so we can accurately read the dimensions - rafFrames(self._opts.renderDelay / 16, function() { - - // fire off all the "before" function that have DOM READS in them - // elements will be in the DOM, however visibily hidden - // so we can read their dimensions if need be - // ******** DOM READ **************** - self._beforeReadFn(); - - // ******** DOM READS ABOVE / DOM WRITES BELOW **************** - - // fire off all the "before" function that have DOM WRITES in them - // ******** DOM WRITE **************** - self._beforeWriteFn(); - - // ensure before css has ran - // ******** DOM WRITE **************** - self._before(); - - // this animation does not have a duration, so it should not animate - // just go straight to the TO properties and call it done - // ******** DOM WRITE **************** - self._progress(1); - - // since there was no animation, immediately run the after - // ******** DOM WRITE **************** - self._after(); - - // since there was no animation, it's done - // fire off all the onFinishes - // and now you know - self._didFinish(true); - - }); - } - } - - /** - * DOM WRITE - */ - stop(opts: PlayOptions = {}) { - var self = this; - var duration = isDefined(opts.duration) ? opts.duration : 0; - var stepValue = isDefined(opts.stepValue) ? opts.stepValue : 1; + this._isAsync = this._hasDuration(opts); // ensure all past transition end events have been cleared this._clearAsync(); - // set the TO properties - // ******** DOM WRITE **************** - self._progress(stepValue); - - if (duration > 30) { - // this animation has a duration, so it should animate - // place all the elements with their TO properties - - // now set the TRANSITION duration - // ******** DOM WRITE **************** - self._setTrans(duration, true); + // recursively kicks off the correct progress step for each child animation + this._playInit(opts); + if (this._isAsync) { + // for the root animation only // set the async TRANSITION END event // and run onFinishes when the transition ends // ******** DOM WRITE **************** - self._asyncEnd(duration, false); + this._asyncEnd(dur, true); + } + + // doubling up RAFs since this animation was probably triggered + // from an input event, and just having one RAF would have this code + // run within the same frame as the triggering input event, and the + // input event probably already did way too much work for one frame + this._raf(() => { + this._raf(this._playDomInspect.bind(this, opts)); + }); + } + + /** + * @private + * DOM WRITE + * RECURSION + */ + _playInit(opts: PlayOptions) { + // always default that an animation does not tween + // a tween requires that an Animation class has an element + // and that it has at least one FROM/TO effect + // and that the FROM/TO effect can tween numeric values + this._twn = false; + this.isPlaying = true; + this.hasCompleted = false; + this._hasDur = (this.getDuration(opts) > ANIMATION_DURATION_MIN); + + for (var i = 0; i < this._cL; i++) { + // ******** DOM WRITE **************** + this._c[i]._playInit(opts); + } + + if (this._hasDur) { + // if there is a duration then we want to start at step 0 + // ******** DOM WRITE **************** + this._progress(0); + + // add the will-change properties + // ******** DOM WRITE **************** + this._willChg(true); + } + } + + /** + * @private + * DOM WRITE + * NO RECURSION + * ROOT ANIMATION + */ + _playDomInspect(opts: PlayOptions) { + // fire off all the "before" function that have DOM READS in them + // elements will be in the DOM, however visibily hidden + // so we can read their dimensions if need be + // ******** DOM READ **************** + this._beforeReadFn(); + + // ******** DOM READS ABOVE / DOM WRITES BELOW **************** + + // fire off all the "before" function that have DOM WRITES in them + // ******** DOM WRITE **************** + this._beforeWriteFn(); + + // ******** DOM WRITE **************** + this._playProgress(opts); + + if (this._isAsync) { + // this animation has a duration so we need another RAF + // for the CSS TRANSITION properties to kick in + this._raf(this._playToStep.bind(this, 1)); + } + } + + /** + * @private + * DOM WRITE + * RECURSION + */ + _playProgress(opts: PlayOptions) { + for (var i = 0; i < this._cL; i++) { + // ******** DOM WRITE **************** + this._c[i]._playProgress(opts); + } + + // stage all of the before css classes and inline styles + // ******** DOM WRITE **************** + this._before(); + + if (this._hasDur) { + // set the CSS TRANSITION duration/easing + // ******** DOM WRITE **************** + this._setTrans(this.getDuration(opts), false); } else { // this animation does not have a duration, so it should not animate // just go straight to the TO properties and call it done // ******** DOM WRITE **************** - self._after(); + this._progress(1); - // since there was no animation, it's done - // fire off all the onFinishes - self._didFinish(false); + // since there was no animation, immediately run the after + // ******** DOM WRITE **************** + this._after(); + + // this animation has no duration, so it has finished + // other animations could still be running + this._didFinish(true); } } /** + * @private * DOM WRITE + * RECURSION */ - _asyncEnd(duration: number, shouldComplete: boolean) { + _playToStep(stepValue: number) { + for (var i = 0; i < this._cL; i++) { + // ******** DOM WRITE **************** + this._c[i]._playToStep(stepValue); + } + + if (this._hasDur) { + // browser had some time to render everything in place + // and the transition duration/easing is set + // now set the TO properties which will trigger the transition to begin + // ******** DOM WRITE **************** + this._progress(stepValue); + } + } + + /** + * @private + * DOM WRITE + * NO RECURSION + * ROOT ANIMATION + */ + _asyncEnd(dur: number, shouldComplete: boolean) { var self = this; function onTransitionEnd(ev: any) { - console.debug('Animation onTransitionEnd', ev.target.nodeName, ev.propertyName); + // congrats! a successful transition completed! + // console.debug('Animation onTransitionEnd', ev.target.nodeName, ev.propertyName); // ensure transition end events and timeouts have been cleared self._clearAsync(); - // set the after styles // ******** DOM WRITE **************** - self._after(); - - // remove will change properties - // ******** DOM WRITE **************** - self._willChg(false); + self._playEnd(); // transition finished - self._didFinish(shouldComplete); + self._didFinishAll(shouldComplete, true, false); } function onTransitionFallback() { - console.debug('Animation onTransitionFallback'); + console.debug('Animation onTransitionFallback, CSS onTransitionEnd did not fire!'); // oh noz! the transition end event didn't fire in time! // instead the fallback timer when first + // if all goes well this fallback should never fire // clear the other async end events from firing - self._tmr = 0; + self._tm = 0; self._clearAsync(); - // too late to have a smooth animation, just finish it - // ******** DOM WRITE **************** - self._setTrans(0, true); - - // ensure the ending progress step gets rendered - // ******** DOM WRITE **************** - self._progress(1); - // set the after styles // ******** DOM WRITE **************** - self._after(); - - // remove will change properties - // ******** DOM WRITE **************** - self._willChg(false); + self._playEnd(1); // transition finished - self._didFinish(shouldComplete); + self._didFinishAll(shouldComplete, true, false); } // set the TRANSITION END event on one of the transition elements - self._unregTrans = transitionEnd(self._transEl(), onTransitionEnd); + self._unrgTrns = transitionEnd(self._transEl(), onTransitionEnd); // set a fallback timeout if the transition end event never fires, or is too slow // transition end fallback: (animation duration + XXms) - self._tmr = nativeTimeout(onTransitionFallback, duration + 400); + self._tm = nativeTimeout(onTransitionFallback, (dur + TRANSITION_END_FALLBACK_PADDING_MS)); } /** - * NO DOM + * @private + * DOM WRITE + * RECURSION */ - _clearAsync() { - this._unregTrans && this._unregTrans(); - if (this._tmr) { - clearTimeout(this._tmr); - this._tmr = 0; + _playEnd(stepValue?: number) { + for (var i = 0; i < this._cL; i++) { + // ******** DOM WRITE **************** + this._c[i]._playEnd(stepValue); + } + + if (this._hasDur) { + if (isDefined(stepValue)) { + // too late to have a smooth animation, just finish it + // ******** DOM WRITE **************** + this._setTrans(0, true); + + // ensure the ending progress step gets rendered + // ******** DOM WRITE **************** + this._progress(stepValue); + } + + // set the after styles + // ******** DOM WRITE **************** + this._after(); + + // remove the will-change properties + // ******** DOM WRITE **************** + this._willChg(false); } } /** + * @private + * NO DOM + * RECURSION + */ + _hasDuration(opts: PlayOptions) { + if (this.getDuration(opts) > ANIMATION_DURATION_MIN) { + return true; + } + + for (var i = 0; i < this._cL; i++) { + if (this._c[i]._hasDuration(opts)) { + return true; + } + } + + return false; + } + + /** + * @private + * NO DOM + * RECURSION + */ + _hasDomReads() { + if (this._rdFn && this._rdFn.length) { + return true; + } + + for (var i = 0; i < this._cL; i++) { + if (this._c[i]._hasDomReads()) { + return true; + } + } + + return false; + } + + /** + * Immediately stop at the end of the animation. + */ + stop(stepValue: number = 1) { + // ensure all past transition end events have been cleared + this._clearAsync(); + this._hasDur = true; + this._playEnd(stepValue); + } + + /** + * @private + * NO DOM + * NO RECURSION + */ + _clearAsync() { + this._unrgTrns && this._unrgTrns(); + this._tm && clearTimeout(this._tm); + this._tm = this._unrgTrns = undefined; + } + + /** + * @private * DOM WRITE + * NO RECURSION */ _progress(stepValue: number) { // bread 'n butter - var i: number; - var prop: string; - var fx: EffectProperty; var val: any; - var transforms: string[]; - var tweenEffect: boolean; - for (i = 0; i < this._c.length; i++) { - // ******** DOM WRITE **************** - this._c[i]._progress(stepValue); - } - - if (this._el.length) { + if (this._fx && this._eL) { // flip the number if we're going in reverse if (this._rv) { stepValue = ((stepValue * -1) + 1); } - transforms = []; + var transforms: string[] = []; - for (prop in this._fx) { - fx = this._fx[prop]; + for (var prop in this._fx) { + var fx = this._fx[prop]; if (fx.from && fx.to) { - tweenEffect = (fx.from.num !== fx.to.num); + var tweenEffect = (fx.from.num !== fx.to.num); if (tweenEffect) { - this.hasTween = true; + this._twn = true; } if (stepValue === 0) { @@ -577,244 +626,200 @@ export class Animation { transforms.push(prop + '(' + val + ')'); } else { - for (i = 0; i < this._el.length; i++) { + for (var i = 0; i < this._eL; i++) { // ******** DOM WRITE **************** - this._el[i].style[prop] = val; + (this._e[i].style)[prop] = val; } } } } - } // place all transforms on the same property if (transforms.length) { - if (!SUPPORTS_WILL_CHANGE) { - // if the element doesn't support will-change - // then auto add translateZ for transform properties + if (!this._rv && stepValue !== 1 || this._rv && stepValue !== 0) { transforms.push('translateZ(0px)'); } - for (i = 0; i < this._el.length; i++) { + for (var i = 0; i < this._eL; i++) { // ******** DOM WRITE **************** - this._el[i].style[CSS.transform] = transforms.join(' '); + (this._e[i].style)[CSS.transform] = transforms.join(' '); } } - } } /** + * @private * DOM WRITE + * NO RECURSION */ - _setTrans(duration: number, forcedLinearEasing: boolean) { - var i: number; - var easing: string; - + _setTrans(dur: number, forcedLinearEasing: boolean) { // set the TRANSITION properties inline on the element - for (i = 0; i < this._c.length; i++) { - // ******** DOM WRITE **************** - this._c[i]._setTrans(duration, forcedLinearEasing); - } - - if (Object.keys(this._fx).length) { - easing = (forcedLinearEasing ? 'linear' : this.getEasing()); - for (i = 0; i < this._el.length; i++) { - if (duration > 0) { - // all parent/child animations should have the same duration + if (this._fx) { + const easing = (forcedLinearEasing ? 'linear' : this.getEasing()); + for (var i = 0; i < this._eL; i++) { + if (dur > 0) { // ******** DOM WRITE **************** - this._el[i].style[CSS.transition] = ''; - this._el[i].style[CSS.transitionDuration] = duration + 'ms'; + (this._e[i].style)[CSS.transition] = ''; + (this._e[i].style)[CSS.transitionDuration] = dur + 'ms'; // each animation can have a different easing if (easing) { // ******** DOM WRITE **************** - this._el[i].style[CSS.transitionTimingFn] = easing; + (this._e[i].style)[CSS.transitionTimingFn] = easing; } } else { - this._el[i].style[CSS.transition] = 'none'; + (this._e[i].style)[CSS.transition] = 'none'; } } } } /** + * @private * DOM WRITE - */ - _willChg(addWillChange: boolean) { - var i: number; - var wc: string[]; - var prop: string; - - for (i = 0; i < this._c.length; i++) { - // ******** DOM WRITE **************** - this._c[i]._willChg(addWillChange); - } - - if (SUPPORTS_WILL_CHANGE) { - wc = []; - - if (addWillChange) { - for (prop in this._fx) { - if (this._fx[prop].wc !== '') { - if (this._fx[prop].wc === 'webkitTransform') { - wc.push('transform', '-webkit-transform'); - - } else { - wc.push(this._fx[prop].wc); - } - } - } - } - - for (i = 0; i < this._el.length; i++) { - // ******** DOM WRITE **************** - this._el[i].style['willChange'] = wc.join(','); - } - } - } - - /** - * DOM WRITE + * NO RECURSION */ _before() { - // before the RENDER_DELAY // before the animations have started - var i: number; - var j: number; - var prop: string; - var ele: HTMLElement; - - // stage all of the child animations - for (i = 0; i < this._c.length; i++) { - // ******** DOM WRITE **************** - this._c[i]._before(); - } - if (!this._rv) { - for (i = 0; i < this._el.length; i++) { - ele = this._el[i]; + let ele: HTMLElement; + for (var i = 0; i < this._eL; i++) { + ele = this._e[i]; // css classes to add before the animation - for (j = 0; j < this._bfAdd.length; j++) { - // ******** DOM WRITE **************** - ele.classList.add(this._bfAdd[j]); + if (this._bfAdd) { + for (var j = 0; j < this._bfAdd.length; j++) { + // ******** DOM WRITE **************** + ele.classList.add(this._bfAdd[j]); + } } // css classes to remove before the animation - for (j = 0; j < this._bfRmv.length; j++) { - // ******** DOM WRITE **************** - ele.classList.remove(this._bfRmv[j]); + if (this._bfRm) { + for (var j = 0; j < this._bfRm.length; j++) { + // ******** DOM WRITE **************** + ele.classList.remove(this._bfRm[j]); + } } // inline styles to add before the animation - for (prop in this._bfSty) { - // ******** DOM WRITE **************** - ele.style[prop] = this._bfSty[prop]; + if (this._bfSty) { + for (var prop in this._bfSty) { + // ******** DOM WRITE **************** + (ele).style[prop] = this._bfSty[prop]; + } } } } } /** + * @private * DOM READ + * RECURSION */ _beforeReadFn() { - var i: number; - - for (i = 0; i < this._c.length; i++) { + for (var i = 0; i < this._cL; i++) { // ******** DOM READ **************** this._c[i]._beforeReadFn(); } - for (i = 0; i < this._bfReadFns.length; i++) { - // ******** DOM READ **************** - this._bfReadFns[i](); + if (this._rdFn) { + for (var i = 0; i < this._rdFn.length; i++) { + // ******** DOM READ **************** + this._rdFn[i](); + } } } /** + * @private * DOM WRITE + * RECURSION */ _beforeWriteFn() { - var i: number; - - for (i = 0; i < this._c.length; i++) { + for (var i = 0; i < this._cL; i++) { // ******** DOM WRITE **************** this._c[i]._beforeWriteFn(); } - for (i = 0; i < this._bfReadFns.length; i++) { - // ******** DOM WRITE **************** - this._bfWriteFns[i](); + if (this._wrFn) { + for (var i = 0; i < this._wrFn.length; i++) { + // ******** DOM WRITE **************** + this._wrFn[i](); + } } } /** + * @private * DOM WRITE + * NO RECURSION */ _after() { - // after the animations have finished - var i: number; - var j: number; - var prop: string; - var ele: HTMLElement; - - for (i = 0; i < this._c.length; i++) { - // ******** DOM WRITE **************** - this._c[i]._after(); - } - - for (i = 0; i < this._el.length; i++) { - ele = this._el[i]; + let ele: HTMLElement; + for (var i = 0; i < this._eL; i++) { + ele = this._e[i]; // remove the transition duration/easing // ******** DOM WRITE **************** - ele.style[CSS.transitionDuration] = ''; - // ******** DOM WRITE **************** - ele.style[CSS.transitionTimingFn] = ''; + (ele).style[CSS.transitionDuration] = (ele).style[CSS.transitionTimingFn] = ''; if (this._rv) { // finished in reverse direction // css classes that were added before the animation should be removed - for (j = 0; j < this._bfAdd.length; j++) { - // ******** DOM WRITE **************** - ele.classList.remove(this._bfAdd[j]); + if (this._bfAdd) { + for (var j = 0; j < this._bfAdd.length; j++) { + // ******** DOM WRITE **************** + ele.classList.remove(this._bfAdd[j]); + } } // css classes that were removed before the animation should be added - for (j = 0; j < this._bfRmv.length; j++) { - // ******** DOM WRITE **************** - ele.classList.add(this._bfRmv[j]); + if (this._bfRm) { + for (var j = 0; j < this._bfRm.length; j++) { + // ******** DOM WRITE **************** + ele.classList.add(this._bfRm[j]); + } } // inline styles that were added before the animation should be removed - for (prop in this._bfSty) { - // ******** DOM WRITE **************** - ele.style[prop] = ''; + if (this._bfSty) { + for (var prop in this._bfSty) { + // ******** DOM WRITE **************** + (ele).style[prop] = ''; + } } } else { // finished in forward direction // css classes to add after the animation - for (j = 0; j < this._afAdd.length; j++) { - // ******** DOM WRITE **************** - ele.classList.add(this._afAdd[j]); + if (this._afAdd) { + for (var j = 0; j < this._afAdd.length; j++) { + // ******** DOM WRITE **************** + ele.classList.add(this._afAdd[j]); + } } // css classes to remove after the animation - for (j = 0; j < this._afRmv.length; j++) { - // ******** DOM WRITE **************** - ele.classList.remove(this._afRmv[j]); + if (this._afRm) { + for (var j = 0; j < this._afRm.length; j++) { + // ******** DOM WRITE **************** + ele.classList.remove(this._afRm[j]); + } } // inline styles to add after the animation - for (prop in this._afSty) { - // ******** DOM WRITE **************** - ele.style[prop] = this._afSty[prop]; + if (this._afSty) { + for (var prop in this._afSty) { + // ******** DOM WRITE **************** + (ele).style[prop] = this._afSty[prop]; + } } } } @@ -822,38 +827,63 @@ export class Animation { } /** + * @private * DOM WRITE + * NO RECURSION + */ + _willChg(addWillChange: boolean) { + let wc: string[]; + + if (addWillChange) { + wc = []; + for (var prop in this._fx) { + if (this._fx[prop].wc === 'webkitTransform') { + wc.push('transform', '-webkit-transform'); + + } else { + wc.push(this._fx[prop].wc); + } + } + } + + for (var i = 0; i < this._eL; i++) { + // ******** DOM WRITE **************** + (this._e[i]).style.willChange = addWillChange ? wc.join(',') : ''; + } + } + + /** + * Start the animation with a user controlled progress. */ progressStart() { - for (var i = 0; i < this._c.length; i++) { + for (var i = 0; i < this._cL; i++) { // ******** DOM WRITE **************** this._c[i].progressStart(); } - // ******** DOM WRITE **************** - this._willChg(true); - // ******** DOM WRITE **************** this._before(); // force no duration, linear easing // ******** DOM WRITE **************** this._setTrans(0, true); + + this._willChg(true); } /** - * DOM WRITE + * Set the progress step for this animation. */ progressStep(stepValue: number) { - let now = Date.now(); + const now = Date.now(); // only update if the last update was more than 16ms ago - if (now - 16 > this._lastUpd) { - this._lastUpd = now; + if (now - 15 > this._upd) { + this._upd = now; stepValue = Math.min(1, Math.max(0, stepValue)); - for (var i = 0; i < this._c.length; i++) { + for (var i = 0; i < this._cL; i++) { // ******** DOM WRITE **************** this._c[i].progressStep(stepValue); } @@ -870,84 +900,124 @@ export class Animation { } /** - * DOM WRITE + * End the progress animation. */ progressEnd(shouldComplete: boolean, currentStepValue: number) { console.debug('Animation, progressEnd, shouldComplete', shouldComplete, 'currentStepValue', currentStepValue); - for (var i = 0; i < this._c.length; i++) { + this._isAsync = (currentStepValue > 0.05 && currentStepValue < 0.95); + + const dur = 64; + const stepValue = shouldComplete ? 1 : 0; + + this._progressEnd(shouldComplete, stepValue, dur, this._isAsync); + + if (this._isAsync) { + // for the root animation only + // set the async TRANSITION END event + // and run onFinishes when the transition ends // ******** DOM WRITE **************** - this._c[i].progressEnd(shouldComplete, currentStepValue); - } + this._asyncEnd(dur, true); - // set all the animations to their final position - // ******** DOM WRITE **************** - this._progress(shouldComplete ? 1 : 0); - - // if it's already at the final position, or close, then it's done - // otherwise we need to add a transition end event listener - if (currentStepValue < 0.05 || currentStepValue > 0.95) { - // the progress was already left off at the point that is finished - // for example, the left menu was dragged all the way open already - // ******** DOM WRITE **************** - this._after(); - - // ******** DOM WRITE **************** - this._willChg(false); - - this._didFinish(shouldComplete); - - } else { - // the stepValue was left off at a point when it needs to finish transition still - // for example, the left menu was opened 75% and needs to finish opening - // ******** DOM WRITE **************** - this._asyncEnd(64, shouldComplete); - - // force quick duration, linear easing - // ******** DOM WRITE **************** - this._setTrans(64, true); + // this animation has a duration so we need another RAF + // for the CSS TRANSITION properties to kick in + this._raf(this._playToStep.bind(this, stepValue)); } } /** - * POSSIBLE DOM READ/WRITE + * @private + * DOM WRITE + * RECURSION + */ + _progressEnd(shouldComplete: boolean, stepValue: number, dur: number, isAsync: boolean) { + for (var i = 0; i < this._cL; i++) { + // ******** DOM WRITE **************** + this._c[i]._progressEnd(shouldComplete, stepValue, dur, isAsync); + } + + if (!isAsync) { + // stop immediately + // set all the animations to their final position + // ******** DOM WRITE **************** + this._progress(stepValue); + this._willChg(false); + this._after(); + this._didFinish(shouldComplete); + + } else { + // animate it back to it's ending position + this.isPlaying = true; + this.hasCompleted = false; + this._hasDur = true; + // ******** DOM WRITE **************** + this._willChg(true); + this._setTrans(dur, false); + } + } + + /** + * Add a callback to fire when the animation has finished. */ onFinish(callback: Function, onceTimeCallback: boolean = false, clearOnFinishCallacks: boolean = false): Animation { if (clearOnFinishCallacks) { - this._fFns = []; - this._fOnceFns = []; + this._fFn = this._fOneFn = undefined; } if (onceTimeCallback) { - this._fOnceFns.push(callback); + this._fOneFn = this._fOneFn || []; + this._fOneFn.push(callback); } else { - this._fFns.push(callback); + this._fFn = this._fFn || []; + this._fFn.push(callback); } return this; } /** - * POSSIBLE DOM READ/WRITE + * @private + * NO DOM + * RECURSION + */ + _didFinishAll(hasCompleted: boolean, finishAsyncAnimations: boolean, finishNoDurationAnimations: boolean) { + for (var i = 0; i < this._cL; i++) { + this._c[i]._didFinishAll(hasCompleted, finishAsyncAnimations, finishNoDurationAnimations); + } + + if (finishAsyncAnimations && this._isAsync || finishNoDurationAnimations && !this._isAsync) { + this._didFinish(hasCompleted); + } + } + + /** + * @private + * NO RECURSION */ _didFinish(hasCompleted: boolean) { this.isPlaying = false; this.hasCompleted = hasCompleted; - var i: number; - for (i = 0; i < this._fFns.length; i++) { - this._fFns[i](this); + if (this._fFn) { + // run all finish callbacks + for (var i = 0; i < this._fFn.length; i++) { + this._fFn[i](this); + } } - for (i = 0; i < this._fOnceFns.length; i++) { - this._fOnceFns[i](this); + + if (this._fOneFn) { + // run all "onetime" finish callbacks + for (var i = 0; i < this._fOneFn.length; i++) { + this._fOneFn[i](this); + } + this._fOneFn.length = 0; } - this._fOnceFns = []; } /** - * NO DOM + * Reverse the animation. */ reverse(shouldReverse: boolean = true): Animation { - for (var i = 0; i < this._c.length; i++) { + for (var i = 0; i < this._cL; i++) { this._c[i].reverse(shouldReverse); } this._rv = shouldReverse; @@ -955,45 +1025,44 @@ export class Animation { } /** - * DOM WRITE + * Recursively destroy this animation and all child animations. */ - destroy(removeElement?: boolean) { - var i: number; - var ele: HTMLElement; - - for (i = 0; i < this._c.length; i++) { - // ******** DOM WRITE **************** - this._c[i].destroy(removeElement); - } - - if (removeElement) { - for (i = 0; i < this._el.length; i++) { - ele = this._el[i]; - // ******** DOM WRITE **************** - ele.parentNode && ele.parentNode.removeChild(ele); - } + destroy() { + for (var i = 0; i < this._cL; i++) { + this._c[i].destroy(); } this._clearAsync(); - this._reset(); + + this.parent = this._e = this._rdFn = this._wrFn = this._raf = null; + + if (this._c) { + this._c.length = this._cL = 0; + } + if (this._fFn) { + this._fFn.length = 0; + } + if (this._fOneFn) { + this._fOneFn.length = 0; + } } /** + * @private * NO DOM */ _transEl(): HTMLElement { // get the lowest level element that has an Animation - var i: number; var targetEl: HTMLElement; - for (i = 0; i < this._c.length; i++) { + for (var i = 0; i < this._cL; i++) { targetEl = this._c[i]._transEl(); if (targetEl) { return targetEl; } } - return (this.hasTween && this._el.length ? this._el[0] : null); + return (this._twn && this._hasDur && this._eL ? this._e[0] : null); } @@ -1018,35 +1087,35 @@ export class Animation { export interface AnimationOptions { animation?: string; - renderDelay?: number; + duration?: number; + easing?: string; + direction?: string; + isRTL?: boolean; + ev?: any; } export interface PlayOptions { duration?: number; - stepValue?: number; } -interface EffectProperty { +export interface EffectProperty { trans: boolean; - wc: string; + wc?: string; to?: EffectState; from?: EffectState; } -interface EffectState { +export interface EffectState { val: any; num: number; unit: string; } -const TRANSFORMS: any = { - '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 TRANSFORMS: {[key: string]: number} = { + '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 AnimationRegistry: {[key: string]: any} = {}; const CSS_VALUE_REGEX = /(^-?\d*\.?\d*)(.*)/; -const SUPPORTS_WILL_CHANGE = (typeof document.documentElement.style['willChange'] !== 'undefined'); - -let AnimationRegistry: any = {}; +const ANIMATION_DURATION_MIN = 32; +const TRANSITION_END_FALLBACK_PADDING_MS = 400; diff --git a/src/animations/builtins.ts b/src/animations/builtins.ts deleted file mode 100644 index 026e19fa97..0000000000 --- a/src/animations/builtins.ts +++ /dev/null @@ -1,50 +0,0 @@ - -import { Animation } from './animation'; - - -class SlideIn extends Animation { - constructor(element: any) { - super(element); - this - .easing('cubic-bezier(0.1,0.7,0.1,1)') - .duration(400) - .fromTo('translateY', '100%', '0%'); - } -} -Animation.register('slide-in', SlideIn); - - -class SlideOut extends Animation { - constructor(element: any) { - super(element); - this - .easing('ease-out') - .duration(250) - .fromTo('translateY', '0%', '100%'); - } -} -Animation.register('slide-out', SlideOut); - - -class FadeIn extends Animation { - constructor(element: any) { - super(element); - this - .easing('ease-in') - .duration(400) - .fromTo('opacity', 0.001, 1, true); - } -} -Animation.register('fade-in', FadeIn); - - -class FadeOut extends Animation { - constructor(element: any) { - super(element); - this - .easing('ease-out') - .duration(250) - .fromTo('opacity', 0.999, 0); - } -} -Animation.register('fade-out', FadeOut); diff --git a/src/transitions/page-transition.ts b/src/transitions/page-transition.ts index 3377841e95..d978acc006 100644 --- a/src/transitions/page-transition.ts +++ b/src/transitions/page-transition.ts @@ -1,9 +1,6 @@ import { Animation } from '../animations/animation'; -import { closest } from '../util/dom'; import { Content } from '../components/content/content'; -import { Tabs } from '../components/tabs/tabs'; -import { Transition, TransitionOptions } from './transition'; -import { ViewController } from '../components/nav/view-controller'; +import { Transition } from './transition'; /** @@ -12,22 +9,21 @@ import { ViewController } from '../components/nav/view-controller'; export class PageTransition extends Transition { enteringPage: Animation; - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); + init() { + if (this.enteringView) { + this.enteringPage = new Animation(this.enteringView.pageRef()); + this.add(this.enteringPage.beforeAddClass('show-page')); - this.enteringPage = new Animation(this.enteringView.pageRef()); - this.enteringPage.before.addClass('show-page'); - this.add(this.enteringPage); - - this.before.addDomReadFn(this.readDimensions.bind(this)); - this.before.addDomWriteFn(this.writeDimensions.bind(this)); + this.beforeAddRead(this.readDimensions.bind(this)); + this.beforeAddWrite(this.writeDimensions.bind(this)); + } } /** * DOM READ */ readDimensions() { - let content = this.enteringView.getContent(); + const content = this.enteringView.getContent(); if (content && content instanceof Content) { content.readDimensions(); } @@ -37,7 +33,7 @@ export class PageTransition extends Transition { * DOM WRITE */ writeDimensions() { - let content = this.enteringView.getContent(); + const content = this.enteringView.getContent(); if (content && content instanceof Content) { content.writeDimensions(); } @@ -45,11 +41,7 @@ export class PageTransition extends Transition { destroy() { super.destroy(); - this.enteringView = this.enteringPage = null; + this.enteringPage = null; } } - -function parsePxUnit(val: string): number { - return (val.indexOf('px') > 0) ? parseInt(val, 10) : 0; -} diff --git a/src/transitions/transition-controller.ts b/src/transitions/transition-controller.ts new file mode 100644 index 0000000000..c01154e91a --- /dev/null +++ b/src/transitions/transition-controller.ts @@ -0,0 +1,53 @@ +import { AnimationOptions } from '../animations/animation'; +import { isPresent } from '../util/util'; +import { NavControllerBase } from '../navigation/nav-controller-base'; +import { Transition } from './transition'; +import { ViewController } from '../navigation/view-controller'; + +/** + * @private + */ +export class TransitionController { + private _ids = 0; + private _trns: {[key: number]: Transition} = {}; + + getRootTrnsId(nav: NavControllerBase): number { + let parent = nav.parent; + while (parent) { + if (isPresent(parent._trnsId)) { + return parent._trnsId; + } + parent = parent.parent; + } + return null; + } + + nextId() { + return this._ids++; + } + + get(trnsId: number, enteringView: ViewController, leavingView: ViewController, opts: AnimationOptions) { + const trns = Transition.createTransition(opts.animation, enteringView, leavingView, opts); + trns.trnsId = trnsId; + + if (!this._trns[trnsId]) { + // we haven't created the root transition yet + this._trns[trnsId] = trns; + + } else { + // we already have a root transition created + // add this new transition as a child to the root + this._trns[trnsId].add(trns); + } + + return trns; + } + + destroy(trnsId: number) { + if (this._trns[trnsId]) { + this._trns[trnsId].destroy(); + delete this._trns[trnsId]; + } + } + +} diff --git a/src/transitions/transition-ios.ts b/src/transitions/transition-ios.ts index a02563ca7e..bacfe6ffad 100644 --- a/src/transitions/transition-ios.ts +++ b/src/transitions/transition-ios.ts @@ -1,7 +1,6 @@ import { Animation } from '../animations/animation'; +import { isPresent } from '../util/util'; import { PageTransition } from './page-transition'; -import { TransitionOptions } from './transition'; -import { ViewController } from '../components/nav/view-controller'; const DURATION = 500; const EASING = 'cubic-bezier(0.36,0.66,0.04,1)'; @@ -14,117 +13,126 @@ const OFF_OPACITY = 0.8; const SHOW_BACK_BTN_CSS = 'show-back-button'; -class IOSTransition extends PageTransition { +export class IOSTransition extends PageTransition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); + init() { + super.init(); - this.duration(opts.duration || DURATION); - this.easing(opts.easing || EASING); + const enteringView = this.enteringView; + const leavingView = this.leavingView; + const opts = this.opts; - // what direction is the transition going - let backDirection = (opts.direction === 'back'); + this.duration(isPresent(opts.duration) ? opts.duration : DURATION); + this.easing(isPresent(opts.easing) ? opts.easing : EASING); - // do they have navbars? - let enteringHasNavbar = enteringView.hasNavbar(); - let leavingHasNavbar = leavingView && leavingView.hasNavbar(); + const backDirection = (opts.direction === 'back'); + const enteringHasNavbar = (enteringView && enteringView.hasNavbar()); + const leavingHasNavbar = (leavingView && leavingView.hasNavbar()); - // entering content - let enteringContent = new Animation(enteringView.contentRef()); - enteringContent.element(enteringView.toolbarRefs()); - this.add(enteringContent); + if (enteringView) { + // get the native element for the entering page + const enteringPageEle: Element = enteringView.pageRef().nativeElement; - if (backDirection) { - // entering content, back direction - enteringContent - .fromTo(TRANSLATEX, OFF_LEFT, CENTER, true) - .fromTo(OPACITY, OFF_OPACITY, 1, true); + // entering content + const enteringContent = new Animation(enteringView.contentRef()); + enteringContent.element(enteringPageEle.querySelectorAll('ion-header > *:not(ion-navbar),ion-footer > *')); + this.add(enteringContent); - } else { - // entering content, forward direction - enteringContent - .before.clearStyles([OPACITY]) - .fromTo(TRANSLATEX, OFF_RIGHT, CENTER, true); - } - - if (enteringHasNavbar) { - // entering page has a navbar - let enteringNavBar = new Animation(enteringView.navbarRef()); - enteringNavBar.before.addClass('show-navbar'); - this.add(enteringNavBar); - - let enteringTitle = new Animation(enteringView.titleRef()); - let enteringNavbarItems = new Animation(enteringView.navbarItemRefs()); - let enteringNavbarBg = new Animation(enteringView.navbarBgRef()); - let enteringBackButton = new Animation(enteringView.backBtnRef()); - enteringNavBar - .add(enteringTitle) - .add(enteringNavbarItems) - .add(enteringNavbarBg) - .add(enteringBackButton); - - enteringTitle.fromTo(OPACITY, 0.01, 1, true); - enteringNavbarItems.fromTo(OPACITY, 0.01, 1, true); - - // set properties depending on direction if (backDirection) { - // entering navbar, back direction - enteringTitle.fromTo(TRANSLATEX, OFF_LEFT, CENTER, true); - - if (enteringView.enableBack()) { - // back direction, entering page has a back button - enteringBackButton - .before.addClass(SHOW_BACK_BTN_CSS) - .fromTo(OPACITY, 0.01, 1, true); - } + // entering content, back direction + enteringContent + .fromTo(TRANSLATEX, OFF_LEFT, CENTER, true) + .fromTo(OPACITY, OFF_OPACITY, 1, true); } else { - // entering navbar, forward direction - enteringTitle.fromTo(TRANSLATEX, OFF_RIGHT, CENTER, true); + // entering content, forward direction + enteringContent + .beforeClearStyles([OPACITY]) + .fromTo(TRANSLATEX, OFF_RIGHT, CENTER, true); + } - if (leavingHasNavbar) { - // entering navbar, forward direction, and there's a leaving navbar - // should just fade in, no sliding - enteringNavbarBg - .before.clearStyles([TRANSLATEX]) - .fromTo(OPACITY, 0.01, 1, true); + if (enteringHasNavbar) { + // entering page has a navbar + const enteringNavbarEle = enteringPageEle.querySelector('ion-navbar'); + + const enteringNavBar = new Animation(enteringNavbarEle); + this.add(enteringNavBar); + + const enteringTitle = new Animation(enteringNavbarEle.querySelector('ion-title')); + const enteringNavbarItems = new Animation(enteringNavbarEle.querySelectorAll('ion-buttons,[menuToggle]')); + const enteringNavbarBg = new Animation(enteringNavbarEle.querySelector('.toolbar-background')); + const enteringBackButton = new Animation(enteringNavbarEle.querySelector('.back-button')); + enteringNavBar + .add(enteringTitle) + .add(enteringNavbarItems) + .add(enteringNavbarBg) + .add(enteringBackButton); + + enteringTitle.fromTo(OPACITY, 0.01, 1, true); + enteringNavbarItems.fromTo(OPACITY, 0.01, 1, true); + + // set properties depending on direction + if (backDirection) { + // entering navbar, back direction + enteringTitle.fromTo(TRANSLATEX, OFF_LEFT, CENTER, true); + + if (enteringView.enableBack()) { + // back direction, entering page has a back button + enteringBackButton + .beforeAddClass(SHOW_BACK_BTN_CSS) + .fromTo(OPACITY, 0.01, 1, true); + } } else { - // entering navbar, forward direction, and there's no leaving navbar - // should just slide in, no fading in - enteringNavbarBg - .before.clearStyles([OPACITY]) - .fromTo(TRANSLATEX, OFF_RIGHT, CENTER, true); - } + // entering navbar, forward direction + enteringTitle.fromTo(TRANSLATEX, OFF_RIGHT, CENTER, true); + + if (leavingHasNavbar) { + // entering navbar, forward direction, and there's a leaving navbar + // should just fade in, no sliding + enteringNavbarBg + .beforeClearStyles([TRANSLATEX]) + .fromTo(OPACITY, 0.01, 1, true); + + } else { + // entering navbar, forward direction, and there's no leaving navbar + // should just slide in, no fading in + enteringNavbarBg + .beforeClearStyles([OPACITY]) + .fromTo(TRANSLATEX, OFF_RIGHT, CENTER, true); + } - if (enteringView.enableBack()) { - // forward direction, entering page has a back button - enteringBackButton - .before.addClass(SHOW_BACK_BTN_CSS) - .fromTo(OPACITY, 0.01, 1, true); + if (enteringView.enableBack()) { + // forward direction, entering page has a back button + enteringBackButton + .beforeAddClass(SHOW_BACK_BTN_CSS) + .fromTo(OPACITY, 0.01, 1, true); - let enteringBackBtnText = new Animation(enteringView.backBtnTextRef()); - enteringBackBtnText.fromTo(TRANSLATEX, '100px', '0px'); - enteringNavBar.add(enteringBackBtnText); + const enteringBackBtnText = new Animation(enteringNavbarEle.querySelector('.back-button-text')); + enteringBackBtnText.fromTo(TRANSLATEX, '100px', '0px'); + enteringNavBar.add(enteringBackBtnText); - } else { - enteringBackButton.before.removeClass(SHOW_BACK_BTN_CSS); + } else { + enteringBackButton.beforeRemoveClass(SHOW_BACK_BTN_CSS); + } } } } // setup leaving view - if (leavingView) { + if (leavingView && leavingView.pageRef()) { // leaving content - let leavingContent = new Animation(leavingView.contentRef()); - leavingContent.element(leavingView.toolbarRefs()); + const leavingPageEle: Element = leavingView.pageRef().nativeElement; + + const leavingContent = new Animation(leavingView.contentRef()); + leavingContent.element(leavingPageEle.querySelectorAll('ion-header > *:not(ion-navbar),ion-footer > *')); this.add(leavingContent); if (backDirection) { // leaving content, back direction leavingContent - .before.clearStyles([OPACITY]) + .beforeClearStyles([OPACITY]) .fromTo(TRANSLATEX, CENTER, '100%'); } else { @@ -136,16 +144,18 @@ class IOSTransition extends PageTransition { if (leavingHasNavbar) { // leaving page has a navbar - let leavingNavBar = new Animation(leavingView.navbarRef()); - let leavingBackButton = new Animation(leavingView.backBtnRef()); - let leavingTitle = new Animation(leavingView.titleRef()); - let leavingNavbarItems = new Animation(leavingView.navbarItemRefs()); - let leavingNavbarBg = new Animation(leavingView.navbarBgRef()); + const leavingNavbarEle: Element = leavingPageEle.querySelector('ion-navbar'); + + const leavingNavBar = new Animation(leavingNavbarEle); + const leavingTitle = new Animation(leavingNavbarEle.querySelector('ion-title')); + const leavingNavbarItems = new Animation(leavingNavbarEle.querySelectorAll('ion-buttons,[menuToggle]')); + const leavingNavbarBg = new Animation(leavingNavbarEle.querySelector('.toolbar-background')); + const leavingBackButton = new Animation(leavingNavbarEle.querySelector('.back-button')); leavingNavBar - .add(leavingBackButton) .add(leavingTitle) .add(leavingNavbarItems) + .add(leavingBackButton) .add(leavingNavbarBg); this.add(leavingNavBar); @@ -162,18 +172,18 @@ class IOSTransition extends PageTransition { // leaving navbar, back direction, and there's an entering navbar // should just fade out, no sliding leavingNavbarBg - .before.clearStyles([TRANSLATEX]) + .beforeClearStyles([TRANSLATEX]) .fromTo('opacity', 0.99, 0); } else { // leaving navbar, back direction, and there's no entering navbar // should just slide out, no fading out leavingNavbarBg - .before.clearStyles([OPACITY]) + .beforeClearStyles([OPACITY]) .fromTo(TRANSLATEX, CENTER, '100%'); } - let leavingBackBtnText = new Animation(leavingView.backBtnTextRef()); + let leavingBackBtnText = new Animation(leavingNavbarEle.querySelector('.back-button-text')); leavingBackBtnText.fromTo(TRANSLATEX, CENTER, (300) + 'px'); leavingNavBar.add(leavingBackBtnText); @@ -187,5 +197,3 @@ class IOSTransition extends PageTransition { } } - -PageTransition.register('ios-transition', IOSTransition); diff --git a/src/transitions/transition-md.ts b/src/transitions/transition-md.ts index f058ae6b89..caf0ec9e8e 100644 --- a/src/transitions/transition-md.ts +++ b/src/transitions/transition-md.ts @@ -1,7 +1,6 @@ import { Animation } from '../animations/animation'; +import { isPresent } from '../util/util'; import { PageTransition } from './page-transition'; -import { TransitionOptions } from './transition'; -import { ViewController } from '../components/nav/view-controller'; const TRANSLATEY = 'translateY'; const OFF_BOTTOM = '40px'; @@ -9,40 +8,44 @@ const CENTER = '0px'; const SHOW_BACK_BTN_CSS = 'show-back-button'; -class MDTransition extends PageTransition { +export class MDTransition extends PageTransition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); + init() { + super.init(); + + const enteringView = this.enteringView; + const leavingView = this.leavingView; + const opts = this.opts; // what direction is the transition going - let backDirection = (opts.direction === 'back'); + const backDirection = (opts.direction === 'back'); - // do they have navbars? - let enteringHasNavbar = enteringView.hasNavbar(); - let leavingHasNavbar = leavingView && leavingView.hasNavbar(); + if (enteringView) { + if (backDirection) { + this.duration(isPresent(opts.duration) ? opts.duration : 200).easing('cubic-bezier(0.47,0,0.745,0.715)'); + this.enteringPage.beforeClearStyles([TRANSLATEY]); - if (backDirection) { - this.duration(opts.duration || 200).easing('cubic-bezier(0.47,0,0.745,0.715)'); - this.enteringPage.before.clearStyles([TRANSLATEY]); - - } else { - this.duration(opts.duration || 280).easing('cubic-bezier(0.36,0.66,0.04,1)'); - this.enteringPage - .fromTo(TRANSLATEY, OFF_BOTTOM, CENTER, true) - .fromTo('opacity', 0.01, 1, true); - } - - if (enteringHasNavbar) { - let enteringNavBar = new Animation(enteringView.navbarRef()); - enteringNavBar.before.addClass('show-navbar'); - this.add(enteringNavBar); - - let enteringBackButton = new Animation(enteringView.backBtnRef()); - this.add(enteringBackButton); - if (enteringView.enableBack()) { - enteringBackButton.before.addClass(SHOW_BACK_BTN_CSS); } else { - enteringBackButton.before.removeClass(SHOW_BACK_BTN_CSS); + this.duration(isPresent(opts.duration) ? opts.duration : 280).easing('cubic-bezier(0.36,0.66,0.04,1)'); + this.enteringPage + .fromTo(TRANSLATEY, OFF_BOTTOM, CENTER, true) + .fromTo('opacity', 0.01, 1, true); + } + + if (enteringView.hasNavbar()) { + const enteringPageEle: Element = enteringView.pageRef().nativeElement; + const enteringNavbarEle: Element = enteringPageEle.querySelector('ion-navbar'); + + const enteringNavBar = new Animation(enteringNavbarEle); + this.add(enteringNavBar); + + const enteringBackButton = new Animation(enteringNavbarEle.querySelector('.back-button')); + this.add(enteringBackButton); + if (enteringView.enableBack()) { + enteringBackButton.beforeAddClass(SHOW_BACK_BTN_CSS); + } else { + enteringBackButton.beforeRemoveClass(SHOW_BACK_BTN_CSS); + } } } @@ -50,12 +53,10 @@ class MDTransition extends PageTransition { if (leavingView && backDirection) { // leaving content this.duration(opts.duration || 200).easing('cubic-bezier(0.47,0,0.745,0.715)'); - let leavingPage = new Animation(leavingView.pageRef()); + const leavingPage = new Animation(leavingView.pageRef()); this.add(leavingPage.fromTo(TRANSLATEY, CENTER, OFF_BOTTOM).fromTo('opacity', 0.99, 0)); } } } - -PageTransition.register('md-transition', MDTransition); diff --git a/src/transitions/transition-wp.ts b/src/transitions/transition-wp.ts index 50bd98960a..eb1adb0eee 100644 --- a/src/transitions/transition-wp.ts +++ b/src/transitions/transition-wp.ts @@ -1,46 +1,49 @@ import { Animation } from '../animations/animation'; +import { isPresent } from '../util/util'; import { PageTransition } from './page-transition'; -import { TransitionOptions } from './transition'; -import { ViewController } from '../components/nav/view-controller'; const SHOW_BACK_BTN_CSS = 'show-back-button'; const SCALE_SMALL = .95; -class WPTransition extends PageTransition { +export class WPTransition extends PageTransition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); + init() { + super.init(); + + const enteringView = this.enteringView; + const leavingView = this.leavingView; + const opts = this.opts; // what direction is the transition going - let backDirection = (opts.direction === 'back'); + const backDirection = (opts.direction === 'back'); - // do they have navbars? - let enteringHasNavbar = enteringView.hasNavbar(); - let leavingHasNavbar = leavingView && leavingView.hasNavbar(); + if (enteringView) { + if (backDirection) { + this.duration(isPresent(opts.duration) ? opts.duration : 120).easing('cubic-bezier(0.47,0,0.745,0.715)'); + this.enteringPage.beforeClearStyles(['scale']); - if (backDirection) { - this.duration(opts.duration || 120).easing('cubic-bezier(0.47,0,0.745,0.715)'); - this.enteringPage.before.clearStyles(['scale']); - - } else { - this.duration(opts.duration || 280).easing('cubic-bezier(0,0 0.05,1)'); - this.enteringPage - .fromTo('scale', SCALE_SMALL, 1, true) - .fromTo('opacity', 0.01, 1, true); - } - - if (enteringHasNavbar) { - let enteringNavBar = new Animation(enteringView.navbarRef()); - enteringNavBar.before.addClass('show-navbar'); - this.add(enteringNavBar); - - let enteringBackButton = new Animation(enteringView.backBtnRef()); - this.add(enteringBackButton); - if (enteringView.enableBack()) { - enteringBackButton.before.addClass(SHOW_BACK_BTN_CSS); } else { - enteringBackButton.before.removeClass(SHOW_BACK_BTN_CSS); + this.duration(isPresent(opts.duration) ? opts.duration : 280).easing('cubic-bezier(0,0 0.05,1)'); + this.enteringPage + .fromTo('scale', SCALE_SMALL, 1, true) + .fromTo('opacity', 0.01, 1, true); + } + + if (enteringView.hasNavbar()) { + const enteringPageEle: Element = enteringView.pageRef().nativeElement; + const enteringNavbarEle: Element = enteringPageEle.querySelector('ion-navbar'); + + const enteringNavBar = new Animation(enteringNavbarEle); + this.add(enteringNavBar); + + const enteringBackButton = new Animation(enteringNavbarEle.querySelector('.back-button')); + this.add(enteringBackButton); + if (enteringView.enableBack()) { + enteringBackButton.beforeAddClass(SHOW_BACK_BTN_CSS); + } else { + enteringBackButton.beforeRemoveClass(SHOW_BACK_BTN_CSS); + } } } @@ -48,12 +51,10 @@ class WPTransition extends PageTransition { if (leavingView && backDirection) { // leaving content this.duration(opts.duration || 200).easing('cubic-bezier(0.47,0,0.745,0.715)'); - let leavingPage = new Animation(leavingView.pageRef()); + const leavingPage = new Animation(leavingView.pageRef()); this.add(leavingPage.fromTo('scale', 1, SCALE_SMALL).fromTo('opacity', 0.99, 0)); } } } - -PageTransition.register('wp-transition', WPTransition); diff --git a/src/transitions/transition.ts b/src/transitions/transition.ts index 0dbaca3fb1..d0d9d89efe 100644 --- a/src/transitions/transition.ts +++ b/src/transitions/transition.ts @@ -1,8 +1,5 @@ -import { Animation } from '../animations/animation'; -import { closest } from '../util/dom'; -import { Content } from '../components/content/content'; -import { Tabs } from '../components/tabs/tabs'; -import { ViewController } from '../components/nav/view-controller'; +import { Animation, AnimationOptions } from '../animations/animation'; +import { ViewController } from '../navigation/view-controller'; /** @@ -21,15 +18,35 @@ import { ViewController } from '../components/nav/view-controller'; * - set inline TO styles - DOM WRITE */ export class Transition extends Animation { + _trnsStart: Function; - constructor(public enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(null, { - renderDelay: opts.renderDelay - }); + parent: Transition; + hasChildTrns: boolean; + trnsId: number; + + + constructor(public enteringView: ViewController, public leavingView: ViewController, opts: AnimationOptions, raf?: Function) { + super(null, opts, raf); } - static createTransition(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions): Transition { - let TransitionClass = TransitionRegistry[opts.animation]; + init() {} + + registerStart(trnsStart: Function) { + this._trnsStart = trnsStart; + } + + start() { + this._trnsStart && this._trnsStart(); + this._trnsStart = null; + } + + destroy() { + super.destroy(); + this.enteringView = this.leavingView = this._trnsStart = null; + } + + static createTransition(transitionName: string, enteringView: ViewController, leavingView: ViewController, opts: AnimationOptions): Transition { + let TransitionClass: any = TransitionRegistry[transitionName]; if (!TransitionClass) { // didn't find a transition animation, default to ios-transition TransitionClass = TransitionRegistry['ios-transition']; @@ -44,14 +61,4 @@ export class Transition extends Animation { } -export interface TransitionOptions { - animation: string; - duration: number; - easing: string; - direction: string; - renderDelay?: number; - isRTL?: boolean; - ev?: any; -} - -let TransitionRegistry: any = {}; +let TransitionRegistry: {[key: string]: Transition} = {};