pulse-js-framework 1.7.4 → 1.7.5
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/cli/analyze.js +127 -46
- package/cli/build.js +51 -13
- package/cli/format.js +64 -8
- package/cli/lint.js +112 -27
- package/cli/utils/cli-ui.js +452 -0
- package/compiler/parser.js +19 -2
- package/core/errors.js +281 -6
- package/package.json +7 -2
- package/runtime/async.js +282 -14
- package/runtime/dom-adapter.js +920 -0
- package/runtime/dom.js +331 -162
- package/runtime/logger.js +144 -69
- package/runtime/logger.prod.js +43 -18
- package/runtime/pulse.js +202 -80
- package/runtime/router.js +27 -39
- package/runtime/store.js +10 -7
- package/runtime/utils.js +279 -18
|
@@ -0,0 +1,920 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse DOM Adapter - Abstraction layer for DOM operations
|
|
3
|
+
*
|
|
4
|
+
* This module provides a pluggable DOM abstraction that enables:
|
|
5
|
+
* - Server-Side Rendering (SSR) with virtual DOM implementations
|
|
6
|
+
* - Simplified testing without browser environment or heavy mocks
|
|
7
|
+
* - Platform-specific optimizations
|
|
8
|
+
*
|
|
9
|
+
* The adapter pattern decouples Pulse from direct browser DOM dependencies,
|
|
10
|
+
* allowing the same reactive code to run in Node.js, Deno, or custom environments.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// DOM Adapter Interface
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Abstract DOM adapter interface.
|
|
19
|
+
* Implementations must provide all methods for DOM manipulation.
|
|
20
|
+
*
|
|
21
|
+
* @interface DOMAdapter
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {Object} DOMAdapter
|
|
26
|
+
* @property {function(string): Element} createElement - Create an element
|
|
27
|
+
* @property {function(string): Text} createTextNode - Create a text node
|
|
28
|
+
* @property {function(string): Comment} createComment - Create a comment node
|
|
29
|
+
* @property {function(): DocumentFragment} createDocumentFragment - Create a fragment
|
|
30
|
+
* @property {function(string): Element|null} querySelector - Query the document
|
|
31
|
+
* @property {function(Element, string, string): void} setAttribute - Set attribute
|
|
32
|
+
* @property {function(Element, string): void} removeAttribute - Remove attribute
|
|
33
|
+
* @property {function(Element, string): string|null} getAttribute - Get attribute
|
|
34
|
+
* @property {function(Node, Node): void} appendChild - Append child to parent
|
|
35
|
+
* @property {function(Node, Node, Node): void} insertBefore - Insert before reference
|
|
36
|
+
* @property {function(Node): void} removeNode - Remove node from parent
|
|
37
|
+
* @property {function(Node): Node|null} getParentNode - Get parent node
|
|
38
|
+
* @property {function(Node): Node|null} getNextSibling - Get next sibling
|
|
39
|
+
* @property {function(Node): Node|null} getFirstChild - Get first child
|
|
40
|
+
* @property {function(Element, string): void} addClass - Add CSS class
|
|
41
|
+
* @property {function(Element, string): void} removeClass - Remove CSS class
|
|
42
|
+
* @property {function(Element, string, *): void} setStyle - Set style property
|
|
43
|
+
* @property {function(Element, string): *} getStyle - Get style property
|
|
44
|
+
* @property {function(Element, string, *): void} setProperty - Set DOM property
|
|
45
|
+
* @property {function(Element, string): *} getProperty - Get DOM property
|
|
46
|
+
* @property {function(Element, string, Function, Object=): void} addEventListener - Add event listener
|
|
47
|
+
* @property {function(Element, string, Function, Object=): void} removeEventListener - Remove event listener
|
|
48
|
+
* @property {function(Node, string): void} setTextContent - Set text content
|
|
49
|
+
* @property {function(Node): string} getTextContent - Get text content
|
|
50
|
+
* @property {function(*): boolean} isNode - Check if value is a Node
|
|
51
|
+
* @property {function(*): boolean} isElement - Check if value is an Element
|
|
52
|
+
* @property {function(Function): void} queueMicrotask - Queue a microtask
|
|
53
|
+
* @property {function(Function, number): number} setTimeout - Set timeout
|
|
54
|
+
* @property {function(number): void} clearTimeout - Clear timeout
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// Browser DOM Adapter
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Browser DOM adapter - default implementation using native browser APIs.
|
|
63
|
+
* This is the production adapter for client-side rendering.
|
|
64
|
+
*
|
|
65
|
+
* @implements {DOMAdapter}
|
|
66
|
+
*/
|
|
67
|
+
export class BrowserDOMAdapter {
|
|
68
|
+
/**
|
|
69
|
+
* Create an element with the specified tag name.
|
|
70
|
+
* @param {string} tagName - The tag name (e.g., 'div', 'span')
|
|
71
|
+
* @returns {Element} The created element
|
|
72
|
+
*/
|
|
73
|
+
createElement(tagName) {
|
|
74
|
+
return document.createElement(tagName);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create a text node with the specified content.
|
|
79
|
+
* @param {string} text - The text content
|
|
80
|
+
* @returns {Text} The created text node
|
|
81
|
+
*/
|
|
82
|
+
createTextNode(text) {
|
|
83
|
+
return document.createTextNode(text);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Create a comment node with the specified content.
|
|
88
|
+
* @param {string} data - The comment content
|
|
89
|
+
* @returns {Comment} The created comment node
|
|
90
|
+
*/
|
|
91
|
+
createComment(data) {
|
|
92
|
+
return document.createComment(data);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Create a document fragment for batched DOM operations.
|
|
97
|
+
* @returns {DocumentFragment} The created fragment
|
|
98
|
+
*/
|
|
99
|
+
createDocumentFragment() {
|
|
100
|
+
return document.createDocumentFragment();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Query the document for an element matching the selector.
|
|
105
|
+
* @param {string} selector - CSS selector
|
|
106
|
+
* @returns {Element|null} The matched element or null
|
|
107
|
+
*/
|
|
108
|
+
querySelector(selector) {
|
|
109
|
+
return document.querySelector(selector);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Set an attribute on an element.
|
|
114
|
+
* @param {Element} element - Target element
|
|
115
|
+
* @param {string} name - Attribute name
|
|
116
|
+
* @param {string} value - Attribute value
|
|
117
|
+
*/
|
|
118
|
+
setAttribute(element, name, value) {
|
|
119
|
+
element.setAttribute(name, value);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Remove an attribute from an element.
|
|
124
|
+
* @param {Element} element - Target element
|
|
125
|
+
* @param {string} name - Attribute name
|
|
126
|
+
*/
|
|
127
|
+
removeAttribute(element, name) {
|
|
128
|
+
element.removeAttribute(name);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get an attribute value from an element.
|
|
133
|
+
* @param {Element} element - Target element
|
|
134
|
+
* @param {string} name - Attribute name
|
|
135
|
+
* @returns {string|null} The attribute value or null
|
|
136
|
+
*/
|
|
137
|
+
getAttribute(element, name) {
|
|
138
|
+
return element.getAttribute(name);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Append a child node to a parent.
|
|
143
|
+
* @param {Node} parent - Parent node
|
|
144
|
+
* @param {Node} child - Child node to append
|
|
145
|
+
*/
|
|
146
|
+
appendChild(parent, child) {
|
|
147
|
+
parent.appendChild(child);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Insert a node before a reference node.
|
|
152
|
+
* @param {Node} parent - Parent node
|
|
153
|
+
* @param {Node} newNode - Node to insert
|
|
154
|
+
* @param {Node} refNode - Reference node (insert before this)
|
|
155
|
+
*/
|
|
156
|
+
insertBefore(parent, newNode, refNode) {
|
|
157
|
+
parent.insertBefore(newNode, refNode);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Remove a node from its parent.
|
|
162
|
+
* @param {Node} node - Node to remove
|
|
163
|
+
*/
|
|
164
|
+
removeNode(node) {
|
|
165
|
+
node.remove();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get the parent node of a node.
|
|
170
|
+
* @param {Node} node - Target node
|
|
171
|
+
* @returns {Node|null} The parent node or null
|
|
172
|
+
*/
|
|
173
|
+
getParentNode(node) {
|
|
174
|
+
return node.parentNode;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get the next sibling of a node.
|
|
179
|
+
* @param {Node} node - Target node
|
|
180
|
+
* @returns {Node|null} The next sibling or null
|
|
181
|
+
*/
|
|
182
|
+
getNextSibling(node) {
|
|
183
|
+
return node.nextSibling;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get the first child of a node.
|
|
188
|
+
* @param {Node} node - Target node
|
|
189
|
+
* @returns {Node|null} The first child or null
|
|
190
|
+
*/
|
|
191
|
+
getFirstChild(node) {
|
|
192
|
+
return node.firstChild;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Add a CSS class to an element.
|
|
197
|
+
* @param {Element} element - Target element
|
|
198
|
+
* @param {string} className - Class name to add
|
|
199
|
+
*/
|
|
200
|
+
addClass(element, className) {
|
|
201
|
+
element.classList.add(className);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Remove a CSS class from an element.
|
|
206
|
+
* @param {Element} element - Target element
|
|
207
|
+
* @param {string} className - Class name to remove
|
|
208
|
+
*/
|
|
209
|
+
removeClass(element, className) {
|
|
210
|
+
element.classList.remove(className);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Set a style property on an element.
|
|
215
|
+
* @param {Element} element - Target element
|
|
216
|
+
* @param {string} prop - Style property name
|
|
217
|
+
* @param {*} value - Style value
|
|
218
|
+
*/
|
|
219
|
+
setStyle(element, prop, value) {
|
|
220
|
+
element.style[prop] = value;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Get a style property from an element.
|
|
225
|
+
* @param {Element} element - Target element
|
|
226
|
+
* @param {string} prop - Style property name
|
|
227
|
+
* @returns {*} The style value
|
|
228
|
+
*/
|
|
229
|
+
getStyle(element, prop) {
|
|
230
|
+
return element.style[prop];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Set a DOM property on an element.
|
|
235
|
+
* @param {Element} element - Target element
|
|
236
|
+
* @param {string} prop - Property name
|
|
237
|
+
* @param {*} value - Property value
|
|
238
|
+
*/
|
|
239
|
+
setProperty(element, prop, value) {
|
|
240
|
+
element[prop] = value;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get a DOM property from an element.
|
|
245
|
+
* @param {Element} element - Target element
|
|
246
|
+
* @param {string} prop - Property name
|
|
247
|
+
* @returns {*} The property value
|
|
248
|
+
*/
|
|
249
|
+
getProperty(element, prop) {
|
|
250
|
+
return element[prop];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Add an event listener to an element.
|
|
255
|
+
* @param {Element} element - Target element
|
|
256
|
+
* @param {string} event - Event name
|
|
257
|
+
* @param {Function} handler - Event handler
|
|
258
|
+
* @param {Object} [options] - Event listener options
|
|
259
|
+
*/
|
|
260
|
+
addEventListener(element, event, handler, options) {
|
|
261
|
+
element.addEventListener(event, handler, options);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Remove an event listener from an element.
|
|
266
|
+
* @param {Element} element - Target element
|
|
267
|
+
* @param {string} event - Event name
|
|
268
|
+
* @param {Function} handler - Event handler
|
|
269
|
+
* @param {Object} [options] - Event listener options
|
|
270
|
+
*/
|
|
271
|
+
removeEventListener(element, event, handler, options) {
|
|
272
|
+
element.removeEventListener(event, handler, options);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Set the text content of a node.
|
|
277
|
+
* @param {Node} node - Target node
|
|
278
|
+
* @param {string} text - Text content
|
|
279
|
+
*/
|
|
280
|
+
setTextContent(node, text) {
|
|
281
|
+
node.textContent = text;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Get the text content of a node.
|
|
286
|
+
* @param {Node} node - Target node
|
|
287
|
+
* @returns {string} The text content
|
|
288
|
+
*/
|
|
289
|
+
getTextContent(node) {
|
|
290
|
+
return node.textContent;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Check if a value is a Node.
|
|
295
|
+
* @param {*} value - Value to check
|
|
296
|
+
* @returns {boolean} True if value is a Node
|
|
297
|
+
*/
|
|
298
|
+
isNode(value) {
|
|
299
|
+
return value instanceof Node;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Check if a value is an Element.
|
|
304
|
+
* @param {*} value - Value to check
|
|
305
|
+
* @returns {boolean} True if value is an Element
|
|
306
|
+
*/
|
|
307
|
+
isElement(value) {
|
|
308
|
+
return value instanceof Element;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Queue a function to run as a microtask.
|
|
313
|
+
* @param {Function} fn - Function to queue
|
|
314
|
+
*/
|
|
315
|
+
queueMicrotask(fn) {
|
|
316
|
+
queueMicrotask(fn);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Set a timeout.
|
|
321
|
+
* @param {Function} fn - Function to call
|
|
322
|
+
* @param {number} delay - Delay in milliseconds
|
|
323
|
+
* @returns {number} Timer ID
|
|
324
|
+
*/
|
|
325
|
+
setTimeout(fn, delay) {
|
|
326
|
+
return setTimeout(fn, delay);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Clear a timeout.
|
|
331
|
+
* @param {number} timerId - Timer ID to clear
|
|
332
|
+
*/
|
|
333
|
+
clearTimeout(timerId) {
|
|
334
|
+
clearTimeout(timerId);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Get the tag name of an element (lowercase).
|
|
339
|
+
* @param {Element} element - Target element
|
|
340
|
+
* @returns {string} The tag name
|
|
341
|
+
*/
|
|
342
|
+
getTagName(element) {
|
|
343
|
+
return element.tagName.toLowerCase();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Get the type attribute of an input element.
|
|
348
|
+
* @param {Element} element - Target element
|
|
349
|
+
* @returns {string|undefined} The type attribute
|
|
350
|
+
*/
|
|
351
|
+
getInputType(element) {
|
|
352
|
+
return element.type?.toLowerCase();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ============================================================================
|
|
357
|
+
// Mock DOM Adapter for Testing
|
|
358
|
+
// ============================================================================
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Simple mock node for testing without a browser environment.
|
|
362
|
+
* Provides minimal DOM-like interface for unit tests.
|
|
363
|
+
*/
|
|
364
|
+
export class MockNode {
|
|
365
|
+
constructor(type, data = '') {
|
|
366
|
+
this.nodeType = type;
|
|
367
|
+
this.nodeName = '';
|
|
368
|
+
this.textContent = data;
|
|
369
|
+
this.parentNode = null;
|
|
370
|
+
this.childNodes = [];
|
|
371
|
+
this.nextSibling = null;
|
|
372
|
+
this.previousSibling = null;
|
|
373
|
+
this._eventListeners = new Map();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
get firstChild() {
|
|
377
|
+
return this.childNodes[0] || null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
appendChild(child) {
|
|
381
|
+
if (child.parentNode) {
|
|
382
|
+
child.parentNode.removeChild(child);
|
|
383
|
+
}
|
|
384
|
+
child.parentNode = this;
|
|
385
|
+
if (this.childNodes.length > 0) {
|
|
386
|
+
const lastChild = this.childNodes[this.childNodes.length - 1];
|
|
387
|
+
lastChild.nextSibling = child;
|
|
388
|
+
child.previousSibling = lastChild;
|
|
389
|
+
}
|
|
390
|
+
child.nextSibling = null;
|
|
391
|
+
this.childNodes.push(child);
|
|
392
|
+
return child;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
insertBefore(newNode, refNode) {
|
|
396
|
+
if (newNode.parentNode) {
|
|
397
|
+
newNode.parentNode.removeChild(newNode);
|
|
398
|
+
}
|
|
399
|
+
const index = refNode ? this.childNodes.indexOf(refNode) : this.childNodes.length;
|
|
400
|
+
if (index === -1) {
|
|
401
|
+
return this.appendChild(newNode);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
newNode.parentNode = this;
|
|
405
|
+
this.childNodes.splice(index, 0, newNode);
|
|
406
|
+
|
|
407
|
+
// Update sibling references
|
|
408
|
+
this._updateSiblings();
|
|
409
|
+
return newNode;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
removeChild(child) {
|
|
413
|
+
const index = this.childNodes.indexOf(child);
|
|
414
|
+
if (index !== -1) {
|
|
415
|
+
this.childNodes.splice(index, 1);
|
|
416
|
+
child.parentNode = null;
|
|
417
|
+
this._updateSiblings();
|
|
418
|
+
}
|
|
419
|
+
return child;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
remove() {
|
|
423
|
+
if (this.parentNode) {
|
|
424
|
+
this.parentNode.removeChild(this);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
_updateSiblings() {
|
|
429
|
+
for (let i = 0; i < this.childNodes.length; i++) {
|
|
430
|
+
const node = this.childNodes[i];
|
|
431
|
+
node.previousSibling = this.childNodes[i - 1] || null;
|
|
432
|
+
node.nextSibling = this.childNodes[i + 1] || null;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
addEventListener(event, handler, options) {
|
|
437
|
+
if (!this._eventListeners.has(event)) {
|
|
438
|
+
this._eventListeners.set(event, []);
|
|
439
|
+
}
|
|
440
|
+
this._eventListeners.get(event).push({ handler, options });
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
removeEventListener(event, handler, options) {
|
|
444
|
+
const listeners = this._eventListeners.get(event);
|
|
445
|
+
if (listeners) {
|
|
446
|
+
const index = listeners.findIndex(l => l.handler === handler);
|
|
447
|
+
if (index !== -1) {
|
|
448
|
+
listeners.splice(index, 1);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
dispatchEvent(event) {
|
|
454
|
+
const listeners = this._eventListeners.get(event.type);
|
|
455
|
+
if (listeners) {
|
|
456
|
+
for (const { handler } of listeners) {
|
|
457
|
+
handler(event);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Mock Element extending MockNode with element-specific features.
|
|
465
|
+
*/
|
|
466
|
+
export class MockElement extends MockNode {
|
|
467
|
+
constructor(tagName) {
|
|
468
|
+
super(1); // Element node type
|
|
469
|
+
this.tagName = tagName.toUpperCase();
|
|
470
|
+
this.nodeName = this.tagName;
|
|
471
|
+
this.id = '';
|
|
472
|
+
this.className = '';
|
|
473
|
+
this._attributes = new Map();
|
|
474
|
+
this._style = {};
|
|
475
|
+
this.type = undefined;
|
|
476
|
+
this.value = '';
|
|
477
|
+
this.checked = false;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
get classList() {
|
|
481
|
+
const self = this;
|
|
482
|
+
return {
|
|
483
|
+
add(className) {
|
|
484
|
+
const classes = self.className ? self.className.split(' ') : [];
|
|
485
|
+
if (!classes.includes(className)) {
|
|
486
|
+
classes.push(className);
|
|
487
|
+
self.className = classes.join(' ');
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
remove(className) {
|
|
491
|
+
const classes = self.className ? self.className.split(' ') : [];
|
|
492
|
+
const index = classes.indexOf(className);
|
|
493
|
+
if (index !== -1) {
|
|
494
|
+
classes.splice(index, 1);
|
|
495
|
+
self.className = classes.join(' ');
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
contains(className) {
|
|
499
|
+
const classes = self.className ? self.className.split(' ') : [];
|
|
500
|
+
return classes.includes(className);
|
|
501
|
+
},
|
|
502
|
+
toggle(className, force) {
|
|
503
|
+
if (force === undefined) {
|
|
504
|
+
force = !this.contains(className);
|
|
505
|
+
}
|
|
506
|
+
if (force) {
|
|
507
|
+
this.add(className);
|
|
508
|
+
} else {
|
|
509
|
+
this.remove(className);
|
|
510
|
+
}
|
|
511
|
+
return force;
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
get style() {
|
|
517
|
+
return this._style;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
setAttribute(name, value) {
|
|
521
|
+
this._attributes.set(name, String(value));
|
|
522
|
+
if (name === 'id') this.id = value;
|
|
523
|
+
if (name === 'class') this.className = value;
|
|
524
|
+
if (name === 'type') this.type = value;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
getAttribute(name) {
|
|
528
|
+
return this._attributes.get(name) ?? null;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
removeAttribute(name) {
|
|
532
|
+
this._attributes.delete(name);
|
|
533
|
+
if (name === 'id') this.id = '';
|
|
534
|
+
if (name === 'class') this.className = '';
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
hasAttribute(name) {
|
|
538
|
+
return this._attributes.has(name);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Mock Text node for testing.
|
|
544
|
+
*/
|
|
545
|
+
export class MockTextNode extends MockNode {
|
|
546
|
+
constructor(text) {
|
|
547
|
+
super(3); // Text node type
|
|
548
|
+
this.nodeName = '#text';
|
|
549
|
+
this.textContent = text;
|
|
550
|
+
this.data = text;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Mock Comment node for testing.
|
|
556
|
+
*/
|
|
557
|
+
export class MockCommentNode extends MockNode {
|
|
558
|
+
constructor(data) {
|
|
559
|
+
super(8); // Comment node type
|
|
560
|
+
this.nodeName = '#comment';
|
|
561
|
+
this.textContent = data;
|
|
562
|
+
this.data = data;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Mock DocumentFragment for testing.
|
|
568
|
+
*/
|
|
569
|
+
export class MockDocumentFragment extends MockNode {
|
|
570
|
+
constructor() {
|
|
571
|
+
super(11); // Document fragment node type
|
|
572
|
+
this.nodeName = '#document-fragment';
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Mock DOM adapter for testing without browser environment.
|
|
578
|
+
* Provides a lightweight, synchronous DOM simulation.
|
|
579
|
+
*
|
|
580
|
+
* @implements {DOMAdapter}
|
|
581
|
+
*/
|
|
582
|
+
export class MockDOMAdapter {
|
|
583
|
+
constructor() {
|
|
584
|
+
this._document = new MockElement('html');
|
|
585
|
+
this._body = new MockElement('body');
|
|
586
|
+
this._document.appendChild(this._body);
|
|
587
|
+
this._timers = new Map();
|
|
588
|
+
this._timerIdCounter = 0;
|
|
589
|
+
this._microtaskQueue = [];
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
createElement(tagName) {
|
|
593
|
+
return new MockElement(tagName);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
createTextNode(text) {
|
|
597
|
+
return new MockTextNode(text);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
createComment(data) {
|
|
601
|
+
return new MockCommentNode(data);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
createDocumentFragment() {
|
|
605
|
+
return new MockDocumentFragment();
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
querySelector(selector) {
|
|
609
|
+
// Simple selector matching for testing
|
|
610
|
+
// In production, you'd want a more complete implementation
|
|
611
|
+
return this._findElement(this._body, selector);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
_findElement(root, selector) {
|
|
615
|
+
// Basic ID selector support
|
|
616
|
+
if (selector.startsWith('#')) {
|
|
617
|
+
const id = selector.slice(1);
|
|
618
|
+
return this._findById(root, id);
|
|
619
|
+
}
|
|
620
|
+
// Basic class selector support
|
|
621
|
+
if (selector.startsWith('.')) {
|
|
622
|
+
const className = selector.slice(1);
|
|
623
|
+
return this._findByClass(root, className);
|
|
624
|
+
}
|
|
625
|
+
// Basic tag selector support
|
|
626
|
+
return this._findByTag(root, selector);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
_findById(node, id) {
|
|
630
|
+
if (node.id === id) return node;
|
|
631
|
+
for (const child of node.childNodes || []) {
|
|
632
|
+
const found = this._findById(child, id);
|
|
633
|
+
if (found) return found;
|
|
634
|
+
}
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
_findByClass(node, className) {
|
|
639
|
+
if (node.classList?.contains(className)) return node;
|
|
640
|
+
for (const child of node.childNodes || []) {
|
|
641
|
+
const found = this._findByClass(child, className);
|
|
642
|
+
if (found) return found;
|
|
643
|
+
}
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
_findByTag(node, tagName) {
|
|
648
|
+
if (node.tagName?.toLowerCase() === tagName.toLowerCase()) return node;
|
|
649
|
+
for (const child of node.childNodes || []) {
|
|
650
|
+
const found = this._findByTag(child, tagName);
|
|
651
|
+
if (found) return found;
|
|
652
|
+
}
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
setAttribute(element, name, value) {
|
|
657
|
+
element.setAttribute(name, value);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
removeAttribute(element, name) {
|
|
661
|
+
element.removeAttribute(name);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
getAttribute(element, name) {
|
|
665
|
+
return element.getAttribute(name);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
appendChild(parent, child) {
|
|
669
|
+
// Handle DocumentFragment - move all children
|
|
670
|
+
if (child instanceof MockDocumentFragment) {
|
|
671
|
+
const children = [...child.childNodes];
|
|
672
|
+
for (const c of children) {
|
|
673
|
+
parent.appendChild(c);
|
|
674
|
+
}
|
|
675
|
+
return child;
|
|
676
|
+
}
|
|
677
|
+
return parent.appendChild(child);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
insertBefore(parent, newNode, refNode) {
|
|
681
|
+
// Handle DocumentFragment - move all children
|
|
682
|
+
if (newNode instanceof MockDocumentFragment) {
|
|
683
|
+
const children = [...newNode.childNodes];
|
|
684
|
+
for (const c of children) {
|
|
685
|
+
parent.insertBefore(c, refNode);
|
|
686
|
+
}
|
|
687
|
+
return newNode;
|
|
688
|
+
}
|
|
689
|
+
return parent.insertBefore(newNode, refNode);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
removeNode(node) {
|
|
693
|
+
node.remove();
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
getParentNode(node) {
|
|
697
|
+
return node.parentNode;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
getNextSibling(node) {
|
|
701
|
+
return node.nextSibling;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
getFirstChild(node) {
|
|
705
|
+
return node.firstChild;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
addClass(element, className) {
|
|
709
|
+
element.classList.add(className);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
removeClass(element, className) {
|
|
713
|
+
element.classList.remove(className);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
setStyle(element, prop, value) {
|
|
717
|
+
element.style[prop] = value;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
getStyle(element, prop) {
|
|
721
|
+
return element.style[prop];
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
setProperty(element, prop, value) {
|
|
725
|
+
element[prop] = value;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
getProperty(element, prop) {
|
|
729
|
+
return element[prop];
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
addEventListener(element, event, handler, options) {
|
|
733
|
+
element.addEventListener(event, handler, options);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
removeEventListener(element, event, handler, options) {
|
|
737
|
+
element.removeEventListener(event, handler, options);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
setTextContent(node, text) {
|
|
741
|
+
node.textContent = text;
|
|
742
|
+
if (node.data !== undefined) {
|
|
743
|
+
node.data = text;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
getTextContent(node) {
|
|
748
|
+
return node.textContent;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
isNode(value) {
|
|
752
|
+
return value instanceof MockNode;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
isElement(value) {
|
|
756
|
+
return value instanceof MockElement;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
queueMicrotask(fn) {
|
|
760
|
+
this._microtaskQueue.push(fn);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
setTimeout(fn, delay) {
|
|
764
|
+
const id = ++this._timerIdCounter;
|
|
765
|
+
this._timers.set(id, { fn, delay, type: 'timeout' });
|
|
766
|
+
return id;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
clearTimeout(timerId) {
|
|
770
|
+
this._timers.delete(timerId);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
getTagName(element) {
|
|
774
|
+
return element.tagName.toLowerCase();
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
getInputType(element) {
|
|
778
|
+
return element.type?.toLowerCase();
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Test helpers
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Flush all pending microtasks (synchronously for testing).
|
|
785
|
+
*/
|
|
786
|
+
flushMicrotasks() {
|
|
787
|
+
while (this._microtaskQueue.length > 0) {
|
|
788
|
+
const fn = this._microtaskQueue.shift();
|
|
789
|
+
fn();
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Run all pending timeouts (synchronously for testing).
|
|
795
|
+
*/
|
|
796
|
+
runAllTimers() {
|
|
797
|
+
for (const [id, { fn }] of this._timers) {
|
|
798
|
+
fn();
|
|
799
|
+
this._timers.delete(id);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Get the mock document body for inspection.
|
|
805
|
+
*/
|
|
806
|
+
getBody() {
|
|
807
|
+
return this._body;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Reset the mock DOM state.
|
|
812
|
+
*/
|
|
813
|
+
reset() {
|
|
814
|
+
this._body.childNodes = [];
|
|
815
|
+
this._timers.clear();
|
|
816
|
+
this._microtaskQueue = [];
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// ============================================================================
|
|
821
|
+
// Global Adapter Management
|
|
822
|
+
// ============================================================================
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* The currently active DOM adapter.
|
|
826
|
+
* Defaults to BrowserDOMAdapter in browser environments.
|
|
827
|
+
* @type {DOMAdapter}
|
|
828
|
+
*/
|
|
829
|
+
let activeAdapter = null;
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Get the currently active DOM adapter.
|
|
833
|
+
* Lazily initializes BrowserDOMAdapter if in browser environment.
|
|
834
|
+
*
|
|
835
|
+
* @returns {DOMAdapter} The active DOM adapter
|
|
836
|
+
* @throws {Error} If no adapter is set and not in browser environment
|
|
837
|
+
*/
|
|
838
|
+
export function getAdapter() {
|
|
839
|
+
if (!activeAdapter) {
|
|
840
|
+
// Auto-initialize in browser environment
|
|
841
|
+
if (typeof document !== 'undefined') {
|
|
842
|
+
activeAdapter = new BrowserDOMAdapter();
|
|
843
|
+
} else {
|
|
844
|
+
throw new Error(
|
|
845
|
+
'[Pulse] No DOM adapter configured. ' +
|
|
846
|
+
'In non-browser environments, call setAdapter() with a MockDOMAdapter or custom implementation.'
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return activeAdapter;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Set the active DOM adapter.
|
|
855
|
+
* Use this to configure SSR, testing, or custom rendering targets.
|
|
856
|
+
*
|
|
857
|
+
* @param {DOMAdapter} adapter - The adapter to use
|
|
858
|
+
*
|
|
859
|
+
* @example
|
|
860
|
+
* // For SSR
|
|
861
|
+
* import { setAdapter, MockDOMAdapter } from 'pulse-js-framework/runtime/dom-adapter';
|
|
862
|
+
* setAdapter(new MockDOMAdapter());
|
|
863
|
+
*
|
|
864
|
+
* // For testing
|
|
865
|
+
* beforeEach(() => {
|
|
866
|
+
* setAdapter(new MockDOMAdapter());
|
|
867
|
+
* });
|
|
868
|
+
*/
|
|
869
|
+
export function setAdapter(adapter) {
|
|
870
|
+
activeAdapter = adapter;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Reset the adapter to browser default (or null in non-browser).
|
|
875
|
+
* Useful for cleanup after tests.
|
|
876
|
+
*/
|
|
877
|
+
export function resetAdapter() {
|
|
878
|
+
activeAdapter = null;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Run a function with a temporary DOM adapter.
|
|
883
|
+
* The previous adapter is restored after the function completes.
|
|
884
|
+
*
|
|
885
|
+
* @param {DOMAdapter} adapter - The adapter to use temporarily
|
|
886
|
+
* @param {Function} fn - The function to run
|
|
887
|
+
* @returns {*} The return value of fn
|
|
888
|
+
*
|
|
889
|
+
* @example
|
|
890
|
+
* const result = withAdapter(new MockDOMAdapter(), () => {
|
|
891
|
+
* return el('div.test', 'Hello');
|
|
892
|
+
* });
|
|
893
|
+
*/
|
|
894
|
+
export function withAdapter(adapter, fn) {
|
|
895
|
+
const prevAdapter = activeAdapter;
|
|
896
|
+
activeAdapter = adapter;
|
|
897
|
+
try {
|
|
898
|
+
return fn();
|
|
899
|
+
} finally {
|
|
900
|
+
activeAdapter = prevAdapter;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// ============================================================================
|
|
905
|
+
// Exports
|
|
906
|
+
// ============================================================================
|
|
907
|
+
|
|
908
|
+
export default {
|
|
909
|
+
BrowserDOMAdapter,
|
|
910
|
+
MockDOMAdapter,
|
|
911
|
+
MockNode,
|
|
912
|
+
MockElement,
|
|
913
|
+
MockTextNode,
|
|
914
|
+
MockCommentNode,
|
|
915
|
+
MockDocumentFragment,
|
|
916
|
+
getAdapter,
|
|
917
|
+
setAdapter,
|
|
918
|
+
resetAdapter,
|
|
919
|
+
withAdapter
|
|
920
|
+
};
|