问题
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
.
回答1:
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:
- The root of the DOM sub-tree from where the search is performed.
- A bitmask determining which types of nodes you are interested in; for your purposes this would be NodeFilter.SHOW_TEXT.
- An object implementing the .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 andNodeFilter.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>
回答2:
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
来源:https://stackoverflow.com/questions/58052522/how-to-find-a-dom-element-by-its-text-content