<commit_before>/* Isso – Ich schrei sonst!
*/
var $ = require("app/dom");
var utils = require("app/utils");
var config = require("app/config");
var api = require("app/api");
var template = require("app/template");
var i18n = require("app/i18n");
var identicons = require("app/lib/identicons");
var globals = require("app/globals");

"use strict";

var Postbox = function(parent) {

    var localStorage = utils.localStorageImpl,
        el = $.htmlify(template.render("postbox", {
        "author":  JSON.parse(localStorage.getItem("isso-author")),
        "email":   JSON.parse(localStorage.getItem("isso-email")),
        "website": JSON.parse(localStorage.getItem("isso-website")),
        "preview": ''
    }));

    // callback on success (e.g. to toggle the reply button)
    el.onsuccess = function() {};

    el.validate = function() {
        if ($(".isso-textarea", this).value.length < 3) {
            $(".isso-textarea", this).focus();
            return false;
        }
        if (config["require-email"] &&
            $("[name='email']", this).value.length <= 0)
        {
          $("[name='email']", this).focus();
          return false;
        }
        if (config["require-author"] &&
            $("[name='author']", this).value.length <= 0)
        {
          $("[name='author']", this).focus();
          return false;
        }
        return true;
    };

    // only display notification checkbox if email is filled in
    var email_edit = function() {
        if (config["reply-notifications"] && $("[name='email']", el).value.length > 0) {
            $(".isso-notification-section", el).show();
        } else {
            $(".isso-notification-section", el).hide();
        }
    };
    $("[name='email']", el).on("input", email_edit);
    email_edit();

    // email is not optional if this config parameter is set
    if (config["require-email"]) {
        $("[for='isso-postbox-email']", el).textContent =
            $("[for='isso-postbox-email']", el).textContent.replace(/ \(.*\)/, "");
    }

    // author is not optional if this config parameter is set
    if (config["require-author"]) {
      $("[for='isso-postbox-author']", el).textContent =
        $("[for='isso-postbox-author']", el).textContent.replace(/ \(.*\)/, "");
    }

    // preview function
    $("[name='preview']", el).on("click", function() {
        api.preview($(".isso-textarea", el).value).then(
            function(html) {
                $(".isso-preview .isso-text", el).innerHTML = html;
                el.classList.add('isso-preview-mode');
            });
    });

    // edit function
    var edit = function() {
        $(".isso-preview .isso-text", el).innerHTML = '';
        el.classList.remove('isso-preview-mode');
    };
    $("[name='edit']", el).on("click", edit);
    $(".isso-preview", el).on("click", edit);

    // submit form, initialize optional fields with `null` and reset form.
    // If replied to a comment, remove form completely.
    $("[type=submit]", el).on("click", function() {
        edit();
        if (! el.validate()) {
            return;
        }

        var author = $("[name=author]", el).value || null,
            email = $("[name=email]", el).value || null,
            website = $("[name=website]", el).value || null;

        localStorage.setItem("isso-author", JSON.stringify(author));
        localStorage.setItem("isso-email", JSON.stringify(email));
        localStorage.setItem("isso-website", JSON.stringify(website));

        api.create($("#isso-thread").getAttribute("data-isso-id"), {
            author: author, email: email, website: website,
            text: $(".isso-textarea", el).value,
            parent: parent || null,
            title: $("#isso-thread").getAttribute("data-title") || null,
            notification: $("[name=notification]", el).checked() ? 1 : 0,
        }).then(function(comment) {
            $(".isso-textarea", el).value = "";
            insert(comment, true);

            if (parent !== null) {
                el.onsuccess();
            }
        });
    });

    return el;
};

var insert_loader = function(comment, lastcreated) {
    var entrypoint;
    if (comment.id === null) {
        entrypoint = $("#isso-root");
        comment.name = 'null';
    } else {
        entrypoint = $("#isso-" + comment.id + " > .isso-follow-up");
        comment.name = comment.id;
    }
    var el = $.htmlify(template.render("comment-loader", {"comment": comment}));

    entrypoint.append(el);

    $("a.isso-load-hidden", el).on("click", function() {
        el.remove();
        api.fetch($("#isso-thread").getAttribute("data-isso-id"),
            config["reveal-on-click"], config["max-comments-nested"],
            comment.id,
            lastcreated).then(
            function(rv) {
                if (rv.total_replies === 0) {
                    return;
                }

                var lastcreated = 0;
                rv.replies.forEach(function(commentObject) {
                    insert(commentObject, false);
                    if(commentObject.created > lastcreated) {
                        lastcreated = commentObject.created;
                    }
                });

                if(rv.hidden_replies > 0) {
                    insert_loader(rv, lastcreated);
                }
            },
            function(err) {
                console.log(err);
            });
    });
};

var insert = function(comment, scrollIntoView) {
    var el = $.htmlify(template.render("comment", {"comment": comment}));

    // update datetime every 60 seconds
    var refresh = function() {
        $(".isso-permalink > time", el).textContent = i18n.ago(
            globals.offset.localTime(), new Date(parseInt(comment.created, 10) * 1000));
        setTimeout(refresh, 60*1000);
    };

    // run once to activate
    refresh();

    if (config["avatar"]) {
        $(".isso-avatar > svg", el).replace(identicons.generate(comment.hash, 4, 48, config));
    }

    var entrypoint;
    if (comment.parent === null) {
        entrypoint = $("#isso-root");
    } else {
        entrypoint = $("#isso-" + comment.parent + " > .isso-follow-up");
    }

    entrypoint.append(el);

    if (scrollIntoView) {
        el.scrollIntoView();
    }

    var footer = $("#isso-" + comment.id + " > .isso-text-wrapper > .isso-comment-footer"),
        header = $("#isso-" + comment.id + " > .isso-text-wrapper > .isso-comment-header"),
        text   = $("#isso-" + comment.id + " > .isso-text-wrapper > .isso-text");

    var form = null;  // XXX: probably a good place for a closure
    $("a.isso-reply", footer).toggle("click",
        function(toggler) {
            form = footer.insertAfter(new Postbox(comment.parent === null ? comment.id : comment.parent));
            form.onsuccess = function() { toggler.next(); };
            $(".isso-textarea", form).focus();
            $("a.isso-reply", footer).textContent = i18n.translate("comment-close");
        },
        function() {
            form.remove();
            $("a.isso-reply", footer).textContent = i18n.translate("comment-reply");
        }
    );

    if (config.vote) {
        var voteLevels = config['vote-levels'];
        if (typeof voteLevels === 'string') {
            // Eg. -5,5,15
            voteLevels = voteLevels.split(',');
        }

        // update vote counter
        var votes = function (value) {
            var span = $("span.isso-votes", footer);
            if (span === null) {
                footer.prepend($.new("span.isso-votes", value));
            } else {
                span.textContent = value;
            }
            if (value) {
                el.classList.remove('isso-no-votes');
            } else {
                el.classList.add('isso-no-votes');
            }
            if (voteLevels) {
                var before = true;
                for (var index = 0; index <= voteLevels.length; index++) {
                    if (before && (index >= voteLevels.length || value < voteLevels[index])) {
                        el.classList.add('isso-vote-level-' + index);
                        before = false;
                    } else {
                        el.classList.remove('isso-vote-level-' + index);
                    }
                }
            }
        };

        $("a.isso-upvote", footer).on("click", function () {
            api.like(comment.id).then(function (rv) {
                votes(rv.likes - rv.dislikes);
            });
        });

        $("a.isso-downvote", footer).on("click", function () {
            api.dislike(comment.id).then(function (rv) {
                votes(rv.likes - rv.dislikes);
            });
        });

        votes(comment.likes - comment.dislikes);
    }

    $("a.isso-edit", footer).toggle("click",
        function(toggler) {
            var edit = $("a.isso-edit", footer);
            var avatar = config["avatar"] || config["gravatar"] ? $(".isso-avatar", el, false)[0] : null;

            edit.textContent = i18n.translate("comment-save");
            edit.insertAfter($.new("a.isso-cancel", i18n.translate("comment-cancel"))).on("click", function() {
                toggler.canceled = true;
                toggler.next();
            });

            toggler.canceled = false;
            api.view(comment.id, 1).then(function(rv) {
                var textarea = $.new("textarea.isso-textarea");
                textarea.setAttribute("rows", 5);
                textarea.setAttribute("minlength", 3);
                textarea.setAttribute("maxlength", 65535);

                textarea.value = rv.text;
                textarea.focus();

                text.classList.remove("isso-text");
                text.classList.add("isso-textarea-wrapper");

                text.textContent = "";
                text.append(textarea);
            });

            if (avatar !== null) {
                avatar.hide();
            }
        },
        function(toggler) {
            var textarea = $(".isso-textarea", text);
            var avatar = config["avatar"] || config["gravatar"] ? $(".isso-avatar", el, false)[0] : null;

            if (! toggler.canceled && textarea !== null) {
                if (textarea.value.length < 3) {
                    textarea.focus();
                    toggler.wait();
                    return;
                } else {
                    api.modify(comment.id, {"text": textarea.value}).then(function(rv) {
                        text.innerHTML = rv.text;
                        comment.text = rv.text;
                    });
                }
            } else {
                text.innerHTML = comment.text;
            }

            text.classList.remove("isso-textarea-wrapper");
            text.classList.add("isso-text");

            if (avatar !== null) {
                avatar.show();
            }

            $("a.isso-cancel", footer).remove();
            $("a.isso-edit", footer).textContent = i18n.translate("comment-edit");
        }
    );

    $("a.isso-delete", footer).toggle("click",
        function(toggler) {
            var del = $("a.isso-delete", footer);
            var state = ! toggler.state;

            del.textContent = i18n.translate("comment-confirm");
            del.on("mouseout", function() {
                del.textContent = i18n.translate("comment-delete");
                toggler.state = state;
                del.onmouseout = null;
            });
        },
        function() {
            var del = $("a.isso-delete", footer);
            api.remove(comment.id).then(function(rv) {
                if (rv) {
                    el.remove();
                } else {
                    $("span.isso-note", header).textContent = i18n.translate("comment-deleted");
                    text.innerHTML = "<p> </p>";
                    $("a.isso-edit", footer).remove();
                    $("a.isso-delete", footer).remove();
                }
                del.textContent = i18n.translate("comment-delete");
            });
        }
    );

    // remove edit and delete buttons when cookie is gone
    var clear = function(button) {
        if (! utils.cookie("isso-" + comment.id)) {
            if ($(button, footer) !== null) {
                $(button, footer).remove();
            }
        } else {
            setTimeout(function() { clear(button); }, 15*1000);
        }
    };

    clear("a.isso-edit");
    clear("a.isso-delete");

    // show direct reply to own comment when cookie is max aged
    var show = function(el) {
        if (utils.cookie("isso-" + comment.id)) {
            setTimeout(function() { show(el); }, 15*1000);
        } else {
            footer.append(el);
        }
    };

    if (! config["reply-to-self"] && utils.cookie("isso-" + comment.id)) {
        show($("a.isso-reply", footer).detach());
    }

    if(comment.hasOwnProperty('replies')) {
        var lastcreated = 0;
        comment.replies.forEach(function(replyObject) {
            insert(replyObject, false);
            if(replyObject.created > lastcreated) {
                lastcreated = replyObject.created;
            }

        });
        if(comment.hidden_replies > 0) {
            insert_loader(comment, lastcreated);
        }

    }

};

