<commit_before>require([
    'chai',
    'jquery',
    'underscore',
    'tests/utils',
    'text!static/all_question_types.xml'
], function (
    chai,
    $,
    _,
    util,
    TEST_XML
) {
    var call = util.call,
        clickQuestion = util.clickQuestion,
        addQuestion = util.addQuestion,
        assert = chai.assert,
        questionTypes = [
            //{
            //    clickBeforeAdd: "questionX", // optional click this node before adding question
            //    path: parent path (must end with /)
            //    type: "QuestionType", // required
            //    nodeId: "questionN", // required
            //    attrs: { // optional
            //        // Any key here implies the same key inputs with a value of 1
            //        "attrName": value
            //    },
            //    inputs { // optional: add tests to check input counts
            //        "inputName": inputCount
            //    }
            //}
            {
                type: 'Text',
                nodeId: 'question1',
                attrs: {
                    keyAttr: 'jr preload key value',
                    dataValue: 'default data value',
                    constraintAttr: '/data/question20 = 2',
                    relevantAttr: '/data/question20',
                    requiredAttr: true,
                    preload: "jr preload",
                    preloadParams: "jr preload param"
                },
                inputs: {
                    calculateAttr: 0
                }
            }, {
                type: 'Trigger',
                nodeId: 'question2',
                inputs: {
                    // TODO add more input counts for each question type
                    calculateAttr: 0,
                    constraintAttr: 1,
                    requiredAttr: 1,
                    relevantAttr: 1
                }
            }, {
                type: 'Trigger',
                nodeId: 'question30',
                attrs: {
                    appearance: ''
                },
            }, {
                type: 'Select',
                nodeId: 'question3',
                inputs: {
                    // TODO add more input counts for each question type
                    calculateAttr: 0,
                    constraintAttr: 1,
                    requiredAttr: 1,
                    relevantAttr: 1
                }
            }, {
                type: 'MSelect',
                nodeId: 'question6'
            }, {
                type: 'Int',
                nodeId: 'question13'
            }, {
                type: 'PhoneNumber',
                nodeId: 'question14'
            }, {
                type: 'Double',
                nodeId: 'question15'
            }, {
                type: 'Long',
                nodeId: 'question16'
            }, {
                type: 'Date',
                nodeId: 'question17'
            }, {
                type: 'Time',
                nodeId: 'question18'
            }, {
                type: 'DateTime',
                nodeId: 'question19'
            }, {
                type: 'DataBindOnly',
                nodeId: 'question20',
                inputs: {
                    calculateAttr: 1,
                    constraintAttr: 0,
                    requiredAttr: 0,
                }
            }, {
                type: 'DataBindOnly',
                nodeId: 'question32',
                attrs: {
                    calculateAttr: '1 + 2'
                },
                inputs: {
                    calculateAttr: 1,
                    constraintAttr: 0,
                    requiredAttr: 0,
                    relevantAttr: 1
                }
            }, {
                clickBeforeAdd: "question19", // insert after question20
                type: 'Repeat',
                nodeId: 'question22'
            }, {
                type: 'FieldList',
                path: 'question22/',
                nodeId: 'question23'
            }, {
                type: 'Group',
                path: 'question22/question23/',
                nodeId: 'question40'
            }, {
                clickBeforeAdd: "question22/question23",
                type: 'Repeat',
                path: 'question22/question23/',
                nodeId: 'question41'
            },{
                clickBeforeAdd: "question22/question23",
                type: 'Image',
                path: 'question22/question23/',
                nodeId: 'question24'
            }, {
                type: 'Audio',
                path: 'question22/question23/',
                nodeId: 'question25'
            }, {
                type: 'Video',
                path: 'question22/question23/',
                nodeId: 'question26'
            }, {
                type: 'Geopoint',
                path: 'question22/question23/',
                nodeId: 'question27'
            }, {
                type: 'Secret',
                path: 'question22/question23/',
                nodeId: 'question28'
            }, {
                type: 'Signature',
                path: 'question22/question23/',
                nodeId: 'question29'
            }, {
                type: 'AndroidIntent',
                path: 'question22/question23/',
                nodeId: 'question7'
            }, {
                clickBeforeAdd: "question19", // insert before question22
                type: 'Group',
                nodeId: 'question21'
            }, {
                type: 'Repeat',
                path: 'question21/',
                nodeId: 'question31',
                attrs: {
                    repeat_count: 2
                }
            }
        ];

    describe("Vellum", function () {
        describe("on load XML", function () {
            before(function (done) {
                util.init({
                    core: {
                        form: TEST_XML,
                        onReady: function () {
                            done();
                        }
                    }
                });
            });

            it("preserves all question types and attributes", function () {
                util.assertXmlEqual(util.call('createXML'), TEST_XML);
            });

            _.each(questionTypes, function(q, index) {
                var nodeId = q.nodeId;
                describe("with " + q.type + "[" + nodeId + "]", function () {
                    before(function (done) {
                        if (index > 0) {
                            clickQuestion((q.path || "") + nodeId);
                        }
                        done();
                    });

                    it("should be selected when clicked", function() {
                        assert.equal(call("getCurrentlySelectedMug").p.nodeID, nodeId);
                    });

                    _.each(q.attrs || {}, function (val, name) {
                        it("should show 1 input for " + name, function() {
                            util.assertInputCount(name, 1, nodeId);
                        });
                    });

                    _.each(q.inputs || {}, function (num, name) {
                        if (q.attrs && q.attrs.hasOwnProperty(name)) {
                            assert.equal(num, 1,
                                "test configuration conflict for " + name);
                        } else {
                            it("should show " + num + " inputs for " + name, function() {
                                util.assertInputCount(name, num, nodeId);
                            });
                        }
                    });
                });
            });
        });

        it("adds all question types and attributes", function (done) {
            // This test takes way too long and needs to be broken up into
            // smaller subtests. LONG timeout is to prevent timeout on travis.
            this.timeout(10000);
            // this also tests
            // - that clicking add question buttons when other questions are
            //   selected adds questions correctly
            // - that a data node is added at the end, and adding a question
            //   when a data node is selected adds to the end of the non-data
            //   nodes
            // - adding standard and other itext
            // - changing itext label and updating question tree
            // - adding itext for multiple languages
            // - automatically creating Itext IDs for constraint and hint
            //   messages
            // - automatic adding of choices when you add a select
            // - automatic generation of media paths for regular questions and choices
            util.init({
                core: {
                    form: null,
                    onReady: function () {
                        _.each(questionTypes, function (q, i) {
                            var prev = (i > 0 ? questionTypes[i - 1] : {}),
                                prevId = q.clickBeforeAdd ||
                                    (prev.nodeId ? (prev.path || "") + prev.nodeId : null);
                            addQuestion.call({prevId: prevId}, q.type, q.nodeId, q.attrs);
                        });

                        function addAllForms() {
                            $(".btn:contains(image)").click();
                            $(".btn:contains(audio)").click();
                            $(".btn:contains(video)").click();
                            $(".btn:contains(long)").click();
                            $(".btn:contains(short)").click();
                            $(".btn:contains(custom)").click();
                            $(".fd-modal-generic-container").find("input")
                                .val("custom");
                            $(".fd-modal-generic-container").find(".btn:contains(Add)").click();
                        }

                        clickQuestion("question1");
                        addAllForms();
                        $("[name='itext-en-label']")
                            .val('question1 en label').change();
                        $("[name='itext-hin-label']")
                            .val('question1 hin label').change();
                        $("[name='itext-en-constraintMsg']")
                            .val('question1 en validation').change();
                        $("[name='itext-hin-constraintMsg']")
                            .val('question1 hin validation').change();
                        $("[name='itext-en-hint']")
                            .val('question1 en hint').change();
                        $("[name='itext-hin-hint']")
                            .val('question1 hin hint').change();
                        $("[name='itext-en-help']")
                            .val('question1 en help').change();
                        $("[name='itext-hin-help']")
                            .val('question1 hin help').change();
                        $("[name='itext-en-label-long']")
                            .val("question1 en long").change();
                        $("[name='itext-hin-label-long']")
                            .val("question1 hin long").change();
                        $("[name='itext-en-label-short']")
                            .val("question1 en short").change();
                        $("[name='itext-hin-label-short']")
                            .val("question1 hin short").change();
                        $("[name='itext-en-label-custom']")
                            .val("question1 en custom").change();
                        $("[name='itext-hin-label-custom']")
                            .val("question1 hin custom").change();

                        clickQuestion("question3/item1");
                        addAllForms();
                        $("[name='itext-en-label-long']")
                            .val("item1 long en").change();
                        $("[name='itext-hin-label-long']")
                            .val("item1 long hin").change();
                        $("[name='itext-en-label-short']")
                            .val("item1 short en").change();
                        $("[name='itext-hin-label-short']")
                            .val("item1 short hin").change();
                        $("[name='itext-en-label-custom']")
                            .val("item1 custom en").change();
                        $("[name='itext-hin-label-custom']")
                            .val("item1 custom hin").change();

                        clickQuestion("question22/question23/question7");
                        $("[name='intent-app-id']").val("app_id").change();
                        $("[name='intent-extra'] .fd-kv-key").val('key1').change();
                        $("[name='intent-extra'] .fd-kv-val").val('value1').change();
                        $("[name='intent-response'] .fd-kv-key").val('key2').change();
                        $("[name='intent-response'] .fd-kv-val").val('value2').change();
                        util.assertXmlEqual(
                            call('createXML'),
                            TEST_XML
                                .replace(' foo="bar"', '')
                                .replace(' spam="eggs"', '')
                                .replace(' foo="baz"', '')
                                .replace(/<unrecognized>[\s\S]+<\/unrecognized>/, '')
                                .replace('non-itext label', '')
                                .replace('non-itext hint', '')
                                .replace(/<instance[^>]+?casedb[^>]+?><\/instance>/, '')
                                .replace(/<setvalue[^>]+?>/, ''),
                            {normalize_xmlns: true}
                        );

                        // should have updated question tree
                        clickQuestion("question1");

                        done();
                    }
                }
            });
        });

        describe("can", function() {
            var changes = [
                    ["Text", "Trigger"],
                    ["Trigger", "Select"],
                    ["Image", "Select"],
                    ["Audio", "Select"],
                    ["Video", "Select"],
                    ["PhoneNumber", "Text"],
                    ["Select", "Text"],
                    ["MSelect", "Text"],
                    ["Select", "MSelect"],
                    ["MSelect", "Select"],
                    ["Select + Choices", "MSelect"],
                    ["MSelect + Choices", "Select"]
                ],
                no_change = [
                    //["Text", "Group"],
                    //["Text", "Repeat"],
                    //["Text", "FieldList"],
                    ["MSelect + Choices", "Text"],
                    ["Select + Choices", "Text"]
                    //["Group", "Text"],
                    //["Repeat", "Text"],
                    //["FieldList", "Text"]
                ];

            before(function (done) {
                util.init({
                    core: {
                        onReady: function () {
                            done();
                        }
                    }
                });
            });

            function setup(from, to) {
                var choices = from.indexOf(" + Choices") > -1;
                from = (choices ? from.replace(" + Choices", "") : from);
                var nodeId = (from + (choices ? "_Choices" : "") + "_to_" + to),
                    mug = addQuestion(from, nodeId);
                if (!choices && from.indexOf("Select") > -1) {
                    util.deleteQuestion(nodeId + "/item1");
                    util.deleteQuestion(nodeId + "/item2");
                }
                assert.equal(mug.p.nodeID, nodeId, "got wrong mug before changing type");
                assert.equal(mug.__className, from, "wrong mug type");
                return mug;
            }

            _.each(changes, function (change) {
                var from = change[0],
                    to = change[1];
                it("change " + from + " to " + to, function () {
                    var mug = setup(from, to);
                    call("changeMugType", mug, to);
                    mug = util.getMug(mug.p.nodeID);
                    assert.equal(mug.__className, to);

                    call("loadXML", call("createXML"));
                    mug = util.getMug(mug.p.nodeID);
                    assert.equal(mug.__className, to);
                });
            });

            _.each(no_change, function (change) {
                var from = change[0],
                    to = change[1];
                it("not change " + from + " to " + to, function () {
                    var mug = setup(from, to),
                        ok = true;
                    try {
                        call("changeMugType", mug, to);
                        ok = false;
                    } catch (error) {
                        assert(String(error).indexOf("Cannot change") > 0, String(error));
                    }
                    assert(ok, "Error not raised when changing " + from + " to " + to);
                    mug = util.getMug(mug.p.nodeID);
                    assert.equal(mug.__className, from.replace(" + Choices", ""));
                });
            });
        });

        it("question type change survives save + load", function (done) {
            function test() {
                addQuestion("Text", "question");
                var mug = call("getMugByPath", "/data/question");

                call("changeMugType", mug, "Trigger");

                util.saveAndReload(function () {
                    // verify type change
                    mug = call("getMugByPath", "/data/question");
                    assert.equal(mug.__className, "Trigger");
                    done();
                });
            }
            util.init({core: {onReady: test}});
        });

        it("should allow user to view longs but not add them", function() {
            util.loadXML("");

            // able to programatically add long
            util.addQuestion("Long", "long");
            util.addQuestion("Int", "int");
            util.assertJSTreeState(
                "long",
                "int"
            );

            // can't change another question to a long
            var $changer = $(".fd-question-changer");
            $changer.children("a").click();
            assert.equal($changer.find("[data-qtype='Text']").length, 1);
            assert.equal($changer.find("[data-qtype='Long']").length, 0);

            // can't add new long with toolbar
            var $toolbar = $(".fd-container-question-type-group");
            assert.equal($toolbar.find("[data-qtype='Text']:not(.btn)").length, 1); // dropdown item, not button
            assert.equal($toolbar.find("[data-qtype='Long']").length, 0);
        });

        it("prevents changing selects with children to non-selects", function() {
            util.loadXML("");
            var select = util.addQuestion("Select", "question1");
            var changerSelector = ".fd-question-changer";

            $(changerSelector + " > a").click();
            var $options = $(changerSelector + " .change-question");
            assert.equal($options.length, 1);
            assert.equal($options.length, $options.filter("[data-qtype*='Select']").length);

            util.deleteQuestion("question1/item1");
            util.deleteQuestion("question1/item2");
            assert.ok($(changerSelector + " .change-question:not([data-qtype*='Select'])").length > 0);
        });
    });
});
<commit_msg>more lint<commit_after>require([
    'chai',
    'jquery',
    'underscore',
    'tests/utils',
    'text!static/all_question_types.xml'
], function (
    chai,
    $,
    _,
    util,
    TEST_XML
) {
    var call = util.call,
        clickQuestion = util.clickQuestion,
        addQuestion = util.addQuestion,
        assert = chai.assert,
        questionTypes = [
            //{
            //    clickBeforeAdd: "questionX", // optional click this node before adding question
            //    path: parent path (must end with /)
            //    type: "QuestionType", // required
            //    nodeId: "questionN", // required
            //    attrs: { // optional
            //        // Any key here implies the same key inputs with a value of 1
            //        "attrName": value
            //    },
            //    inputs { // optional: add tests to check input counts
            //        "inputName": inputCount
            //    }
            //}
            {
                type: 'Text',
                nodeId: 'question1',
                attrs: {
                    keyAttr: 'jr preload key value',
                    dataValue: 'default data value',
                    constraintAttr: '/data/question20 = 2',
                    relevantAttr: '/data/question20',
                    requiredAttr: true,
                    preload: "jr preload",
                    preloadParams: "jr preload param"
                },
                inputs: {
                    calculateAttr: 0
                }
            }, {
                type: 'Trigger',
                nodeId: 'question2',
                inputs: {
                    // TODO add more input counts for each question type
                    calculateAttr: 0,
                    constraintAttr: 1,
                    requiredAttr: 1,
                    relevantAttr: 1
                }
            }, {
                type: 'Trigger',
                nodeId: 'question30',
                attrs: {
                    appearance: ''
                },
            }, {
                type: 'Select',
                nodeId: 'question3',
                inputs: {
                    // TODO add more input counts for each question type
                    calculateAttr: 0,
                    constraintAttr: 1,
                    requiredAttr: 1,
                    relevantAttr: 1
                }
            }, {
                type: 'MSelect',
                nodeId: 'question6'
            }, {
                type: 'Int',
                nodeId: 'question13'
            }, {
                type: 'PhoneNumber',
                nodeId: 'question14'
            }, {
                type: 'Double',
                nodeId: 'question15'
            }, {
                type: 'Long',
                nodeId: 'question16'
            }, {
                type: 'Date',
                nodeId: 'question17'
            }, {
                type: 'Time',
                nodeId: 'question18'
            }, {
                type: 'DateTime',
                nodeId: 'question19'
            }, {
                type: 'DataBindOnly',
                nodeId: 'question20',
                inputs: {
                    calculateAttr: 1,
                    constraintAttr: 0,
                    requiredAttr: 0,
                }
            }, {
                type: 'DataBindOnly',
                nodeId: 'question32',
                attrs: {
                    calculateAttr: '1 + 2'
                },
                inputs: {
                    calculateAttr: 1,
                    constraintAttr: 0,
                    requiredAttr: 0,
                    relevantAttr: 1
                }
            }, {
                clickBeforeAdd: "question19", // insert after question20
                type: 'Repeat',
                nodeId: 'question22'
            }, {
                type: 'FieldList',
                path: 'question22/',
                nodeId: 'question23'
            }, {
                type: 'Group',
                path: 'question22/question23/',
                nodeId: 'question40'
            }, {
                clickBeforeAdd: "question22/question23",
                type: 'Repeat',
                path: 'question22/question23/',
                nodeId: 'question41'
            },{
                clickBeforeAdd: "question22/question23",
                type: 'Image',
                path: 'question22/question23/',
                nodeId: 'question24'
            }, {
                type: 'Audio',
                path: 'question22/question23/',
                nodeId: 'question25'
            }, {
                type: 'Video',
                path: 'question22/question23/',
                nodeId: 'question26'
            }, {
                type: 'Geopoint',
                path: 'question22/question23/',
                nodeId: 'question27'
            }, {
                type: 'Secret',
                path: 'question22/question23/',
                nodeId: 'question28'
            }, {
                type: 'Signature',
                path: 'question22/question23/',
                nodeId: 'question29'
            }, {
                type: 'AndroidIntent',
                path: 'question22/question23/',
                nodeId: 'question7'
            }, {
                clickBeforeAdd: "question19", // insert before question22
                type: 'Group',
                nodeId: 'question21'
            }, {
                type: 'Repeat',
                path: 'question21/',
                nodeId: 'question31',
                attrs: {
                    repeat_count: 2
                }
            }
        ];

    describe("Vellum", function () {
        describe("on load XML", function () {
            before(function (done) {
                util.init({
                    core: {
                        form: TEST_XML,
                        onReady: function () {
                            done();
                        }
                    }
                });
            });

            it("preserves all question types and attributes", function () {
                util.assertXmlEqual(util.call('createXML'), TEST_XML);
            });

            _.each(questionTypes, function(q, index) {
                var nodeId = q.nodeId;
                describe("with " + q.type + "[" + nodeId + "]", function () {
                    before(function (done) {
                        if (index > 0) {
                            clickQuestion((q.path || "") + nodeId);
                        }
                        done();
                    });

                    it("should be selected when clicked", function() {
                        assert.equal(call("getCurrentlySelectedMug").p.nodeID, nodeId);
                    });

                    _.each(q.attrs || {}, function (val, name) {
                        it("should show 1 input for " + name, function() {
                            util.assertInputCount(name, 1, nodeId);
                        });
                    });

                    _.each(q.inputs || {}, function (num, name) {
                        if (q.attrs && q.attrs.hasOwnProperty(name)) {
                            assert.equal(num, 1,
                                "test configuration conflict for " + name);
                        } else {
                            it("should show " + num + " inputs for " + name, function() {
                                util.assertInputCount(name, num, nodeId);
                            });
                        }
                    });
                });
            });
        });

        it("adds all question types and attributes", function (done) {
            // This test takes way too long and needs to be broken up into
            // smaller subtests. LONG timeout is to prevent timeout on travis.
            this.timeout(10000);
            // this also tests
            // - that clicking add question buttons when other questions are
            //   selected adds questions correctly
            // - that a data node is added at the end, and adding a question
            //   when a data node is selected adds to the end of the non-data
            //   nodes
            // - adding standard and other itext
            // - changing itext label and updating question tree
            // - adding itext for multiple languages
            // - automatically creating Itext IDs for constraint and hint
            //   messages
            // - automatic adding of choices when you add a select
            // - automatic generation of media paths for regular questions and choices
            util.init({
                core: {
                    form: null,
                    onReady: function () {
                        _.each(questionTypes, function (q, i) {
                            var prev = (i > 0 ? questionTypes[i - 1] : {}),
                                prevId = q.clickBeforeAdd ||
                                    (prev.nodeId ? (prev.path || "") + prev.nodeId : null);
                            addQuestion.call({prevId: prevId}, q.type, q.nodeId, q.attrs);
                        });

                        function addAllForms() {
                            $(".btn:contains(image)").click();
                            $(".btn:contains(audio)").click();
                            $(".btn:contains(video)").click();
                            $(".btn:contains(long)").click();
                            $(".btn:contains(short)").click();
                            $(".btn:contains(custom)").click();
                            $(".fd-modal-generic-container").find("input")
                                .val("custom");
                            $(".fd-modal-generic-container").find(".btn:contains(Add)").click();
                        }

                        clickQuestion("question1");
                        addAllForms();
                        $("[name='itext-en-label']")
                            .val('question1 en label').change();
                        $("[name='itext-hin-label']")
                            .val('question1 hin label').change();
                        $("[name='itext-en-constraintMsg']")
                            .val('question1 en validation').change();
                        $("[name='itext-hin-constraintMsg']")
                            .val('question1 hin validation').change();
                        $("[name='itext-en-hint']")
                            .val('question1 en hint').change();
                        $("[name='itext-hin-hint']")
                            .val('question1 hin hint').change();
                        $("[name='itext-en-help']")
                            .val('question1 en help').change();
                        $("[name='itext-hin-help']")
                            .val('question1 hin help').change();
                        $("[name='itext-en-label-long']")
                            .val("question1 en long").change();
                        $("[name='itext-hin-label-long']")
                            .val("question1 hin long").change();
                        $("[name='itext-en-label-short']")
                            .val("question1 en short").change();
                        $("[name='itext-hin-label-short']")
                            .val("question1 hin short").change();
                        $("[name='itext-en-label-custom']")
                            .val("question1 en custom").change();
                        $("[name='itext-hin-label-custom']")
                            .val("question1 hin custom").change();

                        clickQuestion("question3/item1");
                        addAllForms();
                        $("[name='itext-en-label-long']")
                            .val("item1 long en").change();
                        $("[name='itext-hin-label-long']")
                            .val("item1 long hin").change();
                        $("[name='itext-en-label-short']")
                            .val("item1 short en").change();
                        $("[name='itext-hin-label-short']")
                            .val("item1 short hin").change();
                        $("[name='itext-en-label-custom']")
                            .val("item1 custom en").change();
                        $("[name='itext-hin-label-custom']")
                            .val("item1 custom hin").change();

                        clickQuestion("question22/question23/question7");
                        $("[name='intent-app-id']").val("app_id").change();
                        $("[name='intent-extra'] .fd-kv-key").val('key1').change();
                        $("[name='intent-extra'] .fd-kv-val").val('value1').change();
                        $("[name='intent-response'] .fd-kv-key").val('key2').change();
                        $("[name='intent-response'] .fd-kv-val").val('value2').change();
                        util.assertXmlEqual(
                            call('createXML'),
                            TEST_XML
                                .replace(' foo="bar"', '')
                                .replace(' spam="eggs"', '')
                                .replace(' foo="baz"', '')
                                .replace(/<unrecognized>[\s\S]+<\/unrecognized>/, '')
                                .replace('non-itext label', '')
                                .replace('non-itext hint', '')
                                .replace(/<instance[^>]+?casedb[^>]+?><\/instance>/, '')
                                .replace(/<setvalue[^>]+?>/, ''),
                            {normalize_xmlns: true}
                        );

                        // should have updated question tree
                        clickQuestion("question1");

                        done();
                    }
                }
            });
        });

        describe("can", function() {
            var changes = [
                    ["Text", "Trigger"],
                    ["Trigger", "Select"],
                    ["Image", "Select"],
                    ["Audio", "Select"],
                    ["Video", "Select"],
                    ["PhoneNumber", "Text"],
                    ["Select", "Text"],
                    ["MSelect", "Text"],
                    ["Select", "MSelect"],
                    ["MSelect", "Select"],
                    ["Select + Choices", "MSelect"],
                    ["MSelect + Choices", "Select"]
                ],
                no_change = [
                    //["Text", "Group"],
                    //["Text", "Repeat"],
                    //["Text", "FieldList"],
                    ["MSelect + Choices", "Text"],
                    ["Select + Choices", "Text"]
                    //["Group", "Text"],
                    //["Repeat", "Text"],
                    //["FieldList", "Text"]
                ];

            before(function (done) {
                util.init({
                    core: {
                        onReady: function () {
                            done();
                        }
                    }
                });
            });

            function setup(from, to) {
                var choices = from.indexOf(" + Choices") > -1;
                from = (choices ? from.replace(" + Choices", "") : from);
                var nodeId = (from + (choices ? "_Choices" : "") + "_to_" + to),
                    mug = addQuestion(from, nodeId);
                if (!choices && from.indexOf("Select") > -1) {
                    util.deleteQuestion(nodeId + "/item1");
                    util.deleteQuestion(nodeId + "/item2");
                }
                assert.equal(mug.p.nodeID, nodeId, "got wrong mug before changing type");
                assert.equal(mug.__className, from, "wrong mug type");
                return mug;
            }

            _.each(changes, function (change) {
                var from = change[0],
                    to = change[1];
                it("change " + from + " to " + to, function () {
                    var mug = setup(from, to);
                    call("changeMugType", mug, to);
                    mug = util.getMug(mug.p.nodeID);
                    assert.equal(mug.__className, to);

                    call("loadXML", call("createXML"));
                    mug = util.getMug(mug.p.nodeID);
                    assert.equal(mug.__className, to);
                });
            });

            _.each(no_change, function (change) {
                var from = change[0],
                    to = change[1];
                it("not change " + from + " to " + to, function () {
                    var mug = setup(from, to),
                        ok = true;
                    try {
                        call("changeMugType", mug, to);
                        ok = false;
                    } catch (error) {
                        assert(String(error).indexOf("Cannot change") > 0, String(error));
                    }
                    assert(ok, "Error not raised when changing " + from + " to " + to);
                    mug = util.getMug(mug.p.nodeID);
                    assert.equal(mug.__className, from.replace(" + Choices", ""));
                });
            });
        });

        it("question type change survives save + load", function (done) {
            function test() {
                addQuestion("Text", "question");
                var mug = call("getMugByPath", "/data/question");

                call("changeMugType", mug, "Trigger");

                util.saveAndReload(function () {
                    // verify type change
                    mug = call("getMugByPath", "/data/question");
                    assert.equal(mug.__className, "Trigger");
                    done();
                });
            }
            util.init({core: {onReady: test}});
        });

        it("should allow user to view longs but not add them", function() {
            util.loadXML("");

            // able to programatically add long
            util.addQuestion("Long", "long");
            util.addQuestion("Int", "int");
            util.assertJSTreeState(
                "long",
                "int"
            );

            // can't change another question to a long
            var $changer = $(".fd-question-changer");
            $changer.children("a").click();
            assert.equal($changer.find("[data-qtype='Text']").length, 1);
            assert.equal($changer.find("[data-qtype='Long']").length, 0);

            // can't add new long with toolbar
            var $toolbar = $(".fd-container-question-type-group");
            assert.equal($toolbar.find("[data-qtype='Text']:not(.btn)").length, 1); // dropdown item, not button
            assert.equal($toolbar.find("[data-qtype='Long']").length, 0);
        });

        it("prevents changing selects with children to non-selects", function() {
            util.loadXML("");
            util.addQuestion("Select", "question1");
            var changerSelector = ".fd-question-changer";

            $(changerSelector + " > a").click();
            var $options = $(changerSelector + " .change-question");
            assert.equal($options.length, 1);
            assert.equal($options.length, $options.filter("[data-qtype*='Select']").length);

            util.deleteQuestion("question1/item1");
            util.deleteQuestion("question1/item2");
            assert.ok($(changerSelector + " .change-question:not([data-qtype*='Select'])").length > 0);
        });
    });
});
<|endoftext|>


