-
-
Notifications
You must be signed in to change notification settings - Fork 1
/
createPseudoNode.mjs
105 lines (92 loc) · 3.29 KB
/
createPseudoNode.mjs
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
// @ts-check
/**
* Creates a pseudo DOM node that acts like a parent node for a real DOM node’s
* child nodes that are between a start and end DOM node, suitable for use as a
* React app root to hydrate and render tags in a region of the document head
* where a real DOM node can’t be used to group child nodes.
*
* Only the exact DOM functionality used internally by React is supported; in
* the future Preact support may be added.
* @see https://github.com/preactjs/preact/issues/3285
* @param {ChildNode} startNode Start DOM node.
* @param {ChildNode} endNode End DOM node.
*/
export default function createPseudoNode(startNode, endNode) {
if (!(startNode instanceof Node)) {
throw new TypeError("Argument 1 `startNode` must be a DOM node.");
}
if (!(endNode instanceof Node)) {
throw new TypeError("Argument 2 `endNode` must be a DOM node.");
}
if (!startNode.parentNode) throw new TypeError("Parent DOM node missing.");
if (startNode.parentNode !== endNode.parentNode) {
throw new TypeError("Start and end DOM nodes must have the same parent.");
}
/**
* @param {ChildNode} childNode
* @returns {ChildNode}
*/
function proxyChildNode(childNode) {
return new Proxy(childNode, {
get: function (target, propertyKey) {
switch (propertyKey) {
case "nextSibling": {
return childNode.nextSibling && childNode.nextSibling !== endNode
? proxyChildNode(childNode.nextSibling)
: null;
}
case "valueOf": {
return () => target;
}
default: {
const value = Reflect.get(target, propertyKey, target);
return typeof value === "function" ? value.bind(target) : value;
}
}
},
});
}
return /** @type {ParentNode} **/ (
new Proxy(startNode.parentNode, {
get: function (target, propertyKey) {
switch (propertyKey) {
case "firstChild": {
return startNode.nextSibling && startNode.nextSibling !== endNode
? proxyChildNode(startNode.nextSibling)
: null;
}
case "appendChild": {
return /** @param {Node} node */ (node) =>
target.insertBefore(node, endNode);
}
case "removeChild": {
return /** @param {ChildNode} node */ (node) =>
target.removeChild(/** @type {ChildNode} */ (node.valueOf()));
}
case "insertBefore": {
return (
/**
* @param {Node} newNode
* @param {ChildNode} referenceNode
*/
(newNode, referenceNode) =>
target.insertBefore(
/** @type {Node} */ (newNode.valueOf()),
/** @type {ChildNode} */ (referenceNode.valueOf()),
)
);
}
default: {
const value = Reflect.get(target, propertyKey, target);
return typeof value === "function" ? value.bind(target) : value;
}
}
},
// React sets custom and standard properties.
// Todo: Avoid mutating the real DOM node.
set: function (target, propertyKey, propertyValue) {
return Reflect.set(target, propertyKey, propertyValue, target);
},
})
);
}