help-layer 1.1.0 → 1.3.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 +101 -10
- package/README.md +106 -10
- package/dist/help-layer.esm.js +8 -8
- package/dist/help-layer.esm.js.map +4 -4
- package/dist/help-layer.iife.js +9 -9
- package/dist/help-layer.iife.js.map +4 -4
- package/dist/types/aria-isolation.d.ts +10 -0
- package/dist/types/floating.d.ts +1 -70
- package/dist/types/floating.floatingui.d.ts +25 -0
- package/dist/types/floating.self.d.ts +26 -0
- package/dist/types/geometry.d.ts +85 -4
- package/dist/types/index.d.ts +4 -4
- package/dist/types/markers.d.ts +42 -4
- package/dist/types/overlap.d.ts +2 -2
- package/dist/types/popup.d.ts +2 -2
- package/dist/types/reference.d.ts +41 -0
- package/dist/types/toggle.d.ts +4 -4
- package/dist/types/types.d.ts +7 -0
- package/package.json +10 -13
- package/src/aria-isolation.js +67 -0
- package/src/blocking-layer.js +1 -1
- package/src/dom-builder.js +22 -4
- package/src/floating.floatingui.js +71 -0
- package/src/floating.js +8 -167
- package/src/floating.self.js +109 -0
- package/src/geometry.js +168 -4
- package/src/index.js +2 -2
- package/src/markers.js +168 -43
- package/src/overlap.js +2 -2
- package/src/popup.js +40 -7
- package/src/reference.js +74 -0
- package/src/style.js +6 -6
- package/src/toggle.js +21 -3
- package/src/types.js +17 -0
package/README.ja.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/help-layer)
|
|
4
4
|
[](./LICENSE)
|
|
5
5
|
[](https://github.com/Y1-Effy/HelpLayer)
|
|
6
|
+
[](https://github.com/Y1-Effy/HelpLayer/actions/workflows/ci.yml)
|
|
6
7
|
|
|
7
8
|
[English](./README.md) | **日本語**
|
|
8
9
|
|
|
@@ -14,9 +15,9 @@
|
|
|
14
15
|
ユーザーが「解説モード」を ON にしている間だけ、知りたい要素の「?」マーカーをクリックして説明を読めます。通常時の見た目は一切変わりません。
|
|
15
16
|
仕組みは透明な遮断レイヤー — 対象要素の近くにマーカーを出しつつ、元アプリのイベントには一切触れずに操作を吸収します。
|
|
16
17
|
|
|
17
|
-
-
|
|
18
|
+
- 実行時依存ゼロ・軽量(プリビルドの IIFE で約 20KB minified)
|
|
18
19
|
- Shadow DOM 貫通・SPA の動的要素・マーカー同士の重なり回避・画面端でのポップアップ自動調整に対応
|
|
19
|
-
- キーボード操作・スクリーンリーダーに配慮(ポップアップは `role="dialog"`、開くとフォーカスを移し閉じるとマーカーへ復帰。モード中はフォーカスを UI 内に封じ込め、`Esc`
|
|
20
|
+
- キーボード操作・スクリーンリーダーに配慮(ポップアップは `role="dialog"`、開くとフォーカスを移し閉じるとマーカーへ復帰。モード中はフォーカスを UI 内に封じ込め、`Esc` で説明を閉じる(開いている説明が無ければ解説モード自体を終了))
|
|
20
21
|
- ON→OFF で追加した DOM・イベント・スタイルを**完全後始末**
|
|
21
22
|
- モダンブラウザ(Chromium / Firefox / WebKit)で動作(e2e を 3 エンジンで検証)
|
|
22
23
|
|
|
@@ -28,8 +29,12 @@
|
|
|
28
29
|
- [クイックスタート](#クイックスタート)
|
|
29
30
|
- [自由配置(要素に紐づけない説明)](#自由配置要素に紐づけない説明)
|
|
30
31
|
- [API](#api)
|
|
32
|
+
- [API の上に作るレシピ(分析・ディープリンク・検索・フレームワーク連携)](./RECIPES.ja.md)
|
|
31
33
|
- [テーマ(CSS カスタムプロパティ)](#テーマcss-カスタムプロパティ)
|
|
34
|
+
- [対応環境(ブラウザ/ランタイム)](#対応環境ブラウザランタイム)
|
|
32
35
|
- [既知の制約](#既知の制約)
|
|
36
|
+
- [パフォーマンス(マーカー数の目安)](#パフォーマンスマーカー数の目安)
|
|
37
|
+
- [アクセシビリティ](#アクセシビリティ)
|
|
33
38
|
- [セキュリティ](#セキュリティ)
|
|
34
39
|
- [開発](#開発)
|
|
35
40
|
|
|
@@ -44,7 +49,7 @@
|
|
|
44
49
|
- **常設ツールチップとの違い** … 説明を常時表示してUIを煩雑にすることがありません。マーカーは
|
|
45
50
|
**モードON中だけ**出るので、**通常時のデザインは一切変わりません**。
|
|
46
51
|
- **DAP系SaaS(Digital Adoption Platform=定着化支援 SaaS)との違い**
|
|
47
|
-
… 外部基盤・契約・トラッキングを必要とせず、**ランニングコスト0
|
|
52
|
+
… 外部基盤・契約・トラッキングを必要とせず、**ランニングコスト0・依存ゼロ・約20KB の完全ローカル動作**。
|
|
48
53
|
CSP / Trusted Types にも対応するため、持ち込み制約の厳しい環境にも入ります。
|
|
49
54
|
|
|
50
55
|
そのうえで共通の核として、**既存コードを書き換えずに後付け**でき、**フレームワーク非依存**で、
|
|
@@ -55,16 +60,19 @@
|
|
|
55
60
|
| 提示形式 | 線形ステップになりがち | 常時表示になりがち | サービス依存 | **モードON中だけ・任意箇所を探索** |
|
|
56
61
|
| 通常時のUI | 実装次第 | 煩雑になりがち | 実装次第 | **一切変えない** |
|
|
57
62
|
| 導入方法 | 多くは要組み込み | CSS/JS を追記 | スニペット+外部基盤+契約 | **後付け・既存コード非改変** |
|
|
58
|
-
| コスト/運用 | 実装次第 | ローカル | 月額+トラッキング運用 | **ランニング0
|
|
63
|
+
| コスト/運用 | 実装次第 | ローカル | 月額+トラッキング運用 | **ランニング0・依存ゼロ** |
|
|
59
64
|
|
|
60
65
|
> ※ HelpLayer は DAP の **フル代替ではありません**。アナリティクスやセグメント別配信、複雑なフロー誘導・
|
|
61
66
|
> オンボーディング自動化といった高機能は対象外で、**「画面内に説明を出す」というコア機能だけを最小コストで満たす**ことに
|
|
62
67
|
> 振り切っています。逆に、強い導線を引きたい・利用状況を計測したいといった目的が主なら、DAP やツアーの方が向きます。
|
|
68
|
+
>
|
|
69
|
+
> とはいえ、これらのいくつか(分析・ディープリンク・検索パネル・フレームワーク連携)は、公開 API の
|
|
70
|
+
> *上に* 数行で作れます。レシピは **[RECIPES.ja.md](./RECIPES.ja.md)** を参照してください。
|
|
63
71
|
|
|
64
72
|
## こんなときに(導入が刺さるケース)
|
|
65
73
|
|
|
66
74
|
- **DAP/ガイド系 SaaS のコストが見合わず、解約を検討している。でも解約すると画面内ヘルプがゼロに戻る。**
|
|
67
|
-
→
|
|
75
|
+
→ 「画面内に説明を出す」というコア機能だけを、依存ゼロ・ランニングコスト0 で自前に残せます。乗り換え後の受け皿に。
|
|
68
76
|
- **SaaS を契約する予算感はないが、ヘルプは拡張したい。**
|
|
69
77
|
→ npm か `<script>` 1本で後付け。月額もアカウントも要りません。
|
|
70
78
|
- **オフィスソフトで別途マニュアルを作る・更新するのが重い。しかも作っても読まれない。**
|
|
@@ -137,7 +145,7 @@ CDN から読む場合は、改ざん検知のため **バージョンを固定*
|
|
|
137
145
|
|
|
138
146
|
```html
|
|
139
147
|
<script
|
|
140
|
-
src="https://unpkg.com/help-layer@1.0
|
|
148
|
+
src="https://unpkg.com/help-layer@1.3.0/dist/help-layer.iife.js"
|
|
141
149
|
integrity="sha384-……(公開版のハッシュに差し替え)"
|
|
142
150
|
crossorigin="anonymous"></script>
|
|
143
151
|
<script>
|
|
@@ -149,7 +157,7 @@ CDN から読む場合は、改ざん検知のため **バージョンを固定*
|
|
|
149
157
|
```
|
|
150
158
|
|
|
151
159
|
> `integrity` のハッシュは公開した実ファイルから生成します。例:
|
|
152
|
-
> `curl -s https://unpkg.com/help-layer@1.0
|
|
160
|
+
> `curl -s https://unpkg.com/help-layer@1.3.0/dist/help-layer.iife.js | openssl dgst -sha384 -binary | openssl base64 -A`
|
|
153
161
|
> (バージョンを固定しないと SRI と不整合になり読み込みが拒否されます。)
|
|
154
162
|
|
|
155
163
|
## 自由配置(要素に紐づけない説明)
|
|
@@ -233,7 +241,7 @@ initHelpLayer({
|
|
|
233
241
|
|
|
234
242
|
| 変数 | 既定 | 用途 |
|
|
235
243
|
|------|------|------|
|
|
236
|
-
| `--help-layer-marker-size` | `
|
|
244
|
+
| `--help-layer-marker-size` | `24px` | マーカー直径(WCAG 2.5.8 の最小ターゲットサイズに合わせた既定) |
|
|
237
245
|
| `--help-layer-marker-bg` | `#2563eb` | マーカー背景色 |
|
|
238
246
|
| `--help-layer-marker-color` | `#fff` | マーカー文字色 |
|
|
239
247
|
| `--help-layer-popup-bg` | `#fff` | ポップアップ背景色 |
|
|
@@ -244,19 +252,102 @@ initHelpLayer({
|
|
|
244
252
|
| `--help-layer-overlay-bg` | `transparent` | 遮断レイヤー(スクリム)背景色。`rgba(0,0,0,0.15)` 等で操作不能状態を可視化 |
|
|
245
253
|
| `--help-layer-overlay-cursor` | `default` | 遮断領域上のカーソル。`not-allowed` / `help` 等 |
|
|
246
254
|
|
|
255
|
+
## 対応環境(ブラウザ/ランタイム)
|
|
256
|
+
|
|
257
|
+
HelpLayer は **現代的な evergreen ブラウザ**(Chrome / Edge・Firefox・Safari)と近年の Electron の
|
|
258
|
+
Chromium を対象とします。**IE11 は非対応であり、構造上対応できません** — ES2020 構文・ES Modules・
|
|
259
|
+
Shadow DOM・`clip-path` に依存しており、IE はいずれも備えていないためです。これは
|
|
260
|
+
パッケージ形式の調整では埋められません。本当に古いランタイムを対象とする必要がある場合、本ライブラリは
|
|
261
|
+
適合しません。
|
|
262
|
+
|
|
263
|
+
下限を決める要素(新しい2つの API は縮退するため、実質的なハード下限はおよそ **2020 年代の evergreen**):
|
|
264
|
+
|
|
265
|
+
| 機能 | 用途 | 下限の目安 | フォールバック |
|
|
266
|
+
|---|---|---|---|
|
|
267
|
+
| ES2020+ES Modules | ライブラリ全体 | evergreen(〜2020) | なし(古い対象はトランスパイル/バンドルが必要) |
|
|
268
|
+
| `requestAnimationFrame` | マーカー/ポップアップの自動配置(毎フレーム追従) | evergreen | なし |
|
|
269
|
+
| open Shadow DOM 貫通 | shadow root 内の対象探索 | evergreen | closed は設計上非対応 |
|
|
270
|
+
| `clip-path: polygon()` | 遮断レイヤーのトグル「穴」 | evergreen(極端に古い Safari は `-webkit-` 必要) | なし |
|
|
271
|
+
| `inert` | ホストを a11y ツリーから除外 | Chrome102 / FF112 / Safari15.5(2023) | 視覚+キーボード遮断のみに縮退 |
|
|
272
|
+
| `Element.checkVisibility()` | 対象が隠れた時にマーカーも隠す | Chrome105 / FF125 / Safari17.4(2024) | 0×0 rect 判定に縮退(`display:none` のみ検出) |
|
|
273
|
+
|
|
274
|
+
### モジュール形式
|
|
275
|
+
|
|
276
|
+
- **ESM(既定)**: `import { initHelpLayer } from 'help-layer'` はビルド済み・テスト済みの
|
|
277
|
+
`dist/help-layer.esm.js` を解決します(自己完結・解決すべき実行時依存なし)。
|
|
278
|
+
- **バンドラ無し/`<script>`/CDN/厳格環境**: グローバル `HelpLayer` を公開する
|
|
279
|
+
自己完結の IIFE ビルドを使用 — [`<script>` だけで使う](#script-だけで使うバンドラなし) 参照。
|
|
280
|
+
- **CommonJS(`require`)**: `require` 入口は提供しません。ブラウザ DOM 専用のため Node の CJS 文脈では
|
|
281
|
+
意味を持ちません。非 ESM のツールチェーンではバンドラ経由で ESM を取り込むか、上記 IIFE を読み込んでください。
|
|
282
|
+
|
|
247
283
|
## 既知の制約
|
|
248
284
|
|
|
249
285
|
- closed な Shadow DOM は JS から到達できないため非対応(open のみ貫通)。
|
|
250
|
-
- マーカーを隅へ重ねるオフセットは既定マーカーサイズ(
|
|
286
|
+
- マーカーを隅へ重ねるオフセットは既定マーカーサイズ(24px)前提。`--help-layer-marker-size` を大きく変えると
|
|
251
287
|
わずかにズレることがあります。
|
|
288
|
+
- **対象要素の「状態」変化は監視しません**(監視するのは「レイアウト」と「DOM 上の有無」のみ)。ON 中、
|
|
289
|
+
マーカーは対象の位置・サイズ変化に追従し、DOM への追加/削除に応じて mount/unmount され、対象自体が
|
|
290
|
+
隠れる/現れる(`display:none` 等)とマーカーも隠れる/戻ります。一方で、対象の**属性・内容の変化**は
|
|
291
|
+
検知しません — 既存要素への `data-help-id` の後付け/除去、`data-help-title` `data-help-text` の書き換え、
|
|
292
|
+
`disabled` 等の状態切り替えはマーカーに反映されません。これは意図的な制約です。文書全体に属性監視
|
|
293
|
+
(`MutationObserver` の `attributes: true, subtree: true`)を張ると、あらゆるクラス/スタイル変更で発火し、
|
|
294
|
+
ドロップイン型ライブラリとしては性能上のアンチパターンになるためです。これらの状態を変えた場合は、
|
|
295
|
+
`update(config)` で作り直す、モードを OFF→ON する、または対象要素を一度 DOM から外して入れ直してください
|
|
296
|
+
(入れ直しは `childList` 監視に乗ります)。
|
|
297
|
+
|
|
298
|
+
## パフォーマンス(マーカー数の目安)
|
|
299
|
+
|
|
300
|
+
コストは **同時に表示しているマーカー数** に比例します(登録した `config` の総数ではありません)。
|
|
301
|
+
表示中の全マーカーは **1 本の共有 `requestAnimationFrame` ループ** でまとめて配置します(マーカーごとの
|
|
302
|
+
監視や外部の配置ライブラリは使いません)。そのためスクロール中・アニメーション中も、1 フレームにつき
|
|
303
|
+
表示中マーカーをまとめた read → compute → write の 1 パスだけが走ります。
|
|
304
|
+
マーカーは **モード ON 中、かつ DOM 上に存在して表示されている対象にのみ** 出ます。`display:none`
|
|
305
|
+
などで隠れた対象(`checkVisibility` が「隠れている」と判定した対象)のマーカーは、再配置・レイアウト
|
|
306
|
+
計測・重なり回避のいずれからも除外されます。つまり効いてくるのは「いま画面に出ている数」です。
|
|
307
|
+
|
|
308
|
+
マーカー同士の重なり回避は 1 パスあたり `O(n²)` ですが、反復回数に上限があり定数も小さく、かつ実際に
|
|
309
|
+
動いたフレームだけ実行されます。1 パスで読む矩形は表示中の参照要素ぶん各 1 回のみ(read と write を
|
|
310
|
+
フェーズ分離するのでレイアウトスラッシングは起きません)。規模で最初に効いてくるのは、表示中マーカー数
|
|
311
|
+
に比例して増える毎フレームの追従コストのほうです。
|
|
312
|
+
|
|
313
|
+
目安(**同時に表示する**マーカー数):
|
|
314
|
+
|
|
315
|
+
- **数百個まで** … 一般的な端末で快適。
|
|
316
|
+
- **〜1000 個** … 実測でも滑らか(同梱のストレスページで自動スクロール中も 60fps を維持)。
|
|
317
|
+
- **それを大きく超える** … スクロール/アニメ中の毎フレーム追従コストがいずれ目立ち始めます。
|
|
318
|
+
|
|
319
|
+
とはいえこのモードは **探索型**(利用者が見たい箇所を選ぶ)なので、一画面に数十個もあれば十分なことが
|
|
320
|
+
ほとんどです。大規模ページでは対象を絞る、ページ/タブ単位で出し分ける(`update(config)` でセットを
|
|
321
|
+
差し替える、別の `attribute` を使う等)として、同時に出す数を抑えるとよいでしょう。これは
|
|
322
|
+
[既知の制約](#既知の制約) で文書全体に属性監視を張らない選択をしたのと同じ理由 — どちらも「割に合わない
|
|
323
|
+
毎フレーム処理」を避けるためです。
|
|
324
|
+
|
|
325
|
+
## アクセシビリティ
|
|
326
|
+
|
|
327
|
+
モード ON 中は、ホストアプリを視覚・ポインタ/キーボードだけでなく **支援技術(AT)に対しても意味的に
|
|
328
|
+
無効化**します。ホストを [`inert`](https://developer.mozilla.org/ja/docs/Web/HTML/Global_attributes/inert)
|
|
329
|
+
属性で a11y ツリーから除外するため、スクリーンリーダーの仮想カーソル(ブラウズモード)でも背景の読み上げ・
|
|
330
|
+
操作ができません。到達できるのはヘルプマーカー・ポップアップ・トグルのみです。ポップアップは
|
|
331
|
+
`aria-modal="true"` を持つ `role="dialog"` で、開くとフォーカスが移り、閉じるとマーカーへ戻ります。
|
|
332
|
+
|
|
333
|
+
この隔離の限定事項:
|
|
334
|
+
|
|
335
|
+
- `inert` は inert なサブツリーの子孫で打ち消せないため、隔離は document body の直下レベルで適用します
|
|
336
|
+
(トグルを透過させる clip-path の"穴"と同じ考え方)。トグルは操作可能である必要があるので、
|
|
337
|
+
**トグルを含む body 直下ブランチは到達可能なまま残します** — 漏れを最小化するにはトグルを body 直下
|
|
338
|
+
(または近い位置)に置いてください(直下の `<body>` 子要素なら漏れゼロ)。
|
|
339
|
+
- `inert` は現行ブラウザで広くサポートされます。非対応環境でも視覚/キーボードの遮断は有効で、AT 除外だけが
|
|
340
|
+
グレースフルに縮退します(エラーにはなりません)。
|
|
252
341
|
|
|
253
342
|
## セキュリティ
|
|
254
343
|
|
|
344
|
+
脆弱性の報告方法・サポート方針・セキュリティリリース方針は [SECURITY.ja.md](./SECURITY.ja.md) を参照してください。報告は公開 issue ではなく GitHub の非公開脆弱性報告をご利用ください。
|
|
345
|
+
|
|
255
346
|
- 設計上、`title` / `text` の描画は `textContent` のみで、`innerHTML` / `eval` / `new Function` は**一切使いません**。
|
|
256
347
|
- 外部通信(`fetch` 等)・`localStorage` / `cookie` などのストレージ利用も**ありません**(完全ローカル動作)。
|
|
257
348
|
- 唯一、未信頼データを HTML/DOM ノードとして挿入しうる経路は `render` オプションです。戻り値はサニタイズされないため、
|
|
258
349
|
ユーザー入力を含む場合は呼び出し側で無害化してください(上記「本文に改行を入れる / リンクを置く」参照)。
|
|
259
|
-
-
|
|
350
|
+
- 実行時依存はありません。CDN 利用時は前述のとおりバージョン固定+SRI を推奨します。
|
|
260
351
|
|
|
261
352
|
### Content Security Policy(CSP)
|
|
262
353
|
|
package/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/help-layer)
|
|
4
4
|
[](./LICENSE)
|
|
5
5
|
[](https://github.com/Y1-Effy/HelpLayer)
|
|
6
|
+
[](https://github.com/Y1-Effy/HelpLayer/actions/workflows/ci.yml)
|
|
6
7
|
|
|
7
8
|
**English** | [日本語](./README.ja.md)
|
|
8
9
|
|
|
@@ -14,9 +15,9 @@ A **framework-agnostic "help mode" library you can drop into any existing web ap
|
|
|
14
15
|
While the mode is ON, it shows a "?" marker next to each target element; clicking it opens a description popup. The normal appearance is completely unchanged.
|
|
15
16
|
It never touches the host app's own event listeners — a transparent blocking layer absorbs interaction instead — so you can adopt it without rewriting existing code.
|
|
16
17
|
|
|
17
|
-
-
|
|
18
|
+
- Zero runtime dependencies; lightweight (the prebuilt IIFE is ~20KB minified)
|
|
18
19
|
- Pierces Shadow DOM, keeps up with dynamically added/removed elements in SPAs, avoids marker-to-marker overlap, and auto-adjusts the popup at screen edges
|
|
19
|
-
- Mindful of keyboard use and screen readers (the popup is `role="dialog"`; opening moves focus and closing returns it to the marker; while the mode is on, focus is trapped within the UI, and `Esc` closes
|
|
20
|
+
- Mindful of keyboard use and screen readers (the popup is `role="dialog"`; opening moves focus and closing returns it to the marker; while the mode is on, focus is trapped within the UI, and `Esc` closes the popup — or exits the mode when no popup is open)
|
|
20
21
|
- Fully cleans up the DOM, listeners, and styles it added when you turn it OFF
|
|
21
22
|
- Works in modern browsers (Chromium / Firefox / WebKit; e2e is verified across all three engines)
|
|
22
23
|
|
|
@@ -28,8 +29,12 @@ It never touches the host app's own event listeners — a transparent blocking l
|
|
|
28
29
|
- [Quick start](#quick-start)
|
|
29
30
|
- [Free placement (descriptions not bound to an element)](#free-placement-descriptions-not-bound-to-an-element)
|
|
30
31
|
- [API](#api)
|
|
32
|
+
- [Recipes on top of the API (analytics, deep-linking, search, frameworks)](./RECIPES.md)
|
|
31
33
|
- [Theming (CSS custom properties)](#theming-css-custom-properties)
|
|
34
|
+
- [Browser & runtime support](#browser--runtime-support)
|
|
32
35
|
- [Known limitations](#known-limitations)
|
|
36
|
+
- [Performance (how many markers)](#performance-how-many-markers)
|
|
37
|
+
- [Accessibility](#accessibility)
|
|
33
38
|
- [Security](#security)
|
|
34
39
|
- [Development](#development)
|
|
35
40
|
|
|
@@ -45,7 +50,7 @@ them on the spot,"** aiming to sacrifice neither the normal look nor your existi
|
|
|
45
50
|
- **vs. always-on tooltips** … It never clutters the UI by showing explanations all the time. Markers
|
|
46
51
|
appear **only while the mode is ON**, so **the normal design is completely unchanged**.
|
|
47
52
|
- **vs. DAP SaaS (Digital Adoption Platform)** … No external platform, contract, or tracking required —
|
|
48
|
-
**zero running cost,
|
|
53
|
+
**zero running cost, zero dependencies, ~20KB, fully local**. It also supports CSP / Trusted Types, so it
|
|
49
54
|
fits environments with strict constraints on what you can bring in.
|
|
50
55
|
|
|
51
56
|
On top of that, they all share a common core: you can **drop it in without rewriting existing code**, it's
|
|
@@ -57,18 +62,21 @@ interaction), and it **fully cleans up on ON→OFF**.
|
|
|
57
62
|
| Presentation | tends to be linear steps | tends to be always visible | service-dependent | **only while ON · explore any spot** |
|
|
58
63
|
| Normal UI | depends on implementation | tends to get cluttered | depends on implementation | **left entirely unchanged** |
|
|
59
64
|
| Adoption | usually needs integration | add CSS/JS | snippet + external platform + contract | **drop-in · no existing-code changes** |
|
|
60
|
-
| Cost / ops | depends on implementation | local | monthly fee + tracking ops | **zero running cost ·
|
|
65
|
+
| Cost / ops | depends on implementation | local | monthly fee + tracking ops | **zero running cost · zero dependencies** |
|
|
61
66
|
|
|
62
67
|
> Note: HelpLayer is **not a full replacement for a DAP**. Advanced features like analytics, segmented
|
|
63
68
|
> delivery, complex flow guidance, and onboarding automation are out of scope — it commits to
|
|
64
69
|
> **satisfying just the core "show explanations in-screen" function at minimal cost**. Conversely, if your
|
|
65
70
|
> main goal is to drive strong funnels or measure usage, a DAP or a tour is the better fit.
|
|
71
|
+
>
|
|
72
|
+
> That said, several of these (analytics, deep-linking, a search palette, framework glue) are easy to
|
|
73
|
+
> build *on top* of the public API in a few lines — see **[RECIPES.md](./RECIPES.md)** for recipes.
|
|
66
74
|
|
|
67
75
|
## When it fits (where adoption pays off)
|
|
68
76
|
|
|
69
77
|
- **A DAP / guide SaaS isn't worth the cost and you're considering canceling — but canceling drops your
|
|
70
78
|
in-screen help back to zero.**
|
|
71
|
-
→ Keep just the core "show explanations in-screen" function in-house, with
|
|
79
|
+
→ Keep just the core "show explanations in-screen" function in-house, with zero dependencies and zero
|
|
72
80
|
running cost. It gives you a place to land after switching away.
|
|
73
81
|
- **You don't have the budget to contract a SaaS, but you want to expand your help.**
|
|
74
82
|
→ Drop it in with npm or a single `<script>`. No monthly fee, no account.
|
|
@@ -143,7 +151,7 @@ When loading from a CDN, we recommend **pinning the version** and adding **SRI (
|
|
|
143
151
|
|
|
144
152
|
```html
|
|
145
153
|
<script
|
|
146
|
-
src="https://unpkg.com/help-layer@1.0
|
|
154
|
+
src="https://unpkg.com/help-layer@1.3.0/dist/help-layer.iife.js"
|
|
147
155
|
integrity="sha384-……(replace with the published file's hash)"
|
|
148
156
|
crossorigin="anonymous"></script>
|
|
149
157
|
<script>
|
|
@@ -155,7 +163,7 @@ When loading from a CDN, we recommend **pinning the version** and adding **SRI (
|
|
|
155
163
|
```
|
|
156
164
|
|
|
157
165
|
> Generate the `integrity` hash from the actually published file, e.g.:
|
|
158
|
-
> `curl -s https://unpkg.com/help-layer@1.0
|
|
166
|
+
> `curl -s https://unpkg.com/help-layer@1.3.0/dist/help-layer.iife.js | openssl dgst -sha384 -binary | openssl base64 -A`
|
|
159
167
|
> (If you don't pin the version, the SRI will mismatch and the browser will refuse to load it.)
|
|
160
168
|
|
|
161
169
|
## Free placement (descriptions not bound to an element)
|
|
@@ -239,7 +247,7 @@ You can change the look just by overriding the following variables in your host
|
|
|
239
247
|
|
|
240
248
|
| Variable | Default | Purpose |
|
|
241
249
|
|------|------|------|
|
|
242
|
-
| `--help-layer-marker-size` | `
|
|
250
|
+
| `--help-layer-marker-size` | `24px` | marker diameter (default meets WCAG 2.5.8 minimum target size) |
|
|
243
251
|
| `--help-layer-marker-bg` | `#2563eb` | marker background color |
|
|
244
252
|
| `--help-layer-marker-color` | `#fff` | marker text color |
|
|
245
253
|
| `--help-layer-popup-bg` | `#fff` | popup background color |
|
|
@@ -250,19 +258,107 @@ You can change the look just by overriding the following variables in your host
|
|
|
250
258
|
| `--help-layer-overlay-bg` | `transparent` | blocking-layer (scrim) background; e.g. `rgba(0,0,0,0.15)` to signal the host is inactive |
|
|
251
259
|
| `--help-layer-overlay-cursor` | `default` | cursor over the blocked area; e.g. `not-allowed` / `help` |
|
|
252
260
|
|
|
261
|
+
## Browser & runtime support
|
|
262
|
+
|
|
263
|
+
HelpLayer targets **modern evergreen browsers** (Chrome / Edge, Firefox, Safari) and the Chromium in
|
|
264
|
+
recent Electron. **Internet Explorer 11 is not supported and cannot be** — the library relies on ES2020
|
|
265
|
+
syntax, ES modules, Shadow DOM, and `clip-path`, none of which IE provides. Packaging
|
|
266
|
+
changes can't bridge this; if you must support genuinely old runtimes, this library is not the right fit.
|
|
267
|
+
|
|
268
|
+
What sets the minimum (the two newest APIs degrade gracefully, so the practical hard floor is roughly
|
|
269
|
+
**2020-era evergreen**):
|
|
270
|
+
|
|
271
|
+
| Feature | Used for | Minimum | Fallback |
|
|
272
|
+
|---|---|---|---|
|
|
273
|
+
| ES2020 + ES modules | the whole library | evergreen (~2020) | none — transpile/bundle for older targets |
|
|
274
|
+
| `requestAnimationFrame` | marker/popup auto-positioning (per-frame tracking) | evergreen | none |
|
|
275
|
+
| Open Shadow DOM piercing | finding targets inside shadow roots | evergreen | closed shadow roots are unsupported by design |
|
|
276
|
+
| `clip-path: polygon()` | the blocking layer's toggle "hole" | evergreen (very old Safari needs `-webkit-`) | none |
|
|
277
|
+
| `inert` | removing the host from the a11y tree | Chrome 102 / FF 112 / Safari 15.5 (2023) | degrades to visual + keyboard blocking only |
|
|
278
|
+
| `Element.checkVisibility()` | hiding a marker when its target is hidden | Chrome 105 / FF 125 / Safari 17.4 (2024) | falls back to a 0×0-rect check (detects `display:none` only) |
|
|
279
|
+
|
|
280
|
+
### Module formats
|
|
281
|
+
|
|
282
|
+
- **ESM (default).** `import { initHelpLayer } from 'help-layer'` resolves to the prebuilt, tested
|
|
283
|
+
`dist/help-layer.esm.js` (self-contained — no runtime dependencies to resolve).
|
|
284
|
+
- **No bundler / `<script>` / CDN / strict environments.** Use the self-contained IIFE build, which
|
|
285
|
+
exposes a global `HelpLayer` — see [Use it with just a `<script>`](#use-it-with-just-a-script-no-bundler).
|
|
286
|
+
- **CommonJS (`require`).** No `require` entry is provided: this is a browser-only DOM library, so a Node
|
|
287
|
+
CJS context can't use it meaningfully. In non-ESM toolchains, consume the ESM build via your bundler,
|
|
288
|
+
or load the IIFE build above.
|
|
289
|
+
|
|
253
290
|
## Known limitations
|
|
254
291
|
|
|
255
292
|
- Closed Shadow DOM is unreachable from JS, so it is unsupported (only open shadow roots are pierced).
|
|
256
|
-
- The offset that overlaps the marker onto a corner assumes the default marker size (
|
|
293
|
+
- The offset that overlaps the marker onto a corner assumes the default marker size (24px). Changing
|
|
257
294
|
`--help-layer-marker-size` significantly may cause a slight drift.
|
|
295
|
+
- **Target *state* changes are not watched** (only *layout* and *presence* are). While ON, a marker
|
|
296
|
+
follows its target's position/size changes and is added/removed as the target enters/leaves the DOM,
|
|
297
|
+
and it hides/reshows when the target itself is hidden/shown (e.g. `display:none`). However, changes
|
|
298
|
+
to a target's **attributes or content** are *not* detected: adding/removing the `data-help-id`
|
|
299
|
+
attribute on an existing element, rewriting `data-help-title` / `data-help-text`, or toggling state
|
|
300
|
+
like `disabled` won't update the markers. This is intentional — watching every attribute mutation
|
|
301
|
+
across the whole document (`MutationObserver` with `attributes: true, subtree: true`) fires on every
|
|
302
|
+
class/style change and is a performance footgun for a drop-in library. If you change such state,
|
|
303
|
+
rebuild via `update(config)`, toggle the mode OFF→ON, or re-insert the target element into the DOM
|
|
304
|
+
(re-insertion is picked up by the `childList` observation).
|
|
305
|
+
|
|
306
|
+
## Performance (how many markers)
|
|
307
|
+
|
|
308
|
+
The cost scales with the number of **markers visible at once**, not with the size of your `config`.
|
|
309
|
+
All visible markers are positioned together in **one shared `requestAnimationFrame` loop** (no
|
|
310
|
+
per-marker watchers, no positioning library), so while the page scrolls or animates each frame does a
|
|
311
|
+
single batched read → compute → write pass over the visible markers.
|
|
312
|
+
Markers only exist **while the mode is ON and only for targets currently present and shown** — a target
|
|
313
|
+
hidden via `display:none` (or otherwise reported hidden by `checkVisibility`) has its marker excluded
|
|
314
|
+
from positioning, layout measurement, and overlap avoidance. So what matters is "how many are on the
|
|
315
|
+
page right now," not how many keys you registered.
|
|
316
|
+
|
|
317
|
+
Marker-to-marker overlap avoidance is `O(n²)` per pass, but it's capped at a few iterations with a small
|
|
318
|
+
constant and runs only when something actually moved; each pass reads every visible reference rect just
|
|
319
|
+
once (reads and writes are phase-separated, so there's no per-marker layout thrashing). What you'd feel
|
|
320
|
+
first at scale is the per-frame tracking, which grows linearly with the number of visible markers.
|
|
321
|
+
|
|
322
|
+
Rough guidance (markers shown **at the same time**):
|
|
323
|
+
|
|
324
|
+
- **Up to a few hundred** — comfortable on typical hardware.
|
|
325
|
+
- **~1000** — still smooth in our measurements (a locked 60fps under continuous auto-scroll on the
|
|
326
|
+
bundled stress page).
|
|
327
|
+
- **Well beyond that** — the per-frame tracking during scroll/animation eventually shows.
|
|
328
|
+
|
|
329
|
+
Even so, the mode is **exploratory** — users pick the spot they want, so a screen rarely needs more than
|
|
330
|
+
a few dozen markers. On large pages, scope your targets, or split them per page/tab (e.g. swap sets with
|
|
331
|
+
`update(config)`, or use a different `attribute`) to keep the number shown at once down. This mirrors the
|
|
332
|
+
deliberate choice in [Known limitations](#known-limitations) not to watch every attribute mutation across
|
|
333
|
+
the document — both avoid per-frame work that doesn't pay for itself.
|
|
334
|
+
|
|
335
|
+
## Accessibility
|
|
336
|
+
|
|
337
|
+
While the mode is ON, the host app is blocked not only visually and for pointer/keyboard input, but
|
|
338
|
+
also **semantically for assistive technology**: the host is removed from the accessibility tree with
|
|
339
|
+
the [`inert`](https://developer.mozilla.org/docs/Web/HTML/Global_attributes/inert) attribute, so a
|
|
340
|
+
screen reader's virtual cursor (browse mode) can't read or activate background content. Only the help
|
|
341
|
+
markers, the popup, and your toggle stay reachable. The popup is a `role="dialog"` with
|
|
342
|
+
`aria-modal="true"`, and focus moves into it on open and returns to the marker on close.
|
|
343
|
+
|
|
344
|
+
Bounded limitations of this isolation:
|
|
345
|
+
|
|
346
|
+
- `inert` can't be cancelled on a descendant of an inert subtree, so isolation is applied at the
|
|
347
|
+
document-body top level (like the clip-path "hole" that lets the toggle show through). The toggle
|
|
348
|
+
must stay operable, so **the top-level branch containing your toggle is left reachable** — keep the
|
|
349
|
+
toggle at/near the body level to minimize what leaks (none if it's a direct `<body>` child).
|
|
350
|
+
- `inert` is broadly supported in current browsers; on engines without it, the visual/keyboard
|
|
351
|
+
blocking still applies, but AT exclusion degrades gracefully (no error).
|
|
258
352
|
|
|
259
353
|
## Security
|
|
260
354
|
|
|
355
|
+
To report a vulnerability and for the support / security-release policy, see [SECURITY.md](./SECURITY.md). Please use GitHub's private vulnerability reporting rather than a public issue.
|
|
356
|
+
|
|
261
357
|
- By design, `title` / `text` are rendered with `textContent` only; `innerHTML` / `eval` / `new Function` are **never used**.
|
|
262
358
|
- There is **no external communication** (`fetch`, etc.) and **no storage use** (`localStorage` / `cookie`) — it runs fully locally.
|
|
263
359
|
- The only path through which untrusted data is inserted into the DOM as HTML / DOM nodes is the `render` option. Its return value is not sanitized, so
|
|
264
360
|
neutralize it on the caller side if it contains user input (see "Line breaks & links in the body" above).
|
|
265
|
-
- The
|
|
361
|
+
- The library has no runtime dependencies. When using a CDN, pin the version and add SRI as noted above.
|
|
266
362
|
|
|
267
363
|
### Content Security Policy (CSP)
|
|
268
364
|
|
package/dist/help-layer.esm.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
function W(e,{toggleEl:t,isLibraryNode:o}){let r=new Set;function n(i){if(i.nodeType!==1)return;let a=i;o(a)||t&&(a===t||a.contains(t))||a.hasAttribute("inert")||(a.toggleAttribute("inert",!0),r.add(a))}for(let i of[...document.body.children])n(i);let l=new MutationObserver(i=>{for(let a of i)a.addedNodes.forEach(n)});l.observe(document.body,{childList:!0}),e.track(()=>{l.disconnect(),r.forEach(i=>i.removeAttribute("inert")),r.clear()})}var we=0;function G(){let e=document.createElement("div");return e.className="help-layer-blocking-layer",e}function U(e,t="?"){let o=document.createElement("button");return o.type="button",o.className="help-layer-marker",o.textContent=t,o.setAttribute("aria-label",`Help: ${e}`),o}function Y(){let e=we++,t=`help-layer-popup-title-${e}`,o=`help-layer-popup-text-${e}`,r=document.createElement("div");r.className="help-layer-popup",r.setAttribute("role","dialog"),r.setAttribute("aria-modal","true"),r.setAttribute("aria-labelledby",t),r.setAttribute("aria-describedby",o),r.tabIndex=-1;let n=document.createElement("div");n.className="help-layer-popup__title",n.id=t;let l=document.createElement("div");l.className="help-layer-popup__text",l.id=o;let i=document.createElement("button");return i.type="button",i.className="help-layer-popup__close",i.textContent="\xD7",i.setAttribute("aria-label","Close"),r.append(n,l,i),{root:r,titleEl:n,textEl:l,closeEl:i}}function J(e,t){let o=e.width||0,r=e.height||0,n=e.left-t.x,l=e.top-t.y;return{x:n,y:l,left:n,top:l,right:n+o,bottom:l+r,width:o,height:r}}var _=12;function Q(e,t,o="top-end"){let[r,n]=o.split("-"),i=n==="start"?_:-_,a,c;return r==="top"||r==="bottom"?(c=r==="top"?e.top-t+_:e.top+e.height-_,n==="start"?a=e.left:n==="end"?a=e.left+e.width-t:a=e.left+e.width/2-t/2,a+=i):(a=r==="left"?e.left-t+_:e.left+e.width-_,n==="start"?c=e.top:n==="end"?c=e.top+e.height-t:c=e.top+e.height/2-t/2,c+=i),{left:a,top:c}}function D(e,t,o,r,n){return{left:e-o.left-r,top:t-o.top-n}}var X=(e,t,o)=>Math.min(Math.max(e,t),Math.max(t,o));function ee(e,t,o,r={}){let n=r.placement??"bottom-start",l=r.offset??8,i=r.padding??8,[a,c]=n.split("-"),m=a==="top"||a==="bottom",h=s=>s==="bottom"?e.top+e.height+l:s==="top"?e.top-t.height-l:s==="right"?e.left+e.width+l:e.left-t.width-l,u=s=>s==="bottom"?o.height-i-(e.top+e.height+l):s==="top"?e.top-l-i:s==="right"?o.width-i-(e.left+e.width+l):e.left-l-i,v={top:"bottom",bottom:"top",left:"right",right:"left"}[a],w=m?t.height:t.width,E=u(a)>=w||u(a)>=u(v)?a:v,b=(s,g,y)=>c==="start"?y:c==="end"?y+s-g:y+s/2-g/2,f,p;return m?(p=h(E),f=b(e.width,t.width,e.left),f=X(f,i,o.width-t.width-i)):(f=h(E),p=b(e.height,t.height,e.top),p=X(p,i,o.height-t.height-i)),{left:f,top:p,placement:c?`${E}-${c}`:E}}function H(e){return{contextElement:document.body,getBoundingClientRect(){return J(e(),{x:window.scrollX,y:window.scrollY})}}}function I(e){if(!(e instanceof Element))return!1;let t=e;for(;t;){if(getComputedStyle(t).position==="fixed")return!0;let o=t.parentElement;if(o)t=o;else{let r=t.getRootNode();t=r instanceof ShadowRoot?r.host:null}}return!1}function j(e){if(!(e instanceof Element))return!1;if(typeof e.checkVisibility=="function")return!e.checkVisibility({visibilityProperty:!0,contentVisibilityAuto:!0});let t=e.getBoundingClientRect();return t.width===0&&t.height===0}function te(e,t,o){e.style.left=`${t}px`,e.style.top=`${o}px`}function Ee(e,t){return e.top===t.top&&e.left===t.left&&e.width===t.width&&e.height===t.height}function oe(e,t){t();let o=e.getBoundingClientRect(),r=!1,n=requestAnimationFrame(function i(){if(r)return;let a=e.getBoundingClientRect();Ee(a,o)||t(),o=a,n=requestAnimationFrame(i)}),l=()=>t();return window.addEventListener("resize",l),()=>{r=!0,cancelAnimationFrame(n),window.removeEventListener("resize",l)}}function ne(e,t,o="bottom-start"){let r=I(e)?"fixed":"absolute";t.style.setProperty("position",r,"important");let n=()=>{let i=e.getBoundingClientRect(),a={width:t.offsetWidth,height:t.offsetHeight},c={width:window.innerWidth,height:window.innerHeight},{left:m,top:h}=ee(i,a,c,{placement:o});if(r==="fixed")te(t,m,h);else{let u=document.body,v=D(m,h,u.getBoundingClientRect(),u.clientLeft,u.clientTop);te(t,v.left,v.top)}},l=oe(e,n);return{update:n,cleanup:l}}function re(e,t,o){return oe(e,o)}function Le(e){let t=e.left,o=e.top,r=e.right,n=e.bottom;return`polygon(
|
|
2
2
|
0px 0px, 100% 0px, 100% 100%, 0px 100%, 0px 0px,
|
|
3
3
|
${t}px ${o}px, ${t}px ${n}px, ${r}px ${n}px, ${r}px ${o}px, ${t}px ${o}px
|
|
4
|
-
)`}function
|
|
4
|
+
)`}function ie(e,{toggleEl:t,onBackgroundClick:o,isLibraryElement:r,onEscape:n}){let l=G();if(document.body.appendChild(l),e.track(()=>l.remove()),t){let v=re(t,l,()=>{l.style.clipPath=Le(t.getBoundingClientRect())});e.track(v)}o&&(l.addEventListener("click",o),e.track(()=>l.removeEventListener("click",o)));let i=document.activeElement;i instanceof HTMLElement&&i!==document.body&&i!==t&&i.blur();let a=u=>{r(u.target)||(u.stopPropagation(),t?t.focus({preventScroll:!0}):u.target instanceof HTMLElement&&u.target.blur())};document.addEventListener("focusin",a,!0),e.track(()=>document.removeEventListener("focusin",a,!0));let c=u=>{r(u.target)||(u.stopPropagation(),u.preventDefault())},m=u=>{if(u.key==="Escape"){u.stopPropagation(),u.preventDefault();return}c(u)},h=u=>{if(u.key==="Escape"){u.stopPropagation(),u.preventDefault(),n&&n();return}c(u)};return document.addEventListener("keydown",h,!0),document.addEventListener("keyup",m,!0),document.addEventListener("keypress",m,!0),e.track(()=>{document.removeEventListener("keydown",h,!0),document.removeEventListener("keyup",m,!0),document.removeEventListener("keypress",m,!0)}),l}function N(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function le(e){return N(e)&&Number.isFinite(e.top)&&Number.isFinite(e.left)}function q(e){if(!N(e))throw new Error("helpConfig must be a plain object");for(let[t,o]of Object.entries(e)){if(!N(o))throw new Error(`helpConfig["${t}"] must be an object`);if(typeof o.title!="string"||o.title==="")throw new Error(`helpConfig["${t}"].title must be a non-empty string`);if(typeof o.text!="string"||o.text==="")throw new Error(`helpConfig["${t}"].text must be a non-empty string`);if(o.position!==void 0&&!le(o.position))throw new Error(`helpConfig["${t}"].position must be { top: finite number, left: finite number }`)}}function se(e){return Object.entries(e).map(([t,o])=>le(o.position)?{key:t,title:o.title,text:o.text,kind:"free",target:null,position:{top:o.position.top,left:o.position.left}}:{key:t,title:o.title,text:o.text,kind:"element",target:null,position:null})}function ae(e,t={}){let o=t.minDistance??26,r=t.iterations??6,n=e.map(l=>({x:l.x,y:l.y}));for(let l=0;l<r;l++){let i=!1;for(let a=0;a<n.length;a++)for(let c=a+1;c<n.length;c++){let m=n[a],h=n[c],u=h.x-m.x,v=h.y-m.y,w=Math.hypot(u,v);if(w>=o)continue;w===0&&(u=1,v=0,w=1);let E=(o-w)/2,b=u/w,f=v/w;m.x-=b*E,m.y-=f*E,h.x+=b*E,h.y+=f*E,i=!0}if(!i)break}return n.map((l,i)=>({dx:l.x-e[i].x,dy:l.y-e[i].y}))}var pe="help-layer-target-highlight",Ce=24;function Ae(e){return e.kind==="free"?H(()=>({top:e.position.top,left:e.position.left,width:0,height:0})):e.target}function ce(e,{onMarkerClick:t,onOverlapResolved:o,onMarkerHidden:r,markerLabel:n="?",markerPlacement:l="top-end"}){let i=new Map,a=null,c=!1,m=0,h=-1;function u(){if(c||i.size===0)return;let p=document.body.getBoundingClientRect(),s=document.body.clientLeft,g=document.body.clientTop,y=[];for(let d of i.values()){if(j(d.reference)){d.hidden||(d.hidden=!0,d.lastBaseEl=null,d.el.style.setProperty("display","none","important"),r&&r(d.record));continue}d.hidden&&(d.hidden=!1,d.el.style.removeProperty("display")),d.refRect=d.reference.getBoundingClientRect(),y.push(d)}if(!m&&y.length){let d=y[0].el.offsetWidth;d>0&&(m=d)}let T=m||Ce,L=y.length!==h;h=y.length;let A=[],$=[];for(let d of y){let P=Q(d.refRect,T,d.placement);$.push({x:P.left+T/2,y:P.top+T/2});let k=d.strategy==="fixed"?{left:P.left,top:P.top}:D(P.left,P.top,p,s,g);A.push(k),(!d.lastBaseEl||d.lastBaseEl.left!==k.left||d.lastBaseEl.top!==k.top)&&(L=!0),d.lastBaseEl=k}if(L&&y.length){let d=ae($),P=!1;for(let k=0;k<y.length;k++){let C=y[k],O=A[k].left+d[k].dx,M=A[k].top+d[k].dy;(C.lastLeft!==O||C.lastTop!==M)&&(C.el.style.left=`${O}px`,C.el.style.top=`${M}px`,C.lastLeft=O,C.lastTop=M,P=!0)}P&&o&&o()}}function v(){a=null,!(c||i.size===0)&&(u(),a=requestAnimationFrame(v))}function w(){a!==null||c||i.size===0||(a=requestAnimationFrame(v))}function E(p){if(i.has(p.id))return;let s=U(p.title,n);document.body.appendChild(s);let g=()=>t(p,s);s.addEventListener("click",g);let y=Ae(p),T=I(y)?"fixed":"absolute";T==="fixed"&&s.style.setProperty("position","fixed","important");let L=p.kind==="element"?p.target:null,A=()=>L&&L.classList.add(pe),$=()=>L&&L.classList.remove(pe);L&&(s.addEventListener("mouseenter",A),s.addEventListener("mouseleave",$),s.addEventListener("focus",A),s.addEventListener("blur",$));let d=!1,P=()=>{d||(d=!0,s.removeEventListener("click",g),L&&(s.removeEventListener("mouseenter",A),s.removeEventListener("mouseleave",$),s.removeEventListener("focus",A),s.removeEventListener("blur",$),$()),s.remove(),i.delete(p.id),w())};i.set(p.id,{record:p,el:s,reference:y,strategy:T,placement:l,cleanup:P,hidden:!1,lastBaseEl:null,lastLeft:void 0,lastTop:void 0}),w()}function b(p){let s=i.get(p);s&&s.cleanup()}function f(p){p.forEach(E),u()}return e.track(()=>{c=!0,a!==null&&(cancelAnimationFrame(a),a=null),[...i.values()].forEach(p=>p.cleanup())}),{mount:E,unmount:b,mountAll:f,has(p){return i.has(p)},findByKey(p){for(let s of i.values())if(s.record.key===p)return s;return null}}}function S(e,t,...o){if(t)try{return t(...o)}catch(r){console.error(`[help-layer] ${e} threw:`,r);return}}var Te=1;function F(e,t,o,r){typeof e.querySelectorAll=="function"&&e.querySelectorAll("*").forEach(n=>{o&&n.matches(t)&&o(n),n.shadowRoot&&(r&&r(n.shadowRoot),F(n.shadowRoot,t,o,r))})}function fe(e,t){let o=[];return F(e,t,r=>o.push(r)),o}function Pe(e){let t=[];return F(e,"*",null,o=>t.push(o)),t}function ue(e,t){let o=[],r=[];if(e.nodeType!==Te)return{matches:o,shadowRoots:r};let n=e;return typeof n.matches=="function"&&n.matches(t)&&o.push(n),n.shadowRoot&&(r.push(n.shadowRoot),F(n.shadowRoot,t,l=>o.push(l),l=>r.push(l))),F(n,t,l=>o.push(l),l=>r.push(l)),{matches:o,shadowRoots:r}}function de({root:e=document,selector:t,onAdded:o,onRemoved:r}){let n=new Set,l=c=>{for(let m of c)m.addedNodes.forEach(h=>{let{matches:u,shadowRoots:v}=ue(h,t);u.forEach(w=>S("observer onAdded",o,w)),v.forEach(a)}),m.removedNodes.forEach(h=>{ue(h,t).matches.forEach(u=>S("observer onRemoved",r,u))})},i=new MutationObserver(l);function a(c){n.has(c)||(n.add(c),i.observe(c,{childList:!0,subtree:!0}))}return a(e),Pe(e).forEach(a),{disconnect(){i.disconnect(),n.clear()}}}var R="data-help-title",K="data-help-text";function V(e="data-help-id"){return`[${e}], [${R}]`}function z(e){let t=new Map;for(let o of e)o.kind==="element"&&t.set(o.key,o);return t}function he(e){return e.filter(t=>t.kind==="free").map(t=>({id:t.key,kind:"free",key:t.key,title:t.title,text:t.text,position:t.position}))}function Z(e,t,o="data-help-id"){let r=e.getAttribute(o),n=r!=null?t.get(r):void 0,l=n?n.title:e.getAttribute(R),i=n?n.text:e.getAttribute(K);return!l||!i?null:{id:e,kind:"element",key:r,title:l,text:i,target:e}}function me(e,t=document,{silent:o=!1,attribute:r="data-help-id"}={}){let n=z(e),l=[];return fe(t,V(r)).forEach(i=>{let a=Z(i,n,r);if(!a){if(!o){let c=i.getAttribute(r);console.warn(c!=null?`[help-layer] element with ${r}="${c}" has no matching helpConfig entry or inline ${R}/${K}`:`[help-layer] element needs both ${R} and ${K} (or a ${r} matching helpConfig)`)}return}l.push(a)}),l}function ye(e,{onClose:t,render:o,popupPlacement:r="bottom-start"}={}){let{root:n,titleEl:l,textEl:i,closeEl:a}=Y();n.style.setProperty("display","none","important"),document.body.appendChild(n),a.addEventListener("click",()=>p());let c=null,m=null,h=null;function u(){h&&(h.cleanup(),h=null)}let v='a[href],button,input,select,textarea,[tabindex]:not([tabindex="-1"])';function w(s){if(s.key!=="Tab")return;let g=[...n.querySelectorAll(v)].filter(A=>A instanceof HTMLElement);if(s.preventDefault(),g.length===0){n.focus({preventScroll:!0});return}let y=g.length,T=g.indexOf(document.activeElement instanceof HTMLElement?document.activeElement:null);(T===-1?s.shiftKey?g[y-1]:g[0]:g[(T+(s.shiftKey?-1:1)+y)%y]).focus({preventScroll:!0})}function E(s,g){l.textContent=s.title;let y=S("render",o,s);i.textContent="",y?i.appendChild(y):i.textContent=s.text,n.style.setProperty("display","block","important"),c=s.id,m=g,u(),h=ne(g,n,r),n.addEventListener("keydown",w,!0),n.focus({preventScroll:!0})}function b(){h&&h.update()}function f(){let s=c!==null;u(),n.removeEventListener("keydown",w,!0),c=null,m=null,n.style.setProperty("display","none","important"),s&&S("onClose",t)}function p(s){let g=s??m;f(),g&&g.isConnected&&typeof g.focus=="function"&&g.focus({preventScroll:!0})}return e.track(()=>{f(),n.remove()}),{root:n,isOpen(s){return c===s},getOpenId(){return c},open:E,close:p,reposition:b}}function ge(){let e=[];return{track(t){e.push(t)},teardownAll(){for(;e.length>0;){let t=e.pop();try{t()}catch(o){console.error("[help-layer] teardown step threw:",o)}}}}}var Se="data-help-layer-style",$e=`
|
|
5
5
|
.help-layer-blocking-layer {
|
|
6
6
|
/* Structural properties !important so a host can't accidentally un-fix or restack the layer and
|
|
7
7
|
defeat the blocking guarantee. */
|
|
@@ -36,15 +36,15 @@ var F="help-layer-popup-title";function z(){let e=document.createElement("div");
|
|
|
36
36
|
pointer-events: auto !important;
|
|
37
37
|
top: 0;
|
|
38
38
|
left: 0;
|
|
39
|
-
width: var(--help-layer-marker-size,
|
|
40
|
-
height: var(--help-layer-marker-size,
|
|
39
|
+
width: var(--help-layer-marker-size, 24px) !important;
|
|
40
|
+
height: var(--help-layer-marker-size, 24px) !important;
|
|
41
41
|
border-radius: 50%;
|
|
42
42
|
background: var(--help-layer-marker-bg, #2563eb);
|
|
43
43
|
color: var(--help-layer-marker-color, #fff);
|
|
44
44
|
font-family: sans-serif;
|
|
45
45
|
font-size: 13px;
|
|
46
46
|
font-weight: bold;
|
|
47
|
-
line-height: var(--help-layer-marker-size,
|
|
47
|
+
line-height: var(--help-layer-marker-size, 24px);
|
|
48
48
|
text-align: center;
|
|
49
49
|
cursor: pointer;
|
|
50
50
|
user-select: none;
|
|
@@ -115,8 +115,8 @@ var F="help-layer-popup-title";function z(){let e=document.createElement("div");
|
|
|
115
115
|
pointer-events: auto !important;
|
|
116
116
|
top: 6px;
|
|
117
117
|
right: 6px;
|
|
118
|
-
width:
|
|
119
|
-
height:
|
|
118
|
+
width: 24px;
|
|
119
|
+
height: 24px;
|
|
120
120
|
padding: 0;
|
|
121
121
|
border: none;
|
|
122
122
|
border-radius: 4px;
|
|
@@ -156,5 +156,5 @@ var F="help-layer-popup-title";function z(){let e=document.createElement("div");
|
|
|
156
156
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.55);
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
|
-
`;function
|
|
159
|
+
`;function be(e){let t=document.createElement("style");return t.setAttribute(Se,""),e&&t.setAttribute("nonce",e),t.textContent=$e,document.head.appendChild(t),t}function xe(e){e.remove()}function _e(e){if(typeof e=="string"){let t=document.querySelector(e);if(!t)throw new Error(`help-layer: toggle element not found for selector "${e}"`);return t}if(e instanceof HTMLElement)return e;throw new Error("help-layer: toggle must be a CSS selector string or a DOM element")}function ve(e){if(!N(e))throw new Error("help-layer: initHelpLayer requires an options object");let{config:t,toggle:o,onEnable:r,onDisable:n,onOpen:l,onClose:i,silent:a=!1,attribute:c="data-help-id",render:m,markerLabel:h="?",markerPlacement:u="top-end",popupPlacement:v="bottom-start",nonce:w}=e,E=t;q(E);let b=o!=null?_e(o):null,f=null,p=null,s=null;function g(){if(f)return;f=ge(),b&&f.track(()=>{b.isConnected&&typeof b.focus=="function"&&b.focus({preventScroll:!0})});let k=be(w);f.track(()=>xe(k));let C=se(E),O=z(C);p=ye(f,{onClose:i,render:m,popupPlacement:v}),s=ce(f,{markerLabel:h,markerPlacement:u,onMarkerClick:(x,B)=>{if(p.isOpen(x.id)){p.close();return}p.open(x,B),S("onOpen",l,x)},onOverlapResolved:()=>p.reposition(),onMarkerHidden:x=>{p.isOpen(x.id)&&p.close(b??void 0)}}),s.mountAll(he(C)),s.mountAll(me(C,document,{silent:a,attribute:c}));let M=de({selector:V(c),onAdded:x=>{let B=Z(x,O,c);B&&!s.has(B.id)&&s.mount(B)},onRemoved:x=>{p.isOpen(x)&&p.close(b??void 0),s.unmount(x)}});f.track(()=>M.disconnect());let ke=ie(f,{toggleEl:b,onBackgroundClick:()=>p.close(),isLibraryElement:x=>!!x&&((b?b.contains(x):!1)||p.root.contains(x)||typeof x.closest=="function"&&!!x.closest(".help-layer-marker")),onEscape:()=>{p.getOpenId()!==null?p.close():L()}});W(f,{toggleEl:b,isLibraryNode:x=>x===ke||x===p.root||!!x.classList&&x.classList.contains("help-layer-marker")})}function y(){f&&(f.teardownAll(),f=null,p=null,s=null)}function T(){f||(g(),S("onEnable",r))}function L(){f&&(y(),S("onDisable",n))}function A(){f?L():T()}function $(k){if(f||T(),!s||!p)return;let C=s.findByKey(k);if(!C){a||console.warn(`[help-layer] open(): no help marker for key "${k}"`);return}p.open(C.record,C.el),S("onOpen",l,C.record)}function d(){p&&p.close()}function P(k){q(k),E=k,f&&(y(),g())}return b&&b.addEventListener("click",A),{enable:T,disable:L,toggle:A,isActive(){return f!==null},open:$,close:d,update:P,destroy(){L(),b&&b.removeEventListener("click",A)}}}function kt(e){return ve(e)}export{kt as initHelpLayer};
|
|
160
160
|
//# sourceMappingURL=help-layer.esm.js.map
|