<commit_before>M.wrap('bitbucket/jillix/dms-tree/dev/tree.js', function (require, module, exports) {
var Bind = require("github/jillix/bind");
var Events = require("github/jillix/events");

var LIST_TEMPLATE_ID = "000000000000000000000004";

module.exports = function(config) {

    var DmsTree = this;
    DmsTree.config = config;
    Events.call(DmsTree, config);

    // run the binds
    for (var i = 0; i < DmsTree.config.binds.length; ++i) {
        Bind.call(DmsTree, config.binds[i]);
    }

    var DRAGGABLE = {
        revert: true,
        zIndex: 2500,
        start: function () {
            $(this).effect("highlight", {}, 1000);
            $(this).css("cursor", "move");
        },
        stop: function() {
            $(this).css("cursor", "default");
            $(".stack").css('z-index', '500');
        }
    };

    var storage = {};
    var currentTemplate;

    var ctrlDown = false;

    $(document).on("keydown", function (e) {
        if (e.keyCode === 17) {
            ctrlDown = true;
        }
    }).on("keyup", function (e) {
        if (e.keyCode === 17) {
            ctrlDown = false;
        }
    });

    ///////////////////
    // HANDLERS
    ///////////////////
    $(DmsTree.dom).on("click", ".list", function () {
        var $item = $(this).closest("li");

        DmsTree.removeActive("*");
        DmsTree.setActive($item);

        var filters = (storage[$item.attr("data-id")] || {}).filters || [];

        DmsTree.emit("setFilters", filters, true);

        return false;
    }).on("click", ".folder", function (e) {

        var $item = $(this);
        $item = $item.closest("li");

        if (DmsTree.isLoading($item)) { return; }

        var dataItem = storage[$item.attr("data-id")];

        if (DmsTree.folderIsOpened($item)) {
            DmsTree.closeFolder($item);
            DmsTree.collapse($item);
            return;
        }

        if (e.ctrlKey) {
            DmsTree.setActive($item);
            return;
        }

        DmsTree.startLoading($item);
        var crudObj = {
            t: LIST_TEMPLATE_ID,
            q: {
                parent: dataItem._id,
                template: DmsTree.template
            },
            o: {
                sort: [["type", -1]]
            }
        };

        DmsTree.emit("find", crudObj, function (err, docs) {

            if (err) {
                alert(err);
                return;
            }

            // TODO Just simulating a timeout
            setTimeout(function () {

            DmsTree.expand($item, docs);

            DmsTree.removeActive(".dms-tree *");
            DmsTree.setActive($item);

            DmsTree.stopLoading($item);
            DmsTree.openFolder($item);
            }, 100);
        });
        return false;
    }).on("click", ".all", function () {

        DmsTree.removeActive(".dms-tree *");
        DmsTree.setActive($(this));

        DmsTree.emit("setFilters", [], true);
        return false;
    });

    ///////////////////////////////
    // Generate the tree from items
    ///////////////////////////////
    DmsTree.buildFrom = function (items) {

        var $tree = $(".dms-tree", DmsTree.dom);
        var $typeTemplates = $(".type-templates", DmsTree.dom);

        switch (Object.prototype.toString.call(items)) {
            // object
            case "[object Object]":

                currentTemplate = items;

                var crudObj = {
                    t: LIST_TEMPLATE_ID,
                    q: {
                        parent: { $exists: false },
                        template: DmsTree.template
                    },
                    o: {
                        sort: [["type", -1]]
                    }
                };

                // -> emit -> bind-crud -> callback -> buildFrom Array
                DmsTree.emit("find", crudObj, function (err, docs) {
                    if (err) {
                        alert(err);
                        return;
                    }

                    DmsTree.buildFrom(docs);
                });

                return;
            // array
            case "[object Array]":
                break;
            // other type
            default:
                return;
        }

        $tree.empty();

        // count
        crudObj = {
            t: LIST_TEMPLATE_ID,
            q: {
                type: { $ne: "folder" },
                template: DmsTree.template
            },
            f: { $none: 1, _id: 0 }
        };

        DmsTree.emit("find", crudObj, function (err, docs) {
            if (err) {
                alert(err);
                return;
            }

            var $all = $(".all-template", $typeTemplates)
                .clone()
                .removeClass("all-template");

            $(".all-value", $all).text(docs.length);
            $tree.prepend($all);
        });

        storage = {};

        // an empty array
        if (!items || !items.length) { return; }

        var $itemsToAppend = $("<div>");

        for (var i in items) {
            var item = items[i];
            var $newItem = $("." + item.type + "-template", $typeTemplates).clone();

            storage[item._id] = item;

            $newItem
                .attr("data-id", item._id)
                .removeClass(item.type + "-template")
                .find(".name").text(item.name);

            $itemsToAppend.append($newItem);
        }

        $itemsToAppend.find("li").draggable(DRAGGABLE);

        $itemsToAppend.find(".folder").closest("li").droppable({
            over: DmsTree.dragAndDrop.over,
            out: DmsTree.dragAndDrop.out,
            drop: DmsTree.dragAndDrop.drop
        });

        $tree.append($itemsToAppend);
        $tree.find("li").hide().slideDown();
    };

    ///////////////////////////////
    // Set active class
    ///////////////////////////////
    DmsTree.setActive = function (jQueryElement) {
        jQueryElement = $(jQueryElement, DmsTree.dom);

        if (ctrlDown && jQueryElement.hasClass("active")) {
            jQueryElement.toggleClass("active");
        }
        else {
            jQueryElement.addClass("active");
        }

        DmsTree.emit("selectionChanged", DmsTree.getActive("item"));
    };

    DmsTree.removeActive = function (jQueryElement) {

        if (ctrlDown) { return; }

        jQueryElement = $(jQueryElement, DmsTree.dom);
        jQueryElement.removeClass("active");

        DmsTree.emit("selectionChanged", DmsTree.getActive("item"));
    };

    //////////////////////////////
    // Get active (item/element)
    //////////////////////////////
    DmsTree.getActive = function (item) {

        var jQueryElements = $(DmsTree.dom).find(".active");
        if (!jQueryElements.length) { return undefined; }

        if (item === "item") {
            var activeItems = [];

            if (jQueryElements.length === 1) {
                activeItems.push(storage[jQueryElements.attr("data-id")]);
                return activeItems
            }

            jQueryElements.each(function () {
                activeItems.push(storage[$(this).attr("data-id")]);
            })

            return activeItems;
        }

        return jQueryElements;
    };

    ///////////////////////////////
    // Open-Close functions
    ///////////////////////////////
    DmsTree.openFolder = function (jQueryElement) {

        jQueryElement = $(jQueryElement, DmsTree.dom);
        if (jQueryElement.attr("data-loading")) { return; }

        var $openClose = jQueryElement.find(".open-close");

        $openClose.children().first().addClass("hide");
        $openClose.children().last().removeClass("hide");
        jQueryElement.find(".chevron").toggleClass("icon-chevron-right icon-chevron-down");
    };

    DmsTree.closeFolder = function (jQueryElement) {
        jQueryElement = $(jQueryElement, DmsTree.dom);
        if (jQueryElement.attr("data-loading")) { return; }

        var $openClose = jQueryElement.find(".open-close");

        $openClose.children().last().addClass("hide");
        $openClose.children().first().removeClass("hide");
        jQueryElement.find(".chevron").toggleClass("icon-chevron-right icon-chevron-down");
    };

    DmsTree.folderIsOpened = function (jQueryElement) {
        jQueryElement = $(jQueryElement, DmsTree.dom);
        var $openClose = jQueryElement.find(".open-close");

        if ($openClose.children().last().hasClass("hide")) { return false; }
        return true;
    };

    ///////////////////////////////
    // Loading functions
    ///////////////////////////////
    DmsTree.startLoading = function (jQueryElement) {
        jQueryElement = $(jQueryElement, DmsTree.dom);
        var $loading = jQueryElement.find(".loading-toggle");

        $loading.children().first().removeClass("hide");
        $loading.children().last().addClass("hide");
        jQueryElement.attr("data-loading", "true");
    };

    DmsTree.stopLoading = function (jQueryElement) {
        jQueryElement = $(jQueryElement, DmsTree.dom);
        var $loading = jQueryElement.find(".loading-toggle");

        $loading.children().first().addClass("hide");
        var $last = $loading.children().last();
        $last.removeClass("hide");
        jQueryElement.attr("data-loading", "");
    };

    DmsTree.isLoading = function (jQueryElement) {
        jQueryElement = $(jQueryElement, DmsTree.dom);
        if (jQueryElement.attr("data-loading")) { return true; }
        return false;
    };

    ////////////////////
    // EXPAND / COLLAPSE
    ////////////////////
    DmsTree.expand = function (jQueryElement, docs) {

        jQueryElement = $(jQueryElement, DmsTree.dom);

        if (jQueryElement.next().prop("tagName") === "UL") { return; }

        var $ul = $("<ul>");
        for (var i in docs) {
            var doc = docs[i];
            storage[doc._id] = doc;

            var templClass = doc.type + "-template";
            var $newItem = $("." + templClass)
                            .clone()
                            .removeClass(templClass);

            $newItem.attr("data-id", doc._id)
                    .find(".name").text(doc.name);

            $ul.append($newItem);
        }

        $ul.find("li").draggable(DRAGGABLE);

        $(".folder", $ul).closest("li").droppable({
            over: DmsTree.dragAndDrop.over,
            out: DmsTree.dragAndDrop.out,
            drop: DmsTree.dragAndDrop.drop
        });

        jQueryElement.after($ul);
        $ul.hide().slideDown();
    };

    DmsTree.collapse = function (jQueryElement) {
        jQueryElement = $(jQueryElement, DmsTree.dom);
        var folderContent = jQueryElement.next();
        folderContent.slideUp(function () {
            folderContent.remove();
        });
    };

    //////////////////////
    // NEW LIST/NEW FOLDER
    //////////////////////
    DmsTree.newList = function (listObj, callback) {

        if (!currentTemplate) { return alert("Select a template, first."); }
        callback = callback || function () {};

        DmsTree.emit("getFilters", function (filters) {

            listObj.filters = filters;
            listObj.template = DmsTree.template;

            var crudObj = {
                t: LIST_TEMPLATE_ID,
                d: listObj
            };

            DmsTree.emit("insert", crudObj, function (err, insertedDoc) {

                closeModals();

                if (err) {
                    callback(err);
                    alert(err);
                    return;
                }

                callback(err, insertedDoc);
                DmsTree.emit("refresh");
            });
        });
    };

    //////////////////////
    // EDIT LIST
    //////////////////////
    DmsTree.editList = function (listObj, callback) {

        var activeItem = DmsTree.getActive("item")[0];

        for (var key in listObj) {
            activeItem[key] = listObj[key];
        }

        var newItem = JSON.parse(JSON.stringify(activeItem));
        delete newItem._id;
        newItem.template = DmsTree.template;

        DmsTree.emit("getFilters", function (filters) {

            newItem.filters = filters;

            var crudObj = {
                t: LIST_TEMPLATE_ID,
                q: {
                    _id: activeItem._id,
                    template: DmsTree.template
                },
                d: newItem
            };

            DmsTree.emit("update", crudObj, function (err) {

                closeModals();
                if (err) { return alert(err); }

                DmsTree.emit("refresh");
            });
        });
    };

    //////////////////////
    // Delete LIST
    //////////////////////
    DmsTree.deleteList = function () {

        var activeItems = DmsTree.getActive("item");
        if (!activeItems || !activeItems.length) { return alert("No list selected."); }

        var _ids = [];

        for (var i in activeItems) {
            _ids.push(activeItems[i]._id);
        }

        var finalIds = [];

        // read all subfolders and files
        (function readRecursive(id) {

            // all files and subfolders were scanned
            if (id.constructor === Array && !id.length) {

                // create the crud object
                var crudObj = {
                    t: LIST_TEMPLATE_ID,
                    q: {
                        _id: { $in: finalIds },
                        template: DmsTree.template
                    }
                };

                // and remove them
                DmsTree.emit("remove", crudObj, function (err) {

                    // close modals
                    closeModals();

                    // if an error appeared, show it
                    if (err) { return alert(err); }

                    // and slide up the lists and remove them from UI
                    DmsTree.getActive().slideUp(function () {
                        $(this).remove();
                    });
                });
            }

            // it's an array, read it!
            if (id.constructor === Array) {

                // id is an array, read recursive
                for (var i = 0; i < id.length; ++i) {

                    // get id to push
                    var idToPush = id[i]._id || id[i];

                    // push into the final ids array
                    finalIds.push(idToPush);

                    // read again
                    readRecursive(idToPush);
                }
                return;
            }

            // get the entire list object
            var list = storage[id];

            // it's a list object
            if (list.type === "folder") {

                // make the crud object
                var crudObj = {
                    t: LIST_TEMPLATE_ID,
                    q: {
                        parent: list._id,
                        template: DmsTree.template
                    }
                };

                // get subfolders and files
                DmsTree.emit("find", crudObj, function (err, data) {

                    // if an error appeared, show it
                    if (err) { return alert(err); }

                    // set data in storage
                    for (var i = 0; i < data.length; ++i) {
                        storage[data[i]._id] = data[i];
                    }

                    // read recursive
                    readRecursive(data);
                });
                return;
            }
        })(_ids);
    };

    ////////////////////////////
    // Drag and drop
    ///////////////////////////
    DmsTree.dragAndDrop = {
        over: function () {
            $(this).addClass("over");
        },
        out: function () {
            $(this).removeClass("over");
        },
        drop: function () {
            // remove class over
            $(this).removeClass("over");

            // get jQuery selected items (dropped items)
            var $itemsToMove = DmsTree.getActive() || $(arguments[1].draggable);

            // TODO Is the target a subfolder of dragged folder?

            // check for length
            if (!$itemsToMove || !$itemsToMove.length) { return; }

            // get the jQuery move target
            var $moveTarget = $(this);

            // get data items (from storage) associated with these jQuery elements
            var moveTargetId = storage[$moveTarget.attr("data-id")]._id;

            // build a list of _ids that will be moved in the new folder
            var itemsToMove = [];
            for (var i = 0; i < $itemsToMove.length; ++i) {
                itemsToMove.push($($itemsToMove[i]).attr("data-id"));
            }

            // make the crud object
            var crudObject = {
                // TODO https://github.com/jillix/crud/issues/18
                q: { "_id": { "$in": itemsToMove }, "template": DmsTree.template },
                d: { "$set": { "parent": moveTargetId } },
                t: LIST_TEMPLATE_ID
            };

            // update the items (set parent as the new id)
            DmsTree.emit("update", crudObject, function (err, data) {
                if (err) { return alert(err); }
                DmsTree.emit("refresh");
            });
        }
    };

    //////////////////////
    // Set template
    //////////////////////
    DmsTree.setTemplate = function (e, template) {

        if (e._id) { template = e; }

        DmsTree.template = (template || {})._id;
        DmsTree.emit("templateSet", DmsTree.template);
    };

    function closeModals() {
        try {
            $(".modal").modal("hide");
        } catch (e) {}
    }

    DmsTree.emit("ready", config);
    DmsTree.on("refresh", function () {
        DmsTree.buildFrom(currentTemplate);
    });
};

return module; });
<commit_msg>Set multi: true when updating the lists.<commit_after>M.wrap('bitbucket/jillix/dms-tree/dev/tree.js', function (require, module, exports) {
var Bind = require("github/jillix/bind");
var Events = require("github/jillix/events");

var LIST_TEMPLATE_ID = "000000000000000000000004";

module.exports = function(config) {

    var DmsTree = this;
    DmsTree.config = config;
    Events.call(DmsTree, config);

    // run the binds
    for (var i = 0; i < DmsTree.config.binds.length; ++i) {
        Bind.call(DmsTree, config.binds[i]);
    }

    var DRAGGABLE = {
        revert: true,
        zIndex: 2500,
        start: function () {
            $(this).effect("highlight", {}, 1000);
            $(this).css("cursor", "move");
        },
        stop: function() {
            $(this).css("cursor", "default");
            $(".stack").css('z-index', '500');
        }
    };

    var storage = {};
    var currentTemplate;

    var ctrlDown = false;

    $(document).on("keydown", function (e) {
        if (e.keyCode === 17) {
            ctrlDown = true;
        }
    }).on("keyup", function (e) {
        if (e.keyCode === 17) {
            ctrlDown = false;
        }
    });

    ///////////////////
    // HANDLERS
    ///////////////////
    $(DmsTree.dom).on("click", ".list", function () {
        var $item = $(this).closest("li");

        DmsTree.removeActive("*");
        DmsTree.setActive($item);

        var filters = (storage[$item.attr("data-id")] || {}).filters || [];

        DmsTree.emit("setFilters", filters, true);

        return false;
    }).on("click", ".folder", function (e) {

        var $item = $(this);
        $item = $item.closest("li");

        if (DmsTree.isLoading($item)) { return; }

        var dataItem = storage[$item.attr("data-id")];

        if (DmsTree.folderIsOpened($item)) {
            DmsTree.closeFolder($item);
            DmsTree.collapse($item);
            return;
        }

        if (e.ctrlKey) {
            DmsTree.setActive($item);
            return;
        }

        DmsTree.startLoading($item);
        var crudObj = {
            t: LIST_TEMPLATE_ID,
            q: {
                parent: dataItem._id,
                template: DmsTree.template
            },
            o: {
                sort: [["type", -1]]
            }
        };

        DmsTree.emit("find", crudObj, function (err, docs) {

            if (err) {
                alert(err);
                return;
            }

            // TODO Just simulating a timeout
            setTimeout(function () {

            DmsTree.expand($item, docs);

            DmsTree.removeActive(".dms-tree *");
            DmsTree.setActive($item);

            DmsTree.stopLoading($item);
            DmsTree.openFolder($item);
            }, 100);
        });
        return false;
    }).on("click", ".all", function () {

        DmsTree.removeActive(".dms-tree *");
        DmsTree.setActive($(this));

        DmsTree.emit("setFilters", [], true);
        return false;
    });

    ///////////////////////////////
    // Generate the tree from items
    ///////////////////////////////
    DmsTree.buildFrom = function (items) {

        var $tree = $(".dms-tree", DmsTree.dom);
        var $typeTemplates = $(".type-templates", DmsTree.dom);

        switch (Object.prototype.toString.call(items)) {
            // object
            case "[object Object]":

                currentTemplate = items;

                var crudObj = {
                    t: LIST_TEMPLATE_ID,
                    q: {
                        parent: { $exists: false },
                        template: DmsTree.template
                    },
                    o: {
                        sort: [["type", -1]]
                    }
                };

                // -> emit -> bind-crud -> callback -> buildFrom Array
                DmsTree.emit("find", crudObj, function (err, docs) {
                    if (err) {
                        alert(err);
                        return;
                    }

                    DmsTree.buildFrom(docs);
                });

                return;
            // array
            case "[object Array]":
                break;
            // other type
            default:
                return;
        }

        $tree.empty();

        // count
        crudObj = {
            t: LIST_TEMPLATE_ID,
            q: {
                type: { $ne: "folder" },
                template: DmsTree.template
            },
            f: { $none: 1, _id: 0 }
        };

        DmsTree.emit("find", crudObj, function (err, docs) {
            if (err) {
                alert(err);
                return;
            }

            var $all = $(".all-template", $typeTemplates)
                .clone()
                .removeClass("all-template");

            $(".all-value", $all).text(docs.length);
            $tree.prepend($all);
        });

        storage = {};

        // an empty array
        if (!items || !items.length) { return; }

        var $itemsToAppend = $("<div>");

        for (var i in items) {
            var item = items[i];
            var $newItem = $("." + item.type + "-template", $typeTemplates).clone();

            storage[item._id] = item;

            $newItem
                .attr("data-id", item._id)
                .removeClass(item.type + "-template")
                .find(".name").text(item.name);

            $itemsToAppend.append($newItem);
        }

        $itemsToAppend.find("li").draggable(DRAGGABLE);

        $itemsToAppend.find(".folder").closest("li").droppable({
            over: DmsTree.dragAndDrop.over,
            out: DmsTree.dragAndDrop.out,
            drop: DmsTree.dragAndDrop.drop
        });

        $tree.append($itemsToAppend);
        $tree.find("li").hide().slideDown();
    };

    ///////////////////////////////
    // Set active class
    ///////////////////////////////
    DmsTree.setActive = function (jQueryElement) {
        jQueryElement = $(jQueryElement, DmsTree.dom);

        if (ctrlDown && jQueryElement.hasClass("active")) {
            jQueryElement.toggleClass("active");
        }
        else {
            jQueryElement.addClass("active");
        }

        DmsTree.emit("selectionChanged", DmsTree.getActive("item"));
    };

    DmsTree.removeActive = function (jQueryElement) {

        if (ctrlDown) { return; }

        jQueryElement = $(jQueryElement, DmsTree.dom);
        jQueryElement.removeClass("active");

        DmsTree.emit("selectionChanged", DmsTree.getActive("item"));
    };

    //////////////////////////////
    // Get active (item/element)
    //////////////////////////////
    DmsTree.getActive = function (item) {

        var jQueryElements = $(DmsTree.dom).find(".active");
        if (!jQueryElements.length) { return undefined; }

        if (item === "item") {
            var activeItems = [];

            if (jQueryElements.length === 1) {
                activeItems.push(storage[jQueryElements.attr("data-id")]);
                return activeItems
            }

            jQueryElements.each(function () {
                activeItems.push(storage[$(this).attr("data-id")]);
            })

            return activeItems;
        }

        return jQueryElements;
    };

    ///////////////////////////////
    // Open-Close functions
    ///////////////////////////////
    DmsTree.openFolder = function (jQueryElement) {

        jQueryElement = $(jQueryElement, DmsTree.dom);
        if (jQueryElement.attr("data-loading")) { return; }

        var $openClose = jQueryElement.find(".open-close");

        $openClose.children().first().addClass("hide");
        $openClose.children().last().removeClass("hide");
        jQueryElement.find(".chevron").toggleClass("icon-chevron-right icon-chevron-down");
    };

    DmsTree.closeFolder = function (jQueryElement) {
        jQueryElement = $(jQueryElement, DmsTree.dom);
        if (jQueryElement.attr("data-loading")) { return; }

        var $openClose = jQueryElement.find(".open-close");

        $openClose.children().last().addClass("hide");
        $openClose.children().first().removeClass("hide");
        jQueryElement.find(".chevron").toggleClass("icon-chevron-right icon-chevron-down");
    };

    DmsTree.folderIsOpened = function (jQueryElement) {
        jQueryElement = $(jQueryElement, DmsTree.dom);
        var $openClose = jQueryElement.find(".open-close");

        if ($openClose.children().last().hasClass("hide")) { return false; }
        return true;
    };

    ///////////////////////////////
    // Loading functions
    ///////////////////////////////
    DmsTree.startLoading = function (jQueryElement) {
        jQueryElement = $(jQueryElement, DmsTree.dom);
        var $loading = jQueryElement.find(".loading-toggle");

        $loading.children().first().removeClass("hide");
        $loading.children().last().addClass("hide");
        jQueryElement.attr("data-loading", "true");
    };

    DmsTree.stopLoading = function (jQueryElement) {
        jQueryElement = $(jQueryElement, DmsTree.dom);
        var $loading = jQueryElement.find(".loading-toggle");

        $loading.children().first().addClass("hide");
        var $last = $loading.children().last();
        $last.removeClass("hide");
        jQueryElement.attr("data-loading", "");
    };

    DmsTree.isLoading = function (jQueryElement) {
        jQueryElement = $(jQueryElement, DmsTree.dom);
        if (jQueryElement.attr("data-loading")) { return true; }
        return false;
    };

    ////////////////////
    // EXPAND / COLLAPSE
    ////////////////////
    DmsTree.expand = function (jQueryElement, docs) {

        jQueryElement = $(jQueryElement, DmsTree.dom);

        if (jQueryElement.next().prop("tagName") === "UL") { return; }

        var $ul = $("<ul>");
        for (var i in docs) {
            var doc = docs[i];
            storage[doc._id] = doc;

            var templClass = doc.type + "-template";
            var $newItem = $("." + templClass)
                            .clone()
                            .removeClass(templClass);

            $newItem.attr("data-id", doc._id)
                    .find(".name").text(doc.name);

            $ul.append($newItem);
        }

        $ul.find("li").draggable(DRAGGABLE);

        $(".folder", $ul).closest("li").droppable({
            over: DmsTree.dragAndDrop.over,
            out: DmsTree.dragAndDrop.out,
            drop: DmsTree.dragAndDrop.drop
        });

        jQueryElement.after($ul);
        $ul.hide().slideDown();
    };

    DmsTree.collapse = function (jQueryElement) {
        jQueryElement = $(jQueryElement, DmsTree.dom);
        var folderContent = jQueryElement.next();
        folderContent.slideUp(function () {
            folderContent.remove();
        });
    };

    //////////////////////
    // NEW LIST/NEW FOLDER
    //////////////////////
    DmsTree.newList = function (listObj, callback) {

        if (!currentTemplate) { return alert("Select a template, first."); }
        callback = callback || function () {};

        DmsTree.emit("getFilters", function (filters) {

            listObj.filters = filters;
            listObj.template = DmsTree.template;

            var crudObj = {
                t: LIST_TEMPLATE_ID,
                d: listObj
            };

            DmsTree.emit("insert", crudObj, function (err, insertedDoc) {

                closeModals();

                if (err) {
                    callback(err);
                    alert(err);
                    return;
                }

                callback(err, insertedDoc);
                DmsTree.emit("refresh");
            });
        });
    };

    //////////////////////
    // EDIT LIST
    //////////////////////
    DmsTree.editList = function (listObj, callback) {

        var activeItem = DmsTree.getActive("item")[0];

        for (var key in listObj) {
            activeItem[key] = listObj[key];
        }

        var newItem = JSON.parse(JSON.stringify(activeItem));
        delete newItem._id;
        newItem.template = DmsTree.template;

        DmsTree.emit("getFilters", function (filters) {

            newItem.filters = filters;

            var crudObj = {
                t: LIST_TEMPLATE_ID,
                q: {
                    _id: activeItem._id,
                    template: DmsTree.template
                },
                d: newItem
            };

            DmsTree.emit("update", crudObj, function (err) {

                closeModals();
                if (err) { return alert(err); }

                DmsTree.emit("refresh");
            });
        });
    };

    //////////////////////
    // Delete LIST
    //////////////////////
    DmsTree.deleteList = function () {

        var activeItems = DmsTree.getActive("item");
        if (!activeItems || !activeItems.length) { return alert("No list selected."); }

        var _ids = [];

        for (var i in activeItems) {
            _ids.push(activeItems[i]._id);
        }

        var finalIds = [];

        // read all subfolders and files
        (function readRecursive(id) {

            // all files and subfolders were scanned
            if (id.constructor === Array && !id.length) {

                // create the crud object
                var crudObj = {
                    t: LIST_TEMPLATE_ID,
                    q: {
                        _id: { $in: finalIds },
                        template: DmsTree.template
                    }
                };

                // and remove them
                DmsTree.emit("remove", crudObj, function (err) {

                    // close modals
                    closeModals();

                    // if an error appeared, show it
                    if (err) { return alert(err); }

                    // and slide up the lists and remove them from UI
                    DmsTree.getActive().slideUp(function () {
                        $(this).remove();
                    });
                });
            }

            // it's an array, read it!
            if (id.constructor === Array) {

                // id is an array, read recursive
                for (var i = 0; i < id.length; ++i) {

                    // get id to push
                    var idToPush = id[i]._id || id[i];

                    // push into the final ids array
                    finalIds.push(idToPush);

                    // read again
                    readRecursive(idToPush);
                }
                return;
            }

            // get the entire list object
            var list = storage[id];

            // it's a list object
            if (list.type === "folder") {

                // make the crud object
                var crudObj = {
                    t: LIST_TEMPLATE_ID,
                    q: {
                        parent: list._id,
                        template: DmsTree.template
                    }
                };

                // get subfolders and files
                DmsTree.emit("find", crudObj, function (err, data) {

                    // if an error appeared, show it
                    if (err) { return alert(err); }

                    // set data in storage
                    for (var i = 0; i < data.length; ++i) {
                        storage[data[i]._id] = data[i];
                    }

                    // read recursive
                    readRecursive(data);
                });
                return;
            }
        })(_ids);
    };

    ////////////////////////////
    // Drag and drop
    ///////////////////////////
    DmsTree.dragAndDrop = {
        over: function () {
            $(this).addClass("over");
        },
        out: function () {
            $(this).removeClass("over");
        },
        drop: function () {
            // remove class over
            $(this).removeClass("over");

            // get jQuery selected items (dropped items)
            var $itemsToMove = DmsTree.getActive() || $(arguments[1].draggable);

            // TODO Is the target a subfolder of dragged folder?

            // check for length
            if (!$itemsToMove || !$itemsToMove.length) { return; }

            // get the jQuery move target
            var $moveTarget = $(this);

            // get data items (from storage) associated with these jQuery elements
            var moveTargetId = storage[$moveTarget.attr("data-id")]._id;

            // build a list of _ids that will be moved in the new folder
            var itemsToMove = [];
            for (var i = 0; i < $itemsToMove.length; ++i) {
                itemsToMove.push($($itemsToMove[i]).attr("data-id"));
            }

            // make the crud object
            var crudObject = {
                q: { "_id": { "$in": itemsToMove }, "template": DmsTree.template },
                d: { "$set": { "parent": moveTargetId } },
                o: { "multi": true },
                t: LIST_TEMPLATE_ID
            };

            // update the items (set parent as the new id)
            DmsTree.emit("update", crudObject, function (err, data) {
                if (err) { return alert(err); }
                DmsTree.emit("refresh");
            });
        }
    };

    //////////////////////
    // Set template
    //////////////////////
    DmsTree.setTemplate = function (e, template) {

        if (e._id) { template = e; }

        DmsTree.template = (template || {})._id;
        DmsTree.emit("templateSet", DmsTree.template);
    };

    function closeModals() {
        try {
            $(".modal").modal("hide");
        } catch (e) {}
    }

    DmsTree.emit("ready", config);
    DmsTree.on("refresh", function () {
        DmsTree.buildFrom(currentTemplate);
    });
};

return module; });
<|endoftext|>


