@hortonstudio/main 1.2.35 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +22 -1
- package/TEMP-before-after-attributes.md +158 -0
- package/animations/hero.js +741 -611
- package/animations/text.js +505 -317
- package/animations/transition.js +36 -21
- package/autoInit/accessibility.js +7 -67
- package/autoInit/counter.js +338 -0
- package/autoInit/custom-values.js +266 -0
- package/autoInit/form.js +471 -0
- package/autoInit/modal.js +43 -38
- package/autoInit/navbar.js +484 -371
- package/autoInit/smooth-scroll.js +86 -84
- package/index.js +140 -88
- package/package.json +1 -1
- package/utils/before-after.js +279 -146
- package/utils/scroll-progress.js +26 -21
- package/utils/toc.js +73 -66
- package/CLAUDE.md +0 -45
- package/debug-version.html +0 -37
@@ -0,0 +1,266 @@
|
|
1
|
+
export function init() {
|
2
|
+
const customValues = new Map();
|
3
|
+
let isInitialized = false;
|
4
|
+
|
5
|
+
// Configuration for performance optimization
|
6
|
+
const config = {
|
7
|
+
// Attributes to search for placeholders
|
8
|
+
searchAttributes: [
|
9
|
+
'href', 'src', 'alt', 'title', 'aria-label', 'data-src',
|
10
|
+
'data-href', 'action', 'placeholder', 'value'
|
11
|
+
],
|
12
|
+
// Elements to exclude from search for performance
|
13
|
+
excludeSelectors: [
|
14
|
+
'script', 'style', 'meta', 'link', 'title', 'head',
|
15
|
+
'[data-hs-custom="list"]', '[data-hs-custom="name"]', '[data-hs-custom="value"]'
|
16
|
+
],
|
17
|
+
// Phone number formatting options
|
18
|
+
phoneFormatting: {
|
19
|
+
// Attributes that should use tel: format
|
20
|
+
telAttributes: ['href'],
|
21
|
+
// Pattern to detect phone numbers (matches various formats)
|
22
|
+
phonePattern: /^[\+]?[1-9]?[\d\s\-\(\)\.]{7,15}$/,
|
23
|
+
// Default country code (US/Canada)
|
24
|
+
defaultCountryCode: '+1',
|
25
|
+
// Clean phone for tel: links (remove all non-digits except +)
|
26
|
+
cleanForTel: (phone) => {
|
27
|
+
const cleaned = phone.replace(/[^\d+]/g, '');
|
28
|
+
// If no country code, add default
|
29
|
+
if (!cleaned.startsWith('+')) {
|
30
|
+
return config.phoneFormatting.defaultCountryCode + cleaned;
|
31
|
+
}
|
32
|
+
return cleaned;
|
33
|
+
},
|
34
|
+
// Format for display (keep original formatting)
|
35
|
+
formatForDisplay: (phone) => phone
|
36
|
+
}
|
37
|
+
};
|
38
|
+
|
39
|
+
// Detect if a value looks like a phone number
|
40
|
+
function isPhoneNumber(value) {
|
41
|
+
return config.phoneFormatting.phonePattern.test(value.trim());
|
42
|
+
}
|
43
|
+
|
44
|
+
// Format value based on context (attribute vs text content)
|
45
|
+
function formatValueForContext(value, isAttribute, attributeName) {
|
46
|
+
if (isPhoneNumber(value)) {
|
47
|
+
// For href attributes, clean the phone number (no tel: prefix)
|
48
|
+
if (isAttribute && config.phoneFormatting.telAttributes.includes(attributeName)) {
|
49
|
+
return config.phoneFormatting.cleanForTel(value);
|
50
|
+
} else {
|
51
|
+
// For display, keep original formatting
|
52
|
+
return config.phoneFormatting.formatForDisplay(value);
|
53
|
+
}
|
54
|
+
}
|
55
|
+
return value;
|
56
|
+
}
|
57
|
+
|
58
|
+
// Extract custom values from data attributes
|
59
|
+
function extractCustomValues() {
|
60
|
+
const customList = document.querySelector('[data-hs-custom="list"]');
|
61
|
+
if (!customList) {
|
62
|
+
return false;
|
63
|
+
}
|
64
|
+
|
65
|
+
const nameElements = customList.querySelectorAll('[data-hs-custom="name"]');
|
66
|
+
const valueElements = customList.querySelectorAll('[data-hs-custom="value"]');
|
67
|
+
|
68
|
+
// Build mapping from name/value pairs
|
69
|
+
for (let i = 0; i < Math.min(nameElements.length, valueElements.length); i++) {
|
70
|
+
const name = nameElements[i].textContent.trim();
|
71
|
+
const value = valueElements[i].textContent.trim();
|
72
|
+
|
73
|
+
if (name && value) {
|
74
|
+
// Store with lowercase key for case-insensitive matching
|
75
|
+
const key = `{{${name.toLowerCase()}}}`;
|
76
|
+
customValues.set(key, value);
|
77
|
+
}
|
78
|
+
}
|
79
|
+
|
80
|
+
return customValues.size > 0;
|
81
|
+
}
|
82
|
+
|
83
|
+
// Replace placeholders in text content
|
84
|
+
function replaceInText(text, isAttribute = false, attributeName = null) {
|
85
|
+
if (!text || typeof text !== 'string') return text;
|
86
|
+
|
87
|
+
let result = text;
|
88
|
+
|
89
|
+
customValues.forEach((value, placeholder) => {
|
90
|
+
// Create case-insensitive regex for exact placeholder match
|
91
|
+
const regex = new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
|
92
|
+
const matches = text.match(regex);
|
93
|
+
if (matches) {
|
94
|
+
// Format value based on context (phone numbers get special treatment)
|
95
|
+
const formattedValue = formatValueForContext(value, isAttribute, attributeName);
|
96
|
+
result = result.replace(regex, formattedValue);
|
97
|
+
}
|
98
|
+
});
|
99
|
+
|
100
|
+
return result;
|
101
|
+
}
|
102
|
+
|
103
|
+
// Replace placeholders in all attributes of an element
|
104
|
+
function replaceInAttributes(element) {
|
105
|
+
config.searchAttributes.forEach(attr => {
|
106
|
+
const value = element.getAttribute(attr);
|
107
|
+
if (value) {
|
108
|
+
const newValue = replaceInText(value, true, attr);
|
109
|
+
if (newValue !== value) {
|
110
|
+
element.setAttribute(attr, newValue);
|
111
|
+
}
|
112
|
+
}
|
113
|
+
});
|
114
|
+
}
|
115
|
+
|
116
|
+
// Check if element should be excluded from processing
|
117
|
+
function shouldExcludeElement(element) {
|
118
|
+
return config.excludeSelectors.some(selector => {
|
119
|
+
return element.matches(selector);
|
120
|
+
});
|
121
|
+
}
|
122
|
+
|
123
|
+
// Process text nodes for placeholder replacement
|
124
|
+
function processTextNodes(element) {
|
125
|
+
const walker = document.createTreeWalker(
|
126
|
+
element,
|
127
|
+
NodeFilter.SHOW_TEXT,
|
128
|
+
{
|
129
|
+
acceptNode: (node) => {
|
130
|
+
// Skip if parent element should be excluded
|
131
|
+
if (shouldExcludeElement(node.parentElement)) {
|
132
|
+
return NodeFilter.FILTER_REJECT;
|
133
|
+
}
|
134
|
+
|
135
|
+
// Only process text nodes with placeholder patterns
|
136
|
+
return node.textContent.includes('{{') && node.textContent.includes('}}')
|
137
|
+
? NodeFilter.FILTER_ACCEPT
|
138
|
+
: NodeFilter.FILTER_SKIP;
|
139
|
+
}
|
140
|
+
}
|
141
|
+
);
|
142
|
+
|
143
|
+
const textNodes = [];
|
144
|
+
let node;
|
145
|
+
while (node = walker.nextNode()) {
|
146
|
+
textNodes.push(node);
|
147
|
+
}
|
148
|
+
|
149
|
+
// Replace placeholders in collected text nodes
|
150
|
+
textNodes.forEach(textNode => {
|
151
|
+
const originalText = textNode.textContent;
|
152
|
+
const newText = replaceInText(originalText);
|
153
|
+
if (newText !== originalText) {
|
154
|
+
textNode.textContent = newText;
|
155
|
+
}
|
156
|
+
});
|
157
|
+
}
|
158
|
+
|
159
|
+
// Process all elements for attribute replacement
|
160
|
+
function processElements(container) {
|
161
|
+
const elements = container.querySelectorAll('*');
|
162
|
+
|
163
|
+
elements.forEach(element => {
|
164
|
+
if (!shouldExcludeElement(element)) {
|
165
|
+
replaceInAttributes(element);
|
166
|
+
}
|
167
|
+
});
|
168
|
+
}
|
169
|
+
|
170
|
+
// Main replacement function
|
171
|
+
function performReplacements() {
|
172
|
+
if (customValues.size === 0) return;
|
173
|
+
|
174
|
+
// Process text content
|
175
|
+
processTextNodes(document.body);
|
176
|
+
|
177
|
+
// Process element attributes
|
178
|
+
processElements(document.body);
|
179
|
+
|
180
|
+
// Also check document root attributes
|
181
|
+
replaceInAttributes(document.documentElement);
|
182
|
+
}
|
183
|
+
|
184
|
+
// Remove the custom values list from DOM
|
185
|
+
function cleanupCustomList() {
|
186
|
+
const customList = document.querySelector('[data-hs-custom="list"]');
|
187
|
+
if (customList) {
|
188
|
+
customList.remove();
|
189
|
+
}
|
190
|
+
}
|
191
|
+
|
192
|
+
// Handle dynamic content with MutationObserver
|
193
|
+
function setupDynamicContentHandler() {
|
194
|
+
const observer = new MutationObserver((mutations) => {
|
195
|
+
let hasNewContent = false;
|
196
|
+
|
197
|
+
mutations.forEach((mutation) => {
|
198
|
+
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
|
199
|
+
mutation.addedNodes.forEach((node) => {
|
200
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
201
|
+
// Check if new content contains placeholders
|
202
|
+
const hasPlaceholders = node.textContent.includes('{{') && node.textContent.includes('}}');
|
203
|
+
const hasAttributePlaceholders = config.searchAttributes.some(attr => {
|
204
|
+
const value = node.getAttribute?.(attr);
|
205
|
+
return value && value.includes('{{') && value.includes('}}');
|
206
|
+
});
|
207
|
+
|
208
|
+
if (hasPlaceholders || hasAttributePlaceholders) {
|
209
|
+
hasNewContent = true;
|
210
|
+
}
|
211
|
+
}
|
212
|
+
});
|
213
|
+
}
|
214
|
+
});
|
215
|
+
|
216
|
+
if (hasNewContent && customValues.size > 0) {
|
217
|
+
// Debounce replacements for performance
|
218
|
+
clearTimeout(observer.timeout);
|
219
|
+
observer.timeout = setTimeout(() => {
|
220
|
+
performReplacements();
|
221
|
+
}, 100);
|
222
|
+
}
|
223
|
+
});
|
224
|
+
|
225
|
+
observer.observe(document.body, {
|
226
|
+
childList: true,
|
227
|
+
subtree: true
|
228
|
+
});
|
229
|
+
|
230
|
+
return observer;
|
231
|
+
}
|
232
|
+
|
233
|
+
// Initialize the custom values system
|
234
|
+
function initializeCustomValues() {
|
235
|
+
if (isInitialized) return;
|
236
|
+
|
237
|
+
// Extract custom values from data attributes
|
238
|
+
const hasCustomValues = extractCustomValues();
|
239
|
+
|
240
|
+
if (hasCustomValues) {
|
241
|
+
// Perform initial replacements
|
242
|
+
performReplacements();
|
243
|
+
|
244
|
+
// Clean up the custom list
|
245
|
+
cleanupCustomList();
|
246
|
+
|
247
|
+
// Set up dynamic content handling
|
248
|
+
setupDynamicContentHandler();
|
249
|
+
|
250
|
+
isInitialized = true;
|
251
|
+
|
252
|
+
return {
|
253
|
+
result: `custom-values initialized with ${customValues.size} replacements`,
|
254
|
+
count: customValues.size
|
255
|
+
};
|
256
|
+
} else {
|
257
|
+
return {
|
258
|
+
result: 'custom-values initialized (no custom values found)',
|
259
|
+
count: 0
|
260
|
+
};
|
261
|
+
}
|
262
|
+
}
|
263
|
+
|
264
|
+
// Initialize on page load
|
265
|
+
return initializeCustomValues();
|
266
|
+
}
|