@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
package/autoInit/form/README.md
DELETED
|
@@ -1,338 +0,0 @@
|
|
|
1
|
-
# **Form System Documentation**
|
|
2
|
-
|
|
3
|
-
## **Overview**
|
|
4
|
-
|
|
5
|
-
The form system provides two main features:
|
|
6
|
-
1. **Honeypot spam prevention** - Blocks bot submissions using a hidden field
|
|
7
|
-
2. **Custom select component** - Fully accessible, styled select dropdowns that sync with native `<select>` elements for form submission
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## **Honeypot Spam Prevention**
|
|
12
|
-
|
|
13
|
-
### **Required Element**
|
|
14
|
-
|
|
15
|
-
**Honeypot Field** *(hidden field in your form)*
|
|
16
|
-
|
|
17
|
-
* data-hs-form="form-handler"
|
|
18
|
-
* Should be hidden with CSS (bots will auto-fill it, humans won't see it)
|
|
19
|
-
|
|
20
|
-
### **What It Does**
|
|
21
|
-
|
|
22
|
-
1. **Listens for form submissions** - Captures all form submit events
|
|
23
|
-
2. **Checks honeypot field** - If the hidden field has a value, it's likely a bot
|
|
24
|
-
3. **Blocks submission** - Prevents form from submitting if honeypot is filled
|
|
25
|
-
|
|
26
|
-
### **Example**
|
|
27
|
-
|
|
28
|
-
```html
|
|
29
|
-
<form>
|
|
30
|
-
<input type="text" name="name" placeholder="Your Name">
|
|
31
|
-
<input type="email" name="email" placeholder="Your Email">
|
|
32
|
-
|
|
33
|
-
<!-- Honeypot field (hidden with CSS) -->
|
|
34
|
-
<input type="text" name="website" data-hs-form="form-handler" style="display: none;">
|
|
35
|
-
|
|
36
|
-
<button type="submit">Submit</button>
|
|
37
|
-
</form>
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
**Result:** Legitimate users never see the honeypot field. Bots auto-fill all fields, triggering the spam prevention.
|
|
41
|
-
|
|
42
|
-
---
|
|
43
|
-
|
|
44
|
-
## **Custom Select Component**
|
|
45
|
-
|
|
46
|
-
### **Required Elements**
|
|
47
|
-
|
|
48
|
-
**Select Wrapper** *(main container)*
|
|
49
|
-
|
|
50
|
-
* data-hs-form="select"
|
|
51
|
-
|
|
52
|
-
**Native Select** *(hidden select element for form submission)*
|
|
53
|
-
|
|
54
|
-
* Standard `<select>` element with `<option>` children
|
|
55
|
-
* Should have `name` attribute for form submission
|
|
56
|
-
* Will be hidden but remains functional
|
|
57
|
-
|
|
58
|
-
**Toggle Button** *(visual button to open/close dropdown)*
|
|
59
|
-
|
|
60
|
-
* `<button>` element or element with `role="button"`
|
|
61
|
-
* Should contain a `<span>` element for displaying selected text
|
|
62
|
-
* Webflow IX must add/remove `.is-active` class on this button
|
|
63
|
-
|
|
64
|
-
**Custom List** *(dropdown container)*
|
|
65
|
-
|
|
66
|
-
* data-hs-form="select-list"
|
|
67
|
-
* First child is used as template for generating options
|
|
68
|
-
* Webflow IX must show/hide this when button is clicked
|
|
69
|
-
|
|
70
|
-
**Template Element** *(first child of select-list)*
|
|
71
|
-
|
|
72
|
-
* Should contain a `<span>` element for option text
|
|
73
|
-
* Will be cloned for each `<option>` in the native select
|
|
74
|
-
* Original template is removed after cloning
|
|
75
|
-
|
|
76
|
-
**Label** *(optional but recommended)*
|
|
77
|
-
|
|
78
|
-
* Standard `<label>` element
|
|
79
|
-
* Can be inside wrapper or reference select by `for` attribute
|
|
80
|
-
|
|
81
|
-
**Typical element layout:**
|
|
82
|
-
|
|
83
|
-
1. Select Wrapper (data-hs-form="select")
|
|
84
|
-
1. Label (optional)
|
|
85
|
-
1. Text: "Choose an option"
|
|
86
|
-
2. Native Select (`<select>`)
|
|
87
|
-
1. Option 1 (`<option value="value1">`)
|
|
88
|
-
1. Text: "Option 1"
|
|
89
|
-
2. Option 2 (`<option value="value2">`)
|
|
90
|
-
1. Text: "Option 2"
|
|
91
|
-
3. Toggle Button (`<button>`)
|
|
92
|
-
1. Button Text (`<span>`)
|
|
93
|
-
1. Text: "Choose an option"
|
|
94
|
-
4. Custom List (data-hs-form="select-list")
|
|
95
|
-
1. Template Element
|
|
96
|
-
1. Text Span (`<span>`)
|
|
97
|
-
1. Text: "Placeholder"
|
|
98
|
-
|
|
99
|
-
### **What It Does**
|
|
100
|
-
|
|
101
|
-
1. **Unwraps Webflow slots** - Removes any `<div>` wrappers inside `<select>` elements
|
|
102
|
-
2. **Clones template** - Uses first child of select-list as template
|
|
103
|
-
3. **Generates options** - Creates custom option elements from native `<option>` elements
|
|
104
|
-
4. **Syncs state** - Keeps custom UI and native select in sync
|
|
105
|
-
5. **ARIA management** - Automatically updates accessibility attributes
|
|
106
|
-
6. **Keyboard navigation** - Full arrow key, Tab, Enter, Space, Escape support
|
|
107
|
-
7. **Focus management** - Handles focus return, tabbing out, and keyboard navigation
|
|
108
|
-
|
|
109
|
-
### **When It Runs**
|
|
110
|
-
|
|
111
|
-
* Runs once on page load (or DOMContentLoaded)
|
|
112
|
-
* Available for manual re-initialization via `window.initCustomSelects()`
|
|
113
|
-
* Runs again on Barba.js page transitions (via `reinitialize()`)
|
|
114
|
-
|
|
115
|
-
### **Interaction Requirements (IX3)**
|
|
116
|
-
|
|
117
|
-
**CRITICAL:** Webflow interactions MUST add/remove `.is-active` class on the toggle button.
|
|
118
|
-
|
|
119
|
-
**Triggers:**
|
|
120
|
-
|
|
121
|
-
1. **Click trigger** on `<button>` element
|
|
122
|
-
1. `Each click: Toggle play/reverse`
|
|
123
|
-
|
|
124
|
-
**Timeline:**
|
|
125
|
-
|
|
126
|
-
1. 0s: Toggle `.is-active` class on the button element
|
|
127
|
-
2. .06s: Rest of animation (fade in, slide, etc.)
|
|
128
|
-
|
|
129
|
-
---
|
|
130
|
-
|
|
131
|
-
## **Accessibility**
|
|
132
|
-
|
|
133
|
-
### **Automatic ARIA Setup**
|
|
134
|
-
|
|
135
|
-
For each select, the system automatically:
|
|
136
|
-
|
|
137
|
-
1. **Button (combobox)**
|
|
138
|
-
- `role="combobox"`
|
|
139
|
-
- `aria-haspopup="listbox"`
|
|
140
|
-
- `aria-expanded="true|false"` (synced with `.is-active` class)
|
|
141
|
-
- `aria-controls="[listbox-id]"`
|
|
142
|
-
- `aria-labelledby="[label-id]"` (if label exists)
|
|
143
|
-
- `aria-activedescendant="[option-id]"` (when navigating with arrows)
|
|
144
|
-
|
|
145
|
-
2. **Select List**
|
|
146
|
-
- `role="listbox"`
|
|
147
|
-
- `tabindex="-1"` (not directly tabbable)
|
|
148
|
-
|
|
149
|
-
3. **Options**
|
|
150
|
-
- `role="option"`
|
|
151
|
-
- `aria-selected="true|false"`
|
|
152
|
-
- `tabindex="0"` when dropdown open, `tabindex="-1"` when closed
|
|
153
|
-
- Unique `id` for aria-activedescendant
|
|
154
|
-
|
|
155
|
-
4. **Label**
|
|
156
|
-
- Connected to native select with `for` attribute
|
|
157
|
-
- Linked to button with `aria-labelledby`
|
|
158
|
-
|
|
159
|
-
### **Focus Management**
|
|
160
|
-
|
|
161
|
-
1. **Opening dropdown** - All options become tabbable
|
|
162
|
-
2. **Closing dropdown** - Focus returns to button if inside list
|
|
163
|
-
3. **Tabbing out** - Automatically closes dropdown
|
|
164
|
-
4. **Arrow navigation** - Focuses options with visual `.focused` class
|
|
165
|
-
|
|
166
|
-
---
|
|
167
|
-
|
|
168
|
-
## **Usage Examples**
|
|
169
|
-
|
|
170
|
-
### **Basic Select**
|
|
171
|
-
|
|
172
|
-
```html
|
|
173
|
-
<div data-hs-form="select">
|
|
174
|
-
<label for="fruit-select">Choose a fruit</label>
|
|
175
|
-
|
|
176
|
-
<select name="fruit" id="fruit-select">
|
|
177
|
-
<option value="">Choose an option</option>
|
|
178
|
-
<option value="apple">Apple</option>
|
|
179
|
-
<option value="banana">Banana</option>
|
|
180
|
-
<option value="orange">Orange</option>
|
|
181
|
-
</select>
|
|
182
|
-
|
|
183
|
-
<button>
|
|
184
|
-
<span>Choose an option</span>
|
|
185
|
-
</button>
|
|
186
|
-
|
|
187
|
-
<div data-hs-form="select-list">
|
|
188
|
-
<div>
|
|
189
|
-
<span>Template</span>
|
|
190
|
-
</div>
|
|
191
|
-
</div>
|
|
192
|
-
</div>
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
**Result:** Creates 4 custom options (including empty "Choose an option"). Native select stays synced for form submission.
|
|
196
|
-
|
|
197
|
-
### **Pre-selected Option**
|
|
198
|
-
|
|
199
|
-
```html
|
|
200
|
-
<select name="size">
|
|
201
|
-
<option value="small">Small</option>
|
|
202
|
-
<option value="medium" selected>Medium</option>
|
|
203
|
-
<option value="large">Large</option>
|
|
204
|
-
</select>
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
**Result:** Button text automatically shows "Medium" on page load. Corresponding custom option has `aria-selected="true"`.
|
|
208
|
-
|
|
209
|
-
### **Options with Special Characters**
|
|
210
|
-
|
|
211
|
-
```html
|
|
212
|
-
<option value='product"deluxe"'>Deluxe Edition</option>
|
|
213
|
-
<option value="user's choice">Custom Option</option>
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
**Result:** Works correctly - uses safe value comparison instead of selector injection.
|
|
217
|
-
|
|
218
|
-
---
|
|
219
|
-
|
|
220
|
-
## **Keyboard Navigation**
|
|
221
|
-
|
|
222
|
-
### **When Button is Focused**
|
|
223
|
-
|
|
224
|
-
1. **Space** or **Enter** � Open/close dropdown
|
|
225
|
-
2. **ArrowDown** � Open dropdown (or focus first option if already open)
|
|
226
|
-
3. **ArrowUp** � Focus last option (if dropdown is open)
|
|
227
|
-
4. **Escape** � Close dropdown (if open)
|
|
228
|
-
|
|
229
|
-
### **When Option is Focused**
|
|
230
|
-
|
|
231
|
-
1. **ArrowDown** � Focus next option
|
|
232
|
-
2. **ArrowUp** � Focus previous option (or close and focus button if on first option)
|
|
233
|
-
3. **Enter** or **Space** � Select option and close dropdown
|
|
234
|
-
4. **Escape** � Close dropdown and return focus to button
|
|
235
|
-
5. **Tab** � Navigate through options, tab out of last option closes dropdown
|
|
236
|
-
|
|
237
|
-
---
|
|
238
|
-
|
|
239
|
-
## **State Synchronization**
|
|
240
|
-
|
|
241
|
-
### **Native Select � Custom UI**
|
|
242
|
-
|
|
243
|
-
When native select value changes (programmatically or via Webflow):
|
|
244
|
-
1. Finds matching custom option by value
|
|
245
|
-
2. Updates button text
|
|
246
|
-
3. Updates `aria-selected` on all options
|
|
247
|
-
|
|
248
|
-
### **Custom UI � Native Select**
|
|
249
|
-
|
|
250
|
-
When user selects custom option:
|
|
251
|
-
1. Updates native select's `value`
|
|
252
|
-
2. Dispatches `change` event on native select
|
|
253
|
-
3. Updates button text
|
|
254
|
-
4. Updates `aria-selected`
|
|
255
|
-
5. Closes dropdown
|
|
256
|
-
|
|
257
|
-
**Both stay in sync at all times.**
|
|
258
|
-
|
|
259
|
-
---
|
|
260
|
-
|
|
261
|
-
## **Key Attributes Summary**
|
|
262
|
-
|
|
263
|
-
| Attribute | Purpose | Required On |
|
|
264
|
-
| ----- | ----- | ----- |
|
|
265
|
-
| `data-hs-form="select"` | Select wrapper | Wrapper div |
|
|
266
|
-
| `data-hs-form="select-list"` | Custom options container | List div |
|
|
267
|
-
| `data-hs-form="form-handler"` | Honeypot spam field | Hidden input (optional) |
|
|
268
|
-
|
|
269
|
-
---
|
|
270
|
-
|
|
271
|
-
## **API**
|
|
272
|
-
|
|
273
|
-
### **Manual Re-initialization**
|
|
274
|
-
|
|
275
|
-
```javascript
|
|
276
|
-
window.initCustomSelects();
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
Re-scans page for new selects and initializes them. Useful for dynamically added content.
|
|
280
|
-
|
|
281
|
-
---
|
|
282
|
-
|
|
283
|
-
## **Notes**
|
|
284
|
-
|
|
285
|
-
1. **Native select required** - Must have actual `<select>` element for form submission
|
|
286
|
-
2. **Button must have span** - `<span>` child is required for text updates
|
|
287
|
-
3. **Template is removed** - First child of select-list is cloned then deleted
|
|
288
|
-
4. **Webflow IX required** - Must toggle `.is-active` class on button
|
|
289
|
-
5. **Barba.js compatible** - Automatically cleans up on destroy/reinitialize
|
|
290
|
-
6. **Silent failures** - Missing required elements cause function to return early (no errors)
|
|
291
|
-
7. **Empty values supported** - Options with `value=""` work correctly
|
|
292
|
-
8. **Special characters safe** - Handles quotes and special characters in option values
|
|
293
|
-
|
|
294
|
-
---
|
|
295
|
-
|
|
296
|
-
## **Common Issues**
|
|
297
|
-
|
|
298
|
-
**Dropdown not opening:**
|
|
299
|
-
|
|
300
|
-
1. Verify Webflow IX adds `.is-active` class to button (not wrapper or list)
|
|
301
|
-
2. Check that button exists and is `<button>` or has `role="button"`
|
|
302
|
-
3. Ensure select-list exists with `data-hs-form="select-list"`
|
|
303
|
-
|
|
304
|
-
**Button text not updating:**
|
|
305
|
-
|
|
306
|
-
1. Verify button has a `<span>` child element
|
|
307
|
-
2. Check that template element has a `<span>` child
|
|
308
|
-
3. Ensure option values match between native and custom
|
|
309
|
-
|
|
310
|
-
**Options not clickable:**
|
|
311
|
-
|
|
312
|
-
1. Check that template element exists as first child of select-list
|
|
313
|
-
2. Verify template has `<span>` for text
|
|
314
|
-
3. Ensure options are being generated (inspect DOM)
|
|
315
|
-
|
|
316
|
-
**Can't tab to options:**
|
|
317
|
-
|
|
318
|
-
1. Verify dropdown is open (`.is-active` class on button)
|
|
319
|
-
2. Check Webflow IX animation shows the select-list
|
|
320
|
-
3. Ensure `display: none` is removed when open
|
|
321
|
-
|
|
322
|
-
**Form not submitting selected value:**
|
|
323
|
-
|
|
324
|
-
1. Native `<select>` must have `name` attribute
|
|
325
|
-
2. Check that native select's value is updating (inspect DOM or use console)
|
|
326
|
-
3. Verify native select is inside `<form>` element
|
|
327
|
-
|
|
328
|
-
**Dropdown doesn't close when tabbing out:**
|
|
329
|
-
|
|
330
|
-
1. Ensure wrapper element properly contains button and list
|
|
331
|
-
2. Check that `data-hs-form="select"` is on wrapper
|
|
332
|
-
3. Verify Webflow IX removes `.is-active` when clicking button again
|
|
333
|
-
|
|
334
|
-
**Spam prevention not working:**
|
|
335
|
-
|
|
336
|
-
1. Verify honeypot field has `data-hs-form="form-handler"`
|
|
337
|
-
2. Ensure field is hidden with CSS (not removed from DOM)
|
|
338
|
-
3. Check that field is inside the `<form>` element
|
package/autoInit/form/form.js
DELETED
|
@@ -1,374 +0,0 @@
|
|
|
1
|
-
export function init() {
|
|
2
|
-
// Centralized cleanup tracking
|
|
3
|
-
const cleanup = {
|
|
4
|
-
observers: [],
|
|
5
|
-
handlers: [],
|
|
6
|
-
honeypotHandler: null
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
const addObserver = (observer) => cleanup.observers.push(observer);
|
|
10
|
-
const addHandler = (element, event, handler, options) => {
|
|
11
|
-
element.addEventListener(event, handler, options);
|
|
12
|
-
cleanup.handlers.push({ element, event, handler, options });
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
// Honeypot spam prevention
|
|
16
|
-
const honeypotHandler = (e) => {
|
|
17
|
-
const form = e.target;
|
|
18
|
-
if (form.tagName !== 'FORM') return;
|
|
19
|
-
|
|
20
|
-
const honeypot = form.querySelector('[data-hs-form="form-handler"]');
|
|
21
|
-
if (honeypot && honeypot.value) {
|
|
22
|
-
// Honeypot filled - likely a bot
|
|
23
|
-
e.preventDefault();
|
|
24
|
-
e.stopPropagation();
|
|
25
|
-
e.stopImmediatePropagation();
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
cleanup.honeypotHandler = honeypotHandler;
|
|
31
|
-
document.addEventListener('submit', honeypotHandler, true);
|
|
32
|
-
|
|
33
|
-
// Simple Custom Select Component for Webflow
|
|
34
|
-
(function() {
|
|
35
|
-
'use strict';
|
|
36
|
-
|
|
37
|
-
// Initialize all custom selects on the page
|
|
38
|
-
function initCustomSelects() {
|
|
39
|
-
// Unwrap any divs inside select elements (Webflow component slots)
|
|
40
|
-
document.querySelectorAll('select > div').forEach(div => {
|
|
41
|
-
const select = div.parentElement;
|
|
42
|
-
while (div.firstChild) {
|
|
43
|
-
select.appendChild(div.firstChild);
|
|
44
|
-
}
|
|
45
|
-
div.remove();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const selectWrappers = document.querySelectorAll('[data-hs-form="select"]');
|
|
49
|
-
|
|
50
|
-
selectWrappers.forEach(wrapper => {
|
|
51
|
-
initSingleSelect(wrapper, addHandler, addObserver);
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Initialize a single custom select
|
|
56
|
-
function initSingleSelect(wrapper, addHandler, addObserver) {
|
|
57
|
-
// Find all required elements
|
|
58
|
-
const realSelect = wrapper.querySelector('select');
|
|
59
|
-
if (!realSelect) return;
|
|
60
|
-
|
|
61
|
-
const selectName = realSelect.getAttribute('name') || 'custom-select';
|
|
62
|
-
const customList = wrapper.querySelector('[data-hs-form="select-list"]');
|
|
63
|
-
const button = wrapper.querySelector('button') || wrapper.querySelector('[role="button"]');
|
|
64
|
-
|
|
65
|
-
if (!customList || !button) return;
|
|
66
|
-
|
|
67
|
-
// Get and clone the option template
|
|
68
|
-
const optionTemplate = customList.firstElementChild;
|
|
69
|
-
if (!optionTemplate) return;
|
|
70
|
-
|
|
71
|
-
const templateClone = optionTemplate.cloneNode(true);
|
|
72
|
-
optionTemplate.remove();
|
|
73
|
-
|
|
74
|
-
// Build options from real select
|
|
75
|
-
const realOptions = realSelect.querySelectorAll('option');
|
|
76
|
-
realOptions.forEach((option, index) => {
|
|
77
|
-
const optionElement = templateClone.cloneNode(true);
|
|
78
|
-
const textSpan = optionElement.querySelector('span');
|
|
79
|
-
|
|
80
|
-
if (textSpan) {
|
|
81
|
-
textSpan.textContent = option.textContent;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Add attributes
|
|
85
|
-
optionElement.setAttribute('data-value', option.value);
|
|
86
|
-
optionElement.setAttribute('role', 'option');
|
|
87
|
-
optionElement.setAttribute('id', `${selectName}-option-${index}`);
|
|
88
|
-
optionElement.setAttribute('tabindex', '-1');
|
|
89
|
-
|
|
90
|
-
// Set selected state if this option is selected
|
|
91
|
-
if (option.selected) {
|
|
92
|
-
optionElement.setAttribute('aria-selected', 'true');
|
|
93
|
-
// Update button text
|
|
94
|
-
const buttonText = button.querySelector('span') || button;
|
|
95
|
-
if (buttonText.tagName === 'SPAN') {
|
|
96
|
-
buttonText.textContent = option.textContent;
|
|
97
|
-
}
|
|
98
|
-
} else {
|
|
99
|
-
optionElement.setAttribute('aria-selected', 'false');
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
customList.appendChild(optionElement);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// Add ARIA attributes
|
|
106
|
-
customList.setAttribute('role', 'listbox');
|
|
107
|
-
customList.setAttribute('id', `${selectName}-listbox`);
|
|
108
|
-
customList.setAttribute('tabindex', '-1');
|
|
109
|
-
|
|
110
|
-
button.setAttribute('role', 'combobox');
|
|
111
|
-
button.setAttribute('aria-haspopup', 'listbox');
|
|
112
|
-
button.setAttribute('aria-controls', `${selectName}-listbox`);
|
|
113
|
-
button.setAttribute('aria-expanded', 'false');
|
|
114
|
-
button.setAttribute('id', `${selectName}-button`);
|
|
115
|
-
|
|
116
|
-
// Find and connect label if exists
|
|
117
|
-
const label = wrapper.querySelector('label') ||
|
|
118
|
-
document.querySelector(`label[for="${realSelect.id}"]`);
|
|
119
|
-
if (label) {
|
|
120
|
-
const labelId = label.id || `${selectName}-label`;
|
|
121
|
-
label.id = labelId;
|
|
122
|
-
// Ensure real select has proper ID for label connection
|
|
123
|
-
if (!realSelect.id) {
|
|
124
|
-
realSelect.id = `${selectName}-select`;
|
|
125
|
-
}
|
|
126
|
-
label.setAttribute('for', realSelect.id);
|
|
127
|
-
button.setAttribute('aria-labelledby', labelId);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Track state
|
|
131
|
-
let isOpen = false;
|
|
132
|
-
|
|
133
|
-
// Update expanded state
|
|
134
|
-
function updateExpandedState(expanded) {
|
|
135
|
-
isOpen = expanded;
|
|
136
|
-
button.setAttribute('aria-expanded', expanded.toString());
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Focus option by index
|
|
140
|
-
function focusOption(index) {
|
|
141
|
-
const options = customList.querySelectorAll('[role="option"]');
|
|
142
|
-
if (index < 0 || index >= options.length) return;
|
|
143
|
-
|
|
144
|
-
// Remove previous focus
|
|
145
|
-
options.forEach(opt => {
|
|
146
|
-
opt.classList.remove('focused');
|
|
147
|
-
opt.setAttribute('tabindex', '-1');
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
// Add new focus
|
|
151
|
-
options[index].classList.add('focused');
|
|
152
|
-
options[index].setAttribute('tabindex', '0');
|
|
153
|
-
options[index].focus();
|
|
154
|
-
button.setAttribute('aria-activedescendant', options[index].id);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Select option
|
|
158
|
-
function selectOption(optionElement) {
|
|
159
|
-
const value = optionElement.getAttribute('data-value') || '';
|
|
160
|
-
const text = optionElement.querySelector('span')?.textContent || optionElement.textContent;
|
|
161
|
-
|
|
162
|
-
// Update real select
|
|
163
|
-
realSelect.value = value;
|
|
164
|
-
realSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
|
165
|
-
|
|
166
|
-
// Update button text
|
|
167
|
-
const buttonText = button.querySelector('span') || button;
|
|
168
|
-
if (buttonText.tagName === 'SPAN') {
|
|
169
|
-
buttonText.textContent = text;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Update aria-selected
|
|
173
|
-
customList.querySelectorAll('[role="option"]').forEach(opt => {
|
|
174
|
-
opt.setAttribute('aria-selected', 'false');
|
|
175
|
-
});
|
|
176
|
-
optionElement.setAttribute('aria-selected', 'true');
|
|
177
|
-
|
|
178
|
-
// Close dropdown by clicking button
|
|
179
|
-
button.click();
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Button keyboard events
|
|
183
|
-
const buttonKeydownHandler = (e) => {
|
|
184
|
-
switch(e.key) {
|
|
185
|
-
case ' ':
|
|
186
|
-
case 'Enter':
|
|
187
|
-
e.preventDefault();
|
|
188
|
-
button.click();
|
|
189
|
-
break;
|
|
190
|
-
|
|
191
|
-
case 'ArrowDown':
|
|
192
|
-
e.preventDefault();
|
|
193
|
-
if (!isOpen) {
|
|
194
|
-
button.click();
|
|
195
|
-
} else {
|
|
196
|
-
focusOption(0);
|
|
197
|
-
}
|
|
198
|
-
break;
|
|
199
|
-
|
|
200
|
-
case 'ArrowUp':
|
|
201
|
-
e.preventDefault();
|
|
202
|
-
if (isOpen) {
|
|
203
|
-
const options = customList.querySelectorAll('[role="option"]');
|
|
204
|
-
focusOption(options.length - 1);
|
|
205
|
-
}
|
|
206
|
-
break;
|
|
207
|
-
|
|
208
|
-
case 'Escape':
|
|
209
|
-
if (isOpen) {
|
|
210
|
-
e.preventDefault();
|
|
211
|
-
button.click();
|
|
212
|
-
}
|
|
213
|
-
break;
|
|
214
|
-
}
|
|
215
|
-
};
|
|
216
|
-
addHandler(button, 'keydown', buttonKeydownHandler);
|
|
217
|
-
|
|
218
|
-
// Option keyboard events (delegated)
|
|
219
|
-
const listKeydownHandler = (e) => {
|
|
220
|
-
const option = e.target.closest('[role="option"]');
|
|
221
|
-
if (!option) return;
|
|
222
|
-
|
|
223
|
-
const options = Array.from(customList.querySelectorAll('[role="option"]'));
|
|
224
|
-
const currentIdx = options.indexOf(option);
|
|
225
|
-
|
|
226
|
-
switch(e.key) {
|
|
227
|
-
case 'ArrowDown':
|
|
228
|
-
e.preventDefault();
|
|
229
|
-
if (currentIdx < options.length - 1) {
|
|
230
|
-
focusOption(currentIdx + 1);
|
|
231
|
-
}
|
|
232
|
-
break;
|
|
233
|
-
|
|
234
|
-
case 'ArrowUp':
|
|
235
|
-
e.preventDefault();
|
|
236
|
-
if (currentIdx === 0) {
|
|
237
|
-
button.click();
|
|
238
|
-
button.focus();
|
|
239
|
-
} else {
|
|
240
|
-
focusOption(currentIdx - 1);
|
|
241
|
-
}
|
|
242
|
-
break;
|
|
243
|
-
|
|
244
|
-
case 'Enter':
|
|
245
|
-
case ' ':
|
|
246
|
-
e.preventDefault();
|
|
247
|
-
selectOption(option);
|
|
248
|
-
break;
|
|
249
|
-
|
|
250
|
-
case 'Escape':
|
|
251
|
-
e.preventDefault();
|
|
252
|
-
button.click();
|
|
253
|
-
button.focus();
|
|
254
|
-
break;
|
|
255
|
-
}
|
|
256
|
-
};
|
|
257
|
-
addHandler(customList, 'keydown', listKeydownHandler);
|
|
258
|
-
|
|
259
|
-
// Option click events
|
|
260
|
-
const listClickHandler = (e) => {
|
|
261
|
-
const option = e.target.closest('[role="option"]');
|
|
262
|
-
if (option) {
|
|
263
|
-
selectOption(option);
|
|
264
|
-
}
|
|
265
|
-
};
|
|
266
|
-
addHandler(customList, 'click', listClickHandler);
|
|
267
|
-
|
|
268
|
-
// Handle tabbing out of select list
|
|
269
|
-
const listFocusoutHandler = (e) => {
|
|
270
|
-
// Check if focus is moving outside the wrapper (or to browser chrome)
|
|
271
|
-
if ((!e.relatedTarget || !wrapper.contains(e.relatedTarget)) && button.classList.contains('is-active')) {
|
|
272
|
-
button.click(); // Close the dropdown
|
|
273
|
-
}
|
|
274
|
-
};
|
|
275
|
-
addHandler(customList, 'focusout', listFocusoutHandler);
|
|
276
|
-
|
|
277
|
-
// Track open/close state via .is-active class on button
|
|
278
|
-
const observer = new MutationObserver(() => {
|
|
279
|
-
const isActive = button.classList.contains('is-active');
|
|
280
|
-
|
|
281
|
-
updateExpandedState(isActive);
|
|
282
|
-
|
|
283
|
-
if (isActive) {
|
|
284
|
-
// Make all options tabbable when opened
|
|
285
|
-
const options = customList.querySelectorAll('[role="option"]');
|
|
286
|
-
options.forEach(opt => {
|
|
287
|
-
opt.setAttribute('tabindex', '0');
|
|
288
|
-
});
|
|
289
|
-
} else {
|
|
290
|
-
// Return focus to button BEFORE aria changes if it was in the list
|
|
291
|
-
if (document.activeElement?.closest('[data-hs-form="select-list"]') === customList) {
|
|
292
|
-
button.focus();
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Reset on close
|
|
296
|
-
button.removeAttribute('aria-activedescendant');
|
|
297
|
-
const options = customList.querySelectorAll('[role="option"]');
|
|
298
|
-
options.forEach(opt => {
|
|
299
|
-
opt.setAttribute('tabindex', '-1');
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
// Observe the button for .is-active class changes
|
|
305
|
-
observer.observe(button, {
|
|
306
|
-
attributes: true,
|
|
307
|
-
attributeFilter: ['class']
|
|
308
|
-
});
|
|
309
|
-
addObserver(observer);
|
|
310
|
-
|
|
311
|
-
// Sync with real select changes
|
|
312
|
-
const selectChangeHandler = () => {
|
|
313
|
-
const selectedOption = realSelect.options[realSelect.selectedIndex];
|
|
314
|
-
if (selectedOption) {
|
|
315
|
-
const options = customList.querySelectorAll('[role="option"]');
|
|
316
|
-
const customOption = Array.from(options).find(opt =>
|
|
317
|
-
opt.getAttribute('data-value') === selectedOption.value
|
|
318
|
-
);
|
|
319
|
-
if (customOption) {
|
|
320
|
-
// Update button text
|
|
321
|
-
const text = customOption.querySelector('span')?.textContent || customOption.textContent;
|
|
322
|
-
const buttonText = button.querySelector('span') || button;
|
|
323
|
-
if (buttonText.tagName === 'SPAN') {
|
|
324
|
-
buttonText.textContent = text;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Update aria-selected
|
|
328
|
-
options.forEach(opt => {
|
|
329
|
-
opt.setAttribute('aria-selected', 'false');
|
|
330
|
-
});
|
|
331
|
-
customOption.setAttribute('aria-selected', 'true');
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
};
|
|
335
|
-
addHandler(realSelect, 'change', selectChangeHandler);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Initialize on DOM ready
|
|
339
|
-
if (document.readyState === 'loading') {
|
|
340
|
-
document.addEventListener('DOMContentLoaded', initCustomSelects);
|
|
341
|
-
} else {
|
|
342
|
-
initCustomSelects();
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Reinitialize for dynamic content
|
|
346
|
-
window.initCustomSelects = initCustomSelects;
|
|
347
|
-
})();
|
|
348
|
-
|
|
349
|
-
return {
|
|
350
|
-
result: "form initialized",
|
|
351
|
-
destroy: () => {
|
|
352
|
-
// Remove honeypot handler
|
|
353
|
-
if (cleanup.honeypotHandler) {
|
|
354
|
-
document.removeEventListener('submit', cleanup.honeypotHandler, true);
|
|
355
|
-
cleanup.honeypotHandler = null;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Disconnect all observers
|
|
359
|
-
cleanup.observers.forEach(obs => obs.disconnect());
|
|
360
|
-
cleanup.observers.length = 0;
|
|
361
|
-
|
|
362
|
-
// Remove all event listeners
|
|
363
|
-
cleanup.handlers.forEach(({ element, event, handler, options }) => {
|
|
364
|
-
element.removeEventListener(event, handler, options);
|
|
365
|
-
});
|
|
366
|
-
cleanup.handlers.length = 0;
|
|
367
|
-
|
|
368
|
-
// Remove window API
|
|
369
|
-
if (window.initCustomSelects) {
|
|
370
|
-
delete window.initCustomSelects;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
};
|
|
374
|
-
}
|