fix(js): flatten DocumentFragment during node insertion
Move fragment children into the target for appendChild, insertBefore, and replaceChild while preserving their order and emptying the fragment. Add runtime regression coverage for the DOM attachment portion of #452.
This commit is contained in:
1 parent
72d66bc5c0
commit
c4f545e68b
2 files changed
+122
No files matched your search
Vendored
+16
@@ -663,6 +663,11 @@ class Node {
|
|||||||
get previousSibling() { return _wrap(+_dom("prev_sibling", this._nid)); }
|
get previousSibling() { return _wrap(+_dom("prev_sibling", this._nid)); }
|
||||||
appendChild(c) {
|
appendChild(c) {
|
||||||
if (!c) return c;
|
if (!c) return c;
|
||||||
|
if (c instanceof DocumentFragment) {
|
||||||
|
const children = Array.from(c.childNodes);
|
||||||
|
for (const child of children) this.appendChild(child);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
_dom("append_child", this._nid, c._nid);
|
_dom("append_child", this._nid, c._nid);
|
||||||
if (globalThis.__mutationObservers?.length) globalThis.__notifyMutation('childList', this._nid, [c._nid], []);
|
if (globalThis.__mutationObservers?.length) globalThis.__notifyMutation('childList', this._nid, [c._nid], []);
|
||||||
if (c instanceof Element && c.tagName === 'SCRIPT') {
|
if (c instanceof Element && c.tagName === 'SCRIPT') {
|
||||||
@@ -745,6 +750,12 @@ class Node {
|
|||||||
}
|
}
|
||||||
replaceChild(newChild, oldChild) {
|
replaceChild(newChild, oldChild) {
|
||||||
if (!oldChild || !newChild) return oldChild;
|
if (!oldChild || !newChild) return oldChild;
|
||||||
|
if (newChild instanceof DocumentFragment) {
|
||||||
|
const children = Array.from(newChild.childNodes);
|
||||||
|
for (const child of children) this.insertBefore(child, oldChild);
|
||||||
|
this.removeChild(oldChild);
|
||||||
|
return oldChild;
|
||||||
|
}
|
||||||
_dom("insert_before", newChild._nid, oldChild._nid);
|
_dom("insert_before", newChild._nid, oldChild._nid);
|
||||||
_dom("remove_child", oldChild._nid);
|
_dom("remove_child", oldChild._nid);
|
||||||
return oldChild;
|
return oldChild;
|
||||||
@@ -752,6 +763,11 @@ class Node {
|
|||||||
insertBefore(n, ref) {
|
insertBefore(n, ref) {
|
||||||
if (!n) return n;
|
if (!n) return n;
|
||||||
if (!ref) { this.appendChild(n); return n; }
|
if (!ref) { this.appendChild(n); return n; }
|
||||||
|
if (n instanceof DocumentFragment) {
|
||||||
|
const children = Array.from(n.childNodes);
|
||||||
|
for (const child of children) this.insertBefore(child, ref);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
_dom("insert_before", n._nid, ref._nid);
|
_dom("insert_before", n._nid, ref._nid);
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1371,6 +1371,112 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn append_child_flattens_document_fragment() {
|
||||||
|
let mut rt = setup_runtime(r#"<main id="host"></main>"#);
|
||||||
|
rt.run_page_init();
|
||||||
|
let result = rt
|
||||||
|
.evaluate(
|
||||||
|
r#"
|
||||||
|
const host = document.getElementById('host');
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
const first = document.createElement('article');
|
||||||
|
const second = document.createElement('article');
|
||||||
|
first.id = 'first';
|
||||||
|
second.id = 'second';
|
||||||
|
first.className = second.className = 'quote';
|
||||||
|
fragment.appendChild(first);
|
||||||
|
fragment.appendChild(second);
|
||||||
|
|
||||||
|
const returned = host.appendChild(fragment);
|
||||||
|
return [
|
||||||
|
returned === fragment,
|
||||||
|
Array.from(host.children).map(node => node.id),
|
||||||
|
host.querySelectorAll('.quote').length,
|
||||||
|
fragment.childNodes.length,
|
||||||
|
first.parentNode === host,
|
||||||
|
first.parentElement === host,
|
||||||
|
];
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
serde_json::json!([true, ["first", "second"], 2, 0, true, true])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn insert_before_flattens_document_fragment_in_order() {
|
||||||
|
let mut rt = setup_runtime(r#"<main id="host"><article id="last"></article></main>"#);
|
||||||
|
rt.run_page_init();
|
||||||
|
let result = rt
|
||||||
|
.evaluate(
|
||||||
|
r#"
|
||||||
|
const host = document.getElementById('host');
|
||||||
|
const last = document.getElementById('last');
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
const first = document.createElement('article');
|
||||||
|
const second = document.createElement('article');
|
||||||
|
first.id = 'first';
|
||||||
|
second.id = 'second';
|
||||||
|
fragment.appendChild(first);
|
||||||
|
fragment.appendChild(second);
|
||||||
|
|
||||||
|
const returned = host.insertBefore(fragment, last);
|
||||||
|
return [
|
||||||
|
returned === fragment,
|
||||||
|
Array.from(host.children).map(node => node.id),
|
||||||
|
fragment.childNodes.length,
|
||||||
|
first.parentElement === host,
|
||||||
|
second.parentElement === host,
|
||||||
|
];
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
serde_json::json!([true, ["first", "second", "last"], 0, true, true])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replace_child_flattens_document_fragment_and_removes_old_child() {
|
||||||
|
let mut rt = setup_runtime(
|
||||||
|
r#"<main id="host"><article id="old"></article><article id="tail"></article></main>"#,
|
||||||
|
);
|
||||||
|
rt.run_page_init();
|
||||||
|
let result = rt
|
||||||
|
.evaluate(
|
||||||
|
r#"
|
||||||
|
const host = document.getElementById('host');
|
||||||
|
const old = document.getElementById('old');
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
const first = document.createElement('article');
|
||||||
|
const second = document.createElement('article');
|
||||||
|
first.id = 'first';
|
||||||
|
second.id = 'second';
|
||||||
|
fragment.appendChild(first);
|
||||||
|
fragment.appendChild(second);
|
||||||
|
|
||||||
|
const returned = host.replaceChild(fragment, old);
|
||||||
|
return [
|
||||||
|
returned === old,
|
||||||
|
Array.from(host.children).map(node => node.id),
|
||||||
|
fragment.childNodes.length,
|
||||||
|
old.parentNode === null,
|
||||||
|
first.parentElement === host,
|
||||||
|
second.parentElement === host,
|
||||||
|
];
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
serde_json::json!([true, ["first", "second", "tail"], 0, true, true, true])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_inner_html() {
|
fn test_inner_html() {
|
||||||
let mut rt = setup_runtime(r#"<div id="x"><p>Hello</p></div>"#);
|
let mut rt = setup_runtime(r#"<div id="x"><p>Hello</p></div>"#);
|
||||||
|
|||||||
Reference in new issue
Block a user