2

Here's my function.

    function duplicate_step_through_highlighted (element_jq, target_jq, char_cb) {
        console.log( element_jq);
        var contents = element_jq.contents();
        for (var i = 0 ; i < contents.length; ++i) {
            // if text node, step
            if (contents[i].nodeType === 3) {
                // insert empty text node
                var new_tn = document.createTextNode('');
                target_jq.append(new_tn);

                // iterate it 
                var text = contents[i].nodeValue;
                for (var j = 0; j < text.length; j++) {
                    char_cb(text[j],new_tn);
                    new_tn.nodeValue += text[j];
                    // *** I want an async delay here *** 
                }
            } else { // type should be 1: element
                // target_jq gets a duplicate element inserted, copying attrs
                var new_elem = $(contents[i].cloneNode(false)).appendTo(target_jq);
                duplicate_step_through_highlighted($(contents[i]),$(new_elem),char_cb);

                // then a recursive call is performed on the newly created element as target_jq
                // and the existing one as element_jq. char_cb is passed in
            }
        }
    }

What I'm doing is rebuilding an HTML element by reconstructing it one character at a time. There is a good reason for doing this, I want the visual effect of it getting "typed in".

So right now there are no delays so my element gets duplicated instantly. I have checked that the result is consistent, but it is becoming clear to me that I will probably need to completely re-write the functionality in order for me to be able to put in an asynchronous delay after each character is inserted.

Will I need to re-write it and have a stack to keep track of my position within the elements?

Steven Lu
  • 41,389
  • 58
  • 210
  • 364
  • Wouldn't it be way easier to export the element as a string and then work on that string instead of working with real elements? – Yoshi Jul 26 '12 at 08:43
  • Too much string <--> dom conversion if I do that. It might not be too slow but I'm not gonna write inefficient code if I can help it. I wanna see the element gain new letters dynamically. I'm actually planning on doing animations on the letter being inserted. – Steven Lu Jul 26 '12 at 08:44

3 Answers3

4

You might want to have a look at my recent answer or this older one (Demo), on how to implement such an effect.


Tip: Don't clone the elements into new ones, just hide them and make them appear part-for-part.

Also, it might be easier not to deal with jQuery instances at all but native DOM elements. So yes, a rewrite might do :-) And I think it does need a stack as well.

function animate(elements, callback) {
/* get: array with hidden elements to be displayes, callback function */
    var i = 0;
    (function iterate() {
        if (i < elements.length) {
            elements[i].style.display = "block"; // show
            animateNode(elements[i], iterate); 
            i++;
        } else if (callback)
            callback();
    })();
    function animateNode(element, callback) {
        var pieces = [];
        if (element.nodeType==1) {
            while (element.hasChildNodes())
                pieces.push(element.removeChild(element.firstChild));
            setTimeout(function childStep() {
                if (pieces.length) {
                    animateNode(pieces[0], childStep); 
                    element.appendChild(pieces.shift());
                } else
                    callback();
            }, 1000/60);
        } else if (element.nodeType==3) {
            pieces = element.data.match(/.{0,2}/g); // 2: Number of chars per frame
            element.data = "";
            (function addText(){
                element.data += pieces.shift();
                setTimeout(pieces.length
                    ? addText
                    : callback,
                  1000/60);
            })();
        }
    }
}

animate($("#foo").children());

Demo at jsfiddle.net