<commit_before>/*
 *  Copyright (c) 2014, Facebook, Inc.
 *  All rights reserved.
 *
 *  This source code is licensed under the BSD-style license found in the
 *  LICENSE file in the root directory of this source tree. An additional grant
 *  of patent rights can be found in the PATENTS file in the same directory.
 */

var React = require('react');
var beautifyHTML = require('js-beautify').html;
var nodeJSX = require('node-jsx');
var _merge = require('lodash.merge');

var DEFAULT_OPTIONS = {
  jsx: {
    extension: '.jsx',
    harmony: false
  },
  doctype: '<!DOCTYPE html>'
};

function createEngine(engineOptions) {
  engineOptions = _merge(DEFAULT_OPTIONS, engineOptions);

  // Don't install the require until the engine is created. This lets us leave
  // the option of using harmony features up to the consumer.
  nodeJSX.install(engineOptions.jsx);

  var moduleDetectRegEx = new RegExp('\\' + engineOptions.jsx.extension + '$');

  function renderFile(filename, options, cb) {
    try {
      var markup = engineOptions.doctype;
      var component = require(filename);
      markup += React.renderComponentToStaticMarkup(component(options));
    } catch (e) {
      return cb(e);
    }

    if (options.settings.env === 'development') {
      // NOTE: This will screw up some things where whitespace is important, and be
      // subtly different than prod. Maybe make this an optional thing.
      markup = beautifyHTML(markup);

      // Remove all files from the module cache that use our extension. If we're
      // using .js, this could be sloooow. On the plus side, we can now make changes
      // to our views without needing to restart the server.
      Object.keys(require.cache).forEach(function(module) {
        if (moduleDetectRegEx.test(require.cache[module].filename)) {
          delete require.cache[module];
        }
      });
    }

    cb(null, markup);
  }

  return renderFile;
}