<commit_before>'use strict';

var nodeFs = require('fs');
var cache = {};

function File ($matcher, $transformer, file) {
  if (cache[file]) {
    return cache[file];
  }

  cache[file] = this;

  this._matcher = $matcher;
  this._transformer = $transformer;
  this._file = file;
}

File.prototype = {
  get code () {
    return this._code || (this._code = nodeFs.readFileSync(this.path).toString());
  },

  get imports () {
    return this._imports || (this._imports = this._matcher(this.path, this.pre));
  },

  get path () {
    return this._file;
  },

  get post () {
    return this._post || (this._post = this._transformer.transform('post', this.pre, {
      imports: this.imports,
      path: this.path
    }));
  },

  get pre () {
    return this._pre || (this._pre = this._transformer.transform('pre', this.code, {
      path: this.path
    }));
  },

  cached: function () {
    return cache[this.path];
  },

  expire: function () {
    delete cache[this.path];
    delete this._code;
    delete this._dependencies;
    delete this._imports;
    delete this._post;
    delete this._pre;
    return this;
  }
};

module.exports = File;
<commit_msg>Remove unused property.<commit_after>'use strict';

var nodeFs = require('fs');
var cache = {};

function File ($matcher, $transformer, file) {
  if (cache[file]) {
    return cache[file];
  }

  cache[file] = this;
  this._matcher = $matcher;
  this._transformer = $transformer;
  this._file = file;
}

