js: implement SVGAnimatedString for SVG className and href (#330)

SVG className and href reflected as plain strings, so el.className.animVal
and el.href.animVal were undefined, which made new URL(svgEl.href.animVal)
throw and broke any SVG-aware script. Add an SVGAnimatedString class
(baseVal/animVal both read the live attribute; baseVal writable) and make
the className and href getters return it for SVG-namespace elements. href
falls back to the legacy xlink:href. HTML elements are unchanged.
This commit is contained in:
SGavrl committed 2026-06-29 10:48:20 +00:00
1 parent 505738450a
commit 117415848e
1 file changed
+44 -1
+44 -1
View File
@@ -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 <a> path because an SVG <a> 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;