How it works:

  • The addText function adds some character to the current text node, and sets a timeout for itself - animation! In case everything is done, it invokes the callback function.
  • childStep runs the animation on a childnode, and passes itself as the callback until no children are left - then nvokes the callback function.
  • Both together, animateNode recursively runs over the node tree and animates the textnodes in thier order.
  • the iterate function calls animateNode (after unhinding them) on all input elements, by passing itself as the callback. After all input elements are finished, it invokes the outer callback which is given as the second argument to animate.
Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Wait, what? You say i'll need a stack but your implementation doesn't need one. This code seems to work really well but I can't seem to wrap my head around it... – Steven Lu Jul 26 '12 at 20:46
  • It's a call stack :-) Also, this solution builds a stack of pieces to append, which are either child-nodes to recursively animate or text snippets to add frame-by-brame. – Bergi Jul 27 '12 at 09:23
  • Can you tell me the *procedure* you went through to produce this code? What sort of rules do you use to convert the code I started with into this? I think I am close to understanding why and how it works but I'm still unsure about where the code blocks when waiting for the timeouts to fire. It seems like that has to be necessary but it isn't clear to me where those parts are. Thanks – Steven Lu Jul 29 '12 at 01:02
  • Actually, I did not convert your code :-) I only looked into what it does, and then thought about how to do it asynchronously, and wrote the solution from scratch. – Bergi Jul 31 '12 at 20:55
  • I see. Thanks for doing that, Bergi, I have been studying this bit of code for quite some time and it has helped me gain a better understanding of functional programming. With continuation passing style, I always struggled with how to simplify it the most (to use fewer functions) because of the sheer confusingness. But after thinking about it for long enough it's clear that iteration simply must be converted into recursion (which may not even be necessary). I am happy to be getting close to a point where I can be comfortable converting synchronous code to CPS, which opens a lot of doors. – Steven Lu Aug 01 '12 at 02:08
2

Here is my solution, it is a more efficient, cleaner and faster way of doing it:

var start = 0; //Makes sure you start from the very beggining of the paragraph.
var text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras viverra sem dolor, nec tempor purus luctus vitae. Nulla massa metus, iaculis et orci euismod, faucibus fringilla metus. Sed pellentesque in libero nec.'; //Your text
var speed = 14; //Of course you can choose your own speed, 0 = instant, the more you add the slower it gets.
function typeWriter() {
  if (start < text.length) {
    document.querySelector('.demo').innerHTML += text.charAt(start);
    start++;
  }
  setTimeout(typeWriter, speed);
}
<body onload="typeWriter();">

<p class="demo"></p>

</body>
codeWithMe
  • 852
  • 12
  • 17
1

I made a simple script to use on my website, it might help those who are looking to achieve this effect.

Here is a link for the repo at Github, here is the explanation:

class Typer {

    constructor(typingSpeed, content, output) {

        this.typingSpeed = typingSpeed;
        // Parses a NodeList to a series of chained promises
        this.parseHtml(Array.from(content), output);
    };

    makePromise(node, output) {

        if (node.nodeType == 1) // element 
        {
            // When a new html tag is detected, append it to the document
            return new Promise((resolve) => {
                var tag = $(node.outerHTML.replace(node.innerHTML, ""));
                tag.appendTo(output);
                resolve(tag);
            });

        } else if (node.nodeType == 3) // text
        {
            // When text is detected, create a promise that appends a character
            // and sleeps for a while before adding the next one, and so on...
            return this.type(node, output, 0);
        } else {
            console.warn("Unknown node type");
        }
    }

    parseHtml(nodes, output) {
        return nodes.reduce((previous, current) => previous
            .then(() => this.makePromise(current, output)
                .then((output) => this.parseHtml(Array.from(current.childNodes), output))), Promise.resolve());
    }

    type(node, output, textPosition) {
        var textIncrement = textPosition + 1;

        var substring = node.data.substring(textPosition, textIncrement);

        if (substring !== "") {
            return new Promise(resolve => setTimeout(resolve, this.typingSpeed))
                .then(() => output.append(substring))
                .then(() => this.type(node, output, textIncrement));
        }

        return Promise.resolve(output);
    }
}
Fábio Junqueira
  • 2,701
  • 21
  • 20
  • My last answer was deleted, maybe because it only had a link? Here is a more complete version – Fábio Junqueira Feb 09 '17 at 12:48
  • 1
    yeah and my comment went along with it. Have you thought about using generators? I think generators are the natural way to express this type of async behavior – Steven Lu Feb 09 '17 at 17:00
  • To be honest, I haven't heard of them before. After googling a bit it seems they could simplify this code immensely! I'm definitely going to take a closer look at it, thanks for the insight :) – Fábio Junqueira Feb 09 '17 at 19:15
  • No worries. JS has always been *capable* of sweet async control flow but only recently is it practically presented in an accessible way... passing around continuations and dealing with all the closures is too mind-bending and error prone! The future is bright that's for sure! – Steven Lu Feb 09 '17 at 20:02