@ionic/core
Version:
Base components for Ionic
305 lines (304 loc) • 11.1 kB
JavaScript
/*!
* (C) Ionic http://ionicframework.com - MIT License
*/
import { config } from "../../global/config";
import { Build, writeTask } from "@stencil/core";
import { LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE, LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE, } from "../../components/nav/constants";
import { createFocusController } from "../focus-controller";
import { raf } from "../helpers";
const iosTransitionAnimation = () => import('./ios.transition');
const mdTransitionAnimation = () => import('./md.transition');
const focusController = createFocusController();
// TODO(FW-2832): types
/**
* Executes the main page transition.
* It also manages the lifecycle of header visibility (if any)
* to prevent visual flickering in iOS. The flickering only
* occurs for a condensed header that is placed above the content.
*
* @param opts Options for the transition.
* @returns A promise that resolves when the transition is complete.
*/
export const transition = (opts) => {
return new Promise((resolve, reject) => {
writeTask(() => {
const transitioningInactiveHeader = getIosIonHeader(opts);
beforeTransition(opts, transitioningInactiveHeader);
runTransition(opts)
.then((result) => {
if (result.animation) {
result.animation.destroy();
}
afterTransition(opts);
resolve(result);
}, (error) => {
afterTransition(opts);
reject(error);
})
.finally(() => {
// Ensure that the header is restored to its original state.
setHeaderTransitionClass(transitioningInactiveHeader, false);
});
});
});
};
const beforeTransition = (opts, transitioningInactiveHeader) => {
const enteringEl = opts.enteringEl;
const leavingEl = opts.leavingEl;
focusController.saveViewFocus(leavingEl);
setZIndex(enteringEl, leavingEl, opts.direction);
// Prevent flickering of the header by adding a class.
setHeaderTransitionClass(transitioningInactiveHeader, true);
if (opts.showGoBack) {
enteringEl.classList.add('can-go-back');
}
else {
enteringEl.classList.remove('can-go-back');
}
setPageHidden(enteringEl, false);
/**
* When transitioning, the page should not
* respond to click events. This resolves small
* issues like users double tapping the ion-back-button.
* These pointer events are removed in `afterTransition`.
*/
enteringEl.style.setProperty('pointer-events', 'none');
if (leavingEl) {
setPageHidden(leavingEl, false);
leavingEl.style.setProperty('pointer-events', 'none');
}
};
const runTransition = async (opts) => {
const animationBuilder = await getAnimationBuilder(opts);
const ani = animationBuilder && Build.isBrowser ? animation(animationBuilder, opts) : noAnimation(opts); // fast path for no animation
return ani;
};
const afterTransition = (opts) => {
const enteringEl = opts.enteringEl;
const leavingEl = opts.leavingEl;
enteringEl.classList.remove('ion-page-invisible');
enteringEl.style.removeProperty('pointer-events');
if (leavingEl !== undefined) {
leavingEl.classList.remove('ion-page-invisible');
leavingEl.style.removeProperty('pointer-events');
}
focusController.setViewFocus(enteringEl);
};
const getAnimationBuilder = async (opts) => {
if (!opts.leavingEl || !opts.animated || opts.duration === 0) {
return undefined;
}
if (opts.animationBuilder) {
return opts.animationBuilder;
}
const getAnimation = opts.mode === 'ios'
? (await iosTransitionAnimation()).iosTransitionAnimation
: (await mdTransitionAnimation()).mdTransitionAnimation;
return getAnimation;
};
const animation = async (animationBuilder, opts) => {
await waitForReady(opts, true);
const trans = animationBuilder(opts.baseEl, opts);
fireWillEvents(opts.enteringEl, opts.leavingEl);
const didComplete = await playTransition(trans, opts);
if (opts.progressCallback) {
opts.progressCallback(undefined);
}
if (didComplete) {
fireDidEvents(opts.enteringEl, opts.leavingEl);
}
return {
hasCompleted: didComplete,
animation: trans,
};
};
const noAnimation = async (opts) => {
const enteringEl = opts.enteringEl;
const leavingEl = opts.leavingEl;
const focusManagerEnabled = config.get('focusManagerPriority', false);
/**
* If the focus manager is enabled then we need to wait for Ionic components to be
* rendered otherwise the component to focus may not be focused because it is hidden.
*/
await waitForReady(opts, focusManagerEnabled);
fireWillEvents(enteringEl, leavingEl);
fireDidEvents(enteringEl, leavingEl);
return {
hasCompleted: true,
};
};
const waitForReady = async (opts, defaultDeep) => {
const deep = opts.deepWait !== undefined ? opts.deepWait : defaultDeep;
if (deep) {
await Promise.all([deepReady(opts.enteringEl), deepReady(opts.leavingEl)]);
}
await notifyViewReady(opts.viewIsReady, opts.enteringEl);
};
const notifyViewReady = async (viewIsReady, enteringEl) => {
if (viewIsReady) {
await viewIsReady(enteringEl);
}
};
const playTransition = (trans, opts) => {
const progressCallback = opts.progressCallback;
const promise = new Promise((resolve) => {
trans.onFinish((currentStep) => resolve(currentStep === 1));
});
// 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(true);
progressCallback(trans);
}
else {
// only the top level transition should actually start "play"
// kick it off and let it play through
// ******** DOM WRITE ****************
trans.play();
}
// create a callback for when the animation is done
return promise;
};
const fireWillEvents = (enteringEl, leavingEl) => {
lifecycle(leavingEl, LIFECYCLE_WILL_LEAVE);
lifecycle(enteringEl, LIFECYCLE_WILL_ENTER);
};
const fireDidEvents = (enteringEl, leavingEl) => {
lifecycle(enteringEl, LIFECYCLE_DID_ENTER);
lifecycle(leavingEl, LIFECYCLE_DID_LEAVE);
};
export const lifecycle = (el, eventName) => {
if (el) {
const ev = new CustomEvent(eventName, {
bubbles: false,
cancelable: false,
});
el.dispatchEvent(ev);
}
};
/**
* Wait two request animation frame loops.
* This allows the framework implementations enough time to mount
* the user-defined contents. This is often needed when using inline
* modals and popovers that accept user components. For popover,
* the contents must be mounted for the popover to be sized correctly.
* For modals, the contents must be mounted for iOS to run the
* transition correctly.
*
* On Angular and React, a single raf is enough time, but for Vue
* we need to wait two rafs. As a result we are using two rafs for
* all frameworks to ensure contents are mounted.
*/
export const waitForMount = () => {
return new Promise((resolve) => raf(() => raf(() => resolve())));
};
export const deepReady = async (el) => {
const element = el;
if (element) {
if (element.componentOnReady != null) {
// eslint-disable-next-line custom-rules/no-component-on-ready-method
const stencilEl = await element.componentOnReady();
if (stencilEl != null) {
return;
}
/**
* Custom elements in Stencil will have __registerHost.
*/
}
else if (element.__registerHost != null) {
/**
* Non-lazy loaded custom elements need to wait
* one frame for component to be loaded.
*/
const waitForCustomElement = new Promise((resolve) => raf(resolve));
await waitForCustomElement;
return;
}
await Promise.all(Array.from(element.children).map(deepReady));
}
};
export const setPageHidden = (el, hidden) => {
if (hidden) {
el.setAttribute('aria-hidden', 'true');
el.classList.add('ion-page-hidden');
}
else {
el.hidden = false;
el.removeAttribute('aria-hidden');
el.classList.remove('ion-page-hidden');
}
};
const setZIndex = (enteringEl, leavingEl, direction) => {
if (enteringEl !== undefined) {
enteringEl.style.zIndex = direction === 'back' ? '99' : '101';
}
if (leavingEl !== undefined) {
leavingEl.style.zIndex = '100';
}
};
/**
* Add a class to ensure that the header (if any)
* does not flicker during the transition. By adding the
* transitioning class, we ensure that the header has
* the necessary styles to prevent the following flickers:
* 1. When entering a page with a condensed header, the
* header should never be visible. However,
* it briefly renders the background color while
* the transition is occurring.
* 2. When leaving a page with a condensed header, the
* header has an opacity of 0 and the pages
* have a z-index which causes the entering page to
* briefly show it's content underneath the leaving page.
* 3. When entering a page or leaving a page with a fade
* header, the header should not have a background color.
* However, it briefly shows the background color while
* the transition is occurring.
*
* @param header The header element to modify.
* @param isTransitioning Whether the transition is occurring.
*/
const setHeaderTransitionClass = (header, isTransitioning) => {
if (!header) {
return;
}
const transitionClass = 'header-transitioning';
if (isTransitioning) {
header.classList.add(transitionClass);
}
else {
header.classList.remove(transitionClass);
}
};
export const getIonPageElement = (element) => {
if (element.classList.contains('ion-page')) {
return element;
}
const ionPage = element.querySelector(':scope > .ion-page, :scope > ion-nav, :scope > ion-tabs');
if (ionPage) {
return ionPage;
}
// idk, return the original element so at least something animates and we don't have a null pointer
return element;
};
/**
* Retrieves the ion-header element from a page based on the
* direction of the transition.
*
* @param opts Options for the transition.
* @returns The ion-header element or null if not found or not in 'ios' mode.
*/
const getIosIonHeader = (opts) => {
const enteringEl = opts.enteringEl;
const leavingEl = opts.leavingEl;
const direction = opts.direction;
const mode = opts.mode;
if (mode !== 'ios') {
return null;
}
const element = direction === 'back' ? leavingEl : enteringEl;
if (!element) {
return null;
}
return element.querySelector('ion-header');
};