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

|
|
12
|
+
|
|
13
|
+
既存の Web アプリに、**既存コードを書き換えずに「画面内ヘルプ」を後付けできる**、フレームワーク非依存のライブラリです。
|
|
14
|
+
ユーザーが「解説モード」を ON にしている間だけ、知りたい要素の「?」マーカーをクリックして説明を読めます。通常時の見た目は一切変わりません。
|
|
15
|
+
仕組みは透明な遮断レイヤー — 対象要素の近くにマーカーを出しつつ、元アプリのイベントには一切触れずに操作を吸収します。
|
|
16
|
+
|
|
17
|
+
- 依存は [`@floating-ui/dom`](https://floating-ui.com/) のみ・軽量(プリビルドの IIFE で約 33KB minified、`@floating-ui/dom` 同梱)
|
|
14
18
|
- Shadow DOM 貫通・SPA の動的要素・マーカー同士の重なり回避・画面端でのポップアップ自動調整に対応
|
|
19
|
+
- キーボード操作・スクリーンリーダーに配慮(ポップアップは `role="dialog"`、開くとフォーカスを移し閉じるとマーカーへ復帰。モード中はフォーカスを UI 内に封じ込め、`Esc` で閉じる)
|
|
15
20
|
- ON→OFF で追加した DOM・イベント・スタイルを**完全後始末**
|
|
21
|
+
- モダンブラウザ(Chromium / Firefox / WebKit)で動作(e2e を 3 エンジンで検証)
|
|
22
|
+
|
|
23
|
+
## 目次
|
|
24
|
+
|
|
25
|
+
- [なぜ HelpLayer か(既存手段との違い)](#なぜ-helplayer-か既存手段との違い)
|
|
26
|
+
- [こんなときに(導入が刺さるケース)](#こんなときに導入が刺さるケース)
|
|
27
|
+
- [インストール](#インストール)
|
|
28
|
+
- [クイックスタート](#クイックスタート)
|
|
29
|
+
- [自由配置(要素に紐づけない説明)](#自由配置要素に紐づけない説明)
|
|
30
|
+
- [API](#api)
|
|
31
|
+
- [テーマ(CSS カスタムプロパティ)](#テーマcss-カスタムプロパティ)
|
|
32
|
+
- [既知の制約](#既知の制約)
|
|
33
|
+
- [セキュリティ](#セキュリティ)
|
|
34
|
+
- [開発](#開発)
|
|
35
|
+
|
|
36
|
+
## なぜ HelpLayer か(既存手段との違い)
|
|
37
|
+
|
|
38
|
+
画面に説明を足す手段はいくつもありますが、それぞれ別の前提を抱えています。HelpLayer は
|
|
39
|
+
**「ユーザーが知りたい箇所だけを、その場で自由に選んで確認できる解説モード」** に振り切ることで、
|
|
40
|
+
通常時の見た目も既存コードも一切犠牲にしないことを狙っています。
|
|
41
|
+
|
|
42
|
+
- **プロダクトツアー型(ステップ案内)との違い** … 決められた順路を上から押し付けるのではなく、
|
|
43
|
+
ユーザーが見たい要素を選んでその場で開ける **探索型**。読み終えたいタイミングも順序もユーザーに委ねます。
|
|
44
|
+
- **常設ツールチップとの違い** … 説明を常時表示してUIを煩雑にすることがありません。マーカーは
|
|
45
|
+
**モードON中だけ**出るので、**通常時のデザインは一切変わりません**。
|
|
46
|
+
- **DAP系SaaS(Digital Adoption Platform=定着化支援 SaaS)との違い**
|
|
47
|
+
… 外部基盤・契約・トラッキングを必要とせず、**ランニングコスト0・依存1つ・約33KB の完全ローカル動作**。
|
|
48
|
+
CSP / Trusted Types にも対応するため、持ち込み制約の厳しい環境にも入ります。
|
|
49
|
+
|
|
50
|
+
そのうえで共通の核として、**既存コードを書き換えずに後付け**でき、**フレームワーク非依存**で、
|
|
51
|
+
元アプリのイベントには触れず(透明な遮断レイヤーで操作を吸収)、**ON→OFF で完全に後始末**します。
|
|
52
|
+
|
|
53
|
+
| | プロダクトツアー型 | 常設ツールチップ | DAP系SaaS | **HelpLayer** |
|
|
54
|
+
|---|---|---|---|---|
|
|
55
|
+
| 提示形式 | 線形ステップになりがち | 常時表示になりがち | サービス依存 | **モードON中だけ・任意箇所を探索** |
|
|
56
|
+
| 通常時のUI | 実装次第 | 煩雑になりがち | 実装次第 | **一切変えない** |
|
|
57
|
+
| 導入方法 | 多くは要組み込み | CSS/JS を追記 | スニペット+外部基盤+契約 | **後付け・既存コード非改変** |
|
|
58
|
+
| コスト/運用 | 実装次第 | ローカル | 月額+トラッキング運用 | **ランニング0・依存1つ** |
|
|
59
|
+
|
|
60
|
+
> ※ HelpLayer は DAP の **フル代替ではありません**。アナリティクスやセグメント別配信、複雑なフロー誘導・
|
|
61
|
+
> オンボーディング自動化といった高機能は対象外で、**「画面内に説明を出す」というコア機能だけを最小コストで満たす**ことに
|
|
62
|
+
> 振り切っています。逆に、強い導線を引きたい・利用状況を計測したいといった目的が主なら、DAP やツアーの方が向きます。
|
|
63
|
+
|
|
64
|
+
## こんなときに(導入が刺さるケース)
|
|
65
|
+
|
|
66
|
+
- **DAP/ガイド系 SaaS のコストが見合わず、解約を検討している。でも解約すると画面内ヘルプがゼロに戻る。**
|
|
67
|
+
→ 「画面内に説明を出す」というコア機能だけを、依存1つ・ランニングコスト0 で自前に残せます。乗り換え後の受け皿に。
|
|
68
|
+
- **SaaS を契約する予算感はないが、ヘルプは拡張したい。**
|
|
69
|
+
→ npm か `<script>` 1本で後付け。月額もアカウントも要りません。
|
|
70
|
+
- **オフィスソフトで別途マニュアルを作る・更新するのが重い。しかも作っても読まれない。**
|
|
71
|
+
→ 説明を画面内のその要素に同居させます(`data-help-title`/`data-help-text` か小さな config だけ)。
|
|
72
|
+
別ドキュメントの保守から解放され、UI と説明がズレません。
|
|
73
|
+
- **オンボーディングは欲しいが、強制的なツアーは押し付けがましい**ので避けたい。
|
|
74
|
+
→ ユーザーが見たい箇所を選んでその場で開く探索型なので、操作を中断させません。
|
|
75
|
+
- **外部 SaaS を持ち込めない環境**(厳格な CSP・プライバシー要件・閉域網・トラッキング不可)。
|
|
76
|
+
→ 外部通信なしの完全ローカル動作で要件を満たします。
|
|
77
|
+
- **React / Vue などフレームワークを問わず**、描画ライブラリにも手を入れずに導入したい。
|
|
78
|
+
→ フレームワーク非依存・後付けで、既存コードを書き換えません。
|
|
79
|
+
|
|
80
|
+
> 業務システム・管理画面は最初に刺さりやすい例として挙げていますが、用途はそこに限りません。
|
|
81
|
+
> もちろん **一般的な Web サイト** でも、申込み・問い合わせ・予約などのフォームで「この項目に何を入れるか」を
|
|
82
|
+
> マーカー+ポップアップで補えます。「説明を後付けしたい既存 Web ページ」全般が対象で、別途マニュアルを
|
|
83
|
+
> 用意する運用の軽い代替にもなります。
|
|
84
|
+
|
|
85
|
+
> 💡 **デスクトップアプリにも使えます。** Electron / Tauri などはアプリ画面を WebView(HTML/DOM)で描画して
|
|
86
|
+
> いるため、Web アプリとまったく同じ感覚で HelpLayer を後付けできます。ネイティブ風の画面に「解説モード」を
|
|
87
|
+
> 足したいときの選択肢としても、意外と素直にハマります。
|
|
16
88
|
|
|
17
89
|
## インストール
|
|
18
90
|
|
|
@@ -22,6 +94,8 @@ npm install help-layer
|
|
|
22
94
|
|
|
23
95
|
バンドラを使わず `<script>` 1本で導入したい場合は、プリビルドの IIFE を読み込めばグローバル `HelpLayer` が生えます(後述)。
|
|
24
96
|
|
|
97
|
+
TypeScript の型定義を同梱しています(`package.json` の `types` が `dist/types` を指す)。TS プロジェクトでは追加設定なしで型補完が効きます。
|
|
98
|
+
|
|
25
99
|
## クイックスタート
|
|
26
100
|
|
|
27
101
|
### 1. config オブジェクトで定義する
|
|
@@ -44,7 +118,7 @@ initHelpLayer({
|
|
|
44
118
|
});
|
|
45
119
|
```
|
|
46
120
|
|
|
47
|
-
### 2.
|
|
121
|
+
### 2. マークアップに直接書く(説明の config 定義なしでも可。`config: {}` 自体は必要)
|
|
48
122
|
|
|
49
123
|
説明をマークアップと同居させたい場合は、`data-help-title` / `data-help-text` を要素に直接書くだけで対象になります。
|
|
50
124
|
`config` と併用でき、**同じキーが config にあれば config が優先**されます。
|
|
@@ -63,7 +137,7 @@ CDN から読む場合は、改ざん検知のため **バージョンを固定*
|
|
|
63
137
|
|
|
64
138
|
```html
|
|
65
139
|
<script
|
|
66
|
-
src="https://unpkg.com/help-layer@1.0.
|
|
140
|
+
src="https://unpkg.com/help-layer@1.0.1/dist/help-layer.iife.js"
|
|
67
141
|
integrity="sha384-……(公開版のハッシュに差し替え)"
|
|
68
142
|
crossorigin="anonymous"></script>
|
|
69
143
|
<script>
|
|
@@ -75,7 +149,7 @@ CDN から読む場合は、改ざん検知のため **バージョンを固定*
|
|
|
75
149
|
```
|
|
76
150
|
|
|
77
151
|
> `integrity` のハッシュは公開した実ファイルから生成します。例:
|
|
78
|
-
> `curl -s https://unpkg.com/help-layer@1.0.
|
|
152
|
+
> `curl -s https://unpkg.com/help-layer@1.0.1/dist/help-layer.iife.js | openssl dgst -sha384 -binary | openssl base64 -A`
|
|
79
153
|
> (バージョンを固定しないと SRI と不整合になり読み込みが拒否されます。)
|
|
80
154
|
|
|
81
155
|
## 自由配置(要素に紐づけない説明)
|
|
@@ -180,7 +254,7 @@ initHelpLayer({
|
|
|
180
254
|
|
|
181
255
|
- 設計上、`title` / `text` の描画は `textContent` のみで、`innerHTML` / `eval` / `new Function` は**一切使いません**。
|
|
182
256
|
- 外部通信(`fetch` 等)・`localStorage` / `cookie` などのストレージ利用も**ありません**(完全ローカル動作)。
|
|
183
|
-
-
|
|
257
|
+
- 唯一、未信頼データを HTML/DOM ノードとして挿入しうる経路は `render` オプションです。戻り値はサニタイズされないため、
|
|
184
258
|
ユーザー入力を含む場合は呼び出し側で無害化してください(上記「本文に改行を入れる / リンクを置く」参照)。
|
|
185
259
|
- ランタイム依存は `@floating-ui/dom` のみ。CDN 利用時は前述のとおりバージョン固定+SRI を推奨します。
|
|
186
260
|
|
package/README.md
CHANGED
|
@@ -6,13 +6,91 @@
|
|
|
6
6
|
|
|
7
7
|
**English** | [日本語](./README.ja.md)
|
|
8
8
|
|
|
9
|
+
🔗 **Live demo: <https://y1-effy.github.io/HelpLayer/>** (Vanilla / React / Vue — turn on "Help mode" top-right, then click an "i")
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
9
13
|
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.
|
|
14
|
+
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
15
|
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
16
|
|
|
13
|
-
- Only one dependency, [`@floating-ui/dom`](https://floating-ui.com/); lightweight (the prebuilt IIFE is ~
|
|
14
|
-
- Pierces Shadow DOM,
|
|
17
|
+
- Only one dependency, [`@floating-ui/dom`](https://floating-ui.com/); lightweight (the prebuilt IIFE is ~33KB minified, with `@floating-ui/dom` bundled in)
|
|
18
|
+
- 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 it)
|
|
15
20
|
- Fully cleans up the DOM, listeners, and styles it added when you turn it OFF
|
|
21
|
+
- Works in modern browsers (Chromium / Firefox / WebKit; e2e is verified across all three engines)
|
|
22
|
+
|
|
23
|
+
## Table of contents
|
|
24
|
+
|
|
25
|
+
- [Why HelpLayer (vs. existing options)](#why-helplayer-vs-existing-options)
|
|
26
|
+
- [When it fits (where adoption pays off)](#when-it-fits-where-adoption-pays-off)
|
|
27
|
+
- [Installation](#installation)
|
|
28
|
+
- [Quick start](#quick-start)
|
|
29
|
+
- [Free placement (descriptions not bound to an element)](#free-placement-descriptions-not-bound-to-an-element)
|
|
30
|
+
- [API](#api)
|
|
31
|
+
- [Theming (CSS custom properties)](#theming-css-custom-properties)
|
|
32
|
+
- [Known limitations](#known-limitations)
|
|
33
|
+
- [Security](#security)
|
|
34
|
+
- [Development](#development)
|
|
35
|
+
|
|
36
|
+
## Why HelpLayer (vs. existing options)
|
|
37
|
+
|
|
38
|
+
There are many ways to add explanations to a screen, but each comes with its own assumptions. HelpLayer
|
|
39
|
+
commits fully to **"a help mode where users freely pick just the spots they want to understand and check
|
|
40
|
+
them on the spot,"** aiming to sacrifice neither the normal look nor your existing code.
|
|
41
|
+
|
|
42
|
+
- **vs. product tours (step-by-step guidance)** … Rather than marching users along a fixed route,
|
|
43
|
+
it's **exploratory**: users pick the element they want and open it right there. The timing and order of
|
|
44
|
+
reading are left entirely to the user.
|
|
45
|
+
- **vs. always-on tooltips** … It never clutters the UI by showing explanations all the time. Markers
|
|
46
|
+
appear **only while the mode is ON**, so **the normal design is completely unchanged**.
|
|
47
|
+
- **vs. DAP SaaS (Digital Adoption Platform)** … No external platform, contract, or tracking required —
|
|
48
|
+
**zero running cost, one dependency, ~33KB, fully local**. It also supports CSP / Trusted Types, so it
|
|
49
|
+
fits environments with strict constraints on what you can bring in.
|
|
50
|
+
|
|
51
|
+
On top of that, they all share a common core: you can **drop it in without rewriting existing code**, it's
|
|
52
|
+
**framework-agnostic**, it never touches the host app's own events (a transparent blocking layer absorbs
|
|
53
|
+
interaction), and it **fully cleans up on ON→OFF**.
|
|
54
|
+
|
|
55
|
+
| | Product tours | Always-on tooltips | DAP SaaS | **HelpLayer** |
|
|
56
|
+
|---|---|---|---|---|
|
|
57
|
+
| Presentation | tends to be linear steps | tends to be always visible | service-dependent | **only while ON · explore any spot** |
|
|
58
|
+
| Normal UI | depends on implementation | tends to get cluttered | depends on implementation | **left entirely unchanged** |
|
|
59
|
+
| 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 · one dependency** |
|
|
61
|
+
|
|
62
|
+
> Note: HelpLayer is **not a full replacement for a DAP**. Advanced features like analytics, segmented
|
|
63
|
+
> delivery, complex flow guidance, and onboarding automation are out of scope — it commits to
|
|
64
|
+
> **satisfying just the core "show explanations in-screen" function at minimal cost**. Conversely, if your
|
|
65
|
+
> main goal is to drive strong funnels or measure usage, a DAP or a tour is the better fit.
|
|
66
|
+
|
|
67
|
+
## When it fits (where adoption pays off)
|
|
68
|
+
|
|
69
|
+
- **A DAP / guide SaaS isn't worth the cost and you're considering canceling — but canceling drops your
|
|
70
|
+
in-screen help back to zero.**
|
|
71
|
+
→ Keep just the core "show explanations in-screen" function in-house, with one dependency and zero
|
|
72
|
+
running cost. It gives you a place to land after switching away.
|
|
73
|
+
- **You don't have the budget to contract a SaaS, but you want to expand your help.**
|
|
74
|
+
→ Drop it in with npm or a single `<script>`. No monthly fee, no account.
|
|
75
|
+
- **Maintaining a separate manual in an office suite is a chore — and nobody reads it even when you do.**
|
|
76
|
+
→ Co-locate the explanation with the very element on screen (`data-help-title` / `data-help-text` or a
|
|
77
|
+
small config). You're freed from maintaining a separate doc, and the UI and its explanation never drift apart.
|
|
78
|
+
- **You want onboarding, but a forced tour feels pushy** and you'd rather avoid it.
|
|
79
|
+
→ It's exploratory — users pick what they want and open it on the spot — so it never interrupts their work.
|
|
80
|
+
- **Environments where you can't bring in an external SaaS** (strict CSP, privacy requirements, closed
|
|
81
|
+
networks, no tracking allowed).
|
|
82
|
+
→ It meets those requirements with fully local operation and no external communication.
|
|
83
|
+
- **Regardless of framework (React / Vue, etc.)**, you want to adopt it without touching your rendering library.
|
|
84
|
+
→ Framework-agnostic and drop-in; it doesn't rewrite your existing code.
|
|
85
|
+
|
|
86
|
+
> Business systems and admin screens are the easiest fit, but the use isn't limited to
|
|
87
|
+
> them. On **ordinary websites** too, you can supplement "what to enter in this field" on signup, contact,
|
|
88
|
+
> or reservation forms with a marker + popup. Any "existing web page you want to add explanations to" is in
|
|
89
|
+
> scope, and it makes a lightweight alternative to maintaining a separate manual.
|
|
90
|
+
|
|
91
|
+
> 💡 **It works for desktop apps, too.** Electron / Tauri and the like render their app screens with a
|
|
92
|
+
> WebView (HTML/DOM), so you can drop HelpLayer in exactly as you would in a web app. It's a surprisingly
|
|
93
|
+
> natural option when you want to add a "help mode" to a native-feeling screen.
|
|
16
94
|
|
|
17
95
|
## Installation
|
|
18
96
|
|
|
@@ -20,7 +98,9 @@ It never touches the host app's own event listeners — a transparent blocking l
|
|
|
20
98
|
npm install help-layer
|
|
21
99
|
```
|
|
22
100
|
|
|
23
|
-
If you'd rather drop it in with a single `<script>` and no bundler, load the prebuilt IIFE
|
|
101
|
+
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).
|
|
102
|
+
|
|
103
|
+
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
104
|
|
|
25
105
|
## Quick start
|
|
26
106
|
|
|
@@ -44,7 +124,7 @@ initHelpLayer({
|
|
|
44
124
|
});
|
|
45
125
|
```
|
|
46
126
|
|
|
47
|
-
### 2. Write it inline in your markup (no config needed)
|
|
127
|
+
### 2. Write it inline in your markup (no per-entry config needed; `config: {}` still required)
|
|
48
128
|
|
|
49
129
|
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
130
|
This can be combined with `config`, and **if the same key exists in `config`, the config wins**.
|
|
@@ -63,7 +143,7 @@ When loading from a CDN, we recommend **pinning the version** and adding **SRI (
|
|
|
63
143
|
|
|
64
144
|
```html
|
|
65
145
|
<script
|
|
66
|
-
src="https://unpkg.com/help-layer@1.0.
|
|
146
|
+
src="https://unpkg.com/help-layer@1.0.1/dist/help-layer.iife.js"
|
|
67
147
|
integrity="sha384-……(replace with the published file's hash)"
|
|
68
148
|
crossorigin="anonymous"></script>
|
|
69
149
|
<script>
|
|
@@ -75,7 +155,7 @@ When loading from a CDN, we recommend **pinning the version** and adding **SRI (
|
|
|
75
155
|
```
|
|
76
156
|
|
|
77
157
|
> Generate the `integrity` hash from the actually published file, e.g.:
|
|
78
|
-
> `curl -s https://unpkg.com/help-layer@1.0.
|
|
158
|
+
> `curl -s https://unpkg.com/help-layer@1.0.1/dist/help-layer.iife.js | openssl dgst -sha384 -binary | openssl base64 -A`
|
|
79
159
|
> (If you don't pin the version, the SRI will mismatch and the browser will refuse to load it.)
|
|
80
160
|
|
|
81
161
|
## Free placement (descriptions not bound to an element)
|
|
@@ -180,7 +260,7 @@ You can change the look just by overriding the following variables in your host
|
|
|
180
260
|
|
|
181
261
|
- By design, `title` / `text` are rendered with `textContent` only; `innerHTML` / `eval` / `new Function` are **never used**.
|
|
182
262
|
- 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
|
|
263
|
+
- 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
264
|
neutralize it on the caller side if it contains user input (see "Line breaks & links in the body" above).
|
|
185
265
|
- The only runtime dependency is `@floating-ui/dom`. When using a CDN, pin the version and add SRI as noted above.
|
|
186
266
|
|
package/dist/help-layer.esm.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
var
|
|
1
|
+
var F="help-layer-popup-title";function z(){let e=document.createElement("div");return e.className="help-layer-blocking-layer",e}function B(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=document.createElement("div");e.className="help-layer-popup",e.setAttribute("role","dialog"),e.setAttribute("aria-labelledby",F),e.tabIndex=-1;let t=document.createElement("div");t.className="help-layer-popup__title",t.id=F;let o=document.createElement("div");o.className="help-layer-popup__text";let r=document.createElement("button");return r.type="button",r.className="help-layer-popup__close",r.textContent="\xD7",r.setAttribute("aria-label","Close"),e.append(t,o,r),{root:e,titleEl:t,textEl:o,closeEl:r}}import{autoUpdate as O,computePosition as q,flip as be,offset as Z,shift as ve}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 G(e){return{contextElement:document.body,getBoundingClientRect(){return K(e(),{x:window.scrollX,y:window.scrollY})}}}function V(e,t,o){e.style.left=`${t}px`,e.style.top=`${o}px`}function U(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}var R=11;function ke(e){let t=e.endsWith("-start");return{mainAxis:-R,crossAxis:t?R:-R}}function W(e,t,o,r="top-end"){let n=U(e)?"fixed":"absolute";return n==="fixed"&&t.style.setProperty("position","fixed","important"),O(e,t,()=>{q(e,t,{placement:r,strategy:n,middleware:[Z(ke(r))]}).then(({x:p,y:d})=>{V(t,p,d),o&&o()}).catch(()=>{})},{animationFrame:!0})}function Y(e,t,o="bottom-start"){let r=U(e)?"fixed":"absolute";t.style.setProperty("position",r,"important");let n=()=>{q(e,t,{placement:o,strategy:r,middleware:[Z(8),be({padding:8}),ve({padding:8})]}).then(({x:p,y:d})=>{V(t,p,d)}).catch(()=>{})},i=O(e,t,n,{animationFrame:!0});return{update:n,cleanup:i}}function X(e,t,o){return O(e,t,o)}function we(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=z();if(document.body.appendChild(i),e.track(()=>i.remove()),t){let b=X(t,i,()=>{i.style.clipPath=we(t.getBoundingClientRect())});e.track(b)}o&&(i.addEventListener("click",o),e.track(()=>i.removeEventListener("click",o)));let p=document.activeElement;p instanceof HTMLElement&&p!==document.body&&p!==t&&p.blur();let d=u=>{r(u.target)||(u.stopPropagation(),t?t.focus({preventScroll:!0}):u.target instanceof HTMLElement&&u.target.blur())};document.addEventListener("focusin",d,!0),e.track(()=>document.removeEventListener("focusin",d,!0));let f=u=>{r(u.target)||(u.stopPropagation(),u.preventDefault())},y=u=>{if(u.key==="Escape"){u.stopPropagation(),u.preventDefault();return}f(u)},h=u=>{if(u.key==="Escape"){u.stopPropagation(),u.preventDefault(),n&&n();return}f(u)};return document.addEventListener("keydown",h,!0),document.addEventListener("keyup",y,!0),document.addEventListener("keypress",y,!0),e.track(()=>{document.removeEventListener("keydown",h,!0),document.removeEventListener("keyup",y,!0),document.removeEventListener("keypress",y,!0)}),i}function P(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function Q(e){return P(e)&&Number.isFinite(e.top)&&Number.isFinite(e.left)}function M(e){if(!P(e))throw new Error("helpConfig must be a plain object");for(let[t,o]of Object.entries(e)){if(!P(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 p=!1;for(let d=0;d<n.length;d++)for(let f=d+1;f<n.length;f++){let y=n[d],h=n[f],u=h.x-y.x,b=h.y-y.y,l=Math.hypot(u,b);if(l>=o)continue;l===0&&(u=1,b=0,l=1);let c=(o-l)/2,m=u/l,a=b/l;y.x-=m*c,y.y-=a*c,h.x+=m*c,h.y+=a*c,p=!0}if(!p)break}return n.map((i,p)=>({dx:i.x-e[p].x,dy:i.y-e[p].y}))}var oe="help-layer-target-highlight";function Ee(e){return e.kind==="free"?G(()=>({top:e.position.top,left:e.position.left,width:0,height:0})):e.target}function ne(e,{onMarkerClick:t,onOverlapResolved:o,markerLabel:r="?",markerPlacement:n="top-end"}){let i=new Map,p=null,d=!1;function f(){p=null;let l=[...i.values()];if(l.length<=1){let a=l.length===1?l[0].el:null;a&&a.style.transform&&(a.style.transform="",o&&o());return}l.forEach(a=>{a.el.style.transform=""});let c=l.map(a=>{let s=a.el.getBoundingClientRect();return{x:s.left+s.width/2,y:s.top+s.height/2}}),m=te(c);l.forEach((a,s)=>{let{dx:x,dy:v}=m[s];a.el.style.transform=x||v?`translate(${x}px, ${v}px)`:""}),o&&o()}function y(){p!==null||d||(p=requestAnimationFrame(f))}function h(l){if(i.has(l.id))return;let c=B(l.title,r);document.body.appendChild(c);let m=()=>t(l,c);c.addEventListener("click",m);let a=W(Ee(l),c,y,n),s=l.kind==="element"?l.target:null,x=()=>s&&s.classList.add(oe),v=()=>s&&s.classList.remove(oe);s&&(c.addEventListener("mouseenter",x),c.addEventListener("mouseleave",v),c.addEventListener("focus",x),c.addEventListener("blur",v));let C=!1,L=()=>{C||(C=!0,a(),c.removeEventListener("click",m),s&&(c.removeEventListener("mouseenter",x),c.removeEventListener("mouseleave",v),c.removeEventListener("focus",x),c.removeEventListener("blur",v),v()),c.remove(),i.delete(l.id),y())};i.set(l.id,{record:l,el:c,cleanup:L})}function u(l){let c=i.get(l);c&&c.cleanup()}function b(l){l.forEach(h)}return e.track(()=>{d=!0,p!==null&&(cancelAnimationFrame(p),p=null),[...i.values()].forEach(l=>l.cleanup())}),{mount:h,unmount:u,mountAll:b,has(l){return i.has(l)},findByKey(l){for(let c of i.values())if(c.record.key===l)return c;return null}}}function k(e,t,...o){if(t)try{return t(...o)}catch(r){console.error(`[help-layer] ${e} threw:`,r);return}}var Ce=1;function S(e,t,o,r){typeof e.querySelectorAll=="function"&&e.querySelectorAll("*").forEach(n=>{o&&n.matches(t)&&o(n),n.shadowRoot&&(r&&r(n.shadowRoot),S(n.shadowRoot,t,o,r))})}function ie(e,t){let o=[];return S(e,t,r=>o.push(r)),o}function Le(e){let t=[];return S(e,"*",null,o=>t.push(o)),t}function re(e,t){let o=[],r=[];if(e.nodeType!==Ce)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),S(n.shadowRoot,t,i=>o.push(i),i=>r.push(i))),S(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=f=>{for(let y of f)y.addedNodes.forEach(h=>{let{matches:u,shadowRoots:b}=re(h,t);u.forEach(l=>k("observer onAdded",o,l)),b.forEach(d)}),y.removedNodes.forEach(h=>{re(h,t).matches.forEach(u=>k("observer onRemoved",r,u))})},p=new MutationObserver(i);function d(f){n.has(f)||(n.add(f),p.observe(f,{childList:!0,subtree:!0}))}return d(e),Le(e).forEach(d),{disconnect(){p.disconnect(),n.clear()}}}var T="data-help-title",I="data-help-text";function N(e="data-help-id"){return`[${e}], [${T}]`}function D(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(T),p=n?n.text:e.getAttribute(I);return!i||!p?null:{id:e,kind:"element",key:r,title:i,text:p,target:e}}function se(e,t=document,{silent:o=!1,attribute:r="data-help-id"}={}){let n=D(e),i=[];return ie(t,N(r)).forEach(p=>{let d=j(p,n,r);if(!d){if(!o){let f=p.getAttribute(r);console.warn(f!=null?`[help-layer] element with ${r}="${f}" has no matching helpConfig entry or inline ${T}/${I}`:`[help-layer] element needs both ${T} and ${I} (or a ${r} matching helpConfig)`)}return}i.push(d)}),i}function pe(e,{onClose:t,render:o,popupPlacement:r="bottom-start"}={}){let{root:n,titleEl:i,textEl:p,closeEl:d}=H();n.style.setProperty("display","none","important"),document.body.appendChild(n),d.addEventListener("click",()=>m());let f=null,y=null,h=null;function u(){h&&(h.cleanup(),h=null)}function b(a,s){i.textContent=a.title;let x=k("render",o,a);p.textContent="",x?p.appendChild(x):p.textContent=a.text,n.style.setProperty("display","block","important"),f=a.id,y=s,u(),h=Y(s,n,r),n.focus({preventScroll:!0})}function l(){h&&h.update()}function c(){let a=f!==null;u(),f=null,y=null,n.style.setProperty("display","none","important"),a&&k("onClose",t)}function m(a){let s=a??y;c(),s&&s.isConnected&&typeof s.focus=="function"&&s.focus({preventScroll:!0})}return e.track(()=>{c(),n.remove()}),{root:n,isOpen(a){return f===a},getOpenId(){return f},open:b,close:m,reposition:l}}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 Ae="data-help-layer-style",Pe=`
|
|
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(Ae,""),e&&t.setAttribute("nonce",e),t.textContent=Pe,document.head.appendChild(t),t}function fe(e){e.remove()}function Se(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(!P(e))throw new Error("help-layer: initHelpLayer requires an options object");let{config:t,toggle:o,onEnable:r,onDisable:n,onOpen:i,onClose:p,silent:d=!1,attribute:f="data-help-id",render:y,markerLabel:h="?",markerPlacement:u="top-end",popupPlacement:b="bottom-start",nonce:l}=e,c=t;M(c);let m=o!=null?Se(o):null,a=null,s=null,x=null;function v(){if(a)return;a=ce(),m&&a.track(()=>{m.isConnected&&typeof m.focus=="function"&&m.focus({preventScroll:!0})});let E=ue(l);a.track(()=>fe(E));let w=ee(c),xe=D(w);s=pe(a,{onClose:p,render:y,popupPlacement:b}),x=ne(a,{markerLabel:h,markerPlacement:u,onMarkerClick:(g,A)=>{if(s.isOpen(g.id)){s.close();return}s.open(g,A),k("onOpen",i,g)},onOverlapResolved:()=>s.reposition()}),x.mountAll(le(w)),x.mountAll(se(w,document,{silent:d,attribute:f}));let ge=ae({selector:N(f),onAdded:g=>{let A=j(g,xe,f);A&&!x.has(A.id)&&x.mount(A)},onRemoved:g=>{s.isOpen(g)&&s.close(m??void 0),x.unmount(g)}});a.track(()=>ge.disconnect()),J(a,{toggleEl:m,onBackgroundClick:()=>s.close(),isLibraryElement:g=>!!g&&((m?m.contains(g):!1)||s.root.contains(g)||typeof g.closest=="function"&&!!g.closest(".help-layer-marker")),onEscape:()=>{s.getOpenId()!==null?s.close():$()}})}function C(){a&&(a.teardownAll(),a=null,s=null,x=null)}function L(){a||(v(),k("onEnable",r))}function $(){a&&(C(),k("onDisable",n))}function _(){a?$():L()}function he(E){if(a||L(),!x||!s)return;let w=x.findByKey(E);if(!w){d||console.warn(`[help-layer] open(): no help marker for key "${E}"`);return}s.open(w.record,w.el),k("onOpen",i,w.record)}function me(){s&&s.close()}function ye(E){M(E),c=E,a&&(C(),v())}return m&&m.addEventListener("click",_),{enable:L,disable:$,toggle:_,isActive(){return a!==null},open:he,close:me,update:ye,destroy(){$(),m&&m.removeEventListener("click",_)}}}function ft(e){return de(e)}export{ft as initHelpLayer};
|
|
158
160
|
//# sourceMappingURL=help-layer.esm.js.map
|