<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

namespace("sozi.editor.view", function (exports) {
    "use strict";

    var h = require("virtual-dom/h");
    var createElement = require("virtual-dom/create-element");
    var diff = require("virtual-dom/diff");
    var patch = require("virtual-dom/patch");

    /*
     * The Timeline view shows a table where columns represent frames
     * and rows represent layers.
     * The user can choose to manipulate only a subset of the layers of the document.
     * The other layers are grouped into a "Default" row.
     *
     * The Timeline view allows the user to:
     *  - choose editable layers from the document
     *  - show/hide layers
     *  - create/delete/reorder frames
     *  - add/remove layers and frames to/from the selection
     *
     * See also:
     *  - templates/sozi.editor.Timeline.html
     */
    exports.Timeline = {

        /*
         * Initialize a Timeline view.
         *
         * The view is rendered and event handlers are set up.
         *
         * Parameters:
         *  - presentation: a presentation object
         *  - selection: a selection object
         *
         * Returns:
         *  - The current view
         */
        init: function (container, presentation, selection, controller) {
            this.presentation = presentation;
            this.selection = selection;
            this.controller = controller;
            this.editableLayers = [];
            this.defaultLayers = [];

            this.vtree = h("div");
            this.rootNode = createElement(this.vtree, {document: document});
            container.appendChild(this.rootNode);

            controller.addListener("repaint", this.repaint.bind(this));
            controller.addListener("load", this.onLoad.bind(this));

            window.addEventListener("resize", this.repaint.bind(this), false);

            return this;
        },

        onLoad: function () {
            this.presentation.layers.forEach(function (layer) {
                if (this.editableLayers.indexOf(layer) < 0) {
                    this.defaultLayers.push(layer);
                }
            }, this);
        },

        toStorable: function () {
            return {
                editableLayers: this.editableLayers.map(function (layer) {
                    return layer.groupId;
                })
            };
        },

        fromStorable: function (storable) {
            if ("editableLayers" in storable) {
                storable.editableLayers.forEach(function (groupId) {
                    var layer = this.presentation.getLayerWithId(groupId);
                    if (layer) {
                        this.editableLayers.push(layer);
                    }
                }, this);
            }
            return this;
        },

        /*
         * Action: Add a layer to the current view.
         *
         * This method is called as a result of a user action
         * in the current view.
         *
         * The layer at the given index is added to the current view
         * and to the selection.
         * The current view is updated.
         *
         * Parameters:
         *  - layerIndex: The index of a layer in the presentation
         */
        addLayer: function (layerIndex) {
            var layer = this.presentation.layers[layerIndex];
            this.editableLayers.push(layer);

            var layerIndexInDefaults = this.defaultLayers.indexOf(layer);
            this.defaultLayers.splice(layerIndexInDefaults, 1);

            this.controller.addLayerToSelection(layer);

            // Force a repaint even if the controller
            // did not modify the selection
            this.repaint();
        },

        /*
         * Action: Remove a layer from the current view.
         *
         * This method is called as a result of a user action
         * in the current view.
         *
         * The layer at the given index is removed from the current view
         * and from the selection.
         * The current view is updated.
         *
         * Parameters:
         *  - layerIndex: The index of a layer in the presentation
         */
        removeLayer: function (layerIndex) {
            var layer = this.presentation.layers[layerIndex];

            var layerIndexInEditable = this.editableLayers.indexOf(layer);
            this.editableLayers.splice(layerIndexInEditable, 1);

            if (this.defaultLayersAreSelected) {
                this.controller.addLayerToSelection(layer);
            }
            else if (this.selection.selectedLayers.length > 1) {
                this.controller.removeLayerFromSelection(layer);
            }
            else {
                this.controller.selectLayers(this.defaultLayers);
            }

            this.defaultLayers.push(layer);

            // Force a repaint even if the controller
            // did not modify the selection
            this.repaint();
        },

        toggleLayerVisibility: function (layerIndex, evt) {
            this.controller.updateLayerVisibility(this.getLayersAtIndex(layerIndex));
            evt.stopPropagation();
        },

        getLayersAtIndex: function (layerIndex) {
            return layerIndex >= 0 ?
                [this.presentation.layers[layerIndex]] :
                this.defaultLayers;
        },

        /*
         * Check whether "default" layers are visible.
         *
         * Returns:
         *  - true if at least one "default" layer is visible, else false
         */
        get defaultLayersAreVisible() {
            return this.defaultLayers.some(function (layer) {
                return layer.isVisible;
            });
        },

        /*
         * Check whether "default" layers are selected.
         *
         * Returns:
         *  - true if all "default" layers are selected, else false
         */
        get defaultLayersAreSelected() {
            return this.defaultLayers.every(function (layer) {
                return this.selection.selectedLayers.indexOf(layer) >= 0;
            }, this);
        },

        updateFrameSelection: function (frameIndex, evt) {
            this.controller.updateFrameSelection(evt.ctrlKey, evt.shiftKey, frameIndex);
            evt.stopPropagation();
        },

        updateLayerSelection: function (layerIndex, evt) {
            this.controller.updateLayerSelection(evt.ctrlKey, evt.shiftKey, this.getLayersAtIndex(layerIndex));
            evt.stopPropagation();
        },

        updateLayerAndFrameSelection: function (layerIndex, frameIndex, evt) {
            this.controller.updateLayerAndFrameSelection(evt.ctrlKey, evt.shiftKey, this.getLayersAtIndex(layerIndex), frameIndex);
            evt.stopPropagation();
        },

        defaultLayerIsNotEmpty: function () {
            return this.defaultLayers.length > 1 || this.defaultLayers[0].svgNodes.length;
        },

        repaint: function () {
            var vtree = this.render();
            this.rootNode = patch(this.rootNode, diff(this.vtree, vtree));
            this.vtree = vtree;

            this.adjust();
        },

        render: function () {
            var self = this;
            return h("div", [
                h("div.timeline-top-left", [
                    h("table.timeline", [
                        h("tr", [
                            h("th",
                                h("button", {
                                    title: "Delete selected frames",
                                    disabled: this.selection.selectedFrames.length ? undefined : "disabled",
                                    onclick: this.controller.deleteFrames.bind(this.controller)
                                },
                                    h("i.fa.fa-trash"))),
                            h("th",
                                h("button", {
                                    title: "Create a new frame",
                                    onclick: this.controller.addFrame.bind(this.controller)
                                },
                                    h("i.fa.fa-plus"))),
                        ]),
                        h("tr",
                            h("th", {colspan: 2},
                                h("select", {
                                    onchange: function () {
                                        var value = this.value;
                                        this.value = "__add__";
                                        self.addLayer(value);
                                    }
                                }, [
                                    h("option", {value: "__add__", selected: "selected"}, "Add layer")
                                ].concat(
                                    this.presentation.layers.slice().reverse()
                                        .filter(function (layer) {
                                            return !layer.auto && this.defaultLayers.indexOf(layer) >= 0;
                                        }, this)
                                        .map(function (layer) {
                                            return h("option", {value: layer.index}, layer.label);
                                        })
                                ))
                            )
                        )
                    ])
                ]),
                h("div.timeline-bottom-left", [
                    h("table.timeline", (this.defaultLayerIsNotEmpty ? [
                        h("tr", [
                            h("th.layer-icons", [
                                this.defaultLayersAreVisible ?
                                    h("i.visibility.fa.fa-eye", {
                                        title: "This layer is visible. Click to hide it.",
                                        onclick: this.toggleLayerVisibility.bind(this, -1)
                                    }) :
                                    h("i.visibility.fa.fa-eye-slash", {
                                        title: "This layer is hidden. Click to show it.",
                                        onclick: this.toggleLayerVisibility.bind(this, -1)
                                    }),
                                h("i.remove.fa.fa-times", {style: {visibility: "hidden"}})
                            ]),
                            h("th", {
                                className: "layer-label" + (this.defaultLayersAreSelected ? " selected" : ""),
                                onclick: this.updateLayerSelection.bind(this, -1)
                            }, "Default")
                        ]),
                    ] : []).concat(
                        this.presentation.layers.slice().reverse()
                            .filter(function (layer) {
                                return this.editableLayers.indexOf(layer) >= 0;
                            }, this)
                            .map(function (layer) {
                                return h("tr", [
                                    h("th.layer-icons", [
                                        layer.isVisible ?
                                            h("i.visibility.fa.fa-eye", {
                                                title: "This layer is visible. Click to hide it.",
                                                onclick: this.toggleLayerVisibility.bind(this, layer.index)
                                            }) :
                                            h("i.visibility.fa.fa-eye-slash", {
                                                title: "This layer is hidden. Click to show it.",
                                                onclick: this.toggleLayerVisibility.bind(this, layer.index)
                                            }),
                                        h("i.remove.fa.fa-times", {
                                            title: "Remove this layer",
                                            onclick: this.removeLayer.bind(this, layer.index)
                                        })
                                    ]),
                                    h("th", {
                                        className: "layer-label" + (this.selection.selectedLayers.indexOf(layer) >= 0 ? " selected" : ""),
                                        onclick: this.updateLayerSelection.bind(this, layer.index)
                                    }, layer.label)
                                ]);
                            }, this)
                    ).concat([
                        h("tr", {style: {visibility: "collapse"}}, [
                            h("th.layer-icons"),
                            h("th.layer-label", "Default")
                        ])
                    ]))
                ]),
                h("div.timeline-top-right", [
                    h("table.timeline", [
                        h("tr", this.presentation.frames.map(function (frame, frameIndex) {
                            return h("th", {
                                className: "frame-index" +
                                    (this.selection.selectedFrames.indexOf(frame) >= 0 ? " selected" : "") +
                                    (frame === this.selection.currentFrame ? " current" : ""),
                                onclick: this.updateFrameSelection.bind(this, frameIndex)
                            }, [
                                h("i.insert-before.fa.fa-arrow-circle-down", {
                                    title: "Insert selection before frame " + frameIndex,
                                    onclick: function (evt) {
                                        self.controller.moveFrames(frameIndex);
                                        evt.stopPropagation();
                                    }
                                }),
                                h("i.insert-after.fa.fa-arrow-circle-down", {
                                    title: "Insert selection after frame " + frameIndex,
                                    onclick: function (evt) {
                                        self.controller.moveFrames(frameIndex + 1);
                                        evt.stopPropagation();
                                    }
                                }),
                                frameIndex.toString()
                            ]);
                        }, this)),
                        h("tr", this.presentation.frames.map(function (frame, frameIndex) {
                            return h("th", {
                                title: frame.title,
                                className: "frame-title" +
                                    (this.selection.selectedFrames.indexOf(frame) >= 0 ? " selected" : "") +
                                    (frame === this.selection.currentFrame ? " current" : ""),
                                onclick: this.updateFrameSelection.bind(this, frameIndex)
                            }, frame.title);
                        }, this))
                    ])
                ]),
                h("div.timeline-bottom-right", {
                    onscroll: function (evt) {
                        self.rootNode.querySelector(".timeline-top-right").scrollLeft = this.scrollLeft;
                        self.rootNode.querySelector(".timeline-bottom-left").scrollTop = this.scrollTop;
                    }
                }, [
                    h("table.timeline", (this.defaultLayerIsNotEmpty ? [
                        h("tr",
                            this.presentation.frames.map(function (frame, frameIndex) {
                                return h("td", {
                                    className:
                                        (this.defaultLayersAreSelected && this.selection.selectedFrames.indexOf(frame) >= 0 ? "selected" : "") +
                                        (frame === this.selection.currentFrame ? " current" : ""),
                                    onclick: this.updateLayerAndFrameSelection.bind(this, -1, frameIndex)
                                });
                            }, this)
                        )
                    ] : []).concat(
                        this.presentation.layers.slice().reverse()
                            .filter(function (layer) {
                                return this.editableLayers.indexOf(layer) >= 0;
                            }, this)
                            .map(function (layer) {
                                return h("tr",
                                    this.presentation.frames.map(function (frame, frameIndex) {
                                        return h("td", {
                                            className:
                                                (this.selection.selectedLayers.indexOf(layer) >= 0 && this.selection.selectedFrames.indexOf(frame) >= 0 ? "selected" : "") +
                                                (frame === this.selection.currentFrame ? " current" : ""),
                                            onclick: this.updateLayerAndFrameSelection.bind(this, layer.index, frameIndex)
                                        }, frame.layerProperties[layer.index].link ? h("i.fa.fa-link") : []);
                                    }, this)
                                );
                            }, this)
                    ).concat([
                        h("tr", {style: {visibility: "collapse"}},
                            this.presentation.frames.map(function (frame) {
                                return h("td", frame.title);
                            })
                        )
                    ]))
                ])
            ]);
        },

        /*
         * Render the current view.
         *
         * The timeline is rendered with respect to the current state of
         * the presentation and the selection.
         * Event handlers are attached to UI elements.
         */
        adjust: function () {
            var topLeft = this.rootNode.querySelector(".timeline-top-left");
            var topRight = this.rootNode.querySelector(".timeline-top-right");
            var bottomLeft = this.rootNode.querySelector(".timeline-bottom-left");
            var bottomRight = this.rootNode.querySelector(".timeline-bottom-right");

            var topLeftTable = topLeft.querySelector("table");
            var topRightTable = topRight.querySelector("table");
            var bottomLeftTable = bottomLeft.querySelector("table");

            var leftWidth = Math.max(topLeftTable.clientWidth, bottomLeftTable.clientWidth);
            var rightWidth = this.rootNode.clientWidth - leftWidth;
            var topHeight = Math.max(topLeftTable.clientHeight, topRightTable.clientHeight);
            var bottomHeight = this.rootNode.clientHeight - topHeight;

            // Fit the width of the left tables,
            // allocate remaining width to the right tables
            topLeft.style.width = bottomLeft.style.width =
            topLeftTable.style.width = bottomLeftTable.style.width = leftWidth + "px";
            topRight.style.width = bottomRight.style.width = rightWidth + "px";

            // Fit the height of the top tables,
            // allocate remaining width to the bottom tables
            topLeft.style.height = topRight.style.height = topHeight + "px";
            bottomLeft.style.height = bottomRight.style.height = bottomHeight + "px";

            // Corresponding rows in left and right tables must have the same height
            var leftRows  = Array.prototype.slice.call(topLeft.querySelectorAll("tr")).concat(Array.prototype.slice.call(bottomLeft.querySelectorAll("tr")));
            var rightRows = Array.prototype.slice.call(topRight.querySelectorAll("tr")).concat(Array.prototype.slice.call(bottomRight.querySelectorAll("tr")));
            leftRows.forEach(function (leftRow, rowIndex) {
                var rightRow = rightRows[rowIndex];
                var maxHeight = Math.max(leftRow.clientHeight, rightRow.clientHeight);
                leftRow.style.height = rightRow.style.height = maxHeight + "px";
            });
        }
    };
});
<commit_msg>Fixed "default" layer visibility in timeline view<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

