@hortonstudio/main 1.9.10 → 1.9.11
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.
|
@@ -14,12 +14,11 @@ Complete pagination system for paginated lists with controls, counters, dot navi
|
|
|
14
14
|
- **Responsive Layouts**: Different items per page for desktop/mobile
|
|
15
15
|
- **Page Controls**: Next/Previous buttons with infinite looping
|
|
16
16
|
- **Page Counter**: Live-updating "X / Y" page indicator
|
|
17
|
-
- **Dot Navigation**: Visual page indicators with click/keyboard support
|
|
17
|
+
- **Dot Navigation**: Visual page indicators with click/keyboard support (semantic buttons)
|
|
18
18
|
- **Auto-height Management**: Smooth height transitions between pages
|
|
19
19
|
- **Focus Management**: Uses `inert` attribute for inactive pages
|
|
20
20
|
- **ARIA Live Announcements**: Screen reader announcements for page changes
|
|
21
21
|
- **Mobile Auto-scroll**: Scrolls controls into view on mobile after navigation
|
|
22
|
-
- **Infinite Mode**: Disable all pagination for continuous scrolling
|
|
23
22
|
|
|
24
23
|
---
|
|
25
24
|
|
|
@@ -45,21 +44,35 @@ Complete pagination system for paginated lists with controls, counters, dot navi
|
|
|
45
44
|
|
|
46
45
|
### **Next Button** *(required)*
|
|
47
46
|
|
|
48
|
-
**Attribute:** `data-hs-pagination="next"`
|
|
47
|
+
**Attribute:** `data-hs-pagination="next"` on wrapper element
|
|
49
48
|
|
|
50
49
|
**What it does:** Navigate to next page. Loops to first page after last page.
|
|
51
50
|
|
|
52
|
-
**
|
|
51
|
+
**Structure:** Uses `data-site-clickable="element"` pattern:
|
|
52
|
+
```html
|
|
53
|
+
<div data-hs-pagination="next" data-site-clickable="element">
|
|
54
|
+
<button type="button">Next</button>
|
|
55
|
+
</div>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Accessibility:** Automatically receives `aria-label="Go to next page"` on the button element
|
|
53
59
|
|
|
54
60
|
---
|
|
55
61
|
|
|
56
62
|
### **Previous Button** *(required)*
|
|
57
63
|
|
|
58
|
-
**Attribute:** `data-hs-pagination="previous"`
|
|
64
|
+
**Attribute:** `data-hs-pagination="previous"` on wrapper element
|
|
59
65
|
|
|
60
66
|
**What it does:** Navigate to previous page. Loops to last page from first page.
|
|
61
67
|
|
|
62
|
-
**
|
|
68
|
+
**Structure:** Uses `data-site-clickable="element"` pattern:
|
|
69
|
+
```html
|
|
70
|
+
<div data-hs-pagination="previous" data-site-clickable="element">
|
|
71
|
+
<button type="button">Previous</button>
|
|
72
|
+
</div>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Accessibility:** Automatically receives `aria-label="Go to previous page"` on the button element
|
|
63
76
|
|
|
64
77
|
---
|
|
65
78
|
|
|
@@ -67,40 +80,33 @@ Complete pagination system for paginated lists with controls, counters, dot navi
|
|
|
67
80
|
|
|
68
81
|
**Attribute:** `data-hs-pagination="controls"`
|
|
69
82
|
|
|
70
|
-
**What it does:** Container for pagination controls AND configuration
|
|
71
|
-
|
|
72
|
-
**Configuration Syntax:**
|
|
73
|
-
```
|
|
74
|
-
data-hs-config="[option], [option], ..."
|
|
75
|
-
```
|
|
83
|
+
**What it does:** Container for pagination controls AND configuration.
|
|
76
84
|
|
|
77
|
-
**Configuration
|
|
85
|
+
**Configuration Attributes:**
|
|
78
86
|
|
|
79
|
-
|
|
87
|
+
**`data-hs-pagination-show`** - Desktop items per page (default: 6)
|
|
80
88
|
```html
|
|
81
|
-
data-hs-
|
|
89
|
+
data-hs-pagination-show="4"
|
|
82
90
|
```
|
|
83
|
-
Shows 6 items per page on desktop (default: 6)
|
|
84
91
|
|
|
85
|
-
|
|
92
|
+
**`data-hs-pagination-show-mobile`** - Mobile items per page (optional, defaults to desktop value)
|
|
86
93
|
```html
|
|
87
|
-
data-hs-
|
|
94
|
+
data-hs-pagination-show-mobile="2"
|
|
88
95
|
```
|
|
89
|
-
Shows 3 items per page on mobile (default: desktop value)
|
|
90
96
|
|
|
91
|
-
**
|
|
97
|
+
**Example:**
|
|
92
98
|
```html
|
|
93
|
-
data-hs-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
**Combined Example:**
|
|
98
|
-
```html
|
|
99
|
-
<div data-hs-pagination="controls" data-hs-config="show-4, show-2-mobile">
|
|
99
|
+
<div data-hs-pagination="controls"
|
|
100
|
+
data-hs-pagination-show="4"
|
|
101
|
+
data-hs-pagination-show-mobile="2">
|
|
100
102
|
<!-- Buttons, counter, etc. -->
|
|
101
103
|
</div>
|
|
102
104
|
```
|
|
103
105
|
|
|
106
|
+
**No Pagination (Infinite Mode):**
|
|
107
|
+
|
|
108
|
+
To disable pagination and show all items, simply don't render the controls element at all. Without controls, the pagination system won't initialize and the list displays naturally.
|
|
109
|
+
|
|
104
110
|
---
|
|
105
111
|
|
|
106
112
|
### **Counter** *(optional)*
|
|
@@ -125,10 +131,11 @@ Disables pagination completely (hides controls and dots, shows all items)
|
|
|
125
131
|
- At least one child element (used as template)
|
|
126
132
|
- Use `.is-active` class to designate active template
|
|
127
133
|
- If only one template, same used for active/inactive states
|
|
134
|
+
- **Dots are semantic `<button>` elements with `type="button"`**
|
|
128
135
|
|
|
129
136
|
**Accessibility:** System automatically adds:
|
|
130
137
|
- Individual dots via separate pagination dots system
|
|
131
|
-
- `role="button"`, `tabindex="0"`, `aria-label` on each dot
|
|
138
|
+
- `role="button"`, `tabindex="0"`, `aria-label` on each dot button
|
|
132
139
|
- `aria-current="page"` on active dot
|
|
133
140
|
|
|
134
141
|
---
|
|
@@ -154,23 +161,34 @@ Disables pagination completely (hides controls and dots, shows all items)
|
|
|
154
161
|
</div>
|
|
155
162
|
|
|
156
163
|
<!-- Controls -->
|
|
157
|
-
<div data-hs-pagination="controls"
|
|
158
|
-
|
|
164
|
+
<div data-hs-pagination="controls"
|
|
165
|
+
data-hs-pagination-show="4"
|
|
166
|
+
data-hs-pagination-show-mobile="2">
|
|
167
|
+
|
|
168
|
+
<div data-hs-pagination="previous" data-site-clickable="element">
|
|
169
|
+
<button type="button">Previous</button>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
159
172
|
<div data-hs-pagination="counter">1 / 2</div>
|
|
160
|
-
|
|
173
|
+
|
|
174
|
+
<div data-hs-pagination="next" data-site-clickable="element">
|
|
175
|
+
<button type="button">Next</button>
|
|
176
|
+
</div>
|
|
161
177
|
</div>
|
|
162
178
|
|
|
163
179
|
<!-- Dots -->
|
|
164
180
|
<div data-hs-pagination="dots">
|
|
165
|
-
<
|
|
166
|
-
<
|
|
181
|
+
<button type="button" class="dot is-active"></button> <!-- Active template -->
|
|
182
|
+
<button type="button" class="dot"></button> <!-- Inactive template -->
|
|
167
183
|
</div>
|
|
168
184
|
</div>
|
|
169
185
|
```
|
|
170
186
|
|
|
171
187
|
---
|
|
172
188
|
|
|
173
|
-
### **
|
|
189
|
+
### **No Pagination (Infinite Mode)**
|
|
190
|
+
|
|
191
|
+
To disable pagination and show all items without pagination controls, simply don't render the controls element:
|
|
174
192
|
|
|
175
193
|
```html
|
|
176
194
|
<div data-hs-pagination="wrapper">
|
|
@@ -180,14 +198,12 @@ Disables pagination completely (hides controls and dots, shows all items)
|
|
|
180
198
|
<div>Item 1</div>
|
|
181
199
|
<div>Item 2</div>
|
|
182
200
|
<div>Item 3</div>
|
|
201
|
+
<div>Item 4</div>
|
|
202
|
+
<div>Item 5</div>
|
|
183
203
|
</div>
|
|
184
204
|
</div>
|
|
185
205
|
|
|
186
|
-
|
|
187
|
-
<!-- Controls hidden automatically -->
|
|
188
|
-
<button data-hs-pagination="previous">Previous</button>
|
|
189
|
-
<button data-hs-pagination="next">Next</button>
|
|
190
|
-
</div>
|
|
206
|
+
<!-- No controls = no pagination initialization -->
|
|
191
207
|
</div>
|
|
192
208
|
```
|
|
193
209
|
|
|
@@ -223,9 +239,11 @@ Uses CSS custom property `--data-hs-break` to detect layout:
|
|
|
223
239
|
| ----- | ----- | ----- | ----- |
|
|
224
240
|
| `data-hs-pagination="wrapper"` | Main container | **Required** | Wrapper div |
|
|
225
241
|
| `data-hs-pagination="list"` | Items to paginate | **Required** | List container |
|
|
226
|
-
| `data-hs-pagination="next"` | Next button | **Required** |
|
|
227
|
-
| `data-hs-pagination="previous"` | Previous button | **Required** |
|
|
242
|
+
| `data-hs-pagination="next"` | Next button wrapper | **Required** | Wrapper div with `data-site-clickable="element"` |
|
|
243
|
+
| `data-hs-pagination="previous"` | Previous button wrapper | **Required** | Wrapper div with `data-site-clickable="element"` |
|
|
228
244
|
| `data-hs-pagination="controls"` | Controls + config | Required for config | Wrapper div |
|
|
245
|
+
| `data-hs-pagination-show` | Desktop items per page | Required on controls | Attribute (number) |
|
|
246
|
+
| `data-hs-pagination-show-mobile` | Mobile items per page | Optional on controls | Attribute (number) |
|
|
229
247
|
| `data-hs-pagination="counter"` | Page counter | Optional | Any element |
|
|
230
248
|
| `data-hs-pagination="dots"` | Dots container | Optional | Wrapper div |
|
|
231
249
|
|
|
@@ -259,7 +277,7 @@ Uses CSS custom property `--data-hs-break` to detect layout:
|
|
|
259
277
|
|
|
260
278
|
### **Initialization:**
|
|
261
279
|
1. Finds all `[data-hs-pagination="wrapper"]` containers
|
|
262
|
-
2. Reads configuration from `data-hs-
|
|
280
|
+
2. Reads configuration from `data-hs-pagination-show` attributes
|
|
263
281
|
3. Calculates total pages based on items and config
|
|
264
282
|
4. Splits items into page lists
|
|
265
283
|
5. Clones pages for infinite loop (adds before/after)
|
|
@@ -280,10 +298,10 @@ Uses CSS custom property `--data-hs-break` to detect layout:
|
|
|
280
298
|
3. Re-initializes pagination if breakpoint changes
|
|
281
299
|
4. Maintains current page position when possible
|
|
282
300
|
|
|
283
|
-
### **Infinite Mode:**
|
|
284
|
-
1.
|
|
285
|
-
2.
|
|
286
|
-
3.
|
|
301
|
+
### **No Pagination (Infinite Mode):**
|
|
302
|
+
1. Don't render controls element in HTML
|
|
303
|
+
2. Pagination system won't initialize
|
|
304
|
+
3. All items display naturally in list
|
|
287
305
|
|
|
288
306
|
---
|
|
289
307
|
|
|
@@ -329,22 +347,27 @@ The pagination system integrates with the separate pagination dots system:
|
|
|
329
347
|
|
|
330
348
|
### **Desktop: 6 items, Mobile: 3 items**
|
|
331
349
|
```html
|
|
332
|
-
<div data-hs-
|
|
350
|
+
<div data-hs-pagination="controls"
|
|
351
|
+
data-hs-pagination-show="6"
|
|
352
|
+
data-hs-pagination-show-mobile="3">
|
|
333
353
|
```
|
|
334
354
|
|
|
335
355
|
### **Desktop: 4 items, Mobile: 2 items**
|
|
336
356
|
```html
|
|
337
|
-
<div data-hs-
|
|
357
|
+
<div data-hs-pagination="controls"
|
|
358
|
+
data-hs-pagination-show="4"
|
|
359
|
+
data-hs-pagination-show-mobile="2">
|
|
338
360
|
```
|
|
339
361
|
|
|
340
362
|
### **Same on all devices (8 items)**
|
|
341
363
|
```html
|
|
342
|
-
<div data-hs-
|
|
364
|
+
<div data-hs-pagination="controls"
|
|
365
|
+
data-hs-pagination-show="8">
|
|
343
366
|
```
|
|
344
367
|
|
|
345
|
-
### **
|
|
368
|
+
### **No pagination (infinite mode)**
|
|
346
369
|
```html
|
|
347
|
-
|
|
370
|
+
<!-- Don't render controls element at all -->
|
|
348
371
|
```
|
|
349
372
|
|
|
350
373
|
---
|
|
@@ -360,10 +383,10 @@ The pagination system integrates with the separate pagination dots system:
|
|
|
360
383
|
- Dots hidden similarly
|
|
361
384
|
- All items displayed on single page
|
|
362
385
|
|
|
363
|
-
### **Infinite Mode:**
|
|
364
|
-
-
|
|
365
|
-
-
|
|
366
|
-
- List
|
|
386
|
+
### **No Pagination (Infinite Mode):**
|
|
387
|
+
- Don't render controls element
|
|
388
|
+
- Pagination system won't initialize
|
|
389
|
+
- List displays all items naturally
|
|
367
390
|
- No pagination logic runs
|
|
368
391
|
|
|
369
392
|
### **Layout Changes:**
|
|
@@ -388,12 +411,13 @@ The pagination system integrates with the separate pagination dots system:
|
|
|
388
411
|
|
|
389
412
|
**Pagination not working:**
|
|
390
413
|
1. Ensure wrapper, list, next, and previous elements exist
|
|
391
|
-
2. Check `data-hs-
|
|
414
|
+
2. Check `data-hs-pagination-show` attribute has valid number
|
|
392
415
|
3. Verify list has children elements
|
|
393
|
-
4.
|
|
416
|
+
4. Ensure button structure uses `data-site-clickable="element"` pattern
|
|
417
|
+
5. Check console for warnings
|
|
394
418
|
|
|
395
419
|
**Items not splitting into pages:**
|
|
396
|
-
1. Ensure `data-hs-
|
|
420
|
+
1. Ensure `data-hs-pagination-show` specifies a number
|
|
397
421
|
2. Check that total items > items per page
|
|
398
422
|
3. Verify CSS custom property `--data-hs-break` is set
|
|
399
423
|
|
|
@@ -409,9 +433,9 @@ The pagination system integrates with the separate pagination dots system:
|
|
|
409
433
|
3. Check ResizeObserver is not being blocked
|
|
410
434
|
|
|
411
435
|
**Controls always hidden:**
|
|
412
|
-
1.
|
|
413
|
-
2.
|
|
414
|
-
3.
|
|
436
|
+
1. Ensure total items > items per page
|
|
437
|
+
2. Verify controls container exists in DOM
|
|
438
|
+
3. Check that `data-hs-pagination-show` is set properly
|
|
415
439
|
|
|
416
440
|
---
|
|
417
441
|
|
|
@@ -12,26 +12,48 @@ export function init() {
|
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
// Initialize all pagination containers
|
|
15
|
-
|
|
16
|
-
.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
try {
|
|
16
|
+
document.querySelectorAll('[data-hs-pagination="wrapper"]')
|
|
17
|
+
.forEach(container => {
|
|
18
|
+
try {
|
|
19
|
+
const instance = initPaginationInstance(container, cleanup);
|
|
20
|
+
if (!instance) {
|
|
21
|
+
console.warn('[hs-pagination] Failed to initialize container', container);
|
|
22
|
+
}
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error('[hs-pagination] Error initializing container:', error, container);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error('[hs-pagination] Critical error during initialization:', error);
|
|
29
|
+
}
|
|
22
30
|
|
|
23
31
|
return {
|
|
24
32
|
result: "pagination initialized",
|
|
25
33
|
destroy: () => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
try {
|
|
35
|
+
// Disconnect all observers
|
|
36
|
+
cleanup.observers.forEach(obs => {
|
|
37
|
+
try {
|
|
38
|
+
obs.disconnect();
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error('[hs-pagination] Error disconnecting observer:', error);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
cleanup.observers.length = 0;
|
|
44
|
+
|
|
45
|
+
// Remove all event listeners
|
|
46
|
+
cleanup.handlers.forEach(({ element, event, handler }) => {
|
|
47
|
+
try {
|
|
48
|
+
element.removeEventListener(event, handler);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error('[hs-pagination] Error removing event listener:', error);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
cleanup.handlers.length = 0;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error('[hs-pagination] Critical error during cleanup:', error);
|
|
56
|
+
}
|
|
35
57
|
}
|
|
36
58
|
};
|
|
37
59
|
}
|
|
@@ -47,18 +69,30 @@ function initPaginationInstance(container, cleanup) {
|
|
|
47
69
|
if (!wrapper) return null;
|
|
48
70
|
|
|
49
71
|
const elements = {
|
|
50
|
-
nextBtn: container.querySelector('[data-hs-pagination="next"]'),
|
|
51
|
-
prevBtn: container.querySelector('[data-hs-pagination="previous"]'),
|
|
52
|
-
counter: container.querySelector('[data-hs-pagination="counter"]'),
|
|
53
72
|
controls: container.querySelector('[data-hs-pagination="controls"]'),
|
|
73
|
+
counter: container.querySelector('[data-hs-pagination="counter"]'),
|
|
54
74
|
dotsWrap: container.querySelector('[data-hs-pagination="dots"]')
|
|
55
75
|
};
|
|
56
76
|
|
|
57
|
-
|
|
77
|
+
// Early exit for infinite mode - no controls means no pagination
|
|
78
|
+
if (!elements.controls) {
|
|
79
|
+
return { initialized: false };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Find next/previous buttons using data-site-clickable pattern
|
|
83
|
+
const nextClickable = container.querySelector('[data-hs-pagination="next"]');
|
|
84
|
+
const prevClickable = container.querySelector('[data-hs-pagination="previous"]');
|
|
85
|
+
const nextBtn = nextClickable?.children[0];
|
|
86
|
+
const prevBtn = prevClickable?.children[0];
|
|
87
|
+
|
|
88
|
+
if (!nextBtn || !prevBtn) {
|
|
58
89
|
console.warn('[hs-pagination] Missing required navigation buttons');
|
|
59
90
|
return null;
|
|
60
91
|
}
|
|
61
92
|
|
|
93
|
+
elements.nextBtn = nextBtn;
|
|
94
|
+
elements.prevBtn = prevBtn;
|
|
95
|
+
|
|
62
96
|
// Add ARIA attributes to buttons
|
|
63
97
|
elements.nextBtn.setAttribute('aria-label', 'Go to next page');
|
|
64
98
|
elements.prevBtn.setAttribute('aria-label', 'Go to previous page');
|
|
@@ -67,26 +101,9 @@ function initPaginationInstance(container, cleanup) {
|
|
|
67
101
|
elements.counter.setAttribute('aria-label', 'Current page');
|
|
68
102
|
}
|
|
69
103
|
|
|
70
|
-
// Parse configuration
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
const isInfiniteMode = configOptions.includes('infinite');
|
|
74
|
-
|
|
75
|
-
const desktopItems = !isInfiniteMode ? (parseInt(config.match(/show-(\d+)(?!-mobile)/)?.[1]) || 6) : 0;
|
|
76
|
-
const mobileItems = !isInfiniteMode ? (parseInt(config.match(/show-(\d+)-mobile/)?.[1]) || desktopItems) : 0;
|
|
77
|
-
|
|
78
|
-
// Early exit for infinite mode - disable pagination completely
|
|
79
|
-
if (isInfiniteMode) {
|
|
80
|
-
if (elements.controls) {
|
|
81
|
-
elements.controls.style.display = 'none';
|
|
82
|
-
elements.controls.setAttribute('aria-hidden', 'true');
|
|
83
|
-
}
|
|
84
|
-
if (elements.dotsWrap) {
|
|
85
|
-
elements.dotsWrap.style.display = 'none';
|
|
86
|
-
elements.dotsWrap.setAttribute('aria-hidden', 'true');
|
|
87
|
-
}
|
|
88
|
-
return { initialized: false };
|
|
89
|
-
}
|
|
104
|
+
// Parse configuration from new attributes
|
|
105
|
+
const desktopItems = parseInt(elements.controls?.getAttribute('data-hs-pagination-show')) || 6;
|
|
106
|
+
const mobileItems = parseInt(elements.controls?.getAttribute('data-hs-pagination-show-mobile')) || desktopItems;
|
|
90
107
|
|
|
91
108
|
const isMobileLayout = () => {
|
|
92
109
|
const breakpoint = getComputedStyle(list).getPropertyValue('--data-hs-break').trim().replace(/"/g, '');
|
|
@@ -102,7 +119,8 @@ function initPaginationInstance(container, cleanup) {
|
|
|
102
119
|
currentIndex: 1,
|
|
103
120
|
currentPage: 1,
|
|
104
121
|
isAnimating: false,
|
|
105
|
-
itemsPerPage: desktopItems
|
|
122
|
+
itemsPerPage: desktopItems,
|
|
123
|
+
dotTemplates: { active: null, inactive: null }
|
|
106
124
|
};
|
|
107
125
|
let wrapperChildren = [];
|
|
108
126
|
|
|
@@ -133,38 +151,84 @@ function initPaginationInstance(container, cleanup) {
|
|
|
133
151
|
|
|
134
152
|
const updateHeight = () => {
|
|
135
153
|
const targetPage = wrapperChildren[state.currentIndex];
|
|
136
|
-
if (targetPage
|
|
154
|
+
if (targetPage && targetPage.offsetHeight !== undefined) {
|
|
155
|
+
wrapper.style.height = targetPage.offsetHeight + 'px';
|
|
156
|
+
}
|
|
137
157
|
};
|
|
138
158
|
|
|
139
159
|
const initializeDots = () => {
|
|
140
160
|
if (!elements.dotsWrap) return;
|
|
141
161
|
|
|
142
|
-
//
|
|
143
|
-
elements.dotsWrap.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
162
|
+
// Find existing dots as templates
|
|
163
|
+
const existingDots = Array.from(elements.dotsWrap.children);
|
|
164
|
+
if (!existingDots.length) return;
|
|
165
|
+
|
|
166
|
+
// Identify active and inactive templates
|
|
167
|
+
const activeDot = existingDots.find(dot => dot.classList.contains('is-active'));
|
|
168
|
+
const inactiveDot = existingDots.find(dot => !dot.classList.contains('is-active'));
|
|
169
|
+
|
|
170
|
+
// Store templates (use same template for both if only one exists)
|
|
171
|
+
state.dotTemplates.active = activeDot ? activeDot.cloneNode(true) : existingDots[0].cloneNode(true);
|
|
172
|
+
state.dotTemplates.inactive = inactiveDot ? inactiveDot.cloneNode(true) : existingDots[0].cloneNode(true);
|
|
173
|
+
|
|
174
|
+
// Clear existing dots
|
|
175
|
+
elements.dotsWrap.innerHTML = '';
|
|
176
|
+
|
|
177
|
+
// Add pagination accessibility attributes
|
|
178
|
+
elements.dotsWrap.setAttribute('role', 'group');
|
|
179
|
+
elements.dotsWrap.setAttribute('aria-label', 'Page navigation');
|
|
180
|
+
|
|
181
|
+
// Create dots for each page
|
|
182
|
+
for (let i = 1; i <= state.totalPages; i++) {
|
|
183
|
+
const dot = (i === 1 ? state.dotTemplates.active : state.dotTemplates.inactive).cloneNode(true);
|
|
184
|
+
|
|
185
|
+
// Add accessibility attributes
|
|
186
|
+
dot.setAttribute('role', 'button');
|
|
187
|
+
dot.setAttribute('tabindex', '0');
|
|
188
|
+
dot.setAttribute('aria-label', `Go to page ${i}`);
|
|
189
|
+
dot.setAttribute('aria-current', i === 1 ? 'page' : 'false');
|
|
190
|
+
dot.setAttribute('data-page', i);
|
|
191
|
+
|
|
192
|
+
// Set initial active state
|
|
193
|
+
if (i === 1) {
|
|
194
|
+
dot.classList.add('is-active');
|
|
195
|
+
} else {
|
|
196
|
+
dot.classList.remove('is-active');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Click handler
|
|
200
|
+
const clickHandler = () => navigateToPage(i);
|
|
201
|
+
dot.addEventListener('click', clickHandler);
|
|
202
|
+
cleanup.handlers.push({ element: dot, event: 'click', handler: clickHandler });
|
|
203
|
+
|
|
204
|
+
// Keyboard handler
|
|
205
|
+
const keyHandler = (e) => {
|
|
206
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
207
|
+
e.preventDefault();
|
|
208
|
+
navigateToPage(i);
|
|
153
209
|
}
|
|
154
210
|
};
|
|
155
|
-
|
|
156
|
-
cleanup.handlers.push({
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
});
|
|
161
|
-
}, 50);
|
|
211
|
+
dot.addEventListener('keydown', keyHandler);
|
|
212
|
+
cleanup.handlers.push({ element: dot, event: 'keydown', handler: keyHandler });
|
|
213
|
+
|
|
214
|
+
elements.dotsWrap.appendChild(dot);
|
|
215
|
+
}
|
|
162
216
|
};
|
|
163
217
|
|
|
164
|
-
const
|
|
165
|
-
if (!elements.dotsWrap
|
|
166
|
-
|
|
167
|
-
elements.dotsWrap.
|
|
218
|
+
const updateActiveDot = () => {
|
|
219
|
+
if (!elements.dotsWrap) return;
|
|
220
|
+
|
|
221
|
+
const dots = Array.from(elements.dotsWrap.children);
|
|
222
|
+
dots.forEach((dot, index) => {
|
|
223
|
+
const dotPage = index + 1;
|
|
224
|
+
if (dotPage === state.currentPage) {
|
|
225
|
+
dot.classList.add('is-active');
|
|
226
|
+
dot.setAttribute('aria-current', 'page');
|
|
227
|
+
} else {
|
|
228
|
+
dot.classList.remove('is-active');
|
|
229
|
+
dot.setAttribute('aria-current', 'false');
|
|
230
|
+
}
|
|
231
|
+
});
|
|
168
232
|
};
|
|
169
233
|
|
|
170
234
|
const initializePagination = (forceItemsPerPage = null) => {
|
|
@@ -172,6 +236,13 @@ function initPaginationInstance(container, cleanup) {
|
|
|
172
236
|
state.itemsPerPage = forceItemsPerPage || (currentIsMobile ? mobileItems : desktopItems);
|
|
173
237
|
state.totalPages = Math.ceil(totalItems / state.itemsPerPage);
|
|
174
238
|
|
|
239
|
+
// Remove old dot event handlers to prevent memory leaks
|
|
240
|
+
if (elements.dotsWrap) {
|
|
241
|
+
cleanup.handlers = cleanup.handlers.filter(({ element }) => {
|
|
242
|
+
return !elements.dotsWrap.contains(element);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
175
246
|
// Clean up previous page lists
|
|
176
247
|
Array.from(wrapper.children).forEach(child => {
|
|
177
248
|
if (child !== list) wrapper.removeChild(child);
|
|
@@ -238,6 +309,9 @@ function initPaginationInstance(container, cleanup) {
|
|
|
238
309
|
let currentLayoutIsMobile = isMobileLayout();
|
|
239
310
|
|
|
240
311
|
const checkLayoutChange = () => {
|
|
312
|
+
// Prevent resize during animation
|
|
313
|
+
if (state.isAnimating) return;
|
|
314
|
+
|
|
241
315
|
const newIsMobile = isMobileLayout();
|
|
242
316
|
if (newIsMobile !== currentLayoutIsMobile) {
|
|
243
317
|
currentLayoutIsMobile = newIsMobile;
|
|
@@ -263,7 +337,7 @@ function initPaginationInstance(container, cleanup) {
|
|
|
263
337
|
updateCounter();
|
|
264
338
|
announcePageChange();
|
|
265
339
|
updateHeight();
|
|
266
|
-
|
|
340
|
+
updateActiveDot();
|
|
267
341
|
|
|
268
342
|
if (isMobileLayout() && elements.controls) {
|
|
269
343
|
setTimeout(() => {
|
|
@@ -277,7 +351,10 @@ function initPaginationInstance(container, cleanup) {
|
|
|
277
351
|
wrapper.style.transition = 'transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
|
|
278
352
|
wrapper.style.transform = `translateX(${-state.currentIndex * 100}%)`;
|
|
279
353
|
|
|
354
|
+
let transitionTimeout = null;
|
|
355
|
+
|
|
280
356
|
const handleTransitionEnd = () => {
|
|
357
|
+
clearTimeout(transitionTimeout);
|
|
281
358
|
wrapper.removeEventListener('transitionend', handleTransitionEnd);
|
|
282
359
|
wrapper.style.transition = '';
|
|
283
360
|
|
|
@@ -285,10 +362,13 @@ function initPaginationInstance(container, cleanup) {
|
|
|
285
362
|
announcePageChange();
|
|
286
363
|
updateHeight();
|
|
287
364
|
manageFocus();
|
|
288
|
-
|
|
365
|
+
updateActiveDot();
|
|
289
366
|
state.isAnimating = false;
|
|
290
367
|
};
|
|
291
368
|
|
|
369
|
+
// Safety timeout in case transitionend never fires
|
|
370
|
+
transitionTimeout = setTimeout(handleTransitionEnd, 1000);
|
|
371
|
+
|
|
292
372
|
wrapper.addEventListener('transitionend', handleTransitionEnd);
|
|
293
373
|
};
|
|
294
374
|
|
|
@@ -303,7 +383,7 @@ function initPaginationInstance(container, cleanup) {
|
|
|
303
383
|
updateCounter();
|
|
304
384
|
announcePageChange();
|
|
305
385
|
updateHeight();
|
|
306
|
-
|
|
386
|
+
updateActiveDot();
|
|
307
387
|
|
|
308
388
|
if (isMobileLayout() && elements.controls) {
|
|
309
389
|
setTimeout(() => {
|
|
@@ -317,7 +397,10 @@ function initPaginationInstance(container, cleanup) {
|
|
|
317
397
|
wrapper.style.transition = 'transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
|
|
318
398
|
wrapper.style.transform = `translateX(${-state.currentIndex * 100}%)`;
|
|
319
399
|
|
|
400
|
+
let transitionTimeout = null;
|
|
401
|
+
|
|
320
402
|
const handleTransitionEnd = () => {
|
|
403
|
+
clearTimeout(transitionTimeout);
|
|
321
404
|
wrapper.removeEventListener('transitionend', handleTransitionEnd);
|
|
322
405
|
wrapper.style.transition = '';
|
|
323
406
|
|
|
@@ -335,10 +418,13 @@ function initPaginationInstance(container, cleanup) {
|
|
|
335
418
|
announcePageChange();
|
|
336
419
|
updateHeight();
|
|
337
420
|
manageFocus();
|
|
338
|
-
|
|
421
|
+
updateActiveDot();
|
|
339
422
|
state.isAnimating = false;
|
|
340
423
|
};
|
|
341
424
|
|
|
425
|
+
// Safety timeout in case transitionend never fires
|
|
426
|
+
transitionTimeout = setTimeout(handleTransitionEnd, 1000);
|
|
427
|
+
|
|
342
428
|
wrapper.addEventListener('transitionend', handleTransitionEnd);
|
|
343
429
|
};
|
|
344
430
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## **Overview**
|
|
4
4
|
|
|
5
|
-
The site settings system collects dynamic values from designated elements and replaces placeholder text (`{{name}}`) throughout the entire page.
|
|
5
|
+
The site settings system collects dynamic values from designated elements and replaces placeholder text (`{{name}}`) throughout the entire page. Supports both text and href values, allowing for proper handling of links like `tel:`, `mailto:`, and URLs without Webflow's default prefixes breaking them.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -22,7 +22,8 @@ The site settings system collects dynamic values from designated elements and re
|
|
|
22
22
|
|
|
23
23
|
* data-site-settings="name"
|
|
24
24
|
* The attribute value is the placeholder name (e.g., "company", "email", "phone")
|
|
25
|
-
* The element's text content is
|
|
25
|
+
* The element's text content is used for text replacements
|
|
26
|
+
* The element's href attribute (if present) is used for link replacements
|
|
26
27
|
|
|
27
28
|
**Typical element layout:**
|
|
28
29
|
|
|
@@ -30,17 +31,29 @@ The site settings system collects dynamic values from designated elements and re
|
|
|
30
31
|
1. Settings List (data-site-settings-element="list")
|
|
31
32
|
1. Setting Element (data-site-settings="company")
|
|
32
33
|
1. Text: "Compass Facilities"
|
|
33
|
-
2. Setting
|
|
34
|
+
2. Link Setting (data-site-settings="email", href="mailto:info@compass.com")
|
|
34
35
|
1. Text: "info@compass.com"
|
|
35
|
-
3. Setting
|
|
36
|
-
1. Text: "555-
|
|
36
|
+
3. Link Setting (data-site-settings="phone", href="tel:+15551234567")
|
|
37
|
+
1. Text: "(555) 123-4567"
|
|
37
38
|
|
|
38
39
|
### **What It Does**
|
|
39
40
|
|
|
40
|
-
1. **Collects settings** - Finds all elements with `[data-site-settings]` and stores
|
|
41
|
-
2. **
|
|
42
|
-
3. **Replaces text placeholders** - Finds all `{{name}}` in page text and replaces with
|
|
43
|
-
4. **Replaces link
|
|
41
|
+
1. **Collects settings** - Finds all elements with `[data-site-settings]` and stores both text and href values
|
|
42
|
+
2. **Separates text and href** - Distinguishes between display text and link URLs
|
|
43
|
+
3. **Replaces text placeholders** - Finds all `{{name}}` in page text and replaces with text value (or href as fallback)
|
|
44
|
+
4. **Replaces link hrefs** - Finds all `{{name}}` in link hrefs:
|
|
45
|
+
- **With dedicated href:** Completely replaces entire href attribute (prevents Webflow's "https://" prefix)
|
|
46
|
+
- **Without dedicated href:** Does normal placeholder replacement using text value
|
|
47
|
+
|
|
48
|
+
### **Text vs Href Behavior**
|
|
49
|
+
|
|
50
|
+
**Text replacement** (in page content):
|
|
51
|
+
- **First choice:** Use text value
|
|
52
|
+
- **Fallback:** Use href value if no text
|
|
53
|
+
|
|
54
|
+
**Href replacement** (in link attributes):
|
|
55
|
+
- **With dedicated href:** Completely replace entire href (fixes Webflow prefix issue)
|
|
56
|
+
- **Without dedicated href:** Normal placeholder replacement with text value
|
|
44
57
|
|
|
45
58
|
### **When It Runs**
|
|
46
59
|
|
|
@@ -88,7 +101,7 @@ For dynamic content where certain parts should be hidden based on CMS conditions
|
|
|
88
101
|
2. Finds all descendants with `[data-site-settings-hide]`
|
|
89
102
|
3. Checks if element has any matching classes
|
|
90
103
|
4. Removes matching elements from clone
|
|
91
|
-
5. Extracts text from cleaned clone
|
|
104
|
+
5. Extracts text and href from cleaned clone
|
|
92
105
|
|
|
93
106
|
---
|
|
94
107
|
|
|
@@ -96,19 +109,38 @@ For dynamic content where certain parts should be hidden based on CMS conditions
|
|
|
96
109
|
|
|
97
110
|
### **Define Settings**
|
|
98
111
|
|
|
112
|
+
#### **Basic Text Settings**
|
|
113
|
+
|
|
99
114
|
```html
|
|
100
115
|
<div data-site-settings-element="wrapper" style="display: none;">
|
|
101
116
|
<div data-site-settings-element="list">
|
|
102
117
|
<div data-site-settings="company">Compass Facilities</div>
|
|
103
|
-
<div data-site-settings="email">info@compass.com</div>
|
|
104
|
-
<div data-site-settings="phone">555-1234</div>
|
|
105
118
|
<div data-site-settings="address">123 Main St, Dallas, TX 75001</div>
|
|
106
119
|
</div>
|
|
107
120
|
</div>
|
|
108
121
|
```
|
|
109
122
|
|
|
123
|
+
#### **Link Settings with Href**
|
|
124
|
+
|
|
125
|
+
```html
|
|
126
|
+
<div data-site-settings-element="wrapper" style="display: none;">
|
|
127
|
+
<div data-site-settings-element="list">
|
|
128
|
+
<!-- Email with both display text and mailto: link -->
|
|
129
|
+
<a data-site-settings="email" href="mailto:info@compass.com">info@compass.com</a>
|
|
130
|
+
|
|
131
|
+
<!-- Phone with formatted display and tel: link -->
|
|
132
|
+
<a data-site-settings="phone" href="tel:+15551234567">(555) 123-4567</a>
|
|
133
|
+
|
|
134
|
+
<!-- Social link -->
|
|
135
|
+
<a data-site-settings="facebook" href="https://facebook.com/compassfacilities">Facebook</a>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
```
|
|
139
|
+
|
|
110
140
|
**Note:** The wrapper is typically hidden with `display: none` since it's only for data storage.
|
|
111
141
|
|
|
142
|
+
---
|
|
143
|
+
|
|
112
144
|
### **Use Settings in Text**
|
|
113
145
|
|
|
114
146
|
```html
|
|
@@ -120,23 +152,52 @@ For dynamic content where certain parts should be hidden based on CMS conditions
|
|
|
120
152
|
**Result:**
|
|
121
153
|
```html
|
|
122
154
|
<h1>Welcome to Compass Facilities</h1>
|
|
123
|
-
<p>Contact us at info@compass.com or call 555-
|
|
155
|
+
<p>Contact us at info@compass.com or call (555) 123-4567</p>
|
|
124
156
|
<p>Visit us at 123 Main St, Dallas, TX 75001</p>
|
|
125
157
|
```
|
|
126
158
|
|
|
127
|
-
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
### **Use Settings in Links (With Dedicated Href)**
|
|
162
|
+
|
|
163
|
+
**IMPORTANT:** When setting has a dedicated href, the ENTIRE href attribute is replaced (not just the placeholder). This prevents Webflow's default "https://" prefix from breaking tel:, mailto:, and other special link types.
|
|
128
164
|
|
|
129
165
|
```html
|
|
130
|
-
|
|
131
|
-
<a href="
|
|
132
|
-
<a href="
|
|
166
|
+
<!-- In Webflow, these links might have href="https://{{email}}" -->
|
|
167
|
+
<a href="{{email}}">Email Us</a>
|
|
168
|
+
<a href="{{phone}}">Call Us</a>
|
|
169
|
+
<a href="{{facebook}}">Follow Us</a>
|
|
133
170
|
```
|
|
134
171
|
|
|
135
172
|
**Result:**
|
|
136
173
|
```html
|
|
174
|
+
<!-- Entire href replaced with dedicated href value -->
|
|
137
175
|
<a href="mailto:info@compass.com">Email Us</a>
|
|
138
|
-
<a href="tel
|
|
139
|
-
<a href="https://
|
|
176
|
+
<a href="tel:+15551234567">Call Us</a>
|
|
177
|
+
<a href="https://facebook.com/compassfacilities">Follow Us</a>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Why this matters:** Webflow often adds `https://` prefix by default. Without complete href replacement:
|
|
181
|
+
- `https://tel:+15551234567` ❌ (broken)
|
|
182
|
+
- `tel:+15551234567` ✅ (works)
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
### **Use Settings in Links (Without Dedicated Href)**
|
|
187
|
+
|
|
188
|
+
When setting has only text (no href), normal placeholder replacement occurs:
|
|
189
|
+
|
|
190
|
+
```html
|
|
191
|
+
<!-- Setting with only text -->
|
|
192
|
+
<div data-site-settings="company-url">compassfacilities.com</div>
|
|
193
|
+
|
|
194
|
+
<!-- Link in page -->
|
|
195
|
+
<a href="https://{{company-url}}">Visit Site</a>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Result:**
|
|
199
|
+
```html
|
|
200
|
+
<a href="https://compassfacilities.com">Visit Site</a>
|
|
140
201
|
```
|
|
141
202
|
|
|
142
203
|
---
|
|
@@ -146,11 +207,11 @@ For dynamic content where certain parts should be hidden based on CMS conditions
|
|
|
146
207
|
You can have multiple lists inside the wrapper for organization:
|
|
147
208
|
|
|
148
209
|
```html
|
|
149
|
-
<div data-site-settings-element="wrapper">
|
|
210
|
+
<div data-site-settings-element="wrapper" style="display: none;">
|
|
150
211
|
<!-- Contact info -->
|
|
151
212
|
<div data-site-settings-element="list">
|
|
152
|
-
<
|
|
153
|
-
<
|
|
213
|
+
<a data-site-settings="email" href="mailto:info@compass.com">info@compass.com</a>
|
|
214
|
+
<a data-site-settings="phone" href="tel:+15551234567">(555) 123-4567</a>
|
|
154
215
|
</div>
|
|
155
216
|
|
|
156
217
|
<!-- Company info -->
|
|
@@ -158,6 +219,12 @@ You can have multiple lists inside the wrapper for organization:
|
|
|
158
219
|
<div data-site-settings="company">Compass Facilities</div>
|
|
159
220
|
<div data-site-settings="established">2020</div>
|
|
160
221
|
</div>
|
|
222
|
+
|
|
223
|
+
<!-- Social links -->
|
|
224
|
+
<div data-site-settings-element="list">
|
|
225
|
+
<a data-site-settings="facebook" href="https://facebook.com/compass">Facebook</a>
|
|
226
|
+
<a data-site-settings="twitter" href="https://twitter.com/compass">Twitter</a>
|
|
227
|
+
</div>
|
|
161
228
|
</div>
|
|
162
229
|
```
|
|
163
230
|
|
|
@@ -172,6 +239,7 @@ All settings are collected regardless of which list they're in.
|
|
|
172
239
|
| `data-site-settings-element="wrapper"` | Settings container | Wrapper div |
|
|
173
240
|
| `data-site-settings-element="list"` | Settings list | List div (at least one) |
|
|
174
241
|
| `data-site-settings="name"` | Setting with name | Individual setting elements |
|
|
242
|
+
| `href` attribute | Link URL for href replacement | Optional (on link settings) |
|
|
175
243
|
| `data-site-settings-hide="class"` | Conditional hide | Optional (inside setting elements) |
|
|
176
244
|
|
|
177
245
|
---
|
|
@@ -187,13 +255,74 @@ All settings are collected regardless of which list they're in.
|
|
|
187
255
|
|
|
188
256
|
---
|
|
189
257
|
|
|
258
|
+
## **Replacement Logic**
|
|
259
|
+
|
|
260
|
+
### **For Text Content (`{{name}}` in page text):**
|
|
261
|
+
1. If setting has text value → use text
|
|
262
|
+
2. If no text but has href value → use href
|
|
263
|
+
3. If neither → skip replacement
|
|
264
|
+
|
|
265
|
+
### **For Link Hrefs (`{{name}}` in href attribute):**
|
|
266
|
+
1. If setting has href value → **completely replace entire href**
|
|
267
|
+
2. If no href but has text value → normal placeholder replacement
|
|
268
|
+
3. If neither → skip replacement
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## **Common Use Cases**
|
|
273
|
+
|
|
274
|
+
### **Phone Number**
|
|
275
|
+
```html
|
|
276
|
+
<!-- Setting with both formatted display and tel: link -->
|
|
277
|
+
<a data-site-settings="phone" href="tel:+15551234567">(555) 123-4567</a>
|
|
278
|
+
|
|
279
|
+
<!-- Text usage -->
|
|
280
|
+
<p>Call us at {{phone}}</p>
|
|
281
|
+
<!-- Result: Call us at (555) 123-4567 -->
|
|
282
|
+
|
|
283
|
+
<!-- Link usage -->
|
|
284
|
+
<a href="{{phone}}">Call Now</a>
|
|
285
|
+
<!-- Result: <a href="tel:+15551234567">Call Now</a> -->
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### **Email Address**
|
|
289
|
+
```html
|
|
290
|
+
<!-- Setting -->
|
|
291
|
+
<a data-site-settings="email" href="mailto:contact@example.com">contact@example.com</a>
|
|
292
|
+
|
|
293
|
+
<!-- Text usage -->
|
|
294
|
+
<p>Email: {{email}}</p>
|
|
295
|
+
<!-- Result: Email: contact@example.com -->
|
|
296
|
+
|
|
297
|
+
<!-- Link usage -->
|
|
298
|
+
<a href="{{email}}">Email Us</a>
|
|
299
|
+
<!-- Result: <a href="mailto:contact@example.com">Email Us</a> -->
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### **Social Media**
|
|
303
|
+
```html
|
|
304
|
+
<!-- Setting -->
|
|
305
|
+
<a data-site-settings="facebook" href="https://facebook.com/mypage">@mypage</a>
|
|
306
|
+
|
|
307
|
+
<!-- Text usage -->
|
|
308
|
+
<p>Follow us: {{facebook}}</p>
|
|
309
|
+
<!-- Result: Follow us: @mypage -->
|
|
310
|
+
|
|
311
|
+
<!-- Link usage -->
|
|
312
|
+
<a href="{{facebook}}">Facebook</a>
|
|
313
|
+
<!-- Result: <a href="https://facebook.com/mypage">Facebook</a> -->
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
190
318
|
## **Notes**
|
|
191
319
|
|
|
192
|
-
1. **Empty elements ignored** - Elements without text
|
|
320
|
+
1. **Empty elements ignored** - Elements without text or href are skipped
|
|
193
321
|
2. **No visual component** - Settings wrapper should be hidden with CSS
|
|
194
322
|
3. **Page-wide replacement** - Placeholders are replaced throughout entire page
|
|
195
323
|
4. **Multiple replacements** - Same placeholder can be used unlimited times
|
|
196
324
|
5. **Barba.js compatible** - Automatically reinitializes on page transitions
|
|
325
|
+
6. **Webflow-friendly** - Complete href replacement prevents default "https://" prefix issues
|
|
197
326
|
|
|
198
327
|
---
|
|
199
328
|
|
|
@@ -202,17 +331,35 @@ All settings are collected regardless of which list they're in.
|
|
|
202
331
|
**Placeholder not replacing:**
|
|
203
332
|
|
|
204
333
|
1. Verify `data-site-settings` attribute matches placeholder name exactly
|
|
205
|
-
2. Check that setting element has text content
|
|
334
|
+
2. Check that setting element has text content or href attribute
|
|
206
335
|
3. Ensure double curly braces: `{{name}}` not `{name}` or `{{ name }}`
|
|
207
336
|
|
|
208
337
|
**Wrong value appearing:**
|
|
209
338
|
|
|
210
339
|
1. Check for duplicate setting names (last one wins)
|
|
211
340
|
2. Verify conditional hide elements are working correctly
|
|
212
|
-
3. Inspect the settings wrapper to confirm expected text
|
|
341
|
+
3. Inspect the settings wrapper to confirm expected text and href
|
|
342
|
+
|
|
343
|
+
**Links not working (tel:, mailto:, etc.):**
|
|
344
|
+
|
|
345
|
+
1. Ensure setting element has `href` attribute with proper protocol
|
|
346
|
+
2. Use `<a>` element for settings that need href replacement
|
|
347
|
+
3. Verify placeholder is directly in href: `href="{{phone}}"` not `href="tel:{{phone}}"`
|
|
348
|
+
4. The system will completely replace the href when setting has dedicated href value
|
|
349
|
+
|
|
350
|
+
**Webflow adding "https://" prefix:**
|
|
351
|
+
|
|
352
|
+
1. This is expected Webflow behavior
|
|
353
|
+
2. System automatically fixes this when setting has dedicated href
|
|
354
|
+
3. Entire href is replaced, removing Webflow's prefix
|
|
355
|
+
4. Example: `https://{{phone}}` → `tel:+15551234567` ✓
|
|
356
|
+
|
|
357
|
+
---
|
|
213
358
|
|
|
214
|
-
**
|
|
359
|
+
## **Best Practices**
|
|
215
360
|
|
|
216
|
-
1.
|
|
217
|
-
2.
|
|
218
|
-
3.
|
|
361
|
+
1. **Use links for contact info** - Phone, email, social media should be `<a>` elements with href
|
|
362
|
+
2. **Format display text** - Use human-readable text, proper href for functionality
|
|
363
|
+
3. **Hide the wrapper** - Always use `display: none` or similar to hide settings container
|
|
364
|
+
4. **Organize with lists** - Group related settings in separate lists for clarity
|
|
365
|
+
5. **Test replacements** - Check both text and link replacements work as expected
|
|
@@ -47,11 +47,15 @@ export function init() {
|
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
const settingName = element.getAttribute('data-site-settings');
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if
|
|
54
|
-
|
|
50
|
+
const settingText = clonedElement.textContent.trim();
|
|
51
|
+
const settingHref = element.getAttribute('href');
|
|
52
|
+
|
|
53
|
+
// Store if name exists and either text or href exists
|
|
54
|
+
if (settingName && (settingText || settingHref)) {
|
|
55
|
+
siteSettings[settingName] = {
|
|
56
|
+
text: settingText || null,
|
|
57
|
+
href: settingHref || null
|
|
58
|
+
};
|
|
55
59
|
}
|
|
56
60
|
});
|
|
57
61
|
});
|
|
@@ -93,8 +97,13 @@ export function init() {
|
|
|
93
97
|
for (const name in siteSettings) {
|
|
94
98
|
const placeholder = `{{${name}}}`;
|
|
95
99
|
if (newText.includes(placeholder)) {
|
|
96
|
-
|
|
97
|
-
|
|
100
|
+
const setting = siteSettings[name];
|
|
101
|
+
// Use text value if available, otherwise use href value, otherwise skip
|
|
102
|
+
const replacementValue = setting.text || setting.href;
|
|
103
|
+
if (replacementValue) {
|
|
104
|
+
newText = newText.replace(new RegExp(placeholder, 'g'), replacementValue);
|
|
105
|
+
hasChanges = true;
|
|
106
|
+
}
|
|
98
107
|
}
|
|
99
108
|
}
|
|
100
109
|
|
|
@@ -112,8 +121,19 @@ export function init() {
|
|
|
112
121
|
for (const name in siteSettings) {
|
|
113
122
|
const placeholder = `{{${name}}}`;
|
|
114
123
|
if (href.includes(placeholder)) {
|
|
115
|
-
|
|
116
|
-
|
|
124
|
+
const setting = siteSettings[name];
|
|
125
|
+
|
|
126
|
+
// If setting has a dedicated href, completely replace the entire href
|
|
127
|
+
// (prevents Webflow's default "https://" from breaking tel:, mailto:, etc.)
|
|
128
|
+
if (setting.href) {
|
|
129
|
+
href = setting.href;
|
|
130
|
+
hasChanges = true;
|
|
131
|
+
}
|
|
132
|
+
// Otherwise, do normal placeholder replacement with text value
|
|
133
|
+
else if (setting.text) {
|
|
134
|
+
href = href.replace(new RegExp(placeholder, 'g'), setting.text);
|
|
135
|
+
hasChanges = true;
|
|
136
|
+
}
|
|
117
137
|
}
|
|
118
138
|
}
|
|
119
139
|
|