<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.model", function (exports) {
    "use strict";

    var LayerProperties = {

        init: function (frame) {
            this.frame = frame;
            this.link = false;
            this.referenceElementId = "";
            this.referenceElementAuto = true;
            this.transitionTimingFunction = "linear";
            this.transitionRelativeZoom = 0;
            this.transitionPathId = "";
            return this;
        },

        initFrom: function (other) {
            this.frame = other.frame;
            this.link = other.link;
            this.referenceElementId = other.referenceElementId;
            this.referenceElementAuto = other.referenceElementAuto;
            this.transitionTimingFunction = other.transitionTimingFunction;
            this.transitionRelativeZoom = other.transitionRelativeZoom;
            this.transitionPathId = other.transitionPathId;
            return this;
        },

        toStorable: function () {
            return {
                link: this.link,
                referenceElementId: this.referenceElementId,
                referenceElementAuto: this.referenceElementAuto,
                transitionTimingFunction: this.transitionTimingFunction,
                transitionRelativeZoom: this.transitionRelativeZoom,
                transitionPathId: this.transitionPathId
            };
        },

        toMinimalStorable: function () {
            return {
                transitionTimingFunction: this.transitionTimingFunction,
                transitionRelativeZoom: this.transitionRelativeZoom,
                transitionPathId: this.transitionPathId
            };
        },

        fromStorable: function (storable) {
            this.link = storable.link;
            this.referenceElementId = storable.referenceElementId;
            this.referenceElementAuto = storable.referenceElementAuto;
            this.transitionTimingFunction = storable.transitionTimingFunction;
            this.transitionRelativeZoom = storable.transitionRelativeZoom;
            this.transitionPathId = storable.transitionPathId;
            return this;
        },

        get index() {
            return this.frame.layerProperties.indexOf(this);
        },

        get referenceElement() {
            return this.frame.presentation.svgRoot.getElementById(this.referenceElementId);
        },

        get transitionPath() {
            return this.frame.presentation.svgRoot.getElementById(this.transitionPathId);
        },

        get referenceElementHide() {
            return this.frame.presentation.elementsToHide.indexOf(this.referenceElementId) >= 0;
        },

        set referenceElementHide(hide) {
            var hidden = this.referenceElementHide;
            if (hide && !hidden) {
                this.frame.presentation.elementsToHide.push(this.referenceElementId);
            }
            else if (!hide && hidden) {
                var index = this.frame.presentation.elementsToHide.indexOf(this.referenceElementId);
                this.frame.presentation.elementsToHide.splice(index, 1);
            }
            if (this.referenceElement) {
                this.referenceElement.style.visibility = hide ? "hidden" : "visible";
            }
        },

        get transitionPathHide() {
            return this.frame.presentation.elementsToHide.indexOf(this.transitionPathId) >= 0;
        },

        set transitionPathHide(hide) {
            var hidden = this.transitionPathHide;
            if (hide && !hidden) {
                this.frame.presentation.elementsToHide.push(this.transitionPathId);
            }
            else if (!hide && hidden) {
                var index = this.frame.presentation.elementsToHide.indexOf(this.transitionPathId);
                this.frame.presentation.elementsToHide.splice(index, 1);
            }
            if (this.transitionPath) {
                this.transitionPath.style.visibility = hide ? "hidden" : "visible";
            }
        }
    };

    exports.Frame = {

        init: function (presentation) {
            this.presentation = presentation;
            this.frameId = presentation.makeFrameId();
            this.title = "New frame";
            this.timeoutMs = 0;
            this.timeoutEnable = false;
            this.transitionDurationMs = 1000;
            this.showInFrameList = true;
            this.layerProperties = presentation.layers.map(function () {
                return Object.create(LayerProperties).init(this);
            }, this);
            this.cameraStates = presentation.layers.map(function () {
                return Object.create(sozi.model.CameraState).init(presentation.svgRoot);
            });
            return this;
        },

        initFrom: function (other) {
            this.presentation = other.presentation;
            this.frameId = other.presentation.makeFrameId();
            this.title = other.title;
            this.timeoutMs = other.timeoutMs;
            this.timeoutEnable = other.timeoutEnable;
            this.transitionDurationMs = other.transitionDurationMs;
            this.showInFrameList = other.showInFrameList;
            this.layerProperties = other.layerProperties.map(function (lp) {
                return Object.create(LayerProperties).initFrom(lp);
            });
            this.cameraStates = other.cameraStates.map(function (cs) {
                return Object.create(sozi.model.CameraState).initFrom(cs);
            });
            return this;
        },

        toStorable: function () {
            var layerProperties = {};
            var cameraStates = {};
            var cameraOffsets = {};

            this.presentation.layers.forEach(function (layer, index) {
                var lp = this.layerProperties[index];
                var cs = this.cameraStates[index];
                var re = lp.referenceElement;

                var key = layer.groupId;
                layerProperties[key] = lp.toStorable();
                cameraStates[key] = cs.toStorable();
                if (re) {
                    cameraOffsets[key] = this.cameraStates[index].offsetFromElement(re);
                }
            }, this);

            return {
                frameId: this.frameId,
                title: this.title,
                timeoutMs: this.timeoutMs,
                timeoutEnable: this.timeoutEnable,
                transitionDurationMs: this.transitionDurationMs,
                showInFrameList: this.showInFrameList,
                layerProperties: layerProperties,
                cameraStates: cameraStates,
                cameraOffsets: cameraOffsets
            };
        },

        toMinimalStorable: function () {
            var layerProperties = {};
            var cameraStates = {};

            this.presentation.layers.forEach(function (layer, index) {
                var lp = this.layerProperties[index];
                var cs = this.cameraStates[index];

                var key = layer.groupId;
                layerProperties[key] = lp.toMinimalStorable();
                cameraStates[key] = cs.toMinimalStorable();
            }, this);

            return {
                frameId: this.frameId,
                title: this.title,
                timeoutMs: this.timeoutMs,
                timeoutEnable: this.timeoutEnable,
                transitionDurationMs: this.transitionDurationMs,
                showInFrameList: this.showInFrameList,
                layerProperties: layerProperties,
                cameraStates: cameraStates
            };
        },

        fromStorable: function (obj) {
            this.frameId = obj.frameId;
            this.title = obj.title;
            this.timeoutMs = obj.timeoutMs;
            this.timeoutEnable = obj.timeoutEnable;
            this.transitionDurationMs = obj.transitionDurationMs;
            this.showInFrameList = obj.showInFrameList;

            // TODO if obj.LayerProperties has keys not in layers, create fake layers marked as "deleted"
            this.presentation.layers.forEach(function (layer, index) {
                var key = layer.groupId;
                if (key in obj.layerProperties) {
                    var lp = this.layerProperties[index];
                    lp.fromStorable(obj.layerProperties[key]);

                    var cs = this.cameraStates[index].fromStorable(obj.cameraStates[key]);
                    var re = lp.referenceElement;
                    if (re) {
                        var ofs = obj.cameraOffsets[key] || {};
                        cs.setAtElement(re, ofs.deltaX, ofs.deltaY,
                                        ofs.widthFactor, ofs.heightFactor,
                                        ofs.deltaAngle);
                        // TODO compare current camera state with stored camera state.
                        // If different, mark the current layer as "dirty".
                    }
                }
                // TODO else, link to "auto" layer
            }, this);

            return this;
        },

        get index() {
            return this.presentation.frames.indexOf(this);
        },

        setAtStates: function (states) {
            states.forEach(function (state, index) {
                this.cameraStates[index].initFrom(state);
            }, this);
        }
    };

    exports.Layer = {

        init: function (presentation, label, auto) {
            this.presentation = presentation;
            this.label = label;
            this.auto = auto;
            this.svgNodes = [];
            return this;
        },

        get groupId() {
            return this.auto ? "__sozi_auto__" : this.svgNodes[0].getAttribute("id");
        },

        get index() {
            return this.presentation.layers.indexOf(this);
        },

        get isVisible() {
            return this.svgNodes.some(function (node) {
                return window.getComputedStyle(node).visibility === "visible";
            });
        },

        set isVisible(visible) {
            this.svgNodes.forEach(function (node) {
                node.style.visibility = visible ? "visible" : "hidden";
            });
        }
    };

    // Constant: the SVG namespace
    var SVG_NS = "http://www.w3.org/2000/svg";

    // Constant: the Inkscape namespace
    // var INKSCAPE_NS = "http://www.inkscape.org/namespaces/inkscape";

    // Constant: The SVG element names that can be found in layers
    var DRAWABLE_TAGS = [ "g", "image", "path", "rect", "circle",
        "ellipse", "line", "polyline", "polygon", "text", "clippath" ];

    exports.Presentation = {

        /*
         * Initialize a Sozi document object.
         *
         * Returns:
         *    - The current presentation object.
         */
        init: function (svgRoot) {
            this.svgRoot = svgRoot;
            this.frames = [];
            this.layers = [];
            this.elementsToHide = [];
            this.aspectWidth = 4;
            this.aspectHeight = 3;

            // Remove attributes that prevent correct rendering
            svgRoot.removeAttribute("viewBox");
            svgRoot.style.width = svgRoot.style.height = "auto";

            // Create an empty wrapper layer for elements that do not belong to a valid layer
            var autoLayer = Object.create(sozi.model.Layer).init(this, "auto", true);

            var svgWrapper = document.createElementNS(SVG_NS, "g");

            // Get all child nodes of the SVG root.
            // Make a copy of svgRoot.childNodes before modifying the document.
            var svgNodeList = Array.prototype.slice.call(svgRoot.childNodes);

            svgNodeList.forEach(function (svgNode) {
                // Remove text nodes and comments
                if (svgNode.tagName === undefined) {
                    svgRoot.removeChild(svgNode);
                }
                // Reorganize SVG elements
                else {
                    var nodeName = svgNode.localName.toLowerCase();
                    var nodeId = svgNode.getAttribute("id");

                    if (DRAWABLE_TAGS.indexOf(nodeName) >= 0) {
                        // The current node is a valid layer if it has the following characteristics:
                        //    - it is an SVG group element
                        //    - it has an id that has not been met before
                        if (nodeName === "g" && nodeId !== null &&
                            this.layers.every(function (layer) { return layer.nodeId !== nodeId; })) {
                            // If the current wrapper layer contains elements,
                            // add it to the document and to the list of layers.
                            if (svgWrapper.firstChild) {
                                svgRoot.insertBefore(svgWrapper, svgNode);
                                autoLayer.svgNodes.push(svgWrapper);

                                // Create a new empty wrapper layer
                                svgWrapper = document.createElementNS(SVG_NS, "g");
                            }

                            // Add the current node as a new layer.
                            var layer = Object.create(sozi.model.Layer).init(this, svgNode.hasAttribute("inkscape:label") ? svgNode.getAttribute("inkscape:label") : ("#" + nodeId), false);
                            layer.svgNodes.push(svgNode);
                            this.layers.push(layer);
                        }
                        else {
                            svgWrapper.appendChild(svgNode);
                        }
                    }
                }
            }, this);

            // If the current wrapper layer contains elements,
            // add it to the document and to the list of layers.
            if (svgWrapper.firstChild) {
                svgRoot.appendChild(svgWrapper);
                autoLayer.svgNodes.push(svgWrapper);
            }

            this.layers.push(autoLayer);

            return this;
        },

        toStorable: function () {
            return {
                aspectWidth: this.aspectWidth,
                aspectHeight: this.aspectHeight,
                frames: this.frames.map(function (frame) {
                    return frame.toStorable();
                }),
                elementsToHide: this.elementsToHide.slice()
            };
        },

        toMinimalStorable: function () {
            return {
                frames: this.frames.map(function (frame) {
                    return frame.toMinimalStorable();
                }),
                elementsToHide: this.elementsToHide.slice()
            };
        },

        fromStorable: function (obj) {
            this.aspectWidth = obj.aspectWidth;
            this.aspectHeight = obj.aspectHeight;

            this.frames = obj.frames.map(function (f) {
                return Object.create(sozi.model.Frame).init(this).fromStorable(f);
            }, this);

            this.elementsToHide = obj.elementsToHide.slice();

            return this;
        },

        get title() {
            var svgTitles = this.svgRoot.getElementsByTagNameNS(SVG_NS, "title");
            return svgTitles.length ? svgTitles[0].firstChild.wholeText.trim() : "Untitled";
        },

        makeFrameId: function () {
            var prefix = "frame";
            var suffix = Math.floor(1000 * (1 + 9 * Math.random()));
            var frameId;
            do {
                frameId = prefix + suffix;
                suffix ++;
            } while (this.frames.some(function (frame) {
                return frame.frameId === frameId;
            }));
            return frameId;
        },

        getFrameWithId: function (frameId) {
            for (var i = 0; i < this.frames.length; i ++) {
                if (this.frames[i].frameId === frameId) {
                    return this.frames[i];
                }
            }
            return null;
        },

        getLayerWithId: function (groupId) {
            for (var i = 0; i < this.layers.length; i ++) {
                if (this.layers[i].groupId === groupId) {
                    return this.layers[i];
                }
            }
            return null;
        },

        updateLinkedLayers: function () {
            if (!this.frames.length) {
                return;
            }

            var firstCameraStates = this.frames[0].cameraStates;
            var defaultCameraState = firstCameraStates[firstCameraStates.length - 1];

            this.layers.forEach(function (layer, layerIndex) {
                var cameraState = defaultCameraState;

                this.frames.forEach(function (frame) {
                    if (frame.layerProperties[layerIndex].link) {
                        frame.cameraStates[layerIndex].initFrom(cameraState);
                    }
                    else {
                        cameraState = frame.cameraStates[layerIndex];
                    }
                }, this);
            }, this);
        }
    };
});
<commit_msg>Prevent frame change when clicking a link<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.model", function (exports) {
    "use strict";

    var LayerProperties = {

        init: function (frame) {
            this.frame = frame;
            this.link = false;
            this.referenceElementId = "";
            this.referenceElementAuto = true;
            this.transitionTimingFunction = "linear";
            this.transitionRelativeZoom = 0;
            this.transitionPathId = "";
            return this;
        },

        initFrom: function (other) {
            this.frame = other.frame;
            this.link = other.link;
            this.referenceElementId = other.referenceElementId;
            this.referenceElementAuto = other.referenceElementAuto;
            this.transitionTimingFunction = other.transitionTimingFunction;
            this.transitionRelativeZoom = other.transitionRelativeZoom;
            this.transitionPathId = other.transitionPathId;
            return this;
        },

        toStorable: function () {
            return {
                link: this.link,
                referenceElementId: this.referenceElementId,
                referenceElementAuto: this.referenceElementAuto,
                transitionTimingFunction: this.transitionTimingFunction,
                transitionRelativeZoom: this.transitionRelativeZoom,
                transitionPathId: this.transitionPathId
            };
        },

        toMinimalStorable: function () {
            return {
                transitionTimingFunction: this.transitionTimingFunction,
                transitionRelativeZoom: this.transitionRelativeZoom,
                transitionPathId: this.transitionPathId
            };
        },

        fromStorable: function (storable) {
            this.link = storable.link;
            this.referenceElementId = storable.referenceElementId;
            this.referenceElementAuto = storable.referenceElementAuto;
            this.transitionTimingFunction = storable.transitionTimingFunction;
            this.transitionRelativeZoom = storable.transitionRelativeZoom;
            this.transitionPathId = storable.transitionPathId;
            return this;
        },

        get index() {
            return this.frame.layerProperties.indexOf(this);
        },

        get referenceElement() {
            return this.frame.presentation.svgRoot.getElementById(this.referenceElementId);
        },

        get transitionPath() {
            return this.frame.presentation.svgRoot.getElementById(this.transitionPathId);
        },

        get referenceElementHide() {
            return this.frame.presentation.elementsToHide.indexOf(this.referenceElementId) >= 0;
        },

        set referenceElementHide(hide) {
            var hidden = this.referenceElementHide;
            if (hide && !hidden) {
                this.frame.presentation.elementsToHide.push(this.referenceElementId);
            }
            else if (!hide && hidden) {
                var index = this.frame.presentation.elementsToHide.indexOf(this.referenceElementId);
                this.frame.presentation.elementsToHide.splice(index, 1);
            }
            if (this.referenceElement) {
                this.referenceElement.style.visibility = hide ? "hidden" : "visible";
            }
        },

        get transitionPathHide() {
            return this.frame.presentation.elementsToHide.indexOf(this.transitionPathId) >= 0;
        },

        set transitionPathHide(hide) {
            var hidden = this.transitionPathHide;
            if (hide && !hidden) {
                this.frame.presentation.elementsToHide.push(this.transitionPathId);
            }
            else if (!hide && hidden) {
                var index = this.frame.presentation.elementsToHide.indexOf(this.transitionPathId);
                this.frame.presentation.elementsToHide.splice(index, 1);
            }
            if (this.transitionPath) {
                this.transitionPath.style.visibility = hide ? "hidden" : "visible";
            }
        }
    };

    exports.Frame = {

        init: function (presentation) {
            this.presentation = presentation;
            this.frameId = presentation.makeFrameId();
            this.title = "New frame";
            this.timeoutMs = 0;
            this.timeoutEnable = false;
            this.transitionDurationMs = 1000;
            this.showInFrameList = true;
            this.layerProperties = presentation.layers.map(function () {
                return Object.create(LayerProperties).init(this);
            }, this);
            this.cameraStates = presentation.layers.map(function () {
                return Object.create(sozi.model.CameraState).init(presentation.svgRoot);
            });
            return this;
        },

        initFrom: function (other) {
            this.presentation = other.presentation;
            this.frameId = other.presentation.makeFrameId();
            this.title = other.title;
            this.timeoutMs = other.timeoutMs;
            this.timeoutEnable = other.timeoutEnable;
            this.transitionDurationMs = other.transitionDurationMs;
            this.showInFrameList = other.showInFrameList;
            this.layerProperties = other.layerProperties.map(function (lp) {
                return Object.create(LayerProperties).initFrom(lp);
            });
            this.cameraStates = other.cameraStates.map(function (cs) {
                return Object.create(sozi.model.CameraState).initFrom(cs);
            });
            return this;
        },

        toStorable: function () {
            var layerProperties = {};
            var cameraStates = {};
            var cameraOffsets = {};

            this.presentation.layers.forEach(function (layer, index) {
                var lp = this.layerProperties[index];
                var cs = this.cameraStates[index];
                var re = lp.referenceElement;

                var key = layer.groupId;
                layerProperties[key] = lp.toStorable();
                cameraStates[key] = cs.toStorable();
                if (re) {
                    cameraOffsets[key] = this.cameraStates[index].offsetFromElement(re);
                }
            }, this);

            return {
                frameId: this.frameId,
                title: this.title,
                timeoutMs: this.timeoutMs,
                timeoutEnable: this.timeoutEnable,
                transitionDurationMs: this.transitionDurationMs,
                showInFrameList: this.showInFrameList,
                layerProperties: layerProperties,
                cameraStates: cameraStates,
                cameraOffsets: cameraOffsets
            };
        },

        toMinimalStorable: function () {
            var layerProperties = {};
            var cameraStates = {};

            this.presentation.layers.forEach(function (layer, index) {
                var lp = this.layerProperties[index];
                var cs = this.cameraStates[index];

                var key = layer.groupId;
                layerProperties[key] = lp.toMinimalStorable();
                cameraStates[key] = cs.toMinimalStorable();
            }, this);

            return {
                frameId: this.frameId,
                title: this.title,
                timeoutMs: this.timeoutMs,
                timeoutEnable: this.timeoutEnable,
                transitionDurationMs: this.transitionDurationMs,
                showInFrameList: this.showInFrameList,
                layerProperties: layerProperties,
                cameraStates: cameraStates
            };
        },

        fromStorable: function (obj) {
            this.frameId = obj.frameId;
            this.title = obj.title;
            this.timeoutMs = obj.timeoutMs;
            this.timeoutEnable = obj.timeoutEnable;
            this.transitionDurationMs = obj.transitionDurationMs;
            this.showInFrameList = obj.showInFrameList;

            // TODO if obj.LayerProperties has keys not in layers, create fake layers marked as "deleted"
            this.presentation.layers.forEach(function (layer, index) {
                var key = layer.groupId;
                if (key in obj.layerProperties) {
                    var lp = this.layerProperties[index];
                    lp.fromStorable(obj.layerProperties[key]);

                    var cs = this.cameraStates[index].fromStorable(obj.cameraStates[key]);
                    var re = lp.referenceElement;
                    if (re) {
                        var ofs = obj.cameraOffsets[key] || {};
                        cs.setAtElement(re, ofs.deltaX, ofs.deltaY,
                                        ofs.widthFactor, ofs.heightFactor,
                                        ofs.deltaAngle);
                        // TODO compare current camera state with stored camera state.
                        // If different, mark the current layer as "dirty".
                    }
                }
                // TODO else, link to "auto" layer
            }, this);

            return this;
        },

        get index() {
            return this.presentation.frames.indexOf(this);
        },

        setAtStates: function (states) {
            states.forEach(function (state, index) {
                this.cameraStates[index].initFrom(state);
            }, this);
        }
    };

    exports.Layer = {

        init: function (presentation, label, auto) {
            this.presentation = presentation;
            this.label = label;
            this.auto = auto;
            this.svgNodes = [];
            return this;
        },

        get groupId() {
            return this.auto ? "__sozi_auto__" : this.svgNodes[0].getAttribute("id");
        },

        get index() {
            return this.presentation.layers.indexOf(this);
        },

        get isVisible() {
            return this.svgNodes.some(function (node) {
                return window.getComputedStyle(node).visibility === "visible";
            });
        },

        set isVisible(visible) {
            this.svgNodes.forEach(function (node) {
                node.style.visibility = visible ? "visible" : "hidden";
            });
        }
    };

    // Constant: the SVG namespace
    var SVG_NS = "http://www.w3.org/2000/svg";

    // Constant: the Inkscape namespace
    // var INKSCAPE_NS = "http://www.inkscape.org/namespaces/inkscape";

    // Constant: The SVG element names that can be found in layers
    var DRAWABLE_TAGS = [ "g", "image", "path", "rect", "circle",
        "ellipse", "line", "polyline", "polygon", "text", "clippath" ];

    exports.Presentation = {

        /*
         * Initialize a Sozi document object.
         *
         * Returns:
         *    - The current presentation object.
         */
        init: function (svgRoot) {
            this.svgRoot = svgRoot;
            this.frames = [];
            this.layers = [];
            this.elementsToHide = [];
            this.aspectWidth = 4;
            this.aspectHeight = 3;

            // Remove attributes that prevent correct rendering
            svgRoot.removeAttribute("viewBox");
            svgRoot.style.width = svgRoot.style.height = "auto";

            // Create an empty wrapper layer for elements that do not belong to a valid layer
            var autoLayer = Object.create(sozi.model.Layer).init(this, "auto", true);

            var svgWrapper = document.createElementNS(SVG_NS, "g");

            // Get all child nodes of the SVG root.
            // Make a copy of svgRoot.childNodes before modifying the document.
            var svgNodeList = Array.prototype.slice.call(svgRoot.childNodes);

            svgNodeList.forEach(function (svgNode) {
                // Remove text nodes and comments
                if (svgNode.tagName === undefined) {
                    svgRoot.removeChild(svgNode);
                }
                // Reorganize SVG elements
                else {
                    var nodeName = svgNode.localName.toLowerCase();
                    var nodeId = svgNode.getAttribute("id");

                    if (DRAWABLE_TAGS.indexOf(nodeName) >= 0) {
                        // The current node is a valid layer if it has the following characteristics:
                        //    - it is an SVG group element
                        //    - it has an id that has not been met before
                        if (nodeName === "g" && nodeId !== null &&
                            this.layers.every(function (layer) { return layer.nodeId !== nodeId; })) {
                            // If the current wrapper layer contains elements,
                            // add it to the document and to the list of layers.
                            if (svgWrapper.firstChild) {
                                svgRoot.insertBefore(svgWrapper, svgNode);
                                autoLayer.svgNodes.push(svgWrapper);

                                // Create a new empty wrapper layer
                                svgWrapper = document.createElementNS(SVG_NS, "g");
                            }

                            // Add the current node as a new layer.
                            var layer = Object.create(sozi.model.Layer).init(this, svgNode.hasAttribute("inkscape:label") ? svgNode.getAttribute("inkscape:label") : ("#" + nodeId), false);
                            layer.svgNodes.push(svgNode);
                            this.layers.push(layer);
                        }
                        else {
                            svgWrapper.appendChild(svgNode);
                        }
                    }
                }
            }, this);

            // If the current wrapper layer contains elements,
            // add it to the document and to the list of layers.
            if (svgWrapper.firstChild) {
                svgRoot.appendChild(svgWrapper);
                autoLayer.svgNodes.push(svgWrapper);
            }

            this.layers.push(autoLayer);

            // Prevent event propagation on hyperlinks
            var links = Array.prototype.slice.call(svgRoot.getElementsByTagName("a"));

            links.forEach(function (link) {
                link.addEventListener("mousedown", function (evt) {
                    evt.stopPropagation();
                }, false);
            });

            return this;
        },

        toStorable: function () {
            return {
                aspectWidth: this.aspectWidth,
                aspectHeight: this.aspectHeight,
                frames: this.frames.map(function (frame) {
                    return frame.toStorable();
                }),
                elementsToHide: this.elementsToHide.slice()
            };
        },

        toMinimalStorable: function () {
            return {
                frames: this.frames.map(function (frame) {
                    return frame.toMinimalStorable();
                }),
                elementsToHide: this.elementsToHide.slice()
            };
        },

        fromStorable: function (obj) {
            this.aspectWidth = obj.aspectWidth;
            this.aspectHeight = obj.aspectHeight;

            this.frames = obj.frames.map(function (f) {
                return Object.create(sozi.model.Frame).init(this).fromStorable(f);
            }, this);

            this.elementsToHide = obj.elementsToHide.slice();

            return this;
        },

        get title() {
            var svgTitles = this.svgRoot.getElementsByTagNameNS(SVG_NS, "title");
            return svgTitles.length ? svgTitles[0].firstChild.wholeText.trim() : "Untitled";
        },

        makeFrameId: function () {
            var prefix = "frame";
            var suffix = Math.floor(1000 * (1 + 9 * Math.random()));
            var frameId;
            do {
                frameId = prefix + suffix;
                suffix ++;
            } while (this.frames.some(function (frame) {
                return frame.frameId === frameId;
            }));
            return frameId;
        },

        getFrameWithId: function (frameId) {
            for (var i = 0; i < this.frames.length; i ++) {
                if (this.frames[i].frameId === frameId) {
                    return this.frames[i];
                }
            }
            return null;
        },

        getLayerWithId: function (groupId) {
            for (var i = 0; i < this.layers.length; i ++) {
                if (this.layers[i].groupId === groupId) {
                    return this.layers[i];
                }
            }
            return null;
        },

        updateLinkedLayers: function () {
            if (!this.frames.length) {
                return;
            }

            var firstCameraStates = this.frames[0].cameraStates;
            var defaultCameraState = firstCameraStates[firstCameraStates.length - 1];

            this.layers.forEach(function (layer, layerIndex) {
                var cameraState = defaultCameraState;

                this.frames.forEach(function (frame) {
                    if (frame.layerProperties[layerIndex].link) {
                        frame.cameraStates[layerIndex].initFrom(cameraState);
                    }
                    else {
                        cameraState = frame.cameraStates[layerIndex];
                    }
                }, this);
            }, this);
        }
    };
});
<|endoftext|>


