@hortonstudio/main 1.9.10 → 1.9.20
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/.prettierrc +8 -0
- package/README.md +146 -0
- package/eslint.config.js +32 -0
- package/index.ts +275 -0
- package/package.json +19 -2
- package/public/bootstrap.js +16 -0
- package/src/animations/animations.ts +93 -0
- package/src/animations/functions/counter/counter.ts +137 -0
- package/src/config.json +570 -0
- package/src/config.ts +105 -0
- package/src/modules/default/README.md +167 -0
- package/src/modules/default/default.ts +71 -0
- package/{autoInit → src/modules/default/functions}/accessibility/README.md +44 -12
- package/src/modules/default/functions/accessibility/accessibility.ts +54 -0
- package/src/modules/default/functions/accordion/README.md +451 -0
- package/src/modules/default/functions/accordion/accordion.ts +189 -0
- package/src/modules/default/functions/comparison/comparison.ts +424 -0
- package/src/modules/default/functions/marquee/marquee.ts +206 -0
- package/src/modules/default/functions/navbar/README.md +393 -0
- package/src/modules/default/functions/navbar/functions/arrow-navigation/arrow-navigation.ts +183 -0
- package/src/modules/default/functions/navbar/functions/dropdown/dropdown.ts +313 -0
- package/src/modules/default/functions/navbar/functions/menu/menu.ts +315 -0
- package/src/modules/default/functions/navbar/navbar.ts +51 -0
- package/{autoInit → src/modules/default/functions}/smooth-scroll/README.md +45 -14
- package/{autoInit/smooth-scroll/smooth-scroll.js → src/modules/default/functions/smooth-scroll/smooth-scroll.ts} +33 -38
- package/{autoInit → src/modules/default/functions}/transition/README.md +59 -32
- package/src/modules/default/functions/transition/transition.ts +290 -0
- package/src/modules/normalize/README.md +172 -0
- package/src/modules/normalize/functions/clickable/README.md +84 -0
- package/src/modules/normalize/functions/clickable/clickable.ts +43 -0
- package/src/modules/normalize/functions/clickable/functions/normalize/README.md +213 -0
- package/src/modules/normalize/functions/clickable/functions/normalize/normalize.ts +68 -0
- package/src/modules/normalize/functions/dupe/README.md +405 -0
- package/src/modules/normalize/functions/dupe/dupe.ts +197 -0
- package/src/modules/normalize/functions/sync/sync.ts +378 -0
- package/src/modules/normalize/normalize.ts +58 -0
- package/src/modules/structure/README.md +190 -0
- package/src/modules/structure/functions/form/README.md +94 -0
- package/src/modules/structure/functions/form/form.ts +54 -0
- package/src/modules/structure/functions/form/functions/honeypot/README.md +77 -0
- package/src/modules/structure/functions/form/functions/honeypot/honeypot.ts +37 -0
- package/src/modules/structure/functions/form/functions/range/README.md +410 -0
- package/src/modules/structure/functions/form/functions/range/range.ts +92 -0
- package/src/modules/structure/functions/form/functions/select/README.md +393 -0
- package/src/modules/structure/functions/form/functions/select/functions/custom-select/custom-select.ts +637 -0
- package/src/modules/structure/functions/form/functions/select/functions/states/states.ts +118 -0
- package/src/modules/structure/functions/form/functions/select/select.ts +48 -0
- package/src/modules/structure/functions/form/functions/test/test.ts +132 -0
- package/src/modules/structure/functions/pagination/README.md +527 -0
- package/src/modules/structure/functions/pagination/pagination.ts +493 -0
- package/src/modules/structure/functions/site-settings/README.md +395 -0
- package/src/modules/structure/functions/site-settings/site-settings.ts +158 -0
- package/{autoInit/accessibility → src/modules/structure}/functions/toc/README.md +18 -15
- package/{autoInit/accessibility/functions/toc/toc.js → src/modules/structure/functions/toc/functions/heading-links/heading-links.ts} +43 -63
- package/src/modules/structure/functions/toc/functions/progress-bar/progress-bar.ts +101 -0
- package/src/modules/structure/functions/toc/toc.ts +35 -0
- package/{autoInit/accessibility → src/modules/structure}/functions/year-replacement/README.md +7 -6
- package/src/modules/structure/functions/year-replacement/year-replacement.ts +59 -0
- package/src/modules/structure/structure.ts +59 -0
- package/src/utils/attributeSelector.ts +78 -0
- package/src/utils/cssVariables.ts +24 -0
- package/src/utils/gsap.ts +198 -0
- package/src/utils/heightAnimator.ts +130 -0
- package/src/utils/modalManager.ts +150 -0
- package/src/utils.ts +54 -0
- package/tsconfig.json +24 -0
- package/vite.config.js +45 -0
- package/.claude/settings.local.json +0 -70
- package/archive/hero.js +0 -794
- package/archive/modal.js +0 -80
- package/archive/text.js +0 -628
- package/autoInit/accessibility/accessibility.js +0 -53
- package/autoInit/accessibility/functions/blog-remover/README.md +0 -61
- package/autoInit/accessibility/functions/blog-remover/blog-remover.js +0 -31
- package/autoInit/accessibility/functions/click-forwarding/README.md +0 -60
- package/autoInit/accessibility/functions/click-forwarding/click-forwarding.js +0 -82
- package/autoInit/accessibility/functions/dropdown/README.md +0 -212
- package/autoInit/accessibility/functions/dropdown/dropdown.js +0 -167
- package/autoInit/accessibility/functions/list-accessibility/README.md +0 -56
- package/autoInit/accessibility/functions/list-accessibility/list-accessibility.js +0 -23
- package/autoInit/accessibility/functions/pagination/README.md +0 -428
- package/autoInit/accessibility/functions/pagination/pagination.js +0 -359
- package/autoInit/accessibility/functions/text-synchronization/README.md +0 -62
- package/autoInit/accessibility/functions/text-synchronization/text-synchronization.js +0 -101
- package/autoInit/accessibility/functions/year-replacement/year-replacement.js +0 -43
- package/autoInit/button/README.md +0 -122
- package/autoInit/button/button.js +0 -51
- package/autoInit/counter/README.md +0 -274
- package/autoInit/counter/counter.js +0 -185
- package/autoInit/form/README.md +0 -338
- package/autoInit/form/form.js +0 -374
- package/autoInit/navbar/README.md +0 -366
- package/autoInit/navbar/navbar.js +0 -786
- package/autoInit/site-settings/README.md +0 -218
- package/autoInit/site-settings/site-settings.js +0 -134
- package/autoInit/transition/transition.js +0 -116
- package/index.js +0 -305
- package/utils/before-after/README.md +0 -520
- package/utils/before-after/before-after.js +0 -653
- package/utils/css-animations/buttons/main/bgbasic/btn-main-bgbasic.html +0 -10
- package/utils/css-animations/buttons/main/bgfill/btn-main-bgfill.html +0 -29
- package/utils/css-animations/buttons/navbar/bgbasic/navbar-main-bgbasic.html +0 -17
- package/utils/css-animations/buttons/navbar/bgbasic/navbar-menu-bgbasic.html +0 -16
- package/utils/css-animations/buttons/navbar/bgfill/navbar-main-bgfill.html +0 -46
- package/utils/css-animations/buttons/navbar/bgfill/navbar-menu-bgfill.html +0 -39
- package/utils/css-animations/buttons/navbar/color/navbar-announce-color.html +0 -5
- package/utils/css-animations/buttons/navbar/color/navbar-main-color.html +0 -7
- package/utils/css-animations/buttons/navbar/color/navbar-menu-color.html +0 -7
- package/utils/css-animations/buttons/navbar/double-slide/navbar-announce-double-slide.html +0 -40
- package/utils/css-animations/buttons/navbar/double-slide/navbar-main-double-slide.html +0 -77
- package/utils/css-animations/buttons/navbar/scale/navbar-announce-scale.html +0 -6
- package/utils/css-animations/buttons/navbar/scale/navbar-main-scale.html +0 -9
- package/utils/css-animations/buttons/navbar/scale/navbar-menu-scale.html +0 -8
- package/utils/css-animations/buttons/navbar/underline/navbar-announce-underline.html +0 -32
- package/utils/css-animations/buttons/navbar/underline/navbar-main-underline.html +0 -56
- package/utils/css-animations/buttons/text/color/text-footer-color.html +0 -5
- package/utils/css-animations/buttons/text/color/text-main-color.html +0 -5
- package/utils/css-animations/buttons/text/double-slide/text-main-double-slide.html +0 -56
- package/utils/css-animations/buttons/text/scale/text-footer-scale.html +0 -6
- package/utils/css-animations/buttons/text/scale/text-main-scale.html +0 -6
- package/utils/css-animations/buttons/text/underline/text-footer-underline.html +0 -45
- package/utils/css-animations/buttons/text/underline/text-main-underline.html +0 -58
- package/utils/css-animations/cards/card-clickable.html +0 -11
- package/utils/css-animations/defaults.html +0 -69
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comparison Module
|
|
3
|
+
*
|
|
4
|
+
* Before/after image comparison slider with GSAP Draggable.
|
|
5
|
+
* Uses template list to manage multiple slides with a single persistent slider.
|
|
6
|
+
*
|
|
7
|
+
* Structure:
|
|
8
|
+
* <!-- Template list (hidden, inside wrapper) -->
|
|
9
|
+
* <div data-hs-comparison="wrapper">
|
|
10
|
+
* <div data-hs-comparison-template="list" class="u-display-none">
|
|
11
|
+
* <div data-hs-comparison-template="item">
|
|
12
|
+
* <h3 data-hs-comparison-template="name">Project 1</h3>
|
|
13
|
+
* <div data-hs-comparison-template="description">Description 1</div>
|
|
14
|
+
* <img data-hs-comparison-template="before" src="before1.jpg" alt="Before">
|
|
15
|
+
* <img data-hs-comparison-template="after" src="after1.jpg" alt="After">
|
|
16
|
+
* </div>
|
|
17
|
+
* </div>
|
|
18
|
+
*
|
|
19
|
+
* <!-- Visible slider elements -->
|
|
20
|
+
* <div data-hs-comparison="image-wrapper">
|
|
21
|
+
* <div data-hs-comparison-image="before">
|
|
22
|
+
* <img src="placeholder.jpg">
|
|
23
|
+
* </div>
|
|
24
|
+
* <div data-hs-comparison-image="after">
|
|
25
|
+
* <img src="placeholder.jpg">
|
|
26
|
+
* </div>
|
|
27
|
+
* </div>
|
|
28
|
+
* <div data-hs-comparison="slider"></div>
|
|
29
|
+
* <span data-hs-comparison="name">Placeholder</span>
|
|
30
|
+
* <span data-hs-comparison="description">Placeholder</span>
|
|
31
|
+
* </div>
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
querySelector,
|
|
36
|
+
querySelectorAll,
|
|
37
|
+
getSelector,
|
|
38
|
+
globalConfig,
|
|
39
|
+
getGsap,
|
|
40
|
+
cssVariables,
|
|
41
|
+
} from '@utils';
|
|
42
|
+
|
|
43
|
+
export async function init(config: any) {
|
|
44
|
+
const gsapLib = getGsap('comparison');
|
|
45
|
+
if (!gsapLib) {
|
|
46
|
+
return { result: 'comparison skipped - GSAP not loaded' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const { gsap, Draggable } = gsapLib;
|
|
50
|
+
|
|
51
|
+
if (!Draggable) {
|
|
52
|
+
console.warn('[comparison] GSAP Draggable plugin not loaded, drag functionality disabled');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const wrappers = querySelectorAll(config, 'wrapper');
|
|
56
|
+
const cleanup = {
|
|
57
|
+
draggables: [] as any[],
|
|
58
|
+
handlers: [] as Array<{ element: Element; event: string; handler: EventListener }>,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Cache template selectors once (they never change)
|
|
62
|
+
const templateNameSelector = getSelector(config, 'template-name');
|
|
63
|
+
const templateDescSelector = getSelector(config, 'template-description');
|
|
64
|
+
const templateBeforeImageSelector = getSelector(config, 'template-before-image');
|
|
65
|
+
const templateAfterImageSelector = getSelector(config, 'template-after-image');
|
|
66
|
+
|
|
67
|
+
// Helper functions with access to config and gsap via closure
|
|
68
|
+
function updateClipPath(wrapper: HTMLElement, percentage: number) {
|
|
69
|
+
// Set CSS variable on wrapper, CSS handles the actual clip-path
|
|
70
|
+
wrapper.style.setProperty(cssVariables.clip, `${percentage}%`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function loadSlide(instance: any, index: number) {
|
|
74
|
+
const templateItem = instance.templateItems[index];
|
|
75
|
+
if (!templateItem) return;
|
|
76
|
+
|
|
77
|
+
// Update name
|
|
78
|
+
const templateName = templateItem.querySelector(templateNameSelector);
|
|
79
|
+
if (instance.nameElement && templateName) {
|
|
80
|
+
instance.nameElement.textContent = templateName.textContent;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Update description
|
|
84
|
+
const templateDesc = templateItem.querySelector(templateDescSelector);
|
|
85
|
+
if (instance.descElement && templateDesc) {
|
|
86
|
+
instance.descElement.textContent = templateDesc.textContent;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Update before image
|
|
90
|
+
const templateBeforeImage = templateItem.querySelector(
|
|
91
|
+
templateBeforeImageSelector
|
|
92
|
+
) as HTMLImageElement;
|
|
93
|
+
|
|
94
|
+
if (instance.beforeImage && templateBeforeImage) {
|
|
95
|
+
instance.beforeImage.src = templateBeforeImage.src;
|
|
96
|
+
instance.beforeImage.alt = templateBeforeImage.alt || '';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Update after image
|
|
100
|
+
const templateAfterImage = templateItem.querySelector(
|
|
101
|
+
templateAfterImageSelector
|
|
102
|
+
) as HTMLImageElement;
|
|
103
|
+
|
|
104
|
+
if (instance.afterImage && templateAfterImage) {
|
|
105
|
+
instance.afterImage.src = templateAfterImage.src;
|
|
106
|
+
instance.afterImage.alt = templateAfterImage.alt || '';
|
|
107
|
+
|
|
108
|
+
// Apply current mode to maintain slider state
|
|
109
|
+
applyMode(instance);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
instance.currentIndex = index;
|
|
113
|
+
|
|
114
|
+
// Update pagination
|
|
115
|
+
updatePagination(instance.wrapper, index);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function applyMode(instance: any) {
|
|
119
|
+
const slider = instance.slider;
|
|
120
|
+
const wrapper = instance.wrapper;
|
|
121
|
+
|
|
122
|
+
switch (instance.mode) {
|
|
123
|
+
case 'before':
|
|
124
|
+
// Set variable to 100% (hide after image completely)
|
|
125
|
+
wrapper.style.setProperty(cssVariables.clip, '100%');
|
|
126
|
+
slider.style.display = 'none';
|
|
127
|
+
break;
|
|
128
|
+
case 'after':
|
|
129
|
+
// Set variable to 0% (show after image completely)
|
|
130
|
+
wrapper.style.setProperty(cssVariables.clip, '0%');
|
|
131
|
+
slider.style.display = 'none';
|
|
132
|
+
break;
|
|
133
|
+
case 'split':
|
|
134
|
+
// Restore slider position without resetting
|
|
135
|
+
updateClipPath(wrapper, instance.sliderPosition);
|
|
136
|
+
slider.style.display = 'flex';
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function setMode(instance: any, mode: string) {
|
|
142
|
+
const wrapper = instance.wrapper;
|
|
143
|
+
const modeButtons = wrapper.querySelectorAll(`[${config.attributes.properties.modeType}]`);
|
|
144
|
+
const activeClass = globalConfig.classes.active;
|
|
145
|
+
|
|
146
|
+
// Update button states
|
|
147
|
+
modeButtons.forEach((btn: Element) => {
|
|
148
|
+
const btnMode = btn.getAttribute(config.attributes.properties.modeType);
|
|
149
|
+
if (btnMode === mode) {
|
|
150
|
+
btn.classList.add(activeClass);
|
|
151
|
+
} else {
|
|
152
|
+
btn.classList.remove(activeClass);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
instance.mode = mode;
|
|
157
|
+
applyMode(instance);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function updatePagination(wrapper: HTMLElement, index: number) {
|
|
161
|
+
const paginationContainer = querySelector(config, 'pagination', wrapper);
|
|
162
|
+
if (!paginationContainer) return;
|
|
163
|
+
|
|
164
|
+
const activeClass = globalConfig.classes.active;
|
|
165
|
+
const dots = Array.from(paginationContainer.children);
|
|
166
|
+
dots.forEach((dot, dotIndex) => {
|
|
167
|
+
if (dotIndex === index) {
|
|
168
|
+
dot.classList.add(activeClass);
|
|
169
|
+
} else {
|
|
170
|
+
dot.classList.remove(activeClass);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function navigateSlide(instance: any, direction: number) {
|
|
176
|
+
const newIndex = instance.currentIndex + direction;
|
|
177
|
+
const maxIndex = instance.templateItems.length - 1;
|
|
178
|
+
|
|
179
|
+
let targetIndex: number;
|
|
180
|
+
if (newIndex > maxIndex) {
|
|
181
|
+
targetIndex = 0;
|
|
182
|
+
} else if (newIndex < 0) {
|
|
183
|
+
targetIndex = maxIndex;
|
|
184
|
+
} else {
|
|
185
|
+
targetIndex = newIndex;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
loadSlide(instance, targetIndex);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function setupNavigation(instance: any, wrapper: HTMLElement) {
|
|
192
|
+
const leftArrow = wrapper.querySelector(`[${config.attributes.properties.navType}="previous"]`);
|
|
193
|
+
const rightArrow = wrapper.querySelector(`[${config.attributes.properties.navType}="next"]`);
|
|
194
|
+
|
|
195
|
+
if (leftArrow) {
|
|
196
|
+
const clickableSelector = getSelector(globalConfig.clickable, 'button');
|
|
197
|
+
const button = (leftArrow.querySelector(clickableSelector) || leftArrow) as HTMLElement;
|
|
198
|
+
button.setAttribute('aria-label', 'Previous image');
|
|
199
|
+
|
|
200
|
+
const handler = (e: Event) => {
|
|
201
|
+
e.preventDefault();
|
|
202
|
+
navigateSlide(instance, -1);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
button.addEventListener('click', handler);
|
|
206
|
+
cleanup.handlers.push({ element: button, event: 'click', handler });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (rightArrow) {
|
|
210
|
+
const clickableSelector = getSelector(globalConfig.clickable, 'button');
|
|
211
|
+
const button = (rightArrow.querySelector(clickableSelector) || rightArrow) as HTMLElement;
|
|
212
|
+
button.setAttribute('aria-label', 'Next image');
|
|
213
|
+
|
|
214
|
+
const handler = (e: Event) => {
|
|
215
|
+
e.preventDefault();
|
|
216
|
+
navigateSlide(instance, 1);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
button.addEventListener('click', handler);
|
|
220
|
+
cleanup.handlers.push({ element: button, event: 'click', handler });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function setupPagination(instance: any, paginationContainer: HTMLElement) {
|
|
225
|
+
const templateDot = paginationContainer.children[0];
|
|
226
|
+
if (!templateDot) return;
|
|
227
|
+
|
|
228
|
+
const activeClass = globalConfig.classes.active;
|
|
229
|
+
paginationContainer.innerHTML = '';
|
|
230
|
+
|
|
231
|
+
instance.templateItems.forEach((_: any, index: number) => {
|
|
232
|
+
const dot = templateDot.cloneNode(true) as HTMLElement;
|
|
233
|
+
|
|
234
|
+
// Remove active class from all dots, then add only to first
|
|
235
|
+
dot.classList.remove(activeClass);
|
|
236
|
+
if (index === 0) {
|
|
237
|
+
dot.classList.add(activeClass);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const clickHandler = () => {
|
|
241
|
+
loadSlide(instance, index);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
dot.addEventListener('click', clickHandler);
|
|
245
|
+
cleanup.handlers.push({ element: dot, event: 'click', handler: clickHandler });
|
|
246
|
+
|
|
247
|
+
paginationContainer.appendChild(dot);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function setupKeyboardNav(instance: any) {
|
|
252
|
+
const keydownHandler = (e: KeyboardEvent) => {
|
|
253
|
+
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
|
254
|
+
e.preventDefault();
|
|
255
|
+
navigateSlide(instance, -1);
|
|
256
|
+
} else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
|
257
|
+
e.preventDefault();
|
|
258
|
+
navigateSlide(instance, 1);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
instance.wrapper.addEventListener('keydown', keydownHandler);
|
|
263
|
+
instance.wrapper.setAttribute('tabindex', '0');
|
|
264
|
+
cleanup.handlers.push({ element: instance.wrapper, event: 'keydown', handler: keydownHandler });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Main initialization loop
|
|
268
|
+
wrappers.forEach((wrapper) => {
|
|
269
|
+
// Find template list inside wrapper
|
|
270
|
+
const templateList = querySelector(config, 'template-list', wrapper);
|
|
271
|
+
if (!templateList) {
|
|
272
|
+
console.warn('[comparison] No template list found in wrapper');
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const templateItemsSelector = getSelector(config, 'template-item');
|
|
277
|
+
const templateItems = Array.from(templateList.querySelectorAll(templateItemsSelector));
|
|
278
|
+
|
|
279
|
+
if (templateItems.length === 0) {
|
|
280
|
+
console.warn('[comparison] No template items found');
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Add simple aria-live for screen reader announcements
|
|
285
|
+
wrapper.setAttribute('aria-live', 'polite');
|
|
286
|
+
wrapper.setAttribute('aria-label', 'Before and after image comparison');
|
|
287
|
+
|
|
288
|
+
// Get references to visible elements and cache them
|
|
289
|
+
const imageWrapper = querySelector(config, 'image-wrapper', wrapper) as HTMLElement;
|
|
290
|
+
const slider = querySelector(config, 'slider', wrapper) as HTMLElement;
|
|
291
|
+
const beforeImageWrapper = querySelector(config, 'before-image', wrapper) as HTMLElement;
|
|
292
|
+
const afterImageWrapper = querySelector(config, 'after-image', wrapper) as HTMLElement;
|
|
293
|
+
const nameElement = querySelector(config, 'name', wrapper) as HTMLElement;
|
|
294
|
+
const descElement = querySelector(config, 'description', wrapper) as HTMLElement;
|
|
295
|
+
|
|
296
|
+
if (!imageWrapper || !slider || !afterImageWrapper) {
|
|
297
|
+
const missing = [];
|
|
298
|
+
if (!imageWrapper) missing.push('image-wrapper (data-hs-comparison="image-wrapper")');
|
|
299
|
+
if (!slider) missing.push('slider (data-hs-comparison="slider")');
|
|
300
|
+
if (!afterImageWrapper) missing.push('after-image (data-hs-comparison-image="after")');
|
|
301
|
+
console.warn(`[comparison] Missing required elements: ${missing.join(', ')}`);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const instance = {
|
|
306
|
+
wrapper: wrapper as HTMLElement,
|
|
307
|
+
templateItems,
|
|
308
|
+
currentIndex: 0,
|
|
309
|
+
mode: 'split',
|
|
310
|
+
sliderPosition: 50,
|
|
311
|
+
imageWrapper,
|
|
312
|
+
slider,
|
|
313
|
+
beforeImage: beforeImageWrapper?.querySelector('img') as HTMLImageElement,
|
|
314
|
+
afterImage: afterImageWrapper?.querySelector('img') as HTMLImageElement,
|
|
315
|
+
nameElement,
|
|
316
|
+
descElement,
|
|
317
|
+
draggable: null as any,
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// Load first slide
|
|
321
|
+
loadSlide(instance, 0);
|
|
322
|
+
|
|
323
|
+
// Set initial clip path to 50%
|
|
324
|
+
updateClipPath(wrapper as HTMLElement, 50);
|
|
325
|
+
|
|
326
|
+
// Setup GSAP Draggable for slider
|
|
327
|
+
if (Draggable) {
|
|
328
|
+
const draggableInstance = Draggable.create(slider, {
|
|
329
|
+
type: 'x',
|
|
330
|
+
bounds: imageWrapper,
|
|
331
|
+
onDrag: function () {
|
|
332
|
+
const rect = imageWrapper.getBoundingClientRect();
|
|
333
|
+
const sliderRect = slider.getBoundingClientRect();
|
|
334
|
+
const x = sliderRect.left - rect.left;
|
|
335
|
+
const percentage = Math.max(0, Math.min(100, (x / rect.width) * 100));
|
|
336
|
+
|
|
337
|
+
updateClipPath(wrapper as HTMLElement, percentage);
|
|
338
|
+
instance.sliderPosition = percentage;
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
cleanup.draggables.push(draggableInstance);
|
|
343
|
+
instance.draggable = draggableInstance[0]; // Store reference to draggable
|
|
344
|
+
|
|
345
|
+
// Click on image wrapper to jump slider
|
|
346
|
+
const clickHandler = (e: MouseEvent) => {
|
|
347
|
+
if (instance.mode !== 'split') return;
|
|
348
|
+
|
|
349
|
+
const rect = imageWrapper.getBoundingClientRect();
|
|
350
|
+
const clickX = e.clientX - rect.left;
|
|
351
|
+
const percentage = Math.max(0, Math.min(100, (clickX / rect.width) * 100));
|
|
352
|
+
|
|
353
|
+
// Calculate target position accounting for slider's center point
|
|
354
|
+
// Draggable starts from slider's initial position (50% = center)
|
|
355
|
+
const centerX = rect.width / 2;
|
|
356
|
+
const offsetX = clickX - centerX;
|
|
357
|
+
|
|
358
|
+
// Update Draggable's x position from center
|
|
359
|
+
if (instance.draggable) {
|
|
360
|
+
gsap.set(slider, { x: offsetX });
|
|
361
|
+
instance.draggable.update();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
updateClipPath(wrapper as HTMLElement, percentage);
|
|
365
|
+
instance.sliderPosition = percentage;
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
imageWrapper.addEventListener('click', clickHandler);
|
|
369
|
+
cleanup.handlers.push({ element: imageWrapper, event: 'click', handler: clickHandler });
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Setup mode buttons
|
|
373
|
+
const modeButtons = wrapper.querySelectorAll(`[${config.attributes.properties.modeType}]`);
|
|
374
|
+
modeButtons.forEach((modeWrapper) => {
|
|
375
|
+
const mode = modeWrapper.getAttribute(config.attributes.properties.modeType);
|
|
376
|
+
const clickableSelector = getSelector(globalConfig.clickable, 'button');
|
|
377
|
+
const button = (modeWrapper.querySelector(clickableSelector) || modeWrapper) as HTMLElement;
|
|
378
|
+
|
|
379
|
+
// Set initial state
|
|
380
|
+
if (mode === instance.mode) {
|
|
381
|
+
modeWrapper.classList.add(globalConfig.classes.active);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const clickHandler = (e: Event) => {
|
|
385
|
+
e.preventDefault();
|
|
386
|
+
setMode(instance, mode!);
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
button.addEventListener('click', clickHandler);
|
|
390
|
+
cleanup.handlers.push({ element: button, event: 'click', handler: clickHandler });
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// Setup navigation
|
|
394
|
+
setupNavigation(instance, wrapper as HTMLElement);
|
|
395
|
+
|
|
396
|
+
// Setup pagination
|
|
397
|
+
const paginationContainer = querySelector(config, 'pagination', wrapper);
|
|
398
|
+
if (paginationContainer && templateItems.length > 1) {
|
|
399
|
+
setupPagination(instance, paginationContainer as HTMLElement);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Setup keyboard navigation
|
|
403
|
+
setupKeyboardNav(instance);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
result: `comparison initialized (${wrappers.length} instances)`,
|
|
408
|
+
destroy: () => {
|
|
409
|
+
// Kill all draggables
|
|
410
|
+
cleanup.draggables.forEach((d) => {
|
|
411
|
+
if (d && d[0] && d[0].kill) d[0].kill();
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// Remove all event listeners
|
|
415
|
+
cleanup.handlers.forEach(({ element, event, handler }) => {
|
|
416
|
+
element.removeEventListener(event, handler);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// Clear arrays
|
|
420
|
+
cleanup.draggables.length = 0;
|
|
421
|
+
cleanup.handlers.length = 0;
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marquee Module
|
|
3
|
+
*
|
|
4
|
+
* GSAP-powered infinite scrolling marquee with scroll-based interactions.
|
|
5
|
+
* Works with normalize/dupe module for content duplication.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* <div data-hs-marquee="wrapper" data-hs-marquee-direction="left">
|
|
9
|
+
* <div data-hs-dupe="child" data-hs-dupe-count="2">
|
|
10
|
+
* <!-- Content here -->
|
|
11
|
+
* </div>
|
|
12
|
+
* </div>
|
|
13
|
+
*
|
|
14
|
+
* Attributes:
|
|
15
|
+
* data-hs-marquee-direction: "left" | "right" (required)
|
|
16
|
+
* data-hs-marquee-duration: Number in seconds (default: "25")
|
|
17
|
+
* data-hs-marquee-scroll: Scroll speed multiplier, 0-1 range (default: "0", disabled)
|
|
18
|
+
* data-hs-marquee-type: "reverse" (optional, enables scroll-direction reversal)
|
|
19
|
+
*
|
|
20
|
+
* Scroll Behavior:
|
|
21
|
+
* - Default: Speeds up when scrolling (magnitude only)
|
|
22
|
+
* - Type "reverse": Scroll down speeds up, scroll up reverses animation
|
|
23
|
+
* - Smooth velocity interpolation for buttery transitions
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
querySelectorAll,
|
|
28
|
+
globalConfig,
|
|
29
|
+
getGsap,
|
|
30
|
+
prefersReducedMotion,
|
|
31
|
+
initScrollVelocityTracking,
|
|
32
|
+
getScrollVelocity,
|
|
33
|
+
getScrollDirection,
|
|
34
|
+
} from '@utils';
|
|
35
|
+
|
|
36
|
+
export async function init(config) {
|
|
37
|
+
// Skip if user prefers reduced motion
|
|
38
|
+
if (prefersReducedMotion()) {
|
|
39
|
+
return { result: 'marquee skipped - prefers-reduced-motion enabled' };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const gsapLib = getGsap('marquee');
|
|
43
|
+
if (!gsapLib) {
|
|
44
|
+
return { result: 'marquee skipped - GSAP not loaded' };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const { gsap, ScrollTrigger } = gsapLib;
|
|
48
|
+
|
|
49
|
+
if (ScrollTrigger) {
|
|
50
|
+
gsap.registerPlugin(ScrollTrigger);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Initialize global scroll velocity tracking (only runs once)
|
|
54
|
+
initScrollVelocityTracking();
|
|
55
|
+
|
|
56
|
+
const marquees = querySelectorAll(config, 'wrapper');
|
|
57
|
+
const cleanup = {
|
|
58
|
+
timelines: [],
|
|
59
|
+
scrollTriggers: [],
|
|
60
|
+
tickers: [],
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
marquees.forEach((marquee) => {
|
|
64
|
+
const direction = marquee.getAttribute(config.attributes.properties.direction);
|
|
65
|
+
const children = Array.from(marquee.children);
|
|
66
|
+
|
|
67
|
+
if (children.length === 0) {
|
|
68
|
+
console.warn('[marquee] No children found in marquee wrapper:', marquee);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Get duration - check attribute first, then use config default
|
|
73
|
+
const durationAttr = marquee.getAttribute(config.attributes.properties.duration);
|
|
74
|
+
const duration = durationAttr ? parseFloat(durationAttr) : parseFloat(config.defaults.duration);
|
|
75
|
+
|
|
76
|
+
// Get scroll multiplier - check attribute first, then use config default
|
|
77
|
+
const scrollAttr = marquee.getAttribute(config.attributes.properties.scrollMultiplier);
|
|
78
|
+
const scrollMultiplier = scrollAttr
|
|
79
|
+
? parseFloat(scrollAttr)
|
|
80
|
+
: parseFloat(config.defaults.scrollMultiplier);
|
|
81
|
+
|
|
82
|
+
// Get marquee type (reverse or default)
|
|
83
|
+
const marqueeType = marquee.getAttribute(config.attributes.properties.type);
|
|
84
|
+
|
|
85
|
+
// Set attributes with defaults if not present (for visibility in DOM)
|
|
86
|
+
if (!durationAttr) {
|
|
87
|
+
marquee.setAttribute(config.attributes.properties.duration, config.defaults.duration);
|
|
88
|
+
}
|
|
89
|
+
if (!scrollAttr) {
|
|
90
|
+
marquee.setAttribute(
|
|
91
|
+
config.attributes.properties.scrollMultiplier,
|
|
92
|
+
config.defaults.scrollMultiplier
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Add class to indicate GSAP is controlling animation
|
|
97
|
+
marquee.classList.add(globalConfig.classes.gsap);
|
|
98
|
+
|
|
99
|
+
// Create GSAP timeline
|
|
100
|
+
const tl = gsap.timeline({ repeat: -1 });
|
|
101
|
+
|
|
102
|
+
// Animate all children together at the same time
|
|
103
|
+
children.forEach((child) => {
|
|
104
|
+
if (direction === 'left') {
|
|
105
|
+
// Left: 0% → -100%
|
|
106
|
+
tl.to(
|
|
107
|
+
child,
|
|
108
|
+
{
|
|
109
|
+
xPercent: -100,
|
|
110
|
+
duration: duration,
|
|
111
|
+
ease: 'none',
|
|
112
|
+
},
|
|
113
|
+
0
|
|
114
|
+
);
|
|
115
|
+
} else if (direction === 'right') {
|
|
116
|
+
// Right: -100% → 0%
|
|
117
|
+
gsap.set(child, { xPercent: -100 }); // Start from -100%
|
|
118
|
+
tl.to(
|
|
119
|
+
child,
|
|
120
|
+
{
|
|
121
|
+
xPercent: 0,
|
|
122
|
+
duration: duration,
|
|
123
|
+
ease: 'none',
|
|
124
|
+
},
|
|
125
|
+
0
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
cleanup.timelines.push(tl);
|
|
131
|
+
|
|
132
|
+
// Add scroll-based speed boost if scroll multiplier is set
|
|
133
|
+
if (scrollMultiplier > 0) {
|
|
134
|
+
let lastTimeScale = 1;
|
|
135
|
+
|
|
136
|
+
const tickerFunc = () => {
|
|
137
|
+
// Get global scroll velocity and direction
|
|
138
|
+
const scrollSpeed = getScrollVelocity();
|
|
139
|
+
const scrollDir = getScrollDirection();
|
|
140
|
+
let newTimeScale = 1;
|
|
141
|
+
|
|
142
|
+
if (marqueeType === 'reverse') {
|
|
143
|
+
// Reverse type: scroll down speeds up, scroll up reverses
|
|
144
|
+
if (scrollDir === 1) {
|
|
145
|
+
// Scrolling down: speed up in normal direction
|
|
146
|
+
newTimeScale = 1 + scrollSpeed * scrollMultiplier;
|
|
147
|
+
} else if (scrollDir === -1) {
|
|
148
|
+
// Scrolling up: reverse the animation
|
|
149
|
+
// Use absolute minimum to prevent getting stuck at 0
|
|
150
|
+
const reverseScale = scrollSpeed * scrollMultiplier;
|
|
151
|
+
newTimeScale = reverseScale > 0.5 ? -reverseScale : 1;
|
|
152
|
+
} else {
|
|
153
|
+
// Not scrolling: return to base speed
|
|
154
|
+
newTimeScale = 1;
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
// Default type: just speed up based on velocity (no direction)
|
|
158
|
+
const speedBoost = scrollSpeed * scrollMultiplier;
|
|
159
|
+
newTimeScale = 1 + speedBoost;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Only update if changed (avoid unnecessary updates)
|
|
163
|
+
if (Math.abs(newTimeScale - lastTimeScale) > 0.001) {
|
|
164
|
+
(tl as any).timeScale(newTimeScale);
|
|
165
|
+
lastTimeScale = newTimeScale;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Add ticker function to GSAP's ticker
|
|
170
|
+
(gsap as any).ticker.add(tickerFunc);
|
|
171
|
+
|
|
172
|
+
// Store ticker function and gsap reference for cleanup
|
|
173
|
+
cleanup.tickers.push({ tickerFunc, gsap });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
result: `marquee initialized (${marquees.length} instances)`,
|
|
179
|
+
destroy: () => {
|
|
180
|
+
// Kill all timelines
|
|
181
|
+
cleanup.timelines.forEach((tl) => {
|
|
182
|
+
if (tl) (tl as any).kill();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Kill all scroll triggers
|
|
186
|
+
cleanup.scrollTriggers.forEach((st) => {
|
|
187
|
+
if (st) (st as any).kill();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Remove all ticker functions
|
|
191
|
+
cleanup.tickers.forEach(({ tickerFunc, gsap: gsapInstance }) => {
|
|
192
|
+
(gsapInstance as any).ticker.remove(tickerFunc);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Remove GSAP class from all marquees
|
|
196
|
+
marquees.forEach((marquee) => {
|
|
197
|
+
marquee.classList.remove(globalConfig.classes.gsap);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Clear arrays
|
|
201
|
+
cleanup.timelines.length = 0;
|
|
202
|
+
cleanup.scrollTriggers.length = 0;
|
|
203
|
+
cleanup.tickers.length = 0;
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|