exports.createEngine = createEngine;
<commit_msg>Stop beautifying by default<commit_after>/*
 *  Copyright (c) 2014, Facebook, Inc.
 *  All rights reserved.
 *
 *  This source code is licensed under the BSD-style license found in the
 *  LICENSE file in the root directory of this source tree. An additional grant
 *  of patent rights can be found in the PATENTS file in the same directory.
 */

var React = require('react');
var beautifyHTML = require('js-beautify').html;
var nodeJSX = require('node-jsx');
var _merge = require('lodash.merge');

var DEFAULT_OPTIONS = {
  jsx: {
    extension: '.jsx',
    harmony: false
  },
  doctype: '<!DOCTYPE html>',
  beautify: false
};

function createEngine(engineOptions) {
  engineOptions = _merge(DEFAULT_OPTIONS, engineOptions);

  // Don't install the require until the engine is created. This lets us leave
  // the option of using harmony features up to the consumer.
  nodeJSX.install(engineOptions.jsx);

  var moduleDetectRegEx = new RegExp('\\' + engineOptions.jsx.extension + '$');

  function renderFile(filename, options, cb) {
    try {
      var markup = engineOptions.doctype;
      var component = require(filename);
      markup += React.renderComponentToStaticMarkup(component(options));
    } catch (e) {
      return cb(e);
    }

    if (engineOptions.beautify) {
      // NOTE: This will screw up some things where whitespace is important, and be
      // subtly different than prod.
      markup = beautifyHTML(markup);
    }

    if (options.settings.env === 'development') {
      // Remove all files from the module cache that use our extension. If we're
      // using .js, this could be sloooow. On the plus side, we can now make changes
      // to our views without needing to restart the server.
      Object.keys(require.cache).forEach(function(module) {
        if (moduleDetectRegEx.test(require.cache[module].filename)) {
          delete require.cache[module];
        }
      });
    }

    cb(null, markup);
  }

  return renderFile;
}

exports.createEngine = createEngine;
<|endoftext|>


<commit_before>"use strict";

var API = require('./api.js');
var extend = require('extend');

/**
 * Creates an instance of object, connected to Nfield API
 * @constructor
 */
function ConnectedInstance (requestOptions, authToken, credentials) {
  this.__REQUEST_OPTIONS = requestOptions;
  this.__TOKEN = authToken;
  this.__CREDENTIALS = credentials;

  this.SurveyFieldwork = {
    status : API.getSurveyStatus.bind(this, this.__REQUEST_OPTIONS, this.__CREDENTIALS, this.__TOKEN),
    start : API.startSurvey.bind(this, this.__REQUEST_OPTIONS, this.__CREDENTIALS, this.__TOKEN),
    stop : API.stopSurvey.bind(this, this.__REQUEST_OPTIONS, this.__CREDENTIALS, this.__TOKEN)
  };
}

/**
 * Creates NfieldClient object
 * @constructor
 */
