Reputation: 105
I try to add new SVG elements to some nodes. For that purpose the nodes the elements are to be added to have to be found by a string contained in there text content, e.g. find any node which has "id0"
inside the <text>
tag.
Here is example of my HTML hierarchy:
<pre>
<svg>
<g>
<g>
<text> id3 </text>
<text> 73% </text>
<svg> ... </svg>
</g>
<g>
<svg> ... </svg>
</g>
<g>
<text> id0 </text>
<text> 11% </text>
<svg> ... </svg>
</g>
<g>
<text> id1 </text>
<text> 66% </text>
<svg> ... </svg>
</g>
<g>
<svg> ... </svg>
</g>
</g>
</svg>
</pre>
I definitely don't know the right solution, but I think it is something like this:
d3.select('svg').select('g').selectAll('g').each(function (d, i) {})
.select('g').select('text').filter(function () {
return (d3.select(this).text() === 'id0')
})
.select(function () {
return this.parentElement;
})
.append('svg')
.attr('width', 400)
.attr('height', 400)
If the tag <text>
contains "id0"
, then return to the parent node and add an SVG element to it. But on the line return this.parentElement;
an error occurs:
Property 'parentElement' does not exist on type 'Window'.
Similar errors occur when I use parentElement
or parent
.
Upvotes: 6
Views: 3771
Reputation: 1
You can use find-dom-nodes-by-content library, that allows conducting a search in a DOM tree in a simple way.
const nodes = findDOMNodesByContent({ text: "Hello" }, document.body);
Upvotes: 0
Reputation: 15607
An alternative is xpath, which permits searching via text:
// if you know there's only one...
const singleResult = document.evaluate(`//*[name()="text" and contains(text(), "id0")]`, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
console.log(singleResult.nodeName, singleResult.textContent)
// if there might be multiple results
const multipleResults = document.evaluate(`//*[name()="text" and contains(text(), "id_multiple")]`, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)
for (let i=0; i < multipleResults.snapshotLength; i++) {
console.log(multipleResults.snapshotItem(i).nodeName, multipleResults.snapshotItem(i).textContent)
}
<svg>
<g>
<g>
<text> id_multiple </text>
<text> 73% </text>
<svg></svg>
</g>
<g>
<svg></svg>
</g>
<g>
<text> id0 </text>
<text> 11% </text>
<svg></svg>
</g>
<g>
<text> id_multiple </text>
<text> 66% </text>
<svg></svg>
</g>
<g>
<svg></svg>
</g>
</g>
</svg>
The iterators (/snaphots) that are returned are unexpected for me - definitely have a read of this excellent answer: https://stackoverflow.com/a/32468320/2586761, and the docs: MDN: document.evaluate.
Note that because "common HTML nodes and svg nodes belong to different namespaces", you need to select SVG nodes like *[name()="svg"]
.
Regarding finding the text, I'd recommend using contains(text(),'needle')
rather than the more explicit text()='needle'
because any whitespace around needle
would cause the selector to return null
.
Interesting xpath vs CSS commentary: What is the difference between cssSelector & Xpath and which is better with respect to performance for cross browser testing?
Note that there's no IE support for document.evaluate
Upvotes: 5
Reputation: 21578
There is no built-in way in D3 to select an element by its text contents, which is due to the fact that D3 internally uses Element.querySelector()
and Element.querySelectorAll()
to select elements from the DOM. Those methods take a CSS Selector string as a single parameter which is defined by the Selectors Level 3 specification. Unfortunately, there is no way of selecting an element based on its contents (this once was possible via the :contains()
pseudo-class, which is gone, though).
Therefore, to build your selection you have to resort to passing a function to .select()
which selects and returns the <text>
element you are interested in. There are various ways of doing this, but I would like to suggest a not so obvious yet elegant approach. This makes use of the little-known NodeIterator
interface which can be used to create an iterator over a list of nodes from the DOM which meet your filter criteria.
The NodeIterator
instance is created by calling Document.createNodeIterator()
which takes three arguments:
NodeFilter.SHOW_TEXT
..acceptNode()
method of the NodeFilter
interface. This method is presented each node of the specified type in document order and must return NodeFilter.FILTER_ACCEPT
for any matching node and NodeFilter.FILTER_REJECT
any other nodes. At this point your implementation would look for a match of the id value with the text contents of the actual text element.You can then call .nextNode()
on the created node iterator to walk through the list of matching nodes. For your task this could be something along the following lines:
document.createNodeIterator(
this, // The root node of the searched DOM sub-tree.
NodeFilter.SHOW_TEXT, // Look for text nodes only.
{
acceptNode(node) { // The filter method of interface NodeFilter
return new RegExp(value).test(node.textContent) // Check if text contains string
? NodeFilter.FILTER_ACCEPT // Found: accept node
: NodeFilter.FILTER_REJECT; // Not found: reject and continue
}
})
.nextNode() // Get first node from iterator.
.parentElement; // Found node is a "pure" text node, get parent <text> element.
Once having this node at hand it is easy to apply whatever modifications you need for that element—i.e. append elements, set attributes… This is also easily adapted to handling multiple nodes, if you were not looking for a unique value but for multiple elements matching the same string. You would just have to return an array of the nodes found by the iterator which could then be directly passed on to D3's .selectAll()
for creating a selection of multiple nodes.
For a working demo have a look at the following snippet:
function nodeIterator(value) {
return function() {
return document.createNodeIterator(
this, // The root node of the searched DOM sub-tree.
NodeFilter.SHOW_TEXT, // Look for text nodes only.
{
acceptNode(node) { // The filter method of interface NodeFilter
return new RegExp(value).test(node.textContent) // Check if text contains string
? NodeFilter.FILTER_ACCEPT // Found: accept node
: NodeFilter.FILTER_REJECT; // Not found: reject and continue
}
})
.nextNode() // Get first node from iterator.
.parentElement; // Found node is a "pure" text node, get parent <text> element.
}
}
const filter = nodeIterator("id0");
const sel = d3.select("svg").select(filter);
// Manipulate the selection:...
// sel.append("g")
// .attr("transform", "...");
console.log(sel.node());
<script src="https://d3js.org/d3.v5.js"></script>
<svg>
<g>
<g>
<text> id3 </text>
<text> 73% </text>
<svg></svg>
</g>
<g>
<svg></svg>
</g>
<g>
<text> id0 </text>
<text> 11% </text>
<svg></svg>
</g>
<g>
<text> id1 </text>
<text> 66% </text>
<svg></svg>
</g>
<g>
<svg></svg>
</g>
</g>
</svg>
Upvotes: 2