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/dist/events.d.ts
CHANGED
|
@@ -24,6 +24,12 @@
|
|
|
24
24
|
export declare const POLITE_VIDEO_READY = "polite-video:ready";
|
|
25
25
|
/** Dispatched on the video when no source could be decoded. Bubbles. */
|
|
26
26
|
export declare const POLITE_VIDEO_FAILED = "polite-video:failed";
|
|
27
|
+
/**
|
|
28
|
+
* Dispatched on the video when the browser refused `play()` until a user gesture.
|
|
29
|
+
* Bubbles. The library keeps retrying on its own; this exists so a host can show
|
|
30
|
+
* a play affordance in the meantime, which the library cannot do for it.
|
|
31
|
+
*/
|
|
32
|
+
export declare const POLITE_VIDEO_BLOCKED = "polite-video:blocked";
|
|
27
33
|
/** Dispatched on the image once it has decoded. Bubbles. */
|
|
28
34
|
export declare const POLITE_IMAGE_READY = "polite-image:ready";
|
|
29
35
|
/**
|
|
@@ -58,11 +64,13 @@ declare global {
|
|
|
58
64
|
interface ElementEventMap {
|
|
59
65
|
'polite-video:ready': CustomEvent<PoliteVideoEventDetail>;
|
|
60
66
|
'polite-video:failed': CustomEvent<PoliteVideoEventDetail>;
|
|
67
|
+
'polite-video:blocked': CustomEvent<PoliteVideoEventDetail>;
|
|
61
68
|
'polite-image:ready': CustomEvent<PoliteImageEventDetail>;
|
|
62
69
|
}
|
|
63
70
|
interface DocumentEventMap {
|
|
64
71
|
'polite-video:ready': CustomEvent<PoliteVideoEventDetail>;
|
|
65
72
|
'polite-video:failed': CustomEvent<PoliteVideoEventDetail>;
|
|
73
|
+
'polite-video:blocked': CustomEvent<PoliteVideoEventDetail>;
|
|
66
74
|
'polite-image:ready': CustomEvent<PoliteImageEventDetail>;
|
|
67
75
|
'polite-video:pausechange': CustomEvent<PolitePauseEventDetail>;
|
|
68
76
|
}
|
package/dist/events.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,2EAA2E;AAC3E,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AAEvD,wEAAwE;AACxE,eAAO,MAAM,mBAAmB,wBAAwB,CAAC;AAEzD,4DAA4D;AAC5D,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AAEvD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,wBAAwB,6BAA6B,CAAC;AAEnE,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,KAAK,EAAE,gBAAgB,CAAC;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC,8CAA8C;IAC9C,KAAK,EAAE,gBAAgB,CAAC;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC,wEAAwE;IACxE,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,eAAe;QACvB,oBAAoB,EAAE,WAAW,CAAC,sBAAsB,CAAC,CAAC;QAC1D,qBAAqB,EAAE,WAAW,CAAC,sBAAsB,CAAC,CAAC;QAC3D,oBAAoB,EAAE,WAAW,CAAC,sBAAsB,CAAC,CAAC;KAC3D;IAED,UAAU,gBAAgB;QACxB,oBAAoB,EAAE,WAAW,CAAC,sBAAsB,CAAC,CAAC;QAC1D,qBAAqB,EAAE,WAAW,CAAC,sBAAsB,CAAC,CAAC;QAC3D,oBAAoB,EAAE,WAAW,CAAC,sBAAsB,CAAC,CAAC;QAC1D,0BAA0B,EAAE,WAAW,CAAC,sBAAsB,CAAC,CAAC;KACjE;CACF"}
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,2EAA2E;AAC3E,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AAEvD,wEAAwE;AACxE,eAAO,MAAM,mBAAmB,wBAAwB,CAAC;AAEzD;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,yBAAyB,CAAC;AAE3D,4DAA4D;AAC5D,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AAEvD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,wBAAwB,6BAA6B,CAAC;AAEnE,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,KAAK,EAAE,gBAAgB,CAAC;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC,8CAA8C;IAC9C,KAAK,EAAE,gBAAgB,CAAC;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC,wEAAwE;IACxE,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,eAAe;QACvB,oBAAoB,EAAE,WAAW,CAAC,sBAAsB,CAAC,CAAC;QAC1D,qBAAqB,EAAE,WAAW,CAAC,sBAAsB,CAAC,CAAC;QAC3D,sBAAsB,EAAE,WAAW,CAAC,sBAAsB,CAAC,CAAC;QAC5D,oBAAoB,EAAE,WAAW,CAAC,sBAAsB,CAAC,CAAC;KAC3D;IAED,UAAU,gBAAgB;QACxB,oBAAoB,EAAE,WAAW,CAAC,sBAAsB,CAAC,CAAC;QAC1D,qBAAqB,EAAE,WAAW,CAAC,sBAAsB,CAAC,CAAC;QAC3D,sBAAsB,EAAE,WAAW,CAAC,sBAAsB,CAAC,CAAC;QAC5D,oBAAoB,EAAE,WAAW,CAAC,sBAAsB,CAAC,CAAC;QAC1D,0BAA0B,EAAE,WAAW,CAAC,sBAAsB,CAAC,CAAC;KACjE;CACF"}
|
package/dist/events.js
CHANGED
|
@@ -24,6 +24,12 @@
|
|
|
24
24
|
export const POLITE_VIDEO_READY = 'polite-video:ready';
|
|
25
25
|
/** Dispatched on the video when no source could be decoded. Bubbles. */
|
|
26
26
|
export const POLITE_VIDEO_FAILED = 'polite-video:failed';
|
|
27
|
+
/**
|
|
28
|
+
* Dispatched on the video when the browser refused `play()` until a user gesture.
|
|
29
|
+
* Bubbles. The library keeps retrying on its own; this exists so a host can show
|
|
30
|
+
* a play affordance in the meantime, which the library cannot do for it.
|
|
31
|
+
*/
|
|
32
|
+
export const POLITE_VIDEO_BLOCKED = 'polite-video:blocked';
|
|
27
33
|
/** Dispatched on the image once it has decoded. Bubbles. */
|
|
28
34
|
export const POLITE_IMAGE_READY = 'polite-image:ready';
|
|
29
35
|
/**
|
package/dist/events.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,2EAA2E;AAC3E,MAAM,CAAC,MAAM,kBAAkB,GAAG,oBAAoB,CAAC;AAEvD,wEAAwE;AACxE,MAAM,CAAC,MAAM,mBAAmB,GAAG,qBAAqB,CAAC;AAEzD,4DAA4D;AAC5D,MAAM,CAAC,MAAM,kBAAkB,GAAG,oBAAoB,CAAC;AAEvD;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,0BAA0B,CAAC"}
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,2EAA2E;AAC3E,MAAM,CAAC,MAAM,kBAAkB,GAAG,oBAAoB,CAAC;AAEvD,wEAAwE;AACxE,MAAM,CAAC,MAAM,mBAAmB,GAAG,qBAAqB,CAAC;AAEzD;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,sBAAsB,CAAC;AAE3D,4DAA4D;AAC5D,MAAM,CAAC,MAAM,kBAAkB,GAAG,oBAAoB,CAAC;AAEvD;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,0BAA0B,CAAC"}
|
package/dist/image.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
@media(scripting:enabled){img[data-polite-reveal]{opacity:0;transition:opacity var(--polite-fade, .35s) ease}img[data-polite-reveal]:not([data-polite-ready]){animation:polite-reveal-failsafe 0s var(--polite-failsafe, 5s) forwards}img[data-polite-reveal][data-polite-ready]{opacity:1}@keyframes polite-reveal-failsafe{to{opacity:1}}}@media(prefers-reduced-motion:reduce){img[data-polite-reveal]{transition:none}}
|
|
1
|
+
@media(scripting:enabled){img[data-polite-reveal][loading=lazy],img[data-polite-reveal=eager]{opacity:0;transition:opacity var(--polite-fade, .35s) ease}img[data-polite-reveal][loading=lazy]:not([data-polite-ready]):not([data-polite-managed]),img[data-polite-reveal=eager]:not([data-polite-ready]):not([data-polite-managed]){animation:polite-reveal-failsafe 0s var(--polite-failsafe, 5s) forwards}img[data-polite-reveal][data-polite-ready]{opacity:1}@keyframes polite-reveal-failsafe{to{opacity:1}}}@media(prefers-reduced-motion:reduce){img[data-polite-reveal]{transition:none}}
|
package/dist/image.d.ts
CHANGED
|
@@ -20,26 +20,19 @@ import { type Target } from './targets.js';
|
|
|
20
20
|
export type ImageTarget = Target<HTMLImageElement>;
|
|
21
21
|
export interface RevealImagesOptions {
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* one does not restore its candidacy -- so fading an eager, above-the-fold
|
|
27
|
-
* image can forfeit the metric it was meant to improve. A lazy image was never
|
|
28
|
-
* an LCP candidate, so the default is risk-free.
|
|
29
|
-
*
|
|
30
|
-
* Turning this on is legitimate and sometimes right: an eager grid that would
|
|
31
|
-
* otherwise cut from its backdrop to the photo on whatever frame the async
|
|
32
|
-
* decode lands looks worse without a fade. It just has to be a decision rather
|
|
33
|
-
* than an accident.
|
|
23
|
+
* @deprecated Ignored since 0.5, and warns. image.css no longer hides an eager
|
|
24
|
+
* image at all, so the opt-in has to be in the markup where the stylesheet can
|
|
25
|
+
* see it: `data-polite-reveal="eager"` on the image.
|
|
34
26
|
*/
|
|
35
27
|
allowEager?: boolean;
|
|
36
28
|
}
|
|
37
29
|
/**
|
|
38
|
-
* Reveals each matching image once it has decoded.
|
|
30
|
+
* Reveals each matching image once it has decoded. With no target it takes every
|
|
31
|
+
* marked image on the page.
|
|
39
32
|
*
|
|
40
33
|
* Returns a function that stops any reveals still pending, for a client-side
|
|
41
34
|
* router tearing the page down before the images resolved.
|
|
42
35
|
*/
|
|
43
|
-
export declare function revealImages(target
|
|
36
|
+
export declare function revealImages(target?: ImageTarget, options?: RevealImagesOptions): () => void;
|
|
44
37
|
export { POLITE_IMAGE_READY, type PoliteImageEventDetail } from './events.js';
|
|
45
38
|
//# sourceMappingURL=image.d.ts.map
|
package/dist/image.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../src/image.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,EAAkB,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAE3D,kEAAkE;AAClE,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,gBAAgB,CAAC,CAAC;AAEnD,MAAM,WAAW,mBAAmB;IAClC
|
|
1
|
+
{"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../src/image.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,EAAkB,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAE3D,kEAAkE;AAClE,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,gBAAgB,CAAC,CAAC;AAEnD,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAmGD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,MAAM,GAAE,WAAuC,EAC/C,OAAO,GAAE,mBAAwB,GAChC,MAAM,IAAI,CAiFZ;AAKD,OAAO,EAAE,kBAAkB,EAAE,KAAK,sBAAsB,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/image.js
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
import { POLITE_IMAGE_READY } from './events.js';
|
|
19
19
|
import { resolveTargets } from './targets.js';
|
|
20
20
|
const READY = 'data-polite-ready';
|
|
21
|
+
/** Written while this module owns an image's reveal, so image.css can stand down. */
|
|
22
|
+
const MANAGED = 'data-polite-managed';
|
|
21
23
|
function markReady(image) {
|
|
22
24
|
image.setAttribute(READY, '');
|
|
23
25
|
image.dispatchEvent(new CustomEvent(POLITE_IMAGE_READY, {
|
|
@@ -26,11 +28,12 @@ function markReady(image) {
|
|
|
26
28
|
}));
|
|
27
29
|
}
|
|
28
30
|
/**
|
|
29
|
-
* Every image
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* Every image a `revealImages()` call has ever claimed, for the stray-image
|
|
32
|
+
* warning. Distinct from `data-polite-managed`, which marks an image only while
|
|
33
|
+
* this module still owns its reveal so the stylesheet's failsafe stands down: a
|
|
34
|
+
* teardown removes the attribute but the claim stays, since the warning is about
|
|
35
|
+
* selectors that never reached an image at all. Weak so a released image is not
|
|
36
|
+
* pinned by the bookkeeping.
|
|
34
37
|
*/
|
|
35
38
|
const managed = new WeakSet();
|
|
36
39
|
let unmanagedCheck;
|
|
@@ -59,6 +62,7 @@ function scheduleUnmanagedCheck() {
|
|
|
59
62
|
}, 1000);
|
|
60
63
|
}
|
|
61
64
|
let warnedNoStylesheet = false;
|
|
65
|
+
let warnedAllowEager = false;
|
|
62
66
|
/**
|
|
63
67
|
* image.css is the only thing that hides a marked image. Without it `data-polite-reveal` is inert:
|
|
64
68
|
* the image is never hidden, so it never fades and everything this module does is invisible.
|
|
@@ -82,6 +86,8 @@ function warnIfUnstyled() {
|
|
|
82
86
|
// removed in this same task so nothing paints it and no scan above ever sees it.
|
|
83
87
|
const probe = document.createElement('img');
|
|
84
88
|
probe.setAttribute('data-polite-reveal', '');
|
|
89
|
+
// The stylesheet hides only lazy images, so an eager probe would read as unstyled everywhere.
|
|
90
|
+
probe.setAttribute('loading', 'lazy');
|
|
85
91
|
document.body.appendChild(probe);
|
|
86
92
|
const opacity = getComputedStyle(probe).opacity;
|
|
87
93
|
probe.remove();
|
|
@@ -95,31 +101,43 @@ function warnIfUnstyled() {
|
|
|
95
101
|
"images will not fade. Import 'polite-media/image.css' from wherever the attribute is emitted.");
|
|
96
102
|
}
|
|
97
103
|
/**
|
|
98
|
-
* Reveals each matching image once it has decoded.
|
|
104
|
+
* Reveals each matching image once it has decoded. With no target it takes every
|
|
105
|
+
* marked image on the page.
|
|
99
106
|
*
|
|
100
107
|
* Returns a function that stops any reveals still pending, for a client-side
|
|
101
108
|
* router tearing the page down before the images resolved.
|
|
102
109
|
*/
|
|
103
|
-
export function revealImages(target, options = {}) {
|
|
110
|
+
export function revealImages(target = 'img[data-polite-reveal]', options = {}) {
|
|
104
111
|
const controller = new AbortController();
|
|
105
112
|
const { signal } = controller;
|
|
113
|
+
if (options.allowEager && !warnedAllowEager) {
|
|
114
|
+
warnedAllowEager = true;
|
|
115
|
+
console.warn('polite-media: allowEager is ignored. To fade an eager image, put ' +
|
|
116
|
+
'data-polite-reveal="eager" on it, where the stylesheet can see the choice.');
|
|
117
|
+
}
|
|
106
118
|
scheduleUnmanagedCheck();
|
|
119
|
+
const claimed = [];
|
|
107
120
|
for (const image of resolveTargets(target)) {
|
|
108
121
|
managed.add(image);
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
122
|
+
// Tells the stylesheet to leave this one alone. The failsafe there is for images nothing
|
|
123
|
+
// reveals, and firing it on one this module owns is worse than not firing it at all: opacity
|
|
124
|
+
// reaches 1 while the picture is still in flight, so when it arrives there is no fade left to
|
|
125
|
+
// run. Measured on a real page, eleven below-the-fold images were revealed that way five
|
|
126
|
+
// seconds in, none of them loaded.
|
|
127
|
+
image.setAttribute(MANAGED, '');
|
|
128
|
+
claimed.push(image);
|
|
129
|
+
// An eager image is never hidden by image.css, because LCP excludes elements at
|
|
130
|
+
// opacity 0 and a deferred module cannot reveal one before first paint. So it
|
|
131
|
+
// is marked ready at once, unless the markup opted it into the fade with the
|
|
132
|
+
// same value the stylesheet keys on.
|
|
116
133
|
//
|
|
117
134
|
// Tested against 'lazy' rather than for 'eager' deliberately. Engines
|
|
118
135
|
// disagree on what an absent or invalid attribute reports: MDN documents
|
|
119
136
|
// only 'eager' and 'lazy', while happy-dom returns 'auto'. Only "lazy" has
|
|
120
137
|
// one agreed spelling, so asking whether it is lazy is answerable
|
|
121
138
|
// everywhere, and everything else correctly falls into the eager branch.
|
|
122
|
-
|
|
139
|
+
const optedIn = image.getAttribute('data-polite-reveal') === 'eager';
|
|
140
|
+
if (!optedIn && image.loading !== 'lazy') {
|
|
123
141
|
markReady(image);
|
|
124
142
|
continue;
|
|
125
143
|
}
|
|
@@ -155,7 +173,16 @@ export function revealImages(target, options = {}) {
|
|
|
155
173
|
image.addEventListener('error', () => markReady(image), { once: true, signal });
|
|
156
174
|
});
|
|
157
175
|
}
|
|
158
|
-
|
|
176
|
+
// Hands anything still unrevealed back to the stylesheet. A router tearing the page down before
|
|
177
|
+
// the images resolved would otherwise leave them owned by a module that has stopped listening,
|
|
178
|
+
// and with the failsafe suppressed that is hidden forever rather than merely unfaded.
|
|
179
|
+
return () => {
|
|
180
|
+
controller.abort();
|
|
181
|
+
for (const image of claimed) {
|
|
182
|
+
if (!image.hasAttribute(READY))
|
|
183
|
+
image.removeAttribute(MANAGED);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
159
186
|
}
|
|
160
187
|
// Re-exported so `polite-media/image` carries the ElementEventMap and
|
|
161
188
|
// DocumentEventMap augmentation too: it only reaches a consumer whose program
|
package/dist/image.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image.js","sourceRoot":"","sources":["../src/image.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,kBAAkB,EAA+B,MAAM,aAAa,CAAC;AAC9E,OAAO,EAAE,cAAc,EAAe,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"image.js","sourceRoot":"","sources":["../src/image.ts"],"names":[],"mappings":"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"}
|
package/dist/layer.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
[data-polite-media]{position:relative}[data-polite-media]
|
|
1
|
+
[data-polite-media]{position:relative}[data-polite-media]>img,[data-polite-media]>picture>img,[data-polite-media] video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
|
package/dist/video.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
[data-polite-media]
|
|
1
|
+
[data-polite-media] video{opacity:0;transition:opacity var(--polite-fade, 0s) ease}[data-polite-media][data-polite-ready] video{opacity:1}[data-polite-media][data-polite-ready]>:is(img,picture){visibility:hidden;transition:visibility 0s linear var(--polite-fade, 0s)}[data-polite-media][data-polite-failed] video{opacity:0}@media(prefers-reduced-motion:reduce){[data-polite-media] video,[data-polite-media]>:is(img,picture){transition:none}}
|
package/dist/video.d.ts
CHANGED
|
@@ -11,5 +11,5 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export { configure, pauseAll, register, registerAll, resumeAll, unregister, unregisterAll, } from './coordinator.js';
|
|
13
13
|
export type { AtOnce, ConfigureOptions, RegisterOptions, VideoTarget } from './coordinator.js';
|
|
14
|
-
export { POLITE_VIDEO_PAUSECHANGE, POLITE_VIDEO_FAILED, POLITE_VIDEO_READY, type PolitePauseEventDetail, type PoliteVideoEventDetail, } from './events.js';
|
|
14
|
+
export { POLITE_VIDEO_BLOCKED, POLITE_VIDEO_PAUSECHANGE, POLITE_VIDEO_FAILED, POLITE_VIDEO_READY, type PolitePauseEventDetail, type PoliteVideoEventDetail, } from './events.js';
|
|
15
15
|
//# sourceMappingURL=video.d.ts.map
|
package/dist/video.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"video.d.ts","sourceRoot":"","sources":["../src/video.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EACL,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,SAAS,EACT,UAAU,EACV,aAAa,GACd,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,MAAM,EAAE,gBAAgB,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAK/F,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,kBAAkB,EAClB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,GAC5B,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"video.d.ts","sourceRoot":"","sources":["../src/video.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EACL,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,SAAS,EACT,UAAU,EACV,aAAa,GACd,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,MAAM,EAAE,gBAAgB,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAK/F,OAAO,EACL,oBAAoB,EACpB,wBAAwB,EACxB,mBAAmB,EACnB,kBAAkB,EAClB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,GAC5B,MAAM,aAAa,CAAC"}
|
package/dist/video.js
CHANGED
|
@@ -13,5 +13,5 @@ export { configure, pauseAll, register, registerAll, resumeAll, unregister, unre
|
|
|
13
13
|
// Loaded for its `declare global` block as much as for the constants: the
|
|
14
14
|
// ElementEventMap and DocumentEventMap augmentation only reaches a consumer if
|
|
15
15
|
// this module is part of their program.
|
|
16
|
-
export { POLITE_VIDEO_PAUSECHANGE, POLITE_VIDEO_FAILED, POLITE_VIDEO_READY, } from './events.js';
|
|
16
|
+
export { POLITE_VIDEO_BLOCKED, POLITE_VIDEO_PAUSECHANGE, POLITE_VIDEO_FAILED, POLITE_VIDEO_READY, } from './events.js';
|
|
17
17
|
//# sourceMappingURL=video.js.map
|
package/dist/video.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"video.js","sourceRoot":"","sources":["../src/video.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EACL,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,SAAS,EACT,UAAU,EACV,aAAa,GACd,MAAM,kBAAkB,CAAC;AAG1B,0EAA0E;AAC1E,+EAA+E;AAC/E,wCAAwC;AACxC,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,kBAAkB,GAGnB,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"video.js","sourceRoot":"","sources":["../src/video.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EACL,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,SAAS,EACT,UAAU,EACV,aAAa,GACd,MAAM,kBAAkB,CAAC;AAG1B,0EAA0E;AAC1E,+EAA+E;AAC/E,wCAAwC;AACxC,OAAO,EACL,oBAAoB,EACpB,wBAAwB,EACxB,mBAAmB,EACnB,kBAAkB,GAGnB,MAAM,aAAa,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polite-media",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Background video, image reveals and next-page image warming. Reveals on the frame that actually painted, plays only what is on screen, respects data and motion.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"video",
|
package/src/coordinator.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { connectionAllowsMedia, mediaQuery, motionAllowed } from './env.js';
|
|
2
2
|
import {
|
|
3
|
+
POLITE_VIDEO_BLOCKED,
|
|
3
4
|
POLITE_VIDEO_PAUSECHANGE,
|
|
4
5
|
POLITE_VIDEO_FAILED,
|
|
5
6
|
POLITE_VIDEO_READY,
|
|
@@ -333,7 +334,11 @@ export interface RegisterOptions {
|
|
|
333
334
|
interface Entry {
|
|
334
335
|
video: HTMLVideoElement;
|
|
335
336
|
target: Element;
|
|
336
|
-
/**
|
|
337
|
+
/**
|
|
338
|
+
* Element carrying reveal state, so CSS can drive poster and video together: the nearest
|
|
339
|
+
* `[data-polite-media]` ancestor, or the parent where none is authored so the warning below has
|
|
340
|
+
* an element to check.
|
|
341
|
+
*/
|
|
337
342
|
host: Element;
|
|
338
343
|
/** How much of the target is visible; 0 when offscreen. */
|
|
339
344
|
ratio: number;
|
|
@@ -543,12 +548,12 @@ function pauseAfterGrace(entry: Entry): void {
|
|
|
543
548
|
* Events, so a host can react without observing attributes or forking. Bubbling
|
|
544
549
|
* because the useful listener is usually on a container, not on each video.
|
|
545
550
|
*/
|
|
546
|
-
function emit(
|
|
551
|
+
function emit(
|
|
552
|
+
entry: Entry,
|
|
553
|
+
type: typeof POLITE_VIDEO_READY | typeof POLITE_VIDEO_FAILED | typeof POLITE_VIDEO_BLOCKED
|
|
554
|
+
): void {
|
|
547
555
|
entry.video.dispatchEvent(
|
|
548
|
-
new CustomEvent<PoliteVideoEventDetail>(
|
|
549
|
-
type === 'ready' ? POLITE_VIDEO_READY : POLITE_VIDEO_FAILED,
|
|
550
|
-
{ bubbles: true, detail: { video: entry.video } }
|
|
551
|
-
)
|
|
556
|
+
new CustomEvent<PoliteVideoEventDetail>(type, { bubbles: true, detail: { video: entry.video } })
|
|
552
557
|
);
|
|
553
558
|
}
|
|
554
559
|
|
|
@@ -559,7 +564,7 @@ function emit(entry: Entry, type: 'ready' | 'failed'): void {
|
|
|
559
564
|
*/
|
|
560
565
|
function markReady(entry: Entry): void {
|
|
561
566
|
entry.host.setAttribute('data-polite-ready', '');
|
|
562
|
-
emit(entry,
|
|
567
|
+
emit(entry, POLITE_VIDEO_READY);
|
|
563
568
|
}
|
|
564
569
|
|
|
565
570
|
function clearReady(entry: Entry): void {
|
|
@@ -569,6 +574,9 @@ function clearReady(entry: Entry): void {
|
|
|
569
574
|
}
|
|
570
575
|
|
|
571
576
|
function armReveal(entry: Entry): void {
|
|
577
|
+
// Still revealed from before it scrolled away, so the frame is on screen and re-arming would
|
|
578
|
+
// only announce `ready` a second time. Only a retraction, which removes the attribute, re-arms.
|
|
579
|
+
if (entry.host.hasAttribute('data-polite-ready')) return;
|
|
572
580
|
entry.cancelReveal?.();
|
|
573
581
|
entry.cancelReveal = revealWhenPainted(entry.video, () => markReady(entry));
|
|
574
582
|
}
|
|
@@ -644,11 +652,10 @@ let warnedNothingToReveal = false;
|
|
|
644
652
|
/**
|
|
645
653
|
* The one misconfiguration that is otherwise undetectable.
|
|
646
654
|
*
|
|
647
|
-
*
|
|
648
|
-
* `
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
* the library looks installed while doing nothing at all.
|
|
655
|
+
* video.css hides a video only inside an authored `[data-polite-media]` box. With
|
|
656
|
+
* none above the video, `host` falls back to its parent, every rule misses, the
|
|
657
|
+
* video is visible from the start, the poster never hides, and the library looks
|
|
658
|
+
* installed while doing nothing at all.
|
|
652
659
|
*
|
|
653
660
|
* Checked here rather than at registration because stylesheets have certainly
|
|
654
661
|
* applied by the time a video starts. The visual test is what separates a genuine
|
|
@@ -667,8 +674,9 @@ function warnIfNothingToReveal(entry: Entry): void {
|
|
|
667
674
|
|
|
668
675
|
warnedNothingToReveal = true;
|
|
669
676
|
console.warn(
|
|
670
|
-
|
|
671
|
-
'nothing. Put the attribute
|
|
677
|
+
'polite-media: no data-polite-media on any box around this video, so revealing it does ' +
|
|
678
|
+
'nothing. Put the attribute on the box holding poster and video, or hide the video with ' +
|
|
679
|
+
'your own CSS.',
|
|
672
680
|
entry.video
|
|
673
681
|
);
|
|
674
682
|
}
|
|
@@ -679,11 +687,27 @@ function warnIfNothingToReveal(entry: Entry): void {
|
|
|
679
687
|
*/
|
|
680
688
|
function markFailed(entry: Entry): void {
|
|
681
689
|
clearReady(entry);
|
|
690
|
+
clearBlocked(entry);
|
|
682
691
|
entry.host.setAttribute('data-polite-failed', '');
|
|
683
|
-
emit(entry,
|
|
692
|
+
emit(entry, POLITE_VIDEO_FAILED);
|
|
684
693
|
unregister(entry.video);
|
|
685
694
|
}
|
|
686
695
|
|
|
696
|
+
/**
|
|
697
|
+
* The browser refused to start playback until a gesture, which MDN reports as `NotAllowedError`.
|
|
698
|
+
* The retry rungs handle recovery; this tells the host, the only party that can offer a play
|
|
699
|
+
* affordance. The other rejection, nothing buffered yet, is transient and not announced.
|
|
700
|
+
*/
|
|
701
|
+
function markBlocked(entry: Entry): void {
|
|
702
|
+
if (entry.host.hasAttribute('data-polite-blocked')) return;
|
|
703
|
+
entry.host.setAttribute('data-polite-blocked', '');
|
|
704
|
+
emit(entry, POLITE_VIDEO_BLOCKED);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function clearBlocked(entry: Entry): void {
|
|
708
|
+
entry.host.removeAttribute('data-polite-blocked');
|
|
709
|
+
}
|
|
710
|
+
|
|
687
711
|
/**
|
|
688
712
|
* Waits for the next user gesture, then re-runs the arbiter.
|
|
689
713
|
*
|
|
@@ -734,20 +758,24 @@ function tryPlay(entry: Entry): void {
|
|
|
734
758
|
// muted is the only condition it can rely on. Set rather than trusted, which
|
|
735
759
|
// also covers markup whose property was changed after parse.
|
|
736
760
|
entry.video.muted = true;
|
|
737
|
-
void entry.video.play().
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
761
|
+
void entry.video.play().then(
|
|
762
|
+
() => clearBlocked(entry),
|
|
763
|
+
(error: unknown) => {
|
|
764
|
+
if ((error as DOMException | null)?.name === 'NotAllowedError') markBlocked(entry);
|
|
765
|
+
armGestureRetry();
|
|
766
|
+
|
|
767
|
+
if (entry.retryArmed) return;
|
|
768
|
+
entry.retryArmed = true;
|
|
769
|
+
entry.video.addEventListener(
|
|
770
|
+
'canplay',
|
|
771
|
+
() => {
|
|
772
|
+
entry.retryArmed = false;
|
|
773
|
+
reconcile();
|
|
774
|
+
},
|
|
775
|
+
{ once: true, signal: entry.listeners.signal }
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
);
|
|
751
779
|
}
|
|
752
780
|
|
|
753
781
|
function onMediaError(entry: Entry): void {
|
|
@@ -1067,19 +1095,22 @@ function detachLifecycle(): void {
|
|
|
1067
1095
|
}
|
|
1068
1096
|
|
|
1069
1097
|
/**
|
|
1070
|
-
*
|
|
1098
|
+
* Puts `<html>` and every declared pause control in step with `userPaused`.
|
|
1071
1099
|
*
|
|
1072
|
-
*
|
|
1073
|
-
* setting it unconditionally would break one of them: `aria-pressed`
|
|
1074
|
-
* control whose label stays constant, while a control that swaps its
|
|
1075
|
-
* between "Pause" and "Play" should not carry it at all -- a screen reader
|
|
1076
|
-
* announce "Play, pressed". Declaring the attribute in markup is the author
|
|
1100
|
+
* `aria-pressed` is maintained rather than added, because MDN describes two valid
|
|
1101
|
+
* patterns and setting it unconditionally would break one of them: `aria-pressed`
|
|
1102
|
+
* is for a control whose label stays constant, while a control that swaps its
|
|
1103
|
+
* label between "Pause" and "Play" should not carry it at all -- a screen reader
|
|
1104
|
+
* would announce "Play, pressed". Declaring the attribute in markup is the author
|
|
1077
1105
|
* saying which pattern they are in.
|
|
1078
1106
|
*
|
|
1079
1107
|
* Restricted to a button role because that is the only role `aria-pressed` is
|
|
1080
1108
|
* valid on, so this cannot emit ARIA that a validator would reject.
|
|
1081
1109
|
*/
|
|
1082
1110
|
function reflectPaused(): void {
|
|
1111
|
+
if (userPaused) document.documentElement.setAttribute('data-polite-paused', '');
|
|
1112
|
+
else document.documentElement.removeAttribute('data-polite-paused');
|
|
1113
|
+
|
|
1083
1114
|
for (const control of document.querySelectorAll('[data-polite-pause-control][aria-pressed]')) {
|
|
1084
1115
|
if (control.matches('button, [role="button"]')) {
|
|
1085
1116
|
control.setAttribute('aria-pressed', String(userPaused));
|
|
@@ -1138,7 +1169,6 @@ function writeStoredPause(paused: boolean): void {
|
|
|
1138
1169
|
function restorePaused(): void {
|
|
1139
1170
|
if (userPaused || !readStoredPause()) return;
|
|
1140
1171
|
userPaused = true;
|
|
1141
|
-
document.documentElement.setAttribute('data-polite-paused', '');
|
|
1142
1172
|
reflectPaused();
|
|
1143
1173
|
}
|
|
1144
1174
|
|
|
@@ -1211,7 +1241,7 @@ export function register(video: HTMLVideoElement, options: RegisterOptions = {})
|
|
|
1211
1241
|
const entry: Entry = {
|
|
1212
1242
|
video,
|
|
1213
1243
|
target,
|
|
1214
|
-
host: video.parentElement ?? video,
|
|
1244
|
+
host: video.closest('[data-polite-media]') ?? video.parentElement ?? video,
|
|
1215
1245
|
ratio: 0,
|
|
1216
1246
|
gated: Boolean(options.until),
|
|
1217
1247
|
hadGate: Boolean(options.until),
|
|
@@ -1227,6 +1257,9 @@ export function register(video: HTMLVideoElement, options: RegisterOptions = {})
|
|
|
1227
1257
|
byTarget.set(target, entry);
|
|
1228
1258
|
reflectActive();
|
|
1229
1259
|
attachLifecycle();
|
|
1260
|
+
// A client-side router replaces <html>'s attributes and the control with the next page's while
|
|
1261
|
+
// this module, and a pause made before the swap, survive. So it is re-asserted per registration.
|
|
1262
|
+
if (userPaused) reflectPaused();
|
|
1230
1263
|
getObserver().observe(target);
|
|
1231
1264
|
getPrefetchObserver()?.observe(target);
|
|
1232
1265
|
|
|
@@ -1253,7 +1286,9 @@ export type VideoTarget = Target<HTMLVideoElement>;
|
|
|
1253
1286
|
|
|
1254
1287
|
/**
|
|
1255
1288
|
* Registers every video a target names, so the common case is one line and
|
|
1256
|
-
* matches `revealImages` on the image side rather than being a second idea.
|
|
1289
|
+
* matches `revealImages` on the image side rather than being a second idea. With
|
|
1290
|
+
* no target it takes every video inside a `[data-polite-media]` box, which is
|
|
1291
|
+
* what every known caller passed by hand.
|
|
1257
1292
|
*
|
|
1258
1293
|
* `observe` is deliberately not accepted. Each observed element maps to exactly
|
|
1259
1294
|
* one entry, so handing the same wrapper to several videos would silently
|
|
@@ -1264,7 +1299,7 @@ export type VideoTarget = Target<HTMLVideoElement>;
|
|
|
1264
1299
|
* client-side router, where module scripts do not re-run.
|
|
1265
1300
|
*/
|
|
1266
1301
|
export function registerAll(
|
|
1267
|
-
target: VideoTarget,
|
|
1302
|
+
target: VideoTarget = '[data-polite-media] video',
|
|
1268
1303
|
options: Omit<RegisterOptions, 'observe'> = {}
|
|
1269
1304
|
): void {
|
|
1270
1305
|
for (const video of resolveTargets(target)) register(video, options);
|
|
@@ -1341,9 +1376,6 @@ function setPaused(paused: boolean): void {
|
|
|
1341
1376
|
if (userPaused === paused) return;
|
|
1342
1377
|
userPaused = paused;
|
|
1343
1378
|
|
|
1344
|
-
if (paused) document.documentElement.setAttribute('data-polite-paused', '');
|
|
1345
|
-
else document.documentElement.removeAttribute('data-polite-paused');
|
|
1346
|
-
|
|
1347
1379
|
reflectPaused();
|
|
1348
1380
|
reconcile();
|
|
1349
1381
|
|
package/src/events.ts
CHANGED
|
@@ -27,6 +27,13 @@ export const POLITE_VIDEO_READY = 'polite-video:ready';
|
|
|
27
27
|
/** Dispatched on the video when no source could be decoded. Bubbles. */
|
|
28
28
|
export const POLITE_VIDEO_FAILED = 'polite-video:failed';
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Dispatched on the video when the browser refused `play()` until a user gesture.
|
|
32
|
+
* Bubbles. The library keeps retrying on its own; this exists so a host can show
|
|
33
|
+
* a play affordance in the meantime, which the library cannot do for it.
|
|
34
|
+
*/
|
|
35
|
+
export const POLITE_VIDEO_BLOCKED = 'polite-video:blocked';
|
|
36
|
+
|
|
30
37
|
/** Dispatched on the image once it has decoded. Bubbles. */
|
|
31
38
|
export const POLITE_IMAGE_READY = 'polite-image:ready';
|
|
32
39
|
|
|
@@ -66,12 +73,14 @@ declare global {
|
|
|
66
73
|
interface ElementEventMap {
|
|
67
74
|
'polite-video:ready': CustomEvent<PoliteVideoEventDetail>;
|
|
68
75
|
'polite-video:failed': CustomEvent<PoliteVideoEventDetail>;
|
|
76
|
+
'polite-video:blocked': CustomEvent<PoliteVideoEventDetail>;
|
|
69
77
|
'polite-image:ready': CustomEvent<PoliteImageEventDetail>;
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
interface DocumentEventMap {
|
|
73
81
|
'polite-video:ready': CustomEvent<PoliteVideoEventDetail>;
|
|
74
82
|
'polite-video:failed': CustomEvent<PoliteVideoEventDetail>;
|
|
83
|
+
'polite-video:blocked': CustomEvent<PoliteVideoEventDetail>;
|
|
75
84
|
'polite-image:ready': CustomEvent<PoliteImageEventDetail>;
|
|
76
85
|
'polite-video:pausechange': CustomEvent<PolitePauseEventDetail>;
|
|
77
86
|
}
|
package/src/image.css
CHANGED
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
* them invisible forever rather than merely unfaded. Per image, an image is
|
|
10
10
|
* hidden only if something is going to reveal it.
|
|
11
11
|
*
|
|
12
|
+
* Only a lazy image is hidden. An eager one may be the Largest Contentful Paint
|
|
13
|
+
* element, LCP excludes anything at opacity 0, and a deferred module cannot
|
|
14
|
+
* reveal it before first paint, so the stylesheet never touches it and the
|
|
15
|
+
* module marks it ready at once. To fade an eager image anyway, say so where
|
|
16
|
+
* this file can see it: `data-polite-reveal="eager"`.
|
|
17
|
+
*
|
|
12
18
|
* A REQUIREMENT rather than a suggestion: the container needs its own visible
|
|
13
19
|
* backdrop, usually a background-color. Unlike video, an image has no poster
|
|
14
20
|
* behind it, so a hidden image over a transparent container shows nothing.
|
|
@@ -33,7 +39,8 @@
|
|
|
33
39
|
*/
|
|
34
40
|
|
|
35
41
|
@media (scripting: enabled) {
|
|
36
|
-
img[data-polite-reveal]
|
|
42
|
+
img[data-polite-reveal][loading='lazy'],
|
|
43
|
+
img[data-polite-reveal='eager'] {
|
|
37
44
|
opacity: 0;
|
|
38
45
|
transition: opacity var(--polite-fade, 350ms) ease;
|
|
39
46
|
}
|
|
@@ -50,11 +57,9 @@
|
|
|
50
57
|
* stylesheet applied stayed at 0 for the whole delay and then snapped to 1
|
|
51
58
|
* unfaded, while Chromium and WebKit revealed it. Cancelling only the animation
|
|
52
59
|
* on such an image put it at 1 immediately.
|
|
53
|
-
*
|
|
54
|
-
* Eager images hid the bug, because they are marked ready inside the same task
|
|
55
|
-
* that applies the stylesheet, before this can take effect.
|
|
56
60
|
*/
|
|
57
|
-
img[data-polite-reveal]:not([data-polite-ready])
|
|
61
|
+
img[data-polite-reveal][loading='lazy']:not([data-polite-ready]):not([data-polite-managed]),
|
|
62
|
+
img[data-polite-reveal='eager']:not([data-polite-ready]):not([data-polite-managed]) {
|
|
58
63
|
animation: polite-reveal-failsafe 0s var(--polite-failsafe, 5s) forwards;
|
|
59
64
|
}
|
|
60
65
|
|