skip to Main Content

I’m working on a text truncation feature in plain JavaScript that’s supposed to shorten the text within HTML elements while preserving the structure. For example, given an HTML element with data-limit="10", it should truncate the content to 10 words and append a "Read more" link to expand the full text.

However, the script isn’t truncating as expected. Instead, it’s cutting off too early or immediately after the headers. Here’s a simplified version of my code and the HTML I’m testing with:

document.addEventListener("DOMContentLoaded", function() {
  function shortenText(element, limit) {
    const originalContent = element.innerHTML; // Save the original content
    const nodes = Array.from(element.childNodes); // All direct child nodes
    let wordCount = 0;
    let truncatedContent = '';

    // Helper function for word counting and truncation
    function traverseNodes(node) {
      if (wordCount >= limit) {
        return;
      }

      if (node.nodeType === Node.TEXT_NODE) {
        const words = node.nodeValue.split(/s+/);
        for (let i = 0; i < words.length; i++) {
          if (wordCount < limit) {
            truncatedContent += words[i] + ' ';
            wordCount++;
          } else {
            truncatedContent += '... ';
            return;
          }
        }
      } else if (node.nodeType === Node.ELEMENT_NODE) {
        if (node.nodeName.toLowerCase() === 'span' && node.classList.contains('more')) {
          return;
        }

        truncatedContent += `<${node.nodeName.toLowerCase()}`;
        Array.from(node.attributes).forEach(attr => {
          truncatedContent += ` ${attr.name}="${attr.value}"`;
        });
        truncatedContent += '>';

        node.childNodes.forEach(childNode => traverseNodes(childNode));

        truncatedContent += `</${node.nodeName.toLowerCase()}>`;
      }
    }

    for (let childNode of nodes) {
      if (wordCount < limit) {
        traverseNodes(childNode);
      }
    }

    element.innerHTML = truncatedContent.trim() + `<span class="more">Read more</span>`;
    element.dataset.originalContent = originalContent; // Save the original content
  }

  function expandText(element) {
    element.innerHTML = element.dataset.originalContent;
  }

  document.querySelectorAll('.shorten-text').forEach(el => {
    const limit = parseInt(el.dataset.limit) || 100; // Default limit to 100 words
    shortenText(el, limit);

    el.addEventListener('click', function(event) {
      if (event.target.classList.contains('more')) {
        expandText(el);
      }
    });
  });
});
* {
  font-family: Calibri, Arial;
  margin: 0px;
  padding: 25px;
  text-align: center;
}

.shorten-text {
  margin: 20px 0;
  padding: 10px;
  border: 1px solid #ccc;
  background-color: #f9f9f9;
}

.more {
  color: blue;
  cursor: pointer;
  text-decoration: underline;
}
<h1>Text Shortening Test</h1>
<div class="shorten-text" data-limit="10">
  <h1>A Headline!</h1>
  <div>A wonderful serenity has taken possession of my entire soul, <strong>like these sweet mornings</strong> of spring which I enjoy with my whole heart. I am alone, and feel the charm of existence in this spot, which was created for the bliss of souls like
    mine.</div>
  <p>I am so happy, my dear friend, so absorbed in the <a href="#">exquisite</a> sense of mere tranquil existence, that I neglect my talents.</p>
</div>

<div class="shorten-text" data-limit="5">
  <h2>Another Header</h2>
  <div>This is a test paragraph to check the shortening function. It should only show a few words.</div>
  <p>This paragraph should be mostly hidden.</p>
</div>

<div class="shorten-text" data-limit="15">
  <h3>Yet Another Header</h3>
  <div>Testing the function with a longer limit to see how it handles more text. The goal is to ensure that the text is truncated properly without breaking the structure of the HTML content.</div>
</div>

What I expect:

  • For the text within the .shorten-text elements to be truncated after
    the specified number of words (excluding any HTML tags or child
    elements).
  • A "Read more" link to appear, which when clicked, expands
    to show the full text.
  • The truncation should not occur within tags
    like <h1>, <b>, <strong>, <a>, etc. It should always cut text outside
    these elements, ensuring the integrity of HTML tags.

What’s happening:

  • Text is sometimes cut off too early or immediately after headers.

Any insights on why the script isn’t working as intended and how to ensure it accurately truncates the text based on the word count would be greatly appreciated!