<commit_before>/********************************************************
 * Copyright (c) 2013 Azavea, Inc.
 * See LICENSE in the project root for copying permission
 ********************************************************/

'use strict';

define(['jquery', 'Leaflet', 'text!templates/neighborhoodList.html', 'text!templates/locationList.html', 'topojson', 'icons', 'common', CEL.serverVars.gmapRequire, 'styling'],
    function($, L, neighborhoodList, locationList, topojson, icons, common) {

        var map,   // Leaflet map
            gmap,    // Google basemap
            locations,    // Store location data
            neighborhoods,    // Store neighborhood data
            template,    // Hold handlebars template
            listView = 'none',    // Keep track of current result list type
            locationLayer = new L.LayerGroup(),   // Location/school layer group
            neighborhoodLayer = new L.LayerGroup(),   // Neighborhood layer group
            neighborhoodGeojson;    // Store neighborhood geojson

        /**
         * Loads Json data for neighborhoods and locations
         * then displays one based on current zoom level
         */
        var loadData = function() {
            $.when(
                $.getJSON(common.getUrl('location-api'), function(data) {
                    locations = data.locations;
                }),
                $.getJSON(common.getUrl('neighborhood-api'), function(data) {
                    neighborhoods = data;
                }),
                $.getJSON(common.getUrl('neighborhoods-topo'), function(data) {
                    neighborhoodLayer = L.geoJson(null, {
                        style: {
                            color: '#666',
                            weight: 1,
                            opacity: 1,
                            fillOpacity: 0.3
                        }
                    });
                    neighborhoodGeojson = topojson.feature(data, data.objects.neighborhoods);
                })).then(displayMap);
        };

        /**
         * Controls logic of whether to display locations or neighborhoods
         * based on current zoom level. Called when map is initialized
         * and after a change in zoom level.
         */
        var displayMap = function() {
            var zoom_level = map.getZoom();
            if (listView !== 'neighborhoods' && zoom_level < 13) {
                // If not already displaying neighborhoods and zoomed out
                listView = 'neighborhoods';
                listResults(neighborhoods);
                locationLayer.clearLayers();
                neighborhoodLayer.clearLayers();
                neighborhoodLayer.addData(neighborhoodGeojson);
                map.addLayer(neighborhoodLayer);
            }
            else if (listView !== 'locations' && zoom_level >= 13) {
                // If not already displaying locations and zoomed in
                map.removeLayer(neighborhoodLayer);
                map.addLayer(locationLayer);
                listView = 'locations';
                listResults(locations);
                for( var i=0; i < locations.length; i++){
                    var lat = locations[i].position.lat,
                        lng = locations[i].position.lng,
                        loc_marker = L.marker([lat, lng], {icon: icons.schoolIcon});
                    locationLayer.addLayer(loc_marker);
                };
            }
        };

        /**
         * Changes list results display using Handlebars templates
         * @param {Array of neighborhoods or locations} data
         */
        var listResults = function(data) {
            if (data.neighborhoods) {
                template = Handlebars.compile(neighborhoodList);
            } else {
                template = Handlebars.compile(locationList);
            }
            $('.locations-wrapper').empty();
            $('.locations-wrapper').append(template(data));
        };

        // Load data and build map when page loads
        $(document).ready( function(){
            map = new L.map('map').setView([41.88, -87.62], 10);    // Initialize Leaflet map
            gmap = new L.Google('ROADMAP');    // Add Google baselayer
            map.addLayer(gmap);
            map.on('zoomend', displayMap);    // Set event handler to call displayMap when zoom changes
            loadData();    // Load initial data
        });

    }
);
<commit_msg>Use currentLayer and layerType as per Bennet's suggestion<commit_after>/********************************************************
 * Copyright (c) 2013 Azavea, Inc.
 * See LICENSE in the project root for copying permission
 ********************************************************/

