@ionic/core
Version:
Base components for Ionic
197 lines (194 loc) • 8.73 kB
JavaScript
/*!
* (C) Ionic http://ionicframework.com - MIT License
*/
import { writeTask } from '@stencil/core/internal/client';
import { c as createAnimation } from './animation.js';
import { t as transitionEndAsync, c as componentOnReady, k as clamp } from './helpers.js';
const getRefresherAnimationType = (contentEl) => {
const previousSibling = contentEl.previousElementSibling;
const hasHeader = previousSibling !== null && previousSibling.tagName === 'ION-HEADER';
return hasHeader ? 'translate' : 'scale';
};
const createPullingAnimation = (type, pullingSpinner, refresherEl) => {
return type === 'scale'
? createScaleAnimation(pullingSpinner, refresherEl)
: createTranslateAnimation(pullingSpinner, refresherEl);
};
const createBaseAnimation = (pullingRefresherIcon) => {
const spinner = pullingRefresherIcon.querySelector('ion-spinner');
const circle = spinner.shadowRoot.querySelector('circle');
const spinnerArrowContainer = pullingRefresherIcon.querySelector('.spinner-arrow-container');
const arrowContainer = pullingRefresherIcon.querySelector('.arrow-container');
const arrow = arrowContainer ? arrowContainer.querySelector('ion-icon') : null;
const baseAnimation = createAnimation().duration(1000).easing('ease-out');
const spinnerArrowContainerAnimation = createAnimation()
.addElement(spinnerArrowContainer)
.keyframes([
{ offset: 0, opacity: '0.3' },
{ offset: 0.45, opacity: '0.3' },
{ offset: 0.55, opacity: '1' },
{ offset: 1, opacity: '1' },
]);
const circleInnerAnimation = createAnimation()
.addElement(circle)
.keyframes([
{ offset: 0, strokeDasharray: '1px, 200px' },
{ offset: 0.2, strokeDasharray: '1px, 200px' },
{ offset: 0.55, strokeDasharray: '100px, 200px' },
{ offset: 1, strokeDasharray: '100px, 200px' },
]);
const circleOuterAnimation = createAnimation()
.addElement(spinner)
.keyframes([
{ offset: 0, transform: 'rotate(-90deg)' },
{ offset: 1, transform: 'rotate(210deg)' },
]);
/**
* Only add arrow animation if present
* this allows users to customize the spinners
* without errors being thrown
*/
if (arrowContainer && arrow) {
const arrowContainerAnimation = createAnimation()
.addElement(arrowContainer)
.keyframes([
{ offset: 0, transform: 'rotate(0deg)' },
{ offset: 0.3, transform: 'rotate(0deg)' },
{ offset: 0.55, transform: 'rotate(280deg)' },
{ offset: 1, transform: 'rotate(400deg)' },
]);
const arrowAnimation = createAnimation()
.addElement(arrow)
.keyframes([
{ offset: 0, transform: 'translateX(2px) scale(0)' },
{ offset: 0.3, transform: 'translateX(2px) scale(0)' },
{ offset: 0.55, transform: 'translateX(-1.5px) scale(1)' },
{ offset: 1, transform: 'translateX(-1.5px) scale(1)' },
]);
baseAnimation.addAnimation([arrowContainerAnimation, arrowAnimation]);
}
return baseAnimation.addAnimation([spinnerArrowContainerAnimation, circleInnerAnimation, circleOuterAnimation]);
};
const createScaleAnimation = (pullingRefresherIcon, refresherEl) => {
/**
* Do not take the height of the refresher icon
* because at this point the DOM has not updated,
* so the refresher icon is still hidden with
* display: none.
* The `ion-refresher` container height
* is roughly the amount we need to offset
* the icon by when pulling down.
*/
const height = refresherEl.clientHeight;
const spinnerAnimation = createAnimation()
.addElement(pullingRefresherIcon)
.keyframes([
{ offset: 0, transform: `scale(0) translateY(-${height}px)` },
{ offset: 1, transform: 'scale(1) translateY(100px)' },
]);
return createBaseAnimation(pullingRefresherIcon).addAnimation([spinnerAnimation]);
};
const createTranslateAnimation = (pullingRefresherIcon, refresherEl) => {
/**
* Do not take the height of the refresher icon
* because at this point the DOM has not updated,
* so the refresher icon is still hidden with
* display: none.
* The `ion-refresher` container height
* is roughly the amount we need to offset
* the icon by when pulling down.
*/
const height = refresherEl.clientHeight;
const spinnerAnimation = createAnimation()
.addElement(pullingRefresherIcon)
.keyframes([
{ offset: 0, transform: `translateY(-${height}px)` },
{ offset: 1, transform: 'translateY(100px)' },
]);
return createBaseAnimation(pullingRefresherIcon).addAnimation([spinnerAnimation]);
};
const createSnapBackAnimation = (pullingRefresherIcon) => {
return createAnimation()
.duration(125)
.addElement(pullingRefresherIcon)
.fromTo('transform', 'translateY(var(--ion-pulling-refresher-translate, 100px))', 'translateY(0px)');
};
// iOS Native Refresher
// -----------------------------
const setSpinnerOpacity = (spinner, opacity) => {
spinner.style.setProperty('opacity', opacity.toString());
};
const handleScrollWhilePulling = (ticks, numTicks, pullAmount) => {
const max = 1;
writeTask(() => {
ticks.forEach((el, i) => {
/**
* Compute the opacity of each tick
* mark as a percentage of the pullAmount
* offset by max / numTicks so
* the tick marks are shown staggered.
*/
const min = i * (max / numTicks);
const range = max - min;
const start = pullAmount - min;
const progression = clamp(0, start / range, 1);
el.style.setProperty('opacity', progression.toString());
});
});
};
const handleScrollWhileRefreshing = (spinner, lastVelocityY) => {
writeTask(() => {
// If user pulls down quickly, the spinner should spin faster
spinner.style.setProperty('--refreshing-rotation-duration', lastVelocityY >= 1.0 ? '0.5s' : '2s');
spinner.style.setProperty('opacity', '1');
});
};
const translateElement = (el, value, duration = 200) => {
if (!el) {
return Promise.resolve();
}
const trans = transitionEndAsync(el, duration);
writeTask(() => {
el.style.setProperty('transition', `${duration}ms all ease-out`);
if (value === undefined) {
el.style.removeProperty('transform');
}
else {
el.style.setProperty('transform', `translate3d(0px, ${value}, 0px)`);
}
});
return trans;
};
// Utils
// -----------------------------
/**
* In order to use the native iOS refresher the device must support rubber band scrolling.
* As part of this, we need to exclude Desktop Safari because it has a slightly different rubber band effect that is not compatible with the native refresher in Ionic.
*
* We also need to be careful not to include devices that spoof their user agent.
* For example, when using iOS emulation in Chrome the user agent will be spoofed such that
* navigator.maxTouchPointer > 0. To work around this,
* we check to see if the apple-pay-logo is supported as a named image which is only
* true on Apple devices.
*
* We previously checked referencEl.style.webkitOverflowScrolling to explicitly check
* for rubber band support. However, this property was removed on iPadOS and it's possible
* that this will be removed on iOS in the future too.
*
*/
const supportsRubberBandScrolling = () => {
return navigator.maxTouchPoints > 0 && CSS.supports('background: -webkit-named-image(apple-pay-logo-black)');
};
const shouldUseNativeRefresher = async (referenceEl, mode) => {
const refresherContent = referenceEl.querySelector('ion-refresher-content');
if (!refresherContent) {
return Promise.resolve(false);
}
await new Promise((resolve) => componentOnReady(refresherContent, resolve));
const pullingSpinner = referenceEl.querySelector('ion-refresher-content .refresher-pulling ion-spinner');
const refreshingSpinner = referenceEl.querySelector('ion-refresher-content .refresher-refreshing ion-spinner');
return (pullingSpinner !== null &&
refreshingSpinner !== null &&
((mode === 'ios' && supportsRubberBandScrolling()) || mode === 'md'));
};
export { setSpinnerOpacity as a, handleScrollWhilePulling as b, createPullingAnimation as c, createSnapBackAnimation as d, supportsRubberBandScrolling as e, getRefresherAnimationType as g, handleScrollWhileRefreshing as h, shouldUseNativeRefresher as s, translateElement as t };