File.prototype = {
  get code () {
    return this._code || (this._code = nodeFs.readFileSync(this.path).toString());
  },

  get imports () {
    return this._imports || (this._imports = this._matcher(this.path, this.pre));
  },

  get path () {
    return this._file;
  },

  get post () {
    return this._post || (this._post = this._transformer.transform('post', this.pre, {
      imports: this.imports,
      path: this.path
    }));
  },

  get pre () {
    return this._pre || (this._pre = this._transformer.transform('pre', this.code, {
      path: this.path
    }));
  },

  cached: function () {
    return cache[this.path];
  },

  expire: function () {
    delete cache[this.path];
    delete this._code;
    delete this._imports;
    delete this._post;
    delete this._pre;
    return this;
  }
};

module.exports = File;
<|endoftext|>


<commit_before>/* global FPSMeter */
// @flow
const PIXI = global.PIXI = require('pixi.js')
const { Howl } = require('howler')
const kbd = require('@dasilvacontin/keyboard')
const io = require('socket.io-client')
const socket = io()

const Ship = require('../common/Ship.js')
const Turn = require('../common/Turn.js')
const Game = require('../common/Game.js')
const GameController = require('./GameController.js')
const PlayerEvent = require('../common/PlayerEvent.js')
const PlayerInput = require('../common/PlayerInput.js')
const C = require('../common/constants.js')
const DEBUG_MODE = Boolean(localStorage.getItem('DEBUG'))
const MUSIC_OFF = Boolean(localStorage.getItem('MUSIC_OFF'))

