@hortonstudio/main 1.4.5 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,7 @@ export function init() {
4
4
 
5
5
  function setupGeneralAccessibility() {
6
6
  setupListAccessibility();
7
+ setupRemoveListAccessibility();
7
8
  setupFAQAccessibility();
8
9
  setupConvertToSpan();
9
10
  setupYearReplacement();
@@ -25,6 +26,67 @@ export function init() {
25
26
  });
26
27
  }
27
28
 
29
+ function setupRemoveListAccessibility() {
30
+ const containers = document.querySelectorAll('[data-hs-a11y="remove-list"]');
31
+
32
+ containers.forEach(container => {
33
+ // Remove role="list" and role="listitem" from container and all descendants
34
+ const elementsWithListRoles = container.querySelectorAll('[role="list"], [role="listitem"]');
35
+ elementsWithListRoles.forEach(element => {
36
+ element.removeAttribute('role');
37
+ });
38
+
39
+ // Also remove from container itself if it has these roles
40
+ if (container.getAttribute('role') === 'list' || container.getAttribute('role') === 'listitem') {
41
+ container.removeAttribute('role');
42
+ }
43
+
44
+ // Convert semantic lists to divs in container and all descendants
45
+ const listsToConvert = container.querySelectorAll('ul, ol, li');
46
+ listsToConvert.forEach(listElement => {
47
+ const newDiv = document.createElement('div');
48
+
49
+ // Copy all attributes except role
50
+ Array.from(listElement.attributes).forEach(attr => {
51
+ if (attr.name !== 'role') {
52
+ newDiv.setAttribute(attr.name, attr.value);
53
+ }
54
+ });
55
+
56
+ // Move all child nodes
57
+ while (listElement.firstChild) {
58
+ newDiv.appendChild(listElement.firstChild);
59
+ }
60
+
61
+ // Replace the element
62
+ listElement.parentNode.replaceChild(newDiv, listElement);
63
+ });
64
+
65
+ // Convert container itself if it's a semantic list
66
+ if (container.tagName.toLowerCase() === 'ul' || container.tagName.toLowerCase() === 'ol' || container.tagName.toLowerCase() === 'li') {
67
+ const newDiv = document.createElement('div');
68
+
69
+ // Copy all attributes except data-hs-a11y and role
70
+ Array.from(container.attributes).forEach(attr => {
71
+ if (attr.name !== 'data-hs-a11y' && attr.name !== 'role') {
72
+ newDiv.setAttribute(attr.name, attr.value);
73
+ }
74
+ });
75
+
76
+ // Move all child nodes
77
+ while (container.firstChild) {
78
+ newDiv.appendChild(container.firstChild);
79
+ }
80
+
81
+ // Replace the container
82
+ container.parentNode.replaceChild(newDiv, container);
83
+ } else {
84
+ // Just remove the attribute if container isn't a semantic list
85
+ container.removeAttribute('data-hs-a11y');
86
+ }
87
+ });
88
+ }
89
+
28
90
  function setupFAQAccessibility() {
29
91
  const faqContainers = document.querySelectorAll('[data-hs-a11y="faq"]');
30
92
 
@@ -32,6 +94,8 @@ export function init() {
32
94
  const button = container.querySelector('button');
33
95
  const contentWrapper = button.parentElement.nextElementSibling;
34
96
 
97
+ if (!contentWrapper) return;
98
+
35
99
  const buttonId = `faq-button-${index}`;
36
100
  const contentId = `faq-content-${index}`;
37
101
 
@@ -1,12 +1,6 @@
1
- // Named constants
2
- const ACCESSIBILITY_UPDATE_DELAY = 100;
3
-
4
1
  export function init() {
5
2
  const config = {
6
3
  duration: 3000,
7
- keyboardStep: 5,
8
- observerThreshold: 0,
9
- observerRootMargin: "0px",
10
4
  };
11
5
 
12
6
  let counters = [];
@@ -16,16 +10,12 @@ export function init() {
16
10
  Object.assign(config, newConfig);
17
11
  }
18
12
 
19
- // Power4 out easing function
20
13
  function easeOutQuart(t) {
21
14
  return 1 - Math.pow(1 - t, 4);
22
15
  }
23
16
 
24
- // Extract number and symbols from text
25
17
  function parseText(text) {
26
- const match = text.match(/([+\-]?)(\d*\.?\d+)([%+\-]?.*)/);
27
- if (!match) return null;
28
-
18
+ const match = text.match(/([^\d.]*)(\d*\.?\d+)(.*)/);
29
19
  return {
30
20
  prefix: match[1] || "",
31
21
  number: parseFloat(match[2]),
@@ -33,37 +23,18 @@ export function init() {
33
23
  };
34
24
  }
35
25
 
36
- // Format number back to text with symbols
37
26
  function formatNumber(value, prefix, suffix, decimals = 0) {
38
- const formattedNumber =
39
- decimals > 0 ? value.toFixed(decimals) : Math.round(value);
27
+ const formattedNumber = decimals > 0 ? value.toFixed(decimals) : Math.round(value);
40
28
  return `${prefix}${formattedNumber}${suffix}`;
41
29
  }
42
30
 
43
- // Check for reduced motion preference
44
31
  function prefersReducedMotion() {
45
- return (
46
- window.matchMedia &&
47
- window.matchMedia("(prefers-reduced-motion: reduce)").matches
48
- );
32
+ return window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
49
33
  }
50
34
 
51
- // Animate a single counter by manipulating text content
52
- function animateCounter(
53
- element,
54
- targetValue,
55
- prefix,
56
- suffix,
57
- decimals,
58
- duration = config.duration,
59
- ) {
60
- // If user prefers reduced motion, show final value immediately
35
+ function animateCounter(element, targetValue, prefix, suffix, decimals) {
61
36
  if (prefersReducedMotion()) {
62
- const originalContent = element.getAttribute("data-hs-original");
63
- if (originalContent) {
64
- element.textContent = originalContent;
65
- }
66
- updateStatsAccessibility();
37
+ element.textContent = element.getAttribute("data-hs-original");
67
38
  return;
68
39
  }
69
40
 
@@ -72,267 +43,112 @@ export function init() {
72
43
 
73
44
  const animate = () => {
74
45
  const elapsed = Date.now() - startTime;
75
- const progress = Math.min(elapsed / duration, 1);
76
-
77
- // Apply power4 out easing
46
+ const progress = Math.min(elapsed / config.duration, 1);
78
47
  const easedProgress = easeOutQuart(progress);
79
- const currentValue =
80
- startValue + (targetValue - startValue) * easedProgress;
48
+ const currentValue = startValue + (targetValue - startValue) * easedProgress;
81
49
 
82
- // Update the text content directly
83
- const animatedText = formatNumber(currentValue, prefix, suffix, decimals);
84
- element.textContent = animatedText;
50
+ element.textContent = formatNumber(currentValue, prefix, suffix, decimals);
85
51
 
86
52
  if (progress < 1) {
87
53
  requestAnimationFrame(animate);
88
54
  } else {
89
- // Animation complete - restore original content
90
- const originalContent = element.getAttribute("data-hs-original");
91
- if (originalContent) {
92
- element.textContent = originalContent;
93
- }
94
-
95
- // Update accessibility after animation completes
96
- updateStatsAccessibility();
55
+ element.textContent = element.getAttribute("data-hs-original");
97
56
  }
98
57
  };
99
58
 
100
- // Ensure we start from 0
101
59
  element.textContent = formatNumber(0, prefix, suffix, decimals);
102
-
103
- // Start animation on next frame
104
60
  requestAnimationFrame(animate);
105
61
  }
106
62
 
107
- // Set up intersection observer
108
63
  function setupObserver() {
109
- const options = {
110
- root: null,
111
- rootMargin: "0px",
112
- threshold: 0,
113
- };
114
-
115
64
  observer = new IntersectionObserver((entries) => {
116
- // Group entries by their container to animate together
117
- const containersToAnimate = new Set();
118
-
119
65
  entries.forEach((entry) => {
120
- if (!entry.target.dataset.animated) {
121
- // Check if bottom of viewport reaches top of element
122
- const rect = entry.target.getBoundingClientRect();
123
- const viewportHeight = window.innerHeight;
124
-
125
- if (rect.top <= viewportHeight) {
126
- // Find the parent container (like stats section)
127
- const container =
128
- entry.target.closest('[data-hs-accessibility="stats"]') ||
129
- entry.target.closest(".g_content") ||
130
- entry.target;
131
- containersToAnimate.add(container);
132
- }
66
+ if (entry.isIntersecting && !entry.target.dataset.animated) {
67
+ const numberElement = entry.target.querySelector('[data-hs-counter="number"]');
68
+ startAnimation(numberElement);
69
+ entry.target.dataset.animated = "true";
133
70
  }
134
71
  });
135
-
136
- // Animate all counters in each container simultaneously
137
- containersToAnimate.forEach((container) => {
138
- const countersInContainer = container.querySelectorAll(
139
- '[data-hs="stats-counter"]:not([data-animated])',
140
- );
141
- countersInContainer.forEach((counter) => {
142
- startAnimation(counter);
143
- counter.dataset.animated = "true";
144
- });
145
- });
146
- }, options);
72
+ }, { threshold: 0 });
147
73
  }
148
74
 
149
- // Start animation for a specific element
150
75
  function startAnimation(element) {
151
- // Get original content from data attribute (the real value to animate to)
152
76
  const originalText = element.getAttribute("data-hs-original");
153
- if (!originalText) return;
154
-
155
77
  const parsed = parseText(originalText);
156
- if (!parsed) {
157
- return;
158
- }
159
-
160
- // Determine decimal places from original number
161
78
  const decimals = (parsed.number.toString().split(".")[1] || "").length;
162
79
 
163
- // Start animation
164
- animateCounter(
165
- element,
166
- parsed.number,
167
- parsed.prefix,
168
- parsed.suffix,
169
- decimals,
170
- config.duration,
171
- );
80
+ animateCounter(element, parsed.number, parsed.prefix, parsed.suffix, decimals);
172
81
  }
173
82
 
174
- // Stats accessibility functionality (moved from accessibility.js)
175
83
  function setupStatsAccessibility() {
176
- const statsElements = document.querySelectorAll(
177
- '[data-hs-accessibility="stats"]',
178
- );
84
+ const statsElements = document.querySelectorAll('[data-hs-counter="wrapper"]');
179
85
 
180
86
  statsElements.forEach((element) => {
181
- // Get all text content from the element and its children
182
- const textContent = extractTextContent(element);
183
-
184
- if (textContent) {
185
- element.setAttribute("aria-label", textContent);
186
- element.setAttribute("role", "img");
187
-
188
- // Hide all child elements from screen readers to prevent redundant reading
189
- const allChildren = element.querySelectorAll("*");
190
- allChildren.forEach((child) => {
191
- child.setAttribute("aria-hidden", "true");
192
- });
193
- }
87
+ const numberElement = element.querySelector('[data-hs-counter="number"]');
88
+ const originalText = numberElement.getAttribute("data-hs-original");
89
+
90
+ element.setAttribute("aria-label", originalText);
91
+ element.setAttribute("role", "img");
92
+
93
+ const allChildren = element.querySelectorAll("*");
94
+ allChildren.forEach((child) => {
95
+ child.setAttribute("aria-hidden", "true");
96
+ });
194
97
  });
195
98
  }
196
99
 
197
- function extractTextContent(element) {
198
- const textParts = [];
199
-
200
- function processNode(node) {
201
- // Skip elements with aria-hidden="true"
202
- if (
203
- node.nodeType === Node.ELEMENT_NODE &&
204
- node.getAttribute("aria-hidden") === "true"
205
- ) {
206
- return;
207
- }
208
-
209
- // Use original content if available for counter elements
210
- if (
211
- node.nodeType === Node.ELEMENT_NODE &&
212
- node.hasAttribute &&
213
- node.hasAttribute("data-hs-original")
214
- ) {
215
- const originalText = node.getAttribute("data-hs-original");
216
- if (originalText) {
217
- textParts.push(originalText);
218
- return; // Don't process children
219
- }
220
- }
221
-
222
- // Process text nodes
223
- if (node.nodeType === Node.TEXT_NODE) {
224
- const text = node.textContent.trim();
225
- if (text && text.length > 0) {
226
- textParts.push(text);
227
- }
228
- }
229
-
230
- // Process child nodes
231
- if (node.childNodes) {
232
- node.childNodes.forEach(processNode);
233
- }
234
- }
235
-
236
- processNode(element);
237
-
238
- // Combine all text parts with spaces and clean up
239
- return textParts.join(" ").replace(/\s+/g, " ").trim();
240
- }
241
-
242
- function updateStatsAccessibility() {
243
- // Update accessibility after counter animations complete
244
- setTimeout(() => {
245
- setupStatsAccessibility();
246
- }, ACCESSIBILITY_UPDATE_DELAY);
247
- }
248
-
249
- // Initialize all counters
250
100
  function initCounters() {
251
- // Find all elements with data-hs="stats-counter"
252
- const counterElements = document.querySelectorAll(
253
- '[data-hs="stats-counter"]',
254
- );
255
-
256
- counterElements.forEach((element) => {
257
- // Parse and validate the text contains a number
258
- const parsed = parseText(element.textContent.trim());
259
- if (parsed) {
260
- // Store original content for accessibility and copy/paste
261
- element.setAttribute("data-hs-original", element.textContent.trim());
262
-
263
- // Keep original content visible until animation starts
264
- // This ensures copy/paste gets real values before animation
265
-
266
- counters.push(element);
267
- }
101
+ const wrappers = document.querySelectorAll('[data-hs-counter="wrapper"]');
102
+
103
+ wrappers.forEach((wrapper) => {
104
+ const numberElement = wrapper.querySelector('[data-hs-counter="number"]');
105
+ const parsed = parseText(numberElement.textContent.trim());
106
+
107
+ numberElement.setAttribute("data-hs-original", numberElement.textContent.trim());
108
+ counters.push(wrapper);
268
109
  });
269
110
 
270
- // Set up intersection observer
271
111
  setupObserver();
272
112
 
273
- // Observe all counter elements
274
113
  counters.forEach((counter) => {
275
114
  observer.observe(counter);
276
115
  });
277
116
 
278
- // Set up initial accessibility (will use data-hs-original for screen readers)
279
117
  setupStatsAccessibility();
280
118
  }
281
119
 
282
- // Method to manually trigger animation (useful for testing)
283
120
  function triggerAnimation(selector) {
284
- const element = document.querySelector(selector);
285
- if (element && !element.dataset.animated) {
286
- startAnimation(element);
287
- element.dataset.animated = "true";
288
- }
121
+ const wrapper = document.querySelector(selector);
122
+ const numberElement = wrapper.querySelector('[data-hs-counter="number"]');
123
+ startAnimation(numberElement);
124
+ wrapper.dataset.animated = "true";
289
125
  }
290
126
 
291
- // Reset all counters (useful for re-triggering animations)
292
127
  function reset() {
293
- counters.forEach((element) => {
294
- delete element.dataset.animated;
295
- // Remove any overlay elements and restore original styling
296
- const overlay = element.querySelector(
297
- 'span[style*="position: absolute"]',
298
- );
299
- if (overlay) {
300
- overlay.remove();
301
- }
302
- element.style.removeProperty("position");
303
- element.style.removeProperty("color");
128
+ counters.forEach((wrapper) => {
129
+ delete wrapper.dataset.animated;
130
+ const numberElement = wrapper.querySelector('[data-hs-counter="number"]');
131
+ numberElement.textContent = numberElement.getAttribute("data-hs-original");
304
132
  });
305
133
 
306
- // Reset observer if it exists
307
- if (observer) {
308
- counters.forEach((counter) => {
309
- observer.unobserve(counter);
310
- });
311
-
312
- // Re-observe all counters
313
- counters.forEach((counter) => {
314
- observer.observe(counter);
315
- });
316
- }
134
+ counters.forEach((counter) => {
135
+ observer.unobserve(counter);
136
+ observer.observe(counter);
137
+ });
317
138
 
318
- // Reset accessibility
319
139
  setupStatsAccessibility();
320
140
  }
321
141
 
322
- // Initialize everything
323
142
  initCounters();
324
143
 
325
- // Global API exposure
326
- if (typeof window !== "undefined") {
327
- window.hsmain = window.hsmain || {};
328
- window.hsmain.counter = {
329
- config,
330
- updateConfig,
331
- triggerAnimation,
332
- reset,
333
- counters,
334
- };
335
- }
144
+ window.hsmain = window.hsmain || {};
145
+ window.hsmain.counter = {
146
+ config,
147
+ updateConfig,
148
+ triggerAnimation,
149
+ reset,
150
+ counters,
151
+ };
336
152
 
337
153
  return { result: "counter initialized" };
338
154
  }
package/autoInit/modal.js CHANGED
@@ -2,32 +2,10 @@ function initModal() {
2
2
  const config = {
3
3
  transitionDuration: 0.3,
4
4
  blurOpacity: 0.5,
5
- breakpoints: {
6
- mobile: 767,
7
- tablet: 991,
8
- },
9
5
  };
10
6
 
11
- function getCurrentBreakpoint() {
12
- const width = window.innerWidth;
13
- if (width <= config.breakpoints.mobile) return "mobile";
14
- if (width <= config.breakpoints.tablet) return "tablet";
15
- return "desktop";
16
- }
17
-
18
- function shouldPreventModal(element) {
19
- const preventAttr = element.getAttribute("data-hs-modalprevent");
20
- if (!preventAttr) return false;
21
-
22
- const currentBreakpoint = getCurrentBreakpoint();
23
- const preventBreakpoints = preventAttr.split(",").map((bp) => bp.trim());
24
-
25
- return preventBreakpoints.includes(currentBreakpoint);
26
- }
27
7
 
28
8
  function openModal(element) {
29
- if (shouldPreventModal(element)) return;
30
-
31
9
  document.body.classList.add("u-overflow-clip");
32
10
 
33
11
  // Add blur to all other modals
@@ -53,29 +31,6 @@ function initModal() {
53
31
  });
54
32
  }
55
33
 
56
- // Store modal states that were closed due to prevention
57
- let preventedModalStates = new Map();
58
-
59
- function handleBreakpointChange() {
60
- document.querySelectorAll("[data-hs-modalprevent]").forEach((element) => {
61
- const elementKey =
62
- element.getAttribute("data-hs-modal") +
63
- "_" +
64
- (element.id || element.className);
65
- const shouldPrevent = shouldPreventModal(element);
66
- const wasStoredAsOpen = preventedModalStates.get(elementKey);
67
-
68
- if (shouldPrevent && element.x) {
69
- preventedModalStates.set(elementKey, true);
70
- element.x = 0;
71
- closeModal(element);
72
- } else if (!shouldPrevent && wasStoredAsOpen) {
73
- preventedModalStates.delete(elementKey);
74
- element.x = 1;
75
- openModal(element);
76
- }
77
- });
78
- }
79
34
 
80
35
  function toggleModal(element) {
81
36
  element.x = ((element.x || 0) + 1) % 2;
@@ -110,10 +65,6 @@ function initModal() {
110
65
  });
111
66
  });
112
67
 
113
- // Handle window resize to check for prevented modals
114
- window.addEventListener("resize", function () {
115
- handleBreakpointChange();
116
- });
117
68
 
118
69
  return { result: "modal initialized" };
119
70
  }