module.exports = {
    insert: insert,
    insert_loader: insert_loader,
    Postbox: Postbox,
};
<commit_msg>js: isso.js: Focus textarea on leaving preview mode<commit_after>/* Isso – Ich schrei sonst!
*/
var $ = require("app/dom");
var utils = require("app/utils");
var config = require("app/config");
var api = require("app/api");
var template = require("app/template");
var i18n = require("app/i18n");
var identicons = require("app/lib/identicons");
var globals = require("app/globals");

"use strict";

var Postbox = function(parent) {

    var localStorage = utils.localStorageImpl,
        el = $.htmlify(template.render("postbox", {
        "author":  JSON.parse(localStorage.getItem("isso-author")),
        "email":   JSON.parse(localStorage.getItem("isso-email")),
        "website": JSON.parse(localStorage.getItem("isso-website")),
        "preview": ''
    }));

    // callback on success (e.g. to toggle the reply button)
    el.onsuccess = function() {};

    el.validate = function() {
        if ($(".isso-textarea", this).value.length < 3) {
            $(".isso-textarea", this).focus();
            return false;
        }
        if (config["require-email"] &&
            $("[name='email']", this).value.length <= 0)
        {
          $("[name='email']", this).focus();
          return false;
        }
        if (config["require-author"] &&
            $("[name='author']", this).value.length <= 0)
        {
          $("[name='author']", this).focus();
          return false;
        }
        return true;
    };

    // only display notification checkbox if email is filled in
    var email_edit = function() {
        if (config["reply-notifications"] && $("[name='email']", el).value.length > 0) {
            $(".isso-notification-section", el).show();
        } else {
            $(".isso-notification-section", el).hide();
        }
    };
    $("[name='email']", el).on("input", email_edit);
    email_edit();

    // email is not optional if this config parameter is set
    if (config["require-email"]) {
        $("[for='isso-postbox-email']", el).textContent =
            $("[for='isso-postbox-email']", el).textContent.replace(/ \(.*\)/, "");
    }

    // author is not optional if this config parameter is set
    if (config["require-author"]) {
      $("[for='isso-postbox-author']", el).textContent =
        $("[for='isso-postbox-author']", el).textContent.replace(/ \(.*\)/, "");
    }

    // preview function
    $("[name='preview']", el).on("click", function() {
        api.preview($(".isso-textarea", el).value).then(
            function(html) {
                $(".isso-preview .isso-text", el).innerHTML = html;
                el.classList.add('isso-preview-mode');
            });
    });

    // edit function
    var edit = function() {
        $(".isso-preview .isso-text", el).innerHTML = '';
        el.classList.remove('isso-preview-mode');
    };
    $("[name='edit']", el).on("click", function() {
      edit();
      $(".isso-textarea", el).focus();
    });
    $(".isso-preview", el).on("click", function() {
      edit();
      $(".isso-textarea", el).focus();
    });

    // submit form, initialize optional fields with `null` and reset form.
    // If replied to a comment, remove form completely.
    $("[type=submit]", el).on("click", function() {
        edit();
        if (! el.validate()) {
            return;
        }

        var author = $("[name=author]", el).value || null,
            email = $("[name=email]", el).value || null,
            website = $("[name=website]", el).value || null;

        localStorage.setItem("isso-author", JSON.stringify(author));
        localStorage.setItem("isso-email", JSON.stringify(email));
        localStorage.setItem("isso-website", JSON.stringify(website));

        api.create($("#isso-thread").getAttribute("data-isso-id"), {
            author: author, email: email, website: website,
            text: $(".isso-textarea", el).value,
            parent: parent || null,
            title: $("#isso-thread").getAttribute("data-title") || null,
            notification: $("[name=notification]", el).checked() ? 1 : 0,
        }).then(function(comment) {
            $(".isso-textarea", el).value = "";
            insert(comment, true);

            if (parent !== null) {
                el.onsuccess();
            }
        });
    });

    return el;
};

var insert_loader = function(comment, lastcreated) {
    var entrypoint;
    if (comment.id === null) {
        entrypoint = $("#isso-root");
        comment.name = 'null';
    } else {
        entrypoint = $("#isso-" + comment.id + " > .isso-follow-up");
        comment.name = comment.id;
    }
    var el = $.htmlify(template.render("comment-loader", {"comment": comment}));

    entrypoint.append(el);

    $("a.isso-load-hidden", el).on("click", function() {
        el.remove();
        api.fetch($("#isso-thread").getAttribute("data-isso-id"),
            config["reveal-on-click"], config["max-comments-nested"],
            comment.id,
            lastcreated).then(
            function(rv) {
                if (rv.total_replies === 0) {
                    return;
                }

                var lastcreated = 0;
                rv.replies.forEach(function(commentObject) {
                    insert(commentObject, false);
                    if(commentObject.created > lastcreated) {
                        lastcreated = commentObject.created;
                    }
                });

                if(rv.hidden_replies > 0) {
                    insert_loader(rv, lastcreated);
                }
            },
            function(err) {
                console.log(err);
            });
    });
};

