@hortonstudio/main 1.4.0 → 1.4.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.
@@ -176,7 +176,9 @@ const CharSplitAnimations = {
176
176
  scrollTrigger: {
177
177
  trigger: textElement,
178
178
  start: config.charSplit.start,
179
- invalidateOnRefresh: false,
179
+ invalidateOnRefresh: true,
180
+ toggleActions: "play none none none",
181
+ once: true,
180
182
  },
181
183
  onComplete: () => {
182
184
  if (textElement.splitTextInstance) {
@@ -247,7 +249,9 @@ const WordSplitAnimations = {
247
249
  scrollTrigger: {
248
250
  trigger: textElement,
249
251
  start: config.wordSplit.start,
250
- invalidateOnRefresh: false,
252
+ invalidateOnRefresh: true,
253
+ toggleActions: "play none none none",
254
+ once: true,
251
255
  },
252
256
  onComplete: () => {
253
257
  if (textElement.splitTextInstance) {
@@ -318,7 +322,9 @@ const LineSplitAnimations = {
318
322
  scrollTrigger: {
319
323
  trigger: textElement,
320
324
  start: config.lineSplit.start,
321
- invalidateOnRefresh: false,
325
+ invalidateOnRefresh: true,
326
+ toggleActions: "play none none none",
327
+ once: true,
322
328
  },
323
329
  onComplete: () => {
324
330
  if (textElement.splitTextInstance) {
@@ -377,7 +383,9 @@ const AppearAnimations = {
377
383
  scrollTrigger: {
378
384
  trigger: element,
379
385
  start: config.appear.start,
380
- invalidateOnRefresh: false,
386
+ invalidateOnRefresh: true,
387
+ toggleActions: "play none none none",
388
+ once: true,
381
389
  },
382
390
  });
383
391
 
@@ -430,7 +438,9 @@ const RevealAnimations = {
430
438
  scrollTrigger: {
431
439
  trigger: element,
432
440
  start: config.reveal.start,
433
- invalidateOnRefresh: false,
441
+ invalidateOnRefresh: true,
442
+ toggleActions: "play none none none",
443
+ once: true,
434
444
  },
435
445
  });
436
446
 
@@ -525,6 +535,23 @@ export async function init() {
525
535
  };
526
536
  window.addEventListener("resize", resizeHandler);
527
537
 
538
+ // Add page load handler for proper ScrollTrigger refresh timing
539
+ const handlePageLoad = () => {
540
+ setTimeout(() => {
541
+ try {
542
+ ScrollTrigger.refresh();
543
+ } catch (error) {
544
+ console.warn("Error refreshing ScrollTrigger on page load:", error);
545
+ }
546
+ }, 100);
547
+ };
548
+
549
+ if (document.readyState === 'complete') {
550
+ handlePageLoad();
551
+ } else {
552
+ window.addEventListener('load', handlePageLoad);
553
+ }
554
+
528
555
  // Initialize API with proper checks
529
556
  if (!window[API_NAME]) {
530
557
  window[API_NAME] = {};
@@ -3,7 +3,190 @@ export function init() {
3
3
  // Stats accessibility has been moved to counter.js
4
4
 
5
5
  function setupGeneralAccessibility() {
6
- // Add any general accessibility features here
6
+ setupListAccessibility();
7
+ setupFAQAccessibility();
8
+ setupConvertToSpan();
9
+ setupYearReplacement();
10
+ setupPreventDefault();
11
+ }
12
+
13
+ function setupListAccessibility() {
14
+ const listElements = document.querySelectorAll('[data-hs-a11y="list"]');
15
+ const listItemElements = document.querySelectorAll('[data-hs-a11y="list-item"]');
16
+
17
+ listElements.forEach(element => {
18
+ element.setAttribute('role', 'list');
19
+ element.removeAttribute('data-hs-a11y');
20
+ });
21
+
22
+ listItemElements.forEach(element => {
23
+ element.setAttribute('role', 'listitem');
24
+ element.removeAttribute('data-hs-a11y');
25
+ });
26
+ }
27
+
28
+ function setupFAQAccessibility() {
29
+ const faqContainers = document.querySelectorAll('[data-hs-a11y="faq"]');
30
+
31
+ faqContainers.forEach((container, index) => {
32
+ const button = container.querySelector('button');
33
+ const contentWrapper = button.parentElement.nextElementSibling;
34
+
35
+ const buttonId = `faq-button-${index}`;
36
+ const contentId = `faq-content-${index}`;
37
+
38
+ button.setAttribute('id', buttonId);
39
+ button.setAttribute('aria-expanded', 'false');
40
+ button.setAttribute('aria-controls', contentId);
41
+
42
+ contentWrapper.setAttribute('id', contentId);
43
+ contentWrapper.setAttribute('aria-hidden', 'true');
44
+ contentWrapper.setAttribute('role', 'region');
45
+ contentWrapper.setAttribute('aria-labelledby', buttonId);
46
+
47
+ if (contentWrapper.style.height !== '0px') {
48
+ button.setAttribute('aria-expanded', 'true');
49
+ contentWrapper.setAttribute('aria-hidden', 'false');
50
+ }
51
+
52
+ function toggleFAQ() {
53
+ const isOpen = button.getAttribute('aria-expanded') === 'true';
54
+
55
+ button.setAttribute('aria-expanded', !isOpen);
56
+ contentWrapper.setAttribute('aria-hidden', isOpen);
57
+ }
58
+
59
+ button.addEventListener('click', toggleFAQ);
60
+
61
+ container.removeAttribute('data-hs-a11y');
62
+ });
63
+ }
64
+
65
+ function setupConvertToSpan() {
66
+ const containers = document.querySelectorAll('[data-hs-a11y="convert-span"]');
67
+
68
+ containers.forEach(container => {
69
+ const skipTags = [
70
+ 'span', 'a', 'button', 'input', 'textarea', 'select', 'img', 'video', 'audio',
71
+ 'iframe', 'object', 'embed', 'canvas', 'svg', 'form', 'table', 'thead', 'tbody',
72
+ 'tr', 'td', 'th', 'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'h1', 'h2', 'h3', 'h4',
73
+ 'h5', 'h6', 'script', 'style', 'link', 'meta', 'title', 'head', 'html', 'body'
74
+ ];
75
+
76
+ // Convert all child elements first
77
+ const elementsToConvert = container.querySelectorAll('*');
78
+
79
+ elementsToConvert.forEach(element => {
80
+ const tagName = element.tagName.toLowerCase();
81
+
82
+ if (!skipTags.includes(tagName)) {
83
+ const newSpan = document.createElement('span');
84
+
85
+ // Copy all attributes except data-hs-a11y
86
+ Array.from(element.attributes).forEach(attr => {
87
+ if (attr.name !== 'data-hs-a11y') {
88
+ newSpan.setAttribute(attr.name, attr.value);
89
+ }
90
+ });
91
+
92
+ // Move all child nodes
93
+ while (element.firstChild) {
94
+ newSpan.appendChild(element.firstChild);
95
+ }
96
+
97
+ // Replace the element
98
+ element.parentNode.replaceChild(newSpan, element);
99
+ }
100
+ });
101
+
102
+ // Convert the container itself to span
103
+ const containerTagName = container.tagName.toLowerCase();
104
+ if (!skipTags.includes(containerTagName)) {
105
+ const newSpan = document.createElement('span');
106
+
107
+ // Copy all attributes except data-hs-a11y
108
+ Array.from(container.attributes).forEach(attr => {
109
+ if (attr.name !== 'data-hs-a11y') {
110
+ newSpan.setAttribute(attr.name, attr.value);
111
+ }
112
+ });
113
+
114
+ // Move all child nodes
115
+ while (container.firstChild) {
116
+ newSpan.appendChild(container.firstChild);
117
+ }
118
+
119
+ // Replace the container
120
+ container.parentNode.replaceChild(newSpan, container);
121
+ } else {
122
+ // Just remove the attribute if container shouldn't be converted
123
+ container.removeAttribute('data-hs-a11y');
124
+ }
125
+ });
126
+ }
127
+
128
+ function setupYearReplacement() {
129
+ const currentYear = new Date().getFullYear().toString();
130
+ const walker = document.createTreeWalker(
131
+ document.body,
132
+ NodeFilter.SHOW_TEXT,
133
+ {
134
+ acceptNode: (node) => {
135
+ return node.textContent.includes('{{year}}')
136
+ ? NodeFilter.FILTER_ACCEPT
137
+ : NodeFilter.FILTER_SKIP;
138
+ }
139
+ }
140
+ );
141
+
142
+ const textNodes = [];
143
+ let node;
144
+ while (node = walker.nextNode()) {
145
+ textNodes.push(node);
146
+ }
147
+
148
+ textNodes.forEach(textNode => {
149
+ const newText = textNode.textContent.replace(/\{\{year\}\}/gi, currentYear);
150
+ if (newText !== textNode.textContent) {
151
+ textNode.textContent = newText;
152
+ }
153
+ });
154
+ }
155
+
156
+ function setupPreventDefault() {
157
+ const elements = document.querySelectorAll('[data-hs-a11y="prevent-default"]');
158
+
159
+ elements.forEach(element => {
160
+ // Prevent click
161
+ element.addEventListener('click', (e) => {
162
+ e.preventDefault();
163
+ e.stopPropagation();
164
+ return false;
165
+ });
166
+
167
+ // Prevent keyboard activation
168
+ element.addEventListener('keydown', (e) => {
169
+ if (e.key === 'Enter' || e.key === ' ') {
170
+ e.preventDefault();
171
+ e.stopPropagation();
172
+ return false;
173
+ }
174
+ });
175
+
176
+ // Additional prevention for anchor links
177
+ if (element.tagName.toLowerCase() === 'a') {
178
+ // Remove or modify href to prevent scroll
179
+ const originalHref = element.getAttribute('href');
180
+ if (originalHref && (originalHref === '#' || originalHref.startsWith('#'))) {
181
+ element.setAttribute('data-original-href', originalHref);
182
+ element.removeAttribute('href');
183
+ element.setAttribute('role', 'button');
184
+ element.setAttribute('tabindex', '0');
185
+ }
186
+ }
187
+
188
+ element.removeAttribute('data-hs-a11y');
189
+ });
7
190
  }
8
191
 
9
192
  setupGeneralAccessibility();
@@ -363,6 +363,16 @@ function setupDynamicDropdowns() {
363
363
  }
364
364
  });
365
365
 
366
+ toggle.addEventListener("click", function(e) {
367
+ if (e.isTrusted) {
368
+ // This is a real user click - prevent it
369
+ e.preventDefault();
370
+ e.stopPropagation();
371
+ return false;
372
+ }
373
+ // Programmatic clicks (from hover/keyboard) proceed normally
374
+ });
375
+
366
376
  document.addEventListener("click", function (e) {
367
377
  if (!wrapper.contains(e.target) && isOpen) {
368
378
  closeDropdown();
@@ -726,7 +736,7 @@ function setupMobileMenuButton() {
726
736
 
727
737
  // Cleanup function for window.mobileMenuState
728
738
  if (typeof window !== "undefined") {
729
- window.addEventListener("unload", () => {
739
+ window.addEventListener("pagehide", () => {
730
740
  if (window.mobileMenuState) {
731
741
  delete window.mobileMenuState;
732
742
  }
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // Version:1.4.0
1
+ // Version:1.4.1
2
2
 
3
3
  const API_NAME = "hsmain";
4
4
 
@@ -31,7 +31,6 @@ const initializeHsMain = async () => {
31
31
  navbar: true,
32
32
  accessibility: true,
33
33
  counter: true,
34
- "custom-values": true,
35
34
  form: true,
36
35
  };
37
36
 
@@ -55,7 +54,6 @@ const initializeHsMain = async () => {
55
54
  navbar: () => import("./autoInit/navbar.js"),
56
55
  accessibility: () => import("./autoInit/accessibility.js"),
57
56
  counter: () => import("./autoInit/counter.js"),
58
- "custom-values": () => import("./autoInit/custom-values.js"),
59
57
  form: () => import("./autoInit/form.js"),
60
58
  };
61
59
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hortonstudio/main",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Animation and utility library for client websites",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -1,266 +0,0 @@
1
- export function init() {
2
- const customValues = new Map();
3
- let isInitialized = false;
4
-
5
- // Configuration for performance optimization
6
- const config = {
7
- // Attributes to search for placeholders
8
- searchAttributes: [
9
- 'href', 'src', 'alt', 'title', 'aria-label', 'data-src',
10
- 'data-href', 'action', 'placeholder', 'value'
11
- ],
12
- // Elements to exclude from search for performance
13
- excludeSelectors: [
14
- 'script', 'style', 'meta', 'link', 'title', 'head',
15
- '[data-hs-custom="list"]', '[data-hs-custom="name"]', '[data-hs-custom="value"]'
16
- ],
17
- // Phone number formatting options
18
- phoneFormatting: {
19
- // Attributes that should use tel: format
20
- telAttributes: ['href'],
21
- // Pattern to detect phone numbers (matches various formats)
22
- phonePattern: /^[\+]?[1-9]?[\d\s\-\(\)\.]{7,15}$/,
23
- // Default country code (US/Canada)
24
- defaultCountryCode: '+1',
25
- // Clean phone for tel: links (remove all non-digits except +)
26
- cleanForTel: (phone) => {
27
- const cleaned = phone.replace(/[^\d+]/g, '');
28
- // If no country code, add default
29
- if (!cleaned.startsWith('+')) {
30
- return config.phoneFormatting.defaultCountryCode + cleaned;
31
- }
32
- return cleaned;
33
- },
34
- // Format for display (keep original formatting)
35
- formatForDisplay: (phone) => phone
36
- }
37
- };
38
-
39
- // Detect if a value looks like a phone number
40
- function isPhoneNumber(value) {
41
- return config.phoneFormatting.phonePattern.test(value.trim());
42
- }
43
-
44
- // Format value based on context (attribute vs text content)
45
- function formatValueForContext(value, isAttribute, attributeName) {
46
- if (isPhoneNumber(value)) {
47
- // For href attributes, clean the phone number (no tel: prefix)
48
- if (isAttribute && config.phoneFormatting.telAttributes.includes(attributeName)) {
49
- return config.phoneFormatting.cleanForTel(value);
50
- } else {
51
- // For display, keep original formatting
52
- return config.phoneFormatting.formatForDisplay(value);
53
- }
54
- }
55
- return value;
56
- }
57
-
58
- // Extract custom values from data attributes
59
- function extractCustomValues() {
60
- const customList = document.querySelector('[data-hs-custom="list"]');
61
- if (!customList) {
62
- return false;
63
- }
64
-
65
- const nameElements = customList.querySelectorAll('[data-hs-custom="name"]');
66
- const valueElements = customList.querySelectorAll('[data-hs-custom="value"]');
67
-
68
- // Build mapping from name/value pairs
69
- for (let i = 0; i < Math.min(nameElements.length, valueElements.length); i++) {
70
- const name = nameElements[i].textContent.trim();
71
- const value = valueElements[i].textContent.trim();
72
-
73
- if (name && value) {
74
- // Store with lowercase key for case-insensitive matching
75
- const key = `{{${name.toLowerCase()}}}`;
76
- customValues.set(key, value);
77
- }
78
- }
79
-
80
- return customValues.size > 0;
81
- }
82
-
83
- // Replace placeholders in text content
84
- function replaceInText(text, isAttribute = false, attributeName = null) {
85
- if (!text || typeof text !== 'string') return text;
86
-
87
- let result = text;
88
-
89
- customValues.forEach((value, placeholder) => {
90
- // Create case-insensitive regex for exact placeholder match
91
- const regex = new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
92
- const matches = text.match(regex);
93
- if (matches) {
94
- // Format value based on context (phone numbers get special treatment)
95
- const formattedValue = formatValueForContext(value, isAttribute, attributeName);
96
- result = result.replace(regex, formattedValue);
97
- }
98
- });
99
-
100
- return result;
101
- }
102
-
103
- // Replace placeholders in all attributes of an element
104
- function replaceInAttributes(element) {
105
- config.searchAttributes.forEach(attr => {
106
- const value = element.getAttribute(attr);
107
- if (value) {
108
- const newValue = replaceInText(value, true, attr);
109
- if (newValue !== value) {
110
- element.setAttribute(attr, newValue);
111
- }
112
- }
113
- });
114
- }
115
-
116
- // Check if element should be excluded from processing
117
- function shouldExcludeElement(element) {
118
- return config.excludeSelectors.some(selector => {
119
- return element.matches(selector);
120
- });
121
- }
122
-
123
- // Process text nodes for placeholder replacement
124
- function processTextNodes(element) {
125
- const walker = document.createTreeWalker(
126
- element,
127
- NodeFilter.SHOW_TEXT,
128
- {
129
- acceptNode: (node) => {
130
- // Skip if parent element should be excluded
131
- if (shouldExcludeElement(node.parentElement)) {
132
- return NodeFilter.FILTER_REJECT;
133
- }
134
-
135
- // Only process text nodes with placeholder patterns
136
- return node.textContent.includes('{{') && node.textContent.includes('}}')
137
- ? NodeFilter.FILTER_ACCEPT
138
- : NodeFilter.FILTER_SKIP;
139
- }
140
- }
141
- );
142
-
143
- const textNodes = [];
144
- let node;
145
- while (node = walker.nextNode()) {
146
- textNodes.push(node);
147
- }
148
-
149
- // Replace placeholders in collected text nodes
150
- textNodes.forEach(textNode => {
151
- const originalText = textNode.textContent;
152
- const newText = replaceInText(originalText);
153
- if (newText !== originalText) {
154
- textNode.textContent = newText;
155
- }
156
- });
157
- }
158
-
159
- // Process all elements for attribute replacement
160
- function processElements(container) {
161
- const elements = container.querySelectorAll('*');
162
-
163
- elements.forEach(element => {
164
- if (!shouldExcludeElement(element)) {
165
- replaceInAttributes(element);
166
- }
167
- });
168
- }
169
-
170
- // Main replacement function
171
- function performReplacements() {
172
- if (customValues.size === 0) return;
173
-
174
- // Process text content
175
- processTextNodes(document.body);
176
-
177
- // Process element attributes
178
- processElements(document.body);
179
-
180
- // Also check document root attributes
181
- replaceInAttributes(document.documentElement);
182
- }
183
-
184
- // Remove the custom values list from DOM
185
- function cleanupCustomList() {
186
- const customList = document.querySelector('[data-hs-custom="list"]');
187
- if (customList) {
188
- customList.remove();
189
- }
190
- }
191
-
192
- // Handle dynamic content with MutationObserver
193
- function setupDynamicContentHandler() {
194
- const observer = new MutationObserver((mutations) => {
195
- let hasNewContent = false;
196
-
197
- mutations.forEach((mutation) => {
198
- if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
199
- mutation.addedNodes.forEach((node) => {
200
- if (node.nodeType === Node.ELEMENT_NODE) {
201
- // Check if new content contains placeholders
202
- const hasPlaceholders = node.textContent.includes('{{') && node.textContent.includes('}}');
203
- const hasAttributePlaceholders = config.searchAttributes.some(attr => {
204
- const value = node.getAttribute?.(attr);
205
- return value && value.includes('{{') && value.includes('}}');
206
- });
207
-
208
- if (hasPlaceholders || hasAttributePlaceholders) {
209
- hasNewContent = true;
210
- }
211
- }
212
- });
213
- }
214
- });
215
-
216
- if (hasNewContent && customValues.size > 0) {
217
- // Debounce replacements for performance
218
- clearTimeout(observer.timeout);
219
- observer.timeout = setTimeout(() => {
220
- performReplacements();
221
- }, 100);
222
- }
223
- });
224
-
225
- observer.observe(document.body, {
226
- childList: true,
227
- subtree: true
228
- });
229
-
230
- return observer;
231
- }
232
-
233
- // Initialize the custom values system
234
- function initializeCustomValues() {
235
- if (isInitialized) return;
236
-
237
- // Extract custom values from data attributes
238
- const hasCustomValues = extractCustomValues();
239
-
240
- if (hasCustomValues) {
241
- // Perform initial replacements
242
- performReplacements();
243
-
244
- // Clean up the custom list
245
- cleanupCustomList();
246
-
247
- // Set up dynamic content handling
248
- setupDynamicContentHandler();
249
-
250
- isInitialized = true;
251
-
252
- return {
253
- result: `custom-values initialized with ${customValues.size} replacements`,
254
- count: customValues.size
255
- };
256
- } else {
257
- return {
258
- result: 'custom-values initialized (no custom values found)',
259
- count: 0
260
- };
261
- }
262
- }
263
-
264
- // Initialize on page load
265
- return initializeCustomValues();
266
- }