@hortonstudio/main 1.2.35 → 1.4.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.
- package/.claude/settings.local.json +22 -1
- package/TEMP-before-after-attributes.md +158 -0
- package/animations/hero.js +741 -611
- package/animations/text.js +505 -317
- package/animations/transition.js +36 -21
- package/autoInit/accessibility.js +7 -67
- package/autoInit/counter.js +338 -0
- package/autoInit/custom-values.js +266 -0
- package/autoInit/form.js +471 -0
- package/autoInit/modal.js +43 -38
- package/autoInit/navbar.js +484 -371
- package/autoInit/smooth-scroll.js +86 -84
- package/index.js +140 -88
- package/package.json +1 -1
- package/utils/before-after.js +279 -146
- package/utils/scroll-progress.js +26 -21
- package/utils/toc.js +73 -66
- package/CLAUDE.md +0 -45
- package/debug-version.html +0 -37
package/autoInit/form.js
ADDED
@@ -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
|
14
|
-
if (width <= config.breakpoints.tablet) return
|
15
|
-
return
|
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(
|
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(
|
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(
|
32
|
-
|
30
|
+
|
31
|
+
document.body.classList.add("u-overflow-clip");
|
32
|
+
|
33
33
|
// Add blur to all other modals
|
34
|
-
document.querySelectorAll(
|
34
|
+
document.querySelectorAll("[data-hs-modal]").forEach((modal) => {
|
35
35
|
if (modal !== element) {
|
36
|
-
modal.style.display =
|
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(
|
45
|
-
|
44
|
+
document.body.classList.remove("u-overflow-clip");
|
45
|
+
|
46
46
|
// Remove blur from all other modals
|
47
|
-
document.querySelectorAll(
|
47
|
+
document.querySelectorAll("[data-hs-modal]").forEach((modal) => {
|
48
48
|
if (modal !== element) {
|
49
|
-
modal.style.display =
|
50
|
-
modal.style.opacity =
|
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(
|
61
|
-
const elementKey =
|
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
|
89
|
-
|
90
|
-
|
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(
|
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(
|
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(
|
114
|
+
window.addEventListener("resize", function () {
|
110
115
|
handleBreakpointChange();
|
111
116
|
});
|
112
117
|
|
113
|
-
return { result:
|
118
|
+
return { result: "modal initialized" };
|
114
119
|
}
|
115
120
|
|
116
121
|
export function init() {
|
117
|
-
if (document.readyState ===
|
118
|
-
document.addEventListener(
|
122
|
+
if (document.readyState === "loading") {
|
123
|
+
document.addEventListener("DOMContentLoaded", initModal);
|
119
124
|
} else {
|
120
125
|
initModal();
|
121
126
|
}
|
122
|
-
|
123
|
-
return { result:
|
124
|
-
}
|
127
|
+
|
128
|
+
return { result: "modal initialized" };
|
129
|
+
}
|