@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,213 @@
|
|
|
1
|
+
# **Normalize Function Documentation**
|
|
2
|
+
|
|
3
|
+
## **Overview**
|
|
4
|
+
|
|
5
|
+
The normalize function ensures clickable wrappers have a consistent DOM structure by keeping either a `<button>` or `<a>` element based on href validity.
|
|
6
|
+
|
|
7
|
+
**Problem**: Designers include both `<button>` and `<a>` elements in wrappers, but only one should exist based on whether there's a valid link destination.
|
|
8
|
+
|
|
9
|
+
**Solution**: Automatically remove the unnecessary element, keeping the semantically correct one.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## **How It Works**
|
|
14
|
+
|
|
15
|
+
### **Detection**
|
|
16
|
+
|
|
17
|
+
1. Finds all elements with `data-hs-clickable="wrapper"`
|
|
18
|
+
2. Checks for direct children: `<button>` and `<a>`
|
|
19
|
+
3. Only processes wrappers that have BOTH elements
|
|
20
|
+
|
|
21
|
+
### **Logic**
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
IF <a> has valid href (not empty, not "#")
|
|
25
|
+
→ Keep <a>, remove <button>
|
|
26
|
+
ELSE
|
|
27
|
+
→ Keep <button>, remove <a>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### **Normalization**
|
|
31
|
+
|
|
32
|
+
- Kept element gets `data-hs-clickable="button"` attribute (if it doesn't already have it)
|
|
33
|
+
- Other element is removed from DOM
|
|
34
|
+
- Other modules can reliably find `[data-hs-clickable="button"]`
|
|
35
|
+
- Won't break if kept element already has the attribute
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## **HTML Structure**
|
|
40
|
+
|
|
41
|
+
### **Before Normalize**
|
|
42
|
+
|
|
43
|
+
```html
|
|
44
|
+
<div data-hs-clickable="wrapper">
|
|
45
|
+
<button class="card">
|
|
46
|
+
<span>Read More</span>
|
|
47
|
+
</button>
|
|
48
|
+
<a href="/blog/post-1" class="card">
|
|
49
|
+
<span>Read More</span>
|
|
50
|
+
</a>
|
|
51
|
+
</div>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### **After Normalize** (valid href)
|
|
55
|
+
|
|
56
|
+
```html
|
|
57
|
+
<div data-hs-clickable="wrapper">
|
|
58
|
+
<a href="/blog/post-1" class="card" data-hs-clickable="button">
|
|
59
|
+
<span>Read More</span>
|
|
60
|
+
</a>
|
|
61
|
+
</div>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### **After Normalize** (no valid href)
|
|
65
|
+
|
|
66
|
+
```html
|
|
67
|
+
<div data-hs-clickable="wrapper">
|
|
68
|
+
<button class="card" data-hs-clickable="button">
|
|
69
|
+
<span>Read More</span>
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## **CMS Integration**
|
|
77
|
+
|
|
78
|
+
Perfect for Webflow CMS where link availability varies:
|
|
79
|
+
|
|
80
|
+
```html
|
|
81
|
+
<!-- Blog post with link -->
|
|
82
|
+
<div data-hs-clickable="wrapper">
|
|
83
|
+
<button>Read More</button>
|
|
84
|
+
<a href="{{wf-link}}">Read More</a>
|
|
85
|
+
<!-- Valid CMS link -->
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<!-- Blog post without link (draft, disabled, etc.) -->
|
|
89
|
+
<div data-hs-clickable="wrapper">
|
|
90
|
+
<button>Read More</button>
|
|
91
|
+
<a href="">Read More</a>
|
|
92
|
+
<!-- Empty CMS link → button kept -->
|
|
93
|
+
</div>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## **Performance**
|
|
99
|
+
|
|
100
|
+
- **Speed**: ~0.3-0.5ms per wrapper (tested with 100 elements: ~30-50ms total)
|
|
101
|
+
- **DOM Operations**: Single `remove()` call per wrapper
|
|
102
|
+
- **Selector**: Uses `:scope >` for direct children only (fast)
|
|
103
|
+
- **Phase**: Runs in Phase 1 (normalize) before visual changes
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## **Barba.js / SPA Compatibility**
|
|
108
|
+
|
|
109
|
+
### **On Page Transition**
|
|
110
|
+
|
|
111
|
+
1. `destroy()` called - removes `data-hs-clickable="button"` attributes
|
|
112
|
+
2. New page DOM loads with both button and link again
|
|
113
|
+
3. `init()` runs - normalizes new DOM
|
|
114
|
+
4. Other modules find normalized structure
|
|
115
|
+
|
|
116
|
+
### **Cleanup Behavior**
|
|
117
|
+
|
|
118
|
+
- Cannot restore deleted elements (would require cloning before removal)
|
|
119
|
+
- Only removes the attribute if it was added during normalization (won't remove if element already had it)
|
|
120
|
+
- Safe for page transitions - new DOM has original structure
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## **Edge Cases**
|
|
125
|
+
|
|
126
|
+
### **Missing Elements**
|
|
127
|
+
|
|
128
|
+
```html
|
|
129
|
+
<!-- Only button → skipped (no normalization needed) -->
|
|
130
|
+
<div data-hs-clickable="wrapper">
|
|
131
|
+
<button>Click Me</button>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<!-- Only link → skipped -->
|
|
135
|
+
<div data-hs-clickable="wrapper">
|
|
136
|
+
<a href="/page">Click Me</a>
|
|
137
|
+
</div>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### **Invalid Hrefs**
|
|
141
|
+
|
|
142
|
+
All treated as invalid, button kept:
|
|
143
|
+
|
|
144
|
+
- `href=""`
|
|
145
|
+
- `href="#"`
|
|
146
|
+
- `href` attribute missing
|
|
147
|
+
|
|
148
|
+
### **Valid Hrefs**
|
|
149
|
+
|
|
150
|
+
All treated as valid, link kept:
|
|
151
|
+
|
|
152
|
+
- `href="/page"`
|
|
153
|
+
- `href="https://example.com"`
|
|
154
|
+
- `href="#section"` (anchor links are valid)
|
|
155
|
+
- `href="javascript:void(0)"` (technically valid, link kept)
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## **Module Integration**
|
|
160
|
+
|
|
161
|
+
Other modules can rely on normalized structure:
|
|
162
|
+
|
|
163
|
+
```javascript
|
|
164
|
+
// Always finds the kept element (button or link)
|
|
165
|
+
const clickables = document.querySelectorAll('[data-hs-clickable="button"]');
|
|
166
|
+
|
|
167
|
+
clickables.forEach((element) => {
|
|
168
|
+
// Could be <button> or <a>, both work the same
|
|
169
|
+
element.addEventListener('click', handleClick);
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## **Technical Details**
|
|
176
|
+
|
|
177
|
+
### **Selectors Used**
|
|
178
|
+
|
|
179
|
+
- Wrapper: `[data-hs-clickable='wrapper']`
|
|
180
|
+
- Direct children: `:scope > button`, `:scope > a`
|
|
181
|
+
- Added attribute: `data-hs-clickable="button"`
|
|
182
|
+
|
|
183
|
+
### **Cleanup Tracking**
|
|
184
|
+
|
|
185
|
+
```javascript
|
|
186
|
+
cleanup.processedWrappers = [
|
|
187
|
+
{
|
|
188
|
+
wrapper: HTMLElement, // The wrapper div
|
|
189
|
+
keptElement: HTMLElement, // Button or link that was kept
|
|
190
|
+
wasLink: Boolean, // True if link was kept
|
|
191
|
+
addedAttribute: Boolean, // True if we added the attribute (vs already existed)
|
|
192
|
+
},
|
|
193
|
+
];
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### **Return Value**
|
|
197
|
+
|
|
198
|
+
```javascript
|
|
199
|
+
{
|
|
200
|
+
result: "normalize processed 42 wrappers",
|
|
201
|
+
destroy: Function
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## **Notes**
|
|
208
|
+
|
|
209
|
+
- Runs in Phase 1 (normalize) with 1s timeout
|
|
210
|
+
- Uses global config for selector consistency
|
|
211
|
+
- Non-destructive: skips wrappers that don't need normalization
|
|
212
|
+
- Fast: querySelector with `:scope` limits search depth
|
|
213
|
+
- Reliable: Works even if other modules fail
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { globalConfig, querySelectorAll } from '@utils';
|
|
2
|
+
|
|
3
|
+
export async function init(config) {
|
|
4
|
+
const cleanup = {
|
|
5
|
+
processedWrappers: [],
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// Find all clickable wrappers using global clickable config
|
|
9
|
+
const wrappers = querySelectorAll(globalConfig.clickable, 'wrapper');
|
|
10
|
+
|
|
11
|
+
wrappers.forEach((wrapper) => {
|
|
12
|
+
// Find direct button and link children
|
|
13
|
+
const button = wrapper.querySelector(':scope > button');
|
|
14
|
+
const link = wrapper.querySelector(':scope > a');
|
|
15
|
+
|
|
16
|
+
// Only process if both exist
|
|
17
|
+
if (!button || !link) return;
|
|
18
|
+
|
|
19
|
+
// Check if link has valid href
|
|
20
|
+
const href = link.getAttribute('href');
|
|
21
|
+
const hasValidHref = href && href !== '' && href !== '#';
|
|
22
|
+
|
|
23
|
+
let keptElement, removedElement;
|
|
24
|
+
|
|
25
|
+
if (hasValidHref) {
|
|
26
|
+
// Keep link, remove button
|
|
27
|
+
keptElement = link;
|
|
28
|
+
removedElement = button;
|
|
29
|
+
} else {
|
|
30
|
+
// Keep button, remove link
|
|
31
|
+
keptElement = button;
|
|
32
|
+
removedElement = link;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Check if kept element already has data-hs-clickable="button"
|
|
36
|
+
const hadAttribute = keptElement.getAttribute('data-hs-clickable') === 'button';
|
|
37
|
+
|
|
38
|
+
// Ensure kept element has data-hs-clickable="button"
|
|
39
|
+
if (!hadAttribute) {
|
|
40
|
+
keptElement.setAttribute('data-hs-clickable', 'button');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Remove the other element
|
|
44
|
+
removedElement.remove();
|
|
45
|
+
|
|
46
|
+
// Track for cleanup
|
|
47
|
+
cleanup.processedWrappers.push({
|
|
48
|
+
wrapper,
|
|
49
|
+
keptElement,
|
|
50
|
+
wasLink: keptElement === link,
|
|
51
|
+
addedAttribute: !hadAttribute,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
result: `normalize processed ${cleanup.processedWrappers.length} wrappers`,
|
|
57
|
+
destroy: () => {
|
|
58
|
+
// On destroy, we can't restore deleted elements
|
|
59
|
+
// Only remove the attribute if we added it
|
|
60
|
+
cleanup.processedWrappers.forEach(({ keptElement, addedAttribute }) => {
|
|
61
|
+
if (addedAttribute) {
|
|
62
|
+
keptElement.removeAttribute('data-hs-clickable');
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
cleanup.processedWrappers.length = 0;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
# **Dupe Function Documentation**
|
|
2
|
+
|
|
3
|
+
## **Overview**
|
|
4
|
+
|
|
5
|
+
The dupe function automatically duplicates DOM elements or their children, useful for patterns, decorations, and marquee effects.
|
|
6
|
+
|
|
7
|
+
**Problem**: Designers need multiple copies of elements for visual effects (marquees, patterns, decorative elements).
|
|
8
|
+
|
|
9
|
+
**Solution**: Automatically clone elements based on simple attributes, runs after clickable normalization.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## **Modes**
|
|
14
|
+
|
|
15
|
+
### **1. Child Mode** (`data-hs-dupe="child"`)
|
|
16
|
+
|
|
17
|
+
Duplicates the first direct child element as siblings.
|
|
18
|
+
|
|
19
|
+
```html
|
|
20
|
+
<!-- Before -->
|
|
21
|
+
<div data-hs-dupe="child">
|
|
22
|
+
<svg class="icon">...</svg>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<!-- After -->
|
|
26
|
+
<div data-hs-dupe="child">
|
|
27
|
+
<svg class="icon">...</svg>
|
|
28
|
+
<svg class="icon">...</svg>
|
|
29
|
+
<!-- cloned -->
|
|
30
|
+
</div>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Use cases:**
|
|
34
|
+
|
|
35
|
+
- Icon duplication for visual effects
|
|
36
|
+
- Pattern backgrounds
|
|
37
|
+
- Decorative elements
|
|
38
|
+
|
|
39
|
+
### **2. Self Mode** (`data-hs-dupe="self"`)
|
|
40
|
+
|
|
41
|
+
Duplicates the element itself as next siblings.
|
|
42
|
+
|
|
43
|
+
```html
|
|
44
|
+
<!-- Before -->
|
|
45
|
+
<div class="container">
|
|
46
|
+
<div data-hs-dupe="self" class="item">Content</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<!-- After -->
|
|
50
|
+
<div class="container">
|
|
51
|
+
<div data-hs-dupe="self" class="item">Content</div>
|
|
52
|
+
<div class="item">Content</div>
|
|
53
|
+
<!-- cloned, attributes removed -->
|
|
54
|
+
</div>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Use cases:**
|
|
58
|
+
|
|
59
|
+
- Marquee items (duplicate cards/logos)
|
|
60
|
+
- Loading skeletons
|
|
61
|
+
- Grid patterns
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## **Count Attribute**
|
|
66
|
+
|
|
67
|
+
Control how many duplicates are created with `data-hs-dupe-count`.
|
|
68
|
+
|
|
69
|
+
**Default:** 1 duplicate (2 total elements)
|
|
70
|
+
|
|
71
|
+
### **Examples**
|
|
72
|
+
|
|
73
|
+
```html
|
|
74
|
+
<!-- Create 5 duplicates (6 total) -->
|
|
75
|
+
<div data-hs-dupe="child" data-hs-dupe-count="5">
|
|
76
|
+
<img src="logo.svg" />
|
|
77
|
+
</div>
|
|
78
|
+
<!-- Result: 1 original + 5 clones = 6 images -->
|
|
79
|
+
|
|
80
|
+
<!-- Marquee with 10 items -->
|
|
81
|
+
<div class="marquee">
|
|
82
|
+
<div data-hs-dupe="self" data-hs-dupe-count="10">
|
|
83
|
+
<span>Scrolling Text</span>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<!-- Result: 11 total items for seamless loop -->
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### **Maximum Limit: 20**
|
|
90
|
+
|
|
91
|
+
Count is capped at 20 to prevent performance issues.
|
|
92
|
+
|
|
93
|
+
```html
|
|
94
|
+
<div data-hs-dupe="child" data-hs-dupe-count="50">
|
|
95
|
+
<div>Item</div>
|
|
96
|
+
</div>
|
|
97
|
+
<!-- Console: "[dupe] Count of 50 exceeds maximum of 20. Capping at 20." -->
|
|
98
|
+
<!-- Result: 1 original + 20 clones = 21 elements -->
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## **Modifiers**
|
|
104
|
+
|
|
105
|
+
Customize cloned elements with optional modifier attributes.
|
|
106
|
+
|
|
107
|
+
### **1. Hidden Modifier** (`data-hs-dupe-hidden`)
|
|
108
|
+
|
|
109
|
+
Adds `aria-hidden="true"` to clones for accessibility.
|
|
110
|
+
|
|
111
|
+
**Use case:** Duplicate text for visual animation effects while keeping screen readers focused on the original.
|
|
112
|
+
|
|
113
|
+
```html
|
|
114
|
+
<button data-hs-dupe="child" data-hs-dupe-hidden>
|
|
115
|
+
<span>Next</span>
|
|
116
|
+
</button>
|
|
117
|
+
<!-- Result: Clone gets aria-hidden="true" -->
|
|
118
|
+
<button data-hs-dupe="child" data-hs-dupe-hidden>
|
|
119
|
+
<span>Next</span>
|
|
120
|
+
<span aria-hidden="true">Next</span>
|
|
121
|
+
<!-- Cloned, hidden from screen readers -->
|
|
122
|
+
</button>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### **2. No-Select Modifier** (`data-hs-dupe-no-select`)
|
|
126
|
+
|
|
127
|
+
Adds `user-select: none` to clones to prevent text selection and copying.
|
|
128
|
+
|
|
129
|
+
**Use case:** Animated text effects where duplicate text shouldn't be selectable/copyable.
|
|
130
|
+
|
|
131
|
+
```html
|
|
132
|
+
<div data-hs-dupe="child" data-hs-dupe-no-select>
|
|
133
|
+
<span>Animated Text</span>
|
|
134
|
+
</div>
|
|
135
|
+
<!-- Result: Clone has inline style user-select: none -->
|
|
136
|
+
<div data-hs-dupe="child" data-hs-dupe-no-select>
|
|
137
|
+
<span>Animated Text</span>
|
|
138
|
+
<span style="user-select: none; -webkit-user-select: none; -ms-user-select: none;"
|
|
139
|
+
>Animated Text</span
|
|
140
|
+
>
|
|
141
|
+
</div>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### **3. Combining Modifiers**
|
|
145
|
+
|
|
146
|
+
Use both modifiers together for complete isolation of clones.
|
|
147
|
+
|
|
148
|
+
```html
|
|
149
|
+
<button data-hs-dupe="child" data-hs-dupe-count="3" data-hs-dupe-hidden data-hs-dupe-no-select>
|
|
150
|
+
<span class="text">Click Me</span>
|
|
151
|
+
</button>
|
|
152
|
+
<!-- Result: 3 clones, all with aria-hidden and user-select: none -->
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Perfect for:**
|
|
156
|
+
|
|
157
|
+
- Text animation effects
|
|
158
|
+
- Decorative duplicate elements
|
|
159
|
+
- Loading animations
|
|
160
|
+
- Visual-only duplicates
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## **Execution Order**
|
|
165
|
+
|
|
166
|
+
Dupe runs **after clickable** in the normalize phase:
|
|
167
|
+
|
|
168
|
+
1. **Clickable** normalizes button/link structure
|
|
169
|
+
2. **Dupe** duplicates the normalized elements
|
|
170
|
+
|
|
171
|
+
### **Example: Clickable + Dupe**
|
|
172
|
+
|
|
173
|
+
```html
|
|
174
|
+
<!-- Before normalize phase -->
|
|
175
|
+
<div data-hs-clickable="wrapper">
|
|
176
|
+
<button>Click</button>
|
|
177
|
+
<a href="/page">Click</a>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<!-- After clickable (keeps link, removes button) -->
|
|
181
|
+
<div data-hs-clickable="wrapper">
|
|
182
|
+
<a href="/page" data-hs-clickable="button">Click</a>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<!-- Then wrap for duplication -->
|
|
186
|
+
<div data-hs-dupe="child" data-hs-dupe-count="3">
|
|
187
|
+
<div data-hs-clickable="wrapper">
|
|
188
|
+
<a href="/page" data-hs-clickable="button">Click</a>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<!-- After dupe -->
|
|
193
|
+
<div data-hs-dupe="child" data-hs-dupe-count="3">
|
|
194
|
+
<div data-hs-clickable="wrapper">
|
|
195
|
+
<a href="/page" data-hs-clickable="button">Click</a>
|
|
196
|
+
</div>
|
|
197
|
+
<div data-hs-clickable="wrapper">
|
|
198
|
+
<a href="/page" data-hs-clickable="button">Click</a>
|
|
199
|
+
</div>
|
|
200
|
+
<div data-hs-clickable="wrapper">
|
|
201
|
+
<a href="/page" data-hs-clickable="button">Click</a>
|
|
202
|
+
</div>
|
|
203
|
+
<div data-hs-clickable="wrapper">
|
|
204
|
+
<a href="/page" data-hs-clickable="button">Click</a>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
<!-- Result: 4 normalized, clickable links ready for marquee -->
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## **Performance**
|
|
213
|
+
|
|
214
|
+
- **Speed**: ~0.1-0.2ms per element duplication
|
|
215
|
+
- **Count of 20**: ~2-4ms total
|
|
216
|
+
- **DOM Operations**: `cloneNode(true)` + `appendChild()` per duplicate
|
|
217
|
+
- **Phase**: Runs in Phase 1 (normalize) after clickable
|
|
218
|
+
|
|
219
|
+
**Benchmark** (100 elements with count=5):
|
|
220
|
+
|
|
221
|
+
- Total time: ~50-100ms
|
|
222
|
+
- No noticeable performance impact
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## **Barba.js / SPA Compatibility**
|
|
227
|
+
|
|
228
|
+
### **On Page Transition**
|
|
229
|
+
|
|
230
|
+
1. `destroy()` called - removes all cloned elements
|
|
231
|
+
2. New page DOM loads with original structure
|
|
232
|
+
3. `init()` runs - duplicates elements again on new DOM
|
|
233
|
+
|
|
234
|
+
### **Cleanup Behavior**
|
|
235
|
+
|
|
236
|
+
- Removes all cloned elements from DOM
|
|
237
|
+
- Original elements remain untouched
|
|
238
|
+
- Attributes stay on original elements
|
|
239
|
+
- Safe for page transitions
|
|
240
|
+
|
|
241
|
+
### **Self Mode Cleanup**
|
|
242
|
+
|
|
243
|
+
- Cloned elements have `data-hs-dupe` attributes removed
|
|
244
|
+
- Only original element retains the attribute
|
|
245
|
+
- Clones are fully removed on destroy
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## **Edge Cases**
|
|
250
|
+
|
|
251
|
+
### **No Child Found (child mode)**
|
|
252
|
+
|
|
253
|
+
```html
|
|
254
|
+
<div data-hs-dupe="child">
|
|
255
|
+
<!-- Empty, no children -->
|
|
256
|
+
</div>
|
|
257
|
+
<!-- Console: "[dupe] No child element found to duplicate" -->
|
|
258
|
+
<!-- No duplication occurs -->
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### **No Parent (self mode)**
|
|
262
|
+
|
|
263
|
+
```html
|
|
264
|
+
<!-- Orphan element, not in DOM -->
|
|
265
|
+
<div data-hs-dupe="self">Content</div>
|
|
266
|
+
<!-- Console: "[dupe] Element has no parent, cannot duplicate self" -->
|
|
267
|
+
<!-- No duplication occurs -->
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### **Invalid Mode**
|
|
271
|
+
|
|
272
|
+
```html
|
|
273
|
+
<div data-hs-dupe="invalid">
|
|
274
|
+
<div>Item</div>
|
|
275
|
+
</div>
|
|
276
|
+
<!-- Console: "[dupe] Invalid mode. Use 'child' or 'self'" -->
|
|
277
|
+
<!-- No duplication occurs -->
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### **Invalid Count**
|
|
281
|
+
|
|
282
|
+
```html
|
|
283
|
+
<!-- Non-numeric -->
|
|
284
|
+
<div data-hs-dupe="child" data-hs-dupe-count="abc">
|
|
285
|
+
<div>Item</div>
|
|
286
|
+
</div>
|
|
287
|
+
<!-- Console: "[dupe] Invalid count, using default of 1" -->
|
|
288
|
+
<!-- Creates 1 duplicate -->
|
|
289
|
+
|
|
290
|
+
<!-- Negative or zero -->
|
|
291
|
+
<div data-hs-dupe="child" data-hs-dupe-count="-5">
|
|
292
|
+
<div>Item</div>
|
|
293
|
+
</div>
|
|
294
|
+
<!-- Console: "[dupe] Invalid count, using default of 1" -->
|
|
295
|
+
<!-- Creates 1 duplicate -->
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## **Use Cases**
|
|
301
|
+
|
|
302
|
+
### **1. Marquee with CMS Items**
|
|
303
|
+
|
|
304
|
+
```html
|
|
305
|
+
<div class="marquee-wrapper">
|
|
306
|
+
<div data-hs-dupe="self" data-hs-dupe-count="5" class="marquee-item">
|
|
307
|
+
<!-- CMS-generated card -->
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
<!-- Creates 6 items for seamless infinite scroll -->
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### **2. Icon Pattern Background**
|
|
314
|
+
|
|
315
|
+
```html
|
|
316
|
+
<div class="pattern" data-hs-dupe="child" data-hs-dupe-count="12">
|
|
317
|
+
<svg class="dot">...</svg>
|
|
318
|
+
</div>
|
|
319
|
+
<!-- Creates 13 dots in a pattern -->
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### **3. Loading Skeleton**
|
|
323
|
+
|
|
324
|
+
```html
|
|
325
|
+
<div class="skeleton-list">
|
|
326
|
+
<div data-hs-dupe="self" data-hs-dupe-count="4" class="skeleton-item">
|
|
327
|
+
<div class="skeleton-line"></div>
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
<!-- Creates 5 skeleton items while loading -->
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### **4. Button Icon Duplication**
|
|
334
|
+
|
|
335
|
+
```html
|
|
336
|
+
<button data-hs-dupe="child">
|
|
337
|
+
<svg class="arrow">→</svg>
|
|
338
|
+
<span>Next</span>
|
|
339
|
+
</button>
|
|
340
|
+
<!-- Duplicates arrow for visual effect -->
|
|
341
|
+
<button data-hs-dupe="child">
|
|
342
|
+
<svg class="arrow">→</svg>
|
|
343
|
+
<svg class="arrow">→</svg>
|
|
344
|
+
<!-- cloned -->
|
|
345
|
+
<span>Next</span>
|
|
346
|
+
</button>
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## **Technical Details**
|
|
352
|
+
|
|
353
|
+
### **Attributes Used**
|
|
354
|
+
|
|
355
|
+
- Element: `data-hs-dupe` (value: "child" or "self")
|
|
356
|
+
- Count: `data-hs-dupe-count` (integer, default: 1, max: 20)
|
|
357
|
+
|
|
358
|
+
### **Cleanup Tracking**
|
|
359
|
+
|
|
360
|
+
```javascript
|
|
361
|
+
cleanup.processedElements = [
|
|
362
|
+
{
|
|
363
|
+
element: HTMLElement, // Original element with data-hs-dupe
|
|
364
|
+
mode: String, // "child" or "self"
|
|
365
|
+
clones: [HTMLElement], // Array of cloned elements
|
|
366
|
+
},
|
|
367
|
+
];
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### **Return Value**
|
|
371
|
+
|
|
372
|
+
```javascript
|
|
373
|
+
{
|
|
374
|
+
result: "dupe processed 42 elements",
|
|
375
|
+
destroy: Function
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## **Integration with Other Modules**
|
|
382
|
+
|
|
383
|
+
### **Works After Clickable**
|
|
384
|
+
|
|
385
|
+
- Clickable normalizes DOM structure first
|
|
386
|
+
- Dupe then duplicates the normalized result
|
|
387
|
+
- Ensures duplicates have consistent structure
|
|
388
|
+
|
|
389
|
+
### **Works Before Visual-DOM**
|
|
390
|
+
|
|
391
|
+
- Structure module (marquee, etc.) finds duplicated elements
|
|
392
|
+
- All duplicates are present before visual animations start
|
|
393
|
+
- No flash of incomplete content
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## **Notes**
|
|
398
|
+
|
|
399
|
+
- Runs in Phase 1 (normalize) after clickable
|
|
400
|
+
- Maximum 20 duplicates per element (performance protection)
|
|
401
|
+
- Deep clone with `cloneNode(true)` - includes all children
|
|
402
|
+
- Self mode removes dupe attributes from clones
|
|
403
|
+
- Child mode leaves original structure intact
|
|
404
|
+
- Compatible with Barba.js page transitions
|
|
405
|
+
- Fast: <0.2ms per duplication operation
|