let playing = false
const musicTracks = [
  'POL-divide-by-zero-short.wav', // +2
  'POL-night-in-motion-short.wav', // +2
  'POL-parallel-fields-short.wav' // +2
  /*
  'POL-catch-me-short.wav', // +1
  'POL-grid-breaking-short.wav', // +1
  'POL-higher-short.wav', // +1
  'POL-humanoid-short.wav', // +1
  'POL-mathrix-short.wav' // +1
  */
]
function getRandomMusicTrack () {
  // night in motion is bae
  const index = Number(localStorage.getItem('trackIndex') || 1)
  const track = musicTracks[index]
  console.log(track)
  return 'sounds/' + track
}
const bgMusic = new Howl({
  urls: [getRandomMusicTrack()],
  buffer: true,
  loop: true
})

const renderer = new PIXI.autoDetectRenderer(
  window.innerWidth,
  window.innerHeight,
  { backgroundColor: C.WALL_COLOR })
document.body.appendChild(renderer.view)

const camera = new PIXI.Container()
const ZOOM = 12

function onResize () {
  const width = window.innerWidth
  const height = window.innerHeight
  renderer.view.style.width = width + 'px'
  renderer.view.style.height = height + 'px'
  renderer.resize(width, height)
}
onResize()
window.addEventListener('resize', onResize)

function padIsKeyDown (gamepad, key) {
  if (gamepad == null) return false
  const { buttons } = gamepad
  const axes = gamepad.axes.map(Math.round)

  switch (key) {
    case kbd.RIGHT_ARROW: return axes[0] === 1 || buttons[15] && buttons[15].pressed
    case kbd.LEFT_ARROW: return axes[0] === -1 || buttons[14] && buttons[14].pressed
    case kbd.UP_ARROW: return buttons[2].pressed
    case 'a': return buttons[4].pressed
    case 'd': return buttons[5].pressed || buttons[6].pressed
    case 's': return buttons[0].pressed
    default: throw new Error('Unsupported key')
  }
}

let sentPing
let ping
// client's Date.now() - server's Date.now()

function sendPing () {
  sentPing = Date.now()
  socket.emit('game:ping')
}
sendPing()

socket.on('game:pong', (serverNow) => {
  ping = (Date.now() - sentPing) / 2
  C.CLIENT_LEAD = Date.now() - (serverNow + ping)
  setTimeout(sendPing, 500)
})

let game, gameController, myShipId
let debugGame, debugGameController
const oldInputs = []

const meter = typeof FPSMeter !== 'undefined'
  ? new FPSMeter()
  : { tickStart: () => {}, tick: () => {} }
function gameLoop () {
  requestAnimationFrame(gameLoop)

  if (game == null) return
  meter.tickStart()

  // get inputs for this turn
  let gamepads = navigator.getGamepads() || []
  game.turn.ships.forEach((ship, i) => {
    if (ship == null || i !== myShipId) return

    const gamepad = gamepads[i]
    const oldInput = oldInputs[i] || new PlayerInput()
    const input = new PlayerInput()

    input.turnL = padIsKeyDown(gamepad, kbd.LEFT_ARROW)
    input.turnR = padIsKeyDown(gamepad, kbd.RIGHT_ARROW)
    input.leanL = padIsKeyDown(gamepad, 'a')
    input.leanR = padIsKeyDown(gamepad, 'd')
    input.gas = padIsKeyDown(gamepad, kbd.UP_ARROW)
    input.boost = padIsKeyDown(gamepad, 's')

    // keyboard
    input.turnL = input.turnL || kbd.isKeyDown(kbd.LEFT_ARROW)
    input.turnR = input.turnR || kbd.isKeyDown(kbd.RIGHT_ARROW)
    input.leanL = input.leanL || kbd.isKeyDown('a')
    input.leanR = input.leanR || kbd.isKeyDown('d')
    input.gas = input.gas || kbd.isKeyDown(kbd.UP_ARROW)
    input.boost = input.boost || kbd.isKeyDown('s')

    // generate PlayerEvents from input - oldInput
    const events = []
    if (input.turnL && !oldInput.turnL) {
      events.push(new PlayerEvent(C.PLAYER_EVENT.TURN_L, input.turnL))
    }
    if (input.turnR && !oldInput.turnR) {
      events.push(new PlayerEvent(C.PLAYER_EVENT.TURN_R, input.turnR))
    }
    if (input.leanL !== oldInput.leanL) {
      events.push(new PlayerEvent(C.PLAYER_EVENT.LEAN_L, input.leanL))
    }
    if (input.leanR !== oldInput.leanR) {
      events.push(new PlayerEvent(C.PLAYER_EVENT.LEAN_R, input.leanR))
    }
    if (input.gas !== oldInput.gas) {
      events.push(new PlayerEvent(C.PLAYER_EVENT.GAS, input.gas))
    }
    if (input.boost !== oldInput.boost) {
      events.push(new PlayerEvent(C.PLAYER_EVENT.BOOST, input.boost))
    }

    if (events.length > 0) {
      game.onPlayerEvents(myShipId, events, game.turnIndex)
      socket.emit('player:events', events, game.turnIndex)
    }
    oldInputs[i] = input
  })

  // perform physics updates
  const currentTurn = game.canTick()
    ? game.tick()
    : game.fakeTick()

  // update ship sprites
  gameController.update(currentTurn)

  // update camera
  const [halfWidth, halfHeight] = [
    window.innerWidth,
    window.innerHeight
  ].map(e => e / 2)

  const player = gameController.ships[myShipId]
  if (player) {
    const { stage } = gameController
    stage.position = new PIXI.Point(
      halfWidth - player.sprite.position.x * stage.scale.x,
      halfHeight - player.sprite.position.y * stage.scale.y)
    camera.pivot = { x: halfWidth, y: halfHeight }
    camera.position = new PIXI.Point(halfWidth, halfHeight)
    camera.rotation += (-player.sprite.rotation - camera.rotation) / 15
  }
  renderer.render(camera)
  meter.tick()
}

// avoid moving the page around
document.addEventListener('keydown', (e: KeyboardEvent) => {
  switch (e.keyCode) {
    case kbd.LEFT_ARROW:
    case kbd.RIGHT_ARROW:
    case kbd.UP_ARROW:
    case kbd.DOWN_ARROW:
    case kbd.SPACE_BAR:
      e.preventDefault()
  }
}, false)