var insert = function(comment, scrollIntoView) {
    var el = $.htmlify(template.render("comment", {"comment": comment}));

    // update datetime every 60 seconds
    var refresh = function() {
        $(".isso-permalink > time", el).textContent = i18n.ago(
            globals.offset.localTime(), new Date(parseInt(comment.created, 10) * 1000));
        setTimeout(refresh, 60*1000);
    };

    // run once to activate
    refresh();

    if (config["avatar"]) {
        $(".isso-avatar > svg", el).replace(identicons.generate(comment.hash, 4, 48, config));
    }

    var entrypoint;
    if (comment.parent === null) {
        entrypoint = $("#isso-root");
    } else {
        entrypoint = $("#isso-" + comment.parent + " > .isso-follow-up");
    }

    entrypoint.append(el);

    if (scrollIntoView) {
        el.scrollIntoView();
    }

    var footer = $("#isso-" + comment.id + " > .isso-text-wrapper > .isso-comment-footer"),
        header = $("#isso-" + comment.id + " > .isso-text-wrapper > .isso-comment-header"),
        text   = $("#isso-" + comment.id + " > .isso-text-wrapper > .isso-text");

    var form = null;  // XXX: probably a good place for a closure
    $("a.isso-reply", footer).toggle("click",
        function(toggler) {
            form = footer.insertAfter(new Postbox(comment.parent === null ? comment.id : comment.parent));
            form.onsuccess = function() { toggler.next(); };
            $(".isso-textarea", form).focus();
            $("a.isso-reply", footer).textContent = i18n.translate("comment-close");
        },
        function() {
            form.remove();
            $("a.isso-reply", footer).textContent = i18n.translate("comment-reply");
        }
    );

    if (config.vote) {
        var voteLevels = config['vote-levels'];
        if (typeof voteLevels === 'string') {
            // Eg. -5,5,15
            voteLevels = voteLevels.split(',');
        }

        // update vote counter
        var votes = function (value) {
            var span = $("span.isso-votes", footer);
            if (span === null) {
                footer.prepend($.new("span.isso-votes", value));
            } else {
                span.textContent = value;
            }
            if (value) {
                el.classList.remove('isso-no-votes');
            } else {
                el.classList.add('isso-no-votes');
            }
            if (voteLevels) {
                var before = true;
                for (var index = 0; index <= voteLevels.length; index++) {
                    if (before && (index >= voteLevels.length || value < voteLevels[index])) {
                        el.classList.add('isso-vote-level-' + index);
                        before = false;
                    } else {
                        el.classList.remove('isso-vote-level-' + index);
                    }
                }
            }
        };

        $("a.isso-upvote", footer).on("click", function () {
            api.like(comment.id).then(function (rv) {
                votes(rv.likes - rv.dislikes);
            });
        });

        $("a.isso-downvote", footer).on("click", function () {
            api.dislike(comment.id).then(function (rv) {
                votes(rv.likes - rv.dislikes);
            });
        });

        votes(comment.likes - comment.dislikes);
    }

    $("a.isso-edit", footer).toggle("click",
        function(toggler) {
            var edit = $("a.isso-edit", footer);
            var avatar = config["avatar"] || config["gravatar"] ? $(".isso-avatar", el, false)[0] : null;

            edit.textContent = i18n.translate("comment-save");
            edit.insertAfter($.new("a.isso-cancel", i18n.translate("comment-cancel"))).on("click", function() {
                toggler.canceled = true;
                toggler.next();
            });

            toggler.canceled = false;
            api.view(comment.id, 1).then(function(rv) {
                var textarea = $.new("textarea.isso-textarea");
                textarea.setAttribute("rows", 5);
                textarea.setAttribute("minlength", 3);
                textarea.setAttribute("maxlength", 65535);

                textarea.value = rv.text;
                textarea.focus();

                text.classList.remove("isso-text");
                text.classList.add("isso-textarea-wrapper");

                text.textContent = "";
                text.append(textarea);
            });

            if (avatar !== null) {
                avatar.hide();
            }
        },
        function(toggler) {
            var textarea = $(".isso-textarea", text);
            var avatar = config["avatar"] || config["gravatar"] ? $(".isso-avatar", el, false)[0] : null;

            if (! toggler.canceled && textarea !== null) {
                if (textarea.value.length < 3) {
                    textarea.focus();
                    toggler.wait();
                    return;
                } else {
                    api.modify(comment.id, {"text": textarea.value}).then(function(rv) {
                        text.innerHTML = rv.text;
                        comment.text = rv.text;
                    });
                }
            } else {
                text.innerHTML = comment.text;
            }

            text.classList.remove("isso-textarea-wrapper");
            text.classList.add("isso-text");

            if (avatar !== null) {
                avatar.show();
            }

            $("a.isso-cancel", footer).remove();
            $("a.isso-edit", footer).textContent = i18n.translate("comment-edit");
        }
    );

    $("a.isso-delete", footer).toggle("click",
        function(toggler) {
            var del = $("a.isso-delete", footer);
            var state = ! toggler.state;

            del.textContent = i18n.translate("comment-confirm");
            del.on("mouseout", function() {
                del.textContent = i18n.translate("comment-delete");
                toggler.state = state;
                del.onmouseout = null;
            });
        },
        function() {
            var del = $("a.isso-delete", footer);
            api.remove(comment.id).then(function(rv) {
                if (rv) {
                    el.remove();
                } else {
                    $("span.isso-note", header).textContent = i18n.translate("comment-deleted");
                    text.innerHTML = "<p> </p>";
                    $("a.isso-edit", footer).remove();
                    $("a.isso-delete", footer).remove();
                }
                del.textContent = i18n.translate("comment-delete");
            });
        }
    );

    // remove edit and delete buttons when cookie is gone
    var clear = function(button) {
        if (! utils.cookie("isso-" + comment.id)) {
            if ($(button, footer) !== null) {
                $(button, footer).remove();
            }
        } else {
            setTimeout(function() { clear(button); }, 15*1000);
        }
    };

    clear("a.isso-edit");
    clear("a.isso-delete");

    // show direct reply to own comment when cookie is max aged
    var show = function(el) {
        if (utils.cookie("isso-" + comment.id)) {
            setTimeout(function() { show(el); }, 15*1000);
        } else {
            footer.append(el);
        }
    };

    if (! config["reply-to-self"] && utils.cookie("isso-" + comment.id)) {
        show($("a.isso-reply", footer).detach());
    }

    if(comment.hasOwnProperty('replies')) {
        var lastcreated = 0;
        comment.replies.forEach(function(replyObject) {
            insert(replyObject, false);
            if(replyObject.created > lastcreated) {
                lastcreated = replyObject.created;
            }

        });
        if(comment.hidden_replies > 0) {
            insert_loader(comment, lastcreated);
        }

    }

};

module.exports = {
    insert: insert,
    insert_loader: insert_loader,
    Postbox: Postbox,
};
<|endoftext|>


<commit_before>module.exports = {
  Tab: require('tab'),
  Tabs: require('tabs')
};
<commit_msg>fixed typo<commit_after>module.exports = {
  Tab: require('./tab'),
  Tabs: require('./tabs')
};
<|endoftext|>


<commit_before>'use strict';

/**
 * @ngdoc directive
 * @name tagsInput
 * @module ngTagsInput
 *
 * @description
 * Renders an input box with tag editing support.
 *
 * @param {string} ngModel Assignable Angular expression to data-bind to.
 * @param {string=} [template=NA] URL or id of a custom template for rendering each tag.
 * @param {string=} [displayProperty=text] Property to be rendered as the tag label.
 * @param {string=} [keyProperty=text] Property to be used as a unique identifier for the tag.
 * @param {string=} [displayProperty=text] Property to be rendered as the tag label.
 * @param {string=} [type=text] Type of the input element. Only 'text', 'email' and 'url' are supported values.
 * @param {string} [text=NA] Assignable Angular expression for data-binding to the element's text.
 * @param {number=} tabindex Tab order of the control.
 * @param {string=} [placeholder=Add a tag] Placeholder text for the control.
 * @param {number=} [minLength=3] Minimum length for a new tag.
 * @param {number=} [maxLength=MAX_SAFE_INTEGER] Maximum length allowed for a new tag.
 * @param {number=} [minTags=0] Sets minTags validation error key if the number of tags added is less than minTags.
 * @param {number=} [maxTags=MAX_SAFE_INTEGER] Sets maxTags validation error key if the number of tags added is greater
 *    than maxTags.
 * @param {boolean=} [allowLeftoverText=false] Sets leftoverText validation error key if there is any leftover text in
 *    the input element when the directive loses focus.
 * @param {string=} [removeTagSymbol=×] (Obsolete) Symbol character for the remove tag button.
 * @param {boolean=} [addOnEnter=true] Flag indicating that a new tag will be added on pressing the ENTER key.
 * @param {boolean=} [addOnSpace=false] Flag indicating that a new tag will be added on pressing the SPACE key.
 * @param {boolean=} [addOnComma=true] Flag indicating that a new tag will be added on pressing the COMMA key.
 * @param {boolean=} [addOnBlur=true] Flag indicating that a new tag will be added when the input field loses focus.
 * @param {boolean=} [addOnPaste=false] Flag indicating that the text pasted into the input field will be split into tags.
 * @param {string=} [pasteSplitPattern=,] Regular expression used to split the pasted text into tags.
 * @param {boolean=} [replaceSpacesWithDashes=true] Flag indicating that spaces will be replaced with dashes.
 * @param {string=} [allowedTagsPattern=.+] Regular expression that determines whether a new tag is valid.
 * @param {boolean=} [enableEditingLastTag=false] Flag indicating that the last tag will be moved back into the new tag
 *    input box instead of being removed when the backspace key is pressed and the input box is empty.
 * @param {boolean=} [addFromAutocompleteOnly=false] Flag indicating that only tags coming from the autocomplete list
 *    will be allowed. When this flag is true, addOnEnter, addOnComma, addOnSpace and addOnBlur values are ignored.
 * @param {boolean=} [spellcheck=true] Flag indicating whether the browser's spellcheck is enabled for the input field or not.
 * @param {expression=} [onTagAdding=NA] Expression to evaluate that will be invoked before adding a new tag. The new
 *    tag is available as $tag. This method must return either true or false. If false, the tag will not be added.
 * @param {expression=} [onTagAdded=NA] Expression to evaluate upon adding a new tag. The new tag is available as $tag.
 * @param {expression=} [onInvalidTag=NA] Expression to evaluate when a tag is invalid. The invalid tag is available as $tag.
 * @param {expression=} [onTagRemoving=NA] Expression to evaluate that will be invoked before removing a tag. The tag
 *    is available as $tag. This method must return either true or false. If false, the tag will not be removed.
 * @param {expression=} [onTagRemoved=NA] Expression to evaluate upon removing an existing tag. The removed tag is
 *    available as $tag.
 * @param {expression=} [onTagClicked=NA] Expression to evaluate upon clicking an existing tag. The clicked tag is available as $tag.
 */