'use strict';

define(['jquery', 'Leaflet', 'text!templates/neighborhoodList.html', 'text!templates/locationList.html', 'topojson', 'icons', 'common', CEL.serverVars.gmapRequire, 'styling'],
    function($, L, neighborhoodList, locationList, topojson, icons, common) {

        var map,   // Leaflet map
            gmap,    // Google basemap
            locations,    // Store location data
            neighborhoods,    // Store neighborhood data
            template,    // Hold handlebars template
            layerType = { none: 'none', neighborhood: 'neighborhood', location: 'location'},
            // Keep track of current layer displayed
            currentLayer = layerType.none,
            locationLayer = new L.LayerGroup(),   // Location/school layer group
            neighborhoodLayer = new L.LayerGroup(),   // Neighborhood layer group
            neighborhoodGeojson;    // Store neighborhood geojson

        /**
         * Loads Json data for neighborhoods and locations
         * then displays one based on current zoom level
         */
        var loadData = function() {
            $.when(
                $.getJSON(common.getUrl('location-api'), function(data) {
                    locations = data.locations;
                }),
                $.getJSON(common.getUrl('neighborhood-api'), function(data) {
                    neighborhoods = data;
                }),
                $.getJSON(common.getUrl('neighborhoods-topo'), function(data) {
                    neighborhoodLayer = L.geoJson(null, {
                        style: {
                            color: '#666',
                            weight: 1,
                            opacity: 1,
                            fillOpacity: 0.3
                        }
                    });
                    neighborhoodGeojson = topojson.feature(data, data.objects.neighborhoods);
                })).then(displayMap);
        };

        /**
         * Controls logic of whether to display locations or neighborhoods
         * based on current zoom level. Called when map is initialized
         * and after a change in zoom level.
         */
        var displayMap = function() {
            var zoom_level = map.getZoom();
            if (currentLayer !== 'neighborhood' && zoom_level < 13) {
                // If not already displaying neighborhoods and zoomed out
                currentLayer = layerType.neighborhood;
                listResults(neighborhoods);
                locationLayer.clearLayers();
                neighborhoodLayer.clearLayers();
                neighborhoodLayer.addData(neighborhoodGeojson);
                map.addLayer(neighborhoodLayer);
            }
            else if (currentLayer !== 'location' && zoom_level >= 13) {
                // If not already displaying locations and zoomed in
                map.removeLayer(neighborhoodLayer);
                map.addLayer(locationLayer);
                currentLayer = layerType.location;
                listResults(locations);
                for(var i = 0; i < locations.length; i++){
                    var lat = locations[i].position.lat,
                        lng = locations[i].position.lng,
                        loc_marker = L.marker([lat, lng], {icon: icons.schoolIcon});
                    locationLayer.addLayer(loc_marker);
                };
            }
        };

        /**
         * Changes list results display using Handlebars templates
         * @param {Array of neighborhoods or locations} data
         */
        var listResults = function(data) {
            if (data.neighborhoods) {
                template = Handlebars.compile(neighborhoodList);
            } else {
                template = Handlebars.compile(locationList);
            }
            $('.locations-wrapper').empty();
            $('.locations-wrapper').append(template(data));
        };

        // Load data and build map when page loads
        $(document).ready( function(){
            map = new L.map('map').setView([41.88, -87.62], 10);    // Initialize Leaflet map
            gmap = new L.Google('ROADMAP');    // Add Google baselayer
            map.addLayer(gmap);
            map.on('zoomend', displayMap);    // Set event handler to call displayMap when zoom changes
            loadData();    // Load initial data
        });

    }
);
<|endoftext|>


