Consider this simple SVG with a <text> element containing some <tspan>s:
<svg>
<text x="10" y="30">
Foo Bar Baz
<tspan x="30" dy="30">FooBar</tspan>
<tspan x="30" dy="30">FooBaz</tspan>
</text>
</svg>
Now suppose we want to change the value of the text itself (in this case, "Foo Bar Baz") while keeping its <tspan> descendants. If we use textContent, innerHTML (modern browsers support innerHTML for SVG) etc that will replace everything, children included:
document.querySelector("text").textContent = "New Foo";
<svg>
<text x="10" y="30">
Foo Bar Baz
<tspan x="30" dy="30">FooBar</tspan>
<tspan x="30" dy="30">FooBaz</tspan>
</text>
</svg>
One workaround is getting the first NodeList element of the child nodes, which is normally the text itself:
document.querySelector("text").childNodes[0].textContent = "New Foo";
<svg>
<text x="10" y="30">
Foo Bar Baz
<tspan x="30" dy="30">FooBar</tspan>
<tspan x="30" dy="30">FooBaz</tspan>
</text>
</svg>
However, this workaround is quite hacky. "Why?", you might ask... well, using an hardcoded index in a collection is hacky enough to me. Also, like I wrote above, the first element in the collection is normally the text itself, but I have no idea if this is implementation-dependent and, therefore, a new browser can simply change that order in the collection, implementing a different sequence.
Hence the proper way in my question's title: is there a property or any other method to specifically change the text content only in an SVG <text> element?
As I see it you have two options. 1) Building on your last example where you look for the text node. You know that there is one text node with content. You can test all the childNodes of <text> to see if it is a text node and then replace that node. 2) You turn the text node into a <tspan> element just like the other child elements. This will make no difference visually and at then same time it is easier to refer to the element and style it if needed. The "cost" for adding this element is minimal.
document.querySelector("text > .first").textContent = "New Foo";
<svg>
<text x="10" y="30">
<tspan class="first">Foo Bar Baz</tspan>
<tspan x="30" dy="30">FooBar</tspan>
<tspan x="30" dy="30">FooBaz</tspan>
</text>
</svg>
You might use the nodeType property in a loop. It is important to notice that other text nodes can be added by the parser, so you also have to choose which one you need. A simple implementation substitutes the first node with nodeType===3. Other implementations are possible, taking into account that other type 3 nodes will have simple texts like \n. This will substitute the first text (not tspan) node that has some letter:
const r = document.querySelector("text");
let hasText = /\w/;
for (x = 0; x < r.childNodes.length; x++){
if (r.childNodes[x].nodeType === 3 && hasText.test(r.childNodes[x].textContent)){
r.childNodes[x].textContent = "New Foo";
break;
}
}
<svg>
<text x="10" y="30">
<tspan x="30" dy="30">FooBar</tspan>
<tspan x="30" dy="30">FooBaz</tspan>
Foo Bar Baz
</text>
</svg>
This same approach how vqf did with some did improvements.
const element = document.querySelector('text');
function changeText(text, node) {
for (const n of node.childNodes) {
let string = n.textContent.trim();
if (!n.hasChildNodes() && string !== '') {
n.textContent = text;
}
}
}
changeText('New Foo Baz', element);
<svg>
<text x="10" y="30">
Foo Bar Baz
<tspan x="30" dy="30">FooBar</tspan>
<tspan x="30" dy="30">FooBaz</tspan>
</text>
</svg>
The second way just clean up first line and append new text using insertAdjacentText which gives more flexible approach.
const element = document.querySelector('text');
function changeText(text, node) {
for (const n of node.childNodes) {
let string = n.textContent.trim();
if (!n.hasChildNodes() && string !== '') {
n.textContent = '';
}
}
node.insertAdjacentText('afterbegin', text);
node.insertAdjacentText('beforeend', text);
}
changeText('New Foo Baz', element);
<svg>
<text x="10" y="30">
Foo Bar Baz
<tspan x="30" dy="30">FooBar</tspan>
<tspan x="30" dy="30">FooBaz</tspan>
</text>
</svg>
A different approach from the others. Replace the Element.children nodes directly using Element.replaceChildren() (No compatible with IE):
const textNode = document.querySelector('text');
function updateTextContent(node, text) {
node.replaceChildren(text, ...node.children);
}
updateTextContent(textNode, 'New Foo');
<svg>
<text x="10" y="30">
Foo Bar Baz
<tspan x="30" dy="30">FooBar</tspan>
<tspan x="30" dy="30">FooBaz</tspan>
</text>
</svg>
If you need support for IE, replace the textContent property value, and then use append to add the children nodes:
function updateTextContent(node, text) {
const children = [...node.children];
node.textContent = "";
node.append(text, ...children);
}
"Is there a property or any other method to specifically change the text content only?"
No, probably not. In fact, I think it might be for the better that such a method doesn't exist. Say for example that your <text> node looked like this:
<text>
Foo
<tspan>Bar</tspan>
Baz
</text>
And now we would call the unknown method to change the text into Hello world. What would the desired outcome be?
<text>
Hello world
<tspan>Bar</tspan>
</text>
<text>
<tspan>Bar</tspan>
Hello world
</text>
or even
<text>
Hello
<tspan>Bar</tspan>
world
</text>
Looking at it like this, it becomes clear that the "text content" can't be seen as a special case that exists apart from the node tree: just like the <tspan> in your example, there can be multiple text nodes in any order and quantity. It's therefore hard to justify a special method that only changes text nodes, without introducing method to only change all the <tspan>, <div> and so on as well.
If you regard all the text nodes as some kind of typeless elements like this:
<text>
<text>Foo</text>
<tspan>Bar</tspan>
<text>Baz</text>
</text>
there is little question about how you would approach targeting and changing your desired element. Options include:
Foo will indeed always be the first element. But just like with the :first selector in CSS, you might rather not want to depend so rigidly on a specific structure.class="text" attribute would be very clear, but then you'd need to wrap your text in an actual element (which might be the best solution, but I'm following your specs here).The snippet below would therefore be a perfectly valid way to alter the first text node in a list of children (written in a functional manner to keep it concise).
Array.from(document.querySelector("text").childNodes)
.find(e => e.nodeType === Node.TEXT_NODE)
.textContent = "New Foo";
<svg>
<text x="10" y="30">
Foo Bar Baz
<tspan x="30" dy="30">FooBar</tspan>
<tspan x="30" dy="30">FooBaz</tspan>
</text>
</svg>