function NfieldClient (defOptions) {

  var defaultRequestCliOptions = {
    baseUrl : 'https://api.nfieldmr.com/',
    headers : {
      'Content-Type' : 'application/json'
    }
  };

  extend(true, defaultRequestCliOptions, defOptions);

  /**
   * Returns a new instance of NfieldClient with new request parameters
   */
  this.defaults = function defaults (options) {
    if (typeof options !== 'object') throw new Error('`options` must be an object with request parameters');
    return new NfieldClient(options);
  };

  /**
   * Connects NfieldClient to API
   * Returns ConnectedInstance
   */
  this.connect = function connect (credentials, callback) {
    if (typeof credentials === 'undefined' || typeof credentials === 'function') throw new Error('not all required parameters provided: no `credentials`');
    if (!credentials.Domain || !credentials.Username || !credentials.Password) throw new Error('not all required parameters provided: no `Domain` or `Username` or `Password`');

    var token = {};
    var promise = API.signIn(defaultRequestCliOptions, credentials).then(function (data) {
      if (data[0].statusCode !== 200) {
        throw new Error(`${data[0].statusCode}: ${data[0].body.Message}`);
      } else {
        token = {
          AuthenticationToken : data[0].body.AuthenticationToken,
          Timestamp : Date.now()
        };
        return new ConnectedInstance(defaultRequestCliOptions, token, credentials);
      }
    }).nodeify(callback);

    return promise;
  };

}

module.exports = {
  NfieldClient : new NfieldClient()
};<commit_msg>Remove 'app\json' header from defaults as unnecessary<commit_after>"use strict";

var API = require('./api.js');
var extend = require('extend');

/**
 * Creates an instance of object, connected to Nfield API
 * @constructor
 */
function ConnectedInstance (requestOptions, authToken, credentials) {
  this.__REQUEST_OPTIONS = requestOptions;
  this.__TOKEN = authToken;
  this.__CREDENTIALS = credentials;

  this.SurveyFieldwork = {
    status : API.getSurveyStatus.bind(this, this.__REQUEST_OPTIONS, this.__CREDENTIALS, this.__TOKEN),
    start : API.startSurvey.bind(this, this.__REQUEST_OPTIONS, this.__CREDENTIALS, this.__TOKEN),
    stop : API.stopSurvey.bind(this, this.__REQUEST_OPTIONS, this.__CREDENTIALS, this.__TOKEN)
  };
}

/**
 * Creates NfieldClient object
 * @constructor
 */
function NfieldClient (defOptions) {

  var defaultRequestCliOptions = {
    baseUrl : 'https://api.nfieldmr.com/'
  };

  extend(true, defaultRequestCliOptions, defOptions);

  /**
   * Returns a new instance of NfieldClient with new request parameters
   */
  this.defaults = function defaults (options) {
    if (typeof options !== 'object') throw new Error('`options` must be an object with request parameters');
    return new NfieldClient(options);
  };

  /**
   * Connects NfieldClient to API
   * Returns ConnectedInstance
   */
  this.connect = function connect (credentials, callback) {
    if (typeof credentials === 'undefined' || typeof credentials === 'function') throw new Error('not all required parameters provided: no `credentials`');
    if (!credentials.Domain || !credentials.Username || !credentials.Password) throw new Error('not all required parameters provided: no `Domain` or `Username` or `Password`');

    var token = {};
    var promise = API.signIn(defaultRequestCliOptions, credentials).then(function (data) {
      if (data[0].statusCode !== 200) {
        throw new Error(`${data[0].statusCode}: ${data[0].body.Message}`);
      } else {
        token = {
          AuthenticationToken : data[0].body.AuthenticationToken,
          Timestamp : Date.now()
        };
        return new ConnectedInstance(defaultRequestCliOptions, token, credentials);
      }
    }).nodeify(callback);

    return promise;
  };

}

module.exports = {
  NfieldClient : new NfieldClient()
};<|endoftext|>


<commit_before>var EventEmitter = require('events').EventEmitter
var fs           = require('fs')
var u            = require('./util')

var exports = module.exports = function (file) {
  //if(!cb) cb = it, it = null
  var fd, blockSize, blockCount

  //if iterator is null, just open the file.

  var emitter = new EventEmitter()

/*
Hmm, so the default case should be an iterator,
and get is just a special-case - it's just an iterator
where start = end. Finding the block that an given key starts in
is the same for an iterator as for a get.
*/

  emitter.open = function (cb) {
    u.open(file, function (err, _stat) {
      if(err) return cb(err)
      emitter._stat = stat
      emitter.opened = true
      cb()
    })
  }

  emitter.iterator = function (opts) {
    if(!emitter.opened)
      throw new Error('SST is not yet opened!')
    //first, just support iterating over the entire sst.
    return BlockIterator(emitter._stat, opts)
      .pipe(u.json(opts.reverse))
  }

}

exports.createSST = function (file, it, cb) {
  ws = fs.createWriteStream(file)
  cb = u.once(cb)
  var meta = {items: 0, length: 0, meta: true}
  ;(function next () {
    it.next(function (err, data) {
      if(err)
        return cb(err)
      if(data) {
        var json = JSON.stringify(data) + '\n'
        meta.items ++
        meta.length += json.length
        if(true === ws.write(json)) next()
        else ws.once('drain', next)
      } else {
        ws.end()
      }
    })
  })()

  ws.on('error', cb)
  ws.on('close', cb)

}

<commit_msg>implement sst interface<commit_after>var EventEmitter = require('events').EventEmitter
var fs           = require('fs')
var u            = require('./util')
var pullRange    = require('pull-stream-range')
var BlockIterator = require('./block-iterator')

var open = function (file, cb) {
  fs.stat(file, function (err, stat) {
    if(err) return cb(err)
    fs.open(file, 'r', function (err, fd) {
      if(err) return cb(err)
      stat.blklength = 512
      stat.length = Math.ceil(stat.size / stat.blksize)
      stat.fd = fd
      cb(null, stat)
    })
  })
}

var exports = module.exports = function (file) {
  //if(!cb) cb = it, it = null
  var fd, blockSize, blockCount

  //if iterator is null, just open the file.

  var emitter = new EventEmitter()

/*
Hmm, so the default case should be an iterator,
and get is just a special-case - it's just an iterator
where start = end. Finding the block that an given key starts in
is the same for an iterator as for a get.
*/

  emitter.open = function (cb) {
    open(file, function (err, stat) {
      if(err) return cb(err)
      emitter._stat = stat
      emitter.opened = true
      cb()
    })
  }

  emitter.all = function (opts) {
    return BlockIterator(emitter._stat, opts)
      .pipe(u.json(opts.reverse))
  }

  emitter.iterator = function (opts) {
    opts = opts || {}
    if(!emitter.opened)
      throw new Error('SST is not yet opened!')

    var range = [opts.start, opts.end]
    if(opts.start && opts.end) range.sort()
    if(opts.reverse) range.reverse()

    opts.start = range[0] || null
    opts.end   = range[1] || null

    //first, just support iterating over the entire sst.
    function getStream(i) {
      var _opts = u.merge({offset: i}, opts)
      if(isNaN(_opts.offset))
        throw new Error('i is not numeb')
      return BlockIterator(emitter._stat, _opts)
        .pipe(u.json(opts.reverse))

    }
    var blockCount = Math.ceil(emitter._stat.size / emitter._stat.blksize)

    function compare (a, b) {
      if(!a || !b) {
        console.log(a, b)
        throw new Error('nully')
      }
      var mult = opts.reverse ? -1 : 1
      return ( a.key < b.key ? -1
             : a.key > b.key ?  1
             :                  0 ) * mult
    }
    var start, end

    if(opts.start) start = {key: opts.start}
    if(opts.end) end = {key: opts.end}

    //console.log('START END', start, end, compare(start, end))

    return pullRange(getStream, compare,
      blockCount,
      start, end)
  }

  emitter.get = function (key, cb) {
    var read = emitter.iterator({start: key, end: key})
    read(null, function (err, data) {
      //close stream if it's not already closing.
      if(!err) read(true, function () {})
      cb(err, data && data.value)
    })
  }

  return emitter
}

exports.createSST = function (file, it, cb) {
  ws = fs.createWriteStream(file)
  cb = u.once(cb)
  var meta = {items: 0, length: 0, meta: true}
  ;(function next () {
    it.next(function (err, data) {
      if(err)
        return cb(err)
      if(data) {
        var json = JSON.stringify(data) + '\n'
        meta.items ++
        meta.length += json.length
        if(true === ws.write(json)) next()
        else ws.once('drain', next)
      } else {
        ws.end()
      }
    })
  })()

  ws.on('error', cb)
  ws.on('close', cb)

}

<|endoftext|>


<commit_before>var WebKit = require('webkitgtk');
var Pool = require('generic-pool').Pool;
var fs = require('fs');
var queue = require('queue-async');
var escapeStringRegexp = require('escape-string-regexp');
var request = require('request');
var Path = require('path');
var Cache = require('adaptative-replacement-cache');

var Dom = module.exports = function(model, opts) {
	// init cache on demand, allow user settings
	if (Dom.settings.max <= 2) console.warn("express-dom expects Dom.settings.max to be > 2");
	if (!Dom.pool) {
		Dom.pool = initPool(Dom.settings);
		// cache puts pressure towards page release, the difference ensures some
		// pages are always available (for a pool of 32, 3 pages are available or being released)
		var cacheSize = Dom.settings.max - Math.floor(Math.log(Dom.settings.max));
		Dom.cache = new Cache(cacheSize);
		Dom.cache.on('eviction', function(key, page) {
			release(page, function(err) {
				if (err) console.error(err);
			})
		});
	}
	var h = new Handler(model, opts);
	return h.chainable;
};

Dom.Handler = Handler;

Dom.settings = {
	min: 1,
	max: 32,
	refreshIdle: false,
	display: process.env.DISPLAY || 0,
	style: fs.readFileSync(__dirname + '/index.css'),
	debug: !!process.env.DEBUG,
	console: !!process.env.DEBUG
};

Dom.plugins = require('./plugins');

Dom.authors = {
	before: [],
	current: [],
	after: []
};

Dom.users = {
	before: [Dom.plugins.nostylesheets],
	current: [],
	after: []
};

Dom.author = function(mw, position) {
	var list = Dom.authors;
	if (!position) position = 'current';
	if (!list[position]) throw new Error("Unknown position for express-dom author middleware", position);
	list[position].push(mw);
	return Dom;
};

Dom.use = function(mw, position) {
	var list = Dom.users;
	if (!position) position = 'current';
	if (!list[position]) throw new Error("Unknown position for express-dom user middleware", position);
	list[position].push(mw);
	return Dom;
};

function Handler(model, opts) {
	this.view = {};
	if (isRemote(model)) {
		this.view.url = model;
	} else {
		this.path = model; // app.get('statics') is available in middleware
	}
	this.opts = opts || {};
	this.chainable = this.middleware.bind(this);
	this.chainable.author = this.author.bind(this);
	this.chainable.use = this.use.bind(this);
	this.authors = {
		before: Dom.authors.before.slice(),
		current: Dom.authors.current.slice(),
		after: Dom.authors.after.slice()
	};
	this.users = {
		before: Dom.users.before.slice(),
		current: Dom.users.current.slice(),
		after: Dom.users.after.slice()
	};
	this.pages = {};
	if (this.init) this.init(); // used by raja
}