tagsInput.directive('tagsInput', function($timeout, $document, $window, tagsInputConfig, tiUtil) {
    function TagList(options, events, onTagAdding, onTagRemoving) {
        var self = {}, getTagText, setTagText, tagIsValid;

        getTagText = function(tag) {
            return tiUtil.safeToString(tag[options.displayProperty]);
        };

        setTagText = function(tag, text) {
            tag[options.displayProperty] = text;
        };

        tagIsValid = function(tag) {
            var tagText = getTagText(tag);

            return tagText &&
                   tagText.length >= options.minLength &&
                   tagText.length <= options.maxLength &&
                   options.allowedTagsPattern.test(tagText) &&
                   !tiUtil.findInObjectArray(self.items, tag, options.keyProperty || options.displayProperty) &&
                   onTagAdding({ $tag: tag });
        };

        self.items = [];

        self.addText = function(text) {
            var tag = {};
            setTagText(tag, text);
            return self.add(tag);
        };

        self.add = function(tag) {
            var tagText = getTagText(tag);

            if (options.replaceSpacesWithDashes) {
                tagText = tiUtil.replaceSpacesWithDashes(tagText);
            }

            setTagText(tag, tagText);

            if (tagIsValid(tag)) {
                self.items.push(tag);
                events.trigger('tag-added', { $tag: tag });
            }
            else if (tagText) {
                events.trigger('invalid-tag', { $tag: tag });
            }

            return tag;
        };

        self.remove = function(index) {
            var tag = self.items[index];

            if (onTagRemoving({ $tag: tag }))  {
                self.items.splice(index, 1);
                self.clearSelection();
                events.trigger('tag-removed', { $tag: tag });
                return tag;
            }
        };

        self.select = function(index) {
            if (index < 0) {
                index = self.items.length - 1;
            }
            else if (index >= self.items.length) {
                index = 0;
            }

            self.index = index;
            self.selected = self.items[index];
        };

        self.selectPrior = function() {
            self.select(--self.index);
        };

        self.selectNext = function() {
            self.select(++self.index);
        };

        self.removeSelected = function() {
            return self.remove(self.index);
        };

        self.clearSelection = function() {
            self.selected = null;
            self.index = -1;
        };

        self.clearSelection();

        return self;
    }

    function validateType(type) {
        return SUPPORTED_INPUT_TYPES.indexOf(type) !== -1;
    }

    return {
        restrict: 'E',
        require: 'ngModel',
        scope: {
            tags: '=ngModel',
            text: '=?',
            onTagAdding: '&',
            onTagAdded: '&',
            onInvalidTag: '&',
            onTagRemoving: '&',
            onTagRemoved: '&',
            onTagClicked: '&'
        },
        replace: false,
        transclude: true,
        templateUrl: 'ngTagsInput/tags-input.html',
        controller: function($scope, $attrs, $element) {
            $scope.events = tiUtil.simplePubSub();

            tagsInputConfig.load('tagsInput', $scope, $attrs, {
                template: [String, 'ngTagsInput/tag-item.html'],
                type: [String, 'text', validateType],
                placeholder: [String, 'Add a tag'],
                tabindex: [Number, null],
                removeTagSymbol: [String, String.fromCharCode(215)],
                replaceSpacesWithDashes: [Boolean, true],
                minLength: [Number, 3],
                maxLength: [Number, MAX_SAFE_INTEGER],
                addOnEnter: [Boolean, true],
                addOnSpace: [Boolean, false],
                addOnComma: [Boolean, true],
                addOnBlur: [Boolean, true],
                addOnPaste: [Boolean, false],
                pasteSplitPattern: [RegExp, /,/],
                allowedTagsPattern: [RegExp, /.+/],
                enableEditingLastTag: [Boolean, false],
                minTags: [Number, 0],
                maxTags: [Number, MAX_SAFE_INTEGER],
                displayProperty: [String, 'text'],
                keyProperty: [String, ''],
                allowLeftoverText: [Boolean, false],
                addFromAutocompleteOnly: [Boolean, false],
                spellcheck: [Boolean, true]
            });

            $scope.tagList = new TagList($scope.options, $scope.events,
                tiUtil.handleUndefinedResult($scope.onTagAdding, true),
                tiUtil.handleUndefinedResult($scope.onTagRemoving, true));

            this.registerAutocomplete = function() {
                var input = $element.find('input');

                return {
                    addTag: function(tag) {
                        return $scope.tagList.add(tag);
                    },
                    focusInput: function() {
                        input[0].focus();
                    },
                    getTags: function() {
                        return $scope.tagList.items;
                    },
                    getCurrentTagText: function() {
                        return $scope.newTag.text();
                    },
                    getOptions: function() {
                        return $scope.options;
                    },
                    on: function(name, handler) {
                        $scope.events.on(name, handler);
                        return this;
                    }
                };
            };

            this.registerTagItem = function() {
                return {
                    getOptions: function() {
                        return $scope.options;
                    },
                    removeTag: function(index) {
                        if ($scope.disabled) {
                            return;
                        }
                        $scope.tagList.remove(index);
                    }
                };
            };
        },
        link: function(scope, element, attrs, ngModelCtrl) {
            var hotkeys = [KEYS.enter, KEYS.comma, KEYS.space, KEYS.backspace, KEYS.delete, KEYS.left, KEYS.right],
                tagList = scope.tagList,
                events = scope.events,
                options = scope.options,
                input = element.find('input'),
                validationOptions = ['minTags', 'maxTags', 'allowLeftoverText'],
                setElementValidity;

            setElementValidity = function() {
                ngModelCtrl.$setValidity('maxTags', tagList.items.length <= options.maxTags);
                ngModelCtrl.$setValidity('minTags', tagList.items.length >= options.minTags);
                ngModelCtrl.$setValidity('leftoverText', scope.hasFocus || options.allowLeftoverText ? true : !scope.newTag.text());
            };

            ngModelCtrl.$isEmpty = function(value) {
                return !value || !value.length;
            };

            scope.newTag = {
                text: function(value) {
                    if (angular.isDefined(value)) {
                        scope.text = value;
                        events.trigger('input-change', value);
                    }
                    else {
                        return scope.text || '';
                    }
                },
                invalid: null
            };

            scope.track = function(tag) {
                return tag[options.keyProperty || options.displayProperty];
            };

            scope.$watch('tags', function(value) {
                if (value) {
                    tagList.items = tiUtil.makeObjectArray(value, options.displayProperty);
                    scope.tags = tagList.items;
                }
                else {
                    tagList.items = [];
                }
            });

            scope.$watch('tags.length', function() {
                setElementValidity();

                // ngModelController won't trigger validators when the model changes (because it's an array),
                // so we need to do it ourselves. Unfortunately this won't trigger any registered formatter.
                ngModelCtrl.$validate();
            });

            attrs.$observe('disabled', function(value) {
                scope.disabled = value;
            });

            scope.eventHandlers = {
                input: {
                    keydown: function($event) {
                        events.trigger('input-keydown', $event);
                    },
                    focus: function() {
                        if (scope.hasFocus) {
                            return;
                        }

                        scope.hasFocus = true;
                        events.trigger('input-focus');
                    },
                    blur: function() {
                        $timeout(function() {
                            var activeElement = $document.prop('activeElement'),
                                lostFocusToBrowserWindow = activeElement === input[0],
                                lostFocusToChildElement = element[0].contains(activeElement);

                            if (lostFocusToBrowserWindow || !lostFocusToChildElement) {
                                scope.hasFocus = false;
                                events.trigger('input-blur');
                            }
                        });
                    },
                    paste: function($event) {
                        $event.getTextData = function() {
                            var clipboardData = $event.clipboardData || ($event.originalEvent && $event.originalEvent.clipboardData);
                            return clipboardData ? clipboardData.getData('text/plain') : $window.clipboardData.getData('Text');
                        };
                        events.trigger('input-paste', $event);
                    }
                },
                host: {
                    click: function() {
                        if (scope.disabled) {
                            return;
                        }
                        input[0].focus();
                    }
                },
                tag: {
                    click: function(tag) {
                        events.trigger('tag-clicked', { $tag: tag });
                    }
                }
            };

            events
                .on('tag-added', scope.onTagAdded)
                .on('invalid-tag', scope.onInvalidTag)
                .on('tag-removed', scope.onTagRemoved)
                .on('tag-clicked', scope.onTagClicked)
                .on('tag-added', function() {
                    scope.newTag.text('');
                })
                .on('tag-added tag-removed', function() {
                    scope.tags = tagList.items;
                    // Ideally we should be able call $setViewValue here and let it in turn call $setDirty and $validate
                    // automatically, but since the model is an array, $setViewValue does nothing and it's up to us to do it.
                    // Unfortunately this won't trigger any registered $parser and there's no safe way to do it.
                    ngModelCtrl.$setDirty();
                })
                .on('invalid-tag', function() {
                    scope.newTag.invalid = true;
                })
                .on('option-change', function(e) {
                    if (validationOptions.indexOf(e.name) !== -1) {
                        setElementValidity();
                    }
                })
                .on('input-change', function() {
                    tagList.clearSelection();
                    scope.newTag.invalid = null;
                })
                .on('input-focus', function() {
                    element.triggerHandler('focus');
                    ngModelCtrl.$setValidity('leftoverText', true);
                })
                .on('input-blur', function() {
                    if (options.addOnBlur && !options.addFromAutocompleteOnly) {
                        tagList.addText(scope.newTag.text());
                    }
                    element.triggerHandler('blur');
                    setElementValidity();
                })
                .on('input-keydown', function(event) {
                    var key = event.keyCode,
                        addKeys = {},
                        shouldAdd, shouldRemove, shouldSelect, shouldEditLastTag;

                    if (tiUtil.isModifierOn(event) || hotkeys.indexOf(key) === -1) {
                        return;
                    }

                    addKeys[KEYS.enter] = options.addOnEnter;
                    addKeys[KEYS.comma] = options.addOnComma;
                    addKeys[KEYS.space] = options.addOnSpace;

                    shouldAdd = !options.addFromAutocompleteOnly && addKeys[key];
                    shouldRemove = (key === KEYS.backspace || key === KEYS.delete) && tagList.selected;
                    shouldEditLastTag = key === KEYS.backspace && scope.newTag.text().length === 0 && options.enableEditingLastTag;
                    shouldSelect = (key === KEYS.backspace || key === KEYS.left || key === KEYS.right) && scope.newTag.text().length === 0 && !options.enableEditingLastTag;

                    if (shouldAdd) {
                        tagList.addText(scope.newTag.text());
                    }
                    else if (shouldEditLastTag) {
                        var tag;

                        tagList.selectPrior();
                        tag = tagList.removeSelected();

                        if (tag) {
                            scope.newTag.text(tag[options.displayProperty]);
                        }
                    }
                    else if (shouldRemove) {
                        tagList.removeSelected();
                    }
                    else if (shouldSelect) {
                        if (key === KEYS.left || key === KEYS.backspace) {
                            tagList.selectPrior();
                        }
                        else if (key === KEYS.right) {
                            tagList.selectNext();
                        }
                    }

                    if (shouldAdd || shouldSelect || shouldRemove || shouldEditLastTag) {
                        event.preventDefault();
                    }
                })
                .on('input-paste', function(event) {
                    if (options.addOnPaste) {
                        var data = event.getTextData();
                        var tags = data.split(options.pasteSplitPattern);

                        if (tags.length > 1) {
                            tags.forEach(function(tag) {
                                tagList.addText(tag);
                            });
                            event.preventDefault();
                        }
                    }
                });
        }
    };
});
<commit_msg>docs(tagsInput): Remove duplicate line [skip ci]<commit_after>'use strict';