socket.on('game:bootstrap', (data) => {
  const { initialTurn, map, turnsSlice, shipId, lastTick } = data
  myShipId = shipId

  // so that I don't go cray cray with the music
  if (MUSIC_OFF || (DEBUG_MODE && shipId !== 0)) {
    console.log('not playing music')
  } else if (!playing) {
    console.log('play music')
    playing = true
    bgMusic.play()
  }

  game = new Game(map)
  game.turns = []
  let lastTurn
  for (let i = 0; i < turnsSlice.length; ++i) {
    let { ships, events, serverEvents } = turnsSlice[i]
    ships = ships.map((rawShip) => rawShip && new Ship(rawShip))
    const turn = new Turn(ships, events, serverEvents)
    game.turns[initialTurn + i] = turn
    lastTurn = turn
  }
  if (lastTurn == null) return
  game.turn = lastTurn
  game.turnIndex = game.turns.length - 1
  game.lastTick = lastTick
  game.lava = initialTurn
  game.resimulateFrom(initialTurn)

  if (gameController != null) {
    camera.removeChild(gameController.stage)
  }

  gameController = new GameController(game)
  gameController.stage.scale = { x: ZOOM, y: ZOOM }
  camera.addChild(gameController.stage)

  if (DEBUG_MODE) {
    debugGame = new Game(map)
    debugGameController = new GameController(debugGame, true)
    debugGameController.stage.alpha = 0.5
    gameController.stage.addChild(debugGameController.stage)
  }

  console.log('got bootstrapped by server')
})

socket.on('server:event', (event, turnIndex) => {
  game.onServerEvent(event, turnIndex)
})

socket.on('player:events', (shipId, events, turnIndex) => {
  // applying own inputs since game might have been bootstrapped after
  // client issued such commands
  // if (shipId === myShipId) return
  if (game == null) return
  try {
    game.onPlayerEvents(shipId, events, turnIndex)
  } catch (e) {
    if (e instanceof C.InvalidTurnError) {
      console.log('got lost, requesting bootstrap')
      socket.emit('player:lost')
    }
  }
})

socket.on('game:debug', (turn) => {
  if (!DEBUG_MODE) return
  debugGameController.update(turn)
})

gameLoop()
socket.emit('game:join', DEBUG_MODE)

const version = require('../package.json').version
document.getElementById('gameVersion').innerHTML = `v${version}`
<commit_msg>Make client log ping and clientLead every 5s<commit_after>/* global FPSMeter */
// @flow
const PIXI = global.PIXI = require('pixi.js')
const { Howl } = require('howler')
const kbd = require('@dasilvacontin/keyboard')
const io = require('socket.io-client')
const socket = io()

const Ship = require('../common/Ship.js')
const Turn = require('../common/Turn.js')
const Game = require('../common/Game.js')
const GameController = require('./GameController.js')
const PlayerEvent = require('../common/PlayerEvent.js')
const PlayerInput = require('../common/PlayerInput.js')
const C = require('../common/constants.js')
const DEBUG_MODE = Boolean(localStorage.getItem('DEBUG'))
const MUSIC_OFF = Boolean(localStorage.getItem('MUSIC_OFF'))

let playing = false
const musicTracks = [
  'POL-divide-by-zero-short.wav', // +2
  'POL-night-in-motion-short.wav', // +2
  'POL-parallel-fields-short.wav' // +2
  /*
  'POL-catch-me-short.wav', // +1
  'POL-grid-breaking-short.wav', // +1
  'POL-higher-short.wav', // +1
  'POL-humanoid-short.wav', // +1
  'POL-mathrix-short.wav' // +1
  */
]
function getRandomMusicTrack () {
  // night in motion is bae
  const index = Number(localStorage.getItem('trackIndex') || 1)
  const track = musicTracks[index]
  console.log(track)
  return 'sounds/' + track
}
const bgMusic = new Howl({
  urls: [getRandomMusicTrack()],
  buffer: true,
  loop: true
})

const renderer = new PIXI.autoDetectRenderer(
  window.innerWidth,
  window.innerHeight,
  { backgroundColor: C.WALL_COLOR })
document.body.appendChild(renderer.view)

const camera = new PIXI.Container()
const ZOOM = 12

function onResize () {
  const width = window.innerWidth
  const height = window.innerHeight
  renderer.view.style.width = width + 'px'
  renderer.view.style.height = height + 'px'
  renderer.resize(width, height)
}
onResize()
window.addEventListener('resize', onResize)

function padIsKeyDown (gamepad, key) {
  if (gamepad == null) return false
  const { buttons } = gamepad
  const axes = gamepad.axes.map(Math.round)

  switch (key) {
    case kbd.RIGHT_ARROW: return axes[0] === 1 || buttons[15] && buttons[15].pressed
    case kbd.LEFT_ARROW: return axes[0] === -1 || buttons[14] && buttons[14].pressed
    case kbd.UP_ARROW: return buttons[2].pressed
    case 'a': return buttons[4].pressed
    case 'd': return buttons[5].pressed || buttons[6].pressed
    case 's': return buttons[0].pressed
    default: throw new Error('Unsupported key')
  }
}

let sentPing
let ping
// client's Date.now() - server's Date.now()

function sendPing () {
  sentPing = Date.now()
  socket.emit('game:ping')
}
sendPing()

socket.on('game:pong', (serverNow) => {
  ping = (Date.now() - sentPing) / 2
  C.CLIENT_LEAD = Date.now() - (serverNow + ping)
  setTimeout(sendPing, 500)
})
setInterval(() => {
  console.log({ ping, clientLead: C.CLIENT_LEAD })
}, 5 * 1000)

let game, gameController, myShipId
let debugGame, debugGameController
const oldInputs = []

const meter = typeof FPSMeter !== 'undefined'
  ? new FPSMeter()
  : { tickStart: () => {}, tick: () => {} }
function gameLoop () {
  requestAnimationFrame(gameLoop)

  if (game == null) return
  meter.tickStart()

  // get inputs for this turn
  let gamepads = navigator.getGamepads() || []
  game.turn.ships.forEach((ship, i) => {
    if (ship == null || i !== myShipId) return

    const gamepad = gamepads[i]
    const oldInput = oldInputs[i] || new PlayerInput()
    const input = new PlayerInput()

    input.turnL = padIsKeyDown(gamepad, kbd.LEFT_ARROW)
    input.turnR = padIsKeyDown(gamepad, kbd.RIGHT_ARROW)
    input.leanL = padIsKeyDown(gamepad, 'a')
    input.leanR = padIsKeyDown(gamepad, 'd')
    input.gas = padIsKeyDown(gamepad, kbd.UP_ARROW)
    input.boost = padIsKeyDown(gamepad, 's')

    // keyboard
    input.turnL = input.turnL || kbd.isKeyDown(kbd.LEFT_ARROW)
    input.turnR = input.turnR || kbd.isKeyDown(kbd.RIGHT_ARROW)
    input.leanL = input.leanL || kbd.isKeyDown('a')
    input.leanR = input.leanR || kbd.isKeyDown('d')
    input.gas = input.gas || kbd.isKeyDown(kbd.UP_ARROW)
    input.boost = input.boost || kbd.isKeyDown('s')

    // generate PlayerEvents from input - oldInput
    const events = []
    if (input.turnL && !oldInput.turnL) {
      events.push(new PlayerEvent(C.PLAYER_EVENT.TURN_L, input.turnL))
    }
    if (input.turnR && !oldInput.turnR) {
      events.push(new PlayerEvent(C.PLAYER_EVENT.TURN_R, input.turnR))
    }
    if (input.leanL !== oldInput.leanL) {
      events.push(new PlayerEvent(C.PLAYER_EVENT.LEAN_L, input.leanL))
    }
    if (input.leanR !== oldInput.leanR) {
      events.push(new PlayerEvent(C.PLAYER_EVENT.LEAN_R, input.leanR))
    }
    if (input.gas !== oldInput.gas) {
      events.push(new PlayerEvent(C.PLAYER_EVENT.GAS, input.gas))
    }
    if (input.boost !== oldInput.boost) {
      events.push(new PlayerEvent(C.PLAYER_EVENT.BOOST, input.boost))
    }

    if (events.length > 0) {
      game.onPlayerEvents(myShipId, events, game.turnIndex)
      socket.emit('player:events', events, game.turnIndex)
    }
    oldInputs[i] = input
  })

  // perform physics updates
  const currentTurn = game.canTick()
    ? game.tick()
    : game.fakeTick()

  // update ship sprites
  gameController.update(currentTurn)

  // update camera
  const [halfWidth, halfHeight] = [
    window.innerWidth,
    window.innerHeight
  ].map(e => e / 2)

  const player = gameController.ships[myShipId]
  if (player) {
    const { stage } = gameController
    stage.position = new PIXI.Point(
      halfWidth - player.sprite.position.x * stage.scale.x,
      halfHeight - player.sprite.position.y * stage.scale.y)
    camera.pivot = { x: halfWidth, y: halfHeight }
    camera.position = new PIXI.Point(halfWidth, halfHeight)
    camera.rotation += (-player.sprite.rotation - camera.rotation) / 15
  }
  renderer.render(camera)
  meter.tick()
}

// avoid moving the page around
document.addEventListener('keydown', (e: KeyboardEvent) => {
  switch (e.keyCode) {
    case kbd.LEFT_ARROW:
    case kbd.RIGHT_ARROW:
    case kbd.UP_ARROW:
    case kbd.DOWN_ARROW:
    case kbd.SPACE_BAR:
      e.preventDefault()
  }
}, false)

socket.on('game:bootstrap', (data) => {
  const { initialTurn, map, turnsSlice, shipId, lastTick } = data
  myShipId = shipId

  // so that I don't go cray cray with the music
  if (MUSIC_OFF || (DEBUG_MODE && shipId !== 0)) {
    console.log('not playing music')
  } else if (!playing) {
    console.log('play music')
    playing = true
    bgMusic.play()
  }

  game = new Game(map)
  game.turns = []
  let lastTurn
  for (let i = 0; i < turnsSlice.length; ++i) {
    let { ships, events, serverEvents } = turnsSlice[i]
    ships = ships.map((rawShip) => rawShip && new Ship(rawShip))
    const turn = new Turn(ships, events, serverEvents)
    game.turns[initialTurn + i] = turn
    lastTurn = turn
  }
  if (lastTurn == null) return
  game.turn = lastTurn
  game.turnIndex = game.turns.length - 1
  game.lastTick = lastTick
  game.lava = initialTurn
  game.resimulateFrom(initialTurn)

  if (gameController != null) {
    camera.removeChild(gameController.stage)
  }

  gameController = new GameController(game)
  gameController.stage.scale = { x: ZOOM, y: ZOOM }
  camera.addChild(gameController.stage)

  if (DEBUG_MODE) {
    debugGame = new Game(map)
    debugGameController = new GameController(debugGame, true)
    debugGameController.stage.alpha = 0.5
    gameController.stage.addChild(debugGameController.stage)
  }

  console.log('got bootstrapped by server')
})

