@hortonstudio/main 1.5.0 → 1.5.1
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.js +11 -6
- package/autoInit/navbar.js +21 -4
- package/index.js +1 -1
- package/package.json +1 -1
- package/test.json +0 -0
- package/autoInit/custom-values.js +0 -224
|
@@ -88,13 +88,13 @@ export function init() {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
function setupFAQAccessibility() {
|
|
91
|
-
const faqContainers = document.querySelectorAll('[data-hs-a11y="faq"]');
|
|
91
|
+
const faqContainers = document.querySelectorAll('[data-hs-a11y="faq-wrap"]');
|
|
92
92
|
|
|
93
93
|
faqContainers.forEach((container, index) => {
|
|
94
|
-
const button = container.querySelector('
|
|
95
|
-
const contentWrapper =
|
|
94
|
+
const button = container.querySelector('[data-hs-a11y="faq-btn"]');
|
|
95
|
+
const contentWrapper = container.querySelector('[data-hs-a11y="faq-content"]');
|
|
96
96
|
|
|
97
|
-
if (!contentWrapper) return;
|
|
97
|
+
if (!button || !contentWrapper) return;
|
|
98
98
|
|
|
99
99
|
const buttonId = `faq-button-${index}`;
|
|
100
100
|
const contentId = `faq-content-${index}`;
|
|
@@ -191,12 +191,16 @@ export function init() {
|
|
|
191
191
|
|
|
192
192
|
function setupYearReplacement() {
|
|
193
193
|
const currentYear = new Date().getFullYear().toString();
|
|
194
|
+
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
|
|
195
|
+
'July', 'August', 'September', 'October', 'November', 'December'];
|
|
196
|
+
const currentMonth = monthNames[new Date().getMonth()];
|
|
197
|
+
|
|
194
198
|
const walker = document.createTreeWalker(
|
|
195
199
|
document.body,
|
|
196
200
|
NodeFilter.SHOW_TEXT,
|
|
197
201
|
{
|
|
198
202
|
acceptNode: (node) => {
|
|
199
|
-
return node.textContent.includes('{{year}}')
|
|
203
|
+
return (node.textContent.includes('{{year}}') || node.textContent.includes('{{month}}'))
|
|
200
204
|
? NodeFilter.FILTER_ACCEPT
|
|
201
205
|
: NodeFilter.FILTER_SKIP;
|
|
202
206
|
}
|
|
@@ -210,7 +214,8 @@ export function init() {
|
|
|
210
214
|
}
|
|
211
215
|
|
|
212
216
|
textNodes.forEach(textNode => {
|
|
213
|
-
|
|
217
|
+
let newText = textNode.textContent.replace(/\{\{year\}\}/gi, currentYear);
|
|
218
|
+
newText = newText.replace(/\{\{month\}\}/gi, currentMonth);
|
|
214
219
|
if (newText !== textNode.textContent) {
|
|
215
220
|
textNode.textContent = newText;
|
|
216
221
|
}
|
package/autoInit/navbar.js
CHANGED
|
@@ -54,9 +54,18 @@ function setupDynamicDropdowns() {
|
|
|
54
54
|
dropdownList.setAttribute("aria-hidden", "true");
|
|
55
55
|
|
|
56
56
|
const menuItems = dropdownList.querySelectorAll("a");
|
|
57
|
-
menuItems.forEach((item) => {
|
|
57
|
+
menuItems.forEach((item, index) => {
|
|
58
58
|
item.setAttribute("role", "menuitem");
|
|
59
59
|
item.setAttribute("tabindex", "-1");
|
|
60
|
+
|
|
61
|
+
// Add context for first item to help screen readers understand dropdown content
|
|
62
|
+
if (index === 0) {
|
|
63
|
+
const toggleText = toggle.textContent?.trim() || "menu";
|
|
64
|
+
const existingLabel = item.getAttribute("aria-label");
|
|
65
|
+
if (!existingLabel) {
|
|
66
|
+
item.setAttribute("aria-label", `${item.textContent?.trim()}, ${toggleText} submenu`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
60
69
|
});
|
|
61
70
|
|
|
62
71
|
let isOpen = false;
|
|
@@ -65,6 +74,8 @@ function setupDynamicDropdowns() {
|
|
|
65
74
|
function openDropdown() {
|
|
66
75
|
if (isOpen) return;
|
|
67
76
|
closeAllDropdowns(wrapper);
|
|
77
|
+
|
|
78
|
+
// Set ARIA states FIRST, before focus changes
|
|
68
79
|
isOpen = true;
|
|
69
80
|
toggle.setAttribute("aria-expanded", "true");
|
|
70
81
|
dropdownList.setAttribute("aria-hidden", "false");
|
|
@@ -83,16 +94,22 @@ function setupDynamicDropdowns() {
|
|
|
83
94
|
function closeDropdown() {
|
|
84
95
|
if (!isOpen) return;
|
|
85
96
|
const shouldRestoreFocus = dropdownList.contains(document.activeElement);
|
|
97
|
+
|
|
98
|
+
// Update ARIA states FIRST to help screen readers understand content is hidden
|
|
86
99
|
isOpen = false;
|
|
87
100
|
currentMenuItemIndex = -1;
|
|
88
|
-
if (shouldRestoreFocus) {
|
|
89
|
-
toggle.focus();
|
|
90
|
-
}
|
|
91
101
|
toggle.setAttribute("aria-expanded", "false");
|
|
92
102
|
dropdownList.setAttribute("aria-hidden", "true");
|
|
93
103
|
menuItems.forEach((item) => {
|
|
94
104
|
item.setAttribute("tabindex", "-1");
|
|
95
105
|
});
|
|
106
|
+
|
|
107
|
+
// Force screen reader virtual cursor refresh by briefly focusing dropdown container then restoring
|
|
108
|
+
if (shouldRestoreFocus) {
|
|
109
|
+
// This helps reset virtual cursor position
|
|
110
|
+
dropdownList.focus();
|
|
111
|
+
toggle.focus();
|
|
112
|
+
}
|
|
96
113
|
|
|
97
114
|
const clickEvent = new MouseEvent("click", {
|
|
98
115
|
bubbles: true,
|
package/index.js
CHANGED
package/package.json
CHANGED
package/test.json
ADDED
|
File without changes
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
export function init() {
|
|
2
|
-
const customValues = new Map();
|
|
3
|
-
let isInitialized = false;
|
|
4
|
-
|
|
5
|
-
const config = {
|
|
6
|
-
searchAttributes: [
|
|
7
|
-
'href', 'src', 'alt', 'title', 'aria-label', 'data-src',
|
|
8
|
-
'data-href', 'action', 'placeholder', 'value'
|
|
9
|
-
],
|
|
10
|
-
excludeSelectors: [
|
|
11
|
-
'script', 'style', 'meta', 'link', 'title', 'head',
|
|
12
|
-
'[data-hs-custom="list"]', '[data-hs-custom="name"]', '[data-hs-custom="value"]'
|
|
13
|
-
],
|
|
14
|
-
phoneFormatting: {
|
|
15
|
-
telAttributes: ['href'],
|
|
16
|
-
phonePattern: /^[\+]?[1-9]?[\d\s\-\(\)\.]{7,15}$/,
|
|
17
|
-
defaultCountryCode: '+1',
|
|
18
|
-
cleanForTel: (phone) => {
|
|
19
|
-
const cleaned = phone.replace(/[^\d+]/g, '');
|
|
20
|
-
if (!cleaned.startsWith('+')) {
|
|
21
|
-
return config.phoneFormatting.defaultCountryCode + cleaned;
|
|
22
|
-
}
|
|
23
|
-
return cleaned;
|
|
24
|
-
},
|
|
25
|
-
formatForDisplay: (phone) => phone
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
function isPhoneNumber(value) {
|
|
30
|
-
return config.phoneFormatting.phonePattern.test(value.trim());
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function formatValueForContext(value, isAttribute, attributeName) {
|
|
34
|
-
if (isPhoneNumber(value)) {
|
|
35
|
-
if (isAttribute && config.phoneFormatting.telAttributes.includes(attributeName)) {
|
|
36
|
-
return config.phoneFormatting.cleanForTel(value);
|
|
37
|
-
} else {
|
|
38
|
-
return config.phoneFormatting.formatForDisplay(value);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return value;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function extractCustomValues() {
|
|
45
|
-
const currentYear = new Date().getFullYear().toString();
|
|
46
|
-
customValues.set('{{year}}', currentYear);
|
|
47
|
-
|
|
48
|
-
const customList = document.querySelector('[data-hs-custom="list"]');
|
|
49
|
-
if (!customList) {
|
|
50
|
-
return customValues.size > 0;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const nameElements = customList.querySelectorAll('[data-hs-custom="name"]');
|
|
54
|
-
const valueElements = customList.querySelectorAll('[data-hs-custom="value"]');
|
|
55
|
-
|
|
56
|
-
for (let i = 0; i < Math.min(nameElements.length, valueElements.length); i++) {
|
|
57
|
-
const name = nameElements[i].textContent.trim();
|
|
58
|
-
const value = valueElements[i].textContent.trim();
|
|
59
|
-
|
|
60
|
-
if (name && value) {
|
|
61
|
-
const key = `{{${name.toLowerCase()}}}`;
|
|
62
|
-
customValues.set(key, value);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return customValues.size > 0;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function replaceInText(text, isAttribute = false, attributeName = null) {
|
|
70
|
-
if (!text || typeof text !== 'string') return text;
|
|
71
|
-
|
|
72
|
-
let result = text;
|
|
73
|
-
|
|
74
|
-
customValues.forEach((value, placeholder) => {
|
|
75
|
-
const regex = new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
|
|
76
|
-
const matches = text.match(regex);
|
|
77
|
-
if (matches) {
|
|
78
|
-
const formattedValue = formatValueForContext(value, isAttribute, attributeName);
|
|
79
|
-
result = result.replace(regex, formattedValue);
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
return result;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function replaceInAttributes(element) {
|
|
87
|
-
config.searchAttributes.forEach(attr => {
|
|
88
|
-
const value = element.getAttribute(attr);
|
|
89
|
-
if (value) {
|
|
90
|
-
const newValue = replaceInText(value, true, attr);
|
|
91
|
-
if (newValue !== value) {
|
|
92
|
-
element.setAttribute(attr, newValue);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function shouldExcludeElement(element) {
|
|
99
|
-
return config.excludeSelectors.some(selector => {
|
|
100
|
-
return element.matches(selector);
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function processTextNodes(element) {
|
|
105
|
-
const walker = document.createTreeWalker(
|
|
106
|
-
element,
|
|
107
|
-
NodeFilter.SHOW_TEXT,
|
|
108
|
-
{
|
|
109
|
-
acceptNode: (node) => {
|
|
110
|
-
if (shouldExcludeElement(node.parentElement)) {
|
|
111
|
-
return NodeFilter.FILTER_REJECT;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return node.textContent.includes('{{') && node.textContent.includes('}}')
|
|
115
|
-
? NodeFilter.FILTER_ACCEPT
|
|
116
|
-
: NodeFilter.FILTER_SKIP;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
const textNodes = [];
|
|
122
|
-
let node;
|
|
123
|
-
while (node = walker.nextNode()) {
|
|
124
|
-
textNodes.push(node);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
textNodes.forEach(textNode => {
|
|
128
|
-
const originalText = textNode.textContent;
|
|
129
|
-
const newText = replaceInText(originalText);
|
|
130
|
-
if (newText !== originalText) {
|
|
131
|
-
textNode.textContent = newText;
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function processElements(container) {
|
|
137
|
-
const elements = container.querySelectorAll('*');
|
|
138
|
-
|
|
139
|
-
elements.forEach(element => {
|
|
140
|
-
if (!shouldExcludeElement(element)) {
|
|
141
|
-
replaceInAttributes(element);
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function performReplacements() {
|
|
147
|
-
if (customValues.size === 0) return;
|
|
148
|
-
|
|
149
|
-
processTextNodes(document.body);
|
|
150
|
-
processElements(document.body);
|
|
151
|
-
replaceInAttributes(document.documentElement);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function cleanupCustomList() {
|
|
155
|
-
const customList = document.querySelector('[data-hs-custom="list"]');
|
|
156
|
-
if (customList) {
|
|
157
|
-
customList.remove();
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function setupDynamicContentHandler() {
|
|
162
|
-
const observer = new MutationObserver((mutations) => {
|
|
163
|
-
let hasNewContent = false;
|
|
164
|
-
|
|
165
|
-
mutations.forEach((mutation) => {
|
|
166
|
-
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
|
|
167
|
-
mutation.addedNodes.forEach((node) => {
|
|
168
|
-
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
169
|
-
const hasPlaceholders = node.textContent.includes('{{') && node.textContent.includes('}}');
|
|
170
|
-
const hasAttributePlaceholders = config.searchAttributes.some(attr => {
|
|
171
|
-
const value = node.getAttribute?.(attr);
|
|
172
|
-
return value && value.includes('{{') && value.includes('}}');
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
if (hasPlaceholders || hasAttributePlaceholders) {
|
|
176
|
-
hasNewContent = true;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
if (hasNewContent && customValues.size > 0) {
|
|
184
|
-
clearTimeout(observer.timeout);
|
|
185
|
-
observer.timeout = setTimeout(() => {
|
|
186
|
-
performReplacements();
|
|
187
|
-
}, 100);
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
observer.observe(document.body, {
|
|
192
|
-
childList: true,
|
|
193
|
-
subtree: true
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
return observer;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function initializeCustomValues() {
|
|
200
|
-
if (isInitialized) return;
|
|
201
|
-
|
|
202
|
-
const hasCustomValues = extractCustomValues();
|
|
203
|
-
|
|
204
|
-
if (hasCustomValues) {
|
|
205
|
-
performReplacements();
|
|
206
|
-
cleanupCustomList();
|
|
207
|
-
setupDynamicContentHandler();
|
|
208
|
-
|
|
209
|
-
isInitialized = true;
|
|
210
|
-
|
|
211
|
-
return {
|
|
212
|
-
result: `custom-values initialized with ${customValues.size} replacements`,
|
|
213
|
-
count: customValues.size
|
|
214
|
-
};
|
|
215
|
-
} else {
|
|
216
|
-
return {
|
|
217
|
-
result: 'custom-values initialized (no custom values found)',
|
|
218
|
-
count: 0
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return initializeCustomValues();
|
|
224
|
-
}
|