/**
 * @ngdoc directive
 * @name tagsInput
 * @module ngTagsInput
 *
 * @description
 * Renders an input box with tag editing support.
 *
 * @param {string} ngModel Assignable Angular expression to data-bind to.
 * @param {string=} [template=NA] URL or id of a custom template for rendering each tag.
 * @param {string=} [displayProperty=text] Property to be rendered as the tag label.
 * @param {string=} [keyProperty=text] Property to be used as a unique identifier for the tag.
 * @param {string=} [type=text] Type of the input element. Only 'text', 'email' and 'url' are supported values.
 * @param {string} [text=NA] Assignable Angular expression for data-binding to the element's text.
 * @param {number=} tabindex Tab order of the control.
 * @param {string=} [placeholder=Add a tag] Placeholder text for the control.
 * @param {number=} [minLength=3] Minimum length for a new tag.
 * @param {number=} [maxLength=MAX_SAFE_INTEGER] Maximum length allowed for a new tag.
 * @param {number=} [minTags=0] Sets minTags validation error key if the number of tags added is less than minTags.
 * @param {number=} [maxTags=MAX_SAFE_INTEGER] Sets maxTags validation error key if the number of tags added is greater
 *    than maxTags.
 * @param {boolean=} [allowLeftoverText=false] Sets leftoverText validation error key if there is any leftover text in
 *    the input element when the directive loses focus.
 * @param {string=} [removeTagSymbol=×] (Obsolete) Symbol character for the remove tag button.
 * @param {boolean=} [addOnEnter=true] Flag indicating that a new tag will be added on pressing the ENTER key.
 * @param {boolean=} [addOnSpace=false] Flag indicating that a new tag will be added on pressing the SPACE key.
 * @param {boolean=} [addOnComma=true] Flag indicating that a new tag will be added on pressing the COMMA key.
 * @param {boolean=} [addOnBlur=true] Flag indicating that a new tag will be added when the input field loses focus.
 * @param {boolean=} [addOnPaste=false] Flag indicating that the text pasted into the input field will be split into tags.
 * @param {string=} [pasteSplitPattern=,] Regular expression used to split the pasted text into tags.
 * @param {boolean=} [replaceSpacesWithDashes=true] Flag indicating that spaces will be replaced with dashes.
 * @param {string=} [allowedTagsPattern=.+] Regular expression that determines whether a new tag is valid.
 * @param {boolean=} [enableEditingLastTag=false] Flag indicating that the last tag will be moved back into the new tag
 *    input box instead of being removed when the backspace key is pressed and the input box is empty.
 * @param {boolean=} [addFromAutocompleteOnly=false] Flag indicating that only tags coming from the autocomplete list
 *    will be allowed. When this flag is true, addOnEnter, addOnComma, addOnSpace and addOnBlur values are ignored.
 * @param {boolean=} [spellcheck=true] Flag indicating whether the browser's spellcheck is enabled for the input field or not.
 * @param {expression=} [onTagAdding=NA] Expression to evaluate that will be invoked before adding a new tag. The new
 *    tag is available as $tag. This method must return either true or false. If false, the tag will not be added.
 * @param {expression=} [onTagAdded=NA] Expression to evaluate upon adding a new tag. The new tag is available as $tag.
 * @param {expression=} [onInvalidTag=NA] Expression to evaluate when a tag is invalid. The invalid tag is available as $tag.
 * @param {expression=} [onTagRemoving=NA] Expression to evaluate that will be invoked before removing a tag. The tag
 *    is available as $tag. This method must return either true or false. If false, the tag will not be removed.
 * @param {expression=} [onTagRemoved=NA] Expression to evaluate upon removing an existing tag. The removed tag is
 *    available as $tag.
 * @param {expression=} [onTagClicked=NA] Expression to evaluate upon clicking an existing tag. The clicked tag is available as $tag.
 */