socket.on('server:event', (event, turnIndex) => {
  game.onServerEvent(event, turnIndex)
})

socket.on('player:events', (shipId, events, turnIndex) => {
  // applying own inputs since game might have been bootstrapped after
  // client issued such commands
  // if (shipId === myShipId) return
  if (game == null) return
  try {
    game.onPlayerEvents(shipId, events, turnIndex)
  } catch (e) {
    if (e instanceof C.InvalidTurnError) {
      console.log('got lost, requesting bootstrap')
      socket.emit('player:lost')
    }
  }
})

socket.on('game:debug', (turn) => {
  if (!DEBUG_MODE) return
  debugGameController.update(turn)
})

gameLoop()
socket.emit('game:join', DEBUG_MODE)

const version = require('../package.json').version
document.getElementById('gameVersion').innerHTML = `v${version}`
<|endoftext|>


<commit_before>/**
 * XHRFormSubmitter JavaScript Library
 * ===================================
 * A lightweigt JavaScript library that do one thing: submiting via XHR
 * requests. This doesn't depend on jQuery!
 *
 * For more information, see README file.
 * Report bugs in the GitHub repository.
 *
 * Developed with love by Ionică Bizău.
 * Licensed under the MIT license:
 *
 * --------------------
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 Ionică Bizău
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
(function (window) {

    var XHRFormSubmitter = function (options) {
        var self = this;
        self._form = options.form;

        /**
         * getFormData
         * Creates an object containing the names and the values of the form
         * inputs.
         *
         * @name getFormData
         * @function
         * @return {Object} The form data object (name: value)
         */
        self.getFormData = function (stringified) {

            var allInputs = self._form.querySelectorAll("[name]")
              , formData = {}
              ;

            for (var i = 0; i < allInputs.length; ++i) {

                var cEl = allInputs[i]
                  , type = cEl.getAttribute("type")
                  , name = cEl.getAttribute("name")
                  , value = null
                  ;

                switch (type) {
                    case "button":
                    case "text":
                    case "url":
                    case "date":
                    case "color":
                    case "datetime":
                    case "datetime-local":
                    case "email":
                    case "file":
                    case "hidden":
                    case "month":
                    case "number":
                    case "password":
                    case "range":
                    case "search":
                    case "submit":
                    case "tel":
                    case "time":
                    case "week":
                        value = cEl.value;
                        break;
                    case "checkbox":
                        value = cEl.checked;
                        break;
                    case "image":
                        value = cEl.getAttribute("src");
                        break;
                    case "radio":
                        value = document.querySelector(
                            "input[name='" + name + "']:checked"
                        ).value;
                        break;
                    default:
                        console.warn("Unhandled type: ", type);
                        break;
                }

                formData[name] = value;
            }

            if (!stringified) {
                return formData;
            }

            stringified = "";
            for (var key in formData) {
                stringified
                    += encodeURIComponent(key) + "="
                    + encodeURIComponent(formData[key]) + "&";
            }
            stringified = stringified.substr(0, stringified.length - 1);
            return stringified;
        };

        /**
         * submit
         * Submit the form using a XMLHttpRequest.
         *
         * @name submit
         * @function
         * @param {Object} submitOps The submit options
         * @return {XMLHttpRequest} The XMLHttpRequest instance
         */
        self.submit = function (submitOps, callback) {

            if (typeof submitOps === "function") {
                callback = submitOps;
                submitOps = undefined;
            }

            submitOps = Object(submitOps);
            callback = callback || function () {};

            var xhr = new XMLHttpRequest()
              , form = self._form
              , url = submitOps.actionUrl || form.getAttribute("action")
              , method = submitOps.method || form.getAttribute("method")
              , data = self.getFormData(true)
              ;

            xhr.open(method, url);
            xhr.setRequestHeader("content-type", "text/plain; charset=utf-8");
            xhr.onreadystatechange = function () {
                if (xhr.readyState !== 4) { return; }
                if (xhr.status >= 400) {
                    return callback(xhr.responseText, null);
                }

                callback(null, xhr.responseText);
            };

            xhr.send(data);
            return xhr;
        };
    };
    window.XHRFormSubmitter = XHRFormSubmitter;
})(window);
<commit_msg>Fixed #2. Disable and enable inputs<commit_after>/**
 * XHRFormSubmitter JavaScript Library
 * ===================================
 * A lightweigt JavaScript library that do one thing: submiting via XHR
 * requests. This doesn't depend on jQuery!
 *
 * For more information, see README file.
 * Report bugs in the GitHub repository.
 *
 * Developed with love by Ionică Bizău.
 * Licensed under the MIT license:
 *
 * --------------------
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 Ionică Bizău
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
(function (window) {

    // All inputs
    var INPUTS = [
        "select"
      , "textarea"
      , "input"
      , "button"
    ];

    var XHRFormSubmitter = function (options) {
        var self = this;
        self._form = options.form;

        /**
         * getFormData
         * Creates an object containing the names and the values of the form
         * inputs.
         *
         * @name getFormData
         * @function
         * @return {Object} The form data object (name: value)
         */
        self.getFormData = function (stringified) {

            var allInputs = self._form.querySelectorAll("[name]")
              , formData = {}
              ;

            for (var i = 0; i < allInputs.length; ++i) {

                var cEl = allInputs[i]
                  , type = cEl.getAttribute("type")
                  , name = cEl.getAttribute("name")
                  , value = null
                  ;

                switch (type) {
                    case "button":
                    case "text":
                    case "url":
                    case "date":
                    case "color":
                    case "datetime":
                    case "datetime-local":
                    case "email":
                    case "file":
                    case "hidden":
                    case "month":
                    case "number":
                    case "password":
                    case "range":
                    case "search":
                    case "submit":
                    case "tel":
                    case "time":
                    case "week":
                        value = cEl.value;
                        break;
                    case "checkbox":
                        value = cEl.checked;
                        break;
                    case "image":
                        value = cEl.getAttribute("src");
                        break;
                    case "radio":
                        value = document.querySelector(
                            "input[name='" + name + "']:checked"
                        ).value;
                        break;
                    default:
                        console.warn("Unhandled type: ", type);
                        break;
                }

                formData[name] = value;
            }

            if (!stringified) {
                return formData;
            }

            stringified = "";
            for (var key in formData) {
                stringified
                    += encodeURIComponent(key) + "="
                    + encodeURIComponent(formData[key]) + "&";
            }
            stringified = stringified.substr(0, stringified.length - 1);
            return stringified;
        };

        /**
         * disableInputs
         * This function disables the inputs. It's cool to be called before
         * submiting the form.
         *
         * @name disableInputs
         * @function
         * @return {Object} The XHRFormSubmitter instance
         */
        self.disableInputs = function () {
            var allInputs = self._form.querySelectorAll(INPUTS.join(","));
            for (var i = 0; i < allInputs.length; ++i) {
                allInputs[i].setAttribute("disabled", "");
            }
            return self;
        };

        /**
         * enableInputs
         * This function enables the inputs. It's cool to be called after
         * receiving the response from server.
         *
         * @name enableInputs
         * @function
         * @return {Object} The XHRFormSubmitter instance
         */
        self.enableInputs = function () {
            var allInputs = self._form.querySelectorAll(INPUTS.join(","));
            for (var i = 0; i < allInputs.length; ++i) {
                allInputs[i].removeAttribute("disabled");
            }
            return self;
        };

        /**
         * submit
         * Submit the form using a XMLHttpRequest.
         *
         * @name submit
         * @function
         * @param {Object} submitOps The submit options
         * @return {Object} The XHRFormSubmitter instance
         */
        self.submit = function (submitOps, callback) {

            if (typeof submitOps === "function") {
                callback = submitOps;
                submitOps = undefined;
            }

            submitOps = Object(submitOps);
            callback = callback || function () {};

            var xhr = new XMLHttpRequest()
              , form = self._form
              , url = submitOps.actionUrl || form.getAttribute("action")
              , method = submitOps.method || form.getAttribute("method")
              , data = self.getFormData(true)
              ;

            xhr.open(method, url);
            xhr.setRequestHeader("content-type", "text/plain; charset=utf-8");
            xhr.onreadystatechange = function () {
                if (xhr.readyState !== 4) { return; }
                if (options.disableOnSubmit === true) {
                    self.enableInputs();
                }
                if (xhr.status >= 400) {
                    return callback(xhr.responseText, null);
                }

                callback(null, xhr.responseText);
            };

            if (options.disableOnSubmit === true) {
                self.disableInputs();
            }

            xhr.send(data);
            return self;
        };
    };
    window.XHRFormSubmitter = XHRFormSubmitter;
})(window);
<|endoftext|>


<commit_before>const Definition = Jymfony.Component.DependencyInjection.Definition;

/**
 * @memberOf Jymfony.Component.DependencyInjection
 */
class ChildDefinition extends Definition {
    /**
     * Constructor.
     *
     * @param {Jymfony.Component.DependencyInjection.Definition} parent
     */
    __construct(parent) {
        super.__construct();

        /**
         * @type {Jymfony.Component.DependencyInjection.Definition}
         *
         * @private
         */
        this._parent = parent;

        /**
         * @type {Object}
         *
         * @private
         */
        this._replacedArguments = {};
    }

    /**
     * Gets the extended definition.
     *
     * @returns {Jymfony.Component.DependencyInjection.Definition}
     */
    getParent() {
        return this._parent;
    }

    /**
     * @inheritdoc
     */
    setArguments(args) {
        for (const [ k, v ] of __jymfony.getEntries(args)) {
            this.replaceArgument(k, v);
        }

        return this;
    }