Handler.prototype.author = function(mw, position) {
	var list = this.authors;
	if (!position) position = 'current';
	if (!list[position]) throw new Error("Unknown position for express-dom author middleware", position);
	list[position].push(mw);
	return this.chainable;
};

Handler.prototype.use = function(mw, position) {
	var list = this.users;
	if (!position) position = 'current';
	if (!list[position]) throw new Error("Unknown position for express-dom user middleware", position);
	list[position].push(mw);
	return this.chainable;
};

Handler.prototype.middleware = function(req, res, next) {
	var h = this;
	if (h.path !== undefined) {
		var path = h.path;
		delete h.path;
		var root = req.app.get('statics');
		if (!root) return next(new Error("Cannot find view, undefined 'statics' application setting"));
		if (!path) path = "index";
		var path = Path.resolve(root, path);
		if (path.indexOf(root) !== 0) return next(new Error("Path outside statics dir\n" + path));
		if (path.slice(-1) == "/") path += "index";
		if (Path.extname(path) != ".html") path += ".html";
		h.view.url = path;
	}
	var url = req.protocol + '://' + req.headers.host + req.url;
	if (url == h.view.url) {
		return next(new Error("The view has the same url as the requested page"));
	}
	h.instance(url, function(err, inst) {
		if (err) return next(err);
		h.build(inst, req, res, function(err) {
			if (err) return next(err);
			h.finish(inst, res);
		});
	});
};

Handler.prototype.build = function(inst, req, res, cb) {
	var h = this;
	queue(1)
	.defer(h.getView.bind(h), req)
	.defer(h.getAuthored.bind(h), inst, req, res)
	.defer(h.getUsed.bind(h), inst, req, res)
	.awaitAll(cb);
};

Handler.prototype.instance = function(url, cb) {
	var h = this;
	var inst = h.pages[url];
	if (!inst) inst = h.pages[url] = {
		author: {url: 'author:' + h.view.url},
		user: {url: url},
		output: function(cb) {
			this.page.html(cb);
		}
	};
	cb(null, inst);
};

Handler.prototype.finish = function(inst, res) {
	res.type('text/html');
	res.set('Last-Modified', inst.user.mtime.toUTCString());
	res.send(inst.user.data);
};

Handler.prototype.getView = function(req, cb) {
	var h = this;
	if (h.view.valid) return cb();
	var loader = isRemote(h.view.url) ? h.loadRemote : h.loadLocal;
	loader.call(h, h.view.url, function(err, body) {
		if (!err || body) {
			h.view.data = body;
			h.view.valid = true;
			h.view.mtime = new Date();
		}	else {
			if (!err) err = new Error("Empty initial html in " + h.view.url);
			if (!err.code || err.code == 'ENOENT') err.code = 404;
			else err.code = 500;
		}
		cb(err);
	});
};

Handler.prototype.loadLocal = fs.readFile;
Handler.prototype.loadRemote = function(url, cb) {
	request(url, function(err, res, body) {
		cb(err, body);
	});
};

Handler.prototype.getAuthored = function(inst, req, res, cb) {
	var h = this;
	if (inst.author.valid) return cb();
	if (h.authors.before.length || h.authors.current.length || h.authors.after.length) {
		acquire(inst.author.url, function(err, page) {
			if (err) return cb(err);
			inst.page = page;
			inst.locked = true;
			page.parentInstance = inst;
			var obj = {
				content: h.view.data,
				console: true
			};
			if (!Dom.settings.debug) obj.style = Dom.settings.style;
			inst.page.preload(inst.user.url, obj);
			h.processMw(inst, h.authors, req, res);
			inst.page.wait('idle').html(function(err, html) {
				if (err) return cb(err);
				inst.author.data = html;
				inst.author.valid = true;
				inst.author.mtime = new Date();
				// release because we are done authoring
				inst.locked = false;
				Dom.cache.del(inst.author.url);
				release(inst.page, function(err) {
					cb(err);
				});
			});
		});
	} else {
		inst.author.data = h.view.data;
		inst.author.valid = true;
		inst.author.mtime = new Date();
		cb();
	}
};

Handler.prototype.getUsed = function(inst, req, res, cb) {
	var h = this;
	if (inst.user.valid) return cb();
	var opts = {};
	for (var key in h.opts) {
		opts[key] = h.opts[key];
	}
	if (!opts.content) opts.content = inst.author.data;
	if (!opts.cookie) opts.cookie = req.get('Cookie');
	if (opts.console === undefined) opts.console = true;
	if (opts.images === undefined) opts.images = false;
	if (opts.style === undefined && !Dom.settings.debug) opts.style = Dom.settings.style;
	acquire(inst.user.url, function(err, page) {
		if (err) return cb(err);
		inst.page = page;
		inst.locked = true;
		page.parentInstance = inst;
		inst.page.load(inst.user.url, opts);
		h.processMw(inst, h.users, req, res);
		inst.page.wait('idle', function(err) {
			if (err) return cb(err);
			inst.user.mtime = new Date();
			inst.output.call(inst, next);
			function next(err, str) {
				if (err) return cb(err);
				inst.user.data = str;
				inst.user.valid = true;
				// released by cache
				checkRelease(inst);
				cb();
			}
		});
	});
};

function acquire(url, cb) {
	var page = Dom.cache.get(url);
	if (page) return cb(null, page);
	Dom.pool.acquire(function(err, page) {
		if (err) return cb(err);
		Dom.cache.set(url, page);
		page.on('busy', function() {
			// busy page has bonus
			Dom.cache.get(url);
		});
		cb(null, page);
	});
}

function release(page, cb) {
	var inst = page.parentInstance;
	if (inst) {
		if (inst.locked) {
			inst.evict = true;
			return cb();
		}
		inst.evict = false;
		delete inst.page;
		delete page.parentInstance;
	}
	page.unload(function(err) {
		page.removeAllListeners();
		if (err) console.error(err);
		Dom.pool.release(page);
		cb(err);
	});
}

function checkRelease(inst) {
	inst.locked = false;
	if (inst.evict) {
		inst.evict = false;
		if (inst.page) release(inst.page, function(err) {
			if (err) console.error(err);
		});
	}
}

Handler.prototype.processMw = function(inst, mwObj, req, res) {
	var list = mwObj.before.concat(mwObj.current).concat(mwObj.after);
	for (var i=0; i < list.length; i++) {
		list[i](inst, req, res);
	}
};

function initPool(settings) {
	var opts = {};
	for (var prop in settings) opts[prop] = settings[prop];
	if (opts.debug) {
		if (opts.display != 0) {
			opts.display = 0;
			console.info("debug is on - using default display 0");
		}
		opts.max = 1;
	}
	if (!opts.name) opts.name = "webkitPool";
	if (!opts.create) opts.create = function(cb) {
		cb(null, WebKit(opts));
	};
	if (!opts.destroy) opts.destroy = function(client) {
		client.destroy();
	};
	if (!opts.max) opts.max = 1;
	var pool = Pool(opts);
	process.on('exit', function() {
		pool.drain(function() {
			pool.destroyAllNow();
		});
	});
	return pool;
}

function isRemote(url) {
	return /^https?:\/\//.test(url);
}

<commit_msg>Simplify pool to the point it works reliably<commit_after>var WebKit = require('webkitgtk');
var fs = require('fs');
var queue = require('queue-async');
var request = require('request');
var Path = require('path');

function Pool(cacheSize) {
	this.list = [];
	this.max = cacheSize;
	this.count = 0;
	this.queue = [];
}

Pool.prototype.acquire = function(cb) {
	var page;
	if (this.count < this.max) {
		this.count++;
		page = WebKit(Dom.settings);
		page.locked = true;
		this.list.push(page);
	} else for (var i=0; i < this.list.length; i++) {
		page = this.list[i];
		if (!page.locked) {
			page.locked = true;
			if (typeof page.unlock == "function") {
				page.unlock();
				page.removeAllListeners();
				delete page.unlock;
			}
			break;
		}
		page = null;
	}
	if (page) {
		cb(null, page);
	} else {
		this.queue.push(cb);
	}
};

Pool.prototype.unlock = function(page, unlockCb) {
	page.unlock = unlockCb;
	page.locked = false;
	setImmediate(this.process.bind(this));
};

Pool.prototype.release = function(page, cb) {
	page.unload(function(err) {
		if (page.unlock) {
			console.warn(new Error("page.unlock is set in Dom.pool.release"));
			delete page.unlock;
		}
		page.removeAllListeners();
		page.locked = false;
		cb();
		setImmediate(this.process.bind(this));
	}.bind(this));
};

Pool.prototype.process = function() {
	var next = this.queue.shift();
	if (next) this.acquire(next);
};

var Dom = module.exports = function(model, opts) {
	if (!Dom.pool) {
		Dom.pool = new Pool(Dom.settings.max);
		Dom.handlers = {}; // hash to store each view <-> middleware handler
		Dom.pages = {}; // store instances by url
	}
	var h = Dom.handlers[model];
	if (h) return h.chainable;
	var h = new Handler(model, opts);
	Dom.handlers[model] = h;
	return h.chainable;
};

Dom.Handler = Handler;

Dom.settings = {
	max: 16,
	display: process.env.DISPLAY || 0,
	style: fs.readFileSync(__dirname + '/index.css'),
	debug: !!process.env.DEBUG,
	console: !!process.env.DEBUG
};

Dom.plugins = require('./plugins');

Dom.authors = {
	before: [],
	current: [],
	after: []
};

Dom.users = {
	before: [Dom.plugins.nostylesheets],
	current: [],
	after: []
};

Dom.author = function(mw, position) {
	var list = Dom.authors;
	if (!position) position = 'current';
	if (!list[position]) throw new Error("Unknown position for express-dom author middleware", position);
	list[position].push(mw);
	return Dom;
};

Dom.use = function(mw, position) {
	var list = Dom.users;
	if (!position) position = 'current';
	if (!list[position]) throw new Error("Unknown position for express-dom user middleware", position);
	list[position].push(mw);
	return Dom;
};

function Handler(model, opts) {
	this.view = {};
	if (isRemote(model)) {
		this.view.url = model;
	} else {
		this.path = model; // app.get('statics') is available in middleware
	}
	this.opts = opts || {};
	this.chainable = this.middleware.bind(this);
	this.chainable.author = this.author.bind(this);
	this.chainable.use = this.use.bind(this);
	this.authors = {
		before: Dom.authors.before.slice(),
		current: Dom.authors.current.slice(),
		after: Dom.authors.after.slice()
	};
	this.users = {
		before: Dom.users.before.slice(),
		current: Dom.users.current.slice(),
		after: Dom.users.after.slice()
	};
	if (this.init) this.init(); // used by raja
}

Handler.prototype.author = function(mw, position) {
	var list = this.authors;
	if (!position) position = 'current';
	if (!list[position]) throw new Error("Unknown position for express-dom author middleware", position);
	list[position].push(mw);
	return this.chainable;
};

