Is it possible to use HTML's .querySelector() to select by xlink attribute in an SVG?
Asked Answered
E

3

53

Given:

<body>
    <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
        <a xlink:href="url"></a>
    </svg>
</body>

Is it possible to use the HTML DOM's .querySelector() or .querySelectorAll() to select the link inside the SVG by the contents of its xlink:href attribute?

This works:

document.querySelector('a')                    // <a xlink:href="url"/>

These don't:

document.querySelector('[href="url"]')         // null
document.querySelector('[xlink:href="url"]')   // Error: not a valid selector
document.querySelector('[xlink\:href="url"]')  // Error: not a valid selector
document.querySelector('[xlink\\:href="url"]') // null

Is there a way of writing that attribute selector to make it 'see' the xlink:href?

Emlen answered 12/4, 2014 at 18:17 Comment(0)
D
65

Query selector can handle namespaces, but it gets tricky because

  1. The syntax for specifying namespaces in CSS selectors is different from html;

  2. The querySelector API doesn't have any method for assigning a namespace prefix (like xlink) to an actual namespace (like "http://www.w3.org/1999/xlink").

On the first point, the relevant part of the CSS specs allows you to specify no namespace (the default), a specific namespace, or any namespace:

@namespace foo "http://www.example.com";
[foo|att=val] { color: blue }
[*|att] { color: yellow }
[|att] { color: green }
[att] { color: green }

The first rule will match only elements with the attribute att in the "http://www.example.com" namespace with the value "val".

The second rule will match only elements with the attribute att regardless of the namespace of the attribute (including no namespace).

The last two rules are equivalent and will match only elements with the attribute att where the attribute is not in a namespace.

See this fiddle, paying attention to the fill styles (default, hover, and active):
https://jsfiddle.net/eg43L/

The Selectors API adopts the CSS selector syntax, but has no equivalent to the @namespace rule for defining a namespace. As a result, selectors with namespaces are not valid but the wildcard namespace token is valid:

If the group of selectors include namespace prefixes that need to be resolved, the implementation must raise a SYNTAX_ERR exception ([DOM-LEVEL-3-CORE], section 1.4).

This specification does not provide support for resolving arbitrary namespace prefixes. However, support for a namespace prefix resolution mechanism may be considered for inclusion in a future version of this specification.

A namespace prefix needs to be resolved if the namespace component is neither empty (e.g. |div), representing the null namespace, or an asterisk (e.g. *|div), representing any namespace. Since the asterisk or empty namespace prefix do not need to be resolved, implementations that support the namespace syntax in Selectors must support these.

(bold added)

Check out the fiddle again, this time paying attention to the console output. The command document.querySelector('[*|href="#url"]') returns the element you want.

One final warning: MDN tells me that IE8- do not support CSS namespaces, so this might not work for them.


Update 2015-01-31:

As @Netsi1964 pointed out in the comments, this doesn't work for custom namespaced attributes in HTML 5 documents, since HTML doesn't support XML namespaces. (It would work in a stand-alone SVG or other XML document including XHTML.)

When the HTML5 parser encounters an attribute like data:myAttribute="value" it treats that as a single string for the attribute name, including the :. To make things more confusing, it auto-lowercases the string.

To get querySelector to select these attributes, you have to include the data: as part of the attribute string. However, since the : has special meaning in CSS selectors, you need to escape it with a \ character. And since you need the \ to get passed through as part of the selector, you need to escape it in your JavaScript.

The successful call therefore looks like:

document.querySelector('[data\\:myattribute="value"]');

To make things a little more logical, I would recommend using all lower-case for your attribute names, since the HTML 5 parser will convert them anyway. Blink/Webkit browser will auto-lowercase selectors you pass querySelector, but that's actually a very problematic bug (in means you can never select SVG elements with mixed-case tag names).

But does the same solution work for xlink:href? No! The HTML 5 parser recognizes xlink:href in SVG markup, and correctly parses it as a namespaced attribute.

Here's the updated fiddle with additional tests. Again, look at the console output to see the results. Tested in Chrome 40, Firefox 35, and IE 11; the only difference in behavior is that Chrome matches the mixed-case selector.

