@fusion.dev/helix-headless 0.55.0 → 0.56.1
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 +259 -49
- 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/media-copy.d.ts +25 -0
- package/dist/video/root/media-copy.js +18 -0
- package/dist/video/root/video-root.d.ts +7 -36
- package/dist/video/root/video-root.js +20 -106
- package/package.json +5 -5
package/custom-elements.json
CHANGED
|
@@ -106950,6 +106950,142 @@
|
|
|
106950
106950
|
}
|
|
106951
106951
|
]
|
|
106952
106952
|
},
|
|
106953
|
+
{
|
|
106954
|
+
"kind": "javascript-module",
|
|
106955
|
+
"path": "src/video/root/extra-controls.ts",
|
|
106956
|
+
"declarations": [
|
|
106957
|
+
{
|
|
106958
|
+
"kind": "function",
|
|
106959
|
+
"name": "offers",
|
|
106960
|
+
"return": {
|
|
106961
|
+
"type": {
|
|
106962
|
+
"text": "boolean"
|
|
106963
|
+
}
|
|
106964
|
+
},
|
|
106965
|
+
"parameters": [
|
|
106966
|
+
{
|
|
106967
|
+
"name": "one",
|
|
106968
|
+
"type": {
|
|
106969
|
+
"text": "string"
|
|
106970
|
+
}
|
|
106971
|
+
},
|
|
106972
|
+
{
|
|
106973
|
+
"name": "player",
|
|
106974
|
+
"type": {
|
|
106975
|
+
"text": "Conditions"
|
|
106976
|
+
}
|
|
106977
|
+
}
|
|
106978
|
+
],
|
|
106979
|
+
"description": "Whether this platform can do a named control at all."
|
|
106980
|
+
},
|
|
106981
|
+
{
|
|
106982
|
+
"kind": "function",
|
|
106983
|
+
"name": "barControls",
|
|
106984
|
+
"return": {
|
|
106985
|
+
"type": {
|
|
106986
|
+
"text": "string[]"
|
|
106987
|
+
}
|
|
106988
|
+
},
|
|
106989
|
+
"parameters": [
|
|
106990
|
+
{
|
|
106991
|
+
"name": "written",
|
|
106992
|
+
"type": {
|
|
106993
|
+
"text": "string"
|
|
106994
|
+
}
|
|
106995
|
+
},
|
|
106996
|
+
{
|
|
106997
|
+
"name": "player",
|
|
106998
|
+
"type": {
|
|
106999
|
+
"text": "Conditions"
|
|
107000
|
+
}
|
|
107001
|
+
}
|
|
107002
|
+
],
|
|
107003
|
+
"description": "The extras asked for, in order, and only the ones this platform can do."
|
|
107004
|
+
},
|
|
107005
|
+
{
|
|
107006
|
+
"kind": "function",
|
|
107007
|
+
"name": "menuControls",
|
|
107008
|
+
"return": {
|
|
107009
|
+
"type": {
|
|
107010
|
+
"text": "string[]"
|
|
107011
|
+
}
|
|
107012
|
+
},
|
|
107013
|
+
"parameters": [
|
|
107014
|
+
{
|
|
107015
|
+
"name": "written",
|
|
107016
|
+
"type": {
|
|
107017
|
+
"text": "string"
|
|
107018
|
+
}
|
|
107019
|
+
},
|
|
107020
|
+
{
|
|
107021
|
+
"name": "onBar",
|
|
107022
|
+
"type": {
|
|
107023
|
+
"text": "readonly string[]"
|
|
107024
|
+
}
|
|
107025
|
+
},
|
|
107026
|
+
{
|
|
107027
|
+
"name": "player",
|
|
107028
|
+
"type": {
|
|
107029
|
+
"text": "Conditions"
|
|
107030
|
+
}
|
|
107031
|
+
}
|
|
107032
|
+
],
|
|
107033
|
+
"description": "The controls asked for behind the overflow button, in order.\n\nA name in both lists is on the bar and not here. Drawing it twice is worse\nthan either placement, and the bar is the one a reader can see."
|
|
107034
|
+
},
|
|
107035
|
+
{
|
|
107036
|
+
"kind": "function",
|
|
107037
|
+
"name": "ownFile",
|
|
107038
|
+
"return": {
|
|
107039
|
+
"type": {
|
|
107040
|
+
"text": "boolean"
|
|
107041
|
+
}
|
|
107042
|
+
},
|
|
107043
|
+
"parameters": [
|
|
107044
|
+
{
|
|
107045
|
+
"name": "src",
|
|
107046
|
+
"type": {
|
|
107047
|
+
"text": "string"
|
|
107048
|
+
}
|
|
107049
|
+
}
|
|
107050
|
+
],
|
|
107051
|
+
"description": "Whether the file a `src` points at is the player's to hand over.\n\nHere because it is the download control's other condition: whether it is\ndrawn at all is `source`, and whether the `download` attribute on it means\nanything is this.\n\n`blob:` and `data:` named rather than compared, because the rule `download`\nactually follows is same-origin *or one of those two*: a clip a consumer\ngenerated in the page has no origin to match and is theirs to save by\ndefinition. Comparing origins alone called both foreign, so the one case\nwhere a download certainly works was the one case it was not offered."
|
|
107052
|
+
}
|
|
107053
|
+
],
|
|
107054
|
+
"exports": [
|
|
107055
|
+
{
|
|
107056
|
+
"kind": "js",
|
|
107057
|
+
"name": "offers",
|
|
107058
|
+
"declaration": {
|
|
107059
|
+
"name": "offers",
|
|
107060
|
+
"module": "src/video/root/extra-controls.ts"
|
|
107061
|
+
}
|
|
107062
|
+
},
|
|
107063
|
+
{
|
|
107064
|
+
"kind": "js",
|
|
107065
|
+
"name": "barControls",
|
|
107066
|
+
"declaration": {
|
|
107067
|
+
"name": "barControls",
|
|
107068
|
+
"module": "src/video/root/extra-controls.ts"
|
|
107069
|
+
}
|
|
107070
|
+
},
|
|
107071
|
+
{
|
|
107072
|
+
"kind": "js",
|
|
107073
|
+
"name": "menuControls",
|
|
107074
|
+
"declaration": {
|
|
107075
|
+
"name": "menuControls",
|
|
107076
|
+
"module": "src/video/root/extra-controls.ts"
|
|
107077
|
+
}
|
|
107078
|
+
},
|
|
107079
|
+
{
|
|
107080
|
+
"kind": "js",
|
|
107081
|
+
"name": "ownFile",
|
|
107082
|
+
"declaration": {
|
|
107083
|
+
"name": "ownFile",
|
|
107084
|
+
"module": "src/video/root/extra-controls.ts"
|
|
107085
|
+
}
|
|
107086
|
+
}
|
|
107087
|
+
]
|
|
107088
|
+
},
|
|
106953
107089
|
{
|
|
106954
107090
|
"kind": "javascript-module",
|
|
106955
107091
|
"path": "src/video/root/fullscreen.ts",
|
|
@@ -107061,6 +107197,118 @@
|
|
|
107061
107197
|
}
|
|
107062
107198
|
]
|
|
107063
107199
|
},
|
|
107200
|
+
{
|
|
107201
|
+
"kind": "javascript-module",
|
|
107202
|
+
"path": "src/video/root/keys.ts",
|
|
107203
|
+
"declarations": [
|
|
107204
|
+
{
|
|
107205
|
+
"kind": "function",
|
|
107206
|
+
"name": "takeKey",
|
|
107207
|
+
"return": {
|
|
107208
|
+
"type": {
|
|
107209
|
+
"text": "boolean"
|
|
107210
|
+
}
|
|
107211
|
+
},
|
|
107212
|
+
"parameters": [
|
|
107213
|
+
{
|
|
107214
|
+
"name": "event",
|
|
107215
|
+
"type": {
|
|
107216
|
+
"text": "KeyboardEvent"
|
|
107217
|
+
}
|
|
107218
|
+
},
|
|
107219
|
+
{
|
|
107220
|
+
"name": "video",
|
|
107221
|
+
"type": {
|
|
107222
|
+
"text": "HTMLVideoElement"
|
|
107223
|
+
}
|
|
107224
|
+
},
|
|
107225
|
+
{
|
|
107226
|
+
"name": "actions",
|
|
107227
|
+
"type": {
|
|
107228
|
+
"text": "KeyActions"
|
|
107229
|
+
}
|
|
107230
|
+
}
|
|
107231
|
+
],
|
|
107232
|
+
"description": "Answers one press, and says whether it was one of these.\n\nNothing is taken while focus is in a range. The scrubber and the volume are\nreal inputs and the arrows are already theirs, so intercepting would mean\nseeking twice for one press."
|
|
107233
|
+
}
|
|
107234
|
+
],
|
|
107235
|
+
"exports": [
|
|
107236
|
+
{
|
|
107237
|
+
"kind": "js",
|
|
107238
|
+
"name": "takeKey",
|
|
107239
|
+
"declaration": {
|
|
107240
|
+
"name": "takeKey",
|
|
107241
|
+
"module": "src/video/root/keys.ts"
|
|
107242
|
+
}
|
|
107243
|
+
}
|
|
107244
|
+
]
|
|
107245
|
+
},
|
|
107246
|
+
{
|
|
107247
|
+
"kind": "javascript-module",
|
|
107248
|
+
"path": "src/video/root/media-copy.ts",
|
|
107249
|
+
"declarations": [
|
|
107250
|
+
{
|
|
107251
|
+
"kind": "function",
|
|
107252
|
+
"name": "copiesOf",
|
|
107253
|
+
"return": {
|
|
107254
|
+
"type": {
|
|
107255
|
+
"text": "MediaCopy[]"
|
|
107256
|
+
}
|
|
107257
|
+
},
|
|
107258
|
+
"parameters": [
|
|
107259
|
+
{
|
|
107260
|
+
"name": "elements",
|
|
107261
|
+
"type": {
|
|
107262
|
+
"text": "readonly Element[]"
|
|
107263
|
+
}
|
|
107264
|
+
}
|
|
107265
|
+
],
|
|
107266
|
+
"description": "The copies to make of what the default slot is given."
|
|
107267
|
+
},
|
|
107268
|
+
{
|
|
107269
|
+
"kind": "function",
|
|
107270
|
+
"name": "sameCopies",
|
|
107271
|
+
"return": {
|
|
107272
|
+
"type": {
|
|
107273
|
+
"text": "boolean"
|
|
107274
|
+
}
|
|
107275
|
+
},
|
|
107276
|
+
"parameters": [
|
|
107277
|
+
{
|
|
107278
|
+
"name": "one",
|
|
107279
|
+
"type": {
|
|
107280
|
+
"text": "readonly MediaCopy[]"
|
|
107281
|
+
}
|
|
107282
|
+
},
|
|
107283
|
+
{
|
|
107284
|
+
"name": "two",
|
|
107285
|
+
"type": {
|
|
107286
|
+
"text": "readonly MediaCopy[]"
|
|
107287
|
+
}
|
|
107288
|
+
}
|
|
107289
|
+
],
|
|
107290
|
+
"description": "Whether two reads describe the same media.\n\nThe same set again is not a change. A video reloads for every new list, and\nthe first `slotchange` reports the children the element has already read."
|
|
107291
|
+
}
|
|
107292
|
+
],
|
|
107293
|
+
"exports": [
|
|
107294
|
+
{
|
|
107295
|
+
"kind": "js",
|
|
107296
|
+
"name": "copiesOf",
|
|
107297
|
+
"declaration": {
|
|
107298
|
+
"name": "copiesOf",
|
|
107299
|
+
"module": "src/video/root/media-copy.ts"
|
|
107300
|
+
}
|
|
107301
|
+
},
|
|
107302
|
+
{
|
|
107303
|
+
"kind": "js",
|
|
107304
|
+
"name": "sameCopies",
|
|
107305
|
+
"declaration": {
|
|
107306
|
+
"name": "sameCopies",
|
|
107307
|
+
"module": "src/video/root/media-copy.ts"
|
|
107308
|
+
}
|
|
107309
|
+
}
|
|
107310
|
+
]
|
|
107311
|
+
},
|
|
107064
107312
|
{
|
|
107065
107313
|
"kind": "javascript-module",
|
|
107066
107314
|
"path": "src/video/root/media-session.ts",
|
|
@@ -107625,16 +107873,6 @@
|
|
|
107625
107873
|
"description": "What the play control is called while it would start the video.",
|
|
107626
107874
|
"attribute": "play-label"
|
|
107627
107875
|
},
|
|
107628
|
-
{
|
|
107629
|
-
"kind": "field",
|
|
107630
|
-
"name": "localFile",
|
|
107631
|
-
"type": {
|
|
107632
|
-
"text": "boolean"
|
|
107633
|
-
},
|
|
107634
|
-
"privacy": "private",
|
|
107635
|
-
"description": "Whether the file this points at is ours to hand over.\n\n`blob:` and `data:` named rather than compared, because the rule `download`\nactually follows is same-origin *or one of those two*: a clip a consumer\ngenerated in the page has no origin to match and is theirs to save by\ndefinition. Comparing origins alone called both foreign, so the one case\nwhere a download certainly works was the one case it was not offered.",
|
|
107636
|
-
"readonly": true
|
|
107637
|
-
},
|
|
107638
107876
|
{
|
|
107639
107877
|
"kind": "field",
|
|
107640
107878
|
"name": "playsInline",
|
|
@@ -107881,23 +108119,14 @@
|
|
|
107881
108119
|
"default": "new MediaSessionController(this)"
|
|
107882
108120
|
},
|
|
107883
108121
|
{
|
|
107884
|
-
"kind": "
|
|
107885
|
-
"name": "
|
|
107886
|
-
"
|
|
107887
|
-
|
|
107888
|
-
"type": {
|
|
107889
|
-
"text": "boolean"
|
|
107890
|
-
}
|
|
108122
|
+
"kind": "field",
|
|
108123
|
+
"name": "conditions",
|
|
108124
|
+
"type": {
|
|
108125
|
+
"text": "Conditions"
|
|
107891
108126
|
},
|
|
107892
|
-
"
|
|
107893
|
-
|
|
107894
|
-
|
|
107895
|
-
"type": {
|
|
107896
|
-
"text": "string"
|
|
107897
|
-
}
|
|
107898
|
-
}
|
|
107899
|
-
],
|
|
107900
|
-
"description": "Whether this platform can do a named control at all."
|
|
108127
|
+
"privacy": "private",
|
|
108128
|
+
"description": "What the roster asks about this player, which is the whole of what it reads.",
|
|
108129
|
+
"readonly": true
|
|
107901
108130
|
},
|
|
107902
108131
|
{
|
|
107903
108132
|
"kind": "field",
|
|
@@ -107916,14 +108145,14 @@
|
|
|
107916
108145
|
"text": "string[]"
|
|
107917
108146
|
},
|
|
107918
108147
|
"privacy": "private",
|
|
107919
|
-
"description": "The controls asked for behind the overflow button, in order
|
|
108148
|
+
"description": "The controls asked for behind the overflow button, in order.",
|
|
107920
108149
|
"readonly": true
|
|
107921
108150
|
},
|
|
107922
108151
|
{
|
|
107923
108152
|
"kind": "field",
|
|
107924
108153
|
"name": "media",
|
|
107925
108154
|
"type": {
|
|
107926
|
-
"text": "
|
|
108155
|
+
"text": "MediaCopy[]"
|
|
107927
108156
|
},
|
|
107928
108157
|
"privacy": "private",
|
|
107929
108158
|
"default": "[]",
|
|
@@ -108157,31 +108386,12 @@
|
|
|
108157
108386
|
"default": "false",
|
|
108158
108387
|
"description": "Whether the first set of copies has been read from the children."
|
|
108159
108388
|
},
|
|
108160
|
-
{
|
|
108161
|
-
"kind": "method",
|
|
108162
|
-
"name": "copiesOf",
|
|
108163
|
-
"privacy": "private",
|
|
108164
|
-
"return": {
|
|
108165
|
-
"type": {
|
|
108166
|
-
"text": "{ attrs: Record<string, string>; tag: 'source' | 'track' }[]"
|
|
108167
|
-
}
|
|
108168
|
-
},
|
|
108169
|
-
"parameters": [
|
|
108170
|
-
{
|
|
108171
|
-
"name": "elements",
|
|
108172
|
-
"type": {
|
|
108173
|
-
"text": "Element[]"
|
|
108174
|
-
}
|
|
108175
|
-
}
|
|
108176
|
-
],
|
|
108177
|
-
"description": "The copies to make of what the default slot is given.\n\nThe attribute list is explicit rather than a blind copy of every attribute:\nthese are two elements with a known and small surface, and copying anything\na consumer happened to put on one would carry their framework's own markers\ninto a shadow root that has no use for them."
|
|
108178
|
-
},
|
|
108179
108389
|
{
|
|
108180
108390
|
"kind": "field",
|
|
108181
108391
|
"name": "handleMedia",
|
|
108182
108392
|
"privacy": "private",
|
|
108183
108393
|
"readonly": true,
|
|
108184
|
-
"description": "Rebuilds the copies when what is slotted changes after the first read
|
|
108394
|
+
"description": "Rebuilds the copies when what is slotted changes after the first read."
|
|
108185
108395
|
},
|
|
108186
108396
|
{
|
|
108187
108397
|
"kind": "field",
|
|
@@ -108344,7 +108554,7 @@
|
|
|
108344
108554
|
"name": "handleKeydown",
|
|
108345
108555
|
"privacy": "private",
|
|
108346
108556
|
"readonly": true,
|
|
108347
|
-
"description": "The keys every player uses, bound to this element rather than the document.\n\nA video is not a command palette: its keys belong to it while a reader is\non it, and a page with two videos on it would otherwise have both answering\nthe same press.\n\
|
|
108557
|
+
"description": "The keys every player uses, bound to this element rather than the document.\n\nA video is not a command palette: its keys belong to it while a reader is\non it, and a page with two videos on it would otherwise have both answering\nthe same press.\n\nA taken press is stopped here rather than in the table, because what the\ndefault would have been is the element's business: the table says what the\nkey means and this says that nothing else gets it."
|
|
108348
108558
|
},
|
|
108349
108559
|
{
|
|
108350
108560
|
"kind": "field",
|
|
@@ -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 './media-session.js';
|
|
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
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the consumer slotted, as the plain data a `<video>` can be given.
|
|
3
|
+
*
|
|
4
|
+
* Separable because it is a read of light DOM and nothing else: no media
|
|
5
|
+
* element, no playback state, nothing drawn. A media element runs resource
|
|
6
|
+
* selection over its own child nodes and a slot is one node, so the element
|
|
7
|
+
* copies rather than slots, and the two questions the copying asks are answered
|
|
8
|
+
* here: which attributes come across, and whether a fresh read differs from the
|
|
9
|
+
* set already held. When to read, and what to do with the answer, stay with the
|
|
10
|
+
* element.
|
|
11
|
+
*/
|
|
12
|
+
/** A `<source>` or `<track>` as the consumer wrote it, reduced to what the copy carries. */
|
|
13
|
+
export interface MediaCopy {
|
|
14
|
+
attrs: Record<string, string>;
|
|
15
|
+
tag: 'source' | 'track';
|
|
16
|
+
}
|
|
17
|
+
/** The copies to make of what the default slot is given. */
|
|
18
|
+
export declare const copiesOf: (elements: readonly Element[]) => MediaCopy[];
|
|
19
|
+
/**
|
|
20
|
+
* Whether two reads describe the same media.
|
|
21
|
+
*
|
|
22
|
+
* The same set again is not a change. A video reloads for every new list, and
|
|
23
|
+
* the first `slotchange` reports the children the element has already read.
|
|
24
|
+
*/
|
|
25
|
+
export declare const sameCopies: (one: readonly MediaCopy[], two: readonly MediaCopy[]) => boolean;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const CARRIED = {
|
|
2
|
+
source: ['media', 'sizes', 'src', 'srcset', 'type'],
|
|
3
|
+
track: ['default', 'kind', 'label', 'src', 'srclang'],
|
|
4
|
+
};
|
|
5
|
+
export const copiesOf = (elements) => elements
|
|
6
|
+
.filter((node) => node instanceof HTMLSourceElement || node instanceof HTMLTrackElement)
|
|
7
|
+
.map((node) => {
|
|
8
|
+
const tag = node instanceof HTMLTrackElement ? 'track' : 'source';
|
|
9
|
+
const attrs = {};
|
|
10
|
+
for (const name of CARRIED[tag]) {
|
|
11
|
+
const value = node.getAttribute(name);
|
|
12
|
+
if (value !== null) {
|
|
13
|
+
attrs[name] = value;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return { attrs, tag };
|
|
17
|
+
});
|
|
18
|
+
export const sameCopies = (one, two) => JSON.stringify(one) === JSON.stringify(two);
|
|
@@ -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. */
|
|
@@ -11,8 +11,11 @@ import { property, state } from 'lit/decorators.js';
|
|
|
11
11
|
import { html } from 'lit/static-html.js';
|
|
12
12
|
import '../../menu/index.js';
|
|
13
13
|
import { activeCue, adoptTrack, CueController, captionsAmong, captionTracks, showCaption, } from './captions.js';
|
|
14
|
+
import { barControls, menuControls, ownFile } from './extra-controls.js';
|
|
14
15
|
import { FullscreenController } from './fullscreen.js';
|
|
15
16
|
import { IdleController } from './idle.js';
|
|
17
|
+
import { takeKey } from './keys.js';
|
|
18
|
+
import { copiesOf, sameCopies } from './media-copy.js';
|
|
16
19
|
import { MediaSessionController, SKIP_SECONDS } from './media-session.js';
|
|
17
20
|
import { RemotePlaybackController } from './remote-playback.js';
|
|
18
21
|
const clock = (seconds) => {
|
|
@@ -85,8 +88,8 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
|
|
|
85
88
|
this.media = [];
|
|
86
89
|
this.mediaRead = false;
|
|
87
90
|
this.handleMedia = (event) => {
|
|
88
|
-
const next =
|
|
89
|
-
if (
|
|
91
|
+
const next = copiesOf(event.target.assignedElements());
|
|
92
|
+
if (!sameCopies(next, this.media)) {
|
|
90
93
|
this.media = next;
|
|
91
94
|
}
|
|
92
95
|
};
|
|
@@ -160,101 +163,30 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
|
|
|
160
163
|
if (this.controls) {
|
|
161
164
|
return;
|
|
162
165
|
}
|
|
163
|
-
if (event.target?.tagName === 'INPUT') {
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
166
|
const video = this.video;
|
|
167
167
|
if (!video) {
|
|
168
168
|
return;
|
|
169
169
|
}
|
|
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
|
-
}
|
|
170
|
+
const taken = takeKey(event, video, {
|
|
171
|
+
fullscreen: () => void this.fullscreen.toggle(),
|
|
172
|
+
seek: (seconds) => this.seek(seconds),
|
|
173
|
+
skipBy: (seconds) => this.skipBy(seconds),
|
|
174
|
+
toggle: () => this.toggle(),
|
|
175
|
+
});
|
|
176
|
+
if (taken) {
|
|
177
|
+
event.preventDefault();
|
|
212
178
|
}
|
|
213
|
-
event.preventDefault();
|
|
214
179
|
};
|
|
215
180
|
this.captions = [];
|
|
216
181
|
}
|
|
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';
|
|
182
|
+
get conditions() {
|
|
183
|
+
return { hydrating: this.hydrating, remote: this.remoteAvailable, source: this.src !== '' };
|
|
251
184
|
}
|
|
252
185
|
get extras() {
|
|
253
|
-
return
|
|
186
|
+
return barControls(this.extraControls, this.conditions);
|
|
254
187
|
}
|
|
255
188
|
get overflow() {
|
|
256
|
-
|
|
257
|
-
return writtenList(this.overflowControls).filter((one) => !onBar.has(one) && this.offers(one));
|
|
189
|
+
return menuControls(this.overflowControls, this.extras, this.conditions);
|
|
258
190
|
}
|
|
259
191
|
get currentSrc() {
|
|
260
192
|
return this.video?.currentSrc ?? '';
|
|
@@ -373,24 +305,6 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
|
|
|
373
305
|
video.volume = Number(event.target.value);
|
|
374
306
|
video.muted = false;
|
|
375
307
|
}
|
|
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
308
|
claimSession() {
|
|
395
309
|
const skips = writtenList(`${this.extraControls} ${this.overflowControls}`);
|
|
396
310
|
this.session.claim({ artwork: this.poster, title: this.label }, {
|
|
@@ -451,7 +365,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
|
|
|
451
365
|
super.willUpdate(changed);
|
|
452
366
|
if (!this.mediaRead && this.childrenKnown) {
|
|
453
367
|
this.mediaRead = true;
|
|
454
|
-
this.media =
|
|
368
|
+
this.media = copiesOf(this.knownChildren.filter((child) => child.slot === ''));
|
|
455
369
|
}
|
|
456
370
|
if (changed.has('media')) {
|
|
457
371
|
this.captions = captionsAmong(this.media);
|
|
@@ -542,7 +456,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
|
|
|
542
456
|
</button>`;
|
|
543
457
|
}
|
|
544
458
|
if (one === 'download') {
|
|
545
|
-
const local = this.
|
|
459
|
+
const local = ownFile(this.src);
|
|
546
460
|
return html `<a
|
|
547
461
|
part="download"
|
|
548
462
|
?download=${local}
|
|
@@ -651,7 +565,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
|
|
|
651
565
|
</${this.tag('menu-item')}>`;
|
|
652
566
|
}
|
|
653
567
|
if (one === 'download') {
|
|
654
|
-
const local = this.
|
|
568
|
+
const local = ownFile(this.src);
|
|
655
569
|
return html `<${this.tag('menu-item')} part="entry"
|
|
656
570
|
exportparts="item: entry-item, mark: entry-mark, start: entry-start"
|
|
657
571
|
?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.56.1",
|
|
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.56.1",
|
|
50
|
+
"@fusion.dev/helix-icons": "0.56.1",
|
|
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.56.1",
|
|
56
|
+
"@fusion.dev/helix-testing": "0.56.1",
|
|
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",
|