namespace("sozi.editor.view", function (exports) {
    "use strict";

    var h = require("virtual-dom/h");
    var createElement = require("virtual-dom/create-element");
    var diff = require("virtual-dom/diff");
    var patch = require("virtual-dom/patch");

    /*
     * The Timeline view shows a table where columns represent frames
     * and rows represent layers.
     * The user can choose to manipulate only a subset of the layers of the document.
     * The other layers are grouped into a "Default" row.
     *
     * The Timeline view allows the user to:
     *  - choose editable layers from the document
     *  - show/hide layers
     *  - create/delete/reorder frames
     *  - add/remove layers and frames to/from the selection
     *
     * See also:
     *  - templates/sozi.editor.Timeline.html
     */
    exports.Timeline = {

        /*
         * Initialize a Timeline view.
         *
         * The view is rendered and event handlers are set up.
         *
         * Parameters:
         *  - presentation: a presentation object
         *  - selection: a selection object
         *
         * Returns:
         *  - The current view
         */
        init: function (container, presentation, selection, controller) {
            this.presentation = presentation;
            this.selection = selection;
            this.controller = controller;
            this.editableLayers = [];
            this.defaultLayers = [];

            this.vtree = h("div");
            this.rootNode = createElement(this.vtree, {document: document});
            container.appendChild(this.rootNode);

            controller.addListener("repaint", this.repaint.bind(this));
            controller.addListener("load", this.onLoad.bind(this));

            window.addEventListener("resize", this.repaint.bind(this), false);

            return this;
        },

        onLoad: function () {
            this.presentation.layers.forEach(function (layer) {
                if (this.editableLayers.indexOf(layer) < 0) {
                    this.defaultLayers.push(layer);
                }
            }, this);
        },

        toStorable: function () {
            return {
                editableLayers: this.editableLayers.map(function (layer) {
                    return layer.groupId;
                })
            };
        },

        fromStorable: function (storable) {
            if (storable.hasOwnProperty("editableLayers")) {
                storable.editableLayers.forEach(function (groupId) {
                    var layer = this.presentation.getLayerWithId(groupId);
                    if (layer) {
                        this.editableLayers.push(layer);
                    }
                }, this);
            }
            return this;
        },

        /*
         * Action: Add a layer to the current view.
         *
         * This method is called as a result of a user action
         * in the current view.
         *
         * The layer at the given index is added to the current view
         * and to the selection.
         * The current view is updated.
         *
         * Parameters:
         *  - layerIndex: The index of a layer in the presentation
         */
        addLayer: function (layerIndex) {
            var layer = this.presentation.layers[layerIndex];
            this.editableLayers.push(layer);

            var layerIndexInDefaults = this.defaultLayers.indexOf(layer);
            this.defaultLayers.splice(layerIndexInDefaults, 1);

            this.controller.addLayerToSelection(layer);

            // Force a repaint even if the controller
            // did not modify the selection
            this.repaint();
        },

        /*
         * Action: Remove a layer from the current view.
         *
         * This method is called as a result of a user action
         * in the current view.
         *
         * The layer at the given index is removed from the current view
         * and from the selection.
         * The current view is updated.
         *
         * Parameters:
         *  - layerIndex: The index of a layer in the presentation
         */
        removeLayer: function (layerIndex) {
            var layer = this.presentation.layers[layerIndex];

            var layerIndexInEditable = this.editableLayers.indexOf(layer);
            this.editableLayers.splice(layerIndexInEditable, 1);

            if (this.defaultLayersAreSelected) {
                this.controller.addLayerToSelection(layer);
            }
            else if (this.selection.selectedLayers.length > 1) {
                this.controller.removeLayerFromSelection(layer);
            }
            else {
                this.controller.selectLayers(this.defaultLayers);
            }

            this.defaultLayers.push(layer);

            // Force a repaint even if the controller
            // did not modify the selection
            this.repaint();
        },

        toggleLayerVisibility: function (layerIndex, evt) {
            this.controller.updateLayerVisibility(this.getLayersAtIndex(layerIndex));
            evt.stopPropagation();
        },

        getLayersAtIndex: function (layerIndex) {
            return layerIndex >= 0 ?
                [this.presentation.layers[layerIndex]] :
                this.defaultLayers;
        },

        /*
         * Check whether "default" layers are visible.
         *
         * Returns:
         *  - true if at least one "default" layer is visible, else false
         */
        get defaultLayersAreVisible() {
            return this.defaultLayers.some(function (layer) {
                return layer.isVisible;
            });
        },

        /*
         * Check whether "default" layers are selected.
         *
         * Returns:
         *  - true if all "default" layers are selected, else false
         */
        get defaultLayersAreSelected() {
            return this.defaultLayers.every(function (layer) {
                return this.selection.selectedLayers.indexOf(layer) >= 0;
            }, this);
        },

        updateFrameSelection: function (frameIndex, evt) {
            this.controller.updateFrameSelection(evt.ctrlKey, evt.shiftKey, frameIndex);
            evt.stopPropagation();
        },

        updateLayerSelection: function (layerIndex, evt) {
            this.controller.updateLayerSelection(evt.ctrlKey, evt.shiftKey, this.getLayersAtIndex(layerIndex));
            evt.stopPropagation();
        },

        updateLayerAndFrameSelection: function (layerIndex, frameIndex, evt) {
            this.controller.updateLayerAndFrameSelection(evt.ctrlKey, evt.shiftKey, this.getLayersAtIndex(layerIndex), frameIndex);
            evt.stopPropagation();
        },

        get defaultLayerIsNotEmpty() {
            return this.defaultLayers.length > 1 || this.defaultLayers[0].svgNodes.length;
        },

        repaint: function () {
            var vtree = this.render();
            this.rootNode = patch(this.rootNode, diff(this.vtree, vtree));
            this.vtree = vtree;

            this.adjust();
        },

        render: function () {
            var self = this;
            return h("div", [
                h("div.timeline-top-left", [
                    h("table.timeline", [
                        h("tr", [
                            h("th",
                                h("button", {
                                    title: "Delete selected frames",
                                    disabled: this.selection.selectedFrames.length ? undefined : "disabled",
                                    onclick: this.controller.deleteFrames.bind(this.controller)
                                },
                                    h("i.fa.fa-trash"))),
                            h("th",
                                h("button", {
                                    title: "Create a new frame",
                                    onclick: this.controller.addFrame.bind(this.controller)
                                },
                                    h("i.fa.fa-plus"))),
                        ]),
                        h("tr",
                            h("th", {colspan: 2},
                                h("select", {
                                    onchange: function () {
                                        var value = this.value;
                                        this.value = "__add__";
                                        self.addLayer(value);
                                    }
                                }, [
                                    h("option", {value: "__add__", selected: "selected"}, "Add layer")
                                ].concat(
                                    this.presentation.layers.slice().reverse()
                                        .filter(function (layer) {
                                            return !layer.auto && this.defaultLayers.indexOf(layer) >= 0;
                                        }, this)
                                        .map(function (layer) {
                                            return h("option", {value: layer.index}, layer.label);
                                        })
                                ))
                            )
                        )
                    ])
                ]),
                h("div.timeline-bottom-left", [
                    h("table.timeline", (this.defaultLayerIsNotEmpty ? [
                        h("tr", [
                            h("th.layer-icons", [
                                this.defaultLayersAreVisible ?
                                    h("i.visibility.fa.fa-eye", {
                                        title: "This layer is visible. Click to hide it.",
                                        onclick: this.toggleLayerVisibility.bind(this, -1)
                                    }) :
                                    h("i.visibility.fa.fa-eye-slash", {
                                        title: "This layer is hidden. Click to show it.",
                                        onclick: this.toggleLayerVisibility.bind(this, -1)
                                    }),
                                h("i.remove.fa.fa-times", {style: {visibility: "hidden"}})
                            ]),
                            h("th", {
                                className: "layer-label" + (this.defaultLayersAreSelected ? " selected" : ""),
                                onclick: this.updateLayerSelection.bind(this, -1)
                            }, "Default")
                        ]),
                    ] : []).concat(
                        this.presentation.layers.slice().reverse()
                            .filter(function (layer) {
                                return this.editableLayers.indexOf(layer) >= 0;
                            }, this)
                            .map(function (layer) {
                                return h("tr", [
                                    h("th.layer-icons", [
                                        layer.isVisible ?
                                            h("i.visibility.fa.fa-eye", {
                                                title: "This layer is visible. Click to hide it.",
                                                onclick: this.toggleLayerVisibility.bind(this, layer.index)
                                            }) :
                                            h("i.visibility.fa.fa-eye-slash", {
                                                title: "This layer is hidden. Click to show it.",
                                                onclick: this.toggleLayerVisibility.bind(this, layer.index)
                                            }),
                                        h("i.remove.fa.fa-times", {
                                            title: "Remove this layer",
                                            onclick: this.removeLayer.bind(this, layer.index)
                                        })
                                    ]),
                                    h("th", {
                                        className: "layer-label" + (this.selection.selectedLayers.indexOf(layer) >= 0 ? " selected" : ""),
                                        onclick: this.updateLayerSelection.bind(this, layer.index)
                                    }, layer.label)
                                ]);
                            }, this)
                    ).concat([
                        h("tr", {style: {visibility: "collapse"}}, [
                            h("th.layer-icons"),
                            h("th.layer-label", "Default")
                        ])
                    ]))
                ]),
                h("div.timeline-top-right", [
                    h("table.timeline", [
                        h("tr", this.presentation.frames.map(function (frame, frameIndex) {
                            return h("th", {
                                className: "frame-index" +
                                    (this.selection.selectedFrames.indexOf(frame) >= 0 ? " selected" : "") +
                                    (frame === this.selection.currentFrame ? " current" : ""),
                                onclick: this.updateFrameSelection.bind(this, frameIndex)
                            }, [
                                h("i.insert-before.fa.fa-arrow-circle-down", {
                                    title: "Insert selection before frame " + frameIndex,
                                    onclick: function (evt) {
                                        self.controller.moveFrames(frameIndex);
                                        evt.stopPropagation();
                                    }
                                }),
                                h("i.insert-after.fa.fa-arrow-circle-down", {
                                    title: "Insert selection after frame " + frameIndex,
                                    onclick: function (evt) {
                                        self.controller.moveFrames(frameIndex + 1);
                                        evt.stopPropagation();
                                    }
                                }),
                                frameIndex.toString()
                            ]);
                        }, this)),
                        h("tr", this.presentation.frames.map(function (frame, frameIndex) {
                            return h("th", {
                                title: frame.title,
                                className: "frame-title" +
                                    (this.selection.selectedFrames.indexOf(frame) >= 0 ? " selected" : "") +
                                    (frame === this.selection.currentFrame ? " current" : ""),
                                onclick: this.updateFrameSelection.bind(this, frameIndex)
                            }, frame.title);
                        }, this))
                    ])
                ]),
                h("div.timeline-bottom-right", {
                    onscroll: function (evt) {
                        self.rootNode.querySelector(".timeline-top-right").scrollLeft = this.scrollLeft;
                        self.rootNode.querySelector(".timeline-bottom-left").scrollTop = this.scrollTop;
                    }
                }, [
                    h("table.timeline", (this.defaultLayerIsNotEmpty ? [
                        h("tr",
                            this.presentation.frames.map(function (frame, frameIndex) {
                                return h("td", {
                                    className:
                                        (this.defaultLayersAreSelected && this.selection.selectedFrames.indexOf(frame) >= 0 ? "selected" : "") +
                                        (frame === this.selection.currentFrame ? " current" : ""),
                                    onclick: this.updateLayerAndFrameSelection.bind(this, -1, frameIndex)
                                });
                            }, this)
                        )
                    ] : []).concat(
                        this.presentation.layers.slice().reverse()
                            .filter(function (layer) {
                                return this.editableLayers.indexOf(layer) >= 0;
                            }, this)
                            .map(function (layer) {
                                return h("tr",
                                    this.presentation.frames.map(function (frame, frameIndex) {
                                        return h("td", {
                                            className:
                                                (this.selection.selectedLayers.indexOf(layer) >= 0 && this.selection.selectedFrames.indexOf(frame) >= 0 ? "selected" : "") +
                                                (frame === this.selection.currentFrame ? " current" : ""),
                                            onclick: this.updateLayerAndFrameSelection.bind(this, layer.index, frameIndex)
                                        }, frame.layerProperties[layer.index].link ? h("i.fa.fa-link") : []);
                                    }, this)
                                );
                            }, this)
                    ).concat([
                        h("tr", {style: {visibility: "collapse"}},
                            this.presentation.frames.map(function (frame) {
                                return h("td", frame.title);
                            })
                        )
                    ]))
                ])
            ]);
        },

        /*
         * Render the current view.
         *
         * The timeline is rendered with respect to the current state of
         * the presentation and the selection.
         * Event handlers are attached to UI elements.
         */
        adjust: function () {
            var topLeft = this.rootNode.querySelector(".timeline-top-left");
            var topRight = this.rootNode.querySelector(".timeline-top-right");
            var bottomLeft = this.rootNode.querySelector(".timeline-bottom-left");
            var bottomRight = this.rootNode.querySelector(".timeline-bottom-right");

            var topLeftTable = topLeft.querySelector("table");
            var topRightTable = topRight.querySelector("table");
            var bottomLeftTable = bottomLeft.querySelector("table");

            var leftWidth = Math.max(topLeftTable.clientWidth, bottomLeftTable.clientWidth);
            var rightWidth = this.rootNode.clientWidth - leftWidth;
            var topHeight = Math.max(topLeftTable.clientHeight, topRightTable.clientHeight);
            var bottomHeight = this.rootNode.clientHeight - topHeight;

            // Fit the width of the left tables,
            // allocate remaining width to the right tables
            topLeft.style.width = bottomLeft.style.width =
            topLeftTable.style.width = bottomLeftTable.style.width = leftWidth + "px";
            topRight.style.width = bottomRight.style.width = rightWidth + "px";

            // Fit the height of the top tables,
            // allocate remaining width to the bottom tables
            topLeft.style.height = topRight.style.height = topHeight + "px";
            bottomLeft.style.height = bottomRight.style.height = bottomHeight + "px";

            // Corresponding rows in left and right tables must have the same height
            var leftRows  = Array.prototype.slice.call(topLeft.querySelectorAll("tr")).concat(Array.prototype.slice.call(bottomLeft.querySelectorAll("tr")));
            var rightRows = Array.prototype.slice.call(topRight.querySelectorAll("tr")).concat(Array.prototype.slice.call(bottomRight.querySelectorAll("tr")));
            leftRows.forEach(function (leftRow, rowIndex) {
                var rightRow = rightRows[rowIndex];
                var maxHeight = Math.max(leftRow.clientHeight, rightRow.clientHeight);
                leftRow.style.height = rightRow.style.height = maxHeight + "px";
            });
        }
    };
});
<|endoftext|>


