summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBen Rohlfs <brohlfs@google.com>2019-12-10 14:29:29 +0000
committerGerrit Code Review <noreply-gerritcodereview@google.com>2019-12-10 14:29:29 +0000
commitbb1c614b7283958d2c3fa6e78e06bf481c48b631 (patch)
tree00276abee0effa7f4f919c935a1606b58de9464c
parent850b1c312ce8c283b2bfd503d7a142f7ca599e84 (diff)
parentd936a7a0e0f49da3d0432a4f03e6a008c6e23c54 (diff)
Merge "Add event interface to Gerrit" into stable-2.16
-rw-r--r--polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface.js142
-rw-r--r--polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html146
-rw-r--r--polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html1
-rw-r--r--polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js37
-rw-r--r--polygerrit-ui/app/test/index.html1
5 files changed, 327 insertions, 0 deletions
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<string, Array<eventCallback>>}
+ */
+ 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 @@
+<!DOCTYPE html>
+<!--
+@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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-api-interface</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="../gr-js-api-interface/gr-js-api-interface.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+ <template>
+ <gr-js-api-interface></gr-js-api-interface>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-event-interface tests', () => {
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('test on Gerrit', () => {
+ setup(() => {
+ fixture('basic');
+ Gerrit.removeAllListeners();
+ });
+
+ test('communicate between plugin and Gerrit', done => {
+ const eventName = 'test-plugin-event';
+ let p;
+ Gerrit.on(eventName, e => {
+ assert.equal(e.value, 'test');
+ assert.equal(e.plugin, p);
+ done();
+ });
+ Gerrit.install(plugin => {
+ p = plugin;
+ Gerrit.emit(eventName, {value: 'test', plugin});
+ }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ });
+
+ test('listen on events from core', done => {
+ const eventName = 'test-plugin-event';
+ Gerrit.on(eventName, e => {
+ assert.equal(e.value, 'test');
+ done();
+ });
+
+ Gerrit.emit(eventName, {value: 'test'});
+ });
+
+ test('communicate across plugins', done => {
+ const eventName = 'test-plugin-event';
+ Gerrit.install(plugin => {
+ Gerrit.on(eventName, e => {
+ assert.equal(e.plugin.getPluginName(), 'testB');
+ done();
+ });
+ }, '0.1',
+ 'http://test.com/plugins/testA/static/testA.js');
+
+ Gerrit.install(plugin => {
+ Gerrit.emit(eventName, {plugin});
+ }, '0.1',
+ 'http://test.com/plugins/testB/static/testB.js');
+ });
+ });
+
+ suite('test on interfaces', () => {
+ let testObj;
+ class TestClass extends EventEmitter {
+ }
+ setup(() => {
+ testObj = new TestClass();
+ });
+
+ test('on', () => {
+ const cbStub = sinon.stub();
+ testObj.on('test', cbStub);
+ testObj.emit('test');
+ testObj.emit('test');
+ assert.isTrue(cbStub.calledTwice);
+ });
+
+ test('once', () => {
+ const cbStub = sinon.stub();
+ testObj.once('test', cbStub);
+ testObj.emit('test');
+ testObj.emit('test');
+ assert.isTrue(cbStub.calledOnce);
+ });
+
+ test('unsubscribe', () => {
+ const cbStub = sinon.stub();
+ const unsubscribe = testObj.on('test', cbStub);
+ testObj.emit('test');
+ unsubscribe();
+ testObj.emit('test');
+ assert.isTrue(cbStub.calledOnce);
+ });
+
+ test('off', () => {
+ const cbStub = sinon.stub();
+ testObj.on('test', cbStub);
+ testObj.emit('test');
+ testObj.off('test', cbStub);
+ testObj.emit('test');
+ assert.isTrue(cbStub.calledOnce);
+ });
+
+ test('removeAllListeners', () => {
+ const cbStub = sinon.stub();
+ testObj.on('test', cbStub);
+ testObj.removeAllListeners('test');
+ testObj.emit('test');
+ assert.isTrue(cbStub.notCalled);
+ });
+ });
+ });
+</script> \ 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.
<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
<dom-module id="gr-js-api-interface">
+ <script src="../gr-event-interface/gr-event-interface.js"></script>
<script src="gr-annotation-actions-context.js"></script>
<script src="gr-annotation-actions-js-api.js"></script>
<script src="gr-change-actions-js-api.js"></script>
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',