@hortonstudio/main 1.6.5 → 1.6.7

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/form.js CHANGED
@@ -571,6 +571,12 @@ export function init() {
571
571
  return isValid;
572
572
  };
573
573
 
574
+ // Helper function to parse comma-separated config values
575
+ const parseFormConfig = (configString) => {
576
+ if (!configString) return [];
577
+ return configString.split(',').map(config => config.trim());
578
+ };
579
+
574
580
  const handleFormSubmit = (event) => {
575
581
  const form = event.target;
576
582
 
@@ -596,12 +602,63 @@ export function init() {
596
602
  removeError(input);
597
603
  });
598
604
 
605
+ // Handle text replacement if this form has replace fields
606
+ const replaceFieldElements = form.querySelectorAll('input[data-hs-form^="replace-field-"], textarea[data-hs-form^="replace-field-"], select[data-hs-form^="replace-field-"]');
607
+ if (replaceFieldElements.length > 0) {
608
+ replaceFieldElements.forEach(field => {
609
+ const dataHsForm = field.getAttribute('data-hs-form');
610
+ const suffix = dataHsForm.replace('replace-field-', '');
611
+ const value = field.value;
612
+
613
+ // Find all matching text elements
614
+ const textElements = document.querySelectorAll(`[data-hs-form="replace-text-${suffix}"]`);
615
+
616
+ textElements.forEach(element => {
617
+ element.textContent = value;
618
+ });
619
+ });
620
+ }
621
+
622
+ // Handle form configuration
623
+ const formWrapper = form.closest('[data-hs-form="wrapper"]');
624
+ let shouldPreventSubmit = false;
625
+
626
+ if (formWrapper && formWrapper.hasAttribute('data-hs-config')) {
627
+ const configString = formWrapper.getAttribute('data-hs-config');
628
+ const configs = parseFormConfig(configString);
629
+
630
+ // Check for prevent-submit config
631
+ if (configs.includes('prevent-submit')) {
632
+ shouldPreventSubmit = true;
633
+ }
634
+
635
+ // Check for click-trigger configs
636
+ configs.forEach(config => {
637
+ if (config.startsWith('click-trigger-')) {
638
+ const trigger = document.querySelector(`[data-hs-form="trigger"][data-hs-config*="${config}"]`);
639
+ if (trigger) {
640
+ setTimeout(() => {
641
+ trigger.click();
642
+ }, 100);
643
+ }
644
+ }
645
+ });
646
+ }
647
+
599
648
  // Trigger final animation if it exists
600
649
  const finalAnimElement = form.querySelector(config.selectors.finalAnim);
601
650
  if (finalAnimElement) {
602
651
  finalAnimElement.click();
603
652
  }
604
653
 
654
+ // Prevent submission if configured to do so
655
+ if (shouldPreventSubmit) {
656
+ event.preventDefault();
657
+ event.stopPropagation();
658
+ event.stopImmediatePropagation();
659
+ return false;
660
+ }
661
+
605
662
  // Don't prevent default - let the form submit naturally with its action/method
606
663
  }
607
664
  };
@@ -739,6 +796,7 @@ export function init() {
739
796
  window.removeEventListener('resize', handleResize);
740
797
  };
741
798
 
799
+
742
800
  // Initialize the form validation system