<commit_before>/* global Marionette, Handlebars */

/**
 *
 * @copyright Copyright (c) 2017, Daniel Calviño Sánchez (danxuliu@gmail.com)
 *
 * @license GNU AGPL version 3 or any later version
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

(function(OCA, Marionette, Handlebars) {

	'use strict';

	OCA.SpreedMe = OCA.SpreedMe || {};
	OCA.SpreedMe.Views = OCA.SpreedMe.Views || {};

	var TEMPLATE_TAB_HEADER_VIEW =
		'{{label}}';

	var TEMPLATE_TAB_VIEW =
		'
' +
		'
' +
		'
' +
		'
' +
		'
' +
		'
';

	var TabHeaderView  = Marionette.View.extend({

		tagName: 'li',
		className: 'tabHeader',

		template: Handlebars.compile(TEMPLATE_TAB_HEADER_VIEW),
		templateContext: function() {
			return {
				label: this.getOption('label')
			};
		},

		events: {
			'click': function() {
				this.triggerMethod('click:tabHeader', this.getOption('tabId'));
			}
		},

		setSelected: function(selected) {
			if (selected) {
				this.$el.addClass('selected');
			} else {
				this.$el.removeClass('selected');
			}
		}

	});

	var TabHeadersView  = Marionette.View.extend({

		tagName: 'ul',
		className: 'tabHeaders',

		// The tab headers are added dynamically using regions, so there is
		// nothing to be rendered with a template.
		template: _.noop,

		childViewTriggers: {
			// Propagate the event to the parent view.
			'click:tabHeader': 'click:tabHeader'
		},

		addTabHeader: function(tabId, tabHeaderOptions) {
			var tabHeaderId = 'tabHeader' + tabId.charAt(0).toUpperCase() + tabId.substr(1);

			tabHeaderOptions.id = tabHeaderId;
			// The "tabId" will be passed by the TabHeaderView when triggering
			// "click:tabHeader" events.
			tabHeaderOptions.tabId = tabId;

			tabHeaderOptions.priority = tabHeaderOptions.priority || 0;

			var tabHeaderView = new TabHeaderView(tabHeaderOptions);

			var tabHeaderIndex = this._getIndexForTabHeaderPriority(tabHeaderOptions.priority);

			// When adding a region and showing a view on it the target element
			// of the region must exist in the parent view. Therefore, a dummy
			// target element, which will be replaced with the tab header
			// itself, has to be added to the parent view.
			var dummyElement = '
';
			if (tabHeaderIndex === 0) {
				this.$el.prepend(dummyElement);
			} else {
				// When two tab headers have the same priority the new one is
				// added after the existing one.
				this.$el.children().eq(tabHeaderIndex-1).after(dummyElement);
			}

			this.addRegion(tabId, { el: '#' + tabHeaderId, replaceElement: true });
			this.showChildView(tabId, tabHeaderView);
		},

		/**
		 * Return the insertion index for a tab header based on its priority.
		 *
		 * Tab headers with higher priorities go before tab headers with lower
		 * priorities; if the priority is the same as one or more of the current
		 * tab headers the new tab header goes after the last of them.
		 *
		 * @param int priority the priority to get its insertion index.
		 * @return int the insertion index.
		 */
		_getIndexForTabHeaderPriority: function(priority) {
			// this.getRegions() returns an object that acts as a map, but it
			// has no "length" property; _.map creates an array, thus ensuring
			// that there is a "length" property to know the current number of
			// tab headers.
			var currentPriorities = _.map(this.getRegions(), function(region) {
				return region.currentView.getOption('priority');
			});

			// By default sort() converts the values to strings and sorts them
			// in ascending order using their Unicode value; a custom function
			// must be used to sort them by their numerical value instead.
			currentPriorities.sort(function(a, b) {
				return a - b;
			}).reverse();

			var index = _.findIndex(currentPriorities, function(currentPriority) {
				return priority > currentPriority;
			});

			if (index === -1) {
				return currentPriorities.length;
			}

			return index;
		},

		selectTabHeader: function(tabId) {
			if (this._currentTabId !== undefined) {
				this.getChildView(this._currentTabId).setSelected(false);
			}

			this._currentTabId = tabId;

			this.getChildView(this._currentTabId).setSelected(true);
		}

	});

	/**
	 * View for tabs (headers and content).
	 *
	 * A TabView contains a set of tab headers and a content area. When a header
	 * is selected its associated content view is shown in the content area;
	 * otherwise its content is hidden (although the header is always shown).
	 */
	var TabView = Marionette.View.extend({

		tagName: 'div',
		className: 'tabs',

		regions: {
			tabHeaders: '.tabHeaders',
			tabContent: '.tab'
		},

		template: Handlebars.compile(TEMPLATE_TAB_VIEW),

		initialize: function() {
			this._tabHeadersView = null;
			this._tabContentViews = {};
		},

		onDestroy: function() {
			_.each(this._tabContentViews, function(tabContentView) {
				// Explicitly destroy all the tab content views, as some of them
				// may be detached from the TabView.
				tabContentView.destroy();
			});
		},

		/**
		 * Adds a new tab.
		 *
		 * The tabHeaderOptions must provide a 'label' string which will be
		 * rendered as the tab header. Optionally, it can provide a 'priority'
		 * integer to set the order of the tab header with respect to the other
		 * tab headers (tabs with higher priorities appear before tabs with
		 * lower priorities; tabs with the same priority are sorted based on
		 * their insertion order); if it is not explicitly set the value 0 is
		 * used. If needed, the tabHeaderOptions can provide other values that
		 * will override the default TabHeaderView properties (for example, it
		 * can provide an 'onRender' function to extend the default rendering of
		 * the header).
		 *
		 * The TabView takes ownership of the given content view, and it will
		 * destroy it when the TabView is destroyed.
		 *
		 * @param string tabId the ID of the tab.
		 * @param Object tabHeaderOptions the options for the constructor of the
		 *        TabHeaderView that will be added as the header of the tab.
		 * @param Marionette.View tabContentView the View to be shown when the
		 *        tab is selected.
		 */
		addTab: function(tabId, tabHeaderOptions, tabContentView) {
			if (this._tabHeadersView === null) {
				this._tabHeadersView = new TabHeadersView();
				this.showChildView('tabHeaders', this._tabHeadersView, { replaceElement: true });
			}

			this._tabHeadersView.addTabHeader(tabId, tabHeaderOptions);

			this._tabContentViews[tabId] = tabContentView;

			if (Object.keys(this._tabContentViews).length === 1) {
				this.selectTab(tabId);
			}
		},

		onChildviewClickTabHeader: function(tabId) {
			this.selectTab(tabId);
		},

		/**
		 * Select the tab associated to the given tabId.
		 *
		 * @param string tabId the ID of the tab to select.
		 */
		selectTab: function(tabId) {
			if (!this._tabContentViews.hasOwnProperty(tabId)) {
				return;
			}

			this._tabHeadersView.selectTabHeader(tabId);

			// With Marionette 3.1 "this.detachChildView('tabContent')" would be
			// used instead of the "preventDestroy" option.
			this.showChildView('tabContent', this._tabContentViews[tabId], { preventDestroy: true } );
		}

	});

	OCA.SpreedMe.Views.TabView = TabView;

})(OCA, Marionette, Handlebars);
<commit_msg>Make calling "selectTabHeader" to implicitly show the content view<commit_after>/* global Marionette, Handlebars */

