mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
419 lines
16 KiB
419 lines
16 KiB
/*!
|
|
* (C) Ionic http://ionicframework.com - MIT License
|
|
*/
|
|
import { c as config, p as printIonWarning } from './index4.js';
|
|
import { writeTask, Build } from '@stencil/core/internal/client';
|
|
import { r as raf } from './helpers.js';
|
|
|
|
const LIFECYCLE_WILL_ENTER = 'ionViewWillEnter';
|
|
const LIFECYCLE_DID_ENTER = 'ionViewDidEnter';
|
|
const LIFECYCLE_WILL_LEAVE = 'ionViewWillLeave';
|
|
const LIFECYCLE_DID_LEAVE = 'ionViewDidLeave';
|
|
const LIFECYCLE_WILL_UNLOAD = 'ionViewWillUnload';
|
|
|
|
/**
|
|
* Moves focus to a specified element. Note that we do not remove the tabindex
|
|
* because that can result in an unintentional blur. Non-focusables can't be
|
|
* focused, so the body will get focused again.
|
|
*/
|
|
const moveFocus = (el) => {
|
|
el.tabIndex = -1;
|
|
el.focus();
|
|
};
|
|
/**
|
|
* Elements that are hidden using `display: none` should not be focused even if
|
|
* they are present in the DOM.
|
|
*/
|
|
const isVisible = (el) => {
|
|
return el.offsetParent !== null;
|
|
};
|
|
/**
|
|
* The focus controller allows us to manage focus within a view so assistive
|
|
* technologies can inform users of changes to the navigation state. Traditional
|
|
* native apps have a way of informing assistive technology about a navigation
|
|
* state change. Mobile browsers have this too, but only when doing a full page
|
|
* load. In a single page app we do not do that, so we need to build this
|
|
* integration ourselves.
|
|
*/
|
|
const createFocusController = () => {
|
|
const saveViewFocus = (referenceEl) => {
|
|
const focusManagerEnabled = config.get('focusManagerPriority', false);
|
|
/**
|
|
* When going back to a previously visited page focus should typically be moved
|
|
* back to the element that was last focused when the user was on this view.
|
|
*/
|
|
if (focusManagerEnabled) {
|
|
const activeEl = document.activeElement;
|
|
if (activeEl !== null && (referenceEl === null || referenceEl === void 0 ? void 0 : referenceEl.contains(activeEl))) {
|
|
activeEl.setAttribute(LAST_FOCUS, 'true');
|
|
}
|
|
}
|
|
};
|
|
const setViewFocus = (referenceEl) => {
|
|
const focusManagerPriorities = config.get('focusManagerPriority', false);
|
|
/**
|
|
* If the focused element is a descendant of the referenceEl then it's possible
|
|
* that the app developer manually moved focus, so we do not want to override that.
|
|
* This can happen with inputs the are focused when a view transitions in.
|
|
*/
|
|
if (Array.isArray(focusManagerPriorities) && !referenceEl.contains(document.activeElement)) {
|
|
/**
|
|
* When going back to a previously visited view focus should always be moved back
|
|
* to the element that the user was last focused on when they were on this view.
|
|
*/
|
|
const lastFocus = referenceEl.querySelector(`[${LAST_FOCUS}]`);
|
|
if (lastFocus && isVisible(lastFocus)) {
|
|
moveFocus(lastFocus);
|
|
return;
|
|
}
|
|
for (const priority of focusManagerPriorities) {
|
|
/**
|
|
* For each recognized case (excluding the default case) make sure to return
|
|
* so that the fallback focus behavior does not run.
|
|
*
|
|
* We intentionally query for specific roles/semantic elements so that the
|
|
* transition manager can work with both Ionic and non-Ionic UI components.
|
|
*
|
|
* If new selectors are added, be sure to remove the outline ring by adding
|
|
* new selectors to rule in core.scss.
|
|
*/
|
|
switch (priority) {
|
|
case 'content':
|
|
const content = referenceEl.querySelector('main, [role="main"]');
|
|
if (content && isVisible(content)) {
|
|
moveFocus(content);
|
|
return;
|
|
}
|
|
break;
|
|
case 'heading':
|
|
const headingOne = referenceEl.querySelector('h1, [role="heading"][aria-level="1"]');
|
|
if (headingOne && isVisible(headingOne)) {
|
|
moveFocus(headingOne);
|
|
return;
|
|
}
|
|
break;
|
|
case 'banner':
|
|
const header = referenceEl.querySelector('header, [role="banner"]');
|
|
if (header && isVisible(header)) {
|
|
moveFocus(header);
|
|
return;
|
|
}
|
|
break;
|
|
default:
|
|
printIonWarning(`Unrecognized focus manager priority value ${priority}`);
|
|
break;
|
|
}
|
|
}
|
|
/**
|
|
* If there is nothing to focus then focus the page so focus at least moves to
|
|
* the correct view. The browser will then determine where within the page to
|
|
* move focus to.
|
|
*/
|
|
moveFocus(referenceEl);
|
|
}
|
|
};
|
|
return {
|
|
saveViewFocus,
|
|
setViewFocus,
|
|
};
|
|
};
|
|
const LAST_FOCUS = 'ion-last-focus';
|
|
|
|
const iosTransitionAnimation = () => import('./ios.transition.js');
|
|
const mdTransitionAnimation = () => import('./md.transition.js');
|
|
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.
|
|
*/
|
|
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);
|
|
};
|
|
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.
|
|
*/
|
|
const waitForMount = () => {
|
|
return new Promise((resolve) => raf(() => raf(() => resolve())));
|
|
};
|
|
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));
|
|
}
|
|
};
|
|
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);
|
|
}
|
|
};
|
|
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');
|
|
};
|
|
|
|
export { LIFECYCLE_WILL_ENTER as L, LIFECYCLE_DID_ENTER as a, LIFECYCLE_WILL_LEAVE as b, LIFECYCLE_DID_LEAVE as c, LIFECYCLE_WILL_UNLOAD as d, deepReady as e, getIonPageElement as g, lifecycle as l, setPageHidden as s, transition as t, waitForMount as w };
|
|
|