    /**
     * @inheritdoc
     */
    getArguments() {
        const args = [ ...this._arguments ];
        for (const [ k, v ] of __jymfony.getEntries(this._replacedArguments)) {
            if (k >= args.length) {
                continue;
            }

            args[k] = v;
        }

        return __jymfony.deepClone(args);
    }

    /**
     * @inheritdoc
     */
    getArgument(index) {
        if (this._replacedArguments.hasOwnProperty(index)) {
            return this._replacedArguments(index);
        }

        return super.getArgument(index);
    }

    /**
     * @inheritdoc
     */
    replaceArgument(index, argument) {
        this._replacedArguments[index] = argument;

        return this;
    }
}

module.exports = ChildDefinition;
<commit_msg>[DependencyInjection] accept any valid id in ChildDefinition constructor<commit_after>const Container = Jymfony.Component.DependencyInjection.Container;
const Definition = Jymfony.Component.DependencyInjection.Definition;

/**
 * @memberOf Jymfony.Component.DependencyInjection
 */
class ChildDefinition extends Definition {
    /**
     * Constructor.
     *
     * @param {*} parent
     */
    __construct(parent) {
        super.__construct();

        /**
         * @type {string}
         *
         * @private
         */
        this._parent = Container.normalizeId(parent);

        /**
         * @type {Object}
         *
         * @private
         */
        this._replacedArguments = {};
    }

    /**
     * Gets the extended definition.
     *
     * @returns {Jymfony.Component.DependencyInjection.Definition}
     */
    getParent() {
        return this._parent;
    }

    /**
     * @inheritdoc
     */
    setArguments(args) {
        for (const [ k, v ] of __jymfony.getEntries(args)) {
            this.replaceArgument(k, v);
        }

        return this;
    }

    /**
     * @inheritdoc
     */
    getArguments() {
        const args = [ ...this._arguments ];
        for (const [ k, v ] of __jymfony.getEntries(this._replacedArguments)) {
            if (k >= args.length) {
                continue;
            }

            args[k] = v;
        }

        return __jymfony.deepClone(args);
    }

    /**
     * @inheritdoc
     */
    getArgument(index) {
        if (this._replacedArguments.hasOwnProperty(index)) {
            return this._replacedArguments(index);
        }

        return super.getArgument(index);
    }

    /**
     * @inheritdoc
     */
    replaceArgument(index, argument) {
        this._replacedArguments[index] = argument;

        return this;
    }
}

module.exports = ChildDefinition;
<|endoftext|>


<commit_before>const config = require('config'); // Explicit import is necessary here for command aliases

const Command = require('../Command');
const RoleDeterminer = require('../Helper/RoleDeterminer');
const { generateRoomName, parseRoomType, convertRoom, archiveRoom } = require('../Helper/SupportRoomHelper');

const messages = {
    'help': 'You can use `!support <type> <IGN>` to create a room where you can contact the staff team for support in private, where `<IGN>` is your Minecraft username and `<type>` is one of the following:{types}',
    'type': '\n- `{type}`: {description}',
    'unknownType': 'Unknown support type `{type}`.\n\n{help}',
    'roomCreated': 'I\'ve created a support room of type `{type}` and added you to it!',
    'roomConverted': 'I\'ve converted this room\'s support type from `{oldType}` to `{newType}`.',
    'roomClosing': 'Closing this support room...',
    'roomClosed': '{user} closed support room `{room}`',
    'supportType': 'Support type',
    'roomParticipants': 'Participants',
    'noPermission': 'Sorry, it appears that you do not have access to perform this action.',
    'noIgnProvided': 'It looks like you forgot to add your Minecraft username to the support command. Usage: `!{command} <IGN>`, where `<IGN>` refers to your Minecraft username.',
    'noIgnProvidedWithType': 'It looks like you forgot to add your Minecraft username to the support command. Usage: `!{command} {type} <IGN>`, where `<IGN>` refers to your Minecraft username.',
    'notASupportRoom': 'You can only execute this command in a support room.',
    'roomCreationError': 'Sorry, it appears that I have encountered an error whilst trying to create a room. Check back later!',
    'supportWelcome': 'Hello {user} (IGN: {IGN})! I have created this room for you to ask a question relating to `{type}` to the staff team in private. Before asking your question, please read through the support rules below.',
    'supportRules': '**• Do not ping staff**\n' +
        'Members of our team (believe it or not) have lives of their own, and being disturbed whilst working or while in class can be very frustrating, so please refrain from pinging any staff without very good cause.  If there\'s a forum thread you can use, it is not good cause to ping staff.\n' +
        '\n' +
        '**• Don\'t expect live support on this server**\n' +
        'Shotbow does not offer live support. Should you be in luck, and a member of staff is actively chatting at the time you have a matter to raise, you may of course ask them for help. Under no conditions should you ping staff members individually for help.\n' +
        '\n' +
        '**• Cheaters**\n' +
        'If someone is cheating use the /report command.  This is the best way to get the attention of staff currently active in patrol duty.  Hackusations are not allowed, and staff should not be pinged to deal with cheaters.\n' +
        '\n' +
        'If you have a question or issue that doesn\'t require the attention of a staff member, it\'s probably better to ask politely in #shotbow without a ping and someone will gladly help you get to where you need to be.\n',
    'supportMessageReceived': 'Thank you for your message! If you have more information, you can continue sending messages in this channel. A staff member will get to you soon. ({roles})'
};

module.exports = Command.extend({
    commandName: 'support',
    commandAliases: Object.keys(config.support.types).map(typeKey => `${typeKey}support`),
    processMessage: async function (message, tokens) {
        const commandParameters = this.extractParameters(tokens);

        /* Handle the case of no or invalid arguments provided */
        if (!commandParameters) {
            const types = Object.keys(this.config.support.types).map(type => this.i18n.__mf(messages.type, {
                type,
                description: this.config.support.types[type].description
            }));
            return message.channel.send(this.i18n.__mf(messages.help, {types}));
        }

        /* Handle the unique cases of "close" and "convert" */
        tokens.shift();
        if (tokens.length > 0
            && commandParameters.command === 'support'
            && (tokens[0].toLowerCase() === 'close' || tokens[0].toLowerCase() === 'convert')) {
            const typeKey = this.parseRoomType(message.channel.name);
            if (!typeKey) {
                return message.channel.send(this.i18n.__mf(messages.notASupportRoom));
            }

            if (!RoleDeterminer.hasOneOfRoles(message.member, this.config.support.types[typeKey].roles)) {
                return message.channel.send(this.i18n.__mf(messages.noPermission));
            }

            if (tokens[0].toLowerCase() === 'close') {
                return await archiveRoom(message, this.i18n, this.discordClient);
            }
            if (tokens[0].toLowerCase() === 'convert') {
                return await convertRoom(message, tokens, this.i18n, this.discordClient);
            }
        }

        /* Get the support type */
        const typeKey = commandParameters.supportType;
        const type = this.config.support.types[typeKey];
        if (!type) {
            const types = Object.keys(this.config.support.types).map(type => this.i18n.__mf(messages.type, {
                type,
                description: this.config.support.types[type].description
            }));
            const helpText = this.i18n.__mf(messages.help, {types});
            return message.channel.send(this.i18n.__mf(messages.unknownType, {type: typeKey, help: helpText}));
        }

        /* Handle the case of no username provided */
        if (!commandParameters.ign) {
            if (commandParameters.command === 'support') {
                return message.channel.send(this.i18n.__mf(messages.noIgnProvidedWithType, {
                    command: commandParameters.command,
                    type: commandParameters.supportType
                }));
            } else {
                return message.channel.send(this.i18n.__mf(messages.noIgnProvided, {
                    command: commandParameters.command
                }));
            }
        }

        /* Create the channel and add the correct people to it */
        const supportCategory = message.guild.channels.cache.get(this.config.support.category);
        const supportChannel = await message.guild.channels.create(generateRoomName(typeKey), {
            type: 'text',
            parent: supportCategory,
        })
            .then(channel => channel.lockPermissions())
            .then(channel => channel.updateOverwrite(message.author.id, {
                ADD_REACTIONS: true,
                ATTACH_FILES: true,
                EMBED_LINKS: true,
                READ_MESSAGE_HISTORY: true,
                SEND_MESSAGES: true,
                USE_EXTERNAL_EMOJIS: true,
                SEND_TTS_MESSAGES: false,
                VIEW_CHANNEL: true
            }))
            .then(async channel => {
                for (const supportRoleId of type.roles) {
                    await channel.updateOverwrite(supportRoleId, {
                        ADD_REACTIONS: true,
                        ATTACH_FILES: true,
                        EMBED_LINKS: true,
                        READ_MESSAGE_HISTORY: true,
                        SEND_MESSAGES: true,
                        USE_EXTERNAL_EMOJIS: true,
                        SEND_TTS_MESSAGES: true,
                        VIEW_CHANNEL: true
                    });
                }
                return channel;
            })
            .catch(() => {
                /* If something went wrong with room creation, we delete it (if it was created) and notify the user */
                supportChannel.delete().catch(() => {});
                return message.channel.send(this.i18n.__mf(messages.roomCreationError));
            });

        const welcomeMessage = await supportChannel.send(this.i18n.__mf(messages.supportWelcome, {
            user: `<@${message.author.id}>`,
            IGN: commandParameters.ign,
            type: typeKey
        }));

        /* Add the collector to deal with post-creation events */
        this.addCollector(supportChannel, message.member, welcomeMessage);

        await supportChannel.send(this.i18n.__mf(messages.supportRules));
        return message.channel.send(this.i18n.__mf(messages.roomCreated, {type: typeKey}));
    },
    addCollector: function (supportChannel, creator, welcomeMessage) {
        const collectorFilter = (message) => message.member.id === creator.id;

        const automaticDelete = this.config.support.automaticDeletion;
        let collector;
        if (automaticDelete < 0) {
            collector = supportChannel.createMessageCollector(collectorFilter);
        } else {
            collector = supportChannel.createMessageCollector(collectorFilter,
                {time: automaticDelete});
            collector.on('end', async (_, reason) => {
                if (reason === 'time') {
                    await archiveRoom(welcomeMessage, this.i18n, this.discordClient);
                }
            });
        }

        collector.once('collect', (message) => {
            const supportType = parseRoomType(supportChannel.name);
            const supportRoles = this.config.support.types[supportType].roles
                .map(supportRole => `<@&${supportRole}>`)
                .join(', ');

            message.channel.send(this.i18n.__mf(messages.supportMessageReceived, {roles: supportRoles}));
            collector.stop();
        });
    },
    extractParameters: function (tokens) {
        const clonedTokens = [...tokens];
        /* Extract the support type */
        const command = clonedTokens[0].substr(this.commandPrefix.length, clonedTokens[0].length);
        clonedTokens.shift();
        let supportType;
        if (command === 'support') {
            if (clonedTokens.length === 0) {
                return null;
            } else {
                supportType = clonedTokens[0];
                clonedTokens.shift();
            }
        } else {
            supportType = command.split('support')[0];
        }

        /* Extract the IGN */
        if (clonedTokens.length === 0) {
            return {
                command,
                supportType: supportType.toLowerCase(),
                ign: null
            }
        }
        const ign = clonedTokens[0];

        /* Return the extracted data in an object */
        return {
            command,
            supportType,
            ign
        }
    }
});
<commit_msg>Patched up a small error introduced in a rebase.<commit_after>const config = require('config'); // Explicit import is necessary here for command aliases

