@wcstack/picture-in-picture 1.16.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/README.ja.md +215 -0
- package/README.md +215 -0
- package/dist/auto.js +3 -0
- package/dist/auto.min.js +1 -0
- package/dist/index.d.ts +192 -0
- package/dist/index.esm.js +393 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.esm.min.js +2 -0
- package/dist/index.esm.min.js.map +1 -0
- package/package.json +71 -0
package/README.ja.md
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# @wcstack/picture-in-picture
|
|
2
|
+
|
|
3
|
+
`@wcstack/picture-in-picture` は wcstack エコシステム向けのヘッドレスな Picture-in-Picture コンポーネントです。
|
|
4
|
+
|
|
5
|
+
視覚的な UI ウィジェットではありません。
|
|
6
|
+
`@wcstack/fullscreen` が要素の全画面状態をリアクティブな state に変えるのと同じように、`<video>` 要素の Picture-in-Picture 状態をリアクティブな state に変える **非同期プリミティブノード** です。
|
|
7
|
+
|
|
8
|
+
`@wcstack/state` と組み合わせると、`<wcs-pip>` はパス契約で直接バインドできます:
|
|
9
|
+
|
|
10
|
+
- **入力サーフェス**: `target` — どの `<video>` 要素を操作するか
|
|
11
|
+
- **出力 state サーフェス**: `active`、`error`
|
|
12
|
+
- **コマンドサーフェス**: `requestPictureInPicture()`、`exitPictureInPicture()`
|
|
13
|
+
|
|
14
|
+
これにより、`document.pictureInPictureElement` を手動でポーリングしたりレイアウトを気にしたりせずに、「ポップアウト」ボタンを HTML 上で宣言的に表現できます。
|
|
15
|
+
|
|
16
|
+
`@wcstack/picture-in-picture` は [CSBC](https://github.com/csbc-dev/arch/blob/main/README.md)(Core / Shell / Binding Contract)アーキテクチャに従います:
|
|
17
|
+
|
|
18
|
+
- **Core**(`PipCore`)が Picture-in-Picture API を解決し(呼び出し時、非キャッシュ)、対象 `<video>` の `enterpictureinpicture`/`leavepictureinpicture` を購読し、`active`/`error` を追跡
|
|
19
|
+
- **Shell**(`<wcs-pip>`)が DOM からどの `<video>` を操作するか解決し、display・ライフサイクル・宣言的コマンドを管理
|
|
20
|
+
- **Binding Contract**(`static wcBindable`)が観測可能な `properties`、書き込み可能な `inputs`、呼び出し可能な `commands` を宣言
|
|
21
|
+
|
|
22
|
+
## なぜ存在するか — そして自分自身を操作しない理由
|
|
23
|
+
|
|
24
|
+
`@wcstack/fullscreen` と同様、この Shell は非表示の**制御タグ**です。自分自身を Picture-in-Picture 化するのではなく、`target` で解決した要素に対して Picture-in-Picture API を実行します。典型的なユースケースは動画プレイヤーの「ポップアウト」ボタンです。
|
|
25
|
+
|
|
26
|
+
`@wcstack/picture-in-picture` は `target` 解決のアーキタイプを `@wcstack/fullscreen` / `@wcstack/intersection` と共有します(詳細な根拠は `docs/fullscreen-tag-design.md` §1 参照)— 同じ3モード `target` 解決、同じ `_safeQuery` never-throw ラッパー、同じ Core 単位1つの `_gen` 世代ガード、同じシンプルな `error` フィールド(permission のような4値state は無し、`docs/fullscreen-tag-design.md` §8 参照)。
|
|
27
|
+
|
|
28
|
+
## スコープ: 古典的な Picture-in-Picture API のみ(`<video>` 限定)
|
|
29
|
+
|
|
30
|
+
「Picture-in-Picture」という名前を持つ、互いに無関係な2つのWeb Platform提案が存在します:
|
|
31
|
+
|
|
32
|
+
- **古典的な Picture-in-Picture API**(`HTMLVideoElement.requestPictureInPicture()`)— `<video>` 限定で、広くサポートされています。**`<wcs-pip>` が包むのはこちらです。**
|
|
33
|
+
- **Document Picture-in-Picture API**(`documentPictureInPicture.requestWindow()`)— 動画に限らず任意のDOMサブツリーを別の常に最前面のウィンドウに浮かせられます。API 形状(別の `Window` を取得してそこへ DOM を移動する)が、本ノードが `fullscreen`/`pointer-lock` と共有する「target を解決し、document レベルの状態を監視する」というアーキタイプと根本的に異なります。
|
|
34
|
+
|
|
35
|
+
**`<wcs-pip>` は古典的な `<video>` 限定 API を対象とします。Document Picture-in-Picture API は v1 のスコープ外です**(`docs/picture-in-picture-tag-design.md` §4 参照)。将来的に別ノード(例: `<wcs-doc-pip>`)として切り出される可能性があります。
|
|
36
|
+
|
|
37
|
+
### `target` は `<video>` 要素に解決されなければならない
|
|
38
|
+
|
|
39
|
+
Fullscreen(任意の `Element` がサポート)と異なり、Picture-in-Picture は `HTMLVideoElement` にのみ定義されています。`target` が `<video>` 以外の要素に解決された場合、`<wcs-pip>` はこれを未解決の target と同じ扱いにします。`requestPictureInPicture()` は例外を投げず、即座に `error` を `{ message: "target must be a <video> element." }` にセットして resolve します。
|
|
40
|
+
|
|
41
|
+
## インストール
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install @wcstack/picture-in-picture
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## クイックスタート
|
|
48
|
+
|
|
49
|
+
### 1. 動画プレイヤーの「ポップアウト」ボタン
|
|
50
|
+
|
|
51
|
+
```html
|
|
52
|
+
<script type="module" src="https://esm.run/@wcstack/state/auto"></script>
|
|
53
|
+
<script type="module" src="https://esm.run/@wcstack/picture-in-picture/auto"></script>
|
|
54
|
+
|
|
55
|
+
<wcs-state>
|
|
56
|
+
<script type="module">
|
|
57
|
+
export default {
|
|
58
|
+
$commandTokens: ["popOut", "backToPage"],
|
|
59
|
+
pipActive: false,
|
|
60
|
+
};
|
|
61
|
+
</script>
|
|
62
|
+
</wcs-state>
|
|
63
|
+
|
|
64
|
+
<video id="player" src="/movie.mp4" controls></video>
|
|
65
|
+
|
|
66
|
+
<wcs-pip target="#player" data-wcs="active: pipActive; command.requestPictureInPicture: $command.popOut; command.exitPictureInPicture: $command.backToPage"></wcs-pip>
|
|
67
|
+
|
|
68
|
+
<button data-wcs="onclick: $command.popOut">ポップアウト</button>
|
|
69
|
+
<button data-wcs="onclick: $command.backToPage; hidden: pipActive|not">ページに戻す</button>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
どちらのボタンも `<wcs-pip>` を直接触りません。クリックがコマンドトークンを emit し、`<wcs-pip>` は `command.requestPictureInPicture: $command.popOut` / `command.exitPictureInPicture: $command.backToPage` でそれらのトークンを購読します([command-token プロトコル](../state/) — コマンドメソッドを持つ要素が emit 側ではなく *購読側* です)。バインドする state パスは事前に必ず宣言する必要があります — ここでは `pipActive: false`。未宣言パスへのバインドは初期化時に throw します。`data-wcs` パスでの否定は先頭 `!` ではなく `|not` フィルタで行います(`pipActive|not`)— パスは前置演算子をサポートしません。
|
|
73
|
+
|
|
74
|
+
### 2. `<video>` を子要素としてラップする(セレクタ不要)
|
|
75
|
+
|
|
76
|
+
```html
|
|
77
|
+
<wcs-state>
|
|
78
|
+
<script type="module">
|
|
79
|
+
export default {
|
|
80
|
+
pipActive: false,
|
|
81
|
+
};
|
|
82
|
+
</script>
|
|
83
|
+
</wcs-state>
|
|
84
|
+
|
|
85
|
+
<wcs-pip data-wcs="active: pipActive">
|
|
86
|
+
<video src="/movie.mp4" controls></video>
|
|
87
|
+
</wcs-pip>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 3. 失敗を報告する(gesture 制約による reject 等)
|
|
91
|
+
|
|
92
|
+
`error` には専用イベントが無く `data-wcs` でバインドできません(下記「出力 state」参照)— コマンドの promise が解決した後に命令的に読みます:
|
|
93
|
+
|
|
94
|
+
```html
|
|
95
|
+
<wcs-state>
|
|
96
|
+
<script type="module">
|
|
97
|
+
export default {
|
|
98
|
+
$commandTokens: ["popOut"],
|
|
99
|
+
pipActive: false,
|
|
100
|
+
};
|
|
101
|
+
</script>
|
|
102
|
+
</wcs-state>
|
|
103
|
+
|
|
104
|
+
<wcs-pip target="#player" data-wcs="active: pipActive; command.requestPictureInPicture: $command.popOut"></wcs-pip>
|
|
105
|
+
<button data-wcs="onclick: $command.popOut">ポップアウト</button>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
const pip = document.querySelector("wcs-pip");
|
|
110
|
+
await pip.requestPictureInPicture();
|
|
111
|
+
if (pip.error) {
|
|
112
|
+
console.log("Picture-in-Picture に入れませんでした:", pip.error);
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## `target` 属性が操作対象を決める
|
|
117
|
+
|
|
118
|
+
| `target` | 操作対象 | `display` | ユースケース |
|
|
119
|
+
|-------------------|-------------------------|-------------|------------------------------|
|
|
120
|
+
| 省略 | 最初の子要素 | `contents` | `<video>` をインラインでラップ |
|
|
121
|
+
| `"#player"` / セレクタ | マッチした要素 | `none` | 独立した制御タグ |
|
|
122
|
+
| `"self"` | 自分自身 | `block` | **常に失敗する** — `<wcs-pip>` 自身が `<video>` になることはないため、`requestPictureInPicture()` は必ず error になる(上記「`target` は `<video>` 要素に解決されなければならない」参照) |
|
|
123
|
+
|
|
124
|
+
`display:contents` は `<video>` 子要素をラップしても自身のボックスを注入しないことを意味します。明示的な `target="self"` のみがボックスを持ちます。`packages/intersection` の `_resolveTarget()` をそのまま流用しています(`docs/fullscreen-tag-design.md` §1)。
|
|
125
|
+
|
|
126
|
+
`@wcstack/fullscreen`(任意の `Element` を全画面化できるため `target="self"` がラッパー自体を全画面化する正当な手段になる)と異なり、本ノードでの `target="self"` は構造的な行き止まりです: `<wcs-pip>` 自身の `tagName` が `VIDEO` になることは無いため、常に `<video>` 限定チェックに失敗し、あらゆる `requestPictureInPicture()` 呼び出しが `error` に帰結します。それでもこのモード自体は、共有する3モード `_resolveTarget()` アーキタイプとの整合のため不正な属性値として拒否はされず受理され続けますが、実用上意味を持つことはありません。
|
|
127
|
+
|
|
128
|
+
## 属性
|
|
129
|
+
|
|
130
|
+
| 属性 | 型 | デフォルト | 説明 |
|
|
131
|
+
|----------|--------|--------------|------|
|
|
132
|
+
| `target` | string | *(省略)* | どの `<video>` を操作するか: 省略 → 最初の子要素、セレクタ → マッチした要素、`self` → 自分自身(常に失敗する — 上記参照)。 |
|
|
133
|
+
|
|
134
|
+
## 出力 state
|
|
135
|
+
|
|
136
|
+
| プロパティ | 型 | イベント | 説明 |
|
|
137
|
+
|------------|-----------|-------------------|------|
|
|
138
|
+
| `active` | `boolean` | `wcs-pip:change` | 解決済みの `<video>` target が現在 document の Picture-in-Picture 要素かどうか。 |
|
|
139
|
+
| `error` | `any` | *(無し — 単なる getter、data-wcs でバインド不可)* | 直近のコマンド失敗(不正なタグ・非対応API・gesture 制約による reject)、または `null`。 |
|
|
140
|
+
|
|
141
|
+
`active` は、`enterpictureinpicture`/`leavepictureinpicture` が**その対象要素自身に**発火するたびに、`document.pictureInPictureElement` と解決済み `<video>` target を比較して導出されます(`document` レベルのイベントではありません — 詳細は下記「イベント購読」参照)。
|
|
142
|
+
|
|
143
|
+
## コマンド
|
|
144
|
+
|
|
145
|
+
| コマンド | 説明 |
|
|
146
|
+
|-------------------------------|------|
|
|
147
|
+
| `requestPictureInPicture()` | `target` の `<video>` を解決し、Picture-in-Picture を要求する。例外を投げない: `<video>` でない target・非対応API・gesture 制約による reject(`NotAllowedError`)は全て `error` に集約される。 |
|
|
148
|
+
| `exitPictureInPicture()` | Picture-in-Picture を終了する。何も PiP 中でなければ**silent no-op**(`@wcstack/fullscreen` の `exitFullscreen()` と同型 — `docs/fullscreen-tag-design.md` §7 参照)。 |
|
|
149
|
+
|
|
150
|
+
### ユーザージェスチャー要件
|
|
151
|
+
|
|
152
|
+
`requestPictureInPicture()` はユーザージェスチャー内(例: クリックハンドラ)から呼び出す必要があります。これはブラウザレベルの要件であり `<wcs-pip>` では回避できません — Fullscreen における同じ制約は `docs/fullscreen-tag-design.md` §3 を参照してください。command-token プロトコルでクリックへ直接配線することを推奨します(`<wcs-pip>` 側で `command.requestPictureInPicture: $command.<token>`、ボタン側で `onclick: $command.<token>` — 上記クイックスタート参照)。その際、*トリガーとなるイベント自体*が本物のユーザージェスチャーであることを確認してください。
|
|
153
|
+
|
|
154
|
+
`setTimeout` の中や `.then()` チェーンの奥から呼び出すと gesture 文脈が失われ、ブラウザがリクエストを拒否します — これは wcstack とは無関係な制約であり、このレイヤーでは修正できません。
|
|
155
|
+
|
|
156
|
+
## イベント購読先: `document` ではなく `<video>` 要素自身
|
|
157
|
+
|
|
158
|
+
Fullscreen の `fullscreenchange`(`document` に発火)と異なり、Picture-in-Picture の `enterpictureinpicture`/`leavepictureinpicture` は**`<video>` 要素自身に**発火します。`PipCore` はこれらのリスナーを解決済みの `<video>` に直接張り、`target` が再解決されるたび(例えば `target` 属性の変更)に旧要素から外し新要素に張り替えます。
|
|
159
|
+
|
|
160
|
+
これにより、複数の `<wcs-pip>` インスタンスは自然に自己フィルタされます。各インスタンスは自分の `<video>` target のイベントのみを受け取るため、あるインスタンスが Picture-in-Picture に入っても他のインスタンスの `active` が誤って `true` になることはありません(`docs/picture-in-picture-tag-design.md` §5 参照)。
|
|
161
|
+
|
|
162
|
+
## Binding Contract(`wcBindable`)
|
|
163
|
+
|
|
164
|
+
Core と Shell の両方が [wc-bindable](https://github.com/csbc-dev) プロトコルを宣言します。
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
// PipCore(ヘッドレス)
|
|
168
|
+
PipCore.wcBindable = {
|
|
169
|
+
protocol: "wc-bindable",
|
|
170
|
+
version: 1,
|
|
171
|
+
properties: [
|
|
172
|
+
{ name: "active", event: "wcs-pip:change", getter: (e) => e.detail.active },
|
|
173
|
+
],
|
|
174
|
+
commands: [
|
|
175
|
+
{ name: "requestPictureInPicture", async: true },
|
|
176
|
+
{ name: "exitPictureInPicture", async: true },
|
|
177
|
+
],
|
|
178
|
+
};
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Shell(`<wcs-pip>`)は Core の `properties`/`commands` を継承し、`target` 入力を宣言します。
|
|
182
|
+
|
|
183
|
+
## Core 単体での利用
|
|
184
|
+
|
|
185
|
+
`PipCore` はフレームワーク非依存です。操作したい `<video>` 要素を解決して渡します(Shell はこの解決を代わりに行います):
|
|
186
|
+
|
|
187
|
+
```js
|
|
188
|
+
import { PipCore } from "@wcstack/picture-in-picture";
|
|
189
|
+
|
|
190
|
+
const core = new PipCore();
|
|
191
|
+
core.addEventListener("wcs-pip:change", (e) => {
|
|
192
|
+
console.log(e.detail.active);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const video = document.querySelector("video");
|
|
196
|
+
core.observe(video); // enterpictureinpicture/leavepictureinpicture を購読
|
|
197
|
+
await core.requestPictureInPicture(video);
|
|
198
|
+
|
|
199
|
+
// 後で
|
|
200
|
+
await core.exitPictureInPicture();
|
|
201
|
+
core.dispose(); // リスナーを外す
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## 注意・制限
|
|
205
|
+
|
|
206
|
+
- **`<video>` 限定。** `target` は `HTMLVideoElement` に解決される必要があります。それ以外の要素は未解決扱いになり、`error` に `{ message: "target must be a <video> element." }` がセットされます(例外は投げません)。
|
|
207
|
+
- **Document Picture-in-Picture API はスコープ外。** 上記「スコープ」参照。
|
|
208
|
+
- **例外を投げない。** 非対応環境・不正なタグの target・gesture 制約による reject は全て `error` に集約されます。
|
|
209
|
+
- **`document.pictureInPictureElement` は document 全体で単一の値**です(`document.fullscreenElement` と同様)。複数の `<wcs-pip>` インスタンスは、自分の `<video>` target の `enterpictureinpicture`/`leavepictureinpicture` リスナーによって自己フィルタされます — 上記「イベント購読先」参照。ただし非対称な点に注意してください: `exitPictureInPicture()` はインスタンス単位にスコープされて**いません** — document全体に作用する `document.exitPictureInPicture()` を呼ぶため、どのインスタンスから呼んでも、現在 Picture-in-Picture 中の `<video>`(別インスタンスの `target` が PiP 化したものであっても)を終了させます(silent no-op の判定も同様に document 全体の「何かが PiP 中か」であり、「自分の target が PiP 中か」ではありません)。これはプラットフォーム API 自体の挙動、および `@wcstack/fullscreen` の `exitFullscreen()`(`docs/fullscreen-tag-design.md` §7 の「スコープの注意(§2.1との非対称)」、および `packages/fullscreen/README.ja.md` の「複数インスタンス」節参照)をそのまま反映したものです。
|
|
210
|
+
- **`desired`/`actual` の二相 state は無し** — 本ノードは単一の `active` boolean と `error` のみを公開し、`permission` より単純な `@wcstack/fullscreen` の state モデルと同型です。
|
|
211
|
+
- **`error` には専用イベントが無く、`data-wcs` でバインドできません。** `@wcstack/fullscreen` と同様、`error` は単なる getter であり、`wcs-pip:error` のような専用イベントを持たず、`static wcBindable.properties` にも宣言されていません — バインディングシステムには購読対象が無いため、リアクティブに観測できません。コマンドの promise が解決した後に `element.error` を命令的に読んでください(例: `await el.requestPictureInPicture(); if (el.error) { ... }`)。
|
|
212
|
+
|
|
213
|
+
## ライセンス
|
|
214
|
+
|
|
215
|
+
MIT
|
package/README.md
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# @wcstack/picture-in-picture
|
|
2
|
+
|
|
3
|
+
`@wcstack/picture-in-picture` is a headless Picture-in-Picture component for the wcstack ecosystem.
|
|
4
|
+
|
|
5
|
+
It is not a visual UI widget.
|
|
6
|
+
It is an **async primitive node** that turns a `<video>` element's Picture-in-Picture state into reactive state — the same way `@wcstack/fullscreen` turns an element's fullscreen state into reactive state.
|
|
7
|
+
|
|
8
|
+
With `@wcstack/state`, `<wcs-pip>` can be bound directly through path contracts:
|
|
9
|
+
|
|
10
|
+
- **input surface**: `target` — which `<video>` element to control
|
|
11
|
+
- **output state surface**: `active`, `error`
|
|
12
|
+
- **command surface**: `requestPictureInPicture()`, `exitPictureInPicture()`
|
|
13
|
+
|
|
14
|
+
This means a "pop out" video button — with no live-region layout thrashing, no manual `document.pictureInPictureElement` polling — can be expressed declaratively in HTML.
|
|
15
|
+
|
|
16
|
+
`@wcstack/picture-in-picture` follows the [CSBC](https://github.com/csbc-dev/arch/blob/main/README.md) (Core / Shell / Binding Contract) architecture:
|
|
17
|
+
|
|
18
|
+
- **Core** (`PipCore`) resolves the Picture-in-Picture API (call-time, never cached), subscribes to `enterpictureinpicture`/`leavepictureinpicture` on the target `<video>`, and tracks `active`/`error`
|
|
19
|
+
- **Shell** (`<wcs-pip>`) resolves *which* `<video>` element to control from the DOM, manages display, lifecycle, and declarative commands
|
|
20
|
+
- **Binding Contract** (`static wcBindable`) declares observable `properties`, writable `inputs`, and callable `commands`
|
|
21
|
+
|
|
22
|
+
## Why this exists — and why it doesn't operate on itself
|
|
23
|
+
|
|
24
|
+
Like `@wcstack/fullscreen`, this Shell is a non-visible **control tag**: it does not put itself into Picture-in-Picture. It resolves a `target` element and invokes the Picture-in-Picture API against *that* element. The typical use case is a video player's "pop out" button.
|
|
25
|
+
|
|
26
|
+
`@wcstack/picture-in-picture` shares its `target`-resolution archetype with `@wcstack/fullscreen` and `@wcstack/intersection` (see `docs/fullscreen-tag-design.md` §1 for the detailed rationale) — the same 3-mode `target` resolution, the same `_safeQuery` never-throw wrapper, the same single Core-level `_gen` generation guard, and the same simple `error` field (no permission-style 4-value state; see `docs/fullscreen-tag-design.md` §8).
|
|
27
|
+
|
|
28
|
+
## Scope: classic Picture-in-Picture API only (`<video>`-only)
|
|
29
|
+
|
|
30
|
+
There are two unrelated web platform proposals under the "Picture-in-Picture" name:
|
|
31
|
+
|
|
32
|
+
- **The classic Picture-in-Picture API** (`HTMLVideoElement.requestPictureInPicture()`) — `<video>`-only, broadly supported. **This is what `<wcs-pip>` wraps.**
|
|
33
|
+
- **The Document Picture-in-Picture API** (`documentPictureInPicture.requestWindow()`) — lets you float an arbitrary DOM subtree in a separate always-on-top window, not limited to video. Its API shape (acquiring a separate `Window` and moving DOM into it) is fundamentally different from the "resolve a target, watch a document-level state" archetype this node shares with `fullscreen`/`pointer-lock`.
|
|
34
|
+
|
|
35
|
+
**`<wcs-pip>` targets the classic, `<video>`-only API. The Document Picture-in-Picture API is out of scope for v1** — see `docs/picture-in-picture-tag-design.md` §4. It may become a separate node (e.g. `<wcs-doc-pip>`) in the future.
|
|
36
|
+
|
|
37
|
+
### The `target` MUST resolve to a `<video>` element
|
|
38
|
+
|
|
39
|
+
Unlike Fullscreen (which any `Element` supports), Picture-in-Picture is only defined on `HTMLVideoElement`. If `target` resolves to a non-`<video>` element, `<wcs-pip>` treats it the same as an unresolved target: `requestPictureInPicture()` never throws — it immediately sets `error` to `{ message: "target must be a <video> element." }` and resolves.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install @wcstack/picture-in-picture
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
### 1. A "pop out" button for a video player
|
|
50
|
+
|
|
51
|
+
```html
|
|
52
|
+
<script type="module" src="https://esm.run/@wcstack/state/auto"></script>
|
|
53
|
+
<script type="module" src="https://esm.run/@wcstack/picture-in-picture/auto"></script>
|
|
54
|
+
|
|
55
|
+
<wcs-state>
|
|
56
|
+
<script type="module">
|
|
57
|
+
export default {
|
|
58
|
+
$commandTokens: ["popOut", "backToPage"],
|
|
59
|
+
pipActive: false,
|
|
60
|
+
};
|
|
61
|
+
</script>
|
|
62
|
+
</wcs-state>
|
|
63
|
+
|
|
64
|
+
<video id="player" src="/movie.mp4" controls></video>
|
|
65
|
+
|
|
66
|
+
<wcs-pip target="#player" data-wcs="active: pipActive; command.requestPictureInPicture: $command.popOut; command.exitPictureInPicture: $command.backToPage"></wcs-pip>
|
|
67
|
+
|
|
68
|
+
<button data-wcs="onclick: $command.popOut">Pop out</button>
|
|
69
|
+
<button data-wcs="onclick: $command.backToPage; hidden: pipActive|not">Back to page</button>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Neither button touches `<wcs-pip>` directly: each click emits a command token, and `<wcs-pip>` subscribes to those tokens via `command.requestPictureInPicture: $command.popOut` / `command.exitPictureInPicture: $command.backToPage` (the [command-token protocol](../state/) — the element with the command method is the *subscriber*, not the emitter). Every bound state path must be declared up front — `pipActive: false` here; binding an undeclared path throws at initialization. Negation in a `data-wcs` path is done with the `|not` filter (`pipActive|not`), not a leading `!` — paths do not support prefix operators.
|
|
73
|
+
|
|
74
|
+
### 2. Wrapping the `<video>` as a child (no selector needed)
|
|
75
|
+
|
|
76
|
+
```html
|
|
77
|
+
<wcs-state>
|
|
78
|
+
<script type="module">
|
|
79
|
+
export default {
|
|
80
|
+
pipActive: false,
|
|
81
|
+
};
|
|
82
|
+
</script>
|
|
83
|
+
</wcs-state>
|
|
84
|
+
|
|
85
|
+
<wcs-pip data-wcs="active: pipActive">
|
|
86
|
+
<video src="/movie.mp4" controls></video>
|
|
87
|
+
</wcs-pip>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 3. Reporting failures (e.g. gesture-context rejection)
|
|
91
|
+
|
|
92
|
+
`error` has no dedicated event and is not `data-wcs` bindable (see "Output state" below) — read it imperatively after the command's promise settles:
|
|
93
|
+
|
|
94
|
+
```html
|
|
95
|
+
<wcs-state>
|
|
96
|
+
<script type="module">
|
|
97
|
+
export default {
|
|
98
|
+
$commandTokens: ["popOut"],
|
|
99
|
+
pipActive: false,
|
|
100
|
+
};
|
|
101
|
+
</script>
|
|
102
|
+
</wcs-state>
|
|
103
|
+
|
|
104
|
+
<wcs-pip target="#player" data-wcs="active: pipActive; command.requestPictureInPicture: $command.popOut"></wcs-pip>
|
|
105
|
+
<button data-wcs="onclick: $command.popOut">Pop out</button>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
const pip = document.querySelector("wcs-pip");
|
|
110
|
+
await pip.requestPictureInPicture();
|
|
111
|
+
if (pip.error) {
|
|
112
|
+
console.log("Could not enter Picture-in-Picture:", pip.error);
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## The `target` attribute decides what is controlled
|
|
117
|
+
|
|
118
|
+
| `target` | controls | `display` | use case |
|
|
119
|
+
|-----------------|-------------------------|-------------|--------------------------|
|
|
120
|
+
| omitted | first element child | `contents` | wrap a `<video>` inline |
|
|
121
|
+
| `"#player"` / sel | the matched element | `none` | separate control tag |
|
|
122
|
+
| `"self"` | the element itself | `block` | **always fails** — `<wcs-pip>` itself can never be a `<video>`, so `requestPictureInPicture()` immediately errors (see "The `target` MUST resolve to a `<video>` element" above) |
|
|
123
|
+
|
|
124
|
+
`display:contents` means wrapping a `<video>` child injects no box of its own. Only the explicit `target="self"` sentinel takes a box. See `packages/intersection`'s `_resolveTarget()` — this Shell reuses it verbatim (docs/fullscreen-tag-design.md §1).
|
|
125
|
+
|
|
126
|
+
Unlike `@wcstack/fullscreen` (where `target="self"` is a legitimate way to fullscreen the wrapper itself, since any `Element` can be fullscreened), `target="self"` here is a structural dead end: `<wcs-pip>`'s own `tagName` is never `VIDEO`, so it always fails the `<video>`-only check and every `requestPictureInPicture()` call resolves into `error`. The mode is still accepted rather than rejected as an invalid attribute value — for parity with the shared 3-mode `_resolveTarget()` archetype — it just never does anything useful.
|
|
127
|
+
|
|
128
|
+
## Attributes
|
|
129
|
+
|
|
130
|
+
| Attribute | Type | Default | Description |
|
|
131
|
+
|-----------|--------|-------------|--------------|
|
|
132
|
+
| `target` | string | *(omitted)* | Which `<video>` to control: omitted → first child, a selector → that element, `self` → this element (always fails — see above). |
|
|
133
|
+
|
|
134
|
+
## Output state
|
|
135
|
+
|
|
136
|
+
| Property | Type | Event | Description |
|
|
137
|
+
|----------|-----------|------------------|--------------|
|
|
138
|
+
| `active` | `boolean` | `wcs-pip:change` | Whether the resolved `<video>` target is currently the document's Picture-in-Picture element. |
|
|
139
|
+
| `error` | `any` | *(none — plain getter, not data-wcs bindable)* | The most recent command failure (wrong tag, unsupported API, gesture-context rejection), or `null`. |
|
|
140
|
+
|
|
141
|
+
`active` is derived from comparing `document.pictureInPictureElement` against the resolved `<video>` target whenever `enterpictureinpicture`/`leavepictureinpicture` fires **on that target element** — not from a `document`-level event (see "Event subscription" below).
|
|
142
|
+
|
|
143
|
+
## Commands
|
|
144
|
+
|
|
145
|
+
| Command | Description |
|
|
146
|
+
|-------------------------------|--------------|
|
|
147
|
+
| `requestPictureInPicture()` | Resolve the `target` `<video>` and request Picture-in-Picture for it. Never throws: a non-`<video>` target, an unsupported API, or a gesture-context rejection (`NotAllowedError`) all funnel into `error`. |
|
|
148
|
+
| `exitPictureInPicture()` | Exit Picture-in-Picture. **Silent no-op** if nothing is currently in Picture-in-Picture (mirrors `@wcstack/fullscreen`'s `exitFullscreen()` — see `docs/fullscreen-tag-design.md` §7). |
|
|
149
|
+
|
|
150
|
+
### User gesture requirement
|
|
151
|
+
|
|
152
|
+
`requestPictureInPicture()` must be called from within a user gesture (e.g. a click handler). This is a browser-level requirement `<wcs-pip>` cannot work around — see `docs/fullscreen-tag-design.md` §3 for the same constraint on Fullscreen. Prefer wiring the command directly to a click via the command-token protocol (`command.requestPictureInPicture: $command.<token>` on `<wcs-pip>`, emitted by a button's `onclick: $command.<token>` — see the Quick Start above), making sure the *triggering* event itself is a genuine user gesture.
|
|
153
|
+
|
|
154
|
+
Calling it from inside a `setTimeout` or deep inside a `.then()` chain loses the gesture context and the browser will reject the request — this is unrelated to wcstack and cannot be fixed at this layer.
|
|
155
|
+
|
|
156
|
+
## Event subscription: the `<video>` element itself, not `document`
|
|
157
|
+
|
|
158
|
+
Unlike Fullscreen's `fullscreenchange` (which fires on `document`), Picture-in-Picture's `enterpictureinpicture`/`leavepictureinpicture` events fire **on the `<video>` element itself**. `PipCore` attaches these listeners directly to the resolved `<video>`, and re-wires them (detaching from the old target, attaching to the new one) whenever `target` is re-resolved (e.g. the `target` attribute changes).
|
|
159
|
+
|
|
160
|
+
This also means multiple `<wcs-pip>` instances naturally self-filter: each instance only hears events from its own `<video>` target, so one instance entering Picture-in-Picture never flips another instance's `active` to `true` (see `docs/picture-in-picture-tag-design.md` §5).
|
|
161
|
+
|
|
162
|
+
## Binding Contract (`wcBindable`)
|
|
163
|
+
|
|
164
|
+
Both the Core and the Shell declare the [wc-bindable](https://github.com/csbc-dev) protocol.
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
// PipCore (headless)
|
|
168
|
+
PipCore.wcBindable = {
|
|
169
|
+
protocol: "wc-bindable",
|
|
170
|
+
version: 1,
|
|
171
|
+
properties: [
|
|
172
|
+
{ name: "active", event: "wcs-pip:change", getter: (e) => e.detail.active },
|
|
173
|
+
],
|
|
174
|
+
commands: [
|
|
175
|
+
{ name: "requestPictureInPicture", async: true },
|
|
176
|
+
{ name: "exitPictureInPicture", async: true },
|
|
177
|
+
],
|
|
178
|
+
};
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The Shell (`<wcs-pip>`) inherits the Core's `properties`/`commands` and declares the `target` input.
|
|
182
|
+
|
|
183
|
+
## Using the Core standalone
|
|
184
|
+
|
|
185
|
+
`PipCore` is framework-agnostic. You resolve and hand it the `<video>` element to control (the Shell does this resolution for you):
|
|
186
|
+
|
|
187
|
+
```js
|
|
188
|
+
import { PipCore } from "@wcstack/picture-in-picture";
|
|
189
|
+
|
|
190
|
+
const core = new PipCore();
|
|
191
|
+
core.addEventListener("wcs-pip:change", (e) => {
|
|
192
|
+
console.log(e.detail.active);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const video = document.querySelector("video");
|
|
196
|
+
core.observe(video); // subscribe to enterpictureinpicture/leavepictureinpicture
|
|
197
|
+
await core.requestPictureInPicture(video);
|
|
198
|
+
|
|
199
|
+
// later
|
|
200
|
+
await core.exitPictureInPicture();
|
|
201
|
+
core.dispose(); // detach listeners
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Notes & limitations
|
|
205
|
+
|
|
206
|
+
- **`<video>`-only.** `target` must resolve to an `HTMLVideoElement`. Any other element is treated as unresolved: `error` is set to `{ message: "target must be a <video> element." }`, never throws.
|
|
207
|
+
- **Document Picture-in-Picture API is out of scope.** See "Scope" above.
|
|
208
|
+
- **Never throws.** Unsupported environments, wrong-tag targets, and gesture-context rejections are all funneled into `error`.
|
|
209
|
+
- **`document.pictureInPictureElement` is a single document-wide value**, like `document.fullscreenElement`. Multiple `<wcs-pip>` instances self-filter via their own `<video>` target's `enterpictureinpicture`/`leavepictureinpicture` listeners — see "Event subscription" above. Note the asymmetry, though: `exitPictureInPicture()` is **not** scoped per instance — it calls the document-global `document.exitPictureInPicture()`, so invoking it on any instance exits whatever `<video>` is currently in Picture-in-Picture, even one entered via another instance's `target` (its silent no-op check is likewise document-wide: "is anything in Picture-in-Picture", not "is *my* target in Picture-in-Picture"). This mirrors the platform API itself and `@wcstack/fullscreen`'s `exitFullscreen()` (see `docs/fullscreen-tag-design.md` §7's "scope note" alongside §2.1, and the "Multiple instances" bullet in `packages/fullscreen/README.md`).
|
|
210
|
+
- **No `desired`/`actual` two-phase state** — this node exposes a single `active` boolean plus `error`, mirroring `@wcstack/fullscreen`'s simpler-than-`permission` state model.
|
|
211
|
+
- **`error` has no dedicated event, and is not `data-wcs` bindable.** Like `@wcstack/fullscreen`, `error` is a plain getter with no `wcs-pip:error` event of its own, and it is not declared in `static wcBindable.properties` — a binding system has nothing to subscribe to and cannot observe it reactively. Read `element.error` imperatively after a command's promise settles (e.g. `await el.requestPictureInPicture(); if (el.error) { ... }`).
|
|
212
|
+
|
|
213
|
+
## License
|
|
214
|
+
|
|
215
|
+
MIT
|
package/dist/auto.js
ADDED
package/dist/auto.min.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{bootstrapPip as o}from"./index.esm.min.js";o();
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
interface IWcBindableProperty {
|
|
2
|
+
readonly name: string;
|
|
3
|
+
readonly event: string;
|
|
4
|
+
readonly getter?: (event: Event) => any;
|
|
5
|
+
}
|
|
6
|
+
interface IWcBindableInput {
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly attribute?: string;
|
|
9
|
+
}
|
|
10
|
+
interface IWcBindableCommand {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly async?: boolean;
|
|
13
|
+
}
|
|
14
|
+
interface IWcBindable {
|
|
15
|
+
readonly protocol: "wc-bindable";
|
|
16
|
+
readonly version: 1;
|
|
17
|
+
readonly properties: readonly IWcBindableProperty[];
|
|
18
|
+
readonly inputs?: readonly IWcBindableInput[];
|
|
19
|
+
readonly commands?: readonly IWcBindableCommand[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ITagNames {
|
|
23
|
+
readonly pip: string;
|
|
24
|
+
}
|
|
25
|
+
interface IWritableTagNames {
|
|
26
|
+
pip?: string;
|
|
27
|
+
}
|
|
28
|
+
interface IConfig {
|
|
29
|
+
readonly tagNames: ITagNames;
|
|
30
|
+
}
|
|
31
|
+
interface IWritableConfig {
|
|
32
|
+
tagNames?: IWritableTagNames;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Value types for PipCore (headless) — the Core's readable value surface.
|
|
37
|
+
* Note that only `active` is *observable* (declared in
|
|
38
|
+
* `wcBindable.properties` with a change event); `error` is an
|
|
39
|
+
* imperative-read-only getter with no event of its own — a wc-bindable
|
|
40
|
+
* binding core will never deliver it, so read it after a command settles
|
|
41
|
+
* (docs/picture-in-picture-tag-design.md, README "Notes & limitations").
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* const core = new PipCore();
|
|
46
|
+
* // bind() only ever delivers "active" — see the note above about "error".
|
|
47
|
+
* bind(core, (name: keyof WcsPipCoreValues, value) => { ... });
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
interface WcsPipCoreValues {
|
|
51
|
+
active: boolean;
|
|
52
|
+
error: any;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Value types for the Shell (`<wcs-pip>`) — identical value surface to the
|
|
56
|
+
* Core (same caveat: only `active` is observable). The Shell adds the
|
|
57
|
+
* `target` input (attribute-mirrored) and no additional observable
|
|
58
|
+
* properties.
|
|
59
|
+
*/
|
|
60
|
+
type WcsPipValues = WcsPipCoreValues;
|
|
61
|
+
|
|
62
|
+
declare function bootstrapPip(userConfig?: IWritableConfig): void;
|
|
63
|
+
|
|
64
|
+
declare function getConfig(): IConfig;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Headless Picture-in-Picture primitive. A thin, framework-agnostic wrapper
|
|
68
|
+
* around the classic Picture-in-Picture API
|
|
69
|
+
* (`HTMLVideoElement.requestPictureInPicture()` / `document.exitPictureInPicture()` /
|
|
70
|
+
* `document.pictureInPictureElement`) exposed through the wc-bindable protocol.
|
|
71
|
+
*
|
|
72
|
+
* This Core follows the same basic pattern as `@wcstack/fullscreen`'s
|
|
73
|
+
* `FullscreenCore` (docs/fullscreen-tag-design.md): target resolution is done
|
|
74
|
+
* by the Shell (this Core receives the resolved element at call time), API
|
|
75
|
+
* resolution is call-time/non-cached, `_gen` is a single Core-level generation
|
|
76
|
+
* guard, and `error` is a simple single field (no permission-style 4-value
|
|
77
|
+
* state). See docs/picture-in-picture-tag-design.md for the differences from
|
|
78
|
+
* Fullscreen:
|
|
79
|
+
*
|
|
80
|
+
* - **§2 target constraint**: the resolved target MUST be a `<video>` element.
|
|
81
|
+
* Picture-in-Picture is only defined as an instance method of
|
|
82
|
+
* `HTMLVideoElement` — unlike Fullscreen, which any `Element` supports. A
|
|
83
|
+
* non-`<video>` target is a never-throw failure: it is treated the same as
|
|
84
|
+
* an unresolved target and reported via `error`.
|
|
85
|
+
* - **§3 event subscription target**: `enterpictureinpicture` /
|
|
86
|
+
* `leavepictureinpicture` fire on the `<video>` element itself, not on
|
|
87
|
+
* `document` (the reverse of Fullscreen's `document`-level
|
|
88
|
+
* `fullscreenchange`). The Core attaches/detaches these listeners directly
|
|
89
|
+
* on the resolved `<video>` element, re-wiring them whenever the target is
|
|
90
|
+
* re-resolved (e.g. the Shell's `target` attribute changes).
|
|
91
|
+
*/
|
|
92
|
+
declare class PipCore extends EventTarget {
|
|
93
|
+
static wcBindable: IWcBindable;
|
|
94
|
+
private _target;
|
|
95
|
+
private _active;
|
|
96
|
+
private _error;
|
|
97
|
+
private _video;
|
|
98
|
+
private _gen;
|
|
99
|
+
private _ready;
|
|
100
|
+
constructor(target?: EventTarget);
|
|
101
|
+
get ready(): Promise<void>;
|
|
102
|
+
get active(): boolean;
|
|
103
|
+
get error(): any;
|
|
104
|
+
/**
|
|
105
|
+
* (Re-)subscribe to `enterpictureinpicture`/`leavepictureinpicture` on
|
|
106
|
+
* `element` (the Shell's resolved `<video>` target). Idempotent when called
|
|
107
|
+
* again with the same element; re-wires the listeners when the element
|
|
108
|
+
* changes (e.g. the `target` attribute was changed), detaching from the
|
|
109
|
+
* previous element first so no stale listener lingers.
|
|
110
|
+
*/
|
|
111
|
+
observe(element: HTMLVideoElement | null): Promise<void>;
|
|
112
|
+
dispose(): void;
|
|
113
|
+
/**
|
|
114
|
+
* Request Picture-in-Picture for `element`. `element` must be a `<video>`
|
|
115
|
+
* (checked before the gesture-context failure path, since a type mismatch is
|
|
116
|
+
* an environment-independent, permanent error — docs/picture-in-picture-tag-design.md §2).
|
|
117
|
+
* Never throws: all failures (wrong tag, unsupported API, gesture-context
|
|
118
|
+
* rejection) are funneled into `error` and the returned promise resolves.
|
|
119
|
+
*/
|
|
120
|
+
requestPictureInPicture(element: HTMLVideoElement | null): Promise<void>;
|
|
121
|
+
/**
|
|
122
|
+
* Exit Picture-in-Picture. Mirrors FullscreenCore.exitFullscreen(): a
|
|
123
|
+
* silent no-op (resolve, no error) when nothing is currently in
|
|
124
|
+
* Picture-in-Picture — see fullscreen-tag-design.md §7.
|
|
125
|
+
*/
|
|
126
|
+
exitPictureInPicture(): Promise<void>;
|
|
127
|
+
private _requestPictureInPictureFn;
|
|
128
|
+
private _exitPictureInPictureFn;
|
|
129
|
+
private _pictureInPictureElement;
|
|
130
|
+
private _onEnter;
|
|
131
|
+
private _onLeave;
|
|
132
|
+
private _syncActive;
|
|
133
|
+
private _detach;
|
|
134
|
+
private _setActive;
|
|
135
|
+
private _setError;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* `<wcs-pip target="...">` — declarative Picture-in-Picture control.
|
|
140
|
+
*
|
|
141
|
+
* Like `<wcs-fullscreen>` (docs/fullscreen-tag-design.md §0/§1), this Shell
|
|
142
|
+
* does not operate on itself: it is a non-visible control tag that resolves a
|
|
143
|
+
* `target` element and invokes Picture-in-Picture commands against it. The
|
|
144
|
+
* `target` attribute resolves in the same 3 modes as `intersection`/`fullscreen`
|
|
145
|
+
* (`self` / a selector / the first element child), reused verbatim from
|
|
146
|
+
* `@wcstack/intersection`'s `_resolveTarget()`/`_safeQuery()`
|
|
147
|
+
* (packages/intersection/src/components/Intersect.ts).
|
|
148
|
+
*
|
|
149
|
+
* Picture-in-Picture-specific difference (docs/picture-in-picture-tag-design.md
|
|
150
|
+
* §2): the resolved target must be a `<video>` element. This Shell resolves the
|
|
151
|
+
* DOM element and hands it to the Core; the Core performs the `tagName ===
|
|
152
|
+
* "VIDEO"` validation (never-throw — a mismatch is treated as an unresolved
|
|
153
|
+
* target and reported via `error`, not thrown).
|
|
154
|
+
*/
|
|
155
|
+
declare class WcsPip extends HTMLElement {
|
|
156
|
+
static hasConnectedCallbackPromise: boolean;
|
|
157
|
+
static observedAttributes: string[];
|
|
158
|
+
static wcBindable: IWcBindable;
|
|
159
|
+
private _core;
|
|
160
|
+
private _connectedCallbackPromise;
|
|
161
|
+
constructor();
|
|
162
|
+
get connectedCallbackPromise(): Promise<void>;
|
|
163
|
+
get target(): string;
|
|
164
|
+
set target(value: string);
|
|
165
|
+
get active(): boolean;
|
|
166
|
+
get error(): any;
|
|
167
|
+
requestPictureInPicture(): Promise<void>;
|
|
168
|
+
exitPictureInPicture(): Promise<void>;
|
|
169
|
+
/**
|
|
170
|
+
* `_resolveTarget()`/`_safeQuery()` copied verbatim from `@wcstack/intersection`
|
|
171
|
+
* (packages/intersection/src/components/Intersect.ts:243-267, 281-287) per the
|
|
172
|
+
* fullscreen/picture-in-picture batch's shared target-resolution archetype
|
|
173
|
+
* (docs/fullscreen-tag-design.md §1).
|
|
174
|
+
*/
|
|
175
|
+
private _resolveTarget;
|
|
176
|
+
private _safeQuery;
|
|
177
|
+
/**
|
|
178
|
+
* Layers the Picture-in-Picture-specific `tagName === "VIDEO"` check on top
|
|
179
|
+
* of `_resolveTarget()` (docs/picture-in-picture-tag-design.md §2). A
|
|
180
|
+
* resolved-but-wrong-tag element is treated as unresolved (`element: null`)
|
|
181
|
+
* so it flows into the same "target not found" failure path as Fullscreen's
|
|
182
|
+
* missing-target case — never-throw, no exception escapes.
|
|
183
|
+
*/
|
|
184
|
+
private _resolveVideoTarget;
|
|
185
|
+
private _observe;
|
|
186
|
+
connectedCallback(): void;
|
|
187
|
+
disconnectedCallback(): void;
|
|
188
|
+
attributeChangedCallback(_name: string, oldValue: string | null, newValue: string | null): void;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export { PipCore, WcsPip, bootstrapPip, getConfig };
|
|
192
|
+
export type { IWritableConfig, IWritableTagNames, WcsPipCoreValues, WcsPipValues };
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
const _config = {
|
|
2
|
+
tagNames: {
|
|
3
|
+
pip: "wcs-pip",
|
|
4
|
+
},
|
|
5
|
+
};
|
|
6
|
+
function deepFreeze(obj) {
|
|
7
|
+
if (obj === null || typeof obj !== "object")
|
|
8
|
+
return obj;
|
|
9
|
+
Object.freeze(obj);
|
|
10
|
+
for (const key of Object.keys(obj)) {
|
|
11
|
+
deepFreeze(obj[key]);
|
|
12
|
+
}
|
|
13
|
+
return obj;
|
|
14
|
+
}
|
|
15
|
+
function deepClone(obj) {
|
|
16
|
+
if (obj === null || typeof obj !== "object")
|
|
17
|
+
return obj;
|
|
18
|
+
const clone = {};
|
|
19
|
+
for (const key of Object.keys(obj)) {
|
|
20
|
+
clone[key] = deepClone(obj[key]);
|
|
21
|
+
}
|
|
22
|
+
return clone;
|
|
23
|
+
}
|
|
24
|
+
let frozenConfig = null;
|
|
25
|
+
const config = _config;
|
|
26
|
+
function getConfig() {
|
|
27
|
+
if (!frozenConfig) {
|
|
28
|
+
frozenConfig = deepFreeze(deepClone(_config));
|
|
29
|
+
}
|
|
30
|
+
return frozenConfig;
|
|
31
|
+
}
|
|
32
|
+
function setConfig(partialConfig) {
|
|
33
|
+
if (partialConfig.tagNames) {
|
|
34
|
+
Object.assign(_config.tagNames, partialConfig.tagNames);
|
|
35
|
+
}
|
|
36
|
+
frozenConfig = null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Headless Picture-in-Picture primitive. A thin, framework-agnostic wrapper
|
|
41
|
+
* around the classic Picture-in-Picture API
|
|
42
|
+
* (`HTMLVideoElement.requestPictureInPicture()` / `document.exitPictureInPicture()` /
|
|
43
|
+
* `document.pictureInPictureElement`) exposed through the wc-bindable protocol.
|
|
44
|
+
*
|
|
45
|
+
* This Core follows the same basic pattern as `@wcstack/fullscreen`'s
|
|
46
|
+
* `FullscreenCore` (docs/fullscreen-tag-design.md): target resolution is done
|
|
47
|
+
* by the Shell (this Core receives the resolved element at call time), API
|
|
48
|
+
* resolution is call-time/non-cached, `_gen` is a single Core-level generation
|
|
49
|
+
* guard, and `error` is a simple single field (no permission-style 4-value
|
|
50
|
+
* state). See docs/picture-in-picture-tag-design.md for the differences from
|
|
51
|
+
* Fullscreen:
|
|
52
|
+
*
|
|
53
|
+
* - **§2 target constraint**: the resolved target MUST be a `<video>` element.
|
|
54
|
+
* Picture-in-Picture is only defined as an instance method of
|
|
55
|
+
* `HTMLVideoElement` — unlike Fullscreen, which any `Element` supports. A
|
|
56
|
+
* non-`<video>` target is a never-throw failure: it is treated the same as
|
|
57
|
+
* an unresolved target and reported via `error`.
|
|
58
|
+
* - **§3 event subscription target**: `enterpictureinpicture` /
|
|
59
|
+
* `leavepictureinpicture` fire on the `<video>` element itself, not on
|
|
60
|
+
* `document` (the reverse of Fullscreen's `document`-level
|
|
61
|
+
* `fullscreenchange`). The Core attaches/detaches these listeners directly
|
|
62
|
+
* on the resolved `<video>` element, re-wiring them whenever the target is
|
|
63
|
+
* re-resolved (e.g. the Shell's `target` attribute changes).
|
|
64
|
+
*/
|
|
65
|
+
class PipCore extends EventTarget {
|
|
66
|
+
static wcBindable = {
|
|
67
|
+
protocol: "wc-bindable",
|
|
68
|
+
version: 1,
|
|
69
|
+
properties: [
|
|
70
|
+
{ name: "active", event: "wcs-pip:change", getter: (e) => e.detail.active },
|
|
71
|
+
],
|
|
72
|
+
commands: [
|
|
73
|
+
{ name: "requestPictureInPicture", async: true },
|
|
74
|
+
{ name: "exitPictureInPicture", async: true },
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
_target;
|
|
78
|
+
_active = false;
|
|
79
|
+
_error = null;
|
|
80
|
+
// The <video> element the Core currently subscribes to for
|
|
81
|
+
// enterpictureinpicture/leavepictureinpicture (null when unresolved/torn down).
|
|
82
|
+
_video = null;
|
|
83
|
+
// Generation guard (§3.4 / fullscreen-tag-design.md §6): bumped on dispose()
|
|
84
|
+
// and each async command start. A completion that lands after dispose() (or
|
|
85
|
+
// after a superseding call) is stale and MUST NOT write state.
|
|
86
|
+
_gen = 0;
|
|
87
|
+
// SSR (§3.8): no asynchronous probe to await, so readiness is immediate.
|
|
88
|
+
_ready = Promise.resolve();
|
|
89
|
+
constructor(target) {
|
|
90
|
+
super();
|
|
91
|
+
this._target = target ?? this;
|
|
92
|
+
}
|
|
93
|
+
get ready() {
|
|
94
|
+
return this._ready;
|
|
95
|
+
}
|
|
96
|
+
get active() {
|
|
97
|
+
return this._active;
|
|
98
|
+
}
|
|
99
|
+
get error() {
|
|
100
|
+
return this._error;
|
|
101
|
+
}
|
|
102
|
+
// --- Lifecycle (§3.5) ---
|
|
103
|
+
/**
|
|
104
|
+
* (Re-)subscribe to `enterpictureinpicture`/`leavepictureinpicture` on
|
|
105
|
+
* `element` (the Shell's resolved `<video>` target). Idempotent when called
|
|
106
|
+
* again with the same element; re-wires the listeners when the element
|
|
107
|
+
* changes (e.g. the `target` attribute was changed), detaching from the
|
|
108
|
+
* previous element first so no stale listener lingers.
|
|
109
|
+
*/
|
|
110
|
+
observe(element) {
|
|
111
|
+
if (this._video === element) {
|
|
112
|
+
return this._ready;
|
|
113
|
+
}
|
|
114
|
+
this._detach();
|
|
115
|
+
this._video = element;
|
|
116
|
+
if (element) {
|
|
117
|
+
element.addEventListener("enterpictureinpicture", this._onEnter);
|
|
118
|
+
element.addEventListener("leavepictureinpicture", this._onLeave);
|
|
119
|
+
}
|
|
120
|
+
this._syncActive();
|
|
121
|
+
return this._ready;
|
|
122
|
+
}
|
|
123
|
+
dispose() {
|
|
124
|
+
this._gen++;
|
|
125
|
+
this._detach();
|
|
126
|
+
this._video = null;
|
|
127
|
+
}
|
|
128
|
+
// --- Commands (§3.6 never-throw) ---
|
|
129
|
+
/**
|
|
130
|
+
* Request Picture-in-Picture for `element`. `element` must be a `<video>`
|
|
131
|
+
* (checked before the gesture-context failure path, since a type mismatch is
|
|
132
|
+
* an environment-independent, permanent error — docs/picture-in-picture-tag-design.md §2).
|
|
133
|
+
* Never throws: all failures (wrong tag, unsupported API, gesture-context
|
|
134
|
+
* rejection) are funneled into `error` and the returned promise resolves.
|
|
135
|
+
*/
|
|
136
|
+
async requestPictureInPicture(element) {
|
|
137
|
+
const gen = ++this._gen;
|
|
138
|
+
if (!element || element.tagName !== "VIDEO") {
|
|
139
|
+
this._setError({ message: "target must be a <video> element." });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// Re-wire to `element` before issuing the platform call: a caller may
|
|
143
|
+
// request a <video> different from the one last passed to observe() (e.g.
|
|
144
|
+
// the Shell's target attribute pointed at nothing at connect time and the
|
|
145
|
+
// matching <video> was only inserted later, so no attributeChangedCallback
|
|
146
|
+
// ever re-resolved it). Without this, `_video` stays stale and
|
|
147
|
+
// `_syncActive()` below (and future enter/leave events) would never
|
|
148
|
+
// recognize `element` as this Core's target, leaving `active` permanently
|
|
149
|
+
// wrong even though the request succeeded (mirrors
|
|
150
|
+
// FullscreenCore.requestFullscreen()'s unconditional `this._resolvedTarget
|
|
151
|
+
// = element` assignment — docs/fullscreen-tag-design.md §6).
|
|
152
|
+
this.observe(element);
|
|
153
|
+
const fn = this._requestPictureInPictureFn(element);
|
|
154
|
+
if (!fn) {
|
|
155
|
+
this._setError({ message: "Picture-in-Picture API is not supported." });
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
await fn.call(element);
|
|
160
|
+
if (gen !== this._gen)
|
|
161
|
+
return; // stale
|
|
162
|
+
this._setError(null);
|
|
163
|
+
this._syncActive(); // belt-and-suspenders (mirrors FullscreenCore's _applyActive() on success)
|
|
164
|
+
}
|
|
165
|
+
catch (e) {
|
|
166
|
+
if (gen !== this._gen)
|
|
167
|
+
return; // stale
|
|
168
|
+
this._setError(e); // e.g. NotAllowedError (gesture-context rejection)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Exit Picture-in-Picture. Mirrors FullscreenCore.exitFullscreen(): a
|
|
173
|
+
* silent no-op (resolve, no error) when nothing is currently in
|
|
174
|
+
* Picture-in-Picture — see fullscreen-tag-design.md §7.
|
|
175
|
+
*/
|
|
176
|
+
async exitPictureInPicture() {
|
|
177
|
+
// no-op checks come BEFORE the generation bump: a call that does nothing
|
|
178
|
+
// must not supersede an in-flight requestPictureInPicture() — bumping
|
|
179
|
+
// first would make the pending request's settle handling stale and
|
|
180
|
+
// silently swallow its error update (mirrors
|
|
181
|
+
// FullscreenCore.exitFullscreen()).
|
|
182
|
+
if (this._pictureInPictureElement() === null)
|
|
183
|
+
return; // already not in PiP: silent no-op
|
|
184
|
+
const fn = this._exitPictureInPictureFn();
|
|
185
|
+
if (!fn)
|
|
186
|
+
return; // unsupported: silent no-op (semantically already "not in PiP")
|
|
187
|
+
const gen = ++this._gen;
|
|
188
|
+
try {
|
|
189
|
+
await fn();
|
|
190
|
+
if (gen !== this._gen)
|
|
191
|
+
return;
|
|
192
|
+
this._setError(null);
|
|
193
|
+
this._syncActive(); // belt-and-suspenders (mirrors FullscreenCore.exitFullscreen()'s success-path _applyActive()); covers a delayed/dropped leavepictureinpicture
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
if (gen !== this._gen)
|
|
197
|
+
return;
|
|
198
|
+
this._setError(e);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// --- Internal: API resolution (call-time, never cached — §3.7) ---
|
|
202
|
+
// Unlike FullscreenCore's _elementFullscreenFn(), a naive direct property
|
|
203
|
+
// lookup (`e.requestPictureInPicture`, walking the prototype chain) here is
|
|
204
|
+
// safe: it cannot recurse into the Shell's own command method,
|
|
205
|
+
// because the resolved target is validated to be a <video> element (§2)
|
|
206
|
+
// before this is called, and <wcs-pip> (the Shell) is never itself a
|
|
207
|
+
// <video>. Fullscreen's own→Element.prototype two-step resolution guards
|
|
208
|
+
// against `target="self"`/no-target resolving to the Shell element, which
|
|
209
|
+
// cannot happen here.
|
|
210
|
+
_requestPictureInPictureFn(el) {
|
|
211
|
+
const e = el;
|
|
212
|
+
return typeof e.requestPictureInPicture === "function" ? e.requestPictureInPicture : undefined;
|
|
213
|
+
}
|
|
214
|
+
_exitPictureInPictureFn() {
|
|
215
|
+
const d = document;
|
|
216
|
+
return typeof d.exitPictureInPicture === "function" ? d.exitPictureInPicture.bind(document) : undefined;
|
|
217
|
+
}
|
|
218
|
+
_pictureInPictureElement() {
|
|
219
|
+
const d = document;
|
|
220
|
+
return d.pictureInPictureElement ?? null;
|
|
221
|
+
}
|
|
222
|
+
// --- Internal: event wiring ---
|
|
223
|
+
_onEnter = () => {
|
|
224
|
+
this._syncActive();
|
|
225
|
+
};
|
|
226
|
+
_onLeave = () => {
|
|
227
|
+
this._syncActive();
|
|
228
|
+
};
|
|
229
|
+
_syncActive() {
|
|
230
|
+
const isActive = this._video !== null && this._pictureInPictureElement() === this._video;
|
|
231
|
+
this._setActive(isActive);
|
|
232
|
+
}
|
|
233
|
+
_detach() {
|
|
234
|
+
if (this._video) {
|
|
235
|
+
this._video.removeEventListener("enterpictureinpicture", this._onEnter);
|
|
236
|
+
this._video.removeEventListener("leavepictureinpicture", this._onLeave);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// --- State setters with event dispatch (§3.3 same-value guard) ---
|
|
240
|
+
_setActive(active) {
|
|
241
|
+
if (this._active === active)
|
|
242
|
+
return;
|
|
243
|
+
this._active = active;
|
|
244
|
+
this._target.dispatchEvent(new CustomEvent("wcs-pip:change", {
|
|
245
|
+
detail: { active },
|
|
246
|
+
bubbles: true,
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
249
|
+
_setError(error) {
|
|
250
|
+
this._error = error;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* `<wcs-pip target="...">` — declarative Picture-in-Picture control.
|
|
256
|
+
*
|
|
257
|
+
* Like `<wcs-fullscreen>` (docs/fullscreen-tag-design.md §0/§1), this Shell
|
|
258
|
+
* does not operate on itself: it is a non-visible control tag that resolves a
|
|
259
|
+
* `target` element and invokes Picture-in-Picture commands against it. The
|
|
260
|
+
* `target` attribute resolves in the same 3 modes as `intersection`/`fullscreen`
|
|
261
|
+
* (`self` / a selector / the first element child), reused verbatim from
|
|
262
|
+
* `@wcstack/intersection`'s `_resolveTarget()`/`_safeQuery()`
|
|
263
|
+
* (packages/intersection/src/components/Intersect.ts).
|
|
264
|
+
*
|
|
265
|
+
* Picture-in-Picture-specific difference (docs/picture-in-picture-tag-design.md
|
|
266
|
+
* §2): the resolved target must be a `<video>` element. This Shell resolves the
|
|
267
|
+
* DOM element and hands it to the Core; the Core performs the `tagName ===
|
|
268
|
+
* "VIDEO"` validation (never-throw — a mismatch is treated as an unresolved
|
|
269
|
+
* target and reported via `error`, not thrown).
|
|
270
|
+
*/
|
|
271
|
+
class WcsPip extends HTMLElement {
|
|
272
|
+
// SSR (§4.4): observe() completes synchronously, but the Shell still exposes
|
|
273
|
+
// connectedCallbackPromise so the state binder can await it uniformly across
|
|
274
|
+
// all IO nodes before snapshotting.
|
|
275
|
+
static hasConnectedCallbackPromise = true;
|
|
276
|
+
static observedAttributes = ["target"];
|
|
277
|
+
static wcBindable = {
|
|
278
|
+
...PipCore.wcBindable,
|
|
279
|
+
inputs: [{ name: "target", attribute: "target" }],
|
|
280
|
+
// Core の commands をそのまま継承(単一情報源)。fullscreen/intersection と同型。
|
|
281
|
+
commands: PipCore.wcBindable.commands,
|
|
282
|
+
};
|
|
283
|
+
_core;
|
|
284
|
+
_connectedCallbackPromise = Promise.resolve();
|
|
285
|
+
constructor() {
|
|
286
|
+
super();
|
|
287
|
+
this._core = new PipCore(this);
|
|
288
|
+
}
|
|
289
|
+
get connectedCallbackPromise() {
|
|
290
|
+
return this._connectedCallbackPromise;
|
|
291
|
+
}
|
|
292
|
+
// --- Attribute accessors ---
|
|
293
|
+
get target() {
|
|
294
|
+
return this.getAttribute("target") ?? "";
|
|
295
|
+
}
|
|
296
|
+
set target(value) {
|
|
297
|
+
this.setAttribute("target", value);
|
|
298
|
+
}
|
|
299
|
+
// --- Core delegated getters ---
|
|
300
|
+
get active() {
|
|
301
|
+
return this._core.active;
|
|
302
|
+
}
|
|
303
|
+
get error() {
|
|
304
|
+
return this._core.error;
|
|
305
|
+
}
|
|
306
|
+
// --- Commands ---
|
|
307
|
+
async requestPictureInPicture() {
|
|
308
|
+
const { element } = this._resolveVideoTarget();
|
|
309
|
+
return this._core.requestPictureInPicture(element);
|
|
310
|
+
}
|
|
311
|
+
async exitPictureInPicture() {
|
|
312
|
+
return this._core.exitPictureInPicture();
|
|
313
|
+
}
|
|
314
|
+
// --- Internal ---
|
|
315
|
+
/**
|
|
316
|
+
* `_resolveTarget()`/`_safeQuery()` copied verbatim from `@wcstack/intersection`
|
|
317
|
+
* (packages/intersection/src/components/Intersect.ts:243-267, 281-287) per the
|
|
318
|
+
* fullscreen/picture-in-picture batch's shared target-resolution archetype
|
|
319
|
+
* (docs/fullscreen-tag-design.md §1).
|
|
320
|
+
*/
|
|
321
|
+
_resolveTarget() {
|
|
322
|
+
const target = this.target;
|
|
323
|
+
if (target === "self") {
|
|
324
|
+
return { element: this, display: "block" };
|
|
325
|
+
}
|
|
326
|
+
if (target !== "") {
|
|
327
|
+
const scope = this.getRootNode();
|
|
328
|
+
return { element: this._safeQuery(scope, target), display: "none" };
|
|
329
|
+
}
|
|
330
|
+
const child = this.firstElementChild;
|
|
331
|
+
if (child) {
|
|
332
|
+
return { element: child, display: "contents" };
|
|
333
|
+
}
|
|
334
|
+
return { element: this, display: "block" };
|
|
335
|
+
}
|
|
336
|
+
_safeQuery(scope, selector) {
|
|
337
|
+
try {
|
|
338
|
+
return scope.querySelector(selector);
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Layers the Picture-in-Picture-specific `tagName === "VIDEO"` check on top
|
|
346
|
+
* of `_resolveTarget()` (docs/picture-in-picture-tag-design.md §2). A
|
|
347
|
+
* resolved-but-wrong-tag element is treated as unresolved (`element: null`)
|
|
348
|
+
* so it flows into the same "target not found" failure path as Fullscreen's
|
|
349
|
+
* missing-target case — never-throw, no exception escapes.
|
|
350
|
+
*/
|
|
351
|
+
_resolveVideoTarget() {
|
|
352
|
+
const { element, display } = this._resolveTarget();
|
|
353
|
+
if (element !== null && element.tagName !== "VIDEO") {
|
|
354
|
+
return { element: null, display };
|
|
355
|
+
}
|
|
356
|
+
return { element: element, display };
|
|
357
|
+
}
|
|
358
|
+
_observe() {
|
|
359
|
+
const { element, display } = this._resolveVideoTarget();
|
|
360
|
+
this.style.display = display;
|
|
361
|
+
this._connectedCallbackPromise = this._core.observe(element);
|
|
362
|
+
}
|
|
363
|
+
// --- Lifecycle ---
|
|
364
|
+
connectedCallback() {
|
|
365
|
+
this._observe();
|
|
366
|
+
}
|
|
367
|
+
disconnectedCallback() {
|
|
368
|
+
this._core.dispose();
|
|
369
|
+
}
|
|
370
|
+
attributeChangedCallback(_name, oldValue, newValue) {
|
|
371
|
+
if (oldValue === newValue)
|
|
372
|
+
return;
|
|
373
|
+
if (!this.isConnected)
|
|
374
|
+
return;
|
|
375
|
+
this._observe();
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function registerComponents() {
|
|
380
|
+
if (!customElements.get(config.tagNames.pip)) {
|
|
381
|
+
customElements.define(config.tagNames.pip, WcsPip);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function bootstrapPip(userConfig) {
|
|
386
|
+
if (userConfig) {
|
|
387
|
+
setConfig(userConfig);
|
|
388
|
+
}
|
|
389
|
+
registerComponents();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export { PipCore, WcsPip, bootstrapPip, getConfig };
|
|
393
|
+
//# sourceMappingURL=index.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.esm.js","sources":["../src/config.ts","../src/core/PipCore.ts","../src/components/Pip.ts","../src/registerComponents.ts","../src/bootstrapPip.ts"],"sourcesContent":["import { IConfig, IWritableConfig } from \"./types.js\";\n\ninterface IInternalConfig extends IConfig {\n tagNames: {\n pip: string;\n };\n}\n\nconst _config: IInternalConfig = {\n tagNames: {\n pip: \"wcs-pip\",\n },\n};\n\nfunction deepFreeze<T>(obj: T): T {\n if (obj === null || typeof obj !== \"object\") return obj;\n Object.freeze(obj);\n for (const key of Object.keys(obj)) {\n deepFreeze((obj as Record<string, unknown>)[key]);\n }\n return obj;\n}\n\nfunction deepClone<T>(obj: T): T {\n if (obj === null || typeof obj !== \"object\") return obj;\n const clone: Record<string, unknown> = {};\n for (const key of Object.keys(obj)) {\n clone[key] = deepClone((obj as Record<string, unknown>)[key]);\n }\n return clone as T;\n}\n\nlet frozenConfig: IConfig | null = null;\n\nexport const config: IConfig = _config as IConfig;\n\nexport function getConfig(): IConfig {\n if (!frozenConfig) {\n frozenConfig = deepFreeze(deepClone(_config));\n }\n return frozenConfig;\n}\n\nexport function setConfig(partialConfig: IWritableConfig): void {\n if (partialConfig.tagNames) {\n Object.assign(_config.tagNames, partialConfig.tagNames);\n }\n frozenConfig = null;\n}\n","import { IWcBindable } from \"../types.js\";\n\n/**\n * Headless Picture-in-Picture primitive. A thin, framework-agnostic wrapper\n * around the classic Picture-in-Picture API\n * (`HTMLVideoElement.requestPictureInPicture()` / `document.exitPictureInPicture()` /\n * `document.pictureInPictureElement`) exposed through the wc-bindable protocol.\n *\n * This Core follows the same basic pattern as `@wcstack/fullscreen`'s\n * `FullscreenCore` (docs/fullscreen-tag-design.md): target resolution is done\n * by the Shell (this Core receives the resolved element at call time), API\n * resolution is call-time/non-cached, `_gen` is a single Core-level generation\n * guard, and `error` is a simple single field (no permission-style 4-value\n * state). See docs/picture-in-picture-tag-design.md for the differences from\n * Fullscreen:\n *\n * - **§2 target constraint**: the resolved target MUST be a `<video>` element.\n * Picture-in-Picture is only defined as an instance method of\n * `HTMLVideoElement` — unlike Fullscreen, which any `Element` supports. A\n * non-`<video>` target is a never-throw failure: it is treated the same as\n * an unresolved target and reported via `error`.\n * - **§3 event subscription target**: `enterpictureinpicture` /\n * `leavepictureinpicture` fire on the `<video>` element itself, not on\n * `document` (the reverse of Fullscreen's `document`-level\n * `fullscreenchange`). The Core attaches/detaches these listeners directly\n * on the resolved `<video>` element, re-wiring them whenever the target is\n * re-resolved (e.g. the Shell's `target` attribute changes).\n */\nexport class PipCore extends EventTarget {\n static wcBindable: IWcBindable = {\n protocol: \"wc-bindable\",\n version: 1,\n properties: [\n { name: \"active\", event: \"wcs-pip:change\", getter: (e: Event) => (e as CustomEvent).detail.active },\n ],\n commands: [\n { name: \"requestPictureInPicture\", async: true },\n { name: \"exitPictureInPicture\", async: true },\n ],\n };\n\n private _target: EventTarget;\n private _active: boolean = false;\n private _error: any = null;\n\n // The <video> element the Core currently subscribes to for\n // enterpictureinpicture/leavepictureinpicture (null when unresolved/torn down).\n private _video: HTMLVideoElement | null = null;\n\n // Generation guard (§3.4 / fullscreen-tag-design.md §6): bumped on dispose()\n // and each async command start. A completion that lands after dispose() (or\n // after a superseding call) is stale and MUST NOT write state.\n private _gen = 0;\n\n // SSR (§3.8): no asynchronous probe to await, so readiness is immediate.\n private _ready: Promise<void> = Promise.resolve();\n\n constructor(target?: EventTarget) {\n super();\n this._target = target ?? this;\n }\n\n get ready(): Promise<void> {\n return this._ready;\n }\n\n get active(): boolean {\n return this._active;\n }\n\n get error(): any {\n return this._error;\n }\n\n // --- Lifecycle (§3.5) ---\n\n /**\n * (Re-)subscribe to `enterpictureinpicture`/`leavepictureinpicture` on\n * `element` (the Shell's resolved `<video>` target). Idempotent when called\n * again with the same element; re-wires the listeners when the element\n * changes (e.g. the `target` attribute was changed), detaching from the\n * previous element first so no stale listener lingers.\n */\n observe(element: HTMLVideoElement | null): Promise<void> {\n if (this._video === element) {\n return this._ready;\n }\n this._detach();\n this._video = element;\n if (element) {\n element.addEventListener(\"enterpictureinpicture\", this._onEnter);\n element.addEventListener(\"leavepictureinpicture\", this._onLeave);\n }\n this._syncActive();\n return this._ready;\n }\n\n dispose(): void {\n this._gen++;\n this._detach();\n this._video = null;\n }\n\n // --- Commands (§3.6 never-throw) ---\n\n /**\n * Request Picture-in-Picture for `element`. `element` must be a `<video>`\n * (checked before the gesture-context failure path, since a type mismatch is\n * an environment-independent, permanent error — docs/picture-in-picture-tag-design.md §2).\n * Never throws: all failures (wrong tag, unsupported API, gesture-context\n * rejection) are funneled into `error` and the returned promise resolves.\n */\n async requestPictureInPicture(element: HTMLVideoElement | null): Promise<void> {\n const gen = ++this._gen;\n if (!element || element.tagName !== \"VIDEO\") {\n this._setError({ message: \"target must be a <video> element.\" });\n return;\n }\n // Re-wire to `element` before issuing the platform call: a caller may\n // request a <video> different from the one last passed to observe() (e.g.\n // the Shell's target attribute pointed at nothing at connect time and the\n // matching <video> was only inserted later, so no attributeChangedCallback\n // ever re-resolved it). Without this, `_video` stays stale and\n // `_syncActive()` below (and future enter/leave events) would never\n // recognize `element` as this Core's target, leaving `active` permanently\n // wrong even though the request succeeded (mirrors\n // FullscreenCore.requestFullscreen()'s unconditional `this._resolvedTarget\n // = element` assignment — docs/fullscreen-tag-design.md §6).\n this.observe(element);\n const fn = this._requestPictureInPictureFn(element);\n if (!fn) {\n this._setError({ message: \"Picture-in-Picture API is not supported.\" });\n return;\n }\n try {\n await fn.call(element);\n if (gen !== this._gen) return; // stale\n this._setError(null);\n this._syncActive(); // belt-and-suspenders (mirrors FullscreenCore's _applyActive() on success)\n } catch (e: any) {\n if (gen !== this._gen) return; // stale\n this._setError(e); // e.g. NotAllowedError (gesture-context rejection)\n }\n }\n\n /**\n * Exit Picture-in-Picture. Mirrors FullscreenCore.exitFullscreen(): a\n * silent no-op (resolve, no error) when nothing is currently in\n * Picture-in-Picture — see fullscreen-tag-design.md §7.\n */\n async exitPictureInPicture(): Promise<void> {\n // no-op checks come BEFORE the generation bump: a call that does nothing\n // must not supersede an in-flight requestPictureInPicture() — bumping\n // first would make the pending request's settle handling stale and\n // silently swallow its error update (mirrors\n // FullscreenCore.exitFullscreen()).\n if (this._pictureInPictureElement() === null) return; // already not in PiP: silent no-op\n const fn = this._exitPictureInPictureFn();\n if (!fn) return; // unsupported: silent no-op (semantically already \"not in PiP\")\n const gen = ++this._gen;\n try {\n await fn();\n if (gen !== this._gen) return;\n this._setError(null);\n this._syncActive(); // belt-and-suspenders (mirrors FullscreenCore.exitFullscreen()'s success-path _applyActive()); covers a delayed/dropped leavepictureinpicture\n } catch (e: any) {\n if (gen !== this._gen) return;\n this._setError(e);\n }\n }\n\n // --- Internal: API resolution (call-time, never cached — §3.7) ---\n\n // Unlike FullscreenCore's _elementFullscreenFn(), a naive direct property\n // lookup (`e.requestPictureInPicture`, walking the prototype chain) here is\n // safe: it cannot recurse into the Shell's own command method,\n // because the resolved target is validated to be a <video> element (§2)\n // before this is called, and <wcs-pip> (the Shell) is never itself a\n // <video>. Fullscreen's own→Element.prototype two-step resolution guards\n // against `target=\"self\"`/no-target resolving to the Shell element, which\n // cannot happen here.\n private _requestPictureInPictureFn(el: HTMLVideoElement): (() => Promise<PictureInPictureWindow>) | undefined {\n const e = el as any;\n return typeof e.requestPictureInPicture === \"function\" ? e.requestPictureInPicture : undefined;\n }\n\n private _exitPictureInPictureFn(): (() => Promise<void>) | undefined {\n const d = document as any;\n return typeof d.exitPictureInPicture === \"function\" ? d.exitPictureInPicture.bind(document) : undefined;\n }\n\n private _pictureInPictureElement(): Element | null {\n const d = document as any;\n return d.pictureInPictureElement ?? null;\n }\n\n // --- Internal: event wiring ---\n\n private _onEnter = (): void => {\n this._syncActive();\n };\n\n private _onLeave = (): void => {\n this._syncActive();\n };\n\n private _syncActive(): void {\n const isActive = this._video !== null && this._pictureInPictureElement() === this._video;\n this._setActive(isActive);\n }\n\n private _detach(): void {\n if (this._video) {\n this._video.removeEventListener(\"enterpictureinpicture\", this._onEnter);\n this._video.removeEventListener(\"leavepictureinpicture\", this._onLeave);\n }\n }\n\n // --- State setters with event dispatch (§3.3 same-value guard) ---\n\n private _setActive(active: boolean): void {\n if (this._active === active) return;\n this._active = active;\n this._target.dispatchEvent(new CustomEvent(\"wcs-pip:change\", {\n detail: { active },\n bubbles: true,\n }));\n }\n\n private _setError(error: any): void {\n this._error = error;\n }\n}\n","import { IWcBindable } from \"../types.js\";\nimport { PipCore } from \"../core/PipCore.js\";\n\n/**\n * `<wcs-pip target=\"...\">` — declarative Picture-in-Picture control.\n *\n * Like `<wcs-fullscreen>` (docs/fullscreen-tag-design.md §0/§1), this Shell\n * does not operate on itself: it is a non-visible control tag that resolves a\n * `target` element and invokes Picture-in-Picture commands against it. The\n * `target` attribute resolves in the same 3 modes as `intersection`/`fullscreen`\n * (`self` / a selector / the first element child), reused verbatim from\n * `@wcstack/intersection`'s `_resolveTarget()`/`_safeQuery()`\n * (packages/intersection/src/components/Intersect.ts).\n *\n * Picture-in-Picture-specific difference (docs/picture-in-picture-tag-design.md\n * §2): the resolved target must be a `<video>` element. This Shell resolves the\n * DOM element and hands it to the Core; the Core performs the `tagName ===\n * \"VIDEO\"` validation (never-throw — a mismatch is treated as an unresolved\n * target and reported via `error`, not thrown).\n */\nexport class WcsPip extends HTMLElement {\n // SSR (§4.4): observe() completes synchronously, but the Shell still exposes\n // connectedCallbackPromise so the state binder can await it uniformly across\n // all IO nodes before snapshotting.\n static hasConnectedCallbackPromise = true;\n\n static observedAttributes = [\"target\"];\n\n static wcBindable: IWcBindable = {\n ...PipCore.wcBindable,\n inputs: [{ name: \"target\", attribute: \"target\" }],\n // Core の commands をそのまま継承(単一情報源)。fullscreen/intersection と同型。\n commands: PipCore.wcBindable.commands,\n };\n\n private _core: PipCore;\n private _connectedCallbackPromise: Promise<void> = Promise.resolve();\n\n constructor() {\n super();\n this._core = new PipCore(this);\n }\n\n get connectedCallbackPromise(): Promise<void> {\n return this._connectedCallbackPromise;\n }\n\n // --- Attribute accessors ---\n\n get target(): string {\n return this.getAttribute(\"target\") ?? \"\";\n }\n\n set target(value: string) {\n this.setAttribute(\"target\", value);\n }\n\n // --- Core delegated getters ---\n\n get active(): boolean {\n return this._core.active;\n }\n\n get error(): any {\n return this._core.error;\n }\n\n // --- Commands ---\n\n async requestPictureInPicture(): Promise<void> {\n const { element } = this._resolveVideoTarget();\n return this._core.requestPictureInPicture(element);\n }\n\n async exitPictureInPicture(): Promise<void> {\n return this._core.exitPictureInPicture();\n }\n\n // --- Internal ---\n\n /**\n * `_resolveTarget()`/`_safeQuery()` copied verbatim from `@wcstack/intersection`\n * (packages/intersection/src/components/Intersect.ts:243-267, 281-287) per the\n * fullscreen/picture-in-picture batch's shared target-resolution archetype\n * (docs/fullscreen-tag-design.md §1).\n */\n private _resolveTarget(): { element: Element | null; display: string } {\n const target = this.target;\n if (target === \"self\") {\n return { element: this, display: \"block\" };\n }\n if (target !== \"\") {\n const scope = this.getRootNode() as Document | ShadowRoot;\n return { element: this._safeQuery(scope, target), display: \"none\" };\n }\n const child = this.firstElementChild;\n if (child) {\n return { element: child, display: \"contents\" };\n }\n return { element: this, display: \"block\" };\n }\n\n private _safeQuery(scope: Document | ShadowRoot, selector: string): Element | null {\n try {\n return scope.querySelector(selector);\n } catch {\n return null;\n }\n }\n\n /**\n * Layers the Picture-in-Picture-specific `tagName === \"VIDEO\"` check on top\n * of `_resolveTarget()` (docs/picture-in-picture-tag-design.md §2). A\n * resolved-but-wrong-tag element is treated as unresolved (`element: null`)\n * so it flows into the same \"target not found\" failure path as Fullscreen's\n * missing-target case — never-throw, no exception escapes.\n */\n private _resolveVideoTarget(): { element: HTMLVideoElement | null; display: string } {\n const { element, display } = this._resolveTarget();\n if (element !== null && element.tagName !== \"VIDEO\") {\n return { element: null, display };\n }\n return { element: element as HTMLVideoElement | null, display };\n }\n\n private _observe(): void {\n const { element, display } = this._resolveVideoTarget();\n this.style.display = display;\n this._connectedCallbackPromise = this._core.observe(element);\n }\n\n // --- Lifecycle ---\n\n connectedCallback(): void {\n this._observe();\n }\n\n disconnectedCallback(): void {\n this._core.dispose();\n }\n\n attributeChangedCallback(_name: string, oldValue: string | null, newValue: string | null): void {\n if (oldValue === newValue) return;\n if (!this.isConnected) return;\n this._observe();\n }\n}\n","import { WcsPip } from \"./components/Pip.js\";\nimport { config } from \"./config.js\";\n\nexport function registerComponents(): void {\n if (!customElements.get(config.tagNames.pip)) {\n customElements.define(config.tagNames.pip, WcsPip);\n }\n}\n","import { setConfig } from \"./config.js\";\nimport { registerComponents } from \"./registerComponents.js\";\nimport { IWritableConfig } from \"./types.js\";\n\nexport function bootstrapPip(userConfig?: IWritableConfig): void {\n if (userConfig) {\n setConfig(userConfig);\n }\n registerComponents();\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e={tagNames:{pip:"wcs-pip"}};function t(e){if(null===e||"object"!=typeof e)return e;Object.freeze(e);for(const r of Object.keys(e))t(e[r]);return e}function r(e){if(null===e||"object"!=typeof e)return e;const t={};for(const i of Object.keys(e))t[i]=r(e[i]);return t}let i=null;const s=e;function n(){return i||(i=t(r(e))),i}class c extends EventTarget{static wcBindable={protocol:"wc-bindable",version:1,properties:[{name:"active",event:"wcs-pip:change",getter:e=>e.detail.active}],commands:[{name:"requestPictureInPicture",async:!0},{name:"exitPictureInPicture",async:!0}]};_target;_active=!1;_error=null;_video=null;_gen=0;_ready=Promise.resolve();constructor(e){super(),this._target=e??this}get ready(){return this._ready}get active(){return this._active}get error(){return this._error}observe(e){return this._video===e||(this._detach(),this._video=e,e&&(e.addEventListener("enterpictureinpicture",this._onEnter),e.addEventListener("leavepictureinpicture",this._onLeave)),this._syncActive()),this._ready}dispose(){this._gen++,this._detach(),this._video=null}async requestPictureInPicture(e){const t=++this._gen;if(!e||"VIDEO"!==e.tagName)return void this._setError({message:"target must be a <video> element."});this.observe(e);const r=this._requestPictureInPictureFn(e);if(r)try{if(await r.call(e),t!==this._gen)return;this._setError(null),this._syncActive()}catch(e){if(t!==this._gen)return;this._setError(e)}else this._setError({message:"Picture-in-Picture API is not supported."})}async exitPictureInPicture(){if(null===this._pictureInPictureElement())return;const e=this._exitPictureInPictureFn();if(!e)return;const t=++this._gen;try{if(await e(),t!==this._gen)return;this._setError(null),this._syncActive()}catch(e){if(t!==this._gen)return;this._setError(e)}}_requestPictureInPictureFn(e){const t=e;return"function"==typeof t.requestPictureInPicture?t.requestPictureInPicture:void 0}_exitPictureInPictureFn(){const e=document;return"function"==typeof e.exitPictureInPicture?e.exitPictureInPicture.bind(document):void 0}_pictureInPictureElement(){return document.pictureInPictureElement??null}_onEnter=()=>{this._syncActive()};_onLeave=()=>{this._syncActive()};_syncActive(){const e=null!==this._video&&this._pictureInPictureElement()===this._video;this._setActive(e)}_detach(){this._video&&(this._video.removeEventListener("enterpictureinpicture",this._onEnter),this._video.removeEventListener("leavepictureinpicture",this._onLeave))}_setActive(e){this._active!==e&&(this._active=e,this._target.dispatchEvent(new CustomEvent("wcs-pip:change",{detail:{active:e},bubbles:!0})))}_setError(e){this._error=e}}class o extends HTMLElement{static hasConnectedCallbackPromise=!0;static observedAttributes=["target"];static wcBindable={...c.wcBindable,inputs:[{name:"target",attribute:"target"}],commands:c.wcBindable.commands};_core;_connectedCallbackPromise=Promise.resolve();constructor(){super(),this._core=new c(this)}get connectedCallbackPromise(){return this._connectedCallbackPromise}get target(){return this.getAttribute("target")??""}set target(e){this.setAttribute("target",e)}get active(){return this._core.active}get error(){return this._core.error}async requestPictureInPicture(){const{element:e}=this._resolveVideoTarget();return this._core.requestPictureInPicture(e)}async exitPictureInPicture(){return this._core.exitPictureInPicture()}_resolveTarget(){const e=this.target;if("self"===e)return{element:this,display:"block"};if(""!==e){const t=this.getRootNode();return{element:this._safeQuery(t,e),display:"none"}}const t=this.firstElementChild;return t?{element:t,display:"contents"}:{element:this,display:"block"}}_safeQuery(e,t){try{return e.querySelector(t)}catch{return null}}_resolveVideoTarget(){const{element:e,display:t}=this._resolveTarget();return null!==e&&"VIDEO"!==e.tagName?{element:null,display:t}:{element:e,display:t}}_observe(){const{element:e,display:t}=this._resolveVideoTarget();this.style.display=t,this._connectedCallbackPromise=this._core.observe(e)}connectedCallback(){this._observe()}disconnectedCallback(){this._core.dispose()}attributeChangedCallback(e,t,r){t!==r&&this.isConnected&&this._observe()}}function u(t){var r;t&&((r=t).tagNames&&Object.assign(e.tagNames,r.tagNames),i=null),customElements.get(s.tagNames.pip)||customElements.define(s.tagNames.pip,o)}export{c as PipCore,o as WcsPip,u as bootstrapPip,n as getConfig};
|
|
2
|
+
//# sourceMappingURL=index.esm.min.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.esm.min.js","sources":["../src/config.ts","../src/core/PipCore.ts","../src/components/Pip.ts","../src/bootstrapPip.ts","../src/registerComponents.ts"],"sourcesContent":["import { IConfig, IWritableConfig } from \"./types.js\";\n\ninterface IInternalConfig extends IConfig {\n tagNames: {\n pip: string;\n };\n}\n\nconst _config: IInternalConfig = {\n tagNames: {\n pip: \"wcs-pip\",\n },\n};\n\nfunction deepFreeze<T>(obj: T): T {\n if (obj === null || typeof obj !== \"object\") return obj;\n Object.freeze(obj);\n for (const key of Object.keys(obj)) {\n deepFreeze((obj as Record<string, unknown>)[key]);\n }\n return obj;\n}\n\nfunction deepClone<T>(obj: T): T {\n if (obj === null || typeof obj !== \"object\") return obj;\n const clone: Record<string, unknown> = {};\n for (const key of Object.keys(obj)) {\n clone[key] = deepClone((obj as Record<string, unknown>)[key]);\n }\n return clone as T;\n}\n\nlet frozenConfig: IConfig | null = null;\n\nexport const config: IConfig = _config as IConfig;\n\nexport function getConfig(): IConfig {\n if (!frozenConfig) {\n frozenConfig = deepFreeze(deepClone(_config));\n }\n return frozenConfig;\n}\n\nexport function setConfig(partialConfig: IWritableConfig): void {\n if (partialConfig.tagNames) {\n Object.assign(_config.tagNames, partialConfig.tagNames);\n }\n frozenConfig = null;\n}\n","import { IWcBindable } from \"../types.js\";\n\n/**\n * Headless Picture-in-Picture primitive. A thin, framework-agnostic wrapper\n * around the classic Picture-in-Picture API\n * (`HTMLVideoElement.requestPictureInPicture()` / `document.exitPictureInPicture()` /\n * `document.pictureInPictureElement`) exposed through the wc-bindable protocol.\n *\n * This Core follows the same basic pattern as `@wcstack/fullscreen`'s\n * `FullscreenCore` (docs/fullscreen-tag-design.md): target resolution is done\n * by the Shell (this Core receives the resolved element at call time), API\n * resolution is call-time/non-cached, `_gen` is a single Core-level generation\n * guard, and `error` is a simple single field (no permission-style 4-value\n * state). See docs/picture-in-picture-tag-design.md for the differences from\n * Fullscreen:\n *\n * - **§2 target constraint**: the resolved target MUST be a `<video>` element.\n * Picture-in-Picture is only defined as an instance method of\n * `HTMLVideoElement` — unlike Fullscreen, which any `Element` supports. A\n * non-`<video>` target is a never-throw failure: it is treated the same as\n * an unresolved target and reported via `error`.\n * - **§3 event subscription target**: `enterpictureinpicture` /\n * `leavepictureinpicture` fire on the `<video>` element itself, not on\n * `document` (the reverse of Fullscreen's `document`-level\n * `fullscreenchange`). The Core attaches/detaches these listeners directly\n * on the resolved `<video>` element, re-wiring them whenever the target is\n * re-resolved (e.g. the Shell's `target` attribute changes).\n */\nexport class PipCore extends EventTarget {\n static wcBindable: IWcBindable = {\n protocol: \"wc-bindable\",\n version: 1,\n properties: [\n { name: \"active\", event: \"wcs-pip:change\", getter: (e: Event) => (e as CustomEvent).detail.active },\n ],\n commands: [\n { name: \"requestPictureInPicture\", async: true },\n { name: \"exitPictureInPicture\", async: true },\n ],\n };\n\n private _target: EventTarget;\n private _active: boolean = false;\n private _error: any = null;\n\n // The <video> element the Core currently subscribes to for\n // enterpictureinpicture/leavepictureinpicture (null when unresolved/torn down).\n private _video: HTMLVideoElement | null = null;\n\n // Generation guard (§3.4 / fullscreen-tag-design.md §6): bumped on dispose()\n // and each async command start. A completion that lands after dispose() (or\n // after a superseding call) is stale and MUST NOT write state.\n private _gen = 0;\n\n // SSR (§3.8): no asynchronous probe to await, so readiness is immediate.\n private _ready: Promise<void> = Promise.resolve();\n\n constructor(target?: EventTarget) {\n super();\n this._target = target ?? this;\n }\n\n get ready(): Promise<void> {\n return this._ready;\n }\n\n get active(): boolean {\n return this._active;\n }\n\n get error(): any {\n return this._error;\n }\n\n // --- Lifecycle (§3.5) ---\n\n /**\n * (Re-)subscribe to `enterpictureinpicture`/`leavepictureinpicture` on\n * `element` (the Shell's resolved `<video>` target). Idempotent when called\n * again with the same element; re-wires the listeners when the element\n * changes (e.g. the `target` attribute was changed), detaching from the\n * previous element first so no stale listener lingers.\n */\n observe(element: HTMLVideoElement | null): Promise<void> {\n if (this._video === element) {\n return this._ready;\n }\n this._detach();\n this._video = element;\n if (element) {\n element.addEventListener(\"enterpictureinpicture\", this._onEnter);\n element.addEventListener(\"leavepictureinpicture\", this._onLeave);\n }\n this._syncActive();\n return this._ready;\n }\n\n dispose(): void {\n this._gen++;\n this._detach();\n this._video = null;\n }\n\n // --- Commands (§3.6 never-throw) ---\n\n /**\n * Request Picture-in-Picture for `element`. `element` must be a `<video>`\n * (checked before the gesture-context failure path, since a type mismatch is\n * an environment-independent, permanent error — docs/picture-in-picture-tag-design.md §2).\n * Never throws: all failures (wrong tag, unsupported API, gesture-context\n * rejection) are funneled into `error` and the returned promise resolves.\n */\n async requestPictureInPicture(element: HTMLVideoElement | null): Promise<void> {\n const gen = ++this._gen;\n if (!element || element.tagName !== \"VIDEO\") {\n this._setError({ message: \"target must be a <video> element.\" });\n return;\n }\n // Re-wire to `element` before issuing the platform call: a caller may\n // request a <video> different from the one last passed to observe() (e.g.\n // the Shell's target attribute pointed at nothing at connect time and the\n // matching <video> was only inserted later, so no attributeChangedCallback\n // ever re-resolved it). Without this, `_video` stays stale and\n // `_syncActive()` below (and future enter/leave events) would never\n // recognize `element` as this Core's target, leaving `active` permanently\n // wrong even though the request succeeded (mirrors\n // FullscreenCore.requestFullscreen()'s unconditional `this._resolvedTarget\n // = element` assignment — docs/fullscreen-tag-design.md §6).\n this.observe(element);\n const fn = this._requestPictureInPictureFn(element);\n if (!fn) {\n this._setError({ message: \"Picture-in-Picture API is not supported.\" });\n return;\n }\n try {\n await fn.call(element);\n if (gen !== this._gen) return; // stale\n this._setError(null);\n this._syncActive(); // belt-and-suspenders (mirrors FullscreenCore's _applyActive() on success)\n } catch (e: any) {\n if (gen !== this._gen) return; // stale\n this._setError(e); // e.g. NotAllowedError (gesture-context rejection)\n }\n }\n\n /**\n * Exit Picture-in-Picture. Mirrors FullscreenCore.exitFullscreen(): a\n * silent no-op (resolve, no error) when nothing is currently in\n * Picture-in-Picture — see fullscreen-tag-design.md §7.\n */\n async exitPictureInPicture(): Promise<void> {\n // no-op checks come BEFORE the generation bump: a call that does nothing\n // must not supersede an in-flight requestPictureInPicture() — bumping\n // first would make the pending request's settle handling stale and\n // silently swallow its error update (mirrors\n // FullscreenCore.exitFullscreen()).\n if (this._pictureInPictureElement() === null) return; // already not in PiP: silent no-op\n const fn = this._exitPictureInPictureFn();\n if (!fn) return; // unsupported: silent no-op (semantically already \"not in PiP\")\n const gen = ++this._gen;\n try {\n await fn();\n if (gen !== this._gen) return;\n this._setError(null);\n this._syncActive(); // belt-and-suspenders (mirrors FullscreenCore.exitFullscreen()'s success-path _applyActive()); covers a delayed/dropped leavepictureinpicture\n } catch (e: any) {\n if (gen !== this._gen) return;\n this._setError(e);\n }\n }\n\n // --- Internal: API resolution (call-time, never cached — §3.7) ---\n\n // Unlike FullscreenCore's _elementFullscreenFn(), a naive direct property\n // lookup (`e.requestPictureInPicture`, walking the prototype chain) here is\n // safe: it cannot recurse into the Shell's own command method,\n // because the resolved target is validated to be a <video> element (§2)\n // before this is called, and <wcs-pip> (the Shell) is never itself a\n // <video>. Fullscreen's own→Element.prototype two-step resolution guards\n // against `target=\"self\"`/no-target resolving to the Shell element, which\n // cannot happen here.\n private _requestPictureInPictureFn(el: HTMLVideoElement): (() => Promise<PictureInPictureWindow>) | undefined {\n const e = el as any;\n return typeof e.requestPictureInPicture === \"function\" ? e.requestPictureInPicture : undefined;\n }\n\n private _exitPictureInPictureFn(): (() => Promise<void>) | undefined {\n const d = document as any;\n return typeof d.exitPictureInPicture === \"function\" ? d.exitPictureInPicture.bind(document) : undefined;\n }\n\n private _pictureInPictureElement(): Element | null {\n const d = document as any;\n return d.pictureInPictureElement ?? null;\n }\n\n // --- Internal: event wiring ---\n\n private _onEnter = (): void => {\n this._syncActive();\n };\n\n private _onLeave = (): void => {\n this._syncActive();\n };\n\n private _syncActive(): void {\n const isActive = this._video !== null && this._pictureInPictureElement() === this._video;\n this._setActive(isActive);\n }\n\n private _detach(): void {\n if (this._video) {\n this._video.removeEventListener(\"enterpictureinpicture\", this._onEnter);\n this._video.removeEventListener(\"leavepictureinpicture\", this._onLeave);\n }\n }\n\n // --- State setters with event dispatch (§3.3 same-value guard) ---\n\n private _setActive(active: boolean): void {\n if (this._active === active) return;\n this._active = active;\n this._target.dispatchEvent(new CustomEvent(\"wcs-pip:change\", {\n detail: { active },\n bubbles: true,\n }));\n }\n\n private _setError(error: any): void {\n this._error = error;\n }\n}\n","import { IWcBindable } from \"../types.js\";\nimport { PipCore } from \"../core/PipCore.js\";\n\n/**\n * `<wcs-pip target=\"...\">` — declarative Picture-in-Picture control.\n *\n * Like `<wcs-fullscreen>` (docs/fullscreen-tag-design.md §0/§1), this Shell\n * does not operate on itself: it is a non-visible control tag that resolves a\n * `target` element and invokes Picture-in-Picture commands against it. The\n * `target` attribute resolves in the same 3 modes as `intersection`/`fullscreen`\n * (`self` / a selector / the first element child), reused verbatim from\n * `@wcstack/intersection`'s `_resolveTarget()`/`_safeQuery()`\n * (packages/intersection/src/components/Intersect.ts).\n *\n * Picture-in-Picture-specific difference (docs/picture-in-picture-tag-design.md\n * §2): the resolved target must be a `<video>` element. This Shell resolves the\n * DOM element and hands it to the Core; the Core performs the `tagName ===\n * \"VIDEO\"` validation (never-throw — a mismatch is treated as an unresolved\n * target and reported via `error`, not thrown).\n */\nexport class WcsPip extends HTMLElement {\n // SSR (§4.4): observe() completes synchronously, but the Shell still exposes\n // connectedCallbackPromise so the state binder can await it uniformly across\n // all IO nodes before snapshotting.\n static hasConnectedCallbackPromise = true;\n\n static observedAttributes = [\"target\"];\n\n static wcBindable: IWcBindable = {\n ...PipCore.wcBindable,\n inputs: [{ name: \"target\", attribute: \"target\" }],\n // Core の commands をそのまま継承(単一情報源)。fullscreen/intersection と同型。\n commands: PipCore.wcBindable.commands,\n };\n\n private _core: PipCore;\n private _connectedCallbackPromise: Promise<void> = Promise.resolve();\n\n constructor() {\n super();\n this._core = new PipCore(this);\n }\n\n get connectedCallbackPromise(): Promise<void> {\n return this._connectedCallbackPromise;\n }\n\n // --- Attribute accessors ---\n\n get target(): string {\n return this.getAttribute(\"target\") ?? \"\";\n }\n\n set target(value: string) {\n this.setAttribute(\"target\", value);\n }\n\n // --- Core delegated getters ---\n\n get active(): boolean {\n return this._core.active;\n }\n\n get error(): any {\n return this._core.error;\n }\n\n // --- Commands ---\n\n async requestPictureInPicture(): Promise<void> {\n const { element } = this._resolveVideoTarget();\n return this._core.requestPictureInPicture(element);\n }\n\n async exitPictureInPicture(): Promise<void> {\n return this._core.exitPictureInPicture();\n }\n\n // --- Internal ---\n\n /**\n * `_resolveTarget()`/`_safeQuery()` copied verbatim from `@wcstack/intersection`\n * (packages/intersection/src/components/Intersect.ts:243-267, 281-287) per the\n * fullscreen/picture-in-picture batch's shared target-resolution archetype\n * (docs/fullscreen-tag-design.md §1).\n */\n private _resolveTarget(): { element: Element | null; display: string } {\n const target = this.target;\n if (target === \"self\") {\n return { element: this, display: \"block\" };\n }\n if (target !== \"\") {\n const scope = this.getRootNode() as Document | ShadowRoot;\n return { element: this._safeQuery(scope, target), display: \"none\" };\n }\n const child = this.firstElementChild;\n if (child) {\n return { element: child, display: \"contents\" };\n }\n return { element: this, display: \"block\" };\n }\n\n private _safeQuery(scope: Document | ShadowRoot, selector: string): Element | null {\n try {\n return scope.querySelector(selector);\n } catch {\n return null;\n }\n }\n\n /**\n * Layers the Picture-in-Picture-specific `tagName === \"VIDEO\"` check on top\n * of `_resolveTarget()` (docs/picture-in-picture-tag-design.md §2). A\n * resolved-but-wrong-tag element is treated as unresolved (`element: null`)\n * so it flows into the same \"target not found\" failure path as Fullscreen's\n * missing-target case — never-throw, no exception escapes.\n */\n private _resolveVideoTarget(): { element: HTMLVideoElement | null; display: string } {\n const { element, display } = this._resolveTarget();\n if (element !== null && element.tagName !== \"VIDEO\") {\n return { element: null, display };\n }\n return { element: element as HTMLVideoElement | null, display };\n }\n\n private _observe(): void {\n const { element, display } = this._resolveVideoTarget();\n this.style.display = display;\n this._connectedCallbackPromise = this._core.observe(element);\n }\n\n // --- Lifecycle ---\n\n connectedCallback(): void {\n this._observe();\n }\n\n disconnectedCallback(): void {\n this._core.dispose();\n }\n\n attributeChangedCallback(_name: string, oldValue: string | null, newValue: string | null): void {\n if (oldValue === newValue) return;\n if (!this.isConnected) return;\n this._observe();\n }\n}\n","import { setConfig } from \"./config.js\";\nimport { registerComponents } from \"./registerComponents.js\";\nimport { IWritableConfig } from \"./types.js\";\n\nexport function bootstrapPip(userConfig?: IWritableConfig): void {\n if (userConfig) {\n setConfig(userConfig);\n }\n registerComponents();\n}\n","import { WcsPip } from \"./components/Pip.js\";\nimport { config } from \"./config.js\";\n\nexport function registerComponents(): void {\n if (!customElements.get(config.tagNames.pip)) {\n customElements.define(config.tagNames.pip, WcsPip);\n }\n}\n"],"names":["_config","tagNames","pip","deepFreeze","obj","Object","freeze","key","keys","deepClone","clone","frozenConfig","config","getConfig","PipCore","EventTarget","static","protocol","version","properties","name","event","getter","e","detail","active","commands","async","_target","_active","_error","_video","_gen","_ready","Promise","resolve","constructor","target","super","this","ready","error","observe","element","_detach","addEventListener","_onEnter","_onLeave","_syncActive","dispose","requestPictureInPicture","gen","tagName","_setError","message","fn","_requestPictureInPictureFn","call","exitPictureInPicture","_pictureInPictureElement","_exitPictureInPictureFn","el","undefined","d","document","bind","pictureInPictureElement","isActive","_setActive","removeEventListener","dispatchEvent","CustomEvent","bubbles","WcsPip","HTMLElement","wcBindable","inputs","attribute","_core","_connectedCallbackPromise","connectedCallbackPromise","getAttribute","value","setAttribute","_resolveVideoTarget","_resolveTarget","display","scope","getRootNode","_safeQuery","child","firstElementChild","selector","querySelector","_observe","style","connectedCallback","disconnectedCallback","attributeChangedCallback","_name","oldValue","newValue","isConnected","bootstrapPip","userConfig","partialConfig","assign","customElements","get","define"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wcstack/picture-in-picture",
|
|
3
|
+
"version": "1.16.0",
|
|
4
|
+
"description": "Declarative Picture-in-Picture component for Web Components. Framework-agnostic <video> Picture-in-Picture control via wc-bindable-protocol.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.esm.js",
|
|
7
|
+
"module": "./dist/index.esm.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.esm.js"
|
|
13
|
+
},
|
|
14
|
+
"./auto": "./dist/auto.min.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"clean": "rimraf dist .tsc-out",
|
|
21
|
+
"build": "rimraf dist .tsc-out && tsc && rollup -c",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"test:watch": "vitest",
|
|
24
|
+
"test:coverage": "vitest run --coverage",
|
|
25
|
+
"lint": "eslint src",
|
|
26
|
+
"version:patch": "npm version patch",
|
|
27
|
+
"version:minor": "npm version minor",
|
|
28
|
+
"version:major": "npm version major",
|
|
29
|
+
"prepublishOnly": "npm run build && npm run test:coverage"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"web-components",
|
|
33
|
+
"picture-in-picture",
|
|
34
|
+
"video",
|
|
35
|
+
"media",
|
|
36
|
+
"custom-elements",
|
|
37
|
+
"wc-bindable",
|
|
38
|
+
"declarative",
|
|
39
|
+
"zero-dependencies",
|
|
40
|
+
"framework-agnostic"
|
|
41
|
+
],
|
|
42
|
+
"author": "mogera551",
|
|
43
|
+
"homepage": "https://wcstack.github.io",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "https://github.com/wcstack/wcstack.git",
|
|
47
|
+
"directory": "packages/picture-in-picture"
|
|
48
|
+
},
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/wcstack/wcstack/issues"
|
|
51
|
+
},
|
|
52
|
+
"license": "MIT",
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@eslint/js": "^9.39.1",
|
|
55
|
+
"@rollup/plugin-terser": "^0.4.4",
|
|
56
|
+
"@rollup/plugin-typescript": "^11.1.6",
|
|
57
|
+
"@vitest/coverage-v8": "^4.0.15",
|
|
58
|
+
"@vitest/ui": "^4.0.15",
|
|
59
|
+
"eslint": "^9.39.1",
|
|
60
|
+
"globals": "^16.5.0",
|
|
61
|
+
"happy-dom": "^20.0.11",
|
|
62
|
+
"rimraf": "^6.0.1",
|
|
63
|
+
"rollup": "^4.22.4",
|
|
64
|
+
"rollup-plugin-dts": "^6.1.1",
|
|
65
|
+
"rollup-plugin-copy": "^3.5.0",
|
|
66
|
+
"tslib": "^2.8.1",
|
|
67
|
+
"typescript": "^5.9.3",
|
|
68
|
+
"typescript-eslint": "^8.49.0",
|
|
69
|
+
"vitest": "^4.0.15"
|
|
70
|
+
}
|
|
71
|
+
}
|