Source: core/application.js

/**
 * @module core/application
 * @requires event/event-manager
 * @requires template
 * @requires ui/popup/popup
 * @requires ui/popup/alert
 * @requires ui/popup/confirm
 * @requires ui/loading
 * @requires ui/popup/growl
 * @requires ui/slot
 */

var Target = require("./target").Target,
    Template = require("./template"),
    MontageWindow = require("../window-loader/montage-window").MontageWindow,
    Slot;

require("./dom");

var FIRST_LOAD_KEY_SUFFIX = "-is-first-load";

/**
 * The application is a singleton, it initially loads and oversees the running program.
 * It is also responsible for window management.
 * The behavior of the application can be modified by implementing a delegate
 * {@link Application#delegate}.
 * It is also possible to subclass the application by specifying an
 * `applicationPrototype"` in the `package.json`.
 *
 * @class Application
 * @extends Target
 */
var Application = exports.Application = Target.specialize( /** @lends Application.prototype # */ {

    /**
     * Provides a reference to the Montage event manager used in the
     * application.
     *
     * @property {EventManager} value
     * @default null
     */
    eventManager: {
        value: null
    },

    /**
     * Provides a reference to the parent application.
     *
     * @property {Application} value
     * @default null
     */
    parentApplication: {
        value: null
    },

    name: {
        value: null
    },

    _isFirstLoad: {
        value: null
    },

    isFirstLoad: {
        get: function () {
            return this._isFirstLoad;
        }
    },

    /**
     * Provides a reference to the main application.
     *
     * @type {Application}
     * @default this
     */
    mainApplication: {
        get: function () {
            // JFD TODO: We should cache the result, would need to update it
            // when the window is detached or attached
            var mainApplication = this;
            while (mainApplication.parentApplication) {
                mainApplication = mainApplication.parentApplication;
            }
            return mainApplication;
        }
    },

    /**
     * possible values: "z-order", "reverse-z-order", "z-order", "reverse-open-order"
     * @private
     * @property {String} value
     */
    _windowsSortOrder: {
        value: "reverse-z-order"
    },

    /**
     * Determines the sort order for the Application.windows array.
     * Possible values are: z-order, reverse-z-order, open-order,
     * reverse-open-order
     *
     * @returns {string}
     * @default "reverse-z-order"
     */
    windowsSortOrder: {
        get: function () {
            if (this.parentApplication === null) {
                return this._windowsSortOrder;
            } else {
                return this.mainApplication.windowsSortOrder;
            }
        },

        set: function (value) {
            if (this.parentApplication === null) {
                if (["z-order", "reverse-z-order", "z-order", "reverse-open-order"].indexOf(value) !== -1) {
                    this._windowsSortOrder = value;
                }
            } else {
                this.mainApplication.windowsSortOrder = value;
            }
        }
    },

    /**
     * Provides a reference to all the windows opened by the main application
     * or any of its descendents, including the main window itself.
     * The list is kept sorted, the sort order is determined by the
     * `Application.windowsSortOrder` property
     *
     * @returns {Array<MontageWindow>}
     */
    windows: {
        get: function () {
            if (this.parentApplication === null) {
                if (!this._windows) {
                    var theWindow = new MontageWindow();
                    theWindow.application = this;
                    theWindow.window = window;
                    this.window = theWindow;

                    this._windows = [this.window];
                    this._multipleWindow = true;
                }
                return this._windows;
            } else {
                return this.mainApplication.windows;
            }
        }
    },

    _window: {
        value: null
    },

    /**
     * Provides a reference to the MontageWindow associated with the
     * application.
     *
     * @returns {MontageWindow}
     */
    window: {
        get: function () {
            if (!this._window && this === this.mainApplication) {
                var theWindow = new MontageWindow();
                theWindow.application = this;
                theWindow.window = window;
                this._window = theWindow;
            }
            return this._window;
        },

        set: function (value) {
            if (!this._window) {
                this._window = value;
            }
        }
    },

    /**
     * An array of the child windows attached to the application.
     * @type {Array<MontageWindow>}
     * @default {Array} []
     */
    attachedWindows: {
        value: []
    },

    /**
     * Returns the event manager for the specified window object.
     * @function
     * @param {Window} aWindow The browser window whose event manager object should be returned.
     * @returns aWindow.defaultEventMananger
     */
    eventManagerForWindow: {
        value: function (aWindow) {
            return aWindow.defaultEventMananger;
        }
    },

    /**
     * Return the top most window of any of the Montage Windows.
     * @type {MontageWindow}
     * @default document.defaultView
     */
    focusWindow: {
        get: function () {
            var windows = this.windows,
                sortOrder = this.windowsSortOrder;

            if (sortOrder === "z-order") {
                return windows[0];
            } else if (sortOrder === "reverse-z-order") {
                return windows[windows.length - 1];
            } else {
                for (var i in windows) {
                    if (windows[i].focused) {
                        return windows[i];
                    }
                }
            }
        }
    },

    /**
     * The application's delegate object, it can implement a
     * `willFinishLoading` method that will be called right after the
     * index.html is loaded.
     * The application delegate is also the next event target after the
     * application.
     * @type {Object}
     * @default null
     */
    delegate: {
        value: null
    },

    nextTarget: {
        get: function () {
            return this.delegate;
        }
    },

    /**
     * Opens a component in a new browser window, and registers the window with
     * the Montage event manager.
     *
     * The component URL must be in the same domain as the calling script. Can
     * be relative to the main application
     *
     * @function
     * @param {string} component, the path to the reel component to open in the
     * new window.
     * @param {string} name, the component main class name.
     * @param {Object} parameters, the new window parameters (accept same
     * parameters than window.open).
     *
     * @example
     * var app = document.application;
     * app.openWindow("docs/help.reel", "Help", "{width:300, height:500}");
     */
    openWindow: {
        value: function (component, name, parameters) {

            var self = this,
                childWindow = new MontageWindow(),
                childApplication,
                event,
                windowParams = {
                    location: false,
                    // height: <pixels>,
                    // width: <pixels>,
                    // left: <pixels>,
                    // top: <pixels>,
                    menubar: false,
                    resizable: true,
                    scrollbars: true,
                    status: false,
                    titlebar: true,
                    toolbar: false
                };

            var loadInfo = {
                module: component,
                name: name,
                parent: window,
                callback: function (aWindow, aComponent) {
                    var sortOrder;

                    // Finishing the window object initialization and let the consumer knows the window is loaded and ready
                    childApplication = aWindow.document.application;
                    childWindow.window = aWindow;
                    childWindow.application = childApplication;
                    childWindow.component = aComponent;
                    childApplication.window = childWindow;

                    self.attachedWindows.push(childWindow);

                    sortOrder = self.mainApplication.windowsSortOrder;
                    if (sortOrder === "z-order" || sortOrder === "reverse-open-order") {
                        self.windows.unshift(childWindow);
                    } else {
                        self.windows.push(childWindow);
                    }

                    event = document.createEvent("CustomEvent");
                    event.initCustomEvent("load", true, true, null);
                    childWindow.dispatchEvent(event);
                }
            };

            // If this is the first time we open a window, let's install a focus listener and make sure the body element is focusable
            // Applicable only on the main application
            if (this === this.mainApplication && !this._multipleWindow) {
                var montageWindow = this.window;    // Will cause to create a Montage Window for the mainApplication and install the needed event handlers
            }

            var param, value, separator = "", stringParamaters = "";
            if (typeof parameters === "object") {

                // merge the windowParams with the parameters
                for (param in parameters) {
                    if (parameters.hasOwnProperty(param)) {
                        windowParams[param] = parameters[param];
                    }
                }
            }

            // now convert the windowParams into a string
            var excludedParams = ["name"];
            for (param in windowParams) {
                if (excludedParams.indexOf(param) === -1) {
                    value = windowParams[param];
                    if (typeof value === "boolean") {
                        value = value ? "yes" : "no";
                    } else {
                        value = String(value);
                        if (value.match(/[ ,"]/)) {
                            value = '"' + value.replace(/"/g, "\\\"") + '"';
                        }
                    }
                    stringParamaters += separator + param + "=" + value;
                    separator = ",";
                }
            }

            global.require.loadPackage({name: "montage"}).then(function (require) {
                var newWindow = window.open(require.location + "window-loader/index.html", "_blank", stringParamaters);
                newWindow.loadInfo = loadInfo;
            });

            return childWindow;
        }
    },

    /**
     * Attach a window to a parent application.
     * When a window open, it is automatically attached to the Application used
     * to create the window.
     * @function
     * @param {MontageWindow} window to detach.
     */
    attachWindow: {
        value: function (montageWindow) {
            var parentApplicaton = montageWindow.application.parentApplication,
                sortOrder;

            if (parentApplicaton !== this) {
                if (parentApplicaton) {
                    parentApplicaton.detachWindow(montageWindow);
                }

                montageWindow.parentApplication = this;
                this.attachedWindows.push(montageWindow);

                sortOrder = this.mainApplication.windowsSortOrder;
                if (sortOrder === "z-order" || sortOrder === "reverse-open-order") {
                    this.windows.unshift(montageWindow);
                } else {
                    this.windows.push(montageWindow);
                }
                montageWindow.focus();
            }
            return montageWindow;
        }
    },

    /**
     * Detach the window from its parent application.
     * If no montageWindow is specified, the current application's windows will
     * be detached.
     * @function
     * @param {MontageWindow} window to detach.
     */
    detachWindow: {
        value: function (montageWindow) {
            var index,
                parentApplicaton,
                windows = this.windows;

            if (montageWindow === undefined) {
                montageWindow = this.window;
            }
            parentApplicaton = montageWindow.application.parentApplication;

            if (parentApplicaton === this) {
                index = this.attachedWindows.indexOf(montageWindow);
                if (index !== -1) {
                    this.attachedWindows.splice(index, 1);
                }
                index = windows.indexOf(montageWindow);
                if (index !== -1) {
                    windows.splice(index, 1);
                }
                montageWindow.application.parentApplication = null;
            } else if (parentApplicaton) {
                parentApplicaton.detachWindow(montageWindow);
            }
            return montageWindow;
        }
    },

    constructor: {
        value: function Application() {
            if (
                typeof window !== "undefined" &&
                    window.loadInfo && !this.parentApplication
            ) {
                this.parentApplication = window.loadInfo.parent.document.application;
            }
        }
    },

    _load: {
        value: function (applicationRequire, callback) {
            var rootComponent,
                self = this;

            this.name = applicationRequire.packageDescription.name;
            this._loadApplicationContext();

            // assign to the exports so that it is available in the deserialization of the template
            exports.application = self;

            return require.async("ui/component").then(function(exports) {

                rootComponent = exports.__root__;

                if (typeof document !== "undefined") {
                    rootComponent.element = document;
                    return Template.instantiateDocument(document, applicationRequire);
                }

            }).then(function (part) {
                self.callDelegateMethod("willFinishLoading", self);
                rootComponent.needsDraw = true;
                if (callback) {
                    callback(self);
                }
                return self;
            });
        }
    },

    _loadApplicationContext: {
        value: function () {
            if (this._isFirstLoad === null) {

                var hasAlreadyBeenLoaded,
                    alreadyLoadedLocalStorageKey = this.name + FIRST_LOAD_KEY_SUFFIX;

                if (typeof localStorage !== "undefined") {
                    localStorage.getItem(alreadyLoadedLocalStorageKey);
                
                    if (hasAlreadyBeenLoaded === null) {
                        try {
                            localStorage.setItem(alreadyLoadedLocalStorageKey, true);
                        } catch (error) {
                            //console.log("Browser is in private mode.");
                        }
                    }
                }

                this._isFirstLoad = !hasAlreadyBeenLoaded;
            }
        }
    },

    /**
     * @private
     */
    _alertPopup: {value: null, enumerable: false},
    /**
     * @private
     */
    _confirmPopup: {value: null, enumerable: false},
    /**
     * @private
     */
    _notifyPopup: {value: null, enumerable: false},
    /**
     * @private
     */
    _zIndex: {value: null},

    /**
     * @private
     */
    _isSystemPopup: {value: function (type) {
        return (type === 'alert' || type === 'confirm' || type === 'notify');
    }},

    /**
     * @private
     */
    _createPopupSlot: {value: function (zIndex) {
        var slotEl = document.createElement('div');
        document.body.appendChild(slotEl);
        slotEl.style.zIndex = zIndex;
        slotEl.style.position = 'absolute';

        var popupSlot = new Slot();
        popupSlot.element = slotEl;
        popupSlot.attachToParentComponent();
        return popupSlot;
    }},

    getPopupSlot: {
        value: function (type, content, callback) {

            var self = this;
            require.async("ui/slot.reel/slot")
            .then(function (exports) {
                Slot = Slot || exports.Slot;
                type = type || "custom";
                var isSystemPopup = self._isSystemPopup(type), zIndex, popupSlot;
                self.popupSlots = self.popupSlots || {};

                if(isSystemPopup) {
                    switch (type) {
                        case "alert":
                            zIndex = 19004;
                            break;
                        case "confirm":
                            zIndex = 19003;
                            break;
                        case "notify":
                            zIndex = 19002;
                            break;
                    }
                } else {
                    // custom popup
                    if(!self._zIndex) {
                        self._zIndex = 17000;
                    } else {
                        self._zIndex = self._zIndex + 1;
                    }
                    zIndex = self._zIndex;
                }

                popupSlot = self.popupSlots[type];
                if (!popupSlot) {
                    popupSlot = self.popupSlots[type] = self._createPopupSlot(zIndex);
                }
                // use the new zIndex for custom popup
                if(!isSystemPopup) {
                    popupSlot.element.style.zIndex = zIndex;
                }

                popupSlot.content = content;
                callback.call(this, popupSlot);

            });
        }
    },

    returnPopupSlot: {value: function (type) {
        var self = this;
        if(self.popupSlots && self.popupSlots[type]) {
            var popupSlot = self.popupSlots[type];
            popupSlot.content = null;
            // is there a way to remove the Slot
            // OR should we remove the slotEl from the DOM to clean up ?
        }

    }},

    /**
     * @private
     */
    _getActivePopupSlots: {
        value: function () {
            var arr = [];
            if(this.popupSlots) {
                var keys = Object.keys(this.popupSlots);
                if(keys && keys.length > 0) {
                    var i, len = keys.length, slot;
                    for(i=0; i< len; i++) {
                        slot = this.popupSlots[keys[i]];
                        if(slot && slot.content !== null) {
                            arr.push(slot);
                        }

                    }
                }
            }
            return arr;
        }
    }
});