const Command = require('../Command');
const RoleDeterminer = require('../Helper/RoleDeterminer');
const { generateRoomName, parseRoomType, convertRoom, archiveRoom } = require('../Helper/SupportRoomHelper');

const messages = {
    'help': 'You can use `!support <type> <IGN>` to create a room where you can contact the staff team for support in private, where `<IGN>` is your Minecraft username and `<type>` is one of the following:{types}',
    'type': '\n- `{type}`: {description}',
    'unknownType': 'Unknown support type `{type}`.\n\n{help}',
    'roomCreated': 'I\'ve created a support room of type `{type}` and added you to it!',
    'roomConverted': 'I\'ve converted this room\'s support type from `{oldType}` to `{newType}`.',
    'roomClosing': 'Closing this support room...',
    'roomClosed': '{user} closed support room `{room}`',
    'supportType': 'Support type',
    'roomParticipants': 'Participants',
    'noPermission': 'Sorry, it appears that you do not have access to perform this action.',
    'noIgnProvided': 'It looks like you forgot to add your Minecraft username to the support command. Usage: `!{command} <IGN>`, where `<IGN>` refers to your Minecraft username.',
    'noIgnProvidedWithType': 'It looks like you forgot to add your Minecraft username to the support command. Usage: `!{command} {type} <IGN>`, where `<IGN>` refers to your Minecraft username.',
    'notASupportRoom': 'You can only execute this command in a support room.',
    'roomCreationError': 'Sorry, it appears that I have encountered an error whilst trying to create a room. Check back later!',
    'supportWelcome': 'Hello {user} (IGN: {IGN})! I have created this room for you to ask a question relating to `{type}` to the staff team in private. Before asking your question, please read through the support rules below.',
    'supportRules': '**• Do not ping staff**\n' +
        'Members of our team (believe it or not) have lives of their own, and being disturbed whilst working or while in class can be very frustrating, so please refrain from pinging any staff without very good cause.  If there\'s a forum thread you can use, it is not good cause to ping staff.\n' +
        '\n' +
        '**• Don\'t expect live support on this server**\n' +
        'Shotbow does not offer live support. Should you be in luck, and a member of staff is actively chatting at the time you have a matter to raise, you may of course ask them for help. Under no conditions should you ping staff members individually for help.\n' +
        '\n' +
        '**• Cheaters**\n' +
        'If someone is cheating use the /report command.  This is the best way to get the attention of staff currently active in patrol duty.  Hackusations are not allowed, and staff should not be pinged to deal with cheaters.\n' +
        '\n' +
        'If you have a question or issue that doesn\'t require the attention of a staff member, it\'s probably better to ask politely in #shotbow without a ping and someone will gladly help you get to where you need to be.\n',
    'supportMessageReceived': 'Thank you for your message! If you have more information, you can continue sending messages in this channel. A staff member will get to you soon. ({roles})'
};

module.exports = Command.extend({
    commandName: 'support',
    commandAliases: Object.keys(config.support.types).map(typeKey => `${typeKey}support`),
    processMessage: async function (message, tokens) {
        const commandParameters = this.extractParameters(tokens);

        /* Handle the case of no or invalid arguments provided */
        if (!commandParameters) {
            const types = Object.keys(this.config.support.types).map(type => this.i18n.__mf(messages.type, {
                type,
                description: this.config.support.types[type].description
            }));
            return message.channel.send(this.i18n.__mf(messages.help, {types}));
        }

        /* Handle the unique cases of "close" and "convert" */
        tokens.shift();
        if (tokens.length > 0
            && commandParameters.command === 'support'
            && (tokens[0].toLowerCase() === 'close' || tokens[0].toLowerCase() === 'convert')) {
            const typeKey = parseRoomType(message.channel.name);
            if (!typeKey) {
                return message.channel.send(this.i18n.__mf(messages.notASupportRoom));
            }

            if (!RoleDeterminer.hasOneOfRoles(message.member, this.config.support.types[typeKey].roles)) {
                return message.channel.send(this.i18n.__mf(messages.noPermission));
            }

            if (tokens[0].toLowerCase() === 'close') {
                return await archiveRoom(message, this.i18n, this.discordClient);
            }
            if (tokens[0].toLowerCase() === 'convert') {
                return await convertRoom(message, tokens, this.i18n, this.discordClient);
            }
        }

        /* Get the support type */
        const typeKey = commandParameters.supportType;
        const type = this.config.support.types[typeKey];
        if (!type) {
            const types = Object.keys(this.config.support.types).map(type => this.i18n.__mf(messages.type, {
                type,
                description: this.config.support.types[type].description
            }));
            const helpText = this.i18n.__mf(messages.help, {types});
            return message.channel.send(this.i18n.__mf(messages.unknownType, {type: typeKey, help: helpText}));
        }

        /* Handle the case of no username provided */
        if (!commandParameters.ign) {
            if (commandParameters.command === 'support') {
                return message.channel.send(this.i18n.__mf(messages.noIgnProvidedWithType, {
                    command: commandParameters.command,
                    type: commandParameters.supportType
                }));
            } else {
                return message.channel.send(this.i18n.__mf(messages.noIgnProvided, {
                    command: commandParameters.command
                }));
            }
        }

        /* Create the channel and add the correct people to it */
        const supportCategory = message.guild.channels.cache.get(this.config.support.category);
        const supportChannel = await message.guild.channels.create(generateRoomName(typeKey), {
            type: 'text',
            parent: supportCategory,
        })
            .then(channel => channel.lockPermissions())
            .then(channel => channel.updateOverwrite(message.author.id, {
                ADD_REACTIONS: true,
                ATTACH_FILES: true,
                EMBED_LINKS: true,
                READ_MESSAGE_HISTORY: true,
                SEND_MESSAGES: true,
                USE_EXTERNAL_EMOJIS: true,
                SEND_TTS_MESSAGES: false,
                VIEW_CHANNEL: true
            }))
            .then(async channel => {
                for (const supportRoleId of type.roles) {
                    await channel.updateOverwrite(supportRoleId, {
                        ADD_REACTIONS: true,
                        ATTACH_FILES: true,
                        EMBED_LINKS: true,
                        READ_MESSAGE_HISTORY: true,
                        SEND_MESSAGES: true,
                        USE_EXTERNAL_EMOJIS: true,
                        SEND_TTS_MESSAGES: true,
                        VIEW_CHANNEL: true
                    });
                }
                return channel;
            })
            .catch(() => {
                /* If something went wrong with room creation, we delete it (if it was created) and notify the user */
                supportChannel.delete().catch(() => {});
                return message.channel.send(this.i18n.__mf(messages.roomCreationError));
            });

        const welcomeMessage = await supportChannel.send(this.i18n.__mf(messages.supportWelcome, {
            user: `<@${message.author.id}>`,
            IGN: commandParameters.ign,
            type: typeKey
        }));

        /* Add the collector to deal with post-creation events */
        this.addCollector(supportChannel, message.member, welcomeMessage);

        await supportChannel.send(this.i18n.__mf(messages.supportRules));
        return message.channel.send(this.i18n.__mf(messages.roomCreated, {type: typeKey}));
    },
    addCollector: function (supportChannel, creator, welcomeMessage) {
        const collectorFilter = (message) => message.member.id === creator.id;

        const automaticDelete = this.config.support.automaticDeletion;
        let collector;
        if (automaticDelete < 0) {
            collector = supportChannel.createMessageCollector(collectorFilter);
        } else {
            collector = supportChannel.createMessageCollector(collectorFilter,
                {time: automaticDelete});
            collector.on('end', async (_, reason) => {
                if (reason === 'time') {
                    await archiveRoom(welcomeMessage, this.i18n, this.discordClient);
                }
            });
        }

        collector.once('collect', (message) => {
            const supportType = parseRoomType(supportChannel.name);
            const supportRoles = this.config.support.types[supportType].roles
                .map(supportRole => `<@&${supportRole}>`)
                .join(', ');

            message.channel.send(this.i18n.__mf(messages.supportMessageReceived, {roles: supportRoles}));
            collector.stop();
        });
    },
    extractParameters: function (tokens) {
        const clonedTokens = [...tokens];
        /* Extract the support type */
        const command = clonedTokens[0].substr(this.commandPrefix.length, clonedTokens[0].length);
        clonedTokens.shift();
        let supportType;
        if (command === 'support') {
            if (clonedTokens.length === 0) {
                return null;
            } else {
                supportType = clonedTokens[0];
                clonedTokens.shift();
            }
        } else {
            supportType = command.split('support')[0];
        }

        /* Extract the IGN */
        if (clonedTokens.length === 0) {
            return {
                command,
                supportType: supportType.toLowerCase(),
                ign: null
            }
        }
        const ign = clonedTokens[0];

        /* Return the extracted data in an object */
        return {
            command,
            supportType,
            ign
        }
    }
});
<|endoftext|>