diff --git a/crates/obscura-js/js/bootstrap.js b/crates/obscura-js/js/bootstrap.js index acc2912..5f1d1df 100644 --- a/crates/obscura-js/js/bootstrap.js +++ b/crates/obscura-js/js/bootstrap.js @@ -959,7 +959,15 @@ class Element extends Node { } get id() { return this.getAttribute("id") || ""; } set id(v) { this.setAttribute("id", v); } - get className() { return this.getAttribute("class") || ""; } + get className() { + // SVG elements reflect class as an SVGAnimatedString (.baseVal/.animVal), + // not a plain string. Anti-fraud sensors read el.className.animVal. + if (this.namespaceURI === "http://www.w3.org/2000/svg") { + if (!this._svgClassName) this._svgClassName = new SVGAnimatedString(this, "class"); + return this._svgClassName; + } + return this.getAttribute("class") || ""; + } set className(v) { this.setAttribute("class", v); } get namespaceURI() { // createElementNS records the requested namespace on _ns; an empty string @@ -1581,6 +1589,16 @@ class Element extends Node { // members use. Other elements reflect the raw attribute. get href() { const ln = this.localName; + // SVG href-bearing elements reflect href as an SVGAnimatedString (with the + // legacy xlink:href as a fallback), not a resolved URL string. Checked + // before the HTML path because an SVG also has localName 'a'. + if (this.namespaceURI === "http://www.w3.org/2000/svg" && + (ln === 'a' || ln === 'image' || ln === 'use' || ln === 'script' || + ln === 'pattern' || ln === 'filter' || ln === 'textPath' || ln === 'mpath' || + ln === 'linearGradient' || ln === 'radialGradient' || ln === 'feImage' || ln === 'tref')) { + if (!this._svgHref) this._svgHref = new SVGAnimatedString(this, "href", "xlink:href"); + return this._svgHref; + } if (ln === 'a' || ln === 'area') { const raw = this.getAttribute('href'); if (raw === null) return ''; @@ -4979,6 +4997,31 @@ globalThis.HTMLLegendElement = Element; globalThis.HTMLProgressElement = Element; globalThis.HTMLDetailsElement = Element; globalThis.HTMLDialogElement = Element; +// SVGAnimatedString backs the className and href reflections on SVG elements. +// baseVal and animVal both read the live attribute (no SMIL animation), and +// baseVal is writable. Used by the SVG-aware get className()/get href() above. +function SVGAnimatedString(el, attr, fallbackAttr) { + this._el = el; + this._attr = attr; + this._fallback = fallbackAttr || null; +} +SVGAnimatedString.prototype._read = function() { + let v = this._el.getAttribute(this._attr); + if (v === null && this._fallback) v = this._el.getAttribute(this._fallback); + return v == null ? '' : v; +}; +Object.defineProperty(SVGAnimatedString.prototype, 'baseVal', { + get() { return this._read(); }, + set(v) { this._el.setAttribute(this._attr, String(v)); }, + configurable: true, enumerable: true, +}); +Object.defineProperty(SVGAnimatedString.prototype, 'animVal', { + get() { return this._read(); }, + configurable: true, enumerable: true, +}); +Object.defineProperty(SVGAnimatedString.prototype, Symbol.toStringTag, { value: 'SVGAnimatedString', configurable: true }); +_markNative(SVGAnimatedString); + globalThis.SVGElement = Element; globalThis.SVGSVGElement = Element; globalThis.CharacterData = CharacterData;