<commit_before>// Read https://zulip.readthedocs.io/en/latest/subsystems/hashchange-system.html
var hashchange = (function () {

var exports = {};

var expected_hash;
var changing_hash = false;

function set_hash(hash) {
    var location = window.location;

    if (history.pushState) {
        if (hash === '' || hash.charAt(0) !== '#') {
            hash = '#' + hash;
        }

        // IE returns pathname as undefined and missing the leading /
        var pathname = location.pathname;
        if (pathname === undefined) {
            pathname = '/';
        } else if (pathname === '' || pathname.charAt(0) !== '/') {
            pathname = '/' + pathname;
        }

        // Build a full URL to not have same origin problems
        var url =  location.protocol + '//' + location.host + pathname + hash;
        history.pushState(null, null, url);
    } else {
        location.hash = hash;
    }
}

exports.changehash = function (newhash) {
    if (changing_hash) {
        return;
    }
    $(document).trigger($.Event('zuliphashchange.zulip'));
    set_hash(newhash);
    favicon.reset();
};

exports.save_narrow = function (operators) {
    if (changing_hash) {
        return;
    }
    var new_hash = hash_util.operators_to_hash(operators);
    exports.changehash(new_hash);
};

exports.parse_narrow = function (hash) {
    var i;
    var operators = [];
    for (i = 1; i < hash.length; i += 2) {
        // We don't construct URLs with an odd number of components,
        // but the user might write one.
        try {
            var operator = hash_util.decodeHashComponent(hash[i]);
            // Do not parse further if empty operator encountered.
            if (operator === '') {
                break;
            }

            var operand  = hash_util.decode_operand(operator, hash[i + 1] || '');
            var negated = false;
            if (operator[0] === '-') {
                negated = true;
                operator = operator.slice(1);
            }
            operators.push({negated: negated, operator: operator, operand: operand});
        } catch (err) {
            return;
        }
    }
    return operators;
};

function activate_home_tab() {
    ui_util.change_tab_to("#home");
    narrow.deactivate();
    floating_recipient_bar.update();
}

// Returns true if this function performed a narrow
function do_hashchange(from_reload) {
    // If window.location.hash changed because our app explicitly
    // changed it, then we don't need to do anything.
    // (This function only neds to jump into action if it changed
    // because e.g. the back button was pressed by the user)
    //
    // The second case is for handling the fact that some browsers
    // automatically convert '#' to '' when you change the hash to '#'.
    if (window.location.hash === expected_hash ||
        expected_hash !== undefined &&
         window.location.hash.replace(/^#/, '') === '' &&
         expected_hash.replace(/^#/, '') === '') {
        return false;
    }

    $(document).trigger($.Event('zuliphashchange.zulip'));

    // NB: In Firefox, window.location.hash is URI-decoded.
    // Even if the URL bar says #%41%42%43%44, the value here will
    // be #ABCD.
    var hash = window.location.hash.split("/");
    switch (hash[0]) {
    case "#narrow":
        ui_util.change_tab_to("#home");
        var operators = exports.parse_narrow(hash);
        if (operators === undefined) {
            // If the narrow URL didn't parse, clear
            // window.location.hash and send them to the home tab
            set_hash('');
            activate_home_tab();
            return false;
        }
        var narrow_opts = {
            change_hash:    false,  // already set
            trigger: 'hash change',
        };
        if (from_reload) {
            blueslip.debug('We are narrowing as part of a reload.');
            if (page_params.initial_narrow_pointer !== undefined) {
                home_msg_list.pre_narrow_offset = page_params.initial_offset;
                narrow_opts.then_select_id = page_params.initial_narrow_pointer;
                narrow_opts.then_select_offset = page_params.initial_narrow_offset;
            }
        }
        narrow.activate(operators, narrow_opts);
        floating_recipient_bar.update();
        return true;
    case "":
    case "#":
        activate_home_tab();
        break;
    case "#streams":
        ui_util.change_tab_to("#streams");
        break;
    case "#keyboard-shortcuts":
        info_overlay.show("keyboard-shortcuts");
        break;
    case "#message-formatting":
        info_overlay.show("message-formatting");
        break;
    case "#search-operators":
        info_overlay.show("search-operators");
        break;
    case "#drafts":
        ui_util.change_tab_to("#drafts");
        break;
    case "#organization":
        ui_util.change_tab_to("#organization");
        break;
    case "#settings":
        ui_util.change_tab_to("#settings");
        break;
    }
    return false;
}

// -- -- -- -- -- -- READ THIS BEFORE TOUCHING ANYTHING BELOW -- -- -- -- -- -- //
// HOW THE HASH CHANGE MECHANISM WORKS:
// When going from a normal view (eg. `narrow/is/private`) to a settings panel
// (eg. `settings/your-bots`) it should trigger the `is_overlay_hash` function and
// return `true` for the current state -- we want to ignore hash changes from
// within the settings page. The previous hash however should return `false` as it
// was outside of the scope of settings.
// there is then an `exit_overlay` function that allows the hash to change exactly
// once without triggering any events. This allows the hash to reset back from
// a settings page to the previous view available before the settings page
// (eg. narrow/is/private). This saves the state, scroll position, and makes the
// hash change functionally inert.
// -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - -- //
var ignore = {
    is_exiting_overlay: false,
    hash_before_overlay: null,
    old_hash: typeof window !== "undefined" ? window.location.hash : "#",
    old_overlay_group: null,
};

function get_main_hash(hash) {
    return hash ? hash.replace(/^#/, "").split(/\//)[0] : "";
}

function get_hash_components() {
    var hash = window.location.hash.split(/\//);

    return {
        base: hash.shift(),
        arguments: hash,
    };
}

// different groups require different reloads. The grouped elements don't
// require a reload or overlay change to run.
var get_hash_group = (function () {
    var groups = [
        ["streams"],
        ["settings", "organization"],
        ["invite"],
    ];

    return function (value) {
        var idx = null;

        _.find(groups, function (o, i) {
            if (o.indexOf(value) !== -1) {
                idx = i;
                return true;
            }
            return false;
        });

        return idx;
    };
}());

function is_overlay_hash(hash) {
    // Hash changes within this list are overlays and should not unnarrow (etc.)
    var overlay_list = ["streams", "drafts", "settings", "organization", "invite"];
    var main_hash = get_main_hash(hash);

    return overlay_list.indexOf(main_hash) > -1;
}

function hashchanged_overlay(old_hash) {
    var base = get_main_hash(window.location.hash);

    // if the old has was a standard non-ignore hash OR the ignore hash
    // base has changed, something needs to run again.

    if (!is_overlay_hash(old_hash || "#") || ignore.old_overlay_group !== get_hash_group(base)) {
        if (ignore.old_overlay_group !== get_hash_group(base)) {
            overlays.close_for_hash_change();
        }

        // now only if the previous one should not have been ignored.
        if (!is_overlay_hash(old_hash || "#")) {
            ignore.hash_before_overlay = old_hash;
        }

        if (base === "streams") {
            subs.launch(get_hash_components());
        } else if (base === "drafts") {
            drafts.launch();
        } else if (/settings|organization/.test(base)) {
            settings.setup_page();
            admin.setup_page();
        } else if (base === "invite") {
            invite.launch();
        }

        ignore.old_overlay_group = get_hash_group(base);
    } else {
        subs.change_state(get_hash_components());
    }
}

function hashchanged(from_reload, e) {
    var old_hash;
    if (e) {
        old_hash = "#" + (e.oldURL || ignore.old_hash).split(/#/).slice(1).join("");
        ignore.old_hash = window.location.hash;
    }

    if (is_overlay_hash(window.location.hash)) {
        hashchanged_overlay(old_hash);
        return;
    }

    // We know we are going to a "main screen" view at this point, but
    // it may have been due to us closing an overlay.
    if (ignore.is_exiting_overlay) {
        // Some click handler or something caused us to exit the overlay,
        // and we updated the browser location.  When we get this function
        // triggered, we already did the work of closing the overlay.
        ignore.is_exiting_overlay = false;
        return;
    }

    // We are changing to a "main screen" view.
    overlays.close_for_hash_change();
    changing_hash = true;
    var ret = do_hashchange(from_reload);
    changing_hash = false;
    return ret;
}

exports.initialize = function () {
    // jQuery doesn't have a hashchange event, so we manually wrap
    // our event handler
    window.onhashchange = blueslip.wrap_function(function (e) {
        hashchanged(false, e);
    });
    hashchanged(true);
};

exports.exit_overlay = function (callback) {
    if (is_overlay_hash(window.location.hash)) {
        ui_util.blur_active_element();
        ignore.is_exiting_overlay = true;
        window.location.hash = ignore.hash_before_overlay || "#";
        if (typeof callback === "function") {
            callback();
        }
    }
};

return exports;

}());
if (typeof module !== 'undefined') {
    module.exports = hashchange;
}
window.hashchange = hashchange;
<commit_msg>hashchange: Rename `ignore` to `state`.<commit_after>// Read https://zulip.readthedocs.io/en/latest/subsystems/hashchange-system.html
var hashchange = (function () {

var exports = {};

var expected_hash;
var changing_hash = false;

function set_hash(hash) {
    var location = window.location;

    if (history.pushState) {
        if (hash === '' || hash.charAt(0) !== '#') {
            hash = '#' + hash;
        }

        // IE returns pathname as undefined and missing the leading /
        var pathname = location.pathname;
        if (pathname === undefined) {
            pathname = '/';
        } else if (pathname === '' || pathname.charAt(0) !== '/') {
            pathname = '/' + pathname;
        }

        // Build a full URL to not have same origin problems
        var url =  location.protocol + '//' + location.host + pathname + hash;
        history.pushState(null, null, url);
    } else {
        location.hash = hash;
    }
}

exports.changehash = function (newhash) {
    if (changing_hash) {
        return;
    }
    $(document).trigger($.Event('zuliphashchange.zulip'));
    set_hash(newhash);
    favicon.reset();
};

exports.save_narrow = function (operators) {
    if (changing_hash) {
        return;
    }
    var new_hash = hash_util.operators_to_hash(operators);
    exports.changehash(new_hash);
};

exports.parse_narrow = function (hash) {
    var i;
    var operators = [];
    for (i = 1; i < hash.length; i += 2) {
        // We don't construct URLs with an odd number of components,
        // but the user might write one.
        try {
            var operator = hash_util.decodeHashComponent(hash[i]);
            // Do not parse further if empty operator encountered.
            if (operator === '') {
                break;
            }

            var operand  = hash_util.decode_operand(operator, hash[i + 1] || '');
            var negated = false;
            if (operator[0] === '-') {
                negated = true;
                operator = operator.slice(1);
            }
            operators.push({negated: negated, operator: operator, operand: operand});
        } catch (err) {
            return;
        }
    }
    return operators;
};

function activate_home_tab() {
    ui_util.change_tab_to("#home");
    narrow.deactivate();
    floating_recipient_bar.update();
}

// Returns true if this function performed a narrow
function do_hashchange(from_reload) {
    // If window.location.hash changed because our app explicitly
    // changed it, then we don't need to do anything.
    // (This function only neds to jump into action if it changed
    // because e.g. the back button was pressed by the user)
    //
    // The second case is for handling the fact that some browsers
    // automatically convert '#' to '' when you change the hash to '#'.
    if (window.location.hash === expected_hash ||
        expected_hash !== undefined &&
         window.location.hash.replace(/^#/, '') === '' &&
         expected_hash.replace(/^#/, '') === '') {
        return false;
    }

    $(document).trigger($.Event('zuliphashchange.zulip'));

    // NB: In Firefox, window.location.hash is URI-decoded.
    // Even if the URL bar says #%41%42%43%44, the value here will
    // be #ABCD.
    var hash = window.location.hash.split("/");
    switch (hash[0]) {
    case "#narrow":
        ui_util.change_tab_to("#home");
        var operators = exports.parse_narrow(hash);
        if (operators === undefined) {
            // If the narrow URL didn't parse, clear
            // window.location.hash and send them to the home tab
            set_hash('');
            activate_home_tab();
            return false;
        }
        var narrow_opts = {
            change_hash:    false,  // already set
            trigger: 'hash change',
        };
        if (from_reload) {
            blueslip.debug('We are narrowing as part of a reload.');
            if (page_params.initial_narrow_pointer !== undefined) {
                home_msg_list.pre_narrow_offset = page_params.initial_offset;
                narrow_opts.then_select_id = page_params.initial_narrow_pointer;
                narrow_opts.then_select_offset = page_params.initial_narrow_offset;
            }
        }
        narrow.activate(operators, narrow_opts);
        floating_recipient_bar.update();
        return true;
    case "":
    case "#":
        activate_home_tab();
        break;
    case "#streams":
        ui_util.change_tab_to("#streams");
        break;
    case "#keyboard-shortcuts":
        info_overlay.show("keyboard-shortcuts");
        break;
    case "#message-formatting":
        info_overlay.show("message-formatting");
        break;
    case "#search-operators":
        info_overlay.show("search-operators");
        break;
    case "#drafts":
        ui_util.change_tab_to("#drafts");
        break;
    case "#organization":
        ui_util.change_tab_to("#organization");
        break;
    case "#settings":
        ui_util.change_tab_to("#settings");
        break;
    }
    return false;
}

// -- -- -- -- -- -- READ THIS BEFORE TOUCHING ANYTHING BELOW -- -- -- -- -- -- //
// HOW THE HASH CHANGE MECHANISM WORKS:
// When going from a normal view (eg. `narrow/is/private`) to a settings panel
// (eg. `settings/your-bots`) it should trigger the `is_overlay_hash` function and
// return `true` for the current state -- we want to ignore hash changes from
// within the settings page. The previous hash however should return `false` as it
// was outside of the scope of settings.
// there is then an `exit_overlay` function that allows the hash to change exactly
// once without triggering any events. This allows the hash to reset back from
// a settings page to the previous view available before the settings page
// (eg. narrow/is/private). This saves the state, scroll position, and makes the
// hash change functionally inert.
// -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - -- //
var state = {
    is_exiting_overlay: false,
    hash_before_overlay: null,
    old_hash: typeof window !== "undefined" ? window.location.hash : "#",
    old_overlay_group: null,
};

function get_main_hash(hash) {
    return hash ? hash.replace(/^#/, "").split(/\//)[0] : "";
}

function get_hash_components() {
    var hash = window.location.hash.split(/\//);

    return {
        base: hash.shift(),
        arguments: hash,
    };
}

// different groups require different reloads. The grouped elements don't
// require a reload or overlay change to run.
var get_hash_group = (function () {
    var groups = [
        ["streams"],
        ["settings", "organization"],
        ["invite"],
    ];

    return function (value) {
        var idx = null;

        _.find(groups, function (o, i) {
            if (o.indexOf(value) !== -1) {
                idx = i;
                return true;
            }
            return false;
        });

        return idx;
    };
}());

function is_overlay_hash(hash) {
    // Hash changes within this list are overlays and should not unnarrow (etc.)
    var overlay_list = ["streams", "drafts", "settings", "organization", "invite"];
    var main_hash = get_main_hash(hash);

    return overlay_list.indexOf(main_hash) > -1;
}

function hashchanged_overlay(old_hash) {
    var base = get_main_hash(window.location.hash);

    // if the old has was a standard non-ignore hash OR the ignore hash
    // base has changed, something needs to run again.

    if (!is_overlay_hash(old_hash || "#") || state.old_overlay_group !== get_hash_group(base)) {
        if (state.old_overlay_group !== get_hash_group(base)) {
            overlays.close_for_hash_change();
        }

        // now only if the previous one should not have been ignored.
        if (!is_overlay_hash(old_hash || "#")) {
            state.hash_before_overlay = old_hash;
        }

        if (base === "streams") {
            subs.launch(get_hash_components());
        } else if (base === "drafts") {
            drafts.launch();
        } else if (/settings|organization/.test(base)) {
            settings.setup_page();
            admin.setup_page();
        } else if (base === "invite") {
            invite.launch();
        }

        state.old_overlay_group = get_hash_group(base);
    } else {
        subs.change_state(get_hash_components());
    }
}

function hashchanged(from_reload, e) {
    var old_hash;
    if (e) {
        old_hash = "#" + (e.oldURL || state.old_hash).split(/#/).slice(1).join("");
        state.old_hash = window.location.hash;
    }

    if (is_overlay_hash(window.location.hash)) {
        hashchanged_overlay(old_hash);
        return;
    }

    // We know we are going to a "main screen" view at this point, but
    // it may have been due to us closing an overlay.
    if (state.is_exiting_overlay) {
        // Some click handler or something caused us to exit the overlay,
        // and we updated the browser location.  When we get this function
        // triggered, we already did the work of closing the overlay.
        state.is_exiting_overlay = false;
        return;
    }

    // We are changing to a "main screen" view.
    overlays.close_for_hash_change();
    changing_hash = true;
    var ret = do_hashchange(from_reload);
    changing_hash = false;
    return ret;
}

exports.initialize = function () {
    // jQuery doesn't have a hashchange event, so we manually wrap
    // our event handler
    window.onhashchange = blueslip.wrap_function(function (e) {
        hashchanged(false, e);
    });
    hashchanged(true);
};

exports.exit_overlay = function (callback) {
    if (is_overlay_hash(window.location.hash)) {
        ui_util.blur_active_element();
        state.is_exiting_overlay = true;
        window.location.hash = state.hash_before_overlay || "#";
        if (typeof callback === "function") {
            callback();
        }
    }
};

return exports;

}());
if (typeof module !== 'undefined') {
    module.exports = hashchange;
}
window.hashchange = hashchange;
<|endoftext|>


<commit_before>var input_pill = function ($parent) {
    // a dictionary of the key codes that are associated with each key
    // to make if/else more human readable.
    var KEY = {
        ENTER: 13,
        BACKSPACE: 8,
        LEFT_ARROW: 37,
        RIGHT_ARROW: 39,
    };

    // a stateful object of this `pill_container` instance.
    // all unique instance information is stored in here.
    var store = {
        pills: [],
        $parent: $parent,
        getKeyFunction: function () {},
        lastUpdated: null,
        lastCreated: {
            keys: null,
            values: null,
        },
    };

    // a dictionary of internal functions. Some of these are exposed as well,
    // and nothing in here should be assumed to be private (due to the passing)
    // of the `this` arg in the `Function.prototype.bind` use in the prototype.
    var funcs = {
        // return the value of the contenteditable input form.
        value: function (input_elem) {
            return input_elem.innerText.trim();
        },

        // clear the value of the input form.
        clear: function (input_elem) {
            input_elem.innerText = "";
        },

        // create the object that will represent the data associated with a pill.
        // each can have a value and an optional key value.
        // the value is a human readable value that is shown, whereas the key
        // can be a hidden ID-type value.
        createPillObject: function (value, optionalKey) {
            // we need a "global" closure variable that will be flipped if the
            // key or pill creation was rejected.
            var rejected = false;

            var reject = function () {
                rejected = true;
            };

            // the user may provide a function to get a key from a value
            // that is entered, so return whatever value is gotten from
            // this function.
            // the default function is noop, so the return type is by
            // default `undefined`.
            if (typeof optionalKey === "undefined") {
                optionalKey = store.getKeyFunction(value, reject);
            }

            // if the `rejected` global is now true, it means that the user's
            // created pill was not accepted, and we should no longer proceed.
            if (rejected) {
                store.$parent.find(".input").addClass("shake");
                return;
            }

            var id = Math.random().toString(16);

            // the user may provide a function to get a key from a value
            // that is entered, so return whatever value is gotten from
            // this function.
            // the default function is noop, so the return type is by
            // default `undefined`.
            if (typeof optionalKey === "undefined") {
                optionalKey = store.getKeyFunction(value);
            }

            var payload = {
                id: id,
                value: value,
                key: optionalKey,
            };

            store.pills.push(payload);

            return payload;
        },

        // the jQuery element representation of the data.
        createPillElement: function (payload) {
            store.lastUpdated = new Date();
            payload.$element = $("
" + payload.value + "
×

");
            return payload.$element;
        },

        // this appends a pill to the end of the container but before the
        // input block.
        appendPill: function (value, optionalKey) {
            var payload = this.createPillObject(value, optionalKey);
            // if the pill object is undefined, then it means the pill was
            // rejected so we should return out of this.
            if (!payload) {
                return false;
            }
            var $pill = this.createPillElement(payload);

            store.$parent.find(".input").before($pill);
        },

        // this prepends a pill to the beginning of the container.
        prependPill: function (value, optionalKey) {
            var payload = this.createPillObject(value, optionalKey);
            if (!payload) {
                return false;
            }
            var $pill = this.createPillElement(payload);

            store.$parent.prepend($pill);
        },

        // this searches given a particlar pill ID for it, removes the node
        // from the DOM, removes it from the array and returns it.
        // this would generally be used for DOM-provoked actions, such as a user
        // clicking on a pill to remove it.
        removePill: function (id) {
            var idx;
            for (var x = 0; x < store.pills.length; x += 1) {
                if (store.pills[x].id === id) {
                    idx = x;
                }
            }

            if (typeof idx === "number") {
                store.pills[idx].$element.remove();
                store.lastUpdated = new Date();
                return store.pills.splice(idx, 1);
            }
        },

        // this will remove the last pill in the container -- by defaulat tied
        // to the "backspace" key when the value of the input is empty.
        removeLastPill: function () {
            var pill = store.pills.pop();
            store.lastUpdated = new Date();

            if (pill) {
                pill.$element.remove();
            }
        },

        // returns all data of the pills exclusive of their elements.
        data: function () {
            return store.pills.map(function (pill) {
                return {
                    value: pill.value,
                    key: pill.key,
                };
            });
        },

        // returns all hidden keys.
        // IMPORTANT: this has a caching mechanism built in to check whether the
        // store has changed since the keys/values were last retrieved so that
        // if there are many successive pulls, it doesn't have to map and create
        // an array every time.
        // this would normally be a micro-optimization, but our codebase's
        // typeaheads will ask for the keys possibly hundreds or thousands of
        // times, so this saves a lot of time.
        keys: (function () {
            var keys = [];
            return function () {
                if (store.lastUpdated >= store.lastCreated.keys) {
                    keys = store.pills.map(function (pill) {
                        return pill.key;
                    });
                    store.lastCreated.keys = new Date();
                }

                return keys;
            };
        }()),

        // returns all human-readable values.
        values: function () {
            var values = [];
            return function () {
                if (store.lastUpdated >= store.lastCreated.values) {
                    values = store.pills.map(function (pill) {
                        return pill.value;
                    });
                    store.lastCreated.values = new Date();
                }

                return values;
            };
        },
    };

    (function events() {
        store.$parent.on("keydown", ".input", function (e) {
            var char = e.keyCode || e.charCode;

            if (char === KEY.ENTER) {
                // regardless of the value of the input, the ENTER keyword
                // should be ignored in favor of keeping content to one line
                // always.
                e.preventDefault();

                // if there is input, grab the input, make a pill from it,
                // and append the pill, then clear the input.
                if (funcs.value(e.target).length > 0) {
                    var value = funcs.value(e.target);

                    // append the pill and by proxy create the pill object.
                    var ret = funcs.appendPill(value);

                    // if the pill to append was rejected, no need to clear the
                    // input; it may have just been a typo or something close but
                    // incorrect.
                    if (ret !== false) {
                        // clear the input.
                        funcs.clear(e.target);
                        e.stopPropagation();
                    }
                }

                return;
            }

            // if the user backspaces and there is input, just do normal char
            // deletion, otherwise delete the last pill in the sequence.
            if (char === KEY.BACKSPACE && funcs.value(e.target).length === 0) {
                e.preventDefault();
                funcs.removeLastPill();

                return;
            }

            // if one is on the ".input" element and back/left arrows, then it
            // should switch to focus the last pill in the list.
            // the rest of the events then will be taken care of in the function
            // below that handles events on the ".pill" class.
            if (char === KEY.LEFT_ARROW) {
                if (window.getSelection().anchorOffset === 0) {
                    store.$parent.find(".pill").last().focus();
                }
            }
        });

        // handle events while hovering on ".pill" elements.
        // the three primary events are next, previous, and delete.
        store.$parent.on("keydown", ".pill", function (e) {
            var char = e.keyCode || e.charCode;

            var $pill = store.$parent.find(".pill:focus");

            if (char === KEY.LEFT_ARROW) {
                $pill.prev().focus();
            } else if (char === KEY.RIGHT_ARROW) {
                $pill.next().focus();
            } else if (char === KEY.BACKSPACE) {
                var $next = $pill.next();
                var id = $pill.data("id");
                funcs.removePill(id);
                $next.focus();
                // the "backspace" key in FireFox will go back a page if you do
                // not prevent it.
                e.preventDefault();
            }
        });

        // when the shake animation is applied to the ".input" on invalid input,
        // we want to remove the class when finished automatically.
        store.$parent.on("animationend", ".input", function () {
            $(this).removeClass("shake");
        });

        // when the "×" is clicked on a pill, it should delete that pill and then
        // select the next pill (or input).
        store.$parent.on("click", ".exit", function () {
            var $pill = $(this).closest(".pill");
            var $next = $pill.next();
            var id = $pill.data("id");

            funcs.removePill(id);
            $next.focus();
        });

        store.$parent.on("click", function (e) {
            if ($(e.target).is(".pill-container")) {
                $(this).find(".input").focus();
            }
        });
    }());

    // the external, user-accessible prototype.
    var prototype = {
        pill: {
            append: funcs.appendPill.bind(funcs),
            prepend: funcs.prependPill.bind(funcs),
            remove: funcs.removePill.bind(funcs),
        },

        data: funcs.data,
        keys: funcs.keys,
        values: funcs.values,

        onPillCreate: function (callback) {
            store.getKeyFunction = callback;
        },
    };

    return prototype;
};

if (typeof module !== 'undefined') {
    module.exports = input_pill;
}
<commit_msg>input-pill: Add validation callback for pills.<commit_after>var input_pill = function ($parent) {
    // a dictionary of the key codes that are associated with each key
    // to make if/else more human readable.
    var KEY = {
        ENTER: 13,
        BACKSPACE: 8,
        LEFT_ARROW: 37,
        RIGHT_ARROW: 39,
    };

    // a stateful object of this `pill_container` instance.
    // all unique instance information is stored in here.
    var store = {
        pills: [],
        $parent: $parent,
        getKeyFunction: function () {},
        validation: function () {},
        lastUpdated: null,
        lastCreated: {
            keys: null,
            values: null,
        },
    };

    // a dictionary of internal functions. Some of these are exposed as well,
    // and nothing in here should be assumed to be private (due to the passing)
    // of the `this` arg in the `Function.prototype.bind` use in the prototype.
    var funcs = {
        // return the value of the contenteditable input form.
        value: function (input_elem) {
            return input_elem.innerText.trim();
        },

        // clear the value of the input form.
        clear: function (input_elem) {
            input_elem.innerText = "";
        },

        // create the object that will represent the data associated with a pill.
        // each can have a value and an optional key value.
        // the value is a human readable value that is shown, whereas the key
        // can be a hidden ID-type value.
        createPillObject: function (value, optionalKey) {
            // we need a "global" closure variable that will be flipped if the
            // key or pill creation was rejected.
            var rejected = false;

            var reject = function () {
                rejected = true;
            };

            // the user may provide a function to get a key from a value
            // that is entered, so return whatever value is gotten from
            // this function.
            // the default function is noop, so the return type is by
            // default `undefined`.
            if (typeof optionalKey === "undefined") {
                optionalKey = store.getKeyFunction(value, reject);

                if (typeof optionalKey === "object" &&
                    optionalKey.key !== undefined && optionalKey.value !== undefined) {
                        value = optionalKey.value;
                        optionalKey = optionalKey.key;
                    }
            }

            // now run a separate round of validation, in case they are using
            // `getKeyFunction` without `reject`, or not using it at all.
            store.validation(value, optionalKey, reject);

            // if the `rejected` global is now true, it means that the user's
            // created pill was not accepted, and we should no longer proceed.
            if (rejected) {
                store.$parent.find(".input").addClass("shake");
                return;
            }

            var id = Math.random().toString(16);

            var payload = {
                id: id,
                value: value,
                key: optionalKey,
            };

            store.pills.push(payload);

            return payload;
        },

        // the jQuery element representation of the data.
        createPillElement: function (payload) {
            store.lastUpdated = new Date();
            payload.$element = $("
" + payload.value + "
×

");
            return payload.$element;
        },

        // this appends a pill to the end of the container but before the
        // input block.
        appendPill: function (value, optionalKey) {
            var payload = this.createPillObject(value, optionalKey);
            // if the pill object is undefined, then it means the pill was
            // rejected so we should return out of this.
            if (!payload) {
                return false;
            }
            var $pill = this.createPillElement(payload);

            store.$parent.find(".input").before($pill);
        },

        // this prepends a pill to the beginning of the container.
        prependPill: function (value, optionalKey) {
            var payload = this.createPillObject(value, optionalKey);
            if (!payload) {
                return false;
            }
            var $pill = this.createPillElement(payload);

            store.$parent.prepend($pill);
        },

        // this searches given a particlar pill ID for it, removes the node
        // from the DOM, removes it from the array and returns it.
        // this would generally be used for DOM-provoked actions, such as a user
        // clicking on a pill to remove it.
        removePill: function (id) {
            var idx;
            for (var x = 0; x < store.pills.length; x += 1) {
                if (store.pills[x].id === id) {
                    idx = x;
                }
            }

            if (typeof idx === "number") {
                store.pills[idx].$element.remove();
                store.lastUpdated = new Date();
                return store.pills.splice(idx, 1);
            }
        },

        // this will remove the last pill in the container -- by defaulat tied
        // to the "backspace" key when the value of the input is empty.
        removeLastPill: function () {
            var pill = store.pills.pop();
            store.lastUpdated = new Date();

            if (pill) {
                pill.$element.remove();
            }
        },

        // returns all data of the pills exclusive of their elements.
        data: function () {
            return store.pills.map(function (pill) {
                return {
                    value: pill.value,
                    key: pill.key,
                };
            });
        },

        // returns all hidden keys.
        // IMPORTANT: this has a caching mechanism built in to check whether the
        // store has changed since the keys/values were last retrieved so that
        // if there are many successive pulls, it doesn't have to map and create
        // an array every time.
        // this would normally be a micro-optimization, but our codebase's
        // typeaheads will ask for the keys possibly hundreds or thousands of
        // times, so this saves a lot of time.
        keys: (function () {
            var keys = [];
            return function () {
                if (store.lastUpdated >= store.lastCreated.keys) {
                    keys = store.pills.map(function (pill) {
                        return pill.key;
                    });
                    store.lastCreated.keys = new Date();
                }

                return keys;
            };
        }()),

        // returns all human-readable values.
        values: function () {
            var values = [];
            return function () {
                if (store.lastUpdated >= store.lastCreated.values) {
                    values = store.pills.map(function (pill) {
                        return pill.value;
                    });
                    store.lastCreated.values = new Date();
                }

                return values;
            };
        },
    };

    (function events() {
        store.$parent.on("keydown", ".input", function (e) {
            var char = e.keyCode || e.charCode;

            if (char === KEY.ENTER) {
                // regardless of the value of the input, the ENTER keyword
                // should be ignored in favor of keeping content to one line
                // always.
                e.preventDefault();

                // if there is input, grab the input, make a pill from it,
                // and append the pill, then clear the input.
                if (funcs.value(e.target).length > 0) {
                    var value = funcs.value(e.target);

                    // append the pill and by proxy create the pill object.
                    var ret = funcs.appendPill(value);

                    // if the pill to append was rejected, no need to clear the
                    // input; it may have just been a typo or something close but
                    // incorrect.
                    if (ret !== false) {
                        // clear the input.
                        funcs.clear(e.target);
                        e.stopPropagation();
                    }
                }

                return;
            }

            // if the user backspaces and there is input, just do normal char
            // deletion, otherwise delete the last pill in the sequence.
            if (char === KEY.BACKSPACE && funcs.value(e.target).length === 0) {
                e.preventDefault();
                funcs.removeLastPill();

                return;
            }

            // if one is on the ".input" element and back/left arrows, then it
            // should switch to focus the last pill in the list.
            // the rest of the events then will be taken care of in the function
            // below that handles events on the ".pill" class.
            if (char === KEY.LEFT_ARROW) {
                if (window.getSelection().anchorOffset === 0) {
                    store.$parent.find(".pill").last().focus();
                }
            }
        });

        // handle events while hovering on ".pill" elements.
        // the three primary events are next, previous, and delete.
        store.$parent.on("keydown", ".pill", function (e) {
            var char = e.keyCode || e.charCode;

            var $pill = store.$parent.find(".pill:focus");

            if (char === KEY.LEFT_ARROW) {
                $pill.prev().focus();
            } else if (char === KEY.RIGHT_ARROW) {
                $pill.next().focus();
            } else if (char === KEY.BACKSPACE) {
                var $next = $pill.next();
                var id = $pill.data("id");
                funcs.removePill(id);
                $next.focus();
                // the "backspace" key in FireFox will go back a page if you do
                // not prevent it.
                e.preventDefault();
            }
        });

        // when the shake animation is applied to the ".input" on invalid input,
        // we want to remove the class when finished automatically.
        store.$parent.on("animationend", ".input", function () {
            $(this).removeClass("shake");
        });

        // when the "×" is clicked on a pill, it should delete that pill and then
        // select the next pill (or input).
        store.$parent.on("click", ".exit", function () {
            var $pill = $(this).closest(".pill");
            var $next = $pill.next();
            var id = $pill.data("id");

            funcs.removePill(id);
            $next.focus();
        });

        store.$parent.on("click", function (e) {
            if ($(e.target).is(".pill-container")) {
                $(this).find(".input").focus();
            }
        });
    }());

    // the external, user-accessible prototype.
    var prototype = {
        pill: {
            append: funcs.appendPill.bind(funcs),
            prepend: funcs.prependPill.bind(funcs),
            remove: funcs.removePill.bind(funcs),
        },

        data: funcs.data,
        keys: funcs.keys,
        values: funcs.values,

        onPillCreate: function (callback) {
            store.getKeyFunction = callback;
        },

        validate: function (callback) {
            store.validation = callback;
        },
    };

    return prototype;
};

if (typeof module !== 'undefined') {
    module.exports = input_pill;
}
<|endoftext|>


<commit_before>define(['jquery', 'underscore'], function ($, _) {

  console.log('Utils.');

  var Utils = {

    // http://detectmobilebrowsers.com/
    // + iPad
    isMobile: function  (ua) {
      var check = false;
      ua = (ua) ? ua : navigator.userAgent;
      (function(a,b){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od|ad)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4)))check = true})( ua );
      return check;
    }
  };
  return Utils;
});
<commit_msg>Delete utils.js<commit_after><|endoftext|>


<commit_before>'use strict';

(function(){
    angular.module('ngInput', [])
    .directive('ngInput', ngInput);

    function ngInput(){

        var uniqueId = 0;
        return {
            restrict: 'E'
          , replace: true
          , templateUrl: 'ng-input.html'
          , scope: {
                theme:       '@'
              , type:        '@'
              , label:       '@'
              , name:        '@'
              , icon:        '@'
              , color:       '@'
              , ngChange:    '&ngChange'
              , ngRequired:  '='
              , ngMinlength: '='
              , ngMaxlength: '='
              , ngPattern:   '='
              , ngTrim:      '='
              , ngModel:     '='
            }
          , compile: compile
        };

        function compile(){

            return {
                post: function(scope, element, attributes){

                    scope.input = element.find('input');
                    scope.label = element.find('label');

                    //Default options
                    attributes.theme = attributes.theme || 'kaede';
                    attributes.type  = attributes.type  || 'text';
                    attributes.color = attributes.color || '1';

                    var item       = 'input_' + uniqueId++
                      , inputClass = 'input--filled';

                    scope.input.attr('id' , item);
                    scope.label.attr('for', item);

                    onInputChange();
                    scope.input.bind('focus', onInputFocus);
                    scope.input.bind('blur', onInputBlur);

                    //To detect browser autofill
                    scope.input.bind('change', onInputChange);

                    scope.$watch(function(){ return scope.input.val(); }, function(newValue){
                        onInputChange();
                    });

                    function onInputChange(){
                        if(scope.input.val().trim() !== '')
                            addClass();
                    }

                    function onInputFocus(){
                        addClass();
                    }

                    function onInputBlur(){
                        if(scope.input.val().trim() === '')
                            removeClass();
                    }

                    function addClass(){
                        element.addClass(inputClass);
                    }

                    function removeClass(){
                        element.removeClass(inputClass);
                    }

                }
            };
        }
    }
})();
<commit_msg>Fixed ng-pattern<commit_after>'use strict';

(function(){
    angular.module('ngInput', [])
    .directive('ngInput', ngInput);

    function ngInput(){

        var uniqueId = 0;
        return {
            restrict: 'E'
          , replace: true
          , templateUrl: 'ng-input.html'
          , scope: {
                theme:       '@'
              , type:        '@'
              , label:       '@'
              , name:        '@'
              , icon:        '@'
              , color:       '@'
              , ngPattern:   '@'
              , ngChange:    '&ngChange'
              , ngRequired:  '='
              , ngMinlength: '='
              , ngMaxlength: '='
              , ngTrim:      '='
              , ngModel:     '='
            }
          , compile: compile
        };

        function compile(){

            return {
                post: function(scope, element, attributes){

                    scope.input = element.find('input');
                    scope.label = element.find('label');

                    //Default options
                    attributes.theme = attributes.theme || 'kaede';
                    attributes.type  = attributes.type  || 'text';
                    attributes.color = attributes.color || '1';

                    var item       = 'input_' + uniqueId++
                      , inputClass = 'input--filled';

                    scope.input.attr('id' , item);
                    scope.label.attr('for', item);

                    onInputChange();
                    scope.input.bind('focus', onInputFocus);
                    scope.input.bind('blur', onInputBlur);

                    //To detect browser autofill
                    scope.input.bind('change', onInputChange);

                    scope.$watch(function(){ return scope.input.val(); }, function(newValue){
                        onInputChange();
                    });

                    function onInputChange(){
                        if(scope.input.val().trim() !== '')
                            addClass();
                    }

                    function onInputFocus(){
                        addClass();
                    }

                    function onInputBlur(){
                        if(scope.input.val().trim() === '')
                            removeClass();
                    }

                    function addClass(){
                        element.addClass(inputClass);
                    }

                    function removeClass(){
                        element.removeClass(inputClass);
                    }

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


<commit_before>// Airbrake JavaScript Notifier
(function() {
    "use strict";

    var NOTICE_XML = '<?xml version="1.0" encoding="UTF-8"?>' +
        '<notice version="2.0">' +
            '<api-key>{key}</api-key>' +
            '<notifier>' +
                '<name>airbrake_js</name>' +
                '<version>0.2.0</version>' +
                '<url>http://airbrake.io</url>' +
            '</notifier>' +
            '<error>' +
                '<class>{exception_class}</class>' +
                '<message>{exception_message}</message>' +
                '<backtrace>{backtrace_lines}</backtrace>' +
            '</error>' +
            '<request>' +
                '<url>{request_url}</url>' +
                '<component>{request_component}</component>' +
                '<action>{request_action}</action>' +
                '{request}' +
            '</request>' +
            '<server-environment>' +
                '<project-root>{project_root}</project-root>' +
                '<environment-name>{environment}</environment-name>' +
            '</server-environment>' +
        '</notice>',
        REQUEST_VARIABLE_GROUP = '<{group_name}>{inner_content}</{group_name}>',
        REQUEST_VARIABLE = '<var key="{key}">{value}</var>',
        BACKTRACE_LINE = '<line method="{method}" file="{file}" number="{number}" />',
        Config,
        Global,
        Util,
        _publicAPI;

    Util = {
        merge: (function() {
            function processProperty (key, dest, src) {
                if (src.hasOwnProperty(key)) {
                    dest[key] = src[key];
                }
            }

            return function() {
                var objects = Array.prototype.slice.call(arguments),
                    obj,
                    key,
                    result = {};

                while (obj = objects.shift()) {
                    for (key in obj) {
                        processProperty(key, result, obj);
                    }
                }

                return result;
            };
        })(),

        escape: function (text) {
            return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
                    .replace(/'/g, ''').replace(/"/g, '"');
        },

        trim: function (text) {
            return text.toString().replace(/^\s+/, '').replace(/\s+$/, '');
        },

        substitute: function (text, data, emptyForUndefinedData) {
            return text.replace(/{([\w_.-]+)}/g, function(match, key) {
                return (key in data) ? data[key] : (emptyForUndefinedData ? '' : match);
            });
        },

        substituteArr: function (text, dataArr, emptyForUndefinedData) {
            var _i = 0, _l = 0,
                returnStr = '';

            for (_i = 0, _l = dataArr.length; _i < _l; _i += 1) {
                returnStr += this.substitute(text, dataArr[_i], emptyForUndefinedData);
            }

            return returnStr;
        },

        processjQueryEventHandlerWrapping: function () {
            if (Config.options.trackJQ === true) {
                Config.jQuery_fn_on_original = Config.jQuery_fn_on_original || jQuery.fn.on;

                jQuery.fn.on = function () {
                    var args = Array.prototype.slice.call(arguments),
                        fnArgIdx = 4;

                    // Search index of function argument
                    while((--fnArgIdx > -1) && (typeof args[fnArgIdx] !== 'function'));

                    // If the function is not found, then subscribe original event handler function
                    if (fnArgIdx === -1) {
                        return Config.jQuery_fn_on_original.apply(this, arguments);
                    }

                    // If the function is found, then subscribe wrapped event handler function
                    args[fnArgIdx] = (function (fnOriginHandler) {
                        return function() {
                            try {
                                fnOriginHandler.apply(this, arguments);
                            } catch (e) {
                                Global.captureException(e);
                            }
                        };
                    })(args[fnArgIdx]);

                    return Config.jQuery_fn_on_original.apply(this, args);
                };
            } else {
                (typeof Config.jQuery_fn_on_original === 'function') && (jQuery.fn.on = Config.jQuery_fn_on_original);
            }
        },

        isjQueryPresent: function () {
            // Currently only 1.7.x version supported
            return (typeof jQuery === 'function') && ('fn' in jQuery) && ('jquery' in jQuery.fn)
                    && (jQuery.fn.jquery.indexOf('1.7') === 0)
        },

        /*
         * Make first letter in a string capital. e.g. 'guessFunctionName' -> 'GuessFunctionName'
         * Is used to generate getter and setter method names.
         */
        capitalizeFirstLetter: function (str) {
            return str[0].toUpperCase() + str.slice(1);
        },

        /*
         * Generate public API from an array of specifically formated objects, e.g.
         *
         * - this will generate 'setEnvironment' and 'getEnvironment' API methods for configObj.xmlData.environment variable:
         * {
         *     variable: 'environment',
         *     namespace: 'xmlData'
         * }
         *
         * - this will define 'method' function as 'captureException' API method
         * {
         *     methodName: 'captureException',
         *     method: (function (...) {...});
         * }
         *
         */
        generatePublicAPI: (function () {
            function _generateSetter (variable, namespace, configObj) {
                return function (value) {
                    configObj[namespace][variable] = value;
                };
            }

            function _generateGetter (variable, namespace, configObj) {
                return function (value) {
                    return configObj[namespace][variable];
                };
            }

            /*
             * publicAPI: array of specifically formated objects
             * configObj: inner configuration object
             */
            return function (publicAPI, configObj) {
                var _i = 0, _m = null, _capitalized = '',
                    returnObj = {};

                for (_i = 0; _i < publicAPI.length; _i += 1) {
                    _m = publicAPI[_i];

                    switch (true) {
                        case (typeof _m.variable !== 'undefined') && (typeof _m.methodName === 'undefined'):
                            _capitalized = Util.capitalizeFirstLetter(_m.variable)
                            returnObj['set' + _capitalized] = _generateSetter(_m.variable, _m.namespace, configObj);
                            returnObj['get' + _capitalized] = _generateGetter(_m.variable, _m.namespace, configObj);

                            break;
                        case (typeof _m.methodName !== 'undefined') && (typeof _m.method !== 'undefined'):
                            returnObj[_m.methodName] = _m.method

                            break;

                        default:
                    }
                }

                return returnObj;
            };
        } ())
    };

    /*
     * The object to store settings. Allocated from the Global (windows scope) so that users can change settings
     * only through the methods, rather than through a direct change of the object fileds. So that we can to handle
     * change settings event (in setter method).
     */
    Config = {
        xmlData: {
            environment: 'environment'
        },

        options: {
            trackJQ: false, // jQuery.fn.jquery
            host: 'api.airbrake.io',
            errorDefaults: {},
            guessFunctionName: false,
            requestType: 'POST',
            outputFormat: 'XML'
        }
    };

    /*
     * The public API definition object. If no 'methodName' and 'method' values specified,
     * getter and setter for 'variable' will be defined.
     */
    _publicAPI = [
        {
            variable: 'environment',
            namespace: 'xmlData'
        }, {
            variable: 'key',
            namespace: 'xmlData'
        }, {
            variable: 'host',
            namespace: 'options'
        }, {
            variable: 'errorDefaults',
            namespace: 'options'
        }, {
            variable: 'guessFunctionName',
            namespace: 'options'
        }, {
            variable: 'requestType',
            namespace: 'options'
        }, {
            variable: 'outputFormat',
            namespace: 'options'
        }, {
            methodName: 'setTrackJQ',
            variable: 'trackJQ',
            namespace: 'options',
            method: (function (value) {
                if (!Util.isjQueryPresent()) {
                    throw Error('Please do not call \'Airbrake.setTrackJQ\' if jQuery does\'t present');
                }

                value = !!value;

                if (Config.options.trackJQ === value) {
                    return;
                }

                Config.options.trackJQ = value;

                Util.processjQueryEventHandlerWrapping();
            })
        }, {
            methodName: 'captureException',
            method: (function (e) {
                new Notifier().notify({
                    message: e.message,
                    stack: e.stack
                });
            })
        }
    ];

    // Share to global scope as Airbrake ("window.Hoptoad" for backward compatibility)
    Global = window.Airbrake = window.Hoptoad = Util.generatePublicAPI(_publicAPI, Config);

    function Notifier() {
        this.options = Util.merge({}, Config.options);
        this.xmlData = Util.merge(this.DEF_XML_DATA, Config.xmlData);
    }

    Notifier.prototype = {
        constructor: Notifier,
        VERSION: '0.2.0',
        ROOT: window.location.protocol + '//' + window.location.host,
        BACKTRACE_MATCHER: /^(.*)\@(.*)\:(\d+)$/,
        backtrace_filters: [/notifier\.js/],
        DEF_XML_DATA: {
            request: {}
        },

        notify: (function () {
            /*
             * Emit GET request via <iframe> element.
             * Data is transmited as a part of query string.
             */
            function _sendGETRequest (url, data) {
                var request = document.createElement('iframe');

                request.style.display = 'none';
                request.src = url + '?data=' + data;

                // When request has been sent, delete iframe
                request.onload = function () {
                    // To avoid infinite progress indicator
                    setTimeout(function() {
                        document.body.removeChild(request);
                    }, 0);
                };

                document.body.appendChild(request);
            }

            /*
             * Cross-domain AJAX POST request.
             *
             * It requires a server setup as described in Cross-Origin Resource Sharing spec:
             * http://www.w3.org/TR/cors/
             */
            function _sendPOSTRequest (url, data) {
                var request = new XMLHttpRequest();

                request.open('POST', url, true);

                request.send(data);
            }

            return function (error) {
                var outputData = '',
                    url = 'http://' + this.options.host + '/notifier_api/v2/notices';

                switch (this.options['outputFormat']) {
                    case 'XML':
                        outputData = escape(this.generateXML(this.generateJSON(error)));

                        break;
                    case 'JSON':
                        outputData = escape(JSON.stringify(this.generateJSON(error)));

                        break;
                    default:
                }

                switch (this.options['requestType']) {
                    case 'POST':
                        _sendPOSTRequest(url, outputData);
                        break;

                    case 'GET':
                        _sendGETRequest(url, outputData);
                        break;

                    default:
                }
            };
        } ()),

        generateJSON: (function () {
            function _generateVariables (inputObj) {
                var key = '', returnArr = [];

                for (key in inputObj) {
                    if (inputObj.hasOwnProperty(key)) {
                        returnArr.push({
                            key: key,
                            value: inputObj[key]
                        });
                    }
                }

                return returnArr;
            }

            function _composeRequestObj (methods, errorObj) {
                var _i = 0,
                    returnObj = {},
                    type = '';

                for (_i = 0; _i < methods.length; _i += 1) {
                    type = methods[_i];
                    if (typeof errorObj[type] !== 'undefined') {
                        returnObj[type] = _generateVariables(errorObj[type]);
                    }
                }

                return returnObj;
            }

            return function (errorWithoutDefaults) {
                var outputData = this.xmlData,
                    error = Util.merge(this.options.errorDefaults, errorWithoutDefaults),

                    component = error.component || '',
                    request_url = (error.url || '' + location.hash),

                    methods = ['cgi-data', 'params', 'session'],
                    _outputData = null;

                _outputData = {
                    request_url: request_url,
                    request_action: (error.action || ''),
                    request_component: component,
                    request: (function () {
                        if (request_url || component) {
                            error['cgi-data'] = error['cgi-data'] || {};
                            error['cgi-data'].HTTP_USER_AGENT = navigator.userAgent;
                            return Util.merge(outputData.request, _composeRequestObj(methods, error));
                        } else {
                            return {}
                        }
                    } ()),

                    project_root: this.ROOT,
                    exception_class: (error.type || 'Error'),
                    exception_message: (error.message || 'Unknown error.'),
                    backtrace_lines: this.generateBacktrace(error)
                }

                outputData = Util.merge(outputData, _outputData);

                return outputData;
            };
        } ()),

        generateXML: (function () {
            function _generateRequestVariableGroups (requestObj) {
                var _group = '',
                    returnStr = '';

                for (_group in requestObj) {
                    if (requestObj.hasOwnProperty(_group)) {
                        returnStr += Util.substitute(REQUEST_VARIABLE_GROUP, {
                            group_name: _group,
                            inner_content: Util.substituteArr(REQUEST_VARIABLE, requestObj[_group], true)
                        }, true);
                    }
                }

                return returnStr;
            }

            return function (JSONdataObj) {
                JSONdataObj.request = _generateRequestVariableGroups(JSONdataObj.request);
                JSONdataObj.backtrace_lines = Util.substituteArr(BACKTRACE_LINE, JSONdataObj.backtrace_lines, true);

                return Util.substitute(NOTICE_XML, JSONdataObj, true);
            };
        } ()),

        generateBacktrace: function (error) {
            var backtrace = [],
                file,
                i,
                matches,
                stacktrace;

            error = error || {};

            if (typeof error.stack !== 'string') {
                try {
                    (0)();
                } catch (e) {
                    error.stack = e.stack;
                }
            }

            stacktrace = this.getStackTrace(error);

            for (i = 0; i < stacktrace.length; i++) {
                matches = stacktrace[i].match(this.BACKTRACE_MATCHER);

                if (matches && this.validBacktraceLine(stacktrace[i])) {
                    file = matches[2].replace(this.ROOT, '[PROJECT_ROOT]');

                    if (i === 0 && matches[2].match(document.location.href)) {
                        // backtrace.push('<line method="" file="internal: " number=""/>');

                        backtrace.push({
                            method: '',
                            file: 'internal: ',
                            number: ''
                        });
                    }

                    // backtrace.push('<line method="' + Util.escape(matches[1]) + '" file="' + Util.escape(file) +
                    //        '" number="' + matches[3] + '" />');

                    backtrace.push({
                        method: matches[1],
                        file: file,
                        number: matches[3]
                    });
                }
            }

            return backtrace;
        },

        getStackTrace: function (error) {
            var i,
                stacktrace = printStackTrace({
                    e: error,
                    guess: this.options.guessFunctionName
                });

            for (i = 0; i < stacktrace.length; i++) {
                if (stacktrace[i].match(/\:\d+$/)) {
                    continue;
                }

                if (stacktrace[i].indexOf('@') === -1) {
                    stacktrace[i] += '@unsupported.js';
                }

                stacktrace[i] += ':0';
            }

            return stacktrace;
        },

        validBacktraceLine: function (line) {
            for (var i = 0; i < this.backtrace_filters.length; i++) {
                if (line.match(this.backtrace_filters[i])) {
                    return false;
                }
            }

            return true;
        },

        generateVariables: function (parameters) {
            var key,
                result = '';

            for (key in parameters) {
                if (parameters.hasOwnProperty(key)) {
                    result += '<var key="' + Util.escape(key) + '">' + Util.escape(parameters[key]) + '</var>';
                }
            }

            return result;
        }
    };

    window.onerror = function (message, file, line) {
        setTimeout(function () {
            new Notifier().notify({
                message: message,
                stack: '()@' + file + ':' + line
            });
        }, 0);

        return true;
    };
})();
<commit_msg>NOTICE_JSON pattern<commit_after>// Airbrake JavaScript Notifier
(function() {
    "use strict";

    var NOTICE_XML = '<?xml version="1.0" encoding="UTF-8"?>' +
        '<notice version="2.0">' +
            '<api-key>{key}</api-key>' +
            '<notifier>' +
                '<name>airbrake_js</name>' +
                '<version>0.2.0</version>' +
                '<url>http://airbrake.io</url>' +
            '</notifier>' +
            '<error>' +
                '<class>{exception_class}</class>' +
                '<message>{exception_message}</message>' +
                '<backtrace>{backtrace_lines}</backtrace>' +
            '</error>' +
            '<request>' +
                '<url>{request_url}</url>' +
                '<component>{request_component}</component>' +
                '<action>{request_action}</action>' +
                '{request}' +
            '</request>' +
            '<server-environment>' +
                '<project-root>{project_root}</project-root>' +
                '<environment-name>{environment}</environment-name>' +
            '</server-environment>' +
        '</notice>',
        REQUEST_VARIABLE_GROUP_XML = '<{group_name}>{inner_content}</{group_name}>',
        REQUEST_VARIABLE_XML = '<var key="{key}">{value}</var>',
        BACKTRACE_LINE_XML = '<line method="{method}" file="{file}" number="{number}" />',
        Config,
        Global,
        Util,
        _publicAPI,

        NOTICE_JSON = {
            "version": "2.0",
            "api-key": "{key}",
            "notifier": {
                "name": "airbrake_js",
                "version": "0.2.0",
                "url": "http://airbrake.io"
            },
            "error": {
                "class": "{exception_class}",
                "message": "{exception_message}",
                "backtrace": []
            },
            "request": {
                "url": "{request_url}",
                "component": "{request_component}",
                "action": "{request_action}"
            },
            "server-environment": {
                "project-root": "{project_root}",
                "environment-name": "{environment}"
            }
        },
        BACKTRACE_LINE_JSON = {
            "method": "{method}",
            "file": "{file}",
            "number": "{number}"
        };

    Util = {
        merge: (function() {
            function processProperty (key, dest, src) {
                if (src.hasOwnProperty(key)) {
                    dest[key] = src[key];
                }
            }

            return function() {
                var objects = Array.prototype.slice.call(arguments),
                    obj,
                    key,
                    result = {};

                while (obj = objects.shift()) {
                    for (key in obj) {
                        processProperty(key, result, obj);
                    }
                }

                return result;
            };
        })(),

        escape: function (text) {
            return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
                    .replace(/'/g, ''').replace(/"/g, '"');
        },

        trim: function (text) {
            return text.toString().replace(/^\s+/, '').replace(/\s+$/, '');
        },

        substitute: function (text, data, emptyForUndefinedData) {
            return text.replace(/{([\w_.-]+)}/g, function(match, key) {
                return (key in data) ? data[key] : (emptyForUndefinedData ? '' : match);
            });
        },

        substituteArr: function (text, dataArr, emptyForUndefinedData) {
            var _i = 0, _l = 0,
                returnStr = '';

            for (_i = 0, _l = dataArr.length; _i < _l; _i += 1) {
                returnStr += this.substitute(text, dataArr[_i], emptyForUndefinedData);
            }

            return returnStr;
        },

        processjQueryEventHandlerWrapping: function () {
            if (Config.options.trackJQ === true) {
                Config.jQuery_fn_on_original = Config.jQuery_fn_on_original || jQuery.fn.on;

                jQuery.fn.on = function () {
                    var args = Array.prototype.slice.call(arguments),
                        fnArgIdx = 4;

                    // Search index of function argument
                    while((--fnArgIdx > -1) && (typeof args[fnArgIdx] !== 'function'));

                    // If the function is not found, then subscribe original event handler function
                    if (fnArgIdx === -1) {
                        return Config.jQuery_fn_on_original.apply(this, arguments);
                    }

                    // If the function is found, then subscribe wrapped event handler function
                    args[fnArgIdx] = (function (fnOriginHandler) {
                        return function() {
                            try {
                                fnOriginHandler.apply(this, arguments);
                            } catch (e) {
                                Global.captureException(e);
                            }
                        };
                    })(args[fnArgIdx]);

                    return Config.jQuery_fn_on_original.apply(this, args);
                };
            } else {
                (typeof Config.jQuery_fn_on_original === 'function') && (jQuery.fn.on = Config.jQuery_fn_on_original);
            }
        },

        isjQueryPresent: function () {
            // Currently only 1.7.x version supported
            return (typeof jQuery === 'function') && ('fn' in jQuery) && ('jquery' in jQuery.fn)
                    && (jQuery.fn.jquery.indexOf('1.7') === 0)
        },

        /*
         * Make first letter in a string capital. e.g. 'guessFunctionName' -> 'GuessFunctionName'
         * Is used to generate getter and setter method names.
         */
        capitalizeFirstLetter: function (str) {
            return str[0].toUpperCase() + str.slice(1);
        },

        /*
         * Generate public API from an array of specifically formated objects, e.g.
         *
         * - this will generate 'setEnvironment' and 'getEnvironment' API methods for configObj.xmlData.environment variable:
         * {
         *     variable: 'environment',
         *     namespace: 'xmlData'
         * }
         *
         * - this will define 'method' function as 'captureException' API method
         * {
         *     methodName: 'captureException',
         *     method: (function (...) {...});
         * }
         *
         */
        generatePublicAPI: (function () {
            function _generateSetter (variable, namespace, configObj) {
                return function (value) {
                    configObj[namespace][variable] = value;
                };
            }

            function _generateGetter (variable, namespace, configObj) {
                return function (value) {
                    return configObj[namespace][variable];
                };
            }

            /*
             * publicAPI: array of specifically formated objects
             * configObj: inner configuration object
             */
            return function (publicAPI, configObj) {
                var _i = 0, _m = null, _capitalized = '',
                    returnObj = {};

                for (_i = 0; _i < publicAPI.length; _i += 1) {
                    _m = publicAPI[_i];

                    switch (true) {
                        case (typeof _m.variable !== 'undefined') && (typeof _m.methodName === 'undefined'):
                            _capitalized = Util.capitalizeFirstLetter(_m.variable)
                            returnObj['set' + _capitalized] = _generateSetter(_m.variable, _m.namespace, configObj);
                            returnObj['get' + _capitalized] = _generateGetter(_m.variable, _m.namespace, configObj);

                            break;
                        case (typeof _m.methodName !== 'undefined') && (typeof _m.method !== 'undefined'):
                            returnObj[_m.methodName] = _m.method

                            break;

                        default:
                    }
                }

                return returnObj;
            };
        } ())
    };

    /*
     * The object to store settings. Allocated from the Global (windows scope) so that users can change settings
     * only through the methods, rather than through a direct change of the object fileds. So that we can to handle
     * change settings event (in setter method).
     */
    Config = {
        xmlData: {
            environment: 'environment'
        },

        options: {
            trackJQ: false, // jQuery.fn.jquery
            host: 'api.airbrake.io',
            errorDefaults: {},
            guessFunctionName: false,
            requestType: 'POST',
            outputFormat: 'XML'
        }
    };

    /*
     * The public API definition object. If no 'methodName' and 'method' values specified,
     * getter and setter for 'variable' will be defined.
     */
    _publicAPI = [
        {
            variable: 'environment',
            namespace: 'xmlData'
        }, {
            variable: 'key',
            namespace: 'xmlData'
        }, {
            variable: 'host',
            namespace: 'options'
        }, {
            variable: 'errorDefaults',
            namespace: 'options'
        }, {
            variable: 'guessFunctionName',
            namespace: 'options'
        }, {
            variable: 'requestType',
            namespace: 'options'
        }, {
            variable: 'outputFormat',
            namespace: 'options'
        }, {
            methodName: 'setTrackJQ',
            variable: 'trackJQ',
            namespace: 'options',
            method: (function (value) {
                if (!Util.isjQueryPresent()) {
                    throw Error('Please do not call \'Airbrake.setTrackJQ\' if jQuery does\'t present');
                }

                value = !!value;

                if (Config.options.trackJQ === value) {
                    return;
                }

                Config.options.trackJQ = value;

                Util.processjQueryEventHandlerWrapping();
            })
        }, {
            methodName: 'captureException',
            method: (function (e) {
                new Notifier().notify({
                    message: e.message,
                    stack: e.stack
                });
            })
        }
    ];

    // Share to global scope as Airbrake ("window.Hoptoad" for backward compatibility)
    Global = window.Airbrake = window.Hoptoad = Util.generatePublicAPI(_publicAPI, Config);

    function Notifier() {
        this.options = Util.merge({}, Config.options);
        this.xmlData = Util.merge(this.DEF_XML_DATA, Config.xmlData);
    }

    Notifier.prototype = {
        constructor: Notifier,
        VERSION: '0.2.0',
        ROOT: window.location.protocol + '//' + window.location.host,
        BACKTRACE_MATCHER: /^(.*)\@(.*)\:(\d+)$/,
        backtrace_filters: [/notifier\.js/],
        DEF_XML_DATA: {
            request: {}
        },

        notify: (function () {
            /*
             * Emit GET request via <iframe> element.
             * Data is transmited as a part of query string.
             */
            function _sendGETRequest (url, data) {
                var request = document.createElement('iframe');

                request.style.display = 'none';
                request.src = url + '?data=' + data;

                // When request has been sent, delete iframe
                request.onload = function () {
                    // To avoid infinite progress indicator
                    setTimeout(function() {
                        document.body.removeChild(request);
                    }, 0);
                };

                document.body.appendChild(request);
            }

            /*
             * Cross-domain AJAX POST request.
             *
             * It requires a server setup as described in Cross-Origin Resource Sharing spec:
             * http://www.w3.org/TR/cors/
             */
            function _sendPOSTRequest (url, data) {
                var request = new XMLHttpRequest();

                request.open('POST', url, true);

                request.send(data);
            }

            return function (error) {
                var outputData = '',
                    url = 'http://' + this.options.host + '/notifier_api/v2/notices';

                switch (this.options['outputFormat']) {
                    case 'XML':
                        outputData = escape(this.generateXML(this.generateJSON(error)));

                        break;
                    case 'JSON':
                        outputData = escape(JSON.stringify(this.generateJSON(error)));

                        break;
                    default:
                }

                switch (this.options['requestType']) {
                    case 'POST':
                        _sendPOSTRequest(url, outputData);
                        break;

                    case 'GET':
                        _sendGETRequest(url, outputData);
                        break;

                    default:
                }
            };
        } ()),

        generateJSON: (function () {
            function _generateVariables (inputObj) {
                var key = '', returnArr = [];

                for (key in inputObj) {
                    if (inputObj.hasOwnProperty(key)) {
                        returnArr.push({
                            key: key,
                            value: inputObj[key]
                        });
                    }
                }

                return returnArr;
            }

            function _composeRequestObj (methods, errorObj) {
                var _i = 0,
                    returnObj = {},
                    type = '';

                for (_i = 0; _i < methods.length; _i += 1) {
                    type = methods[_i];
                    if (typeof errorObj[type] !== 'undefined') {
                        returnObj[type] = _generateVariables(errorObj[type]);
                    }
                }

                return returnObj;
            }

            return function (errorWithoutDefaults) {
                var outputData = this.xmlData,
                    error = Util.merge(this.options.errorDefaults, errorWithoutDefaults),

                    component = error.component || '',
                    request_url = (error.url || '' + location.hash),

                    methods = ['cgi-data', 'params', 'session'],
                    _outputData = null;

                _outputData = {
                    request_url: request_url,
                    request_action: (error.action || ''),
                    request_component: component,
                    request: (function () {
                        if (request_url || component) {
                            error['cgi-data'] = error['cgi-data'] || {};
                            error['cgi-data'].HTTP_USER_AGENT = navigator.userAgent;
                            return Util.merge(outputData.request, _composeRequestObj(methods, error));
                        } else {
                            return {}
                        }
                    } ()),

                    project_root: this.ROOT,
                    exception_class: (error.type || 'Error'),
                    exception_message: (error.message || 'Unknown error.'),
                    backtrace_lines: this.generateBacktrace(error)
                }

                outputData = Util.merge(outputData, _outputData);

                return outputData;
            };
        } ()),

        generateXML: (function () {
            function _generateRequestVariableGroups (requestObj) {
                var _group = '',
                    returnStr = '';

                for (_group in requestObj) {
                    if (requestObj.hasOwnProperty(_group)) {
                        returnStr += Util.substitute(REQUEST_VARIABLE_GROUP_XML, {
                            group_name: _group,
                            inner_content: Util.substituteArr(REQUEST_VARIABLE_XML, requestObj[_group], true)
                        }, true);
                    }
                }

                return returnStr;
            }

            return function (JSONdataObj) {
                JSONdataObj.request = _generateRequestVariableGroups(JSONdataObj.request);
                JSONdataObj.backtrace_lines = Util.substituteArr(BACKTRACE_LINE_XML, JSONdataObj.backtrace_lines, true);

                return Util.substitute(NOTICE_XML, JSONdataObj, true);
            };
        } ()),

        generateBacktrace: function (error) {
            var backtrace = [],
                file,
                i,
                matches,
                stacktrace;

            error = error || {};

            if (typeof error.stack !== 'string') {
                try {
                    (0)();
                } catch (e) {
                    error.stack = e.stack;
                }
            }

            stacktrace = this.getStackTrace(error);

            for (i = 0; i < stacktrace.length; i++) {
                matches = stacktrace[i].match(this.BACKTRACE_MATCHER);

                if (matches && this.validBacktraceLine(stacktrace[i])) {
                    file = matches[2].replace(this.ROOT, '[PROJECT_ROOT]');

                    if (i === 0 && matches[2].match(document.location.href)) {
                        // backtrace.push('<line method="" file="internal: " number=""/>');

                        backtrace.push({
                            method: '',
                            file: 'internal: ',
                            number: ''
                        });
                    }

                    // backtrace.push('<line method="' + Util.escape(matches[1]) + '" file="' + Util.escape(file) +
                    //        '" number="' + matches[3] + '" />');

                    backtrace.push({
                        method: matches[1],
                        file: file,
                        number: matches[3]
                    });
                }
            }

            return backtrace;
        },

        getStackTrace: function (error) {
            var i,
                stacktrace = printStackTrace({
                    e: error,
                    guess: this.options.guessFunctionName
                });

            for (i = 0; i < stacktrace.length; i++) {
                if (stacktrace[i].match(/\:\d+$/)) {
                    continue;
                }

                if (stacktrace[i].indexOf('@') === -1) {
                    stacktrace[i] += '@unsupported.js';
                }

                stacktrace[i] += ':0';
            }

            return stacktrace;
        },

        validBacktraceLine: function (line) {
            for (var i = 0; i < this.backtrace_filters.length; i++) {
                if (line.match(this.backtrace_filters[i])) {
                    return false;
                }
            }

            return true;
        },

        generateVariables: function (parameters) {
            var key,
                result = '';

            for (key in parameters) {
                if (parameters.hasOwnProperty(key)) {
                    result += '<var key="' + Util.escape(key) + '">' + Util.escape(parameters[key]) + '</var>';
                }
            }

            return result;
        }
    };

    window.onerror = function (message, file, line) {
        setTimeout(function () {
            new Notifier().notify({
                message: message,
                stack: '()@' + file + ':' + line
            });
        }, 0);

        return true;
    };
})();
<|endoftext|>