tagsInput.directive('tagsInput', function($timeout, $document, $window, tagsInputConfig, tiUtil) {
    function TagList(options, events, onTagAdding, onTagRemoving) {
        var self = {}, getTagText, setTagText, tagIsValid;

        getTagText = function(tag) {
            return tiUtil.safeToString(tag[options.displayProperty]);
        };

        setTagText = function(tag, text) {
            tag[options.displayProperty] = text;
        };

        tagIsValid = function(tag) {
            var tagText = getTagText(tag);

            return tagText &&
                   tagText.length >= options.minLength &&
                   tagText.length <= options.maxLength &&
                   options.allowedTagsPattern.test(tagText) &&
                   !tiUtil.findInObjectArray(self.items, tag, options.keyProperty || options.displayProperty) &&
                   onTagAdding({ $tag: tag });
        };

        self.items = [];

        self.addText = function(text) {
            var tag = {};
            setTagText(tag, text);
            return self.add(tag);
        };

        self.add = function(tag) {
            var tagText = getTagText(tag);

            if (options.replaceSpacesWithDashes) {
                tagText = tiUtil.replaceSpacesWithDashes(tagText);
            }

            setTagText(tag, tagText);

            if (tagIsValid(tag)) {
                self.items.push(tag);
                events.trigger('tag-added', { $tag: tag });
            }
            else if (tagText) {
                events.trigger('invalid-tag', { $tag: tag });
            }

            return tag;
        };

        self.remove = function(index) {
            var tag = self.items[index];

            if (onTagRemoving({ $tag: tag }))  {
                self.items.splice(index, 1);
                self.clearSelection();
                events.trigger('tag-removed', { $tag: tag });
                return tag;
            }
        };

        self.select = function(index) {
            if (index < 0) {
                index = self.items.length - 1;
            }
            else if (index >= self.items.length) {
                index = 0;
            }

            self.index = index;
            self.selected = self.items[index];
        };

        self.selectPrior = function() {
            self.select(--self.index);
        };

        self.selectNext = function() {
            self.select(++self.index);
        };

        self.removeSelected = function() {
            return self.remove(self.index);
        };

        self.clearSelection = function() {
            self.selected = null;
            self.index = -1;
        };

        self.clearSelection();

        return self;
    }

    function validateType(type) {
        return SUPPORTED_INPUT_TYPES.indexOf(type) !== -1;
    }

    return {
        restrict: 'E',
        require: 'ngModel',
        scope: {
            tags: '=ngModel',
            text: '=?',
            onTagAdding: '&',
            onTagAdded: '&',
            onInvalidTag: '&',
            onTagRemoving: '&',
            onTagRemoved: '&',
            onTagClicked: '&'
        },
        replace: false,
        transclude: true,
        templateUrl: 'ngTagsInput/tags-input.html',
        controller: function($scope, $attrs, $element) {
            $scope.events = tiUtil.simplePubSub();

            tagsInputConfig.load('tagsInput', $scope, $attrs, {
                template: [String, 'ngTagsInput/tag-item.html'],
                type: [String, 'text', validateType],
                placeholder: [String, 'Add a tag'],
                tabindex: [Number, null],
                removeTagSymbol: [String, String.fromCharCode(215)],
                replaceSpacesWithDashes: [Boolean, true],
                minLength: [Number, 3],
                maxLength: [Number, MAX_SAFE_INTEGER],
                addOnEnter: [Boolean, true],
                addOnSpace: [Boolean, false],
                addOnComma: [Boolean, true],
                addOnBlur: [Boolean, true],
                addOnPaste: [Boolean, false],
                pasteSplitPattern: [RegExp, /,/],
                allowedTagsPattern: [RegExp, /.+/],
                enableEditingLastTag: [Boolean, false],
                minTags: [Number, 0],
                maxTags: [Number, MAX_SAFE_INTEGER],
                displayProperty: [String, 'text'],
                keyProperty: [String, ''],
                allowLeftoverText: [Boolean, false],
                addFromAutocompleteOnly: [Boolean, false],
                spellcheck: [Boolean, true]
            });

            $scope.tagList = new TagList($scope.options, $scope.events,
                tiUtil.handleUndefinedResult($scope.onTagAdding, true),
                tiUtil.handleUndefinedResult($scope.onTagRemoving, true));

            this.registerAutocomplete = function() {
                var input = $element.find('input');

                return {
                    addTag: function(tag) {
                        return $scope.tagList.add(tag);
                    },
                    focusInput: function() {
                        input[0].focus();
                    },
                    getTags: function() {
                        return $scope.tagList.items;
                    },
                    getCurrentTagText: function() {
                        return $scope.newTag.text();
                    },
                    getOptions: function() {
                        return $scope.options;
                    },
                    on: function(name, handler) {
                        $scope.events.on(name, handler);
                        return this;
                    }
                };
            };

            this.registerTagItem = function() {
                return {
                    getOptions: function() {
                        return $scope.options;
                    },
                    removeTag: function(index) {
                        if ($scope.disabled) {
                            return;
                        }
                        $scope.tagList.remove(index);
                    }
                };
            };
        },
        link: function(scope, element, attrs, ngModelCtrl) {
            var hotkeys = [KEYS.enter, KEYS.comma, KEYS.space, KEYS.backspace, KEYS.delete, KEYS.left, KEYS.right],
                tagList = scope.tagList,
                events = scope.events,
                options = scope.options,
                input = element.find('input'),
                validationOptions = ['minTags', 'maxTags', 'allowLeftoverText'],
                setElementValidity;

            setElementValidity = function() {
                ngModelCtrl.$setValidity('maxTags', tagList.items.length <= options.maxTags);
                ngModelCtrl.$setValidity('minTags', tagList.items.length >= options.minTags);
                ngModelCtrl.$setValidity('leftoverText', scope.hasFocus || options.allowLeftoverText ? true : !scope.newTag.text());
            };

            ngModelCtrl.$isEmpty = function(value) {
                return !value || !value.length;
            };

            scope.newTag = {
                text: function(value) {
                    if (angular.isDefined(value)) {
                        scope.text = value;
                        events.trigger('input-change', value);
                    }
                    else {
                        return scope.text || '';
                    }
                },
                invalid: null
            };

            scope.track = function(tag) {
                return tag[options.keyProperty || options.displayProperty];
            };

            scope.$watch('tags', function(value) {
                if (value) {
                    tagList.items = tiUtil.makeObjectArray(value, options.displayProperty);
                    scope.tags = tagList.items;
                }
                else {
                    tagList.items = [];
                }
            });

            scope.$watch('tags.length', function() {
                setElementValidity();

                // ngModelController won't trigger validators when the model changes (because it's an array),
                // so we need to do it ourselves. Unfortunately this won't trigger any registered formatter.
                ngModelCtrl.$validate();
            });

            attrs.$observe('disabled', function(value) {
                scope.disabled = value;
            });

            scope.eventHandlers = {
                input: {
                    keydown: function($event) {
                        events.trigger('input-keydown', $event);
                    },
                    focus: function() {
                        if (scope.hasFocus) {
                            return;
                        }

                        scope.hasFocus = true;
                        events.trigger('input-focus');
                    },
                    blur: function() {
                        $timeout(function() {
                            var activeElement = $document.prop('activeElement'),
                                lostFocusToBrowserWindow = activeElement === input[0],
                                lostFocusToChildElement = element[0].contains(activeElement);

                            if (lostFocusToBrowserWindow || !lostFocusToChildElement) {
                                scope.hasFocus = false;
                                events.trigger('input-blur');
                            }
                        });
                    },
                    paste: function($event) {
                        $event.getTextData = function() {
                            var clipboardData = $event.clipboardData || ($event.originalEvent && $event.originalEvent.clipboardData);
                            return clipboardData ? clipboardData.getData('text/plain') : $window.clipboardData.getData('Text');
                        };
                        events.trigger('input-paste', $event);
                    }
                },
                host: {
                    click: function() {
                        if (scope.disabled) {
                            return;
                        }
                        input[0].focus();
                    }
                },
                tag: {
                    click: function(tag) {
                        events.trigger('tag-clicked', { $tag: tag });
                    }
                }
            };

            events
                .on('tag-added', scope.onTagAdded)
                .on('invalid-tag', scope.onInvalidTag)
                .on('tag-removed', scope.onTagRemoved)
                .on('tag-clicked', scope.onTagClicked)
                .on('tag-added', function() {
                    scope.newTag.text('');
                })
                .on('tag-added tag-removed', function() {
                    scope.tags = tagList.items;
                    // Ideally we should be able call $setViewValue here and let it in turn call $setDirty and $validate
                    // automatically, but since the model is an array, $setViewValue does nothing and it's up to us to do it.
                    // Unfortunately this won't trigger any registered $parser and there's no safe way to do it.
                    ngModelCtrl.$setDirty();
                })
                .on('invalid-tag', function() {
                    scope.newTag.invalid = true;
                })
                .on('option-change', function(e) {
                    if (validationOptions.indexOf(e.name) !== -1) {
                        setElementValidity();
                    }
                })
                .on('input-change', function() {
                    tagList.clearSelection();
                    scope.newTag.invalid = null;
                })
                .on('input-focus', function() {
                    element.triggerHandler('focus');
                    ngModelCtrl.$setValidity('leftoverText', true);
                })
                .on('input-blur', function() {
                    if (options.addOnBlur && !options.addFromAutocompleteOnly) {
                        tagList.addText(scope.newTag.text());
                    }
                    element.triggerHandler('blur');
                    setElementValidity();
                })
                .on('input-keydown', function(event) {
                    var key = event.keyCode,
                        addKeys = {},
                        shouldAdd, shouldRemove, shouldSelect, shouldEditLastTag;

                    if (tiUtil.isModifierOn(event) || hotkeys.indexOf(key) === -1) {
                        return;
                    }

                    addKeys[KEYS.enter] = options.addOnEnter;
                    addKeys[KEYS.comma] = options.addOnComma;
                    addKeys[KEYS.space] = options.addOnSpace;

                    shouldAdd = !options.addFromAutocompleteOnly && addKeys[key];
                    shouldRemove = (key === KEYS.backspace || key === KEYS.delete) && tagList.selected;
                    shouldEditLastTag = key === KEYS.backspace && scope.newTag.text().length === 0 && options.enableEditingLastTag;
                    shouldSelect = (key === KEYS.backspace || key === KEYS.left || key === KEYS.right) && scope.newTag.text().length === 0 && !options.enableEditingLastTag;

                    if (shouldAdd) {
                        tagList.addText(scope.newTag.text());
                    }
                    else if (shouldEditLastTag) {
                        var tag;

                        tagList.selectPrior();
                        tag = tagList.removeSelected();

                        if (tag) {
                            scope.newTag.text(tag[options.displayProperty]);
                        }
                    }
                    else if (shouldRemove) {
                        tagList.removeSelected();
                    }
                    else if (shouldSelect) {
                        if (key === KEYS.left || key === KEYS.backspace) {
                            tagList.selectPrior();
                        }
                        else if (key === KEYS.right) {
                            tagList.selectNext();
                        }
                    }

                    if (shouldAdd || shouldSelect || shouldRemove || shouldEditLastTag) {
                        event.preventDefault();
                    }
                })
                .on('input-paste', function(event) {
                    if (options.addOnPaste) {
                        var data = event.getTextData();
                        var tags = data.split(options.pasteSplitPattern);

                        if (tags.length > 1) {
                            tags.forEach(function(tag) {
                                tagList.addText(tag);
                            });
                            event.preventDefault();
                        }
                    }
                });
        }
    };
});
<|endoftext|>


