@hortonstudio/main 1.2.35 → 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.
@@ -0,0 +1,471 @@
1
+ export function init() {
2
+ const config = {
3
+ selectors: {
4
+ form: 'form',
5
+ errorTemplate: '[data-hs-form="form-error"]',
6
+ requiredStep: '[data-hs-form="required-step"]',
7
+ nextButton: '[data-hs-form="next-button"]',
8
+ nextAnim: '[data-hs-form="next-anim"]',
9
+ finalAnim: '[data-hs-form="final-anim"]'
10
+ },
11
+ errorMessages: {
12
+ email: 'Please enter a valid email address',
13
+ tel: 'Please enter a valid phone number',
14
+ number: 'Please enter a valid number',
15
+ url: 'Please enter a valid URL',
16
+ text: 'This field is required',
17
+ textarea: 'This field is required',
18
+ select: 'Please select an option',
19
+ checkbox: 'Please check this box',
20
+ radio: 'Please select an option',
21
+ default: 'This field is required'
22
+ }
23
+ };
24
+
25
+ const activeErrors = new Map();
26
+ let errorTemplate = null;
27
+ let originalErrorTemplate = null;
28
+ let isValidating = false;
29
+
30
+ const initializeForms = () => {
31
+ const forms = document.querySelectorAll(config.selectors.form);
32
+ forms.forEach(form => {
33
+ form.setAttribute('novalidate', '');
34
+
35
+ // Completely disable all browser validation methods
36
+ form.checkValidity = () => true;
37
+ form.reportValidity = () => true;
38
+
39
+ // Override all input validation as well
40
+ const inputs = form.querySelectorAll('input, textarea, select');
41
+ inputs.forEach(input => {
42
+ // Remove required attribute and store it
43
+ if (input.hasAttribute('required')) {
44
+ input.removeAttribute('required');
45
+ input.setAttribute('data-was-required', 'true');
46
+ }
47
+
48
+ // Override validation methods
49
+ input.checkValidity = () => true;
50
+ input.reportValidity = () => true;
51
+ input.setCustomValidity = () => {};
52
+ });
53
+ });
54
+
55
+ errorTemplate = document.querySelector(config.selectors.errorTemplate);
56
+ if (!errorTemplate) {
57
+ console.warn('Form validation: Error template not found');
58
+ } else {
59
+ // Clone the template, clean it up, and remove original from DOM
60
+ originalErrorTemplate = errorTemplate.cloneNode(true);
61
+
62
+ // Clean up the cloned template
63
+ originalErrorTemplate.removeAttribute('data-hs-form');
64
+ originalErrorTemplate.removeAttribute('style');
65
+ originalErrorTemplate.textContent = ''; // Clear any placeholder text
66
+
67
+ // Remove the original template from the DOM
68
+ errorTemplate.remove();
69
+ }
70
+ };
71
+
72
+ const getErrorMessage = (input) => {
73
+ const inputType = input.type || input.tagName.toLowerCase();
74
+ const value = input.value.trim();
75
+
76
+ // If field is empty and was required, return required message
77
+ if (input.hasAttribute('data-was-required') && !value) {
78
+ return config.errorMessages[inputType] || config.errorMessages.default;
79
+ }
80
+
81
+ // For non-empty invalid fields, return type-specific messages
82
+ if (value) {
83
+ switch (inputType) {
84
+ case 'email':
85
+ return 'Please enter a valid email address';
86
+ case 'url':
87
+ return 'Please enter a valid URL';
88
+ case 'number':
89
+ if (input.hasAttribute('min') && parseFloat(value) < parseFloat(input.min)) {
90
+ return `Number must be at least ${input.min}`;
91
+ }
92
+ if (input.hasAttribute('max') && parseFloat(value) > parseFloat(input.max)) {
93
+ return `Number must be no more than ${input.max}`;
94
+ }
95
+ return 'Please enter a valid number';
96
+ case 'tel':
97
+ return 'Please enter a valid phone number';
98
+ default:
99
+ if (input.hasAttribute('minlength') && value.length < parseInt(input.minlength)) {
100
+ return `Must be at least ${input.minlength} characters`;
101
+ }
102
+ if (input.hasAttribute('maxlength') && value.length > parseInt(input.maxlength)) {
103
+ return `Must be no more than ${input.maxlength} characters`;
104
+ }
105
+ if (input.hasAttribute('pattern')) {
106
+ return 'Please match the required format';
107
+ }
108
+ return config.errorMessages[inputType] || config.errorMessages.default;
109
+ }
110
+ }
111
+
112
+ return config.errorMessages[inputType] || config.errorMessages.default;
113
+ };
114
+
115
+ const createErrorElement = (message) => {
116
+ if (!originalErrorTemplate) return null;
117
+
118
+ const errorElement = originalErrorTemplate.cloneNode(true);
119
+ errorElement.textContent = message;
120
+
121
+ return errorElement;
122
+ };
123
+
124
+ const positionError = (errorElement, input) => {
125
+ const inputRect = input.getBoundingClientRect();
126
+ const errorRect = errorElement.getBoundingClientRect();
127
+
128
+ let top = inputRect.bottom + window.scrollY + 8;
129
+ let left = inputRect.left + window.scrollX + (inputRect.width / 2) - (errorRect.width / 2);
130
+
131
+ // Check viewport boundaries
132
+ const viewportWidth = window.innerWidth;
133
+ const viewportHeight = window.innerHeight;
134
+
135
+ // Adjust horizontal position
136
+ if (left < 10) {
137
+ left = 10;
138
+ } else if (left + errorRect.width > viewportWidth - 10) {
139
+ left = viewportWidth - errorRect.width - 10;
140
+ }
141
+
142
+ // Adjust vertical position if error would be below viewport
143
+ if (inputRect.bottom + errorRect.height + 16 > viewportHeight) {
144
+ top = inputRect.top + window.scrollY - errorRect.height - 8;
145
+ }
146
+
147
+ const finalTop = top - window.scrollY;
148
+ const finalLeft = left - window.scrollX;
149
+
150
+ // Only set the minimal positioning styles needed
151
+ errorElement.style.position = 'fixed';
152
+ errorElement.style.top = `${finalTop}px`;
153
+ errorElement.style.left = `${finalLeft}px`;
154
+ errorElement.style.zIndex = '9999';
155
+ };
156
+
157
+ const showError = (input, message) => {
158
+ removeError(input);
159
+
160
+ const errorElement = createErrorElement(message);
161
+ if (!errorElement) return;
162
+ document.body.appendChild(errorElement);
163
+ positionError(errorElement, input);
164
+
165
+ activeErrors.set(input, errorElement);
166
+
167
+ input.setAttribute('aria-invalid', 'true');
168
+ input.setAttribute('aria-describedby', `error-${Date.now()}`);
169
+ errorElement.id = input.getAttribute('aria-describedby');
170
+ };
171
+
172
+ const removeError = (input) => {
173
+ const errorElement = activeErrors.get(input);
174
+ if (errorElement) {
175
+ errorElement.remove();
176
+ activeErrors.delete(input);
177
+ input.removeAttribute('aria-invalid');
178
+ input.removeAttribute('aria-describedby');
179
+ }
180
+ };
181
+
182
+ const customValidateField = (field) => {
183
+ const value = field.value.trim();
184
+ const type = field.type || field.tagName.toLowerCase();
185
+
186
+ // Check if field was required (now stored in data-was-required)
187
+ if (field.hasAttribute('data-was-required')) {
188
+ // Special handling for checkboxes and radio buttons
189
+ if (type === 'checkbox' || type === 'radio') {
190
+ return field.checked;
191
+ }
192
+
193
+ // For other field types, check if empty
194
+ if (!value) {
195
+ return false;
196
+ }
197
+
198
+ // Type-specific validation for non-empty values
199
+ switch (type) {
200
+ case 'email':
201
+ // Basic email validation
202
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
203
+ return emailRegex.test(value);
204
+
205
+ case 'url':
206
+ // Basic URL validation
207
+ try {
208
+ new URL(value);
209
+ return true;
210
+ } catch {
211
+ return false;
212
+ }
213
+
214
+ case 'number':
215
+ // Number validation
216
+ const num = parseFloat(value);
217
+ if (isNaN(num)) return false;
218
+
219
+ // Check min/max if present
220
+ if (field.hasAttribute('min') && num < parseFloat(field.min)) return false;
221
+ if (field.hasAttribute('max') && num > parseFloat(field.max)) return false;
222
+ return true;
223
+
224
+ case 'tel':
225
+ // Basic phone validation (at least 10 digits)
226
+ const phoneRegex = /\d{10,}/;
227
+ return phoneRegex.test(value.replace(/\D/g, ''));
228
+
229
+ default:
230
+ // For text, textarea, etc. - check minlength/maxlength
231
+ if (field.hasAttribute('minlength') && value.length < parseInt(field.minlength)) return false;
232
+ if (field.hasAttribute('maxlength') && value.length > parseInt(field.maxlength)) return false;
233
+
234
+ // Check pattern if present
235
+ if (field.hasAttribute('pattern')) {
236
+ const pattern = new RegExp(field.pattern);
237
+ return pattern.test(value);
238
+ }
239
+
240
+ return true;
241
+ }
242
+ }
243
+
244
+ return true; // Field is valid if not required or passes validation
245
+ };
246
+
247
+ const validateContainer = (container) => {
248
+ const requiredFields = container.querySelectorAll('input[data-was-required], textarea[data-was-required], select[data-was-required]');
249
+ let isValid = true;
250
+ let firstInvalidField = null;
251
+
252
+ requiredFields.forEach((field) => {
253
+ const fieldValid = customValidateField(field);
254
+
255
+ if (!fieldValid) {
256
+ isValid = false;
257
+
258
+ if (!firstInvalidField) {
259
+ firstInvalidField = field;
260
+ }
261
+ }
262
+ });
263
+
264
+ if (!isValid && firstInvalidField) {
265
+ activeErrors.forEach((_, input) => {
266
+ removeError(input);
267
+ });
268
+
269
+ firstInvalidField.focus();
270
+
271
+ const message = getErrorMessage(firstInvalidField);
272
+ showError(firstInvalidField, message);
273
+ }
274
+
275
+ return isValid;
276
+ };
277
+
278
+ const handleFormSubmit = (event) => {
279
+ const form = event.target;
280
+
281
+ // Set validation flag to prevent click outside interference
282
+ isValidating = true;
283
+
284
+ const isValid = validateContainer(form);
285
+
286
+ // Reset validation flag after a brief delay
287
+ setTimeout(() => {
288
+ isValidating = false;
289
+ }, 200);
290
+
291
+ if (!isValid) {
292
+ // Only prevent submission if form is invalid
293
+ event.preventDefault();
294
+ event.stopPropagation();
295
+ event.stopImmediatePropagation();
296
+ return false;
297
+ } else {
298
+ // Clear all errors before submission
299
+ activeErrors.forEach((_, input) => {
300
+ removeError(input);
301
+ });
302
+
303
+ // Trigger final animation if it exists
304
+ const finalAnimElement = form.querySelector(config.selectors.finalAnim);
305
+ if (finalAnimElement) {
306
+ finalAnimElement.click();
307
+ }
308
+
309
+ // Don't prevent default - let the form submit naturally with its action/method
310
+ }
311
+ };
312
+
313
+ const handleNextButtonClick = (event) => {
314
+ event.preventDefault();
315
+
316
+ const button = event.target;
317
+ const requiredStepContainer = button.closest(config.selectors.requiredStep);
318
+
319
+ if (!requiredStepContainer) {
320
+ return;
321
+ }
322
+
323
+ // Set validation flag to prevent click outside interference
324
+ isValidating = true;
325
+
326
+ const isValid = validateContainer(requiredStepContainer);
327
+
328
+ // Reset validation flag after a brief delay
329
+ setTimeout(() => {
330
+ isValidating = false;
331
+ }, 200);
332
+
333
+ if (isValid) {
334
+ const nextAnimElement = requiredStepContainer.querySelector(config.selectors.nextAnim);
335
+ if (nextAnimElement) {
336
+ nextAnimElement.click();
337
+ }
338
+ }
339
+ };
340
+
341
+ const handleInputChange = (event) => {
342
+ const input = event.target;
343
+ if (activeErrors.has(input)) {
344
+ removeError(input);
345
+ }
346
+ };
347
+
348
+ const handleClickOutside = (event) => {
349
+ const clickedElement = event.target;
350
+
351
+ // Don't process click outside during validation
352
+ if (isValidating) {
353
+ return;
354
+ }
355
+
356
+ // Don't remove errors if clicking on next button
357
+ if (clickedElement.closest(config.selectors.nextButton)) {
358
+ return;
359
+ }
360
+
361
+ // Don't remove errors immediately after they're created
362
+ setTimeout(() => {
363
+ // Double check validation flag hasn't been set during timeout
364
+ if (isValidating) {
365
+ return;
366
+ }
367
+
368
+ activeErrors.forEach((errorElement, input) => {
369
+ if (input !== clickedElement && !input.contains(clickedElement) &&
370
+ errorElement !== clickedElement && !errorElement.contains(clickedElement)) {
371
+ removeError(input);
372
+ }
373
+ });
374
+ }, 100); // Small delay to prevent immediate removal
375
+ };
376
+
377
+ const handleScroll = () => {
378
+ activeErrors.forEach((errorElement, input) => {
379
+ const inputRect = input.getBoundingClientRect();
380
+ const isVisible = inputRect.top >= 0 && inputRect.bottom <= window.innerHeight;
381
+
382
+ if (isVisible) {
383
+ positionError(errorElement, input);
384
+ } else {
385
+ errorElement.style.display = 'none';
386
+ }
387
+ });
388
+ };
389
+
390
+ const handleResize = () => {
391
+ activeErrors.forEach((errorElement, input) => {
392
+ positionError(errorElement, input);
393
+ });
394
+ };
395
+
396
+ const setupEventListeners = () => {
397
+ // Global invalid event prevention - this catches ALL invalid events
398
+ document.addEventListener('invalid', (e) => {
399
+ e.preventDefault();
400
+ e.stopPropagation();
401
+ e.stopImmediatePropagation();
402
+ return false;
403
+ }, true);
404
+
405
+ // Form submit listeners - use capture phase to ensure we catch it first
406
+ document.addEventListener('submit', handleFormSubmit, true);
407
+
408
+ // Next button listeners
409
+ document.addEventListener('click', (event) => {
410
+ if (event.target.closest(config.selectors.nextButton)) {
411
+ handleNextButtonClick(event);
412
+ }
413
+ });
414
+
415
+ // Input change listeners
416
+ document.addEventListener('input', handleInputChange);
417
+
418
+ // Click outside listeners
419
+ document.addEventListener('click', handleClickOutside);
420
+
421
+ // Scroll and resize listeners
422
+ window.addEventListener('scroll', handleScroll);
423
+ window.addEventListener('resize', handleResize);
424
+ };
425
+
426
+ const destroy = () => {
427
+ // Clean up all active errors
428
+ activeErrors.forEach((_, input) => {
429
+ removeError(input);
430
+ });
431
+
432
+ // Remove event listeners
433
+ document.removeEventListener('invalid', (e) => {
434
+ e.preventDefault();
435
+ e.stopPropagation();
436
+ e.stopImmediatePropagation();
437
+ return false;
438
+ }, true);
439
+ document.removeEventListener('submit', handleFormSubmit, true);
440
+ document.removeEventListener('input', handleInputChange);
441
+ document.removeEventListener('click', handleClickOutside);
442
+ window.removeEventListener('scroll', handleScroll);
443
+ window.removeEventListener('resize', handleResize);
444
+ };
445
+
446
+ // Initialize the form validation system
447
+ const initializeFormValidation = () => {
448
+ try {
449
+ initializeForms();
450
+ setupEventListeners();
451
+
452
+ return {
453
+ result: "form initialized",
454
+ destroy
455
+ };
456
+ } catch (error) {
457
+ console.error('Form validation initialization failed:', error);
458
+ return {
459
+ result: "form initialization failed",
460
+ destroy
461
+ };
462
+ }
463
+ };
464
+
465
+ // Handle DOM ready state
466
+ if (document.readyState === "loading") {
467
+ document.addEventListener("DOMContentLoaded", initializeFormValidation);
468
+ } else {
469
+ return initializeFormValidation();
470
+ }
471
+ }
package/autoInit/modal.js CHANGED
@@ -4,36 +4,36 @@ function initModal() {
4
4
  blurOpacity: 0.5,
5
5
  breakpoints: {
6
6
  mobile: 767,
7
- tablet: 991
8
- }
7
+ tablet: 991,
8
+ },
9
9
  };
