@bauer-group/accessibility-widget 1.0.3
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/LICENSE +21 -0
- package/README.md +259 -0
- package/dist/accessibility-widget-core.min.js +6 -0
- package/dist/accessibility-widget-core.min.js.map +7 -0
- package/dist/accessibility-widget-loader.min.js +14 -0
- package/dist/accessibility-widget-loader.min.js.map +7 -0
- package/dist/accessibility-widget.min.css +7 -0
- package/dist/accessibility-widget.min.css.map +7 -0
- package/dist/config.d.ts +33 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/core.d.ts +31 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/features/apply.d.ts +3 -0
- package/dist/features/apply.d.ts.map +1 -0
- package/dist/features/profile.d.ts +3 -0
- package/dist/features/profile.d.ts.map +1 -0
- package/dist/features/reading-guide.d.ts +2 -0
- package/dist/features/reading-guide.d.ts.map +1 -0
- package/dist/features/reading-mask.d.ts +2 -0
- package/dist/features/reading-mask.d.ts.map +1 -0
- package/dist/features/structure-nav.d.ts +2 -0
- package/dist/features/structure-nav.d.ts.map +1 -0
- package/dist/features/tts.d.ts +7 -0
- package/dist/features/tts.d.ts.map +1 -0
- package/dist/focus-trap.d.ts +6 -0
- package/dist/focus-trap.d.ts.map +1 -0
- package/dist/i18n/ar.d.ts +3 -0
- package/dist/i18n/ar.d.ts.map +1 -0
- package/dist/i18n/bn.d.ts +3 -0
- package/dist/i18n/bn.d.ts.map +1 -0
- package/dist/i18n/cs.d.ts +3 -0
- package/dist/i18n/cs.d.ts.map +1 -0
- package/dist/i18n/de.d.ts +3 -0
- package/dist/i18n/de.d.ts.map +1 -0
- package/dist/i18n/el.d.ts +3 -0
- package/dist/i18n/el.d.ts.map +1 -0
- package/dist/i18n/en.d.ts +3 -0
- package/dist/i18n/en.d.ts.map +1 -0
- package/dist/i18n/es.d.ts +3 -0
- package/dist/i18n/es.d.ts.map +1 -0
- package/dist/i18n/fa.d.ts +3 -0
- package/dist/i18n/fa.d.ts.map +1 -0
- package/dist/i18n/fr.d.ts +3 -0
- package/dist/i18n/fr.d.ts.map +1 -0
- package/dist/i18n/he.d.ts +3 -0
- package/dist/i18n/he.d.ts.map +1 -0
- package/dist/i18n/hi.d.ts +3 -0
- package/dist/i18n/hi.d.ts.map +1 -0
- package/dist/i18n/hu.d.ts +3 -0
- package/dist/i18n/hu.d.ts.map +1 -0
- package/dist/i18n/id.d.ts +3 -0
- package/dist/i18n/id.d.ts.map +1 -0
- package/dist/i18n/index.d.ts +6 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/it.d.ts +3 -0
- package/dist/i18n/it.d.ts.map +1 -0
- package/dist/i18n/ja.d.ts +3 -0
- package/dist/i18n/ja.d.ts.map +1 -0
- package/dist/i18n/ko.d.ts +3 -0
- package/dist/i18n/ko.d.ts.map +1 -0
- package/dist/i18n/nl.d.ts +3 -0
- package/dist/i18n/nl.d.ts.map +1 -0
- package/dist/i18n/pl.d.ts +3 -0
- package/dist/i18n/pl.d.ts.map +1 -0
- package/dist/i18n/pt.d.ts +3 -0
- package/dist/i18n/pt.d.ts.map +1 -0
- package/dist/i18n/ro.d.ts +3 -0
- package/dist/i18n/ro.d.ts.map +1 -0
- package/dist/i18n/ru.d.ts +3 -0
- package/dist/i18n/ru.d.ts.map +1 -0
- package/dist/i18n/sv.d.ts +3 -0
- package/dist/i18n/sv.d.ts.map +1 -0
- package/dist/i18n/th.d.ts +3 -0
- package/dist/i18n/th.d.ts.map +1 -0
- package/dist/i18n/tr.d.ts +3 -0
- package/dist/i18n/tr.d.ts.map +1 -0
- package/dist/i18n/types.d.ts +44 -0
- package/dist/i18n/types.d.ts.map +1 -0
- package/dist/i18n/uk.d.ts +3 -0
- package/dist/i18n/uk.d.ts.map +1 -0
- package/dist/i18n/ur.d.ts +3 -0
- package/dist/i18n/ur.d.ts.map +1 -0
- package/dist/i18n/vi.d.ts +3 -0
- package/dist/i18n/vi.d.ts.map +1 -0
- package/dist/i18n/zh.d.ts +3 -0
- package/dist/i18n/zh.d.ts.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/integrity.json +9 -0
- package/dist/integrity.txt +12 -0
- package/dist/loader.d.ts +2 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/panel/drag.d.ts +34 -0
- package/dist/panel/drag.d.ts.map +1 -0
- package/dist/panel/panel.d.ts +23 -0
- package/dist/panel/panel.d.ts.map +1 -0
- package/dist/state.d.ts +18 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/styles/critical.d.ts +16 -0
- package/dist/styles/critical.d.ts.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/locale.d.ts +11 -0
- package/dist/types/locale.d.ts.map +1 -0
- package/dist/types/widget.d.ts +207 -0
- package/dist/types/widget.d.ts.map +1 -0
- package/dist/util/debug.d.ts +8 -0
- package/dist/util/debug.d.ts.map +1 -0
- package/dist/util/dom.d.ts +19 -0
- package/dist/util/dom.d.ts.map +1 -0
- package/dist/util/events.d.ts +38 -0
- package/dist/util/events.d.ts.map +1 -0
- package/dist/util/feature-icons.d.ts +33 -0
- package/dist/util/feature-icons.d.ts.map +1 -0
- package/dist/util/language-names.d.ts +12 -0
- package/dist/util/language-names.d.ts.map +1 -0
- package/dist/util/svg.d.ts +38 -0
- package/dist/util/svg.d.ts.map +1 -0
- package/package.json +67 -0
- package/src/config.ts +213 -0
- package/src/core.ts +173 -0
- package/src/features/apply.ts +37 -0
- package/src/features/profile.ts +18 -0
- package/src/features/reading-guide.ts +25 -0
- package/src/features/reading-mask.ts +25 -0
- package/src/features/structure-nav.ts +43 -0
- package/src/features/tts.ts +73 -0
- package/src/focus-trap.ts +35 -0
- package/src/globals.d.ts +63 -0
- package/src/i18n/ar.ts +48 -0
- package/src/i18n/bn.ts +48 -0
- package/src/i18n/cs.ts +48 -0
- package/src/i18n/de.ts +65 -0
- package/src/i18n/el.ts +48 -0
- package/src/i18n/en.ts +65 -0
- package/src/i18n/es.ts +48 -0
- package/src/i18n/fa.ts +48 -0
- package/src/i18n/fr.ts +48 -0
- package/src/i18n/he.ts +48 -0
- package/src/i18n/hi.ts +48 -0
- package/src/i18n/hu.ts +48 -0
- package/src/i18n/id.ts +48 -0
- package/src/i18n/index.ts +70 -0
- package/src/i18n/it.ts +48 -0
- package/src/i18n/ja.ts +48 -0
- package/src/i18n/ko.ts +48 -0
- package/src/i18n/nl.ts +48 -0
- package/src/i18n/pl.ts +48 -0
- package/src/i18n/pt.ts +48 -0
- package/src/i18n/ro.ts +48 -0
- package/src/i18n/ru.ts +48 -0
- package/src/i18n/sv.ts +48 -0
- package/src/i18n/th.ts +48 -0
- package/src/i18n/tr.ts +48 -0
- package/src/i18n/types.ts +36 -0
- package/src/i18n/uk.ts +48 -0
- package/src/i18n/ur.ts +48 -0
- package/src/i18n/vi.ts +48 -0
- package/src/i18n/zh.ts +48 -0
- package/src/index.ts +9 -0
- package/src/loader.ts +533 -0
- package/src/panel/drag.ts +210 -0
- package/src/panel/panel.ts +617 -0
- package/src/state.ts +91 -0
- package/src/styles/critical.ts +56 -0
- package/src/styles/widget.css +739 -0
- package/src/types/index.ts +2 -0
- package/src/types/locale.ts +55 -0
- package/src/types/widget.ts +300 -0
- package/src/util/debug.ts +12 -0
- package/src/util/dom.ts +68 -0
- package/src/util/events.ts +54 -0
- package/src/util/feature-icons.ts +163 -0
- package/src/util/language-names.ts +41 -0
- package/src/util/svg.ts +93 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native language labels for the runtime language switcher.
|
|
3
|
+
*
|
|
4
|
+
* Each entry is the language's endonym (its own native name), not a localized
|
|
5
|
+
* translation. Showing "Deutsch" to a Japanese reader is more useful than
|
|
6
|
+
* "German" — the user who needs German will recognise their own word for it.
|
|
7
|
+
*
|
|
8
|
+
* The autonym pattern also sidesteps N×N translation matrices.
|
|
9
|
+
*/
|
|
10
|
+
import type { Locale } from '../types/index.js';
|
|
11
|
+
export declare const LANGUAGE_NAMES: Record<Locale, string>;
|
|
12
|
+
//# sourceMappingURL=language-names.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"language-names.d.ts","sourceRoot":"","sources":["../../src/util/language-names.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAEhD,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA6BjD,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface SvgPath {
|
|
2
|
+
d: string;
|
|
3
|
+
fill?: string;
|
|
4
|
+
stroke?: string;
|
|
5
|
+
strokeWidth?: number | string;
|
|
6
|
+
}
|
|
7
|
+
export interface SvgCircle {
|
|
8
|
+
cx: number;
|
|
9
|
+
cy: number;
|
|
10
|
+
r: number;
|
|
11
|
+
fill?: string;
|
|
12
|
+
stroke?: string;
|
|
13
|
+
strokeWidth?: number | string;
|
|
14
|
+
}
|
|
15
|
+
export interface SvgIconOptions {
|
|
16
|
+
viewBox?: string;
|
|
17
|
+
width?: number;
|
|
18
|
+
height?: number;
|
|
19
|
+
paths?: readonly SvgPath[];
|
|
20
|
+
circles?: readonly SvgCircle[];
|
|
21
|
+
/**
|
|
22
|
+
* Lucide-style outline icon. Applies fill:none, stroke:currentColor,
|
|
23
|
+
* stroke-width:2, round caps/joins at the <svg> root, so individual
|
|
24
|
+
* paths/circles don't need to repeat these attributes.
|
|
25
|
+
*/
|
|
26
|
+
stroke?: boolean;
|
|
27
|
+
ariaHidden?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build an SVG element programmatically — no innerHTML, no DOMParser,
|
|
31
|
+
* so no XSS surface even if path data ever comes from config.
|
|
32
|
+
*/
|
|
33
|
+
export declare function buildIcon(opts: SvgIconOptions): SVGSVGElement;
|
|
34
|
+
/** Accessibility person icon (BFSG signature icon). */
|
|
35
|
+
export declare const ICON_ACCESSIBILITY: SvgIconOptions;
|
|
36
|
+
/** Close / X icon (Lucide: x). */
|
|
37
|
+
export declare const ICON_CLOSE: SvgIconOptions;
|
|
38
|
+
//# sourceMappingURL=svg.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"svg.d.ts","sourceRoot":"","sources":["../../src/util/svg.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,OAAO;IACtB,CAAC,EAAE,MAAM,CAAC;IACV,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,CAAC,EAAE,MAAM,CAAC;IACV,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;IAC3B,OAAO,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;IAC/B;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,cAAc,GAAG,aAAa,CAsC7D;AAED,uDAAuD;AACvD,eAAO,MAAM,kBAAkB,EAAE,cAOhC,CAAC;AAEF,kCAAkC;AAClC,eAAO,MAAM,UAAU,EAAE,cAIxB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bauer-group/accessibility-widget",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "Lazy-loading Accessibility-Widget nach BFSG/EN 301 549/WCAG 2.2 AA. Loader ~4 KB gzip, Core ~14 KB gzip. Kein Tracking, kein Cookie, kein DOM-/ARIA-Override.",
|
|
5
|
+
"license": "AGPL-3.0-only",
|
|
6
|
+
"author": "BAUER GROUP <info@bauer-group.com>",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/bauer-group/SaaS-AccessibilityWidget.git",
|
|
10
|
+
"directory": "packages/widget"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/bauer-group/SaaS-AccessibilityWidget/tree/main/packages/widget",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "./dist/accessibility-widget-loader.min.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/accessibility-widget-loader.min.js"
|
|
20
|
+
},
|
|
21
|
+
"./loader": "./dist/accessibility-widget-loader.min.js",
|
|
22
|
+
"./core": "./dist/accessibility-widget-core.min.js",
|
|
23
|
+
"./styles": "./dist/accessibility-widget.min.css"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"src",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsx scripts/build.ts && tsc -p tsconfig.build.json",
|
|
36
|
+
"dev": "tsx scripts/build.ts --watch",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"test:watch": "vitest",
|
|
39
|
+
"test:coverage": "vitest run --coverage",
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"size": "tsx scripts/measure-size.ts",
|
|
42
|
+
"clean": "rimraf dist coverage"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/jsdom": "^21.1.7",
|
|
46
|
+
"@types/node": "^24.0.0",
|
|
47
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
48
|
+
"esbuild": "^0.25.0",
|
|
49
|
+
"jsdom": "^26.0.0",
|
|
50
|
+
"rimraf": "^6.0.1",
|
|
51
|
+
"tsx": "^4.19.0",
|
|
52
|
+
"typescript": "^5.8.0",
|
|
53
|
+
"vitest": "^3.1.0"
|
|
54
|
+
},
|
|
55
|
+
"keywords": [
|
|
56
|
+
"accessibility",
|
|
57
|
+
"a11y",
|
|
58
|
+
"bfsg",
|
|
59
|
+
"wcag",
|
|
60
|
+
"wcag2.2",
|
|
61
|
+
"en301549",
|
|
62
|
+
"eaa",
|
|
63
|
+
"widget",
|
|
64
|
+
"lazy-loading",
|
|
65
|
+
"barrierefreiheit"
|
|
66
|
+
]
|
|
67
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FEATURE_IDS,
|
|
3
|
+
POSITIONS,
|
|
4
|
+
type FeatureId,
|
|
5
|
+
type WidgetConfig,
|
|
6
|
+
type Position,
|
|
7
|
+
} from './types/widget.js';
|
|
8
|
+
import type { Locale } from './types/locale.js';
|
|
9
|
+
import { normalizeLocale, isLocale } from './types/index.js';
|
|
10
|
+
import { warnIfDebug } from './util/debug.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Fully-resolved config with no optional fields — every loader / panel /
|
|
14
|
+
* feature call site can rely on the presence of every key.
|
|
15
|
+
*/
|
|
16
|
+
export interface ResolvedConfig {
|
|
17
|
+
corePath: string;
|
|
18
|
+
cssPath: string;
|
|
19
|
+
coreIntegrity: string | null;
|
|
20
|
+
cssIntegrity: string | null;
|
|
21
|
+
locale: Locale;
|
|
22
|
+
position: Position;
|
|
23
|
+
offset: { x: number; y: number };
|
|
24
|
+
zIndex: number;
|
|
25
|
+
primaryColor: string;
|
|
26
|
+
buttonLabel: string | null;
|
|
27
|
+
storageKey: string;
|
|
28
|
+
initialFeatures: Readonly<Partial<Record<FeatureId, boolean>>>;
|
|
29
|
+
disabledFeatures: ReadonlySet<FeatureId>;
|
|
30
|
+
statementUrl: string | null;
|
|
31
|
+
disclaimer: string | null;
|
|
32
|
+
hidePoweredBy: boolean;
|
|
33
|
+
draggableFab: boolean;
|
|
34
|
+
respectReducedMotion: boolean;
|
|
35
|
+
hideOnPrint: boolean;
|
|
36
|
+
debug: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const DEFAULTS = {
|
|
40
|
+
corePath: '/accessibility-widget/accessibility-widget-core.min.js',
|
|
41
|
+
cssPath: '/accessibility-widget/accessibility-widget.min.css',
|
|
42
|
+
position: 'bottom-right' as Position,
|
|
43
|
+
offset: { x: 20, y: 20 },
|
|
44
|
+
zIndex: 2_147_483_646,
|
|
45
|
+
primaryColor: '#0058a3',
|
|
46
|
+
storageKey: 'accessibility-widget',
|
|
47
|
+
draggableFab: false,
|
|
48
|
+
respectReducedMotion: true,
|
|
49
|
+
hideOnPrint: true,
|
|
50
|
+
debug: false,
|
|
51
|
+
} as const;
|
|
52
|
+
|
|
53
|
+
// Accepts #rgb, #rrggbb, #rrggbbaa, rgb/rgba/hsl/hsla/color() functional forms,
|
|
54
|
+
// and named colors (anything CSS accepts). For the primaryColor field we
|
|
55
|
+
// want ANY valid CSS color, so the regex is permissive — just not empty.
|
|
56
|
+
const NON_TRIVIAL_COLOR_RE =
|
|
57
|
+
/^(?:#[0-9a-f]{3,8}|(?:rgb|rgba|hsl|hsla|color|lab|lch|oklab|oklch)\s*\(.+\)|[a-z]+)$/i;
|
|
58
|
+
|
|
59
|
+
export function resolveConfig(input: WidgetConfig | undefined, navLang: string): ResolvedConfig {
|
|
60
|
+
const cfg = input ?? {};
|
|
61
|
+
const debug = cfg.debug ?? DEFAULTS.debug;
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
corePath: cfg.corePath ?? DEFAULTS.corePath,
|
|
65
|
+
cssPath: cfg.cssPath ?? DEFAULTS.cssPath,
|
|
66
|
+
coreIntegrity: cfg.coreIntegrity ?? null,
|
|
67
|
+
cssIntegrity: cfg.cssIntegrity ?? null,
|
|
68
|
+
locale: resolveLocale(cfg.locale, navLang, debug),
|
|
69
|
+
position: resolvePosition(cfg.position, debug),
|
|
70
|
+
offset: resolveOffset(cfg.offset, debug),
|
|
71
|
+
zIndex: resolveZIndex(cfg.zIndex, debug),
|
|
72
|
+
primaryColor: resolvePrimaryColor(cfg.primaryColor, debug),
|
|
73
|
+
buttonLabel: cfg.buttonLabel ?? null,
|
|
74
|
+
storageKey: resolveStorageKey(cfg.storageKey, debug),
|
|
75
|
+
initialFeatures: resolveInitialFeatures(cfg.initialFeatures, debug),
|
|
76
|
+
disabledFeatures: resolveDisabledFeatures(cfg.disabledFeatures, debug),
|
|
77
|
+
statementUrl: resolveStatementUrl(cfg.statementUrl, debug),
|
|
78
|
+
disclaimer: resolveDisclaimer(cfg.disclaimer),
|
|
79
|
+
hidePoweredBy: Boolean(cfg.hidePoweredBy),
|
|
80
|
+
draggableFab: cfg.draggableFab ?? DEFAULTS.draggableFab,
|
|
81
|
+
respectReducedMotion: cfg.respectReducedMotion ?? DEFAULTS.respectReducedMotion,
|
|
82
|
+
hideOnPrint: cfg.hideOnPrint ?? DEFAULTS.hideOnPrint,
|
|
83
|
+
debug,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─── individual field resolvers ──────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
function resolveLocale(requested: WidgetConfig['locale'], navLang: string, debug: boolean): Locale {
|
|
90
|
+
if (!requested || requested === 'auto') return normalizeLocale(navLang, 'de');
|
|
91
|
+
if (isLocale(requested)) return requested;
|
|
92
|
+
if (debug)
|
|
93
|
+
warnIfDebug(
|
|
94
|
+
`config.locale "${String(requested)}" is not supported; falling back to auto-detect`,
|
|
95
|
+
);
|
|
96
|
+
return normalizeLocale(navLang, 'de');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function resolvePosition(requested: WidgetConfig['position'], debug: boolean): Position {
|
|
100
|
+
if (!requested) return DEFAULTS.position;
|
|
101
|
+
if ((POSITIONS as readonly string[]).includes(requested)) return requested;
|
|
102
|
+
if (debug)
|
|
103
|
+
warnIfDebug(
|
|
104
|
+
`config.position "${String(requested)}" is not one of ${POSITIONS.join(', ')}; using default`,
|
|
105
|
+
);
|
|
106
|
+
return DEFAULTS.position;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resolveOffset(
|
|
110
|
+
requested: WidgetConfig['offset'],
|
|
111
|
+
debug: boolean,
|
|
112
|
+
): { x: number; y: number } {
|
|
113
|
+
if (!requested) return { ...DEFAULTS.offset };
|
|
114
|
+
const x = finiteOr(requested.x, DEFAULTS.offset.x, 'config.offset.x', debug);
|
|
115
|
+
const y = finiteOr(requested.y, DEFAULTS.offset.y, 'config.offset.y', debug);
|
|
116
|
+
return { x, y };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function resolveZIndex(requested: WidgetConfig['zIndex'], debug: boolean): number {
|
|
120
|
+
return finiteOr(requested, DEFAULTS.zIndex, 'config.zIndex', debug);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function resolvePrimaryColor(requested: WidgetConfig['primaryColor'], debug: boolean): string {
|
|
124
|
+
if (requested === undefined) return DEFAULTS.primaryColor;
|
|
125
|
+
const trimmed = String(requested).trim();
|
|
126
|
+
if (!trimmed) {
|
|
127
|
+
if (debug) warnIfDebug('config.primaryColor is empty; using default');
|
|
128
|
+
return DEFAULTS.primaryColor;
|
|
129
|
+
}
|
|
130
|
+
if (!NON_TRIVIAL_COLOR_RE.test(trimmed)) {
|
|
131
|
+
if (debug)
|
|
132
|
+
warnIfDebug(
|
|
133
|
+
`config.primaryColor "${trimmed}" does not look like a CSS color; browsers may reject it`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return trimmed;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function resolveStorageKey(requested: WidgetConfig['storageKey'], debug: boolean): string {
|
|
140
|
+
if (requested === undefined) return DEFAULTS.storageKey;
|
|
141
|
+
const trimmed = String(requested).trim();
|
|
142
|
+
if (!trimmed) {
|
|
143
|
+
if (debug) warnIfDebug('config.storageKey is empty; using default');
|
|
144
|
+
return DEFAULTS.storageKey;
|
|
145
|
+
}
|
|
146
|
+
return trimmed;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function resolveInitialFeatures(
|
|
150
|
+
requested: WidgetConfig['initialFeatures'],
|
|
151
|
+
debug: boolean,
|
|
152
|
+
): Readonly<Partial<Record<FeatureId, boolean>>> {
|
|
153
|
+
if (!requested) return Object.freeze({});
|
|
154
|
+
const valid: Partial<Record<FeatureId, boolean>> = {};
|
|
155
|
+
const validIds = new Set<string>(FEATURE_IDS);
|
|
156
|
+
for (const [key, value] of Object.entries(requested)) {
|
|
157
|
+
if (!validIds.has(key)) {
|
|
158
|
+
if (debug) warnIfDebug(`config.initialFeatures: unknown feature id "${key}" ignored`);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
valid[key as FeatureId] = Boolean(value);
|
|
162
|
+
}
|
|
163
|
+
return Object.freeze(valid);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function resolveDisabledFeatures(
|
|
167
|
+
requested: WidgetConfig['disabledFeatures'],
|
|
168
|
+
debug: boolean,
|
|
169
|
+
): ReadonlySet<FeatureId> {
|
|
170
|
+
if (!requested) return new Set();
|
|
171
|
+
const result = new Set<FeatureId>();
|
|
172
|
+
const validIds = new Set<string>(FEATURE_IDS);
|
|
173
|
+
for (const id of requested) {
|
|
174
|
+
if (!validIds.has(id)) {
|
|
175
|
+
if (debug) warnIfDebug(`config.disabledFeatures: unknown feature id "${id}" ignored`);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
result.add(id);
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function resolveDisclaimer(requested: WidgetConfig['disclaimer']): string | null {
|
|
184
|
+
if (requested === undefined || requested === null) return null;
|
|
185
|
+
const trimmed = String(requested).trim();
|
|
186
|
+
return trimmed || null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function resolveStatementUrl(
|
|
190
|
+
requested: WidgetConfig['statementUrl'],
|
|
191
|
+
debug: boolean,
|
|
192
|
+
): string | null {
|
|
193
|
+
if (!requested) return null;
|
|
194
|
+
const trimmed = String(requested).trim();
|
|
195
|
+
if (!trimmed) return null;
|
|
196
|
+
// A best-effort sanity check: block javascript: / data: schemes to avoid
|
|
197
|
+
// footguns when the host interpolates user input.
|
|
198
|
+
if (/^\s*(?:javascript|data):/i.test(trimmed)) {
|
|
199
|
+
if (debug) warnIfDebug(`config.statementUrl "${trimmed}" uses a blocked scheme; ignoring`);
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
return trimmed;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function finiteOr(value: unknown, fallback: number, label: string, debug: boolean): number {
|
|
206
|
+
if (value === undefined || value === null) return fallback;
|
|
207
|
+
const n = Number(value);
|
|
208
|
+
if (!Number.isFinite(n)) {
|
|
209
|
+
if (debug) warnIfDebug(`${label} "${String(value)}" is not a finite number; using ${fallback}`);
|
|
210
|
+
return fallback;
|
|
211
|
+
}
|
|
212
|
+
return n;
|
|
213
|
+
}
|
package/src/core.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* BAUER GROUP Accessibility Widget — Core (on-demand bundle)
|
|
3
|
+
* SPDX-License-Identifier: AGPL-3.0-only · © 2026 BAUER GROUP
|
|
4
|
+
* AGPL-3.0-only or commercial (info@bauer-group.com) — see LICENSE / LICENSING.md
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
isLocale,
|
|
8
|
+
PROFILE_IDS,
|
|
9
|
+
type Locale,
|
|
10
|
+
type ProfileId,
|
|
11
|
+
type WidgetConfig,
|
|
12
|
+
type WidgetState,
|
|
13
|
+
} from './types/index.js';
|
|
14
|
+
import { resolveConfig, type ResolvedConfig } from './config.js';
|
|
15
|
+
import { applyState } from './features/apply.js';
|
|
16
|
+
import { applyProfile } from './features/profile.js';
|
|
17
|
+
import { loadState, saveState, clearState } from './state.js';
|
|
18
|
+
import { openPanel, type PanelHandle } from './panel/panel.js';
|
|
19
|
+
import { dispatchWidgetEvent } from './util/events.js';
|
|
20
|
+
|
|
21
|
+
export interface CoreOpenOptions {
|
|
22
|
+
trigger?: HTMLElement;
|
|
23
|
+
config?: WidgetConfig;
|
|
24
|
+
locale?: Locale | 'auto';
|
|
25
|
+
statementUrl?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let panel: PanelHandle | null = null;
|
|
29
|
+
let lastTrigger: HTMLElement | null = null;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Read the host-supplied config from window every time (over reading it
|
|
33
|
+
* once at module load) so late mutations — e.g. SPA navigation that rewrites
|
|
34
|
+
* AccessibilityWidgetConfig — are honored.
|
|
35
|
+
*/
|
|
36
|
+
function readUserConfig(): WidgetConfig {
|
|
37
|
+
if (typeof window === 'undefined') return {};
|
|
38
|
+
return window.AccessibilityWidgetConfig ?? {};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveActiveLocale(
|
|
42
|
+
config: ResolvedConfig,
|
|
43
|
+
state: WidgetState,
|
|
44
|
+
explicit?: Locale | 'auto',
|
|
45
|
+
): Locale {
|
|
46
|
+
if (explicit && explicit !== 'auto' && isLocale(explicit)) return explicit;
|
|
47
|
+
if (state.locale && isLocale(state.locale)) return state.locale;
|
|
48
|
+
return config.locale;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function open(opts: CoreOpenOptions = {}): void {
|
|
52
|
+
if (panel) return;
|
|
53
|
+
const mergedInput: WidgetConfig = {
|
|
54
|
+
...readUserConfig(),
|
|
55
|
+
...(opts.config ?? {}),
|
|
56
|
+
...(opts.locale && opts.locale !== 'auto' ? { locale: opts.locale } : {}),
|
|
57
|
+
};
|
|
58
|
+
const config: ResolvedConfig = resolveConfig(mergedInput, navigator.language);
|
|
59
|
+
const state = loadState(config.storageKey);
|
|
60
|
+
const locale: Locale = resolveActiveLocale(config, state, opts.locale);
|
|
61
|
+
|
|
62
|
+
lastTrigger = opts.trigger ?? (document.activeElement as HTMLElement | null);
|
|
63
|
+
applyState(state);
|
|
64
|
+
|
|
65
|
+
panel = openPanel({
|
|
66
|
+
config,
|
|
67
|
+
locale,
|
|
68
|
+
state,
|
|
69
|
+
// opts.statementUrl wins over config.statementUrl — covers the
|
|
70
|
+
// "open the widget with a one-off statement link" edge case.
|
|
71
|
+
statementUrl: opts.statementUrl ?? config.statementUrl ?? undefined,
|
|
72
|
+
onClose: close,
|
|
73
|
+
onStateChange: (next) => {
|
|
74
|
+
dispatchWidgetEvent('stateChange', { state: next });
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
dispatchWidgetEvent('open', { trigger: lastTrigger });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function close(): void {
|
|
81
|
+
panel?.destroy();
|
|
82
|
+
panel = null;
|
|
83
|
+
lastTrigger?.focus();
|
|
84
|
+
lastTrigger = null;
|
|
85
|
+
dispatchWidgetEvent('close', {});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function set(id: string, value: unknown): void {
|
|
89
|
+
const cfg = resolveConfig(readUserConfig(), navigator.language);
|
|
90
|
+
if (cfg.disabledFeatures.has(id as never)) {
|
|
91
|
+
if (cfg.debug) console.warn(`[aw] core.set: feature "${id}" is disabled by config; ignoring`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const state = loadState(cfg.storageKey);
|
|
95
|
+
if (id in state.features) {
|
|
96
|
+
(state.features as Record<string, boolean>)[id] = Boolean(value);
|
|
97
|
+
} else {
|
|
98
|
+
(state as unknown as Record<string, unknown>)[id] = value;
|
|
99
|
+
}
|
|
100
|
+
saveState(cfg.storageKey, state);
|
|
101
|
+
applyState(state);
|
|
102
|
+
panel?.rerender();
|
|
103
|
+
dispatchWidgetEvent('stateChange', { state });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function applyProfileById(id: string): boolean {
|
|
107
|
+
const cfg = resolveConfig(readUserConfig(), navigator.language);
|
|
108
|
+
if (!(PROFILE_IDS as readonly string[]).includes(id)) {
|
|
109
|
+
if (cfg.debug) console.warn(`[aw] core.applyProfile: unknown profile "${id}"; ignoring`);
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
const state = loadState(cfg.storageKey);
|
|
113
|
+
const next = applyProfile(state, id as ProfileId);
|
|
114
|
+
// Strip features the host has disabled so profile presets can't re-enable them.
|
|
115
|
+
for (const disabled of cfg.disabledFeatures) {
|
|
116
|
+
next.features[disabled] = false;
|
|
117
|
+
}
|
|
118
|
+
saveState(cfg.storageKey, next);
|
|
119
|
+
applyState(next);
|
|
120
|
+
panel?.rerender();
|
|
121
|
+
dispatchWidgetEvent('profileApplied', { profile: id as ProfileId, state: next });
|
|
122
|
+
dispatchWidgetEvent('stateChange', { state: next });
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function setLocale(next: string): boolean {
|
|
127
|
+
const cfg = resolveConfig(readUserConfig(), navigator.language);
|
|
128
|
+
if (!isLocale(next)) {
|
|
129
|
+
if (cfg.debug)
|
|
130
|
+
console.warn(`[aw] core.setLocale: "${next}" is not a supported locale; ignoring`);
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
const state = loadState(cfg.storageKey);
|
|
134
|
+
if (state.locale === next) return true;
|
|
135
|
+
const updated: WidgetState = { ...state, locale: next };
|
|
136
|
+
saveState(cfg.storageKey, updated);
|
|
137
|
+
panel?.setLocale(next);
|
|
138
|
+
dispatchWidgetEvent('localeChanged', { locale: next });
|
|
139
|
+
dispatchWidgetEvent('stateChange', { state: updated });
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function reset(): void {
|
|
144
|
+
const cfg = resolveConfig(readUserConfig(), navigator.language);
|
|
145
|
+
clearState(cfg.storageKey);
|
|
146
|
+
const fresh = loadState(cfg.storageKey);
|
|
147
|
+
applyState(fresh);
|
|
148
|
+
panel?.rerender();
|
|
149
|
+
dispatchWidgetEvent('reset', {});
|
|
150
|
+
dispatchWidgetEvent('stateChange', { state: fresh });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function getState(): WidgetState {
|
|
154
|
+
const cfg = resolveConfig(readUserConfig(), navigator.language);
|
|
155
|
+
return loadState(cfg.storageKey);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const api = {
|
|
159
|
+
open,
|
|
160
|
+
close,
|
|
161
|
+
set,
|
|
162
|
+
applyProfile: applyProfileById,
|
|
163
|
+
setLocale,
|
|
164
|
+
reset,
|
|
165
|
+
getState,
|
|
166
|
+
version: __AW_VERSION__,
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
if (typeof window !== 'undefined') {
|
|
170
|
+
window.AccessibilityWidgetCore = api;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export default api;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { WidgetState } from '../types/index.js';
|
|
2
|
+
import { toggleAttr } from '../util/dom.js';
|
|
3
|
+
import { readingMaskApply } from './reading-mask.js';
|
|
4
|
+
import { readingGuideApply } from './reading-guide.js';
|
|
5
|
+
|
|
6
|
+
const HTML_ATTR = {
|
|
7
|
+
contrast: 'data-aw-contrast',
|
|
8
|
+
grayscale: 'data-aw-grayscale',
|
|
9
|
+
invert: 'data-aw-invert',
|
|
10
|
+
dyslexia: 'data-aw-dyslexia',
|
|
11
|
+
highlight: 'data-aw-highlight-links',
|
|
12
|
+
pauseAnim: 'data-aw-pause-animations',
|
|
13
|
+
bigCursor: 'data-aw-big-cursor',
|
|
14
|
+
focus: 'data-aw-focus',
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
export function applyState(state: WidgetState): void {
|
|
18
|
+
const html = document.documentElement;
|
|
19
|
+
html.setAttribute('data-aw-instant', '1');
|
|
20
|
+
|
|
21
|
+
html.style.setProperty('--aw-font-scale', String(state.fontSizeLevel));
|
|
22
|
+
html.style.setProperty('--aw-line-height', String(state.lineHeightLevel));
|
|
23
|
+
html.style.setProperty('--aw-letter-spacing', `${state.letterSpacingLevel}em`);
|
|
24
|
+
|
|
25
|
+
const f = state.features;
|
|
26
|
+
toggleAttr(html, HTML_ATTR.contrast, f.contrast ? state.contrastMode : false);
|
|
27
|
+
toggleAttr(html, HTML_ATTR.grayscale, f.grayscale ? '1' : false);
|
|
28
|
+
toggleAttr(html, HTML_ATTR.invert, f.invertColors ? '1' : false);
|
|
29
|
+
toggleAttr(html, HTML_ATTR.dyslexia, f.dyslexiaFont ? '1' : false);
|
|
30
|
+
toggleAttr(html, HTML_ATTR.highlight, f.highlightLinks ? '1' : false);
|
|
31
|
+
toggleAttr(html, HTML_ATTR.pauseAnim, f.pauseAnimations ? '1' : false);
|
|
32
|
+
toggleAttr(html, HTML_ATTR.bigCursor, f.bigCursor ? '1' : false);
|
|
33
|
+
toggleAttr(html, HTML_ATTR.focus, f.focusOutline ? '1' : false);
|
|
34
|
+
|
|
35
|
+
readingMaskApply(f.readingMask);
|
|
36
|
+
readingGuideApply(f.readingGuide);
|
|
37
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { PROFILES, type ProfileId, type WidgetState } from '../types/index.js';
|
|
2
|
+
|
|
3
|
+
export function applyProfile(state: WidgetState, profileId: ProfileId): WidgetState {
|
|
4
|
+
const preset = PROFILES[profileId];
|
|
5
|
+
const next: WidgetState = {
|
|
6
|
+
features: { ...state.features },
|
|
7
|
+
fontSizeLevel: preset.fontSizeLevel ?? state.fontSizeLevel,
|
|
8
|
+
lineHeightLevel: preset.lineHeightLevel ?? state.lineHeightLevel,
|
|
9
|
+
letterSpacingLevel: preset.letterSpacingLevel ?? state.letterSpacingLevel,
|
|
10
|
+
contrastMode: preset.contrastMode ?? state.contrastMode,
|
|
11
|
+
};
|
|
12
|
+
if (preset.features) {
|
|
13
|
+
for (const [k, v] of Object.entries(preset.features)) {
|
|
14
|
+
next.features[k as keyof typeof next.features] = v as boolean;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return next;
|
|
18
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
let guideEl: HTMLDivElement | null = null;
|
|
2
|
+
let handler: ((e: MouseEvent | TouchEvent) => void) | null = null;
|
|
3
|
+
|
|
4
|
+
export function readingGuideApply(on: boolean): void {
|
|
5
|
+
if (on && !guideEl) {
|
|
6
|
+
guideEl = document.createElement('div');
|
|
7
|
+
guideEl.className = 'aw-reading-guide';
|
|
8
|
+
guideEl.setAttribute('aria-hidden', 'true');
|
|
9
|
+
document.body.appendChild(guideEl);
|
|
10
|
+
handler = (e) => {
|
|
11
|
+
const y = 'clientY' in e ? e.clientY : (e.touches[0]?.clientY ?? 0);
|
|
12
|
+
guideEl!.style.top = `${y - 16}px`;
|
|
13
|
+
};
|
|
14
|
+
document.addEventListener('mousemove', handler as EventListener, { passive: true });
|
|
15
|
+
document.addEventListener('touchmove', handler as EventListener, { passive: true });
|
|
16
|
+
} else if (!on && guideEl) {
|
|
17
|
+
if (handler) {
|
|
18
|
+
document.removeEventListener('mousemove', handler as EventListener);
|
|
19
|
+
document.removeEventListener('touchmove', handler as EventListener);
|
|
20
|
+
}
|
|
21
|
+
guideEl.remove();
|
|
22
|
+
guideEl = null;
|
|
23
|
+
handler = null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
let maskEl: HTMLDivElement | null = null;
|
|
2
|
+
let handler: ((e: MouseEvent | TouchEvent) => void) | null = null;
|
|
3
|
+
|
|
4
|
+
export function readingMaskApply(on: boolean): void {
|
|
5
|
+
if (on && !maskEl) {
|
|
6
|
+
maskEl = document.createElement('div');
|
|
7
|
+
maskEl.className = 'aw-reading-mask';
|
|
8
|
+
maskEl.setAttribute('aria-hidden', 'true');
|
|
9
|
+
document.body.appendChild(maskEl);
|
|
10
|
+
handler = (e) => {
|
|
11
|
+
const y = 'clientY' in e ? e.clientY : (e.touches[0]?.clientY ?? 0);
|
|
12
|
+
maskEl!.style.setProperty('--aw-mask-y', `${y}px`);
|
|
13
|
+
};
|
|
14
|
+
document.addEventListener('mousemove', handler as EventListener, { passive: true });
|
|
15
|
+
document.addEventListener('touchmove', handler as EventListener, { passive: true });
|
|
16
|
+
} else if (!on && maskEl) {
|
|
17
|
+
if (handler) {
|
|
18
|
+
document.removeEventListener('mousemove', handler as EventListener);
|
|
19
|
+
document.removeEventListener('touchmove', handler as EventListener);
|
|
20
|
+
}
|
|
21
|
+
maskEl.remove();
|
|
22
|
+
maskEl = null;
|
|
23
|
+
handler = null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const NAV_ID = 'aw-struct-nav';
|
|
2
|
+
|
|
3
|
+
export function structureNavToggle(label: string): boolean {
|
|
4
|
+
const existing = document.getElementById(NAV_ID);
|
|
5
|
+
if (existing) {
|
|
6
|
+
existing.remove();
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const nav = document.createElement('nav');
|
|
11
|
+
nav.id = NAV_ID;
|
|
12
|
+
nav.setAttribute('aria-label', label);
|
|
13
|
+
|
|
14
|
+
const closeBtn = document.createElement('button');
|
|
15
|
+
closeBtn.type = 'button';
|
|
16
|
+
closeBtn.className = 'aw-struct-nav__close';
|
|
17
|
+
closeBtn.textContent = '×';
|
|
18
|
+
closeBtn.setAttribute('aria-label', label);
|
|
19
|
+
closeBtn.addEventListener('click', () => nav.remove());
|
|
20
|
+
|
|
21
|
+
const ul = document.createElement('ul');
|
|
22
|
+
document.querySelectorAll<HTMLHeadingElement>('h1, h2, h3').forEach((h, i) => {
|
|
23
|
+
if (!h.id) h.id = `aw-h-${i}`;
|
|
24
|
+
const level = Number(h.tagName.substring(1));
|
|
25
|
+
const li = document.createElement('li');
|
|
26
|
+
li.dataset.level = String(level);
|
|
27
|
+
const a = document.createElement('a');
|
|
28
|
+
a.href = `#${h.id}`;
|
|
29
|
+
a.textContent = (h.textContent ?? '').trim().slice(0, 80);
|
|
30
|
+
a.addEventListener('click', () => {
|
|
31
|
+
setTimeout(() => nav.remove(), 150);
|
|
32
|
+
});
|
|
33
|
+
li.appendChild(a);
|
|
34
|
+
ul.appendChild(li);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
nav.appendChild(closeBtn);
|
|
38
|
+
nav.appendChild(ul);
|
|
39
|
+
document.body.appendChild(nav);
|
|
40
|
+
|
|
41
|
+
setTimeout(() => ul.querySelector<HTMLAnchorElement>('a')?.focus(), 10);
|
|
42
|
+
return true;
|
|
43
|
+
}
|