polite-media 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -0
- package/README.md +77 -38
- package/dist/coordinator.d.ts +4 -2
- package/dist/coordinator.d.ts.map +1 -1
- package/dist/coordinator.js +52 -26
- package/dist/coordinator.js.map +1 -1
- package/dist/events.d.ts +8 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +6 -0
- package/dist/events.js.map +1 -1
- package/dist/image.css +1 -1
- package/dist/image.d.ts +6 -13
- package/dist/image.d.ts.map +1 -1
- package/dist/image.js +43 -16
- package/dist/image.js.map +1 -1
- package/dist/layer.css +1 -1
- package/dist/video.css +1 -1
- package/dist/video.d.ts +1 -1
- package/dist/video.d.ts.map +1 -1
- package/dist/video.js +1 -1
- package/dist/video.js.map +1 -1
- package/package.json +1 -1
- package/src/coordinator.ts +74 -42
- package/src/events.ts +9 -0
- package/src/image.css +10 -5
- package/src/image.ts +52 -27
- package/src/layer.css +3 -2
- package/src/video.css +19 -13
- package/src/video.ts +1 -0
package/src/image.ts
CHANGED
|
@@ -24,22 +24,16 @@ export type ImageTarget = Target<HTMLImageElement>;
|
|
|
24
24
|
|
|
25
25
|
export interface RevealImagesOptions {
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* one does not restore its candidacy -- so fading an eager, above-the-fold
|
|
31
|
-
* image can forfeit the metric it was meant to improve. A lazy image was never
|
|
32
|
-
* an LCP candidate, so the default is risk-free.
|
|
33
|
-
*
|
|
34
|
-
* Turning this on is legitimate and sometimes right: an eager grid that would
|
|
35
|
-
* otherwise cut from its backdrop to the photo on whatever frame the async
|
|
36
|
-
* decode lands looks worse without a fade. It just has to be a decision rather
|
|
37
|
-
* than an accident.
|
|
27
|
+
* @deprecated Ignored since 0.5, and warns. image.css no longer hides an eager
|
|
28
|
+
* image at all, so the opt-in has to be in the markup where the stylesheet can
|
|
29
|
+
* see it: `data-polite-reveal="eager"` on the image.
|
|
38
30
|
*/
|
|
39
31
|
allowEager?: boolean;
|
|
40
32
|
}
|
|
41
33
|
|
|
42
34
|
const READY = 'data-polite-ready';
|
|
35
|
+
/** Written while this module owns an image's reveal, so image.css can stand down. */
|
|
36
|
+
const MANAGED = 'data-polite-managed';
|
|
43
37
|
|
|
44
38
|
function markReady(image: HTMLImageElement): void {
|
|
45
39
|
image.setAttribute(READY, '');
|
|
@@ -52,11 +46,12 @@ function markReady(image: HTMLImageElement): void {
|
|
|
52
46
|
}
|
|
53
47
|
|
|
54
48
|
/**
|
|
55
|
-
* Every image
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
49
|
+
* Every image a `revealImages()` call has ever claimed, for the stray-image
|
|
50
|
+
* warning. Distinct from `data-polite-managed`, which marks an image only while
|
|
51
|
+
* this module still owns its reveal so the stylesheet's failsafe stands down: a
|
|
52
|
+
* teardown removes the attribute but the claim stays, since the warning is about
|
|
53
|
+
* selectors that never reached an image at all. Weak so a released image is not
|
|
54
|
+
* pinned by the bookkeeping.
|
|
60
55
|
*/
|
|
61
56
|
const managed = new WeakSet<HTMLImageElement>();
|
|
62
57
|
|
|
@@ -90,6 +85,7 @@ function scheduleUnmanagedCheck(): void {
|
|
|
90
85
|
}
|
|
91
86
|
|
|
92
87
|
let warnedNoStylesheet = false;
|
|
88
|
+
let warnedAllowEager = false;
|
|
93
89
|
|
|
94
90
|
/**
|
|
95
91
|
* image.css is the only thing that hides a marked image. Without it `data-polite-reveal` is inert:
|
|
@@ -114,6 +110,8 @@ function warnIfUnstyled(): void {
|
|
|
114
110
|
// removed in this same task so nothing paints it and no scan above ever sees it.
|
|
115
111
|
const probe = document.createElement('img');
|
|
116
112
|
probe.setAttribute('data-polite-reveal', '');
|
|
113
|
+
// The stylesheet hides only lazy images, so an eager probe would read as unstyled everywhere.
|
|
114
|
+
probe.setAttribute('loading', 'lazy');
|
|
117
115
|
document.body.appendChild(probe);
|
|
118
116
|
const opacity = getComputedStyle(probe).opacity;
|
|
119
117
|
probe.remove();
|
|
@@ -131,33 +129,52 @@ function warnIfUnstyled(): void {
|
|
|
131
129
|
}
|
|
132
130
|
|
|
133
131
|
/**
|
|
134
|
-
* Reveals each matching image once it has decoded.
|
|
132
|
+
* Reveals each matching image once it has decoded. With no target it takes every
|
|
133
|
+
* marked image on the page.
|
|
135
134
|
*
|
|
136
135
|
* Returns a function that stops any reveals still pending, for a client-side
|
|
137
136
|
* router tearing the page down before the images resolved.
|
|
138
137
|
*/
|
|
139
|
-
export function revealImages(
|
|
138
|
+
export function revealImages(
|
|
139
|
+
target: ImageTarget = 'img[data-polite-reveal]',
|
|
140
|
+
options: RevealImagesOptions = {}
|
|
141
|
+
): () => void {
|
|
140
142
|
const controller = new AbortController();
|
|
141
143
|
const { signal } = controller;
|
|
142
144
|
|
|
145
|
+
if (options.allowEager && !warnedAllowEager) {
|
|
146
|
+
warnedAllowEager = true;
|
|
147
|
+
console.warn(
|
|
148
|
+
'polite-media: allowEager is ignored. To fade an eager image, put ' +
|
|
149
|
+
'data-polite-reveal="eager" on it, where the stylesheet can see the choice.'
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
143
153
|
scheduleUnmanagedCheck();
|
|
144
154
|
|
|
155
|
+
const claimed: HTMLImageElement[] = [];
|
|
156
|
+
|
|
145
157
|
for (const image of resolveTargets(target)) {
|
|
146
158
|
managed.add(image);
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
159
|
+
// Tells the stylesheet to leave this one alone. The failsafe there is for images nothing
|
|
160
|
+
// reveals, and firing it on one this module owns is worse than not firing it at all: opacity
|
|
161
|
+
// reaches 1 while the picture is still in flight, so when it arrives there is no fade left to
|
|
162
|
+
// run. Measured on a real page, eleven below-the-fold images were revealed that way five
|
|
163
|
+
// seconds in, none of them loaded.
|
|
164
|
+
image.setAttribute(MANAGED, '');
|
|
165
|
+
claimed.push(image);
|
|
166
|
+
// An eager image is never hidden by image.css, because LCP excludes elements at
|
|
167
|
+
// opacity 0 and a deferred module cannot reveal one before first paint. So it
|
|
168
|
+
// is marked ready at once, unless the markup opted it into the fade with the
|
|
169
|
+
// same value the stylesheet keys on.
|
|
154
170
|
//
|
|
155
171
|
// Tested against 'lazy' rather than for 'eager' deliberately. Engines
|
|
156
172
|
// disagree on what an absent or invalid attribute reports: MDN documents
|
|
157
173
|
// only 'eager' and 'lazy', while happy-dom returns 'auto'. Only "lazy" has
|
|
158
174
|
// one agreed spelling, so asking whether it is lazy is answerable
|
|
159
175
|
// everywhere, and everything else correctly falls into the eager branch.
|
|
160
|
-
|
|
176
|
+
const optedIn = image.getAttribute('data-polite-reveal') === 'eager';
|
|
177
|
+
if (!optedIn && image.loading !== 'lazy') {
|
|
161
178
|
markReady(image);
|
|
162
179
|
continue;
|
|
163
180
|
}
|
|
@@ -193,7 +210,15 @@ export function revealImages(target: ImageTarget, options: RevealImagesOptions =
|
|
|
193
210
|
});
|
|
194
211
|
}
|
|
195
212
|
|
|
196
|
-
|
|
213
|
+
// Hands anything still unrevealed back to the stylesheet. A router tearing the page down before
|
|
214
|
+
// the images resolved would otherwise leave them owned by a module that has stopped listening,
|
|
215
|
+
// and with the failsafe suppressed that is hidden forever rather than merely unfaded.
|
|
216
|
+
return () => {
|
|
217
|
+
controller.abort();
|
|
218
|
+
for (const image of claimed) {
|
|
219
|
+
if (!image.hasAttribute(READY)) image.removeAttribute(MANAGED);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
197
222
|
}
|
|
198
223
|
|
|
199
224
|
// Re-exported so `polite-media/image` carries the ElementEventMap and
|
package/src/layer.css
CHANGED
|
@@ -50,8 +50,9 @@
|
|
|
50
50
|
* object-fit needs the image for its own reason: it "Applies to: replaced
|
|
51
51
|
* elements", and a `<picture>` is a container rather than one.
|
|
52
52
|
*/
|
|
53
|
-
[data-polite-media] >
|
|
54
|
-
[data-polite-media] > picture > img
|
|
53
|
+
[data-polite-media] > img,
|
|
54
|
+
[data-polite-media] > picture > img,
|
|
55
|
+
[data-polite-media] video {
|
|
55
56
|
position: absolute;
|
|
56
57
|
inset: 0;
|
|
57
58
|
width: 100%;
|
package/src/video.css
CHANGED
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
* is why it works at any video size, and it would be undone by a single width
|
|
7
7
|
* declaration here.
|
|
8
8
|
*
|
|
9
|
-
* `data-polite-media` goes on the
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* `data-polite-media` goes on the box in your markup, not from script. It has
|
|
10
|
+
* to be authored: a <video preload="none"> paints an empty box, and if the base
|
|
11
|
+
* "hidden" state only arrived once JS ran, a page whose bundle failed would
|
|
12
|
+
* show that empty box stacked over the poster. Authored, the safe state is the
|
|
13
|
+
* default and the library only ever has to reveal.
|
|
14
14
|
*
|
|
15
|
-
* Expected shape -- poster
|
|
15
|
+
* Expected shape -- poster a direct child, video anywhere inside, one box:
|
|
16
16
|
*
|
|
17
17
|
* <div class="whatever-you-like" data-polite-media>
|
|
18
18
|
* <img src="poster.avif" alt="">
|
|
@@ -21,9 +21,15 @@
|
|
|
21
21
|
* </video>
|
|
22
22
|
* </div>
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* The video rules use a descendant combinator so a wrapper between box and
|
|
25
|
+
* video is fine; the poster rule stays a direct child so a logo deeper in the
|
|
26
|
+
* box is left alone. One box holds one video, and boxes must not nest: the
|
|
27
|
+
* ready state reveals every video inside the box.
|
|
28
|
+
*
|
|
29
|
+
* Attributes the library sets on that box:
|
|
30
|
+
* data-polite-ready a frame has actually painted
|
|
31
|
+
* data-polite-failed no source could be decoded; the poster is now permanent
|
|
32
|
+
* data-polite-blocked play() refused until a gesture; the library is retrying
|
|
27
33
|
* And on <html>:
|
|
28
34
|
* data-polite-paused the user stopped playback via a [data-polite-pause-control] control
|
|
29
35
|
*
|
|
@@ -38,12 +44,12 @@
|
|
|
38
44
|
* of an async decode landing. Set the property to override both at once.
|
|
39
45
|
*/
|
|
40
46
|
|
|
41
|
-
[data-polite-media]
|
|
47
|
+
[data-polite-media] video {
|
|
42
48
|
opacity: 0;
|
|
43
49
|
transition: opacity var(--polite-fade, 0s) ease;
|
|
44
50
|
}
|
|
45
51
|
|
|
46
|
-
[data-polite-media][data-polite-ready]
|
|
52
|
+
[data-polite-media][data-polite-ready] video {
|
|
47
53
|
opacity: 1;
|
|
48
54
|
}
|
|
49
55
|
|
|
@@ -62,12 +68,12 @@
|
|
|
62
68
|
* half-decoded frame stuck at opacity 1 is exactly the state a viewer would
|
|
63
69
|
* report as broken.
|
|
64
70
|
*/
|
|
65
|
-
[data-polite-media][data-polite-failed]
|
|
71
|
+
[data-polite-media][data-polite-failed] video {
|
|
66
72
|
opacity: 0;
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
@media (prefers-reduced-motion: reduce) {
|
|
70
|
-
[data-polite-media]
|
|
76
|
+
[data-polite-media] video,
|
|
71
77
|
[data-polite-media] > :is(img, picture) {
|
|
72
78
|
transition: none;
|
|
73
79
|
}
|
package/src/video.ts
CHANGED
|
@@ -24,6 +24,7 @@ export type { AtOnce, ConfigureOptions, RegisterOptions, VideoTarget } from './c
|
|
|
24
24
|
// ElementEventMap and DocumentEventMap augmentation only reaches a consumer if
|
|
25
25
|
// this module is part of their program.
|
|
26
26
|
export {
|
|
27
|
+
POLITE_VIDEO_BLOCKED,
|
|
27
28
|
POLITE_VIDEO_PAUSECHANGE,
|
|
28
29
|
POLITE_VIDEO_FAILED,
|
|
29
30
|
POLITE_VIDEO_READY,
|