0

I want to wrap each letter inside an HTML element into it's own span (for animation purposes).

This is doable by splitting the text into chars and creating multiple spans (See jQuery - How to wrap each character from a string in spans).

Only this way word wrapping will not work anymore since words/letters are broken up into spans. Also child elements (like strong) are removed.

Example:

$.fn.convertToSeperateLetters = function() {
  return this.each(function() {
    var html = $(this).text().replace(/\S/g, '<span class="letter">$&</span>');
    return $(this).html(html)
  });
}

$('p').convertToSeperateLetters();
p,
.word,
.letter {
  padding: 3px 1px;
}

p {
  border: 1px solid red;
}

.word {
  display: inline-block;
  border: 1px solid green;
  margin-left: 2px;
  margin-right: 2px;
}

.letter {
  display: inline-block;
  border: 1px solid blue;
}

.word-wrapped {
  width: 360px;
  background: lightgrey;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<p>Hello World</p>
<p>Hello <strong>World</strong></p>

<div class="word-wrapped">
  <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. </p>
</div>
Andreas Furster
  • 1,558
  • 1
  • 12
  • 28
  • 1
    If you accept your answer, it can be used for "does this answer your question". Eg someone is asking how to do this here: https://stackoverflow.com/questions/66389433/jquery-prevent-replace-on-href – freedomn-m Feb 26 '21 at 16:49
  • @freedomn-m Thanks. Good catch! I answered the question there. – Andreas Furster Mar 01 '21 at 09:16

1 Answers1

1

I've made an solution that also wraps each word into a span. This way wrapping still works on whole words.

Also (1 level) of sub-elements is preserved in the end result. For example:

a <strong>bc</strong> d

Is converted into:

<span class="word">
  <span class="letter">a</span>
</span>
<span class="word">
  <span class="letter">
    <strong>b</strong>
  </span>
  <span class="letter">
    <strong>c</strong>
  </span>
</span>
<span class="word">
  <span class="letter">d</span>
</span>

Known issues:

  • Only one level of sub-elements works (So <p>Hello <strong><u>W</u>orld</strong></p> won't)
  • Text against another element create two separate words (So <p>User<strong>name</strong></p> are two words)

The full solution:

$.fn.convertToSeperateLetters = function() {
  return this.each(function() {
    var $el = $(this);
    var elements = convertToSeperateLetters($el, false);

    $el.empty().append(elements);

    return $el;
  });
}

$('p').convertToSeperateLetters();

function convertToSeperateLetters($element, asSubNode) {
  var elements = [];

  var childNodes = $element.contents();

  // Loop through all child nodes of selected element
  for (var c = 0; c < childNodes.length; c++) {
    var node = childNodes[c];
    var type = node.nodeType;

    // Process a child element
    if (type == Node.ELEMENT_NODE) {
      Array.prototype.push.apply(elements, convertToSeperateLetters($(node), true));
    }

    // Process a piece of text
    else if (type == Node.TEXT_NODE) {
      var text = node.nodeValue;

      // Process each word
      var words = text.split(' ');
      for (var w = 0; w < words.length; w++) {
        var word = words[w];

        // Skip empty words
        if (word == '') continue;

        // Wrap each word into span
        var $word = $('<span/>').addClass('word');
        for (var l = 0; l < word.length; l++) {
          var letter = word[l];

          // Wrap each letter into span
          var $letter = $('<span/>').addClass('letter');

          if (!asSubNode) {
            $letter.html(letter);
          }

          if (asSubNode) {
            var $subNode = $element.clone().empty().html(letter);
            $letter.append($subNode);
          }

          $word.append($letter);
        }

        elements.push($word);
      }
    }
  }
  return elements;
}
p,
.word,
.letter {
  padding: 3px 1px;
}

p {
  border: 1px solid red;
}

.word {
  display: inline-block;
  border: 1px solid green;
  margin-left: 2px;
  margin-right: 2px;
}

.letter {
  display: inline-block;
  border: 1px solid blue;
}

.word-wrapped {
  width: 360px;
  background: lightgrey;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<p>Hello World</p>
<p>Hello <strong>World</strong></p>

<div class="word-wrapped">
  <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. </p>
</div>
Andreas Furster
  • 1,558
  • 1
  • 12
  • 28