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:
1 parent
dc2a2e3b52
commit
990f2a63c8
1 file changed
+565
-52
Vendored
+565
-52
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user