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>
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
andadjustedOffset
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>