/**
 *
 * @copyright Copyright (c) 2017, Daniel Calviño Sánchez (danxuliu@gmail.com)
 *
 * @license GNU AGPL version 3 or any later version
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

(function(OCA, Marionette, Handlebars) {

	'use strict';

	OCA.SpreedMe = OCA.SpreedMe || {};
	OCA.SpreedMe.Views = OCA.SpreedMe.Views || {};

	var TEMPLATE_TAB_HEADER_VIEW =
		'{{label}}';

	var TEMPLATE_TAB_VIEW =
		'
' +
		'
' +
		'
' +
		'
' +
		'
' +
		'
';

	var TabHeaderView  = Marionette.View.extend({

		tagName: 'li',
		className: 'tabHeader',

		template: Handlebars.compile(TEMPLATE_TAB_HEADER_VIEW),
		templateContext: function() {
			return {
				label: this.getOption('label')
			};
		},

		events: {
			'click': function() {
				this.triggerMethod('click:tabHeader', this.getOption('tabId'));
			}
		},

		setSelected: function(selected) {
			if (selected) {
				this.$el.addClass('selected');
			} else {
				this.$el.removeClass('selected');
			}
		}

	});

	var TabHeadersView  = Marionette.View.extend({

		tagName: 'ul',
		className: 'tabHeaders',

		// The tab headers are added dynamically using regions, so there is
		// nothing to be rendered with a template.
		template: _.noop,

		childViewEvents: {
			'click:tabHeader': 'selectTabHeader'
		},

		addTabHeader: function(tabId, tabHeaderOptions) {
			var tabHeaderId = 'tabHeader' + tabId.charAt(0).toUpperCase() + tabId.substr(1);

			tabHeaderOptions.id = tabHeaderId;
			// The "tabId" will be passed by the TabHeaderView when triggering
			// "click:tabHeader" events.
			tabHeaderOptions.tabId = tabId;

			tabHeaderOptions.priority = tabHeaderOptions.priority || 0;

			var tabHeaderView = new TabHeaderView(tabHeaderOptions);

			var tabHeaderIndex = this._getIndexForTabHeaderPriority(tabHeaderOptions.priority);

			// When adding a region and showing a view on it the target element
			// of the region must exist in the parent view. Therefore, a dummy
			// target element, which will be replaced with the tab header
			// itself, has to be added to the parent view.
			var dummyElement = '
';
			if (tabHeaderIndex === 0) {
				this.$el.prepend(dummyElement);
			} else {
				// When two tab headers have the same priority the new one is
				// added after the existing one.
				this.$el.children().eq(tabHeaderIndex-1).after(dummyElement);
			}

			this.addRegion(tabId, { el: '#' + tabHeaderId, replaceElement: true });
			this.showChildView(tabId, tabHeaderView);
		},

		/**
		 * Return the insertion index for a tab header based on its priority.
		 *
		 * Tab headers with higher priorities go before tab headers with lower
		 * priorities; if the priority is the same as one or more of the current
		 * tab headers the new tab header goes after the last of them.
		 *
		 * @param int priority the priority to get its insertion index.
		 * @return int the insertion index.
		 */
		_getIndexForTabHeaderPriority: function(priority) {
			// this.getRegions() returns an object that acts as a map, but it
			// has no "length" property; _.map creates an array, thus ensuring
			// that there is a "length" property to know the current number of
			// tab headers.
			var currentPriorities = _.map(this.getRegions(), function(region) {
				return region.currentView.getOption('priority');
			});

			// By default sort() converts the values to strings and sorts them
			// in ascending order using their Unicode value; a custom function
			// must be used to sort them by their numerical value instead.
			currentPriorities.sort(function(a, b) {
				return a - b;
			}).reverse();

			var index = _.findIndex(currentPriorities, function(currentPriority) {
				return priority > currentPriority;
			});

			if (index === -1) {
				return currentPriorities.length;
			}

			return index;
		},

		selectTabHeader: function(tabId) {
			if (this._currentTabId !== undefined) {
				this.getChildView(this._currentTabId).setSelected(false);
			}

			this._currentTabId = tabId;

			this.getChildView(this._currentTabId).setSelected(true);

			this.triggerMethod('select:tabHeader', tabId);
		}

	});

	/**
	 * View for tabs (headers and content).
	 *
	 * A TabView contains a set of tab headers and a content area. When a header
	 * is selected its associated content view is shown in the content area;
	 * otherwise its content is hidden (although the header is always shown).
	 */
	var TabView = Marionette.View.extend({

		tagName: 'div',
		className: 'tabs',

		regions: {
			tabHeaders: '.tabHeaders',
			tabContent: '.tab'
		},

		template: Handlebars.compile(TEMPLATE_TAB_VIEW),

		initialize: function() {
			this._tabHeadersView = null;
			this._tabContentViews = {};
		},

		onDestroy: function() {
			_.each(this._tabContentViews, function(tabContentView) {
				// Explicitly destroy all the tab content views, as some of them
				// may be detached from the TabView.
				tabContentView.destroy();
			});
		},

		/**
		 * Adds a new tab.
		 *
		 * The tabHeaderOptions must provide a 'label' string which will be
		 * rendered as the tab header. Optionally, it can provide a 'priority'
		 * integer to set the order of the tab header with respect to the other
		 * tab headers (tabs with higher priorities appear before tabs with
		 * lower priorities; tabs with the same priority are sorted based on
		 * their insertion order); if it is not explicitly set the value 0 is
		 * used. If needed, the tabHeaderOptions can provide other values that
		 * will override the default TabHeaderView properties (for example, it
		 * can provide an 'onRender' function to extend the default rendering of
		 * the header).
		 *
		 * The TabView takes ownership of the given content view, and it will
		 * destroy it when the TabView is destroyed.
		 *
		 * @param string tabId the ID of the tab.
		 * @param Object tabHeaderOptions the options for the constructor of the
		 *        TabHeaderView that will be added as the header of the tab.
		 * @param Marionette.View tabContentView the View to be shown when the
		 *        tab is selected.
		 */
		addTab: function(tabId, tabHeaderOptions, tabContentView) {
			if (this._tabHeadersView === null) {
				this._tabHeadersView = new TabHeadersView();
				this.showChildView('tabHeaders', this._tabHeadersView, { replaceElement: true });
			}

			this._tabHeadersView.addTabHeader(tabId, tabHeaderOptions);

			this._tabContentViews[tabId] = tabContentView;

			if (Object.keys(this._tabContentViews).length === 1) {
				this.selectTab(tabId);
			}
		},

		/**
		 * Select the tab associated to the given tabId.
		 *
		 * @param string tabId the ID of the tab to select.
		 */
		selectTab: function(tabId) {
			if (!this._tabContentViews.hasOwnProperty(tabId)) {
				return;
			}

			this._tabHeadersView.selectTabHeader(tabId);
		},

		/**
		 * Shows the content view associated to the selected tab header.
		 *
		 * Only for internal use as an event handler.
		 *
		 * @param string tabId the ID of the selected tab.
		 */
		onChildviewSelectTabHeader: function(tabId) {
			// With Marionette 3.1 "this.detachChildView('tabContent')" would be
			// used instead of the "preventDestroy" option.
			this.showChildView('tabContent', this._tabContentViews[tabId], { preventDestroy: true } );
		}

	});

	OCA.SpreedMe.Views.TabView = TabView;

})(OCA, Marionette, Handlebars);
<|endoftext|>