10
10
 
11
11
  function getCurrentBreakpoint() {
12
12
  const width = window.innerWidth;
13
- if (width <= config.breakpoints.mobile) return 'mobile';
14
- if (width <= config.breakpoints.tablet) return 'tablet';
15
- return 'desktop';
13
+ if (width <= config.breakpoints.mobile) return "mobile";
14
+ if (width <= config.breakpoints.tablet) return "tablet";
15
+ return "desktop";
16
16
  }
17
17
 
18
18
  function shouldPreventModal(element) {
19
- const preventAttr = element.getAttribute('data-hs-modalprevent');
19
+ const preventAttr = element.getAttribute("data-hs-modalprevent");
20
20
  if (!preventAttr) return false;
21
-
21
+
22
22
  const currentBreakpoint = getCurrentBreakpoint();
23
- const preventBreakpoints = preventAttr.split(',').map(bp => bp.trim());
24
-
23
+ const preventBreakpoints = preventAttr.split(",").map((bp) => bp.trim());
24
+
25
25
  return preventBreakpoints.includes(currentBreakpoint);
26
26
  }
27
27
 
28
28
  function openModal(element) {
29
29
  if (shouldPreventModal(element)) return;
30
-
31
- document.body.classList.add('u-overflow-clip');
32
-
30
+
31
+ document.body.classList.add("u-overflow-clip");
32
+
33
33
  // Add blur to all other modals
34
- document.querySelectorAll('[data-hs-modal]').forEach(modal => {
34
+ document.querySelectorAll("[data-hs-modal]").forEach((modal) => {
35
35
  if (modal !== element) {
36
- modal.style.display = 'block';
36
+ modal.style.display = "block";
37
37
  modal.style.opacity = config.blurOpacity;
38
38
  modal.style.transition = `opacity ${config.transitionDuration}s ease`;
39
39
  }
@@ -41,13 +41,13 @@ function initModal() {
41
41
  }
42
42
 
43
43
  function closeModal(element) {
44
- document.body.classList.remove('u-overflow-clip');
45
-
44
+ document.body.classList.remove("u-overflow-clip");
45
+
46
46
  // Remove blur from all other modals
47
- document.querySelectorAll('[data-hs-modal]').forEach(modal => {
47
+ document.querySelectorAll("[data-hs-modal]").forEach((modal) => {
48
48
  if (modal !== element) {
49
- modal.style.display = 'none';
50
- modal.style.opacity = '0';
49
+ modal.style.display = "none";
50
+ modal.style.opacity = "0";
51
51
  modal.style.transition = `opacity ${config.transitionDuration}s ease`;
52
52
  }
53
53
  });
@@ -57,11 +57,14 @@ function initModal() {
57
57
  let preventedModalStates = new Map();
58
58
 
59
59
  function handleBreakpointChange() {
60
- document.querySelectorAll('[data-hs-modalprevent]').forEach(element => {
61
- const elementKey = element.getAttribute('data-hs-modal') + '_' + (element.id || element.className);
60
+ document.querySelectorAll("[data-hs-modalprevent]").forEach((element) => {
61
+ const elementKey =
62
+ element.getAttribute("data-hs-modal") +
63
+ "_" +
64
+ (element.id || element.className);
62
65
  const shouldPrevent = shouldPreventModal(element);
63
66
  const wasStoredAsOpen = preventedModalStates.get(elementKey);
64
-
67
+
65
68
  if (shouldPrevent && element.x) {
66
69
  preventedModalStates.set(elementKey, true);
67
70
  element.x = 0;
@@ -76,7 +79,7 @@ function initModal() {
76
79
 
77
80
  function toggleModal(element) {
78
81
  element.x = ((element.x || 0) + 1) % 2;
79
-
82
+
80
83
  if (element.x) {
81
84
  openModal(element);
82
85
  } else {
@@ -85,40 +88,42 @@ function initModal() {
85
88
  }
86
89
 
87
90
  // Initialize openclose functionality
88
- document.querySelectorAll('[data-hs-modal="openclose"]').forEach(trigger => {
89
- trigger.addEventListener('click', function() {
90
- toggleModal(this);
91
+ document
92
+ .querySelectorAll('[data-hs-modal="openclose"]')
93
+ .forEach((trigger) => {
94
+ trigger.addEventListener("click", function () {
95
+ toggleModal(this);
96
+ });
91
97
  });
92
- });
93
98
 
94
99
  // Initialize open functionality
95
- document.querySelectorAll('[data-hs-modal="open"]').forEach(trigger => {
96
- trigger.addEventListener('click', function() {
100
+ document.querySelectorAll('[data-hs-modal="open"]').forEach((trigger) => {
101
+ trigger.addEventListener("click", function () {
97
102
  openModal(this);
98
103
  });
99
104
  });
100
105
 
101
106
  // Initialize close functionality
102
- document.querySelectorAll('[data-hs-modal="close"]').forEach(trigger => {
103
- trigger.addEventListener('click', function() {
107
+ document.querySelectorAll('[data-hs-modal="close"]').forEach((trigger) => {
108
+ trigger.addEventListener("click", function () {
104
109
  closeModal(this);
105
110
  });
106
111
  });
107
112
 
108
113
  // Handle window resize to check for prevented modals
109
- window.addEventListener('resize', function() {
114
+ window.addEventListener("resize", function () {
110
115
  handleBreakpointChange();
111
116
  });
112
117
 
113
- return { result: 'modal initialized' };
118
+ return { result: "modal initialized" };
114
119
  }
115
120
 
116
121
  export function init() {
117
- if (document.readyState === 'loading') {
118
- document.addEventListener('DOMContentLoaded', initModal);
122
+ if (document.readyState === "loading") {
123
+ document.addEventListener("DOMContentLoaded", initModal);
119
124
  } else {
120
125
  initModal();
121
126
  }
122
-
123
- return { result: 'modal initialized' };
124
- }
127
+
128
+ return { result: "modal initialized" };
129
+ }