@fusion.dev/helix-headless 0.56.0 → 0.57.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/custom-elements.json +79 -427
- package/dist/video/root/extra-controls.d.ts +43 -0
- package/dist/video/root/extra-controls.js +41 -0
- package/dist/video/root/keys.d.ts +25 -0
- package/dist/video/root/keys.js +50 -0
- package/dist/video/root/video-root.d.ts +7 -36
- package/dist/video/root/video-root.js +20 -112
- package/package.json +5 -5
- package/dist/video/root/captions.d.ts +0 -85
- package/dist/video/root/captions.js +0 -45
- package/dist/video/root/fullscreen.d.ts +0 -28
- package/dist/video/root/fullscreen.js +0 -28
- package/dist/video/root/idle.d.ts +0 -34
- package/dist/video/root/idle.js +0 -24
- package/dist/video/root/media-session.d.ts +0 -54
- package/dist/video/root/media-session.js +0 -49
- package/dist/video/root/remote-playback.d.ts +0 -34
- package/dist/video/root/remote-playback.js +0 -38
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which extra controls a player offers, and which of them go behind the button.
|
|
3
|
+
*
|
|
4
|
+
* Separable because every answer here is taken from two written lists and three
|
|
5
|
+
* facts, before anything is drawn: what the consumer asked for, what this
|
|
6
|
+
* platform can do, and what is already on the bar. Nothing in it reaches the
|
|
7
|
+
* media element and nothing in it renders, which is what keeps the roster one
|
|
8
|
+
* decision rather than a condition spread across two templates.
|
|
9
|
+
*/
|
|
10
|
+
/** What the existence of a control depends on, which only the player can answer. */
|
|
11
|
+
export interface Conditions {
|
|
12
|
+
/** Whether this update is hydrating a server's markup. */
|
|
13
|
+
hydrating: boolean;
|
|
14
|
+
/** Whether anything on the network has offered to play this. */
|
|
15
|
+
remote: boolean;
|
|
16
|
+
/** Whether a file is named, which is what a download needs. */
|
|
17
|
+
source: boolean;
|
|
18
|
+
}
|
|
19
|
+
/** Whether this platform can do a named control at all. */
|
|
20
|
+
export declare const offers: (one: string, player: Conditions) => boolean;
|
|
21
|
+
/** The extras asked for, in order, and only the ones this platform can do. */
|
|
22
|
+
export declare const barControls: (written: string, player: Conditions) => string[];
|
|
23
|
+
/**
|
|
24
|
+
* The controls asked for behind the overflow button, in order.
|
|
25
|
+
*
|
|
26
|
+
* A name in both lists is on the bar and not here. Drawing it twice is worse
|
|
27
|
+
* than either placement, and the bar is the one a reader can see.
|
|
28
|
+
*/
|
|
29
|
+
export declare const menuControls: (written: string, onBar: readonly string[], player: Conditions) => string[];
|
|
30
|
+
/**
|
|
31
|
+
* Whether the file a `src` points at is the player's to hand over.
|
|
32
|
+
*
|
|
33
|
+
* Here because it is the download control's other condition: whether it is
|
|
34
|
+
* drawn at all is `source`, and whether the `download` attribute on it means
|
|
35
|
+
* anything is this.
|
|
36
|
+
*
|
|
37
|
+
* `blob:` and `data:` named rather than compared, because the rule `download`
|
|
38
|
+
* actually follows is same-origin *or one of those two*: a clip a consumer
|
|
39
|
+
* generated in the page has no origin to match and is theirs to save by
|
|
40
|
+
* definition. Comparing origins alone called both foreign, so the one case
|
|
41
|
+
* where a download certainly works was the one case it was not offered.
|
|
42
|
+
*/
|
|
43
|
+
export declare const ownFile: (src: string) => boolean;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { writtenList } from '@fusion.dev/helix-core';
|
|
2
|
+
export const offers = (one, player) => {
|
|
3
|
+
if (one === 'pip') {
|
|
4
|
+
return !player.hydrating && typeof document !== 'undefined' && document.pictureInPictureEnabled;
|
|
5
|
+
}
|
|
6
|
+
if (one === 'remote') {
|
|
7
|
+
return player.remote;
|
|
8
|
+
}
|
|
9
|
+
if (one === 'download') {
|
|
10
|
+
return player.source;
|
|
11
|
+
}
|
|
12
|
+
if (one === 'next' || one === 'previous') {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
if (one === 'forward' || one === 'rewind') {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
return one === 'speed';
|
|
19
|
+
};
|
|
20
|
+
export const barControls = (written, player) => writtenList(written).filter((one) => offers(one, player));
|
|
21
|
+
export const menuControls = (written, onBar, player) => {
|
|
22
|
+
const bar = new Set(onBar);
|
|
23
|
+
return writtenList(written).filter((one) => !bar.has(one) && offers(one, player));
|
|
24
|
+
};
|
|
25
|
+
export const ownFile = (src) => {
|
|
26
|
+
if (src === '') {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
if (src.startsWith('blob:') || src.startsWith('data:')) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
if (typeof location === 'undefined') {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
return new URL(src, location.href).origin === location.origin;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What each key does to a player, and how far it moves.
|
|
3
|
+
*
|
|
4
|
+
* Separable because the table is a convention rather than a fact about this
|
|
5
|
+
* element: Space and `k` play, the arrows scrub and change the volume, `m`
|
|
6
|
+
* mutes, `f` fills the screen, and `j` and `l` jump the distance the bar's own
|
|
7
|
+
* buttons jump. What it needs is the media element and four actions, which is
|
|
8
|
+
* the same handful the media session hands to a headset. Whether the keys are
|
|
9
|
+
* bound at all, and what a taken press does to the event, stay with the element.
|
|
10
|
+
*/
|
|
11
|
+
/** What a press asks the player for, which is all a key can do. */
|
|
12
|
+
export interface KeyActions {
|
|
13
|
+
fullscreen: () => void;
|
|
14
|
+
seek: (seconds: number) => void;
|
|
15
|
+
skipBy: (seconds: number) => void;
|
|
16
|
+
toggle: () => void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Answers one press, and says whether it was one of these.
|
|
20
|
+
*
|
|
21
|
+
* Nothing is taken while focus is in a range. The scrubber and the volume are
|
|
22
|
+
* real inputs and the arrows are already theirs, so intercepting would mean
|
|
23
|
+
* seeking twice for one press.
|
|
24
|
+
*/
|
|
25
|
+
export declare const takeKey: (event: KeyboardEvent, video: HTMLVideoElement, actions: KeyActions) => boolean;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { SKIP_SECONDS } from '@fusion.dev/helix-core';
|
|
2
|
+
export const takeKey = (event, video, actions) => {
|
|
3
|
+
if (event.target?.tagName === 'INPUT') {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
const step = event.shiftKey ? SKIP_SECONDS : 5;
|
|
7
|
+
switch (event.key) {
|
|
8
|
+
case ' ':
|
|
9
|
+
case 'k': {
|
|
10
|
+
actions.toggle();
|
|
11
|
+
break;
|
|
12
|
+
}
|
|
13
|
+
case 'ArrowRight': {
|
|
14
|
+
actions.seek(video.currentTime + step);
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
case 'ArrowLeft': {
|
|
18
|
+
actions.seek(video.currentTime - step);
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
case 'ArrowUp': {
|
|
22
|
+
video.volume = Math.min(video.volume + 0.1, 1);
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
case 'ArrowDown': {
|
|
26
|
+
video.volume = Math.max(video.volume - 0.1, 0);
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
case 'j': {
|
|
30
|
+
actions.skipBy(-SKIP_SECONDS);
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
case 'l': {
|
|
34
|
+
actions.skipBy(SKIP_SECONDS);
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
case 'm': {
|
|
38
|
+
video.muted = !video.muted;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
case 'f': {
|
|
42
|
+
actions.fullscreen();
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
default: {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
};
|
|
@@ -232,16 +232,6 @@ export declare class HelixHeadlessVideo extends HelixElement {
|
|
|
232
232
|
* What the play control is called while it would start the video.
|
|
233
233
|
*/
|
|
234
234
|
playLabel: string;
|
|
235
|
-
/**
|
|
236
|
-
* Whether the file this points at is ours to hand over.
|
|
237
|
-
*
|
|
238
|
-
* `blob:` and `data:` named rather than compared, because the rule `download`
|
|
239
|
-
* actually follows is same-origin *or one of those two*: a clip a consumer
|
|
240
|
-
* generated in the page has no origin to match and is theirs to save by
|
|
241
|
-
* definition. Comparing origins alone called both foreign, so the one case
|
|
242
|
-
* where a download certainly works was the one case it was not offered.
|
|
243
|
-
*/
|
|
244
|
-
private get localFile();
|
|
245
235
|
/**
|
|
246
236
|
* Whether it plays where it sits rather than taking over the screen.
|
|
247
237
|
*/
|
|
@@ -355,16 +345,11 @@ export declare class HelixHeadlessVideo extends HelixElement {
|
|
|
355
345
|
private readonly idler;
|
|
356
346
|
private readonly remote;
|
|
357
347
|
private readonly session;
|
|
358
|
-
/**
|
|
359
|
-
private
|
|
348
|
+
/** What the roster asks about this player, which is the whole of what it reads. */
|
|
349
|
+
private get conditions();
|
|
360
350
|
/** The extras asked for, in order, and only the ones this platform can do. */
|
|
361
351
|
private get extras();
|
|
362
|
-
/**
|
|
363
|
-
* The controls asked for behind the overflow button, in order.
|
|
364
|
-
*
|
|
365
|
-
* A name in both lists is on the bar and not here. Drawing it twice is worse
|
|
366
|
-
* than either placement, and the bar is the one a reader can see.
|
|
367
|
-
*/
|
|
352
|
+
/** The controls asked for behind the overflow button, in order. */
|
|
368
353
|
private get overflow();
|
|
369
354
|
/**
|
|
370
355
|
* The sources and caption tracks to copy into the video, as the consumer
|
|
@@ -468,21 +453,7 @@ export declare class HelixHeadlessVideo extends HelixElement {
|
|
|
468
453
|
private takeVolume;
|
|
469
454
|
/** Whether the first set of copies has been read from the children. */
|
|
470
455
|
private mediaRead;
|
|
471
|
-
/**
|
|
472
|
-
* The copies to make of what the default slot is given.
|
|
473
|
-
*
|
|
474
|
-
* The attribute list is explicit rather than a blind copy of every attribute:
|
|
475
|
-
* these are two elements with a known and small surface, and copying anything
|
|
476
|
-
* a consumer happened to put on one would carry their framework's own markers
|
|
477
|
-
* into a shadow root that has no use for them.
|
|
478
|
-
*/
|
|
479
|
-
private copiesOf;
|
|
480
|
-
/**
|
|
481
|
-
* Rebuilds the copies when what is slotted changes after the first read.
|
|
482
|
-
*
|
|
483
|
-
* The same set again is not a change. A video reloads for every new list, and
|
|
484
|
-
* the first `slotchange` reports the children this element already read.
|
|
485
|
-
*/
|
|
456
|
+
/** Rebuilds the copies when what is slotted changes after the first read. */
|
|
486
457
|
private readonly handleMedia;
|
|
487
458
|
private readonly handleLoaded;
|
|
488
459
|
private readonly handleTime;
|
|
@@ -568,9 +539,9 @@ export declare class HelixHeadlessVideo extends HelixElement {
|
|
|
568
539
|
* on it, and a page with two videos on it would otherwise have both answering
|
|
569
540
|
* the same press.
|
|
570
541
|
*
|
|
571
|
-
*
|
|
572
|
-
*
|
|
573
|
-
*
|
|
542
|
+
* A taken press is stopped here rather than in the table, because what the
|
|
543
|
+
* default would have been is the element's business: the table says what the
|
|
544
|
+
* key means and this says that nothing else gets it.
|
|
574
545
|
*/
|
|
575
546
|
private readonly handleKeydown;
|
|
576
547
|
/** The caption tracks among the copies, named as the menu names them. */
|
|
@@ -4,17 +4,14 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
import { customElement, drawIcon, HelixElement, HelixNextEvent, HelixPreviousEvent, onUnlessFalse, writtenList, } from '@fusion.dev/helix-core';
|
|
7
|
+
import { activeCue, adoptTrack, CueController, captionsAmong, captionTracks, copiesOf, customElement, drawIcon, FullscreenController, HelixElement, HelixNextEvent, HelixPreviousEvent, IdleController, MediaSessionController, onUnlessFalse, RemotePlaybackController, SKIP_SECONDS, sameCopies, showCaption, writtenList, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { airplay, arrowClockwise, arrowCounterClockwise, closedCaptioning, cornersIn, cornersOut, dotsThreeVertical, download, gauge, pause, pictureInPicture, play, skipBack, skipForward, speakerHigh, speakerX, } from '@fusion.dev/helix-icons';
|
|
9
9
|
import { nothing } from 'lit';
|
|
10
10
|
import { property, state } from 'lit/decorators.js';
|
|
11
11
|
import { html } from 'lit/static-html.js';
|
|
12
12
|
import '../../menu/index.js';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import { IdleController } from './idle.js';
|
|
16
|
-
import { MediaSessionController, SKIP_SECONDS } from './media-session.js';
|
|
17
|
-
import { RemotePlaybackController } from './remote-playback.js';
|
|
13
|
+
import { barControls, menuControls, ownFile } from './extra-controls.js';
|
|
14
|
+
import { takeKey } from './keys.js';
|
|
18
15
|
const clock = (seconds) => {
|
|
19
16
|
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
20
17
|
return '0:00';
|
|
@@ -85,8 +82,8 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
|
|
|
85
82
|
this.media = [];
|
|
86
83
|
this.mediaRead = false;
|
|
87
84
|
this.handleMedia = (event) => {
|
|
88
|
-
const next =
|
|
89
|
-
if (
|
|
85
|
+
const next = copiesOf(event.target.assignedElements());
|
|
86
|
+
if (!sameCopies(next, this.media)) {
|
|
90
87
|
this.media = next;
|
|
91
88
|
}
|
|
92
89
|
};
|
|
@@ -160,101 +157,30 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
|
|
|
160
157
|
if (this.controls) {
|
|
161
158
|
return;
|
|
162
159
|
}
|
|
163
|
-
if (event.target?.tagName === 'INPUT') {
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
160
|
const video = this.video;
|
|
167
161
|
if (!video) {
|
|
168
162
|
return;
|
|
169
163
|
}
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
this.seek(video.currentTime + step);
|
|
179
|
-
break;
|
|
180
|
-
}
|
|
181
|
-
case 'ArrowLeft': {
|
|
182
|
-
this.seek(video.currentTime - step);
|
|
183
|
-
break;
|
|
184
|
-
}
|
|
185
|
-
case 'ArrowUp': {
|
|
186
|
-
video.volume = Math.min(video.volume + 0.1, 1);
|
|
187
|
-
break;
|
|
188
|
-
}
|
|
189
|
-
case 'ArrowDown': {
|
|
190
|
-
video.volume = Math.max(video.volume - 0.1, 0);
|
|
191
|
-
break;
|
|
192
|
-
}
|
|
193
|
-
case 'j': {
|
|
194
|
-
this.skipBy(-SKIP_SECONDS);
|
|
195
|
-
break;
|
|
196
|
-
}
|
|
197
|
-
case 'l': {
|
|
198
|
-
this.skipBy(SKIP_SECONDS);
|
|
199
|
-
break;
|
|
200
|
-
}
|
|
201
|
-
case 'm': {
|
|
202
|
-
video.muted = !video.muted;
|
|
203
|
-
break;
|
|
204
|
-
}
|
|
205
|
-
case 'f': {
|
|
206
|
-
void this.fullscreen.toggle();
|
|
207
|
-
break;
|
|
208
|
-
}
|
|
209
|
-
default: {
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
164
|
+
const taken = takeKey(event, video, {
|
|
165
|
+
fullscreen: () => void this.fullscreen.toggle(),
|
|
166
|
+
seek: (seconds) => this.seek(seconds),
|
|
167
|
+
skipBy: (seconds) => this.skipBy(seconds),
|
|
168
|
+
toggle: () => this.toggle(),
|
|
169
|
+
});
|
|
170
|
+
if (taken) {
|
|
171
|
+
event.preventDefault();
|
|
212
172
|
}
|
|
213
|
-
event.preventDefault();
|
|
214
173
|
};
|
|
215
174
|
this.captions = [];
|
|
216
175
|
}
|
|
217
|
-
get
|
|
218
|
-
|
|
219
|
-
return false;
|
|
220
|
-
}
|
|
221
|
-
if (this.src.startsWith('blob:') || this.src.startsWith('data:')) {
|
|
222
|
-
return true;
|
|
223
|
-
}
|
|
224
|
-
if (typeof location === 'undefined') {
|
|
225
|
-
return false;
|
|
226
|
-
}
|
|
227
|
-
try {
|
|
228
|
-
return new URL(this.src, location.href).origin === location.origin;
|
|
229
|
-
}
|
|
230
|
-
catch {
|
|
231
|
-
return false;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
offers(one) {
|
|
235
|
-
if (one === 'pip') {
|
|
236
|
-
return !this.hydrating && typeof document !== 'undefined' && document.pictureInPictureEnabled;
|
|
237
|
-
}
|
|
238
|
-
if (one === 'remote') {
|
|
239
|
-
return this.remoteAvailable;
|
|
240
|
-
}
|
|
241
|
-
if (one === 'download') {
|
|
242
|
-
return this.src !== '';
|
|
243
|
-
}
|
|
244
|
-
if (one === 'next' || one === 'previous') {
|
|
245
|
-
return true;
|
|
246
|
-
}
|
|
247
|
-
if (one === 'forward' || one === 'rewind') {
|
|
248
|
-
return true;
|
|
249
|
-
}
|
|
250
|
-
return one === 'speed';
|
|
176
|
+
get conditions() {
|
|
177
|
+
return { hydrating: this.hydrating, remote: this.remoteAvailable, source: this.src !== '' };
|
|
251
178
|
}
|
|
252
179
|
get extras() {
|
|
253
|
-
return
|
|
180
|
+
return barControls(this.extraControls, this.conditions);
|
|
254
181
|
}
|
|
255
182
|
get overflow() {
|
|
256
|
-
|
|
257
|
-
return writtenList(this.overflowControls).filter((one) => !onBar.has(one) && this.offers(one));
|
|
183
|
+
return menuControls(this.overflowControls, this.extras, this.conditions);
|
|
258
184
|
}
|
|
259
185
|
get currentSrc() {
|
|
260
186
|
return this.video?.currentSrc ?? '';
|
|
@@ -373,24 +299,6 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
|
|
|
373
299
|
video.volume = Number(event.target.value);
|
|
374
300
|
video.muted = false;
|
|
375
301
|
}
|
|
376
|
-
copiesOf(elements) {
|
|
377
|
-
return elements
|
|
378
|
-
.filter((node) => node instanceof HTMLSourceElement || node instanceof HTMLTrackElement)
|
|
379
|
-
.map((node) => {
|
|
380
|
-
const tag = node instanceof HTMLTrackElement ? 'track' : 'source';
|
|
381
|
-
const names = tag === 'track'
|
|
382
|
-
? ['default', 'kind', 'label', 'src', 'srclang']
|
|
383
|
-
: ['media', 'sizes', 'src', 'srcset', 'type'];
|
|
384
|
-
const attrs = {};
|
|
385
|
-
for (const name of names) {
|
|
386
|
-
const value = node.getAttribute(name);
|
|
387
|
-
if (value !== null) {
|
|
388
|
-
attrs[name] = value;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
return { attrs, tag };
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
302
|
claimSession() {
|
|
395
303
|
const skips = writtenList(`${this.extraControls} ${this.overflowControls}`);
|
|
396
304
|
this.session.claim({ artwork: this.poster, title: this.label }, {
|
|
@@ -451,7 +359,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
|
|
|
451
359
|
super.willUpdate(changed);
|
|
452
360
|
if (!this.mediaRead && this.childrenKnown) {
|
|
453
361
|
this.mediaRead = true;
|
|
454
|
-
this.media =
|
|
362
|
+
this.media = copiesOf(this.knownChildren.filter((child) => child.slot === ''));
|
|
455
363
|
}
|
|
456
364
|
if (changed.has('media')) {
|
|
457
365
|
this.captions = captionsAmong(this.media);
|
|
@@ -542,7 +450,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
|
|
|
542
450
|
</button>`;
|
|
543
451
|
}
|
|
544
452
|
if (one === 'download') {
|
|
545
|
-
const local = this.
|
|
453
|
+
const local = ownFile(this.src);
|
|
546
454
|
return html `<a
|
|
547
455
|
part="download"
|
|
548
456
|
?download=${local}
|
|
@@ -651,7 +559,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
|
|
|
651
559
|
</${this.tag('menu-item')}>`;
|
|
652
560
|
}
|
|
653
561
|
if (one === 'download') {
|
|
654
|
-
const local = this.
|
|
562
|
+
const local = ownFile(this.src);
|
|
655
563
|
return html `<${this.tag('menu-item')} part="entry"
|
|
656
564
|
exportparts="item: entry-item, mark: entry-mark, start: entry-start"
|
|
657
565
|
?download=${local}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusion.dev/helix-headless",
|
|
3
3
|
"description": "Headless component tier. Structure, accessibility, state, and form participation. No styling.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.57.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ross Sheppard <ross.sheppard@fusion.dev>",
|
|
7
7
|
"repository": {
|
|
@@ -46,14 +46,14 @@
|
|
|
46
46
|
},
|
|
47
47
|
"bugs": "https://github.com/project-fusion/fusion/issues",
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@fusion.dev/helix-core": "0.
|
|
50
|
-
"@fusion.dev/helix-icons": "0.
|
|
49
|
+
"@fusion.dev/helix-core": "0.57.0",
|
|
50
|
+
"@fusion.dev/helix-icons": "0.57.0",
|
|
51
51
|
"uqr": "^0.1.3"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@custom-elements-manifest/analyzer": "^0.11.0",
|
|
55
|
-
"@fusion.dev/helix-cem": "0.
|
|
56
|
-
"@fusion.dev/helix-testing": "0.
|
|
55
|
+
"@fusion.dev/helix-cem": "0.57.0",
|
|
56
|
+
"@fusion.dev/helix-testing": "0.57.0",
|
|
57
57
|
"@fusion.dev/scripts": "0.0.0-dev",
|
|
58
58
|
"@happy-dom/global-registrator": "^20.0.0",
|
|
59
59
|
"@lit-labs/ssr": "^4.1.0",
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
2
|
-
/** One caption track, named as the menu names it. */
|
|
3
|
-
export interface Caption {
|
|
4
|
-
label: string;
|
|
5
|
-
language: string;
|
|
6
|
-
}
|
|
7
|
-
/** A `<track>` as the consumer wrote it, reduced to what naming it needs. */
|
|
8
|
-
interface TrackCopy {
|
|
9
|
-
attrs: Record<string, string>;
|
|
10
|
-
tag: string;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* The tracks a reader could choose between, in the order they were written.
|
|
14
|
-
*
|
|
15
|
-
* Filtered by kind, because `textTracks` also carries `chapters`, `metadata`
|
|
16
|
-
* and `descriptions`, and a menu offering "Chapters" as a subtitle language
|
|
17
|
-
* would be offering something that draws no words. Read off the video rather
|
|
18
|
-
* than off the slot: a `<track>` added later is in one and not the other, and
|
|
19
|
-
* the video is the one that has resolved them.
|
|
20
|
-
*/
|
|
21
|
-
export declare const captionTracks: (video: HTMLVideoElement | null) => TextTrack[];
|
|
22
|
-
/**
|
|
23
|
-
* Shows one track and disables every other, or shows none at `-1`.
|
|
24
|
-
*
|
|
25
|
-
* Every other, rather than only the one that was on. Two tracks in `hidden`
|
|
26
|
-
* both fire `cuechange`, so the rendered cue becomes whichever fired last and
|
|
27
|
-
* a reader who switched language gets the two interleaved. Measured as two
|
|
28
|
-
* subtitles alternating line by line.
|
|
29
|
-
*
|
|
30
|
-
* `hidden`, never `showing`. Hidden is the mode where a track still fires
|
|
31
|
-
* `cuechange` and still fills `activeCues`, and the browser draws nothing,
|
|
32
|
-
* which is exactly what is wanted when something else is doing the drawing.
|
|
33
|
-
*/
|
|
34
|
-
export declare const showCaption: (tracks: readonly TextTrack[], index: number) => void;
|
|
35
|
-
/**
|
|
36
|
-
* Whether a track the platform started showing is now ours to draw.
|
|
37
|
-
*
|
|
38
|
-
* A `<track default>` arrives `showing`, which is the browser drawing it.
|
|
39
|
-
* Moved to `hidden` so the cues keep coming and we draw them instead. What
|
|
40
|
-
* comes back is whether captions are on, which is not always "off": a button
|
|
41
|
-
* that began there reported the opposite of what was on screen.
|
|
42
|
-
*/
|
|
43
|
-
export declare const adoptTrack: (track: TextTrack) => boolean;
|
|
44
|
-
/**
|
|
45
|
-
* The caption tracks among the copies, before the render that draws them.
|
|
46
|
-
*
|
|
47
|
-
* By the platform's own reading of `kind`, which is what makes a `<track>` a
|
|
48
|
-
* caption at all: missing is `subtitles` and anything it does not know is
|
|
49
|
-
* `metadata`. Read from the video's tracks after the render instead, the count
|
|
50
|
-
* arrived a render late and asked for one more. Choosing one still goes
|
|
51
|
-
* through the video's tracks, which hold them in the same order.
|
|
52
|
-
*/
|
|
53
|
-
export declare const captionsAmong: (media: readonly TrackCopy[]) => Caption[];
|
|
54
|
-
/**
|
|
55
|
-
* The cue on screen now, as nodes, or nothing between cues.
|
|
56
|
-
*
|
|
57
|
-
* Nodes rather than a string, because `VTTCue.text` is the cue's *source*:
|
|
58
|
-
* WebVTT carries markup, and a caption file in the wild is full of it. `<v
|
|
59
|
-
* Hildy>` names who is speaking, `<b>` and `<i>` and `<u>` mark emphasis, and
|
|
60
|
-
* `<c.loud>` classes a line. Rendered as text every one of those arrives on
|
|
61
|
-
* screen as visible angle brackets.
|
|
62
|
-
*
|
|
63
|
-
* `getCueAsHTML` is the platform parsing its own format, so nothing here has
|
|
64
|
-
* to know the grammar and no cue text is ever treated as HTML: what comes
|
|
65
|
-
* back is a fragment the browser built, not a string anybody interpolated.
|
|
66
|
-
*/
|
|
67
|
-
export declare const activeCue: (track: TextTrack | undefined) => DocumentFragment | undefined;
|
|
68
|
-
/**
|
|
69
|
-
* Listens to one track's cues at a time, so the next one can take it off.
|
|
70
|
-
*
|
|
71
|
-
* Taken off whatever it was on before adding it here. `follow` runs from
|
|
72
|
-
* `loadedmetadata`, which fires again for every source a reader moves to, and
|
|
73
|
-
* a consumer adding a `<track>` later gets another. Added without removing, a
|
|
74
|
-
* playlist of six left six listeners on tracks that between them fire for
|
|
75
|
-
* every cue in the video.
|
|
76
|
-
*/
|
|
77
|
-
export declare class CueController implements ReactiveController {
|
|
78
|
-
private readonly listener;
|
|
79
|
-
private track;
|
|
80
|
-
constructor(host: ReactiveControllerHost, listener: () => void);
|
|
81
|
-
follow(track: TextTrack): void;
|
|
82
|
-
hostDisconnected(): void;
|
|
83
|
-
private stop;
|
|
84
|
-
}
|
|
85
|
-
export {};
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
export const captionTracks = (video) => {
|
|
2
|
-
const tracks = video?.textTracks;
|
|
3
|
-
if (!tracks) {
|
|
4
|
-
return [];
|
|
5
|
-
}
|
|
6
|
-
return [...tracks].filter((one) => one.kind === 'subtitles' || one.kind === 'captions');
|
|
7
|
-
};
|
|
8
|
-
export const showCaption = (tracks, index) => {
|
|
9
|
-
tracks.forEach((track, at) => {
|
|
10
|
-
track.mode = at === index ? 'hidden' : 'disabled';
|
|
11
|
-
});
|
|
12
|
-
};
|
|
13
|
-
export const adoptTrack = (track) => {
|
|
14
|
-
if (track.mode === 'showing') {
|
|
15
|
-
track.mode = 'hidden';
|
|
16
|
-
}
|
|
17
|
-
return track.mode === 'hidden';
|
|
18
|
-
};
|
|
19
|
-
export const captionsAmong = (media) => media
|
|
20
|
-
.filter((one) => one.tag === 'track' &&
|
|
21
|
-
(one.attrs.kind === undefined ||
|
|
22
|
-
['captions', 'subtitles'].includes(one.attrs.kind.toLowerCase())))
|
|
23
|
-
.map((one) => ({ label: one.attrs.label ?? '', language: one.attrs.srclang ?? '' }));
|
|
24
|
-
export const activeCue = (track) => {
|
|
25
|
-
const active = track?.activeCues?.[0];
|
|
26
|
-
return active instanceof VTTCue ? active.getCueAsHTML() : undefined;
|
|
27
|
-
};
|
|
28
|
-
export class CueController {
|
|
29
|
-
constructor(host, listener) {
|
|
30
|
-
this.listener = listener;
|
|
31
|
-
host.addController(this);
|
|
32
|
-
}
|
|
33
|
-
follow(track) {
|
|
34
|
-
this.stop();
|
|
35
|
-
this.track = track;
|
|
36
|
-
track.addEventListener('cuechange', this.listener);
|
|
37
|
-
}
|
|
38
|
-
hostDisconnected() {
|
|
39
|
-
this.stop();
|
|
40
|
-
}
|
|
41
|
-
stop() {
|
|
42
|
-
this.track?.removeEventListener('cuechange', this.listener);
|
|
43
|
-
this.track = undefined;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
2
|
-
/**
|
|
3
|
-
* Into fullscreen and back, and honest about where the element is.
|
|
4
|
-
*
|
|
5
|
-
* The state follows `fullscreenchange` rather than the request, so a refused
|
|
6
|
-
* request leaves a button that correctly reports "not fullscreen".
|
|
7
|
-
*/
|
|
8
|
-
export declare class FullscreenController implements ReactiveController {
|
|
9
|
-
private readonly host;
|
|
10
|
-
private readonly report;
|
|
11
|
-
constructor(host: ReactiveControllerHost & HTMLElement, report: (full: boolean) => void);
|
|
12
|
-
/**
|
|
13
|
-
* Into fullscreen and back, and quiet about being refused.
|
|
14
|
-
*
|
|
15
|
-
* `requestFullscreen` rejects rather than returning false, and the two
|
|
16
|
-
* refusals a consumer will actually meet are a Permissions Policy that does
|
|
17
|
-
* not grant `fullscreen` to the frame the player is in, and a call without
|
|
18
|
-
* user activation. Both are `TypeError: Permissions check failed`, which is
|
|
19
|
-
* the same message for two different causes.
|
|
20
|
-
*
|
|
21
|
-
* Caught rather than `void`ed, or a refusal becomes an unhandled rejection
|
|
22
|
-
* naming neither the element nor the reason.
|
|
23
|
-
*/
|
|
24
|
-
toggle(): Promise<void>;
|
|
25
|
-
hostConnected(): void;
|
|
26
|
-
hostDisconnected(): void;
|
|
27
|
-
private readonly handleChange;
|
|
28
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
export class FullscreenController {
|
|
2
|
-
constructor(host, report) {
|
|
3
|
-
this.host = host;
|
|
4
|
-
this.report = report;
|
|
5
|
-
this.handleChange = () => {
|
|
6
|
-
this.report(document.fullscreenElement === this.host);
|
|
7
|
-
};
|
|
8
|
-
host.addController(this);
|
|
9
|
-
}
|
|
10
|
-
async toggle() {
|
|
11
|
-
try {
|
|
12
|
-
if (document.fullscreenElement === this.host) {
|
|
13
|
-
await document.exitFullscreen();
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
|
-
await this.host.requestFullscreen();
|
|
17
|
-
}
|
|
18
|
-
catch (error) {
|
|
19
|
-
console.warn(`<${this.host.localName}> could not enter fullscreen. If it is inside an iframe, that frame needs allow="fullscreen".`, error);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
hostConnected() {
|
|
23
|
-
document.addEventListener('fullscreenchange', this.handleChange);
|
|
24
|
-
}
|
|
25
|
-
hostDisconnected() {
|
|
26
|
-
document.removeEventListener('fullscreenchange', this.handleChange);
|
|
27
|
-
}
|
|
28
|
-
}
|