dom
dom copied to clipboard
Hook for SVG and HTML script element insertion
Given http://software.hixie.ch/utilities/js/live-dom-viewer/?saved=4098 we need to do something special for script elements, in particular when inserted from a DocumentFragment node. It's my understanding this only affects script elements, but I haven't verified.
https://github.com/whatwg/html/issues/1127 has an idea as to how to address this.
Maybe the way to go here is to always notify post-DocumentFragment node insert for everyone. That seems to be what https://searchfox.org/mozilla-central/source/dom/base/nsINode.cpp#2404 is doing anyway.
Oh joy, if I change things to
<!DOCTYPE html>
<script>
var s1 = document.createElement("script"); s1.textContent = "w(1 + ' ' + document.head.childElementCount); document.head.removeChild(s2)"
var s2 = document.createElement("script"); s2.textContent = "w(2 + ' ' + document.head.childElementCount + ' ' + s2.parentNode)"
var df = document.createDocumentFragment(); df.appendChild(s1); df.appendChild(s2)
document.head.appendChild(df)
</script>
s2 will not execute in Chrome and Edge, but does execute in Firefox. Safari will throw an error for s1 because in Safari s2 is not in the tree yet when s1 executes.
So maybe another thing that's needed here is that script elements without parent do not execute?
Per https://html.spec.whatwg.org/#prepare-a-script browsers need to do a "connected" check, which means Firefox has a bug. (I want to check though if that check is correct or if it needs to be browsing-context connected.)
Another concern here is how this slightly post-insert notification interacts with slot changes. Basically, at what point do the shadow tree changes happen relative to notifying external consumers (which can execute script)?
With the following example I still get a slotchange event. I think that means that kind of tracking should happen before script execution, similar to queuing the mutation record.
<div id=host></div>
<script>
const host = document.getElementById("host"),
shadow = host.attachShadow({ mode: "closed" }),
slot = shadow.appendChild(document.createElement("slot")),
df = document.createDocumentFragment(),
s1 = df.appendChild(document.createElement("script")),
s2 = df.appendChild(document.createElement("script"));
slot.addEventListener("slotchange", e => console.log(e));
s1.textContent = "df.appendChild(s1); df.appendChild(s2);";
s2.textContent = "console.log('hmm');";
host.appendChild(df);
</script>
I also tested custom elements:
<script>
customElements.define("ce-1", class CE1 extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
console.log("ce-1 connected");
}
});
const df = document.createDocumentFragment(),
s = df.appendChild(document.createElement("script")),
ce1 = df.appendChild(document.createElement("ce-1"));
s.textContent = "df.appendChild(ce1);";
document.head.appendChild(df);
</script>
This logs "ce-1 connected" in Chrome and Firefox. It doesn't log anything in Safari. I'm inclined to side with Chrome and Firefox here.
<div id=host></div>
<script>
const host = document.getElementById("host"),
shadow = host.attachShadow({ mode: "closed" }),
slot = shadow.appendChild(document.createElement("slot")),
df = document.createDocumentFragment(),
s1 = df.appendChild(document.createElement("script")),
s2 = df.appendChild(document.createElement("script"));
slot.addEventListener("slotchange", e => console.log(e));
s1.textContent = "df.appendChild(s1); df.appendChild(s2);";
s2.textContent = "console.log('hmm');";
host.appendChild(df);
</script>
<script>
customElements.define("ce-1", class CE1 extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
console.log("ce-1 connected");
}
});
const df = document.createDocumentFragment(),
s = df.appendChild(document.createElement("script")),
ce1 = df.appendChild(document.createElement("ce-1"));
s.textContent = "df.appendChild(ce1);";
document.head.appendChild(df);
</script>
Another snippet that exposes differences between Firefox and Safari:
<body>
<select id="select"><option selected>1</option></select>
<script>
var select = document.getElementById("select");
var script = document.createElement("script");
script.textContent = "console.log(select.selectedIndex);";
var option = document.createElement("option");
option.textContent = "2";
option.selected = true;
select.append(script, option);
</script>
Changing selectedness of options is defined as part of inserting steps, AFAIK. This prints 0 in Safari and 1 in Firefox.
Oh joy, if I change things to
<!DOCTYPE html> <script> var s1 = document.createElement("script"); s1.textContent = "w(1 + ' ' + document.head.childElementCount); document.head.removeChild(s2)" var s2 = document.createElement("script"); s2.textContent = "w(2 + ' ' + document.head.childElementCount + ' ' + s2.parentNode)" var df = document.createDocumentFragment(); df.appendChild(s1); df.appendChild(s2) document.head.appendChild(df) </script>s2 will not execute in Chrome and Edge, but does execute in Firefox. Safari will throw an error for s1 because in Safari s2 is not in the tree yet when s1 executes.
So maybe another thing that's needed here is that
scriptelements without parent do not execute?
Safari follows the current spec entirely for this one snippet, here is what the Live DOM Viewer says:
log: 1 1
error: NotFoundError: The object can not be found here. on line 1
log: 2 2 [object HTMLHeadElement]
Which means it first inserts s1 and runs it, which raises a NotFoundError because s2 is not a child of head, then it inserts s2 and runs it.
I'm not really sure what to do with this information, in which way do we want the spec to go? Do we want to specify less differences between inserting a DocumentFragment and a plain old node?
This logs "ce-1 connected" in Chrome and Firefox. It doesn't log anything in Safari. I'm inclined to side with Chrome and Firefox here.
I guess that answers my question: we want children being inserted as part of a DocumentFragment node to behave more as if they were inserted as part of a plain old node.