From d936a7a0e0f49da3d0432a4f03e6a008c6e23c54 Mon Sep 17 00:00:00 2001 From: Tao Zhou Date: Tue, 3 Dec 2019 15:16:29 +0100 Subject: Add event interface to Gerrit This is to solve problems like communicating among Gerrit components, plugins and Gerrit, and plugins themselves. Original discussion and one classic problem to solve: https://gerrit-review.googlesource.com/c/gerrit/+/242732 ``` // pluginA Gerrit.install(plugin => { // do something Gerrit.emit("event-name", {plugin: plugin}); }); // Gerrit Gerrit.on("event-name", event => { // event.plugin should be pluginA }); ``` Change-Id: I8b9703f5fafd5fc00054d6d4bce4628d4aba6624 --- .../gr-event-interface/gr-event-interface.js | 142 ++++++++++++++++++++ .../gr-event-interface_test.html | 146 +++++++++++++++++++++ .../gr-js-api-interface/gr-js-api-interface.html | 1 + .../shared/gr-js-api-interface/gr-public-js-api.js | 37 ++++++ polygerrit-ui/app/test/index.html | 1 + 5 files changed, 327 insertions(+) create mode 100644 polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface.js create mode 100644 polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html diff --git a/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface.js b/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface.js new file mode 100644 index 0000000000..d024bb2df3 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface.js @@ -0,0 +1,142 @@ +/** + * @license + * Copyright (C) 2019 The Android Open Source Project + * + * 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 + * + * http://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. + */ + +(function(window) { + 'use strict'; + + // Avoid duplicate registeration + if (window.EventEmitter) return; + + /** + * An lite implementation of + * https://nodejs.org/api/events.html#events_class_eventemitter. + * + * This is unrelated to the native DOM events, you should use it when you want + * to enable EventEmitter interface on any class. + * + * @example + * + * class YourClass extends EventEmitter { + * // now all instance of YourClass will have this EventEmitter interface + * } + * + */ + class EventEmitter { + constructor() { + /** + * Shared events map from name to the listeners. + * @type {!Object>} + */ + this._listenersMap = new Map(); + } + + /** + * Register an event listener to an event. + * + * @param {string} eventName + * @param {eventCallback} cb + * @returns {Function} Unsubscribe method + */ + addListener(eventName, cb) { + if (!eventName || !cb) { + console.warn('A valid eventname and callback is required!'); + return; + } + + const listeners = this._listenersMap.get(eventName) || []; + listeners.push(cb); + this._listenersMap.set(eventName, listeners); + + return () => { + this.off(eventName, cb); + }; + } + + // Alias for addListener. + on(eventName, cb) { + return this.addListener(eventName, cb); + } + + // Attach event handler only once. Automatically removed. + once(eventName, cb) { + const onceWrapper = (...args) => { + cb(...args); + this.off(eventName, onceWrapper); + }; + return this.on(eventName, onceWrapper); + } + + /** + * De-register an event listener to an event. + * + * @param {string} eventName + * @param {eventCallback} cb + */ + removeListener(eventName, cb) { + let listeners = this._listenersMap.get(eventName) || []; + listeners = listeners.filter(listener => listener !== cb); + this._listenersMap.set(eventName, listeners); + } + + // Alias to removeListener + off(eventName, cb) { + this.removeListener(eventName, cb); + } + + /** + * Synchronously calls each of the listeners registered for + * the event named eventName, in the order they were registered, + * passing the supplied detail to each. + * + * Returns true if the event had listeners, false otherwise. + * + * @param {string} eventName + * @param {*} detail + */ + emit(eventName, detail) { + const listeners = this._listenersMap.get(eventName) || []; + for (const listener of listeners) { + try { + listener(detail); + } catch (e) { + console.error(e); + } + } + return listeners.length !== 0; + } + + // Alias to emit. + dispatch(eventName, detail) { + return this.emit(eventName, detail); + } + + /** + * Remove listeners for a specific event or all. + * + * @param {string} eventName if not provided, will remove all + */ + removeAllListeners(eventName) { + if (eventName) { + this._listenersMap.set(eventName, []); + } else { + this._listenersMap = new Map(); + } + } + } + + window.EventEmitter = EventEmitter; +})(window); \ No newline at end of file diff --git a/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html b/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html new file mode 100644 index 0000000000..137ed25042 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html @@ -0,0 +1,146 @@ + + + + +gr-api-interface + + + + + + + + + + + + + \ No newline at end of file diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html index d8a662ec37..d95fd0afd6 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html @@ -30,6 +30,7 @@ limitations under the License. + diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js index 36a428d966..1311105572 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js @@ -662,6 +662,43 @@ } }; + // TODO(taoalpha): List all internal supported event names. + // Also convert this to inherited class once we move Gerrit to class. + Gerrit._eventEmitter = new EventEmitter(); + ['addListener', + 'dispatch', + 'emit', + 'off', + 'on', + 'once', + 'removeAllListeners', + 'removeListener', + ].forEach(method => { + /** + * Enabling EventEmitter interface on Gerrit. + * + * This will enable to signal across different parts of js code without relying on DOM, + * including core to core, plugin to plugin and also core to plugin. + * + * @example + * + * // Emit this event from pluginA + * Gerrit.install(pluginA => { + * fetch("some-api").then(() => { + * Gerrit.on("your-special-event", {plugin: pluginA}); + * }); + * }); + * + * // Listen on your-special-event from pluignB + * Gerrit.install(pluginB => { + * Gerrit.on("your-special-event", ({plugin}) => { + * // do something, plugin is pluginA + * }); + * }); + */ + Gerrit[method] = Gerrit._eventEmitter[method].bind(Gerrit._eventEmitter); + }); + window.Gerrit = Gerrit; // Preloaded plugins should be installed after Gerrit.install() is set, diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html index 5784eadec7..75ba7054d5 100644 --- a/polygerrit-ui/app/test/index.html +++ b/polygerrit-ui/app/test/index.html @@ -149,6 +149,7 @@ limitations under the License. 'settings/gr-settings-view/gr-settings-view_test.html', 'settings/gr-ssh-editor/gr-ssh-editor_test.html', 'settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html', + 'shared/gr-event-interface/gr-event-interface_test.html', 'shared/gr-account-label/gr-account-label_test.html', 'shared/gr-account-link/gr-account-link_test.html', 'shared/gr-alert/gr-alert_test.html', -- cgit v1.2.3