help-layer 1.0.1 → 1.2.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 +141 -8
- package/README.md +150 -8
- package/dist/help-layer.esm.js +7 -5
- package/dist/help-layer.esm.js.map +4 -4
- package/dist/help-layer.iife.js +6 -4
- package/dist/help-layer.iife.js.map +4 -4
- package/dist/types/aria-isolation.d.ts +10 -0
- package/dist/types/floating.d.ts +21 -1
- package/dist/types/markers.d.ts +4 -1
- package/package.json +11 -5
- package/src/aria-isolation.js +67 -0
- package/src/dom-builder.js +12 -3
- package/src/floating.js +89 -1
- package/src/markers.js +14 -2
- package/src/style.js +3 -1
- package/src/toggle.js +19 -1
package/README.ja.md
CHANGED
|
@@ -3,16 +3,91 @@
|
|
|
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
|
|
|
9
|
-
|
|
10
|
-
モード ON 中だけ対象要素の近くに「?」マーカーを出し、クリックで説明ポップアップを表示します。
|
|
11
|
-
元アプリのイベントには一切触れず、透明な遮断レイヤーで操作を吸収するので、既存コードを書き換えずに導入できます。
|
|
10
|
+
🔗 **ライブデモ: <https://y1-effy.github.io/HelpLayer/>**(Vanilla / React / Vue。右上の「解説モード」を ON にして「i」をクリック)
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
既存の Web アプリに、**既存コードを書き換えずに「画面内ヘルプ」を後付けできる**、フレームワーク非依存のライブラリです。
|
|
15
|
+
ユーザーが「解説モード」を ON にしている間だけ、知りたい要素の「?」マーカーをクリックして説明を読めます。通常時の見た目は一切変わりません。
|
|
16
|
+
仕組みは透明な遮断レイヤー — 対象要素の近くにマーカーを出しつつ、元アプリのイベントには一切触れずに操作を吸収します。
|
|
17
|
+
|
|
18
|
+
- 依存は [`@floating-ui/dom`](https://floating-ui.com/) のみ・軽量(プリビルドの IIFE で約 33KB minified、`@floating-ui/dom` 同梱)
|
|
14
19
|
- Shadow DOM 貫通・SPA の動的要素・マーカー同士の重なり回避・画面端でのポップアップ自動調整に対応
|
|
20
|
+
- キーボード操作・スクリーンリーダーに配慮(ポップアップは `role="dialog"`、開くとフォーカスを移し閉じるとマーカーへ復帰。モード中はフォーカスを UI 内に封じ込め、`Esc` で説明を閉じる(開いている説明が無ければ解説モード自体を終了))
|
|
15
21
|
- ON→OFF で追加した DOM・イベント・スタイルを**完全後始末**
|
|
22
|
+
- モダンブラウザ(Chromium / Firefox / WebKit)で動作(e2e を 3 エンジンで検証)
|
|
23
|
+
|
|
24
|
+
## 目次
|
|
25
|
+
|
|
26
|
+
- [なぜ HelpLayer か(既存手段との違い)](#なぜ-helplayer-か既存手段との違い)
|
|
27
|
+
- [こんなときに(導入が刺さるケース)](#こんなときに導入が刺さるケース)
|
|
28
|
+
- [インストール](#インストール)
|
|
29
|
+
- [クイックスタート](#クイックスタート)
|
|
30
|
+
- [自由配置(要素に紐づけない説明)](#自由配置要素に紐づけない説明)
|
|
31
|
+
- [API](#api)
|
|
32
|
+
- [テーマ(CSS カスタムプロパティ)](#テーマcss-カスタムプロパティ)
|
|
33
|
+
- [対応環境(ブラウザ/ランタイム)](#対応環境ブラウザランタイム)
|
|
34
|
+
- [既知の制約](#既知の制約)
|
|
35
|
+
- [アクセシビリティ](#アクセシビリティ)
|
|
36
|
+
- [セキュリティ](#セキュリティ)
|
|
37
|
+
- [開発](#開発)
|
|
38
|
+
|
|
39
|
+
## なぜ HelpLayer か(既存手段との違い)
|
|
40
|
+
|
|
41
|
+
画面に説明を足す手段はいくつもありますが、それぞれ別の前提を抱えています。HelpLayer は
|
|
42
|
+
**「ユーザーが知りたい箇所だけを、その場で自由に選んで確認できる解説モード」** に振り切ることで、
|
|
43
|
+
通常時の見た目も既存コードも一切犠牲にしないことを狙っています。
|
|
44
|
+
|
|
45
|
+
- **プロダクトツアー型(ステップ案内)との違い** … 決められた順路を上から押し付けるのではなく、
|
|
46
|
+
ユーザーが見たい要素を選んでその場で開ける **探索型**。読み終えたいタイミングも順序もユーザーに委ねます。
|
|
47
|
+
- **常設ツールチップとの違い** … 説明を常時表示してUIを煩雑にすることがありません。マーカーは
|
|
48
|
+
**モードON中だけ**出るので、**通常時のデザインは一切変わりません**。
|
|
49
|
+
- **DAP系SaaS(Digital Adoption Platform=定着化支援 SaaS)との違い**
|
|
50
|
+
… 外部基盤・契約・トラッキングを必要とせず、**ランニングコスト0・依存1つ・約33KB の完全ローカル動作**。
|
|
51
|
+
CSP / Trusted Types にも対応するため、持ち込み制約の厳しい環境にも入ります。
|
|
52
|
+
|
|
53
|
+
そのうえで共通の核として、**既存コードを書き換えずに後付け**でき、**フレームワーク非依存**で、
|
|
54
|
+
元アプリのイベントには触れず(透明な遮断レイヤーで操作を吸収)、**ON→OFF で完全に後始末**します。
|
|
55
|
+
|
|
56
|
+
| | プロダクトツアー型 | 常設ツールチップ | DAP系SaaS | **HelpLayer** |
|
|
57
|
+
|---|---|---|---|---|
|
|
58
|
+
| 提示形式 | 線形ステップになりがち | 常時表示になりがち | サービス依存 | **モードON中だけ・任意箇所を探索** |
|
|
59
|
+
| 通常時のUI | 実装次第 | 煩雑になりがち | 実装次第 | **一切変えない** |
|
|
60
|
+
| 導入方法 | 多くは要組み込み | CSS/JS を追記 | スニペット+外部基盤+契約 | **後付け・既存コード非改変** |
|
|
61
|
+
| コスト/運用 | 実装次第 | ローカル | 月額+トラッキング運用 | **ランニング0・依存1つ** |
|
|
62
|
+
|
|
63
|
+
> ※ HelpLayer は DAP の **フル代替ではありません**。アナリティクスやセグメント別配信、複雑なフロー誘導・
|
|
64
|
+
> オンボーディング自動化といった高機能は対象外で、**「画面内に説明を出す」というコア機能だけを最小コストで満たす**ことに
|
|
65
|
+
> 振り切っています。逆に、強い導線を引きたい・利用状況を計測したいといった目的が主なら、DAP やツアーの方が向きます。
|
|
66
|
+
|
|
67
|
+
## こんなときに(導入が刺さるケース)
|
|
68
|
+
|
|
69
|
+
- **DAP/ガイド系 SaaS のコストが見合わず、解約を検討している。でも解約すると画面内ヘルプがゼロに戻る。**
|
|
70
|
+
→ 「画面内に説明を出す」というコア機能だけを、依存1つ・ランニングコスト0 で自前に残せます。乗り換え後の受け皿に。
|
|
71
|
+
- **SaaS を契約する予算感はないが、ヘルプは拡張したい。**
|
|
72
|
+
→ npm か `<script>` 1本で後付け。月額もアカウントも要りません。
|
|
73
|
+
- **オフィスソフトで別途マニュアルを作る・更新するのが重い。しかも作っても読まれない。**
|
|
74
|
+
→ 説明を画面内のその要素に同居させます(`data-help-title`/`data-help-text` か小さな config だけ)。
|
|
75
|
+
別ドキュメントの保守から解放され、UI と説明がズレません。
|
|
76
|
+
- **オンボーディングは欲しいが、強制的なツアーは押し付けがましい**ので避けたい。
|
|
77
|
+
→ ユーザーが見たい箇所を選んでその場で開く探索型なので、操作を中断させません。
|
|
78
|
+
- **外部 SaaS を持ち込めない環境**(厳格な CSP・プライバシー要件・閉域網・トラッキング不可)。
|
|
79
|
+
→ 外部通信なしの完全ローカル動作で要件を満たします。
|
|
80
|
+
- **React / Vue などフレームワークを問わず**、描画ライブラリにも手を入れずに導入したい。
|
|
81
|
+
→ フレームワーク非依存・後付けで、既存コードを書き換えません。
|
|
82
|
+
|
|
83
|
+
> 業務システム・管理画面は最初に刺さりやすい例として挙げていますが、用途はそこに限りません。
|
|
84
|
+
> もちろん **一般的な Web サイト** でも、申込み・問い合わせ・予約などのフォームで「この項目に何を入れるか」を
|
|
85
|
+
> マーカー+ポップアップで補えます。「説明を後付けしたい既存 Web ページ」全般が対象で、別途マニュアルを
|
|
86
|
+
> 用意する運用の軽い代替にもなります。
|
|
87
|
+
|
|
88
|
+
> 💡 **デスクトップアプリにも使えます。** Electron / Tauri などはアプリ画面を WebView(HTML/DOM)で描画して
|
|
89
|
+
> いるため、Web アプリとまったく同じ感覚で HelpLayer を後付けできます。ネイティブ風の画面に「解説モード」を
|
|
90
|
+
> 足したいときの選択肢としても、意外と素直にハマります。
|
|
16
91
|
|
|
17
92
|
## インストール
|
|
18
93
|
|
|
@@ -22,6 +97,8 @@ npm install help-layer
|
|
|
22
97
|
|
|
23
98
|
バンドラを使わず `<script>` 1本で導入したい場合は、プリビルドの IIFE を読み込めばグローバル `HelpLayer` が生えます(後述)。
|
|
24
99
|
|
|
100
|
+
TypeScript の型定義を同梱しています(`package.json` の `types` が `dist/types` を指す)。TS プロジェクトでは追加設定なしで型補完が効きます。
|
|
101
|
+
|
|
25
102
|
## クイックスタート
|
|
26
103
|
|
|
27
104
|
### 1. config オブジェクトで定義する
|
|
@@ -44,7 +121,7 @@ initHelpLayer({
|
|
|
44
121
|
});
|
|
45
122
|
```
|
|
46
123
|
|
|
47
|
-
### 2.
|
|
124
|
+
### 2. マークアップに直接書く(説明の config 定義なしでも可。`config: {}` 自体は必要)
|
|
48
125
|
|
|
49
126
|
説明をマークアップと同居させたい場合は、`data-help-title` / `data-help-text` を要素に直接書くだけで対象になります。
|
|
50
127
|
`config` と併用でき、**同じキーが config にあれば config が優先**されます。
|
|
@@ -63,7 +140,7 @@ CDN から読む場合は、改ざん検知のため **バージョンを固定*
|
|
|
63
140
|
|
|
64
141
|
```html
|
|
65
142
|
<script
|
|
66
|
-
src="https://unpkg.com/help-layer@1.
|
|
143
|
+
src="https://unpkg.com/help-layer@1.2.0/dist/help-layer.iife.js"
|
|
67
144
|
integrity="sha384-……(公開版のハッシュに差し替え)"
|
|
68
145
|
crossorigin="anonymous"></script>
|
|
69
146
|
<script>
|
|
@@ -75,7 +152,7 @@ CDN から読む場合は、改ざん検知のため **バージョンを固定*
|
|
|
75
152
|
```
|
|
76
153
|
|
|
77
154
|
> `integrity` のハッシュは公開した実ファイルから生成します。例:
|
|
78
|
-
> `curl -s https://unpkg.com/help-layer@1.
|
|
155
|
+
> `curl -s https://unpkg.com/help-layer@1.2.0/dist/help-layer.iife.js | openssl dgst -sha384 -binary | openssl base64 -A`
|
|
79
156
|
> (バージョンを固定しないと SRI と不整合になり読み込みが拒否されます。)
|
|
80
157
|
|
|
81
158
|
## 自由配置(要素に紐づけない説明)
|
|
@@ -170,17 +247,73 @@ initHelpLayer({
|
|
|
170
247
|
| `--help-layer-overlay-bg` | `transparent` | 遮断レイヤー(スクリム)背景色。`rgba(0,0,0,0.15)` 等で操作不能状態を可視化 |
|
|
171
248
|
| `--help-layer-overlay-cursor` | `default` | 遮断領域上のカーソル。`not-allowed` / `help` 等 |
|
|
172
249
|
|
|
250
|
+
## 対応環境(ブラウザ/ランタイム)
|
|
251
|
+
|
|
252
|
+
HelpLayer は **現代的な evergreen ブラウザ**(Chrome / Edge・Firefox・Safari)と近年の Electron の
|
|
253
|
+
Chromium を対象とします。**IE11 は非対応であり、構造上対応できません** — ES2020 構文・ES Modules・
|
|
254
|
+
`ResizeObserver`・Shadow DOM・`clip-path` に依存しており、IE はいずれも備えていないためです。これは
|
|
255
|
+
パッケージ形式の調整では埋められません。本当に古いランタイムを対象とする必要がある場合、本ライブラリは
|
|
256
|
+
適合しません。
|
|
257
|
+
|
|
258
|
+
下限を決める要素(新しい2つの API は縮退するため、実質的なハード下限はおよそ **2020 年代の evergreen**):
|
|
259
|
+
|
|
260
|
+
| 機能 | 用途 | 下限の目安 | フォールバック |
|
|
261
|
+
|---|---|---|---|
|
|
262
|
+
| ES2020+ES Modules | ライブラリ全体 | evergreen(〜2020) | なし(古い対象はトランスパイル/バンドルが必要) |
|
|
263
|
+
| `ResizeObserver`(`@floating-ui/dom` 経由) | マーカー/ポップアップの自動配置 | evergreen(〜2020) | なし |
|
|
264
|
+
| open Shadow DOM 貫通 | shadow root 内の対象探索 | evergreen | closed は設計上非対応 |
|
|
265
|
+
| `clip-path: polygon()` | 遮断レイヤーのトグル「穴」 | evergreen(極端に古い Safari は `-webkit-` 必要) | なし |
|
|
266
|
+
| `inert` | ホストを a11y ツリーから除外 | Chrome102 / FF112 / Safari15.5(2023) | 視覚+キーボード遮断のみに縮退 |
|
|
267
|
+
| `Element.checkVisibility()` | 対象が隠れた時にマーカーも隠す | Chrome105 / FF125 / Safari17.4(2024) | 0×0 rect 判定に縮退(`display:none` のみ検出) |
|
|
268
|
+
|
|
269
|
+
### モジュール形式
|
|
270
|
+
|
|
271
|
+
- **ESM(既定)**: `import { initHelpLayer } from 'help-layer'` はビルド済み・テスト済みの
|
|
272
|
+
`dist/help-layer.esm.js` を解決します(`@floating-ui/dom` は external のままで、バンドラ/npm が解決)。
|
|
273
|
+
- **バンドラ無し/`<script>`/CDN/厳格環境**: `@floating-ui/dom` を同梱しグローバル `HelpLayer` を公開する
|
|
274
|
+
自己完結の IIFE ビルドを使用 — [`<script>` だけで使う](#script-だけで使うバンドラなし) 参照。
|
|
275
|
+
- **CommonJS(`require`)**: `require` 入口は提供しません。ブラウザ DOM 専用のため Node の CJS 文脈では
|
|
276
|
+
意味を持ちません。非 ESM のツールチェーンではバンドラ経由で ESM を取り込むか、上記 IIFE を読み込んでください。
|
|
277
|
+
|
|
173
278
|
## 既知の制約
|
|
174
279
|
|
|
175
280
|
- closed な Shadow DOM は JS から到達できないため非対応(open のみ貫通)。
|
|
176
281
|
- マーカーを隅へ重ねるオフセットは既定マーカーサイズ(22px)前提。`--help-layer-marker-size` を大きく変えると
|
|
177
282
|
わずかにズレることがあります。
|
|
283
|
+
- **対象要素の「状態」変化は監視しません**(監視するのは「レイアウト」と「DOM 上の有無」のみ)。ON 中、
|
|
284
|
+
マーカーは対象の位置・サイズ変化に追従し、DOM への追加/削除に応じて mount/unmount され、対象自体が
|
|
285
|
+
隠れる/現れる(`display:none` 等)とマーカーも隠れる/戻ります。一方で、対象の**属性・内容の変化**は
|
|
286
|
+
検知しません — 既存要素への `data-help-id` の後付け/除去、`data-help-title` `data-help-text` の書き換え、
|
|
287
|
+
`disabled` 等の状態切り替えはマーカーに反映されません。これは意図的な制約です。文書全体に属性監視
|
|
288
|
+
(`MutationObserver` の `attributes: true, subtree: true`)を張ると、あらゆるクラス/スタイル変更で発火し、
|
|
289
|
+
ドロップイン型ライブラリとしては性能上のアンチパターンになるためです。これらの状態を変えた場合は、
|
|
290
|
+
`update(config)` で作り直す、モードを OFF→ON する、または対象要素を一度 DOM から外して入れ直してください
|
|
291
|
+
(入れ直しは `childList` 監視に乗ります)。
|
|
292
|
+
|
|
293
|
+
## アクセシビリティ
|
|
294
|
+
|
|
295
|
+
モード ON 中は、ホストアプリを視覚・ポインタ/キーボードだけでなく **支援技術(AT)に対しても意味的に
|
|
296
|
+
無効化**します。ホストを [`inert`](https://developer.mozilla.org/ja/docs/Web/HTML/Global_attributes/inert)
|
|
297
|
+
属性で a11y ツリーから除外するため、スクリーンリーダーの仮想カーソル(ブラウズモード)でも背景の読み上げ・
|
|
298
|
+
操作ができません。到達できるのはヘルプマーカー・ポップアップ・トグルのみです。ポップアップは
|
|
299
|
+
`aria-modal="true"` を持つ `role="dialog"` で、開くとフォーカスが移り、閉じるとマーカーへ戻ります。
|
|
300
|
+
|
|
301
|
+
この隔離の限定事項:
|
|
302
|
+
|
|
303
|
+
- `inert` は inert なサブツリーの子孫で打ち消せないため、隔離は document body の直下レベルで適用します
|
|
304
|
+
(トグルを透過させる clip-path の"穴"と同じ考え方)。トグルは操作可能である必要があるので、
|
|
305
|
+
**トグルを含む body 直下ブランチは到達可能なまま残します** — 漏れを最小化するにはトグルを body 直下
|
|
306
|
+
(または近い位置)に置いてください(直下の `<body>` 子要素なら漏れゼロ)。
|
|
307
|
+
- `inert` は現行ブラウザで広くサポートされます。非対応環境でも視覚/キーボードの遮断は有効で、AT 除外だけが
|
|
308
|
+
グレースフルに縮退します(エラーにはなりません)。
|
|
178
309
|
|
|
179
310
|
## セキュリティ
|
|
180
311
|
|
|
312
|
+
脆弱性の報告方法・サポート方針・セキュリティリリース方針は [SECURITY.ja.md](./SECURITY.ja.md) を参照してください。報告は公開 issue ではなく GitHub の非公開脆弱性報告をご利用ください。
|
|
313
|
+
|
|
181
314
|
- 設計上、`title` / `text` の描画は `textContent` のみで、`innerHTML` / `eval` / `new Function` は**一切使いません**。
|
|
182
315
|
- 外部通信(`fetch` 等)・`localStorage` / `cookie` などのストレージ利用も**ありません**(完全ローカル動作)。
|
|
183
|
-
-
|
|
316
|
+
- 唯一、未信頼データを HTML/DOM ノードとして挿入しうる経路は `render` オプションです。戻り値はサニタイズされないため、
|
|
184
317
|
ユーザー入力を含む場合は呼び出し側で無害化してください(上記「本文に改行を入れる / リンクを置く」参照)。
|
|
185
318
|
- ランタイム依存は `@floating-ui/dom` のみ。CDN 利用時は前述のとおりバージョン固定+SRI を推奨します。
|
|
186
319
|
|
package/README.md
CHANGED
|
@@ -3,16 +3,97 @@
|
|
|
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
|
|
|
10
|
+
🔗 **Live demo: <https://y1-effy.github.io/HelpLayer/>** (Vanilla / React / Vue — turn on "Help mode" top-right, then click an "i")
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+
|
|
9
14
|
A **framework-agnostic "help mode" library you can drop into any existing web app**.
|
|
10
|
-
While the mode is ON, it shows a "?" marker next to each target element; clicking it opens a description popup.
|
|
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.
|
|
11
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.
|
|
12
17
|
|
|
13
|
-
- Only one dependency, [`@floating-ui/dom`](https://floating-ui.com/); lightweight (the prebuilt IIFE is ~
|
|
14
|
-
- Pierces Shadow DOM,
|
|
18
|
+
- Only one dependency, [`@floating-ui/dom`](https://floating-ui.com/); lightweight (the prebuilt IIFE is ~33KB minified, with `@floating-ui/dom` bundled in)
|
|
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
|
|
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)
|
|
15
21
|
- Fully cleans up the DOM, listeners, and styles it added when you turn it OFF
|
|
22
|
+
- Works in modern browsers (Chromium / Firefox / WebKit; e2e is verified across all three engines)
|
|
23
|
+
|
|
24
|
+
## Table of contents
|
|
25
|
+
|
|
26
|
+
- [Why HelpLayer (vs. existing options)](#why-helplayer-vs-existing-options)
|
|
27
|
+
- [When it fits (where adoption pays off)](#when-it-fits-where-adoption-pays-off)
|
|
28
|
+
- [Installation](#installation)
|
|
29
|
+
- [Quick start](#quick-start)
|
|
30
|
+
- [Free placement (descriptions not bound to an element)](#free-placement-descriptions-not-bound-to-an-element)
|
|
31
|
+
- [API](#api)
|
|
32
|
+
- [Theming (CSS custom properties)](#theming-css-custom-properties)
|
|
33
|
+
- [Browser & runtime support](#browser--runtime-support)
|
|
34
|
+
- [Known limitations](#known-limitations)
|
|
35
|
+
- [Accessibility](#accessibility)
|
|
36
|
+
- [Security](#security)
|
|
37
|
+
- [Development](#development)
|
|
38
|
+
|
|
39
|
+
## Why HelpLayer (vs. existing options)
|
|
40
|
+
|
|
41
|
+
There are many ways to add explanations to a screen, but each comes with its own assumptions. HelpLayer
|
|
42
|
+
commits fully to **"a help mode where users freely pick just the spots they want to understand and check
|
|
43
|
+
them on the spot,"** aiming to sacrifice neither the normal look nor your existing code.
|
|
44
|
+
|
|
45
|
+
- **vs. product tours (step-by-step guidance)** … Rather than marching users along a fixed route,
|
|
46
|
+
it's **exploratory**: users pick the element they want and open it right there. The timing and order of
|
|
47
|
+
reading are left entirely to the user.
|
|
48
|
+
- **vs. always-on tooltips** … It never clutters the UI by showing explanations all the time. Markers
|
|
49
|
+
appear **only while the mode is ON**, so **the normal design is completely unchanged**.
|
|
50
|
+
- **vs. DAP SaaS (Digital Adoption Platform)** … No external platform, contract, or tracking required —
|
|
51
|
+
**zero running cost, one dependency, ~33KB, fully local**. It also supports CSP / Trusted Types, so it
|
|
52
|
+
fits environments with strict constraints on what you can bring in.
|
|
53
|
+
|
|
54
|
+
On top of that, they all share a common core: you can **drop it in without rewriting existing code**, it's
|
|
55
|
+
**framework-agnostic**, it never touches the host app's own events (a transparent blocking layer absorbs
|
|
56
|
+
interaction), and it **fully cleans up on ON→OFF**.
|
|
57
|
+
|
|
58
|
+
| | Product tours | Always-on tooltips | DAP SaaS | **HelpLayer** |
|
|
59
|
+
|---|---|---|---|---|
|
|
60
|
+
| Presentation | tends to be linear steps | tends to be always visible | service-dependent | **only while ON · explore any spot** |
|
|
61
|
+
| Normal UI | depends on implementation | tends to get cluttered | depends on implementation | **left entirely unchanged** |
|
|
62
|
+
| Adoption | usually needs integration | add CSS/JS | snippet + external platform + contract | **drop-in · no existing-code changes** |
|
|
63
|
+
| Cost / ops | depends on implementation | local | monthly fee + tracking ops | **zero running cost · one dependency** |
|
|
64
|
+
|
|
65
|
+
> Note: HelpLayer is **not a full replacement for a DAP**. Advanced features like analytics, segmented
|
|
66
|
+
> delivery, complex flow guidance, and onboarding automation are out of scope — it commits to
|
|
67
|
+
> **satisfying just the core "show explanations in-screen" function at minimal cost**. Conversely, if your
|
|
68
|
+
> main goal is to drive strong funnels or measure usage, a DAP or a tour is the better fit.
|
|
69
|
+
|
|
70
|
+
## When it fits (where adoption pays off)
|
|
71
|
+
|
|
72
|
+
- **A DAP / guide SaaS isn't worth the cost and you're considering canceling — but canceling drops your
|
|
73
|
+
in-screen help back to zero.**
|
|
74
|
+
→ Keep just the core "show explanations in-screen" function in-house, with one dependency and zero
|
|
75
|
+
running cost. It gives you a place to land after switching away.
|
|
76
|
+
- **You don't have the budget to contract a SaaS, but you want to expand your help.**
|
|
77
|
+
→ Drop it in with npm or a single `<script>`. No monthly fee, no account.
|
|
78
|
+
- **Maintaining a separate manual in an office suite is a chore — and nobody reads it even when you do.**
|
|
79
|
+
→ Co-locate the explanation with the very element on screen (`data-help-title` / `data-help-text` or a
|
|
80
|
+
small config). You're freed from maintaining a separate doc, and the UI and its explanation never drift apart.
|
|
81
|
+
- **You want onboarding, but a forced tour feels pushy** and you'd rather avoid it.
|
|
82
|
+
→ It's exploratory — users pick what they want and open it on the spot — so it never interrupts their work.
|
|
83
|
+
- **Environments where you can't bring in an external SaaS** (strict CSP, privacy requirements, closed
|
|
84
|
+
networks, no tracking allowed).
|
|
85
|
+
→ It meets those requirements with fully local operation and no external communication.
|
|
86
|
+
- **Regardless of framework (React / Vue, etc.)**, you want to adopt it without touching your rendering library.
|
|
87
|
+
→ Framework-agnostic and drop-in; it doesn't rewrite your existing code.
|
|
88
|
+
|
|
89
|
+
> Business systems and admin screens are the easiest fit, but the use isn't limited to
|
|
90
|
+
> them. On **ordinary websites** too, you can supplement "what to enter in this field" on signup, contact,
|
|
91
|
+
> or reservation forms with a marker + popup. Any "existing web page you want to add explanations to" is in
|
|
92
|
+
> scope, and it makes a lightweight alternative to maintaining a separate manual.
|
|
93
|
+
|
|
94
|
+
> 💡 **It works for desktop apps, too.** Electron / Tauri and the like render their app screens with a
|
|
95
|
+
> WebView (HTML/DOM), so you can drop HelpLayer in exactly as you would in a web app. It's a surprisingly
|
|
96
|
+
> natural option when you want to add a "help mode" to a native-feeling screen.
|
|
16
97
|
|
|
17
98
|
## Installation
|
|
18
99
|
|
|
@@ -20,7 +101,9 @@ It never touches the host app's own event listeners — a transparent blocking l
|
|
|
20
101
|
npm install help-layer
|
|
21
102
|
```
|
|
22
103
|
|
|
23
|
-
If you'd rather drop it in with a single `<script>` and no bundler, load the prebuilt IIFE
|
|
104
|
+
If you'd rather drop it in with a single `<script>` and no bundler, load the prebuilt IIFE, which exposes a global `HelpLayer` (see below).
|
|
105
|
+
|
|
106
|
+
TypeScript type definitions are bundled (`package.json`'s `types` points to `dist/types`), so type completion works with no extra setup in TS projects.
|
|
24
107
|
|
|
25
108
|
## Quick start
|
|
26
109
|
|
|
@@ -44,7 +127,7 @@ initHelpLayer({
|
|
|
44
127
|
});
|
|
45
128
|
```
|
|
46
129
|
|
|
47
|
-
### 2. Write it inline in your markup (no config needed)
|
|
130
|
+
### 2. Write it inline in your markup (no per-entry config needed; `config: {}` still required)
|
|
48
131
|
|
|
49
132
|
If you'd rather keep descriptions next to your markup, just add `data-help-title` / `data-help-text` to an element and it becomes a target.
|
|
50
133
|
This can be combined with `config`, and **if the same key exists in `config`, the config wins**.
|
|
@@ -63,7 +146,7 @@ When loading from a CDN, we recommend **pinning the version** and adding **SRI (
|
|
|
63
146
|
|
|
64
147
|
```html
|
|
65
148
|
<script
|
|
66
|
-
src="https://unpkg.com/help-layer@1.
|
|
149
|
+
src="https://unpkg.com/help-layer@1.2.0/dist/help-layer.iife.js"
|
|
67
150
|
integrity="sha384-……(replace with the published file's hash)"
|
|
68
151
|
crossorigin="anonymous"></script>
|
|
69
152
|
<script>
|
|
@@ -75,7 +158,7 @@ When loading from a CDN, we recommend **pinning the version** and adding **SRI (
|
|
|
75
158
|
```
|
|
76
159
|
|
|
77
160
|
> Generate the `integrity` hash from the actually published file, e.g.:
|
|
78
|
-
> `curl -s https://unpkg.com/help-layer@1.
|
|
161
|
+
> `curl -s https://unpkg.com/help-layer@1.2.0/dist/help-layer.iife.js | openssl dgst -sha384 -binary | openssl base64 -A`
|
|
79
162
|
> (If you don't pin the version, the SRI will mismatch and the browser will refuse to load it.)
|
|
80
163
|
|
|
81
164
|
## Free placement (descriptions not bound to an element)
|
|
@@ -170,17 +253,76 @@ You can change the look just by overriding the following variables in your host
|
|
|
170
253
|
| `--help-layer-overlay-bg` | `transparent` | blocking-layer (scrim) background; e.g. `rgba(0,0,0,0.15)` to signal the host is inactive |
|
|
171
254
|
| `--help-layer-overlay-cursor` | `default` | cursor over the blocked area; e.g. `not-allowed` / `help` |
|
|
172
255
|
|
|
256
|
+
## Browser & runtime support
|
|
257
|
+
|
|
258
|
+
HelpLayer targets **modern evergreen browsers** (Chrome / Edge, Firefox, Safari) and the Chromium in
|
|
259
|
+
recent Electron. **Internet Explorer 11 is not supported and cannot be** — the library relies on ES2020
|
|
260
|
+
syntax, ES modules, `ResizeObserver`, Shadow DOM, and `clip-path`, none of which IE provides. Packaging
|
|
261
|
+
changes can't bridge this; if you must support genuinely old runtimes, this library is not the right fit.
|
|
262
|
+
|
|
263
|
+
What sets the minimum (the two newest APIs degrade gracefully, so the practical hard floor is roughly
|
|
264
|
+
**2020-era evergreen**):
|
|
265
|
+
|
|
266
|
+
| Feature | Used for | Minimum | Fallback |
|
|
267
|
+
|---|---|---|---|
|
|
268
|
+
| ES2020 + ES modules | the whole library | evergreen (~2020) | none — transpile/bundle for older targets |
|
|
269
|
+
| `ResizeObserver` (via `@floating-ui/dom`) | marker/popup auto-positioning | evergreen (~2020) | none |
|
|
270
|
+
| Open Shadow DOM piercing | finding targets inside shadow roots | evergreen | closed shadow roots are unsupported by design |
|
|
271
|
+
| `clip-path: polygon()` | the blocking layer's toggle "hole" | evergreen (very old Safari needs `-webkit-`) | none |
|
|
272
|
+
| `inert` | removing the host from the a11y tree | Chrome 102 / FF 112 / Safari 15.5 (2023) | degrades to visual + keyboard blocking only |
|
|
273
|
+
| `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) |
|
|
274
|
+
|
|
275
|
+
### Module formats
|
|
276
|
+
|
|
277
|
+
- **ESM (default).** `import { initHelpLayer } from 'help-layer'` resolves to the prebuilt, tested
|
|
278
|
+
`dist/help-layer.esm.js` (with `@floating-ui/dom` left external for your bundler/npm to resolve).
|
|
279
|
+
- **No bundler / `<script>` / CDN / strict environments.** Use the self-contained IIFE build, which
|
|
280
|
+
bundles `@floating-ui/dom` and exposes a global `HelpLayer` — see [Use it with just a `<script>`](#use-it-with-just-a-script-no-bundler).
|
|
281
|
+
- **CommonJS (`require`).** No `require` entry is provided: this is a browser-only DOM library, so a Node
|
|
282
|
+
CJS context can't use it meaningfully. In non-ESM toolchains, consume the ESM build via your bundler,
|
|
283
|
+
or load the IIFE build above.
|
|
284
|
+
|
|
173
285
|
## Known limitations
|
|
174
286
|
|
|
175
287
|
- Closed Shadow DOM is unreachable from JS, so it is unsupported (only open shadow roots are pierced).
|
|
176
288
|
- The offset that overlaps the marker onto a corner assumes the default marker size (22px). Changing
|
|
177
289
|
`--help-layer-marker-size` significantly may cause a slight drift.
|
|
290
|
+
- **Target *state* changes are not watched** (only *layout* and *presence* are). While ON, a marker
|
|
291
|
+
follows its target's position/size changes and is added/removed as the target enters/leaves the DOM,
|
|
292
|
+
and it hides/reshows when the target itself is hidden/shown (e.g. `display:none`). However, changes
|
|
293
|
+
to a target's **attributes or content** are *not* detected: adding/removing the `data-help-id`
|
|
294
|
+
attribute on an existing element, rewriting `data-help-title` / `data-help-text`, or toggling state
|
|
295
|
+
like `disabled` won't update the markers. This is intentional — watching every attribute mutation
|
|
296
|
+
across the whole document (`MutationObserver` with `attributes: true, subtree: true`) fires on every
|
|
297
|
+
class/style change and is a performance footgun for a drop-in library. If you change such state,
|
|
298
|
+
rebuild via `update(config)`, toggle the mode OFF→ON, or re-insert the target element into the DOM
|
|
299
|
+
(re-insertion is picked up by the `childList` observation).
|
|
300
|
+
|
|
301
|
+
## Accessibility
|
|
302
|
+
|
|
303
|
+
While the mode is ON, the host app is blocked not only visually and for pointer/keyboard input, but
|
|
304
|
+
also **semantically for assistive technology**: the host is removed from the accessibility tree with
|
|
305
|
+
the [`inert`](https://developer.mozilla.org/docs/Web/HTML/Global_attributes/inert) attribute, so a
|
|
306
|
+
screen reader's virtual cursor (browse mode) can't read or activate background content. Only the help
|
|
307
|
+
markers, the popup, and your toggle stay reachable. The popup is a `role="dialog"` with
|
|
308
|
+
`aria-modal="true"`, and focus moves into it on open and returns to the marker on close.
|
|
309
|
+
|
|
310
|
+
Bounded limitations of this isolation:
|
|
311
|
+
|
|
312
|
+
- `inert` can't be cancelled on a descendant of an inert subtree, so isolation is applied at the
|
|
313
|
+
document-body top level (like the clip-path "hole" that lets the toggle show through). The toggle
|
|
314
|
+
must stay operable, so **the top-level branch containing your toggle is left reachable** — keep the
|
|
315
|
+
toggle at/near the body level to minimize what leaks (none if it's a direct `<body>` child).
|
|
316
|
+
- `inert` is broadly supported in current browsers; on engines without it, the visual/keyboard
|
|
317
|
+
blocking still applies, but AT exclusion degrades gracefully (no error).
|
|
178
318
|
|
|
179
319
|
## Security
|
|
180
320
|
|
|
321
|
+
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.
|
|
322
|
+
|
|
181
323
|
- By design, `title` / `text` are rendered with `textContent` only; `innerHTML` / `eval` / `new Function` are **never used**.
|
|
182
324
|
- There is **no external communication** (`fetch`, etc.) and **no storage use** (`localStorage` / `cookie`) — it runs fully locally.
|
|
183
|
-
- The only path through which untrusted data
|
|
325
|
+
- 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
|
|
184
326
|
neutralize it on the caller side if it contains user input (see "Line breaks & links in the body" above).
|
|
185
327
|
- The only runtime dependency is `@floating-ui/dom`. When using a CDN, pin the version and add SRI as noted above.
|
|
186
328
|
|
package/dist/help-layer.esm.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
function B(e,{toggleEl:t,isLibraryNode:o}){let r=new Set;function n(a){if(a.nodeType!==1)return;let c=a;o(c)||t&&(c===t||c.contains(t))||c.hasAttribute("inert")||(c.toggleAttribute("inert",!0),r.add(c))}for(let a of[...document.body.children])n(a);let i=new MutationObserver(a=>{for(let c of a)c.addedNodes.forEach(n)});i.observe(document.body,{childList:!0}),e.track(()=>{i.disconnect(),r.forEach(a=>a.removeAttribute("inert")),r.clear()})}var ve=0;function F(){let e=document.createElement("div");return e.className="help-layer-blocking-layer",e}function z(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 H(){let e=`help-layer-popup-title-${ve++}`,t=document.createElement("div");t.className="help-layer-popup",t.setAttribute("role","dialog"),t.setAttribute("aria-modal","true"),t.setAttribute("aria-labelledby",e),t.tabIndex=-1;let o=document.createElement("div");o.className="help-layer-popup__title",o.id=e;let r=document.createElement("div");r.className="help-layer-popup__text";let n=document.createElement("button");return n.type="button",n.className="help-layer-popup__close",n.textContent="\xD7",n.setAttribute("aria-label","Close"),t.append(o,r,n),{root:t,titleEl:o,textEl:r,closeEl:n}}import{autoUpdate as R,computePosition as q,flip as ke,offset as V,shift as we}from"@floating-ui/dom";function K(e,t){let o=e.width||0,r=e.height||0,n=e.left-t.x,i=e.top-t.y;return{x:n,y:i,left:n,top:i,right:n+o,bottom:i+r,width:o,height:r}}function Z(e){return{contextElement:document.body,getBoundingClientRect(){return K(e(),{x:window.scrollX,y:window.scrollY})}}}function G(e,t,o){e.style.left=`${t}px`,e.style.top=`${o}px`}function W(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 Ee(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}var O=11;function Le(e){let t=e.endsWith("-start");return{mainAxis:-O,crossAxis:t?O:-O}}function Y(e,t,o,r="top-end",n){let i=W(e)?"fixed":"absolute";i==="fixed"&&t.style.setProperty("position","fixed","important");let a=!1;return R(e,t,()=>{if(Ee(e)){t.style.setProperty("display","none","important"),!a&&n&&n(),a=!0,o&&o();return}a=!1,t.style.removeProperty("display"),q(e,t,{placement:r,strategy:i,middleware:[V(Le(r))]}).then(({x:d,y})=>{G(t,d,y),o&&o()}).catch(()=>{})},{animationFrame:!0})}function U(e,t,o="bottom-start"){let r=W(e)?"fixed":"absolute";t.style.setProperty("position",r,"important");let n=()=>{q(e,t,{placement:o,strategy:r,middleware:[V(8),ke({padding:8}),we({padding:8})]}).then(({x:a,y:c})=>{G(t,a,c)}).catch(()=>{})},i=R(e,t,n,{animationFrame:!0});return{update:n,cleanup:i}}function X(e,t,o){return R(e,t,o)}function Ce(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
|
-
${t}px ${
|
|
4
|
-
)`}function
|
|
3
|
+
${t}px ${o}px, ${t}px ${n}px, ${r}px ${n}px, ${r}px ${o}px, ${t}px ${o}px
|
|
4
|
+
)`}function J(e,{toggleEl:t,onBackgroundClick:o,isLibraryElement:r,onEscape:n}){let i=F();if(document.body.appendChild(i),e.track(()=>i.remove()),t){let g=X(t,i,()=>{i.style.clipPath=Ce(t.getBoundingClientRect())});e.track(g)}o&&(i.addEventListener("click",o),e.track(()=>i.removeEventListener("click",o)));let a=document.activeElement;a instanceof HTMLElement&&a!==document.body&&a!==t&&a.blur();let c=f=>{r(f.target)||(f.stopPropagation(),t?t.focus({preventScroll:!0}):f.target instanceof HTMLElement&&f.target.blur())};document.addEventListener("focusin",c,!0),e.track(()=>document.removeEventListener("focusin",c,!0));let d=f=>{r(f.target)||(f.stopPropagation(),f.preventDefault())},y=f=>{if(f.key==="Escape"){f.stopPropagation(),f.preventDefault();return}d(f)},m=f=>{if(f.key==="Escape"){f.stopPropagation(),f.preventDefault(),n&&n();return}d(f)};return document.addEventListener("keydown",m,!0),document.addEventListener("keyup",y,!0),document.addEventListener("keypress",y,!0),e.track(()=>{document.removeEventListener("keydown",m,!0),document.removeEventListener("keyup",y,!0),document.removeEventListener("keypress",y,!0)}),i}function T(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function Q(e){return T(e)&&Number.isFinite(e.top)&&Number.isFinite(e.left)}function M(e){if(!T(e))throw new Error("helpConfig must be a plain object");for(let[t,o]of Object.entries(e)){if(!T(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&&!Q(o.position))throw new Error(`helpConfig["${t}"].position must be { top: finite number, left: finite number }`)}}function ee(e){return Object.entries(e).map(([t,o])=>Q(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 te(e,t={}){let o=t.minDistance??26,r=t.iterations??6,n=e.map(i=>({x:i.x,y:i.y}));for(let i=0;i<r;i++){let a=!1;for(let c=0;c<n.length;c++)for(let d=c+1;d<n.length;d++){let y=n[c],m=n[d],f=m.x-y.x,g=m.y-y.y,b=Math.hypot(f,g);if(b>=o)continue;b===0&&(f=1,g=0,b=1);let p=(o-b)/2,l=f/b,u=g/b;y.x-=l*p,y.y-=u*p,m.x+=l*p,m.y+=u*p,a=!0}if(!a)break}return n.map((i,a)=>({dx:i.x-e[a].x,dy:i.y-e[a].y}))}var oe="help-layer-target-highlight";function Ae(e){return e.kind==="free"?Z(()=>({top:e.position.top,left:e.position.left,width:0,height:0})):e.target}function ne(e,{onMarkerClick:t,onOverlapResolved:o,onMarkerHidden:r,markerLabel:n="?",markerPlacement:i="top-end"}){let a=new Map,c=null,d=!1;function y(){c=null;let p=[...a.values()].filter(s=>s.el.style.display!=="none");if(p.length<=1){let s=p.length===1?p[0].el:null;s&&s.style.transform&&(s.style.transform="",o&&o());return}p.forEach(s=>{s.el.style.transform=""});let l=p.map(s=>{let h=s.el.getBoundingClientRect();return{x:h.left+h.width/2,y:h.top+h.height/2}}),u=te(l);p.forEach((s,h)=>{let{dx:w,dy:v}=u[h];s.el.style.transform=w||v?`translate(${w}px, ${v}px)`:""}),o&&o()}function m(){c!==null||d||(c=requestAnimationFrame(y))}function f(p){if(a.has(p.id))return;let l=z(p.title,n);document.body.appendChild(l);let u=()=>t(p,l);l.addEventListener("click",u);let s=Y(Ae(p),l,m,i,()=>r&&r(p)),h=p.kind==="element"?p.target:null,w=()=>h&&h.classList.add(oe),v=()=>h&&h.classList.remove(oe);h&&(l.addEventListener("mouseenter",w),l.addEventListener("mouseleave",v),l.addEventListener("focus",w),l.addEventListener("blur",v));let C=!1,A=()=>{C||(C=!0,s(),l.removeEventListener("click",u),h&&(l.removeEventListener("mouseenter",w),l.removeEventListener("mouseleave",v),l.removeEventListener("focus",w),l.removeEventListener("blur",v),v()),l.remove(),a.delete(p.id),m())};a.set(p.id,{record:p,el:l,cleanup:A})}function g(p){let l=a.get(p);l&&l.cleanup()}function b(p){p.forEach(f)}return e.track(()=>{d=!0,c!==null&&(cancelAnimationFrame(c),c=null),[...a.values()].forEach(p=>p.cleanup())}),{mount:f,unmount:g,mountAll:b,has(p){return a.has(p)},findByKey(p){for(let l of a.values())if(l.record.key===p)return l;return null}}}function k(e,t,...o){if(t)try{return t(...o)}catch(r){console.error(`[help-layer] ${e} threw:`,r);return}}var Se=1;function P(e,t,o,r){typeof e.querySelectorAll=="function"&&e.querySelectorAll("*").forEach(n=>{o&&n.matches(t)&&o(n),n.shadowRoot&&(r&&r(n.shadowRoot),P(n.shadowRoot,t,o,r))})}function ie(e,t){let o=[];return P(e,t,r=>o.push(r)),o}function Te(e){let t=[];return P(e,"*",null,o=>t.push(o)),t}function re(e,t){let o=[],r=[];if(e.nodeType!==Se)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),P(n.shadowRoot,t,i=>o.push(i),i=>r.push(i))),P(n,t,i=>o.push(i),i=>r.push(i)),{matches:o,shadowRoots:r}}function ae({root:e=document,selector:t,onAdded:o,onRemoved:r}){let n=new Set,i=d=>{for(let y of d)y.addedNodes.forEach(m=>{let{matches:f,shadowRoots:g}=re(m,t);f.forEach(b=>k("observer onAdded",o,b)),g.forEach(c)}),y.removedNodes.forEach(m=>{re(m,t).matches.forEach(f=>k("observer onRemoved",r,f))})},a=new MutationObserver(i);function c(d){n.has(d)||(n.add(d),a.observe(d,{childList:!0,subtree:!0}))}return c(e),Te(e).forEach(c),{disconnect(){a.disconnect(),n.clear()}}}var $="data-help-title",N="data-help-text";function D(e="data-help-id"){return`[${e}], [${$}]`}function I(e){let t=new Map;for(let o of e)o.kind==="element"&&t.set(o.key,o);return t}function le(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 j(e,t,o="data-help-id"){let r=e.getAttribute(o),n=r!=null?t.get(r):void 0,i=n?n.title:e.getAttribute($),a=n?n.text:e.getAttribute(N);return!i||!a?null:{id:e,kind:"element",key:r,title:i,text:a,target:e}}function se(e,t=document,{silent:o=!1,attribute:r="data-help-id"}={}){let n=I(e),i=[];return ie(t,D(r)).forEach(a=>{let c=j(a,n,r);if(!c){if(!o){let d=a.getAttribute(r);console.warn(d!=null?`[help-layer] element with ${r}="${d}" has no matching helpConfig entry or inline ${$}/${N}`:`[help-layer] element needs both ${$} and ${N} (or a ${r} matching helpConfig)`)}return}i.push(c)}),i}function pe(e,{onClose:t,render:o,popupPlacement:r="bottom-start"}={}){let{root:n,titleEl:i,textEl:a,closeEl:c}=H();n.style.setProperty("display","none","important"),document.body.appendChild(n),c.addEventListener("click",()=>l());let d=null,y=null,m=null;function f(){m&&(m.cleanup(),m=null)}function g(u,s){i.textContent=u.title;let h=k("render",o,u);a.textContent="",h?a.appendChild(h):a.textContent=u.text,n.style.setProperty("display","block","important"),d=u.id,y=s,f(),m=U(s,n,r),n.focus({preventScroll:!0})}function b(){m&&m.update()}function p(){let u=d!==null;f(),d=null,y=null,n.style.setProperty("display","none","important"),u&&k("onClose",t)}function l(u){let s=u??y;p(),s&&s.isConnected&&typeof s.focus=="function"&&s.focus({preventScroll:!0})}return e.track(()=>{p(),n.remove()}),{root:n,isOpen(u){return d===u},getOpenId(){return d},open:g,close:l,reposition:b}}function ce(){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 Pe="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. */
|
|
@@ -26,7 +26,9 @@ var z="help-layer-popup-title";function B(){let e=document.createElement("div");
|
|
|
26
26
|
border: none;
|
|
27
27
|
/* Structural properties are !important so a host's broad rules (e.g. button { display:none }) can't
|
|
28
28
|
hide or distort the marker. top/left stay non-important because place() writes them inline per
|
|
29
|
-
frame; !important there would override that and pin the marker to 0,0. Theme stays var()-driven.
|
|
29
|
+
frame; !important there would override that and pin the marker to 0,0. Theme stays var()-driven.
|
|
30
|
+
Note: for targets in a position:fixed subtree, floating.js overrides this with an inline
|
|
31
|
+
position:fixed !important (inline important beats this rule) so the marker doesn't jitter. */
|
|
30
32
|
position: absolute !important;
|
|
31
33
|
display: block !important;
|
|
32
34
|
visibility: visible !important;
|
|
@@ -154,5 +156,5 @@ var z="help-layer-popup-title";function B(){let e=document.createElement("div");
|
|
|
154
156
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.55);
|
|
155
157
|
}
|
|
156
158
|
}
|
|
157
|
-
`;function
|
|
159
|
+
`;function ue(e){let t=document.createElement("style");return t.setAttribute(Pe,""),e&&t.setAttribute("nonce",e),t.textContent=$e,document.head.appendChild(t),t}function fe(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 de(e){if(!T(e))throw new Error("help-layer: initHelpLayer requires an options object");let{config:t,toggle:o,onEnable:r,onDisable:n,onOpen:i,onClose:a,silent:c=!1,attribute:d="data-help-id",render:y,markerLabel:m="?",markerPlacement:f="top-end",popupPlacement:g="bottom-start",nonce:b}=e,p=t;M(p);let l=o!=null?_e(o):null,u=null,s=null,h=null;function w(){if(u)return;u=ce(),l&&u.track(()=>{l.isConnected&&typeof l.focus=="function"&&l.focus({preventScroll:!0})});let L=ue(b);u.track(()=>fe(L));let E=ee(p),xe=I(E);s=pe(u,{onClose:a,render:y,popupPlacement:g}),h=ne(u,{markerLabel:m,markerPlacement:f,onMarkerClick:(x,S)=>{if(s.isOpen(x.id)){s.close();return}s.open(x,S),k("onOpen",i,x)},onOverlapResolved:()=>s.reposition(),onMarkerHidden:x=>{s.isOpen(x.id)&&s.close(l??void 0)}}),h.mountAll(le(E)),h.mountAll(se(E,document,{silent:c,attribute:d}));let be=ae({selector:D(d),onAdded:x=>{let S=j(x,xe,d);S&&!h.has(S.id)&&h.mount(S)},onRemoved:x=>{s.isOpen(x)&&s.close(l??void 0),h.unmount(x)}});u.track(()=>be.disconnect());let ge=J(u,{toggleEl:l,onBackgroundClick:()=>s.close(),isLibraryElement:x=>!!x&&((l?l.contains(x):!1)||s.root.contains(x)||typeof x.closest=="function"&&!!x.closest(".help-layer-marker")),onEscape:()=>{s.getOpenId()!==null?s.close():A()}});B(u,{toggleEl:l,isLibraryNode:x=>x===ge||x===s.root||!!x.classList&&x.classList.contains("help-layer-marker")})}function v(){u&&(u.teardownAll(),u=null,s=null,h=null)}function C(){u||(w(),k("onEnable",r))}function A(){u&&(v(),k("onDisable",n))}function _(){u?A():C()}function he(L){if(u||C(),!h||!s)return;let E=h.findByKey(L);if(!E){c||console.warn(`[help-layer] open(): no help marker for key "${L}"`);return}s.open(E.record,E.el),k("onOpen",i,E.record)}function me(){s&&s.close()}function ye(L){M(L),p=L,u&&(v(),w())}return l&&l.addEventListener("click",_),{enable:C,disable:A,toggle:_,isActive(){return u!==null},open:he,close:me,update:ye,destroy(){A(),l&&l.removeEventListener("click",_)}}}function bt(e){return de(e)}export{bt as initHelpLayer};
|
|
158
160
|
//# sourceMappingURL=help-layer.esm.js.map
|