<commit_before>window.onload = function(){
  esviji.run();
}

var esviji = {
  VIEWPORT_WIDTH: 320,
  VIEWPORT_HEIGHT: 480,
  EMPTY: 0,
  ROCK: -1,
  board: null,
  theme: 'metro',
  themes: {
    'metro': {
      'rock': 'metro',
      'regularPieces': ['m1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'm9', 'm10', 'm11', 'm12', 'm13', 'm14']
    }
  },
  currentPieces: [],
  drawnCurrentPieces: [],
  validPieces: [],
  currentPiece: 0,
  currentPosX: 9,
  currentPosY: 7,
  maxAvailablePieces: 0,
  nbPieces: 0,
  level: 0,
  score: 0,
  scoreThisTurn: 0,
  drawnScore: null,
  lives: 10,
  drawnLevelAndLives: null,
  viewportWidth: 0,
  viewportHeight: 0,

  init: function init() {
    esviji.board = new ScaleRaphael('board', esviji.VIEWPORT_WIDTH, esviji.VIEWPORT_HEIGHT);
    esviji.updateViewportSize();

    var windowAddEvent = window.attachEvent || window.addEventListener;
    windowAddEvent('onresize', esviji.updateViewportSize, false);

    var header = esviji.board.path('M 1 21 l 0 205 l 35 0 l 0 -35 l 35 0 l 0 -35 l 35 0 l 0 -35 l 35 0 l 0 -35 l 35 0 l 0 -35 l 35 0 l 108 0 l 0 -30 z'),
        title = esviji.board.print(0, 60, "esviji", esviji.board.getFont('ChewyRegular'), 60);

    esviji.drawScore();
    esviji.drawLevel();
    esviji.drawLives();

    header.attr({
      'fill': '#9999cc',
      'stroke': '#666699',
      'stroke-width': 2
    });

    title.attr({
      'fill': '#666699',
      'stroke': '#333366',
      'stroke-width': 2
    });

    esviji.maxAvailablePieces = esviji.themes[esviji.theme].regularPieces.length;
  },

  updateViewportSize: function updateViewportSize() {
    // http://andylangton.co.uk/articles/javascript/get-viewport-size-javascript/
    var w = window,
        d = document,
        e = d.documentElement,
        b = d.getElementsByTagName('body')[0];

    esviji.viewportWidth = w.innerWidth || e.clientWidth || b.clientWidth;
    esviji.viewportHeight = w.innerHeight || e.clientHeight || b.clientHeight;

    esviji.board.changeSize(esviji.viewportWidth, esviji.viewportHeight, true, false);
  },

  scaledX: function scaledX(x) {
    return x * esviji.VIEWPORT_WIDTH / esviji.board.width;
  },

  scaledY: function scaledY(y) {
    return y * esviji.VIEWPORT_HEIGHT / esviji.board.height;
  },

  nextLevel: function nextLevel() {
    esviji.level++;
    esviji.drawLevel();
    esviji.lives++;
    esviji.drawLives();
    esviji.nbPieces = Math.min(esviji.maxAvailablePieces, Math.floor(5 + (esviji.level / 5)));

    esviji.initPieces();
    esviji.erasePieces();
    esviji.drawPieces();

    esviji.startNewTurn();
  },

  xToCoord: function xToCoord(x) {
    return x * 35 - 30;
  },

  yToCoord: function yToCoord(y) {
    return esviji.VIEWPORT_HEIGHT - 35 * y;
  },

  startNewTurn: function startNewTurn() {
    esviji.currentPosX = 9;
    esviji.currentPosY = 8;
    esviji.scoreThisTurn = 0;
    esviji.currentDirX = -1;
    esviji.currentDirY = 0;
    esviji.getValidPieces();
    if (esviji.validPieces.length == 0) {
      // no more valid piece, end of the turn
      esviji.nextLevel();
    } else {
      if (esviji.validPieces.indexOf(esviji.currentPiece) == -1) {
        esviji.lives--;
        esviji.drawLives();
        if (esviji.lives == 0) {
          gameOver = esviji.board.print(5, 200, "Game Over", esviji.board.getFont('ChewyRegular'), 64).attr({'fill': 'red', 'stroke': 'black', 'stroke-width': 2});
          //TODO: really stop the game
        }
        esviji.currentPiece = esviji.validPieces[Math.floor(Math.random() * esviji.validPieces.length)];
      }
      pieceFile = 'themes/' + esviji.theme + '/' + esviji.themes[esviji.theme].regularPieces[esviji.currentPiece - 1] + '.svg';
      esviji.drawnCurrentPiece = esviji.board.image(pieceFile, esviji.xToCoord(esviji.currentPosX), esviji.yToCoord(esviji.currentPosY), 30, 30);
      esviji.drawnCurrentPiece.drag(function(dx, dy) {
        this.attr({
          x: esviji.xToCoord(9),
          y: Math.min(Math.max(yBeforeDrag + esviji.scaledY(dy), esviji.yToCoord(12)), esviji.yToCoord(1))
        });
      }, function () {
        xBeforeDrag = this.attr('x');
        yBeforeDrag = this.attr('y');
      }, function () {
        yAfterDrag = this.attr('y');
        diff = 1000;
        for (i = 1; i <= 12; i++) {
          thisDiff = Math.abs(yAfterDrag - esviji.yToCoord(i));
          if (thisDiff < diff) {
            diff = thisDiff;
            esviji.currentPosY = i;
          }
        }
        esviji.drawnCurrentPiece.animate({'y': esviji.yToCoord(esviji.currentPosY)}, 500, 'elastic', esviji.playUserChoice);
      });

    }
  },

  playUserChoice: function playUserChoice () {
    var stopped = false;
    if (esviji.currentPosY == 1 && esviji.currentDirY == -1) {
      stopped = true;
    } else {
      if (esviji.currentPosX == 1 && esviji.currentDirX == -1) {
        esviji.currentDirX = 0;
        esviji.currentDirY = -1;
      } else {
        nextPiece = esviji.currentPieces[esviji.currentPosX + esviji.currentDirX][esviji.currentPosY + esviji.currentDirY];
        if (nextPiece == esviji.ROCK) {
          if (esviji.currentDirX == -1) {
            esviji.currentDirX = 0;
            esviji.currentDirY = -1;
          } else {
            stopped = true;
          }
        } else {
          if (nextPiece == esviji.EMPTY) {
            esviji.currentPosX += esviji.currentDirX;
            esviji.currentPosY += esviji.currentDirY;
          } else {
            if (nextPiece == esviji.currentPiece) {
              esviji.currentPosX += esviji.currentDirX;
              esviji.currentPosY += esviji.currentDirY;
              esviji.currentPieces[esviji.currentPosX][esviji.currentPosY] = esviji.EMPTY;
              esviji.drawnCurrentPieces[esviji.currentPosX][esviji.currentPosY].remove();
              esviji.scoreThisTurn++;
            } else {
              if (esviji.scoreThisTurn > 0) {
                esviji.currentPiece = nextPiece;
              }
              stopped = true;
            }
          }
        }
      }
    }
    if (!stopped) {
      esviji.drawnCurrentPiece.animate({'x': esviji.xToCoord(esviji.currentPosX), 'y': esviji.yToCoord(esviji.currentPosY), 'rotate': 360}, 200, 'linear', esviji.playUserChoice);
    } else {
      esviji.score += Math.pow(esviji.scoreThisTurn, 2);
      esviji.drawScore();
      esviji.drawnCurrentPiece.remove();
      esviji.makePiecesFall();
      esviji.startNewTurn();
    }
  },

  makePiecesFall: function makePiecesFall() {
    var abovePieces;

    for(x = 1; x <= 6; x++) {
      for (y = 1; y <= 5; y++) {
        if (esviji.currentPieces[x][y] == esviji.EMPTY) {
          abovePieces = 0;
          for (z = y; z <= 5; z++) {
            if (esviji.currentPieces[x][z + 1] != esviji.EMPTY && esviji.currentPieces[x][z + 1] != esviji.ROCK) {
              abovePieces++;
            }
            if (esviji.currentPieces[x][z + 1] == esviji.ROCK) {
              z = 5;
            } else {
              esviji.currentPieces[x][z] = esviji.currentPieces[x][z + 1];
              esviji.currentPieces[x][z + 1] = esviji.EMPTY;
              if (esviji.drawnCurrentPieces[x][z + 1] != null) {
                esviji.drawnCurrentPieces[x][z] = esviji.drawnCurrentPieces[x][z + 1];
                esviji.drawnCurrentPieces[x][z].animate({'y': esviji.yToCoord(z)}, 500, 'bounce');
                esviji.drawnCurrentPieces[x][z + 1] = null;
              }
            }
          }
          if (abovePieces > 0) {
            y--;
          }
        }
      }
    }
  },

  initPieces: function initPieces() {
    esviji.currentPieces = [];

    for(x = 1; x <= 8; x++) {
      esviji.currentPieces[x] = [];
      for (y = 1; y <= 12; y++) {
        if (x > 6) {
          esviji.currentPieces[x][y] = esviji.EMPTY;
        } else {
          if (y > 6) {
            if (y - 6 > x) {
              esviji.currentPieces[x][y] = esviji.ROCK;
            } else {
              esviji.currentPieces[x][y] = esviji.EMPTY;
            }
          } else {
            esviji.currentPieces[x][y] = 1 + Math.floor(Math.random() * esviji.nbPieces);
          }
        }
      }
    }

    // add rocks in the middle after level 10
    if (esviji.level > 10) {
      nbRocks = Math.floor((esviji.level - 5) / 5);
      positionedRocks = 0;
      while (positionedRocks < nbRocks) {
        rock_x = 1 + Math.floor(Math.random() * 6);
        rock_y = 1 + Math.floor(Math.random() * 6);
        if (esviji.currentPieces[rock_x][rock_y] != esviji.ROCK) {
          esviji.currentPieces[rock_x][rock_y] = esviji.ROCK;
          positionedRocks++;
        }
      }
    }
  },

  drawPieces: function drawPieces() {
    esviji.drawnCurrentPieces = [];
    for(x = 1; x <= 6; x++) {
      esviji.drawnCurrentPieces[x] = [];
      for (y = 1; y <= 6; y++) {
        if (esviji.currentPieces[x][y] != esviji.EMPTY) {
          if (esviji.currentPieces[x][y] == esviji.ROCK) {
            pieceFile = 'themes/' + esviji.theme + '/' + esviji.themes[esviji.theme].rock + '.svg';
          } else {
            pieceFile = 'themes/' + esviji.theme + '/' + esviji.themes[esviji.theme].regularPieces[esviji.currentPieces[x][y] - 1] + '.svg';
          }
          piece_x = esviji.xToCoord(x);
          piece_y = esviji.yToCoord(y);
          esviji.drawnCurrentPieces[x][y] = esviji.board.image(pieceFile, piece_x, -30, 30, 30);
          esviji.drawnCurrentPieces[x][y].animate({'y': piece_y}, 2000, 'bounce');
        }
      }
    }
  },

  erasePieces: function erasePieces() {
    for(x = 1; x <= 6; x++) {
      if (esviji.drawnCurrentPieces[x] != undefined) {
        for (y = 1; y <= 6; y++) {
          if (esviji.drawnCurrentPieces[x][y] != null) {
            esviji.drawnCurrentPieces[x][y].remove();
          }
        }
      }
    }
  },

  getValidPieces: function getValidPieces() {
    var x, y, dir_x, dir_y, found;

    esviji.validPieces = [];

    for (y_start = 1; y_start <= 12; y_start++) {
      x = 9;
      y = y_start;
      dir_x = -1;
      dir_y = 0;
      found = false;
      while (!found) {
        if (y == 1 && dir_y == -1) {
          found = true;
        } else {
          if (x == 1 && dir_x == -1) {
            dir_x = 0;
            dir_y = -1;
          } else {
            nextPiece = esviji.currentPieces[x + dir_x][y + dir_y];
            if (nextPiece == esviji.ROCK) {
              if (dir_x == -1) {
                dir_x = 0;
                dir_y = -1;
              } else {
                found = true;
              }
            } else {
              if (nextPiece == esviji.EMPTY) {
                x += dir_x;
                y += dir_y;
              } else {
                if (esviji.validPieces.indexOf(nextPiece) == -1) {
                  esviji.validPieces.push(nextPiece);
                }
                found = true;
              }
            }
          }
        }
      }
    }
  },

  drawScore: function drawScore() {
    if (esviji.drawnScore != null) {
      esviji.drawnScore.remove();
    }
    esviji.drawnScore = esviji.board.print(170, 38, "score: " + esviji.score, esviji.board.getFont('ChewyRegular'), 24);
    esviji.drawnScore.attr({'fill': '#333366'});
  },

  drawLevel: function drawLevel() {
    if (esviji.drawnLevel != null) {
      esviji.drawnLevel.remove();
    }
    esviji.drawnLevel = esviji.board.print(10, 110, 'level ' + esviji.level, esviji.board.getFont('ChewyRegular'), 20);
    esviji.drawnLevel.attr({'fill': '#333366'});
  },

  drawLives: function drawLives() {
    if (esviji.drawnLives != null) {
      esviji.drawnLives.remove();
    }
    esviji.drawnLives = esviji.board.print(10, 140, esviji.lives + ' lives', esviji.board.getFont('ChewyRegular'), 20);
    esviji.drawnLives.attr({'fill': '#333366'});
  },

  run: function run() {
    esviji.init();
    esviji.nextLevel();
  }
}<commit_msg>white background<commit_after>window.onload = function(){
  esviji.run();
}

var esviji = {
  VIEWPORT_WIDTH: 320,
  VIEWPORT_HEIGHT: 480,
  EMPTY: 0,
  ROCK: -1,
  board: null,
  theme: 'metro',
  themes: {
    'metro': {
      'rock': 'metro',
      'regularPieces': ['m1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'm9', 'm10', 'm11', 'm12', 'm13', 'm14']
    }
  },
  currentPieces: [],
  drawnCurrentPieces: [],
  validPieces: [],
  currentPiece: 0,
  currentPosX: 9,
  currentPosY: 7,
  maxAvailablePieces: 0,
  nbPieces: 0,
  level: 0,
  score: 0,
  scoreThisTurn: 0,
  drawnScore: null,
  lives: 10,
  drawnLevelAndLives: null,
  viewportWidth: 0,
  viewportHeight: 0,

  init: function init() {
    esviji.board = new ScaleRaphael('board', esviji.VIEWPORT_WIDTH, esviji.VIEWPORT_HEIGHT);
    esviji.updateViewportSize();

    var windowAddEvent = window.attachEvent || window.addEventListener;
    windowAddEvent('onresize', esviji.updateViewportSize, false);

    var background = esviji.board.rect(1, 1, esviji.VIEWPORT_WIDTH, esviji.VIEWPORT_HEIGHT),
        header = esviji.board.path('M 1 21 l 0 205 l 35 0 l 0 -35 l 35 0 l 0 -35 l 35 0 l 0 -35 l 35 0 l 0 -35 l 35 0 l 0 -35 l 35 0 l 108 0 l 0 -30 z'),
        title = esviji.board.print(0, 60, "esviji", esviji.board.getFont('ChewyRegular'), 60);

    esviji.drawScore();
    esviji.drawLevel();
    esviji.drawLives();

    background.attr({ 'fill': '#ffffff'});

    header.attr({
      'fill': '#9999cc',
      'stroke': '#666699',
      'stroke-width': 2
    });

    title.attr({
      'fill': '#666699',
      'stroke': '#333366',
      'stroke-width': 2
    });

    esviji.maxAvailablePieces = esviji.themes[esviji.theme].regularPieces.length;
  },

  updateViewportSize: function updateViewportSize() {
    // http://andylangton.co.uk/articles/javascript/get-viewport-size-javascript/
    var w = window,
        d = document,
        e = d.documentElement,
        b = d.getElementsByTagName('body')[0];

    esviji.viewportWidth = w.innerWidth || e.clientWidth || b.clientWidth;
    esviji.viewportHeight = w.innerHeight || e.clientHeight || b.clientHeight;

    esviji.board.changeSize(esviji.viewportWidth, esviji.viewportHeight, true, false);
  },

  scaledX: function scaledX(x) {
    return x * esviji.VIEWPORT_WIDTH / esviji.board.width;
  },

  scaledY: function scaledY(y) {
    return y * esviji.VIEWPORT_HEIGHT / esviji.board.height;
  },

  nextLevel: function nextLevel() {
    esviji.level++;
    esviji.drawLevel();
    esviji.lives++;
    esviji.drawLives();
    esviji.nbPieces = Math.min(esviji.maxAvailablePieces, Math.floor(5 + (esviji.level / 5)));

    esviji.initPieces();
    esviji.erasePieces();
    esviji.drawPieces();

    esviji.startNewTurn();
  },

  xToCoord: function xToCoord(x) {
    return x * 35 - 30;
  },

  yToCoord: function yToCoord(y) {
    return esviji.VIEWPORT_HEIGHT - 35 * y;
  },

  startNewTurn: function startNewTurn() {
    esviji.currentPosX = 9;
    esviji.currentPosY = 8;
    esviji.scoreThisTurn = 0;
    esviji.currentDirX = -1;
    esviji.currentDirY = 0;
    esviji.getValidPieces();
    if (esviji.validPieces.length == 0) {
      // no more valid piece, end of the turn
      esviji.nextLevel();
    } else {
      if (esviji.validPieces.indexOf(esviji.currentPiece) == -1) {
        esviji.lives--;
        esviji.drawLives();
        if (esviji.lives == 0) {
          gameOver = esviji.board.print(5, 200, "Game Over", esviji.board.getFont('ChewyRegular'), 64).attr({'fill': 'red', 'stroke': 'black', 'stroke-width': 2});
          //TODO: really stop the game
        }
        esviji.currentPiece = esviji.validPieces[Math.floor(Math.random() * esviji.validPieces.length)];
      }
      pieceFile = 'themes/' + esviji.theme + '/' + esviji.themes[esviji.theme].regularPieces[esviji.currentPiece - 1] + '.svg';
      esviji.drawnCurrentPiece = esviji.board.image(pieceFile, esviji.xToCoord(esviji.currentPosX), esviji.yToCoord(esviji.currentPosY), 30, 30);
      esviji.drawnCurrentPiece.drag(function(dx, dy) {
        this.attr({
          x: esviji.xToCoord(9),
          y: Math.min(Math.max(yBeforeDrag + esviji.scaledY(dy), esviji.yToCoord(12)), esviji.yToCoord(1))
        });
      }, function () {
        xBeforeDrag = this.attr('x');
        yBeforeDrag = this.attr('y');
      }, function () {
        yAfterDrag = this.attr('y');
        diff = 1000;
        for (i = 1; i <= 12; i++) {
          thisDiff = Math.abs(yAfterDrag - esviji.yToCoord(i));
          if (thisDiff < diff) {
            diff = thisDiff;
            esviji.currentPosY = i;
          }
        }
        esviji.drawnCurrentPiece.animate({'y': esviji.yToCoord(esviji.currentPosY)}, 500, 'elastic', esviji.playUserChoice);
      });

    }
  },

  playUserChoice: function playUserChoice () {
    var stopped = false;
    if (esviji.currentPosY == 1 && esviji.currentDirY == -1) {
      stopped = true;
    } else {
      if (esviji.currentPosX == 1 && esviji.currentDirX == -1) {
        esviji.currentDirX = 0;
        esviji.currentDirY = -1;
      } else {
        nextPiece = esviji.currentPieces[esviji.currentPosX + esviji.currentDirX][esviji.currentPosY + esviji.currentDirY];
        if (nextPiece == esviji.ROCK) {
          if (esviji.currentDirX == -1) {
            esviji.currentDirX = 0;
            esviji.currentDirY = -1;
          } else {
            stopped = true;
          }
        } else {
          if (nextPiece == esviji.EMPTY) {
            esviji.currentPosX += esviji.currentDirX;
            esviji.currentPosY += esviji.currentDirY;
          } else {
            if (nextPiece == esviji.currentPiece) {
              esviji.currentPosX += esviji.currentDirX;
              esviji.currentPosY += esviji.currentDirY;
              esviji.currentPieces[esviji.currentPosX][esviji.currentPosY] = esviji.EMPTY;
              esviji.drawnCurrentPieces[esviji.currentPosX][esviji.currentPosY].remove();
              esviji.scoreThisTurn++;
            } else {
              if (esviji.scoreThisTurn > 0) {
                esviji.currentPiece = nextPiece;
              }
              stopped = true;
            }
          }
        }
      }
    }
    if (!stopped) {
      esviji.drawnCurrentPiece.animate({'x': esviji.xToCoord(esviji.currentPosX), 'y': esviji.yToCoord(esviji.currentPosY), 'rotate': 360}, 200, 'linear', esviji.playUserChoice);
    } else {
      esviji.score += Math.pow(esviji.scoreThisTurn, 2);
      esviji.drawScore();
      esviji.drawnCurrentPiece.remove();
      esviji.makePiecesFall();
      esviji.startNewTurn();
    }
  },

  makePiecesFall: function makePiecesFall() {
    var abovePieces;

    for(x = 1; x <= 6; x++) {
      for (y = 1; y <= 5; y++) {
        if (esviji.currentPieces[x][y] == esviji.EMPTY) {
          abovePieces = 0;
          for (z = y; z <= 5; z++) {
            if (esviji.currentPieces[x][z + 1] != esviji.EMPTY && esviji.currentPieces[x][z + 1] != esviji.ROCK) {
              abovePieces++;
            }
            if (esviji.currentPieces[x][z + 1] == esviji.ROCK) {
              z = 5;
            } else {
              esviji.currentPieces[x][z] = esviji.currentPieces[x][z + 1];
              esviji.currentPieces[x][z + 1] = esviji.EMPTY;
              if (esviji.drawnCurrentPieces[x][z + 1] != null) {
                esviji.drawnCurrentPieces[x][z] = esviji.drawnCurrentPieces[x][z + 1];
                esviji.drawnCurrentPieces[x][z].animate({'y': esviji.yToCoord(z)}, 500, 'bounce');
                esviji.drawnCurrentPieces[x][z + 1] = null;
              }
            }
          }
          if (abovePieces > 0) {
            y--;
          }
        }
      }
    }
  },

  initPieces: function initPieces() {
    esviji.currentPieces = [];

    for(x = 1; x <= 8; x++) {
      esviji.currentPieces[x] = [];
      for (y = 1; y <= 12; y++) {
        if (x > 6) {
          esviji.currentPieces[x][y] = esviji.EMPTY;
        } else {
          if (y > 6) {
            if (y - 6 > x) {
              esviji.currentPieces[x][y] = esviji.ROCK;
            } else {
              esviji.currentPieces[x][y] = esviji.EMPTY;
            }
          } else {
            esviji.currentPieces[x][y] = 1 + Math.floor(Math.random() * esviji.nbPieces);
          }
        }
      }
    }

    // add rocks in the middle after level 10
    if (esviji.level > 10) {
      nbRocks = Math.floor((esviji.level - 5) / 5);
      positionedRocks = 0;
      while (positionedRocks < nbRocks) {
        rock_x = 1 + Math.floor(Math.random() * 6);
        rock_y = 1 + Math.floor(Math.random() * 6);
        if (esviji.currentPieces[rock_x][rock_y] != esviji.ROCK) {
          esviji.currentPieces[rock_x][rock_y] = esviji.ROCK;
          positionedRocks++;
        }
      }
    }
  },

  drawPieces: function drawPieces() {
    esviji.drawnCurrentPieces = [];
    for(x = 1; x <= 6; x++) {
      esviji.drawnCurrentPieces[x] = [];
      for (y = 1; y <= 6; y++) {
        if (esviji.currentPieces[x][y] != esviji.EMPTY) {
          if (esviji.currentPieces[x][y] == esviji.ROCK) {
            pieceFile = 'themes/' + esviji.theme + '/' + esviji.themes[esviji.theme].rock + '.svg';
          } else {
            pieceFile = 'themes/' + esviji.theme + '/' + esviji.themes[esviji.theme].regularPieces[esviji.currentPieces[x][y] - 1] + '.svg';
          }
          piece_x = esviji.xToCoord(x);
          piece_y = esviji.yToCoord(y);
          esviji.drawnCurrentPieces[x][y] = esviji.board.image(pieceFile, piece_x, -30, 30, 30);
          esviji.drawnCurrentPieces[x][y].animate({'y': piece_y}, 2000, 'bounce');
        }
      }
    }
  },

  erasePieces: function erasePieces() {
    for(x = 1; x <= 6; x++) {
      if (esviji.drawnCurrentPieces[x] != undefined) {
        for (y = 1; y <= 6; y++) {
          if (esviji.drawnCurrentPieces[x][y] != null) {
            esviji.drawnCurrentPieces[x][y].remove();
          }
        }
      }
    }
  },

  getValidPieces: function getValidPieces() {
    var x, y, dir_x, dir_y, found;

    esviji.validPieces = [];

    for (y_start = 1; y_start <= 12; y_start++) {
      x = 9;
      y = y_start;
      dir_x = -1;
      dir_y = 0;
      found = false;
      while (!found) {
        if (y == 1 && dir_y == -1) {
          found = true;
        } else {
          if (x == 1 && dir_x == -1) {
            dir_x = 0;
            dir_y = -1;
          } else {
            nextPiece = esviji.currentPieces[x + dir_x][y + dir_y];
            if (nextPiece == esviji.ROCK) {
              if (dir_x == -1) {
                dir_x = 0;
                dir_y = -1;
              } else {
                found = true;
              }
            } else {
              if (nextPiece == esviji.EMPTY) {
                x += dir_x;
                y += dir_y;
              } else {
                if (esviji.validPieces.indexOf(nextPiece) == -1) {
                  esviji.validPieces.push(nextPiece);
                }
                found = true;
              }
            }
          }
        }
      }
    }
  },

  drawScore: function drawScore() {
    if (esviji.drawnScore != null) {
      esviji.drawnScore.remove();
    }
    esviji.drawnScore = esviji.board.print(170, 38, "score: " + esviji.score, esviji.board.getFont('ChewyRegular'), 24);
    esviji.drawnScore.attr({'fill': '#333366'});
  },

  drawLevel: function drawLevel() {
    if (esviji.drawnLevel != null) {
      esviji.drawnLevel.remove();
    }
    esviji.drawnLevel = esviji.board.print(10, 110, 'level ' + esviji.level, esviji.board.getFont('ChewyRegular'), 20);
    esviji.drawnLevel.attr({'fill': '#333366'});
  },

  drawLives: function drawLives() {
    if (esviji.drawnLives != null) {
      esviji.drawnLives.remove();
    }
    esviji.drawnLives = esviji.board.print(10, 140, esviji.lives + ' lives', esviji.board.getFont('ChewyRegular'), 20);
    esviji.drawnLives.attr({'fill': '#333366'});
  },

  run: function run() {
    esviji.init();
    esviji.nextLevel();
  }
}<|endoftext|>


<commit_before>//code snippet I created while working on a landing page

var isMobile = {
    Android: function() {
        return navigator.userAgent.match(/Android/i);
    },
    BlackBerry: function() {
        return navigator.userAgent.match(/BlackBerry/i);
    },
    iOS: function() {
        return navigator.userAgent.match(/iPhone|iPad|iPod/i);
    },
    Opera: function() {
        return navigator.userAgent.match(/Opera Mini/i);
    },
    Windows: function() {
        return navigator.userAgent.match(/IEMobile/i);
    },
    any: function() {
        return (isMobile.Android() || isMobile.BlackBerry() || isMobile.iOS() || isMobile.Opera() || isMobile.Windows());
    }
};

//use to create new objects
function jsonObject() {};

//Video functions
function Video() {
  this.mobileVidsArray = [
    "http://d2jq83h1vduk6u.cloudfront.net/carnival_111016_v4_mobile.mp4",
    "http://d2jq83h1vduk6u.cloudfront.net/Carnival_final_v04a_mobile.mp4",
    "http://d2jq83h1vduk6u.cloudfront.net/carnival_final_mobile.mp4"
  ]
  this.mobileVidsObj = new jsonObject();

  this.videos = $("video");
  this.videoPausePlay();
  this.createMobileVids(); //call before isMobileVideo() to pass json object
  this.isMobileVideo();
}

Video.prototype = {

  constructor: Video,

  createMobileVids: function() { //create json object from mobileVidsArray
    this.mobileVidsObj.Videos = new jsonObject();
    for (var i = 1; i <= this.mobileVidsArray.length; i++) {
      var sourceNum = "Source" + i; //Source1, Source2, Source3...
      this.mobileVidsObj.Videos[sourceNum] = this.mobileVidsArray[i-1]; //set keys as Source# and value as video link
    }
            // this.mobileVidsObj = {
            //   "Videos": {
            //     "Source1":"link",
            //     "Source2":"link",
            //     "Source3":"link",
            //   }
            // }
  },

  videoPausePlay: function() { //pause play a video on click
    var videoComponents = $("video, .play-button");
    var videos = this.videos;
    var pauseNotClicked = this.pauseNotClicked;

    videoComponents.click(function(e) {
      e.preventDefault();

      var dataAttribute = $(this)[0].getAttribute('data-attribute');
      var video = $("#video-" + dataAttribute)[0];
      var playButton = $("#video-" + dataAttribute + "-pb");

      if (video.paused == true) {
        playButton.hide();
        video.play();
        pauseNotClicked(videos, dataAttribute);
      }

      else {
        playButton.show();
        video.pause();
      }

    });
  },

  pauseNotClicked: function(videos, dataAttribute) { //pause currently playing videos when another video is clicked on for playing
    videos.each(function(index) {
      var currentDataAtt = videos[index].getAttribute('data-attribute'); //find data-attribute of current video element
      var playButton = $("#video-" + currentDataAtt + "-pb"); //play button for current video element

      if (currentDataAtt !== dataAttribute) { //pause all other videos when current video is clicked to play
        videos[index].pause();
        playButton.show();
      }
      else {
        videos[index].play();
        playButton.hide();
      }
    });

  },

  isMobileVideo: function() { //find out if a mobile video is needed because user agent matches a mobile device
    if( isMobile.any()) {
      var videos = this.videos;
      var mobileVidsObj = this.mobileVidsObj;
      videos.each(function(index) {
        var vidID = videos[index].getAttribute('data-attribute');
        var vidSource = "Source" + vidID
        videos[index].src = mobileVidsObj.Videos[vidSource];
      });
    }
  }
}

//Page scroll functions
function Page() {
  this.pageScroll();
}

Page.prototype = {

    constructor: Page,

    calculateDistance: function() {
        this.videoOffset = $('#video-1').offset().top;
        this.videoHeight = $('#video-1').height();
        this.windowHeight = $(window).height();

        this.distance = this.videoOffset - this.windowHeight + this.videoHeight;
        return this.distance;
    },

    pageScroll: function() {
      var page = $('html,body');
      var arrow = $('#scroll-arrow');
      var pageOn = this.pageOn;
      var pageOff = this.pageOff;
      var calculateDistance = this.calculateDistance;

      arrow.click(function(e) {

        var distance = calculateDistance(); //taken into account window resizing, recalculate distance

        e.preventDefault();
        pageOn(page);

        page.animate( {
          scrollTop: distance
        }, 1000, function() {
          pageOff(page);
        });

      });
    },
    pageOn: function(page) {
      page.on("scroll mousedown wheel DOMMouseScroll mousewheel keyup touchmove", function() {
        page.stop(); //stop scroll animation if user does something manual
      });
    },
    pageOff: function(page) {
      page.off("scroll mousedown wheel DOMMouseScroll mousewheel keyup touchmove");
    }
}

//initialize
function init() {
  new Page();
  new Video();
  isMobile.any();
}

document.addEventListener('DOMContentLoaded', function() {
  init();
}, false);

<commit_msg>update code<commit_after>//code snippet for a landing page involving multiple videos

"use strict";

const isMobile = {
    Android: function() {
        return navigator.userAgent.match(/Android/i);
    },
    BlackBerry: function() {
        return navigator.userAgent.match(/BlackBerry/i);
    },
    iOS: function() {
        return navigator.userAgent.match(/iPhone|iPad|iPod/i);
    },
    Opera: function() {
        return navigator.userAgent.match(/Opera Mini/i);
    },
    Windows: function() {
        return navigator.userAgent.match(/IEMobile/i);
    },
    any: function() {
        return (isMobile.Android() || isMobile.BlackBerry() || isMobile.iOS() || isMobile.Opera() || isMobile.Windows());
    }
};

//use to create new objects
function jsonObject() {};

//Video functions
function Video() {
  this.mobileVidsArray = [
    "http://d2jq83h1vduk6u.cloudfront.net/carnival_111016_v4_mobile.mp4",
    "http://d2jq83h1vduk6u.cloudfront.net/Carnival_final_v04a_mobile.mp4",
    "http://d2jq83h1vduk6u.cloudfront.net/carnival_final_mobile.mp4"
  ]
  this.mobileVidsObj = new jsonObject();

  this.videos = $("video");
  this.videoPausePlay();
  this.createMobileVids(); //call before isMobileVideo() to pass json object
  this.isMobileVideo();
}

Video.prototype = {

  constructor: Video,

  createMobileVids: function() { //create json object from mobileVidsArray
    this.mobileVidsObj.Videos = new jsonObject();
    for (let i = 1; i <= this.mobileVidsArray.length; i++) {
      let sourceNum = "Source" + i; //Source1, Source2, Source3...
      this.mobileVidsObj.Videos[sourceNum] = this.mobileVidsArray[i-1]; //set keys as Source# and value as video link
    }
            // this.mobileVidsObj = {
            //   "Videos": {
            //     "Source1":"link",
            //     "Source2":"link",
            //     "Source3":"link",
            //   }
            // }
  },

  videoPausePlay: function() { //pause play a video on click
    const videoComponents = $("video, .play-button");
    const videos = this.videos;
    const pauseNotClicked = this.pauseNotClicked;

    videoComponents.click(function(e) {
      e.preventDefault();

      const dataAttribute = $(this)[0].getAttribute('data-attribute');
      const video = $("#video-" + dataAttribute)[0];
      const playButton = $("#video-" + dataAttribute + "-pb");

      if (video.paused == true) {
        playButton.hide();
        video.play();
        pauseNotClicked(videos, dataAttribute);
      }

      else {
        playButton.show();
        video.pause();
      }

    });
  },

  pauseNotClicked: function(videos, dataAttribute) { //pause currently playing videos when another video is clicked on for playing
    videos.each(function(index) {
      const currentDataAtt = videos[index].getAttribute('data-attribute'); //find data-attribute of current video element
      const playButton = $("#video-" + currentDataAtt + "-pb"); //play button for current video element

      if (currentDataAtt !== dataAttribute) { //pause all other videos when current video is clicked to play
        videos[index].pause();
        playButton.show();
      }
      else {
        videos[index].play();
        playButton.hide();
      }
    });

  },

  isMobileVideo: function() { //find out if a mobile video is needed because user agent matches a mobile device
    if( isMobile.any()) {
      const videos = this.videos;
      const mobileVidsObj = this.mobileVidsObj;
      videos.each(function(index) {
        const vidID = videos[index].getAttribute('data-attribute');
        const vidSource = "Source" + vidID
        videos[index].src = mobileVidsObj.Videos[vidSource];
      });
    }
  }
}

//Page scroll functions
function Page() {
  this.pageScroll();
}

Page.prototype = {

    constructor: Page,

    calculateDistance: function(el) {
        el.videoOffset = $('#video-1').offset().top;
        el.videoHeight = $('#video-1').height();
        el.windowHeight = $(window).height();

        const distance = el.videoOffset - el.windowHeight + el.videoHeight;
        return distance;
    },

    pageScroll: function() {
      const page = $('html,body');
      const arrow = $('#scroll-arrow');
      const pageOn = this.pageOn;
      const pageOff = this.pageOff;
      const calculateDistance = this.calculateDistance;

      arrow.click(function(e) {
        const distance = calculateDistance(this); //taken into account window resizing, recalculate distance

        e.preventDefault();
        pageOn(page);

        page.animate( {
          scrollTop: distance
        }, 1000, function() {
          pageOff(page);
        });

      });
    },
    pageOn: function(page) {
      page.on("scroll mousedown wheel DOMMouseScroll mousewheel keyup touchmove", function() {
        page.stop(); //stop scroll animation if user does something manual
      });
    },
    pageOff: function(page) {
      page.off("scroll mousedown wheel DOMMouseScroll mousewheel keyup touchmove");
    }
}

//initialize
function init() {
  new Page();
  new Video();
  isMobile.any();
}

document.addEventListener('DOMContentLoaded', function() {
  init();
}, false);

<|endoftext|>


<commit_before>var http = require('http')
var fs = require('fs')
var getPort = require('get-port');
var srt2vtt = require('srt2vtt');
var internalIp = require('internal-ip');

var srtToVtt = function(sourceFile, destFile) {
  var srtFileContent = fs.readFileSync(sourceFile);
  srt2vtt(srtFileContent, function(err, data) {
    fs.writeFileSync(destFile, data);
  });
};

var attachSubtitles = function(ctx) {

  ctx.options.media.tracks = [{
    trackId: 1,
    type: 'TEXT',
    trackContentId: ctx.options.subtitles,
    trackContentType: 'text/vtt',
    name: 'English',
    language: 'en-US',
    subtype: 'SUBTITLES'
  }];

  ctx.options.media.textTrackStyle = {
    backgroundColor: '#FFFFFF00'
  };

  ctx.options.activeTrackIds = [1];
}

/*
** Handles subtitles, the process is the following:
**  - Is there subtitles in the command line ?
**  - Is there a media defined ?
**  - Are those subtitles stored locally (.srt) or on a distant server (.vtt) ?
**  - If they are stored remotely they are read remotely.
**  - If they are stored locally we need to convert and serve them via http.
*/
var subtitles = function(ctx, next) {
  if (!ctx.options.subtitles) return next();
  if (!ctx.options.media) ctx.options.media = {};

  var path = ctx.options.subtitles;

  if (!(fs.existsSync(path))) {
    // assume it's a HTTP URL.
    // Maybe we need to explicity check?
    attachSubtitles(ctx);
    return next();
  }

  var newpath = path + '.vtt';
  srtToVtt(path, newpath);
  path = newpath;

  fs.readFile(path, 'binary', function(err, file) {
    getPort(function(err, port) {
      if (err) return next();
      var addr = 'http://' + internalIp() + ':' + port;

      http.createServer(function(req, res) {
        res.writeHead(200, {
          'Access-Control-Allow-Origin': '*',
          'Content-Length': file.length,
          'Content-type': 'text/vtt;charset=utf-8'
        });

        res.end(file);
      }).listen(port);

      ctx.options.subtitles = addr;
      attachSubtitles(ctx);
      next();
    });
  });

}

module.exports = subtitles;
<commit_msg>keep srt/vtt in memory instead of writing a file to disk<commit_after>var http = require('http')
var fs = require('fs')
var getPort = require('get-port');
var srt2vtt = require('srt2vtt');
var internalIp = require('internal-ip');
var extname = require('path').extname;

var srtToVtt = function(sourceFile, cb) {
  var srtFileContent = fs.readFileSync(sourceFile);
  if (extname(sourceFile).toLowerCase() !== '.srt') {
    return cb(null, srtFileContent);
  }
  srt2vtt(srtFileContent, function(err, data) {
    if (err) return cb(err);
    cb(null, data);
  });
};

var attachSubtitles = function(ctx) {

  ctx.options.media.tracks = [{
    trackId: 1,
    type: 'TEXT',
    trackContentId: ctx.options.subtitles,
    trackContentType: 'text/vtt',
    name: 'English',
    language: 'en-US',
    subtype: 'SUBTITLES'
  }];

  ctx.options.media.textTrackStyle = {
    backgroundColor: '#FFFFFF00'
  };

  ctx.options.activeTrackIds = [1];
}

/*
** Handles subtitles, the process is the following:
**  - Is there subtitles in the command line ?
**  - Is there a media defined ?
**  - Are those subtitles stored locally (.srt) or on a distant server (.vtt) ?
**  - If they are stored remotely they are read remotely.
**  - If they are stored locally we need to convert and serve them via http.
*/
var subtitles = function(ctx, next) {
  if (!ctx.options.subtitles) return next();
  if (!ctx.options.media) ctx.options.media = {};

  var path = ctx.options.subtitles;

  if (!(fs.existsSync(path))) {
    // assume it's a HTTP URL.
    // Maybe we need to explicity check?
    attachSubtitles(ctx);
    return next();
  }

  srtToVtt(path, function(err, data) {
    if (err) return next();
    getPort(function(err, port) {
      if (err) return next();
      var addr = 'http://' + internalIp() + ':' + port;
      http.createServer(function(req, res) {
        res.writeHead(200, {
          'Access-Control-Allow-Origin': '*',
          'Content-Length': data.length,
          'Content-type': 'text/vtt;charset=utf-8'
        });
        res.end(data);
      }).listen(port);

      ctx.options.subtitles = addr;
      attachSubtitles(ctx);
      next();
    });
  });
}

module.exports = subtitles;
<|endoftext|>