Loop through the div children, and if the child is a text element, and after trimming it it still has length, put a span before that child element and wrap it in a span.
What follows is a breakdown of my above explanation, to describe how it works:
Make it easy to see progress
I started with your HTML code:
<div>
<span>first</span>
second
<span>third</span>
fourth
<div>
and colored the text red, with span text being colored green.
div {
color: red;
}
div span {
color: green;
}
That way I had a nice visual way to give me immediate feedback on my progress.
Get the elements
I’ve used a simple div selector in my example, but you can modify the selector to target the exact content that you need.
const div = document.querySelector("div");
div.childNodes.forEach(wrapTextContent);
The childNodes method gives us all child nodes that div section. If I wanted to ignore non-element things like text nodes or comments I would use children instead. But, we are explicitly wanting to include those so childNodes it is.
I also find it really useful to use separate functions such as wrapTextContent, as that lets us put off the details of how they are done until later, and the function name tells us exactly what we expect to achieve.
Ignore things that aren’t text
It can be tempting to just check if the element is a text element, but then you end up having deeply nested code. Instead of that, check if it’s not a text element and do an early return. That way you are using what’s called a guard clause, to protect against unwanted things.
We just need to check if the nodeType is a text node. There’s a range of values for the different types of nodes. Historically I would just check if it’s value equals 3, but now we have Node.TEXT_NODE which means the same thing, and is easier for people to understand when looking at the code.
function wrapTextContent(node) {
if (node.nodeType !== Node.TEXT_NODE) {
return;
}
...
}
Ignore empty text areas
When the node is confirmed as being a text node, we can then get the text content of the node and trim it. If the trimmed content doesn’t have any length to it, we don’t want to deal with that and can make an early return too.
function wrapTextContent(node) {
...
if (node.textContent.trim() === 0) {
return;
}
...
}
Add a span element
After confirming that it’s a text node with content, we can add a span element to the page.
It makes sense for the task of wrapping content with a span to be contained in a separate function, so I’ve used a separate wrapNode function for that.
The part of the wrapNode function that puts the span before the node is as follows:
function wrapNode(node, wrapper) {
node.parentNode.insertBefore(wrapper, node);
...
}
Move the text content into the span
This last part is really easy, for the appendChild method actually moves instead, when the elements is already in the DOM.
function wrapNode(node, wrapper) {
...
wrapper.appendChild(node);
}
Protect the local namespace
Another thing that I did with the code is to put it in an IIFE (immediately invoked function expression). That protects the local namespace from my code, so that function names and variables don’t end up leaking out to affect other code that might run.
(function iife() {
...
}());
It doesn’t have to be called iife either. Instead I find that it’s beneficial to give it a meaningful name for what the code is supposed to achieve.
The div selector is the main thing to configure with this code. To achieve that, instead of using a separate IIFE for the code, we can remove the immediately invoked part and use it as a separate function instead.
The parameter that the function uses I’ve named parentSelector. If it was just called selector that might become confusing, so I’ve deliberately named it as parentSelector to help inform us that a parent selector is required.