@hortonstudio/main 1.5.1 → 1.6.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/autoInit/accessibility.js +184 -13
- package/autoInit/navbar.js +0 -9
- package/autoInit/transition.js +118 -0
- package/index.js +13 -22
- package/package.json +1 -1
- package/TEMP-before-after-attributes.md +0 -158
- package/animations/transition.js +0 -82
- package/styles.css +0 -24
- package/test.json +0 -0
- package/utils/scroll-progress.js +0 -34
- package/utils/toc.js +0 -84
- /package/{animations → archive}/hero.js +0 -0
- /package/{autoInit → archive}/modal.js +0 -0
- /package/{animations → archive}/text.js +0 -0
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export function init() {
|
|
2
|
-
|
|
3
|
-
// Stats accessibility has been moved to counter.js
|
|
2
|
+
|
|
4
3
|
|
|
5
4
|
function setupGeneralAccessibility() {
|
|
6
5
|
setupListAccessibility();
|
|
@@ -9,6 +8,8 @@ export function init() {
|
|
|
9
8
|
setupConvertToSpan();
|
|
10
9
|
setupYearReplacement();
|
|
11
10
|
setupPreventDefault();
|
|
11
|
+
setupRichTextAccessibility();
|
|
12
|
+
setupSummaryAccessibility();
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
function setupListAccessibility() {
|
|
@@ -17,12 +18,10 @@ export function init() {
|
|
|
17
18
|
|
|
18
19
|
listElements.forEach(element => {
|
|
19
20
|
element.setAttribute('role', 'list');
|
|
20
|
-
element.removeAttribute('data-hs-a11y');
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
listItemElements.forEach(element => {
|
|
24
24
|
element.setAttribute('role', 'listitem');
|
|
25
|
-
element.removeAttribute('data-hs-a11y');
|
|
26
25
|
});
|
|
27
26
|
}
|
|
28
27
|
|
|
@@ -82,7 +81,6 @@ export function init() {
|
|
|
82
81
|
container.parentNode.replaceChild(newDiv, container);
|
|
83
82
|
} else {
|
|
84
83
|
// Just remove the attribute if container isn't a semantic list
|
|
85
|
-
container.removeAttribute('data-hs-a11y');
|
|
86
84
|
}
|
|
87
85
|
});
|
|
88
86
|
}
|
|
@@ -108,11 +106,6 @@ export function init() {
|
|
|
108
106
|
contentWrapper.setAttribute('role', 'region');
|
|
109
107
|
contentWrapper.setAttribute('aria-labelledby', buttonId);
|
|
110
108
|
|
|
111
|
-
if (contentWrapper.style.height !== '0px') {
|
|
112
|
-
button.setAttribute('aria-expanded', 'true');
|
|
113
|
-
contentWrapper.setAttribute('aria-hidden', 'false');
|
|
114
|
-
}
|
|
115
|
-
|
|
116
109
|
function toggleFAQ() {
|
|
117
110
|
const isOpen = button.getAttribute('aria-expanded') === 'true';
|
|
118
111
|
|
|
@@ -122,7 +115,6 @@ export function init() {
|
|
|
122
115
|
|
|
123
116
|
button.addEventListener('click', toggleFAQ);
|
|
124
117
|
|
|
125
|
-
container.removeAttribute('data-hs-a11y');
|
|
126
118
|
});
|
|
127
119
|
}
|
|
128
120
|
|
|
@@ -184,7 +176,6 @@ export function init() {
|
|
|
184
176
|
container.parentNode.replaceChild(newSpan, container);
|
|
185
177
|
} else {
|
|
186
178
|
// Just remove the attribute if container shouldn't be converted
|
|
187
|
-
container.removeAttribute('data-hs-a11y');
|
|
188
179
|
}
|
|
189
180
|
});
|
|
190
181
|
}
|
|
@@ -254,7 +245,187 @@ export function init() {
|
|
|
254
245
|
}
|
|
255
246
|
}
|
|
256
247
|
|
|
257
|
-
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function setupSummaryAccessibility() {
|
|
252
|
+
const summaryContainers = document.querySelectorAll('[data-hs-a11y="summary-wrap"]');
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
summaryContainers.forEach((container, index) => {
|
|
256
|
+
|
|
257
|
+
const button = container.querySelector('[data-hs-a11y="summary-btn"]');
|
|
258
|
+
const contentWrapper = container.querySelector('[data-hs-a11y="summary-content"]');
|
|
259
|
+
|
|
260
|
+
if (!button || !contentWrapper) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
const buttonId = `summary-button-${index}`;
|
|
266
|
+
const contentId = `summary-content-${index}`;
|
|
267
|
+
|
|
268
|
+
// Get original button text from first text node only
|
|
269
|
+
const walker = document.createTreeWalker(
|
|
270
|
+
button,
|
|
271
|
+
NodeFilter.SHOW_TEXT,
|
|
272
|
+
null,
|
|
273
|
+
false
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
let firstTextNode = walker.nextNode();
|
|
277
|
+
while (firstTextNode && !firstTextNode.textContent.trim()) {
|
|
278
|
+
firstTextNode = walker.nextNode();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const originalButtonText = firstTextNode ? firstTextNode.textContent.trim() : button.textContent.trim();
|
|
282
|
+
|
|
283
|
+
// Function to update all text nodes in button
|
|
284
|
+
function updateButtonText(newText) {
|
|
285
|
+
// Find all text nodes and update them
|
|
286
|
+
const walker = document.createTreeWalker(
|
|
287
|
+
button,
|
|
288
|
+
NodeFilter.SHOW_TEXT,
|
|
289
|
+
null,
|
|
290
|
+
false
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const textNodes = [];
|
|
294
|
+
let node;
|
|
295
|
+
while (node = walker.nextNode()) {
|
|
296
|
+
if (node.textContent.trim()) {
|
|
297
|
+
textNodes.push(node);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
textNodes.forEach(textNode => {
|
|
302
|
+
textNode.textContent = newText;
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
button.setAttribute('id', buttonId);
|
|
307
|
+
button.setAttribute('aria-expanded', 'false');
|
|
308
|
+
button.setAttribute('aria-controls', contentId);
|
|
309
|
+
button.setAttribute('aria-label', 'View Summary');
|
|
310
|
+
|
|
311
|
+
contentWrapper.setAttribute('id', contentId);
|
|
312
|
+
contentWrapper.setAttribute('aria-hidden', 'true');
|
|
313
|
+
contentWrapper.setAttribute('role', 'region');
|
|
314
|
+
contentWrapper.setAttribute('aria-labelledby', buttonId);
|
|
315
|
+
|
|
316
|
+
// Summary is closed by default - no need to check initial state
|
|
317
|
+
|
|
318
|
+
function toggleSummary() {
|
|
319
|
+
const isOpen = button.getAttribute('aria-expanded') === 'true';
|
|
320
|
+
|
|
321
|
+
if (isOpen) {
|
|
322
|
+
// Closing
|
|
323
|
+
button.setAttribute('aria-expanded', 'false');
|
|
324
|
+
button.setAttribute('aria-label', 'View Summary');
|
|
325
|
+
updateButtonText(originalButtonText);
|
|
326
|
+
contentWrapper.setAttribute('aria-hidden', 'true');
|
|
327
|
+
} else {
|
|
328
|
+
// Opening
|
|
329
|
+
button.setAttribute('aria-expanded', 'true');
|
|
330
|
+
button.setAttribute('aria-label', 'Close Summary');
|
|
331
|
+
updateButtonText('Close');
|
|
332
|
+
contentWrapper.setAttribute('aria-hidden', 'false');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
button.addEventListener('click', toggleSummary);
|
|
338
|
+
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function setupRichTextAccessibility() {
|
|
343
|
+
const contentAreas = document.querySelectorAll('[data-hs-a11y="rich-content"]');
|
|
344
|
+
const tocLists = document.querySelectorAll('[data-hs-a11y="rich-toc"]');
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
contentAreas.forEach((contentArea) => {
|
|
348
|
+
|
|
349
|
+
// Since there's only 1 content area and 1 TOC list per page, use the first TOC list
|
|
350
|
+
const tocList = tocLists[0];
|
|
351
|
+
|
|
352
|
+
if (!tocList) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (tocList.children.length === 0) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
const template = tocList.children[0];
|
|
362
|
+
tocList.innerHTML = "";
|
|
363
|
+
const h2Headings = contentArea.querySelectorAll("h2");
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
// Create sections and wrap content
|
|
367
|
+
h2Headings.forEach((heading) => {
|
|
368
|
+
const sectionId = heading.textContent
|
|
369
|
+
.toLowerCase()
|
|
370
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
371
|
+
.replace(/(^-|-$)/g, "");
|
|
372
|
+
|
|
373
|
+
const section = document.createElement("div");
|
|
374
|
+
section.id = sectionId;
|
|
375
|
+
heading.parentNode.insertBefore(section, heading);
|
|
376
|
+
section.appendChild(heading);
|
|
377
|
+
let nextElement = section.nextElementSibling;
|
|
378
|
+
while (nextElement && nextElement.tagName !== "H2") {
|
|
379
|
+
const elementToMove = nextElement;
|
|
380
|
+
nextElement = nextElement.nextElementSibling;
|
|
381
|
+
section.appendChild(elementToMove);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// Create TOC entries
|
|
386
|
+
h2Headings.forEach((heading, index) => {
|
|
387
|
+
const tocItem = template.cloneNode(true);
|
|
388
|
+
const link = tocItem.querySelector("a");
|
|
389
|
+
const sectionId = heading.parentElement.id;
|
|
390
|
+
link.href = "#" + sectionId;
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
// Bold numbered text
|
|
394
|
+
const number = document.createElement("strong");
|
|
395
|
+
number.textContent = index + 1 + ". ";
|
|
396
|
+
|
|
397
|
+
// Clear the link and add the number + text
|
|
398
|
+
link.innerHTML = "";
|
|
399
|
+
link.appendChild(number);
|
|
400
|
+
link.appendChild(document.createTextNode(heading.textContent));
|
|
401
|
+
|
|
402
|
+
// Add click handler for smooth scrolling
|
|
403
|
+
link.addEventListener("click", (e) => {
|
|
404
|
+
e.preventDefault();
|
|
405
|
+
|
|
406
|
+
const targetSection = document.getElementById(sectionId);
|
|
407
|
+
if (targetSection) {
|
|
408
|
+
targetSection.scrollIntoView({ behavior: "smooth" });
|
|
409
|
+
// Focus on the section for accessibility (will only show outline for keyboard users due to CSS)
|
|
410
|
+
setTimeout(() => {
|
|
411
|
+
targetSection.focus();
|
|
412
|
+
}, 100);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// Ensure sections are focusable for keyboard users but use CSS to control focus visibility
|
|
417
|
+
const targetSection = document.getElementById(sectionId);
|
|
418
|
+
if (targetSection) {
|
|
419
|
+
targetSection.setAttribute("tabindex", "-1");
|
|
420
|
+
// Use focus-visible to only show outline for keyboard focus
|
|
421
|
+
targetSection.style.outline = "none";
|
|
422
|
+
targetSection.style.setProperty("outline", "none", "important");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Add item to the TOC list
|
|
426
|
+
tocList.appendChild(tocItem);
|
|
427
|
+
});
|
|
428
|
+
|
|
258
429
|
});
|
|
259
430
|
}
|
|
260
431
|
|
package/autoInit/navbar.js
CHANGED
|
@@ -252,15 +252,6 @@ function setupDynamicDropdowns() {
|
|
|
252
252
|
}
|
|
253
253
|
});
|
|
254
254
|
|
|
255
|
-
toggle.addEventListener("click", function(e) {
|
|
256
|
-
if (e.isTrusted) {
|
|
257
|
-
// This is a real user click - prevent it
|
|
258
|
-
e.preventDefault();
|
|
259
|
-
e.stopPropagation();
|
|
260
|
-
return false;
|
|
261
|
-
}
|
|
262
|
-
// Programmatic clicks (from hover/keyboard) proceed normally
|
|
263
|
-
});
|
|
264
255
|
|
|
265
256
|
document.addEventListener("click", function (e) {
|
|
266
257
|
if (!wrapper.contains(e.target) && isOpen) {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Page Transition Module
|
|
2
|
+
const API_NAME = "hsmain";
|
|
3
|
+
let initialized = false;
|
|
4
|
+
|
|
5
|
+
export async function init() {
|
|
6
|
+
console.log('[Transition] Module init called');
|
|
7
|
+
|
|
8
|
+
// Wait for Webflow to be ready before initializing transitions
|
|
9
|
+
window[API_NAME].afterWebflowReady(() => {
|
|
10
|
+
console.log('[Transition] afterWebflowReady callback fired');
|
|
11
|
+
setTimeout(() => {
|
|
12
|
+
console.log('[Transition] Attempting to initialize transitions');
|
|
13
|
+
initTransitions();
|
|
14
|
+
initialized = true;
|
|
15
|
+
}, 50);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return { result: "anim-transition initialized" };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function initTransitions() {
|
|
22
|
+
console.log('[Transition] initTransitions called');
|
|
23
|
+
const transitionTrigger = document.querySelector(".transition-trigger");
|
|
24
|
+
const transitionElement = document.querySelector(".transition");
|
|
25
|
+
console.log('[Transition] transitionTrigger found:', transitionTrigger);
|
|
26
|
+
console.log('[Transition] transitionElement found:', transitionElement);
|
|
27
|
+
|
|
28
|
+
let excludedClass = "no-transition";
|
|
29
|
+
|
|
30
|
+
// Page Load - Trigger entrance animation with optional delay
|
|
31
|
+
if (transitionTrigger) {
|
|
32
|
+
// Check if this is the first page load of the session
|
|
33
|
+
const isFirstLoad = !sessionStorage.getItem('transition-loaded');
|
|
34
|
+
const delayAttr = transitionElement?.getAttribute('data-hs-delay');
|
|
35
|
+
const delaySeconds = delayAttr ? parseFloat(delayAttr) : 0;
|
|
36
|
+
|
|
37
|
+
console.log('[Transition] First load:', isFirstLoad);
|
|
38
|
+
console.log('[Transition] Delay attribute:', delayAttr);
|
|
39
|
+
console.log('[Transition] Delay seconds:', delaySeconds);
|
|
40
|
+
|
|
41
|
+
const triggerAnimation = () => {
|
|
42
|
+
console.log('[Transition] Triggering page load transition');
|
|
43
|
+
Webflow.push(function () {
|
|
44
|
+
transitionTrigger.click();
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
if (isFirstLoad && delaySeconds > 0) {
|
|
49
|
+
console.log(`[Transition] Delaying first load by ${delaySeconds} seconds`);
|
|
50
|
+
setTimeout(triggerAnimation, delaySeconds * 1000);
|
|
51
|
+
sessionStorage.setItem('transition-loaded', 'true');
|
|
52
|
+
} else {
|
|
53
|
+
triggerAnimation();
|
|
54
|
+
if (isFirstLoad) {
|
|
55
|
+
sessionStorage.setItem('transition-loaded', 'true');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Monitor for transition completion
|
|
61
|
+
function waitForTransitionComplete(callback) {
|
|
62
|
+
if (!transitionElement) return;
|
|
63
|
+
|
|
64
|
+
const checkComplete = () => {
|
|
65
|
+
if (transitionElement.classList.contains('transition-done')) {
|
|
66
|
+
console.log('[Transition] Animation complete detected');
|
|
67
|
+
callback();
|
|
68
|
+
} else {
|
|
69
|
+
setTimeout(checkComplete, 50);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
checkComplete();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// On Link Click
|
|
76
|
+
console.log('[Transition] Setting up click handler');
|
|
77
|
+
document.addEventListener("click", function (e) {
|
|
78
|
+
const link = e.target.closest("a");
|
|
79
|
+
|
|
80
|
+
if (
|
|
81
|
+
link &&
|
|
82
|
+
link.hostname === window.location.hostname &&
|
|
83
|
+
link.getAttribute("href") &&
|
|
84
|
+
link.getAttribute("href").indexOf("#") === -1 &&
|
|
85
|
+
!link.classList.contains(excludedClass) &&
|
|
86
|
+
link.getAttribute("target") !== "_blank" &&
|
|
87
|
+
transitionTrigger
|
|
88
|
+
) {
|
|
89
|
+
console.log('[Transition] Triggering exit transition to:', link.getAttribute("href"));
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
|
|
92
|
+
let transitionURL = link.getAttribute("href");
|
|
93
|
+
|
|
94
|
+
// Trigger exit animation
|
|
95
|
+
transitionTrigger.click();
|
|
96
|
+
|
|
97
|
+
// Wait for animation to complete before navigating
|
|
98
|
+
waitForTransitionComplete(() => {
|
|
99
|
+
console.log('[Transition] Navigating to:', transitionURL);
|
|
100
|
+
window.location = transitionURL;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// On Back Button Tap
|
|
106
|
+
window.onpageshow = function (event) {
|
|
107
|
+
if (event.persisted) {
|
|
108
|
+
window.location.reload();
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Hide Transition on Window Width Resize
|
|
113
|
+
window.addEventListener("resize", function () {
|
|
114
|
+
if (transitionElement) {
|
|
115
|
+
transitionElement.style.display = "none";
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
package/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Version:1.
|
|
1
|
+
// Version:1.6.0
|
|
2
2
|
|
|
3
3
|
const API_NAME = "hsmain";
|
|
4
4
|
|
|
@@ -13,25 +13,19 @@ const initializeHsMain = async () => {
|
|
|
13
13
|
|
|
14
14
|
const queuedModules = Array.isArray(window[API_NAME]) ? window[API_NAME] : [];
|
|
15
15
|
|
|
16
|
-
const animationModules = {
|
|
17
|
-
"data-hs-anim-text": true,
|
|
18
|
-
"data-hs-anim-hero": true,
|
|
19
|
-
"data-hs-anim-transition": true,
|
|
20
|
-
};
|
|
16
|
+
const animationModules = {};
|
|
21
17
|
|
|
22
18
|
const utilityModules = {
|
|
23
|
-
"data-hs-util-toc": true,
|
|
24
|
-
"data-hs-util-progress": true,
|
|
25
19
|
"data-hs-util-ba": true,
|
|
26
20
|
};
|
|
27
21
|
|
|
28
22
|
const autoInitModules = {
|
|
29
23
|
"smooth-scroll": true,
|
|
30
|
-
modal: true,
|
|
31
24
|
navbar: true,
|
|
32
25
|
accessibility: true,
|
|
33
26
|
counter: true,
|
|
34
27
|
form: true,
|
|
28
|
+
transition: true,
|
|
35
29
|
};
|
|
36
30
|
|
|
37
31
|
const allDataAttributes = { ...animationModules, ...utilityModules };
|
|
@@ -43,14 +37,9 @@ const initializeHsMain = async () => {
|
|
|
43
37
|
});
|
|
44
38
|
|
|
45
39
|
const moduleMap = {
|
|
46
|
-
|
|
47
|
-
"data-hs-anim-hero": () => import("./animations/hero.js"),
|
|
48
|
-
"data-hs-anim-transition": () => import("./animations/transition.js"),
|
|
49
|
-
"data-hs-util-toc": () => import("./utils/toc.js"),
|
|
50
|
-
"data-hs-util-progress": () => import("./utils/scroll-progress.js"),
|
|
40
|
+
transition: () => import("./autoInit/transition.js"),
|
|
51
41
|
"data-hs-util-ba": () => import("./utils/before-after.js"),
|
|
52
42
|
"smooth-scroll": () => import("./autoInit/smooth-scroll.js"),
|
|
53
|
-
modal: () => import("./autoInit/modal.js"),
|
|
54
43
|
navbar: () => import("./autoInit/navbar.js"),
|
|
55
44
|
accessibility: () => import("./autoInit/accessibility.js"),
|
|
56
45
|
counter: () => import("./autoInit/counter.js"),
|
|
@@ -255,13 +244,7 @@ const initializeHsMain = async () => {
|
|
|
255
244
|
loadModule(moduleName);
|
|
256
245
|
}
|
|
257
246
|
|
|
258
|
-
|
|
259
|
-
!scripts.some((script) => script.hasAttribute("data-hs-anim-transition"))
|
|
260
|
-
) {
|
|
261
|
-
document.querySelectorAll(".transition").forEach((element) => {
|
|
262
|
-
element.style.display = "none";
|
|
263
|
-
});
|
|
264
|
-
}
|
|
247
|
+
// Transition is now auto-loaded, so no need to hide transition elements
|
|
265
248
|
};
|
|
266
249
|
|
|
267
250
|
document.querySelectorAll(".w-richtext").forEach((richtext) => {
|
|
@@ -274,6 +257,14 @@ const initializeHsMain = async () => {
|
|
|
274
257
|
processModules();
|
|
275
258
|
await waitForWebflow();
|
|
276
259
|
|
|
260
|
+
// Small delay to ensure all scripts are fully initialized
|
|
261
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
262
|
+
|
|
263
|
+
// Force Webflow to reinitialize (not redraw) - rescans DOM and rebinds interactions
|
|
264
|
+
if (window.Webflow && typeof window.Webflow.ready === 'function') {
|
|
265
|
+
window.Webflow.ready();
|
|
266
|
+
}
|
|
267
|
+
|
|
277
268
|
window[API_NAME].loaded = true;
|
|
278
269
|
readyCallbacks.forEach((callback) => {
|
|
279
270
|
try {
|
package/package.json
CHANGED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
# Before-After Component Attributes Guide
|
|
2
|
-
|
|
3
|
-
This is a temporary reference for the `data-hs-ba` attributes used in the before-after utility.
|
|
4
|
-
|
|
5
|
-
## Required Structure
|
|
6
|
-
|
|
7
|
-
### Main Container
|
|
8
|
-
```html
|
|
9
|
-
<div data-hs-ba="wrapper">
|
|
10
|
-
<!-- All before-after content goes here -->
|
|
11
|
-
</div>
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
### Individual Slide/Item
|
|
15
|
-
Each comparison should be a direct child of the wrapper:
|
|
16
|
-
```html
|
|
17
|
-
<div data-hs-ba="wrapper">
|
|
18
|
-
<div class="slide-1">
|
|
19
|
-
<!-- First comparison -->
|
|
20
|
-
</div>
|
|
21
|
-
<div class="slide-2">
|
|
22
|
-
<!-- Second comparison -->
|
|
23
|
-
</div>
|
|
24
|
-
</div>
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## Image Elements
|
|
28
|
-
|
|
29
|
-
### Image Wrapper Container
|
|
30
|
-
```html
|
|
31
|
-
<div data-hs-ba="image-wrapper">
|
|
32
|
-
<!-- Before and after images go inside here -->
|
|
33
|
-
</div>
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
### Before Image
|
|
37
|
-
```html
|
|
38
|
-
<img src="before.jpg" data-hs-ba="image-before" alt="Before image">
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
### After Image
|
|
42
|
-
```html
|
|
43
|
-
<img src="after.jpg" data-hs-ba="image-after" alt="After image">
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
## Interactive Controls
|
|
47
|
-
|
|
48
|
-
### Mode Buttons
|
|
49
|
-
```html
|
|
50
|
-
<!-- Show only before image -->
|
|
51
|
-
<button data-hs-ba="mode-before">Before</button>
|
|
52
|
-
|
|
53
|
-
<!-- Show split view with slider -->
|
|
54
|
-
<button data-hs-ba="mode-split">Split</button>
|
|
55
|
-
|
|
56
|
-
<!-- Show only after image -->
|
|
57
|
-
<button data-hs-ba="mode-after">After</button>
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
### Navigation Arrows
|
|
61
|
-
```html
|
|
62
|
-
<!-- Previous slide -->
|
|
63
|
-
<button data-hs-ba="left">←</button>
|
|
64
|
-
|
|
65
|
-
<!-- Next slide -->
|
|
66
|
-
<button data-hs-ba="right">→</button>
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
### Slider Handle (for split mode)
|
|
70
|
-
```html
|
|
71
|
-
<div data-hs-ba="slider"></div>
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## Pagination
|
|
75
|
-
|
|
76
|
-
### Pagination Container with Template
|
|
77
|
-
```html
|
|
78
|
-
<div data-hs-ba="pagination">
|
|
79
|
-
<!-- Single template dot - system will clone this for each slide -->
|
|
80
|
-
<div class="ba_1_pagination_dot"></div>
|
|
81
|
-
</div>
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
**How it works**:
|
|
85
|
-
- Place ONE template dot inside the pagination container
|
|
86
|
-
- The system will automatically clone this template for each slide
|
|
87
|
-
- Each generated dot gets proper accessibility attributes
|
|
88
|
-
- The `is-active` class is automatically managed
|
|
89
|
-
|
|
90
|
-
**Generated Structure** (example with 3 slides):
|
|
91
|
-
```html
|
|
92
|
-
<div data-hs-ba="pagination">
|
|
93
|
-
<div class="ba_1_pagination_dot is-active" role="button" tabindex="0" aria-label="Go to slide 1" aria-current="true"></div>
|
|
94
|
-
<div class="ba_1_pagination_dot" role="button" tabindex="0" aria-label="Go to slide 2" aria-current="false"></div>
|
|
95
|
-
<div class="ba_1_pagination_dot" role="button" tabindex="0" aria-label="Go to slide 3" aria-current="false"></div>
|
|
96
|
-
</div>
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
## Complete Example
|
|
100
|
-
|
|
101
|
-
```html
|
|
102
|
-
<div data-hs-ba="wrapper">
|
|
103
|
-
<!-- Slide 1 -->
|
|
104
|
-
<div class="ba-slide">
|
|
105
|
-
<div data-hs-ba="image-wrapper" class="ba-image_wrap">
|
|
106
|
-
<img src="before1.jpg" data-hs-ba="image-before" alt="Before">
|
|
107
|
-
<img src="after1.jpg" data-hs-ba="image-after" alt="After">
|
|
108
|
-
<div data-hs-ba="slider"></div>
|
|
109
|
-
</div>
|
|
110
|
-
|
|
111
|
-
<!-- Mode controls -->
|
|
112
|
-
<div class="controls">
|
|
113
|
-
<button data-hs-ba="mode-before">Before</button>
|
|
114
|
-
<button data-hs-ba="mode-split" class="is-active">Split</button>
|
|
115
|
-
<button data-hs-ba="mode-after">After</button>
|
|
116
|
-
</div>
|
|
117
|
-
|
|
118
|
-
<!-- Navigation -->
|
|
119
|
-
<button data-hs-ba="left">Previous</button>
|
|
120
|
-
<button data-hs-ba="right">Next</button>
|
|
121
|
-
|
|
122
|
-
<!-- Pagination -->
|
|
123
|
-
<div data-hs-ba="pagination">
|
|
124
|
-
<div class="ba_1_pagination_dot"></div> <!-- Template dot -->
|
|
125
|
-
</div>
|
|
126
|
-
</div>
|
|
127
|
-
|
|
128
|
-
<!-- Slide 2 -->
|
|
129
|
-
<div class="ba-slide">
|
|
130
|
-
<!-- Similar structure... -->
|
|
131
|
-
</div>
|
|
132
|
-
|
|
133
|
-
<!-- Slide 3 -->
|
|
134
|
-
<div class="ba-slide">
|
|
135
|
-
<!-- Similar structure... -->
|
|
136
|
-
</div>
|
|
137
|
-
</div>
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
## Default Behavior
|
|
141
|
-
|
|
142
|
-
- **Default mode**: Split view at 50% position
|
|
143
|
-
- **Active states**: Elements with `is-active` class are highlighted
|
|
144
|
-
- **Keyboard support**: Arrow keys navigate slides or move slider
|
|
145
|
-
- **Touch support**: Dragging works on the slider handle
|
|
146
|
-
- **Accessibility**: Full ARIA support with proper labels
|
|
147
|
-
|
|
148
|
-
## Notes
|
|
149
|
-
|
|
150
|
-
- Each slide can have its own controls (mode buttons, navigation, pagination)
|
|
151
|
-
- The wrapper image container should have `data-hs-ba="image-wrapper"` for proper click handling
|
|
152
|
-
- Pagination dots are clicked to jump to specific slides
|
|
153
|
-
- Mode buttons toggle between before, split, and after views
|
|
154
|
-
- The slider only appears in split mode
|
|
155
|
-
|
|
156
|
-
---
|
|
157
|
-
|
|
158
|
-
*This is a temporary file for development reference. Delete when no longer needed.*
|
package/animations/transition.js
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
// Page Transition Module
|
|
2
|
-
const API_NAME = "hsmain";
|
|
3
|
-
export async function init() {
|
|
4
|
-
// Register the transition logic to run after library is ready
|
|
5
|
-
window[API_NAME].afterReady(() => {
|
|
6
|
-
// Only run if jQuery is available
|
|
7
|
-
if (typeof $ !== "undefined") {
|
|
8
|
-
initTransitions();
|
|
9
|
-
}
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
return { result: "anim-transition initialized" };
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function initTransitions() {
|
|
16
|
-
let transitionTrigger = $(".transition-trigger");
|
|
17
|
-
let introDurationMS = 800;
|
|
18
|
-
let exitDurationMS = 400;
|
|
19
|
-
let excludedClass = "no-transition";
|
|
20
|
-
|
|
21
|
-
// On Page Load
|
|
22
|
-
if (transitionTrigger.length > 0) {
|
|
23
|
-
function triggerTransition() {
|
|
24
|
-
if (window.Webflow && window.Webflow.push) {
|
|
25
|
-
Webflow.push(function () {
|
|
26
|
-
transitionTrigger.click();
|
|
27
|
-
});
|
|
28
|
-
} else {
|
|
29
|
-
// Non-Webflow initialization
|
|
30
|
-
setTimeout(() => {
|
|
31
|
-
transitionTrigger.click();
|
|
32
|
-
}, 100);
|
|
33
|
-
}
|
|
34
|
-
$("body").addClass("no-scroll-transition");
|
|
35
|
-
setTimeout(() => {
|
|
36
|
-
$("body").removeClass("no-scroll-transition");
|
|
37
|
-
}, introDurationMS);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Wait for full page load (images, fonts, etc.) for Safari
|
|
41
|
-
if (document.readyState === "complete") {
|
|
42
|
-
triggerTransition();
|
|
43
|
-
} else {
|
|
44
|
-
window.addEventListener("load", triggerTransition, { once: true });
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// On Link Click
|
|
49
|
-
$("a").on("click", function (e) {
|
|
50
|
-
if (
|
|
51
|
-
$(this).prop("hostname") == window.location.host &&
|
|
52
|
-
$(this).attr("href").indexOf("#") === -1 &&
|
|
53
|
-
!$(this).hasClass(excludedClass) &&
|
|
54
|
-
$(this).attr("target") !== "_blank" &&
|
|
55
|
-
transitionTrigger.length > 0
|
|
56
|
-
) {
|
|
57
|
-
e.preventDefault();
|
|
58
|
-
$("body").addClass("no-scroll-transition");
|
|
59
|
-
let transitionURL = $(this).attr("href");
|
|
60
|
-
transitionTrigger.click();
|
|
61
|
-
setTimeout(function () {
|
|
62
|
-
window.location = transitionURL;
|
|
63
|
-
}, exitDurationMS);
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// On Back Button Tap
|
|
68
|
-
window.onpageshow = function (event) {
|
|
69
|
-
if (event.persisted) {
|
|
70
|
-
window.location.reload();
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
// Hide Transition on Window Width Resize
|
|
75
|
-
setTimeout(() => {
|
|
76
|
-
$(window).on("resize", function () {
|
|
77
|
-
setTimeout(() => {
|
|
78
|
-
$(".transition").css("display", "none");
|
|
79
|
-
}, 50);
|
|
80
|
-
});
|
|
81
|
-
}, introDurationMS);
|
|
82
|
-
}
|
package/styles.css
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/* transition */
|
|
2
|
-
body .transition {display: block}
|
|
3
|
-
.w-editor .transition {display: none;}
|
|
4
|
-
.no-scroll-transition {overflow: hidden; position: relative;}
|
|
5
|
-
|
|
6
|
-
/* splittext */
|
|
7
|
-
.line-mask, .word-mask, .char-mask {
|
|
8
|
-
padding-bottom: .1em;
|
|
9
|
-
margin-bottom: -.1em;
|
|
10
|
-
padding-inline: .1em;
|
|
11
|
-
margin-inline: -.1em;
|
|
12
|
-
will-change: transform, opacity;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
/* TOC focus handling - only show focus outline for keyboard navigation */
|
|
17
|
-
[data-hs-toc] div[id]:focus:not(:focus-visible) {
|
|
18
|
-
outline: none !important;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
[data-hs-toc] div[id]:focus-visible {
|
|
22
|
-
outline: 2px solid #007bff !important;
|
|
23
|
-
outline-offset: 2px;
|
|
24
|
-
}
|
package/test.json
DELETED
|
File without changes
|
package/utils/scroll-progress.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
export async function init() {
|
|
2
|
-
const progressBar = document.querySelector('[data-hs-progress="bar"]');
|
|
3
|
-
const progressContent = document.querySelector(
|
|
4
|
-
'[data-hs-progress="wrapper"]',
|
|
5
|
-
);
|
|
6
|
-
|
|
7
|
-
// Check if elements exist before using them
|
|
8
|
-
if (!progressBar || !progressContent) {
|
|
9
|
-
return {
|
|
10
|
-
result: "util-scroll-progress initialized",
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// Check if gsap exists before using it
|
|
15
|
-
if (typeof gsap !== "undefined") {
|
|
16
|
-
gsap.set(progressBar, { width: "0%" });
|
|
17
|
-
|
|
18
|
-
// Create the scroll progress animation
|
|
19
|
-
gsap.to(progressBar, {
|
|
20
|
-
width: "100%",
|
|
21
|
-
ease: "none",
|
|
22
|
-
scrollTrigger: {
|
|
23
|
-
trigger: progressContent,
|
|
24
|
-
start: "top bottom",
|
|
25
|
-
end: "bottom bottom",
|
|
26
|
-
scrub: true,
|
|
27
|
-
},
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return {
|
|
32
|
-
result: "util-scroll-progress initialized",
|
|
33
|
-
};
|
|
34
|
-
}
|
package/utils/toc.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
export async function init() {
|
|
2
|
-
const contentArea = document.querySelector('[data-hs-toc="content"]');
|
|
3
|
-
const tocList = document.querySelector('[data-hs-toc="list"]');
|
|
4
|
-
|
|
5
|
-
// Check main elements
|
|
6
|
-
if (!contentArea) {
|
|
7
|
-
return;
|
|
8
|
-
}
|
|
9
|
-
if (!tocList) {
|
|
10
|
-
return;
|
|
11
|
-
}
|
|
12
|
-
if (tocList.children.length === 0) {
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const template = tocList.children[0];
|
|
17
|
-
tocList.innerHTML = "";
|
|
18
|
-
const h2Headings = contentArea.querySelectorAll("h2");
|
|
19
|
-
|
|
20
|
-
// Create sections and wrap content
|
|
21
|
-
h2Headings.forEach((heading, index) => {
|
|
22
|
-
const sectionId = heading.textContent
|
|
23
|
-
.toLowerCase()
|
|
24
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
25
|
-
.replace(/(^-|-$)/g, "");
|
|
26
|
-
const section = document.createElement("div");
|
|
27
|
-
section.id = sectionId;
|
|
28
|
-
heading.parentNode.insertBefore(section, heading);
|
|
29
|
-
section.appendChild(heading);
|
|
30
|
-
let nextElement = section.nextElementSibling;
|
|
31
|
-
while (nextElement && nextElement.tagName !== "H2") {
|
|
32
|
-
const elementToMove = nextElement;
|
|
33
|
-
nextElement = nextElement.nextElementSibling;
|
|
34
|
-
section.appendChild(elementToMove);
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
// Create TOC entries
|
|
39
|
-
h2Headings.forEach((heading, index) => {
|
|
40
|
-
const tocItem = template.cloneNode(true);
|
|
41
|
-
const link = tocItem.querySelector("a");
|
|
42
|
-
const sectionId = heading.parentElement.id;
|
|
43
|
-
link.href = "#" + sectionId;
|
|
44
|
-
|
|
45
|
-
// Bold numbered text
|
|
46
|
-
const number = document.createElement("strong");
|
|
47
|
-
number.textContent = index + 1 + ". ";
|
|
48
|
-
|
|
49
|
-
// Clear the link and add the number + text
|
|
50
|
-
link.innerHTML = "";
|
|
51
|
-
link.appendChild(number);
|
|
52
|
-
link.appendChild(document.createTextNode(heading.textContent));
|
|
53
|
-
|
|
54
|
-
// Add click handler for smooth scrolling
|
|
55
|
-
link.addEventListener("click", (e) => {
|
|
56
|
-
e.preventDefault();
|
|
57
|
-
|
|
58
|
-
const targetSection = document.getElementById(sectionId);
|
|
59
|
-
if (targetSection) {
|
|
60
|
-
targetSection.scrollIntoView({ behavior: "smooth" });
|
|
61
|
-
// Focus on the section for accessibility (will only show outline for keyboard users due to CSS)
|
|
62
|
-
setTimeout(() => {
|
|
63
|
-
targetSection.focus();
|
|
64
|
-
}, 100);
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
// Ensure sections are focusable for keyboard users but use CSS to control focus visibility
|
|
69
|
-
const targetSection = document.getElementById(sectionId);
|
|
70
|
-
if (targetSection) {
|
|
71
|
-
targetSection.setAttribute("tabindex", "-1");
|
|
72
|
-
// Use focus-visible to only show outline for keyboard focus
|
|
73
|
-
targetSection.style.outline = "none";
|
|
74
|
-
targetSection.style.setProperty("outline", "none", "important");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Add item to the TOC list
|
|
78
|
-
tocList.appendChild(tocItem);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
result: "util-toc initialized",
|
|
83
|
-
};
|
|
84
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|