Handler.prototype.use = function(mw, position) {
	var list = this.users;
	if (!position) position = 'current';
	if (!list[position]) throw new Error("Unknown position for express-dom user middleware", position);
	list[position].push(mw);
	return this.chainable;
};

Handler.prototype.middleware = function(req, res, next) {
	var h = this;
	if (h.path !== undefined) {
		var path = h.path;
		delete h.path;
		var root = req.app.get('statics');
		if (!root) return next(new Error("Cannot find view, undefined 'statics' application setting"));
		if (!path) path = "index";
		var path = Path.resolve(root, path);
		if (path.indexOf(root) !== 0) return next(new Error("Path outside statics dir\n" + path));
		if (path.slice(-1) == "/") path += "index";
		if (Path.extname(path) != ".html") path += ".html";
		h.view.url = path;
	}
	var url = req.protocol + '://' + req.headers.host + req.url;
	if (url == h.view.url) {
		return next(new Error("The view has the same url as the requested page"));
	}
	h.instance(url, function(err, inst) {
		if (err) return next(err);
		h.build(inst, req, res, function(err) {
			if (err) return next(err);
			h.finish(inst, res);
		});
	});
};

Handler.prototype.build = function(inst, req, res, cb) {
	var h = this;
	queue(1)
	.defer(h.getView.bind(h), req)
	.defer(h.getAuthored.bind(h), inst, req, res)
	.defer(h.getUsed.bind(h), inst, req, res)
	.awaitAll(cb);
};

Handler.prototype.instance = function(url, cb) {
	var h = this;
	var inst = Dom.pages[url];
	if (!inst) inst = Dom.pages[url] = {
		author: {url: 'author:' + h.view.url},
		user: {url: url},
		output: function(page, cb) {
			page.html(cb);
		}
	};
	cb(null, inst);
};

Handler.prototype.finish = function(inst, res) {
	res.type('text/html');
	res.set('Last-Modified', inst.user.mtime.toUTCString());
	res.send(inst.user.data);
};

Handler.prototype.getView = function(req, cb) {
	var h = this;
	if (h.view.valid) {
		return cb();
	}
	var loader = isRemote(h.view.url) ? h.loadRemote : h.loadLocal;
	loader.call(h, h.view.url, function(err, body) {
		if (!err || body) {
			h.view.data = body;
			h.view.valid = true;
			h.view.mtime = new Date();
		}	else {
			if (!err) err = new Error("Empty initial html in " + h.view.url);
			if (!err.code || err.code == 'ENOENT') err.code = 404;
			else err.code = 500;
		}
		cb(err);
	});
};

Handler.prototype.loadLocal = fs.readFile;
Handler.prototype.loadRemote = function(url, cb) {
	request(url, function(err, res, body) {
		cb(err, body);
	});
};

Handler.prototype.getAuthored = function(inst, req, res, cb) {
	var h = this;
	if (inst.author.valid) {
		return cb();
	}
	if (h.authors.before.length || h.authors.current.length || h.authors.after.length) {
		Dom.pool.acquire(function(err, page) {
			if (err) return cb(err);
			inst.page = page;
			var obj = {
				content: h.view.data,
				console: true
			};
			if (!Dom.settings.debug) obj.style = Dom.settings.style;
			inst.page.preload(inst.user.url, obj);
			h.processMw(inst, h.authors, req, res);
			inst.page.wait('idle').html(function(err, html) {
				delete inst.page;
				Dom.pool.release(page, function(perr) {
					if (err) return cb(err);
					inst.author.data = html;
					inst.author.valid = true;
					inst.author.mtime = new Date();
					cb();
				});
			});
		});
	} else {
		inst.author.data = h.view.data;
		inst.author.valid = true;
		inst.author.mtime = new Date();
		cb();
	}
};

Handler.prototype.getUsed = function(inst, req, res, cb) {
	var h = this;
	if (inst.user.valid) return cb();
	var opts = {};
	for (var key in h.opts) {
		opts[key] = h.opts[key];
	}
	if (!opts.content) opts.content = inst.author.data;
	if (!opts.cookie) opts.cookie = req.get('Cookie');
	if (opts.console === undefined) opts.console = true;
	if (opts.images === undefined) opts.images = false;
	if (opts.style === undefined && !Dom.settings.debug) opts.style = Dom.settings.style;
	Dom.pool.acquire(function(err, page) {
		if (err) return cb(err);
		inst.page = page;
		inst.page.load(inst.user.url, opts);
		h.processMw(inst, h.users, req, res);
		inst.page.wait('idle', function(err) {
			if (err) return cb(err);
			inst.user.mtime = new Date();
			inst.output(inst.page, next);
			function next(err, str) {
				Dom.pool.unlock(inst.page, function() {
					delete this.page;
				}.bind(inst));
				if (err) return cb(err);
				inst.user.data = str;
				inst.user.valid = true;
				cb();
			}
		});
	});
};

Handler.prototype.processMw = function(inst, mwObj, req, res) {
	var list = mwObj.before.concat(mwObj.current).concat(mwObj.after);
	for (var i=0; i < list.length; i++) {
		list[i](inst, req, res);
	}
};

function isRemote(url) {
	return /^https?:\/\//.test(url);
}

<|endoftext|>


<commit_before>require('dotenv').config();

process.on('unhandledRejection', (error) => {
	logger.error(error.stack);
});

const {rtmClient, webClient} = require('./lib/slack.ts');
const {createEventAdapter} = require('@slack/events-api');
const {createMessageAdapter} = require('@slack/interactive-messages');
const fastify = require('fastify')({logger: true});
const logger = require('./lib/logger.js');

fastify.register(require('fastify-formbody'));

let word2vecInstalled = true;
try {
	require.resolve('word2vec');
} catch (e) {
	word2vecInstalled = false;
}

const plugins = [
	require('./ricochet-robots'),
];

const eventClient = createEventAdapter(process.env.SIGNING_SECRET);
const messageClient = createMessageAdapter(process.env.SIGNING_SECRET);
(async () => {
	await Promise.all(plugins.map(async (plugin) => {
		if (typeof plugin === 'function') {
			await plugin({rtmClient, webClient, eventClient, messageClient});
		}
		if (typeof plugin.default === 'function') {
			await plugin.default({rtmClient, webClient, eventClient, messageClient});
		}
		if (typeof plugin.server === 'function') {
			await fastify.register(plugin.server({rtmClient, webClient, eventClient, messageClient}));
		}
	}));

	logger.info('Launched');
	webClient.chat.postMessage({
		channel: process.env.CHANNEL_SANDBOX,
		text: 'ｼｭｯｼｭｯ (起動音)',
	});
})();

fastify.use('/slack-event', eventClient.expressMiddleware());
fastify.use('/slack-message', messageClient.requestListener());
fastify.listen(process.env.PORT || 21864);

let firstLogin = true;
rtmClient.on('authenticated', (data) => {
	logger.info(`Logged in as ${data.self.name} of team ${data.team.name}`);
	if (!firstLogin) {
		webClient.chat.postMessage({
			channel: process.env.CHANNEL_SANDBOX,
			text: '再接続しました',
		});
	}
	firstLogin = false;
});
rtmClient.start();
<commit_msg>restore requires for other bots, whcih was removed by mistake<commit_after>require('dotenv').config();

process.on('unhandledRejection', (error) => {
	logger.error(error.stack);
});

const {rtmClient, webClient} = require('./lib/slack.ts');
const {createEventAdapter} = require('@slack/events-api');
const {createMessageAdapter} = require('@slack/interactive-messages');
const fastify = require('fastify')({logger: true});
const logger = require('./lib/logger.js');

fastify.register(require('fastify-formbody'));

let word2vecInstalled = true;
try {
	require.resolve('word2vec');
} catch (e) {
	word2vecInstalled = false;
}

const plugins = [
	require('./mahjong'),
	require('./pocky'),
	require('./emoji-notifier'),
	require('./sushi-bot'),
	require('./shogi'),
	require('./tiobot'),
	require('./checkin'),
	require('./tahoiya'),
	require('./channel-notifier'),
	require('./prime'),
	require('./dajare'),
	require('./sunrise'),
	require('./ahokusa'),
	...(word2vecInstalled ? [require('./vocabwar')] : []),
	require('./ricochet-robots'),
	require('./scrapbox'),
	require('./slack-log'),
	require('./welcome'),
	require('./deploy'),
	require('./achievements'),
	require('./mail-hook'),
	require('./wordhero'),
	require('./wordhero/crossword'),
	require('./oauth'),
	require('./tunnel'),
	require('./voiperrobot'),
];

const eventClient = createEventAdapter(process.env.SIGNING_SECRET);
const messageClient = createMessageAdapter(process.env.SIGNING_SECRET);
(async () => {
	await Promise.all(plugins.map(async (plugin) => {
		if (typeof plugin === 'function') {
			await plugin({rtmClient, webClient, eventClient, messageClient});
		}
		if (typeof plugin.default === 'function') {
			await plugin.default({rtmClient, webClient, eventClient, messageClient});
		}
		if (typeof plugin.server === 'function') {
			await fastify.register(plugin.server({rtmClient, webClient, eventClient, messageClient}));
		}
	}));

	logger.info('Launched');
	webClient.chat.postMessage({
		channel: process.env.CHANNEL_SANDBOX,
		text: 'ｼｭｯｼｭｯ (起動音)',
	});
})();

fastify.use('/slack-event', eventClient.expressMiddleware());
fastify.use('/slack-message', messageClient.requestListener());
fastify.listen(process.env.PORT || 21864);

let firstLogin = true;
rtmClient.on('authenticated', (data) => {
	logger.info(`Logged in as ${data.self.name} of team ${data.team.name}`);
	if (!firstLogin) {
		webClient.chat.postMessage({
			channel: process.env.CHANNEL_SANDBOX,
			text: '再接続しました',
		});
	}
	firstLogin = false;
});
rtmClient.start();
<|endoftext|>


<commit_before>//! License: MIT Adapted (as stated in the LICENSE file)

var placeholder = "Search, drag and drop video, or paste its URL...";

var playImgSrc = "//cdn.rawgit.com/iconic/open-iconic/master/png/media-play-4x.png";
var removeImgSrc = "//cdn.rawgit.com/iconic/open-iconic/master/png/x-4x.png";

var videoUrl;
var videoName;
var videoTime = null;

var videos = [];
var videoCounter = 0;
var videoIteration = 0;
var bufferTime = 2000;
var waitEndTime = 1500;

var videoPaused;
var stayPaused;
var backRestart;

var loopTimer;
var progressTimer;

function Timer(callback, delay) {
  var id, started, remaining = delay, running;
  this.resume = function() {
    running = true;
    started = new Date();
    id = window.setTimeout(callback, remaining);
  }
  this.pause = function() {
    running = false;
    window.clearTimeout(id);
    remaining -= new Date() - started;
  }
  this.getTimeLeft = function() {
    if (running) {
      this.pause();
      this.resume();
    }
    return remaining;
  }
  this.getStateRunning = function() {
    return running;
  }
  this.resume();
}

Array.prototype.move = function(from, to) {
  this.splice(to, 0, this.splice(from, 1)[0]);
};

function msConversion(millis) {
  var minutes = Math.floor(millis / 60000);
  var seconds = ((millis % 60000) / 1000).toFixed(0);
  return minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
}

