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
|
import React from 'dom-chef';
import {css} from 'code-tag';
import onetime from 'onetime';
import {ParseSelector} from 'typed-query-selector/parser.js';
import getCallerID from './caller-id.js';
import isDevelopmentVersion from './is-development-version.js';
type ObserverListener<ExpectedElement extends Element> = (element: ExpectedElement, options: SignalAsOptions) => void;
const animation = 'rgh-selector-observer';
const getListener = <
Selector extends string,
ExpectedElement extends ParseSelector<Selector, HTMLElement>,
>(
seenMark: string,
selector: Selector,
callback: ObserverListener<ExpectedElement>,
signal?: AbortSignal,
) => (event: AnimationEvent) => {
const target = event.target as ExpectedElement;
// The target can match a selector even if the animation actually happened on a ::before pseudo-element, so it needs an explicit exclusion here
if (target.classList.contains(seenMark) || !target.matches(selector)) {
return;
}
// Removes this specific selector’s animation once it was seen
target.classList.add(seenMark);
callback(target, {signal});
};
const registerAnimation = onetime((): void => {
document.head.append(<style>{`@keyframes ${animation} {}`}</style>);
});
export default function observe<
Selector extends string,
ExpectedElement extends ParseSelector<Selector, HTMLElement>,
>(
selectors: Selector | readonly Selector[],
listener: ObserverListener<ExpectedElement>,
{signal}: SignalAsOptions = {},
): void {
if (signal?.aborted) {
return;
}
const selector = String(selectors); // Array#toString() creates a comma-separated string
const seenMark = 'rgh-seen-' + getCallerID();
registerAnimation();
const rule = document.createElement('style');
if (isDevelopmentVersion()) {
// For debuggability
rule.setAttribute('s', selector);
}
rule.textContent = css`
:where(${String(selector)}):not(.${seenMark}) {
animation: 1ms ${animation};
}
`;
document.body.prepend(rule);
signal?.addEventListener('abort', () => {
rule.remove();
});
window.addEventListener('animationstart', getListener(seenMark, selector, listener, signal), {signal});
}
|