743
801
  const initializeFormValidation = () => {
744
802
  try {
@@ -27,16 +27,12 @@ function setupDynamicDropdowns() {
27
27
  };
28
28
 
29
29
  dropdownWrappers.forEach((wrapper) => {
30
- const toggle = wrapper.querySelector("a");
31
- const allElements = wrapper.querySelectorAll("*");
32
- let dropdownList = null;
33
-
34
- for (const element of allElements) {
35
- const links = element.querySelectorAll("a");
36
- if (links.length >= 2 && !element.contains(toggle)) {
37
- dropdownList = element;
38
- break;
39
- }
30
+ const toggle = wrapper.querySelector('[data-hs-nav="dropdown-toggle"]');
31
+ const dropdownList = wrapper.querySelector('[data-hs-nav="dropdown-list"]');
32
+
33
+ if (!toggle || !dropdownList) {
34
+ console.warn("Dropdown wrapper missing required elements:", wrapper);
35
+ return;
40
36
  }
41
37
 
42
38
  const toggleText = toggle.textContent?.trim() || "dropdown";
@@ -68,183 +64,129 @@ function setupDynamicDropdowns() {
68
64
  }
69
65
  });
70
66
 
71
- let isOpen = false;
72
67
  let currentMenuItemIndex = -1;
73
68
 
74
- function openDropdown() {
75
- if (isOpen) return;
76
- closeAllDropdowns(wrapper);
77
-
78
- // Set ARIA states FIRST, before focus changes
79
- isOpen = true;
80
- toggle.setAttribute("aria-expanded", "true");
81
- dropdownList.setAttribute("aria-hidden", "false");
82
- menuItems.forEach((item) => {
83
- item.setAttribute("tabindex", "0");
84
- });
69
+ // Set initial ARIA states
70
+ updateARIAStates();
85
71
 
86
- const clickEvent = new MouseEvent("click", {
87
- bubbles: true,
88
- cancelable: true,
89
- view: window,
90
- });
91
- wrapper.dispatchEvent(clickEvent);
72
+ // Check if dropdown is open by looking for is-active class on wrapper
73
+ function isDropdownOpen() {
74
+ return wrapper.classList.contains('is-active');
92
75
  }
93
76
 
94
- function closeDropdown() {
95
- if (!isOpen) return;
96
- const shouldRestoreFocus = dropdownList.contains(document.activeElement);
97
-
98
- // Update ARIA states FIRST to help screen readers understand content is hidden
99
- isOpen = false;
100
- currentMenuItemIndex = -1;
101
- toggle.setAttribute("aria-expanded", "false");
102
- dropdownList.setAttribute("aria-hidden", "true");
103
- menuItems.forEach((item) => {
104
- item.setAttribute("tabindex", "-1");
105
- });
77
+ // Update ARIA states based on current visual state
78
+ function updateARIAStates() {
79
+ const isOpen = isDropdownOpen();
80
+ const wasOpen = toggle.getAttribute("aria-expanded") === "true";
106
81
 
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();
82
+ // If dropdown is closing (was open, now closed), focus the toggle first
83
+ if (wasOpen && !isOpen && dropdownList.contains(document.activeElement)) {
111
84
  toggle.focus();
112
85
  }
113
-
114
- const clickEvent = new MouseEvent("click", {
115
- bubbles: true,
116
- cancelable: true,
117
- view: window,
86
+
87
+ toggle.setAttribute("aria-expanded", isOpen ? "true" : "false");
88
+ dropdownList.setAttribute("aria-hidden", isOpen ? "false" : "true");
89
+ menuItems.forEach((item) => {
90
+ item.setAttribute("tabindex", isOpen ? "0" : "-1");
118
91
  });
119
- wrapper.dispatchEvent(clickEvent);
120
- }
121
92
 
122
- wrapper.addEventListener("mouseenter", () => {
123
93
  if (!isOpen) {
124
- openDropdown();
94
+ currentMenuItemIndex = -1;
125
95
  }
126
- });
96
+ }
127
97
 
128
- wrapper.addEventListener("mouseleave", () => {
129
- if (isOpen) {
130
- closeDropdown();
131
- }
132
- });
98
+ // Monitor for class changes and update ARIA states
99
+ function monitorDropdownState() {
100
+ const observer = new MutationObserver(() => {
101
+ updateARIAStates();
102
+ });
103
+
104
+ observer.observe(wrapper, {
105
+ attributes: true,
106
+ attributeFilter: ['class']
107
+ });
108
+ }
109
+
110
+ // Initialize monitoring
111
+ monitorDropdownState();
133
112
 
134
- document.addEventListener("keydown", function (e) {
135
- if (!isOpen) return;
136
- if (!wrapper.contains(document.activeElement)) return;
137
-
138
- if (e.key === "ArrowDown") {
139
- e.preventDefault();
140
- if (document.activeElement === toggle) {
141
- currentMenuItemIndex = 0;
142
- menuItems[currentMenuItemIndex].focus();
143
- } else {
144
- if (currentMenuItemIndex === menuItems.length - 1) {
145
- const nextElement =
146
- wrapper.nextElementSibling &&
147
- wrapper.nextElementSibling.querySelector("a, button");
148
- if (nextElement) {
149
- closeDropdown();
150
- nextElement.focus();
151
- return;
152
- }
153
- }
154
- if (currentMenuItemIndex < menuItems.length - 1) {
155
- currentMenuItemIndex = currentMenuItemIndex + 1;
156
- menuItems[currentMenuItemIndex].focus();
157
- }
158
- }
159
- } else if (e.key === "ArrowUp") {
160
- e.preventDefault();
161
- if (document.activeElement === toggle) {
162
- currentMenuItemIndex = menuItems.length - 1;
163
- menuItems[currentMenuItemIndex].focus();
164
- } else {
165
- if (currentMenuItemIndex === 0) {
166
- const prevElement =
167
- wrapper.previousElementSibling &&
168
- wrapper.previousElementSibling.querySelector("a, button");
169
- if (prevElement) {
170
- closeDropdown();
171
- prevElement.focus();
172
- return;
173
- } else {
174
- closeDropdown();
175
- toggle.focus();
176
- return;
177
- }
178
- }
179
- if (currentMenuItemIndex > 0) {
180
- currentMenuItemIndex = currentMenuItemIndex - 1;
181
- menuItems[currentMenuItemIndex].focus();
182
- }
183
- }
184
- } else if (e.key === "Tab") {
185
- if (e.shiftKey) {
186
- if (document.activeElement === menuItems[0]) {
187
- e.preventDefault();
188
- closeDropdown();
189
- toggle.focus();
190
- }
191
- } else {
192
- if (document.activeElement === menuItems[menuItems.length - 1]) {
193
- e.preventDefault();
194
- const nextElement =
195
- wrapper.nextElementSibling &&
196
- wrapper.nextElementSibling.querySelector("a, button");
197
- closeDropdown();
198
- if (nextElement) {
199
- nextElement.focus();
200
- }
201
- }
202
- }
203
- } else if (e.key === "Escape") {
204
- e.preventDefault();
205
- closeDropdown();
206
- toggle.focus();
207
- } else if (e.key === "Home") {
208
- e.preventDefault();
209
- currentMenuItemIndex = 0;
210
- menuItems[0].focus();
211
- } else if (e.key === "End") {
212
- e.preventDefault();
213
- currentMenuItemIndex = menuItems.length - 1;
214
- menuItems[menuItems.length - 1].focus();
215
- } else if (e.key === " ") {
216
- e.preventDefault();
217
- }
218
- });
113
+ // Hover interactions now handled entirely by native Webflow
219
114
 
115
+ // Add keyboard interactions that trigger programmatic mouse events
220
116
  toggle.addEventListener("keydown", function (e) {
221
117
  if (e.key === "ArrowDown") {
222
118
  e.preventDefault();
223
- openDropdown();
224
- if (menuItems.length > 0) {
225
- setTimeout(() => {
226
- currentMenuItemIndex = 0;
227
- menuItems[0].focus();
228
- }, 100);
119
+
120
+ if (!isDropdownOpen()) {
121
+ // Trigger programmatic mouseenter to open dropdown
122
+ const mouseEnterEvent = new MouseEvent("mouseenter", {
123
+ bubbles: false,
124
+ cancelable: false,
125
+ view: window,
126
+ relatedTarget: null
127
+ });
128
+ wrapper.dispatchEvent(mouseEnterEvent);
129
+
130
+ // Focus first menu item after a brief delay
131
+ if (menuItems.length > 0) {
132
+ setTimeout(() => {
133
+ currentMenuItemIndex = 0;
134
+ menuItems[0].focus();
135
+ }, 100);
136
+ }
229
137
  }
230
138
  } else if (e.key === " ") {
231
139
  e.preventDefault();
232
- if (isOpen) {
233
- closeDropdown();
234
- } else {
235
- openDropdown();
140
+
141
+ if (!isDropdownOpen()) {
142
+ // Trigger programmatic mouseenter to open dropdown
143
+ const mouseEnterEvent = new MouseEvent("mouseenter", {
144
+ bubbles: false,
145
+ cancelable: false,
146
+ view: window,
147
+ relatedTarget: null
148
+ });
149
+ wrapper.dispatchEvent(mouseEnterEvent);
150
+
151
+ // Focus first menu item after a brief delay
236
152
  if (menuItems.length > 0) {
237
- currentMenuItemIndex = 0;
238
- menuItems[0].focus();
153
+ setTimeout(() => {
154
+ currentMenuItemIndex = 0;
155
+ menuItems[0].focus();
156
+ }, 100);
239
157
  }
158
+ } else {
159
+ // Trigger mouse leave to close dropdown
160
+ const mouseOutEvent = new MouseEvent("mouseout", {
161
+ bubbles: true,
162
+ cancelable: false,
163
+ view: window,
164
+ relatedTarget: document.body
165
+ });
166
+ wrapper.dispatchEvent(mouseOutEvent);
167
+
168
+ const mouseLeaveEvent = new MouseEvent("mouseleave", {
169
+ bubbles: false,
170
+ cancelable: false,
171
+ view: window,
172
+ relatedTarget: document.body
173
+ });
174
+ wrapper.dispatchEvent(mouseLeaveEvent);
240
175
  }
241
176
  } else if (e.key === "ArrowUp") {
242
177
  e.preventDefault();
243
- if (isOpen) {
244
- currentMenuItemIndex = menuItems.length - 1;
245
- menuItems[currentMenuItemIndex].focus();
246
- } else {
247
- openDropdown();
178
+
179
+ if (!isDropdownOpen()) {
180
+ // Trigger programmatic mouseenter to open dropdown
181
+ const mouseEnterEvent = new MouseEvent("mouseenter", {
182
+ bubbles: false,
183
+ cancelable: false,
184
+ view: window,
185
+ relatedTarget: null
186
+ });
187
+ wrapper.dispatchEvent(mouseEnterEvent);
188
+
189
+ // Focus last menu item after a brief delay
248
190
  if (menuItems.length > 0) {
249
191
  setTimeout(() => {
250
192
  currentMenuItemIndex = menuItems.length - 1;
@@ -254,21 +196,99 @@ function setupDynamicDropdowns() {
254
196
  }
255
197
  } else if (e.key === "Escape") {
256
198
  e.preventDefault();
257
- closeDropdown();
199
+
200
+ if (isDropdownOpen()) {
201
+ // Trigger mouse leave to close dropdown
202
+ const mouseOutEvent = new MouseEvent("mouseout", {
203
+ bubbles: true,
204
+ cancelable: false,
205
+ view: window,
206
+ relatedTarget: document.body
207
+ });
208
+ wrapper.dispatchEvent(mouseOutEvent);
209
+
210
+ const mouseLeaveEvent = new MouseEvent("mouseleave", {
211
+ bubbles: false,
212
+ cancelable: false,
213
+ view: window,
214
+ relatedTarget: document.body
215
+ });
216
+ wrapper.dispatchEvent(mouseLeaveEvent);
217
+ }
258
218
  }
259
219
  });
260
220
 
221
+ // Handle navigation within open dropdown
222
+ document.addEventListener("keydown", function (e) {
223
+ if (!isDropdownOpen()) return;
224
+ if (!wrapper.contains(document.activeElement)) return;
261
225
 
262
- document.addEventListener("click", function (e) {
263
- if (!wrapper.contains(e.target) && isOpen) {
264
- closeDropdown();
226
+ if (e.key === "ArrowDown") {
227
+ e.preventDefault();
228
+ if (currentMenuItemIndex < menuItems.length - 1) {
229
+ currentMenuItemIndex++;
230
+ menuItems[currentMenuItemIndex].focus();
231
+ }
232
+ } else if (e.key === "ArrowUp") {
233
+ e.preventDefault();
234
+ if (currentMenuItemIndex > 0) {
235
+ currentMenuItemIndex--;
236
+ menuItems[currentMenuItemIndex].focus();
237
+ } else if (currentMenuItemIndex === 0) {
238
+ // On first item, trigger mouse leave to close dropdown
239
+ const mouseOutEvent = new MouseEvent("mouseout", {
240
+ bubbles: true,
241
+ cancelable: false,
242
+ view: window,
243
+ relatedTarget: document.body
244
+ });
245
+ wrapper.dispatchEvent(mouseOutEvent);
246
+
247
+ const mouseLeaveEvent = new MouseEvent("mouseleave", {
248
+ bubbles: false,
249
+ cancelable: false,
250
+ view: window,
251
+ relatedTarget: document.body
252
+ });
253
+ wrapper.dispatchEvent(mouseLeaveEvent);
254
+
255
+ // Focus back to toggle
256
+ toggle.focus();
257
+ currentMenuItemIndex = -1;
258
+ } else {
259
+ // Go back to toggle
260
+ toggle.focus();
261
+ currentMenuItemIndex = -1;
262
+ }
263
+ } else if (e.key === "Escape") {
264
+ e.preventDefault();
265
+
266
+ // Trigger mouse leave to close dropdown
267
+ const mouseOutEvent = new MouseEvent("mouseout", {
268
+ bubbles: true,
269
+ cancelable: false,
270
+ view: window,
271
+ relatedTarget: document.body
272
+ });
273
+ wrapper.dispatchEvent(mouseOutEvent);
274
+
275
+ const mouseLeaveEvent = new MouseEvent("mouseleave", {
276
+ bubbles: false,
277
+ cancelable: false,
278
+ view: window,
279
+ relatedTarget: document.body
280
+ });
281
+ wrapper.dispatchEvent(mouseLeaveEvent);
265
282
  }
266
283
  });
267
284
 
268
285
  allDropdowns.push({
269
286
  wrapper,
270
- isOpen: () => isOpen,
271
- closeDropdown,
287
+ isOpen: isDropdownOpen,
288
+ closeDropdown: () => {
289
+ // closeDropdown now handled by native Webflow interactions
290
+ // This is kept for API compatibility but does nothing
291
+ },
272
292
  toggle,
273
293
  dropdownList,
274
294
  });
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // Version:1.6.5
1
+ // Version:1.6.7
2
2
  const API_NAME = "hsmain";
3
3
 
4
4
  const initializeHsMain = async () => {
@@ -16,6 +16,7 @@ const initializeHsMain = async () => {
16
16
 
17
17
  const utilityModules = {
18
18
  "data-hs-util-ba": true,
19
+ "data-hs-util-slider": true,
19
20
  };
20
21
 
21
22
  const autoInitModules = {
@@ -38,6 +39,7 @@ const initializeHsMain = async () => {
38
39
  const moduleMap = {
39
40
  transition: () => import("./autoInit/transition.js"),
40
41
  "data-hs-util-ba": () => import("./utils/before-after.js"),
42
+ "data-hs-util-slider": () => import("./utils/slider.js"),
41
43
  "smooth-scroll": () => import("./autoInit/smooth-scroll.js"),
42
44
  navbar: () => import("./autoInit/navbar.js"),
43
45
  accessibility: () => import("./autoInit/accessibility.js"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hortonstudio/main",
3
- "version": "1.6.5",
3
+ "version": "1.6.7",
4
4
  "description": "Animation and utility library for client websites",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -0,0 +1,95 @@
1
+ export const init = () => {
2
+ // Null checks for DOM elements
3
+ const wrapper = document.querySelector('[data-hs-slider="wrapper"]');
4
+ const nextBtn = document.querySelector('[data-hs-slider="next"]');
5
+ const prevBtn = document.querySelector('[data-hs-slider="previous"]');
6
+
7
+ // Early return if required elements don't exist
8
+ if (!wrapper || !nextBtn || !prevBtn) {
9
+ return;
10
+ }
11
+
12
+ // Validate slides exist and have length
13
+ const slides = wrapper.children;
14
+ if (!slides || slides.length === 0) {
15
+ return;
16
+ }
17
+
18
+ // Check if gsap is available globally
19
+ if (typeof gsap === 'undefined') {
20
+ return;
21
+ }
22
+
23
+ const totalSlides = slides.length;
24
+ let currentIndex = 0;
25
+ let isAnimating = false;
26
+
27
+ const firstClone = slides[0].cloneNode(true);
28
+ const lastClone = slides[slides.length - 1].cloneNode(true);
29
+
30
+ wrapper.appendChild(firstClone);
31
+ wrapper.insertBefore(lastClone, slides[0]);
32
+
33
+ currentIndex = 1;
34
+ gsap.set(wrapper, { xPercent: -100 });
35
+
36
+ nextBtn.addEventListener('click', () => {
37
+ if (isAnimating) return;
38
+ isAnimating = true;
39
+
40
+ if (currentIndex === totalSlides) {
41
+ currentIndex++;
42
+ gsap.to(wrapper, {
43
+ xPercent: -currentIndex * 100,
44
+ duration: 0.5,
45
+ ease: "power2.inOut",
46
+ onComplete: () => {
47
+ gsap.set(wrapper, { xPercent: -100 });
48
+ currentIndex = 1;
49
+ isAnimating = false;
50
+ }
51
+ });
52
+ } else {
53
+ currentIndex++;
54
+ gsap.to(wrapper, {
55
+ xPercent: -currentIndex * 100,
56
+ duration: 0.5,
57
+ ease: "power2.inOut",
58
+ onComplete: () => {
59
+ isAnimating = false;
60
+ }
61
+ });
62
+ }
63
+ });
64
+
65
+ prevBtn.addEventListener('click', () => {
66
+ if (isAnimating) return;
67
+ isAnimating = true;
68
+
69
+ if (currentIndex === 1) {
70
+ currentIndex--;
71
+ gsap.to(wrapper, {
72
+ xPercent: -currentIndex * 100,
73
+ duration: 0.5,
74
+ ease: "power2.inOut",
75
+ onComplete: () => {
76
+ gsap.set(wrapper, { xPercent: -totalSlides * 100 });
77
+ currentIndex = totalSlides;
78
+ isAnimating = false;
79
+ }
80
+ });
81
+ } else {
82
+ currentIndex--;
83
+ gsap.to(wrapper, {
84
+ xPercent: -currentIndex * 100,
85
+ duration: 0.5,
86
+ ease: "power2.inOut",
87
+ onComplete: () => {
88
+ isAnimating = false;
89
+ }
90
+ });
91
+ }
92
+ });
93
+ };
94
+
95
+ export const version = "1.0.0";