Thanks!

2

Answers


  1. In your for(…i < words.length…) loop, you are counting empty strings created with your node.nodeValue.split(/s+/)

    Just change the if() in that loop to check for empty strings.

    i.e. change:

    if (wordCount < limit) {
    

    to:

    if (words[i] && wordCount < limit) {
    

    Run the code snippet to see it working.

    document.addEventListener("DOMContentLoaded", function() {
      function shortenText(element, limit) {
        const originalContent = element.innerHTML; // Save the original content
        const nodes = Array.from(element.childNodes); // All direct child nodes
        let wordCount = 0;
        let truncatedContent = '';
    
        // Helper function for word counting and truncation
        function traverseNodes(node) {
          if (wordCount >= limit) {
            return;
          }
    
          if (node.nodeType === Node.TEXT_NODE) {
            const words = node.nodeValue.split(/s+/);
            for (let i = 0; i < words.length; i++) {
            // NOTE Don't call empty strings
            // if (wordCount < limit) {
            if (words[i] && wordCount < limit) {
                truncatedContent += words[i] + ' ';
                wordCount++;
              } else {
                truncatedContent += '... ';
                return;
              }
            }
          } else if (node.nodeType === Node.ELEMENT_NODE) {
            if (node.nodeName.toLowerCase() === 'span' && node.classList.contains('more')) {
              return;
            }
    
            truncatedContent += `<${node.nodeName.toLowerCase()}`;
            Array.from(node.attributes).forEach(attr => {
              truncatedContent += ` ${attr.name}="${attr.value}"`;
            });
            truncatedContent += '>';
    
            node.childNodes.forEach(childNode => traverseNodes(childNode));
    
            truncatedContent += `</${node.nodeName.toLowerCase()}>`;
          }
        }
    
        for (let childNode of nodes) {
          if (wordCount < limit) {
            traverseNodes(childNode);
          }
        }
    
        element.innerHTML = truncatedContent.trim() + `<span class="more">Read more</span>`;
        element.dataset.originalContent = originalContent; // Save the original content
      }
    
      function expandText(element) {
        element.innerHTML = element.dataset.originalContent;
      }
    
      document.querySelectorAll('.shorten-text').forEach(el => {
        const limit = parseInt(el.dataset.limit) || 100; // Default limit to 100 words
        shortenText(el, limit);
    
        el.addEventListener('click', function(event) {
          if (event.target.classList.contains('more')) {
            expandText(el);
          }
        });
      });
    });
    * {
      font-family: Calibri, Arial;
      margin: 0px;
      padding: 25px;
      text-align: center;
    }
    
    .shorten-text {
      margin: 20px 0;
      padding: 10px;
      border: 1px solid #ccc;
      background-color: #f9f9f9;
    }
    
    .more {
      color: blue;
      cursor: pointer;
      text-decoration: underline;
    }
    <h1>Text Shortening Test</h1>
    <div class="shorten-text" data-limit="10">
      <h1>A Headline!</h1>
      <div>A wonderful serenity has taken possession of my entire soul, <strong>like these sweet mornings</strong> of spring which I enjoy with my whole heart. I am alone, and feel the charm of existence in this spot, which was created for the bliss of souls like
        mine.</div>
      <p>I am so happy, my dear friend, so absorbed in the <a href="#">exquisite</a> sense of mere tranquil existence, that I neglect my talents.</p>
    </div>
    
    <div class="shorten-text" data-limit="5">
      <h2>Another Header</h2>
      <div>This is a test paragraph to check the shortening function. It should only show a few words.</div>
      <p>This paragraph should be mostly hidden.</p>
    </div>
    
    <div class="shorten-text" data-limit="15">
      <h3>Yet Another Header</h3>
      <div>Testing the function with a longer limit to see how it handles more text. The goal is to ensure that the text is truncated properly without breaking the structure of the HTML content.</div>
    </div>
    Login or Signup to reply.
  2. As you are using NODE_TYPE for checking the type of node, but h1 is also considered as a text node thus its words are also counted in the truncated string. So you can explore on some other way to distinguish between these nodes, or you can apply the shortenText logic on the div that contain the actual text.

    Also before checking for words length you can filter those by checking is there are some empty string or not in the words array, as you are using regex for removing specific strings thus there are some occurences of empty string in the words array.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search