<commit_before>import { Fragment } from 'react'
import Head from 'next/head'
import Link from 'next/link'
import Layout from 'components/Layout'
import client from 'libs/Client'
import { intToDate } from 'libs/date'
import SocialShareButtons from 'components/SocialShareButtons'

const domain = process.env.NEXT_PUBLIC_URL

export default function Wallpaper({ wallpaper }) {
  const { filename, title, copyright, date, tags } = wallpaper

  const t = Object.entries(tags).sort((a, b) => b[1] - a[1])

  const tagFields = t.map((l, i) => (
    <Fragment key={i}><Link href={`/bingwallpapers/tags/${l[0]}`}>{l[0]}</Link> </Fragment>
  ))

  return (
    <Layout>

        <title key="title">{title} - Bing Wallpapers - {process.env.NEXT_PUBLIC_NAME}</title>
        <meta name="keywords" content={t.reduce((a, c) => `${a}, ${c[0]}`, '')} />
        <meta name="twitter:card" content="summary_large_image" />
        <meta property="og:title" content={`${title} - Bing Wallpapers - ${process.env.NEXT_PUBLIC_NAME}`} />
        <meta property="og:description" content={`${title} ${copyright}`} />
        <meta property="og:image" content={`https://images.sonurai.com/${filename}.jpg`} />

{title}

      <a href={`https://images.sonurai.com/${filename}.jpg`}>
        <img className="img-fluid" src={`https://images.sonurai.com/${filename}.jpg`} alt={title} />

{copyright} - {intToDate(date)}

        <SocialShareButtons
          url={`${domain}/bingwallpapers/${wallpaper.id}`}
          media={`https://images.sonurai.com/${filename}.jpg`}
          desc={`${title} - ${process.env.NEXT_PUBLIC_NAME}`}
          size={40}
        />

{tagFields}

    </Layout>
  )
}

