mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-18 03:00:58 +08:00
226 lines
7.5 KiB
TypeScript
226 lines
7.5 KiB
TypeScript
import { readTask, writeTask } from '@stencil/core';
|
|
import { clamp } from '@utils/helpers';
|
|
|
|
const TRANSITION = 'all 0.2s ease-in-out';
|
|
|
|
interface HeaderIndex {
|
|
el: HTMLIonHeaderElement;
|
|
toolbars: ToolbarIndex[] | [];
|
|
}
|
|
|
|
interface ToolbarIndex {
|
|
el: HTMLElement;
|
|
background: HTMLElement;
|
|
ionTitleEl: HTMLIonTitleElement | undefined;
|
|
innerTitleEl: HTMLElement;
|
|
ionButtonsEl: HTMLElement[] | [];
|
|
}
|
|
|
|
export const cloneElement = (tagName: string) => {
|
|
const getCachedEl = document.querySelector(`${tagName}.ion-cloned-element`);
|
|
if (getCachedEl !== null) {
|
|
return getCachedEl;
|
|
}
|
|
|
|
const clonedEl = document.createElement(tagName);
|
|
clonedEl.classList.add('ion-cloned-element');
|
|
clonedEl.style.setProperty('display', 'none');
|
|
document.body.appendChild(clonedEl);
|
|
|
|
return clonedEl;
|
|
};
|
|
|
|
export const createHeaderIndex = (headerEl: HTMLElement | undefined): HeaderIndex | undefined => {
|
|
if (!headerEl) {
|
|
return;
|
|
}
|
|
|
|
const toolbars = headerEl.querySelectorAll('ion-toolbar');
|
|
|
|
return {
|
|
el: headerEl,
|
|
toolbars: Array.from(toolbars).map((toolbar: HTMLIonToolbarElement) => {
|
|
const ionTitleEl = toolbar.querySelector('ion-title');
|
|
return {
|
|
el: toolbar,
|
|
background: toolbar.shadowRoot!.querySelector('.toolbar-background'),
|
|
ionTitleEl,
|
|
innerTitleEl: ionTitleEl ? ionTitleEl.shadowRoot!.querySelector('.toolbar-title') : null,
|
|
ionButtonsEl: Array.from(toolbar.querySelectorAll('ion-buttons')),
|
|
} as ToolbarIndex;
|
|
}),
|
|
} as HeaderIndex;
|
|
};
|
|
|
|
export const handleContentScroll = (scrollEl: HTMLElement, scrollHeaderIndex: HeaderIndex, contentEl: HTMLElement) => {
|
|
readTask(() => {
|
|
const scrollTop = scrollEl.scrollTop;
|
|
const scale = clamp(1, 1 + -scrollTop / 500, 1.1);
|
|
|
|
// Native refresher should not cause titles to scale
|
|
const nativeRefresher = contentEl.querySelector('ion-refresher.refresher-native');
|
|
if (nativeRefresher === null) {
|
|
writeTask(() => {
|
|
scaleLargeTitles(scrollHeaderIndex.toolbars, scale);
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
export const setToolbarBackgroundOpacity = (headerEl: HTMLIonHeaderElement, opacity?: number) => {
|
|
/**
|
|
* Fading in the backdrop opacity
|
|
* should happen after the large title
|
|
* has collapsed, so it is handled
|
|
* by handleHeaderFade()
|
|
*/
|
|
if (headerEl.collapse === 'fade') {
|
|
return;
|
|
}
|
|
|
|
if (opacity === undefined) {
|
|
headerEl.style.removeProperty('--opacity-scale');
|
|
} else {
|
|
headerEl.style.setProperty('--opacity-scale', opacity.toString());
|
|
}
|
|
};
|
|
|
|
const handleToolbarBorderIntersection = (
|
|
ev: IntersectionObserverEntry[],
|
|
mainHeaderIndex: HeaderIndex,
|
|
scrollTop: number
|
|
) => {
|
|
if (!ev[0].isIntersecting) {
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* There is a bug in Safari where overflow scrolling on a non-body element
|
|
* does not always reset the scrollTop position to 0 when letting go. It will
|
|
* set to 1 once the rubber band effect has ended. This causes the background to
|
|
* appear slightly on certain app setups.
|
|
*
|
|
* Additionally, we check if user is rubber banding (scrolling is negative)
|
|
* as this can mean they are using pull to refresh. Once the refresher starts,
|
|
* the content is transformed which can cause the intersection observer to erroneously
|
|
* fire here as well.
|
|
*/
|
|
const scale = ev[0].intersectionRatio > 0.9 || scrollTop <= 0 ? 0 : ((1 - ev[0].intersectionRatio) * 100) / 75;
|
|
|
|
setToolbarBackgroundOpacity(mainHeaderIndex.el, scale === 1 ? undefined : scale);
|
|
};
|
|
|
|
/**
|
|
* If toolbars are intersecting, hide the scrollable toolbar content
|
|
* and show the primary toolbar content. If the toolbars are not intersecting,
|
|
* hide the primary toolbar content and show the scrollable toolbar content
|
|
*/
|
|
export const handleToolbarIntersection = (
|
|
ev: any, // TODO(FW-2832): type (IntersectionObserverEntry[] triggers errors which should be sorted)
|
|
mainHeaderIndex: HeaderIndex,
|
|
scrollHeaderIndex: HeaderIndex,
|
|
scrollEl: HTMLElement
|
|
) => {
|
|
writeTask(() => {
|
|
const scrollTop = scrollEl.scrollTop;
|
|
handleToolbarBorderIntersection(ev, mainHeaderIndex, scrollTop);
|
|
|
|
const event = ev[0];
|
|
|
|
const intersection = event.intersectionRect;
|
|
const intersectionArea = intersection.width * intersection.height;
|
|
const rootArea = event.rootBounds.width * event.rootBounds.height;
|
|
|
|
const isPageHidden = intersectionArea === 0 && rootArea === 0;
|
|
const leftDiff = Math.abs(intersection.left - event.boundingClientRect.left);
|
|
const rightDiff = Math.abs(intersection.right - event.boundingClientRect.right);
|
|
const isPageTransitioning = intersectionArea > 0 && (leftDiff >= 5 || rightDiff >= 5);
|
|
|
|
if (isPageHidden || isPageTransitioning) {
|
|
return;
|
|
}
|
|
|
|
if (event.isIntersecting) {
|
|
setHeaderActive(mainHeaderIndex, false);
|
|
setHeaderActive(scrollHeaderIndex);
|
|
} else {
|
|
/**
|
|
* There is a bug with IntersectionObserver on Safari
|
|
* where `event.isIntersecting === false` when cancelling
|
|
* a swipe to go back gesture. Checking the intersection
|
|
* x, y, width, and height provides a workaround. This bug
|
|
* does not happen when using Safari + Web Animations,
|
|
* only Safari + CSS Animations.
|
|
*/
|
|
|
|
const hasValidIntersection =
|
|
(intersection.x === 0 && intersection.y === 0) || (intersection.width !== 0 && intersection.height !== 0);
|
|
|
|
if (hasValidIntersection && scrollTop > 0) {
|
|
setHeaderActive(mainHeaderIndex);
|
|
setHeaderActive(scrollHeaderIndex, false);
|
|
setToolbarBackgroundOpacity(mainHeaderIndex.el);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
export const setHeaderActive = (headerIndex: HeaderIndex, active = true) => {
|
|
const headerEl = headerIndex.el;
|
|
|
|
if (active) {
|
|
headerEl.classList.remove('header-collapse-condense-inactive');
|
|
headerEl.removeAttribute('aria-hidden');
|
|
} else {
|
|
headerEl.classList.add('header-collapse-condense-inactive');
|
|
headerEl.setAttribute('aria-hidden', 'true');
|
|
}
|
|
};
|
|
|
|
export const scaleLargeTitles = (toolbars: ToolbarIndex[] = [], scale = 1, transition = false) => {
|
|
toolbars.forEach((toolbar) => {
|
|
const ionTitle = toolbar.ionTitleEl;
|
|
const titleDiv = toolbar.innerTitleEl;
|
|
if (!ionTitle || ionTitle.size !== 'large') {
|
|
return;
|
|
}
|
|
|
|
titleDiv.style.transition = transition ? TRANSITION : '';
|
|
titleDiv.style.transform = `scale3d(${scale}, ${scale}, 1)`;
|
|
});
|
|
};
|
|
|
|
export const handleHeaderFade = (scrollEl: HTMLElement, baseEl: HTMLElement, condenseHeader: HTMLElement | null) => {
|
|
readTask(() => {
|
|
const scrollTop = scrollEl.scrollTop;
|
|
const baseElHeight = baseEl.clientHeight;
|
|
const fadeStart = condenseHeader ? condenseHeader.clientHeight : 0;
|
|
|
|
/**
|
|
* If we are using fade header with a condense
|
|
* header, then the toolbar backgrounds should
|
|
* not begin to fade in until the condense
|
|
* header has fully collapsed.
|
|
*
|
|
* Additionally, the main content should not
|
|
* overflow out of the container until the
|
|
* condense header has fully collapsed. When
|
|
* using just the condense header the content
|
|
* should overflow out of the container.
|
|
*/
|
|
if (condenseHeader !== null && scrollTop < fadeStart) {
|
|
baseEl.style.setProperty('--opacity-scale', '0');
|
|
scrollEl.style.setProperty('clip-path', `inset(${baseElHeight}px 0px 0px 0px)`);
|
|
return;
|
|
}
|
|
|
|
const distanceToStart = scrollTop - fadeStart;
|
|
const fadeDuration = 10;
|
|
const scale = clamp(0, distanceToStart / fadeDuration, 1);
|
|
writeTask(() => {
|
|
scrollEl.style.removeProperty('clip-path');
|
|
baseEl.style.setProperty('--opacity-scale', scale.toString());
|
|
});
|
|
});
|
|
};
|