@pure-ds/core 0.4.37 → 0.5.2
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/dist/types/pds.d.ts +34 -11
- package/dist/types/public/assets/auto-definer-XWHRBQPU.d.ts +9 -0
- package/dist/types/public/assets/auto-definer-XWHRBQPU.d.ts.map +1 -0
- package/dist/types/public/assets/chunk-746HIXIK.d.ts +52 -0
- package/dist/types/public/assets/chunk-746HIXIK.d.ts.map +1 -0
- package/dist/types/public/assets/chunk-APJV5T3J.d.ts +106 -0
- package/dist/types/public/assets/chunk-APJV5T3J.d.ts.map +1 -0
- package/dist/types/public/assets/chunk-BEPKFFM7.d.ts +398 -0
- package/dist/types/public/assets/chunk-BEPKFFM7.d.ts.map +1 -0
- package/dist/types/public/assets/chunk-ISS7UH5H.d.ts +2424 -0
- package/dist/types/public/assets/chunk-ISS7UH5H.d.ts.map +1 -0
- package/dist/types/public/assets/chunk-RUPLQUDG.d.ts +582 -0
- package/dist/types/public/assets/chunk-RUPLQUDG.d.ts.map +1 -0
- package/dist/types/public/assets/chunk-USML4NYF.d.ts +18 -0
- package/dist/types/public/assets/chunk-USML4NYF.d.ts.map +1 -0
- package/dist/types/public/assets/chunk-Z47A3HLT.d.ts +3 -0
- package/dist/types/public/assets/chunk-Z47A3HLT.d.ts.map +1 -0
- package/dist/types/public/assets/js/auto-definer-HZLD2XF4.d.ts +9 -0
- package/dist/types/public/assets/js/auto-definer-HZLD2XF4.d.ts.map +1 -0
- package/dist/types/public/assets/js/chunk-6A6DFAIG.d.ts +88 -0
- package/dist/types/public/assets/js/chunk-6A6DFAIG.d.ts.map +1 -0
- package/dist/types/public/assets/js/chunk-746HIXIK.d.ts +52 -0
- package/dist/types/public/assets/js/chunk-746HIXIK.d.ts.map +1 -0
- package/dist/types/public/assets/js/chunk-A3TZGIYX.d.ts +4 -0
- package/dist/types/public/assets/js/chunk-A3TZGIYX.d.ts.map +1 -0
- package/dist/types/public/assets/js/chunk-BEPKFFM7.d.ts +398 -0
- package/dist/types/public/assets/js/chunk-BEPKFFM7.d.ts.map +1 -0
- package/dist/types/public/assets/js/chunk-OTTRJ5MB.d.ts +1695 -0
- package/dist/types/public/assets/js/chunk-OTTRJ5MB.d.ts.map +1 -0
- package/dist/types/public/assets/js/chunk-RBPKHG76.d.ts +747 -0
- package/dist/types/public/assets/js/chunk-RBPKHG76.d.ts.map +1 -0
- package/dist/types/public/assets/js/chunk-RUPLQUDG.d.ts +582 -0
- package/dist/types/public/assets/js/chunk-RUPLQUDG.d.ts.map +1 -0
- package/dist/types/public/assets/js/chunk-SMD2R3CX.d.ts +68 -0
- package/dist/types/public/assets/js/chunk-SMD2R3CX.d.ts.map +1 -0
- package/dist/types/public/assets/js/chunk-Y73DA2D5.d.ts +15 -0
- package/dist/types/public/assets/js/chunk-Y73DA2D5.d.ts.map +1 -0
- package/dist/types/public/assets/js/chunks/auto-definer-X7MSXKTU.d.ts +9 -0
- package/dist/types/public/assets/js/chunks/auto-definer-X7MSXKTU.d.ts.map +1 -0
- package/dist/types/public/assets/js/chunks/chunk-7BDQH5CT.d.ts +485 -0
- package/dist/types/public/assets/js/chunks/chunk-7BDQH5CT.d.ts.map +1 -0
- package/dist/types/public/assets/js/chunks/chunk-MWB3S7NG.d.ts +3 -0
- package/dist/types/public/assets/js/chunks/chunk-MWB3S7NG.d.ts.map +1 -0
- package/dist/types/public/assets/js/chunks/chunk-WIMLORAU.d.ts +5 -0
- package/dist/types/public/assets/js/chunks/chunk-WIMLORAU.d.ts.map +1 -0
- package/dist/types/public/assets/js/chunks/chunk-WN4Y2ELN.d.ts +833 -0
- package/dist/types/public/assets/js/chunks/chunk-WN4Y2ELN.d.ts.map +1 -0
- package/dist/types/public/assets/js/chunks/chunk-XQOUIBLO.d.ts +1687 -0
- package/dist/types/public/assets/js/chunks/chunk-XQOUIBLO.d.ts.map +1 -0
- package/dist/types/public/assets/js/chunks/font-loader-VN5SRNOD.d.ts +5 -0
- package/dist/types/public/assets/js/chunks/font-loader-VN5SRNOD.d.ts.map +1 -0
- package/dist/types/public/assets/js/chunks/pds-live-validation-BQPWN5JG.d.ts +38 -0
- package/dist/types/public/assets/js/chunks/pds-live-validation-BQPWN5JG.d.ts.map +1 -0
- package/dist/types/public/assets/js/common-WIAC4WAJ.d.ts +4 -0
- package/dist/types/public/assets/js/common-WIAC4WAJ.d.ts.map +1 -0
- package/dist/types/public/assets/js/pds-config-WEBAXXSM.d.ts +4 -0
- package/dist/types/public/assets/js/pds-config-WEBAXXSM.d.ts.map +1 -0
- package/dist/types/public/assets/js/pds-core/pds-generator.d.ts +700 -0
- package/dist/types/public/assets/js/pds-core/pds-generator.d.ts.map +1 -0
- package/dist/types/public/assets/js/pds-core/pds-utilities.d.ts +27 -0
- package/dist/types/public/assets/js/pds-core/pds-utilities.d.ts.map +1 -0
- package/dist/types/public/assets/js/pds-enums-DCBZHS64.d.ts +3 -0
- package/dist/types/public/assets/js/pds-enums-DCBZHS64.d.ts.map +1 -0
- package/dist/types/public/assets/js/pds-gen.d.ts +106 -0
- package/dist/types/public/assets/js/pds-gen.d.ts.map +1 -0
- package/dist/types/public/assets/js/pds-live.d.ts +11 -0
- package/dist/types/public/assets/js/pds-live.d.ts.map +1 -0
- package/dist/types/public/assets/js/pds-manager.d.ts +1047 -0
- package/dist/types/public/assets/js/pds-manager.d.ts.map +1 -0
- package/dist/types/public/assets/js/pds-ontology-2DICJXHO.d.ts +9 -0
- package/dist/types/public/assets/js/pds-ontology-2DICJXHO.d.ts.map +1 -0
- package/dist/types/public/assets/js/pds-query-B54LBKKR.d.ts +70 -0
- package/dist/types/public/assets/js/pds-query-B54LBKKR.d.ts.map +1 -0
- package/dist/types/public/assets/js/pds.d.ts +1 -18
- package/dist/types/public/assets/js/pds.d.ts.map +1 -1
- package/dist/types/public/assets/pds-ontology-ZO6TJHO3.d.ts +9 -0
- package/dist/types/public/assets/pds-ontology-ZO6TJHO3.d.ts.map +1 -0
- package/dist/types/src/js/common/pds-core/pds-config.d.ts +757 -0
- package/dist/types/src/js/common/pds-core/pds-config.d.ts.map +1 -0
- package/dist/types/src/js/common/pds-core/pds-enhancers.d.ts +28 -0
- package/dist/types/src/js/common/pds-core/pds-enhancers.d.ts.map +1 -0
- package/dist/types/src/js/common/pds-core/pds-enums.d.ts +87 -0
- package/dist/types/src/js/common/pds-core/pds-enums.d.ts.map +1 -0
- package/dist/types/src/js/common/pds-core/pds-generator.d.ts +700 -0
- package/dist/types/src/js/common/pds-core/pds-generator.d.ts.map +1 -0
- package/dist/types/src/js/common/pds-core/pds-ontology.d.ts +380 -0
- package/dist/types/src/js/common/pds-core/pds-ontology.d.ts.map +1 -0
- package/dist/types/src/js/common/pds-core/pds-paths.d.ts +37 -0
- package/dist/types/src/js/common/pds-core/pds-paths.d.ts.map +1 -0
- package/dist/types/src/js/common/pds-core/pds-query.d.ts +102 -0
- package/dist/types/src/js/common/pds-core/pds-query.d.ts.map +1 -0
- package/dist/types/src/js/common/pds-core/pds-registry.d.ts +35 -0
- package/dist/types/src/js/common/pds-core/pds-registry.d.ts.map +1 -0
- package/dist/types/src/js/common/pds-core/pds-utilities.d.ts +27 -0
- package/dist/types/src/js/common/pds-core/pds-utilities.d.ts.map +1 -0
- package/dist/types/src/js/pds-core/pds-generator.d.ts +38 -46
- package/dist/types/src/js/pds-core/pds-generator.d.ts.map +1 -1
- package/dist/types/src/js/pds-core/pds-live.d.ts +39 -0
- package/dist/types/src/js/pds-core/pds-live.d.ts.map +1 -0
- package/dist/types/src/js/pds-core/pds-runtime.d.ts +39 -0
- package/dist/types/src/js/pds-core/pds-runtime.d.ts.map +1 -0
- package/dist/types/src/js/pds-core/pds-start-helpers.d.ts +60 -0
- package/dist/types/src/js/pds-core/pds-start-helpers.d.ts.map +1 -0
- package/dist/types/src/js/pds-core/pds-utilities.d.ts +27 -0
- package/dist/types/src/js/pds-core/pds-utilities.d.ts.map +1 -0
- package/dist/types/src/js/pds-gen.d.ts +48 -0
- package/dist/types/src/js/pds-gen.d.ts.map +1 -0
- package/dist/types/src/js/pds-live-runtime.d.ts +7 -0
- package/dist/types/src/js/pds-live-runtime.d.ts.map +1 -0
- package/dist/types/src/js/pds-live-validation.d.ts +44 -0
- package/dist/types/src/js/pds-live-validation.d.ts.map +1 -0
- package/dist/types/src/js/pds-live.d.ts +11 -0
- package/dist/types/src/js/pds-live.d.ts.map +1 -0
- package/dist/types/src/js/pds-manager.d.ts +2 -0
- package/dist/types/src/js/pds-manager.d.ts.map +1 -0
- package/dist/types/src/js/pds.d.ts +6 -33
- package/dist/types/src/js/pds.d.ts.map +1 -1
- package/package.json +11 -12
- package/packages/pds-cli/bin/{generate-css-data.mjs → generate-css-data.js} +563 -563
- package/packages/pds-cli/bin/{generate-manifest.mjs → generate-manifest.js} +352 -352
- package/packages/pds-cli/bin/{pds-build-icons.mjs → pds-build-icons.js} +152 -152
- package/packages/pds-cli/bin/{pds-dx.mjs → pds-dx.js} +114 -114
- package/packages/pds-cli/bin/{pds-init-config.mjs → pds-init-config.js} +34 -34
- package/packages/pds-cli/bin/{pds-setup-copilot.mjs → pds-setup-copilot.js} +106 -106
- package/packages/pds-cli/bin/{pds-static.mjs → pds-static.js} +597 -581
- package/packages/pds-cli/bin/{pds.mjs → pds.js} +127 -127
- package/packages/pds-cli/bin/postinstall.mjs +522 -563
- package/packages/pds-cli/bin/{sync-assets.mjs → sync-assets.js} +251 -251
- package/packages/pds-cli/lib/{asset-roots.mjs → asset-roots.js} +47 -47
- package/packages/pds-cli/lib/{fs-writer.mjs → fs-writer.js} +75 -75
- package/public/assets/js/app.js +95 -118
- package/public/assets/js/pds-manager.js +3251 -0
- package/public/assets/js/pds.js +10 -3201
- package/readme.md +2014 -2016
- package/src/js/pds-core/pds-enhancers.js +518 -518
- package/src/js/pds-core/pds-enums.js +86 -86
- package/src/js/pds-core/pds-generator.js +255 -185
- package/src/js/pds-core/pds-live.js +434 -0
- package/src/js/pds-core/pds-paths.js +109 -109
- package/src/js/pds-core/pds-registry.js +79 -79
- package/src/js/pds-core/pds-runtime.js +184 -0
- package/src/js/pds-core/pds-start-helpers.js +405 -0
- package/src/js/pds.d.ts +34 -11
- package/src/js/pds.js +43 -1182
- package/getting-started.md +0 -626
- package/src/js/pds-core/pds.d.ts +0 -129
|
@@ -1,518 +1,518 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PDS Enhancers - Single Source of Truth
|
|
3
|
-
*
|
|
4
|
-
* This file defines all progressive enhancements for the Pure Design System.
|
|
5
|
-
* Each enhancer has:
|
|
6
|
-
* - selector: CSS selector to target elements
|
|
7
|
-
* - description: Human-readable explanation
|
|
8
|
-
* - demoHtml: Example usage markup
|
|
9
|
-
* - run: Enhancement function (added at the end)
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
// ============================================================================
|
|
13
|
-
// ENHANCEMENT METADATA DEFINITIONS
|
|
14
|
-
// ============================================================================
|
|
15
|
-
|
|
16
|
-
const enhancerDefinitions = [
|
|
17
|
-
{
|
|
18
|
-
selector: ".accordion",
|
|
19
|
-
description:
|
|
20
|
-
"Ensures only one <details> element can be open at a time within the accordion.",
|
|
21
|
-
demoHtml: `
|
|
22
|
-
<div class="accordion">
|
|
23
|
-
<details>
|
|
24
|
-
<summary>Section 1</summary>
|
|
25
|
-
<p>Content for section 1</p>
|
|
26
|
-
</details>
|
|
27
|
-
<details>
|
|
28
|
-
<summary>Section 2</summary>
|
|
29
|
-
<p>Content for section 2</p>
|
|
30
|
-
</details>
|
|
31
|
-
<details>
|
|
32
|
-
<summary>Section 3</summary>
|
|
33
|
-
<p>Content for section 3</p>
|
|
34
|
-
</details>
|
|
35
|
-
</div>
|
|
36
|
-
`.trim(),
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
selector: "nav[data-dropdown]",
|
|
40
|
-
description:
|
|
41
|
-
"Enhances a nav element with data-dropdown to function as a dropdown menu.",
|
|
42
|
-
demoHtml: `
|
|
43
|
-
<nav data-dropdown>
|
|
44
|
-
<button class="btn-primary">Menu</button>
|
|
45
|
-
<menu>
|
|
46
|
-
<li><a href="#">Item 1</a></li>
|
|
47
|
-
<li><a href="#">Item 2</a></li>
|
|
48
|
-
</menu>
|
|
49
|
-
</nav>
|
|
50
|
-
`.trim(),
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
selector: "label[data-toggle]",
|
|
54
|
-
description: "Creates a toggle switch element from a checkbox.",
|
|
55
|
-
demoHtml: `
|
|
56
|
-
<label data-toggle>
|
|
57
|
-
<input type="checkbox">
|
|
58
|
-
<span data-label>Enable notifications</span>
|
|
59
|
-
</label>
|
|
60
|
-
`.trim(),
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
selector: 'input[type="range"]',
|
|
64
|
-
description: "Enhances range inputs with an attached <output>.",
|
|
65
|
-
demoHtml: `
|
|
66
|
-
<label class="range-output">
|
|
67
|
-
<span data-label>Volume</span>
|
|
68
|
-
<input type="range" min="0" max="100" value="40">
|
|
69
|
-
</label>
|
|
70
|
-
`.trim(),
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
selector: "form[data-required]",
|
|
74
|
-
description:
|
|
75
|
-
"Enhances required form fields using an asterisk in the label.",
|
|
76
|
-
demoHtml: `
|
|
77
|
-
<form data-required action="#" method="post">
|
|
78
|
-
<label>
|
|
79
|
-
<span>Field Label</span>
|
|
80
|
-
<input type="text" required>
|
|
81
|
-
</label>
|
|
82
|
-
<nav class="form-actions">
|
|
83
|
-
<button type="submit" class="btn-primary">Submit</button>
|
|
84
|
-
</nav>
|
|
85
|
-
</form>
|
|
86
|
-
`.trim(),
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
selector: "fieldset[role=group][data-open]",
|
|
90
|
-
description:
|
|
91
|
-
"Enhances a checkbox/radio group to be open (have a way to add and remove items).",
|
|
92
|
-
demoHtml: `
|
|
93
|
-
<fieldset role="group" data-open>
|
|
94
|
-
<label>
|
|
95
|
-
<span data-label>Test</span>
|
|
96
|
-
<input value="lala" name="test1" type="radio" />
|
|
97
|
-
</label>
|
|
98
|
-
</fieldset>
|
|
99
|
-
`.trim(),
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
selector: "button, a[class*='btn-']",
|
|
103
|
-
description:
|
|
104
|
-
"Automatically manages spinner icon for buttons with .btn-working class",
|
|
105
|
-
demoHtml: `
|
|
106
|
-
<button class="btn-primary btn-working">
|
|
107
|
-
<span>Saving</span>
|
|
108
|
-
</button>
|
|
109
|
-
`.trim(),
|
|
110
|
-
},
|
|
111
|
-
];
|
|
112
|
-
|
|
113
|
-
// ============================================================================
|
|
114
|
-
// ENHANCEMENT RUNTIME FUNCTIONS
|
|
115
|
-
// ============================================================================
|
|
116
|
-
|
|
117
|
-
function enhanceAccordion(elem) {
|
|
118
|
-
if (elem.dataset.enhancedAccordion) return;
|
|
119
|
-
elem.dataset.enhancedAccordion = "true";
|
|
120
|
-
|
|
121
|
-
elem.addEventListener("toggle", (event) => {
|
|
122
|
-
// Only handle toggle events from direct child details elements
|
|
123
|
-
// to avoid closing parent details when nested accordions are used
|
|
124
|
-
if (event.target.open && event.target.parentElement === elem) {
|
|
125
|
-
elem.querySelectorAll(":scope > details[open]").forEach((details) => {
|
|
126
|
-
if (details !== event.target) {
|
|
127
|
-
details.open = false;
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
}, true);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function enhanceDropdown(elem) {
|
|
135
|
-
if (elem.dataset.enhancedDropdown) return;
|
|
136
|
-
elem.dataset.enhancedDropdown = "true";
|
|
137
|
-
const menu = elem.querySelector("menu");
|
|
138
|
-
if (!menu) return;
|
|
139
|
-
|
|
140
|
-
const trigger =
|
|
141
|
-
elem.querySelector("[data-dropdown-toggle]") ||
|
|
142
|
-
elem.querySelector("button");
|
|
143
|
-
|
|
144
|
-
if (trigger && !trigger.hasAttribute("type")) {
|
|
145
|
-
trigger.setAttribute("type", "button");
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (!menu.id) {
|
|
149
|
-
menu.id = `dropdown-${Math.random().toString(36).slice(2, 9)}`;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
menu.setAttribute("role", menu.getAttribute("role") || "menu");
|
|
153
|
-
if (!menu.hasAttribute("aria-hidden")) {
|
|
154
|
-
menu.setAttribute("aria-hidden", "true");
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (trigger) {
|
|
158
|
-
trigger.setAttribute("aria-haspopup", "true");
|
|
159
|
-
trigger.setAttribute("aria-controls", menu.id);
|
|
160
|
-
trigger.setAttribute("aria-expanded", "false");
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const resolveDirection = () => {
|
|
164
|
-
const mode = (elem.getAttribute("data-mode") || "auto").toLowerCase();
|
|
165
|
-
if (mode === "up" || mode === "down") return mode;
|
|
166
|
-
const rect = elem.getBoundingClientRect();
|
|
167
|
-
const spaceBelow = Math.max(0, window.innerHeight - rect.bottom);
|
|
168
|
-
const spaceAbove = Math.max(0, rect.top);
|
|
169
|
-
return spaceAbove > spaceBelow ? "up" : "down";
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
const openMenu = () => {
|
|
173
|
-
elem.dataset.dropdownDirection = resolveDirection();
|
|
174
|
-
menu.setAttribute("aria-hidden", "false");
|
|
175
|
-
trigger?.setAttribute("aria-expanded", "true");
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
const closeMenu = () => {
|
|
179
|
-
menu.setAttribute("aria-hidden", "true");
|
|
180
|
-
trigger?.setAttribute("aria-expanded", "false");
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
const toggleMenu = () => {
|
|
184
|
-
if (menu.getAttribute("aria-hidden") === "false") {
|
|
185
|
-
closeMenu();
|
|
186
|
-
} else {
|
|
187
|
-
openMenu();
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
trigger?.addEventListener("click", (event) => {
|
|
192
|
-
event.preventDefault();
|
|
193
|
-
event.stopPropagation();
|
|
194
|
-
toggleMenu();
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
document.addEventListener("click", (event) => {
|
|
198
|
-
if (!elem.contains(event.target)) {
|
|
199
|
-
closeMenu();
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
elem.addEventListener("keydown", (event) => {
|
|
204
|
-
if (event.key === "Escape") {
|
|
205
|
-
closeMenu();
|
|
206
|
-
trigger?.focus();
|
|
207
|
-
}
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
elem.addEventListener("focusout", (event) => {
|
|
211
|
-
if (!event.relatedTarget || !elem.contains(event.relatedTarget)) {
|
|
212
|
-
closeMenu();
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function enhanceToggle(elem) {
|
|
218
|
-
if (elem.dataset.enhancedToggle) return;
|
|
219
|
-
elem.dataset.enhancedToggle = "true";
|
|
220
|
-
const checkbox = elem.querySelector('input[type="checkbox"]');
|
|
221
|
-
if (!checkbox) return;
|
|
222
|
-
|
|
223
|
-
if (!elem.hasAttribute("tabindex")) {
|
|
224
|
-
elem.setAttribute("tabindex", "0");
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
elem.setAttribute("role", "switch");
|
|
228
|
-
elem.setAttribute("aria-checked", checkbox.checked ? "true" : "false");
|
|
229
|
-
|
|
230
|
-
const toggleSwitch = document.createElement("span");
|
|
231
|
-
toggleSwitch.className = "toggle-switch";
|
|
232
|
-
toggleSwitch.setAttribute("role", "presentation");
|
|
233
|
-
toggleSwitch.setAttribute("aria-hidden", "true");
|
|
234
|
-
const knob = document.createElement("span");
|
|
235
|
-
knob.className = "toggle-knob";
|
|
236
|
-
toggleSwitch.appendChild(knob);
|
|
237
|
-
elem.insertBefore(toggleSwitch, checkbox.nextSibling);
|
|
238
|
-
|
|
239
|
-
const updateAria = () => {
|
|
240
|
-
elem.setAttribute("aria-checked", checkbox.checked ? "true" : "false");
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
const toggle = () => {
|
|
244
|
-
if (checkbox.disabled) return;
|
|
245
|
-
checkbox.checked = !checkbox.checked;
|
|
246
|
-
updateAria();
|
|
247
|
-
checkbox.dispatchEvent(new Event("change", { bubbles: true }));
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
elem.addEventListener("click", (event) => {
|
|
251
|
-
event.preventDefault();
|
|
252
|
-
toggle();
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
elem.addEventListener("keydown", (event) => {
|
|
256
|
-
if (event.key === " " || event.key === "Enter") {
|
|
257
|
-
event.preventDefault();
|
|
258
|
-
toggle();
|
|
259
|
-
}
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
checkbox.addEventListener("change", updateAria);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function enhanceRange(elem) {
|
|
266
|
-
if (elem.dataset.enhancedRange) return;
|
|
267
|
-
|
|
268
|
-
const label = elem.closest("label");
|
|
269
|
-
const hasRangeOutputClass = label?.classList.contains("range-output");
|
|
270
|
-
|
|
271
|
-
const inputId =
|
|
272
|
-
elem.id || `range-${Math.random().toString(36).substring(2, 11)}`;
|
|
273
|
-
const outputId = `${inputId}-output`;
|
|
274
|
-
elem.id = inputId;
|
|
275
|
-
|
|
276
|
-
if (hasRangeOutputClass) {
|
|
277
|
-
const labelSpan = label.querySelector("span");
|
|
278
|
-
if (labelSpan && !labelSpan.classList.contains("range-output-wrapper")) {
|
|
279
|
-
const wrapper = document.createElement("span");
|
|
280
|
-
wrapper.className = "range-output-wrapper";
|
|
281
|
-
wrapper.style.display = "flex";
|
|
282
|
-
wrapper.style.justifyContent = "space-between";
|
|
283
|
-
wrapper.style.alignItems = "center";
|
|
284
|
-
|
|
285
|
-
const textSpan = document.createElement("span");
|
|
286
|
-
textSpan.textContent = labelSpan.textContent;
|
|
287
|
-
wrapper.appendChild(textSpan);
|
|
288
|
-
|
|
289
|
-
const output = document.createElement("output");
|
|
290
|
-
output.id = outputId;
|
|
291
|
-
output.setAttribute("for", inputId);
|
|
292
|
-
output.style.color =
|
|
293
|
-
"var(--surface-text-secondary, var(--color-text-secondary))";
|
|
294
|
-
output.style.fontSize = "0.875rem";
|
|
295
|
-
output.textContent = elem.value;
|
|
296
|
-
wrapper.appendChild(output);
|
|
297
|
-
|
|
298
|
-
labelSpan.textContent = "";
|
|
299
|
-
labelSpan.appendChild(wrapper);
|
|
300
|
-
|
|
301
|
-
const updateOutput = () => {
|
|
302
|
-
output.textContent = elem.value;
|
|
303
|
-
};
|
|
304
|
-
elem.addEventListener("input", updateOutput);
|
|
305
|
-
}
|
|
306
|
-
} else {
|
|
307
|
-
let container = elem.closest(".range-container");
|
|
308
|
-
if (!container) {
|
|
309
|
-
container = document.createElement("div");
|
|
310
|
-
container.className = "range-container";
|
|
311
|
-
elem.parentNode?.insertBefore(container, elem);
|
|
312
|
-
container.appendChild(elem);
|
|
313
|
-
}
|
|
314
|
-
container.style.position = "relative";
|
|
315
|
-
|
|
316
|
-
const bubble = document.createElement("output");
|
|
317
|
-
bubble.id = outputId;
|
|
318
|
-
bubble.setAttribute("for", inputId);
|
|
319
|
-
bubble.className = "range-bubble";
|
|
320
|
-
bubble.setAttribute("aria-live", "polite");
|
|
321
|
-
container.appendChild(bubble);
|
|
322
|
-
|
|
323
|
-
const updateBubble = () => {
|
|
324
|
-
const min = parseFloat(elem.min) || 0;
|
|
325
|
-
const max = parseFloat(elem.max) || 100;
|
|
326
|
-
const value = parseFloat(elem.value);
|
|
327
|
-
const pct = (value - min) / (max - min);
|
|
328
|
-
bubble.style.left = `calc(${pct * 100}% )`;
|
|
329
|
-
bubble.textContent = String(value);
|
|
330
|
-
};
|
|
331
|
-
const show = () => bubble.classList.add("visible");
|
|
332
|
-
const hide = () => bubble.classList.remove("visible");
|
|
333
|
-
elem.addEventListener("input", updateBubble);
|
|
334
|
-
elem.addEventListener("pointerdown", show);
|
|
335
|
-
elem.addEventListener("pointerup", hide);
|
|
336
|
-
elem.addEventListener("pointerleave", hide);
|
|
337
|
-
elem.addEventListener("focus", show);
|
|
338
|
-
elem.addEventListener("blur", hide);
|
|
339
|
-
updateBubble();
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
elem.dataset.enhancedRange = "1";
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function enhanceRequired(elem) {
|
|
346
|
-
|
|
347
|
-
if (elem.dataset.enhancedRequired) return;
|
|
348
|
-
elem.dataset.enhancedRequired = "true";
|
|
349
|
-
|
|
350
|
-
const enhanceRequiredField = (input) => {
|
|
351
|
-
|
|
352
|
-
const label = input.closest("label");
|
|
353
|
-
if (!label) return;
|
|
354
|
-
if (label.querySelector(".required-asterisk")) return;
|
|
355
|
-
|
|
356
|
-
const asterisk = document.createElement("span");
|
|
357
|
-
asterisk.classList.add("required-asterisk");
|
|
358
|
-
asterisk.textContent = "*";
|
|
359
|
-
asterisk.style.marginLeft = "4px";
|
|
360
|
-
label.querySelector("span").appendChild(asterisk);
|
|
361
|
-
|
|
362
|
-
const form = input.closest("form");
|
|
363
|
-
if (form && !form.querySelector(".required-legend")) {
|
|
364
|
-
const legend = document.createElement("small");
|
|
365
|
-
legend.classList.add("required-legend");
|
|
366
|
-
legend.textContent = "* Required fields";
|
|
367
|
-
form.insertBefore(
|
|
368
|
-
legend,
|
|
369
|
-
form.querySelector(".form-actions") || form.lastElementChild
|
|
370
|
-
);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
elem.querySelectorAll("[required]").forEach((input) => {
|
|
375
|
-
enhanceRequiredField(input);
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function enhanceOpenGroup(elem) {
|
|
381
|
-
if (elem.dataset.enhancedOpenGroup) return;
|
|
382
|
-
elem.dataset.enhancedOpenGroup = "true";
|
|
383
|
-
|
|
384
|
-
elem.classList.add("flex", "flex-wrap", "buttons");
|
|
385
|
-
|
|
386
|
-
const addInput = document.createElement("input");
|
|
387
|
-
addInput.type = "text";
|
|
388
|
-
addInput.placeholder = "Add item...";
|
|
389
|
-
addInput.classList.add("input-text", "input-sm");
|
|
390
|
-
addInput.style.width = "auto";
|
|
391
|
-
const firstInput = elem.querySelector(
|
|
392
|
-
'input[type="radio"], input[type="checkbox"]'
|
|
393
|
-
);
|
|
394
|
-
|
|
395
|
-
elem.appendChild(addInput);
|
|
396
|
-
addInput.addEventListener("keydown", (event) => {
|
|
397
|
-
if (event.key === "Enter" || event.key === "Tab") {
|
|
398
|
-
const value = addInput.value.trim();
|
|
399
|
-
if (value) {
|
|
400
|
-
event.preventDefault();
|
|
401
|
-
|
|
402
|
-
const type = firstInput.type === "radio" ? "radio" : "checkbox";
|
|
403
|
-
const id = `open-group-${Math.random()
|
|
404
|
-
.toString(36)
|
|
405
|
-
.substring(2, 11)}`;
|
|
406
|
-
const label = document.createElement("label");
|
|
407
|
-
|
|
408
|
-
const span = document.createElement("span");
|
|
409
|
-
span.setAttribute("data-label", "");
|
|
410
|
-
span.textContent = value;
|
|
411
|
-
|
|
412
|
-
const input = document.createElement("input");
|
|
413
|
-
input.type = type;
|
|
414
|
-
input.name =
|
|
415
|
-
firstInput.name || elem.getAttribute("data-name") || "open-group";
|
|
416
|
-
input.value = value;
|
|
417
|
-
input.id = id;
|
|
418
|
-
|
|
419
|
-
label.appendChild(span);
|
|
420
|
-
label.appendChild(input);
|
|
421
|
-
|
|
422
|
-
elem.insertBefore(label, addInput);
|
|
423
|
-
addInput.value = "";
|
|
424
|
-
}
|
|
425
|
-
} else if (event.key === "Backspace" && addInput.value === "") {
|
|
426
|
-
event.preventDefault();
|
|
427
|
-
const labels = elem.querySelectorAll("label");
|
|
428
|
-
if (labels.length > 0) {
|
|
429
|
-
const lastLabel = labels[labels.length - 1];
|
|
430
|
-
lastLabel.remove();
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
function enhanceButtonWorking(elem) {
|
|
437
|
-
if (elem.dataset.enhancedBtnWorking) return;
|
|
438
|
-
elem.dataset.enhancedBtnWorking = "true";
|
|
439
|
-
|
|
440
|
-
let originalIcon = null;
|
|
441
|
-
let addedIcon = false;
|
|
442
|
-
|
|
443
|
-
const observer = new MutationObserver((mutations) => {
|
|
444
|
-
mutations.forEach((mutation) => {
|
|
445
|
-
if (mutation.attributeName === "class") {
|
|
446
|
-
const hasWorking = elem.classList.contains("btn-working");
|
|
447
|
-
const icon = elem.querySelector("pds-icon");
|
|
448
|
-
|
|
449
|
-
if (hasWorking) {
|
|
450
|
-
if (icon) {
|
|
451
|
-
if (!originalIcon) {
|
|
452
|
-
originalIcon = icon.getAttribute("icon");
|
|
453
|
-
}
|
|
454
|
-
icon.setAttribute("icon", "circle-notch");
|
|
455
|
-
} else {
|
|
456
|
-
const newIcon = document.createElement("pds-icon");
|
|
457
|
-
newIcon.setAttribute("icon", "circle-notch");
|
|
458
|
-
newIcon.setAttribute("size", "sm");
|
|
459
|
-
elem.insertBefore(newIcon, elem.firstChild);
|
|
460
|
-
addedIcon = true;
|
|
461
|
-
}
|
|
462
|
-
} else if (mutation.oldValue?.includes("btn-working")) {
|
|
463
|
-
if (icon) {
|
|
464
|
-
if (addedIcon) {
|
|
465
|
-
icon.remove();
|
|
466
|
-
addedIcon = false;
|
|
467
|
-
} else if (originalIcon) {
|
|
468
|
-
icon.setAttribute("icon", originalIcon);
|
|
469
|
-
originalIcon = null;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
});
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
observer.observe(elem, {
|
|
478
|
-
attributes: true,
|
|
479
|
-
attributeFilter: ["class"],
|
|
480
|
-
attributeOldValue: true,
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
// ============================================================================
|
|
485
|
-
// EXPORTS
|
|
486
|
-
// ============================================================================
|
|
487
|
-
|
|
488
|
-
// Map selectors to their run functions
|
|
489
|
-
const enhancerRunners = new Map([
|
|
490
|
-
[".accordion", enhanceAccordion],
|
|
491
|
-
["nav[data-dropdown]", enhanceDropdown],
|
|
492
|
-
["label[data-toggle]", enhanceToggle],
|
|
493
|
-
['input[type="range"]', enhanceRange],
|
|
494
|
-
["form[data-required]", enhanceRequired],
|
|
495
|
-
["fieldset[role=group][data-open]", enhanceOpenGroup],
|
|
496
|
-
["button, a[class*='btn-']", enhanceButtonWorking],
|
|
497
|
-
]);
|
|
498
|
-
|
|
499
|
-
/**
|
|
500
|
-
* Complete enhancers with runtime functions.
|
|
501
|
-
* Used by PDS.enhancer() and AutoDefiner at runtime.
|
|
502
|
-
*
|
|
503
|
-
* This is the canonical runtime array of enhancer objects.
|
|
504
|
-
*/
|
|
505
|
-
export const defaultPDSEnhancers = enhancerDefinitions.map((meta) => ({
|
|
506
|
-
...meta,
|
|
507
|
-
run: enhancerRunners.get(meta.selector) || (() => {}),
|
|
508
|
-
}));
|
|
509
|
-
|
|
510
|
-
/**
|
|
511
|
-
* Metadata-only export for build tools and documentation.
|
|
512
|
-
* This is semantically identical to enhancerDefinitions but exported
|
|
513
|
-
* for tooling that wants to explicitly access metadata without run functions.
|
|
514
|
-
*
|
|
515
|
-
* Build tools can safely import defaultPDSEnhancers too - Node.js won't
|
|
516
|
-
* execute browser-only DOM code in the run functions.
|
|
517
|
-
*/
|
|
518
|
-
export const defaultPDSEnhancerMetadata = enhancerDefinitions;
|
|
1
|
+
/**
|
|
2
|
+
* PDS Enhancers - Single Source of Truth
|
|
3
|
+
*
|
|
4
|
+
* This file defines all progressive enhancements for the Pure Design System.
|
|
5
|
+
* Each enhancer has:
|
|
6
|
+
* - selector: CSS selector to target elements
|
|
7
|
+
* - description: Human-readable explanation
|
|
8
|
+
* - demoHtml: Example usage markup
|
|
9
|
+
* - run: Enhancement function (added at the end)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// ENHANCEMENT METADATA DEFINITIONS
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
const enhancerDefinitions = [
|
|
17
|
+
{
|
|
18
|
+
selector: ".accordion",
|
|
19
|
+
description:
|
|
20
|
+
"Ensures only one <details> element can be open at a time within the accordion.",
|
|
21
|
+
demoHtml: `
|
|
22
|
+
<div class="accordion">
|
|
23
|
+
<details>
|
|
24
|
+
<summary>Section 1</summary>
|
|
25
|
+
<p>Content for section 1</p>
|
|
26
|
+
</details>
|
|
27
|
+
<details>
|
|
28
|
+
<summary>Section 2</summary>
|
|
29
|
+
<p>Content for section 2</p>
|
|
30
|
+
</details>
|
|
31
|
+
<details>
|
|
32
|
+
<summary>Section 3</summary>
|
|
33
|
+
<p>Content for section 3</p>
|
|
34
|
+
</details>
|
|
35
|
+
</div>
|
|
36
|
+
`.trim(),
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
selector: "nav[data-dropdown]",
|
|
40
|
+
description:
|
|
41
|
+
"Enhances a nav element with data-dropdown to function as a dropdown menu.",
|
|
42
|
+
demoHtml: `
|
|
43
|
+
<nav data-dropdown>
|
|
44
|
+
<button class="btn-primary">Menu</button>
|
|
45
|
+
<menu>
|
|
46
|
+
<li><a href="#">Item 1</a></li>
|
|
47
|
+
<li><a href="#">Item 2</a></li>
|
|
48
|
+
</menu>
|
|
49
|
+
</nav>
|
|
50
|
+
`.trim(),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
selector: "label[data-toggle]",
|
|
54
|
+
description: "Creates a toggle switch element from a checkbox.",
|
|
55
|
+
demoHtml: `
|
|
56
|
+
<label data-toggle>
|
|
57
|
+
<input type="checkbox">
|
|
58
|
+
<span data-label>Enable notifications</span>
|
|
59
|
+
</label>
|
|
60
|
+
`.trim(),
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
selector: 'input[type="range"]',
|
|
64
|
+
description: "Enhances range inputs with an attached <output>.",
|
|
65
|
+
demoHtml: `
|
|
66
|
+
<label class="range-output">
|
|
67
|
+
<span data-label>Volume</span>
|
|
68
|
+
<input type="range" min="0" max="100" value="40">
|
|
69
|
+
</label>
|
|
70
|
+
`.trim(),
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
selector: "form[data-required]",
|
|
74
|
+
description:
|
|
75
|
+
"Enhances required form fields using an asterisk in the label.",
|
|
76
|
+
demoHtml: `
|
|
77
|
+
<form data-required action="#" method="post">
|
|
78
|
+
<label>
|
|
79
|
+
<span>Field Label</span>
|
|
80
|
+
<input type="text" required>
|
|
81
|
+
</label>
|
|
82
|
+
<nav class="form-actions">
|
|
83
|
+
<button type="submit" class="btn-primary">Submit</button>
|
|
84
|
+
</nav>
|
|
85
|
+
</form>
|
|
86
|
+
`.trim(),
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
selector: "fieldset[role=group][data-open]",
|
|
90
|
+
description:
|
|
91
|
+
"Enhances a checkbox/radio group to be open (have a way to add and remove items).",
|
|
92
|
+
demoHtml: `
|
|
93
|
+
<fieldset role="group" data-open>
|
|
94
|
+
<label>
|
|
95
|
+
<span data-label>Test</span>
|
|
96
|
+
<input value="lala" name="test1" type="radio" />
|
|
97
|
+
</label>
|
|
98
|
+
</fieldset>
|
|
99
|
+
`.trim(),
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
selector: "button, a[class*='btn-']",
|
|
103
|
+
description:
|
|
104
|
+
"Automatically manages spinner icon for buttons with .btn-working class",
|
|
105
|
+
demoHtml: `
|
|
106
|
+
<button class="btn-primary btn-working">
|
|
107
|
+
<span>Saving</span>
|
|
108
|
+
</button>
|
|
109
|
+
`.trim(),
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
// ============================================================================
|
|
114
|
+
// ENHANCEMENT RUNTIME FUNCTIONS
|
|
115
|
+
// ============================================================================
|
|
116
|
+
|
|
117
|
+
function enhanceAccordion(elem) {
|
|
118
|
+
if (elem.dataset.enhancedAccordion) return;
|
|
119
|
+
elem.dataset.enhancedAccordion = "true";
|
|
120
|
+
|
|
121
|
+
elem.addEventListener("toggle", (event) => {
|
|
122
|
+
// Only handle toggle events from direct child details elements
|
|
123
|
+
// to avoid closing parent details when nested accordions are used
|
|
124
|
+
if (event.target.open && event.target.parentElement === elem) {
|
|
125
|
+
elem.querySelectorAll(":scope > details[open]").forEach((details) => {
|
|
126
|
+
if (details !== event.target) {
|
|
127
|
+
details.open = false;
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}, true);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function enhanceDropdown(elem) {
|
|
135
|
+
if (elem.dataset.enhancedDropdown) return;
|
|
136
|
+
elem.dataset.enhancedDropdown = "true";
|
|
137
|
+
const menu = elem.querySelector("menu");
|
|
138
|
+
if (!menu) return;
|
|
139
|
+
|
|
140
|
+
const trigger =
|
|
141
|
+
elem.querySelector("[data-dropdown-toggle]") ||
|
|
142
|
+
elem.querySelector("button");
|
|
143
|
+
|
|
144
|
+
if (trigger && !trigger.hasAttribute("type")) {
|
|
145
|
+
trigger.setAttribute("type", "button");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!menu.id) {
|
|
149
|
+
menu.id = `dropdown-${Math.random().toString(36).slice(2, 9)}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
menu.setAttribute("role", menu.getAttribute("role") || "menu");
|
|
153
|
+
if (!menu.hasAttribute("aria-hidden")) {
|
|
154
|
+
menu.setAttribute("aria-hidden", "true");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (trigger) {
|
|
158
|
+
trigger.setAttribute("aria-haspopup", "true");
|
|
159
|
+
trigger.setAttribute("aria-controls", menu.id);
|
|
160
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const resolveDirection = () => {
|
|
164
|
+
const mode = (elem.getAttribute("data-mode") || "auto").toLowerCase();
|
|
165
|
+
if (mode === "up" || mode === "down") return mode;
|
|
166
|
+
const rect = elem.getBoundingClientRect();
|
|
167
|
+
const spaceBelow = Math.max(0, window.innerHeight - rect.bottom);
|
|
168
|
+
const spaceAbove = Math.max(0, rect.top);
|
|
169
|
+
return spaceAbove > spaceBelow ? "up" : "down";
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const openMenu = () => {
|
|
173
|
+
elem.dataset.dropdownDirection = resolveDirection();
|
|
174
|
+
menu.setAttribute("aria-hidden", "false");
|
|
175
|
+
trigger?.setAttribute("aria-expanded", "true");
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const closeMenu = () => {
|
|
179
|
+
menu.setAttribute("aria-hidden", "true");
|
|
180
|
+
trigger?.setAttribute("aria-expanded", "false");
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const toggleMenu = () => {
|
|
184
|
+
if (menu.getAttribute("aria-hidden") === "false") {
|
|
185
|
+
closeMenu();
|
|
186
|
+
} else {
|
|
187
|
+
openMenu();
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
trigger?.addEventListener("click", (event) => {
|
|
192
|
+
event.preventDefault();
|
|
193
|
+
event.stopPropagation();
|
|
194
|
+
toggleMenu();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
document.addEventListener("click", (event) => {
|
|
198
|
+
if (!elem.contains(event.target)) {
|
|
199
|
+
closeMenu();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
elem.addEventListener("keydown", (event) => {
|
|
204
|
+
if (event.key === "Escape") {
|
|
205
|
+
closeMenu();
|
|
206
|
+
trigger?.focus();
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
elem.addEventListener("focusout", (event) => {
|
|
211
|
+
if (!event.relatedTarget || !elem.contains(event.relatedTarget)) {
|
|
212
|
+
closeMenu();
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function enhanceToggle(elem) {
|
|
218
|
+
if (elem.dataset.enhancedToggle) return;
|
|
219
|
+
elem.dataset.enhancedToggle = "true";
|
|
220
|
+
const checkbox = elem.querySelector('input[type="checkbox"]');
|
|
221
|
+
if (!checkbox) return;
|
|
222
|
+
|
|
223
|
+
if (!elem.hasAttribute("tabindex")) {
|
|
224
|
+
elem.setAttribute("tabindex", "0");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
elem.setAttribute("role", "switch");
|
|
228
|
+
elem.setAttribute("aria-checked", checkbox.checked ? "true" : "false");
|
|
229
|
+
|
|
230
|
+
const toggleSwitch = document.createElement("span");
|
|
231
|
+
toggleSwitch.className = "toggle-switch";
|
|
232
|
+
toggleSwitch.setAttribute("role", "presentation");
|
|
233
|
+
toggleSwitch.setAttribute("aria-hidden", "true");
|
|
234
|
+
const knob = document.createElement("span");
|
|
235
|
+
knob.className = "toggle-knob";
|
|
236
|
+
toggleSwitch.appendChild(knob);
|
|
237
|
+
elem.insertBefore(toggleSwitch, checkbox.nextSibling);
|
|
238
|
+
|
|
239
|
+
const updateAria = () => {
|
|
240
|
+
elem.setAttribute("aria-checked", checkbox.checked ? "true" : "false");
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const toggle = () => {
|
|
244
|
+
if (checkbox.disabled) return;
|
|
245
|
+
checkbox.checked = !checkbox.checked;
|
|
246
|
+
updateAria();
|
|
247
|
+
checkbox.dispatchEvent(new Event("change", { bubbles: true }));
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
elem.addEventListener("click", (event) => {
|
|
251
|
+
event.preventDefault();
|
|
252
|
+
toggle();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
elem.addEventListener("keydown", (event) => {
|
|
256
|
+
if (event.key === " " || event.key === "Enter") {
|
|
257
|
+
event.preventDefault();
|
|
258
|
+
toggle();
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
checkbox.addEventListener("change", updateAria);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function enhanceRange(elem) {
|
|
266
|
+
if (elem.dataset.enhancedRange) return;
|
|
267
|
+
|
|
268
|
+
const label = elem.closest("label");
|
|
269
|
+
const hasRangeOutputClass = label?.classList.contains("range-output");
|
|
270
|
+
|
|
271
|
+
const inputId =
|
|
272
|
+
elem.id || `range-${Math.random().toString(36).substring(2, 11)}`;
|
|
273
|
+
const outputId = `${inputId}-output`;
|
|
274
|
+
elem.id = inputId;
|
|
275
|
+
|
|
276
|
+
if (hasRangeOutputClass) {
|
|
277
|
+
const labelSpan = label.querySelector("span");
|
|
278
|
+
if (labelSpan && !labelSpan.classList.contains("range-output-wrapper")) {
|
|
279
|
+
const wrapper = document.createElement("span");
|
|
280
|
+
wrapper.className = "range-output-wrapper";
|
|
281
|
+
wrapper.style.display = "flex";
|
|
282
|
+
wrapper.style.justifyContent = "space-between";
|
|
283
|
+
wrapper.style.alignItems = "center";
|
|
284
|
+
|
|
285
|
+
const textSpan = document.createElement("span");
|
|
286
|
+
textSpan.textContent = labelSpan.textContent;
|
|
287
|
+
wrapper.appendChild(textSpan);
|
|
288
|
+
|
|
289
|
+
const output = document.createElement("output");
|
|
290
|
+
output.id = outputId;
|
|
291
|
+
output.setAttribute("for", inputId);
|
|
292
|
+
output.style.color =
|
|
293
|
+
"var(--surface-text-secondary, var(--color-text-secondary))";
|
|
294
|
+
output.style.fontSize = "0.875rem";
|
|
295
|
+
output.textContent = elem.value;
|
|
296
|
+
wrapper.appendChild(output);
|
|
297
|
+
|
|
298
|
+
labelSpan.textContent = "";
|
|
299
|
+
labelSpan.appendChild(wrapper);
|
|
300
|
+
|
|
301
|
+
const updateOutput = () => {
|
|
302
|
+
output.textContent = elem.value;
|
|
303
|
+
};
|
|
304
|
+
elem.addEventListener("input", updateOutput);
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
let container = elem.closest(".range-container");
|
|
308
|
+
if (!container) {
|
|
309
|
+
container = document.createElement("div");
|
|
310
|
+
container.className = "range-container";
|
|
311
|
+
elem.parentNode?.insertBefore(container, elem);
|
|
312
|
+
container.appendChild(elem);
|
|
313
|
+
}
|
|
314
|
+
container.style.position = "relative";
|
|
315
|
+
|
|
316
|
+
const bubble = document.createElement("output");
|
|
317
|
+
bubble.id = outputId;
|
|
318
|
+
bubble.setAttribute("for", inputId);
|
|
319
|
+
bubble.className = "range-bubble";
|
|
320
|
+
bubble.setAttribute("aria-live", "polite");
|
|
321
|
+
container.appendChild(bubble);
|
|
322
|
+
|
|
323
|
+
const updateBubble = () => {
|
|
324
|
+
const min = parseFloat(elem.min) || 0;
|
|
325
|
+
const max = parseFloat(elem.max) || 100;
|
|
326
|
+
const value = parseFloat(elem.value);
|
|
327
|
+
const pct = (value - min) / (max - min);
|
|
328
|
+
bubble.style.left = `calc(${pct * 100}% )`;
|
|
329
|
+
bubble.textContent = String(value);
|
|
330
|
+
};
|
|
331
|
+
const show = () => bubble.classList.add("visible");
|
|
332
|
+
const hide = () => bubble.classList.remove("visible");
|
|
333
|
+
elem.addEventListener("input", updateBubble);
|
|
334
|
+
elem.addEventListener("pointerdown", show);
|
|
335
|
+
elem.addEventListener("pointerup", hide);
|
|
336
|
+
elem.addEventListener("pointerleave", hide);
|
|
337
|
+
elem.addEventListener("focus", show);
|
|
338
|
+
elem.addEventListener("blur", hide);
|
|
339
|
+
updateBubble();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
elem.dataset.enhancedRange = "1";
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function enhanceRequired(elem) {
|
|
346
|
+
|
|
347
|
+
if (elem.dataset.enhancedRequired) return;
|
|
348
|
+
elem.dataset.enhancedRequired = "true";
|
|
349
|
+
|
|
350
|
+
const enhanceRequiredField = (input) => {
|
|
351
|
+
|
|
352
|
+
const label = input.closest("label");
|
|
353
|
+
if (!label) return;
|
|
354
|
+
if (label.querySelector(".required-asterisk")) return;
|
|
355
|
+
|
|
356
|
+
const asterisk = document.createElement("span");
|
|
357
|
+
asterisk.classList.add("required-asterisk");
|
|
358
|
+
asterisk.textContent = "*";
|
|
359
|
+
asterisk.style.marginLeft = "4px";
|
|
360
|
+
label.querySelector("span").appendChild(asterisk);
|
|
361
|
+
|
|
362
|
+
const form = input.closest("form");
|
|
363
|
+
if (form && !form.querySelector(".required-legend")) {
|
|
364
|
+
const legend = document.createElement("small");
|
|
365
|
+
legend.classList.add("required-legend");
|
|
366
|
+
legend.textContent = "* Required fields";
|
|
367
|
+
form.insertBefore(
|
|
368
|
+
legend,
|
|
369
|
+
form.querySelector(".form-actions") || form.lastElementChild
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
elem.querySelectorAll("[required]").forEach((input) => {
|
|
375
|
+
enhanceRequiredField(input);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function enhanceOpenGroup(elem) {
|
|
381
|
+
if (elem.dataset.enhancedOpenGroup) return;
|
|
382
|
+
elem.dataset.enhancedOpenGroup = "true";
|
|
383
|
+
|
|
384
|
+
elem.classList.add("flex", "flex-wrap", "buttons");
|
|
385
|
+
|
|
386
|
+
const addInput = document.createElement("input");
|
|
387
|
+
addInput.type = "text";
|
|
388
|
+
addInput.placeholder = "Add item...";
|
|
389
|
+
addInput.classList.add("input-text", "input-sm");
|
|
390
|
+
addInput.style.width = "auto";
|
|
391
|
+
const firstInput = elem.querySelector(
|
|
392
|
+
'input[type="radio"], input[type="checkbox"]'
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
elem.appendChild(addInput);
|
|
396
|
+
addInput.addEventListener("keydown", (event) => {
|
|
397
|
+
if (event.key === "Enter" || event.key === "Tab") {
|
|
398
|
+
const value = addInput.value.trim();
|
|
399
|
+
if (value) {
|
|
400
|
+
event.preventDefault();
|
|
401
|
+
|
|
402
|
+
const type = firstInput.type === "radio" ? "radio" : "checkbox";
|
|
403
|
+
const id = `open-group-${Math.random()
|
|
404
|
+
.toString(36)
|
|
405
|
+
.substring(2, 11)}`;
|
|
406
|
+
const label = document.createElement("label");
|
|
407
|
+
|
|
408
|
+
const span = document.createElement("span");
|
|
409
|
+
span.setAttribute("data-label", "");
|
|
410
|
+
span.textContent = value;
|
|
411
|
+
|
|
412
|
+
const input = document.createElement("input");
|
|
413
|
+
input.type = type;
|
|
414
|
+
input.name =
|
|
415
|
+
firstInput.name || elem.getAttribute("data-name") || "open-group";
|
|
416
|
+
input.value = value;
|
|
417
|
+
input.id = id;
|
|
418
|
+
|
|
419
|
+
label.appendChild(span);
|
|
420
|
+
label.appendChild(input);
|
|
421
|
+
|
|
422
|
+
elem.insertBefore(label, addInput);
|
|
423
|
+
addInput.value = "";
|
|
424
|
+
}
|
|
425
|
+
} else if (event.key === "Backspace" && addInput.value === "") {
|
|
426
|
+
event.preventDefault();
|
|
427
|
+
const labels = elem.querySelectorAll("label");
|
|
428
|
+
if (labels.length > 0) {
|
|
429
|
+
const lastLabel = labels[labels.length - 1];
|
|
430
|
+
lastLabel.remove();
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function enhanceButtonWorking(elem) {
|
|
437
|
+
if (elem.dataset.enhancedBtnWorking) return;
|
|
438
|
+
elem.dataset.enhancedBtnWorking = "true";
|
|
439
|
+
|
|
440
|
+
let originalIcon = null;
|
|
441
|
+
let addedIcon = false;
|
|
442
|
+
|
|
443
|
+
const observer = new MutationObserver((mutations) => {
|
|
444
|
+
mutations.forEach((mutation) => {
|
|
445
|
+
if (mutation.attributeName === "class") {
|
|
446
|
+
const hasWorking = elem.classList.contains("btn-working");
|
|
447
|
+
const icon = elem.querySelector("pds-icon");
|
|
448
|
+
|
|
449
|
+
if (hasWorking) {
|
|
450
|
+
if (icon) {
|
|
451
|
+
if (!originalIcon) {
|
|
452
|
+
originalIcon = icon.getAttribute("icon");
|
|
453
|
+
}
|
|
454
|
+
icon.setAttribute("icon", "circle-notch");
|
|
455
|
+
} else {
|
|
456
|
+
const newIcon = document.createElement("pds-icon");
|
|
457
|
+
newIcon.setAttribute("icon", "circle-notch");
|
|
458
|
+
newIcon.setAttribute("size", "sm");
|
|
459
|
+
elem.insertBefore(newIcon, elem.firstChild);
|
|
460
|
+
addedIcon = true;
|
|
461
|
+
}
|
|
462
|
+
} else if (mutation.oldValue?.includes("btn-working")) {
|
|
463
|
+
if (icon) {
|
|
464
|
+
if (addedIcon) {
|
|
465
|
+
icon.remove();
|
|
466
|
+
addedIcon = false;
|
|
467
|
+
} else if (originalIcon) {
|
|
468
|
+
icon.setAttribute("icon", originalIcon);
|
|
469
|
+
originalIcon = null;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
observer.observe(elem, {
|
|
478
|
+
attributes: true,
|
|
479
|
+
attributeFilter: ["class"],
|
|
480
|
+
attributeOldValue: true,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ============================================================================
|
|
485
|
+
// EXPORTS
|
|
486
|
+
// ============================================================================
|
|
487
|
+
|
|
488
|
+
// Map selectors to their run functions
|
|
489
|
+
const enhancerRunners = new Map([
|
|
490
|
+
[".accordion", enhanceAccordion],
|
|
491
|
+
["nav[data-dropdown]", enhanceDropdown],
|
|
492
|
+
["label[data-toggle]", enhanceToggle],
|
|
493
|
+
['input[type="range"]', enhanceRange],
|
|
494
|
+
["form[data-required]", enhanceRequired],
|
|
495
|
+
["fieldset[role=group][data-open]", enhanceOpenGroup],
|
|
496
|
+
["button, a[class*='btn-']", enhanceButtonWorking],
|
|
497
|
+
]);
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Complete enhancers with runtime functions.
|
|
501
|
+
* Used by PDS.enhancer() and AutoDefiner at runtime.
|
|
502
|
+
*
|
|
503
|
+
* This is the canonical runtime array of enhancer objects.
|
|
504
|
+
*/
|
|
505
|
+
export const defaultPDSEnhancers = enhancerDefinitions.map((meta) => ({
|
|
506
|
+
...meta,
|
|
507
|
+
run: enhancerRunners.get(meta.selector) || (() => {}),
|
|
508
|
+
}));
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Metadata-only export for build tools and documentation.
|
|
512
|
+
* This is semantically identical to enhancerDefinitions but exported
|
|
513
|
+
* for tooling that wants to explicitly access metadata without run functions.
|
|
514
|
+
*
|
|
515
|
+
* Build tools can safely import defaultPDSEnhancers too - Node.js won't
|
|
516
|
+
* execute browser-only DOM code in the run functions.
|
|
517
|
+
*/
|
|
518
|
+
export const defaultPDSEnhancerMetadata = enhancerDefinitions;
|