export async function getServerSideProps({ query }) {
  const { id } = query
  const data = await client.getWallpaper(id)

  if (!data.wallpaper) {
    return { notFound: true }
  }

  if (!isNaN(Number(id))) {
    return { redirect: { destination: `/bingwallpapers/${data.wallpaper.id}`, permanent: true } }
  }

  return { props: { wallpaper: data.wallpaper } }
}
<commit_msg>Fix CLS<commit_after>import { Fragment } from 'react'
import Head from 'next/head'
import Link from 'next/link'
import Layout from 'components/Layout'
import client from 'libs/Client'
import { intToDate } from 'libs/date'
import SocialShareButtons from 'components/SocialShareButtons'

const domain = process.env.NEXT_PUBLIC_URL

export default function Wallpaper({ wallpaper }) {
  const { filename, title, copyright, date, tags } = wallpaper

  const t = Object.entries(tags).sort((a, b) => b[1] - a[1])

  const tagFields = t.map((l, i) => (
    <Fragment key={i}><Link href={`/bingwallpapers/tags/${l[0]}`}>{l[0]}</Link> </Fragment>
  ))

  return (
    <Layout>

        <title key="title">{title} - Bing Wallpapers - {process.env.NEXT_PUBLIC_NAME}</title>
        <meta name="keywords" content={t.reduce((a, c) => `${a}, ${c[0]}`, '')} />
        <meta name="twitter:card" content="summary_large_image" />
        <meta property="og:title" content={`${title} - Bing Wallpapers - ${process.env.NEXT_PUBLIC_NAME}`} />
        <meta property="og:description" content={`${title} ${copyright}`} />
        <meta property="og:image" content={`https://images.sonurai.com/${filename}.jpg`} />

{title}

      <a href={`https://images.sonurai.com/${filename}.jpg`}>
        <img className="img-fluid" src={`https://images.sonurai.com/${filename}.jpg`} width={1920} height={1200} alt={title} />

{copyright} - {intToDate(date)}

        <SocialShareButtons
          url={`${domain}/bingwallpapers/${wallpaper.id}`}
          media={`https://images.sonurai.com/${filename}.jpg`}
          desc={`${title} - ${process.env.NEXT_PUBLIC_NAME}`}
          size={40}
        />

{tagFields}

    </Layout>
  )
}

export async function getServerSideProps({ query }) {
  const { id } = query
  const data = await client.getWallpaper(id)

  if (!data.wallpaper) {
    return { notFound: true }
  }

  if (!isNaN(Number(id))) {
    return { redirect: { destination: `/bingwallpapers/${data.wallpaper.id}`, permanent: true } }
  }

  return { props: { wallpaper: data.wallpaper } }
}
<|endoftext|>


<commit_before>'use strict';

var ConstDependency = require('webpack/lib/dependencies/ConstDependency');

/**
 * Adds support for Jest API.
 * e.g. jest.dontMock('./foo') => jest.dontMock(1);
 * @constructor
 */
function JestpackPlugin() {}

/**
 * Resolves module paths.
 * @param  {Object} expr     The expression.
 * @param  {Number} argIndex Which argument to resolve, defaults to the first arg.
 */
function resolveArgument(expr, argIndex) {
    if (typeof argIndex === 'undefined') {
        argIndex = 0;
    }
    if (!expr.arguments.length || !expr.arguments[argIndex]) {
        return;
    }
    this.applyPluginsBailResult('call require:commonjs:item', expr,
            this.evaluateExpression(expr.arguments[argIndex]));
}

/**
 * Resolves Jest API `moduleName` arguments and updates the Webpack runtime.
 * @return {Undefined} undefined.
 */
JestpackPlugin.prototype.apply = function(compiler) {

    compiler.parser.plugin('call jest.dontMock', resolveArgument);
    compiler.parser.plugin('call jest.mock', resolveArgument);
    compiler.parser.plugin('call jest.genMockFromModule', resolveArgument);
    compiler.parser.plugin('call jest.setMock', resolveArgument);
    compiler.parser.plugin('call jest._registerManualMock', function(expr) {
        resolveArgument.call(this, expr);
        resolveArgument.call(this, expr, 1);
    });
    compiler.parser.plugin('call require.requireActual', resolveArgument);
    compiler.parser.plugin('expression require.requireActual', function(expr) {
        var dep = new ConstDependency('__webpack_require__.requireActual', expr.range);
        dep.loc = expr.loc;
        this.state.current.addDependency(dep);
        return true;
    });

    compiler.plugin('compilation', function(compilation) {

        // Replace `__webpack_require__` with `require` as defined by Jest's JSDom env.
        compilation.mainTemplate.plugin('module-require', function() {
            return 'jest._webpackRequire';
        });

        // Make all references to the module cache global so 'jestpack/ModuleLoader'
        // can manage the object *and the reference*.
        compilation.mainTemplate.plugin('local-vars', function(source) {
            return source.replace('var installedModules', 'window.installedModules');
        });

        // Expose `__webpack_require__` for 'jestpack/ModuleLoader'.
        compilation.mainTemplate.plugin('startup', function(source) {
            return [
                '',
                'jest._setupWebpackRequire(__webpack_require__);',
                source
            ].join('\n');
        });

    });

};

module.exports = JestpackPlugin;
<commit_msg>Convert Plugin to es6<commit_after>'use strict';

const ConstDependency = require('webpack/lib/dependencies/ConstDependency');

/**
 * Resolves module paths.
 * @param  {Object} expr     The expression.
 * @param  {Number} argIndex Which argument to resolve, defaults to the first arg.
 */
function resolveArgument(expr, argIndex) {
    if (typeof argIndex === 'undefined') {
        argIndex = 0;
    }
    if (!expr.arguments.length || !expr.arguments[argIndex]) {
        return;
    }
    this.applyPluginsBailResult('call require:commonjs:item', expr,
            this.evaluateExpression(expr.arguments[argIndex]));
}

/**
 * Adds support for Jest API.
 * e.g. jest.dontMock('./foo') => jest.dontMock(1);
 * @constructor
 */
class JestpackPlugin {

