Javascript live typing animation that handles HTML tags

I’m building an HTML/CSS/JavaScript + jQuery web-application. I have a page that calls an API, which returns a string with HTML code. Here’s an example:

// Data returned by server

data = {
  text: "<b>Here is some example text<b> followed by line breaks<br><br><br><br><br",
};

I need to implement live typing animation to display the data. I’ve got that working (see code below), but when there’s an HTML tag such as <b> or <br>, the < characters briefly flash on the screen before the element is properly displayed. Is there some kind of decoding function I need to run before calling the function to display the text?

// The text we want to animate
let text =
  "Here is some example text followed by a line break<br><br>and another line break<br><br><b>as well as some bold text.<b>";

// The current position of the animation
let index = 0;

// Function to trigger the live typing animation
function typingAnimation(id) {
  setTimeout(() => {
    index++;
    updateText(id);
    if (index < text.length) {
      typingAnimation(id);
    }
  }, 50);
}

// Function to update the text 
function updateText(id) {
  // Get the element with our id
  const typing = document.getElementById(id);
  // Split our text into lines based on newline characters or <br> tags
  const lines = text.substring(0, index).split(/n|<br>/);

  // Check if our element exists before updating it
  if (typing == null) {
    return;
  }

  //------> EDIT:: Attach click listener for text div so the user can click to skip animation
  $("#skipAnimationBtn").on("click", () => {
    index = text.length;

  });

  // Update the element with our new lines
  typing.innerHTML = lines
    .map((line, index) => {
      // Check if this is the last line in our text
      const isLastLine = index === lines.length - 1;

      // Add a line break if this isn't the last line of text
      const lineBreak = isLastLine ? "" : "<br>";

      // Return our line of text with or without a line break
      return `${line}${lineBreak}`;
    })
    .join("");
}

typingAnimation("typing-animation");
#parent {
  display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<div id="parent">
  <button id="skipAnimationBtn">Click to Skip Animation</button>
  <div id="typing-animation"></div>
</div>

2

The break tags are easy, just replace those with a new line tag /n.

It gets complicated with the bold tags. The only way I see to do this is to replace() the bold tags within the initial string, defining a newly reformatted text variable. A function within a replace method to get a start and end range of the bold characters and save them in an array of objects.

Then iterate over the reformatted text variable and use a conditional defining a boolean variable with .some() to check the range start and end, when we have a match, wrap those characters in a span tag that has a class which can be formatted in CSS to add the bold formatting.

Then in the updateText() method we swap out formattedText accordingly using the current index, apply the bold formatting and then assign the innerHTML.

I have further comments in the snippet to help explain the logic in more detail.

EDIT: Added logic => tagOffset and adjustedOffset to track the
exact amount of characters after the bold tags were removed
within the replace method. The indexing was getting off because it was
still referring to the indexes of the bold tag characters within the string.

let text = "Ferrars all spirits his imagine effects amongst neither.<br><br>Sure last upon he same as knew next. <b>End friendship sufficient assistance</b> can prosperous met. As game he show it park do. Was has <b>unknown few certain </b>ten promise. No finished my <b>an likewise cheerful packages we are.</b>For assurance concluded son <br><br>son something depending discourse see led collected. <b>Packages oh no denoting my advanced humoured.</b> Pressed be so thought natural.<br>";

const skip = document.querySelector('#skipAnimation');

// replace <br> tags with newline characters
text = text.replace(/<br>/g, 'n');

// define an array to hold any bold strings
let boldTextRange = [];

// we need to track positions of the shift of characters when we remvoe the break tags
let replacedText = ''; // initialize a variable to track the character offset

// initialize a variable to keep a closer track on the 
// positions of the characters before removing bold tags
let tagOffset = 0; 

