Merge pull request #454 from lisa0314/fix/452-document-fragment-insertion

fix(js): flatten DocumentFragment during node insertion
This commit is contained in:
SGavrl authored and GitHub committed 2026-07-22 10:13:02 +00:00
commit 730bd01f57
2 files changed
+122

No files matched your search

+16
View File
@@ -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;
} }
+106
View File
@@ -1377,6 +1377,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>"#);