    /**
     * Resolves Jest API `moduleName` arguments and updates the Webpack runtime.
     * @param  {Object} compiler The Webpack compiler.
     */
    apply(compiler) {

        compiler.parser.plugin('call jest.dontMock', resolveArgument);
        compiler.parser.plugin('call jest.mock', resolveArgument);
        compiler.parser.plugin('call jest.genMockFromModule', resolveArgument);
        compiler.parser.plugin('call jest.setMock', resolveArgument);
        compiler.parser.plugin('call jest._registerManualMock', function(expr) {
            resolveArgument.call(this, expr);
            resolveArgument.call(this, expr, 1);
        });
        compiler.parser.plugin('call require.requireActual', resolveArgument);
        compiler.parser.plugin('expression require.requireActual', function(expr) {
            var dep = new ConstDependency('__webpack_require__.requireActual', expr.range);
            dep.loc = expr.loc;
            this.state.current.addDependency(dep);
            return true;
        });

        compiler.plugin('compilation', compilation => {

            // Replace `__webpack_require__` with `require` as defined by Jest's JSDom env.
            compilation.mainTemplate.plugin('module-require', () => {
                return 'jest._webpackRequire';
            });

            // Make all references to the module cache global so 'jestpack/ModuleLoader'
            // can manage the object *and the reference*.
            compilation.mainTemplate.plugin('local-vars', source => {
                return source.replace('var installedModules', 'window.installedModules');
            });

            // Provide `__webpack_require__` to 'jestpack/ModuleLoader'.
            compilation.mainTemplate.plugin('startup', source => {
                return [
                    '',
                    'jest._setupWebpackRequire(__webpack_require__);',
                    source
                ].join('\n');
            });
        });
    }
}

module.exports = JestpackPlugin;
<|endoftext|>


<commit_before>/**
 * SettingsModule component.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * External dependencies
 */
import PropTypes from 'prop-types';
import { filter, map } from 'lodash';
import classnames from 'classnames';

/**
 * WordPress dependencies
 */
import { Component, Fragment } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { compose } from '@wordpress/compose';
import { applyFilters } from '@wordpress/hooks';
import { ESCAPE } from '@wordpress/keycodes';

/**
 * Internal dependencies
 */
import Data from 'googlesitekit-data';
import PencilIcon from '../../../svg/pencil.svg';
import TrashIcon from '../../../svg/trash.svg';
import {
	activateOrDeactivateModule,
	getReAuthURL,
	showErrorNotification,
	getModulesData,
	listFormat,
} from '../../util';
import { refreshAuthentication } from '../../util/refresh-authentication';
import Link from '../Link';
import Button from '../../components/Button';
import data, { TYPE_MODULES } from '../../components/data';
import SettingsOverlay from '../../components/settings/SettingsOverlay';
import Spinner from '../Spinner';
import GenericError from '../legacy-notifications/generic-error';
import Dialog from '../../components/Dialog';
import ModuleIcon from '../ModuleIcon';
import { CORE_MODULES } from '../../googlesitekit/modules/datastore/constants';
import SettingsRenderer from '../settings/SettingsRenderer';
import VisuallyHidden from '../VisuallyHidden';
import { Grid, Row, Cell } from '../../material-components/layout';
const { withSelect } = Data;

/**
 * A single module. Keeps track of its own active state and settings.
 */
class SettingsModule extends Component {
	constructor( props ) {
		super( props );
		this.state = {
			isSaving: false,
			setupComplete: props.setupComplete,
			dialogActive: false,
		};

		this.deactivate = this.deactivate.bind( this );
		this.activateOrDeactivate = this.activateOrDeactivate.bind( this );
		this.handleDialog = this.handleDialog.bind( this );
		this.handleCloseModal = this.handleCloseModal.bind( this );
		this.handleConfirmRemoveModule = this.handleConfirmRemoveModule.bind( this );
		this.handleConfirmOrCancel = this.handleConfirmOrCancel.bind( this );
		this.handleCancel = this.handleCancel.bind( this );
		this.handleEdit = this.handleEdit.bind( this );
	}

	componentDidMount() {
		global.addEventListener( 'keyup', this.handleCloseModal, false );
	}

	componentWillUnmount() {
		global.removeEventListener( 'keyup', this.handleCloseModal );
	}

	async activateOrDeactivate() {
		try {
			this.setState( { isSaving: true } );

			await activateOrDeactivateModule(
				data,
				this.props.slug,
				false
			);

			await refreshAuthentication();
			data.invalidateCacheGroup( TYPE_MODULES, this.props.slug );

			this.setState( {
				isSaving: false,
			} );

			global.location = getReAuthURL( this.props.slug, false );
		} catch ( err ) {
			showErrorNotification( GenericError, {
				id: 'activate-module-error',
				title: __( 'Internal Server Error', 'google-site-kit' ),
				description: err.message,
				format: 'small',
				type: 'win-error',
			} );
			this.setState( { isSaving: false } );
		}
	}

	deactivate() {
		const { autoActivate } = this.props;

		if ( autoActivate ) {
			return;
		}

		this.activateOrDeactivate();
	}

	handleDialog() {
		this.setState( ( prevState ) => {
			return {
				dialogActive: ! prevState.dialogActive,
			};
		} );
	}

	// Handle user click on the confirm removal button.
	handleConfirmRemoveModule() {
		this.deactivate();
	}

	handleCloseModal( e ) {
		if ( ESCAPE === e.keyCode ) {
			this.setState( {
				dialogActive: false,
			} );
		}
	}

	handleConfirmOrCancel() {
		const {
			setupComplete,
		} = this.state;

		const {
			hasSettings,
			slug,
			onCancel,
			onConfirm,
		} = this.props;

		if ( hasSettings && setupComplete && typeof onConfirm === 'function' ) {
			onConfirm( slug );
		} else if ( typeof onCancel === 'function' ) {
			onCancel( slug );
		}
	}

	handleCancel() {
		const {
			slug,
			onCancel,
		} = this.props;

		if ( typeof onCancel === 'function' ) {
			onCancel( slug );
		}
	}

	handleEdit() {
		const {
			slug,
			onEdit,
		} = this.props;

		if ( typeof onEdit === 'function' ) {
			onEdit( slug );
		}
	}

	// Find modules that depend on a module.
	getDependentModules() {
		const { slug } = this.props;
		const modules = getModulesData();
		const dependants = {};

		if ( modules[ slug ].dependants ) {
			modules[ slug ].dependants.forEach( ( dependantSlug ) => {
				if ( modules[ dependantSlug ] ) {
					dependants[ dependantSlug ] = modules[ dependantSlug ];
				}
			} );
		}

		return dependants;
	}

