@wcstack/pointer-lock 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 ADDED
@@ -0,0 +1,143 @@
1
+ # @wcstack/pointer-lock
2
+
3
+ `@wcstack/pointer-lock` は wcstack エコシステム向けのヘッドレスな Pointer Lock API コンポーネントです。
4
+
5
+ 視覚的な UI ウィジェットではありません。
6
+ `@wcstack/fullscreen` が Fullscreen API のアクティブ状態をリアクティブな state に変えるのと同じように、Pointer Lock API のロック状態をリアクティブな state に変える **非同期プリミティブノード** です。
7
+
8
+ `@wcstack/state` と組み合わせると、`<wcs-pointer-lock>` はパス契約で直接バインドできます:
9
+
10
+ - **入力サーフェス**: `target`
11
+ - **出力 state サーフェス**: `active`、`error`
12
+ - **コマンド**: `requestPointerLock`、`exitPointerLock`
13
+
14
+ `@wcstack/pointer-lock` は [CSBC](https://github.com/csbc-dev/arch/blob/main/README.md)(Core / Shell / Binding Contract)アーキテクチャに従います:
15
+
16
+ - **Core**(`PointerLockCore`)が `Element.requestPointerLock()` / `document.exitPointerLock()` / `document.pointerLockElement` をラップし、`document`単位で発火する `pointerlockchange` イベントを自身が解決した target と自己判定でフィルタする
17
+ - **Shell**(`<wcs-pointer-lock target="...">`)がDOMから「どの要素を操作するか」を解決し、display とライフサイクルを管理
18
+ - **Binding Contract**(`static wcBindable`)が観測可能な `active` プロパティと `requestPointerLock`/`exitPointerLock` コマンドを宣言
19
+
20
+ ## 用途が限定的なノード — 使う前に必ず読んでください
21
+
22
+ 他の大半の wcstack IO ノードと異なり、`<wcs-pointer-lock>` は本プロジェクトの主眼である**宣言的な SPA UI 構築**を主たる対象としていません。Pointer Lock API の実際の用途はほぼ排他的に、マウスの**相対移動量**(`movementX`/`movementY`)を必要とする**ゲームや canvas/WebGL 描画 UI**(FPS視点操作、お絵描きツールのパン操作等)に限られます。こうした利用者は多くの場合すでに命令的な `requestAnimationFrame` ループを回しており、動画プレイヤーが `<wcs-fullscreen>` を使う場合ほど、入力を宣言的なバインディング層経由にする動機は強くありません。
23
+
24
+ このノードは「ロックのON/OFFを切り替える宣言的なスイッチ」(例: command-token プロトコル経由の「マウスルック有効化」ボタン)が欲しい場合に使ってください。`movementX`/`movementY` の取得源としては使えません(後述)。
25
+
26
+ ## `movementX`/`movementY` はスコープ外(v1)
27
+
28
+ ポインタロック中に発火する `mousemove` イベントは `movementX`/`movementY`(相対移動量)を持ちますが、**本Coreは現時点のいかなるバージョンでもこれを公開しません。** これらは高頻度データ(環境によっては毎秒数百イベントに達しうる)であり、本プロトコルが前提とする同値ガード付きの宣言的 `properties` モデルに馴染みません。そのまま `wc-bindable` に流すと、バインドされた state を毎フレーム単位の更新で溢れさせるリスクがあります。
29
+
30
+ 将来のバージョンで追加する場合の設計意図(`docs/pointer-lock-tag-design.md` §3 参照)は、明示的な opt-in の背後に置き、`@wcstack/debounce`/`@wcstack/throttle` と組み合わせてレート制限することです。これにより、opt-in していないインスタンスには「無制限のファイアホースを流さない」という性質を保てます。現時点で生の `movementX`/`movementY` が必要な場合は、本ノードの `active` state と並行して、自前の命令的コードで `mousemove` を直接読んでください。
31
+
32
+ ## `target` 属性がロック対象を決める
33
+
34
+ `@wcstack/fullscreen` と同様、この Shell は**自分自身をロックしません** — `target` 属性で指し示した**参照先の要素**を操作する非表示の制御タグであり、`@wcstack/intersection` と同じ3モード解決を使います:
35
+
36
+ | `target` | 操作対象 | `display` |
37
+ |------------------|--------------------------|-------------|
38
+ | 省略 | 先頭の子要素 | `contents` |
39
+ | `"#selector"` | 一致した要素 | `none` |
40
+ | `"self"` | 自分自身 | `block` |
41
+
42
+ ## インストール
43
+
44
+ ```bash
45
+ npm install @wcstack/pointer-lock
46
+ ```
47
+
48
+ ## クイックスタート
49
+
50
+ ```html
51
+ <script type="module" src="https://esm.run/@wcstack/state/auto"></script>
52
+ <script type="module" src="https://esm.run/@wcstack/pointer-lock/auto"></script>
53
+
54
+ <wcs-state>
55
+ <script type="module">
56
+ export default {
57
+ $commandTokens: ["lockPointer", "unlockPointer"],
58
+ locked: false,
59
+ };
60
+ </script>
61
+ </wcs-state>
62
+
63
+ <canvas id="scene" width="640" height="480"></canvas>
64
+ <wcs-pointer-lock target="#scene"
65
+ data-wcs="active: locked; command.requestPointerLock: $command.lockPointer; command.exitPointerLock: $command.unlockPointer">
66
+ </wcs-pointer-lock>
67
+
68
+ <button data-wcs="onclick: $command.lockPointer">マウスルックを有効化</button>
69
+ <button data-wcs="hidden: locked|not; onclick: $command.unlockPointer">解除</button>
70
+ ```
71
+
72
+ ボタンは`<wcs-pointer-lock>`に直接触れません。クリックが`lockPointer`/`unlockPointer`コマンドトークンを発行し、`<wcs-pointer-lock>`が`command.requestPointerLock: $command.lockPointer` / `command.exitPointerLock: $command.unlockPointer`でそれを購読します([command-tokenプロトコル](../state/) — commandメソッドを持つ要素が*購読者*であり、発行者ではありません)。
73
+
74
+ バインドするstateパスは事前にすべて宣言する必要があります — ここでは`locked: false`。未宣言パスをバインドすると初期化時に例外になります。`data-wcs`パス内の否定は先頭`!`ではなく`|not`フィルタ(`locked|not`)で行います — パスはプレフィックス演算子をサポートしません。
75
+
76
+ `requestPointerLock()` は **user gesture 文脈を必須とします** — 後述。
77
+
78
+ ## user gesture 制約
79
+
80
+ `Element.requestPointerLock()` はuser gesture文脈(同期的なクリックハンドラ等)の外から呼ばれると `NotAllowedError` で reject します。本ノードは自らgestureを生成できません。**実際のuser gesture内から`requestPointerLock`を呼ぶ責務は呼び出し元にあります。** command-token プロトコル(`<wcs-pointer-lock>`上の`command.requestPointerLock: $command.<token>`、ボタンの`onclick: $command.<token>`が発行する)を配線してください — `setTimeout`内や`.then()`チェーンの奥から呼ぶとgesture文脈を失い呼び出しがrejectされますが、例外は伝播しません(never-throw、`error`に格納されます)。
81
+
82
+ ## 観測可能プロパティ(出力)
83
+
84
+ | プロパティ | イベント | 説明 |
85
+ | ---------- | ---------------------------- | ------------ |
86
+ | `active` | `wcs-pointer-lock:change` | `document.pointerLockElement` がこのインスタンスの解決済み target と一致していれば `true`、それ以外は `false`。 |
87
+
88
+ `error`(後述コマンド参照)は単純な getter として公開され、`wcBindable` の property ではありません — コマンド呼び出しの副作用としてのみ変化します(`@wcstack/fullscreen` と同型)。直近の失敗は次のいずれか: rejectされたPromise(gesture外呼び出しなら`NotAllowedError`等)、プラットフォームAPI非対応なら`{ message: "Pointer Lock API is not supported." }`、`target`が要素へ未解決なら`{ message: "Pointer Lock target could not be resolved." }`。直近の呼び出しが成功済み・まだ何も失敗していない場合は`null`。
89
+
90
+ ## コマンド
91
+
92
+ | コマンド | Async | 説明 |
93
+ | --------------------- | ----- | ------------ |
94
+ | `requestPointerLock` | あり | `target`を解決し、それに対して`requestPointerLock()`を呼びます。never-throw: 失敗(targetが未解決、gesture不在の`NotAllowedError`、非対応API等)は例外でなく `error` に格納されます。 |
95
+ | `exitPointerLock` | **無し** | `document.exitPointerLock()` を呼びます。**同期API** — `@wcstack/fullscreen`の`exitFullscreen()`(Promiseベース)と異なり、`exitPointerLock()`は`void`を返します。何もロックされていない、またはAPI非対応時はsilent no-opです。 |
96
+
97
+ ## 属性 / 入力
98
+
99
+ | 属性 | 説明 |
100
+ | --------- | ------------ |
101
+ | `target` | ロック対象の要素を指すセレクタ(または`"self"`)。上記「`target`属性がロック対象を決める」を参照。省略時は先頭の子要素。 |
102
+
103
+ ## 複数インスタンス — 「documentがロック中か」ではなくインスタンスごとに`active`を見る
104
+
105
+ `document.pointerLockElement` はdocument全体で単一の値しか持ちません(同時にロックできる要素は高々1つ)。複数の`<wcs-pointer-lock>`インスタンス(例: `target="#a"`と`target="#b"`)が同時に存在する場合、各インスタンスは`document.pointerLockElement`を**自分自身の**解決済みtargetと比較します(単に「documentがロック中か」ではありません)。`#a`をロックすると、`target="#a"`のインスタンスは`active: true`、`target="#b"`のインスタンスは`active: false`を報告します — document全体では何か(`#a`)がロックされているにもかかわらずです。
106
+
107
+ ## ベンダープレフィックス
108
+
109
+ 一部の古いWebKit実装は標準名の代わりに`webkitRequestPointerLock`/`webkitExitPointerLock`/`webkitPointerLockElement`/`webkitpointerlockchange`イベントを公開します。API解決は**呼び出し時**(キャッシュしない)に行われ、標準名を優先し、無ければレガシー名にフォールバックします — これにより非対応環境(どちらの名前も存在しない)を正しく検出でき、テストがAPIを自由にinstall/removeできます。
110
+
111
+ ## 注意・制限
112
+
113
+ - **user gesture が必須。** 上記参照 — プラットフォームの制約であり、本ノードで回避する手段はありません。
114
+ - **`exitPointerLock()`は同期API**、`@wcstack/fullscreen`のPromiseベースの`exitFullscreen()`とは異なります。単独の`_gen`世代ガードは持ちません(非同期の`requestPointerLock()`のみがガードを必要とします)。それでも非準拠実装が例外を投げないよう防御的に`try/catch`で包んでいます。
115
+ - **`movementX`/`movementY`はv1ではスコープ外。** 上記参照。
116
+ - **autoTriggerなし。** `requestPointerLock()`はuser gesture文脈を必要とするため、主な起動経路は`data-*target`のクリックショートカットではなくcommand-tokenプロトコル(`<wcs-pointer-lock>`上の`command.requestPointerLock: $command.<token>`)です。
117
+ - **SSR(`@wcstack/server`)。** `static hasConnectedCallbackPromise = true`を宣言し`connectedCallbackPromise`を公開します。`observe()`が同期的なため、このpromiseは常に即座にsettleします。
118
+
119
+ ## ヘッドレス利用(`PointerLockCore`)
120
+
121
+ CoreはDOM非依存(Pointer Lockプラットフォーム APIの呼び出しを除く)で、`@wc-bindable/core`の`bind()`と直接使えます:
122
+
123
+ ```typescript
124
+ import { PointerLockCore } from "@wcstack/pointer-lock";
125
+
126
+ const lock = new PointerLockCore();
127
+ lock.addEventListener("wcs-pointer-lock:change", (e) => {
128
+ console.log((e as CustomEvent).detail); // boolean —新しい active 値そのもの
129
+ });
130
+
131
+ const canvas = document.querySelector("#scene")!;
132
+ lock.observe(canvas); // document の pointerlockchange を購読し、canvas に対して自己判定
133
+ await lock.requestPointerLock(canvas); // user gesture 内から呼ぶ必要がある
134
+ console.log(lock.active, lock.error);
135
+
136
+ // 後始末:
137
+ lock.exitPointerLock(); // 同期的
138
+ lock.dispose(); // document リスナーを外す
139
+ ```
140
+
141
+ ## ライセンス
142
+
143
+ MIT
package/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # @wcstack/pointer-lock
2
+
3
+ `@wcstack/pointer-lock` is a headless Pointer Lock API component for the wcstack ecosystem.
4
+
5
+ It is not a visual UI widget.
6
+ It is an **async primitive node** that turns the Pointer Lock API's lock state into reactive state — the same way `@wcstack/fullscreen` turns the Fullscreen API's active state into reactive state.
7
+
8
+ With `@wcstack/state`, `<wcs-pointer-lock>` can be bound directly through path contracts:
9
+
10
+ - **input surface**: `target`
11
+ - **output state surface**: `active`, `error`
12
+ - **commands**: `requestPointerLock`, `exitPointerLock`
13
+
14
+ `@wcstack/pointer-lock` follows the [CSBC](https://github.com/csbc-dev/arch/blob/main/README.md) (Core / Shell / Binding Contract) architecture:
15
+
16
+ - **Core** (`PointerLockCore`) wraps `Element.requestPointerLock()` / `document.exitPointerLock()` / `document.pointerLockElement`, self-filtering the `document`-scoped `pointerlockchange` event against its own resolved target
17
+ - **Shell** (`<wcs-pointer-lock target="...">`) resolves *which* element to operate on from the DOM, manages display and lifecycle
18
+ - **Binding Contract** (`static wcBindable`) declares the observable `active` property and the `requestPointerLock`/`exitPointerLock` commands
19
+
20
+ ## A narrow-purpose node — read this before reaching for it
21
+
22
+ Unlike most wcstack IO nodes, `<wcs-pointer-lock>` is **not** aimed at the project's main use case of building declarative SPA UI. The Pointer Lock API's real-world usage is almost exclusively **games and canvas/WebGL rendering UI** that need the mouse's *relative* movement (`movementX`/`movementY`) — first-person camera controls, drawing-tool panning, and similar. Those consumers typically already run an imperative `requestAnimationFrame` loop and have less reason to route input through a declarative binding layer than, say, a video player reaching for `<wcs-fullscreen>`.
23
+
24
+ Reach for this node when you need a declarative *lock on/off* switch (e.g. a "Enable mouse look" button wired via the command-token protocol) — not as a source of `movementX`/`movementY` data. See below.
25
+
26
+ ## `movementX`/`movementY` are out of scope (v1)
27
+
28
+ `mousemove` events fired while the pointer is locked carry `movementX`/`movementY` deltas. **This Core does not expose them, in any version up to this one.** They are high-frequency data (potentially hundreds of events/sec) that do not fit the same-value-guarded, declarative `properties` model this protocol is built around — piping them through `wc-bindable` as-is would risk flooding the bound state with per-frame updates.
29
+
30
+ If a future version adds them, the design intent (see `docs/pointer-lock-tag-design.md` §3) is to gate them behind an explicit opt-in and pair them with `@wcstack/debounce`/`@wcstack/throttle` for rate-limiting, keeping the "no unbounded firehose" property intact for instances that don't opt in. For now, if you need raw `movementX`/`movementY`, read them directly off `mousemove` in your own imperative code alongside this node's `active` state.
31
+
32
+ ## The `target` attribute decides what is locked
33
+
34
+ Like `@wcstack/fullscreen`, this Shell does not lock *itself* — it is a hidden control tag that operates on a **referenced element** via the `target` attribute, using the same 3-mode resolution as `@wcstack/intersection`:
35
+
36
+ | `target` | operates on | `display` |
37
+ |------------------|--------------------------|-------------|
38
+ | *omitted* | first element child | `contents` |
39
+ | `"#selector"` | the matched element | `none` |
40
+ | `"self"` | the element itself | `block` |
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ npm install @wcstack/pointer-lock
46
+ ```
47
+
48
+ ## Quick Start
49
+
50
+ ```html
51
+ <script type="module" src="https://esm.run/@wcstack/state/auto"></script>
52
+ <script type="module" src="https://esm.run/@wcstack/pointer-lock/auto"></script>
53
+
54
+ <wcs-state>
55
+ <script type="module">
56
+ export default {
57
+ $commandTokens: ["lockPointer", "unlockPointer"],
58
+ locked: false,
59
+ };
60
+ </script>
61
+ </wcs-state>
62
+
63
+ <canvas id="scene" width="640" height="480"></canvas>
64
+ <wcs-pointer-lock target="#scene"
65
+ data-wcs="active: locked; command.requestPointerLock: $command.lockPointer; command.exitPointerLock: $command.unlockPointer">
66
+ </wcs-pointer-lock>
67
+
68
+ <button data-wcs="onclick: $command.lockPointer">Enable mouse look</button>
69
+ <button data-wcs="hidden: locked|not; onclick: $command.unlockPointer">Release</button>
70
+ ```
71
+
72
+ The buttons never touch `<wcs-pointer-lock>` directly: their clicks emit the `lockPointer`/`unlockPointer` command tokens, and `<wcs-pointer-lock>` subscribes to them via `command.requestPointerLock: $command.lockPointer` / `command.exitPointerLock: $command.unlockPointer` (the [command-token protocol](../state/) — the element with the command method is the *subscriber*, not the emitter).
73
+
74
+ Every bound state path must be declared up front — `locked: false` here; binding an undeclared path throws at initialization. Negation in a `data-wcs` path is done with the `|not` filter (`locked|not`), not a leading `!` — paths do not support prefix operators.
75
+
76
+ `requestPointerLock()` **requires a user-gesture context** — see below.
77
+
78
+ ## User gesture requirement
79
+
80
+ `Element.requestPointerLock()` rejects with `NotAllowedError` when called outside a user-gesture context (e.g. a synchronous click handler). This node cannot manufacture a gesture on your behalf: **the responsibility for calling `requestPointerLock` from within an actual user gesture belongs to the caller.** Wire the command-token protocol (`command.requestPointerLock: $command.<token>` on `<wcs-pointer-lock>`, emitted by a button's `onclick: $command.<token>`) — calling it from a `setTimeout` or deep inside a `.then()` chain loses the gesture context and the call will reject, `error` will be set, but no exception will propagate (never-throw).
81
+
82
+ ## Observable Properties (outputs)
83
+
84
+ | Property | Event | Description |
85
+ | ---------- | ---------------------------- | ------------ |
86
+ | `active` | `wcs-pointer-lock:change` | `true` when `document.pointerLockElement` is this instance's resolved target, `false` otherwise. |
87
+
88
+ `error` (see Commands below) is exposed as a plain getter, not as a `wcBindable` property — it only changes as a side effect of a command call, mirroring `@wcstack/fullscreen`. The most recent failure is one of: a rejected promise (e.g. `NotAllowedError` for a gesture-less call), `{ message: "Pointer Lock API is not supported." }` when the platform API is missing, `{ message: "Pointer Lock target could not be resolved." }` when `target` did not resolve to an element, or `null` if the last attempt succeeded / nothing has failed yet.
89
+
90
+ ## Commands
91
+
92
+ | Command | Async | Description |
93
+ | --------------------- | ----- | ------------ |
94
+ | `requestPointerLock` | yes | Resolve `target` and call `requestPointerLock()` on it. Never-throw: failures (an unresolvable `target`, `NotAllowedError` for a missing gesture, or an unsupported API) are captured into `error`, not thrown. |
95
+ | `exitPointerLock` | **no**| Calls `document.exitPointerLock()`. **Synchronous** — unlike `@wcstack/fullscreen`'s `exitFullscreen()` (which is Promise-based), `exitPointerLock()` returns `void`. Silent no-op if nothing is currently locked or the API is unsupported. |
96
+
97
+ ## Attributes / Inputs
98
+
99
+ | Attribute | Description |
100
+ | --------- | ------------ |
101
+ | `target` | Selector (or `"self"`) identifying the element to lock. See "The `target` attribute decides what is locked" above. Omitted → first element child. |
102
+
103
+ ## Multiple instances — read `active` per-instance, not "is *anything* locked"
104
+
105
+ `document.pointerLockElement` is a single document-wide value — at most one element can be locked at a time. When several `<wcs-pointer-lock>` instances exist simultaneously (e.g. `target="#a"` and `target="#b"`), each instance compares `document.pointerLockElement` against **its own** resolved target, not merely "is the document locked". Locking `#a` makes the `target="#a"` instance report `active: true` and the `target="#b"` instance report `active: false` — even though *some* element (`#a`) is locked document-wide.
106
+
107
+ ## Vendor prefixes
108
+
109
+ Some older WebKit builds expose `webkitRequestPointerLock` / `webkitExitPointerLock` / `webkitPointerLockElement` / the `webkitpointerlockchange` event instead of the standard names. API resolution happens **at call time** (never cached), probing the standard name first and falling back to the legacy name — this lets an unsupported environment (neither name present) be detected correctly and lets tests install/remove the API freely.
110
+
111
+ ## Notes & limitations
112
+
113
+ - **User gesture required.** See above — this is a platform constraint, not something this node can work around.
114
+ - **`exitPointerLock()` is synchronous**, unlike `@wcstack/fullscreen`'s Promise-based `exitFullscreen()`. It carries no `_gen` generation guard of its own (only `requestPointerLock()`, being asynchronous, needs one); it is still wrapped in `try/catch` defensively so a non-conformant implementation can never throw out of it.
115
+ - **`movementX`/`movementY` are out of scope for v1.** See above.
116
+ - **No autoTrigger.** Because `requestPointerLock()` needs a user-gesture context, the primary activation path is the command-token protocol (`command.requestPointerLock: $command.<token>` on `<wcs-pointer-lock>`) rather than a `data-*target` click shortcut.
117
+ - **SSR (`@wcstack/server`).** Declares `static hasConnectedCallbackPromise = true` and exposes `connectedCallbackPromise`; since `observe()` is synchronous, this promise always settles immediately.
118
+
119
+ ## Headless usage (`PointerLockCore`)
120
+
121
+ The Core has no DOM dependency (beyond calling the Pointer Lock platform API) and can be used directly with `bind()` from `@wc-bindable/core`:
122
+
123
+ ```typescript
124
+ import { PointerLockCore } from "@wcstack/pointer-lock";
125
+
126
+ const lock = new PointerLockCore();
127
+ lock.addEventListener("wcs-pointer-lock:change", (e) => {
128
+ console.log((e as CustomEvent).detail); // boolean — the new `active` value directly
129
+ });
130
+
131
+ const canvas = document.querySelector("#scene")!;
132
+ lock.observe(canvas); // subscribe to document pointerlockchange, self-filtering on `canvas`
133
+ await lock.requestPointerLock(canvas); // must be called from within a user gesture
134
+ console.log(lock.active, lock.error);
135
+
136
+ // later, when done:
137
+ lock.exitPointerLock(); // synchronous
138
+ lock.dispose(); // detach the document listener
139
+ ```
140
+
141
+ ## License
142
+
143
+ MIT
package/dist/auto.js ADDED
@@ -0,0 +1,3 @@
1
+ import { bootstrapPointerLock } from "./index.esm.js";
2
+
3
+ bootstrapPointerLock();
@@ -0,0 +1 @@
1
+ import{bootstrapPointerLock}from"./index.esm.min.js";bootstrapPointerLock();
@@ -0,0 +1,202 @@
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 pointerLock: string;
24
+ }
25
+ interface IWritableTagNames {
26
+ pointerLock?: string;
27
+ }
28
+ interface IConfig {
29
+ readonly tagNames: ITagNames;
30
+ }
31
+ interface IWritableConfig {
32
+ tagNames?: IWritableTagNames;
33
+ }
34
+
35
+ /**
36
+ * Value types for PointerLockCore (headless) — the Core's readable value
37
+ * surface. 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/pointer-lock-tag-design.md §2, docs/fullscreen-tag-design.md §8).
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * const core = new PointerLockCore();
46
+ * // bind() only ever delivers "active" — see the note above about "error".
47
+ * bind(core, (name: keyof WcsPointerLockCoreValues, value) => { ... });
48
+ * ```
49
+ */
50
+ interface WcsPointerLockCoreValues {
51
+ active: boolean;
52
+ error: any;
53
+ }
54
+ /**
55
+ * Value types for the Shell (`<wcs-pointer-lock>`) — identical value surface
56
+ * to the Core (same caveat: only `active` is observable). The Shell
57
+ * additionally accepts a `target` attribute
58
+ * (see docs/pointer-lock-tag-design.md / docs/fullscreen-tag-design.md §1).
59
+ */
60
+ type WcsPointerLockValues = WcsPointerLockCoreValues;
61
+
62
+ declare function bootstrapPointerLock(userConfig?: IWritableConfig): void;
63
+
64
+ declare function getConfig(): IConfig;
65
+
66
+ /**
67
+ * Headless Pointer Lock primitive. A thin, framework-agnostic wrapper around
68
+ * the Pointer Lock API (`Element.requestPointerLock()` /
69
+ * `document.exitPointerLock()` / `document.pointerLockElement` / the
70
+ * `document`-scoped `pointerlockchange` event) exposed through the
71
+ * wc-bindable protocol.
72
+ *
73
+ * This Core follows the same basic pattern as `FullscreenCore`
74
+ * (docs/fullscreen-tag-design.md, referenced by docs/pointer-lock-tag-design.md
75
+ * §1): target resolution happens in the Shell, `pointerlockchange` is
76
+ * subscribed on `document` (not on the target element) and each instance
77
+ * self-filters by comparing `document.pointerLockElement` against its own
78
+ * resolved target, API resolution is call-time (never cached) and probes the
79
+ * standard name before the legacy (`webkit`-prefixed) name, and a single
80
+ * Core-level `_gen` generation guard protects the asynchronous
81
+ * `requestPointerLock()` call from stale resolution after dispose().
82
+ *
83
+ * Key difference from Fullscreen (docs/pointer-lock-tag-design.md §2):
84
+ * `exitPointerLock()` is a *synchronous* platform API (it returns `void`, not
85
+ * a `Promise`), so the Core's `exitPointerLock()` command is synchronous too
86
+ * and carries no `_gen` guard of its own — it is wrapped in `try/catch` only
87
+ * as a defensive measure (never-throw), not because it can go stale.
88
+ *
89
+ * Scope note (docs/pointer-lock-tag-design.md §3): `movementX`/`movementY`
90
+ * are intentionally NOT exposed by this Core (v1 scope). They are
91
+ * high-frequency `mousemove` data unsuited to the same-value-guarded
92
+ * declarative `properties` surface; see the design doc for the rationale and
93
+ * the planned `debounce`/`throttle`-based opt-in for a future version.
94
+ */
95
+ declare class PointerLockCore extends EventTarget {
96
+ static wcBindable: IWcBindable;
97
+ private _target;
98
+ private _active;
99
+ private _error;
100
+ private _resolvedTarget;
101
+ private _subscribed;
102
+ private _gen;
103
+ private _ready;
104
+ constructor(target?: EventTarget);
105
+ get ready(): Promise<void>;
106
+ get active(): boolean;
107
+ get error(): any;
108
+ observe(target: Element | null): Promise<void>;
109
+ dispose(): void;
110
+ /**
111
+ * Request pointer lock on `element`. Never-throw: a missing API or a
112
+ * rejected promise (e.g. called outside a user-gesture context —
113
+ * `NotAllowedError`, docs/fullscreen-tag-design.md §3) is captured into
114
+ * `error` rather than propagated. `element` may be `null` when the Shell's
115
+ * `target` selector did not resolve (docs/pointer-lock-tag-design.md §1
116
+ * defers error representation to FullscreenCore verbatim — this null-target
117
+ * case mirrors `FullscreenCore.requestFullscreen(null)`,
118
+ * docs/fullscreen-tag-design.md §6): distinct from "API is not supported"
119
+ * below, so a typo'd selector doesn't masquerade as an unsupported platform.
120
+ */
121
+ requestPointerLock(element: Element | null): Promise<void>;
122
+ /**
123
+ * Exit pointer lock. Synchronous platform API (docs/pointer-lock-tag-design.md
124
+ * §2) — returns `void`, not a `Promise`. Silent no-op when nothing is
125
+ * currently locked or the API is unsupported (mirrors
126
+ * `FullscreenCore.exitFullscreen()`'s no-op contract,
127
+ * docs/fullscreen-tag-design.md §7). Wrapped in try/catch defensively: even
128
+ * though the platform API is synchronous and documented as not throwing in
129
+ * this case, a synchronous throw from a non-conformant/fake implementation
130
+ * must never escape (never-throw).
131
+ */
132
+ exitPointerLock(): void;
133
+ private _requestPointerLockFn;
134
+ private _exitPointerLockFn;
135
+ private _pointerLockElement;
136
+ private _pointerLockChangeEventName;
137
+ private _onChange;
138
+ private _applyActive;
139
+ private _setActive;
140
+ private _setError;
141
+ }
142
+
143
+ /**
144
+ * `<wcs-pointer-lock target="...">` — declarative Pointer Lock API control.
145
+ *
146
+ * Like `<wcs-fullscreen>` (docs/fullscreen-tag-design.md §0), this Shell does
147
+ * not lock itself — it operates on a *referenced* element via the `target`
148
+ * attribute, using the same 3-mode resolution rule as `intersection`
149
+ * (`_resolveTarget()`/`_safeQuery()`, copied verbatim per
150
+ * docs/pointer-lock-tag-design.md §1 / docs/fullscreen-tag-design.md §1):
151
+ *
152
+ * | `target` | operates on | display |
153
+ * |-----------------|-------------------------|-------------|
154
+ * | omitted | first element child | `contents` |
155
+ * | `"#selector"` | the matched element | `none` |
156
+ * | `"self"` | the element itself | `block` |
157
+ *
158
+ * `requestPointerLock()` requires a user-gesture context (docs/fullscreen-tag-design.md
159
+ * §3) — the primary activation path is the command-token protocol
160
+ * (`command.requestPointerLock: $command.<token>` on `<wcs-pointer-lock>`,
161
+ * emitted by a button's `onclick: $command.<token>`), not an
162
+ * autoTrigger attribute (none is provided in v1,
163
+ * docs/pointer-lock-tag-design.md §4).
164
+ *
165
+ * `movementX`/`movementY` are intentionally out of scope for v1
166
+ * (docs/pointer-lock-tag-design.md §3) — do not add them without revisiting
167
+ * the design doc.
168
+ */
169
+ declare class WcsPointerLock extends HTMLElement {
170
+ static hasConnectedCallbackPromise: boolean;
171
+ static observedAttributes: string[];
172
+ static wcBindable: IWcBindable;
173
+ private _core;
174
+ private _connectedCallbackPromise;
175
+ constructor();
176
+ get connectedCallbackPromise(): Promise<void>;
177
+ get target(): string;
178
+ set target(value: string);
179
+ get active(): boolean;
180
+ get error(): any;
181
+ /**
182
+ * Resolve `target` and request pointer lock on it. Requires a user-gesture
183
+ * context. never-throw: an unresolvable target or an unsupported/rejected
184
+ * API call are both surfaced via `error`, never thrown (mirrors
185
+ * `<wcs-fullscreen>`'s `requestFullscreen()`, docs/fullscreen-tag-design.md
186
+ * §3/§6 — the Shell passes the (possibly `null`) resolved element straight
187
+ * through and lets the Core set `error`, rather than silently no-op'ing
188
+ * here).
189
+ */
190
+ requestPointerLock(): Promise<void>;
191
+ /** Exit pointer lock. Synchronous command — silent no-op if nothing is locked. */
192
+ exitPointerLock(): void;
193
+ connectedCallback(): void;
194
+ disconnectedCallback(): void;
195
+ attributeChangedCallback(name: string): void;
196
+ private _applyDisplayAndObserve;
197
+ private _resolveTarget;
198
+ private _safeQuery;
199
+ }
200
+
201
+ export { PointerLockCore, WcsPointerLock, bootstrapPointerLock, getConfig };
202
+ export type { IWritableConfig, IWritableTagNames, WcsPointerLockCoreValues, WcsPointerLockValues };
@@ -0,0 +1,416 @@
1
+ const _config = {
2
+ tagNames: {
3
+ pointerLock: "wcs-pointer-lock",
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 Pointer Lock primitive. A thin, framework-agnostic wrapper around
41
+ * the Pointer Lock API (`Element.requestPointerLock()` /
42
+ * `document.exitPointerLock()` / `document.pointerLockElement` / the
43
+ * `document`-scoped `pointerlockchange` event) exposed through the
44
+ * wc-bindable protocol.
45
+ *
46
+ * This Core follows the same basic pattern as `FullscreenCore`
47
+ * (docs/fullscreen-tag-design.md, referenced by docs/pointer-lock-tag-design.md
48
+ * §1): target resolution happens in the Shell, `pointerlockchange` is
49
+ * subscribed on `document` (not on the target element) and each instance
50
+ * self-filters by comparing `document.pointerLockElement` against its own
51
+ * resolved target, API resolution is call-time (never cached) and probes the
52
+ * standard name before the legacy (`webkit`-prefixed) name, and a single
53
+ * Core-level `_gen` generation guard protects the asynchronous
54
+ * `requestPointerLock()` call from stale resolution after dispose().
55
+ *
56
+ * Key difference from Fullscreen (docs/pointer-lock-tag-design.md §2):
57
+ * `exitPointerLock()` is a *synchronous* platform API (it returns `void`, not
58
+ * a `Promise`), so the Core's `exitPointerLock()` command is synchronous too
59
+ * and carries no `_gen` guard of its own — it is wrapped in `try/catch` only
60
+ * as a defensive measure (never-throw), not because it can go stale.
61
+ *
62
+ * Scope note (docs/pointer-lock-tag-design.md §3): `movementX`/`movementY`
63
+ * are intentionally NOT exposed by this Core (v1 scope). They are
64
+ * high-frequency `mousemove` data unsuited to the same-value-guarded
65
+ * declarative `properties` surface; see the design doc for the rationale and
66
+ * the planned `debounce`/`throttle`-based opt-in for a future version.
67
+ */
68
+ class PointerLockCore extends EventTarget {
69
+ static wcBindable = {
70
+ protocol: "wc-bindable",
71
+ version: 1,
72
+ // `active`'s CustomEvent detail is the bare boolean value itself — no
73
+ // getter needed (docs/pointer-lock-tag-design.md §2). This differs from
74
+ // FullscreenCore's `{ active }`-shaped detail + getter.
75
+ properties: [
76
+ { name: "active", event: "wcs-pointer-lock:change" },
77
+ ],
78
+ commands: [
79
+ { name: "requestPointerLock", async: true },
80
+ // Synchronous platform API (document.exitPointerLock() returns void) —
81
+ // no `async` flag (docs/pointer-lock-tag-design.md §2).
82
+ { name: "exitPointerLock" },
83
+ ],
84
+ };
85
+ _target;
86
+ _active = false;
87
+ _error = null;
88
+ // The element this instance last resolved requestPointerLock()/observe()
89
+ // against, kept so the document-scoped `pointerlockchange` handler can
90
+ // self-filter under multiple concurrent instances (docs/fullscreen-tag-design.md
91
+ // §2.1, inherited verbatim by pointer-lock per docs/pointer-lock-tag-design.md §1).
92
+ _resolvedTarget = null;
93
+ // True once observe() has attached the live `document` listener. Guards
94
+ // observe() so a redundant call does not re-subscribe; dispose() resets it
95
+ // so a later observe() resumes cleanly.
96
+ _subscribed = false;
97
+ // Core-level generation guard (§3.4 of the guidelines / §6 of
98
+ // fullscreen-tag-design.md): only requestPointerLock() is asynchronous and
99
+ // needs it. exitPointerLock() is synchronous and has no stale-resolution
100
+ // race to guard against.
101
+ _gen = 0;
102
+ // SSR (§3.8): no asynchronous probe to await — observe() completes
103
+ // synchronously, so readiness is immediate.
104
+ _ready = Promise.resolve();
105
+ constructor(target) {
106
+ super();
107
+ this._target = target ?? this;
108
+ }
109
+ get ready() {
110
+ return this._ready;
111
+ }
112
+ get active() {
113
+ return this._active;
114
+ }
115
+ get error() {
116
+ return this._error;
117
+ }
118
+ // Lifecycle (§3.5). Idempotent: a second observe() while already subscribed
119
+ // updates the tracked resolved target without re-subscribing to `document`.
120
+ observe(target) {
121
+ this._resolvedTarget = target;
122
+ if (!this._subscribed) {
123
+ this._subscribed = true;
124
+ document.addEventListener(this._pointerLockChangeEventName(), this._onChange);
125
+ }
126
+ this._applyActive();
127
+ return this._ready;
128
+ }
129
+ dispose() {
130
+ this._gen++; // invalidate any in-flight requestPointerLock() resolution
131
+ if (this._subscribed) {
132
+ this._subscribed = false;
133
+ document.removeEventListener(this._pointerLockChangeEventName(), this._onChange);
134
+ }
135
+ this._resolvedTarget = null;
136
+ }
137
+ /**
138
+ * Request pointer lock on `element`. Never-throw: a missing API or a
139
+ * rejected promise (e.g. called outside a user-gesture context —
140
+ * `NotAllowedError`, docs/fullscreen-tag-design.md §3) is captured into
141
+ * `error` rather than propagated. `element` may be `null` when the Shell's
142
+ * `target` selector did not resolve (docs/pointer-lock-tag-design.md §1
143
+ * defers error representation to FullscreenCore verbatim — this null-target
144
+ * case mirrors `FullscreenCore.requestFullscreen(null)`,
145
+ * docs/fullscreen-tag-design.md §6): distinct from "API is not supported"
146
+ * below, so a typo'd selector doesn't masquerade as an unsupported platform.
147
+ */
148
+ async requestPointerLock(element) {
149
+ const gen = ++this._gen;
150
+ this._resolvedTarget = element;
151
+ if (!element) {
152
+ this._setError({ message: "Pointer Lock target could not be resolved." });
153
+ return;
154
+ }
155
+ const fn = this._requestPointerLockFn(element);
156
+ if (!fn) {
157
+ // Resolved synchronously in the same tick as the call — dispose()
158
+ // cannot have run yet, so no staleness check is needed here (matches
159
+ // the reference `requestFullscreen()` implementation,
160
+ // docs/fullscreen-tag-design.md §6).
161
+ this._setError({ message: "Pointer Lock API is not supported." });
162
+ return;
163
+ }
164
+ try {
165
+ // fn is already bound to `element` by _requestPointerLockFn().
166
+ await fn();
167
+ if (gen !== this._gen)
168
+ return; // stale
169
+ this._setError(null);
170
+ this._applyActive();
171
+ }
172
+ catch (e) {
173
+ if (gen !== this._gen)
174
+ return; // stale
175
+ this._setError(e);
176
+ }
177
+ }
178
+ /**
179
+ * Exit pointer lock. Synchronous platform API (docs/pointer-lock-tag-design.md
180
+ * §2) — returns `void`, not a `Promise`. Silent no-op when nothing is
181
+ * currently locked or the API is unsupported (mirrors
182
+ * `FullscreenCore.exitFullscreen()`'s no-op contract,
183
+ * docs/fullscreen-tag-design.md §7). Wrapped in try/catch defensively: even
184
+ * though the platform API is synchronous and documented as not throwing in
185
+ * this case, a synchronous throw from a non-conformant/fake implementation
186
+ * must never escape (never-throw).
187
+ */
188
+ exitPointerLock() {
189
+ try {
190
+ if (this._pointerLockElement() === null)
191
+ return; // already unlocked: silent no-op
192
+ const fn = this._exitPointerLockFn();
193
+ if (!fn)
194
+ return; // unsupported: silent no-op (semantically already "not locked")
195
+ fn();
196
+ this._setError(null);
197
+ this._applyActive();
198
+ }
199
+ catch (e) {
200
+ this._setError(e);
201
+ }
202
+ }
203
+ // --- API resolution (call-time, never cached — §3.7) ---
204
+ // Resolved from `Element.prototype` rather than `el.requestPointerLock`
205
+ // directly: when `target="self"`, `el` is the `<wcs-pointer-lock>` Shell
206
+ // itself, whose own class declares an instance method also named
207
+ // `requestPointerLock()` (the wcBindable command). Reading the property off
208
+ // the instance would pick up that Shell method instead of the native
209
+ // platform API and recurse infinitely (Shell.requestPointerLock() ->
210
+ // Core.requestPointerLock() -> resolves "el.requestPointerLock" -> the same
211
+ // Shell method again). Going through `Element.prototype` sidesteps the name
212
+ // collision — note this does NOT pick up an override on a subclass's own
213
+ // prototype (e.g. `WcsPointerLock.prototype`); it deliberately jumps
214
+ // straight to the platform-defined layer. Both the standard and legacy name
215
+ // are resolved the same way, for symmetry — matching FullscreenCore's
216
+ // `_elementFullscreenFn` (docs/fullscreen-tag-design.md §4) ONLY in that one
217
+ // respect. Unlike that Core, this one does not check `el`'s own properties
218
+ // first: there is no test-stub/per-element monkey-patch path to accommodate
219
+ // here (mocks.ts installs the fakes directly on `Element.prototype`), so it
220
+ // goes straight there for both names.
221
+ _requestPointerLockFn(el) {
222
+ const proto = Element.prototype;
223
+ const standard = proto.requestPointerLock;
224
+ if (typeof standard === "function")
225
+ return standard.bind(el);
226
+ const legacy = proto.webkitRequestPointerLock;
227
+ return typeof legacy === "function" ? legacy.bind(el) : undefined;
228
+ }
229
+ _exitPointerLockFn() {
230
+ const d = document;
231
+ return d.exitPointerLock?.bind(document) ?? d.webkitExitPointerLock?.bind(document);
232
+ }
233
+ _pointerLockElement() {
234
+ const d = document;
235
+ return d.pointerLockElement ?? d.webkitPointerLockElement ?? null;
236
+ }
237
+ // NOTE (test-environment caveat, not a production concern): happy-dom
238
+ // always implements `document.onpointerlockchange` (as `null`) regardless
239
+ // of which fake API surface a test installs, so `"onpointerlockchange" in
240
+ // document` can never observably be `false` under this test runner and the
241
+ // `webkitpointerlockchange` branch below cannot be driven through
242
+ // `observe()` in a unit test. The branch is still correct and required for
243
+ // real legacy WebKit builds that lack `onpointerlockchange` entirely — kept
244
+ // as documented, deliberate, untestable-in-this-harness code per
245
+ // docs/pointer-lock-tag-design.md.
246
+ /* v8 ignore next 3 */
247
+ _pointerLockChangeEventName() {
248
+ return "onpointerlockchange" in document ? "pointerlockchange" : "webkitpointerlockchange";
249
+ }
250
+ _onChange = () => {
251
+ this._applyActive();
252
+ };
253
+ // Self-filter (docs/fullscreen-tag-design.md §2.1): compares against this
254
+ // instance's own resolved target, not merely "is *something* locked" — so
255
+ // multiple concurrent instances each report the correct `active` value.
256
+ _applyActive() {
257
+ const next = this._resolvedTarget !== null && this._pointerLockElement() === this._resolvedTarget;
258
+ this._setActive(next);
259
+ }
260
+ // Same-value guard (MUST, §3.3 of the guidelines). detail itself is the
261
+ // bare boolean value (no getter needed) per docs/pointer-lock-tag-design.md
262
+ // §2 — unlike FullscreenCore's `{ active }`-shaped detail + getter.
263
+ _setActive(v) {
264
+ if (this._active === v)
265
+ return;
266
+ this._active = v;
267
+ this._target.dispatchEvent(new CustomEvent("wcs-pointer-lock:change", {
268
+ detail: v,
269
+ bubbles: true,
270
+ }));
271
+ }
272
+ _setError(e) {
273
+ this._error = e;
274
+ }
275
+ }
276
+
277
+ /**
278
+ * `<wcs-pointer-lock target="...">` — declarative Pointer Lock API control.
279
+ *
280
+ * Like `<wcs-fullscreen>` (docs/fullscreen-tag-design.md §0), this Shell does
281
+ * not lock itself — it operates on a *referenced* element via the `target`
282
+ * attribute, using the same 3-mode resolution rule as `intersection`
283
+ * (`_resolveTarget()`/`_safeQuery()`, copied verbatim per
284
+ * docs/pointer-lock-tag-design.md §1 / docs/fullscreen-tag-design.md §1):
285
+ *
286
+ * | `target` | operates on | display |
287
+ * |-----------------|-------------------------|-------------|
288
+ * | omitted | first element child | `contents` |
289
+ * | `"#selector"` | the matched element | `none` |
290
+ * | `"self"` | the element itself | `block` |
291
+ *
292
+ * `requestPointerLock()` requires a user-gesture context (docs/fullscreen-tag-design.md
293
+ * §3) — the primary activation path is the command-token protocol
294
+ * (`command.requestPointerLock: $command.<token>` on `<wcs-pointer-lock>`,
295
+ * emitted by a button's `onclick: $command.<token>`), not an
296
+ * autoTrigger attribute (none is provided in v1,
297
+ * docs/pointer-lock-tag-design.md §4).
298
+ *
299
+ * `movementX`/`movementY` are intentionally out of scope for v1
300
+ * (docs/pointer-lock-tag-design.md §3) — do not add them without revisiting
301
+ * the design doc.
302
+ */
303
+ class WcsPointerLock extends HTMLElement {
304
+ // SSR (§4.4): the Core subscribes synchronously on connect, but the Shell
305
+ // still exposes connectedCallbackPromise so the state binder can await it
306
+ // uniformly across all IO nodes before snapshotting.
307
+ static hasConnectedCallbackPromise = true;
308
+ static observedAttributes = ["target"];
309
+ static wcBindable = {
310
+ ...PointerLockCore.wcBindable,
311
+ inputs: [{ name: "target", attribute: "target" }],
312
+ // Core の commands をそのまま継承(単一情報源)。network/intersection と同型。
313
+ commands: PointerLockCore.wcBindable.commands,
314
+ };
315
+ _core;
316
+ _connectedCallbackPromise = Promise.resolve();
317
+ constructor() {
318
+ super();
319
+ this._core = new PointerLockCore(this);
320
+ }
321
+ get connectedCallbackPromise() {
322
+ return this._connectedCallbackPromise;
323
+ }
324
+ // --- Attribute accessors ---
325
+ get target() {
326
+ return this.getAttribute("target") ?? "";
327
+ }
328
+ set target(value) {
329
+ this.setAttribute("target", value);
330
+ }
331
+ // --- Core delegated getters ---
332
+ get active() {
333
+ return this._core.active;
334
+ }
335
+ get error() {
336
+ return this._core.error;
337
+ }
338
+ // --- Commands ---
339
+ /**
340
+ * Resolve `target` and request pointer lock on it. Requires a user-gesture
341
+ * context. never-throw: an unresolvable target or an unsupported/rejected
342
+ * API call are both surfaced via `error`, never thrown (mirrors
343
+ * `<wcs-fullscreen>`'s `requestFullscreen()`, docs/fullscreen-tag-design.md
344
+ * §3/§6 — the Shell passes the (possibly `null`) resolved element straight
345
+ * through and lets the Core set `error`, rather than silently no-op'ing
346
+ * here).
347
+ */
348
+ async requestPointerLock() {
349
+ const { element } = this._resolveTarget();
350
+ await this._core.requestPointerLock(element);
351
+ }
352
+ /** Exit pointer lock. Synchronous command — silent no-op if nothing is locked. */
353
+ exitPointerLock() {
354
+ this._core.exitPointerLock();
355
+ }
356
+ // --- Lifecycle ---
357
+ connectedCallback() {
358
+ this._applyDisplayAndObserve();
359
+ }
360
+ disconnectedCallback() {
361
+ this._core.dispose();
362
+ }
363
+ attributeChangedCallback(name) {
364
+ if (name === "target" && this.isConnected) {
365
+ this._applyDisplayAndObserve();
366
+ }
367
+ }
368
+ // --- Internal ---
369
+ _applyDisplayAndObserve() {
370
+ const { element, display } = this._resolveTarget();
371
+ this.style.display = display;
372
+ this._connectedCallbackPromise = this._core.observe(element);
373
+ }
374
+ // Copied verbatim from packages/intersection/src/components/Intersect.ts
375
+ // (§1 of docs/pointer-lock-tag-design.md / docs/fullscreen-tag-design.md).
376
+ _resolveTarget() {
377
+ const target = this.target;
378
+ if (target === "self") {
379
+ return { element: this, display: "block" };
380
+ }
381
+ if (target !== "") {
382
+ const scope = this.getRootNode();
383
+ return { element: this._safeQuery(scope, target), display: "none" };
384
+ }
385
+ const child = this.firstElementChild;
386
+ if (child) {
387
+ return { element: child, display: "contents" };
388
+ }
389
+ return { element: this, display: "block" };
390
+ }
391
+ // Copied verbatim from packages/intersection/src/components/Intersect.ts.
392
+ _safeQuery(scope, selector) {
393
+ try {
394
+ return scope.querySelector(selector);
395
+ }
396
+ catch {
397
+ return null;
398
+ }
399
+ }
400
+ }
401
+
402
+ function registerComponents() {
403
+ if (!customElements.get(config.tagNames.pointerLock)) {
404
+ customElements.define(config.tagNames.pointerLock, WcsPointerLock);
405
+ }
406
+ }
407
+
408
+ function bootstrapPointerLock(userConfig) {
409
+ if (userConfig) {
410
+ setConfig(userConfig);
411
+ }
412
+ registerComponents();
413
+ }
414
+
415
+ export { PointerLockCore, WcsPointerLock, bootstrapPointerLock, getConfig };
416
+ //# sourceMappingURL=index.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.esm.js","sources":["../src/config.ts","../src/core/PointerLockCore.ts","../src/components/PointerLock.ts","../src/registerComponents.ts","../src/bootstrapPointerLock.ts"],"sourcesContent":["import { IConfig, IWritableConfig } from \"./types.js\";\n\ninterface IInternalConfig extends IConfig {\n tagNames: {\n pointerLock: string;\n };\n}\n\nconst _config: IInternalConfig = {\n tagNames: {\n pointerLock: \"wcs-pointer-lock\",\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 Pointer Lock primitive. A thin, framework-agnostic wrapper around\n * the Pointer Lock API (`Element.requestPointerLock()` /\n * `document.exitPointerLock()` / `document.pointerLockElement` / the\n * `document`-scoped `pointerlockchange` event) exposed through the\n * wc-bindable protocol.\n *\n * This Core follows the same basic pattern as `FullscreenCore`\n * (docs/fullscreen-tag-design.md, referenced by docs/pointer-lock-tag-design.md\n * §1): target resolution happens in the Shell, `pointerlockchange` is\n * subscribed on `document` (not on the target element) and each instance\n * self-filters by comparing `document.pointerLockElement` against its own\n * resolved target, API resolution is call-time (never cached) and probes the\n * standard name before the legacy (`webkit`-prefixed) name, and a single\n * Core-level `_gen` generation guard protects the asynchronous\n * `requestPointerLock()` call from stale resolution after dispose().\n *\n * Key difference from Fullscreen (docs/pointer-lock-tag-design.md §2):\n * `exitPointerLock()` is a *synchronous* platform API (it returns `void`, not\n * a `Promise`), so the Core's `exitPointerLock()` command is synchronous too\n * and carries no `_gen` guard of its own — it is wrapped in `try/catch` only\n * as a defensive measure (never-throw), not because it can go stale.\n *\n * Scope note (docs/pointer-lock-tag-design.md §3): `movementX`/`movementY`\n * are intentionally NOT exposed by this Core (v1 scope). They are\n * high-frequency `mousemove` data unsuited to the same-value-guarded\n * declarative `properties` surface; see the design doc for the rationale and\n * the planned `debounce`/`throttle`-based opt-in for a future version.\n */\nexport class PointerLockCore extends EventTarget {\n static wcBindable: IWcBindable = {\n protocol: \"wc-bindable\",\n version: 1,\n // `active`'s CustomEvent detail is the bare boolean value itself — no\n // getter needed (docs/pointer-lock-tag-design.md §2). This differs from\n // FullscreenCore's `{ active }`-shaped detail + getter.\n properties: [\n { name: \"active\", event: \"wcs-pointer-lock:change\" },\n ],\n commands: [\n { name: \"requestPointerLock\", async: true },\n // Synchronous platform API (document.exitPointerLock() returns void) —\n // no `async` flag (docs/pointer-lock-tag-design.md §2).\n { name: \"exitPointerLock\" },\n ],\n };\n\n private _target: EventTarget;\n private _active = false;\n private _error: any = null;\n\n // The element this instance last resolved requestPointerLock()/observe()\n // against, kept so the document-scoped `pointerlockchange` handler can\n // self-filter under multiple concurrent instances (docs/fullscreen-tag-design.md\n // §2.1, inherited verbatim by pointer-lock per docs/pointer-lock-tag-design.md §1).\n private _resolvedTarget: Element | null = null;\n\n // True once observe() has attached the live `document` listener. Guards\n // observe() so a redundant call does not re-subscribe; dispose() resets it\n // so a later observe() resumes cleanly.\n private _subscribed = false;\n\n // Core-level generation guard (§3.4 of the guidelines / §6 of\n // fullscreen-tag-design.md): only requestPointerLock() is asynchronous and\n // needs it. exitPointerLock() is synchronous and has no stale-resolution\n // race to guard against.\n private _gen = 0;\n\n // SSR (§3.8): no asynchronous probe to await — observe() completes\n // synchronously, 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). Idempotent: a second observe() while already subscribed\n // updates the tracked resolved target without re-subscribing to `document`.\n observe(target: Element | null): Promise<void> {\n this._resolvedTarget = target;\n if (!this._subscribed) {\n this._subscribed = true;\n document.addEventListener(this._pointerLockChangeEventName(), this._onChange);\n }\n this._applyActive();\n return this._ready;\n }\n\n dispose(): void {\n this._gen++; // invalidate any in-flight requestPointerLock() resolution\n if (this._subscribed) {\n this._subscribed = false;\n document.removeEventListener(this._pointerLockChangeEventName(), this._onChange);\n }\n this._resolvedTarget = null;\n }\n\n /**\n * Request pointer lock on `element`. Never-throw: a missing API or a\n * rejected promise (e.g. called outside a user-gesture context —\n * `NotAllowedError`, docs/fullscreen-tag-design.md §3) is captured into\n * `error` rather than propagated. `element` may be `null` when the Shell's\n * `target` selector did not resolve (docs/pointer-lock-tag-design.md §1\n * defers error representation to FullscreenCore verbatim — this null-target\n * case mirrors `FullscreenCore.requestFullscreen(null)`,\n * docs/fullscreen-tag-design.md §6): distinct from \"API is not supported\"\n * below, so a typo'd selector doesn't masquerade as an unsupported platform.\n */\n async requestPointerLock(element: Element | null): Promise<void> {\n const gen = ++this._gen;\n this._resolvedTarget = element;\n if (!element) {\n this._setError({ message: \"Pointer Lock target could not be resolved.\" });\n return;\n }\n const fn = this._requestPointerLockFn(element);\n if (!fn) {\n // Resolved synchronously in the same tick as the call — dispose()\n // cannot have run yet, so no staleness check is needed here (matches\n // the reference `requestFullscreen()` implementation,\n // docs/fullscreen-tag-design.md §6).\n this._setError({ message: \"Pointer Lock API is not supported.\" });\n return;\n }\n try {\n // fn is already bound to `element` by _requestPointerLockFn().\n await fn();\n if (gen !== this._gen) return; // stale\n this._setError(null);\n this._applyActive();\n } catch (e: any) {\n if (gen !== this._gen) return; // stale\n this._setError(e);\n }\n }\n\n /**\n * Exit pointer lock. Synchronous platform API (docs/pointer-lock-tag-design.md\n * §2) — returns `void`, not a `Promise`. Silent no-op when nothing is\n * currently locked or the API is unsupported (mirrors\n * `FullscreenCore.exitFullscreen()`'s no-op contract,\n * docs/fullscreen-tag-design.md §7). Wrapped in try/catch defensively: even\n * though the platform API is synchronous and documented as not throwing in\n * this case, a synchronous throw from a non-conformant/fake implementation\n * must never escape (never-throw).\n */\n exitPointerLock(): void {\n try {\n if (this._pointerLockElement() === null) return; // already unlocked: silent no-op\n const fn = this._exitPointerLockFn();\n if (!fn) return; // unsupported: silent no-op (semantically already \"not locked\")\n fn();\n this._setError(null);\n this._applyActive();\n } catch (e: any) {\n this._setError(e);\n }\n }\n\n // --- API resolution (call-time, never cached — §3.7) ---\n\n // Resolved from `Element.prototype` rather than `el.requestPointerLock`\n // directly: when `target=\"self\"`, `el` is the `<wcs-pointer-lock>` Shell\n // itself, whose own class declares an instance method also named\n // `requestPointerLock()` (the wcBindable command). Reading the property off\n // the instance would pick up that Shell method instead of the native\n // platform API and recurse infinitely (Shell.requestPointerLock() ->\n // Core.requestPointerLock() -> resolves \"el.requestPointerLock\" -> the same\n // Shell method again). Going through `Element.prototype` sidesteps the name\n // collision — note this does NOT pick up an override on a subclass's own\n // prototype (e.g. `WcsPointerLock.prototype`); it deliberately jumps\n // straight to the platform-defined layer. Both the standard and legacy name\n // are resolved the same way, for symmetry — matching FullscreenCore's\n // `_elementFullscreenFn` (docs/fullscreen-tag-design.md §4) ONLY in that one\n // respect. Unlike that Core, this one does not check `el`'s own properties\n // first: there is no test-stub/per-element monkey-patch path to accommodate\n // here (mocks.ts installs the fakes directly on `Element.prototype`), so it\n // goes straight there for both names.\n private _requestPointerLockFn(el: Element): (() => Promise<void>) | undefined {\n const proto = Element.prototype as any;\n const standard = proto.requestPointerLock;\n if (typeof standard === \"function\") return standard.bind(el);\n const legacy = proto.webkitRequestPointerLock;\n return typeof legacy === \"function\" ? legacy.bind(el) : undefined;\n }\n\n private _exitPointerLockFn(): (() => void) | undefined {\n const d = document as any;\n return d.exitPointerLock?.bind(document) ?? d.webkitExitPointerLock?.bind(document);\n }\n\n private _pointerLockElement(): Element | null {\n const d = document as any;\n return d.pointerLockElement ?? d.webkitPointerLockElement ?? null;\n }\n\n // NOTE (test-environment caveat, not a production concern): happy-dom\n // always implements `document.onpointerlockchange` (as `null`) regardless\n // of which fake API surface a test installs, so `\"onpointerlockchange\" in\n // document` can never observably be `false` under this test runner and the\n // `webkitpointerlockchange` branch below cannot be driven through\n // `observe()` in a unit test. The branch is still correct and required for\n // real legacy WebKit builds that lack `onpointerlockchange` entirely — kept\n // as documented, deliberate, untestable-in-this-harness code per\n // docs/pointer-lock-tag-design.md.\n /* v8 ignore next 3 */\n private _pointerLockChangeEventName(): string {\n return \"onpointerlockchange\" in document ? \"pointerlockchange\" : \"webkitpointerlockchange\";\n }\n\n private _onChange = (): void => {\n this._applyActive();\n };\n\n // Self-filter (docs/fullscreen-tag-design.md §2.1): compares against this\n // instance's own resolved target, not merely \"is *something* locked\" — so\n // multiple concurrent instances each report the correct `active` value.\n private _applyActive(): void {\n const next = this._resolvedTarget !== null && this._pointerLockElement() === this._resolvedTarget;\n this._setActive(next);\n }\n\n // Same-value guard (MUST, §3.3 of the guidelines). detail itself is the\n // bare boolean value (no getter needed) per docs/pointer-lock-tag-design.md\n // §2 — unlike FullscreenCore's `{ active }`-shaped detail + getter.\n private _setActive(v: boolean): void {\n if (this._active === v) return;\n this._active = v;\n this._target.dispatchEvent(new CustomEvent(\"wcs-pointer-lock:change\", {\n detail: v,\n bubbles: true,\n }));\n }\n\n private _setError(e: any): void {\n this._error = e;\n }\n}\n","import { IWcBindable } from \"../types.js\";\nimport { PointerLockCore } from \"../core/PointerLockCore.js\";\n\n/**\n * `<wcs-pointer-lock target=\"...\">` — declarative Pointer Lock API control.\n *\n * Like `<wcs-fullscreen>` (docs/fullscreen-tag-design.md §0), this Shell does\n * not lock itself — it operates on a *referenced* element via the `target`\n * attribute, using the same 3-mode resolution rule as `intersection`\n * (`_resolveTarget()`/`_safeQuery()`, copied verbatim per\n * docs/pointer-lock-tag-design.md §1 / docs/fullscreen-tag-design.md §1):\n *\n * | `target` | operates on | display |\n * |-----------------|-------------------------|-------------|\n * | omitted | first element child | `contents` |\n * | `\"#selector\"` | the matched element | `none` |\n * | `\"self\"` | the element itself | `block` |\n *\n * `requestPointerLock()` requires a user-gesture context (docs/fullscreen-tag-design.md\n * §3) — the primary activation path is the command-token protocol\n * (`command.requestPointerLock: $command.<token>` on `<wcs-pointer-lock>`,\n * emitted by a button's `onclick: $command.<token>`), not an\n * autoTrigger attribute (none is provided in v1,\n * docs/pointer-lock-tag-design.md §4).\n *\n * `movementX`/`movementY` are intentionally out of scope for v1\n * (docs/pointer-lock-tag-design.md §3) — do not add them without revisiting\n * the design doc.\n */\nexport class WcsPointerLock extends HTMLElement {\n // SSR (§4.4): the Core subscribes synchronously on connect, but the Shell\n // still exposes connectedCallbackPromise so the state binder can await it\n // uniformly across all IO nodes before snapshotting.\n static hasConnectedCallbackPromise = true;\n\n static observedAttributes = [\"target\"];\n\n static wcBindable: IWcBindable = {\n ...PointerLockCore.wcBindable,\n inputs: [{ name: \"target\", attribute: \"target\" }],\n // Core の commands をそのまま継承(単一情報源)。network/intersection と同型。\n commands: PointerLockCore.wcBindable.commands,\n };\n\n private _core: PointerLockCore;\n private _connectedCallbackPromise: Promise<void> = Promise.resolve();\n\n constructor() {\n super();\n this._core = new PointerLockCore(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 /**\n * Resolve `target` and request pointer lock on it. Requires a user-gesture\n * context. never-throw: an unresolvable target or an unsupported/rejected\n * API call are both surfaced via `error`, never thrown (mirrors\n * `<wcs-fullscreen>`'s `requestFullscreen()`, docs/fullscreen-tag-design.md\n * §3/§6 — the Shell passes the (possibly `null`) resolved element straight\n * through and lets the Core set `error`, rather than silently no-op'ing\n * here).\n */\n async requestPointerLock(): Promise<void> {\n const { element } = this._resolveTarget();\n await this._core.requestPointerLock(element);\n }\n\n /** Exit pointer lock. Synchronous command — silent no-op if nothing is locked. */\n exitPointerLock(): void {\n this._core.exitPointerLock();\n }\n\n // --- Lifecycle ---\n\n connectedCallback(): void {\n this._applyDisplayAndObserve();\n }\n\n disconnectedCallback(): void {\n this._core.dispose();\n }\n\n attributeChangedCallback(name: string): void {\n if (name === \"target\" && this.isConnected) {\n this._applyDisplayAndObserve();\n }\n }\n\n // --- Internal ---\n\n private _applyDisplayAndObserve(): void {\n const { element, display } = this._resolveTarget();\n this.style.display = display;\n this._connectedCallbackPromise = this._core.observe(element);\n }\n\n // Copied verbatim from packages/intersection/src/components/Intersect.ts\n // (§1 of docs/pointer-lock-tag-design.md / docs/fullscreen-tag-design.md).\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 // Copied verbatim from packages/intersection/src/components/Intersect.ts.\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","import { WcsPointerLock } from \"./components/PointerLock.js\";\nimport { config } from \"./config.js\";\n\nexport function registerComponents(): void {\n if (!customElements.get(config.tagNames.pointerLock)) {\n customElements.define(config.tagNames.pointerLock, WcsPointerLock);\n }\n}\n","import { setConfig } from \"./config.js\";\nimport { registerComponents } from \"./registerComponents.js\";\nimport { IWritableConfig } from \"./types.js\";\n\nexport function bootstrapPointerLock(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:{pointerLock:"wcs-pointer-lock"}};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 n of Object.keys(e))t[n]=r(e[n]);return t}let n=null;const s=e;function i(){return n||(n=t(r(e))),n}class o extends EventTarget{static wcBindable={protocol:"wc-bindable",version:1,properties:[{name:"active",event:"wcs-pointer-lock:change"}],commands:[{name:"requestPointerLock",async:!0},{name:"exitPointerLock"}]};_target;_active=!1;_error=null;_resolvedTarget=null;_subscribed=!1;_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._resolvedTarget=e,this._subscribed||(this._subscribed=!0,document.addEventListener(this._pointerLockChangeEventName(),this._onChange)),this._applyActive(),this._ready}dispose(){this._gen++,this._subscribed&&(this._subscribed=!1,document.removeEventListener(this._pointerLockChangeEventName(),this._onChange)),this._resolvedTarget=null}async requestPointerLock(e){const t=++this._gen;if(this._resolvedTarget=e,!e)return void this._setError({message:"Pointer Lock target could not be resolved."});const r=this._requestPointerLockFn(e);if(r)try{if(await r(),t!==this._gen)return;this._setError(null),this._applyActive()}catch(e){if(t!==this._gen)return;this._setError(e)}else this._setError({message:"Pointer Lock API is not supported."})}exitPointerLock(){try{if(null===this._pointerLockElement())return;const e=this._exitPointerLockFn();if(!e)return;e(),this._setError(null),this._applyActive()}catch(e){this._setError(e)}}_requestPointerLockFn(e){const t=Element.prototype,r=t.requestPointerLock;if("function"==typeof r)return r.bind(e);const n=t.webkitRequestPointerLock;return"function"==typeof n?n.bind(e):void 0}_exitPointerLockFn(){const e=document;return e.exitPointerLock?.bind(document)??e.webkitExitPointerLock?.bind(document)}_pointerLockElement(){const e=document;return e.pointerLockElement??e.webkitPointerLockElement??null}_pointerLockChangeEventName(){return"onpointerlockchange"in document?"pointerlockchange":"webkitpointerlockchange"}_onChange=()=>{this._applyActive()};_applyActive(){const e=null!==this._resolvedTarget&&this._pointerLockElement()===this._resolvedTarget;this._setActive(e)}_setActive(e){this._active!==e&&(this._active=e,this._target.dispatchEvent(new CustomEvent("wcs-pointer-lock:change",{detail:e,bubbles:!0})))}_setError(e){this._error=e}}class c extends HTMLElement{static hasConnectedCallbackPromise=!0;static observedAttributes=["target"];static wcBindable={...o.wcBindable,inputs:[{name:"target",attribute:"target"}],commands:o.wcBindable.commands};_core;_connectedCallbackPromise=Promise.resolve();constructor(){super(),this._core=new o(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 requestPointerLock(){const{element:e}=this._resolveTarget();await this._core.requestPointerLock(e)}exitPointerLock(){this._core.exitPointerLock()}connectedCallback(){this._applyDisplayAndObserve()}disconnectedCallback(){this._core.dispose()}attributeChangedCallback(e){"target"===e&&this.isConnected&&this._applyDisplayAndObserve()}_applyDisplayAndObserve(){const{element:e,display:t}=this._resolveTarget();this.style.display=t,this._connectedCallbackPromise=this._core.observe(e)}_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}}}function a(t){var r;t&&((r=t).tagNames&&Object.assign(e.tagNames,r.tagNames),n=null),customElements.get(s.tagNames.pointerLock)||customElements.define(s.tagNames.pointerLock,c)}export{o as PointerLockCore,c as WcsPointerLock,a as bootstrapPointerLock,i 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/PointerLockCore.ts","../src/components/PointerLock.ts","../src/bootstrapPointerLock.ts","../src/registerComponents.ts"],"sourcesContent":["import { IConfig, IWritableConfig } from \"./types.js\";\n\ninterface IInternalConfig extends IConfig {\n tagNames: {\n pointerLock: string;\n };\n}\n\nconst _config: IInternalConfig = {\n tagNames: {\n pointerLock: \"wcs-pointer-lock\",\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 Pointer Lock primitive. A thin, framework-agnostic wrapper around\n * the Pointer Lock API (`Element.requestPointerLock()` /\n * `document.exitPointerLock()` / `document.pointerLockElement` / the\n * `document`-scoped `pointerlockchange` event) exposed through the\n * wc-bindable protocol.\n *\n * This Core follows the same basic pattern as `FullscreenCore`\n * (docs/fullscreen-tag-design.md, referenced by docs/pointer-lock-tag-design.md\n * §1): target resolution happens in the Shell, `pointerlockchange` is\n * subscribed on `document` (not on the target element) and each instance\n * self-filters by comparing `document.pointerLockElement` against its own\n * resolved target, API resolution is call-time (never cached) and probes the\n * standard name before the legacy (`webkit`-prefixed) name, and a single\n * Core-level `_gen` generation guard protects the asynchronous\n * `requestPointerLock()` call from stale resolution after dispose().\n *\n * Key difference from Fullscreen (docs/pointer-lock-tag-design.md §2):\n * `exitPointerLock()` is a *synchronous* platform API (it returns `void`, not\n * a `Promise`), so the Core's `exitPointerLock()` command is synchronous too\n * and carries no `_gen` guard of its own — it is wrapped in `try/catch` only\n * as a defensive measure (never-throw), not because it can go stale.\n *\n * Scope note (docs/pointer-lock-tag-design.md §3): `movementX`/`movementY`\n * are intentionally NOT exposed by this Core (v1 scope). They are\n * high-frequency `mousemove` data unsuited to the same-value-guarded\n * declarative `properties` surface; see the design doc for the rationale and\n * the planned `debounce`/`throttle`-based opt-in for a future version.\n */\nexport class PointerLockCore extends EventTarget {\n static wcBindable: IWcBindable = {\n protocol: \"wc-bindable\",\n version: 1,\n // `active`'s CustomEvent detail is the bare boolean value itself — no\n // getter needed (docs/pointer-lock-tag-design.md §2). This differs from\n // FullscreenCore's `{ active }`-shaped detail + getter.\n properties: [\n { name: \"active\", event: \"wcs-pointer-lock:change\" },\n ],\n commands: [\n { name: \"requestPointerLock\", async: true },\n // Synchronous platform API (document.exitPointerLock() returns void) —\n // no `async` flag (docs/pointer-lock-tag-design.md §2).\n { name: \"exitPointerLock\" },\n ],\n };\n\n private _target: EventTarget;\n private _active = false;\n private _error: any = null;\n\n // The element this instance last resolved requestPointerLock()/observe()\n // against, kept so the document-scoped `pointerlockchange` handler can\n // self-filter under multiple concurrent instances (docs/fullscreen-tag-design.md\n // §2.1, inherited verbatim by pointer-lock per docs/pointer-lock-tag-design.md §1).\n private _resolvedTarget: Element | null = null;\n\n // True once observe() has attached the live `document` listener. Guards\n // observe() so a redundant call does not re-subscribe; dispose() resets it\n // so a later observe() resumes cleanly.\n private _subscribed = false;\n\n // Core-level generation guard (§3.4 of the guidelines / §6 of\n // fullscreen-tag-design.md): only requestPointerLock() is asynchronous and\n // needs it. exitPointerLock() is synchronous and has no stale-resolution\n // race to guard against.\n private _gen = 0;\n\n // SSR (§3.8): no asynchronous probe to await — observe() completes\n // synchronously, 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). Idempotent: a second observe() while already subscribed\n // updates the tracked resolved target without re-subscribing to `document`.\n observe(target: Element | null): Promise<void> {\n this._resolvedTarget = target;\n if (!this._subscribed) {\n this._subscribed = true;\n document.addEventListener(this._pointerLockChangeEventName(), this._onChange);\n }\n this._applyActive();\n return this._ready;\n }\n\n dispose(): void {\n this._gen++; // invalidate any in-flight requestPointerLock() resolution\n if (this._subscribed) {\n this._subscribed = false;\n document.removeEventListener(this._pointerLockChangeEventName(), this._onChange);\n }\n this._resolvedTarget = null;\n }\n\n /**\n * Request pointer lock on `element`. Never-throw: a missing API or a\n * rejected promise (e.g. called outside a user-gesture context —\n * `NotAllowedError`, docs/fullscreen-tag-design.md §3) is captured into\n * `error` rather than propagated. `element` may be `null` when the Shell's\n * `target` selector did not resolve (docs/pointer-lock-tag-design.md §1\n * defers error representation to FullscreenCore verbatim — this null-target\n * case mirrors `FullscreenCore.requestFullscreen(null)`,\n * docs/fullscreen-tag-design.md §6): distinct from \"API is not supported\"\n * below, so a typo'd selector doesn't masquerade as an unsupported platform.\n */\n async requestPointerLock(element: Element | null): Promise<void> {\n const gen = ++this._gen;\n this._resolvedTarget = element;\n if (!element) {\n this._setError({ message: \"Pointer Lock target could not be resolved.\" });\n return;\n }\n const fn = this._requestPointerLockFn(element);\n if (!fn) {\n // Resolved synchronously in the same tick as the call — dispose()\n // cannot have run yet, so no staleness check is needed here (matches\n // the reference `requestFullscreen()` implementation,\n // docs/fullscreen-tag-design.md §6).\n this._setError({ message: \"Pointer Lock API is not supported.\" });\n return;\n }\n try {\n // fn is already bound to `element` by _requestPointerLockFn().\n await fn();\n if (gen !== this._gen) return; // stale\n this._setError(null);\n this._applyActive();\n } catch (e: any) {\n if (gen !== this._gen) return; // stale\n this._setError(e);\n }\n }\n\n /**\n * Exit pointer lock. Synchronous platform API (docs/pointer-lock-tag-design.md\n * §2) — returns `void`, not a `Promise`. Silent no-op when nothing is\n * currently locked or the API is unsupported (mirrors\n * `FullscreenCore.exitFullscreen()`'s no-op contract,\n * docs/fullscreen-tag-design.md §7). Wrapped in try/catch defensively: even\n * though the platform API is synchronous and documented as not throwing in\n * this case, a synchronous throw from a non-conformant/fake implementation\n * must never escape (never-throw).\n */\n exitPointerLock(): void {\n try {\n if (this._pointerLockElement() === null) return; // already unlocked: silent no-op\n const fn = this._exitPointerLockFn();\n if (!fn) return; // unsupported: silent no-op (semantically already \"not locked\")\n fn();\n this._setError(null);\n this._applyActive();\n } catch (e: any) {\n this._setError(e);\n }\n }\n\n // --- API resolution (call-time, never cached — §3.7) ---\n\n // Resolved from `Element.prototype` rather than `el.requestPointerLock`\n // directly: when `target=\"self\"`, `el` is the `<wcs-pointer-lock>` Shell\n // itself, whose own class declares an instance method also named\n // `requestPointerLock()` (the wcBindable command). Reading the property off\n // the instance would pick up that Shell method instead of the native\n // platform API and recurse infinitely (Shell.requestPointerLock() ->\n // Core.requestPointerLock() -> resolves \"el.requestPointerLock\" -> the same\n // Shell method again). Going through `Element.prototype` sidesteps the name\n // collision — note this does NOT pick up an override on a subclass's own\n // prototype (e.g. `WcsPointerLock.prototype`); it deliberately jumps\n // straight to the platform-defined layer. Both the standard and legacy name\n // are resolved the same way, for symmetry — matching FullscreenCore's\n // `_elementFullscreenFn` (docs/fullscreen-tag-design.md §4) ONLY in that one\n // respect. Unlike that Core, this one does not check `el`'s own properties\n // first: there is no test-stub/per-element monkey-patch path to accommodate\n // here (mocks.ts installs the fakes directly on `Element.prototype`), so it\n // goes straight there for both names.\n private _requestPointerLockFn(el: Element): (() => Promise<void>) | undefined {\n const proto = Element.prototype as any;\n const standard = proto.requestPointerLock;\n if (typeof standard === \"function\") return standard.bind(el);\n const legacy = proto.webkitRequestPointerLock;\n return typeof legacy === \"function\" ? legacy.bind(el) : undefined;\n }\n\n private _exitPointerLockFn(): (() => void) | undefined {\n const d = document as any;\n return d.exitPointerLock?.bind(document) ?? d.webkitExitPointerLock?.bind(document);\n }\n\n private _pointerLockElement(): Element | null {\n const d = document as any;\n return d.pointerLockElement ?? d.webkitPointerLockElement ?? null;\n }\n\n // NOTE (test-environment caveat, not a production concern): happy-dom\n // always implements `document.onpointerlockchange` (as `null`) regardless\n // of which fake API surface a test installs, so `\"onpointerlockchange\" in\n // document` can never observably be `false` under this test runner and the\n // `webkitpointerlockchange` branch below cannot be driven through\n // `observe()` in a unit test. The branch is still correct and required for\n // real legacy WebKit builds that lack `onpointerlockchange` entirely — kept\n // as documented, deliberate, untestable-in-this-harness code per\n // docs/pointer-lock-tag-design.md.\n /* v8 ignore next 3 */\n private _pointerLockChangeEventName(): string {\n return \"onpointerlockchange\" in document ? \"pointerlockchange\" : \"webkitpointerlockchange\";\n }\n\n private _onChange = (): void => {\n this._applyActive();\n };\n\n // Self-filter (docs/fullscreen-tag-design.md §2.1): compares against this\n // instance's own resolved target, not merely \"is *something* locked\" — so\n // multiple concurrent instances each report the correct `active` value.\n private _applyActive(): void {\n const next = this._resolvedTarget !== null && this._pointerLockElement() === this._resolvedTarget;\n this._setActive(next);\n }\n\n // Same-value guard (MUST, §3.3 of the guidelines). detail itself is the\n // bare boolean value (no getter needed) per docs/pointer-lock-tag-design.md\n // §2 — unlike FullscreenCore's `{ active }`-shaped detail + getter.\n private _setActive(v: boolean): void {\n if (this._active === v) return;\n this._active = v;\n this._target.dispatchEvent(new CustomEvent(\"wcs-pointer-lock:change\", {\n detail: v,\n bubbles: true,\n }));\n }\n\n private _setError(e: any): void {\n this._error = e;\n }\n}\n","import { IWcBindable } from \"../types.js\";\nimport { PointerLockCore } from \"../core/PointerLockCore.js\";\n\n/**\n * `<wcs-pointer-lock target=\"...\">` — declarative Pointer Lock API control.\n *\n * Like `<wcs-fullscreen>` (docs/fullscreen-tag-design.md §0), this Shell does\n * not lock itself — it operates on a *referenced* element via the `target`\n * attribute, using the same 3-mode resolution rule as `intersection`\n * (`_resolveTarget()`/`_safeQuery()`, copied verbatim per\n * docs/pointer-lock-tag-design.md §1 / docs/fullscreen-tag-design.md §1):\n *\n * | `target` | operates on | display |\n * |-----------------|-------------------------|-------------|\n * | omitted | first element child | `contents` |\n * | `\"#selector\"` | the matched element | `none` |\n * | `\"self\"` | the element itself | `block` |\n *\n * `requestPointerLock()` requires a user-gesture context (docs/fullscreen-tag-design.md\n * §3) — the primary activation path is the command-token protocol\n * (`command.requestPointerLock: $command.<token>` on `<wcs-pointer-lock>`,\n * emitted by a button's `onclick: $command.<token>`), not an\n * autoTrigger attribute (none is provided in v1,\n * docs/pointer-lock-tag-design.md §4).\n *\n * `movementX`/`movementY` are intentionally out of scope for v1\n * (docs/pointer-lock-tag-design.md §3) — do not add them without revisiting\n * the design doc.\n */\nexport class WcsPointerLock extends HTMLElement {\n // SSR (§4.4): the Core subscribes synchronously on connect, but the Shell\n // still exposes connectedCallbackPromise so the state binder can await it\n // uniformly across all IO nodes before snapshotting.\n static hasConnectedCallbackPromise = true;\n\n static observedAttributes = [\"target\"];\n\n static wcBindable: IWcBindable = {\n ...PointerLockCore.wcBindable,\n inputs: [{ name: \"target\", attribute: \"target\" }],\n // Core の commands をそのまま継承(単一情報源)。network/intersection と同型。\n commands: PointerLockCore.wcBindable.commands,\n };\n\n private _core: PointerLockCore;\n private _connectedCallbackPromise: Promise<void> = Promise.resolve();\n\n constructor() {\n super();\n this._core = new PointerLockCore(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 /**\n * Resolve `target` and request pointer lock on it. Requires a user-gesture\n * context. never-throw: an unresolvable target or an unsupported/rejected\n * API call are both surfaced via `error`, never thrown (mirrors\n * `<wcs-fullscreen>`'s `requestFullscreen()`, docs/fullscreen-tag-design.md\n * §3/§6 — the Shell passes the (possibly `null`) resolved element straight\n * through and lets the Core set `error`, rather than silently no-op'ing\n * here).\n */\n async requestPointerLock(): Promise<void> {\n const { element } = this._resolveTarget();\n await this._core.requestPointerLock(element);\n }\n\n /** Exit pointer lock. Synchronous command — silent no-op if nothing is locked. */\n exitPointerLock(): void {\n this._core.exitPointerLock();\n }\n\n // --- Lifecycle ---\n\n connectedCallback(): void {\n this._applyDisplayAndObserve();\n }\n\n disconnectedCallback(): void {\n this._core.dispose();\n }\n\n attributeChangedCallback(name: string): void {\n if (name === \"target\" && this.isConnected) {\n this._applyDisplayAndObserve();\n }\n }\n\n // --- Internal ---\n\n private _applyDisplayAndObserve(): void {\n const { element, display } = this._resolveTarget();\n this.style.display = display;\n this._connectedCallbackPromise = this._core.observe(element);\n }\n\n // Copied verbatim from packages/intersection/src/components/Intersect.ts\n // (§1 of docs/pointer-lock-tag-design.md / docs/fullscreen-tag-design.md).\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 // Copied verbatim from packages/intersection/src/components/Intersect.ts.\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","import { setConfig } from \"./config.js\";\nimport { registerComponents } from \"./registerComponents.js\";\nimport { IWritableConfig } from \"./types.js\";\n\nexport function bootstrapPointerLock(userConfig?: IWritableConfig): void {\n if (userConfig) {\n setConfig(userConfig);\n }\n registerComponents();\n}\n","import { WcsPointerLock } from \"./components/PointerLock.js\";\nimport { config } from \"./config.js\";\n\nexport function registerComponents(): void {\n if (!customElements.get(config.tagNames.pointerLock)) {\n customElements.define(config.tagNames.pointerLock, WcsPointerLock);\n }\n}\n"],"names":["_config","tagNames","pointerLock","deepFreeze","obj","Object","freeze","key","keys","deepClone","clone","frozenConfig","config","getConfig","PointerLockCore","EventTarget","static","protocol","version","properties","name","event","commands","async","_target","_active","_error","_resolvedTarget","_subscribed","_gen","_ready","Promise","resolve","constructor","target","super","this","ready","active","error","observe","document","addEventListener","_pointerLockChangeEventName","_onChange","_applyActive","dispose","removeEventListener","requestPointerLock","element","gen","_setError","message","fn","_requestPointerLockFn","e","exitPointerLock","_pointerLockElement","_exitPointerLockFn","el","proto","Element","prototype","standard","bind","legacy","webkitRequestPointerLock","undefined","d","webkitExitPointerLock","pointerLockElement","webkitPointerLockElement","next","_setActive","v","dispatchEvent","CustomEvent","detail","bubbles","WcsPointerLock","HTMLElement","wcBindable","inputs","attribute","_core","_connectedCallbackPromise","connectedCallbackPromise","getAttribute","value","setAttribute","_resolveTarget","connectedCallback","_applyDisplayAndObserve","disconnectedCallback","attributeChangedCallback","isConnected","display","style","scope","getRootNode","_safeQuery","child","firstElementChild","selector","querySelector","bootstrapPointerLock","userConfig","partialConfig","assign","customElements","get","define"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@wcstack/pointer-lock",
3
+ "version": "1.16.0",
4
+ "description": "Declarative Pointer Lock component for Web Components. Framework-agnostic Pointer Lock API 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
+ "pointer-lock",
34
+ "mouse",
35
+ "game-input",
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/pointer-lock"
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
+ }