Can I wrap each line of multi-line text in a span?

It seems like you're asking how to split the text where it is naturally wrapped by the browser. Unfortunately, this isn't straightforward at all. Neither is it robust — consider the following scenario:

  • User browses to your page, the div is rendered and the onload event fires,
  • 3 span elements are created from the text node, 1 for each wrapped line of text,
  • The user resizes the browser and the size of the div changes.

The result is that the spans no longer correlate to where the lines start and finish. Of course, this scenario is avoidable using fixed-width elements or you can rejig the whole thing when the browser resizes, but that's just an example of how it can break.

Still, it's not easy. A similar question has come up before (albeit, with a different goal) and two solutions appeared, which could both be of help here:

Solution 1: getClientRects()

Don't actually wrap the text in spans, but get the position and dimensions of each line of text using getClientRects(). Then, create the number of spans necessary and position/resize them behind each line of text.

Pros

  • Fast; getClientRects returns the position of each line
  • Simple; the code is more elegant than solution 2

Cons

  • Wrapped text must be contained by an inline element.
  • No styling will actually apply to the text (like font-weight or font-color). Only useful for things like background-color or border.

The demo provided with the answer shows how you can highlight the line of text currently beneath the mouse.

Solution 2: Split, join, loop, merge

Split the text into an array using the split() method with a word boundary or white-space as the argument passed. Rejoin the array into a string with </span><span> between each element and wrap the whole thing with <span> and </span>, and replace the original text node with the resulting HTML in the containing element. Now, iterate over each of those span elements checking its y position within the container. When the y position increases, you know that you've reached a new line and the previous elements can be merged into a single span.

Pros

  • Each line can be styled with any CSS property, like font-weight or text-decoration.
  • Each line can have its own event handlers.

Cons

  • Slow and unwieldy due to the numerous DOM and string operations

Conclusion

There may be other ways to achieve your goal, but I'm not sure of any myself. TextNode.splitText(n) can split a TextNode in twain (!) when passed a numeric index of the character you want to split on. Neither of the above solutions are perfect, and both break as soon as the containing element resizes.


I put together a fiddle implementing solution #2 by Andy E (above). I.e. Split, join, loop, merge

Here's the algorithm:

var spanInserted = $('#someText').html().split(" ").join(" </span><span>");
var wrapped = ("<span>").concat(spanInserted, "</span>");
$('#someText').html(wrapped);
var refPos = $('#someText span:first-child').position().top;
var newPos;
$('#someText span').each(function(index) {
    newPos = $(this).position().top   
    if (index == 0){
       return;
    }
    if (newPos == refPos){
        $(this).prepend($(this).prev().text() + " ");
        $(this).prev().remove();
    } 
    refPos = newPos;
});

Enjoy...


var classes = ",red-bg,orange-bg,yellow-bg".split(",")
var txt = $('#quote').html().split("\n")
//this gives you FIVE items because of the leading and trailing CRs
//so we skip the first and last item in the loop
var output = ""
for(var x=1;x<txt.length-1;x++) {
    output = output + "<span class='"+classes[x]+"'>"+txt[x]+"</span>"
}
$('#quote').html(output)