﻿(function ($) {

    $.extend({ pwTyper: {

        wrap: function (s) {

            // remove legacy span.pwtext elements			
            s = s.replace(/(<span[^>]*?class="[^"]*pwText[^"]*>)([^<]*)(<\/span>)/g, '$2');

            // wraps all text nodes in a span.pwtext element 
            s = s.replace(/(>|^(?!<))([^<]+)(<|(?!>)$)/g, '$1<span class="pwText">$2</span>$3');

            // groups special characters such as &amp; into a span.pwSpecial element
            // only match special characters within existing .pwText elements (not in attributes)
            s = s.replace(/<span class="pwText">[^<]*&\S*;[^<]*<\/span>/g, function (m) { return m.replace(/(&[^;]+;)/g, '</span><span class="pwSpecial">$1</span><span class="pwText">'); });

            // remove empty pwText tags and wrap in a span.pwText if needed
            // s = s.replace(/<span class="pwText">[\n\r]+?<\/span>/g, "");

            return s;
        },

        // adds the next letter 
        type: function (G) {

            // increase dataIndex, which moves to the next element
            if (G.charCount > G.data[G.dataIndex].count) {
                G.dataIndex++;
            }

            // If typing is complete, restore the original state
            if (G.dataIndex == G.dataLength) {
                G.thisElement.data("finished", true);
                G.thisElement.html(G.content);


                // if typing is complete for ALL elements
                if (G.thisElement.data("callback")) {
                    var finished = true;
                    $(G.thisElement.data("get")).each(function () {
                        if (!$(this).data("finished")) { finished = false; }
                    });
                    if (finished) {
                        var callback = G.thisElement.data("callback");
                        if (callback) {
                            callback.call();
                        }
                    }
                }
                return false;
            }

            // show the current element and all previous elements which are still hidden
            var newOrder = G.data[G.dataIndex].element.data("order");
            for (G.order; G.order <= newOrder; G.order++) {
                G.thisElement.find('.order-' + G.order).removeClass('pwHidden');
            }

            // type the next character
            G.data[G.dataIndex].element.html(G.data[G.dataIndex].text.substr(0, G.charCount - ((G.dataIndex > 0) ? G.data[G.dataIndex - 1].count : 0)));
            G.delay = Math.round(G.minInterval + (Math.random() * (G.maxInterval - G.minInterval)));
            G.thisElement.data("int", setTimeout(function () { $.pwTyper.type(G) }, G.delay));

            G.charCount++;

            // Stores the G data in the element to use in pause and stop functions
            G.thisElement.data("G", G);
        },

        // removes the last letter 
        untype: function (G) {

            // increase dataIndex, which moves to the next element
            if (G.dataIndex > 0 && G.charCount <= G.data[G.dataIndex - 1].count) {
                G.dataIndex--;
            }

            // If untyping is complete
            if (G.charCount === 0) {
                G.thisElement.data("finished", true);
                G.thisElement.html("");

                // if untyping is complete for ALL elements
                if (G.thisElement.data("callback") && $(G.thisElement.data("get")).data('finished')) {
                    var finished = true;
                    $(G.thisElement.data("get")).each(function () {
                        if (!$(this).data("finished")) { finished = false; }
                    });
                    if (finished) {
                        var callback = G.thisElement.data("callback");
                        if (callback) {
                            callback.call();
                        }
                    }
                }
                return false;
            }

            // show the current element and all previous elements which are still hidden
            var newOrder = G.data[G.dataIndex].element.data("order");
            for (G.order; G.order > newOrder; G.order--) {
                G.thisElement.find('.order-' + G.order).remove();
            }

            // type the next character
            G.data[G.dataIndex].element.html(G.data[G.dataIndex].text.substr(0, G.charCount - 1 - ((G.dataIndex > 0) ? G.data[G.dataIndex - 1].count : 0)));
            G.delay = Math.round(G.minInterval + (Math.random() * (G.maxInterval - G.minInterval)));
            G.thisElement.data("int", setTimeout(function () { $.pwTyper.untype(G) }, G.delay));

            G.charCount--;

            // Stores the G data in the element to use in pause and stop functions
            G.thisElement.data("G", G);
        },

        createCSS: function (selector, declaration) {

            // test for IE
            var ua = navigator.userAgent.toLowerCase();
            var isIE = (/msie/.test(ua)) && !(/opera/.test(ua)) && (/win/.test(ua));

            // create the style node for all browsers, if it doesn't already exists	
            var style_node = document.getElementById('pwTyperStyles');
            if (!style_node) {
                style_node = document.createElement("style");
                style_node.setAttribute("type", "text/css");
                style_node.setAttribute("media", "screen");
                style_node.setAttribute("id", "pwTyperStyles");
            }

            // if the rule doesn't already exist, add it
            if (style_node.innerHTML.indexOf(selector + " {" + declaration + "}") === -1) {

                // append a rule for good browsers
                if (!isIE) style_node.appendChild(document.createTextNode(selector + " {" + declaration + "}\n"));

                // append the style node
                document.getElementsByTagName("head")[0].appendChild(style_node);

                // use alternative methods for IE
                if (isIE && document.styleSheets && document.styleSheets.length > 0) {
                    var last_style_node = document.styleSheets[document.styleSheets.length - 1];
                    if (typeof (last_style_node.addRule) == "object") last_style_node.addRule(selector, declaration);
                }
            }
        }
    }
    });


    $.fn.extend({

        stopTyper: function () {
            clearInterval(this.data("int"));
            return this;
        },

        resumeTyper: function () {
            this.data('func').call($.pwTyper, this.data("G"));
            return this;
        },

        finishTyper: function () {
            clearInterval(this.data("int"));
            if (this.data('func') == $.pwTyper.type) {
                this.html(this.data("content"));
            } else {
                this.empty();
            }

            var callback = this.data("callback");
            if (callback) {
                callback.call();
            }

            return this;
        },

        type: function (options) {

            // add CSS styles if they haven't already been added
            $.pwTyper.createCSS('.pwHidden', 'display:none;');

            clearInterval(this.data("int"));

            // Default settings
            var settings = {
                minInterval: 30,
                maxInterval: 90
            };

            // Processing settings
            settings = jQuery.extend(settings, options || {});

            this.data("func", $.pwTyper.type);
            this.data("get", this.get());
            this.data("callback", (settings.callback) ? settings.callback : null);


            return this.each(function () {

                var G = {
                    charCount: 0,
                    charTotal: 0,
                    data: [],
                    dataLength: 0,
                    dataIndex: 0,
                    thisElement: $(this),
                    order: 0,
                    delay: 0,
                    newText: "",
                    content: "",
                    minInterval: settings.minInterval,
                    maxInterval: settings.maxInterval
                };

                if (!settings.content) {
                    G.content = G.thisElement.html();
                } else if (settings.content instanceof jQuery) {
                    G.content = $(settings.content).html();
                } else {
                    G.content = settings.content;
                }
                G.thisElement.data("finished", false);
                G.thisElement.data("content", G.content);

                // wraps all text nodes in a pwText span element 
                G.newText = $.pwTyper.wrap(G.content);

                // Creates an order for all elements to progressively show them as the typing happens
                G.thisElement.html(G.newText).find('*').each(function (i) {
                    $(this).addClass("pwHidden").data("order", i).addClass("order-" + i);
                });

                // empties the text from the span elements and stores it in the 'data' variable
                G.thisElement.find('.pwText').each(function (i) {
                    G.data[i] = {
                        order: $(this).data("order"),
                        text: $(this).html(),
                        element: $(this),
                        count: (i > 0) ? $(this).html().length + G.data[i - 1].count : $(this).html().length
                    };
                    $(this).empty();
                });

                G.dataLength = G.data.length;
                G.charTotal = G.data[G.dataLength - 1].count;

                // if a time is specified, calculate the delay
                if (settings.time) {
                    G.delay = Math.floor(settings.time / G.charTotal);
                    if (G.delay === 0) { G.delay = 1; }
                    if (settings.deviation) {
                        if (settings.deviation > 1) { settings.deviation = 1; }
                        G.minInterval = Math.round(G.delay * (1 - settings.deviation));
                        G.maxInterval = G.delay + (G.delay - G.minInterval);
                        if (G.minInterval === 0) { G.minInterval = 1; }
                    } else {
                        G.minInterval = G.delay;
                        G.maxInterval = G.delay;
                    }
                }

                if (settings.delay) {
                    G.thisElement.data("int", setTimeout(function () { $.pwTyper.type(G) }, settings.delay));
                } else {
                    $.pwTyper.type(G);
                }
            });
        },

        untype: function (options) {

            // add CSS styles if they haven't already been added
            $.pwTyper.createCSS('.pwHidden', 'display:none;');

            clearInterval(this.data("int"));

            // Default settings
            var settings = {
                minInterval: 30,
                maxInterval: 90
            };

            // Processing settings
            settings = jQuery.extend(settings, options || {});

            this.data("func", $.pwTyper.untype);
            this.data("get", this.get());
            this.data("callback", (settings.callback) ? settings.callback : null);


            return this.each(function () {

                var G = {
                    charCount: 0,
                    charTotal: 0,
                    data: [],
                    dataLength: 0,
                    dataIndex: 0,
                    thisElement: $(this),
                    order: 0,
                    delay: 0,
                    newText: "",
                    content: $(this).html(),
                    minInterval: settings.minInterval,
                    maxInterval: settings.maxInterval
                };
                G.thisElement.data("finished", false);

                // wraps all text nodes in a pwText span element 
                G.newText = $.pwTyper.wrap(G.content);

                // Creates an order for all elements to progressively show them as the typing happens
                G.thisElement.html(G.newText).find('*').each(function (i) {
                    $(this).data("order", i).addClass("order-" + i);
                });

                // takes the text from the span elements and stores it in the 'data' variable
                G.thisElement.find('.pwText').each(function (i) {
                    G.data[i] = { order: $(this).data("order"), text: $(this).html(), element: $(this), count: (i > 0) ? $(this).html().length + G.data[i - 1].count : $(this).html().length };
                });

                if (G.data.length > 0) {
                    G.dataIndex = G.data.length - 1;
                    G.charTotal = G.data[G.dataIndex].count;
                    G.charCount = G.charTotal;
                    G.order = G.data[G.dataIndex].element.data("order");

                    // if a time is specified, calculate the delay
                    if (settings.time) {
                        G.delay = Math.floor(settings.time / G.charTotal);
                        if (G.delay === 0) { G.delay = 1; }
                        if (settings.deviation) {
                            if (settings.deviation > 1) { settings.deviation = 1; }
                            G.minInterval = Math.round(G.delay * (1 - settings.deviation));
                            G.maxInterval = G.delay + (G.delay - G.minInterval);
                            if (G.minInterval === 0) { G.minInterval = 1; }
                        } else {
                            G.minInterval = G.delay;
                            G.maxInterval = G.delay;
                        }
                    }
                }

                if (settings.delay) {
                    G.thisElement.data("int", setTimeout(function () { $.pwTyper.untype(G) }, settings.delay));
                } else {
                    $.pwTyper.untype(G);
                }
            });

        }
    });

})(jQuery);