	render() {
		const {
			setupComplete,
			dialogActive,
		} = this.state;

		const {
			name,
			slug,
			homepage,
			isEditing,
			isOpen,
			handleAccordion,
			hasSettings,
			canSubmitChanges,
			provides,
			isSaving,
			error,
			autoActivate,
		} = this.props;
		const moduleKey = `${ slug }-module`;
		const isConnected = applyFilters( `googlesitekit.Connected-${ slug }`, setupComplete );
		const connectedClassName = isConnected
			? 'googlesitekit-settings-module__status-icon--connected'
			: 'googlesitekit-settings-module__status-icon--not-connected';

		/* translators: %s: module name */
		const subtitle = sprintf( __( 'By disconnecting the %s module from Site Kit, you will no longer have access to:', 'google-site-kit' ), name );

		const isSavingModule = isSaving === `${ slug }-module`;

		// Disable other modules during editing
		const modulesBeingEdited = filter( isEditing, ( module ) => module );
		const editActive = 0 < modulesBeingEdited.length;

		const dependentModules = listFormat( map( this.getDependentModules(), 'name' ) );

		// Set button text based on state.
		let buttonText = __( 'Close', 'google-site-kit' );
		if ( hasSettings && setupComplete ) {
			if ( isSavingModule ) {
				buttonText = __( 'Saving…', 'google-site-kit' );
			} else {
				buttonText = __( 'Confirm Changes', 'google-site-kit' );
			}
		}

		return (
			<div
				className={ classnames(
					'googlesitekit-settings-module',
					'googlesitekit-settings-module--active',
					`googlesitekit-settings-module--${ slug }`,
					{ 'googlesitekit-settings-module--error': error && editActive && isEditing[ moduleKey ] }
				) }
				key={ moduleKey }
			>
				{ editActive && ! isEditing[ moduleKey ] && <SettingsOverlay compress={ ! isOpen } /> }
				<button
					className={ classnames(
						'googlesitekit-settings-module__header',
						{ 'googlesitekit-settings-module__header--open': isOpen }
					) }
					id={ `googlesitekit-settings-module__header--${ slug }` }
					type="button"
					role="tab"
					aria-selected={ !! isOpen }
					aria-expanded={ !! isOpen }
					aria-controls={ `googlesitekit-settings-module__content--${ slug }` }
					onClick={ handleAccordion.bind( null, slug ) }
				>

									<ModuleIcon slug={ slug } size={ 24 } className="googlesitekit-settings-module__title-icon" />
									{ name }

									{
										isConnected
											? sprintf(
												/* translators: %s: module name. */
												__( '%s is connected', 'google-site-kit' ),
												name
											)
											: sprintf(
												/* translators: %s: module name. */
												__( '%s is not connected', 'google-site-kit' ),
												name
											)
									}

										<VisuallyHidden>
											{ isConnected
												? __( 'Connected', 'google-site-kit' )
												: __( 'Not Connected', 'google-site-kit' )
											}
										</VisuallyHidden>

				</button>
				<div
					className={ classnames(
						'googlesitekit-settings-module__content',
						{ 'googlesitekit-settings-module__content--open': isOpen }
					) }
					id={ `googlesitekit-settings-module__content--${ slug }` }
					role="tabpanel"
					aria-hidden={ ! isOpen }
					aria-labelledby={ `googlesitekit-settings-module__header--${ slug }` }
				>
					<Grid>
						<Row>
							<Cell size={ 12 }>
								<SettingsRenderer
									slug={ slug }
									isEditing={ isEditing[ moduleKey ] }
									isOpen={ isOpen }
								/>
							</Cell>
						</Row>
					</Grid>

									{ isEditing[ moduleKey ] || isSavingModule ? (
										<Fragment>
											<Button
												onClick={ this.handleConfirmOrCancel }
												disabled={ isSavingModule || ! canSubmitChanges }
												id={ hasSettings && setupComplete ? `confirm-changes-${ slug }` : `close-${ slug }` }
											>
												{ buttonText }
											</Button>
											<Spinner isSaving={ isSavingModule } />
											{ hasSettings &&
											<Link
												className="googlesitekit-settings-module__footer-cancel"
												onClick={ this.handleCancel }
												inherit
											>
												{ __( 'Cancel', 'google-site-kit' ) }
											</Link>
											}
										</Fragment>
									) : ( ( hasSettings || ! autoActivate ) &&
									<Link
										className="googlesitekit-settings-module__edit-button"
										onClick={ this.handleEdit }
										inherit
									>
										{ __( 'Edit', 'google-site-kit' ) }
										<PencilIcon
											className="googlesitekit-settings-module__edit-button-icon"
											width="10"
											height="10"
										/>
									</Link>
									) }

									{ isEditing[ moduleKey ] && ! autoActivate && (
										<Link
											className="googlesitekit-settings-module__remove-button"
											onClick={ this.handleDialog }
											inherit
											danger
										>
											{
												/* translators: %s: module name */
												sprintf( __( 'Disconnect %s from Site Kit', 'google-site-kit' ), name )
											}
											<TrashIcon
												className="googlesitekit-settings-module__remove-button-icon"
												width="13"
												height="13"
											/>
										</Link>
									) }
									{ ! isEditing[ moduleKey ] && (
										<Link
											href={ homepage }
											className="googlesitekit-settings-module__cta-button"
											inherit
											external
										>
											{
												/* translators: %s: module name */
												sprintf( __( 'See full details in %s', 'google-site-kit' ), name )
											}
										</Link>
									) }

				<Dialog
					dialogActive={ dialogActive }
					handleDialog={ this.handleDialog }
					/* translators: %s: module name */
					title={ sprintf( __( 'Disconnect %s from Site Kit?', 'google-site-kit' ), name ) }
					subtitle={ subtitle }
					onKeyPress={ this.handleCloseModal }
					provides={ provides }
					handleConfirm={ this.handleConfirmRemoveModule }
					dependentModules={ dependentModules
						? sprintf(
							/* translators: %1$s: module name, %2$s: list of dependent modules */
							__( 'these active modules depend on %1$s and will also be disconnected: %2$s', 'google-site-kit' ),
							name,
							dependentModules
						) : false
					}
					danger
				/>

		);
	}
}

SettingsModule.propTypes = {
	name: PropTypes.string,
	slug: PropTypes.string,
	homepage: PropTypes.string,
	isEditing: PropTypes.object,
	handleDialog: PropTypes.func,
	hasSettings: PropTypes.bool,
	canSubmitChanges: PropTypes.bool,
	required: PropTypes.array,
	active: PropTypes.bool,
	setupComplete: PropTypes.bool,
	onEdit: PropTypes.func,
	onConfirm: PropTypes.func,
	onCancel: PropTypes.func,
};

SettingsModule.defaultProps = {
	name: '',
	slug: '',
	homepage: '',
	isEditing: {},
	handleDialog: null,
	active: false,
	setupComplete: false,
	onEdit: null,
	onConfirm: null,
	onCancel: null,
};

export default compose( [
	withSelect( ( select, { slug } ) => {
		const module = select( CORE_MODULES ).getModule( slug );
		const canSubmitChanges = select( CORE_MODULES ).canSubmitChanges( slug );

		return {
			hasSettings: !! module?.SettingsEditComponent,
			canSubmitChanges,
		};
	} ),
] )( SettingsModule );
<commit_msg>Don't force deactivation.<commit_after>/**
 * SettingsModule component.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * External dependencies
 */
import PropTypes from 'prop-types';
import { filter, map } from 'lodash';
import classnames from 'classnames';

/**
 * WordPress dependencies
 */
import { Component, Fragment } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { compose } from '@wordpress/compose';
import { applyFilters } from '@wordpress/hooks';
import { ESCAPE } from '@wordpress/keycodes';

/**
 * Internal dependencies
 */
import Data from 'googlesitekit-data';
import PencilIcon from '../../../svg/pencil.svg';
import TrashIcon from '../../../svg/trash.svg';
import {
	activateOrDeactivateModule,
	getReAuthURL,
	showErrorNotification,
	getModulesData,
	listFormat,
} from '../../util';
import { refreshAuthentication } from '../../util/refresh-authentication';
import Link from '../Link';
import Button from '../../components/Button';
import data, { TYPE_MODULES } from '../../components/data';
import SettingsOverlay from '../../components/settings/SettingsOverlay';
import Spinner from '../Spinner';
import GenericError from '../legacy-notifications/generic-error';
import Dialog from '../../components/Dialog';
import ModuleIcon from '../ModuleIcon';
import { CORE_MODULES } from '../../googlesitekit/modules/datastore/constants';
import SettingsRenderer from '../settings/SettingsRenderer';
import VisuallyHidden from '../VisuallyHidden';
import { Grid, Row, Cell } from '../../material-components/layout';
const { withSelect } = Data;

/**
 * A single module. Keeps track of its own active state and settings.
 */
class SettingsModule extends Component {
	constructor( props ) {
		super( props );
		this.state = {
			isSaving: false,
			setupComplete: props.setupComplete,
			dialogActive: false,
		};

		this.deactivate = this.deactivate.bind( this );
		this.activateOrDeactivate = this.activateOrDeactivate.bind( this );
		this.handleDialog = this.handleDialog.bind( this );
		this.handleCloseModal = this.handleCloseModal.bind( this );
		this.handleConfirmRemoveModule = this.handleConfirmRemoveModule.bind( this );
		this.handleConfirmOrCancel = this.handleConfirmOrCancel.bind( this );
		this.handleCancel = this.handleCancel.bind( this );
		this.handleEdit = this.handleEdit.bind( this );
	}

	componentDidMount() {
		global.addEventListener( 'keyup', this.handleCloseModal, false );
	}

	componentWillUnmount() {
		global.removeEventListener( 'keyup', this.handleCloseModal );
	}

	async activateOrDeactivate() {
		try {
			this.setState( { isSaving: true } );

			await activateOrDeactivateModule(
				data,
				this.props.slug
			);

			await refreshAuthentication();
			data.invalidateCacheGroup( TYPE_MODULES, this.props.slug );

			this.setState( {
				isSaving: false,
			} );

			global.location = getReAuthURL( this.props.slug, false );
		} catch ( err ) {
			showErrorNotification( GenericError, {
				id: 'activate-module-error',
				title: __( 'Internal Server Error', 'google-site-kit' ),
				description: err.message,
				format: 'small',
				type: 'win-error',
			} );
			this.setState( { isSaving: false } );
		}
	}

	deactivate() {
		const { autoActivate } = this.props;

		if ( autoActivate ) {
			return;
		}

		this.activateOrDeactivate();
	}

	handleDialog() {
		this.setState( ( prevState ) => {
			return {
				dialogActive: ! prevState.dialogActive,
			};
		} );
	}

	// Handle user click on the confirm removal button.
	handleConfirmRemoveModule() {
		this.deactivate();
	}

