summaryrefslogtreecommitdiff
path: root/node_modules/swup/src/modules/awaitAnimations.ts
blob: e4b77e74cf46ac38e4f0af0957091c4232f1cf1a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import { queryAll } from '../utils.js';
import type Swup from '../Swup.js';
import type { Options } from '../Swup.js';

const TRANSITION = 'transition';
const ANIMATION = 'animation';

type AnimationType = typeof TRANSITION | typeof ANIMATION;
type AnimationEndEvent = `${AnimationType}end`;
type AnimationProperty = 'Delay' | 'Duration';
type AnimationStyleKey = `${AnimationType}${AnimationProperty}` | 'transitionProperty';

export type AnimationDirection = 'in' | 'out';

/**
 * Return a Promise that resolves when all CSS animations and transitions
 * are done on the page. Filters by selector or takes elements directly.
 */
export async function awaitAnimations(
	this: Swup,
	{
		selector,
		elements
	}: {
		selector: Options['animationSelector'];
		elements?: NodeListOf<HTMLElement> | HTMLElement[];
	}
): Promise<void> {
	// Allow usage of swup without animations: { animationSelector: false }
	if (selector === false && !elements) {
		return;
	}

	// Allow passing in elements
	let animatedElements: HTMLElement[] = [];
	if (elements) {
		animatedElements = Array.from(elements);
	} else if (selector) {
		animatedElements = queryAll(selector, document.body);
		// Warn if no elements match the selector, but keep things going
		if (!animatedElements.length) {
			console.warn(`[swup] No elements found matching animationSelector \`${selector}\``);
			return;
		}
	}

	const awaitedAnimations = animatedElements.map((el) => awaitAnimationsOnElement(el));
	const hasAnimations = awaitedAnimations.filter(Boolean).length > 0;
	if (!hasAnimations) {
		if (selector) {
			console.warn(
				`[swup] No CSS animation duration defined on elements matching \`${selector}\``
			);
		}
		return;
	}

	await Promise.all(awaitedAnimations);
}

function awaitAnimationsOnElement(element: HTMLElement): Promise<void> | false {
	const { type, timeout, propCount } = getTransitionInfo(element);

	// Resolve immediately if no transition defined
	if (!type || !timeout) {
		return false;
	}

	return new Promise((resolve) => {
		const endEvent: AnimationEndEvent = `${type}end`;
		const startTime = performance.now();
		let propsTransitioned = 0;

		const end = () => {
			element.removeEventListener(endEvent, onEnd);
			resolve();
		};

		const onEnd = (event: TransitionEvent | AnimationEvent) => {
			// Skip transitions on child elements
			if (event.target !== element) {
				return;
			}

			// Skip transitions that happened before we started listening
			const elapsedTime = (performance.now() - startTime) / 1000;
			if (elapsedTime < event.elapsedTime) {
				return;
			}

			// End if all properties have transitioned
			if (++propsTransitioned >= propCount) {
				end();
			}
		};

		setTimeout(() => {
			if (propsTransitioned < propCount) {
				end();
			}
		}, timeout + 1);

		element.addEventListener(endEvent, onEnd);
	});
}

function getTransitionInfo(element: Element) {
	const styles = window.getComputedStyle(element);

	const transitionDelays = getStyleProperties(styles, `${TRANSITION}Delay`);
	const transitionDurations = getStyleProperties(styles, `${TRANSITION}Duration`);
	const transitionTimeout = calculateTimeout(transitionDelays, transitionDurations);

	const animationDelays = getStyleProperties(styles, `${ANIMATION}Delay`);
	const animationDurations = getStyleProperties(styles, `${ANIMATION}Duration`);
	const animationTimeout = calculateTimeout(animationDelays, animationDurations);

	const timeout = Math.max(transitionTimeout, animationTimeout);
	const type: AnimationType | null =
		timeout > 0 ? (transitionTimeout > animationTimeout ? TRANSITION : ANIMATION) : null;
	const propCount = type
		? type === TRANSITION
			? transitionDurations.length
			: animationDurations.length
		: 0;

	return {
		type,
		timeout,
		propCount
	};
}

export function getStyleProperties(styles: CSSStyleDeclaration, key: AnimationStyleKey): string[] {
	return (styles[key] || '').split(', ');
}

export function calculateTimeout(delays: string[], durations: string[]): number {
	while (delays.length < durations.length) {
		delays = delays.concat(delays);
	}

	return Math.max(...durations.map((duration, i) => toMs(duration) + toMs(delays[i])));
}

export function toMs(time: string): number {
	return parseFloat(time) * 1000;
}