@hortonstudio/main 1.9.6 → 1.9.8
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/autoInit/accessibility/README.md +94 -0
- package/autoInit/accessibility/accessibility.js +52 -0
- package/autoInit/accessibility/functions/blog-remover/README.md +61 -0
- package/autoInit/accessibility/functions/blog-remover/blog-remover.js +31 -0
- package/autoInit/accessibility/functions/click-forwarding/README.md +60 -0
- package/autoInit/accessibility/functions/click-forwarding/click-forwarding.js +82 -0
- package/autoInit/accessibility/functions/dropdown/README.md +212 -0
- package/autoInit/accessibility/functions/dropdown/dropdown.js +167 -0
- package/autoInit/accessibility/functions/list-accessibility/README.md +56 -0
- package/autoInit/accessibility/functions/list-accessibility/list-accessibility.js +23 -0
- package/autoInit/accessibility/functions/text-synchronization/README.md +62 -0
- package/autoInit/accessibility/functions/text-synchronization/text-synchronization.js +101 -0
- package/autoInit/accessibility/functions/toc/README.md +79 -0
- package/autoInit/accessibility/functions/toc/toc.js +191 -0
- package/autoInit/accessibility/functions/year-replacement/README.md +54 -0
- package/autoInit/accessibility/functions/year-replacement/year-replacement.js +43 -0
- package/autoInit/button/README.md +122 -0
- package/autoInit/counter/README.md +274 -0
- package/autoInit/{counter.js → counter/counter.js} +20 -5
- package/autoInit/form/README.md +338 -0
- package/autoInit/{form.js → form/form.js} +44 -29
- package/autoInit/navbar/README.md +366 -0
- package/autoInit/site-settings/README.md +218 -0
- package/autoInit/smooth-scroll/README.md +386 -0
- package/autoInit/transition/README.md +301 -0
- package/autoInit/{transition.js → transition/transition.js} +13 -2
- package/index.js +7 -7
- package/package.json +1 -1
- package/autoInit/accessibility.js +0 -786
- /package/autoInit/{button.js → button/button.js} +0 -0
- /package/autoInit/{site-settings.js → site-settings/site-settings.js} +0 -0
- /package/autoInit/{smooth-scroll.js → smooth-scroll/smooth-scroll.js} +0 -0
|
@@ -1,786 +0,0 @@
|
|
|
1
|
-
export function init() {
|
|
2
|
-
// Centralized cleanup tracking
|
|
3
|
-
const cleanup = {
|
|
4
|
-
observers: [],
|
|
5
|
-
handlers: [],
|
|
6
|
-
scrollTimeout: null
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
const addObserver = (observer) => cleanup.observers.push(observer);
|
|
10
|
-
const addHandler = (element, event, handler, options) => {
|
|
11
|
-
element.addEventListener(event, handler, options);
|
|
12
|
-
cleanup.handlers.push({ element, event, handler, options });
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
function setupBlogListCleanup() {
|
|
16
|
-
const wrappers = document.querySelectorAll('[data-site-blog="wrapper"]');
|
|
17
|
-
|
|
18
|
-
wrappers.forEach(wrapper => {
|
|
19
|
-
// Check if wrapper has the delete-if-no-list config
|
|
20
|
-
const shouldDelete = wrapper.getAttribute('data-site-blog-config') === 'delete-if-no-list';
|
|
21
|
-
|
|
22
|
-
if (!shouldDelete) {
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Check if there's a descendant with data-site-blog="list"
|
|
27
|
-
const hasList = wrapper.querySelector('[data-site-blog="list"]') !== null;
|
|
28
|
-
|
|
29
|
-
// Delete wrapper if it doesn't have a list
|
|
30
|
-
if (!hasList) {
|
|
31
|
-
wrapper.remove();
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function setupGeneralAccessibility() {
|
|
37
|
-
setupListAccessibility();
|
|
38
|
-
setupRemoveListAccessibility();
|
|
39
|
-
setupFAQAccessibility(addHandler);
|
|
40
|
-
setupConvertToSpan();
|
|
41
|
-
setupYearReplacement();
|
|
42
|
-
setupPreventDefault(addHandler);
|
|
43
|
-
setupRichTextAccessibility(addObserver, addHandler, cleanup);
|
|
44
|
-
setupSummaryAccessibility(addHandler);
|
|
45
|
-
setupCustomValuesReplacement();
|
|
46
|
-
setupClickForwarding(addHandler);
|
|
47
|
-
setupTextSynchronization(addObserver);
|
|
48
|
-
setupBlogListCleanup();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function setupListAccessibility() {
|
|
52
|
-
const listElements = document.querySelectorAll('[data-hs-a11y="list"]');
|
|
53
|
-
const listItemElements = document.querySelectorAll('[data-hs-a11y="list-item"]');
|
|
54
|
-
|
|
55
|
-
listElements.forEach(element => {
|
|
56
|
-
element.setAttribute('role', 'list');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
listItemElements.forEach(element => {
|
|
60
|
-
element.setAttribute('role', 'listitem');
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function setupRemoveListAccessibility() {
|
|
65
|
-
const containers = document.querySelectorAll('[data-hs-a11y="remove-list"]');
|
|
66
|
-
|
|
67
|
-
containers.forEach(container => {
|
|
68
|
-
// Remove role="list" and role="listitem" from container and all descendants
|
|
69
|
-
const elementsWithListRoles = container.querySelectorAll('[role="list"], [role="listitem"]');
|
|
70
|
-
elementsWithListRoles.forEach(element => {
|
|
71
|
-
element.removeAttribute('role');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// Also remove from container itself if it has these roles
|
|
75
|
-
if (container.getAttribute('role') === 'list' || container.getAttribute('role') === 'listitem') {
|
|
76
|
-
container.removeAttribute('role');
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Convert semantic lists to divs in container and all descendants
|
|
80
|
-
const listsToConvert = container.querySelectorAll('ul, ol, li');
|
|
81
|
-
listsToConvert.forEach(listElement => {
|
|
82
|
-
const newDiv = document.createElement('div');
|
|
83
|
-
|
|
84
|
-
// Copy all attributes except role
|
|
85
|
-
Array.from(listElement.attributes).forEach(attr => {
|
|
86
|
-
if (attr.name !== 'role') {
|
|
87
|
-
newDiv.setAttribute(attr.name, attr.value);
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
// Move all child nodes
|
|
92
|
-
while (listElement.firstChild) {
|
|
93
|
-
newDiv.appendChild(listElement.firstChild);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Replace the element
|
|
97
|
-
listElement.parentNode.replaceChild(newDiv, listElement);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
// Convert container itself if it's a semantic list
|
|
101
|
-
if (container.tagName.toLowerCase() === 'ul' || container.tagName.toLowerCase() === 'ol' || container.tagName.toLowerCase() === 'li') {
|
|
102
|
-
const newDiv = document.createElement('div');
|
|
103
|
-
|
|
104
|
-
// Copy all attributes except data-hs-a11y and role
|
|
105
|
-
Array.from(container.attributes).forEach(attr => {
|
|
106
|
-
if (attr.name !== 'data-hs-a11y' && attr.name !== 'role') {
|
|
107
|
-
newDiv.setAttribute(attr.name, attr.value);
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// Move all child nodes
|
|
112
|
-
while (container.firstChild) {
|
|
113
|
-
newDiv.appendChild(container.firstChild);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Replace the container
|
|
117
|
-
container.parentNode.replaceChild(newDiv, container);
|
|
118
|
-
} else {
|
|
119
|
-
// Just remove the attribute if container isn't a semantic list
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function setupFAQAccessibility(addHandler) {
|
|
125
|
-
const faqContainers = document.querySelectorAll('[data-hs-a11y="faq-wrap"]');
|
|
126
|
-
|
|
127
|
-
faqContainers.forEach((container, index) => {
|
|
128
|
-
const button = container.querySelector('[data-hs-a11y="faq-btn"]');
|
|
129
|
-
const contentWrapper = container.querySelector('[data-hs-a11y="faq-content"]');
|
|
130
|
-
|
|
131
|
-
if (!button || !contentWrapper) return;
|
|
132
|
-
|
|
133
|
-
const buttonId = `faq-button-${index}`;
|
|
134
|
-
const contentId = `faq-content-${index}`;
|
|
135
|
-
|
|
136
|
-
button.setAttribute('id', buttonId);
|
|
137
|
-
button.setAttribute('aria-expanded', 'false');
|
|
138
|
-
button.setAttribute('aria-controls', contentId);
|
|
139
|
-
|
|
140
|
-
contentWrapper.setAttribute('id', contentId);
|
|
141
|
-
contentWrapper.setAttribute('aria-hidden', 'true');
|
|
142
|
-
contentWrapper.setAttribute('role', 'region');
|
|
143
|
-
contentWrapper.setAttribute('aria-labelledby', buttonId);
|
|
144
|
-
|
|
145
|
-
function toggleFAQ() {
|
|
146
|
-
const isOpen = button.getAttribute('aria-expanded') === 'true';
|
|
147
|
-
|
|
148
|
-
button.setAttribute('aria-expanded', !isOpen);
|
|
149
|
-
contentWrapper.setAttribute('aria-hidden', isOpen);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
addHandler(button, 'click', toggleFAQ);
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function setupConvertToSpan() {
|
|
157
|
-
const containers = document.querySelectorAll('[data-hs-a11y="convert-span"]');
|
|
158
|
-
|
|
159
|
-
containers.forEach(container => {
|
|
160
|
-
const skipTags = [
|
|
161
|
-
'span', 'a', 'button', 'input', 'textarea', 'select', 'img', 'video', 'audio',
|
|
162
|
-
'iframe', 'object', 'embed', 'canvas', 'svg', 'form', 'table', 'thead', 'tbody',
|
|
163
|
-
'tr', 'td', 'th', 'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'h1', 'h2', 'h3', 'h4',
|
|
164
|
-
'h5', 'h6', 'script', 'style', 'link', 'meta', 'title', 'head', 'html', 'body'
|
|
165
|
-
];
|
|
166
|
-
|
|
167
|
-
// Convert all child elements first
|
|
168
|
-
const elementsToConvert = container.querySelectorAll('*');
|
|
169
|
-
|
|
170
|
-
elementsToConvert.forEach(element => {
|
|
171
|
-
const tagName = element.tagName.toLowerCase();
|
|
172
|
-
|
|
173
|
-
if (!skipTags.includes(tagName)) {
|
|
174
|
-
const newSpan = document.createElement('span');
|
|
175
|
-
|
|
176
|
-
// Copy all attributes except data-hs-a11y
|
|
177
|
-
Array.from(element.attributes).forEach(attr => {
|
|
178
|
-
if (attr.name !== 'data-hs-a11y') {
|
|
179
|
-
newSpan.setAttribute(attr.name, attr.value);
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
// Move all child nodes
|
|
184
|
-
while (element.firstChild) {
|
|
185
|
-
newSpan.appendChild(element.firstChild);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Replace the element
|
|
189
|
-
element.parentNode.replaceChild(newSpan, element);
|
|
190
|
-
}
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
// Convert the container itself to span
|
|
194
|
-
const containerTagName = container.tagName.toLowerCase();
|
|
195
|
-
if (!skipTags.includes(containerTagName)) {
|
|
196
|
-
const newSpan = document.createElement('span');
|
|
197
|
-
|
|
198
|
-
// Copy all attributes except data-hs-a11y
|
|
199
|
-
Array.from(container.attributes).forEach(attr => {
|
|
200
|
-
if (attr.name !== 'data-hs-a11y') {
|
|
201
|
-
newSpan.setAttribute(attr.name, attr.value);
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
// Move all child nodes
|
|
206
|
-
while (container.firstChild) {
|
|
207
|
-
newSpan.appendChild(container.firstChild);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Replace the container
|
|
211
|
-
container.parentNode.replaceChild(newSpan, container);
|
|
212
|
-
} else {
|
|
213
|
-
// Just remove the attribute if container shouldn't be converted
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function setupYearReplacement() {
|
|
219
|
-
const currentYear = new Date().getFullYear().toString();
|
|
220
|
-
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
|
|
221
|
-
'July', 'August', 'September', 'October', 'November', 'December'];
|
|
222
|
-
const currentMonth = monthNames[new Date().getMonth()];
|
|
223
|
-
|
|
224
|
-
const walker = document.createTreeWalker(
|
|
225
|
-
document.body,
|
|
226
|
-
NodeFilter.SHOW_TEXT,
|
|
227
|
-
{
|
|
228
|
-
acceptNode: (node) => {
|
|
229
|
-
return (node.textContent.includes('{{year}}') || node.textContent.includes('{{month}}'))
|
|
230
|
-
? NodeFilter.FILTER_ACCEPT
|
|
231
|
-
: NodeFilter.FILTER_SKIP;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
const textNodes = [];
|
|
237
|
-
let node;
|
|
238
|
-
while (node = walker.nextNode()) {
|
|
239
|
-
textNodes.push(node);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
textNodes.forEach(textNode => {
|
|
243
|
-
let newText = textNode.textContent.replace(/\{\{year\}\}/gi, currentYear);
|
|
244
|
-
newText = newText.replace(/\{\{month\}\}/gi, currentMonth);
|
|
245
|
-
if (newText !== textNode.textContent) {
|
|
246
|
-
textNode.textContent = newText;
|
|
247
|
-
}
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function setupPreventDefault(addHandler) {
|
|
252
|
-
const elements = document.querySelectorAll('[data-hs-a11y="prevent-default"]');
|
|
253
|
-
|
|
254
|
-
elements.forEach(element => {
|
|
255
|
-
// Prevent click
|
|
256
|
-
const clickHandler = (e) => {
|
|
257
|
-
e.preventDefault();
|
|
258
|
-
e.stopPropagation();
|
|
259
|
-
return false;
|
|
260
|
-
};
|
|
261
|
-
addHandler(element, 'click', clickHandler);
|
|
262
|
-
|
|
263
|
-
// Prevent keyboard activation
|
|
264
|
-
const keydownHandler = (e) => {
|
|
265
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
266
|
-
e.preventDefault();
|
|
267
|
-
e.stopPropagation();
|
|
268
|
-
return false;
|
|
269
|
-
}
|
|
270
|
-
};
|
|
271
|
-
addHandler(element, 'keydown', keydownHandler);
|
|
272
|
-
|
|
273
|
-
// Additional prevention for anchor links
|
|
274
|
-
if (element.tagName.toLowerCase() === 'a') {
|
|
275
|
-
// Remove or modify href to prevent scroll
|
|
276
|
-
const originalHref = element.getAttribute('href');
|
|
277
|
-
if (originalHref && (originalHref === '#' || originalHref.startsWith('#'))) {
|
|
278
|
-
element.setAttribute('data-original-href', originalHref);
|
|
279
|
-
element.removeAttribute('href');
|
|
280
|
-
element.setAttribute('role', 'button');
|
|
281
|
-
element.setAttribute('tabindex', '0');
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function setupSummaryAccessibility(addHandler) {
|
|
288
|
-
const summaryContainers = document.querySelectorAll('[data-hs-a11y="summary-wrap"]');
|
|
289
|
-
|
|
290
|
-
summaryContainers.forEach((container, index) => {
|
|
291
|
-
const button = container.querySelector('[data-hs-a11y="summary-btn"]');
|
|
292
|
-
const contentWrapper = container.querySelector('[data-hs-a11y="summary-content"]');
|
|
293
|
-
|
|
294
|
-
if (!button || !contentWrapper) {
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const buttonId = `summary-button-${index}`;
|
|
299
|
-
const contentId = `summary-content-${index}`;
|
|
300
|
-
|
|
301
|
-
// Get original button text from first text node only
|
|
302
|
-
const walker = document.createTreeWalker(
|
|
303
|
-
button,
|
|
304
|
-
NodeFilter.SHOW_TEXT,
|
|
305
|
-
null,
|
|
306
|
-
false
|
|
307
|
-
);
|
|
308
|
-
|
|
309
|
-
let firstTextNode = walker.nextNode();
|
|
310
|
-
while (firstTextNode && !firstTextNode.textContent.trim()) {
|
|
311
|
-
firstTextNode = walker.nextNode();
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const originalButtonText = firstTextNode ? firstTextNode.textContent.trim() : button.textContent.trim();
|
|
315
|
-
|
|
316
|
-
// Function to update all text nodes in button
|
|
317
|
-
function updateButtonText(newText) {
|
|
318
|
-
// Find all text nodes and update them
|
|
319
|
-
const walker = document.createTreeWalker(
|
|
320
|
-
button,
|
|
321
|
-
NodeFilter.SHOW_TEXT,
|
|
322
|
-
null,
|
|
323
|
-
false
|
|
324
|
-
);
|
|
325
|
-
|
|
326
|
-
const textNodes = [];
|
|
327
|
-
let node;
|
|
328
|
-
while (node = walker.nextNode()) {
|
|
329
|
-
if (node.textContent.trim()) {
|
|
330
|
-
textNodes.push(node);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
textNodes.forEach(textNode => {
|
|
335
|
-
textNode.textContent = newText;
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
button.setAttribute('id', buttonId);
|
|
340
|
-
button.setAttribute('aria-expanded', 'false');
|
|
341
|
-
button.setAttribute('aria-controls', contentId);
|
|
342
|
-
button.setAttribute('aria-label', 'View Summary');
|
|
343
|
-
|
|
344
|
-
contentWrapper.setAttribute('id', contentId);
|
|
345
|
-
contentWrapper.setAttribute('aria-hidden', 'true');
|
|
346
|
-
contentWrapper.setAttribute('role', 'region');
|
|
347
|
-
contentWrapper.setAttribute('aria-labelledby', buttonId);
|
|
348
|
-
|
|
349
|
-
// Summary is closed by default - no need to check initial state
|
|
350
|
-
|
|
351
|
-
function toggleSummary() {
|
|
352
|
-
const isOpen = button.getAttribute('aria-expanded') === 'true';
|
|
353
|
-
|
|
354
|
-
if (isOpen) {
|
|
355
|
-
// Closing
|
|
356
|
-
button.setAttribute('aria-expanded', 'false');
|
|
357
|
-
button.setAttribute('aria-label', 'View Summary');
|
|
358
|
-
updateButtonText(originalButtonText);
|
|
359
|
-
contentWrapper.setAttribute('aria-hidden', 'true');
|
|
360
|
-
} else {
|
|
361
|
-
// Opening
|
|
362
|
-
button.setAttribute('aria-expanded', 'true');
|
|
363
|
-
button.setAttribute('aria-label', 'Close Summary');
|
|
364
|
-
updateButtonText('Close');
|
|
365
|
-
contentWrapper.setAttribute('aria-hidden', 'false');
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
addHandler(button, 'click', toggleSummary);
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function setupCustomValuesReplacement() {
|
|
374
|
-
const customValuesList = document.querySelector('[data-hs-a11y="custom-values-list"]');
|
|
375
|
-
|
|
376
|
-
if (!customValuesList) {
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// Collect all custom values data
|
|
381
|
-
const customValues = {};
|
|
382
|
-
const descendants = customValuesList.getElementsByTagName('*');
|
|
383
|
-
|
|
384
|
-
Array.from(descendants).forEach(descendant => {
|
|
385
|
-
const nameElement = descendant.querySelector('[data-hs-a11y="custom-values-name"]');
|
|
386
|
-
const valueElement = descendant.querySelector('[data-hs-a11y="custom-values-value"]');
|
|
387
|
-
|
|
388
|
-
if (nameElement && valueElement) {
|
|
389
|
-
const name = nameElement.textContent.trim();
|
|
390
|
-
const value = valueElement.textContent.trim();
|
|
391
|
-
|
|
392
|
-
if (name && value) {
|
|
393
|
-
customValues[name] = value;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
// If no custom values found, exit early
|
|
399
|
-
if (Object.keys(customValues).length === 0) {
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Replace text content efficiently using TreeWalker
|
|
404
|
-
const walker = document.createTreeWalker(
|
|
405
|
-
document.body,
|
|
406
|
-
NodeFilter.SHOW_TEXT,
|
|
407
|
-
{
|
|
408
|
-
acceptNode: (node) => {
|
|
409
|
-
// Check if any custom value names exist in the text content
|
|
410
|
-
const text = node.textContent;
|
|
411
|
-
for (const name in customValues) {
|
|
412
|
-
if (text.includes(`{{${name}}}`)) {
|
|
413
|
-
return NodeFilter.FILTER_ACCEPT;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
return NodeFilter.FILTER_SKIP;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
);
|
|
420
|
-
|
|
421
|
-
const textNodes = [];
|
|
422
|
-
let node;
|
|
423
|
-
while (node = walker.nextNode()) {
|
|
424
|
-
textNodes.push(node);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// Replace text in collected nodes
|
|
428
|
-
textNodes.forEach(textNode => {
|
|
429
|
-
let newText = textNode.textContent;
|
|
430
|
-
let hasChanges = false;
|
|
431
|
-
|
|
432
|
-
for (const name in customValues) {
|
|
433
|
-
const placeholder = `{{${name}}}`;
|
|
434
|
-
if (newText.includes(placeholder)) {
|
|
435
|
-
newText = newText.replace(new RegExp(placeholder, 'g'), customValues[name]);
|
|
436
|
-
hasChanges = true;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
if (hasChanges) {
|
|
441
|
-
textNode.textContent = newText;
|
|
442
|
-
}
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
// Replace link hrefs
|
|
446
|
-
const links = document.querySelectorAll('a[href]');
|
|
447
|
-
links.forEach(link => {
|
|
448
|
-
let href = link.getAttribute('href');
|
|
449
|
-
let hasChanges = false;
|
|
450
|
-
|
|
451
|
-
for (const name in customValues) {
|
|
452
|
-
const placeholder = `{{${name}}}`;
|
|
453
|
-
if (href.includes(placeholder)) {
|
|
454
|
-
href = href.replace(new RegExp(placeholder, 'g'), customValues[name]);
|
|
455
|
-
hasChanges = true;
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
if (hasChanges) {
|
|
460
|
-
link.setAttribute('href', href);
|
|
461
|
-
}
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
function setupClickForwarding(addHandler) {
|
|
466
|
-
// Find all clickable elements (custom styled elements users click)
|
|
467
|
-
const clickableElements = document.querySelectorAll('[data-hs-a11y*="clickable"]');
|
|
468
|
-
|
|
469
|
-
clickableElements.forEach(clickableElement => {
|
|
470
|
-
const attribute = clickableElement.getAttribute('data-hs-a11y');
|
|
471
|
-
|
|
472
|
-
// Parse the attribute: "click-trigger-[identifier], clickable"
|
|
473
|
-
const parts = attribute.split(',').map(part => part.trim());
|
|
474
|
-
|
|
475
|
-
// Find the part with click-trigger and the part with clickable
|
|
476
|
-
const triggerPart = parts.find(part => part.startsWith('click-trigger-'));
|
|
477
|
-
const rolePart = parts.find(part => part === 'clickable');
|
|
478
|
-
|
|
479
|
-
if (!triggerPart || !rolePart) {
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// Extract identifier from "click-trigger-[identifier]"
|
|
484
|
-
const identifier = triggerPart.replace('click-trigger-', '').trim();
|
|
485
|
-
|
|
486
|
-
// Find the corresponding trigger element
|
|
487
|
-
const triggerSelector = `[data-hs-a11y*="click-trigger-${identifier}"][data-hs-a11y*="trigger"]`;
|
|
488
|
-
const triggerElement = document.querySelector(triggerSelector);
|
|
489
|
-
|
|
490
|
-
if (!triggerElement) {
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// Add click event listener to forward clicks
|
|
495
|
-
const clickHandler = (event) => {
|
|
496
|
-
// Prevent default behavior on the clickable element
|
|
497
|
-
event.preventDefault();
|
|
498
|
-
event.stopPropagation();
|
|
499
|
-
|
|
500
|
-
// Trigger click on the target element
|
|
501
|
-
triggerElement.click();
|
|
502
|
-
};
|
|
503
|
-
addHandler(clickableElement, 'click', clickHandler);
|
|
504
|
-
|
|
505
|
-
// Also handle keyboard events for accessibility
|
|
506
|
-
const keydownHandler = (event) => {
|
|
507
|
-
if (event.key === 'Enter' || event.key === ' ') {
|
|
508
|
-
event.preventDefault();
|
|
509
|
-
event.stopPropagation();
|
|
510
|
-
triggerElement.click();
|
|
511
|
-
}
|
|
512
|
-
};
|
|
513
|
-
addHandler(clickableElement, 'keydown', keydownHandler);
|
|
514
|
-
|
|
515
|
-
// Ensure clickable element is keyboard accessible
|
|
516
|
-
if (!clickableElement.hasAttribute('tabindex')) {
|
|
517
|
-
clickableElement.setAttribute('tabindex', '0');
|
|
518
|
-
}
|
|
519
|
-
if (!clickableElement.hasAttribute('role')) {
|
|
520
|
-
clickableElement.setAttribute('role', 'button');
|
|
521
|
-
}
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
function setupTextSynchronization(addObserver) {
|
|
526
|
-
// Find all original elements (source of truth)
|
|
527
|
-
const originalElements = document.querySelectorAll('[data-hs-a11y*="original"]');
|
|
528
|
-
|
|
529
|
-
originalElements.forEach(originalElement => {
|
|
530
|
-
const attribute = originalElement.getAttribute('data-hs-a11y');
|
|
531
|
-
|
|
532
|
-
// Parse the attribute: "match-text-[identifier], original"
|
|
533
|
-
const parts = attribute.split(',').map(part => part.trim());
|
|
534
|
-
|
|
535
|
-
// Find the part with match-text and the part with original
|
|
536
|
-
const textPart = parts.find(part => part.startsWith('match-text-'));
|
|
537
|
-
const rolePart = parts.find(part => part === 'original');
|
|
538
|
-
|
|
539
|
-
if (!textPart || !rolePart) {
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
// Extract identifier from "match-text-[identifier]"
|
|
544
|
-
const identifier = textPart.replace('match-text-', '').trim();
|
|
545
|
-
|
|
546
|
-
// Find all corresponding match elements
|
|
547
|
-
const matchSelector = `[data-hs-a11y*="match-text-${identifier}"][data-hs-a11y*="match"]`;
|
|
548
|
-
const matchElements = document.querySelectorAll(matchSelector);
|
|
549
|
-
|
|
550
|
-
if (matchElements.length === 0) {
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// Function to synchronize text and aria-label
|
|
555
|
-
function synchronizeContent() {
|
|
556
|
-
const originalText = originalElement.textContent;
|
|
557
|
-
const originalAriaLabel = originalElement.getAttribute('aria-label');
|
|
558
|
-
|
|
559
|
-
matchElements.forEach(matchElement => {
|
|
560
|
-
// Copy text content
|
|
561
|
-
matchElement.textContent = originalText;
|
|
562
|
-
|
|
563
|
-
// Synchronize aria-label
|
|
564
|
-
if (originalAriaLabel) {
|
|
565
|
-
// If original has aria-label, copy it to match
|
|
566
|
-
matchElement.setAttribute('aria-label', originalAriaLabel);
|
|
567
|
-
} else {
|
|
568
|
-
// If original has no aria-label, remove it from match (keep in sync)
|
|
569
|
-
if (matchElement.hasAttribute('aria-label')) {
|
|
570
|
-
matchElement.removeAttribute('aria-label');
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
});
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
// Initial synchronization
|
|
577
|
-
synchronizeContent();
|
|
578
|
-
|
|
579
|
-
// Set up MutationObserver to watch for changes
|
|
580
|
-
const observer = new MutationObserver((mutations) => {
|
|
581
|
-
let shouldSync = false;
|
|
582
|
-
|
|
583
|
-
mutations.forEach((mutation) => {
|
|
584
|
-
if (mutation.type === 'childList' ||
|
|
585
|
-
mutation.type === 'characterData' ||
|
|
586
|
-
(mutation.type === 'attributes' && mutation.attributeName === 'aria-label')) {
|
|
587
|
-
shouldSync = true;
|
|
588
|
-
}
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
if (shouldSync) {
|
|
592
|
-
synchronizeContent();
|
|
593
|
-
}
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
// Observe text changes and attribute changes
|
|
597
|
-
observer.observe(originalElement, {
|
|
598
|
-
childList: true,
|
|
599
|
-
subtree: true,
|
|
600
|
-
characterData: true,
|
|
601
|
-
attributes: true,
|
|
602
|
-
attributeFilter: ['aria-label']
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
addObserver(observer);
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
function setupRichTextAccessibility(addObserver, addHandler, cleanup) {
|
|
610
|
-
const contentAreas = document.querySelectorAll('[data-hs-a11y="rich-content"]');
|
|
611
|
-
const tocLists = document.querySelectorAll('[data-hs-a11y="rich-toc"]');
|
|
612
|
-
|
|
613
|
-
contentAreas.forEach((contentArea) => {
|
|
614
|
-
// Since there's only 1 content area and 1 TOC list per page, use the first TOC list
|
|
615
|
-
const tocList = tocLists[0];
|
|
616
|
-
|
|
617
|
-
if (!tocList) {
|
|
618
|
-
return;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
if (tocList.children.length === 0) {
|
|
622
|
-
return;
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
const template = tocList.children[0].cloneNode(true);
|
|
626
|
-
// Remove is-active class from template if it exists
|
|
627
|
-
const templateLink = template.querySelector("a");
|
|
628
|
-
if (templateLink) {
|
|
629
|
-
templateLink.classList.remove('is-active');
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
// Clear all original TOC items
|
|
633
|
-
tocList.innerHTML = "";
|
|
634
|
-
|
|
635
|
-
const h2Headings = contentArea.querySelectorAll("h2");
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
// Create sections and wrap content
|
|
639
|
-
h2Headings.forEach((heading) => {
|
|
640
|
-
const sectionId = heading.textContent
|
|
641
|
-
.toLowerCase()
|
|
642
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
643
|
-
.replace(/(^-|-$)/g, "");
|
|
644
|
-
|
|
645
|
-
const section = document.createElement("div");
|
|
646
|
-
section.id = sectionId;
|
|
647
|
-
heading.parentNode.insertBefore(section, heading);
|
|
648
|
-
section.appendChild(heading);
|
|
649
|
-
let nextElement = section.nextElementSibling;
|
|
650
|
-
while (nextElement && nextElement.tagName !== "H2") {
|
|
651
|
-
const elementToMove = nextElement;
|
|
652
|
-
nextElement = nextElement.nextElementSibling;
|
|
653
|
-
section.appendChild(elementToMove);
|
|
654
|
-
}
|
|
655
|
-
});
|
|
656
|
-
|
|
657
|
-
// Create TOC entries
|
|
658
|
-
h2Headings.forEach((heading, index) => {
|
|
659
|
-
const tocItem = template.cloneNode(true);
|
|
660
|
-
const link = tocItem.querySelector("a");
|
|
661
|
-
const sectionId = heading.parentElement.id;
|
|
662
|
-
link.href = "#" + sectionId;
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
// Bold numbered text
|
|
666
|
-
const number = document.createElement("strong");
|
|
667
|
-
number.textContent = index + 1 + ". ";
|
|
668
|
-
|
|
669
|
-
// Clear the link and add the number + text
|
|
670
|
-
link.innerHTML = "";
|
|
671
|
-
link.appendChild(number);
|
|
672
|
-
link.appendChild(document.createTextNode(heading.textContent));
|
|
673
|
-
|
|
674
|
-
// Add click handler for smooth scrolling
|
|
675
|
-
const clickHandler = (e) => {
|
|
676
|
-
e.preventDefault();
|
|
677
|
-
|
|
678
|
-
const targetSection = document.getElementById(sectionId);
|
|
679
|
-
if (targetSection) {
|
|
680
|
-
targetSection.scrollIntoView({ behavior: "smooth" });
|
|
681
|
-
// Focus on the section for accessibility (will only show outline for keyboard users due to CSS)
|
|
682
|
-
setTimeout(() => {
|
|
683
|
-
targetSection.focus();
|
|
684
|
-
}, 100);
|
|
685
|
-
}
|
|
686
|
-
};
|
|
687
|
-
addHandler(link, "click", clickHandler);
|
|
688
|
-
|
|
689
|
-
// Ensure sections are focusable for keyboard users but use CSS to control focus visibility
|
|
690
|
-
const targetSection = document.getElementById(sectionId);
|
|
691
|
-
if (targetSection) {
|
|
692
|
-
targetSection.setAttribute("tabindex", "-1");
|
|
693
|
-
// Use focus-visible to only show outline for keyboard focus
|
|
694
|
-
targetSection.style.outline = "none";
|
|
695
|
-
targetSection.style.setProperty("outline", "none", "important");
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
// Add item to the TOC list
|
|
699
|
-
tocList.appendChild(tocItem);
|
|
700
|
-
});
|
|
701
|
-
|
|
702
|
-
// Set up IntersectionObserver for active state (Webflow-style)
|
|
703
|
-
const sections = Array.from(h2Headings).map(heading => heading.parentElement);
|
|
704
|
-
const tocLinks = tocList.querySelectorAll('a');
|
|
705
|
-
let currentActive = null;
|
|
706
|
-
|
|
707
|
-
const updateActiveLink = () => {
|
|
708
|
-
const windowHeight = window.innerHeight;
|
|
709
|
-
const trigger = windowHeight * 0.75; // 25% from bottom
|
|
710
|
-
|
|
711
|
-
let newActive = null;
|
|
712
|
-
|
|
713
|
-
// Find the last section whose top is above the trigger point
|
|
714
|
-
for (let i = sections.length - 1; i >= 0; i--) {
|
|
715
|
-
const rect = sections[i].getBoundingClientRect();
|
|
716
|
-
if (rect.top <= trigger) {
|
|
717
|
-
newActive = sections[i].id;
|
|
718
|
-
break;
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
// Only update if active section changed
|
|
723
|
-
if (newActive !== currentActive) {
|
|
724
|
-
currentActive = newActive;
|
|
725
|
-
|
|
726
|
-
// Remove all is-active
|
|
727
|
-
tocLinks.forEach(link => link.classList.remove('is-active'));
|
|
728
|
-
|
|
729
|
-
// Add to current
|
|
730
|
-
if (currentActive) {
|
|
731
|
-
const activeLink = Array.from(tocLinks).find(link =>
|
|
732
|
-
link.getAttribute('href') === `#${currentActive}`
|
|
733
|
-
);
|
|
734
|
-
if (activeLink) {
|
|
735
|
-
activeLink.classList.add('is-active');
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
};
|
|
740
|
-
|
|
741
|
-
const observerOptions = {
|
|
742
|
-
rootMargin: '0px 0px -75% 0px',
|
|
743
|
-
threshold: 0
|
|
744
|
-
};
|
|
745
|
-
|
|
746
|
-
const observer = new IntersectionObserver(() => {
|
|
747
|
-
updateActiveLink();
|
|
748
|
-
}, observerOptions);
|
|
749
|
-
|
|
750
|
-
// Observe all sections
|
|
751
|
-
sections.forEach(section => observer.observe(section));
|
|
752
|
-
addObserver(observer);
|
|
753
|
-
|
|
754
|
-
// Also update on scroll for smoother tracking
|
|
755
|
-
const scrollHandler = () => {
|
|
756
|
-
if (cleanup.scrollTimeout) clearTimeout(cleanup.scrollTimeout);
|
|
757
|
-
cleanup.scrollTimeout = setTimeout(updateActiveLink, 50);
|
|
758
|
-
};
|
|
759
|
-
addHandler(window, 'scroll', scrollHandler);
|
|
760
|
-
|
|
761
|
-
});
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
setupGeneralAccessibility();
|
|
765
|
-
|
|
766
|
-
return {
|
|
767
|
-
result: "accessibility initialized",
|
|
768
|
-
destroy: () => {
|
|
769
|
-
// Disconnect all observers
|
|
770
|
-
cleanup.observers.forEach(obs => obs.disconnect());
|
|
771
|
-
cleanup.observers.length = 0;
|
|
772
|
-
|
|
773
|
-
// Remove all event listeners
|
|
774
|
-
cleanup.handlers.forEach(({ element, event, handler, options }) => {
|
|
775
|
-
element.removeEventListener(event, handler, options);
|
|
776
|
-
});
|
|
777
|
-
cleanup.handlers.length = 0;
|
|
778
|
-
|
|
779
|
-
// Clear scroll timeout
|
|
780
|
-
if (cleanup.scrollTimeout) {
|
|
781
|
-
clearTimeout(cleanup.scrollTimeout);
|
|
782
|
-
cleanup.scrollTimeout = null;
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
};
|
|
786
|
-
}
|