<commit_before>(function() {
'use strict';

angular.module('tags-input', []);

/**
 * @ngdoc directive
 * @name tagsInput.directive:tagsInput
 *
 * @description
 * ngTagsInput is an Angular directive that renders an input box with tag editing support.
 *
 * @param {string} ngModel Assignable angular expression to data-bind to.
 * @param {string} ngClass CSS class to style the control.
 * @param {number} tabindex Tab order of the control.
 * @param {string=Add a tag} placeholder Placeholder text for the control.
 * @param {number=3} minLength Minimum length for a new tag.
 * @param {number=} maxLength Maximum length allowed for a new tag.
 * @param {string=×} removeTagSymbol Symbol character for the remove tag button.
 * @param {boolean=true} addOnEnter Flag indicating that a new tag will be added on pressing the ENTER key.
 * @param {boolean=false} addOnSpace Flag indicating that a new tag will be added on pressing the SPACE key.
 * @param {boolean=true} addOnComma Flag indicating that a new tag will be added on pressing the COMMA key.
 * @param {boolean=true} replaceSpacesWithDashes Flag indicating that spaces will be replaced with dashes.
 * @param {string=^[a-zA-Z0-9\s]+$*} allowedTagsPattern Regular expression that determines whether a new tag is valid.
 * @param {boolean=false} enableEditingLastTag Flag indicating that the last tag will be moved back into
 *                                             the new tag input box instead of being removed when the backspace key
 *                                             is pressed and the input box is empty.
 */
angular.module('tags-input').directive('tagsInput', function($interpolate) {
    function loadOptions(scope, attrs) {
        function getStr(name, defaultValue) {
            return attrs[name] ? $interpolate(attrs[name])(scope.$parent) : defaultValue;
        }

        function getInt(name, defaultValue) {
            var value = getStr(name, null);
            return value ? parseInt(value, 10) : defaultValue;
        }

        function getBool(name, defaultValue) {
            var value = getStr(name, null);
            return value ? value === 'true' : defaultValue;
        }

        scope.options = {
            cssClass: getStr('ngClass', ''),
            placeholder: getStr('placeholder', 'Add a tag'),
            tabindex: getInt('tabindex', ''),
            removeTagSymbol: getStr('removeTagSymbol', String.fromCharCode(215)),
            replaceSpacesWithDashes: getBool('replaceSpacesWithDashes', true),
            minLength: getInt('minLength', 3),
            maxLength: getInt('maxLength', ''),
            addOnEnter: getBool('addOnEnter', true),
            addOnSpace: getBool('addOnSpace', false),
            addOnComma: getBool('addOnComma', true),
            allowedTagsPattern: new RegExp(getStr('allowedTagsPattern', '^[a-zA-Z0-9\\s]+$')),
            enableEditingLastTag: getBool('enableEditingLastTag', false)
        };
    }

    return {
        restrict: 'A,E',
        scope: { tags: '=ngModel' },
        replace: false,
        transclude: true,
        template: '
' +
                  '
' +
                  '
' +
                  '
' +
                  '        {{ tag }}' +
                  '        <button type="button" ng-click="remove($index)">{{ options.removeTagSymbol }}</button>' +
                  '
' +
                  '
' +
                  '    <input type="text" placeholder="{{ options.placeholder }}" size="{{ options.placeholder.length }}" maxlength="{{ options.maxLength }}" tabindex="{{ options.tabindex }}" ng-model="newTag" ng-change="newTagChange()">' +
                  '
' +
                  '
',
        controller: function($scope, $attrs, $element) {
            var shouldRemoveLastTag;

            loadOptions($scope, $attrs);

            $scope.newTag = '';
            $scope.tags = $scope.tags || [];

            $scope.tryAdd = function() {
                var changed = false;
                var tag = $scope.newTag;

                if (tag.length >= $scope.options.minLength && $scope.options.allowedTagsPattern.test(tag)) {

                    if ($scope.options.replaceSpacesWithDashes) {
                        tag = tag.replace(/\s/g, '-');
                    }

                    if ($scope.tags.indexOf(tag) === -1) {
                        $scope.tags.push(tag);
                    }

                    $scope.newTag = '';
                    changed = true;
                }
                return changed;
            };

            $scope.tryRemoveLast = function() {
                var changed = false;
                if ($scope.tags.length > 0) {
                    if ($scope.options.enableEditingLastTag) {
                        $scope.newTag = $scope.tags.pop();
                    }
                    else {
                        if (shouldRemoveLastTag) {
                            $scope.tags.pop();

                            shouldRemoveLastTag = false;
                        }
                        else {
                            shouldRemoveLastTag = true;
                        }
                    }
                    changed = true;
                }
                return changed;
            };

            $scope.remove = function(index) {
                $scope.tags.splice(index, 1);
            };

            $scope.getCssClass = function(index) {
                var isLastTag = index === $scope.tags.length - 1;
                return shouldRemoveLastTag && isLastTag ? 'selected' : '';
            };

            $scope.$watch(function() { return $scope.newTag.length > 0; }, function() {
                shouldRemoveLastTag = false;
            });

            $scope.newTagChange = angular.noop;

            this.getNewTagInput = function() {
                var input = $element.find('input');
                input.changeValue = function(value) {
                    $scope.newTag = value;
                };

                input.change = function(handler) {
                    $scope.newTagChange = function() {
                        handler($scope.newTag);
                    };
                };

                return input;
            };
        },
        link: function(scope, element) {
            var hotkeys = [KEYS.enter, KEYS.comma, KEYS.space, KEYS.backspace];
            var input = element.find('input');

            input.bind('keydown', function(e) {
                var key;

                // This hack is needed because jqLite doesn't implement stopImmediatePropagation properly.
                // I've sent a PR to Angular addressing this issue and hopefully it'll be fixed soon.
                // https://github.com/angular/angular.js/pull/4833
                if (e.isImmediatePropagationStopped && e.isImmediatePropagationStopped()) {
                    return;
                }

                if (hotkeys.indexOf(e.keyCode) === -1) {
                    return;
                }

                key = e.keyCode;

                if (key === KEYS.enter && scope.options.addOnEnter ||
                    key === KEYS.comma && scope.options.addOnComma ||
                    key === KEYS.space && scope.options.addOnSpace) {

                    if (scope.tryAdd()) {
                        scope.$apply();
                    }
                    e.preventDefault();
                }
                else if (key === KEYS.backspace && this.value.length === 0) {
                    if (scope.tryRemoveLast()) {
                        scope.$apply();

                        e.preventDefault();
                    }
                }
            });

            element.find('div').bind('click', function() {
                input[0].focus();
            });
        }
    };
});

}());<commit_msg>Reformatted tags input template<commit_after>(function() {
'use strict';

angular.module('tags-input', []);

/**
 * @ngdoc directive
 * @name tagsInput.directive:tagsInput
 *
 * @description
 * ngTagsInput is an Angular directive that renders an input box with tag editing support.
 *
 * @param {string} ngModel Assignable angular expression to data-bind to.
 * @param {string} ngClass CSS class to style the control.
 * @param {number} tabindex Tab order of the control.
 * @param {string=Add a tag} placeholder Placeholder text for the control.
 * @param {number=3} minLength Minimum length for a new tag.
 * @param {number=} maxLength Maximum length allowed for a new tag.
 * @param {string=×} removeTagSymbol Symbol character for the remove tag button.
 * @param {boolean=true} addOnEnter Flag indicating that a new tag will be added on pressing the ENTER key.
 * @param {boolean=false} addOnSpace Flag indicating that a new tag will be added on pressing the SPACE key.
 * @param {boolean=true} addOnComma Flag indicating that a new tag will be added on pressing the COMMA key.
 * @param {boolean=true} replaceSpacesWithDashes Flag indicating that spaces will be replaced with dashes.
 * @param {string=^[a-zA-Z0-9\s]+$*} allowedTagsPattern Regular expression that determines whether a new tag is valid.
 * @param {boolean=false} enableEditingLastTag Flag indicating that the last tag will be moved back into
 *                                             the new tag input box instead of being removed when the backspace key
 *                                             is pressed and the input box is empty.
 */
angular.module('tags-input').directive('tagsInput', function($interpolate) {
    function loadOptions(scope, attrs) {
        function getStr(name, defaultValue) {
            return attrs[name] ? $interpolate(attrs[name])(scope.$parent) : defaultValue;
        }

        function getInt(name, defaultValue) {
            var value = getStr(name, null);
            return value ? parseInt(value, 10) : defaultValue;
        }

        function getBool(name, defaultValue) {
            var value = getStr(name, null);
            return value ? value === 'true' : defaultValue;
        }

        scope.options = {
            cssClass: getStr('ngClass', ''),
            placeholder: getStr('placeholder', 'Add a tag'),
            tabindex: getInt('tabindex', ''),
            removeTagSymbol: getStr('removeTagSymbol', String.fromCharCode(215)),
            replaceSpacesWithDashes: getBool('replaceSpacesWithDashes', true),
            minLength: getInt('minLength', 3),
            maxLength: getInt('maxLength', ''),
            addOnEnter: getBool('addOnEnter', true),
            addOnSpace: getBool('addOnSpace', false),
            addOnComma: getBool('addOnComma', true),
            allowedTagsPattern: new RegExp(getStr('allowedTagsPattern', '^[a-zA-Z0-9\\s]+$')),
            enableEditingLastTag: getBool('enableEditingLastTag', false)
        };
    }

    return {
        restrict: 'A,E',
        scope: { tags: '=ngModel' },
        replace: false,
        transclude: true,
        template: '
' +
                  '
' +
                  '
' +
                  '
' +
                  '        {{ tag }}' +
                  '        <button type="button" ng-click="remove($index)">{{ options.removeTagSymbol }}</button>' +
                  '
' +
                  '
' +
                  '    <input type="text"' +
                  '           placeholder="{{ options.placeholder }}"' +
                  '           size="{{ options.placeholder.length }}"' +
                  '           maxlength="{{ options.maxLength }}"' +
                  '           tabindex="{{ options.tabindex }}"' +
                  '           ng-model="newTag"' +
                  '           ng-change="newTagChange()">' +
                  '
' +
                  '
',
        controller: function($scope, $attrs, $element) {
            var shouldRemoveLastTag;

            loadOptions($scope, $attrs);

            $scope.newTag = '';
            $scope.tags = $scope.tags || [];

            $scope.tryAdd = function() {
                var changed = false;
                var tag = $scope.newTag;

                if (tag.length >= $scope.options.minLength && $scope.options.allowedTagsPattern.test(tag)) {

                    if ($scope.options.replaceSpacesWithDashes) {
                        tag = tag.replace(/\s/g, '-');
                    }

                    if ($scope.tags.indexOf(tag) === -1) {
                        $scope.tags.push(tag);
                    }

                    $scope.newTag = '';
                    changed = true;
                }
                return changed;
            };

            $scope.tryRemoveLast = function() {
                var changed = false;
                if ($scope.tags.length > 0) {
                    if ($scope.options.enableEditingLastTag) {
                        $scope.newTag = $scope.tags.pop();
                    }
                    else {
                        if (shouldRemoveLastTag) {
                            $scope.tags.pop();

                            shouldRemoveLastTag = false;
                        }
                        else {
                            shouldRemoveLastTag = true;
                        }
                    }
                    changed = true;
                }
                return changed;
            };

            $scope.remove = function(index) {
                $scope.tags.splice(index, 1);
            };

            $scope.getCssClass = function(index) {
                var isLastTag = index === $scope.tags.length - 1;
                return shouldRemoveLastTag && isLastTag ? 'selected' : '';
            };

            $scope.$watch(function() { return $scope.newTag.length > 0; }, function() {
                shouldRemoveLastTag = false;
            });

            $scope.newTagChange = angular.noop;

            this.getNewTagInput = function() {
                var input = $element.find('input');
                input.changeValue = function(value) {
                    $scope.newTag = value;
                };

                input.change = function(handler) {
                    $scope.newTagChange = function() {
                        handler($scope.newTag);
                    };
                };

                return input;
            };
        },
        link: function(scope, element) {
            var hotkeys = [KEYS.enter, KEYS.comma, KEYS.space, KEYS.backspace];
            var input = element.find('input');

            input.bind('keydown', function(e) {
                var key;

                // This hack is needed because jqLite doesn't implement stopImmediatePropagation properly.
                // I've sent a PR to Angular addressing this issue and hopefully it'll be fixed soon.
                // https://github.com/angular/angular.js/pull/4833
                if (e.isImmediatePropagationStopped && e.isImmediatePropagationStopped()) {
                    return;
                }

                if (hotkeys.indexOf(e.keyCode) === -1) {
                    return;
                }

                key = e.keyCode;

                if (key === KEYS.enter && scope.options.addOnEnter ||
                    key === KEYS.comma && scope.options.addOnComma ||
                    key === KEYS.space && scope.options.addOnSpace) {

                    if (scope.tryAdd()) {
                        scope.$apply();
                    }
                    e.preventDefault();
                }
                else if (key === KEYS.backspace && this.value.length === 0) {
                    if (scope.tryRemoveLast()) {
                        scope.$apply();

                        e.preventDefault();
                    }
                }
            });

            element.find('div').bind('click', function() {
                input[0].focus();
            });
        }
    };
});

}());<|endoftext|>


<commit_before>var _ = require('lodash')                               // import _ from 'lodash'
var taskOps = require('../constants/task-op')           // import * as taskOps from '../constants/task-op'