Duong answered 13/4, 2014 at 20:13 Comment(13)
Ha! That's brilliant! It works! Thank you. (I don't care about IE8, as it doesn't do SVG anyway).Emlen
Just to be clear, that will select any attribute with localName href, not just attributes in the "http://www.w3.org/1999/xlink" namespace, correct?Hachure
Yes. The wildcard means "any or no namespace". So if you want to distinguish between a elements in your SVG and other anchors or elements with an href property, you'll need to be more explicit with the rest of the selector. (And yes, I totally wasn't thinking about the fact that IE8 was irrelevant for SVG...)Duong
I have added a namespace data to a SVG element, and use an custom attribute in that namespace onewaybinding, but I cannot detect elements with that attribute using: document.querySelectorAll("[*|onewaybinding]"). So I returned to good old data attributes, it gives an validation error, but I can find the elements using querySelector :-) data-onewaybinding can be found using document.querySelector("[data-onewaybinding]")Amianthus
@Amianthus That's annoying (but good to know!). I played around a bit -- see the updates to the answer. I think I'd go with your solution as well, and damn the validators. The W3C SVG working group has been discussing formally allowing HTML 5-style data- attributes on SVG elements in SVG 2, just because so many people already use them. However, there's no way to really describe them in XML validation schemes.Duong
han! document.querySelector('[*|href="#url"]') doesn't work in IE... (9 to 11 tested)Oudh
@Oudh Confirmed. I'm not sure why I did not catch that last year. The xlink selector works in the CSS (the hover effect in the fiddle), but you can't use namespaces with querySelector. No chance you can test to see if it works in Edge, can you?Duong
(Also, I'm not sure what has changed with JSFiddle, but IE refuses to run the script in the fiddle at all; I had to try the querySelector calls directly from the console line.)Duong
Just installed an Edge VM, can confirm there too. And it doesn't even throw an error, it just doesn't find anything... The only check that can be done seems to first append one element with such namespaced attribute and then try to get it...Oudh
Sorry I'm so late, but is there any way to select the full 'xlink:href', instead of just 'href' with a namespace that may or may not be 'xlink'? Strangely, that was the main purpose of the question, but it was not even brought up in the answer or any of these comments. They did not even say "You can't do that".Lava
@Lava No, there is no direct way from JavaScript to explicitly specify a namespace. You can do it from CSS though, with @namespace, so in modern browsers you could set a CSS variable, and then test it's value from JS.Duong
Notice this webpage is served with response header: Content-Type: application/xhtml+xml. In this case, I can select with '*|nonNumeric' (case sensitive), I can also select with 'nonNumeric'. I wasn't expecting the last part, I thought "will match only elements with the attribute is not in a namespace", but (the element type) is in a namespace... document.querySelector('nonNumeric').namespaceURI has a valHashish
OK I was searching using a type selector, which has different behavior than an attribute selector, agree? Type selector: "if no default namespace has been declared for selectors, this is equivalent to *|E. Otherwise it is equivalent to ns|E where ns is the default namespace." Attribute selector: "attribute selectors without a namespace component apply only to attributes that have no namespace (equivalent to "|attr"; these attributes are said to be in the "per-element-type namespace partition")"Hashish
S
17

[*|href] will match both html href and svg xlink:href, then use :not([href]) to exclude html href.

document.querySelectorAll('[*|href]:not([href])')

tested in chrome

Saiga answered 10/10, 2016 at 7:57 Comment(2)
Finally, someone with an answer that actually works! (Clever, too!)Sorci
extended answer: If you want match exact value: document.querySelector('[*|href="#icon-edit"]:not([href])') #icon-edit is the value of xlink:href attributeSpinifex
H
8

Unfortunately not.

querySelector doesn't handle XML namespaces, so there is no easy way to do this that way. You can however use an XPath query.

var result = document.evaluate(
    // Search for all nodes with an href attribute in the xlink namespace.
    '//*[@xlink:href="url"]',
    document,
    function(prefix){
        return {
            xlink: "http://www.w3.org/1999/xlink"
        }[prefix] || null;
    },
    XPathResult.ORDERED_NODE_ITERATOR_TYPE
);

var element = result.iterateNext();

If you need full cross-browser support, such as for IE, which does not have a document.evaluate, you can polyfill it with wicked-good-xpath.

Of course, depending on your usage, it may be easier to do this (which I think will work on IE):

var element = Array.prototype.filter.call(document.querySelectorAll('a'),
    function(el){
    return el.getAttributeNS('http://www.w3.org/1999/xlink', 'href') === 'url';
})[0] || null;
Hachure answered 12/4, 2014 at 18:30 Comment(4)
It appears there is no Internet Explorer support for XPath queries.Emlen
I'm upvoting because I learned something useful, but I hesitate to mark it correct because I don't want others to start using things that don't work x-browser. I suppose the correct answer is 'no'.Emlen
Yes, I already implemented that filtered array version, although it looks more like [edit: ah, gack, can't put code in here readably].Emlen
Ok, thanks, all useful stuff. I'm going to stick with the filtered array version as I don't own the site I'm currently writing for, and I need to be mindful of external libraries.Emlen

© 2022 - 2024 — McMap. All rights reserved.