function highlight(i) {
  $("tr:nth-child(" + i + ")").attr("id", "newSelected");
  $("tr.selected").removeClass("selected");
  $("#newSelected").addClass("selected");
  $("#newSelected").removeAttr("id");
}

function addVideoToList(name, time) {
  name = decodeURIComponent(name);
  $("#videosTable").append("
" + name + "<button class=\"tableButton removeButton\" onclick=\"actionRemoveVideo(this);\"></button>" +
  "<button class=\"tableButton playButton\" onclick=\"actionPlayVideo(this);\"></button>	" + time + "
");
}

function resetTimer(which) {
  if (which != 0) {
      which.pause();
    }
    which = 0;
}

var ActionTimers = function() {
  this.pause = function() {
    loopTimer.pause();
    progressTimer.pause();
  }
  this.resume = function() {
    loopTimer.resume();
    progressTimer.resume();
  }
  this.clear = function() {
    resetTimer(loopTimer);
    resetTimer(progressTimer);
    $("#progress").css("width", "0%");
    $("#currentTime").text("0:00");
    $("#videoTime").text("0:00");
  }
}
var actionTimers = new ActionTimers();

function videoProgress() {
  var time = videos[videoIteration]["time"] * 1000;
  $("#videoTime").text(msConversion(time));
  function progressLoop() {
    var currentTime = (time + waitEndTime) - loopTimer.getTimeLeft();
    var currentPercent = (currentTime / time) * 100;
    progressTimer = new Timer(function() {
      if (currentTime > 0) {
        $("#progress").css("width", currentPercent + "%");
        $("#currentTime").text(msConversion(currentTime));
      }
      if (currentTime < time) {
        progressLoop();
      }
    }, 500);
  }
  progressLoop();
}

function playVideo() {
  highlight(videoIteration);
  document.title = "Streamly - " + decodeURIComponent(videos[videoIteration]["name"]);
  var embedUrl = videos[videoIteration]["url"];

  var autoplay = "";
  if (!stayPaused) {
    autoplay = "?autoplay=1";
  }

  if (embedUrl.search(/file:\/\//i) == -1) {
    embedUrl = "https://www.youtube.com/embed/" + videos[videoIteration]["url"] + autoplay;
  }
  $("#youtube").attr("src", embedUrl);

  backRestart = false;
  window.setTimeout(function() {
    backRestart = true;
  }, 3000);
}

function loopVideo() {
  videoIteration++;
  playVideo();
  loopTimer = new Timer(function() {
    if (videoIteration < videoCounter) {
      actionTimers.clear();
      loopVideo();
    }
    else {
      actionTimers.clear();
      $("#youtube").attr("src", "");
      if (videos[0] !== null && videos[0] !== undefined) {
        document.title = "Streamly - " + decodeURIComponent(videos[0]);
      }
      else {
        document.title = "Streamly";
      }
    }
  }, (videos[videoIteration]["time"] * 1000) + bufferTime + waitEndTime);

  videoProgress();

  if (stayPaused) {
    actionTimers.pause();
  }
}

function pauseVideo() {
  if (!videoPaused) {
    actionTimers.pause();
    videoPaused = true;
    if (videos[0] !== null && videos[0] !== undefined) {
      document.title = "Streamly - " + decodeURIComponent(videos[0]);
    }
  }
  else {
    actionTimers.resume();
    videoPaused = false;
    stayPaused = false;
    document.title = "Streamly - " + decodeURIComponent(videos[videoIteration]["name"]);
  }
  $("#pauseOverlay").css("display", "none");
  setTimeout(function(){
    $("#pauseOverlay").css("display", "block");
    $("#youtube").blur();
  }, 500);
}

function forwardVideo() {
  if (videoIteration + 1 <= videoCounter) {
    actionTimers.clear();

    if (videoPaused) {
      stayPaused = true;
    }
    loopVideo();
  }
}

function backVideo() {
  if (videoPaused) {
    stayPaused = true;
  }
  if (!backRestart) {
    if (videoIteration - 2 > -1) {
      videoIteration = videoIteration - 2;
      actionTimers.clear();
      loopVideo();
    }
  }
  else {
    videoIteration = videoIteration - 1;
    actionTimers.clear();
    loopVideo();
  }
}

function setPlaylist() {
  if (videos.length > 1) {
    var playlist = JSON.stringify(videos);
    playlist = window.btoa(playlist);
    playlist = encodeURIComponent(playlist);
    window.location.hash = playlist;
  }
  else {
    window.location.hash = "";
  }
}

function getPlaylist() {
  if (window.location.hash.substr(1) !== "") {
    var playlist = window.location.hash.substr(1);
    playlist = decodeURIComponent(playlist);
    playlist = window.atob(playlist);
    playlist = JSON.parse(playlist);
    videos = playlist;

    if (videos[0] != undefined) {
      $("#playlistNameBox").val(decodeURIComponent(videos[0]));
    }

    for (i = 1; i < videos.length; i++) {
      videoCounter = i;
      var printTime = msConversion(videos[videoCounter]["time"] * 1000);
      addVideoToList(videos[videoCounter]["name"], printTime);
    }
    loopVideo();
  }
}

function getVideoData() {
  $.ajax({
    url: videoUrl,
    type: 'GET',
    success: function(res) {
      var data = $(res.responseText);
      try {
        videoName = data.find("span#eow-title");
        videoName = videoName[0].textContent;
        videoName = $("
").html(videoName).text();
        videoName = videoName.trim();
      } catch(err) {
        videoName = prompt("Please enter the name of the video", "");
      }
      videoName = encodeURIComponent(videoName).replace(/%20/g, " ");
      try {
        videoTime = null;
        for (iteration in data) {
          var str = data[iteration].innerHTML;
          if (videoTime == null && typeof str != "undefined") {
            videoTime = str.match(/,"length_seconds":"\d+",/g);
          }
        }
        videoTime = videoTime[0];
        videoTime = videoTime.replace(/,"length_seconds":"/g, "").replace(/",/g, "");
        videoTime = +videoTime * 1000;
      } catch(err) {
        videoTime = prompt("Please enter the length of the video", "3:00");
        videoTime = videoTime.split(":");
        videoTime = (+videoTime[0]) * 60 + (+videoTime[1]);
        videoTime = videoTime * 1000;
      }
    },
    complete: function(jqXHR, textStatus) {
      $("#inputBox").val("").attr("placeholder", placeholder);
      addVideo();
    },
    error: function(jqXHR, textStatus, errorThrown) {
      videoName = prompt("Please enter the name of the video", "");

      videoTime = prompt("Please enter the length of the video", "3:00");
      videoTime = videoTime.split(":");
      videoTime = (+videoTime[0]) * 60 + (+videoTime[1]);
      videoTime = videoTime * 1000;
    }
  });
}

function addVideo() {
  videoCounter++;
  var video = {};
  video["name"] = videoName;
  video["time"] = videoTime / 1000;
  video["url"] = videoUrl.replace(/^htt(p|ps):\/\/www\.youtube\.com\/watch\?v=/i, "");
  videos[videoCounter] = video;

  var printTime = msConversion(videoTime);

  addVideoToList(videoName, printTime);

  setPlaylist();
  makeSortable();

  if (videoCounter == 1 || (loopTimer.getStateRunning() === false && !videoPaused)) {
    loopVideo();
  }
}

function actionPlayVideo(element) {
  var index = $(".playButton").index(element);
  videoIteration = index;
  if (videoPaused) {
    stayPaused = true;
  }
  actionTimers.clear();
  loopVideo();
}

function actionRemoveVideo(element) {
  var index = $(".removeButton").index(element) + 1;
  if (index == videoIteration) {
    if (videoIteration + 1 <= videoCounter) {
      forwardVideo();
      videoIteration--;
    }
    else {
      actionTimers.clear();
      $("#youtube").attr("src", "");
      document.title = "Streamly";
      videoIteration--;
    }
  }
  else if (index < videoIteration) {
    videoIteration--;
  }
  videoCounter--;
  videos.splice(index, 1);
  $("tr:nth-child(" + index + ")").remove();
  setPlaylist();
  makeSortable();
}

function actionMoveVideo(oldIndex, newIndex) {
  oldIndex++;
  newIndex++;
  videos.move(oldIndex, newIndex);
  if (oldIndex == videoIteration) {
    videoIteration = newIndex;
  }
}

function makeSortable() {
  $("#videosTable").sortable("destroy");
  var oldIndex;
  $("#videosTable").sortable({
    containerSelector: "table",
    itemPath: "> tbody",
    itemSelector: "tr",
    placeholder: "
",
    onDragStart: function ($item, container, _super) {
      oldIndex = $item.index();
    },
    onDrop: function ($item, container, _super) {
      actionMoveVideo(oldIndex, $item.index());
      setPlaylist();
    }
  });
}

function urlValidate(url) {
  var isValidYouTube = /^htt(p|ps):\/\/www\.youtube\.com\/watch\?v=.+$/i;
  var isValidFile = /^file:\/\/.+$/i;

  url = url.trim();
  url = url.replace(/&list=.+/g, "");

  if (url.search(isValidYouTube) > -1 || url.search(isValidFile) > -1) {
    return url;
  }
  else {
    return false;
  }
}

function input(type) {
  var inputBox = $("#inputBox").val();
  var playlistNameBox = $("#playlistNameBox").val();
  switch (type) {
    case 0:
      if (inputBox != "") {
        window.open("https://www.youtube.com/results?search_query=" + inputBox.replace(/ /g, "+"));
        $("#inputBox").val("").attr("placeholder", placeholder);
      }
      break;
    case 1:
      if (inputBox != "") {
        inputBox = urlValidate(inputBox);
        if (inputBox) {
          videoUrl = inputBox;
          $("#inputBox").val("").attr("placeholder", "Loading video data from YouTube...");
          getVideoData();
        }
        else {
          alert("That video's URL seems broken\n\nTry copying it again, or drag and drop the video directly");
        }
      }
      break;
    case 2:
      if (playlistNameBox != "") {
        videos[0] = encodeURIComponent(playlistNameBox).replace(/%20/g, " ");
        setPlaylist();
      }
      else {
        videos[0] = undefined;
      }
      break;
  }
}

document.addEventListener("drop", function(event) {
  event.preventDefault();
  var data = event.dataTransfer.getData("URL");

  $("#inputBox").val(data);
  input(1);
});

document.addEventListener("dragover", function(event) {
  event.preventDefault();
});
<commit_msg>remove the instructions link when user adds a video<commit_after>//! License: MIT Adapted (as stated in the LICENSE file)

var placeholder = "Search, drag and drop video, or paste its URL...";

var playImgSrc = "//cdn.rawgit.com/iconic/open-iconic/master/png/media-play-4x.png";
var removeImgSrc = "//cdn.rawgit.com/iconic/open-iconic/master/png/x-4x.png";

var videoUrl;
var videoName;
var videoTime = null;

var videos = [];
var videoCounter = 0;
var videoIteration = 0;
var bufferTime = 2000;
var waitEndTime = 1500;

var videoPaused;
var stayPaused;
var backRestart;

var loopTimer;
var progressTimer;

function Timer(callback, delay) {
  var id, started, remaining = delay, running;
  this.resume = function() {
    running = true;
    started = new Date();
    id = window.setTimeout(callback, remaining);
  }
  this.pause = function() {
    running = false;
    window.clearTimeout(id);
    remaining -= new Date() - started;
  }
  this.getTimeLeft = function() {
    if (running) {
      this.pause();
      this.resume();
    }
    return remaining;
  }
  this.getStateRunning = function() {
    return running;
  }
  this.resume();
}

Array.prototype.move = function(from, to) {
  this.splice(to, 0, this.splice(from, 1)[0]);
};

function msConversion(millis) {
  var minutes = Math.floor(millis / 60000);
  var seconds = ((millis % 60000) / 1000).toFixed(0);
  return minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
}

function highlight(i) {
  $("tr:nth-child(" + i + ")").attr("id", "newSelected");
  $("tr.selected").removeClass("selected");
  $("#newSelected").addClass("selected");
  $("#newSelected").removeAttr("id");
}

function addVideoToList(name, time) {
  name = decodeURIComponent(name);
  $("#videosTable").append("
" + name + "<button class=\"tableButton removeButton\" onclick=\"actionRemoveVideo(this);\"></button>" +
  "<button class=\"tableButton playButton\" onclick=\"actionPlayVideo(this);\"></button>	" + time + "
");
}

function resetTimer(which) {
  if (which != 0) {
      which.pause();
    }
    which = 0;
}

var ActionTimers = function() {
  this.pause = function() {
    loopTimer.pause();
    progressTimer.pause();
  }
  this.resume = function() {
    loopTimer.resume();
    progressTimer.resume();
  }
  this.clear = function() {
    resetTimer(loopTimer);
    resetTimer(progressTimer);
    $("#progress").css("width", "0%");
    $("#currentTime").text("0:00");
    $("#videoTime").text("0:00");
  }
}
var actionTimers = new ActionTimers();

function videoProgress() {
  var time = videos[videoIteration]["time"] * 1000;
  $("#videoTime").text(msConversion(time));
  function progressLoop() {
    var currentTime = (time + waitEndTime) - loopTimer.getTimeLeft();
    var currentPercent = (currentTime / time) * 100;
    progressTimer = new Timer(function() {
      if (currentTime > 0) {
        $("#progress").css("width", currentPercent + "%");
        $("#currentTime").text(msConversion(currentTime));
      }
      if (currentTime < time) {
        progressLoop();
      }
    }, 500);
  }
  progressLoop();
}

function playVideo() {
  highlight(videoIteration);
  document.title = "Streamly - " + decodeURIComponent(videos[videoIteration]["name"]);
  var embedUrl = videos[videoIteration]["url"];

  var autoplay = "";
  if (!stayPaused) {
    autoplay = "?autoplay=1";
  }

  if (embedUrl.search(/file:\/\//i) == -1) {
    embedUrl = "https://www.youtube.com/embed/" + videos[videoIteration]["url"] + autoplay;
  }
  $("#youtube").attr("src", embedUrl);

  backRestart = false;
  window.setTimeout(function() {
    backRestart = true;
  }, 3000);
}

function loopVideo() {
  videoIteration++;
  playVideo();
  loopTimer = new Timer(function() {
    if (videoIteration < videoCounter) {
      actionTimers.clear();
      loopVideo();
    }
    else {
      actionTimers.clear();
      $("#youtube").attr("src", "");
      if (videos[0] !== null && videos[0] !== undefined) {
        document.title = "Streamly - " + decodeURIComponent(videos[0]);
      }
      else {
        document.title = "Streamly";
      }
    }
  }, (videos[videoIteration]["time"] * 1000) + bufferTime + waitEndTime);

  videoProgress();

  if (stayPaused) {
    actionTimers.pause();
  }
}

function pauseVideo() {
  if (!videoPaused) {
    actionTimers.pause();
    videoPaused = true;
    if (videos[0] !== null && videos[0] !== undefined) {
      document.title = "Streamly - " + decodeURIComponent(videos[0]);
    }
  }
  else {
    actionTimers.resume();
    videoPaused = false;
    stayPaused = false;
    document.title = "Streamly - " + decodeURIComponent(videos[videoIteration]["name"]);
  }
  $("#pauseOverlay").css("display", "none");
  setTimeout(function(){
    $("#pauseOverlay").css("display", "block");
    $("#youtube").blur();
  }, 500);
}

function forwardVideo() {
  if (videoIteration + 1 <= videoCounter) {
    actionTimers.clear();

    if (videoPaused) {
      stayPaused = true;
    }
    loopVideo();
  }
}

function backVideo() {
  if (videoPaused) {
    stayPaused = true;
  }
  if (!backRestart) {
    if (videoIteration - 2 > -1) {
      videoIteration = videoIteration - 2;
      actionTimers.clear();
      loopVideo();
    }
  }
  else {
    videoIteration = videoIteration - 1;
    actionTimers.clear();
    loopVideo();
  }
}

function setPlaylist() {
  if (videos.length > 1) {
    var playlist = JSON.stringify(videos);
    playlist = window.btoa(playlist);
    playlist = encodeURIComponent(playlist);
    window.location.hash = playlist;
  }
  else {
    window.location.hash = "";
  }
}

function getPlaylist() {
  if (window.location.hash.substr(1) !== "") {
    var playlist = window.location.hash.substr(1);
    playlist = decodeURIComponent(playlist);
    playlist = window.atob(playlist);
    playlist = JSON.parse(playlist);
    videos = playlist;

    if (videos[0] != undefined) {
      $("#playlistNameBox").val(decodeURIComponent(videos[0]));
    }

    for (i = 1; i < videos.length; i++) {
      videoCounter = i;
      var printTime = msConversion(videos[videoCounter]["time"] * 1000);
      addVideoToList(videos[videoCounter]["name"], printTime);
    }
    loopVideo();
  }
}

function getVideoData() {
  $.ajax({
    url: videoUrl,
    type: 'GET',
    success: function(res) {
      var data = $(res.responseText);
      try {
        videoName = data.find("span#eow-title");
        videoName = videoName[0].textContent;
        videoName = $("
").html(videoName).text();
        videoName = videoName.trim();
      } catch(err) {
        videoName = prompt("Please enter the name of the video", "");
      }
      videoName = encodeURIComponent(videoName).replace(/%20/g, " ");
      try {
        videoTime = null;
        for (iteration in data) {
          var str = data[iteration].innerHTML;
          if (videoTime == null && typeof str != "undefined") {
            videoTime = str.match(/,"length_seconds":"\d+",/g);
          }
        }
        videoTime = videoTime[0];
        videoTime = videoTime.replace(/,"length_seconds":"/g, "").replace(/",/g, "");
        videoTime = +videoTime * 1000;
      } catch(err) {
        videoTime = prompt("Please enter the length of the video", "3:00");
        videoTime = videoTime.split(":");
        videoTime = (+videoTime[0]) * 60 + (+videoTime[1]);
        videoTime = videoTime * 1000;
      }
    },
    complete: function(jqXHR, textStatus) {
      $("#inputBox").val("").attr("placeholder", placeholder);
      addVideo();
    },
    error: function(jqXHR, textStatus, errorThrown) {
      videoName = prompt("Please enter the name of the video", "");

      videoTime = prompt("Please enter the length of the video", "3:00");
      videoTime = videoTime.split(":");
      videoTime = (+videoTime[0]) * 60 + (+videoTime[1]);
      videoTime = videoTime * 1000;
    }
  });
}

function addVideo() {
  videoCounter++;
  var video = {};
  video["name"] = videoName;
  video["time"] = videoTime / 1000;
  video["url"] = videoUrl.replace(/^htt(p|ps):\/\/www\.youtube\.com\/watch\?v=/i, "");
  videos[videoCounter] = video;

  var printTime = msConversion(videoTime);

  addVideoToList(videoName, printTime);

  setPlaylist();
  makeSortable();

  $("#gettingStarted").remove();

  if (videoCounter == 1 || (loopTimer.getStateRunning() === false && !videoPaused)) {
    loopVideo();
  }
}

function actionPlayVideo(element) {
  var index = $(".playButton").index(element);
  videoIteration = index;
  if (videoPaused) {
    stayPaused = true;
  }
  actionTimers.clear();
  loopVideo();
}

function actionRemoveVideo(element) {
  var index = $(".removeButton").index(element) + 1;
  if (index == videoIteration) {
    if (videoIteration + 1 <= videoCounter) {
      forwardVideo();
      videoIteration--;
    }
    else {
      actionTimers.clear();
      $("#youtube").attr("src", "");
      document.title = "Streamly";
      videoIteration--;
    }
  }
  else if (index < videoIteration) {
    videoIteration--;
  }
  videoCounter--;
  videos.splice(index, 1);
  $("tr:nth-child(" + index + ")").remove();
  setPlaylist();
  makeSortable();
}

function actionMoveVideo(oldIndex, newIndex) {
  oldIndex++;
  newIndex++;
  videos.move(oldIndex, newIndex);
  if (oldIndex == videoIteration) {
    videoIteration = newIndex;
  }
}

function makeSortable() {
  $("#videosTable").sortable("destroy");
  var oldIndex;
  $("#videosTable").sortable({
    containerSelector: "table",
    itemPath: "> tbody",
    itemSelector: "tr",
    placeholder: "
",
    onDragStart: function ($item, container, _super) {
      oldIndex = $item.index();
    },
    onDrop: function ($item, container, _super) {
      actionMoveVideo(oldIndex, $item.index());
      setPlaylist();
    }
  });
}

function urlValidate(url) {
  var isValidYouTube = /^htt(p|ps):\/\/www\.youtube\.com\/watch\?v=.+$/i;
  var isValidFile = /^file:\/\/.+$/i;

  url = url.trim();
  url = url.replace(/&list=.+/g, "");

  if (url.search(isValidYouTube) > -1 || url.search(isValidFile) > -1) {
    return url;
  }
  else {
    return false;
  }
}

function input(type) {
  var inputBox = $("#inputBox").val();
  var playlistNameBox = $("#playlistNameBox").val();
  switch (type) {
    case 0:
      if (inputBox != "") {
        window.open("https://www.youtube.com/results?search_query=" + inputBox.replace(/ /g, "+"));
        $("#inputBox").val("").attr("placeholder", placeholder);
      }
      break;
    case 1:
      if (inputBox != "") {
        inputBox = urlValidate(inputBox);
        if (inputBox) {
          videoUrl = inputBox;
          $("#inputBox").val("").attr("placeholder", "Loading video data from YouTube...");
          getVideoData();
        }
        else {
          alert("That video's URL seems broken\n\nTry copying it again, or drag and drop the video directly");
        }
      }
      break;
    case 2:
      if (playlistNameBox != "") {
        videos[0] = encodeURIComponent(playlistNameBox).replace(/%20/g, " ");
        setPlaylist();
      }
      else {
        videos[0] = undefined;
      }
      break;
  }
}

document.addEventListener("drop", function(event) {
  event.preventDefault();
  var data = event.dataTransfer.getData("URL");

  $("#inputBox").val(data);
  input(1);
});

document.addEventListener("dragover", function(event) {
  event.preventDefault();
});
<|endoftext|>