Reputation: 342
As part of a larger script, I've been trying to make a page that would take a block of text from another function and "type" it out onto the screen:
function typeOut(page,nChar){
var txt = document.getElementById("text");
if (nChar < page.length){
txt.innerHTML = txt.innerHTML + page[nChar];
setTimeout(function () { typeOut(page, nChar + 1); }, 20);
}
}
typeOut('Hello, <b>world</b>!', 0)
<div id="text">
This basically works the way I want it to, but if the block of text I pass it has any HTML tags in it (like <a href>
links), those show up as plain-text instead of being interpreted. Is there any way to get around that and force it to display the HTML elements correctly?
Upvotes: 2
Views: 999
Reputation: 33
So there is a correct solution, pre-render behind the screen. And i have a solution: use DOMParser
(parseFromString) to get DOM structure, then traversing nodes.
function createTypewriter(element, htmlString, typingSpeed = 100) {
// parse HTML,get tree structure
const parser = new DOMParser();
const doc = parser.parseFromString(htmlString, "text/html");
const fragment = doc.body;
// recursively parse the DOM tree and generate a node queue
const nodes = [];
function traverse(node) {
if (node.nodeType === Node.TEXT_NODE) {
if (node.nodeValue.trim()) {
nodes.push({ type: "text", content: node.nodeValue });
}
} else if (node.nodeType === Node.ELEMENT_NODE) {
const clone = document.createElement(node.tagName.toLowerCase());
for (const attr of node.attributes) {
clone.setAttribute(attr.name, attr.value);
}
nodes.push({ type: "tag-start", element: clone });
for (const child of node.childNodes) {
traverse(child);
}
nodes.push({ type: "tag-end", element: clone });
}
}
traverse(fragment);
let cursor = 0;
const stack = []; // For handling nested tags
function typeNext() {
if (cursor >= nodes.length) return; // util content completed
const currentNode = nodes[cursor];
if (currentNode.type === "tag-start") {
// start tag,insert to current level
const elementNode = currentNode.element;
(stack[stack.length - 1] || element).appendChild(elementNode);
stack.push(elementNode); // push to stack
cursor++;
typeNext();
} else if (currentNode.type === "tag-end") {
// end tag
stack.pop();
cursor++;
typeNext();
} else if (currentNode.type === "text") {
const text = currentNode.content;
let textCursor = 0;
const parent = stack[stack.length - 1] || element; // current level
function typeText() {
if (textCursor < text.length) {
parent.appendChild(document.createTextNode(text[textCursor]));
textCursor++;
setTimeout(typeText, typingSpeed);
} else {
cursor++;
typeNext();
}
}
typeText();
}
}
typeNext();
}
const container = document.getElementById("root");
const htmlContent = "<span>This is a <strong>strong</strong> tag</span>";
createTypewriter(container, htmlContent, 100);
<div id="root"></div>
Upvotes: 0
Reputation: 816422
The problem is that you will create invalid HTML in the process, which the browser will try to correct. So apparently when you add <
or >
, it will automatically encode that character to not break the structure.
A proper solution would not work literally with every character of the text, but would process the HTML element by element. I.e. whenever you encounter an element in the source HTML, you would clone the element and add it to target element. Then you would process its text nodes character by character.
Here is a solution I hacked together (meaning, it can probably be improved a lot):
function typeOut(html, target) {
var d = document.createElement('div');
d.innerHTML = html;
var source = d.firstChild;
var i = 0;
(function process() {
if (source) {
if (source.nodeType === 3) { // process text node
if (i === 0) { // create new text node
target = target.appendChild(document.createTextNode(''));
target.nodeValue = source.nodeValue.charAt(i++);
// stop and continue to next node
} else if (i === source.nodeValue.length) {
if (source.nextSibling) {
source = source.nextSibling;
target = target.parentNode;
}
else {
source = source.parentNode.nextSibling;
target = target.parentNode.parentNode;
}
i = 0;
} else { // add to text node
target.nodeValue += source.nodeValue.charAt(i++);
}
} else if (source.nodeType === 1) { // clone element node
var clone = source.cloneNode();
clone.innerHTML = '';
target.appendChild(clone);
if (source.firstChild) {
source = source.firstChild;
target = clone;
} else {
source = source.nextSibling;
}
}
setTimeout(process, 20);
}
}());
}
Upvotes: 4
Reputation: 812
Your code should work. Example here : http://jsfiddle.net/hqKVe/2/
The issue is probably that the content of page[nChar]
has HTML chars escaped.
The easiest solution is to use the html()
function of jQuery (if you use jQuery). There a good example given by Canavar here : How to decode HTML entities using jQuery?
If you are not using jQuery, you have to unescape the string by yourself. In practice, just do the opposite of what is described here : Fastest method to escape HTML tags as HTML entities?
Upvotes: -1