Complete ShadowRoot child mutation semantics
This commit is contained in:
1 parent
2117fa0b8c
commit
a4a3ea49ba
2 files changed
+105
-13
No files matched your search
Vendored
+50
-11
@@ -9004,6 +9004,17 @@ Element.prototype.attachShadow = function attachShadow(opts) {
|
|||||||
}
|
}
|
||||||
const host = this;
|
const host = this;
|
||||||
const children = [];
|
const children = [];
|
||||||
|
const assertInsertable = (node, operation) => {
|
||||||
|
const createsComposedCycle = node instanceof ShadowRoot
|
||||||
|
|| node === host
|
||||||
|
|| !!(node?.contains && node.contains(host));
|
||||||
|
if (createsComposedCycle) {
|
||||||
|
throw new DOMException(
|
||||||
|
`Failed to execute '${operation}' on 'Node': The new child would contain the parent.`,
|
||||||
|
"HierarchyRequestError"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
const shadow = {
|
const shadow = {
|
||||||
mode: opts.mode,
|
mode: opts.mode,
|
||||||
host: host,
|
host: host,
|
||||||
@@ -9022,44 +9033,72 @@ Element.prototype.attachShadow = function attachShadow(opts) {
|
|||||||
get firstElementChild() { return children.find(c => c.nodeType === 1) || null; },
|
get firstElementChild() { return children.find(c => c.nodeType === 1) || null; },
|
||||||
get children() { return children.filter(c => c.nodeType === 1); },
|
get children() { return children.filter(c => c.nodeType === 1); },
|
||||||
appendChild(c) {
|
appendChild(c) {
|
||||||
if (c) {
|
if (!c) return c;
|
||||||
|
assertInsertable(c, "appendChild");
|
||||||
|
if (c instanceof DocumentFragment) {
|
||||||
|
for (const child of Array.from(c.childNodes)) shadow.appendChild(child);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
if (c._shadowParent) c._shadowParent.removeChild(c);
|
if (c._shadowParent) c._shadowParent.removeChild(c);
|
||||||
else if (c.parentNode) c.parentNode.removeChild(c);
|
else if (c.parentNode) c.parentNode.removeChild(c);
|
||||||
children.push(c);
|
children.push(c);
|
||||||
c._shadowParent = shadow;
|
c._shadowParent = shadow;
|
||||||
}
|
|
||||||
return c;
|
return c;
|
||||||
},
|
},
|
||||||
insertBefore(n, ref) {
|
insertBefore(n, ref) {
|
||||||
if (!n) return n;
|
if (!n) return n;
|
||||||
if (!ref) { shadow.appendChild(n); return n; }
|
if (!ref) { shadow.appendChild(n); return n; }
|
||||||
const idx = children.indexOf(ref);
|
if (!children.includes(ref)) {
|
||||||
if (idx >= 0) {
|
throw new DOMException(
|
||||||
|
"Failed to execute 'insertBefore' on 'Node': The reference node is not a child of this node.",
|
||||||
|
"NotFoundError"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (n === ref) return n;
|
||||||
|
assertInsertable(n, "insertBefore");
|
||||||
|
if (n instanceof DocumentFragment) {
|
||||||
|
for (const child of Array.from(n.childNodes)) shadow.insertBefore(child, ref);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
if (n._shadowParent) n._shadowParent.removeChild(n);
|
if (n._shadowParent) n._shadowParent.removeChild(n);
|
||||||
else if (n.parentNode) n.parentNode.removeChild(n);
|
else if (n.parentNode) n.parentNode.removeChild(n);
|
||||||
|
const idx = children.indexOf(ref);
|
||||||
children.splice(idx, 0, n);
|
children.splice(idx, 0, n);
|
||||||
n._shadowParent = shadow;
|
n._shadowParent = shadow;
|
||||||
}
|
|
||||||
else shadow.appendChild(n);
|
|
||||||
return n;
|
return n;
|
||||||
},
|
},
|
||||||
removeChild(c) {
|
removeChild(c) {
|
||||||
const idx = children.indexOf(c);
|
const idx = children.indexOf(c);
|
||||||
if (idx >= 0) {
|
if (idx < 0) {
|
||||||
|
throw new DOMException(
|
||||||
|
"Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.",
|
||||||
|
"NotFoundError"
|
||||||
|
);
|
||||||
|
}
|
||||||
children.splice(idx, 1);
|
children.splice(idx, 1);
|
||||||
if (c._shadowParent === shadow) c._shadowParent = null;
|
if (c._shadowParent === shadow) c._shadowParent = null;
|
||||||
}
|
|
||||||
return c;
|
return c;
|
||||||
},
|
},
|
||||||
replaceChild(n, o) {
|
replaceChild(n, o) {
|
||||||
const idx = children.indexOf(o);
|
if (!children.includes(o)) {
|
||||||
if (idx >= 0) {
|
throw new DOMException(
|
||||||
|
"Failed to execute 'replaceChild' on 'Node': The node to be replaced is not a child of this node.",
|
||||||
|
"NotFoundError"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (n === o) return o;
|
||||||
|
assertInsertable(n, "replaceChild");
|
||||||
|
if (n instanceof DocumentFragment) {
|
||||||
|
for (const child of Array.from(n.childNodes)) shadow.insertBefore(child, o);
|
||||||
|
shadow.removeChild(o);
|
||||||
|
return o;
|
||||||
|
}
|
||||||
if (n._shadowParent) n._shadowParent.removeChild(n);
|
if (n._shadowParent) n._shadowParent.removeChild(n);
|
||||||
else if (n.parentNode) n.parentNode.removeChild(n);
|
else if (n.parentNode) n.parentNode.removeChild(n);
|
||||||
|
const idx = children.indexOf(o);
|
||||||
children[idx] = n;
|
children[idx] = n;
|
||||||
n._shadowParent = shadow;
|
n._shadowParent = shadow;
|
||||||
if (o._shadowParent === shadow) o._shadowParent = null;
|
if (o._shadowParent === shadow) o._shadowParent = null;
|
||||||
}
|
|
||||||
return o;
|
return o;
|
||||||
},
|
},
|
||||||
querySelector(s) {
|
querySelector(s) {
|
||||||
|
|||||||
@@ -1993,7 +1993,50 @@ mod tests {
|
|||||||
inside.getRootNode() === root,
|
inside.getRootNode() === root,
|
||||||
root.textContent
|
root.textContent
|
||||||
];
|
];
|
||||||
return [inserted, removed, moved, parsed];
|
|
||||||
|
const a = document.createElement("a");
|
||||||
|
const b = document.createElement("b");
|
||||||
|
const c = document.createElement("i");
|
||||||
|
root.replaceChildren(a, b, c);
|
||||||
|
root.insertBefore(a, c);
|
||||||
|
const movedWithin = Array.from(root.children, el => el.localName);
|
||||||
|
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
const x = document.createElement("x-one");
|
||||||
|
const y = document.createElement("x-two");
|
||||||
|
fragment.append(x, y);
|
||||||
|
root.insertBefore(fragment, c);
|
||||||
|
const flattened = [
|
||||||
|
Array.from(root.children, el => el.localName),
|
||||||
|
fragment.childNodes.length,
|
||||||
|
x.parentNode === root,
|
||||||
|
y.parentNode === root
|
||||||
|
];
|
||||||
|
|
||||||
|
root.replaceChild(b, c);
|
||||||
|
const replaced = [
|
||||||
|
Array.from(root.children, el => el.localName),
|
||||||
|
c.parentNode === null,
|
||||||
|
b.parentNode === root
|
||||||
|
];
|
||||||
|
|
||||||
|
const detached = document.createElement("detached-node");
|
||||||
|
const errors = [];
|
||||||
|
for (const operation of [
|
||||||
|
() => root.insertBefore(detached, c),
|
||||||
|
() => root.removeChild(c),
|
||||||
|
() => root.replaceChild(detached, c),
|
||||||
|
() => root.appendChild(root),
|
||||||
|
() => root.appendChild(host)
|
||||||
|
]) {
|
||||||
|
try {
|
||||||
|
operation();
|
||||||
|
errors.push("none");
|
||||||
|
} catch (error) {
|
||||||
|
errors.push(error.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [inserted, removed, moved, parsed, movedWithin, flattened, replaced, errors];
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -2016,7 +2059,17 @@ mod tests {
|
|||||||
],
|
],
|
||||||
[true, true, true],
|
[true, true, true],
|
||||||
[true, true, true],
|
[true, true, true],
|
||||||
[true, true, "inside"]
|
[true, true, "inside"],
|
||||||
|
["b", "a", "i"],
|
||||||
|
[["b", "a", "x-one", "x-two", "i"], 0, true, true],
|
||||||
|
[["a", "x-one", "x-two", "b"], true, true],
|
||||||
|
[
|
||||||
|
"NotFoundError",
|
||||||
|
"NotFoundError",
|
||||||
|
"NotFoundError",
|
||||||
|
"HierarchyRequestError",
|
||||||
|
"HierarchyRequestError"
|
||||||
|
]
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user