	handleCloseModal( e ) {
		if ( ESCAPE === e.keyCode ) {
			this.setState( {
				dialogActive: false,
			} );
		}
	}

	handleConfirmOrCancel() {
		const {
			setupComplete,
		} = this.state;

		const {
			hasSettings,
			slug,
			onCancel,
			onConfirm,
		} = this.props;

		if ( hasSettings && setupComplete && typeof onConfirm === 'function' ) {
			onConfirm( slug );
		} else if ( typeof onCancel === 'function' ) {
			onCancel( slug );
		}
	}

	handleCancel() {
		const {
			slug,
			onCancel,
		} = this.props;

		if ( typeof onCancel === 'function' ) {
			onCancel( slug );
		}
	}

	handleEdit() {
		const {
			slug,
			onEdit,
		} = this.props;

		if ( typeof onEdit === 'function' ) {
			onEdit( slug );
		}
	}

	// Find modules that depend on a module.
	getDependentModules() {
		const { slug } = this.props;
		const modules = getModulesData();
		const dependants = {};

		if ( modules[ slug ].dependants ) {
			modules[ slug ].dependants.forEach( ( dependantSlug ) => {
				if ( modules[ dependantSlug ] ) {
					dependants[ dependantSlug ] = modules[ dependantSlug ];
				}
			} );
		}

		return dependants;
	}

	render() {
		const {
			setupComplete,
			dialogActive,
		} = this.state;

		const {
			name,
			slug,
			homepage,
			isEditing,
			isOpen,
			handleAccordion,
			hasSettings,
			canSubmitChanges,
			provides,
			isSaving,
			error,
			autoActivate,
		} = this.props;
		const moduleKey = `${ slug }-module`;
		const isConnected = applyFilters( `googlesitekit.Connected-${ slug }`, setupComplete );
		const connectedClassName = isConnected
			? 'googlesitekit-settings-module__status-icon--connected'
			: 'googlesitekit-settings-module__status-icon--not-connected';

		/* translators: %s: module name */
		const subtitle = sprintf( __( 'By disconnecting the %s module from Site Kit, you will no longer have access to:', 'google-site-kit' ), name );

		const isSavingModule = isSaving === `${ slug }-module`;

		// Disable other modules during editing
		const modulesBeingEdited = filter( isEditing, ( module ) => module );
		const editActive = 0 < modulesBeingEdited.length;

		const dependentModules = listFormat( map( this.getDependentModules(), 'name' ) );

		// Set button text based on state.
		let buttonText = __( 'Close', 'google-site-kit' );
		if ( hasSettings && setupComplete ) {
			if ( isSavingModule ) {
				buttonText = __( 'Saving…', 'google-site-kit' );
			} else {
				buttonText = __( 'Confirm Changes', 'google-site-kit' );
			}
		}

		return (
			<div
				className={ classnames(
					'googlesitekit-settings-module',
					'googlesitekit-settings-module--active',
					`googlesitekit-settings-module--${ slug }`,
					{ 'googlesitekit-settings-module--error': error && editActive && isEditing[ moduleKey ] }
				) }
				key={ moduleKey }
			>
				{ editActive && ! isEditing[ moduleKey ] && <SettingsOverlay compress={ ! isOpen } /> }
				<button
					className={ classnames(
						'googlesitekit-settings-module__header',
						{ 'googlesitekit-settings-module__header--open': isOpen }
					) }
					id={ `googlesitekit-settings-module__header--${ slug }` }
					type="button"
					role="tab"
					aria-selected={ !! isOpen }
					aria-expanded={ !! isOpen }
					aria-controls={ `googlesitekit-settings-module__content--${ slug }` }
					onClick={ handleAccordion.bind( null, slug ) }
				>

									<ModuleIcon slug={ slug } size={ 24 } className="googlesitekit-settings-module__title-icon" />
									{ name }

									{
										isConnected
											? sprintf(
												/* translators: %s: module name. */
												__( '%s is connected', 'google-site-kit' ),
												name
											)
											: sprintf(
												/* translators: %s: module name. */
												__( '%s is not connected', 'google-site-kit' ),
												name
											)
									}

										<VisuallyHidden>
											{ isConnected
												? __( 'Connected', 'google-site-kit' )
												: __( 'Not Connected', 'google-site-kit' )
											}
										</VisuallyHidden>

				</button>
				<div
					className={ classnames(
						'googlesitekit-settings-module__content',
						{ 'googlesitekit-settings-module__content--open': isOpen }
					) }
					id={ `googlesitekit-settings-module__content--${ slug }` }
					role="tabpanel"
					aria-hidden={ ! isOpen }
					aria-labelledby={ `googlesitekit-settings-module__header--${ slug }` }
				>
					<Grid>
						<Row>
							<Cell size={ 12 }>
								<SettingsRenderer
									slug={ slug }
									isEditing={ isEditing[ moduleKey ] }
									isOpen={ isOpen }
								/>
							</Cell>
						</Row>
					</Grid>

									{ isEditing[ moduleKey ] || isSavingModule ? (
										<Fragment>
											<Button
												onClick={ this.handleConfirmOrCancel }
												disabled={ isSavingModule || ! canSubmitChanges }
												id={ hasSettings && setupComplete ? `confirm-changes-${ slug }` : `close-${ slug }` }
											>
												{ buttonText }
											</Button>
											<Spinner isSaving={ isSavingModule } />
											{ hasSettings &&
											<Link
												className="googlesitekit-settings-module__footer-cancel"
												onClick={ this.handleCancel }
												inherit
											>
												{ __( 'Cancel', 'google-site-kit' ) }
											</Link>
											}
										</Fragment>
									) : ( ( hasSettings || ! autoActivate ) &&
									<Link
										className="googlesitekit-settings-module__edit-button"
										onClick={ this.handleEdit }
										inherit
									>
										{ __( 'Edit', 'google-site-kit' ) }
										<PencilIcon
											className="googlesitekit-settings-module__edit-button-icon"
											width="10"
											height="10"
										/>
									</Link>
									) }

									{ isEditing[ moduleKey ] && ! autoActivate && (
										<Link
											className="googlesitekit-settings-module__remove-button"
											onClick={ this.handleDialog }
											inherit
											danger
										>
											{
												/* translators: %s: module name */
												sprintf( __( 'Disconnect %s from Site Kit', 'google-site-kit' ), name )
											}
											<TrashIcon
												className="googlesitekit-settings-module__remove-button-icon"
												width="13"
												height="13"
											/>
										</Link>
									) }
									{ ! isEditing[ moduleKey ] && (
										<Link
											href={ homepage }
											className="googlesitekit-settings-module__cta-button"
											inherit
											external
										>
											{
												/* translators: %s: module name */
												sprintf( __( 'See full details in %s', 'google-site-kit' ), name )
											}
										</Link>
									) }

				<Dialog
					dialogActive={ dialogActive }
					handleDialog={ this.handleDialog }
					/* translators: %s: module name */
					title={ sprintf( __( 'Disconnect %s from Site Kit?', 'google-site-kit' ), name ) }
					subtitle={ subtitle }
					onKeyPress={ this.handleCloseModal }
					provides={ provides }
					handleConfirm={ this.handleConfirmRemoveModule }
					dependentModules={ dependentModules
						? sprintf(
							/* translators: %1$s: module name, %2$s: list of dependent modules */
							__( 'these active modules depend on %1$s and will also be disconnected: %2$s', 'google-site-kit' ),
							name,
							dependentModules
						) : false
					}
					danger
				/>

		);
	}
}

SettingsModule.propTypes = {
	name: PropTypes.string,
	slug: PropTypes.string,
	homepage: PropTypes.string,
	isEditing: PropTypes.object,
	handleDialog: PropTypes.func,
	hasSettings: PropTypes.bool,
	canSubmitChanges: PropTypes.bool,
	required: PropTypes.array,
	active: PropTypes.bool,
	setupComplete: PropTypes.bool,
	onEdit: PropTypes.func,
	onConfirm: PropTypes.func,
	onCancel: PropTypes.func,
};

SettingsModule.defaultProps = {
	name: '',
	slug: '',
	homepage: '',
	isEditing: {},
	handleDialog: null,
	active: false,
	setupComplete: false,
	onEdit: null,
	onConfirm: null,
	onCancel: null,
};

export default compose( [
	withSelect( ( select, { slug } ) => {
		const module = select( CORE_MODULES ).getModule( slug );
		const canSubmitChanges = select( CORE_MODULES ).canSubmitChanges( slug );

		return {
			hasSettings: !! module?.SettingsEditComponent,
			canSubmitChanges,
		};
	} ),
] )( SettingsModule );
<|endoftext|>