feat(js): popover, legacy events, CDATA/PI, DOMTokenList, namedItem, attachShadow (#231)

Next batch of WPT dom/shadow-dom conformance fixes in bootstrap.js:
- Popover API: popover reflection, showPopover/hidePopover/togglePopover, ToggleEvent, :popover-open
- Legacy event init: initEvent/initUIEvent/initMouseEvent/initKeyboardEvent/initCompositionEvent, CompositionEvent
- Document.createCDATASection / createProcessingInstruction (XML-gated)
- classList as a real DOMTokenList (+ relList/sandbox/sizes/htmlFor), spec-correct namespaceURI
- getElementsByClassName ASCII-whitespace tokenization
- HTMLCollection.namedItem and named access; createRange on document/iframe/DOMParser
- attachShadow mode validation and a real ShadowRoot (cloneNode throws, activeElement, styleSheets)

querySelectorAll returns a NodeList; named access on live HTMLCollections is
resolved lazily via a Proxy so query hot paths stay at native speed.
This commit is contained in:
SGavrl authored and GitHub committed 2026-06-03 10:36:02 +00:00
1 parent dc2a2e3b52
commit 990f2a63c8
1 file changed
+565 -52
+565 -52
View File
@@ -121,6 +121,41 @@ const _eventRegistry = globalThis._eventRegistry;
const _formValues = globalThis._formValues; const _formValues = globalThis._formValues;
const _formChecked = globalThis._formChecked; const _formChecked = globalThis._formChecked;
const _domParse = (cmd, a1, a2) => { try { return JSON.parse(_dom(cmd, a1, a2)); } catch { return null; } }; const _domParse = (cmd, a1, a2) => { try { return JSON.parse(_dom(cmd, a1, a2)); } catch { return null; } };
// HTML "ASCII whitespace": U+0009 TAB, U+000A LF, U+000C FF, U+000D CR, U+0020 SPACE.
// Class token splitting (classList, getElementsByClassName) uses exactly this set.
// JS \s is wider (U+000B, U+00A0, U+2028, etc.), so it must not be used here.
const _ASCII_WS = /[ \t\n\f\r]+/;
function _splitAsciiWhitespace(s) {
// WebIDL DOMString coercion: null -> "null", undefined -> "undefined".
return String(s).split(_ASCII_WS).filter(Boolean);
}
// Shared getElementsByClassName: split the argument into an ordered set of
// tokens on ASCII whitespace, then return descendants (in tree order) whose
// class attribute contains every token, as an HTMLCollection (so namedItem and
// named access work on the result). `root` must expose querySelectorAll.
function _getElementsByClassName(root, classNames) {
const tokens = _splitAsciiWhitespace(classNames);
if (tokens.length === 0) return HTMLCollection._from([]);
// Fast path: a single CSS-identifier token goes straight to the native
// selector engine (the common case). Only multi-token sets or exotic class
// names (NBSP, leading digits, etc.) fall back to the O(n) JS scan below.
if (tokens.length === 1 && /^[A-Za-z_-][\w-]*$/.test(tokens[0])) {
return HTMLCollection._from(root.querySelectorAll("." + tokens[0]));
}
const all = root.querySelectorAll("*");
const matched = [];
for (let i = 0; i < all.length; i++) {
const el = all[i];
const elTokens = _splitAsciiWhitespace(el.getAttribute ? (el.getAttribute("class") || "") : "");
let ok = true;
for (let t = 0; t < tokens.length; t++) {
if (elTokens.indexOf(tokens[t]) < 0) { ok = false; break; }
}
if (ok) matched.push(el);
}
return HTMLCollection._from(matched);
}
const _consoleFn = (level, args) => { const _consoleFn = (level, args) => {
try { Deno.core.ops.op_console_msg(level, args.map(a => { try { Deno.core.ops.op_console_msg(level, args.map(a => {
if (a === null) return "null"; if (a === null) return "null";
@@ -468,6 +503,134 @@ class Comment extends CharacterData {
cloneNode() { return document.createComment(this.data); } cloneNode() { return document.createComment(this.data); }
} }
// DOMTokenList backs class/rel/sandbox/etc. attribute reflection. It parses the
// associated content attribute as an ordered set of tokens and writes changes
// straight back, so reads and writes stay live with the element. A Proxy is
// layered on top so numeric indexing (list[0]) hits item().
class DOMTokenList {
constructor(el, attr, supportedTokens) {
// Non-enumerable so the element <-> token-list cycle is not visible to
// enumeration/serialization (JSON.stringify(classList) would otherwise
// throw "circular structure").
Object.defineProperty(this, "_el", { value: el, writable: true, enumerable: false });
Object.defineProperty(this, "_attr", { value: attr, writable: true, enumerable: false });
Object.defineProperty(this, "_supported", { value: supportedTokens || null, writable: true, enumerable: false });
return new Proxy(this, {
get(t, k, r) {
if (typeof k === "string" && /^\d+$/.test(k)) return t.item(+k);
return Reflect.get(t, k, r);
},
has(t, k) {
if (typeof k === "string" && /^\d+$/.test(k)) return +k < t.length;
return Reflect.has(t, k);
},
});
}
get [Symbol.toStringTag]() { return "DOMTokenList"; }
_tokens() {
const v = this._el.getAttribute(this._attr);
if (!v) return [];
const seen = new Set();
const out = [];
for (const tok of v.split(/[ \t\n\f\r]+/)) {
if (tok && !seen.has(tok)) { seen.add(tok); out.push(tok); }
}
return out;
}
_write(tokens) {
this._el.setAttribute(this._attr, tokens.join(" "));
}
get length() { return this._tokens().length; }
get value() { return this._el.getAttribute(this._attr) || ""; }
set value(v) { this._el.setAttribute(this._attr, String(v)); }
item(i) { const t = this._tokens(); return (i >= 0 && i < t.length) ? t[i] : null; }
contains(token) { return this._tokens().includes(String(token)); }
add(...tokens) {
const t = this._tokens();
for (const raw of tokens) {
const tok = String(raw);
if (tok === "") throw new DOMException("The token provided must not be empty.", "SyntaxError");
if (/[ \t\n\f\r]/.test(tok)) throw new DOMException("The token provided contains HTML space characters, which are not valid in tokens.", "InvalidCharacterError");
if (!t.includes(tok)) t.push(tok);
}
this._write(t);
}
remove(...tokens) {
let t = this._tokens();
for (const raw of tokens) {
const tok = String(raw);
if (tok === "") throw new DOMException("The token provided must not be empty.", "SyntaxError");
if (/[ \t\n\f\r]/.test(tok)) throw new DOMException("The token provided contains HTML space characters, which are not valid in tokens.", "InvalidCharacterError");
t = t.filter((x) => x !== tok);
}
this._write(t);
}
toggle(token, force) {
const tok = String(token);
if (tok === "") throw new DOMException("The token provided must not be empty.", "SyntaxError");
if (/[ \t\n\f\r]/.test(tok)) throw new DOMException("The token provided contains HTML space characters, which are not valid in tokens.", "InvalidCharacterError");
const t = this._tokens();
const has = t.includes(tok);
if (has) {
if (force === true) return true;
this._write(t.filter((x) => x !== tok));
return false;
}
if (force === false) return false;
t.push(tok);
this._write(t);
return true;
}
replace(token, newToken) {
const a = String(token), b = String(newToken);
if (a === "" || b === "") throw new DOMException("The token provided must not be empty.", "SyntaxError");
if (/[ \t\n\f\r]/.test(a) || /[ \t\n\f\r]/.test(b)) throw new DOMException("The token provided contains HTML space characters, which are not valid in tokens.", "InvalidCharacterError");
const t = this._tokens();
const i = t.indexOf(a);
if (i === -1) return false;
if (t.includes(b) && b !== a) { t.splice(i, 1); } else { t[i] = b; }
this._write(t);
return true;
}
supports(token) {
if (!this._supported) throw new TypeError("DOMTokenList has no supported tokens.");
return this._supported.includes(String(token).toLowerCase());
}
forEach(cb, thisArg) {
const t = this._tokens();
for (let i = 0; i < t.length; i++) cb.call(thisArg, t[i], i, this);
}
*values() { yield* this._tokens(); }
*keys() { const t = this._tokens(); for (let i = 0; i < t.length; i++) yield i; }
*entries() { const t = this._tokens(); for (let i = 0; i < t.length; i++) yield [i, t[i]]; }
[Symbol.iterator]() { return this._tokens()[Symbol.iterator](); }
toString() { return this.value; }
}
// CDATASection: a Text-derived node (nodeType 4) used only in XML documents.
// Extends Text so data/length/textContent/childNodes reuse the working text
// node machinery; only the type-identifying getters differ.
class CDATASection extends Text {
get nodeName() { return "#cdata-section"; }
get nodeType() { return 4; }
get nodeValue() { return this.data; }
set nodeValue(v) { this.data = v; }
cloneNode() { return new CDATASection(+_dom("create_text_node", this.data)); }
}
// ProcessingInstruction: nodeType 7, nodeName === target. Extends CharacterData
// and carries a separate target. Backed by a text node so data/nodeValue/
// textContent/length work without native PI support.
class ProcessingInstruction extends CharacterData {
constructor(nid, target) { super(nid); this._target = target; }
get target() { return this._target; }
get nodeName() { return this._target; }
get nodeType() { return 7; }
get nodeValue() { return this.data; }
set nodeValue(v) { this.data = v; }
cloneNode() { return new ProcessingInstruction(+_dom("create_text_node", this.data), this._target); }
}
class Element extends Node { class Element extends Node {
constructor(nid) { constructor(nid) {
super(nid); super(nid);
@@ -480,8 +643,11 @@ class Element extends Node {
get className() { return this.getAttribute("class") || ""; } get className() { return this.getAttribute("class") || ""; }
set className(v) { this.setAttribute("class", v); } set className(v) { this.setAttribute("class", v); }
get namespaceURI() { get namespaceURI() {
const tag = this.localName; // createElementNS records the requested namespace on _ns; an empty string
if (tag === "svg" || this._ns === "http://www.w3.org/2000/svg") return "http://www.w3.org/2000/svg"; // maps to the null namespace per spec. Elements made via createElement (or
// parsed) have no _ns: default to XHTML, except <svg> which is SVG.
if (this._ns !== undefined) return this._ns === "" ? null : this._ns;
if (this.localName === "svg") return "http://www.w3.org/2000/svg";
return "http://www.w3.org/1999/xhtml"; return "http://www.w3.org/1999/xhtml";
} }
get innerHTML() { return _domParse("inner_html", this._nid) ?? ""; } get innerHTML() { return _domParse("inner_html", this._nid) ?? ""; }
@@ -512,7 +678,7 @@ class Element extends Node {
set innerText(v) { this.textContent = v; } set innerText(v) { this.textContent = v; }
get children() { get children() {
const ids = _domParse("element_children", this._nid) || []; const ids = _domParse("element_children", this._nid) || [];
return ids.map(_wrapEl).filter(Boolean); return HTMLCollection._from(ids.map(_wrapEl).filter(Boolean));
} }
get content() { get content() {
// <template>.content is a DocumentFragment; <meta>.content reflects // <template>.content is a DocumentFragment; <meta>.content reflects
@@ -539,28 +705,53 @@ class Element extends Node {
get nextElementSibling() { let s = this.nextSibling; while(s && s.nodeType !== 1) s = s.nextSibling; return s; } get nextElementSibling() { let s = this.nextSibling; while(s && s.nodeType !== 1) s = s.nextSibling; return s; }
get previousElementSibling() { let s = this.previousSibling; while(s && s.nodeType !== 1) s = s.previousSibling; return s; } get previousElementSibling() { let s = this.previousSibling; while(s && s.nodeType !== 1) s = s.previousSibling; return s; }
get classList() { get classList() {
const el = this; if (!this._classList) this._classList = new DOMTokenList(this, "class");
const obj = { return this._classList;
add: (...c) => { const s = new Set((el.className||"").split(/\s+/).filter(Boolean)); c.forEach(x=>s.add(x)); el.className=[...s].join(" "); }, }
remove: (...c) => { const s = new Set((el.className||"").split(/\s+/).filter(Boolean)); c.forEach(x=>s.delete(x)); el.className=[...s].join(" "); }, get relList() {
contains: c => (el.className||"").split(/\s+/).includes(c), const ns = this.namespaceURI, ln = this.localName;
toggle: (c, force) => { const has = obj.contains(c); if(force===true||(!has&&force!==false)){obj.add(c);return true;} obj.remove(c); return false; }, const ok = (ns === "http://www.w3.org/2000/svg" && ln === "a") ||
get length() { return (el.className||"").split(/\s+/).filter(Boolean).length; }, (ns === "http://www.w3.org/1999/xhtml" && (ln === "a" || ln === "area" || ln === "link"));
item: i => (el.className||"").split(/\s+/).filter(Boolean)[i] || null, if (!ok) return undefined;
forEach: (cb) => (el.className||"").split(/\s+/).filter(Boolean).forEach(cb), if (!this._relList) this._relList = new DOMTokenList(this, "rel");
toString: () => el.className || "", return this._relList;
}; }
return obj; get sandbox() {
if (this.namespaceURI !== "http://www.w3.org/1999/xhtml" || this.localName !== "iframe") return undefined;
if (!this._sandboxList) this._sandboxList = new DOMTokenList(this, "sandbox");
return this._sandboxList;
}
get sizes() {
if (this.namespaceURI !== "http://www.w3.org/1999/xhtml" || this.localName !== "link") return undefined;
if (!this._sizesList) this._sizesList = new DOMTokenList(this, "sizes");
return this._sizesList;
}
get htmlFor() {
if (this.namespaceURI !== "http://www.w3.org/1999/xhtml") return undefined;
const ln = this.localName;
if (ln === "output") {
if (!this._htmlForList) this._htmlForList = new DOMTokenList(this, "for");
return this._htmlForList;
}
if (ln === "label") return this.getAttribute("for") || "";
return undefined;
}
set htmlFor(v) {
if (this.namespaceURI === "http://www.w3.org/1999/xhtml" && this.localName === "label") {
this.setAttribute("for", String(v));
}
} }
get style() { return this._style; } get style() { return this._style; }
set style(v) { if (typeof v === "string") this._style.cssText = v; } set style(v) { if (typeof v === "string") this._style.cssText = v; }
getAttribute(n) { return _domParse("get_attribute", this._nid, n); } getAttribute(n) { return _domParse("get_attribute", this._nid, n); }
setAttribute(n, v) { setAttribute(n, v) {
const popoverPrev = (n === "popover" || (typeof n === "string" && n.length === 7 && n.toLowerCase() === "popover")) ? this.popover : undefined;
_dom("set_attribute", this._nid, n + "\0" + String(v)); _dom("set_attribute", this._nid, n + "\0" + String(v));
if (popoverPrev !== undefined) this._popoverTypeMaybeChanged(popoverPrev);
if (globalThis.__mutationObservers?.length) globalThis.__notifyMutation('attributes', this._nid, [], [], n); if (globalThis.__mutationObservers?.length) globalThis.__notifyMutation('attributes', this._nid, [], [], n);
} }
setAttributeNS(ns, n, v) { this.setAttribute(n, v); } // Simplified NS handling setAttributeNS(ns, n, v) { this.setAttribute(n, v); } // Simplified NS handling
removeAttribute(n) { _dom("remove_attribute", this._nid, n); } removeAttribute(n) { const popoverPrev = (n === "popover" || (typeof n === "string" && n.length === 7 && n.toLowerCase() === "popover")) ? this.popover : undefined; _dom("remove_attribute", this._nid, n); if (popoverPrev !== undefined) this._popoverTypeMaybeChanged(popoverPrev); }
removeAttributeNS(ns, n) { this.removeAttribute(n); } removeAttributeNS(ns, n) { this.removeAttribute(n); }
hasAttribute(n) { return this.getAttribute(n) !== null; } hasAttribute(n) { return this.getAttribute(n) !== null; }
hasAttributes() { return true; } // Simplified hasAttributes() { return true; } // Simplified
@@ -593,14 +784,20 @@ class Element extends Node {
querySelector(s) { return _wrapEl(+_dom("query_selector_scoped", this._nid, s)); } querySelector(s) { return _wrapEl(+_dom("query_selector_scoped", this._nid, s)); }
querySelectorAll(s) { querySelectorAll(s) {
const ids = _domParse("query_selector_all_scoped", this._nid, s) || []; const ids = _domParse("query_selector_all_scoped", this._nid, s) || [];
const list = ids.map(_wrapEl).filter(Boolean); return _nodeList(ids.map(_wrapEl).filter(Boolean));
list.item = (i) => list[i] || null;
list.forEach = Array.prototype.forEach.bind(list);
return list;
} }
getElementsByTagName(t) { return this.querySelectorAll(t); } getElementsByTagName(t) { return HTMLCollection._from(this.querySelectorAll(t)); }
getElementsByClassName(c) { return this.querySelectorAll("." + c); } getElementsByClassName(c) { return _getElementsByClassName(this, c); }
matches(s) { matches(s) {
// :popover-open is a JS-observable popover state, not understood by the
// native selector engine. Handle it here (and strip it from compound
// selectors so the rest can still be matched natively).
if (typeof s === "string" && s.indexOf(":popover-open") !== -1) {
if (this._popoverState !== "showing") return false;
const rest = s.replace(/:popover-open/g, "").trim();
if (rest === "") return true;
return this.matches(rest);
}
const parent = this.parentNode; const parent = this.parentNode;
if (!parent || !parent.querySelectorAll) return false; if (!parent || !parent.querySelectorAll) return false;
const matches = parent.querySelectorAll(s); const matches = parent.querySelectorAll(s);
@@ -710,6 +907,97 @@ class Element extends Node {
} }
focus() { globalThis.__obscura_focused = this; globalThis.__obscura_click_target = this; } focus() { globalThis.__obscura_focused = this; globalThis.__obscura_click_target = this; }
blur() { if (globalThis.__obscura_focused === this) globalThis.__obscura_focused = null; } blur() { if (globalThis.__obscura_focused === this) globalThis.__obscura_focused = null; }
// --- Popover API (HTML "popover") ---------------------------------------
// Read the popover content attribute case-insensitively. The HTML parser
// lowercases attribute names, but runtime setAttribute("PoPoVeR", ...)
// preserves case, and the IDL reflection matches the name ASCII-case-
// insensitively. Returns the raw stored string, or null if absent.
_popoverAttrValue() {
const v = this.getAttribute("popover");
if (v !== null) return v;
const names = _domParse("attribute_names", this._nid) || [];
for (let i = 0; i < names.length; i++) {
if (names[i].toLowerCase() === "popover") return this.getAttribute(names[i]);
}
return null;
}
// The reflected (effective) popover type: null (No Popover), "auto",
// "hint", or "manual". Empty string maps to "auto"; any non-keyword value
// (invalid) maps to "manual".
get popover() {
const raw = this._popoverAttrValue();
if (raw === null) return null;
const v = String(raw).toLowerCase();
if (v === "auto" || v === "hint" || v === "manual") return v;
if (v === "") return "auto";
return "manual";
}
set popover(value) {
if (value === null || value === undefined) { this._popoverRemoveAttr(); return; }
this.setAttribute("popover", String(value));
}
_popoverRemoveAttr() {
if (this.getAttribute("popover") !== null) { this.removeAttribute("popover"); return; }
const names = _domParse("attribute_names", this._nid) || [];
for (let i = 0; i < names.length; i++) {
if (names[i].toLowerCase() === "popover") { this.removeAttribute(names[i]); return; }
}
}
// "check popover validity". expectedToBeShowing is true for hide, false for
// show. Throws NotSupportedError when there is no valid popover type, and
// InvalidStateError when the element is not connected; returns false (no
// throw) when the current state does not match expectedToBeShowing.
_checkPopoverValidity(expectedToBeShowing) {
if (this.popover === null) throw new DOMException("Not supported on elements that don't have a valid value for the popover attribute", "NotSupportedError");
const showing = this._popoverState === "showing";
if ((expectedToBeShowing && !showing) || (!expectedToBeShowing && showing)) return false;
if (!this.isConnected) throw new DOMException("Invalid on popover elements which aren't connected", "InvalidStateError");
return true;
}
showPopover() {
if (!this._checkPopoverValidity(/*expectedToBeShowing*/false)) return;
const beforeEvent = new ToggleEvent("beforetoggle", { cancelable: true, oldState: "closed", newState: "open" });
if (!this.dispatchEvent(beforeEvent)) return;
// The beforetoggle handler may have changed our type or shown us; re-check.
if (!this._checkPopoverValidity(/*expectedToBeShowing*/false)) return;
this._popoverState = "showing";
const target = this;
setTimeout(() => { try { target.dispatchEvent(new ToggleEvent("toggle", { oldState: "closed", newState: "open" })); } catch (e) {} }, 0);
}
hidePopover() {
if (!this._checkPopoverValidity(/*expectedToBeShowing*/true)) return;
this.dispatchEvent(new ToggleEvent("beforetoggle", { oldState: "open", newState: "closed" }));
this._popoverState = "hidden";
const target = this;
setTimeout(() => { try { target.dispatchEvent(new ToggleEvent("toggle", { oldState: "open", newState: "closed" })); } catch (e) {} }, 0);
}
togglePopover(force) {
let options = force;
if (options && typeof options === "object") force = options.force;
const showing = this._popoverState === "showing";
if (showing && (force === undefined || force === null || force === false)) {
this.hidePopover();
} else if (force === undefined || force === null || force === true) {
this.showPopover();
}
return this._popoverState === "showing";
}
// Called from setAttribute/removeAttribute/IDL setter when the popover
// attribute may have changed. If the effective type changed while showing,
// hide the popover (firing the hide events) per the HTML spec.
_popoverTypeMaybeChanged(prevType) {
const newType = this.popover;
if (this._popoverState === "showing" && prevType !== newType) {
// Hide directly. Do not call hidePopover(): it re-validates against the
// popover attribute, which may now be removed (No Popover), and would
// throw NotSupportedError. This mirrors the spec hide with throw=false.
this.dispatchEvent(new ToggleEvent("beforetoggle", { oldState: "open", newState: "closed" }));
this._popoverState = "hidden";
const target = this;
setTimeout(() => { try { target.dispatchEvent(new ToggleEvent("toggle", { oldState: "open", newState: "closed" })); } catch (e) {} }, 0);
}
}
get value() { get value() {
const tag = this.localName; const tag = this.localName;
if (tag === 'select') { if (tag === 'select') {
@@ -846,7 +1134,7 @@ class Element extends Node {
} }
get options() { get options() {
if (this.localName !== 'select') return []; if (this.localName !== 'select') return [];
return this.querySelectorAll('option'); return HTMLCollection._from(this.querySelectorAll('option'));
} }
get selectedIndex() { get selectedIndex() {
const opts = this.options; const opts = this.options;
@@ -1067,7 +1355,20 @@ class Document extends Node {
get ownerDocument() { return null; } // Document has no ownerDocument get ownerDocument() { return null; } // Document has no ownerDocument
get compatMode() { return "CSS1Compat"; } get compatMode() { return "CSS1Compat"; }
get characterSet() { return "UTF-8"; } get characterSet() { return "UTF-8"; }
get contentType() { return "text/html"; } get contentType() {
const url = this.URL || "";
// data: URLs carry their MIME type explicitly.
const dm = /^data:([^,;]+)/i.exec(url);
if (dm) {
const mime = dm[1].toLowerCase();
if (mime === "application/xhtml+xml") return "application/xhtml+xml";
if (mime === "text/xml") return "text/xml";
if (mime === "application/xml" || mime.endsWith("+xml")) return "application/xml";
}
if (/\.xhtml(?:[?#]|$)/i.test(url)) return "application/xhtml+xml";
if (/\.(?:xml|svg)(?:[?#]|$)/i.test(url)) return "application/xml";
return "text/html";
}
get readyState() { return globalThis.__documentReadyState__ || 'complete'; } get readyState() { return globalThis.__documentReadyState__ || 'complete'; }
get currentScript() { get currentScript() {
// Next.js / Turbopack chunk loader reads document.currentScript.src to // Next.js / Turbopack chunk loader reads document.currentScript.src to
@@ -1082,13 +1383,10 @@ class Document extends Node {
querySelector(s) { return _wrapEl(+_dom("query_selector", s)); } querySelector(s) { return _wrapEl(+_dom("query_selector", s)); }
querySelectorAll(s) { querySelectorAll(s) {
const ids = _domParse("query_selector_all", s) || []; const ids = _domParse("query_selector_all", s) || [];
const list = ids.map(_wrapEl).filter(Boolean); return _nodeList(ids.map(_wrapEl).filter(Boolean));
list.item = (i) => list[i] || null;
list.forEach = Array.prototype.forEach.bind(list);
return list;
} }
getElementsByTagName(t) { return this.querySelectorAll(t); } getElementsByTagName(t) { return HTMLCollection._from(this.querySelectorAll(t)); }
getElementsByClassName(c) { return this.querySelectorAll("." + c); } getElementsByClassName(c) { return _getElementsByClassName(this, c); }
getElementsByName(name) { return this.querySelectorAll('[name="' + String(name).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"]'); } getElementsByName(name) { return this.querySelectorAll('[name="' + String(name).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"]'); }
createElement(t) { createElement(t) {
const el = _wrapEl(+_dom("create_element", t.toLowerCase())); const el = _wrapEl(+_dom("create_element", t.toLowerCase()));
@@ -1109,6 +1407,37 @@ class Document extends Node {
_cache.set(nid, n); _cache.set(nid, n);
return n; return n;
} }
createCDATASection(data) {
// Spec: throw NotSupportedError on an HTML document, reject data
// containing "]]>", then return a CDATASection node.
if (!_isXMLDocument(this)) {
throw new DOMException("createCDATASection is not supported in HTML documents", "NotSupportedError");
}
const str = String(data);
if (str.indexOf("]]>") !== -1) {
throw new DOMException("CDATA section data must not contain ']]>'", "InvalidCharacterError");
}
const nid = +_dom("create_text_node", str);
const n = new CDATASection(nid);
_cache.set(nid, n);
return n;
}
createProcessingInstruction(target, data) {
// Spec: not gated on document type. Reject targets that are not an XML
// Name, then reject data containing "?>", then return a PI node.
const tgt = String(target);
const str = String(data);
if (!_isValidPITarget(tgt)) {
throw new DOMException("Invalid processing instruction target", "InvalidCharacterError");
}
if (str.indexOf("?>") !== -1) {
throw new DOMException("Processing instruction data must not contain '?>'", "InvalidCharacterError");
}
const nid = +_dom("create_text_node", str);
const n = new ProcessingInstruction(nid, tgt);
_cache.set(nid, n);
return n;
}
createDocumentFragment() { createDocumentFragment() {
const nid = +_dom("create_document_fragment"); const nid = +_dom("create_document_fragment");
const frag = new DocumentFragment(nid); const frag = new DocumentFragment(nid);
@@ -1127,6 +1456,7 @@ class Document extends Node {
'focusevent': FocusEvent, 'focusevent': FocusEvent,
'inputevent': InputEvent, 'inputevent': InputEvent,
'uievent': UIEvent, 'uievents': UIEvent, 'uievent': UIEvent, 'uievents': UIEvent,
'compositionevent': CompositionEvent,
'wheelevent': WheelEvent, 'wheelevent': WheelEvent,
'pointerevent': PointerEvent, 'pointerevent': PointerEvent,
'errorevent': ErrorEvent, 'errorevent': ErrorEvent,
@@ -1137,7 +1467,7 @@ class Document extends Node {
const Cls = map[String(type || '').toLowerCase()] || Event; const Cls = map[String(type || '').toLowerCase()] || Event;
return new Cls(''); return new Cls('');
} }
createRange() { return { setStart(){}, setEnd(){}, collapse(){}, selectNodeContents(){}, cloneContents(){ return document.createDocumentFragment(); } }; } createRange() { return new Range(); }
addEventListener(type, fn, opts) { addEventListener(type, fn, opts) {
if (typeof fn !== 'function') return; if (typeof fn !== 'function') return;
if (!this._listeners) this._listeners = {}; if (!this._listeners) this._listeners = {};
@@ -1328,13 +1658,11 @@ class DocumentFragment extends Node {
querySelector(s) { return _wrapEl(+_dom("query_selector_scoped", this._nid, s)); } querySelector(s) { return _wrapEl(+_dom("query_selector_scoped", this._nid, s)); }
querySelectorAll(s) { querySelectorAll(s) {
const ids = _domParse("query_selector_all_scoped", this._nid, s) || []; const ids = _domParse("query_selector_all_scoped", this._nid, s) || [];
const list = ids.map(_wrapEl).filter(Boolean); return _nodeList(ids.map(_wrapEl).filter(Boolean));
list.item = (i) => list[i] || null;
return list;
} }
get children() { get children() {
const ids = _domParse("element_children", this._nid) || []; const ids = _domParse("element_children", this._nid) || [];
return ids.map(_wrapEl).filter(Boolean); return HTMLCollection._from(ids.map(_wrapEl).filter(Boolean));
} }
get firstElementChild() { return this.children[0] || null; } get firstElementChild() { return this.children[0] || null; }
get lastElementChild() { const ch = this.children; return ch[ch.length - 1] || null; } get lastElementChild() { const ch = this.children; return ch[ch.length - 1] || null; }
@@ -2426,7 +2754,8 @@ globalThis.__notifyMutation = function(type, target_nid, addedNodes, removedNode
} }
}; };
globalThis.ShadowRoot = class ShadowRoot {}; globalThis.ShadowRoot = class ShadowRoot extends DocumentFragment {};
globalThis.__obscura_shadowHostNames = new Set(['article','aside','blockquote','body','div','footer','h1','h2','h3','h4','h5','h6','header','main','nav','p','section','span']);
globalThis.customElements = { globalThis.customElements = {
_registry: new Map(), _registry: new Map(),
_whenDefinedResolvers: new Map(), _whenDefinedResolvers: new Map(),
@@ -2639,7 +2968,7 @@ globalThis.Event = class Event {
constructor(t,o={}) { this.type=t;this.bubbles=!!o.bubbles;this.cancelable=!!o.cancelable;this.composed=!!o.composed;this.defaultPrevented=false;this.target=null;this.currentTarget=null;this.eventPhase=0;this.timeStamp=Date.now();this._propagationStopped=false;this._immediatePropagationStopped=false; } constructor(t,o={}) { this.type=t;this.bubbles=!!o.bubbles;this.cancelable=!!o.cancelable;this.composed=!!o.composed;this.defaultPrevented=false;this.target=null;this.currentTarget=null;this.eventPhase=0;this.timeStamp=Date.now();this._propagationStopped=false;this._immediatePropagationStopped=false; }
get isTrusted() { return true; } get isTrusted() { return true; }
preventDefault() { if (this.cancelable) this.defaultPrevented=true; } stopPropagation(){ this._propagationStopped=true; } stopImmediatePropagation(){ this._propagationStopped=true; this._immediatePropagationStopped=true; } preventDefault() { if (this.cancelable) this.defaultPrevented=true; } stopPropagation(){ this._propagationStopped=true; } stopImmediatePropagation(){ this._propagationStopped=true; this._immediatePropagationStopped=true; }
initEvent(type,bubbles,cancelable) { this.type=type;this.bubbles=!!bubbles;this.cancelable=!!cancelable;this.defaultPrevented=false;this._propagationStopped=false;this._immediatePropagationStopped=false; } initEvent(type,bubbles,cancelable) { if (arguments.length < 1) throw new TypeError("Failed to execute 'initEvent' on 'Event': 1 argument required, but only 0 present."); this.type=String(type);this.bubbles=!!bubbles;this.cancelable=!!cancelable;this.defaultPrevented=false;this._propagationStopped=false;this._immediatePropagationStopped=false; }
composedPath() { composedPath() {
if (!this.target) return []; if (!this.target) return [];
const path = []; const path = [];
@@ -2661,16 +2990,69 @@ globalThis.CustomEvent = class extends Event {
this.detail = detail; this.detail = detail;
} }
}; };
globalThis.MouseEvent = class extends Event { constructor(t,o={}) { super(t,o);this.clientX=o.clientX||0;this.clientY=o.clientY||0; } }; globalThis.MouseEvent = class extends Event {
globalThis.KeyboardEvent = class extends Event { constructor(t,o={}) { super(t,o);this.key=o.key||"";this.code=o.code||""; } }; constructor(t,o={}) { super(t,o);this.view=o.view||null;this.detail=o.detail||0;this.screenX=o.screenX||0;this.screenY=o.screenY||0;this.clientX=o.clientX||0;this.clientY=o.clientY||0;this.ctrlKey=!!o.ctrlKey;this.altKey=!!o.altKey;this.shiftKey=!!o.shiftKey;this.metaKey=!!o.metaKey;this.button=o.button||0;this.buttons=o.buttons||0;this.relatedTarget=o.relatedTarget||null; }
globalThis.FocusEvent = class extends Event {}; // Legacy DOM Level 2 initializer. Positional signature per UI Events spec.
initMouseEvent(type,canBubble,cancelable,view,detail,screenX,screenY,clientX,clientY,ctrlKey,altKey,shiftKey,metaKey,button,relatedTarget) {
if (arguments.length < 1) throw new TypeError("Failed to execute 'initMouseEvent' on 'MouseEvent': 1 argument required, but only 0 present.");
this.initEvent(type,canBubble,cancelable);
this.view=view===undefined?null:view;
this.detail=detail||0;
this.screenX=screenX||0;
this.screenY=screenY||0;
this.clientX=clientX||0;
this.clientY=clientY||0;
this.ctrlKey=!!ctrlKey;
this.altKey=!!altKey;
this.shiftKey=!!shiftKey;
this.metaKey=!!metaKey;
this.button=button||0;
this.relatedTarget=relatedTarget===undefined?null:relatedTarget;
}
};
globalThis.KeyboardEvent = class extends Event {
constructor(t,o={}) { super(t,o);this.view=o.view||null;this.detail=o.detail||0;this.key=o.key||"";this.code=o.code||"";this.location=o.location||0;this.ctrlKey=!!o.ctrlKey;this.altKey=!!o.altKey;this.shiftKey=!!o.shiftKey;this.metaKey=!!o.metaKey;this.repeat=!!o.repeat; }
// Legacy DOM Level 3 initializer. Positional signature per the WebKit/Gecko form.
initKeyboardEvent(type,canBubble,cancelable,view,key,location,ctrlKey,altKey,shiftKey,metaKey) {
if (arguments.length < 1) throw new TypeError("Failed to execute 'initKeyboardEvent' on 'KeyboardEvent': 1 argument required, but only 0 present.");
this.initEvent(type,canBubble,cancelable);
this.view=view===undefined?null:view;
this.key=key===undefined?"":String(key);
this.location=location||0;
this.ctrlKey=!!ctrlKey;
this.altKey=!!altKey;
this.shiftKey=!!shiftKey;
this.metaKey=!!metaKey;
}
};
globalThis.FocusEvent = class extends Event { constructor(t,o={}) { super(t,o);this.relatedTarget=o.relatedTarget||null; } };
globalThis.InputEvent = class extends Event { constructor(t,o={}) { super(t,o);this.data=o.data||null;this.inputType=o.inputType||""; } }; globalThis.InputEvent = class extends Event { constructor(t,o={}) { super(t,o);this.data=o.data||null;this.inputType=o.inputType||""; } };
globalThis.ErrorEvent = class extends Event { constructor(t,o={}) { super(t,o);this.message=o.message||"";this.error=o.error||null; } }; globalThis.ErrorEvent = class extends Event { constructor(t,o={}) { super(t,o);this.message=o.message||"";this.error=o.error||null; } };
globalThis.PointerEvent = class extends Event { constructor(t,o={}) { super(t,o); } }; globalThis.PointerEvent = class extends Event { constructor(t,o={}) { super(t,o); } };
globalThis.AnimationEvent = class extends Event {}; globalThis.AnimationEvent = class extends Event {};
globalThis.TransitionEvent = class extends Event {}; globalThis.TransitionEvent = class extends Event {};
globalThis.UIEvent = class extends Event {}; globalThis.UIEvent = class extends Event {
globalThis.WheelEvent = class extends Event {}; constructor(t,o={}) { super(t,o);this.view=o.view||null;this.detail=o.detail||0; }
// Legacy DOM Level 2 initializer. Positional signature per UI Events spec.
initUIEvent(type,canBubble,cancelable,view,detail) {
if (arguments.length < 1) throw new TypeError("Failed to execute 'initUIEvent' on 'UIEvent': 1 argument required, but only 0 present.");
this.initEvent(type,canBubble,cancelable);
this.view=view===undefined?null:view;
this.detail=detail||0;
}
};
globalThis.WheelEvent = class extends Event { constructor(t,o={}) { super(t,o);this.deltaX=o.deltaX||0;this.deltaY=o.deltaY||0;this.deltaZ=o.deltaZ||0;this.deltaMode=o.deltaMode||0; } };
globalThis.CompositionEvent = class extends Event {
constructor(t,o={}) { super(t,o);this.view=o.view||null;this.detail=o.detail||0;this.data=o.data||""; }
// Legacy DOM Level 3 initializer. Positional signature per UI Events spec.
initCompositionEvent(type,canBubble,cancelable,view,data) {
if (arguments.length < 1) throw new TypeError("Failed to execute 'initCompositionEvent' on 'CompositionEvent': 1 argument required, but only 0 present.");
this.initEvent(type,canBubble,cancelable);
this.view=view===undefined?null:view;
this.data=data===undefined?"":String(data);
}
};
globalThis.PopStateEvent = class extends Event { globalThis.PopStateEvent = class extends Event {
constructor(type, init) { constructor(type, init) {
super(type, init || {}); super(type, init || {});
@@ -2686,6 +3068,19 @@ globalThis.MessageEvent = class extends Event { constructor(t,o={}) { super(t,o)
globalThis.ClipboardEvent = class extends Event {}; globalThis.ClipboardEvent = class extends Event {};
globalThis.SubmitEvent = class extends Event {}; globalThis.SubmitEvent = class extends Event {};
// ToggleEvent backs the popover beforetoggle/toggle events. oldState and
// newState are "open"/"closed". These events do not bubble; beforetoggle is
// cancelable only for the closed -> open (show) transition, toggle is never
// cancelable. See HTML "popover" and html/semantics/popovers WPT.
globalThis.ToggleEvent = class ToggleEvent extends Event {
constructor(type, init = {}) {
super(type, init);
this.oldState = init.oldState !== undefined ? String(init.oldState) : "";
this.newState = init.newState !== undefined ? String(init.newState) : "";
}
};
_markNative(globalThis.ToggleEvent);
globalThis.AbortController = class AbortController { constructor(){this.signal={aborted:false,addEventListener(){},removeEventListener(){},onabort:null};} abort(){this.signal.aborted=true;} }; globalThis.AbortController = class AbortController { constructor(){this.signal={aborted:false,addEventListener(){},removeEventListener(){},onabort:null};} abort(){this.signal.aborted=true;} };
globalThis.AbortSignal = { timeout(ms){return {aborted:false,addEventListener(){},removeEventListener(){}}; } }; globalThis.AbortSignal = { timeout(ms){return {aborted:false,addEventListener(){},removeEventListener(){}}; } };
if (typeof Blob === "undefined") globalThis.Blob = class Blob { constructor(parts=[],opts={}){this._data=parts.join("");this.size=this._data.length;this.type=opts.type||"";} async text(){return this._data;} }; if (typeof Blob === "undefined") globalThis.Blob = class Blob { constructor(parts=[],opts={}){this._data=parts.join("");this.size=this._data.length;this.type=opts.type||"";} async text(){return this._data;} };
@@ -2768,7 +3163,7 @@ globalThis.DOMParser = class DOMParser {
return root.querySelectorAll(t); return root.querySelectorAll(t);
}, },
getElementsByClassName(c) { getElementsByClassName(c) {
return root.querySelectorAll("." + c); return _getElementsByClassName(root, c);
}, },
getElementsByName(n) { getElementsByName(n) {
return root.querySelectorAll(`[name="${n}"]`); return root.querySelectorAll(`[name="${n}"]`);
@@ -2778,6 +3173,20 @@ globalThis.DOMParser = class DOMParser {
createTextNode: (t) => document.createTextNode(t), createTextNode: (t) => document.createTextNode(t),
createComment: (t) => document.createComment(t), createComment: (t) => document.createComment(t),
createDocumentFragment: () => document.createDocumentFragment(), createDocumentFragment: () => document.createDocumentFragment(),
createRange: () => new Range(),
createEvent: (type) => document.createEvent(type),
createCDATASection: (data) => {
if (mimeType === "text/html") throw new DOMException("createCDATASection is not supported in HTML documents", "NotSupportedError");
const s = String(data);
if (s.indexOf("]]>") !== -1) throw new DOMException("CDATA section data must not contain ']]>'", "InvalidCharacterError");
return new CDATASection(+_dom("create_text_node", s));
},
createProcessingInstruction: (target, data) => {
const t = String(target), s = String(data);
if (!_isValidPITarget(t)) throw new DOMException("Invalid processing instruction target", "InvalidCharacterError");
if (s.indexOf("?>") !== -1) throw new DOMException("Processing instruction data must not contain '?>'", "InvalidCharacterError");
return new ProcessingInstruction(+_dom("create_text_node", s), t);
},
adoptNode: (n) => n, adoptNode: (n) => n,
importNode: (n) => n, importNode: (n) => n,
cloneNode: function (deep) { cloneNode: function (deep) {
@@ -2986,7 +3395,7 @@ globalThis.HTMLImageElement = Element;
globalThis.HTMLInputElement = Element; globalThis.HTMLInputElement = Element;
globalThis.HTMLButtonElement = Element; globalThis.HTMLButtonElement = Element;
globalThis.HTMLFormElement = class HTMLFormElement extends Element { globalThis.HTMLFormElement = class HTMLFormElement extends Element {
get elements() { return this.querySelectorAll("input, select, textarea, button, fieldset, output, object"); } get elements() { return HTMLCollection._from(this.querySelectorAll("input, select, textarea, button, fieldset, output, object")); }
get length() { return this.elements.length; } get length() { return this.elements.length; }
// Inherit submit() from Element.prototype: it dispatches the cancelable // Inherit submit() from Element.prototype: it dispatches the cancelable
// 'submit' event and (if not prevented) builds form data and navigates. // 'submit' event and (if not prevented) builds form data and navigates.
@@ -3028,6 +3437,22 @@ globalThis.SVGSVGElement = Element;
globalThis.CharacterData = CharacterData; globalThis.CharacterData = CharacterData;
globalThis.Text = Text; globalThis.Text = Text;
globalThis.Comment = Comment; globalThis.Comment = Comment;
globalThis.CDATASection = CDATASection;
globalThis.ProcessingInstruction = ProcessingInstruction;
// True when the document was loaded from an XML/XHTML source. Obscura has no
// native XML tree, so this is inferred from contentType (derived from the URL).
function _isXMLDocument(doc) {
const ct = (doc && doc.contentType) || "text/html";
return ct !== "text/html";
}
// XML Name production, sufficient for createProcessingInstruction targets.
const _piNameStart = "A-Za-z_:\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD";
const _piNameChar = _piNameStart + "0-9.\\u00B7\\u0300-\\u036F\\u203F-\\u2040\\-";
const _piNameRe = new RegExp("^[" + _piNameStart + "][" + _piNameChar + "]*$");
function _isValidPITarget(target) {
return typeof target === "string" && target.length > 0 && _piNameRe.test(target);
}
globalThis.DocumentFragment = DocumentFragment; globalThis.DocumentFragment = DocumentFragment;
globalThis.DocumentType = DocumentType; globalThis.DocumentType = DocumentType;
globalThis.Node = Node; globalThis.Node = Node;
@@ -3042,15 +3467,63 @@ for (const _proto of [Document.prototype, DocumentFragment.prototype]) {
} }
globalThis.EventTarget = Node; globalThis.EventTarget = Node;
globalThis.HTMLCollection = class HTMLCollection extends Array { globalThis.HTMLCollection = class HTMLCollection extends Array {
item(i) { return this[i] != null ? this[i] : null; } item(i) {
i = i >>> 0;
return this[i] != null ? this[i] : null;
}
namedItem(name) { namedItem(name) {
if (!name) return null; if (name === undefined || name === null || name === "") return null;
for (const el of this) { name = String(name);
if (el && (el.id === name || (typeof el.getAttribute === "function" && el.getAttribute("name") === name))) return el; for (let i = 0; i < this.length; i++) {
const el = this[i];
if (!el) continue;
// id always contributes; name only for HTML elements in HTML documents.
if (el.id === name) return el;
if (_isHTMLEl(el) && typeof el.getAttribute === "function" && el.getAttribute("name") === name) return el;
} }
return null; return null;
} }
// Factory: build an HTMLCollection from an array of elements. Named access
// (collection[name]) is served lazily by a Proxy so there is NO per-element
// work at build time (eager defineProperty per id was an O(n) build cost that
// made querySelectorAll on large result sets ~26x slower). The Proxy only
// resolves a name when an unknown string key is actually read.
static _from(arr) {
const c = new HTMLCollection();
if (arr) for (let i = 0; i < arr.length; i++) { if (arr[i]) c[c.length] = arr[i]; }
return new Proxy(c, _htmlCollectionProxy);
}
}; };
_markNative(HTMLCollection.prototype.item);
_markNative(HTMLCollection.prototype.namedItem);
// Shared (allocated once) Proxy traps for HTMLCollection named access. Indices,
// length, and inherited methods resolve normally via Reflect; only an unknown
// non-numeric string key falls back to namedItem(), so item/namedItem and the
// Array methods are never shadowed and id="namedItem" cannot recurse.
const _htmlCollectionProxy = {
get(t, k, r) {
const v = Reflect.get(t, k, r);
if (v !== undefined || typeof k !== "string") return v;
return t.namedItem ? (t.namedItem(k) || undefined) : undefined;
},
has(t, k) {
if (Reflect.has(t, k)) return true;
return typeof k === "string" && !!(t.namedItem && t.namedItem(k));
},
};
// True for elements in the HTML namespace (the only ones whose name attribute
// contributes to an HTMLCollection's supported property names).
function _isHTMLEl(el) {
return !!el && (el.namespaceURI === undefined || el.namespaceURI === "http://www.w3.org/1999/xhtml");
}
// Build a NodeList (no named access, per spec) for querySelectorAll. Kept light
// on purpose: querySelectorAll is the hottest query API.
function _nodeList(els) {
const nl = new NodeList();
for (let i = 0; i < els.length; i++) nl[nl.length] = els[i];
return nl;
}
globalThis.DOMTokenList = DOMTokenList;
globalThis.NodeList = class NodeList extends Array { globalThis.NodeList = class NodeList extends Array {
item(i) { return this[i] != null ? this[i] : null; } item(i) { return this[i] != null ? this[i] : null; }
}; };
@@ -3081,6 +3554,7 @@ globalThis.Range = class Range { setStart(){} setEnd(){} collapse(){} selectNode
Element.prototype.addEventListener, Element.prototype.removeEventListener, Element.prototype.addEventListener, Element.prototype.removeEventListener,
Element.prototype.dispatchEvent, Element.prototype.click, Element.prototype.dispatchEvent, Element.prototype.click,
Element.prototype.focus, Element.prototype.blur, Element.prototype.focus, Element.prototype.blur,
Element.prototype.showPopover, Element.prototype.hidePopover, Element.prototype.togglePopover,
Element.prototype.cloneNode, Element.prototype.attachShadow, Element.prototype.cloneNode, Element.prototype.attachShadow,
Element.prototype.insertAdjacentHTML, Element.prototype.scrollIntoView, Element.prototype.insertAdjacentHTML, Element.prototype.scrollIntoView,
Element.prototype.append, Element.prototype.prepend, Element.prototype.remove, Element.prototype.append, Element.prototype.prepend, Element.prototype.remove,
@@ -3096,6 +3570,7 @@ globalThis.Range = class Range { setStart(){} setEnd(){} collapse(){} selectNode
Document.prototype.querySelectorAll, Document.prototype.getElementsByTagName, Document.prototype.querySelectorAll, Document.prototype.getElementsByTagName,
Document.prototype.createElement, Document.prototype.createElementNS, Document.prototype.createElement, Document.prototype.createElementNS,
Document.prototype.createTextNode, Document.prototype.createComment, Document.prototype.createTextNode, Document.prototype.createComment,
Document.prototype.createCDATASection, Document.prototype.createProcessingInstruction,
Document.prototype.createDocumentFragment, Document.prototype.createEvent, Document.prototype.createDocumentFragment, Document.prototype.createEvent,
Document.prototype.hasFocus, Document.prototype.hasFocus,
Storage, Storage.prototype.getItem, Storage.prototype.setItem, Storage, Storage.prototype.getItem, Storage.prototype.setItem,
@@ -3166,7 +3641,7 @@ class _IframeDocument {
return this._root.querySelectorAll(tag); return this._root.querySelectorAll(tag);
} }
getElementsByClassName(cls) { getElementsByClassName(cls) {
return this._root.querySelectorAll('.' + cls); return _getElementsByClassName(this._root, cls);
} }
createElement(tag) { return document.createElement(tag); } createElement(tag) { return document.createElement(tag); }
createElementNS(ns, tag) { return document.createElementNS(ns, tag); } createElementNS(ns, tag) { return document.createElementNS(ns, tag); }
@@ -3174,6 +3649,7 @@ class _IframeDocument {
createComment(text) { return document.createComment(text); } createComment(text) { return document.createComment(text); }
createDocumentFragment() { return document.createDocumentFragment(); } createDocumentFragment() { return document.createDocumentFragment(); }
createEvent(type) { return document.createEvent(type); } createEvent(type) { return document.createEvent(type); }
createRange() { return new Range(); }
hasFocus() { return false; } hasFocus() { return false; }
get cookie() { return ''; } get cookie() { return ''; }
@@ -3525,10 +4001,21 @@ _markNative(Element.prototype.toDataURL);
_markNative(Element.prototype.toBlob); _markNative(Element.prototype.toBlob);
Element.prototype.attachShadow = function attachShadow(opts) { Element.prototype.attachShadow = function attachShadow(opts) {
var _mode = opts == null ? undefined : opts.mode;
if (_mode !== 'open' && _mode !== 'closed') {
throw new TypeError('Failed to execute attachShadow on Element: the mode value is not a valid ShadowRootMode.');
}
var _ln = (this.localName || '').toLowerCase();
if (!globalThis.__obscura_shadowHostNames.has(_ln) && _ln.indexOf('-') === -1) {
throw new DOMException('Failed to execute attachShadow on Element: this element does not support attachShadow', 'NotSupportedError');
}
if (this._shadowRoot) {
throw new DOMException('Failed to execute attachShadow on Element: the element already hosts a shadow tree.', 'NotSupportedError');
}
const host = this; const host = this;
const children = []; const children = [];
const shadow = { const shadow = {
mode: opts?.mode || 'open', mode: opts.mode,
host: host, host: host,
get innerHTML() { return children.map(c => c.outerHTML || c.textContent || '').join(''); }, get innerHTML() { return children.map(c => c.outerHTML || c.textContent || '').join(''); },
set innerHTML(v) { set innerHTML(v) {
@@ -3595,14 +4082,40 @@ Element.prototype.attachShadow = function attachShadow(opts) {
addEventListener() {}, removeEventListener() {}, dispatchEvent() { return true; }, addEventListener() {}, removeEventListener() {}, dispatchEvent() { return true; },
setHTMLUnsafe(v) { this.innerHTML = String(v == null ? "" : v); }, setHTMLUnsafe(v) { this.innerHTML = String(v == null ? "" : v); },
getHTML() { return this.innerHTML; }, getHTML() { return this.innerHTML; },
cloneNode() { return shadow; }, // Own textContent: ShadowRoot now extends DocumentFragment, so without
// these the inherited Node accessors run against this._nid. The setter in
// particular would target the host document and wipe it. Operate on the
// shadow's own `children` store instead.
get textContent() { return children.map(c => c.textContent || "").join(""); },
set textContent(v) {
children.length = 0;
if (v != null && v !== "") children.push(document.createTextNode(String(v)));
},
hasChildNodes() { return children.length > 0; },
// A detached fragment id backs any inherited nid-based method we do not
// override, so they stay non-destructive (operate on an empty fragment)
// rather than falling through to node 0 / the document.
_nid: +_dom("create_document_fragment"),
activeElement: null,
get styleSheets() { return []; },
cloneNode() { throw new DOMException('Failed to execute cloneNode on Node: ShadowRoot nodes are not clonable.', 'NotSupportedError'); },
}; };
this.shadowRoot = shadow; Object.setPrototypeOf(shadow, ShadowRoot.prototype);
this._shadowRoot = shadow;
return shadow; return shadow;
}; };
_markNative(Element.prototype.attachShadow); _markNative(Element.prototype.attachShadow);
Object.defineProperty(Element.prototype, 'shadowRoot', {
configurable: true,
enumerable: true,
get: function () {
var sr = this._shadowRoot;
return sr && sr.mode === 'open' ? sr : null;
},
});
// setHTMLUnsafe / getHTML: shims over innerHTML. setHTMLUnsafe parses markup // setHTMLUnsafe / getHTML: shims over innerHTML. setHTMLUnsafe parses markup
// like innerHTML (declarative shadow roots inside are not expanded yet, but the // like innerHTML (declarative shadow roots inside are not expanded yet, but the
// call no longer throws so the rest of a test file can run); getHTML serializes // call no longer throws so the rest of a test file can run); getHTML serializes