UNPKG

@phosphor/application

Version:

PhosphorJS - Pluggable Application

484 lines (483 loc) 17.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); /*----------------------------------------------------------------------------- | Copyright (c) 2014-2017, PhosphorJS Contributors | | Distributed under the terms of the BSD 3-Clause License. | | The full license is in the file LICENSE, distributed with this software. |----------------------------------------------------------------------------*/ var commands_1 = require("@phosphor/commands"); var coreutils_1 = require("@phosphor/coreutils"); var widgets_1 = require("@phosphor/widgets"); /** * A class for creating pluggable applications. * * #### Notes * The `Application` class is useful when creating large, complex * UI applications with the ability to be safely extended by third * party code via plugins. */ var Application = /** @class */ (function () { /** * Construct a new application. * * @param options - The options for creating the application. */ function Application(options) { this._started = false; this._pluginMap = Private.createPluginMap(); this._serviceMap = Private.createServiceMap(); this._delegate = new coreutils_1.PromiseDelegate(); // Create the application command registry. var commands = new commands_1.CommandRegistry(); // Create the application context menu. var renderer = options.contextMenuRenderer; var contextMenu = new widgets_1.ContextMenu({ commands: commands, renderer: renderer }); // Initialize the application state. this.commands = commands; this.contextMenu = contextMenu; this.shell = options.shell; } Object.defineProperty(Application.prototype, "started", { /** * A promise which resolves after the application has started. * * #### Notes * This promise will resolve after the `start()` method is called, * when all the bootstrapping and shell mounting work is complete. */ get: function () { return this._delegate.promise; }, enumerable: true, configurable: true }); /** * Test whether a plugin is registered with the application. * * @param id - The id of the plugin of interest. * * @returns `true` if the plugin is registered, `false` otherwise. */ Application.prototype.hasPlugin = function (id) { return id in this._pluginMap; }; /** * List the IDs of the plugins registered with the application. * * @returns A new array of the registered plugin IDs. */ Application.prototype.listPlugins = function () { return Object.keys(this._pluginMap); }; /** * Register a plugin with the application. * * @param plugin - The plugin to register. * * #### Notes * An error will be thrown if a plugin with the same id is already * registered, or if the plugin has a circular dependency. * * If the plugin provides a service which has already been provided * by another plugin, the new service will override the old service. */ Application.prototype.registerPlugin = function (plugin) { // Throw an error if the plugin id is already registered. if (plugin.id in this._pluginMap) { throw new Error("Plugin '" + plugin.id + "' is already registered."); } // Create the normalized plugin data. var data = Private.createPluginData(plugin); // Ensure the plugin does not cause a cyclic dependency. Private.ensureNoCycle(data, this._pluginMap, this._serviceMap); // Add the service token to the service map. if (data.provides) { this._serviceMap.set(data.provides, data.id); } // Add the plugin to the plugin map. this._pluginMap[data.id] = data; }; /** * Register multiple plugins with the application. * * @param plugins - The plugins to register. * * #### Notes * This calls `registerPlugin()` for each of the given plugins. */ Application.prototype.registerPlugins = function (plugins) { for (var _i = 0, plugins_1 = plugins; _i < plugins_1.length; _i++) { var plugin = plugins_1[_i]; this.registerPlugin(plugin); } }; /** * Activate the plugin with the given id. * * @param id - The ID of the plugin of interest. * * @returns A promise which resolves when the plugin is activated * or rejects with an error if it cannot be activated. */ Application.prototype.activatePlugin = function (id) { var _this = this; // Reject the promise if the plugin is not registered. var data = this._pluginMap[id]; if (!data) { return Promise.reject(new Error("Plugin '" + id + "' is not registered.")); } // Resolve immediately if the plugin is already activated. if (data.activated) { return Promise.resolve(undefined); } // Return the pending resolver promise if it exists. if (data.promise) { return data.promise; } // Resolve the required services for the plugin. var required = data.requires.map(function (t) { return _this.resolveRequiredService(t); }); // Resolve the optional services for the plugin. var optional = data.optional.map(function (t) { return _this.resolveOptionalService(t); }); // Create the array of promises to resolve. var promises = required.concat(optional); // Setup the resolver promise for the plugin. data.promise = Promise.all(promises).then(function (services) { return data.activate.apply(undefined, [_this].concat(services)); }).then(function (service) { data.service = service; data.activated = true; data.promise = null; }).catch(function (error) { data.promise = null; throw error; }); // Return the pending resolver promise. return data.promise; }; /** * Resolve a required service of a given type. * * @param token - The token for the service type of interest. * * @returns A promise which resolves to an instance of the requested * service, or rejects with an error if it cannot be resolved. * * #### Notes * Services are singletons. The same instance will be returned each * time a given service token is resolved. * * If the plugin which provides the service has not been activated, * resolving the service will automatically activate the plugin. * * User code will not typically call this method directly. Instead, * the required services for the user's plugins will be resolved * automatically when the plugin is activated. */ Application.prototype.resolveRequiredService = function (token) { // Reject the promise if there is no provider for the type. var id = this._serviceMap.get(token); if (!id) { return Promise.reject(new Error("No provider for: " + token.name + ".")); } // Resolve immediately if the plugin is already activated. var data = this._pluginMap[id]; if (data.activated) { return Promise.resolve(data.service); } // Otherwise, activate the plugin and wait on the results. return this.activatePlugin(id).then(function () { return data.service; }); }; /** * Resolve an optional service of a given type. * * @param token - The token for the service type of interest. * * @returns A promise which resolves to an instance of the requested * service, or `null` if it cannot be resolved. * * #### Notes * Services are singletons. The same instance will be returned each * time a given service token is resolved. * * If the plugin which provides the service has not been activated, * resolving the service will automatically activate the plugin. * * User code will not typically call this method directly. Instead, * the optional services for the user's plugins will be resolved * automatically when the plugin is activated. */ Application.prototype.resolveOptionalService = function (token) { // Resolve with `null` if there is no provider for the type. var id = this._serviceMap.get(token); if (!id) { return Promise.resolve(null); } // Resolve immediately if the plugin is already activated. var data = this._pluginMap[id]; if (data.activated) { return Promise.resolve(data.service); } // Otherwise, activate the plugin and wait on the results. return this.activatePlugin(id).then(function () { return data.service; }).catch(function (reason) { console.error(reason); return null; }); }; /** * Start the application. * * @param options - The options for starting the application. * * @returns A promise which resolves when all bootstrapping work * is complete and the shell is mounted to the DOM. * * #### Notes * This should be called once by the application creator after all * initial plugins have been registered. * * If a plugin fails to the load, the error will be logged and the * other valid plugins will continue to be loaded. * * Bootstrapping the application consists of the following steps: * 1. Activate the startup plugins * 2. Wait for those plugins to activate * 3. Attach the shell widget to the DOM * 4. Add the application event listeners */ Application.prototype.start = function (options) { var _this = this; if (options === void 0) { options = {}; } // Return immediately if the application is already started. if (this._started) { return this._delegate.promise; } // Mark the application as started; this._started = true; // Parse the host id for attaching the shell. var hostID = options.hostID || ''; // Collect the ids of the startup plugins. var startups = Private.collectStartupPlugins(this._pluginMap, options); // Generate the activation promises. var promises = startups.map(function (id) { return _this.activatePlugin(id).catch(function (error) { console.error("Plugin '" + id + "' failed to activate."); console.error(error); }); }); // Wait for the plugins to activate, then finalize startup. Promise.all(promises).then(function () { _this.attachShell(hostID); _this.addEventListeners(); _this._delegate.resolve(undefined); }); // Return the pending delegate promise. return this._delegate.promise; }; /** * Handle the DOM events for the application. * * @param event - The DOM event sent to the application. * * #### Notes * This method implements the DOM `EventListener` interface and is * called in response to events registered for the application. It * should not be called directly by user code. */ Application.prototype.handleEvent = function (event) { switch (event.type) { case 'resize': this.evtResize(event); break; case 'keydown': this.evtKeydown(event); break; case 'contextmenu': this.evtContextMenu(event); break; } }; /** * Attach the application shell to the DOM. * * @param id - The id of the host node for the shell, or `''`. * * #### Notes * If the id is not provided, the document body will be the host. * * A subclass may reimplement this method as needed. */ Application.prototype.attachShell = function (id) { widgets_1.Widget.attach(this.shell, (id && document.getElementById(id)) || document.body); }; /** * Add the application event listeners. * * #### Notes * The default implementation of this method adds listeners for * `'keydown'` and `'resize'` events. * * A subclass may reimplement this method as needed. */ Application.prototype.addEventListeners = function () { document.addEventListener('contextmenu', this); document.addEventListener('keydown', this, true); window.addEventListener('resize', this); }; /** * A method invoked on a document `'keydown'` event. * * #### Notes * The default implementation of this method invokes the key down * processing method of the application command registry. * * A subclass may reimplement this method as needed. */ Application.prototype.evtKeydown = function (event) { this.commands.processKeydownEvent(event); }; /** * A method invoked on a document `'contextmenu'` event. * * #### Notes * The default implementation of this method opens the application * `contextMenu` at the current mouse position. * * If the application context menu has no matching content *or* if * the shift key is pressed, the default browser context menu will * be opened instead. * * A subclass may reimplement this method as needed. */ Application.prototype.evtContextMenu = function (event) { if (event.shiftKey) { return; } if (this.contextMenu.open(event)) { event.preventDefault(); event.stopPropagation(); } }; /** * A method invoked on a window `'resize'` event. * * #### Notes * The default implementation of this method updates the shell. * * A subclass may reimplement this method as needed. */ Application.prototype.evtResize = function (event) { this.shell.update(); }; return Application; }()); exports.Application = Application; /** * The namespace for the module implementation details. */ var Private; (function (Private) { /** * Create a new plugin map. */ function createPluginMap() { return Object.create(null); } Private.createPluginMap = createPluginMap; /** * Create a new service map. */ function createServiceMap() { return new Map(); } Private.createServiceMap = createServiceMap; /** * Create a normalized plugin data object for the given plugin. */ function createPluginData(plugin) { return { id: plugin.id, service: null, promise: null, activated: false, activate: plugin.activate, provides: plugin.provides || null, autoStart: plugin.autoStart || false, requires: plugin.requires ? plugin.requires.slice() : [], optional: plugin.optional ? plugin.optional.slice() : [] }; } Private.createPluginData = createPluginData; /** * Ensure no cycle is present in the plugin resolution graph. * * If a cycle is detected, an error will be thrown. */ function ensureNoCycle(data, pluginMap, serviceMap) { var dependencies = data.requires.concat(data.optional); // Bail early if there cannot be a cycle. if (!data.provides || dependencies.length === 0) { return; } // Setup a stack to trace service resolution. var trace = [data.id]; // Throw an exception if a cycle is present. if (dependencies.some(visit)) { throw new Error("Cycle detected: " + trace.join(' -> ') + "."); } function visit(token) { if (token === data.provides) { return true; } var id = serviceMap.get(token); if (!id) { return false; } var other = pluginMap[id]; var otherDependencies = other.requires.concat(other.optional); if (otherDependencies.length === 0) { return false; } trace.push(id); if (otherDependencies.some(visit)) { return true; } trace.pop(); return false; } } Private.ensureNoCycle = ensureNoCycle; /** * Collect the IDs of the plugins to activate on startup. */ function collectStartupPlugins(pluginMap, options) { // Create a map to hold the plugin IDs. var resultMap = Object.create(null); // Collect the auto-start plugins. for (var id in pluginMap) { if (pluginMap[id].autoStart) { resultMap[id] = true; } } // Add the startup plugins. if (options.startPlugins) { for (var _i = 0, _a = options.startPlugins; _i < _a.length; _i++) { var id = _a[_i]; resultMap[id] = true; } } // Remove the ignored plugins. if (options.ignorePlugins) { for (var _b = 0, _c = options.ignorePlugins; _b < _c.length; _b++) { var id = _c[_b]; delete resultMap[id]; } } // Return the final startup plugins. return Object.keys(resultMap); } Private.collectStartupPlugins = collectStartupPlugins; })(Private || (Private = {}));