var createFn    = require('./create.js')
var convertFn   = require('./convert.js')
var echoFn      = require('./echo.js')
var emailFn     = require('./email.js')
var executeFn   = require('./execute.js')
var filterFn    = require('./filter.js')
var inputFn     = require('./input.js')
var insertFn    = require('./insert.js')
var limitFn     = require('./limit.js')
var listFn      = require('./list.js')
var outputFn    = require('./output.js')
var readFn      = require('./read.js')
var renderFn    = require('./render.js')
var requestFn   = require('./request.js')
var selectFn    = require('./select.js')
var setFn       = require('./set.js')
var sleepFn     = require('./sleep.js')
var taskFn      = require('./task.js')
var transformFn = require('./transform.js')
var writeFn     = require('./write.js')

var toCode = function(json) {
  var type = _.get(json, 'type', '')

  switch (type) {
    default:
      return taskFn.toCode(json)

    case taskOps.TASK_OP_CREATE:     return createFn.toCode(json)
    case taskOps.TASK_OP_CONVERT:    return convertFn.toCode(json)
    case taskOps.TASK_OP_ECHO:       return echoFn.toCode(json)
    case taskOps.TASK_OP_EMAIL_SEND: return emailFn.toCode(json)
    case taskOps.TASK_OP_EXECUTE:    return executeFn.toCode(json)
    case taskOps.TASK_OP_FILTER:     return filterFn.toCode(json)
    case taskOps.TASK_OP_INSERT:     return insertFn.toCode(json)
    case taskOps.TASK_OP_LIMIT:      return limitFn.toCode(json)
    case taskOps.TASK_OP_READ:       return readFn.toCode(json)
    case taskOps.TASK_OP_RENDER:     return renderFn.toCode(json)
    case taskOps.TASK_OP_REQUEST:    return requestFn.toCode(json)
    case taskOps.TASK_OP_SELECT:     return selectFn.toCode(json)
    case taskOps.TASK_OP_SET:        return setFn.toCode(json)
    case taskOps.TASK_OP_SLEEP:      return sleepFn.toCode(json)
    case taskOps.TASK_OP_WRITE:      return writeFn.toCode(json)
  }
}

module.exports = {
  create:     createFn,
  convert:    convertFn,
  echo:       echoFn,
  email:      emailFn,
  execute:    executeFn.execute,
  input:      inputFn,
  insert:     insertFn,
  filter:     filterFn,
  javascript: executeFn.javascript,
  limit:      limitFn,
  list:       listFn,
  output:     outputFn,
  python:     executeFn.python,
  read:       readFn,
  render:     renderFn,
  request:    requestFn,
  select:     selectFn,
  set:        setFn,
  sleep:      sleepFn,
  task:       taskFn,
  transform:  transformFn,
  write:      writeFn,

  // expose all task 'toCode' calls as a single function
  toCode:     toCode
}

/*
import inputFn from './input'
import outputFn from './output'
import convertFn from './convert'
import echoFn from './echo'
import emailFn from './email'
import * as executeFns from './execute'
import filterFn from './filter'
import limitFn from './limit'
import listFn from './list'
import readFn from './read'
import renderFn from './render'
import requestFn from './request'
import selectFn from './select'
import setFn from './set'
import sleepFn from './sleep'
import transformFn from './task'
import transformFn from './transform'
import writeFn from './write'

export const input      = inputFn
export const output     = outputFn
export const convert    = convertFn
export const echo       = echoFn
export const email      = emailFn
export const execute    = executeFns.execute
export const filter     = filterFn
export const javascript = executeFns.javascript
export const limit      = limitFn
export const list       = listFn
export const python     = executeFns.python
export const read       = readFn
export const render     = renderFn
export const request    = requestFn
export const select     = selectFn
export const set        = setFn
export const sleep      = sleepFn
export const transform  = taskFn
export const transform  = transformFn
export const write      = writeFn
*/
<commit_msg>More 'type' => 'op'.<commit_after>var _ = require('lodash')                               // import _ from 'lodash'
var taskOps = require('../constants/task-op')           // import * as taskOps from '../constants/task-op'

var createFn    = require('./create.js')
var convertFn   = require('./convert.js')
var echoFn      = require('./echo.js')
var emailFn     = require('./email.js')
var executeFn   = require('./execute.js')
var filterFn    = require('./filter.js')
var inputFn     = require('./input.js')
var insertFn    = require('./insert.js')
var limitFn     = require('./limit.js')
var listFn      = require('./list.js')
var outputFn    = require('./output.js')
var readFn      = require('./read.js')
var renderFn    = require('./render.js')
var requestFn   = require('./request.js')
var selectFn    = require('./select.js')
var setFn       = require('./set.js')
var sleepFn     = require('./sleep.js')
var taskFn      = require('./task.js')
var transformFn = require('./transform.js')
var writeFn     = require('./write.js')

var toCode = function(json) {
  var type = _.get(json, 'op', '')

  switch (type) {
    default:
      return taskFn.toCode(json)

    case taskOps.TASK_OP_CREATE:     return createFn.toCode(json)
    case taskOps.TASK_OP_CONVERT:    return convertFn.toCode(json)
    case taskOps.TASK_OP_ECHO:       return echoFn.toCode(json)
    case taskOps.TASK_OP_EMAIL_SEND: return emailFn.toCode(json)
    case taskOps.TASK_OP_EXECUTE:    return executeFn.toCode(json)
    case taskOps.TASK_OP_FILTER:     return filterFn.toCode(json)
    case taskOps.TASK_OP_INSERT:     return insertFn.toCode(json)
    case taskOps.TASK_OP_LIMIT:      return limitFn.toCode(json)
    case taskOps.TASK_OP_READ:       return readFn.toCode(json)
    case taskOps.TASK_OP_RENDER:     return renderFn.toCode(json)
    case taskOps.TASK_OP_REQUEST:    return requestFn.toCode(json)
    case taskOps.TASK_OP_SELECT:     return selectFn.toCode(json)
    case taskOps.TASK_OP_SET:        return setFn.toCode(json)
    case taskOps.TASK_OP_SLEEP:      return sleepFn.toCode(json)
    case taskOps.TASK_OP_WRITE:      return writeFn.toCode(json)
  }
}

module.exports = {
  create:     createFn,
  convert:    convertFn,
  echo:       echoFn,
  email:      emailFn,
  execute:    executeFn.execute,
  input:      inputFn,
  insert:     insertFn,
  filter:     filterFn,
  javascript: executeFn.javascript,
  limit:      limitFn,
  list:       listFn,
  output:     outputFn,
  python:     executeFn.python,
  read:       readFn,
  render:     renderFn,
  request:    requestFn,
  select:     selectFn,
  set:        setFn,
  sleep:      sleepFn,
  task:       taskFn,
  transform:  transformFn,
  write:      writeFn,

  // expose all task 'toCode' calls as a single function
  toCode:     toCode
}

/*
import inputFn from './input'
import outputFn from './output'
import convertFn from './convert'
import echoFn from './echo'
import emailFn from './email'
import * as executeFns from './execute'
import filterFn from './filter'
import limitFn from './limit'
import listFn from './list'
import readFn from './read'
import renderFn from './render'
import requestFn from './request'
import selectFn from './select'
import setFn from './set'
import sleepFn from './sleep'
import transformFn from './task'
import transformFn from './transform'
import writeFn from './write'

export const input      = inputFn
export const output     = outputFn
export const convert    = convertFn
export const echo       = echoFn
export const email      = emailFn
export const execute    = executeFns.execute
export const filter     = filterFn
export const javascript = executeFns.javascript
export const limit      = limitFn
export const list       = listFn
export const python     = executeFns.python
export const read       = readFn
export const render     = renderFn
export const request    = requestFn
export const select     = selectFn
export const set        = setFn
export const sleep      = sleepFn
export const transform  = taskFn
export const transform  = transformFn
export const write      = writeFn
*/
<|endoftext|>


<commit_before>var should = require('should');
var request = require('supertest');
var server = require('../../../app');

process.env.A127_ENV = 'test';

describe('controllers', function() {

  describe('hello_world', function() {

    describe('GET /hello', function() {

      it('should return a default string', function(done) {

        request(server)
          .get('/hello')
          .set('Accept', 'application/json')
          .expect('Content-Type', /json/)
          .expect(200)
          .end(function(err, res) {
            should.not.exist(err);

            res.body.should.eql('Hello, stranger!');

            done();
          });
      });

      it('should accept a name parameter', function(done) {

        request(server)
          .get('/hello')
          .query({
            name: 'Scott'
          })
          .set('Accept', 'application/json')
          .expect('Content-Type', /json/)
          .expect(200)
          .end(function(err, res) {
            should.not.exist(err);

            res.body.should.eql('Hello, Scott!');

            done();
          });
      });
    });
  });
});
<commit_msg>Update hello_world.js<commit_after>var should = require('should');
var request = require('supertest');
var server = require('../../../app');

process.env.A127_ENV = 'test';

describe('controllers', function() {

  describe('hello_world', function() {

    describe('GET /hello', function() {

      it('should return a default string', function(done) {

        request(server)
          .get('/hello')
          .set('Accept', 'application/json')
          .expect('Content-Type', /json/)
          .expect(200)
          .end(function(err, res) {
            should.not.exist(err);

            res.body.should.eql('Hello, stranger!');

            done();
          });
      });

      it('should accept a name parameter', function(done) {

        request(server)
          .get('/hello')
          .query({
            name: 'Scott'
          })
          .set('Accept', 'application/json')
          .expect('Content-Type', /json/)
          .expect(200)
          .end(function(err, res) {
            should.not.exist(err);

            res.body.should.eql('Hello, Scott!');

            done();
          });
      });
    });
  });
});
<|endoftext|>