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

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?


Solution 1:

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.

Solution 2:

[*|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

Solution 3:

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;