adobe/brackets

View on GitHub
src/LiveDevelopment/MultiBrowserImpl/protocol/remote/LiveDevProtocolRemote.js

Summary

Maintainability
A
2 hrs
Test Coverage
/*
 * Copyright (c) 2014 - present Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint evil: true */

// This is the script that Brackets live development injects into HTML pages in order to
// establish and maintain the live development socket connection. Note that Brackets may
// also inject other scripts via "evaluate" once this has connected back to Brackets.

(function (global) {
    "use strict";

    // This protocol handler assumes that there is also an injected transport script that
    // has the following methods:
    //     setCallbacks(obj) - a method that takes an object with a "message" callback that
    //         will be called with the message string whenever a message is received by the transport.
    //     send(msgStr) - sends the given message string over the transport.
    var transport = global._Brackets_LiveDev_Transport;

    /**
     * Manage messaging between Editor and Browser at the protocol layer.
     * Handle messages that arrives through the current transport and dispatch them
     * to subscribers. Subscribers are handlers that implements remote commands/functions.
     * Property 'method' of messages body is used as the 'key' to identify message types.
     * Provide a 'send' operation that allows remote commands sending messages to the Editor.
     */
    var MessageBroker = {

        /**
         * Collection of handlers (subscribers) per each method.
         * To be pushed by 'on' and consumed by 'trigger' stored this way:
         *      handlers[method] = [handler1, handler2, ...]
         */
        handlers: {},

         /**
          * Dispatch messages to handlers according to msg.method value.
          * @param {Object} msg Message to be dispatched.
          */
        trigger: function (msg) {
            var msgHandlers;
            if (!msg.method) {
                // no message type, ignoring it
                // TODO: should we trigger a generic event?
                console.log("[Brackets LiveDev] Received message without method.");
                return;
            }
            // get handlers for msg.method
            msgHandlers = this.handlers[msg.method];

            if (msgHandlers && msgHandlers.length > 0) {
                // invoke handlers with the received message
                msgHandlers.forEach(function (handler) {
                    try {
                        // TODO: check which context should be used to call handlers here.
                        handler(msg);
                        return;
                    } catch (e) {
                        console.log("[Brackets LiveDev] Error executing a handler for " + msg.method);
                        console.log(e.stack);
                        return;
                    }
                });
            } else {
                // no subscribers, ignore it.
                // TODO: any other default handling? (eg. specific respond, trigger as a generic event, etc.);
                console.log("[Brackets LiveDev] No subscribers for message " + msg.method);
                return;
            }
        },

        /**
         * Send a response of a particular message to the Editor.
         * Original message must provide an 'id' property
         * @param {Object} orig Original message.
         * @param {Object} response Message to be sent as the response.
         */
        respond: function (orig, response) {
            if (!orig.id) {
                console.log("[Brackets LiveDev] Trying to send a response for a message with no ID");
                return;
            }
            response.id = orig.id;
            this.send(response);
        },

        /**
         * Subscribe handlers to specific messages.
         * @param {string} method Message type.
         * @param {function} handler.
         * TODO: add handler name or any identification mechanism to then implement 'off'?
         */
        on: function (method, handler) {
            if (!method || !handler) {
                return;
            }
            if (!this.handlers[method]) {
                //initialize array
                this.handlers[method] = [];
            }
            // add handler to the stack
            this.handlers[method].push(handler);
        },

        /**
         * Send a message to the Editor.
         * @param {string} msgStr Message to be sent.
         */
        send: function (msgStr) {
            transport.send(JSON.stringify(msgStr));
        }
    };

    /**
     * Runtime Domain. Implements remote commands for "Runtime.*"
     */
    var Runtime = {
        /**
         * Evaluate an expresion and return its result.
         */
        evaluate: function (msg) {
            console.log("Runtime.evaluate");
            var result = eval(msg.params.expression);
            MessageBroker.respond(msg, {
                result: JSON.stringify(result) // TODO: in original protocol this is an object handle
            });
        }
    };

    // subscribe handler to method Runtime.evaluate
    MessageBroker.on("Runtime.evaluate", Runtime.evaluate);

    /**
     * CSS Domain.
     */
    var CSS = {

        setStylesheetText : function (msg) {

            if (!msg || !msg.params || !msg.params.text || !msg.params.url) {
                return;
            }

            var i,
                node;

            var head = window.document.getElementsByTagName('head')[0];
            // create an style element to replace the one loaded with <link>
            var s = window.document.createElement('style');
            s.type = 'text/css';
            s.appendChild(window.document.createTextNode(msg.params.text));

            for (i = 0; i < window.document.styleSheets.length; i++) {
                node = window.document.styleSheets[i];
                if (node.ownerNode.id === msg.params.url) {
                    head.insertBefore(s, node.ownerNode); // insert the style element here
                    // now can remove the style element previously created (if any)
                    node.ownerNode.parentNode.removeChild(node.ownerNode);
                } else if (node.href === msg.params.url  && !node.disabled) {
                    // if the link element to change
                    head.insertBefore(s, node.ownerNode); // insert the style element here
                    node.disabled = true;
                    i++; // since we have just inserted a stylesheet
                }
            }
            s.id = msg.params.url;
        },

        /**
        * retrieves the content of the stylesheet
        * TODO: it now depends on reloadCSS implementation
        */
        getStylesheetText: function (msg) {
            var i,
                sheet,
                text = "";
            for (i = 0; i < window.document.styleSheets.length; i++) {
                sheet = window.document.styleSheets[i];
                // if it was already 'reloaded'
                if (sheet.ownerNode.id ===  msg.params.url) {
                    text = sheet.ownerNode.textContent;
                } else if (sheet.href === msg.params.url && !sheet.disabled) {
                    var j,
                        rules;

                    // Deal with Firefox's SecurityError when accessing sheets
                    // from other domains, and Chrome returning `undefined`.
                    try {
                        rules = window.document.styleSheets[i].cssRules;
                    } catch (e) {
                        if (e.name !== "SecurityError") {
                            throw e;
                        }
                    }
                    if (!rules) {
                        return;
                    }

                    for (j = 0; j < rules.length; j++) {
                        text += rules[j].cssText + '\n';
                    }
                }
            }

            MessageBroker.respond(msg, {
                text: text
            });
        }
    };

    MessageBroker.on("CSS.setStylesheetText", CSS.setStylesheetText);
    MessageBroker.on("CSS.getStylesheetText", CSS.getStylesheetText);

    /**
     * Page Domain.
     */
    var Page = {
        /**
         * Reload the current page optionally ignoring cache.
         * @param {Object} msg
         */
        reload: function (msg) {
            // just reload the page
            window.location.reload(msg.params.ignoreCache);
        },

        /**
         * Navigate to a different page.
         * @param {Object} msg
         */
        navigate: function (msg) {
            if (msg.params.url) {
                // navigate to a new page.
                window.location.replace(msg.params.url);
            }
        }
    };

    // subscribe handler to method Page.reload
    MessageBroker.on("Page.reload", Page.reload);
    MessageBroker.on("Page.navigate", Page.navigate);
    MessageBroker.on("ConnectionClose", Page.close);



    // By the time this executes, there must already be an active transport.
    if (!transport) {
        console.error("[Brackets LiveDev] No transport set");
        return;
    }

    var ProtocolManager = {

        _documentObserver: {},

        _protocolHandler: {},

        enable: function () {
            transport.setCallbacks(this._protocolHandler);
            transport.enable();
        },

        onConnect: function () {
            this._documentObserver.start(window.document, transport);
        },

        onClose: function () {
            var body = window.document.getElementsByTagName("body")[0],
                overlay = window.document.createElement("div"),
                background = window.document.createElement("div"),
                status = window.document.createElement("div");

            overlay.style.width = "100%";
            overlay.style.height = "100%";
            overlay.style.zIndex = 2227;
            overlay.style.position = "fixed";
            overlay.style.top = 0;
            overlay.style.left = 0;

            background.style.backgroundColor = "#fff";
            background.style.opacity = 0.5;
            background.style.width = "100%";
            background.style.height = "100%";
            background.style.position = "fixed";
            background.style.top = 0;
            background.style.left = 0;

            status.textContent = "Live Development Session has Ended";
            status.style.width = "100%";
            status.style.color = "#fff";
            status.style.backgroundColor = "#666";
            status.style.position = "fixed";
            status.style.top = 0;
            status.style.left = 0;
            status.style.padding = "0.2em";
            status.style.verticalAlign = "top";
            status.style.textAlign = "center";
            overlay.appendChild(background);
            overlay.appendChild(status);
            body.appendChild(overlay);

            // change the title as well
            window.document.title = "(Brackets Live Preview: closed) " + window.document.title;
        },

        setDocumentObserver: function (documentOberver) {
            if (!documentOberver) {
                return;
            }
            this._documentObserver = documentOberver;
        },

        setProtocolHandler: function (protocolHandler) {
            if (!protocolHandler) {
                return;
            }
            this._protocolHandler = protocolHandler;
        }
    };

    // exposing ProtocolManager
    global._Brackets_LiveDev_ProtocolManager = ProtocolManager;

    /**
     * The remote handler for the protocol.
     */
    var ProtocolHandler = {
        /**
         * Handles a message from the transport. Parses it as JSON and delegates
         * to MessageBroker who is in charge of routing them to handlers.
         * @param {string} msgStr The protocol message as stringified JSON.
         */
        message: function (msgStr) {
            var msg;
            try {
                msg = JSON.parse(msgStr);
            } catch (e) {
                console.log("[Brackets LiveDev] Malformed message received: ", msgStr);
                return;
            }
            // delegates handling/routing to MessageBroker.
            MessageBroker.trigger(msg);
        },

        close: function (evt) {
            ProtocolManager.onClose();
        },

        connect: function (evt) {
            ProtocolManager.onConnect();
        }
    };

    ProtocolManager.setProtocolHandler(ProtocolHandler);

    window.addEventListener('load', function () {
        ProtocolManager.enable();
    });
    
    /**
    * Sends the message containing tagID which is being clicked
    * to the editor in order to change the cursor position to
    * the HTML tag corresponding to the clicked element.
    */
    function onDocumentClick(event) {
        var element = event.target;
        if (element && element.hasAttribute('data-brackets-id')) {
            MessageBroker.send({"tagId": element.getAttribute('data-brackets-id')});
        }
    }
    window.document.addEventListener("click", onDocumentClick);

}(this));