@hortonstudio/main 1.6.6 → 1.7.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 +2 -1
- package/autoInit/accessibility.js +142 -0
- package/autoInit/form.js +58 -0
- package/autoInit/navbar.js +6 -10
- package/autoInit/transition.js +34 -4
- package/index.js +3 -1
- package/package.json +1 -1
- package/utils/slider.js +222 -0
|
@@ -11,6 +11,8 @@ export function init() {
|
|
|
11
11
|
setupRichTextAccessibility();
|
|
12
12
|
setupSummaryAccessibility();
|
|
13
13
|
setupCustomValuesReplacement();
|
|
14
|
+
setupClickForwarding();
|
|
15
|
+
setupTextSynchronization();
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
function setupListAccessibility() {
|
|
@@ -432,6 +434,146 @@ export function init() {
|
|
|
432
434
|
});
|
|
433
435
|
}
|
|
434
436
|
|
|
437
|
+
function setupClickForwarding() {
|
|
438
|
+
// Find all clickable elements (custom styled elements users click)
|
|
439
|
+
const clickableElements = document.querySelectorAll('[data-hs-a11y*="clickable"]');
|
|
440
|
+
|
|
441
|
+
clickableElements.forEach(clickableElement => {
|
|
442
|
+
const attribute = clickableElement.getAttribute('data-hs-a11y');
|
|
443
|
+
|
|
444
|
+
// Parse the attribute: "click-trigger-[identifier], clickable"
|
|
445
|
+
const parts = attribute.split(',').map(part => part.trim());
|
|
446
|
+
|
|
447
|
+
// Find the part with click-trigger and the part with clickable
|
|
448
|
+
const triggerPart = parts.find(part => part.startsWith('click-trigger-'));
|
|
449
|
+
const rolePart = parts.find(part => part === 'clickable');
|
|
450
|
+
|
|
451
|
+
if (!triggerPart || !rolePart) {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Extract identifier from "click-trigger-[identifier]"
|
|
456
|
+
const identifier = triggerPart.replace('click-trigger-', '').trim();
|
|
457
|
+
|
|
458
|
+
// Find the corresponding trigger element
|
|
459
|
+
const triggerSelector = `[data-hs-a11y*="click-trigger-${identifier}"][data-hs-a11y*="trigger"]`;
|
|
460
|
+
const triggerElement = document.querySelector(triggerSelector);
|
|
461
|
+
|
|
462
|
+
if (!triggerElement) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Add click event listener to forward clicks
|
|
467
|
+
clickableElement.addEventListener('click', (event) => {
|
|
468
|
+
// Prevent default behavior on the clickable element
|
|
469
|
+
event.preventDefault();
|
|
470
|
+
event.stopPropagation();
|
|
471
|
+
|
|
472
|
+
// Trigger click on the target element
|
|
473
|
+
triggerElement.click();
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// Also handle keyboard events for accessibility
|
|
477
|
+
clickableElement.addEventListener('keydown', (event) => {
|
|
478
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
479
|
+
event.preventDefault();
|
|
480
|
+
event.stopPropagation();
|
|
481
|
+
triggerElement.click();
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// Ensure clickable element is keyboard accessible
|
|
486
|
+
if (!clickableElement.hasAttribute('tabindex')) {
|
|
487
|
+
clickableElement.setAttribute('tabindex', '0');
|
|
488
|
+
}
|
|
489
|
+
if (!clickableElement.hasAttribute('role')) {
|
|
490
|
+
clickableElement.setAttribute('role', 'button');
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function setupTextSynchronization() {
|
|
496
|
+
// Find all original elements (source of truth)
|
|
497
|
+
const originalElements = document.querySelectorAll('[data-hs-a11y*="original"]');
|
|
498
|
+
|
|
499
|
+
originalElements.forEach(originalElement => {
|
|
500
|
+
const attribute = originalElement.getAttribute('data-hs-a11y');
|
|
501
|
+
|
|
502
|
+
// Parse the attribute: "match-text-[identifier], original"
|
|
503
|
+
const parts = attribute.split(',').map(part => part.trim());
|
|
504
|
+
|
|
505
|
+
// Find the part with match-text and the part with original
|
|
506
|
+
const textPart = parts.find(part => part.startsWith('match-text-'));
|
|
507
|
+
const rolePart = parts.find(part => part === 'original');
|
|
508
|
+
|
|
509
|
+
if (!textPart || !rolePart) {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Extract identifier from "match-text-[identifier]"
|
|
514
|
+
const identifier = textPart.replace('match-text-', '').trim();
|
|
515
|
+
|
|
516
|
+
// Find all corresponding match elements
|
|
517
|
+
const matchSelector = `[data-hs-a11y*="match-text-${identifier}"][data-hs-a11y*="match"]`;
|
|
518
|
+
const matchElements = document.querySelectorAll(matchSelector);
|
|
519
|
+
|
|
520
|
+
if (matchElements.length === 0) {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Function to synchronize text and aria-label
|
|
525
|
+
function synchronizeContent() {
|
|
526
|
+
const originalText = originalElement.textContent;
|
|
527
|
+
const originalAriaLabel = originalElement.getAttribute('aria-label');
|
|
528
|
+
|
|
529
|
+
matchElements.forEach(matchElement => {
|
|
530
|
+
// Copy text content
|
|
531
|
+
matchElement.textContent = originalText;
|
|
532
|
+
|
|
533
|
+
// Synchronize aria-label
|
|
534
|
+
if (originalAriaLabel) {
|
|
535
|
+
// If original has aria-label, copy it to match
|
|
536
|
+
matchElement.setAttribute('aria-label', originalAriaLabel);
|
|
537
|
+
} else {
|
|
538
|
+
// If original has no aria-label, remove it from match (keep in sync)
|
|
539
|
+
if (matchElement.hasAttribute('aria-label')) {
|
|
540
|
+
matchElement.removeAttribute('aria-label');
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Initial synchronization
|
|
547
|
+
synchronizeContent();
|
|
548
|
+
|
|
549
|
+
// Set up MutationObserver to watch for changes
|
|
550
|
+
const observer = new MutationObserver((mutations) => {
|
|
551
|
+
let shouldSync = false;
|
|
552
|
+
|
|
553
|
+
mutations.forEach((mutation) => {
|
|
554
|
+
if (mutation.type === 'childList' ||
|
|
555
|
+
mutation.type === 'characterData' ||
|
|
556
|
+
(mutation.type === 'attributes' && mutation.attributeName === 'aria-label')) {
|
|
557
|
+
shouldSync = true;
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
if (shouldSync) {
|
|
562
|
+
synchronizeContent();
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
// Observe text changes and attribute changes
|
|
567
|
+
observer.observe(originalElement, {
|
|
568
|
+
childList: true,
|
|
569
|
+
subtree: true,
|
|
570
|
+
characterData: true,
|
|
571
|
+
attributes: true,
|
|
572
|
+
attributeFilter: ['aria-label']
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
435
577
|
function setupRichTextAccessibility() {
|
|
436
578
|
const contentAreas = document.querySelectorAll('[data-hs-a11y="rich-content"]');
|
|
437
579
|
const tocLists = document.querySelectorAll('[data-hs-a11y="rich-toc"]');
|
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 {
|
package/autoInit/navbar.js
CHANGED
|
@@ -27,16 +27,12 @@ function setupDynamicDropdowns() {
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
dropdownWrappers.forEach((wrapper) => {
|
|
30
|
-
const toggle = wrapper.querySelector("
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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";
|
package/autoInit/transition.js
CHANGED
|
@@ -16,12 +16,21 @@ export async function init() {
|
|
|
16
16
|
function initTransitions() {
|
|
17
17
|
const transitionTrigger = document.querySelector(".transition-trigger");
|
|
18
18
|
const transitionElement = document.querySelector(".transition");
|
|
19
|
-
|
|
20
|
-
let excludedClass = "no-transition";
|
|
21
19
|
|
|
22
20
|
// Page Load - Trigger entrance animation with optional delay
|
|
23
21
|
if (transitionTrigger) {
|
|
24
|
-
// Check if
|
|
22
|
+
// Check if entrance transition should be skipped
|
|
23
|
+
const skipEntrance = sessionStorage.getItem('skip-entrance-transition');
|
|
24
|
+
if (skipEntrance) {
|
|
25
|
+
sessionStorage.removeItem('skip-entrance-transition');
|
|
26
|
+
// Keep transition element hidden when skipping animation
|
|
27
|
+
if (transitionElement) {
|
|
28
|
+
transitionElement.style.display = "none";
|
|
29
|
+
}
|
|
30
|
+
return; // Skip entrance animation
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Check if this is the first page load of the session
|
|
25
34
|
const isFirstLoad = !sessionStorage.getItem('transition-loaded');
|
|
26
35
|
const delayAttr = transitionElement?.getAttribute('data-hs-delay');
|
|
27
36
|
const delaySeconds = delayAttr ? parseFloat(delayAttr) : 0;
|
|
@@ -58,6 +67,18 @@ function initTransitions() {
|
|
|
58
67
|
checkComplete();
|
|
59
68
|
}
|
|
60
69
|
|
|
70
|
+
// Helper function to check if element or any ancestor has data-hs-transition="prevent"
|
|
71
|
+
function hasTransitionPrevented(element) {
|
|
72
|
+
let current = element;
|
|
73
|
+
while (current && current !== document) {
|
|
74
|
+
if (current.getAttribute && current.getAttribute('data-hs-transition') === 'prevent') {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
current = current.parentElement;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
61
82
|
// On Link Click
|
|
62
83
|
document.addEventListener("click", function (e) {
|
|
63
84
|
const link = e.target.closest("a");
|
|
@@ -67,10 +88,19 @@ function initTransitions() {
|
|
|
67
88
|
link.hostname === window.location.hostname &&
|
|
68
89
|
link.getAttribute("href") &&
|
|
69
90
|
link.getAttribute("href").indexOf("#") === -1 &&
|
|
70
|
-
!link.classList.contains(excludedClass) &&
|
|
71
91
|
link.getAttribute("target") !== "_blank" &&
|
|
72
92
|
transitionTrigger
|
|
73
93
|
) {
|
|
94
|
+
// Check if transitions are prevented
|
|
95
|
+
const transitionPrevented = hasTransitionPrevented(link);
|
|
96
|
+
|
|
97
|
+
if (transitionPrevented) {
|
|
98
|
+
// Set flag to prevent entrance animation on next page
|
|
99
|
+
sessionStorage.setItem('skip-entrance-transition', 'true');
|
|
100
|
+
// Navigate normally without exit animation
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
74
104
|
e.preventDefault();
|
|
75
105
|
|
|
76
106
|
let transitionURL = link.getAttribute("href");
|
package/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Version:1.
|
|
1
|
+
// Version:1.7.0
|
|
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
package/utils/slider.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// Infinite carousel slider
|
|
2
|
+
const initInfiniteCarousel = () => {
|
|
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
|
+
if (!wrapper || !nextBtn || !prevBtn) return;
|
|
8
|
+
const slides = wrapper.children;
|
|
9
|
+
if (!slides?.length) return;
|
|
10
|
+
|
|
11
|
+
const state = { currentIndex: 1, totalSlides: slides.length, isAnimating: false };
|
|
12
|
+
|
|
13
|
+
wrapper.appendChild(slides[0].cloneNode(true));
|
|
14
|
+
wrapper.insertBefore(slides[slides.length - 1].cloneNode(true), slides[0]);
|
|
15
|
+
|
|
16
|
+
gsap.set(wrapper, { xPercent: -100 });
|
|
17
|
+
|
|
18
|
+
const navigate = (direction) => {
|
|
19
|
+
if (state.isAnimating) return;
|
|
20
|
+
state.isAnimating = true;
|
|
21
|
+
state.currentIndex += direction;
|
|
22
|
+
|
|
23
|
+
const isLoop = (direction > 0 && state.currentIndex > state.totalSlides) ||
|
|
24
|
+
(direction < 0 && state.currentIndex < 1);
|
|
25
|
+
|
|
26
|
+
gsap.to(wrapper, {
|
|
27
|
+
xPercent: -state.currentIndex * 100,
|
|
28
|
+
duration: 0.5,
|
|
29
|
+
ease: "power2.inOut",
|
|
30
|
+
onComplete: () => {
|
|
31
|
+
if (isLoop) {
|
|
32
|
+
state.currentIndex = direction > 0 ? 1 : state.totalSlides;
|
|
33
|
+
gsap.set(wrapper, { xPercent: -state.currentIndex * 100 });
|
|
34
|
+
}
|
|
35
|
+
state.isAnimating = false;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
nextBtn.addEventListener('click', () => navigate(1));
|
|
41
|
+
prevBtn.addEventListener('click', () => navigate(-1));
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Infinite pagination slider
|
|
45
|
+
const initInfinitePagination = () => {
|
|
46
|
+
document.querySelectorAll('[data-hs-slider="pagination-list"]')
|
|
47
|
+
.forEach(list => initPaginationInstance(list));
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const initPaginationInstance = (originalList) => {
|
|
51
|
+
const wrapper = originalList.parentElement;
|
|
52
|
+
if (!wrapper) return;
|
|
53
|
+
|
|
54
|
+
const container = wrapper.closest('[data-hs-slider*="pagination"]') || wrapper.parentElement;
|
|
55
|
+
const elements = {
|
|
56
|
+
nextBtn: container.querySelector('[data-hs-slider="pagination-next"]'),
|
|
57
|
+
prevBtn: container.querySelector('[data-hs-slider="pagination-previous"]'),
|
|
58
|
+
counter: container.querySelector('[data-hs-slider="pagination-counter"]'),
|
|
59
|
+
controls: container.querySelector('[data-hs-slider="pagination-controls"]')
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (!elements.nextBtn || !elements.prevBtn) return;
|
|
63
|
+
|
|
64
|
+
elements.nextBtn.setAttribute('aria-label', 'Go to next page');
|
|
65
|
+
elements.prevBtn.setAttribute('aria-label', 'Go to previous page');
|
|
66
|
+
if (elements.counter) {
|
|
67
|
+
elements.counter.setAttribute('aria-live', 'polite');
|
|
68
|
+
elements.counter.setAttribute('aria-label', 'Current page');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const config = originalList.getAttribute('data-hs-config') || '';
|
|
72
|
+
const desktopItems = parseInt(config.match(/show-(\d+)(?!-mobile)/)?.[1]) || 6;
|
|
73
|
+
const mobileItems = parseInt(config.match(/show-(\d+)-mobile/)?.[1]) || desktopItems;
|
|
74
|
+
|
|
75
|
+
const isMobileLayout = () => getComputedStyle(originalList).display === 'flex';
|
|
76
|
+
const allItems = Array.from(originalList.children);
|
|
77
|
+
const totalItems = allItems.length;
|
|
78
|
+
if (!totalItems) return;
|
|
79
|
+
|
|
80
|
+
const state = { totalPages: 1, currentIndex: 1, currentPage: 1, isAnimating: false, itemsPerPage: desktopItems };
|
|
81
|
+
let wrapperChildren = [];
|
|
82
|
+
|
|
83
|
+
const initializePagination = (forceItemsPerPage = null) => {
|
|
84
|
+
const currentIsMobile = isMobileLayout();
|
|
85
|
+
state.itemsPerPage = forceItemsPerPage || (currentIsMobile ? mobileItems : desktopItems);
|
|
86
|
+
state.totalPages = Math.ceil(totalItems / state.itemsPerPage);
|
|
87
|
+
|
|
88
|
+
Array.from(wrapper.children).forEach(child => {
|
|
89
|
+
if (child !== originalList) wrapper.removeChild(child);
|
|
90
|
+
});
|
|
91
|
+
originalList.innerHTML = '';
|
|
92
|
+
allItems.forEach(item => originalList.appendChild(item));
|
|
93
|
+
|
|
94
|
+
if (state.totalPages <= 1) {
|
|
95
|
+
if (elements.controls) {
|
|
96
|
+
if (elements.controls.contains(document.activeElement)) document.activeElement.blur();
|
|
97
|
+
elements.controls.style.display = 'none';
|
|
98
|
+
elements.controls.setAttribute('aria-hidden', 'true');
|
|
99
|
+
}
|
|
100
|
+
Object.assign(state, { totalPages: 1, currentIndex: 1, currentPage: 1, isAnimating: false });
|
|
101
|
+
wrapper.style.cssText = `transform: translateX(0%); height: ${originalList.offsetHeight}px;`;
|
|
102
|
+
wrapperChildren = [originalList];
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (elements.controls) {
|
|
107
|
+
elements.controls.style.display = '';
|
|
108
|
+
elements.controls.removeAttribute('aria-hidden');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const pageLists = Array.from({ length: state.totalPages }, (_, page) => {
|
|
112
|
+
const pageList = originalList.cloneNode(false);
|
|
113
|
+
const startIndex = page * state.itemsPerPage;
|
|
114
|
+
const endIndex = Math.min(startIndex + state.itemsPerPage, totalItems);
|
|
115
|
+
allItems.slice(startIndex, endIndex).forEach(item => pageList.appendChild(item.cloneNode(true)));
|
|
116
|
+
return pageList;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
wrapper.insertBefore(pageLists[pageLists.length - 1].cloneNode(true), originalList);
|
|
120
|
+
pageLists.slice(1).forEach(page => wrapper.appendChild(page));
|
|
121
|
+
wrapper.appendChild(pageLists[0].cloneNode(true));
|
|
122
|
+
|
|
123
|
+
originalList.innerHTML = '';
|
|
124
|
+
Array.from(pageLists[0].children).forEach(item => originalList.appendChild(item));
|
|
125
|
+
|
|
126
|
+
Object.assign(state, { currentIndex: 1, currentPage: 1, isAnimating: false });
|
|
127
|
+
wrapperChildren = Array.from(wrapper.children);
|
|
128
|
+
wrapper.style.transform = 'translateX(-100%)';
|
|
129
|
+
|
|
130
|
+
updateCounter();
|
|
131
|
+
updateHeight();
|
|
132
|
+
manageFocus();
|
|
133
|
+
return state.totalPages;
|
|
134
|
+
};
|
|
135
|
+
const liveRegion = document.createElement('div');
|
|
136
|
+
liveRegion.className = 'sr-only';
|
|
137
|
+
liveRegion.setAttribute('aria-live', 'assertive');
|
|
138
|
+
liveRegion.setAttribute('aria-atomic', 'true');
|
|
139
|
+
liveRegion.style.cssText = 'position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden;';
|
|
140
|
+
container.appendChild(liveRegion);
|
|
141
|
+
|
|
142
|
+
const updateCounter = () => elements.counter && (elements.counter.textContent = `${state.currentPage} / ${state.totalPages}`);
|
|
143
|
+
|
|
144
|
+
const announcePageChange = () => {
|
|
145
|
+
liveRegion.textContent = `Page ${state.currentPage} of ${state.totalPages}`;
|
|
146
|
+
setTimeout(() => liveRegion.textContent = '', 1000);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const manageFocus = () => wrapperChildren.forEach((page, index) => {
|
|
150
|
+
page[index === state.currentIndex ? 'removeAttribute' : 'setAttribute']('inert', '');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const updateHeight = () => {
|
|
154
|
+
const targetPage = wrapperChildren[state.currentIndex];
|
|
155
|
+
if (targetPage) wrapper.style.height = targetPage.offsetHeight + 'px';
|
|
156
|
+
};
|
|
157
|
+
let currentLayoutIsMobile = isMobileLayout();
|
|
158
|
+
|
|
159
|
+
const checkLayoutChange = () => {
|
|
160
|
+
const newIsMobile = isMobileLayout();
|
|
161
|
+
if (newIsMobile !== currentLayoutIsMobile) {
|
|
162
|
+
currentLayoutIsMobile = newIsMobile;
|
|
163
|
+
initializePagination();
|
|
164
|
+
} else {
|
|
165
|
+
updateHeight();
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const resizeObserver = new ResizeObserver(checkLayoutChange);
|
|
170
|
+
resizeObserver.observe(wrapper);
|
|
171
|
+
initializePagination();
|
|
172
|
+
|
|
173
|
+
const navigate = (direction) => {
|
|
174
|
+
if (state.isAnimating || state.totalPages <= 1) return;
|
|
175
|
+
state.isAnimating = true;
|
|
176
|
+
state.currentIndex += direction;
|
|
177
|
+
|
|
178
|
+
state.currentPage = state.currentIndex > state.totalPages ? 1 :
|
|
179
|
+
state.currentIndex < 1 ? state.totalPages : state.currentIndex;
|
|
180
|
+
|
|
181
|
+
updateCounter();
|
|
182
|
+
announcePageChange();
|
|
183
|
+
updateHeight();
|
|
184
|
+
|
|
185
|
+
wrapper.style.transition = 'transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
|
|
186
|
+
wrapper.style.transform = `translateX(${-state.currentIndex * 100}%)`;
|
|
187
|
+
|
|
188
|
+
const handleTransitionEnd = () => {
|
|
189
|
+
wrapper.removeEventListener('transitionend', handleTransitionEnd);
|
|
190
|
+
wrapper.style.transition = '';
|
|
191
|
+
|
|
192
|
+
if (state.currentIndex > state.totalPages) {
|
|
193
|
+
state.currentIndex = 1;
|
|
194
|
+
state.currentPage = 1;
|
|
195
|
+
wrapper.style.transform = 'translateX(-100%)';
|
|
196
|
+
} else if (state.currentIndex < 1) {
|
|
197
|
+
state.currentIndex = state.totalPages;
|
|
198
|
+
state.currentPage = state.totalPages;
|
|
199
|
+
wrapper.style.transform = `translateX(${-state.totalPages * 100}%)`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
updateCounter();
|
|
203
|
+
announcePageChange();
|
|
204
|
+
updateHeight();
|
|
205
|
+
manageFocus();
|
|
206
|
+
state.isAnimating = false;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
wrapper.addEventListener('transitionend', handleTransitionEnd);
|
|
210
|
+
};
|
|
211
|
+
elements.nextBtn.addEventListener('click', () => navigate(1));
|
|
212
|
+
elements.prevBtn.addEventListener('click', () => navigate(-1));
|
|
213
|
+
|
|
214
|
+
return () => resizeObserver.disconnect();
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export const init = () => {
|
|
218
|
+
initInfiniteCarousel();
|
|
219
|
+
initInfinitePagination();
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
export const version = "1.0.0";
|