@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.
@@ -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
+ }