diff --git a/core/api.txt b/core/api.txt index 519178c90b..4daf11105d 100644 --- a/core/api.txt +++ b/core/api.txt @@ -663,7 +663,7 @@ ion-menu-controller,method,isAnimating,isAnimating() => Promise ion-menu-controller,method,isEnabled,isEnabled(menu?: string | null | undefined) => Promise ion-menu-controller,method,isOpen,isOpen(menu?: string | null | undefined) => Promise ion-menu-controller,method,open,open(menu?: string | null | undefined) => Promise -ion-menu-controller,method,registerAnimation,registerAnimation(name: string, animation: AnimationBuilder) => Promise +ion-menu-controller,method,registerAnimation,registerAnimation(name: string, animation: AnimationBuilder | ((menu: MenuI) => IonicAnimation)) => Promise ion-menu-controller,method,swipeGesture,swipeGesture(enable: boolean, menu?: string | null | undefined) => Promise ion-menu-controller,method,toggle,toggle(menu?: string | null | undefined) => Promise diff --git a/core/src/components.d.ts b/core/src/components.d.ts index a9a0c4ddde..106229e28a 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -25,12 +25,14 @@ import { HeaderFn, HeaderHeightFn, InputChangeEventDetail, + IonicAnimation, ItemHeightFn, ItemRenderFn, ItemReorderEventDetail, LoadingOptions, MenuChangeEventDetail, MenuControllerI, + MenuI, ModalOptions, NavComponent, NavOptions, @@ -1425,7 +1427,7 @@ export namespace Components { * @param name The name of the animation to register. * @param animation The animation function to register. */ - 'registerAnimation': (name: string, animation: AnimationBuilder) => Promise; + 'registerAnimation': (name: string, animation: AnimationBuilder | ((menu: MenuI) => IonicAnimation)) => Promise; /** * Enable or disable the ability to swipe open the menu. * @param enable If `true`, the menu swipe gesture should be enabled. diff --git a/core/src/components/action-sheet/action-sheet.tsx b/core/src/components/action-sheet/action-sheet.tsx index de07d4b646..1835de4033 100644 --- a/core/src/components/action-sheet/action-sheet.tsx +++ b/core/src/components/action-sheet/action-sheet.tsx @@ -1,7 +1,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, h } from '@stencil/core'; import { getIonMode } from '../../global/ionic-global'; -import { ActionSheetButton, Animation, AnimationBuilder, CssClassMap, OverlayEventDetail, OverlayInterface } from '../../interface'; +import { ActionSheetButton, AnimationBuilder, CssClassMap, OverlayEventDetail, OverlayInterface } from '../../interface'; import { BACKDROP, dismiss, eventMethod, isCancel, present, safeCall } from '../../utils/overlays'; import { getClassMap } from '../../utils/theme'; @@ -24,7 +24,7 @@ import { mdLeaveAnimation } from './animations/md.leave'; export class ActionSheet implements ComponentInterface, OverlayInterface { presented = false; - animation?: Animation; + animation?: any; mode = getIonMode(this); @Element() el!: HTMLElement; diff --git a/core/src/components/action-sheet/animations/ios.enter.ts b/core/src/components/action-sheet/animations/ios.enter.ts index cd132bf64c..2b9f1b6790 100644 --- a/core/src/components/action-sheet/animations/ios.enter.ts +++ b/core/src/components/action-sheet/animations/ios.enter.ts @@ -1,27 +1,25 @@ -import { Animation } from '../../../interface'; +import { IonicAnimation } from '../../../interface'; +import { createAnimation } from '../../../utils/animation/animation'; /** * iOS Action Sheet Enter Animation */ -export const iosEnterAnimation = (AnimationC: Animation, baseEl: HTMLElement): Promise => { - const baseAnimation = new AnimationC(); +export const iosEnterAnimation = (baseEl: HTMLElement): IonicAnimation => { + const baseAnimation = createAnimation(); + const backdropAnimation = createAnimation(); + const wrapperAnimation = createAnimation(); - const backdropAnimation = new AnimationC(); - backdropAnimation.addElement(baseEl.querySelector('ion-backdrop')); + backdropAnimation + .addElement(baseEl.querySelector('ion-backdrop')) + .fromTo('opacity', 0.01, 0.4); - const wrapperAnimation = new AnimationC(); - wrapperAnimation.addElement(baseEl.querySelector('.action-sheet-wrapper')); + wrapperAnimation + .addElement(baseEl.querySelector('.action-sheet-wrapper')) + .fromTo('transform', 'translateY(100%)', 'translateY(0%)'); - backdropAnimation.fromTo('opacity', 0.01, 0.4); - - wrapperAnimation.fromTo('translateY', '100%', '0%'); - - const ani = baseAnimation + return baseAnimation .addElement(baseEl) .easing('cubic-bezier(.36,.66,.04,1)') .duration(400) - .add(backdropAnimation) - .add(wrapperAnimation); - - return Promise.resolve(ani); + .addAnimation([backdropAnimation, wrapperAnimation]); }; diff --git a/core/src/components/action-sheet/animations/ios.leave.ts b/core/src/components/action-sheet/animations/ios.leave.ts index 8fe15082b5..5021833c7c 100644 --- a/core/src/components/action-sheet/animations/ios.leave.ts +++ b/core/src/components/action-sheet/animations/ios.leave.ts @@ -1,27 +1,25 @@ -import { Animation } from '../../../interface'; +import { IonicAnimation } from '../../../interface'; +import { createAnimation } from '../../../utils/animation/animation'; /** * iOS Action Sheet Leave Animation */ -export const iosLeaveAnimation = (AnimationC: Animation, baseEl: HTMLElement): Promise => { - const baseAnimation = new AnimationC(); +export const iosLeaveAnimation = (baseEl: HTMLElement): IonicAnimation => { + const baseAnimation = createAnimation(); + const backdropAnimation = createAnimation(); + const wrapperAnimation = createAnimation(); - const backdropAnimation = new AnimationC(); - backdropAnimation.addElement(baseEl.querySelector('ion-backdrop')); + backdropAnimation + .addElement(baseEl.querySelector('ion-backdrop')) + .fromTo('opacity', 0.4, 0); - const wrapperAnimation = new AnimationC(); - wrapperAnimation.addElement(baseEl.querySelector('.action-sheet-wrapper')); + wrapperAnimation + .addElement(baseEl.querySelector('.action-sheet-wrapper')) + .fromTo('transform', 'translateY(0%)', 'translateY(100%)'); - backdropAnimation.fromTo('opacity', 0.4, 0); - - wrapperAnimation.fromTo('translateY', '0%', '100%'); - - const ani = baseAnimation + return baseAnimation .addElement(baseEl) .easing('cubic-bezier(.36,.66,.04,1)') .duration(450) - .add(backdropAnimation) - .add(wrapperAnimation); - - return Promise.resolve(ani); + .addAnimation([backdropAnimation, wrapperAnimation]); }; diff --git a/core/src/components/action-sheet/animations/md.enter.ts b/core/src/components/action-sheet/animations/md.enter.ts index a11c60c460..517c0598e6 100644 --- a/core/src/components/action-sheet/animations/md.enter.ts +++ b/core/src/components/action-sheet/animations/md.enter.ts @@ -1,27 +1,25 @@ - -import { Animation } from '../../../interface'; +import { IonicAnimation } from '../../../interface'; +import { createAnimation } from '../../../utils/animation/animation'; /** * MD Action Sheet Enter Animation */ -export const mdEnterAnimation = (AnimationC: Animation, baseEl: HTMLElement): Promise => { - const baseAnimation = new AnimationC(); +export const mdEnterAnimation = (baseEl: HTMLElement): IonicAnimation => { + const baseAnimation = createAnimation(); + const backdropAnimation = createAnimation(); + const wrapperAnimation = createAnimation(); - const backdropAnimation = new AnimationC(); - backdropAnimation.addElement(baseEl.querySelector('ion-backdrop')); + backdropAnimation + .addElement(baseEl.querySelector('ion-backdrop')) + .fromTo('opacity', 0.01, 0.32); - const wrapperAnimation = new AnimationC(); - wrapperAnimation.addElement(baseEl.querySelector('.action-sheet-wrapper')); + wrapperAnimation + .addElement(baseEl.querySelector('.action-sheet-wrapper')) + .fromTo('transform', 'translateY(100%)', 'translateY(0%)'); - backdropAnimation.fromTo('opacity', 0.01, 0.32); - wrapperAnimation.fromTo('translateY', '100%', '0%'); - - const ani = baseAnimation + return baseAnimation .addElement(baseEl) .easing('cubic-bezier(.36,.66,.04,1)') .duration(400) - .add(backdropAnimation) - .add(wrapperAnimation); - - return Promise.resolve(ani); + .addAnimation([backdropAnimation, wrapperAnimation]); }; diff --git a/core/src/components/action-sheet/animations/md.leave.ts b/core/src/components/action-sheet/animations/md.leave.ts index a84c5d0031..bc5dcb2404 100644 --- a/core/src/components/action-sheet/animations/md.leave.ts +++ b/core/src/components/action-sheet/animations/md.leave.ts @@ -1,27 +1,25 @@ -import { Animation } from '../../../interface'; +import { IonicAnimation } from '../../../interface'; +import { createAnimation } from '../../../utils/animation/animation'; /** * MD Action Sheet Leave Animation */ -export const mdLeaveAnimation = (AnimationC: Animation, baseEl: HTMLElement): Promise => { +export const mdLeaveAnimation = (baseEl: HTMLElement): IonicAnimation => { + const baseAnimation = createAnimation(); + const backdropAnimation = createAnimation(); + const wrapperAnimation = createAnimation(); - const baseAnimation = new AnimationC(); + backdropAnimation + .addElement(baseEl.querySelector('ion-backdrop')) + .fromTo('opacity', 0.32, 0); - const backdropAnimation = new AnimationC(); - backdropAnimation.addElement(baseEl.querySelector('ion-backdrop')); + wrapperAnimation + .addElement(baseEl.querySelector('.action-sheet-wrapper')) + .fromTo('transform', 'translateY(0%)', 'translateY(100%)'); - const wrapperAnimation = new AnimationC(); - wrapperAnimation.addElement(baseEl.querySelector('.action-sheet-wrapper')); - - backdropAnimation.fromTo('opacity', 0.32, 0); - wrapperAnimation.fromTo('translateY', '0%', '100%'); - - const ani = baseAnimation + return baseAnimation .addElement(baseEl) .easing('cubic-bezier(.36,.66,.04,1)') .duration(450) - .add(backdropAnimation) - .add(wrapperAnimation); - - return Promise.resolve(ani); + .addAnimation([backdropAnimation, wrapperAnimation]); }; diff --git a/core/src/components/alert/animations/ios.enter.ts b/core/src/components/alert/animations/ios.enter.ts index d8eb042bac..5366ab58aa 100644 --- a/core/src/components/alert/animations/ios.enter.ts +++ b/core/src/components/alert/animations/ios.enter.ts @@ -1,27 +1,28 @@ -import { Animation } from '../../../interface'; +import { IonicAnimation } from '../../../interface'; +import { createAnimation } from '../../../utils/animation/animation'; /** * iOS Alert Enter Animation */ -export const iosEnterAnimation = (AnimationC: Animation, baseEl: HTMLElement): Promise => { - const baseAnimation = new AnimationC(); +export const iosEnterAnimation = (baseEl: HTMLElement): IonicAnimation => { + const baseAnimation = createAnimation(); + const backdropAnimation = createAnimation(); + const wrapperAnimation = createAnimation(); - const backdropAnimation = new AnimationC(); - backdropAnimation.addElement(baseEl.querySelector('ion-backdrop')); + backdropAnimation + .addElement(baseEl.querySelector('ion-backdrop')) + .fromTo('opacity', 0.01, 0.3); - const wrapperAnimation = new AnimationC(); - wrapperAnimation.addElement(baseEl.querySelector('.alert-wrapper')); + wrapperAnimation + .addElement(baseEl.querySelector('.alert-wrapper')) + .keyframes([ + { offset: 0, opacity: 0.01, transform: 'scale(1.1)' }, + { offset: 1, opacity: 1, transform: 'scale(1)' } + ]); - backdropAnimation.fromTo('opacity', 0.01, 0.3); - - wrapperAnimation.fromTo('opacity', 0.01, 1).fromTo('scale', 1.1, 1); - - const ani = baseAnimation + return baseAnimation .addElement(baseEl) .easing('ease-in-out') .duration(200) - .add(backdropAnimation) - .add(wrapperAnimation); - - return Promise.resolve(ani); + .addAnimation([backdropAnimation, wrapperAnimation]); }; diff --git a/core/src/components/alert/animations/ios.leave.ts b/core/src/components/alert/animations/ios.leave.ts index 8639222dfd..aa024c7a64 100644 --- a/core/src/components/alert/animations/ios.leave.ts +++ b/core/src/components/alert/animations/ios.leave.ts @@ -1,27 +1,28 @@ -import { Animation } from '../../../interface'; +import { IonicAnimation } from '../../../interface'; +import { createAnimation } from '../../../utils/animation/animation'; /** * iOS Alert Leave Animation */ -export const iosLeaveAnimation = (AnimationC: Animation, baseEl: HTMLElement): Promise => { - const baseAnimation = new AnimationC(); +export const iosLeaveAnimation = (baseEl: HTMLElement): IonicAnimation => { + const baseAnimation = createAnimation(); + const backdropAnimation = createAnimation(); + const wrapperAnimation = createAnimation(); - const backdropAnimation = new AnimationC(); - backdropAnimation.addElement(baseEl.querySelector('ion-backdrop')); + backdropAnimation + .addElement(baseEl.querySelector('ion-backdrop')) + .fromTo('opacity', 0.3, 0); - const wrapperAnimation = new AnimationC(); - wrapperAnimation.addElement(baseEl.querySelector('.alert-wrapper')); + wrapperAnimation + .addElement(baseEl.querySelector('.alert-wrapper')) + .keyframes([ + { offset: 0, opacity: 0.99, transform: 'scale(1)' }, + { offset: 1, opacity: 0, transform: 'scale(0.9)' } + ]); - backdropAnimation.fromTo('opacity', 0.3, 0); - - wrapperAnimation.fromTo('opacity', 0.99, 0).fromTo('scale', 1, 0.9); - - const ani = baseAnimation + return baseAnimation .addElement(baseEl) .easing('ease-in-out') .duration(200) - .add(backdropAnimation) - .add(wrapperAnimation); - - return Promise.resolve(ani); + .addAnimation([backdropAnimation, wrapperAnimation]); }; diff --git a/core/src/components/alert/animations/md.enter.ts b/core/src/components/alert/animations/md.enter.ts index 8de2c382f4..fcf2cdb2df 100644 --- a/core/src/components/alert/animations/md.enter.ts +++ b/core/src/components/alert/animations/md.enter.ts @@ -1,25 +1,28 @@ -import { Animation } from '../../../interface'; +import { IonicAnimation } from '../../../interface'; +import { createAnimation } from '../../../utils/animation/animation'; /** * Md Alert Enter Animation */ -export const mdEnterAnimation = (AnimationC: Animation, baseEl: HTMLElement): Promise => { - const baseAnimation = new AnimationC(); +export const mdEnterAnimation = (baseEl: HTMLElement): IonicAnimation => { + const baseAnimation = createAnimation(); + const backdropAnimation = createAnimation(); + const wrapperAnimation = createAnimation(); - const backdropAnimation = new AnimationC(); - backdropAnimation.addElement(baseEl.querySelector('ion-backdrop')); + backdropAnimation + .addElement(baseEl.querySelector('ion-backdrop')) + .fromTo('opacity', 0.01, 0.32); - const wrapperAnimation = new AnimationC(); - wrapperAnimation.addElement(baseEl.querySelector('.alert-wrapper')); + wrapperAnimation + .addElement(baseEl.querySelector('.alert-wrapper')) + .keyframes([ + { offset: 0, opacity: 0.01, transform: 'scale(0.9)' }, + { offset: 1, opacity: 1, transform: 'scale(1)' } + ]); - backdropAnimation.fromTo('opacity', 0.01, 0.32); - - wrapperAnimation.fromTo('opacity', 0.01, 1).fromTo('scale', 0.9, 1); - - return Promise.resolve(baseAnimation + return baseAnimation .addElement(baseEl) .easing('ease-in-out') .duration(150) - .add(backdropAnimation) - .add(wrapperAnimation)); + .addAnimation([backdropAnimation, wrapperAnimation]); }; diff --git a/core/src/components/alert/animations/md.leave.ts b/core/src/components/alert/animations/md.leave.ts index 8f87c978de..6754ca3ede 100644 --- a/core/src/components/alert/animations/md.leave.ts +++ b/core/src/components/alert/animations/md.leave.ts @@ -1,25 +1,25 @@ -import { Animation } from '../../../interface'; +import { IonicAnimation } from '../../../interface'; +import { createAnimation } from '../../../utils/animation/animation'; /** * Md Alert Leave Animation */ -export const mdLeaveAnimation = (AnimationC: Animation, baseEl: HTMLElement): Promise => { - const baseAnimation = new AnimationC(); +export const mdLeaveAnimation = (baseEl: HTMLElement): IonicAnimation => { + const baseAnimation = createAnimation(); + const backdropAnimation = createAnimation(); + const wrapperAnimation = createAnimation(); - const backdropAnimation = new AnimationC(); - backdropAnimation.addElement(baseEl.querySelector('ion-backdrop')); + backdropAnimation + .addElement(baseEl.querySelector('ion-backdrop')) + .fromTo('opacity', 0.32, 0); - const wrapperAnimation = new AnimationC(); - wrapperAnimation.addElement(baseEl.querySelector('.alert-wrapper')); + wrapperAnimation + .addElement(baseEl.querySelector('.alert-wrapper')) + .fromTo('opacity', 0.99, 0); - backdropAnimation.fromTo('opacity', 0.32, 0); - - wrapperAnimation.fromTo('opacity', 0.99, 0); - - return Promise.resolve(baseAnimation + return baseAnimation .addElement(baseEl) .easing('ease-in-out') .duration(150) - .add(backdropAnimation) - .add(wrapperAnimation)); + .addAnimation([backdropAnimation, wrapperAnimation]); }; diff --git a/core/src/components/loading/animations/ios.enter.ts b/core/src/components/loading/animations/ios.enter.ts index 026effeca5..c5c3114377 100644 --- a/core/src/components/loading/animations/ios.enter.ts +++ b/core/src/components/loading/animations/ios.enter.ts @@ -1,26 +1,28 @@ -import { Animation } from '../../../interface'; +import { IonicAnimation } from '../../../interface'; +import { createAnimation } from '../../../utils/animation/animation'; /** * iOS Loading Enter Animation */ -export const iosEnterAnimation = (AnimationC: Animation, baseEl: HTMLElement): Promise => { - const baseAnimation = new AnimationC(); +export const iosEnterAnimation = (baseEl: HTMLElement): IonicAnimation => { + const baseAnimation = createAnimation(); + const backdropAnimation = createAnimation(); + const wrapperAnimation = createAnimation(); - const backdropAnimation = new AnimationC(); - backdropAnimation.addElement(baseEl.querySelector('ion-backdrop')); + backdropAnimation + .addElement(baseEl.querySelector('ion-backdrop')) + .fromTo('opacity', 0.01, 0.3); - const wrapperAnimation = new AnimationC(); - wrapperAnimation.addElement(baseEl.querySelector('.loading-wrapper')); + wrapperAnimation + .addElement(baseEl.querySelector('.loading-wrapper')) + .keyframes([ + { offset: 0, opacity: 0.01, transform: 'scale(1.1)' }, + { offset: 1, opacity: 1, transform: 'scale(1)' } + ]); - backdropAnimation.fromTo('opacity', 0.01, 0.3); - - wrapperAnimation.fromTo('opacity', 0.01, 1) - .fromTo('scale', 1.1, 1); - - return Promise.resolve(baseAnimation + return baseAnimation .addElement(baseEl) .easing('ease-in-out') .duration(200) - .add(backdropAnimation) - .add(wrapperAnimation)); + .addAnimation([backdropAnimation, wrapperAnimation]); }; diff --git a/core/src/components/loading/animations/ios.leave.ts b/core/src/components/loading/animations/ios.leave.ts index 9ca250e180..c8d22c99b4 100644 --- a/core/src/components/loading/animations/ios.leave.ts +++ b/core/src/components/loading/animations/ios.leave.ts @@ -1,26 +1,28 @@ -import { Animation } from '../../../interface'; +import { IonicAnimation } from '../../../interface'; +import { createAnimation } from '../../../utils/animation/animation'; /** * iOS Loading Leave Animation */ -export const iosLeaveAnimation = (AnimationC: Animation, baseEl: HTMLElement): Promise => { - const baseAnimation = new AnimationC(); +export const iosLeaveAnimation = (baseEl: HTMLElement): IonicAnimation => { + const baseAnimation = createAnimation(); + const backdropAnimation = createAnimation(); + const wrapperAnimation = createAnimation(); - const backdropAnimation = new AnimationC(); - backdropAnimation.addElement(baseEl.querySelector('ion-backdrop')); + backdropAnimation + .addElement(baseEl.querySelector('ion-backdrop')) + .fromTo('opacity', 0.3, 0); - const wrapperAnimation = new AnimationC(); - wrapperAnimation.addElement(baseEl.querySelector('.loading-wrapper')); + wrapperAnimation + .addElement(baseEl.querySelector('.loading-wrapper')) + .keyframes([ + { offset: 0, opacity: 0.99, transform: 'scale(1)' }, + { offset: 1, opacity: 0, transform: 'scale(0.9)' } + ]); - backdropAnimation.fromTo('opacity', 0.3, 0); - - wrapperAnimation.fromTo('opacity', 0.99, 0) - .fromTo('scale', 1, 0.9); - - return Promise.resolve(baseAnimation + return baseAnimation .addElement(baseEl) .easing('ease-in-out') .duration(200) - .add(backdropAnimation) - .add(wrapperAnimation)); + .addAnimation([backdropAnimation, wrapperAnimation]); }; diff --git a/core/src/components/loading/animations/md.enter.ts b/core/src/components/loading/animations/md.enter.ts index bb93b14bb9..6f02750a23 100644 --- a/core/src/components/loading/animations/md.enter.ts +++ b/core/src/components/loading/animations/md.enter.ts @@ -1,25 +1,28 @@ -import { Animation } from '../../../interface'; +import { IonicAnimation } from '../../../interface'; +import { createAnimation } from '../../../utils/animation/animation'; /** * Md Loading Enter Animation */ -export const mdEnterAnimation = (AnimationC: Animation, baseEl: HTMLElement): Promise => { - const baseAnimation = new AnimationC(); +export const mdEnterAnimation = (baseEl: HTMLElement): IonicAnimation => { + const baseAnimation = createAnimation(); + const backdropAnimation = createAnimation(); + const wrapperAnimation = createAnimation(); - const backdropAnimation = new AnimationC(); - backdropAnimation.addElement(baseEl.querySelector('ion-backdrop')); + backdropAnimation + .addElement(baseEl.querySelector('ion-backdrop')) + .fromTo('opacity', 0.01, 0.32); - const wrapperAnimation = new AnimationC(); - wrapperAnimation.addElement(baseEl.querySelector('.loading-wrapper')); + wrapperAnimation + .addElement(baseEl.querySelector('.loading-wrapper')) + .keyframes([ + { offset: 0, opacity: 0.01, transform: 'scale(1.1)' }, + { offset: 1, opacity: 1, transform: 'scale(1)' } + ]); - backdropAnimation.fromTo('opacity', 0.01, 0.32); - - wrapperAnimation.fromTo('opacity', 0.01, 1).fromTo('scale', 1.1, 1); - - return Promise.resolve(baseAnimation + return baseAnimation .addElement(baseEl) .easing('ease-in-out') .duration(200) - .add(backdropAnimation) - .add(wrapperAnimation)); + .addAnimation([backdropAnimation, wrapperAnimation]); }; diff --git a/core/src/components/loading/animations/md.leave.ts b/core/src/components/loading/animations/md.leave.ts index 697dbd1742..fdbe41c85d 100644 --- a/core/src/components/loading/animations/md.leave.ts +++ b/core/src/components/loading/animations/md.leave.ts @@ -1,25 +1,28 @@ -import { Animation } from '../../../interface'; +import { IonicAnimation } from '../../../interface'; +import { createAnimation } from '../../../utils/animation/animation'; /** * Md Loading Leave Animation */ -export const mdLeaveAnimation = (AnimationC: Animation, baseEl: HTMLElement): Promise => { - const baseAnimation = new AnimationC(); +export const mdLeaveAnimation = (baseEl: HTMLElement): IonicAnimation => { + const baseAnimation = createAnimation(); + const backdropAnimation = createAnimation(); + const wrapperAnimation = createAnimation(); - const backdropAnimation = new AnimationC(); - backdropAnimation.addElement(baseEl.querySelector('ion-backdrop')); + backdropAnimation + .addElement(baseEl.querySelector('ion-backdrop')) + .fromTo('opacity', 0.32, 0); - const wrapperAnimation = new AnimationC(); - wrapperAnimation.addElement(baseEl.querySelector('.loading-wrapper')); + wrapperAnimation + .addElement(baseEl.querySelector('.loading-wrapper')) + .keyframes([ + { offset: 0, opacity: 0.99, transform: 'scale(1)' }, + { offset: 1, opacity: 0, transform: 'scale(0.9)' } + ]); - backdropAnimation.fromTo('opacity', 0.32, 0); - - wrapperAnimation.fromTo('opacity', 0.99, 0).fromTo('scale', 1, 0.9); - - return Promise.resolve(baseAnimation + return baseAnimation .addElement(baseEl) .easing('ease-in-out') .duration(200) - .add(backdropAnimation) - .add(wrapperAnimation)); + .addAnimation([backdropAnimation, wrapperAnimation]); }; diff --git a/core/src/components/menu-controller/animations/base.ts b/core/src/components/menu-controller/animations/base.ts index 096a3f4567..b8f90a94a7 100644 --- a/core/src/components/menu-controller/animations/base.ts +++ b/core/src/components/menu-controller/animations/base.ts @@ -1,4 +1,5 @@ -import { Animation } from '../../../interface'; +import { IonicAnimation } from '../../../interface'; +import { createAnimation } from '../../../utils/animation/animation'; /** * baseAnimation @@ -6,16 +7,14 @@ import { Animation } from '../../../interface'; * type will provide their own animations for open and close * and registers itself with Menu. */ -export const baseAnimation = (AnimationC: Animation): Promise => { +export const baseAnimation = (): IonicAnimation => { // https://material.io/guidelines/motion/movement.html#movement-movement-in-out-of-screen-bounds // https://material.io/guidelines/motion/duration-easing.html#duration-easing-natural-easing-curves // "Apply the sharp curve to items temporarily leaving the screen that may return // from the same exit point. When they return, use the deceleration curve. On mobile, // this transition typically occurs over 300ms" -- MD Motion Guide - return Promise.resolve( - new AnimationC() + return createAnimation() .easing('cubic-bezier(0.0, 0.0, 0.2, 1)') // Deceleration curve (Entering the screen) - .easingReverse('cubic-bezier(0.4, 0.0, 0.6, 1)') // Sharp curve (Temporarily leaving the screen) - .duration(300)); + .duration(300); }; diff --git a/core/src/components/menu-controller/animations/overlay.ts b/core/src/components/menu-controller/animations/overlay.ts index bea382ddee..dc8e4ab234 100644 --- a/core/src/components/menu-controller/animations/overlay.ts +++ b/core/src/components/menu-controller/animations/overlay.ts @@ -1,17 +1,22 @@ -import { Animation, MenuI } from '../../../interface'; +import { IonicAnimation, MenuI } from '../../../interface'; +import { createAnimation } from '../../../utils/animation/animation'; import { baseAnimation } from './base'; -const BOX_SHADOW_WIDTH = 8; /** * Menu Overlay Type * The menu slides over the content. The content * itself, which is under the menu, does not move. */ -export const menuOverlayAnimation = (AnimationC: Animation, _: HTMLElement, menu: MenuI): Promise => { +export const menuOverlayAnimation = (menu: MenuI): IonicAnimation => { let closedX: string; let openedX: string; + + const BOX_SHADOW_WIDTH = 8; const width = menu.width + BOX_SHADOW_WIDTH; + const menuAnimation = createAnimation(); + const backdropAnimation = createAnimation(); + if (menu.isEndSide) { // right side closedX = width + 'px'; @@ -23,16 +28,13 @@ export const menuOverlayAnimation = (AnimationC: Animation, _: HTMLElement, menu openedX = '0px'; } - const menuAnimation = new AnimationC() + menuAnimation .addElement(menu.menuInnerEl) - .fromTo('translateX', closedX, openedX); + .fromTo('transform', `translateX(${closedX})`, `translateX(${openedX})`); - const backdropAnimation = new AnimationC() + backdropAnimation .addElement(menu.backdropEl) .fromTo('opacity', 0.01, 0.32); - return baseAnimation(AnimationC).then(animation => { - return animation.add(menuAnimation) - .add(backdropAnimation); - }); + return baseAnimation().addAnimation([menuAnimation, backdropAnimation]); }; diff --git a/core/src/components/menu-controller/animations/push.ts b/core/src/components/menu-controller/animations/push.ts index 4abd3e5244..6dfcd5d21a 100644 --- a/core/src/components/menu-controller/animations/push.ts +++ b/core/src/components/menu-controller/animations/push.ts @@ -1,4 +1,5 @@ -import { Animation, MenuI } from '../../../interface'; +import { IonicAnimation, MenuI } from '../../../interface'; +import { createAnimation } from '../../../utils/animation/animation'; import { baseAnimation } from './base'; @@ -7,10 +8,10 @@ import { baseAnimation } from './base'; * The content slides over to reveal the menu underneath. * The menu itself also slides over to reveal its bad self. */ -export const menuPushAnimation = (AnimationC: Animation, _: HTMLElement, menu: MenuI): Promise => { - +export const menuPushAnimation = (menu: MenuI): IonicAnimation => { let contentOpenedX: string; let menuClosedX: string; + const width = menu.width; if (menu.isEndSide) { @@ -21,21 +22,18 @@ export const menuPushAnimation = (AnimationC: Animation, _: HTMLElement, menu: M contentOpenedX = width + 'px'; menuClosedX = -width + 'px'; } - const menuAnimation = new AnimationC() + + const menuAnimation = createAnimation() .addElement(menu.menuInnerEl) - .fromTo('translateX', menuClosedX, '0px'); + .fromTo('transform', `translateX(${menuClosedX})`, 'translateX(0px)'); - const contentAnimation = new AnimationC() + const contentAnimation = createAnimation() .addElement(menu.contentEl) - .fromTo('translateX', '0px', contentOpenedX); + .fromTo('transform', 'translateX(0px)', `translateX(${contentOpenedX})`); - const backdropAnimation = new AnimationC() + const backdropAnimation = createAnimation() .addElement(menu.backdropEl) .fromTo('opacity', 0.01, 0.32); - return baseAnimation(AnimationC).then(animation => { - return animation.add(menuAnimation) - .add(backdropAnimation) - .add(contentAnimation); - }); + return baseAnimation().addAnimation([menuAnimation, backdropAnimation, contentAnimation]); }; diff --git a/core/src/components/menu-controller/animations/reveal.ts b/core/src/components/menu-controller/animations/reveal.ts index 35de1304a8..493b3d70b3 100644 --- a/core/src/components/menu-controller/animations/reveal.ts +++ b/core/src/components/menu-controller/animations/reveal.ts @@ -1,4 +1,5 @@ -import { Animation, MenuI } from '../../../interface'; +import { IonicAnimation, MenuI } from '../../../interface'; +import { createAnimation } from '../../../utils/animation/animation'; import { baseAnimation } from './base'; @@ -7,14 +8,12 @@ import { baseAnimation } from './base'; * The content slides over to reveal the menu underneath. * The menu itself, which is under the content, does not move. */ -export const menuRevealAnimation = (AnimationC: Animation, _: HTMLElement, menu: MenuI): Promise => { +export const menuRevealAnimation = (menu: MenuI): IonicAnimation => { const openedX = (menu.width * (menu.isEndSide ? -1 : 1)) + 'px'; - const contentOpen = new AnimationC() + const contentOpen = createAnimation() .addElement(menu.contentEl) - .fromTo('translateX', '0px', openedX); + .fromTo('transform', 'translateX(0px)', `translateX(${openedX})`); - return baseAnimation(AnimationC).then(animation => { - return animation.add(contentOpen); - }); + return baseAnimation().addAnimation(contentOpen); }; diff --git a/core/src/components/menu-controller/menu-controller.ts b/core/src/components/menu-controller/menu-controller.ts index 1baeabc98b..21b53c0851 100644 --- a/core/src/components/menu-controller/menu-controller.ts +++ b/core/src/components/menu-controller/menu-controller.ts @@ -1,7 +1,7 @@ import { Build, Component, Method } from '@stencil/core'; import { config } from '../../global/config'; -import { Animation, AnimationBuilder, MenuControllerI, MenuI } from '../../interface'; +import { AnimationBuilder, IonicAnimation, MenuControllerI, MenuI } from '../../interface'; import { menuOverlayAnimation } from './animations/overlay'; import { menuPushAnimation } from './animations/push'; @@ -14,7 +14,7 @@ import { menuRevealAnimation } from './animations/reveal'; export class MenuController implements MenuControllerI { private menus: MenuI[] = []; - private menuAnimations = new Map(); + private menuAnimations = new Map IonicAnimation) | AnimationBuilder>(); constructor() { this.registerAnimation('reveal', menuRevealAnimation); @@ -226,7 +226,7 @@ export class MenuController implements MenuControllerI { * @param animation The animation function to register. */ @Method() - async registerAnimation(name: string, animation: AnimationBuilder) { + async registerAnimation(name: string, animation: AnimationBuilder | ((menu: MenuI) => IonicAnimation)) { this.menuAnimations.set(name, animation); } @@ -278,13 +278,14 @@ export class MenuController implements MenuControllerI { return menu._setOpen(shouldOpen, animated); } - async _createAnimation(type: string, menuCmp: MenuI): Promise { - const animationBuilder = this.menuAnimations.get(type); + async _createAnimation(type: string, menuCmp: MenuI): Promise { + const animationBuilder = this.menuAnimations.get(type) as any; if (!animationBuilder) { throw new Error('animation not registered'); } - const animation = await import('../../utils/animation') - .then(mod => mod.create(animationBuilder, null, menuCmp)); + + const animation = animationBuilder(menuCmp); + if (!config.getBoolean('animated', true)) { animation.duration(0); } diff --git a/core/src/components/menu-controller/readme.md b/core/src/components/menu-controller/readme.md index 351268b9bc..f51a9fe48a 100644 --- a/core/src/components/menu-controller/readme.md +++ b/core/src/components/menu-controller/readme.md @@ -116,7 +116,7 @@ Type: `Promise` -### `registerAnimation(name: string, animation: AnimationBuilder) => Promise` +### `registerAnimation(name: string, animation: AnimationBuilder | ((menu: MenuI) => IonicAnimation)) => Promise` Registers a new animation that can be used with any `ion-menu` by passing the name of the animation in its `type` property. diff --git a/core/src/components/menu/menu.tsx b/core/src/components/menu/menu.tsx index d81dea6bc6..66a37fdef7 100644 --- a/core/src/components/menu/menu.tsx +++ b/core/src/components/menu/menu.tsx @@ -2,7 +2,7 @@ import { Build, Component, ComponentInterface, Element, Event, EventEmitter, Hos import { config } from '../../global/config'; import { getIonMode } from '../../global/ionic-global'; -import { Animation, Gesture, GestureDetail, MenuChangeEventDetail, MenuControllerI, MenuI, Side } from '../../interface'; +import { Gesture, GestureDetail, IonicAnimation, MenuChangeEventDetail, MenuControllerI, MenuI, Side } from '../../interface'; import { GESTURE_CONTROLLER } from '../../utils/gesture'; import { assert, isEndSide as isEnd } from '../../utils/helpers'; @@ -16,7 +16,7 @@ import { assert, isEndSide as isEnd } from '../../utils/helpers'; }) export class Menu implements ComponentInterface, MenuI { - private animation?: Animation; + private animation?: any; private lastOnEnd = 0; private gesture?: Gesture; private blocker = GESTURE_CONTROLLER.createBlocker({ disableScroll: true }); @@ -309,10 +309,15 @@ export class Menu implements ComponentInterface, MenuI { } // Create new animation this.animation = await this.menuCtrl!._createAnimation(this.type!, this); + this.animation.fill('both'); } private async startAnimation(shouldOpen: boolean, animated: boolean): Promise { - const ani = this.animation!.reverse(!shouldOpen); + const isReversed = !shouldOpen; + const ani = (this.animation as IonicAnimation)! + .direction((isReversed) ? 'reverse' : 'normal') + .easing((isReversed) ? 'cubic-bezier(0.4, 0.0, 0.6, 1)' : 'cubic-bezier(0.0, 0.0, 0.2, 1)'); + if (animated) { await ani.playAsync(); } else { @@ -358,7 +363,9 @@ export class Menu implements ComponentInterface, MenuI { } // the cloned animation should not use an easing curve during seek - this.animation.reverse(this._isOpen).progressStart(); + (this.animation as IonicAnimation) + .direction((this._isOpen) ? 'reverse' : 'normal') + .progressStart(true); } private onMove(detail: GestureDetail) { @@ -369,7 +376,10 @@ export class Menu implements ComponentInterface, MenuI { const delta = computeDelta(detail.deltaX, this._isOpen, this.isEndSide); const stepValue = delta / this.width; - this.animation.progressStep(stepValue); + + this.animation + + .progressStep(stepValue); } private onEnd(detail: GestureDetail) { @@ -410,8 +420,7 @@ export class Menu implements ComponentInterface, MenuI { this.lastOnEnd = detail.timeStamp; this.animation .onFinish(() => this.afterAnimation(shouldOpen), { - clearExistingCallbacks: true, - oneTimeCallback: true + oneTime: true }) .progressEnd(shouldComplete, stepValue, realDur); } @@ -492,7 +501,7 @@ export class Menu implements ComponentInterface, MenuI { assert(this._isOpen, 'menu cannot be closed'); this.isAnimating = true; - const ani = this.animation!.reverse(true); + const ani = (this.animation as IonicAnimation)!.direction('reverse'); ani.playSync(); this.afterAnimation(false); } diff --git a/core/src/components/menu/test/basic/index.html b/core/src/components/menu/test/basic/index.html index 8172b9ce05..24f0204476 100644 --- a/core/src/components/menu/test/basic/index.html +++ b/core/src/components/menu/test/basic/index.html @@ -10,7 +10,12 @@ - + diff --git a/core/src/components/modal/animations/ios.enter.ts b/core/src/components/modal/animations/ios.enter.ts index 96bb87dadd..891926849c 100644 --- a/core/src/components/modal/animations/ios.enter.ts +++ b/core/src/components/modal/animations/ios.enter.ts @@ -1,29 +1,29 @@ -import { Animation } from '../../../interface'; +import { IonicAnimation } from '../../../interface'; +import { createAnimation } from '../../../utils/animation/animation'; /** * iOS Modal Enter Animation */ -export const iosEnterAnimation = (AnimationC: Animation, baseEl: HTMLElement): Promise => { - const baseAnimation = new AnimationC(); +export const iosEnterAnimation = (baseEl: HTMLElement): IonicAnimation => { + const baseAnimation = createAnimation(); + const backdropAnimation = createAnimation(); + const wrapperAnimation = createAnimation(); - const backdropAnimation = new AnimationC(); - backdropAnimation.addElement(baseEl.querySelector('ion-backdrop')); + backdropAnimation + .addElement(baseEl.querySelector('ion-backdrop')) + .fromTo('opacity', 0.01, 0.4); - const wrapperAnimation = new AnimationC(); - wrapperAnimation.addElement(baseEl.querySelector('.modal-wrapper')); + wrapperAnimation + .addElement(baseEl.querySelector('.modal-wrapper')) + .beforeStyles({ 'opacity': 1 }) + .fromTo('transform', 'translateY(100%)', 'translateY(0%)'); - wrapperAnimation.beforeStyles({ 'opacity': 1 }) - .fromTo('translateY', '100%', '0%'); - - backdropAnimation.fromTo('opacity', 0.01, 0.4); - - return Promise.resolve(baseAnimation + return baseAnimation .addElement(baseEl) .easing('cubic-bezier(0.36,0.66,0.04,1)') .duration(400) .beforeAddClass('show-modal') - .add(backdropAnimation) - .add(wrapperAnimation)); + .addAnimation([backdropAnimation, wrapperAnimation]); }; /** diff --git a/core/src/components/modal/animations/ios.leave.ts b/core/src/components/modal/animations/ios.leave.ts index 16882a715f..e90b0dcf95 100644 --- a/core/src/components/modal/animations/ios.leave.ts +++ b/core/src/components/modal/animations/ios.leave.ts @@ -1,28 +1,28 @@ -import { Animation } from '../../../interface'; +import { IonicAnimation } from '../../../interface'; +import { createAnimation } from '../../../utils/animation/animation'; /** * iOS Modal Leave Animation */ -export const iosLeaveAnimation = (AnimationC: Animation, baseEl: HTMLElement): Promise => { - const baseAnimation = new AnimationC(); - - const backdropAnimation = new AnimationC(); - backdropAnimation.addElement(baseEl.querySelector('ion-backdrop')); - - const wrapperAnimation = new AnimationC(); +export const iosLeaveAnimation = (baseEl: HTMLElement): IonicAnimation => { + const baseAnimation = createAnimation(); + const backdropAnimation = createAnimation(); + const wrapperAnimation = createAnimation(); const wrapperEl = baseEl.querySelector('.modal-wrapper'); - wrapperAnimation.addElement(wrapperEl); const wrapperElRect = wrapperEl!.getBoundingClientRect(); - wrapperAnimation.beforeStyles({ 'opacity': 1 }) - .fromTo('translateY', '0%', `${(baseEl.ownerDocument as any).defaultView.innerHeight - wrapperElRect.top}px`); + backdropAnimation + .addElement(baseEl.querySelector('ion-backdrop')) + .fromTo('opacity', 0.4, 0.0); - backdropAnimation.fromTo('opacity', 0.4, 0.0); + wrapperAnimation + .addElement(wrapperEl) + .beforeStyles({ 'opacity': 1 }) + .fromTo('transform', 'translateY(0%)', `translateY(${(baseEl.ownerDocument as any).defaultView.innerHeight - wrapperElRect.top}px)`); - return Promise.resolve(baseAnimation + return baseAnimation .addElement(baseEl) .easing('ease-out') .duration(250) - .add(backdropAnimation) - .add(wrapperAnimation)); + .addAnimation([backdropAnimation, wrapperAnimation]); }; diff --git a/core/src/components/modal/animations/md.enter.ts b/core/src/components/modal/animations/md.enter.ts index eca6c4890b..f4c84806ef 100644 --- a/core/src/components/modal/animations/md.enter.ts +++ b/core/src/components/modal/animations/md.enter.ts @@ -1,28 +1,29 @@ -import { Animation } from '../../../interface'; +import { IonicAnimation } from '../../../interface'; +import { createAnimation } from '../../../utils/animation/animation'; /** * Md Modal Enter Animation */ -export const mdEnterAnimation = (AnimationC: Animation, baseEl: HTMLElement): Promise => { - const baseAnimation = new AnimationC(); +export const mdEnterAnimation = (baseEl: HTMLElement): IonicAnimation => { + const baseAnimation = createAnimation(); + const backdropAnimation = createAnimation(); + const wrapperAnimation = createAnimation(); - const backdropAnimation = new AnimationC(); - backdropAnimation.addElement(baseEl.querySelector('ion-backdrop')); - - const wrapperAnimation = new AnimationC(); - wrapperAnimation.addElement(baseEl.querySelector('.modal-wrapper')); + backdropAnimation + .addElement(baseEl.querySelector('ion-backdrop')) + .fromTo('opacity', 0.01, 0.32); wrapperAnimation - .fromTo('opacity', 0.01, 1) - .fromTo('translateY', '40px', '0px'); + .addElement(baseEl.querySelector('.modal-wrapper')) + .keyframes([ + { offset: 0, opacity: 0.01, transform: 'translateY(40px)' }, + { offset: 1, opacity: 1, transform: 'translateY(0px)' } + ]); - backdropAnimation.fromTo('opacity', 0.01, 0.32); - - return Promise.resolve(baseAnimation + return baseAnimation .addElement(baseEl) .easing('cubic-bezier(0.36,0.66,0.04,1)') .duration(280) .beforeAddClass('show-modal') - .add(backdropAnimation) - .add(wrapperAnimation)); + .addAnimation([backdropAnimation, wrapperAnimation]); }; diff --git a/core/src/components/modal/animations/md.leave.ts b/core/src/components/modal/animations/md.leave.ts index 486253b400..92fa5e5445 100644 --- a/core/src/components/modal/animations/md.leave.ts +++ b/core/src/components/modal/animations/md.leave.ts @@ -1,28 +1,29 @@ -import { Animation } from '../../../interface'; +import { IonicAnimation } from '../../../interface'; +import { createAnimation } from '../../../utils/animation/animation'; /** * Md Modal Leave Animation */ -export const mdLeaveAnimation = (AnimationC: Animation, baseEl: HTMLElement): Promise => { - const baseAnimation = new AnimationC(); - - const backdropAnimation = new AnimationC(); - backdropAnimation.addElement(baseEl.querySelector('ion-backdrop')); - - const wrapperAnimation = new AnimationC(); +export const mdLeaveAnimation = (baseEl: HTMLElement): IonicAnimation => { + const baseAnimation = createAnimation(); + const backdropAnimation = createAnimation(); + const wrapperAnimation = createAnimation(); const wrapperEl = baseEl.querySelector('.modal-wrapper'); - wrapperAnimation.addElement(wrapperEl); + + backdropAnimation + .addElement(baseEl.querySelector('ion-backdrop')) + .fromTo('opacity', 0.32, 0.0); wrapperAnimation - .fromTo('opacity', 0.99, 0) - .fromTo('translateY', '0px', '40px'); + .addElement(wrapperEl) + .keyframes([ + { offset: 0, opacity: 0.99, transform: 'translateY(0px)' }, + { offset: 1, opacity: 0, transform: 'translateY(40px)' } + ]); - backdropAnimation.fromTo('opacity', 0.32, 0.0); - - return Promise.resolve(baseAnimation + return baseAnimation .addElement(baseEl) .easing('cubic-bezier(0.47,0,0.745,0.715)') .duration(200) - .add(backdropAnimation) - .add(wrapperAnimation)); + .addAnimation([backdropAnimation, wrapperAnimation]); }; diff --git a/core/src/components/nav/nav.tsx b/core/src/components/nav/nav.tsx index 56ef070bc4..eed8403d1c 100644 --- a/core/src/components/nav/nav.tsx +++ b/core/src/components/nav/nav.tsx @@ -2,7 +2,7 @@ import { Build, Component, Element, Event, EventEmitter, Method, Prop, Watch, h import { config } from '../../global/config'; import { getIonMode } from '../../global/ionic-global'; -import { Animation, AnimationBuilder, ComponentProps, FrameworkDelegate, Gesture, NavComponent, NavOptions, NavOutlet, NavResult, RouteID, RouteWrite, RouterDirection, TransitionDoneFn, TransitionInstruction, ViewController } from '../../interface'; +import { Animation, AnimationBuilder, ComponentProps, FrameworkDelegate, Gesture, IonicAnimation, NavComponent, NavOptions, NavOutlet, NavResult, RouteID, RouteWrite, RouterDirection, TransitionDoneFn, TransitionInstruction, ViewController } from '../../interface'; import { assert } from '../../utils/helpers'; import { TransitionOptions, lifecycle, setPageHidden, transition } from '../../utils/transition'; @@ -17,7 +17,7 @@ import { VIEW_STATE_ATTACHED, VIEW_STATE_DESTROYED, VIEW_STATE_NEW, convertToVie export class Nav implements NavOutlet { private transInstr: TransitionInstruction[] = []; - private sbAni?: Animation; + private sbAni?: Animation | IonicAnimation; private useRouter = false; private isTransitioning = false; private destroyed = false; @@ -823,7 +823,7 @@ export class Nav implements NavOutlet { const opts = ti.opts!; const progressCallback = opts.progressAnimation - ? (ani: Animation | undefined) => this.sbAni = ani + ? (ani: IonicAnimation | Animation | undefined) => this.sbAni = ani : undefined; const mode = getIonMode(this); const enteringEl = enteringView.element!; diff --git a/core/src/components/nav/test/nav-controller.spec.ts b/core/src/components/nav/test/nav-controller.spec.ts index 8156c5a415..695e980620 100644 --- a/core/src/components/nav/test/nav-controller.spec.ts +++ b/core/src/components/nav/test/nav-controller.spec.ts @@ -119,8 +119,9 @@ describe('NavController', () => { mockViews(nav, [view1]); const view2 = mockView(MockView2); + await nav.push(view2, null, null, trnsDone); - + const hasCompleted = true; const requiresTransition = true; expect(trnsDone).toHaveBeenCalledWith( @@ -923,6 +924,27 @@ describe('NavController', () => { const MockView3 = 'mock-view3'; const MockView4 = 'mock-view4'; const MockView5 = 'mock-view5'; + + const mockWebAnimation = (el: HTMLElement) => { + window.Animation = () => {}; + + el.animate = () => { + const animation = { + stop: () => {}, + pause: () => {}, + cancel: () => {}, + onfinish: undefined + } + + animation.play = () => { + if (animation.onfinish) { + animation.onfinish(); + } + } + + return animation; + } + } function mockView(component?: any, params?: ComponentProps) { if (!component) { @@ -931,6 +953,9 @@ describe('NavController', () => { const view = new ViewController(component, params); view.element = document.createElement(component) as HTMLElement; + + mockWebAnimation(view.element); + return view; } diff --git a/core/src/components/picker/animations/ios.enter.ts b/core/src/components/picker/animations/ios.enter.ts index 39fcb3c5e9..1825e1410f 100644 --- a/core/src/components/picker/animations/ios.enter.ts +++ b/core/src/components/picker/animations/ios.enter.ts @@ -1,25 +1,25 @@ -import { Animation } from '../../../interface'; +import { IonicAnimation } from '../../../interface'; +import { createAnimation } from '../../../utils/animation/animation'; /** * iOS Picker Enter Animation */ -export const iosEnterAnimation = (AnimationC: Animation, baseEl: HTMLElement): Promise => { - const baseAnimation = new AnimationC(); +export const iosEnterAnimation = (baseEl: HTMLElement): IonicAnimation => { + const baseAnimation = createAnimation(); + const backdropAnimation = createAnimation(); + const wrapperAnimation = createAnimation(); - const backdropAnimation = new AnimationC(); - backdropAnimation.addElement(baseEl.querySelector('ion-backdrop')); + backdropAnimation + .addElement(baseEl.querySelector('ion-backdrop')) + .fromTo('opacity', 0.01, 0.26); - const wrapperAnimation = new AnimationC(); - wrapperAnimation.addElement(baseEl.querySelector('.picker-wrapper')); + wrapperAnimation + .addElement(baseEl.querySelector('.picker-wrapper')) + .fromTo('transform', 'translateY(100%)', 'translateY(0%)'); - backdropAnimation.fromTo('opacity', 0.01, 0.26); - - wrapperAnimation.fromTo('translateY', '100%', '0%'); - - return Promise.resolve(baseAnimation + return baseAnimation .addElement(baseEl) .easing('cubic-bezier(.36,.66,.04,1)') .duration(400) - .add(backdropAnimation) - .add(wrapperAnimation)); + .addAnimation([backdropAnimation, wrapperAnimation]); }; diff --git a/core/src/components/picker/animations/ios.leave.ts b/core/src/components/picker/animations/ios.leave.ts index 6f8eea7d4b..25e6ce692c 100644 --- a/core/src/components/picker/animations/ios.leave.ts +++ b/core/src/components/picker/animations/ios.leave.ts @@ -1,25 +1,25 @@ -import { Animation } from '../../../interface'; +import { IonicAnimation } from '../../../interface'; +import { createAnimation } from '../../../utils/animation/animation'; /** * iOS Picker Leave Animation */ -export const iosLeaveAnimation = (AnimationC: Animation, baseEl: HTMLElement): Promise => { - const baseAnimation = new AnimationC(); +export const iosLeaveAnimation = (baseEl: HTMLElement): IonicAnimation => { + const baseAnimation = createAnimation(); + const backdropAnimation = createAnimation(); + const wrapperAnimation = createAnimation(); - const backdropAnimation = new AnimationC(); - backdropAnimation.addElement(baseEl.querySelector('ion-backdrop')); + backdropAnimation + .addElement(baseEl.querySelector('ion-backdrop')) + .fromTo('opacity', 0.26, 0.01); - const wrapperAnimation = new AnimationC(); - wrapperAnimation.addElement(baseEl.querySelector('.picker-wrapper')); + wrapperAnimation + .addElement(baseEl.querySelector('.picker-wrapper')) + .fromTo('transform', 'translateY(0%)', 'translateY(100%)'); - backdropAnimation.fromTo('opacity', 0.26, 0.01); - - wrapperAnimation.fromTo('translateY', '0%', '100%'); - - return Promise.resolve(baseAnimation + return baseAnimation .addElement(baseEl) .easing('cubic-bezier(.36,.66,.04,1)') .duration(400) - .add(backdropAnimation) - .add(wrapperAnimation)); + .addAnimation([backdropAnimation, wrapperAnimation]); }; diff --git a/core/src/components/popover/animations/ios.enter.ts b/core/src/components/popover/animations/ios.enter.ts index bb2d7e80b4..de8b538705 100644 --- a/core/src/components/popover/animations/ios.enter.ts +++ b/core/src/components/popover/animations/ios.enter.ts @@ -1,9 +1,10 @@ -import { Animation } from '../../../interface'; +import { IonicAnimation } from '../../../interface'; +import { createAnimation } from '../../../utils/animation/animation'; /** * iOS Popover Enter Animation */ -export const iosEnterAnimation = (AnimationC: Animation, baseEl: HTMLElement, ev?: Event): Promise => { +export const iosEnterAnimation = (baseEl: HTMLElement, ev?: Event): IonicAnimation => { let originY = 'top'; let originX = 'left'; @@ -97,22 +98,23 @@ export const iosEnterAnimation = (AnimationC: Animation, baseEl: HTMLElement, ev contentEl.style.transformOrigin = originY + ' ' + originX; - const baseAnimation = new AnimationC(); + const baseAnimation = createAnimation(); + const backdropAnimation = createAnimation(); + const wrapperAnimation = createAnimation(); - const backdropAnimation = new AnimationC(); - backdropAnimation.addElement(baseEl.querySelector('ion-backdrop')); - backdropAnimation.fromTo('opacity', 0.01, 0.08); + backdropAnimation + .addElement(baseEl.querySelector('ion-backdrop')) + .fromTo('opacity', 0.01, 0.08); - const wrapperAnimation = new AnimationC(); - wrapperAnimation.addElement(baseEl.querySelector('.popover-wrapper')); - wrapperAnimation.fromTo('opacity', 0.01, 1); + wrapperAnimation + .addElement(baseEl.querySelector('.popover-wrapper')) + .fromTo('opacity', 0.01, 1); - return Promise.resolve(baseAnimation + return baseAnimation .addElement(baseEl) .easing('ease') .duration(100) - .add(backdropAnimation) - .add(wrapperAnimation)); + .addAnimation([backdropAnimation, wrapperAnimation]); }; const POPOVER_IOS_BODY_PADDING = 5; diff --git a/core/src/components/popover/animations/ios.leave.ts b/core/src/components/popover/animations/ios.leave.ts index 89ce868689..82674d1049 100644 --- a/core/src/components/popover/animations/ios.leave.ts +++ b/core/src/components/popover/animations/ios.leave.ts @@ -1,24 +1,25 @@ -import { Animation } from '../../../interface'; +import { IonicAnimation } from '../../../interface'; +import { createAnimation } from '../../../utils/animation/animation'; /** * iOS Popover Leave Animation */ -export const iosLeaveAnimation = (AnimationC: Animation, baseEl: HTMLElement): Promise => { - const baseAnimation = new AnimationC(); +export const iosLeaveAnimation = (baseEl: HTMLElement): IonicAnimation => { + const baseAnimation = createAnimation(); + const backdropAnimation = createAnimation(); + const wrapperAnimation = createAnimation(); - const backdropAnimation = new AnimationC(); - backdropAnimation.addElement(baseEl.querySelector('ion-backdrop')); + backdropAnimation + .addElement(baseEl.querySelector('ion-backdrop')) + .fromTo('opacity', 0.08, 0); - const wrapperAnimation = new AnimationC(); - wrapperAnimation.addElement(baseEl.querySelector('.popover-wrapper')); + wrapperAnimation + .addElement(baseEl.querySelector('.popover-wrapper')) + .fromTo('opacity', 0.99, 0); - wrapperAnimation.fromTo('opacity', 0.99, 0); - backdropAnimation.fromTo('opacity', 0.08, 0); - - return Promise.resolve(baseAnimation + return baseAnimation .addElement(baseEl) .easing('ease') .duration(500) - .add(backdropAnimation) - .add(wrapperAnimation)); + .addAnimation([backdropAnimation, wrapperAnimation]); }; diff --git a/core/src/components/popover/animations/md.enter.ts b/core/src/components/popover/animations/md.enter.ts index 0a776b5263..7d8dc5bb83 100644 --- a/core/src/components/popover/animations/md.enter.ts +++ b/core/src/components/popover/animations/md.enter.ts @@ -1,9 +1,11 @@ -import { Animation } from '../../../interface'; +import { IonicAnimation } from '../../../interface'; +import { createAnimation } from '../../../utils/animation/animation'; /** * Md Popover Enter Animation */ -export const mdEnterAnimation = (AnimationC: Animation, baseEl: HTMLElement, ev?: Event): Promise => { +export const mdEnterAnimation = (baseEl: HTMLElement, ev?: Event): IonicAnimation => { + const POPOVER_MD_BODY_PADDING = 12; const doc = (baseEl.ownerDocument as any); const isRTL = doc.dir === 'rtl'; @@ -76,36 +78,36 @@ export const mdEnterAnimation = (AnimationC: Animation, baseEl: HTMLElement, ev? contentEl.style.bottom = POPOVER_MD_BODY_PADDING + 'px'; } - contentEl.style.top = popoverCSS.top + 'px'; - contentEl.style.left = popoverCSS.left + 'px'; - contentEl.style.transformOrigin = originY + ' ' + originX; + const baseAnimation = createAnimation(); + const backdropAnimation = createAnimation(); + const wrapperAnimation = createAnimation(); + const contentAnimation = createAnimation(); + const viewportAnimation = createAnimation(); - const baseAnimation = new AnimationC(); + backdropAnimation + .addElement(baseEl.querySelector('ion-backdrop')) + .fromTo('opacity', 0.01, 0.32); - const backdropAnimation = new AnimationC(); - backdropAnimation.addElement(baseEl.querySelector('ion-backdrop')); - backdropAnimation.fromTo('opacity', 0.01, 0.32); + wrapperAnimation + .addElement(baseEl.querySelector('.popover-wrapper')) + .fromTo('opacity', 0.01, 1); - const wrapperAnimation = new AnimationC(); - wrapperAnimation.addElement(baseEl.querySelector('.popover-wrapper')); - wrapperAnimation.fromTo('opacity', 0.01, 1); + contentAnimation + .addElement(contentEl) + .beforeStyles({ + 'top': `${popoverCSS.top}px`, + 'left': `${popoverCSS.left}px`, + 'transform-origin': `${originY} ${originX}` + }) + .fromTo('transform', 'scale(0.001)', 'scale(1)'); - const contentAnimation = new AnimationC(); - contentAnimation.addElement(baseEl.querySelector('.popover-content')); - contentAnimation.fromTo('scale', 0.001, 1); + viewportAnimation + .addElement(baseEl.querySelector('.popover-viewport')) + .fromTo('opacity', 0.01, 1); - const viewportAnimation = new AnimationC(); - viewportAnimation.addElement(baseEl.querySelector('.popover-viewport')); - viewportAnimation.fromTo('opacity', 0.01, 1); - - return Promise.resolve(baseAnimation + return baseAnimation .addElement(baseEl) .easing('cubic-bezier(0.36,0.66,0.04,1)') .duration(300) - .add(backdropAnimation) - .add(wrapperAnimation) - .add(contentAnimation) - .add(viewportAnimation)); + .addAnimation([backdropAnimation, wrapperAnimation, contentAnimation, viewportAnimation]); }; - -const POPOVER_MD_BODY_PADDING = 12; diff --git a/core/src/components/popover/animations/md.leave.ts b/core/src/components/popover/animations/md.leave.ts index 5c5dc6a65d..c33259a69a 100644 --- a/core/src/components/popover/animations/md.leave.ts +++ b/core/src/components/popover/animations/md.leave.ts @@ -1,24 +1,25 @@ -import { Animation } from '../../../interface'; +import { IonicAnimation } from '../../../interface'; +import { createAnimation } from '../../../utils/animation/animation'; /** * Md Popover Leave Animation */ -export const mdLeaveAnimation = (AnimationC: Animation, baseEl: HTMLElement): Promise => { - const baseAnimation = new AnimationC(); +export const mdLeaveAnimation = (baseEl: HTMLElement): IonicAnimation => { + const baseAnimation = createAnimation(); + const backdropAnimation = createAnimation(); + const wrapperAnimation = createAnimation(); - const backdropAnimation = new AnimationC(); - backdropAnimation.addElement(baseEl.querySelector('ion-backdrop')); + backdropAnimation + .addElement(baseEl.querySelector('ion-backdrop')) + .fromTo('opacity', 0.32, 0); - const wrapperAnimation = new AnimationC(); - wrapperAnimation.addElement(baseEl.querySelector('.popover-wrapper')); + wrapperAnimation + .addElement(baseEl.querySelector('.popover-wrapper')) + .fromTo('opacity', 0.99, 0); - wrapperAnimation.fromTo('opacity', 0.99, 0); - backdropAnimation.fromTo('opacity', 0.32, 0); - - return Promise.resolve(baseAnimation + return baseAnimation .addElement(baseEl) .easing('ease') .duration(500) - .add(backdropAnimation) - .add(wrapperAnimation)); + .addAnimation([backdropAnimation, wrapperAnimation]); }; diff --git a/core/src/components/router-outlet/route-outlet.tsx b/core/src/components/router-outlet/route-outlet.tsx index ac6c78b345..92ffad535a 100644 --- a/core/src/components/router-outlet/route-outlet.tsx +++ b/core/src/components/router-outlet/route-outlet.tsx @@ -2,7 +2,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Method, Pr import { config } from '../../global/config'; import { getIonMode } from '../../global/ionic-global'; -import { Animation, AnimationBuilder, ComponentProps, ComponentRef, FrameworkDelegate, Gesture, NavOutlet, RouteID, RouteWrite, RouterDirection, RouterOutletOptions, SwipeGestureHandler } from '../../interface'; +import { Animation, AnimationBuilder, ComponentProps, ComponentRef, FrameworkDelegate, Gesture, IonicAnimation, NavOutlet, RouteID, RouteWrite, RouterDirection, RouterOutletOptions, SwipeGestureHandler } from '../../interface'; import { attachComponent, detachComponent } from '../../utils/framework-delegate'; import { transition } from '../../utils/transition'; @@ -17,7 +17,7 @@ export class RouterOutlet implements ComponentInterface, NavOutlet { private activeComponent: any; private waitPromise?: Promise; private gesture?: Gesture; - private ani?: Animation; + private ani?: IonicAnimation | Animation; @Element() el!: HTMLElement; diff --git a/core/src/components/toast/animations/ios.enter.ts b/core/src/components/toast/animations/ios.enter.ts index 116fd5b008..3d5e93a06d 100644 --- a/core/src/components/toast/animations/ios.enter.ts +++ b/core/src/components/toast/animations/ios.enter.ts @@ -1,24 +1,24 @@ -import { Animation } from '../../../interface'; +import { IonicAnimation } from '../../../interface'; +import { createAnimation } from '../../../utils/animation/animation'; /** * iOS Toast Enter Animation */ -export const iosEnterAnimation = (AnimationC: Animation, baseEl: ShadowRoot, position: string): Promise => { - const baseAnimation = new AnimationC(); - - const wrapperAnimation = new AnimationC(); +export const iosEnterAnimation = (baseEl: ShadowRoot, position: string): IonicAnimation => { + const baseAnimation = createAnimation(); + const wrapperAnimation = createAnimation(); const hostEl = baseEl.host || baseEl; const wrapperEl = baseEl.querySelector('.toast-wrapper') as HTMLElement; - wrapperAnimation.addElement(wrapperEl); - const bottom = `calc(-10px - var(--ion-safe-area-bottom, 0px))`; const top = `calc(10px + var(--ion-safe-area-top, 0px))`; + wrapperAnimation.addElement(wrapperEl); + switch (position) { case 'top': - wrapperAnimation.fromTo('translateY', '-100%', top); + wrapperAnimation.fromTo('transform', 'translateY(-100%)', `translateY(${top})`); break; case 'middle': const topPosition = Math.floor( @@ -28,12 +28,12 @@ export const iosEnterAnimation = (AnimationC: Animation, baseEl: ShadowRoot, pos wrapperAnimation.fromTo('opacity', 0.01, 1); break; default: - wrapperAnimation.fromTo('translateY', '100%', bottom); + wrapperAnimation.fromTo('transform', 'translateY(100%)', `translateY(${bottom})`); break; } - return Promise.resolve(baseAnimation + return baseAnimation .addElement(hostEl) .easing('cubic-bezier(.155,1.105,.295,1.12)') .duration(400) - .add(wrapperAnimation)); + .addAnimation(wrapperAnimation); }; diff --git a/core/src/components/toast/animations/ios.leave.ts b/core/src/components/toast/animations/ios.leave.ts index db26035b2b..516edadf74 100644 --- a/core/src/components/toast/animations/ios.leave.ts +++ b/core/src/components/toast/animations/ios.leave.ts @@ -1,35 +1,35 @@ -import { Animation } from '../../../interface'; +import { IonicAnimation } from '../../../interface'; +import { createAnimation } from '../../../utils/animation/animation'; /** * iOS Toast Leave Animation */ -export const iosLeaveAnimation = (AnimationC: Animation, baseEl: ShadowRoot, position: string): Promise => { - const baseAnimation = new AnimationC(); - - const wrapperAnimation = new AnimationC(); +export const iosLeaveAnimation = (baseEl: ShadowRoot, position: string): IonicAnimation => { + const baseAnimation = createAnimation(); + const wrapperAnimation = createAnimation(); const hostEl = baseEl.host || baseEl; const wrapperEl = baseEl.querySelector('.toast-wrapper') as HTMLElement; - wrapperAnimation.addElement(wrapperEl); - const bottom = `calc(-10px - var(--ion-safe-area-bottom, 0px))`; const top = `calc(10px + var(--ion-safe-area-top, 0px))`; + wrapperAnimation.addElement(wrapperEl); + switch (position) { case 'top': - wrapperAnimation.fromTo('translateY', top, '-100%'); + wrapperAnimation.fromTo('transform', `translateY(${top})`, 'translateY(-100%)'); break; case 'middle': wrapperAnimation.fromTo('opacity', 0.99, 0); break; default: - wrapperAnimation.fromTo('translateY', bottom, '100%'); + wrapperAnimation.fromTo('transform', `translateY(${bottom})`, 'translateY(100%)'); break; } - return Promise.resolve(baseAnimation + return baseAnimation .addElement(hostEl) .easing('cubic-bezier(.36,.66,.04,1)') .duration(300) - .add(wrapperAnimation)); + .addAnimation(wrapperAnimation); }; diff --git a/core/src/components/toast/animations/md.enter.ts b/core/src/components/toast/animations/md.enter.ts index 00c6f27db8..50210be35a 100644 --- a/core/src/components/toast/animations/md.enter.ts +++ b/core/src/components/toast/animations/md.enter.ts @@ -1,21 +1,21 @@ -import { Animation } from '../../../interface'; +import { IonicAnimation } from '../../../interface'; +import { createAnimation } from '../../../utils/animation/animation'; /** * MD Toast Enter Animation */ -export const mdEnterAnimation = (AnimationC: Animation, baseEl: ShadowRoot, position: string): Promise => { - const baseAnimation = new AnimationC(); - - const wrapperAnimation = new AnimationC(); +export const mdEnterAnimation = (baseEl: ShadowRoot, position: string): IonicAnimation => { + const baseAnimation = createAnimation(); + const wrapperAnimation = createAnimation(); const hostEl = baseEl.host || baseEl; const wrapperEl = baseEl.querySelector('.toast-wrapper') as HTMLElement; - wrapperAnimation.addElement(wrapperEl); - const bottom = `calc(8px + var(--ion-safe-area-bottom, 0px))`; const top = `calc(8px + var(--ion-safe-area-top, 0px))`; + wrapperAnimation.addElement(wrapperEl); + switch (position) { case 'top': wrapperEl.style.top = top; @@ -33,9 +33,9 @@ export const mdEnterAnimation = (AnimationC: Animation, baseEl: ShadowRoot, posi wrapperAnimation.fromTo('opacity', 0.01, 1); break; } - return Promise.resolve(baseAnimation + return baseAnimation .addElement(hostEl) .easing('cubic-bezier(.36,.66,.04,1)') .duration(400) - .add(wrapperAnimation)); + .addAnimation(wrapperAnimation); }; diff --git a/core/src/components/toast/animations/md.leave.ts b/core/src/components/toast/animations/md.leave.ts index da8fa7d586..95f07b48b0 100644 --- a/core/src/components/toast/animations/md.leave.ts +++ b/core/src/components/toast/animations/md.leave.ts @@ -1,23 +1,23 @@ -import { Animation } from '../../../interface'; +import { IonicAnimation } from '../../../interface'; +import { createAnimation } from '../../../utils/animation/animation'; /** * md Toast Leave Animation */ -export const mdLeaveAnimation = (AnimationC: Animation, baseEl: ShadowRoot): Promise => { - const baseAnimation = new AnimationC(); - - const wrapperAnimation = new AnimationC(); +export const mdLeaveAnimation = (baseEl: ShadowRoot): IonicAnimation => { + const baseAnimation = createAnimation(); + const wrapperAnimation = createAnimation(); const hostEl = baseEl.host || baseEl; const wrapperEl = baseEl.querySelector('.toast-wrapper') as HTMLElement; - wrapperAnimation.addElement(wrapperEl); + wrapperAnimation + .addElement(wrapperEl) + .fromTo('opacity', 0.99, 0); - wrapperAnimation.fromTo('opacity', 0.99, 0); - - return Promise.resolve(baseAnimation + return baseAnimation .addElement(hostEl) .easing('cubic-bezier(.36,.66,.04,1)') .duration(300) - .add(wrapperAnimation)); + .addAnimation(wrapperAnimation); }; diff --git a/core/src/index.ts b/core/src/index.ts index 3f3851302d..3b532fae4e 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -1,5 +1,8 @@ import 'ionicons'; +export { createAnimation } from './utils/animation/animation'; +export { createGesture } from './utils/gesture'; + export * from './utils/config'; export * from './components/nav/constants'; export { isPlatform, Platforms, getPlatforms } from './utils/platform'; diff --git a/core/src/interface.d.ts b/core/src/interface.d.ts index 1614a23696..80e4190cb3 100644 --- a/core/src/interface.d.ts +++ b/core/src/interface.d.ts @@ -31,7 +31,8 @@ export * from './components/toggle/toggle-interface'; export * from './components/virtual-scroll/virtual-scroll-interface'; // Types from utils -export * from './utils/animation/animation-interface'; +export { Animation as IonicAnimation } from './utils/animation/animation-interface'; +export * from './utils/animation/old-animation/animation-interface'; export * from './utils/overlays-interface'; export * from './global/config'; export { Gesture, GestureDetail } from './utils/gesture'; diff --git a/core/src/utils/animation/animation-interface.ts b/core/src/utils/animation/animation-interface.ts index eb658cf80b..aa8c86e531 100644 --- a/core/src/utils/animation/animation-interface.ts +++ b/core/src/utils/animation/animation-interface.ts @@ -1,64 +1,71 @@ - -export interface AnimationController { - create(animationBuilder?: AnimationBuilder, baseEl?: any, opts?: any): Promise; -} - export interface Animation { - new (): any; - parent: Animation | undefined; - hasChildren: boolean; - addElement(el: Node | Node[] | NodeList): Animation; - add(childAnimation: Animation): Animation; - duration(milliseconds: number): Animation; - easing(name: string): Animation; - easingReverse(name: string): Animation; - getDuration(opts?: PlayOptions): number; - getEasing(): string; - from(prop: string, val: any): Animation; - to(prop: string, val: any, clearProperyAfterTransition?: boolean): Animation; - fromTo(prop: string, fromVal: any, toVal: any, clearProperyAfterTransition?: boolean): Animation; - beforeAddClass(className: string): Animation; - beforeRemoveClass(className: string): Animation; - beforeStyles(styles: { [property: string]: any; }): Animation; - beforeClearStyles(propertyNames: string[]): Animation; - beforeAddRead(domReadFn: () => void): Animation; - beforeAddWrite(domWriteFn: () => void): Animation; - afterAddClass(className: string): Animation; - afterRemoveClass(className: string): Animation; - afterStyles(styles: { [property: string]: any; }): Animation; + parentAnimation: Animation | undefined; + elements: HTMLElement[]; + childAnimations: Animation[]; + + animationFinish(): void; + play(): Animation; + playAsync(): Promise; + playSync(): Animation; + pause(): Animation; + stop(): Animation; + destroy(): Animation; + + progressStart(forceLinearEasing: boolean): Animation; + progressStep(step: number): Animation; + progressEnd(shouldComplete: boolean, step: number, dur: number | undefined): Animation; + + from(property: string, value: any): Animation; + to(property: string, value: any): Animation; + fromTo(property: string, fromValue: any, toValue: any): Animation; + keyframes(keyframes: any[]): Animation; + + addAnimation(animationToADd: Animation | Animation[] | undefined | null): Animation; + addElement(el: Element | Element[] | Node | Node[] | NodeList | undefined | null): Animation; + iterations(iterations: number): Animation; + fill(fill: AnimationFill | undefined): Animation; + direction(direction: AnimationDirection | undefined): Animation; + duration(duration: number): Animation; + easing(easing: string): Animation; + delay(delay: number): Animation; + parent(animation: Animation): Animation; + update(deep: boolean): Animation; + + getKeyframes(): any[]; + getDirection(): AnimationDirection | undefined; + getFill(): AnimationFill | undefined; + getDelay(): number | undefined; + getIterations(): number | undefined; + getEasing(): string | undefined; + getDuration(): number | undefined; + getWebAnimations(): any[]; + + afterAddRead(readFn: () => void): Animation; + afterAddWrite(writeFn: () => void): Animation; afterClearStyles(propertyNames: string[]): Animation; - play(opts?: PlayOptions): void; - playSync(): void; - playAsync(opts?: PlayOptions): Promise; - reverse(shouldReverse?: boolean): Animation; - stop(stepValue?: number): void; - progressStart(): void; - progressStep(stepValue: number): void; - progressEnd(shouldComplete: boolean, currentStepValue: number, dur: number): void; - onFinish(callback: (animation?: Animation) => void, opts?: {oneTimeCallback?: boolean, clearExistingCallbacks?: boolean}): Animation; - destroy(): void; - isRoot(): boolean; - hasCompleted: boolean; + afterStyles(styles: { [property: string]: any }): Animation; + afterRemoveClass(className: string | string[] | undefined): Animation; + afterAddClass(className: string | string[] | undefined): Animation; + + beforeAddRead(readFn: () => void): Animation; + beforeAddWrite(writeFn: () => void): Animation; + beforeClearStyles(propertyNames: string[]): Animation; + beforeStyles(styles: { [property: string]: any }): Animation; + beforeRemoveClass(className: string | string[] | undefined): Animation; + beforeAddClass(className: string | string[] | undefined): Animation; + + onFinish(callback: any): Animation; + clearOnFinish(): Animation; } -export type AnimationBuilder = (Animation: Animation, baseEl: any, opts?: any) => Promise; - -export interface PlayOptions { - duration?: number; - promise?: boolean; +export interface AnimationOnFinishCallback { + callback: (didComplete: boolean, animation: Animation) => void; + opts: AnimationOnFinishOptions; } -export interface EffectProperty { - effectName: string; - trans: boolean; - wc?: string; - to?: EffectState; - from?: EffectState; - [state: string]: any; +export interface AnimationOnFinishOptions { + oneTime: boolean; } -export interface EffectState { - val: any; - num: number; - effectUnit: string; -} +export type AnimationDirection = 'normal' | 'reverse' | 'alternate' | 'alternate-reverse'; +export type AnimationFill = 'auto' | 'none' | 'forwards' | 'backwards' | 'both'; diff --git a/core/src/utils/animation/animation-utils.ts b/core/src/utils/animation/animation-utils.ts new file mode 100644 index 0000000000..6cf773076a --- /dev/null +++ b/core/src/utils/animation/animation-utils.ts @@ -0,0 +1,95 @@ + +export const setStyleProperty = (element: HTMLElement, propertyName: string, value: string | null) => { + element.style.setProperty(propertyName, value); +}; + +export const removeStyleProperty = (element: HTMLElement, propertyName: string) => { + element.style.removeProperty(propertyName); +}; + +export const animationEnd = (el: HTMLElement | null, callback: (ev?: TransitionEvent) => void) => { + let unRegTrans: (() => void) | undefined; + const opts: any = { passive: true }; + + const unregister = () => { + if (unRegTrans) { + unRegTrans(); + } + }; + + const onTransitionEnd = (ev: Event) => { + if (el === ev.target) { + unregister(); + callback(ev as TransitionEvent); + } + }; + + if (el) { + el.addEventListener('webkitAnimationEnd', onTransitionEnd, opts); + el.addEventListener('animationend', onTransitionEnd, opts); + + unRegTrans = () => { + el.removeEventListener('webkitAnimationEnd', onTransitionEnd, opts); + el.removeEventListener('animationend', onTransitionEnd, opts); + }; + } + + return unregister; +}; + +export const generateKeyframeRules = (keyframes: any[] = []) => { + return keyframes.map(keyframe => { + const offset = keyframe.offset; + + const frameString = []; + for (const property in keyframe) { + if (keyframe.hasOwnProperty(property) && property !== 'offset') { + frameString.push(`${property}: ${keyframe[property]};`); + } + } + + return `${offset * 100}% { ${frameString.join(' ')} }`; + }).join(' '); +}; + +const keyframeIds: string[] = []; + +export const generateKeyframeName = (keyframeRules: string) => { + let index = keyframeIds.indexOf(keyframeRules); + if (index < 0) { + index = (keyframeIds.push(keyframeRules) - 1); + } + return `ion-animation-${index}`; +}; + +export const getStyleContainer = (element: HTMLElement) => { + const rootNode = (element.getRootNode() as any); + return (rootNode.head || rootNode); +}; + +export const createKeyframeStylesheet = (keyframeName: string, keyframeRules: string, element: HTMLElement): HTMLElement => { + const styleContainer = getStyleContainer(element); + + const existingStylesheet = styleContainer.querySelector('#' + keyframeName); + if (existingStylesheet) { + return existingStylesheet; + } + + const stylesheet = (element.ownerDocument || document).createElement('style'); + stylesheet.id = keyframeName; + stylesheet.innerHTML = `@keyframes ${keyframeName} { ${keyframeRules} } @keyframes ${keyframeName}-alt { ${keyframeRules} }`; + + styleContainer.appendChild(stylesheet); + + return stylesheet; +}; + +export const addClassToArray = (classes: string[] = [], className: string | string[] | undefined): string[] => { + if (className !== undefined) { + const classNameToAppend = (Array.isArray(className)) ? className : [className]; + + return [...classes, ...classNameToAppend]; + } + + return classes; +}; diff --git a/core/src/utils/animation/animation.ts b/core/src/utils/animation/animation.ts new file mode 100644 index 0000000000..cb3fe2cfb2 --- /dev/null +++ b/core/src/utils/animation/animation.ts @@ -0,0 +1,1126 @@ +// TODO: Add more tests. until then, be sure to manually test menu and swipe to go back/routing transitions + +import { Animation, AnimationDirection, AnimationFill, AnimationOnFinishCallback, AnimationOnFinishOptions } from './animation-interface'; +import { addClassToArray, animationEnd, createKeyframeStylesheet, generateKeyframeName, generateKeyframeRules, removeStyleProperty, setStyleProperty } from './animation-utils'; + +export const createAnimation = () => { + let _delay: number | undefined; + let _duration: number | undefined; + let _easing: string | undefined; + let _iterations: number | undefined; + let _fill: AnimationFill | undefined; + let _direction: AnimationDirection | undefined; + let _keyframes: any[] = []; + let beforeAddClasses: string[] = []; + let beforeRemoveClasses: string[] = []; + let initialized = false; + let parentAnimation: Animation | undefined; + let beforeStylesValue: { [property: string]: any } = {}; + let afterAddClasses: string[] = []; + let afterRemoveClasses: string[] = []; + let afterStylesValue: { [property: string]: any } = {}; + let numAnimationsRunning = 0; + let shouldForceLinearEasing = false; + let shouldForceSyncPlayback = false; + let cssAnimationsTimerFallback: any; + let forceDirectionValue: AnimationDirection | undefined; + let forceDurationValue: number | undefined; + let forceDelayValue: number | undefined; + let willComplete = true; + let finished = false; + let shouldCalculateNumAnimations = true; + let keyframeName: string | undefined; + let ani: Animation; + + const onFinishCallbacks: AnimationOnFinishCallback[] = []; + const onFinishOneTimeCallbacks: AnimationOnFinishCallback[] = []; + const elements: HTMLElement[] = []; + const childAnimations: Animation[] = []; + const stylesheets: HTMLElement[] = []; + const _beforeAddReadFunctions: any[] = []; + const _beforeAddWriteFunctions: any[] = []; + const _afterAddReadFunctions: any[] = []; + const _afterAddWriteFunctions: any[] = []; + const webAnimations: any[] = []; + const supportsWebAnimations = (typeof (window as any).Animation === 'function'); + const ANIMATION_END_FALLBACK_PADDING_MS = 400; + + /** + * Returns the raw Web Animations object + * for all elements in an Animation. + * This will return an empty array on + * browsers that do not support + * the Web Animations API. + */ + const getWebAnimations = () => { + return webAnimations; + }; + + /** + * Destroy the animation and all child animations. + */ + const destroy = () => { + cleanUp(); + + elements.length = 0; + childAnimations.length = 0; + clearOnFinish(); + + initialized = false; + shouldCalculateNumAnimations = true; + + childAnimations.forEach(childAnimation => { + childAnimation.destroy(); + }); + + return ani; + }; + + /** + * Cancels any Web Animations, removes + * any animation properties from the + * animation's elements, and removes the + * animation's stylesheets from the DOM. + */ + const cleanUp = () => { + cleanUpElements(); + cleanUpStyleSheets(); + }; + + /** + * Add a callback to be run + * upon the animation ending + */ + const onFinish = (callback: any, opts?: AnimationOnFinishOptions) => { + const callbacks = (opts && opts.oneTime) ? onFinishOneTimeCallbacks : onFinishCallbacks; + callbacks.push({ callback, opts } as AnimationOnFinishCallback); + + return ani; + }; + + /** + * Clears all callbacks + */ + const clearOnFinish = () => { + onFinishCallbacks.length = 0; + onFinishOneTimeCallbacks.length = 0; + + return ani; + }; + + /** + * Cancels any Web Animations and removes + * any animation properties from the + * the animation's elements. + */ + const cleanUpElements = () => { + if (supportsWebAnimations) { + getWebAnimations().forEach(animation => { + animation.cancel(); + }); + + webAnimations.length = 0; + } else { + elements.forEach(element => { + removeStyleProperty(element, 'animation-name'); + removeStyleProperty(element, 'animation-duration'); + removeStyleProperty(element, 'animation-timing-function'); + removeStyleProperty(element, 'animation-iteration-count'); + removeStyleProperty(element, 'animation-delay'); + removeStyleProperty(element, 'animation-play-state'); + removeStyleProperty(element, 'animation-fill-mode'); + removeStyleProperty(element, 'animation-direction'); + }); + } + }; + + /** + * Removes the animation's stylesheets + * from the DOM. + */ + const cleanUpStyleSheets = () => { + stylesheets.forEach(stylesheet => { + stylesheet.parentNode!.removeChild(stylesheet); + }); + + stylesheets.length = 0; + }; + + /** + * Add a function that performs a + * DOM read to be run before the + * animation starts + */ + const beforeAddRead = (readFn: () => void) => { + _beforeAddReadFunctions.push(readFn); + + return ani; + }; + + /** + * Add a function that performs a + * DOM write to be run before the + * animation starts + */ + const beforeAddWrite = (writeFn: () => void) => { + _beforeAddWriteFunctions.push(writeFn); + + return ani; + }; + + /** + * Add a function that performs a + * DOM read to be run after the + * animation end + */ + const afterAddRead = (readFn: () => void) => { + _afterAddReadFunctions.push(readFn); + + return ani; + }; + + /** + * Add a function that performs a + * DOM write to be run after the + * animation end + */ + const afterAddWrite = (writeFn: () => void) => { + _afterAddWriteFunctions.push(writeFn); + + return ani; + }; + + /** + * Add a class to the animation's + * elements before the animation starts + */ + const beforeAddClass = (className: string | string[] | undefined) => { + beforeAddClasses = addClassToArray(beforeAddClasses, className); + + return ani; + }; + + /** + * Remove a class from the animation's + * elements before the animation starts + */ + const beforeRemoveClass = (className: string | string[] | undefined) => { + beforeRemoveClasses = addClassToArray(beforeRemoveClasses, className); + + return ani; + }; + + /** + * Set CSS inline styles to the animation's + * elements before the animation begins. + */ + const beforeStyles = (styles: { [property: string]: any } = {}) => { + beforeStylesValue = styles; + return ani; + }; + + /** + * Clear CSS inline styles from the animation's + * elements before the animation begins. + */ + const beforeClearStyles = (propertyNames: string[] = []) => { + for (const property of propertyNames) { + beforeStylesValue[property] = ''; + } + + return ani; + }; + + /** + * Add CSS class to the animation's + * elements after the animation ends. + */ + const afterAddClass = (className: string | string[] | undefined) => { + afterAddClasses = addClassToArray(afterAddClasses, className); + + return ani; + }; + + /** + * Remove CSS class from the animation's + * elements after the animation ends. + */ + const afterRemoveClass = (className: string | string[] | undefined) => { + afterRemoveClasses = addClassToArray(afterRemoveClasses, className); + + return ani; + }; + + /** + * Set CSS inline styles to the animation's + * elements after the animation ends. + */ + const afterStyles = (styles: { [property: string]: any } = {}) => { + afterStylesValue = styles; + + return ani; + }; + + /** + * Clear CSS inline styles from the animation's + * elements after the animation ends. + */ + const afterClearStyles = (propertyNames: string[] = []) => { + for (const property of propertyNames) { + afterStylesValue[property] = ''; + } + + return ani; + }; + + /** + * Returns the animation's fill mode. + */ + const getFill = () => { + if (_fill !== undefined) { return _fill; } + if (parentAnimation) { return parentAnimation.getFill(); } + + return undefined; + }; + + /** + * Returns the animation's direction. + */ + const getDirection = () => { + if (forceDirectionValue !== undefined) { return forceDirectionValue; } + if (_direction !== undefined) { return _direction; } + if (parentAnimation) { return parentAnimation.getDirection(); } + + return undefined; + + }; + + /** + * Returns the animation's easing. + */ + const getEasing = () => { + if (shouldForceLinearEasing) { return 'linear'; } + if (_easing !== undefined) { return _easing; } + if (parentAnimation) { return parentAnimation.getEasing(); } + + return undefined; + }; + + /** + * Gets the animation's duration in milliseconds. + */ + const getDuration = () => { + if (shouldForceSyncPlayback) { return 0; } + if (forceDurationValue !== undefined) { return forceDurationValue; } + if (_duration !== undefined) { return _duration; } + if (parentAnimation) { return parentAnimation.getDuration(); } + + return undefined; + }; + + /** + * Gets the number of iterations the animation will run. + */ + const getIterations = () => { + if (_iterations !== undefined) { return _iterations; } + if (parentAnimation) { return parentAnimation.getIterations(); } + + return undefined; + }; + + /** + * Gets the animation's delay in milliseconds. + */ + const getDelay = () => { + if (forceDelayValue !== undefined) { return forceDelayValue; } + if (_delay !== undefined) { return _delay; } + if (parentAnimation) { return parentAnimation.getDelay(); } + + return undefined; + }; + + /** + * Get an array of keyframes for the animation. + */ + const getKeyframes = () => { + return _keyframes; + }; + + /** + * Sets whether the animation should play forwards, + * backwards, or alternating back and forth. + */ + const direction = (animationDirection: AnimationDirection) => { + _direction = animationDirection; + + update(true); + + return ani; + }; + + /** + * Sets how the animation applies styles to its + * elements before and after the animation's execution. + */ + const fill = (animationFill: AnimationFill) => { + _fill = animationFill; + + update(true); + + return ani; + + }; + + /** + * Sets when an animation starts (in milliseconds). + */ + const delay = (animationDelay: number) => { + _delay = animationDelay; + + update(true); + + return ani; + }; + + /** + * Sets how the animation progresses through the + * duration of each cycle. + */ + const easing = (animationEasing: string) => { + _easing = animationEasing; + + update(true); + + return ani; + }; + + /** + * Sets the length of time the animation takes + * to complete one cycle. + */ + const duration = (animationDuration: number) => { + _duration = animationDuration; + + update(true); + + return ani; + }; + + /** + * Sets the number of times the animation cycle + * should be played before stopping. + */ + const iterations = (animationIterations: number) => { + _iterations = animationIterations; + + update(true); + + return ani; + }; + + /** + * Sets the parent animation. + */ + const parent = (animation: Animation) => { + parentAnimation = animation; + + return ani; + }; + + /** + * Add one or more elements to the animation + */ + const addElement = (el: Element | Element[] | Node | Node[] | NodeList | undefined | null) => { + if (el != null) { + + if ((el as Node).nodeType === 1) { + elements.push(el as any); + } else if ((el as NodeList).length >= 0) { + for (let i = 0; i < (el as NodeList).length; i++) { + elements.push((el as any)[i]); + } + } else { + console.error('Invalid addElement value'); + } + } + + return ani; + }; + + /** + * Group one or more animations together to be controlled by a parent animation. + */ + const addAnimation = (animationToAdd: Animation | Animation[] | undefined | null) => { + if (animationToAdd != null) { + const parentAnim = ani; + const animationsToAdd = animationToAdd as Animation[]; + if (animationsToAdd.length >= 0) { + for (const animation of animationsToAdd) { + animation.parent(parentAnim); + childAnimations.push(animation); + } + } else { + (animationToAdd as Animation).parent(parentAnim); + childAnimations.push(animationToAdd as Animation); + } + } + + return ani; + }; + + /** + * Set the keyframes for the animation. + */ + const keyframes = (keyframeValues: any[]) => { + _keyframes = keyframeValues; + + return ani; + }; + + /** + * Runs all before read callbacks + */ + const runBeforeRead = () => { + _beforeAddReadFunctions.forEach(callback => { + callback(); + }); + }; + + /** + * Runs all before write callbacks + */ + const runBeforeWrite = () => { + _beforeAddWriteFunctions.forEach(callback => { + callback(); + }); + }; + + /** + * Updates styles and classes before animation runs + */ + const runBeforeStyles = () => { + const addClasses = beforeAddClasses; + const removeClasses = beforeRemoveClasses; + const styles = beforeStylesValue; + + elements.forEach((el: HTMLElement) => { + const elementClassList = el.classList; + + elementClassList.add(...addClasses); + elementClassList.remove(...removeClasses); + + for (const property in styles) { + if (styles.hasOwnProperty(property)) { + setStyleProperty(el, property, styles[property]); + } + } + }); + }; + + /** + * Run all "before" animation hooks. + */ + const beforeAnimation = () => { + runBeforeRead(); + runBeforeWrite(); + runBeforeStyles(); + }; + + /** + * Runs all after read callbacks + */ + const runAfterRead = () => { + _afterAddReadFunctions.forEach(callback => { + callback(); + }); + }; + + /** + * Runs all after write callbacks + */ + const runAfterWrite = () => { + _afterAddWriteFunctions.forEach(callback => { + callback(); + }); + }; + + /** + * Updates styles and classes before animation ends + */ + const runAfterStyles = () => { + const addClasses = afterAddClasses; + const removeClasses = afterRemoveClasses; + const styles = afterStylesValue; + + elements.forEach((el: HTMLElement) => { + const elementClassList = el.classList; + + elementClassList.add(...addClasses); + elementClassList.remove(...removeClasses); + + for (const property in styles) { + if (styles.hasOwnProperty(property)) { + setStyleProperty(el, property, styles[property]); + } + } + }); + }; + + /** + * Run all "after" animation hooks. + */ + const afterAnimation = () => { + clearCSSAnimationsTimeout(); + runAfterRead(); + runAfterWrite(); + runAfterStyles(); + + const didComplete = willComplete; + + onFinishCallbacks.forEach(onFinishCallback => { + onFinishCallback.callback(didComplete, ani); + }); + + onFinishOneTimeCallbacks.forEach(onFinishCallback => { + onFinishCallback.callback(didComplete, ani); + }); + + onFinishOneTimeCallbacks.length = 0; + + shouldCalculateNumAnimations = true; + finished = true; + }; + + const animationFinish = () => { + if (numAnimationsRunning === 0) { return; } + + numAnimationsRunning--; + + if (numAnimationsRunning === 0) { + afterAnimation(); + if (parentAnimation) { + parentAnimation.animationFinish(); + } + } + }; + + const initializeCSSAnimation = () => { + cleanUpStyleSheets(); + + elements.forEach(element => { + if (_keyframes.length > 0) { + const keyframeRules = generateKeyframeRules(_keyframes); + keyframeName = generateKeyframeName(keyframeRules); + const stylesheet = createKeyframeStylesheet(keyframeName, keyframeRules, element); + stylesheets.push(stylesheet); + + setStyleProperty(element, 'animation-name', stylesheet.id || null); + setStyleProperty(element, 'animation-duration', (getDuration() !== undefined) ? `${getDuration()}ms` : null); + setStyleProperty(element, 'animation-timing-function', getEasing() || null); + setStyleProperty(element, 'animation-delay', (getDelay() !== undefined) ? `${getDelay()}ms` : null); + setStyleProperty(element, 'animation-fill-mode', getFill() || null); + setStyleProperty(element, 'animation-direction', getDirection() || null); + + const iterationsCount = + (getIterations() !== undefined) ? + (getIterations() === Infinity) ? 'infinite' : getIterations()!.toString() + : null; + + setStyleProperty(element, 'animation-iteration-count', iterationsCount); + setStyleProperty(element, 'animation-play-state', 'paused'); + } + }); + }; + + const initializeWebAnimation = () => { + elements.forEach(element => { + const animation = element.animate(_keyframes, { + delay: getDelay(), + duration: getDuration(), + easing: getEasing(), + iterations: getIterations(), + fill: getFill(), + direction: getDirection() + }); + + animation.pause(); + + webAnimations.push(animation); + }); + + if (getWebAnimations().length > 0) { + webAnimations[0].onfinish = () => { + animationFinish(); + }; + } + + }; + + const initializeAnimation = () => { + beforeAnimation(); + + if (_keyframes.length > 0) { + if (supportsWebAnimations) { + initializeWebAnimation(); + } else { + initializeCSSAnimation(); + } + } + + initialized = true; + }; + + const setAnimationStep = (step: number) => { + step = Math.min(Math.max(step, 0), 0.999); + if (supportsWebAnimations) { + getWebAnimations().forEach(animation => { + animation.currentTime = animation.effect.getComputedTiming().delay + (getDuration()! * step); + animation.pause(); + }); + + } else { + const animationDelay = getDelay() || 0; + const animationDuration = `-${animationDelay + (getDuration()! * step)}ms`; + + elements.forEach(element => { + if (_keyframes.length > 0) { + setStyleProperty(element, 'animation-delay', animationDuration); + setStyleProperty(element, 'animation-play-state', 'paused'); + } + }); + } + }; + + const updateWebAnimation = () => { + getWebAnimations().forEach(animation => { + animation.effect.updateTiming({ + delay: getDelay(), + duration: getDuration(), + easing: getEasing(), + iterations: getIterations(), + fill: getFill(), + direction: getDirection() + }); + }); + }; + + const updateCSSAnimation = (toggleAnimationName = true) => { + elements.forEach(element => { + setStyleProperty(element, 'animation-name', keyframeName || null); + setStyleProperty(element, 'animation-duration', (getDuration() !== undefined) ? `${getDuration()}ms` : null); + setStyleProperty(element, 'animation-timing-function', getEasing() || null); + setStyleProperty(element, 'animation-delay', (getDelay() !== undefined) ? `${getDelay()}ms` : null); + setStyleProperty(element, 'animation-fill-mode', getFill() || null); + setStyleProperty(element, 'animation-direction', getDirection() || null); + + const iterationsCount = + (getIterations() !== undefined) ? + (getIterations() === Infinity) ? 'infinite' : getIterations()!.toString() + : null; + + setStyleProperty(element, 'animation-iteration-count', iterationsCount); + + if (toggleAnimationName) { + setStyleProperty(element, 'animation-name', `${keyframeName}-alt`); + } + + requestAnimationFrame(() => { + setStyleProperty(element, 'animation-name', keyframeName || null); + }); + }); + }; + + /** + * Updates any existing animations. + */ + const update = (deep = false, toggleAnimationName = true) => { + if (deep) { + childAnimations.forEach(animation => { + animation.update(deep); + }); + } + + if (supportsWebAnimations) { + updateWebAnimation(); + } else { + updateCSSAnimation(toggleAnimationName); + } + + return ani; + }; + + const progressStart = (forceLinearEasing = false) => { + childAnimations.forEach(animation => { + animation.progressStart(forceLinearEasing); + }); + + pause(false); + shouldForceLinearEasing = forceLinearEasing; + + if (!initialized) { + initializeAnimation(); + } else { + update(); + setAnimationStep(0); + } + + return ani; + }; + + const progressStep = (step: number) => { + childAnimations.forEach(animation => { + animation.progressStep(step); + }); + + if (getDuration() !== undefined) { + setAnimationStep(step); + } + + return ani; + }; + + // TODO: Need to clean this up + const progressEnd = (shouldComplete: boolean, step: number, dur: number | undefined) => { + childAnimations.forEach(animation => { + animation.progressEnd(shouldComplete, step, dur); + }); + + if (dur !== undefined) { + forceDurationValue = dur; + } + + finished = false; + shouldForceLinearEasing = false; + willComplete = shouldComplete; + + if (!shouldComplete) { + onFinish(() => { + pause(false); + + willComplete = true; + forceDurationValue = undefined; + + if (supportsWebAnimations) { + forceDirectionValue = undefined; + forceDelayValue = undefined; + progressStep(1); + } else { + + /** + * Parent animations at this point may not have finished + */ + forceDirectionValue = (getDirection() === 'reverse') ? 'normal' : 'reverse'; + forceDelayValue = 0; + update(); + + forceDirectionValue = undefined; + } + }, { + oneTime: true + }); + + forceDirectionValue = (getDirection() === 'reverse') ? 'normal' : 'reverse'; + + if (supportsWebAnimations) { + update(); + progressStep(1 - step); + } else { + forceDelayValue = ((1 - step) * getDuration()!) * -1; + update(false, false); + } + } else { + onFinish(() => { + pause(false); + + forceDurationValue = undefined; + forceDelayValue = undefined; + + if (supportsWebAnimations) { + progressStep(1); + } + }, { + oneTime: true + }); + + if (!supportsWebAnimations) { + forceDelayValue = (step * getDuration()!) * -1; + update(false, false); + } + } + + if (!parentAnimation) { + play(); + } + + return ani; + }; + + /** + * Pause the animation. + */ + const pause = (deep = true) => { + if (deep) { + childAnimations.forEach(animation => { + animation.pause(); + }); + } + + if (initialized) { + if (supportsWebAnimations) { + getWebAnimations().forEach(animation => { + animation.pause(); + }); + } else { + elements.forEach(element => { + setStyleProperty(element, 'animation-play-state', 'paused'); + }); + } + } + + return ani; + }; + + /** + * Play the animation asynchronously. + * This returns a promise that resolves + * when the animation has ended. + */ + const playAsync = () => { + return new Promise(resolve => { + onFinish(resolve, { oneTime: true }); + play(); + + return ani; + }); + }; + + /** + * Play the animation synchronously. This + * is the equivalent of running the animation + * with a duration of 0ms. + */ + const playSync = () => { + shouldForceSyncPlayback = true; + + onFinish(() => shouldForceSyncPlayback = false, { oneTime: true }); + play(); + + return ani; + }; + + const onAnimationEndFallback = () => { + cssAnimationsTimerFallback = undefined; + animationFinish(); + }; + + const clearCSSAnimationsTimeout = () => { + if (cssAnimationsTimerFallback) { + clearTimeout(cssAnimationsTimerFallback); + } + }; + + const playCSSAnimations = () => { + clearCSSAnimationsTimeout(); + + elements.forEach(element => { + if (_keyframes.length > 0) { + setStyleProperty(element, 'animation-play-state', 'running'); + } + }); + + const animationDelay = getDelay() || 0; + const animationDuration = getDuration() || 0; + const visibleElements = elements.filter(element => element.offsetParent !== null); + if (visibleElements.length === 0 || _keyframes.length === 0 || elements.length === 0) { + /** + * CSS Animations will not fire an `animationend` event + * for elements with `display: none`. The Web Animations API + * accounts for this, but using raw CSS Animations requires + * this workaround. + */ + animationFinish(); + } else if (_keyframes.length > 0 && elements.length > 0) { + /** + * This is a catchall in the event that a CSS Animation did not finish. + * The Web Animations API has mechanisms in place for preventing this. + */ + cssAnimationsTimerFallback = setTimeout(onAnimationEndFallback, animationDelay + animationDuration + ANIMATION_END_FALLBACK_PADDING_MS); + + animationEnd(elements[0], () => { + clearCSSAnimationsTimeout(); + animationFinish(); + }); + } + }; + + const playWebAnimations = () => { + getWebAnimations().forEach(animation => { + animation.play(); + }); + + if (_keyframes.length === 0 || elements.length === 0) { + animationFinish(); + } + }; + + const resetCSSAnimations = () => { + elements.forEach(element => { + const newKeyframeName = (keyframeName !== undefined) ? `${keyframeName}-alt` : null; + setStyleProperty(element, 'animation-name', newKeyframeName); + }); + }; + + const resetAnimation = () => { + if (supportsWebAnimations) { + setAnimationStep(0); + } else { + resetCSSAnimations(); + } + }; + + /** + * Play the animation + */ + const play = () => { + if (!initialized) { + initializeAnimation(); + } + + if (finished) { + resetAnimation(); + finished = false; + } + + if (shouldCalculateNumAnimations) { + numAnimationsRunning = childAnimations.length + 1; + shouldCalculateNumAnimations = false; + } + + childAnimations.forEach(animation => { + animation.play(); + }); + + if (supportsWebAnimations) { + playWebAnimations(); + } else { + playCSSAnimations(); + } + + return ani; + }; + + /** + * Stop the animation and reset + * all elements to their initial state + */ + const stop = () => { + childAnimations.forEach(animation => { + animation.stop(); + }); + + if (initialized) { + cleanUp(); + initialized = false; + } + + return ani; + }; + + const from = (property: string, value: any) => { + const firstFrame = _keyframes[0]; + + if (firstFrame != null && (firstFrame.offset === undefined || firstFrame.offset === 0)) { + firstFrame[property] = value; + } else { + const object: any = { + offset: 0 + }; + object[property] = value; + + _keyframes = [ + object, + ..._keyframes + ]; + } + + return ani; + }; + + const to = (property: string, value: any) => { + const lastFrame = _keyframes[_keyframes.length - 1]; + + if (lastFrame != null && (lastFrame.offset === undefined || lastFrame.offset === 1)) { + lastFrame[property] = value; + } else { + + const object: any = { + offset: 1 + }; + object[property] = value; + + _keyframes = [ + ..._keyframes, + object + ]; + } + + return ani; + }; + + const fromTo = (property: string, fromValue: any, toValue: any) => { + return from(property, fromValue).to(property, toValue); + }; + + return ani = { + parentAnimation, + elements, + childAnimations, + animationFinish, + from, + to, + fromTo, + parent, + play, + playAsync, + playSync, + pause, + stop, + destroy, + keyframes, + addAnimation, + addElement, + update, + fill, + direction, + iterations, + duration, + easing, + delay, + getWebAnimations, + getKeyframes, + getFill, + getDirection, + getDelay, + getIterations, + getEasing, + getDuration, + afterAddRead, + afterAddWrite, + afterClearStyles, + afterStyles, + afterRemoveClass, + afterAddClass, + beforeAddRead, + beforeAddWrite, + beforeClearStyles, + beforeStyles, + beforeRemoveClass, + beforeAddClass, + onFinish, + clearOnFinish, + + progressStart, + progressStep, + progressEnd + } as Animation; +}; diff --git a/core/src/utils/animation/old-animation/animation-interface.ts b/core/src/utils/animation/old-animation/animation-interface.ts new file mode 100644 index 0000000000..eb658cf80b --- /dev/null +++ b/core/src/utils/animation/old-animation/animation-interface.ts @@ -0,0 +1,64 @@ + +export interface AnimationController { + create(animationBuilder?: AnimationBuilder, baseEl?: any, opts?: any): Promise; +} + +export interface Animation { + new (): any; + parent: Animation | undefined; + hasChildren: boolean; + addElement(el: Node | Node[] | NodeList): Animation; + add(childAnimation: Animation): Animation; + duration(milliseconds: number): Animation; + easing(name: string): Animation; + easingReverse(name: string): Animation; + getDuration(opts?: PlayOptions): number; + getEasing(): string; + from(prop: string, val: any): Animation; + to(prop: string, val: any, clearProperyAfterTransition?: boolean): Animation; + fromTo(prop: string, fromVal: any, toVal: any, clearProperyAfterTransition?: boolean): Animation; + beforeAddClass(className: string): Animation; + beforeRemoveClass(className: string): Animation; + beforeStyles(styles: { [property: string]: any; }): Animation; + beforeClearStyles(propertyNames: string[]): Animation; + beforeAddRead(domReadFn: () => void): Animation; + beforeAddWrite(domWriteFn: () => void): Animation; + afterAddClass(className: string): Animation; + afterRemoveClass(className: string): Animation; + afterStyles(styles: { [property: string]: any; }): Animation; + afterClearStyles(propertyNames: string[]): Animation; + play(opts?: PlayOptions): void; + playSync(): void; + playAsync(opts?: PlayOptions): Promise; + reverse(shouldReverse?: boolean): Animation; + stop(stepValue?: number): void; + progressStart(): void; + progressStep(stepValue: number): void; + progressEnd(shouldComplete: boolean, currentStepValue: number, dur: number): void; + onFinish(callback: (animation?: Animation) => void, opts?: {oneTimeCallback?: boolean, clearExistingCallbacks?: boolean}): Animation; + destroy(): void; + isRoot(): boolean; + hasCompleted: boolean; +} + +export type AnimationBuilder = (Animation: Animation, baseEl: any, opts?: any) => Promise; + +export interface PlayOptions { + duration?: number; + promise?: boolean; +} + +export interface EffectProperty { + effectName: string; + trans: boolean; + wc?: string; + to?: EffectState; + from?: EffectState; + [state: string]: any; +} + +export interface EffectState { + val: any; + num: number; + effectUnit: string; +} diff --git a/core/src/utils/animation/animator.ts b/core/src/utils/animation/old-animation/animator.ts similarity index 100% rename from core/src/utils/animation/animator.ts rename to core/src/utils/animation/old-animation/animator.ts diff --git a/core/src/utils/animation/index.ts b/core/src/utils/animation/old-animation/index.ts similarity index 65% rename from core/src/utils/animation/index.ts rename to core/src/utils/animation/old-animation/index.ts index 6e8d3dd5fc..afecd60466 100644 --- a/core/src/utils/animation/index.ts +++ b/core/src/utils/animation/old-animation/index.ts @@ -1,10 +1,10 @@ -import { Animation, AnimationBuilder } from '../../interface'; +import { Animation, AnimationBuilder } from '../../../interface'; import { Animator } from './animator'; export const create = (animationBuilder?: AnimationBuilder, baseEl?: any, opts?: any): Promise => { if (animationBuilder) { - return animationBuilder(Animator as any, baseEl, opts); + return animationBuilder(baseEl, opts); } return Promise.resolve(new Animator() as any); }; diff --git a/core/src/utils/animation/transition-end.ts b/core/src/utils/animation/old-animation/transition-end.ts similarity index 100% rename from core/src/utils/animation/transition-end.ts rename to core/src/utils/animation/old-animation/transition-end.ts diff --git a/core/src/utils/animation/test/animation.spec.ts b/core/src/utils/animation/test/animation.spec.ts new file mode 100644 index 0000000000..83409dcf31 --- /dev/null +++ b/core/src/utils/animation/test/animation.spec.ts @@ -0,0 +1,246 @@ +import { createAnimation } from '../animation'; + +describe('Animation Class', () => { + + describe('addElement()', () => { + let animation; + beforeEach(() => { + animation = createAnimation(); + }); + + it('should add 1 element', () => { + const el = document.createElement('p'); + animation.addElement(el); + + expect(animation.elements.length).toEqual(1); + }); + + it('should add multiple elements', () => { + const els = [ + document.createElement('p'), + document.createElement('p'), + document.createElement('p') + ]; + + animation.addElement(els); + + expect(animation.elements.length).toEqual(els.length); + }); + + it('should not error when trying to add null or undefined', () => { + const el = document.createElement('p'); + animation.addElement(el); + + animation.addElement(null); + animation.addElement(undefined); + + expect(animation.elements.length).toEqual(1); + }); + }); + + describe('addAnimation()', () => { + let animation; + beforeEach(() => { + animation = createAnimation(); + }); + + it('should add 1 animation', () => { + const newAnimation = createAnimation(); + animation.addAnimation(newAnimation); + + expect(animation.childAnimations.length).toEqual(1); + }); + + it('should add multiple animations', () => { + animation.addAnimation([createAnimation(), createAnimation(), createAnimation()]); + + expect(animation.childAnimations.length).toEqual(3); + }); + + it('should not error when trying to add null or undefined', () => { + animation.addAnimation(null); + animation.addAnimation(undefined); + + expect(animation.childAnimations.length).toEqual(0); + }) + }); + + describe('Animation Keyframes', () => { + let animation; + beforeEach(() => { + animation = createAnimation('my-animation'); + }); + + it('should generate a keyframe', () => { + animation.keyframes([ + { transform: 'scale(1)', opacity: 1, offset: 0 }, + { transform: 'scale(0.5)', opacity: 0.5, offset: 0.5 }, + { transform: 'scale(0)', opacity: 0, offset: 1 } + ]); + + expect(animation.getKeyframes().length).toEqual(3); + }); + }); + + describe('Animation Config Methods', () => { + let animation; + beforeEach(() => { + animation = createAnimation(); + }); + + it('should get undefined when easing not set', () => { + expect(animation.getEasing()).toEqual(undefined); + }); + + it('should get parent easing when child easing is not set', () => { + const childAnimation = createAnimation(); + animation + .addAnimation(childAnimation) + .easing('linear'); + + expect(childAnimation.getEasing()).toEqual('linear'); + }); + + it('should get prefer child easing over parent easing', () => { + const childAnimation = createAnimation(); + childAnimation.easing('linear'); + + animation + .addAnimation(childAnimation) + .easing('ease-in-out'); + + expect(childAnimation.getEasing()).toEqual('linear'); + }); + + it('should get linear easing when forceLinear is set', () => { + animation.easing('ease-in-out'); + + animation.progressStart(true); + expect(animation.getEasing()).toEqual('linear'); + + animation.progressEnd(); + expect(animation.getEasing()).toEqual('ease-in-out'); + }); + + it('should get undefined when duration not set', () => { + expect(animation.getDuration()).toEqual(undefined); + }); + + it('should get parent duration when child duration is not set', () => { + const childAnimation = createAnimation(); + animation + .addAnimation(childAnimation) + .duration(500); + + expect(childAnimation.getDuration()).toEqual(500); + }); + + it('should get prefer child duration over parent duration', () => { + const childAnimation = createAnimation(); + childAnimation.duration(500); + + animation + .addAnimation(childAnimation) + .duration(1000); + + expect(childAnimation.getDuration()).toEqual(500); + }); + + it('should get undefined when delay not set', () => { + expect(animation.getDelay()).toEqual(undefined); + }); + + it('should get parent delay when child delay is not set', () => { + const childAnimation = createAnimation(); + animation + .addAnimation(childAnimation) + .delay(500); + + expect(childAnimation.getDelay()).toEqual(500); + }); + + it('should get prefer child delay over parent delay', () => { + const childAnimation = createAnimation(); + childAnimation.delay(500); + + animation + .addAnimation(childAnimation) + .delay(1000); + + expect(childAnimation.getDelay()).toEqual(500); + }); + + it('should get undefined when iterations not set', () => { + expect(animation.getIterations()).toEqual(undefined); + }); + + it('should get parent iterations when child iterations is not set', () => { + const childAnimation = createAnimation(); + animation + .addAnimation(childAnimation) + .iterations(2); + + expect(childAnimation.getIterations()).toEqual(2); + }); + + it('should get prefer child iterations over parent iterations', () => { + const childAnimation = createAnimation(); + childAnimation.iterations(2); + + animation + .addAnimation(childAnimation) + .iterations(1); + + expect(childAnimation.getIterations()).toEqual(2); + }); + + it('should get undefined when fill not set', () => { + expect(animation.getFill()).toEqual(undefined); + }); + + it('should get parent fill when child fill is not set', () => { + const childAnimation = createAnimation(); + animation + .addAnimation(childAnimation) + .fill('both'); + + expect(childAnimation.getFill()).toEqual('both'); + }); + + it('should get prefer child fill over parent fill', () => { + const childAnimation = createAnimation(); + childAnimation.fill('none'); + + animation + .addAnimation(childAnimation) + .fill('forwards'); + + expect(childAnimation.getFill()).toEqual('none'); + }); + + it('should get undefined when direction not set', () => { + expect(animation.getDirection()).toEqual(undefined); + }); + + it('should get parent direction when child direction is not set', () => { + const childAnimation = createAnimation(); + animation + .addAnimation(childAnimation) + .direction('alternate'); + + expect(childAnimation.getDirection()).toEqual('alternate'); + }); + + it('should get prefer child direction over parent direction', () => { + const childAnimation = createAnimation(); + childAnimation.direction('alternate-reverse'); + + animation + .addAnimation(childAnimation) + .direction('normal'); + + expect(childAnimation.getDirection()).toEqual('alternate-reverse'); + }); + + }) +}); \ No newline at end of file diff --git a/core/src/utils/animation/test/animationbuilder/e2e.ts b/core/src/utils/animation/test/animationbuilder/e2e.ts new file mode 100644 index 0000000000..39b16dfbe7 --- /dev/null +++ b/core/src/utils/animation/test/animationbuilder/e2e.ts @@ -0,0 +1,65 @@ +import { newE2EPage } from '@stencil/core/testing'; + +import { listenForEvent, waitForFunctionTestContext } from '../../../test/utils'; + +const navChanged = () => new Promise(resolve => window.addEventListener('ionRouteDidChange', resolve)); +const ROUTE_CHANGED = 'onRouteChanged'; + +test('animation:backwards-compatibility animationbuilder', async () => { + const page = await newE2EPage({ url: '/src/utils/animation/test/animationbuilder?_forceAnimationBuilder=true' }); + await testNavigation(page); +}); + +test('animation:backwards-compatibility animation', async () => { + const page = await newE2EPage({ url: '/src/utils/animation/test/animationbuilder' }); + await testNavigation(page); +}); + +test('animation:ios-transition web', async () => { + const page = await newE2EPage({ url: '/src/utils/animation/test/animationbuilder?ionic:mode=ios' }); + await testNavigation(page); +}); + +test('animation:ios-transition css', async () => { + const page = await newE2EPage({ url: '/src/utils/animation/test/animationbuilder?ionic:mode=ios&ionic:_forceCSSAnimations=true' }); + await testNavigation(page); +}); + +const testNavigation = async page => { + const screenshotCompares = []; + const body = await page.$('body'); + await listenForEvent(page, 'ionRouteDidChange', body, ROUTE_CHANGED); + const routeChangedCount: any = { count: 0 }; + await page.exposeFunction(ROUTE_CHANGED, () => { + routeChangedCount.count += 1; + }); + + screenshotCompares.push(await page.compareScreenshot()); + + page.click('page-root ion-button.next'); + await waitForNavChange(page, routeChangedCount); + page.click('page-one ion-button.next'); + await waitForNavChange(page, routeChangedCount); + page.click('page-two ion-button.next'); + await waitForNavChange(page, routeChangedCount); + page.click('page-three ion-back-button'); + await waitForNavChange(page, routeChangedCount); + page.click('page-two ion-back-button'); + await waitForNavChange(page, routeChangedCount); + page.click('page-one ion-back-button'); + await waitForNavChange(page, routeChangedCount); + + screenshotCompares.push(await page.compareScreenshot()); + + for (const screenshotCompare of screenshotCompares) { + expect(screenshotCompare).toMatchScreenshot(); + } +}; + +const waitForNavChange = async (page, routeChangedCount) => { + await waitForFunctionTestContext((payload: any) => { + return payload.routeChangedCount.count === 1; + }, { routeChangedCount }); + + routeChangedCount.count = 0; +}; diff --git a/core/src/utils/animation/test/animationbuilder/index.html b/core/src/utils/animation/test/animationbuilder/index.html new file mode 100644 index 0000000000..4bf70132c6 --- /dev/null +++ b/core/src/utils/animation/test/animationbuilder/index.html @@ -0,0 +1,190 @@ + + + + + + Animation - Animation Builder + + + + + + + + + + + + + + + + + + + + + diff --git a/core/src/utils/animation/test/basic/e2e.ts b/core/src/utils/animation/test/basic/e2e.ts new file mode 100644 index 0000000000..30f43d7c4c --- /dev/null +++ b/core/src/utils/animation/test/basic/e2e.ts @@ -0,0 +1,53 @@ +import { newE2EPage } from '@stencil/core/testing'; + +import { listenForEvent, waitForFunctionTestContext } from '../../../test/utils'; + +test(`animation:web: basic`, async () => { + const page = await newE2EPage({ url: '/src/utils/animation/test/basic' }); + const screenshotCompares = []; + + screenshotCompares.push(await page.compareScreenshot()); + + const ANIMATION_FINISHED = 'onIonAnimationFinished'; + const animationFinishedCount: any = { count: 0 }; + await page.exposeFunction(ANIMATION_FINISHED, () => { + animationFinishedCount.count += 1; + }); + + const square = await page.$('.square-a'); + await listenForEvent(page, 'ionAnimationFinished', square, ANIMATION_FINISHED); + + await page.click('.play'); + await page.waitForSelector('.play'); + + await waitForFunctionTestContext((payload: any) => { + return payload.animationFinishedCount.count === 1; + }, { animationFinishedCount }); + + screenshotCompares.push(await page.compareScreenshot()); +}); + +test(`animation:css: basic`, async () => { + const page = await newE2EPage({ url: '/src/utils/animation/test/basic?ionic:_forceCSSAnimations=true' }); + const screenshotCompares = []; + + screenshotCompares.push(await page.compareScreenshot()); + + const ANIMATION_FINISHED = 'onIonAnimationFinished'; + const animationFinishedCount: any = { count: 0 }; + await page.exposeFunction(ANIMATION_FINISHED, () => { + animationFinishedCount.count += 1; + }); + + const square = await page.$('.square-a'); + await listenForEvent(page, 'ionAnimationFinished', square, ANIMATION_FINISHED); + + await page.click('.play'); + await page.waitForSelector('.play'); + + await waitForFunctionTestContext((payload: any) => { + return payload.animationFinishedCount.count === 1; + }, { animationFinishedCount }); + + screenshotCompares.push(await page.compareScreenshot()); +}); diff --git a/core/src/utils/animation/test/basic/index.html b/core/src/utils/animation/test/basic/index.html new file mode 100644 index 0000000000..a6ffc2cc1e --- /dev/null +++ b/core/src/utils/animation/test/basic/index.html @@ -0,0 +1,97 @@ + + + + + + Animation - Basic + + + + + + + + + + + + + + + Animations + + + + +
+ Play + Pause + Stop + Destroy + +
+ Hello +
+
+
+ + + + + diff --git a/core/src/utils/animation/test/chaining/index.html b/core/src/utils/animation/test/chaining/index.html new file mode 100644 index 0000000000..4232590929 --- /dev/null +++ b/core/src/utils/animation/test/chaining/index.html @@ -0,0 +1,227 @@ + + + + + + Animation - Basic + + + + + + + + + + + + + + + Animations + + + + +
+ Play + Pause + Stop + Destroy + +
+
Hello
+
+ +
+
Hello
+
+ +
+
Hello
+
+ +
+
    +
  • Animation A
  • +
+ +
    +
  • Animation B
  • +
+ +
    +
  • Animation C
  • +
      +
    • Animation C sub A
    • +
    • Animation C sub B
    • +
    +
+
+
+
+ + + + + diff --git a/core/src/utils/animation/test/display/e2e.ts b/core/src/utils/animation/test/display/e2e.ts new file mode 100644 index 0000000000..71299ee1de --- /dev/null +++ b/core/src/utils/animation/test/display/e2e.ts @@ -0,0 +1,54 @@ +import { newE2EPage } from '@stencil/core/testing'; + +import { listenForEvent, waitForFunctionTestContext } from '../../../test/utils'; + +test(`animation:web: display`, async () => { + const page = await newE2EPage({ url: '/src/utils/animation/test/display' }); + const screenshotCompares = []; + + screenshotCompares.push(await page.compareScreenshot()); + + const ANIMATION_FINISHED = 'onIonAnimationFinished'; + const animationStatus = []; + await page.exposeFunction(ANIMATION_FINISHED, (ev: any) => { + animationStatus.push(ev.detail); + }); + + const squareA = await page.$('.square-a'); + await listenForEvent(page, 'ionAnimationFinished', squareA, ANIMATION_FINISHED); + + await page.click('.play'); + await page.waitForSelector('.play'); + + await waitForFunctionTestContext((payload: any) => { + return payload.animationStatus.join(', ') === ['AnimationBFinished', 'AnimationAFinished', 'AnimationRootFinished'].join(', '); + + }, { animationStatus }); + screenshotCompares.push(await page.compareScreenshot()); +}); + +test(`animation:css: display`, async () => { + const page = await newE2EPage({ url: '/src/utils/animation/test/display?ionic:_forceCSSAnimations=true' }); + const screenshotCompares = []; + + screenshotCompares.push(await page.compareScreenshot()); + + const ANIMATION_FINISHED = 'onIonAnimationFinished'; + const animationStatus = []; + await page.exposeFunction(ANIMATION_FINISHED, (ev: any) => { + animationStatus.push(ev.detail); + }); + + const squareA = await page.$('.square-a'); + await listenForEvent(page, 'ionAnimationFinished', squareA, ANIMATION_FINISHED); + + await page.click('.play'); + await page.waitForSelector('.play'); + + await waitForFunctionTestContext((payload: any) => { + // CSS Animations do not account for elements with `display: none` very well, so we need to add a workaround for this. + return payload.animationStatus.join(', ') === ['AnimationAFinished', 'AnimationBFinished', 'AnimationRootFinished'].join(', '); + + }, { animationStatus }); + screenshotCompares.push(await page.compareScreenshot()); +}); diff --git a/core/src/utils/animation/test/display/index.html b/core/src/utils/animation/test/display/index.html new file mode 100644 index 0000000000..15d2dfa10e --- /dev/null +++ b/core/src/utils/animation/test/display/index.html @@ -0,0 +1,131 @@ + + + + + + Animation - Display + + + + + + + + + + + + + + + Animations + + + + +
+ Play + Pause + Stop + Destroy + +
+
Hello
+
+ +
+
Hello
+
+
+
+ + + + + diff --git a/core/src/utils/animation/test/gesture/index.html b/core/src/utils/animation/test/gesture/index.html new file mode 100644 index 0000000000..1649252617 --- /dev/null +++ b/core/src/utils/animation/test/gesture/index.html @@ -0,0 +1,173 @@ + + + + + + Animation - Basic + + + + + + + + + + + + + + + Animations + + + + +
+
+ Drag along the track to animate the elements +
+
+
+ Hello +
+ +
+ Hello +
+ +
+ Hello +
+
+
+ + + + + diff --git a/core/src/utils/animation/test/hooks/e2e.ts b/core/src/utils/animation/test/hooks/e2e.ts new file mode 100644 index 0000000000..e4bec98179 --- /dev/null +++ b/core/src/utils/animation/test/hooks/e2e.ts @@ -0,0 +1,144 @@ +import { newE2EPage } from '@stencil/core/testing'; + +import { listenForEvent, waitForFunctionTestContext } from '../../../test/utils'; + +test(`animation:web: hooks`, async () => { + const page = await newE2EPage({ url: '/src/utils/animation/test/hooks' }); + const screenshotCompares = []; + + screenshotCompares.push(await page.compareScreenshot()); + + const square = await page.$('.square-a'); + + const styles = await getStyles(page, '.square-a'); + expect(styles.paddingBottom).toEqual('20px'); + expect(styles.color).toEqual('rgb(0, 0, 0)'); + + const classList = await getClassList(square); + expect(classList.includes('hello-world')).toEqual(true); + expect(classList.includes('test-class')).toEqual(false); + + await waitForEventToBeCalled('afterWrite', page, square, async () => { + await waitForEventToBeCalled('afterRead', page, square, async () => { + await waitForEventToBeCalled('ionAnimationFinished', page, square, async () => { + await waitForEventToBeCalled('beforeWrite', page, square, async () => { + await waitForEventToBeCalled('beforeRead', page, square, async () => { + await page.click('.play'); + await page.waitForSelector('.play'); + + // Test beforeRemoveClass and beforeAddClass + const webClassListAgain = await getClassList(square); + expect(webClassListAgain.includes('hello-world')).toEqual(false); + expect(webClassListAgain.includes('test-class')).toEqual(true); + + // Test beforeStyles and beforeClearStyles + const webStylesAgain = await getStyles(page, '.square-a'); + expect(webStylesAgain.paddingBottom).toEqual('0px'); + expect(webStylesAgain.color).toEqual('rgb(128, 0, 128)'); + }); + }); + }); + }); + }); + + // Test afterRemoveClass and afterAddClass + const classListAgain = await getClassList(square); + expect(classListAgain.includes('hello-world')).toEqual(true); + expect(classListAgain.includes('test-class')).toEqual(false); + + // Test afterStyles and afterClearStyles + const stylesAgain = await getStyles(page, '.square-a'); + expect(stylesAgain.paddingBottom).toEqual('20px'); + expect(stylesAgain.color).toEqual('rgb(0, 0, 0)'); + + screenshotCompares.push(await page.compareScreenshot()); +}); + +test(`animation:css: hooks`, async () => { + const page = await newE2EPage({ url: '/src/utils/animation/test/hooks?ionic:_forceCSSAnimations=true' }); + const screenshotCompares = []; + + screenshotCompares.push(await page.compareScreenshot()); + + const square = await page.$('.square-a'); + + const styles = await getStyles(page, '.square-a'); + expect(styles.paddingBottom).toEqual('20px'); + expect(styles.color).toEqual('rgb(0, 0, 0)'); + + const classList = await getClassList(square); + expect(classList.includes('hello-world')).toEqual(true); + expect(classList.includes('test-class')).toEqual(false); + + await waitForEventToBeCalled('afterWrite', page, square, async () => { + await waitForEventToBeCalled('afterRead', page, square, async () => { + await waitForEventToBeCalled('ionAnimationFinished', page, square, async () => { + await waitForEventToBeCalled('beforeWrite', page, square, async () => { + await waitForEventToBeCalled('beforeRead', page, square, async () => { + await page.click('.play'); + await page.waitForSelector('.play'); + + // Test beforeRemoveClass and beforeAddClass + const cssClassListAgain = await getClassList(square); + expect(cssClassListAgain.includes('hello-world')).toEqual(false); + expect(cssClassListAgain.includes('test-class')).toEqual(true); + + // Test beforeStyles and beforeClearStyles + const cssStylesAgain = await getStyles(page, '.square-a'); + expect(cssStylesAgain.paddingBottom).toEqual('0px'); + expect(cssStylesAgain.color).toEqual('rgb(128, 0, 128)'); + }); + }); + }); + }); + }); + + // Test afterRemoveClass and afterAddClass + const classListAgain = await getClassList(square); + expect(classListAgain.includes('hello-world')).toEqual(true); + expect(classListAgain.includes('test-class')).toEqual(false); + + // Test afterStyles and afterClearStyles + const stylesAgain = await getStyles(page, '.square-a'); + expect(stylesAgain.paddingBottom).toEqual('20px'); + expect(stylesAgain.color).toEqual('rgb(0, 0, 0)'); + + screenshotCompares.push(await page.compareScreenshot()); +}); + +const waitForEventToBeCalled = (eventName: string, page: any, el: HTMLElement, fn: any, num = 1) => { + return new Promise(async resolve => { + const EVENT_FIRED = `on${eventName}`; + const eventFiredCount: any = { count: 0 }; + await page.exposeFunction(EVENT_FIRED, () => { + eventFiredCount.count += 1; + }); + + await listenForEvent(page, eventName, el, EVENT_FIRED); + + if (fn) { + await fn(); + } + + await waitForFunctionTestContext((payload: any) => { + return payload.eventFiredCount.count === payload.num; + }, { eventFiredCount, num }); + + return resolve(); + }); +}; + +const getStyles = async (page: any, selector: string) => { + return page.evaluate((payload: any) => { + const el = document.querySelector(payload.selector); + + return JSON.parse(JSON.stringify(getComputedStyle(el))); + }, { selector }); +}; + +const getClassList = async (el: HTMLElement) => { + const classListObject = await el.getProperty('classList'); + const jsonValue = await classListObject.jsonValue(); + + return Object.values(jsonValue); +}; diff --git a/core/src/utils/animation/test/hooks/index.html b/core/src/utils/animation/test/hooks/index.html new file mode 100644 index 0000000000..4d296285a7 --- /dev/null +++ b/core/src/utils/animation/test/hooks/index.html @@ -0,0 +1,121 @@ + + + + + + Animation - Basic + + + + + + + + + + + + + + + Animations + + + + +
+ Play + Pause + Stop + Destroy + +
+ Hello +
+
+
+ + + + + diff --git a/core/src/utils/animation/test/multiple/e2e.ts b/core/src/utils/animation/test/multiple/e2e.ts new file mode 100644 index 0000000000..dbaee98ce5 --- /dev/null +++ b/core/src/utils/animation/test/multiple/e2e.ts @@ -0,0 +1,53 @@ +import { newE2EPage } from '@stencil/core/testing'; + +import { listenForEvent, waitForFunctionTestContext } from '../../../test/utils'; + +test(`animation:web: multiple`, async () => { + const page = await newE2EPage({ url: '/src/utils/animation/test/multiple' }); + const screenshotCompares = []; + + screenshotCompares.push(await page.compareScreenshot()); + + const ANIMATION_FINISHED = 'onIonAnimationFinished'; + const animationStatus = []; + await page.exposeFunction(ANIMATION_FINISHED, (ev: any) => { + animationStatus.push(ev.detail); + }); + + const squareA = await page.$('.square-a'); + await listenForEvent(page, 'ionAnimationFinished', squareA, ANIMATION_FINISHED); + + await page.click('.play'); + await page.waitForSelector('.play'); + + await waitForFunctionTestContext((payload: any) => { + return payload.animationStatus.join(', ') === ['AnimationCSubBFinished', 'AnimationBFinished', 'AnimationCSubAFinished', 'AnimationCFinished', 'AnimationAFinished', 'AnimationRootFinished'].join(', '); + + }, { animationStatus }); + screenshotCompares.push(await page.compareScreenshot()); +}); + +test(`animation:css: multiple`, async () => { + const page = await newE2EPage({ url: '/src/utils/animation/test/multiple?ionic:_forceCSSAnimations=true' }); + const screenshotCompares = []; + + screenshotCompares.push(await page.compareScreenshot()); + + const ANIMATION_FINISHED = 'onIonAnimationFinished'; + const animationStatus = []; + await page.exposeFunction(ANIMATION_FINISHED, (ev: any) => { + animationStatus.push(ev.detail); + }); + + const squareA = await page.$('.square-a'); + await listenForEvent(page, 'ionAnimationFinished', squareA, ANIMATION_FINISHED); + + await page.click('.play'); + await page.waitForSelector('.play'); + + await waitForFunctionTestContext((payload: any) => { + return payload.animationStatus.join(', ') === ['AnimationCSubBFinished', 'AnimationBFinished', 'AnimationCSubAFinished', 'AnimationCFinished', 'AnimationAFinished', 'AnimationRootFinished'].join(', '); + + }, { animationStatus }); + screenshotCompares.push(await page.compareScreenshot()); +}); diff --git a/core/src/utils/animation/test/multiple/index.html b/core/src/utils/animation/test/multiple/index.html new file mode 100644 index 0000000000..51f324a61c --- /dev/null +++ b/core/src/utils/animation/test/multiple/index.html @@ -0,0 +1,207 @@ + + + + + + Animation - Basic + + + + + + + + + + + + + + + Animations + + + + +
+ Play + Pause + Stop + Destroy + +
+
Hello
+
+ +
+
Hello
+
+ +
+
Hello
+
+
+
+
+ + + + + diff --git a/core/src/utils/animation/test/reuse/index.html b/core/src/utils/animation/test/reuse/index.html new file mode 100644 index 0000000000..19448bf963 --- /dev/null +++ b/core/src/utils/animation/test/reuse/index.html @@ -0,0 +1,77 @@ + + + + + + Animation - Reuse + + + + + + + + + + + + + + + Animations + + + + +
+
+ Hello +
+
+ Hello +
+
+
+ + + + + diff --git a/core/src/utils/gesture/index.ts b/core/src/utils/gesture/index.ts index 717e5247c8..f7a2263fbf 100644 --- a/core/src/utils/gesture/index.ts +++ b/core/src/utils/gesture/index.ts @@ -1,5 +1,3 @@ -import { writeTask } from '@stencil/core'; - import { GESTURE_CONTROLLER } from './gesture-controller'; import { createPointerEvents } from './pointer-events'; import { createPanRecognizer } from './recognizers'; @@ -92,7 +90,7 @@ export const createGesture = (config: GestureConfig): Gesture => { if (!isMoveQueued && hasFiredStart) { isMoveQueued = true; calcGestureData(detail, ev); - writeTask(fireOnMove); + requestAnimationFrame(fireOnMove); } return; } diff --git a/core/src/utils/overlays-interface.ts b/core/src/utils/overlays-interface.ts index e4f3ddcda8..713d4163d1 100644 --- a/core/src/utils/overlays-interface.ts +++ b/core/src/utils/overlays-interface.ts @@ -1,7 +1,7 @@ import { EventEmitter } from '@stencil/core'; import { HTMLStencilElement } from '@stencil/core/internal'; -import { Animation, AnimationBuilder, Mode } from '../interface'; +import { Animation, AnimationBuilder, IonicAnimation, Mode } from '../interface'; export interface OverlayEventDetail { data?: T; @@ -15,7 +15,7 @@ export interface OverlayInterface { keyboardClose: boolean; overlayIndex: number; presented: boolean; - animation?: Animation; + animation?: Animation | IonicAnimation; enterAnimation?: AnimationBuilder; leaveAnimation?: AnimationBuilder; diff --git a/core/src/utils/overlays.ts b/core/src/utils/overlays.ts index 9d64289a03..1b0a732726 100644 --- a/core/src/utils/overlays.ts +++ b/core/src/utils/overlays.ts @@ -1,5 +1,8 @@ import { config } from '../global/config'; -import { ActionSheetOptions, AlertOptions, AnimationBuilder, BackButtonEvent, HTMLIonOverlayElement, IonicConfig, LoadingOptions, ModalOptions, OverlayInterface, PickerOptions, PopoverOptions, ToastOptions } from '../interface'; +import { ActionSheetOptions, AlertOptions, AnimationBuilder, BackButtonEvent, HTMLIonOverlayElement, IonicAnimation, IonicConfig, LoadingOptions, ModalOptions, OverlayInterface, PickerOptions, PopoverOptions, ToastOptions } from '../interface'; + +// TODO: Remove when removing AnimationBuilder +export type IonicAnimationInterface = (baseEl: any, opts: any) => IonicAnimation; let lastId = 0; @@ -111,8 +114,8 @@ export const getOverlay = (doc: Document, overlayTag?: string, id?: string): HTM export const present = async ( overlay: OverlayInterface, name: keyof IonicConfig, - iosEnterAnimation: AnimationBuilder, - mdEnterAnimation: AnimationBuilder, + iosEnterAnimation: AnimationBuilder | IonicAnimationInterface, + mdEnterAnimation: AnimationBuilder | IonicAnimationInterface, opts?: any ) => { if (overlay.presented) { @@ -137,8 +140,8 @@ export const dismiss = async ( data: any | undefined, role: string | undefined, name: keyof IonicConfig, - iosLeaveAnimation: AnimationBuilder, - mdLeaveAnimation: AnimationBuilder, + iosLeaveAnimation: AnimationBuilder | IonicAnimationInterface, + mdLeaveAnimation: AnimationBuilder | IonicAnimationInterface, opts?: any ): Promise => { if (!overlay.presented) { @@ -170,7 +173,7 @@ const getAppRoot = (doc: Document) => { const overlayAnimation = async ( overlay: OverlayInterface, - animationBuilder: AnimationBuilder, + animationBuilder: AnimationBuilder | IonicAnimationInterface, baseEl: any, opts: any ): Promise => { @@ -183,7 +186,17 @@ const overlayAnimation = async ( baseEl.classList.remove('overlay-hidden'); const aniRoot = baseEl.shadowRoot || overlay.el; - const animation = await import('./animation').then(mod => mod.create(animationBuilder, aniRoot, opts)); + + /** + * TODO: Remove AnimationBuilder + */ + const animation = await import('./animation/old-animation').then(mod => mod.create(animationBuilder as AnimationBuilder, aniRoot, opts)); + const isAnimationBuilder = (animation as any).fill === undefined; + + if (!isAnimationBuilder) { + (animation as any).fill('both'); + } + overlay.animation = animation; if (!overlay.animated || !config.getBoolean('animated', true)) { animation.duration(0); @@ -196,9 +209,16 @@ const overlayAnimation = async ( } }); } - await animation.playAsync(); - const hasCompleted = animation.hasCompleted; - animation.destroy(); + const animationResult = await animation.playAsync(); + + /** + * TODO: Remove AnimationBuilder + */ + const hasCompleted = (typeof animationResult as any === 'boolean') ? animationResult : (animation as any).hasCompleted; + if (isAnimationBuilder) { + animation.destroy(); + } + overlay.animation = undefined; return hasCompleted; }; diff --git a/core/src/utils/test/utils.ts b/core/src/utils/test/utils.ts index 6ecb7aa31a..9cba4a1b98 100644 --- a/core/src/utils/test/utils.ts +++ b/core/src/utils/test/utils.ts @@ -35,7 +35,7 @@ export const listenForEvent = async (page: any, eventType: string, element: any, try { return await page.evaluate((scopeEventType: string, scopeElement: any, scopeCallbackName: string) => { scopeElement.addEventListener(scopeEventType, (e: any) => { - (window as any)[scopeCallbackName](e); + (window as any)[scopeCallbackName]({ detail: e.detail }); }); }, eventType, element, callbackName); } catch (err) { diff --git a/core/src/utils/transition/index.ts b/core/src/utils/transition/index.ts index 4168455c84..2f8f940359 100644 --- a/core/src/utils/transition/index.ts +++ b/core/src/utils/transition/index.ts @@ -1,11 +1,14 @@ import { writeTask } from '@stencil/core'; import { LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE, LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE } from '../../components/nav/constants'; -import { Animation, AnimationBuilder, NavDirection, NavOptions } from '../../interface'; +import { Animation, AnimationBuilder, IonicAnimation, NavDirection, NavOptions } from '../../interface'; const iosTransitionAnimation = () => import('./ios.transition'); const mdTransitionAnimation = () => import('./md.transition'); +// TODO: Remove when removing AnimationBuilder +export type IonicAnimationInterface = (navEl: HTMLElement, opts: TransitionOptions) => IonicAnimation; + export const transition = (opts: TransitionOptions): Promise => { return new Promise((resolve, reject) => { writeTask(() => { @@ -43,6 +46,7 @@ const beforeTransition = (opts: TransitionOptions) => { const runTransition = async (opts: TransitionOptions): Promise => { const animationBuilder = await getAnimationBuilder(opts); + const ani = (animationBuilder) ? animation(animationBuilder, opts) : noAnimation(opts); // fast path for no animation @@ -59,35 +63,50 @@ const afterTransition = (opts: TransitionOptions) => { } }; -const getAnimationBuilder = async (opts: TransitionOptions): Promise => { +const getAnimationBuilder = async (opts: TransitionOptions): Promise => { if (!opts.leavingEl || !opts.animated || opts.duration === 0) { return undefined; } + if (opts.animationBuilder) { return opts.animationBuilder; } - const builder = (opts.mode === 'ios') + + const getAnimation = (opts.mode === 'ios') ? (await iosTransitionAnimation()).iosTransitionAnimation : (await mdTransitionAnimation()).mdTransitionAnimation; - return builder; + return getAnimation; }; -const animation = async (animationBuilder: AnimationBuilder, opts: TransitionOptions): Promise => { +const animation = async (animationBuilder: IonicAnimationInterface | AnimationBuilder, opts: TransitionOptions): Promise => { await waitForReady(opts, true); - const trans = await import('../animation').then(mod => mod.create(animationBuilder, opts.baseEl, opts)); + let trans: Animation | IonicAnimation; + + try { + trans = await import('../animation/old-animation').then(mod => mod.create(animationBuilder as AnimationBuilder, opts.baseEl, opts)); + } catch (err) { + trans = (animationBuilder as IonicAnimationInterface)(opts.baseEl, opts); + } + fireWillEvents(opts.enteringEl, opts.leavingEl); - await playTransition(trans, opts); + + const didComplete = await playTransition(trans, opts); + + // TODO: Remove AnimationBuilder + (trans as any).hasCompleted = didComplete; + if (opts.progressCallback) { opts.progressCallback(undefined); } - if (trans.hasCompleted) { + if ((trans as any).hasCompleted) { fireDidEvents(opts.enteringEl, opts.leavingEl); } + return { - hasCompleted: trans.hasCompleted, + hasCompleted: (trans as any).hasCompleted, animation: trans }; }; @@ -126,15 +145,17 @@ const notifyViewReady = async (viewIsReady: undefined | ((enteringEl: HTMLElemen } }; -const playTransition = (trans: Animation, opts: TransitionOptions): Promise => { +const playTransition = (trans: IonicAnimation | Animation, opts: TransitionOptions): Promise => { const progressCallback = opts.progressCallback; - const promise = new Promise(resolve => trans.onFinish(resolve)); + + // TODO: Remove AnimationBuilder + const promise = new Promise(resolve => trans.onFinish(resolve)); // cool, let's do this, start the transition if (progressCallback) { // this is a swipe to go back, just get the transition progress ready // kick off the swipe animation start - trans.progressStart(); + trans.progressStart(true); progressCallback(trans); } else { @@ -214,7 +235,7 @@ const setZIndex = ( }; export interface TransitionOptions extends NavOptions { - progressCallback?: ((ani: Animation | undefined) => void); + progressCallback?: ((ani: IonicAnimation | Animation | undefined) => void); baseEl: any; enteringEl: HTMLElement; leavingEl: HTMLElement | undefined; @@ -222,5 +243,5 @@ export interface TransitionOptions extends NavOptions { export interface TransitionResult { hasCompleted: boolean; - animation?: Animation; + animation?: Animation | IonicAnimation; } diff --git a/core/src/utils/transition/ios.transition.ts b/core/src/utils/transition/ios.transition.ts index 919b3ec4b1..a934967ce6 100644 --- a/core/src/utils/transition/ios.transition.ts +++ b/core/src/utils/transition/ios.transition.ts @@ -1,78 +1,78 @@ -import { Animation } from '../../interface'; +import { IonicAnimation } from '../../interface'; +import { createAnimation } from '../animation/animation'; import { TransitionOptions } from '../transition'; export const shadow = (el: T): ShadowRoot | T => { return el.shadowRoot || el; }; -export const iosTransitionAnimation = (AnimationC: Animation, navEl: HTMLElement, opts: TransitionOptions): Promise => { +export const iosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptions): IonicAnimation => { + try { + const DURATION = 540; + const EASING = 'cubic-bezier(0.32,0.72,0,1)'; + const OPACITY = 'opacity'; + const TRANSFORM = 'transform'; + const CENTER = '0%'; + const OFF_OPACITY = 0.8; - const DURATION = 540; - const EASING = 'cubic-bezier(0.32,0.72,0,1)'; - const OPACITY = 'opacity'; - const TRANSFORM = 'transform'; - const TRANSLATEX = 'translateX'; - const CENTER = '0%'; - const OFF_OPACITY = 0.8; + const isRTL = (navEl.ownerDocument as any).dir === 'rtl'; + const OFF_RIGHT = isRTL ? '-99.5%' : '99.5%'; + const OFF_LEFT = isRTL ? '33%' : '-33%'; - const backDirection = (opts.direction === 'back'); - const isRTL = (navEl.ownerDocument as any).dir === 'rtl'; - const OFF_RIGHT = isRTL ? '-99.5%' : '99.5%'; - const OFF_LEFT = isRTL ? '33%' : '-33%'; + const enteringEl = opts.enteringEl; + const leavingEl = opts.leavingEl; - const enteringEl = opts.enteringEl; - const leavingEl = opts.leavingEl; - const contentEl = enteringEl.querySelector(':scope > ion-content'); - const headerEls = enteringEl.querySelectorAll(':scope > ion-header > *:not(ion-toolbar), :scope > ion-footer > *'); - const enteringToolBarEls = enteringEl.querySelectorAll(':scope > ion-header > ion-toolbar'); - const enteringContent = new AnimationC(); + const backDirection = (opts.direction === 'back'); + const contentEl = enteringEl.querySelector(':scope > ion-content'); + const headerEls = enteringEl.querySelectorAll(':scope > ion-header > *:not(ion-toolbar), :scope > ion-footer > *'); + const enteringToolBarEls = enteringEl.querySelectorAll(':scope > ion-header > ion-toolbar'); - const rootTransition = new AnimationC(); - rootTransition - .addElement(enteringEl) - .duration(opts.duration || DURATION) - .easing(opts.easing || EASING) - .beforeRemoveClass('ion-page-invisible'); + const rootAnimation = createAnimation(); + const enteringContentAnimation = createAnimation(); - if (leavingEl && navEl) { - const navDecor = new AnimationC(); - navDecor - .addElement(navEl); + rootAnimation + .addElement(enteringEl) + .duration(opts.duration || DURATION) + .easing(opts.easing || EASING) + .fill('both') + .beforeRemoveClass('ion-page-invisible'); - rootTransition.add(navDecor); - } + if (leavingEl && navEl) { + const navDecorAnimation = createAnimation(); + navDecorAnimation.addElement(navEl); + rootAnimation.addAnimation(navDecorAnimation); + } - if (!contentEl && enteringToolBarEls.length === 0 && headerEls.length === 0) { - enteringContent.addElement(enteringEl.querySelector(':scope > .ion-page, :scope > ion-nav, :scope > ion-tabs')); - } else { - enteringContent - .addElement(contentEl) - .addElement(headerEls); - } + if (!contentEl && enteringToolBarEls.length === 0 && headerEls.length === 0) { + enteringContentAnimation.addElement(enteringEl.querySelector(':scope > .ion-page, :scope > ion-nav, :scope > ion-tabs')); + } else { + enteringContentAnimation.addElement(contentEl); + enteringContentAnimation.addElement(headerEls); + } - rootTransition.add(enteringContent); + rootAnimation.addAnimation(enteringContentAnimation); - if (backDirection) { - enteringContent - .beforeClearStyles([OPACITY]) - .fromTo(TRANSLATEX, OFF_LEFT, CENTER, true) - .fromTo(OPACITY, OFF_OPACITY, 1, true); - } else { - // entering content, forward direction - enteringContent - .beforeClearStyles([OPACITY]) - .fromTo(TRANSLATEX, OFF_RIGHT, CENTER, true); + if (backDirection) { + enteringContentAnimation + .beforeClearStyles([OPACITY]) + .fromTo('transform', `translateX(${OFF_LEFT})`, `translateX(${CENTER})`) + .fromTo(OPACITY, OFF_OPACITY, 1); + } else { + // entering content, forward direction + enteringContentAnimation + .beforeClearStyles([OPACITY]) + .fromTo('transform', `translateX(${OFF_RIGHT})`, `translateX(${CENTER})`); + } if (contentEl) { const enteringTransitionEffectEl = shadow(contentEl).querySelector('.transition-effect'); - if (enteringTransitionEffectEl) { const enteringTransitionCoverEl = enteringTransitionEffectEl.querySelector('.transition-cover'); const enteringTransitionShadowEl = enteringTransitionEffectEl.querySelector('.transition-shadow'); - const enteringTransitionEffect = new AnimationC(); - const enteringTransitionCover = new AnimationC(); - const enteringTransitionShadow = new AnimationC(); + const enteringTransitionEffect = createAnimation(); + const enteringTransitionCover = createAnimation(); + const enteringTransitionShadow = createAnimation(); enteringTransitionEffect .addElement(enteringTransitionEffectEl) @@ -82,113 +82,111 @@ export const iosTransitionAnimation = (AnimationC: Animation, navEl: HTMLElement enteringTransitionCover .addElement(enteringTransitionCoverEl) .beforeClearStyles([OPACITY]) - .fromTo(OPACITY, 0, 0.1, true); + .fromTo(OPACITY, 0, 0.1); enteringTransitionShadow .addElement(enteringTransitionShadowEl) .beforeClearStyles([OPACITY]) - .fromTo(OPACITY, 0.70, 0.03, true); + .fromTo(OPACITY, 0.03, 0.70); - enteringContent - .add(enteringTransitionEffect) - .add(enteringTransitionCover) - .add(enteringTransitionShadow); + enteringTransitionEffect.addAnimation([enteringTransitionCover, enteringTransitionShadow]); + enteringContentAnimation.addAnimation([enteringTransitionEffect]); } } - } - enteringToolBarEls.forEach(enteringToolBarEl => { - const enteringToolBar = new AnimationC(); - const enteringTitle = new AnimationC(); - const enteringToolBarButtons = new AnimationC(); - const enteringToolBarItems = new AnimationC(); - const enteringToolBarBg = new AnimationC(); - const enteringBackButton = new AnimationC(); - const backButtonEl = enteringToolBarEl.querySelector('ion-back-button'); + enteringToolBarEls.forEach(enteringToolBarEl => { + const enteringToolBar = createAnimation(); + enteringToolBar.addElement(enteringToolBarEl); + rootAnimation.addAnimation(enteringToolBar); - enteringToolBar.addElement(enteringToolBarEl); - rootTransition.add(enteringToolBar); + const enteringTitle = createAnimation(); + enteringTitle.addElement(enteringToolBarEl.querySelector('ion-title')); - enteringTitle.addElement(enteringToolBarEl.querySelector('ion-title')); + const enteringToolBarButtons = createAnimation(); + enteringToolBarButtons.addElement(enteringToolBarEl.querySelectorAll('ion-buttons,[menuToggle]')); - enteringToolBarButtons.addElement(enteringToolBarEl.querySelectorAll('ion-buttons,[menuToggle]')); + const enteringToolBarItems = createAnimation(); + enteringToolBarItems.addElement(enteringToolBarEl.querySelectorAll(':scope > *:not(ion-title):not(ion-buttons):not([menuToggle])')); - enteringToolBarItems.addElement(enteringToolBarEl.querySelectorAll(':scope > *:not(ion-title):not(ion-buttons):not([menuToggle])')); + const enteringToolBarBg = createAnimation(); + enteringToolBarBg.addElement(shadow(enteringToolBarEl).querySelector('.toolbar-background')); - enteringToolBarBg.addElement(shadow(enteringToolBarEl).querySelector('.toolbar-background')); - - if (backButtonEl) { - enteringBackButton.addElement(backButtonEl); - } - - enteringToolBar - .add(enteringTitle) - .add(enteringToolBarButtons) - .add(enteringToolBarItems) - .add(enteringToolBarBg) - .add(enteringBackButton); - - enteringTitle.fromTo(OPACITY, 0.01, 1, true); - enteringToolBarButtons.fromTo(OPACITY, 0.01, 1, true); - enteringToolBarItems.fromTo(OPACITY, 0.01, 1, true); - - if (backDirection) { - enteringTitle.fromTo(TRANSLATEX, OFF_LEFT, CENTER, true); - - enteringToolBarItems.fromTo(TRANSLATEX, OFF_LEFT, CENTER, true); - - // back direction, entering page has a back button - enteringBackButton.fromTo(OPACITY, 0.01, 1, true); - } else { - // entering toolbar, forward direction - enteringTitle.fromTo(TRANSLATEX, OFF_RIGHT, CENTER, true); - - enteringToolBarItems.fromTo(TRANSLATEX, OFF_RIGHT, CENTER, true); - - enteringToolBarBg - .beforeClearStyles([OPACITY]) - .fromTo(OPACITY, 0.01, 1, true); - - // forward direction, entering page has a back button - enteringBackButton.fromTo(OPACITY, 0.01, 1, true); + const enteringBackButton = createAnimation(); + const backButtonEl = enteringToolBarEl.querySelector('ion-back-button'); if (backButtonEl) { - const enteringBackBtnText = new AnimationC(); - enteringBackBtnText - .addElement(shadow(backButtonEl).querySelector('.button-text')) - .fromTo(TRANSLATEX, (isRTL ? '-100px' : '100px'), '0px'); - - enteringToolBar.add(enteringBackBtnText); + enteringBackButton.addElement(backButtonEl); } - } - }); - // setup leaving view - if (leavingEl) { - const leavingContent = new AnimationC(); - const leavingContentEl = leavingEl.querySelector(':scope > ion-content'); + enteringToolBar.addAnimation([enteringTitle, enteringToolBarButtons, enteringToolBarItems, enteringToolBarBg, enteringBackButton]); + enteringTitle.fromTo(OPACITY, 0.01, 1); + enteringToolBarButtons.fromTo(OPACITY, 0.01, 1); + enteringToolBarItems.fromTo(OPACITY, 0.01, 1); - leavingContent - .addElement(leavingContentEl) - .addElement(leavingEl.querySelectorAll(':scope > ion-header > *:not(ion-toolbar), :scope > ion-footer > *')); + if (backDirection) { + enteringTitle.fromTo('transform', `translateX(${OFF_LEFT})`, `translateX(${CENTER})`); - rootTransition.add(leavingContent); + enteringToolBarItems.fromTo('transform', `translateX(${OFF_LEFT})`, `translateX(${CENTER})`); - if (backDirection) { - // leaving content, back direction - leavingContent - .beforeClearStyles([OPACITY]) - .fromTo(TRANSLATEX, CENTER, (isRTL ? '-100%' : '100%')); + // back direction, entering page has a back button + enteringBackButton.fromTo(OPACITY, 0.01, 1); + } else { + // entering toolbar, forward direction + enteringTitle.fromTo('transform', `translateX(${OFF_RIGHT})`, `translateX(${CENTER})`); + + enteringToolBarItems.fromTo('transform', `translateX(${OFF_RIGHT})`, `translateX(${CENTER})`); + + enteringToolBarBg + .beforeClearStyles([OPACITY]) + .fromTo(OPACITY, 0.01, 1); + + // forward direction, entering page has a back button + enteringBackButton.fromTo(OPACITY, 0.01, 1); + + if (backButtonEl) { + const enteringBackBtnText = createAnimation(); + enteringBackBtnText + .addElement(shadow(backButtonEl).querySelector('.button-text')) + .fromTo(`transform`, (isRTL ? 'translateX(-100px)' : 'translateX(100px)'), 'translateX(0px)'); + + enteringToolBar.addAnimation(enteringBackBtnText); + } + } + }); + + // setup leaving view + if (leavingEl) { + + const leavingContent = createAnimation(); + const leavingContentEl = leavingEl.querySelector(':scope > ion-content'); + + leavingContent.addElement(leavingContentEl); + leavingContent.addElement(leavingEl.querySelectorAll(':scope > ion-header > *:not(ion-toolbar), :scope > ion-footer > *')); + rootAnimation.addAnimation(leavingContent); + + if (backDirection) { + // leaving content, back direction + leavingContent + .beforeClearStyles([OPACITY]) + .fromTo('transform', `translateX(${CENTER})`, (isRTL ? 'translateX(-100%)' : 'translateX(100%)')); + + } else { + // leaving content, forward direction + leavingContent + .fromTo('transform', `translateX(${CENTER})`, `translateX(${OFF_LEFT})`) + .fromTo(OPACITY, 1, OFF_OPACITY); + } if (leavingContentEl) { const leavingTransitionEffectEl = shadow(leavingContentEl).querySelector('.transition-effect'); + if (leavingTransitionEffectEl) { const leavingTransitionCoverEl = leavingTransitionEffectEl.querySelector('.transition-cover'); const leavingTransitionShadowEl = leavingTransitionEffectEl.querySelector('.transition-shadow'); - const leavingTransitionEffect = new AnimationC(); - const leavingTransitionCover = new AnimationC(); - const leavingTransitionShadow = new AnimationC(); + const leavingTransitionEffect = createAnimation(); + const leavingTransitionCover = createAnimation(); + const leavingTransitionShadow = createAnimation(); leavingTransitionEffect .addElement(leavingTransitionEffectEl) @@ -198,104 +196,89 @@ export const iosTransitionAnimation = (AnimationC: Animation, navEl: HTMLElement leavingTransitionCover .addElement(leavingTransitionCoverEl) .beforeClearStyles([OPACITY]) - .fromTo(OPACITY, 0.1, 0, true); + .fromTo(OPACITY, 0.1, 0); leavingTransitionShadow .addElement(leavingTransitionShadowEl) .beforeClearStyles([OPACITY]) - .fromTo(OPACITY, 0.70, 0.03, true); + .fromTo(OPACITY, 0.70, 0.03); - leavingContent - .add(leavingTransitionEffect) - .add(leavingTransitionCover) - .add(leavingTransitionShadow); + leavingTransitionEffect.addAnimation([leavingTransitionCover, leavingTransitionShadow]); + leavingContent.addAnimation([leavingTransitionEffect]); } } - } else { - // leaving content, forward direction - leavingContent - .fromTo(TRANSLATEX, CENTER, OFF_LEFT, true) - .fromTo(OPACITY, 1, OFF_OPACITY, true); + const leavingToolBarEls = leavingEl.querySelectorAll(':scope > ion-header > ion-toolbar'); + leavingToolBarEls.forEach(leavingToolBarEl => { + const leavingToolBar = createAnimation(); + leavingToolBar.addElement(leavingToolBarEl); + + const leavingTitle = createAnimation(); + leavingTitle.addElement(leavingToolBarEl.querySelector('ion-title')); + + const leavingToolBarButtons = createAnimation(); + leavingToolBarButtons.addElement(leavingToolBarEl.querySelectorAll('ion-buttons,[menuToggle]')); + + const leavingToolBarItems = createAnimation(); + const leavingToolBarItemEls = leavingToolBarEl.querySelectorAll(':scope > *:not(ion-title):not(ion-buttons):not([menuToggle])'); + if (leavingToolBarItemEls.length > 0) { + leavingToolBarItems.addElement(leavingToolBarItemEls); + } + + const leavingToolBarBg = createAnimation(); + leavingToolBarBg.addElement(shadow(leavingToolBarEl).querySelector('.toolbar-background')); + + const leavingBackButton = createAnimation(); + const backButtonEl = leavingToolBarEl.querySelector('ion-back-button'); + if (backButtonEl) { + leavingBackButton.addElement(backButtonEl); + } + + leavingToolBar.addAnimation([leavingTitle, leavingToolBarButtons, leavingToolBarItems, leavingBackButton, leavingToolBarBg]); + rootAnimation.addAnimation(leavingToolBar); + + // fade out leaving toolbar items + leavingBackButton.fromTo(OPACITY, 0.99, 0); + leavingTitle.fromTo(OPACITY, 0.99, 0); + leavingToolBarButtons.fromTo(OPACITY, 0.99, 0); + leavingToolBarItems.fromTo(OPACITY, 0.99, 0); + + if (backDirection) { + // leaving toolbar, back direction + leavingTitle.fromTo('transform', `translateX(${CENTER})`, (isRTL ? 'translateX(-100%)' : 'translateX(100%)')); + leavingToolBarItems.fromTo('transform', `translateX(${CENTER})`, (isRTL ? 'translateX(-100%)' : 'translateX(100%)')); + + // leaving toolbar, back direction, and there's no entering toolbar + // should just slide out, no fading out + leavingToolBarBg + .beforeClearStyles([OPACITY]) + .fromTo(OPACITY, 1, 0.01); + + if (backButtonEl) { + const leavingBackBtnText = createAnimation(); + leavingBackBtnText.addElement(shadow(backButtonEl).querySelector('.button-text')); + leavingBackBtnText.fromTo('transform', `translateX(${CENTER})`, `translateX(${(isRTL ? -124 : 124) + 'px'})`); + leavingToolBar.addAnimation(leavingBackBtnText); + } + + } else { + // leaving toolbar, forward direction + leavingTitle + .fromTo('transform', `translateX(${CENTER})`, `translateX(${OFF_LEFT})`) + .afterClearStyles([TRANSFORM]); + leavingToolBarItems + .fromTo('transform', `translateX(${CENTER})`, `translateX(${OFF_LEFT})`) + .afterClearStyles([TRANSFORM, OPACITY]); + + leavingBackButton.afterClearStyles([OPACITY]); + leavingTitle.afterClearStyles([OPACITY]); + leavingToolBarButtons.afterClearStyles([OPACITY]); + } + }); } - const leavingToolBarEls = leavingEl.querySelectorAll(':scope > ion-header > ion-toolbar'); - leavingToolBarEls.forEach(leavingToolBarEl => { - const leavingToolBar = new AnimationC(); - const leavingTitle = new AnimationC(); - const leavingToolBarButtons = new AnimationC(); - const leavingToolBarItems = new AnimationC(); - const leavingToolBarItemEls = leavingToolBarEl.querySelectorAll(':scope > *:not(ion-title):not(ion-buttons):not([menuToggle])'); - const leavingToolBarBg = new AnimationC(); - const leavingBackButton = new AnimationC(); - const backButtonEl = leavingToolBarEl.querySelector('ion-back-button'); - - leavingToolBar.addElement(leavingToolBarEl); - - leavingTitle.addElement(leavingToolBarEl.querySelector('ion-title')); - - leavingToolBarButtons.addElement(leavingToolBarEl.querySelectorAll('ion-buttons,[menuToggle]')); - - if (leavingToolBarItemEls.length > 0) { - leavingToolBarItems.addElement(leavingToolBarItemEls); - } - - leavingToolBarBg.addElement(shadow(leavingToolBarEl).querySelector('.toolbar-background')); - - if (backButtonEl) { - leavingBackButton.addElement(backButtonEl); - } - - leavingToolBar - .add(leavingTitle) - .add(leavingToolBarButtons) - .add(leavingToolBarItems) - .add(leavingBackButton) - .add(leavingToolBarBg); - - rootTransition.add(leavingToolBar); - - // fade out leaving toolbar items - leavingBackButton.fromTo(OPACITY, 0.99, 0); - leavingTitle.fromTo(OPACITY, 0.99, 0); - leavingToolBarButtons.fromTo(OPACITY, 0.99, 0, 0); - leavingToolBarItems.fromTo(OPACITY, 0.99, 0); - - if (backDirection) { - // leaving toolbar, back direction - leavingTitle.fromTo(TRANSLATEX, CENTER, (isRTL ? '-100%' : '100%')); - leavingToolBarItems.fromTo(TRANSLATEX, CENTER, (isRTL ? '-100%' : '100%')); - - // leaving toolbar, back direction, and there's no entering toolbar - // should just slide out, no fading out - leavingToolBarBg - .beforeClearStyles([OPACITY]) - .fromTo(OPACITY, 1, 0.01); - - if (backButtonEl) { - const leavingBackBtnText = new AnimationC(); - leavingBackBtnText.addElement(shadow(backButtonEl).querySelector('.button-text')); - leavingBackBtnText.fromTo(TRANSLATEX, CENTER, (isRTL ? -124 : 124) + 'px'); - leavingToolBar.add(leavingBackBtnText); - } - - } else { - // leaving toolbar, forward direction - leavingTitle - .fromTo(TRANSLATEX, CENTER, OFF_LEFT) - .afterClearStyles([TRANSFORM]); - - leavingToolBarItems - .fromTo(TRANSLATEX, CENTER, OFF_LEFT) - .afterClearStyles([TRANSFORM, OPACITY]); - - leavingBackButton.afterClearStyles([OPACITY]); - leavingTitle.afterClearStyles([OPACITY]); - leavingToolBarButtons.afterClearStyles([OPACITY]); - } - }); + return rootAnimation; + } catch (err) { + throw err; } - - // Return the rootTransition promise - return Promise.resolve(rootTransition); }; diff --git a/core/src/utils/transition/md.transition.ts b/core/src/utils/transition/md.transition.ts index bb256e7492..17d9d937cb 100644 --- a/core/src/utils/transition/md.transition.ts +++ b/core/src/utils/transition/md.transition.ts @@ -1,17 +1,18 @@ -import { Animation } from '../../interface'; +import { IonicAnimation } from '../../interface'; +import { createAnimation } from '../animation/animation'; import { TransitionOptions } from '../transition'; -export const mdTransitionAnimation = (AnimationC: Animation, _: HTMLElement, opts: TransitionOptions): Promise => { - const TRANSLATEY = 'translateY'; +export const mdTransitionAnimation = (_: HTMLElement, opts: TransitionOptions): IonicAnimation => { const OFF_BOTTOM = '40px'; const CENTER = '0px'; const backDirection = (opts.direction === 'back'); const enteringEl = opts.enteringEl; const leavingEl = opts.leavingEl; + const ionPageElement = getIonPageElement(enteringEl); const enteringToolbarEle = ionPageElement.querySelector('ion-toolbar'); - const rootTransition = new AnimationC(); + const rootTransition = createAnimation(); rootTransition .addElement(ionPageElement) @@ -27,15 +28,15 @@ export const mdTransitionAnimation = (AnimationC: Animation, _: HTMLElement, opt rootTransition .duration(opts.duration || 280) .easing('cubic-bezier(0.36,0.66,0.04,1)') - .fromTo(TRANSLATEY, OFF_BOTTOM, CENTER, true) - .fromTo('opacity', 0.01, 1, true); + .fromTo('transform', `translateY(${OFF_BOTTOM})`, `translateY(${CENTER})`) + .fromTo('opacity', 0.01, 1); } // Animate toolbar if it's there if (enteringToolbarEle) { - const enteringToolBar = new AnimationC(); + const enteringToolBar = createAnimation(); enteringToolBar.addElement(enteringToolbarEle); - rootTransition.add(enteringToolBar); + rootTransition.addAnimation(enteringToolBar); } // setup leaving view @@ -45,16 +46,16 @@ export const mdTransitionAnimation = (AnimationC: Animation, _: HTMLElement, opt .duration(opts.duration || 200) .easing('cubic-bezier(0.47,0,0.745,0.715)'); - const leavingPage = new AnimationC(); + const leavingPage = createAnimation(); leavingPage .addElement(getIonPageElement(leavingEl)) - .fromTo(TRANSLATEY, CENTER, OFF_BOTTOM) + .fromTo('transform', `translateY(${CENTER})`, `translateY(${OFF_BOTTOM})`) .fromTo('opacity', 1, 0); - rootTransition.add(leavingPage); + rootTransition.addAnimation(leavingPage); } - return Promise.resolve(rootTransition); + return rootTransition; }; const getIonPageElement = (element: HTMLElement) => {