window.io.js 7.34 KB
/*
 * ADOBE CONFIDENTIAL
 * __________________
 *
 *  Copyright 2015 Adobe Systems Incorporated
 *  All Rights Reserved.
 *
 * NOTICE:  All information contained herein is, and remains
 * the property of Adobe Systems Incorporated and its suppliers,
 * if any.  The intellectual and technical concepts contained
 * herein are proprietary to Adobe Systems Incorporated and its
 * suppliers and are protected by trade secret or copyright law.
 * Dissemination of this information or reproduction of this material
 * is strictly forbidden unless prior written permission is obtained
 * from Adobe Systems Incorporated.
 */
define('window.io', ['underscore', 'eventemitter'], function(_, EventEmitter) {
    'use strict';

    /**
     * Default options for the component.
     *
     * @typedef {Object} WindowIOOptions
     * @type {Object}
     *
     * @property {Window} window       The target window to send the messages to
     * @property {Object} windows      An object containing (key,window) representing the list of target windows to send the messages to
     * @property {String} [namespace]  Optional namespace
     */
    var DEFAULTS = {
        window: null,
        windows: null,
        namespace: 'screens-player'
    };

    /**
     * Small helper class to facility inter-window communication.
     * @param {WindowIOOptions} options configuration options
     * @constructor
     */
    var WindowIO = function(options) {
        options = _.defaults({}, options, DEFAULTS);

        if (options.window === window) {
            throw new Error('WindowIO bridge can\'t talk to itself yet');
        }

        this.targetWindows = {};

        if (options.windows || options.window) {
            this.addWindows(options.windows || {single: options.window});
        }

        this._namespace = options.namespace;
        this._onMessageHandler = this._onMessage.bind(this);

        // debugging events
        this.emit = function(msg, data, key) {
            console.log('[window.io] WindowIO.on(' + msg + ') in ' + key);
            this._super.emit.apply(this, arguments);
        };

        window.addEventListener('message', this._onMessageHandler, false);
        var self = this;
        var callback = function() {
            window.removeEventListener('unload', callback, false);
            self.destroy();
        };
        window.addEventListener('unload', callback, false);
    };

    /* eslint quote-props: [2, "consistent"] */
    WindowIO.prototype = _.create(EventEmitter.prototype, _.assign({
        '_super': EventEmitter.prototype,
        'constructor': WindowIO
    }, {

        /**
         * Internal method that receives this windows' messages
         * @param {Event} e the event
         * @private
         */
        _onMessage: function(e) {
            // currently we just trust the origin
            var payload = _.isObject(e.data) ? e.data : JSON.parse(e.data);
            if (payload && payload.namespace === this._namespace) {
                var key = null;
                // try to find the key of the source window that sent the message
                for (var k in this.targetWindows) {
                    if (this.targetWindows[k] === e.source) {
                        key = k;
                        break;
                    }
                }

                if (!key && !this.targetWindows.single && (window.location.origin !== e.origin)) {
                    // Add this new source to the list of sources
                    // if none had been provided to the constructor
                    // This happens if we cannot read windows outside this iFrame
                    key = 'single';
                    this.addWindow(key, e.source);
                    console.log('Lazy loaded firmware window with origin ' + e.origin + ' on message ' + payload.type);
                }

                if (key) { // if the origin is unknown
                    // key was found, window was identified
                    this.emit(payload.type, payload.data, key);
                }
            }
        },

        /**
         * Posts a message to all the targeted window(s)
         * @param {string} type Message type
         * @param {object} data Message data
         * @param {string|Array} keys Array or String - Window keys(s) to post the message to (optional)
         */
        postMessage: function(type, data, keys) {
            // if keys is null: broadcast to all targetWindows
            // if keys is an array, limit broadcast to keys in the array
            // by default, assume keys is a string containing the unique key to broadcast to
            keys = Array.prototype.concat([], keys || Object.keys(this.targetWindows));

            // Send the message to all windows matching the specified key,
            // but don't send them twice if the window is registered with multiple keys
            var windows = keys.reduce(function(windowList, key) {
                var win = this.targetWindows[key];
                if (win && win.postMessage && windowList.indexOf(win) === -1) {
                    windowList.push(win);
                }
                return windowList;
            }.bind(this), []);
            windows.forEach(function(win) {
                console.log('[window.io] WindowIO.postMessage(' + type + ')');
                var payload = {
                    'namespace': this._namespace,
                    'type': type,
                    'data': data
                };
                win.postMessage(JSON.stringify(payload), '*');
            }.bind(this));
        },

        addWindow: function(key, w) {
            this.targetWindows[key] = w;
            try {
                console.log('[window.io] addWindow - Current window ' + window.location.href + ' posts messages to ' + w.location.href + '. Registered with key ' + key);
            } catch (e) {
                // might not be able to read location
                console.log('[window.io] addWindow - Current window ' + window.location.href + ' posts messages to a protected window. Registered with key ' + key);
            }
        },

        addWindows: function(windows) {
            for (var key in windows) {
                this.addWindow(key, windows[key]);
            }
        },

        removeWindow: function(keyOrWin) {
            for (var key in this.targetWindows) {
                if (key === keyOrWin || this.targetWindows[key] === keyOrWin) {
                    var w = this.targetWindows[key];
                    delete this.targetWindows[key];
                    try {
                        console.log('[window.io] removeWindow - Current window ' + window.location.href + ' does not post messages to ' + w.location.href + ' anymore. Registered key was ' + key);
                    } catch (e) {
                        // might not be able to read location
                        console.log('[window.io] removeWindow - Current window ' + window.location.href + ' does not post messages to a protected window anymore. Registered key was ' + key);
                    }
                    w = null;
                }
            }
        },

        /**
         * Releases all bound data.
         */
        destroy: function() {
            if (this._onMessageHandler) {
                window.removeEventListener('message', this._onMessageHandler);
                this._onMessageHandler = null;
            }
        }

    }));

    return WindowIO;
});