I am trying to achieve typing effect which i did but the problem is that the text i have and is being typed inside a <p>
tag as it's inner HTML has also some HTML tags like <span></span>
and because the typing is char by char it gets typed out is <span>
instead of rendered as the element. Is there any way to achieve the effect while keeping the HTML tags as they are? I have highlighted words that are wrapped in spans what i ultimately want to achieve is have the words typed out as highlighted...
function typeWriter() {
if (i < txt.length) {
document.getElementById("content_html").innerHTML += txt.charAt(i);
i++;
setTimeout(typeWriter, self.typing.speed);
}
}
typeWriter();
Here is an example of a text i am trying to type out :
<p>Surface chemistry deals with phenomena that occur at the surfaces or interfaces. Many important phenomena, noticeable amongst this being corrosion, electrode processes, heterogeneous catalysis, dissolution, and crystallization occur at interfaces. The subject of surface chemistry finds many applications in industry, analytical work, and daily life situations.</p>
It contains a <p>
tag which can in the future change to any other HTML tag since this is ing from the server...
I am trying to achieve typing effect which i did but the problem is that the text i have and is being typed inside a <p>
tag as it's inner HTML has also some HTML tags like <span></span>
and because the typing is char by char it gets typed out is <span>
instead of rendered as the element. Is there any way to achieve the effect while keeping the HTML tags as they are? I have highlighted words that are wrapped in spans what i ultimately want to achieve is have the words typed out as highlighted...
function typeWriter() {
if (i < txt.length) {
document.getElementById("content_html").innerHTML += txt.charAt(i);
i++;
setTimeout(typeWriter, self.typing.speed);
}
}
typeWriter();
Here is an example of a text i am trying to type out :
<p>Surface chemistry deals with phenomena that occur at the surfaces or interfaces. Many important phenomena, noticeable amongst this being corrosion, electrode processes, heterogeneous catalysis, dissolution, and crystallization occur at interfaces. The subject of surface chemistry finds many applications in industry, analytical work, and daily life situations.</p>
It contains a <p>
tag which can in the future change to any other HTML tag since this is ing from the server...
-
So to clarify, you want the text inside the
<p>
tag to have the type writer effect. You want any words inside the<p>
tag that have an anchor tag wrapper around them to display the same type writer effect - BUT rather than stripping the element of it's anchor properties it will retain them and once the type writer effect has finished you can click on the link as you would expect? – Aaron McGuire Commented Aug 19, 2019 at 12:50 - No i have a parent <p> tag that should contain any text ing from server, but the text from server can contain HTML Tags itself, like words wrapped in spans for some effects... – myoda999 Commented Aug 19, 2019 at 12:53
- 1 How about this workaround using a library. jsfiddle/we8tchjk – little_coder Commented Aug 19, 2019 at 13:14
- @little_coder that library does the right job, but i have one error it never stops typing out the text over and over again, will have to look for the error in its script file – myoda999 Commented Aug 19, 2019 at 13:29
- @little_coder the library does the job perfectly! The error was on my side, i am using Vue.js and it did run in a loop because of the dynamic content render. This is the best option to keep styles, inside tags and dynamic content while achieving the effect, please add this as an answer so i can accept it – myoda999 Commented Aug 19, 2019 at 13:33
7 Answers
Reset to default 2I don't fully understand your question, but you can also achieve a typing animation with pure CSS as shown in this Codepen which could remove some of the issues you are having.
animation: typing 7s steps(15, end), /* # of steps = # of chars */
blink-caret .5s step-end infinite alternate;
I belive you are looking for
document.getElementById("demo").innerText = "<p>asd</p>";
instead of innerHTML
This way it will render exacly "<p>asd</p>"
instead of creating an element
Try This out Where I Am Rendering The Whole Text After You Have Appended Everything In It.
<script>
var i = 0;
txt = "<p>test this out</p>";
function typeWriter() {
if (i < txt.length) {
document.getElementById("content_html").innerHTML += txt.charAt(i);
i++;
setTimeout(typeWriter, 100);
} else {
document.getElementById("content_html").innerHTML = document.getElementById("content_html").innerText;
}
}
typeWriter();
</script>
So I wanted to make it myself because task is interesting. I know that @little_coder has already provide the best answer in ments to question.
This is what I have done:
js:
// text
txt = "asd<p>test this out<span>with more </span>and between text<span class='second'>second</span></p>afe";
//shared array
var instructions = [] ;
// typeWriter
var i = 0; //
var j = 0;
var elem = '';
var elem_value = '';
var speed = 50;
function typeWriter() {
if(j < instructions.length){
if (typeof instructions[j][1] == 'string'){
if (i < txt.length) {
instructions[j][0].innerHTML += instructions[j][1].charAt(i);
i++;
setTimeout(typeWriter, speed);
}else{
j=j+1;
i = 0;
setTimeout(typeWriter, speed);
}
}
else if(typeof instructions[j][1] == 'object'){
console.log("ins", instructions[j][0]);
instructions[j][0].appendChild(instructions[j][1]);
j=j+1;
i=0;
typeWriter();
}
}
}
//
// recreateNode
parser = new DOMParser();
function recreateNode(list, container){
doc = parser.parseFromString(list, "text/html");
doc.body.childNodes.forEach(function(a){
console.log(a);
if(a.nodeName == '#text'){
instructions.push([container, a.nodeValue])
}
else{ // if there is element to create
b = a.cloneNode(true); // handle deep elements
c = a.cloneNode(false); // this way I can get ONLY the element with attributes and classes
/* container.appendChild(c) */; // I append only element
instructions.push([container, c]);
recreateNode(b.innerHTML, c); // b will be appended to c
}
});
}
// init
parent = document.getElementById("content_html")
recreateNode(txt, parent);
typeWriter();
First I created recreateNode
that creates instructions
array that stores steps for recreating html structure of the text
. Using this instructions in typeWriter
I make typing effect
or create html element
. Keeping in instructions container
value I know where to put next text.
Everything would be ok but when it es to .appendChild
or at least I think this is the cause... appending elements takes time that makes typing effect
not being fluent.
js fiddle: https://jsfiddle/an5gzL7f/3/
Thanks in advance for any tips how it could work fast
I just got through a similar challenge while updating TypeIt (https://typeitjs.) so that it can handle typing nested HTML. In short, the approach I used was repeatedly traversing over all the childNodes
of the parent element, pulling out each nested node and expanding it into an array of queue items to be later typed.
It got a little hairy working through it all, but if it's helpful, you're wele to search through the code to see how I'm doing it:
https://github./alexmacarthur/typeit/blob/master/src/helpers/chunkStrings.js#L98
A lot of it is very TypeIt-specific -- particularly how I construct objects whose properties contain the information needed to reconstruct those elements.
The most elegant solution is probably the one using the CSS only. However it es with the downside, that if the text has a line break or is not written in a monospace font, it's basically unpracticable. I took the liberty to write a patch of code to acpany the rest of the solutions that are all useful in their own way.
The downside to my solution is that i used the textContent
property, which will not render as HTML. I refuse to use innerHTML for security reasons. So adding <a>
tags is probably not the best idea at the moment.
The goal of my approach is to use the window.requestAnimationFrame
hook, because it's so powerful. And the rest of the code is still a bit cluttered but at least it's self explanatory.
let paragraphNode = document.querySelector('p');
let textObject = {
actionCounter: 0,
typeDelay: 6,
cursorDelay: 22,
cursorShow: false,
currentIndex: 0,
stringLength: paragraphNode.textContent.length,
textContent: paragraphNode.textContent
}
paragraphNode.textContent = "";
const writeText = ()=>{
textObject.actionCounter ++;
if( textObject.actionCounter % textObject.typeDelay == 0 ){
textObject.currentIndex += 1;
}
let string = textObject.textContent.substring(0, textObject.currentIndex);
if( textObject.actionCounter % textObject.cursorDelay == 0 ){
textObject.cursorShow = !textObject.cursorShow;
}
paragraphNode.textContent = (textObject.cursorShow) ? string + "|" : string;
window.requestAnimationFrame(writeText);
}
window.requestAnimationFrame(writeText);
Here is the codepen example i worked out. The only thing missing at the time of writing is a person brave enough to replace textContent
with innerHTML
and possibly check if the next letter is a '<' and add the whole tag at once. Might be tricky though. I strongly remend against it.
typewriter on codepen
Here's a solution I coded (it's in TypeScript, but I included plain JavaScript code at the bottom of this answer).
https://codepen.io/SonicBoomNFA/pen/VwOKPbm
Essentially, it performs a depth-first search (DFS) traversal of the given element (call it root
) and finds every Node
that is an instance of Text
(https://developer.mozilla/en-US/docs/Web/API/Text). Upon finding each Text
, it stores that Text
node, the node's original text content, an array containing the words from the node split using regex, the number of words restored, and the number of characters restored (both initially set to 0). It stores that record in a list, so we end up with a list of sequential records for all the Text
nodes in root
.
To add all the text back, I essentially traverse the list and look for the earliest record that contains a node that has not received all of its text contents back. I add a character or a word to that node (depending on what setting is chosen) and keep going until either its original text value has been fully restored (at which point we iterate to the next available node) or the maximum number of words/characters has been reached for the given percentage. To remove all the text, pretty much the opposite is done.
Upsides
- The original HTML is pletely preserved. No elements are added or removed, so things like event listeners and variable references remain intact.
- I keep track of the words/characters added/removed outside of the actual addition/removal functions, so each time the functions are run, they will iterate back to the node that needs to be updated without having to remodify the previous nodes (so the number of modifications = O(c) or O(w), where c = the number of characters in the root and w = the number of words in the root).
Downsides
- I didn't calculate how adding/removing words affects the number of characters that were added/removed (and vice versa), but as long as those stats are reset when the functions are run, it doesn't matter.
- Even though pleted nodes are not remodified, I didn't spend the time to make the loops just jump straight to the node that needs to be updated. I think that's a negligible cost though.
// returns a Promise that resolves after the given number of milliseconds passes
function wait(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
// forms a list of data related to text Text nodes in a DFS traversal (so it's sequential)
function constructInfixTextNodeList(node) {
const infixTextNodeList = [];
constructInfixTextNodeListR(node, infixTextNodeList);
return infixTextNodeList;
}
function constructInfixTextNodeListR(node, infixTextNodeList) {
if (node instanceof Text) {
const datum = [node, node.nodeValue, node.nodeValue.match(/\W*((\w+)'?(\w+)?)\W*/g) ?? [], 0, 0];
infixTextNodeList.push(datum);
}
else {
for (const childNode of node.childNodes) {
constructInfixTextNodeListR(childNode, infixTextNodeList);
}
}
}
// erase the text content of each Text node
function eraseText(infixTextNodeList) {
for (const nodeList of infixTextNodeList) {
nodeList[0].nodeValue = '';
}
}
// returns the number of characters in a Text node
function numCharsInNode(datum) { return datum[2].reduce((sum, str) => sum + str.length, 0); }
// returns the number of words in a Text node
function numWordsInNode(datum) { return datum[2].length; }
// returns the total number of words and characters in the root element (just considering text from Text nodes)
function totalWordsAndChars(infixTextNodeList) {
const [totalWords, totalChars] = infixTextNodeList.reduce((sumTuple, datum) => [
sumTuple[0] + numWordsInNode(datum),
sumTuple[1] + numCharsInNode(datum)
], [0,0]);
return { totalWords, totalChars };
}
// add to the Text nodes such that only the given percentage of total words or characters are shown overall
// Note that if 'by-word' is used, only ttStats.wordsAdded gets updated, not ttStats.charsAdded (and vice versa)
function typeTextIterAdd(infixTextNodeList, percentage, ttStats, splitType) {
// maximum amount of words and characters that can be present given current percentage
const wordAllowance = Math.ceil(ttStats.totalWords * percentage);
const charAllowance = Math.ceil(ttStats.totalChars * percentage);
switch(splitType) {
case 'by-word':
// while the total number of words added is less than allowance...
while (ttStats.wordsAdded < wordAllowance) {
// for each text node entry...
for (const datum of infixTextNodeList) {
const [textNode, trueVal, words] = datum;
// add string fragments until text node is either pleted or until the allowance is reached
let wordsToAdd = '';
while(ttStats.wordsAdded < wordAllowance && datum[3] < words.length) {
wordsToAdd += words[datum[3]];
datum[3] += 1; // increment nextWordIndex
ttStats.wordsAdded += 1;
}
textNode.nodeValue += wordsToAdd;
}
}
break;
case 'by-character':
// while the total number of chars added is less than allowance...
while (ttStats.charsAdded < charAllowance) {
// for each text node entry...
for (const datum of infixTextNodeList) {
const [textNode, trueVal] = datum;
// add characters until text node value is restored pleted or until the allowance is reached
let charsToAdd = '';
while(ttStats.charsAdded < charAllowance && datum[4] < numCharsInNode(datum)) {
charsToAdd += trueVal[datum[4]];
datum[4] += 1; // increment nextCharIndex
ttStats.charsAdded += 1;
}
textNode.nodeValue += charsToAdd;
}
}
break;
default:
throw new Error('what/??');
}
}
// remove from the Text nodes such that only the given percentage of total words or characters are shown overall
// Note that if 'by-word' is used, only ttStats.wordsRemoved gets updated, not ttStats.charsRemoved (and vice versa)
function typeTextIterRemove(infixTextNodeList, percentage, ttStats, splitType) {
// minimum amount of words and characters that can be present given current percentage
const wordAllowance = Math.ceil(ttStats.totalWords * percentage);
const charAllowance = Math.ceil(ttStats.totalChars * percentage);
const reversedList = [...infixTextNodeList];
reversedList.reverse();
switch(splitType) {
case 'by-word':
// while the total number of words added is less than allowance...
while (ttStats.wordsRemoved < wordAllowance) {
// for each text node entry...
for (const datum of reversedList) {
const [textNode, _, words] = datum;
// add string fragments until text node is either pleted or until the allowance is reached
let wordsToRemove = '';
while(ttStats.wordsRemoved < wordAllowance && datum[3] > 0) {
datum[3] -= 1; // decrement nextWordIndex
wordsToRemove += words[datum[3]];
ttStats.wordsRemoved += 1;
}
textNode.nodeValue = textNode.nodeValue.substring(0, textNode.nodeValue.length - wordsToRemove.length)
}
}
break;
case 'by-character':
// while the total number of chars added is less than allowance...
while (ttStats.charsRemoved < charAllowance) {
// for each text node entry...
for (const datum of reversedList) {
const [textNode, trueVal] = datum;
// add characters until text node value is restored pleted or until the allowance is reached
let charsToRemove = '';
while(ttStats.charsRemoved < charAllowance && datum[4] > 0) {
datum[4] -= 1; // decrement nextCharIndex
charsToRemove += trueVal[datum[4]];
ttStats.charsRemoved += 1;
}
textNode.nodeValue = textNode.nodeValue.substring(0, textNode.nodeValue.length - charsToRemove.length);
}
}
break;
default:
throw new Error(`Invalid splitType value ${splitType}`);
}
}
// ************************************************************************************************************************* /
// ************************************************************************************************************************* /
// ************************************************************************************************************************* /
// ************************************************************************************************************************* /
// ************************************************************************************************************************* /
// ************************************************************************************************************************* /
// ************************************************************************************************************************* /
// ************************************************************************************************************************* /
// ************************************************************************************************************************* /
// ************************************************************************************************************************* /
// ************************************************************************************************************************* /
// ************************************************************************************************************************* /
// first, form list of data related to each Text node
const infixList = constructInfixTextNodeList(document.querySelector('.target'));
// then erase the text contents of each node
eraseText(infixList);
const ttStats = {...totalWordsAndChars(infixList), wordsAdded: 0, charsAdded: 0, wordsRemoved: 0, charsRemoved: 0};
let percentage = 0;
let alpha = 0.01; // rate to change percentage
async function typeText(splitType) {
while(percentage <= 1) {
typeTextIterAdd(infixList, percentage, ttStats, splitType);
await wait(100);
percentage += alpha;
}
// percentage might jump from 0.9999999 to 1.0000007 because of JavaScript inaccuracy
// if so, forcefully set percentage to 1 to ensure the full text is restored
if (percentage > 1) {
percentage = 1;
typeTextIterAdd(infixList, percentage, ttStats, splitType);
}
}
async function removeText(splitType) {
while(percentage >= 0) {
typeTextIterRemove(infixList, 1 - percentage, ttStats, splitType);
await wait(100);
percentage -= alpha;
}
if (percentage < 0) {
percentage = 0;
typeTextIterRemove(infixList, 1 - percentage, ttStats, splitType);
}
}
// ************************************************************************************************************************* /
// ************************************************************************************************************************* /
// ************************************************************************************************************************* /
// ************************************************************************************************************************* /
// ************************************************************************************************************************* /
// ************************************************************************************************************************* /
// ************************************************************************************************************************* /
// ************************************************************************************************************************* /
// ************************************************************************************************************************* /
// ************************************************************************************************************************* /
// ************************************************************************************************************************* /
// ************************************************************************************************************************* /
async function addAndRemoveText(splitType) {
while (true) {
ttStats.wordsRemoved = 0;
ttStats.wordsAdded = 0;
ttStats.charsRemoved = 0;
ttStats.charsAdded = 0;
await typeText(splitType);
await removeText(splitType);
}
}
addAndRemoveText('by-character');
div span {
color: red;
}
span span {
color: blue;
}
p {
color: green;
}
<div class="target">Testing, everything is <span>interesting!<span> What?? ∞</span>hmmm</span> stuff ain't<br/>real.
<p>Maybe if we tried together? But... <span>What would Juan think?</span></p></div>