Ben Kugler
Ben Kugler

Reputation: 342

How to append characters to innerHTML one-by-one, while ensuring that HTML tags are correctly interpreted?

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

Answers (3)

d1zzzy
d1zzzy

Reputation: 33

So there is a correct solution, pre-render behind the screen. And i have a solution: use DOMParserparseFromString) 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

Felix Kling
Felix Kling

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);
        }
    }());
}

DEMO

Upvotes: 4

Juljan
Juljan

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

Related Questions