-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathutil.dom.mjs
More file actions
257 lines (230 loc) · 7.82 KB
/
util.dom.mjs
File metadata and controls
257 lines (230 loc) · 7.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
/**
* @fileoverview DOM Utilities - Pure helper functions for DOM manipulation
* @module util.dom
* @version 3.0.0
* @author hnldesign
* @since 2022
*
* @description
* Provides lightweight DOM manipulation utilities with no side effects.
* Pure functions for element creation, script path resolution, and CSS loading.
*/
import {logger} from './core.log.mjs';
export const NAME = 'dom';
// ============================================================================
// CONSTANTS
// ============================================================================
/** @private Default timeout for node waiting (30 seconds) */
const DEFAULT_WAIT_TIMEOUT = 30000;
/** @private Default polling interval for node waiting (100ms) */
const DEFAULT_WAIT_INTERVAL = 100;
/** @private Regex for extracting URLs from error stack traces (legacy fallback) */
const URL_EXTRACTION_REGEX = /\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»""'']))/ig;
// ============================================================================
// PUBLIC API
// ============================================================================
/**
* Waits for a complex DOM node to appear, supporting shadow DOM traversal.
*
* Continuously polls using provided getter function until node is found or timeout.
* Useful for elements rendered asynchronously or within shadow roots.
*
* Returns cleanup function to cancel waiting (prevents memory leaks in SPAs).
*
* @param {Function} getNode - Function returning target node or null
* Example: () => document.querySelector('host')?.shadowRoot.querySelector('child')
* @param {Function} callback - Called with found node as argument
* @param {number} [timeout=30000] - Maximum wait time in milliseconds
* @param {number} [interval=100] - Polling interval in milliseconds
* @returns {Function} Cleanup function to cancel waiting
* @throws {TypeError} If getNode or callback are not functions
*
* @example
* // Simple element
* const cleanup = waitForComplexNode(
* () => document.querySelector('.my-element'),
* node => console.log('Found:', node)
* );
*
* @example
* // Shadow DOM element
* waitForComplexNode(
* () => document.querySelector('my-component')
* ?.shadowRoot
* ?.querySelector('.inner-element'),
* node => initFeature(node)
* );
*
* @example
* // With cleanup in SPA
* const cleanup = waitForComplexNode(getNode, callback);
* // Later, on route change:
* cleanup();
*/
export function waitForComplexNode(getNode, callback, timeout = DEFAULT_WAIT_TIMEOUT, interval = DEFAULT_WAIT_INTERVAL) {
if (typeof getNode !== 'function') {
throw new TypeError('getNode must be a function');
}
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function');
}
const startTime = Date.now();
let timerId = null;
let cancelled = false;
(function checkNode() {
if (cancelled) return;
let node = null;
try {
node = getNode();
} catch (error) {
// Shadow DOM not ready yet, treat as not found
node = null;
}
if (node) {
callback(node);
return;
}
if (Date.now() - startTime < timeout) {
timerId = setTimeout(checkNode, interval);
} else {
logger.warn(NAME, `Node not found within ${timeout / 1000} seconds`);
}
})();
// Return cleanup function
return () => {
cancelled = true;
if (timerId !== null) {
clearTimeout(timerId);
timerId = null;
}
};
}
/**
* Converts HTML string to DOM Node or NodeList.
*
* Uses <template> element for efficient parsing without triggering resource loads
* or script execution. Template content is garbage collected after return.
*
* @param {string} string - HTML string to parse
* @returns {Element|HTMLCollection} Single element or collection of elements
* @throws {TypeError} If string is not a string type
*
* @example
* // Single element
* const div = parseHTML('<div class="box">Content</div>');
* document.body.appendChild(div);
*
* @example
* // Multiple elements
* const nodes = parseHTML('<div>First</div><span>Second</span>');
* nodes.forEach(node => document.body.appendChild(node));
*
* @example
* // Complex structure
* const card = parseHTML(`
* <article class="card">
* <h2>Title</h2>
* <p>Description</p>
* </article>
* `);
*/
export function parseHTML(string) {
if (typeof string !== 'string') {
throw new TypeError('Input must be a string');
}
// <template> uses documentFragment internally - memory efficient,
// doesn't trigger resource loads or script execution
const template = document.createElement('template');
template.innerHTML = string.trim();
const content = template.content;
return content.childElementCount === 1
? content.firstElementChild
: content.children;
}
/** @deprecated Use parseHTML() instead. Removed in v4.0 */
export function stringToObj(string) {
logger.warn(NAME, 'stringToObj() is deprecated, use parseHTML() instead');
return parseHTML(string);
}
/**
* Resolves path of current script file.
*
* Modern browsers: Uses import.meta.url (ES6 modules)
* Legacy browsers: Extracts from error stack trace
*
* Browser support:
* - Modern: Chrome 64+, Safari 11.1+, Firefox 62+ (ES6 module baseline)
* - Legacy: Chrome 10+, Safari 6+, Firefox 4+ (Error.stack support)
*
* @returns {string} Path to current script directory (no trailing slash)
*
* @example
* // In module at /assets/js/modules/mymodule.mjs
* const path = getScriptPath();
* // Returns: '/assets/js/modules'
*
* @example
* // Load sibling resource
* const modulePath = getScriptPath();
* const cssPath = `${modulePath}/styles.css`;
*/
export function getScriptPath() {
// Modern: ES6 module with import.meta
const hasImportMeta = typeof import.meta !== 'undefined'
&& typeof import.meta.url === 'string';
if (hasImportMeta) {
return new URL(import.meta.url).pathname
.split('/')
.slice(0, -1)
.join('/');
}
// Legacy: Extract from error stack trace
try {
const stack = new Error().stack;
const urls = stack.match(URL_EXTRACTION_REGEX);
if (urls && urls.length > 0) {
const lastUrl = urls[urls.length - 1];
return lastUrl.substring(0, lastUrl.lastIndexOf('/'));
}
} catch (error) {
logger.error(NAME, 'Failed to extract script path from stack trace');
}
// Absolute fallback
return '/';
}
/**
* Dynamically loads CSS file into document head.
*
* Creates <link rel="stylesheet"> element and appends to <head>.
* Does not wait for stylesheet to load - use link.onload if needed.
*
* @param {string} src - URL of CSS file to load
* @returns {HTMLLinkElement} Created link element (for load tracking)
* @throws {Error} If src parameter is empty or missing
*
* @example
* // Basic usage
* writeCSS('/assets/css/module.css');
*
* @example
* // Track load completion
* const link = writeCSS('/assets/css/module.css');
* link.onload = () => console.log('CSS loaded');
* link.onerror = () => console.error('CSS failed to load');
*
* @example
* // Relative to script path
* const modulePath = getScriptPath();
* writeCSS(`${modulePath}/styles.css`);
*/
export function writeCSS(src) {
if (!src || typeof src !== 'string') {
throw new Error('CSS file path must be a non-empty string');
}
const link = document.createElement('link');
link.setAttribute('type', 'text/css');
link.setAttribute('rel', 'stylesheet');
link.setAttribute('href', src);
document.head.appendChild(link);
return link;
}