// strip <b> tags from the string and define a start and end 
// range object and push that into the boldTextRange array
replacedText = text.replace(/<b>(.*?)</b>/g, (match, newText, offset) => {
  const adjustedOffset = offset - tagOffset; // FIX
  boldTextRange.push({ 
    start: adjustedOffset, 
    end: adjustedOffset + newText.length 
  });
  tagOffset += match.length - newText.length; // track how many characters were removed exactly
  return newText; // return unformatted text
});

// get the length of the stripped and formatted text
let totalLength = replacedText.length;

// define the index to track where in the typing animation
let index = 0;

// added a skip animation, since index is defined outside update  
// methods scope and changed inside the scope, you can that
// simply place logic right after indexes initial definition
function skipAnimation(){
  return index = text.length;
}
skip.addEventListener('click', skipAnimation);


// function for the typing animation
function typingAnimation(id) {
  setTimeout(() => {
    index++;
    updateText(id);
    if (index < totalLength) {
      typingAnimation(id);
    }
  }, 50);
}

// helper function to apply bold formatting 
function applyBoldFormatting(replacedText) {
  // define an empty variable to hold the formatted text
  let formattedText = '';
  // set a boolean to track wrapping in span
  let boldBool = false;

  // iterate over the text and apply bold formatting
  for (let i = 0; i < replacedText.length; i++) {

    // define a boolean to track the range start and end for bold formatted text
    const isBold = boldTextRange.some(range => i >= range.start && i < range.end);
    
    // conditional to check if character is bold and set 
    // formatted text span tags open and closing tags
    if (isBold && !boldBool) {
      formattedText += `<span class="bold">`;
      boldBool = true; // set boolean to track range
    } else if (!isBold && boldBool) {
      formattedText += `</span>`; // close out the span tag
      boldBool = false; // reset boolean to false 
    }

    // add the next character to the string
    formattedText += replacedText[i];
  }

  // return the formatted string
  return formattedText;
}

// function to update the text in the element
function updateText(id) {
  // get the element to display the typing animation
  const typing = document.getElementById(id);
  if (!typing) return;

  // geet the text up to the current index
  let currentText = replacedText.substring(0, index);

  // pass in the currentText to apply bold formatting
  let formattedText = applyBoldFormatting(currentText);

  // replace newline characters with <br> for proper line breaks
  formattedText = formattedText.replace(/n/g, '<br>');

  // display the formatted text animation
  typing.innerHTML = formattedText;
}

// start the typing animation
typingAnimation("typing-animation");
.bold {
  font-weight: bold;
}
<div>
  <button id="skipAnimation">Skip Animation</button>
  <div id="typing-animation"></div>
</div>

8

Your issue stems from the broken regex (which is not even necessary) & to be fair you shouldn’t parse HTML with regex anyway.

While not the best approach you can get the number of opened and closed tags, and continue slicing up the string until you complete a tag.

Here is some code I threw around in JSFiddle. Seems to be working right.

// The text we want to animate
let text =
  "<br>Here is some example text followed by a line break<br><br>and another line break<br><br><b>as well as some bold text.<b>";

// The current position of the animation
let index = 0;

// Function to trigger the live typing animation
function typingAnimation(id) {
  setTimeout(() => {
    index++;
    updateText(id);
    if (index < text.length) {
      typingAnimation(id);
    }
  }, 50);
}

// Function to update the text 
function updateText(id) {
  // Get the element with our id
  const typing = document.getElementById(id);
  
  // Split our text into lines
  let lines = text.substring(0, index).split("");
  // Get the amount of opened tags
  let opcount = lines.filter(x => x === '<').length;
  // Get the amount of closed tags
  let endcount = lines.filter(x => x === '>').length;
  
  // Repeat until we have matching number of both open and closed tags
  do {
    index++;
    lines = text.substring(0, index).split("");
    opcount = lines.filter(x => x === '<').length;
    endcount = lines.filter(x => x === '>').length;
  } while(opcount !== endcount);
  
  // Check if our element exists before updating it
  if (typing == null) {
    return;
  }

  // Update the element with our new lines
  typing.innerHTML = lines.join("");
}

typingAnimation("typing-animation");

1

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật