From ab491f77a0120ce7145624173fce85b5396e7413 Mon Sep 17 00:00:00 2001 From: Viktar Donich Date: Tue, 15 Aug 2017 08:02:58 -0700 Subject: [PATCH 1/2] Provide plugin DOM hooks for components In order to simplify custom components usage by plugins, provide DOM hooks for plugin-defined custom components. Previously DOM hook API was only provided for API-generated placeholder elements. Other changes: - endpoint insertions now can be performed at any point in time and will retrospectively attached to DOM for fully initialized endpoints as well - helper method for querying all hook instances - getLastAttached() method returns a promise to simplify singleton element setup and usage (e.g. popups) Change-Id: I1c95146f76ee52bea3b9e01b666112ce6448efcd --- Documentation/dev-plugins-pg.txt | 8 +- .../plugins/gr-dom-hooks/gr-dom-hooks.js | 109 +++++++++++++++--- .../gr-dom-hooks/gr-dom-hooks_test.html | 98 ++++++++++++++-- .../gr-endpoint-decorator.js | 56 ++++++--- .../gr-endpoint-decorator_test.html | 49 +++++++- .../plugins/gr-theme-api/gr-theme-api.js | 2 +- .../gr-change-reply-js-api.js | 2 +- .../gr-plugin-endpoints.js | 20 +++- .../gr-plugin-endpoints_test.html | 27 ++++- .../gr-js-api-interface/gr-public-js-api.js | 26 +++-- 10 files changed, 327 insertions(+), 70 deletions(-) diff --git a/Documentation/dev-plugins-pg.txt b/Documentation/dev-plugins-pg.txt index 9d36758117..e1bf39e22e 100644 --- a/Documentation/dev-plugins-pg.txt +++ b/Documentation/dev-plugins-pg.txt @@ -45,14 +45,14 @@ hook is a custom element that is instantiated for the plugin endpoint. In the decoration case, a hook is set with a `content` attribute that points to the DOM element. -1. Get the DOM hook API instance via `plugin.getDomHook(endpointName)` +1. Get the DOM hook API instance via `plugin.hook(endpointName)` 2. Set up an `onAttached` callback 3. Callback is called when the hook element is created and inserted into DOM 4. Use element.content to get UI element ``` js Gerrit.install(function(plugin) { - const domHook = plugin.getDomHook('reply-text'); + const domHook = plugin.hook('reply-text'); domHook.onAttached(element => { if (!element.content) { return; } // element.content is a reply dialog text area. @@ -70,7 +70,7 @@ NOTE: TODO: Insert link to the full endpoints API. ``` js Gerrit.install(function(plugin) { - const domHook = plugin.getDomHook('reply-text'); + const domHook = plugin.hook('reply-text'); domHook.onAttached(element => { if (!element.content) { return; } element.content.style.border = '1px red dashed'; @@ -86,7 +86,7 @@ option. ``` js Gerrit.install(function(plugin) { - const domHook = plugin.getDomHook('header-title', {replace: true}); + const domHook = plugin.hook('header-title', {replace: true}); domHook.onAttached(element => { element.appendChild(document.createElement('my-site-header')); }); diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js index 02a2085d20..889333b383 100644 --- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js +++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js @@ -14,49 +14,122 @@ (function(window) { 'use strict'; - function GrDomHooks(plugin) { + function GrDomHooksManager(plugin) { this._plugin = plugin; this._hooks = {}; } - GrDomHooks.prototype._getName = function(endpointName) { - return this._plugin.getPluginName() + '-autogenerated-' + endpointName; + GrDomHooksManager.prototype._getHookName = function(endpointName, + opt_moduleName) { + if (opt_moduleName) { + return endpointName + ' ' + opt_moduleName; + } else { + return this._plugin.getPluginName() + '-autogenerated-' + endpointName; + } }; - GrDomHooks.prototype.getDomHook = function(endpointName) { - const hookName = this._getName(endpointName); + GrDomHooksManager.prototype.getDomHook = function(endpointName, + opt_moduleName) { + const hookName = this._getHookName(endpointName, opt_moduleName); if (!this._hooks[hookName]) { - this._hooks[hookName] = new GrDomHook(hookName); + this._hooks[hookName] = new GrDomHook(hookName, opt_moduleName); } return this._hooks[hookName]; }; - function GrDomHook(hookName) { + function GrDomHook(hookName, opt_moduleName) { + this._instances = []; this._callbacks = []; - // Expose to closure. - const callbacks = this._callbacks; - this._componentClass = Polymer({ + if (opt_moduleName) { + this._moduleName = opt_moduleName; + } else { + this._moduleName = hookName; + this._createPlaceholder(hookName); + } + } + + GrDomHook.prototype._createPlaceholder = function(hookName) { + Polymer({ is: hookName, properties: { plugin: Object, content: Object, }, - attached() { - callbacks.forEach(callback => { - callback(this); - }); - }, }); - } + }; + GrDomHook.prototype.handleInstanceDetached = function(instance) { + const index = this._instances.indexOf(instance); + if (index !== -1) { + this._instances.splice(index, 1); + } + }; + + GrDomHook.prototype.handleInstanceAttached = function(instance) { + this._instances.push(instance); + this._callbacks.forEach(callback => callback(instance)); + }; + + /** + * Get instance of last DOM hook element attached into the endpoint. + * Returns a Promise, that's resolved when attachment is done. + * @return {!Promise} + */ + GrDomHook.prototype.getLastAttached = function() { + if (this._instances.length) { + return Promise.resolve(this._instances.slice(-1)[0]); + } + if (!this._lastAttachedPromise) { + let resolve; + const promise = new Promise(r => resolve = r); + this._callbacks.push(resolve); + this._lastAttachedPromise = promise.then(element => { + this._lastAttachedPromise = null; + const index = this._callbacks.indexOf(resolve); + if (index !== -1) { + this._callbacks.splice(index, 1); + } + return element; + }); + } + return this._lastAttachedPromise; + }; + + /** + * Get all DOM hook elements. + */ + GrDomHook.prototype.getAllAttached = function() { + return this._instances; + }; + + /** + * Install a new callback to invoke when a new instance of DOM hook element + * is attached. + * @param {function(Element)} callback + */ GrDomHook.prototype.onAttached = function(callback) { this._callbacks.push(callback); return this; }; + /** + * Name of DOM hook element that will be installed into the endpoint. + */ GrDomHook.prototype.getModuleName = function() { - return this._componentClass.prototype.is; + return this._moduleName; }; - window.GrDomHooks = GrDomHooks; + GrDomHook.prototype.getPublicAPI = function() { + const result = {}; + const exposedMethods = [ + 'onAttached', 'getLastAttached', 'getAllAttached', 'getModuleName', + ]; + for (const p of exposedMethods) { + result[p] = this[p].bind(this); + } + return result; + }; + + window.GrDomHook = GrDomHook; + window.GrDomHooksManager = GrDomHooksManager; })(window); diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html index f92f5c54ee..f5a7f6fe0e 100644 --- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html +++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html @@ -33,28 +33,110 @@ limitations under the License. diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js index 7e74494ab6..bcd2378c85 100644 --- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js +++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js @@ -19,6 +19,17 @@ properties: { name: String, + /** @type {!Map} */ + _domHooks: { + type: Map, + value() { return new Map(); }, + }, + }, + + detached() { + for (const [el, domHook] of this._domHooks) { + domHook.handleInstanceDetached(el); + } }, _import(url) { @@ -31,33 +42,48 @@ const el = document.createElement(name); el.plugin = plugin; el.content = this.getContentChildren()[0]; - return Polymer.dom(this.root).appendChild(el); + this._appendChild(el); + return el; }, _initReplacement(name, plugin) { this.getContentChildren().forEach(node => node.remove()); const el = document.createElement(name); el.plugin = plugin; - return Polymer.dom(this.root).appendChild(el); + this._appendChild(el); + return el; + }, + + _appendChild(el) { + Polymer.dom(this.root).appendChild(el); + }, + + _initModule({moduleName, plugin, type, domHook}) { + let el; + switch (type) { + case 'decorate': + el = this._initDecoration(moduleName, plugin); + break; + case 'replace': + el = this._initReplacement(moduleName, plugin); + break; + } + if (el) { + domHook.handleInstanceAttached(el); + } + this._domHooks.set(el, domHook); }, ready() { + Gerrit._endpoints.onNewEndpoint(this.name, this._initModule.bind(this)); Gerrit.awaitPluginsLoaded().then(() => Promise.all( Gerrit._endpoints.getPlugins(this.name).map( pluginUrl => this._import(pluginUrl))) - ).then(() => { - const modulesData = Gerrit._endpoints.getDetails(this.name); - for (const {moduleName, plugin, type} of modulesData) { - switch (type) { - case 'decorate': - this._initDecoration(moduleName, plugin); - break; - case 'replace': - this._initReplacement(moduleName, plugin); - break; - } - } - }); + ).then(() => + Gerrit._endpoints + .getDetails(this.name) + .forEach(this._initModule, this) + ); }, }); })(); diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html index 8b96dee22e..e7d1930473 100644 --- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html +++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html @@ -34,10 +34,20 @@ limitations under the License. let sandbox; let element; let plugin; + let domHookStub; setup(done => { + Gerrit._endpoints = new GrPluginEndpoints(); + sandbox = sinon.sandbox.create(); + domHookStub = { + handleInstanceAttached: sandbox.stub(), + handleInstanceDetached: sandbox.stub(), + }; + sandbox.stub( + GrDomHooksManager.prototype, 'getDomHook').returns(domHookStub); + // NB: Order is important. Gerrit.install(p => { plugin = p; @@ -45,11 +55,12 @@ limitations under the License. plugin.registerCustomComponent('foo', 'other-module', {replace: true}); }, '0.1', 'http://some/plugin/url.html'); + sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true); sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve()); element = fixture('basic'); - sandbox.stub(element, '_initDecoration'); - sandbox.stub(element, '_initReplacement'); + sandbox.stub(element, '_initDecoration').returns({}); + sandbox.stub(element, '_initReplacement').returns({}); sandbox.stub(element, 'importHref', (url, resolve) => resolve()); flush(done); @@ -65,13 +76,39 @@ limitations under the License. }); test('inits decoration dom hook', () => { - assert.isTrue( - element._initDecoration.calledWith('some-module', plugin)); + assert.strictEqual( + element._initDecoration.lastCall.args[0], 'some-module'); + assert.strictEqual( + element._initDecoration.lastCall.args[1], plugin); }); test('inits replacement dom hook', () => { - assert.isTrue( - element._initReplacement.calledWith('other-module', plugin)); + assert.strictEqual( + element._initReplacement.lastCall.args[0], 'other-module'); + assert.strictEqual( + element._initReplacement.lastCall.args[1], plugin); + }); + + test('calls dom hook handleInstanceAttached', () => { + assert.equal(domHookStub.handleInstanceAttached.callCount, 2); + }); + + test('calls dom hook handleInstanceDetached', () => { + element.detached(); + assert.equal(domHookStub.handleInstanceDetached.callCount, 2); + }); + + test('installs modules on late registration', done => { + domHookStub.handleInstanceAttached.reset(); + plugin.registerCustomComponent('foo', 'noob-noob'); + flush(() => { + assert.equal(domHookStub.handleInstanceAttached.callCount, 1); + assert.strictEqual( + element._initDecoration.lastCall.args[0], 'noob-noob'); + assert.strictEqual( + element._initDecoration.lastCall.args[1], plugin); + done(); + }); }); }); diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js index e91ab0abc0..d57b301767 100644 --- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js +++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js @@ -22,7 +22,7 @@ } GrThemeApi.prototype.setHeaderLogoAndTitle = function(logoUrl, title) { - this.plugin.getDomHook('header-title', {replace: true}).onAttached( + this.plugin.hook('header-title', {replace: true}).onAttached( element => { const customHeader = document.createElement('gr-custom-plugin-header'); diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js index df407a9465..65aa36487e 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js @@ -49,7 +49,7 @@ GrChangeReplyInterface.prototype.addReplyTextChangedCallback = function(handler) { - this.plugin.getDomHook('reply-text').onAttached(el => { + this.plugin.hook('reply-text').onAttached(el => { if (!el.content) { return; } el.content.addEventListener('value-changed', e => { handler(e.detail.value); diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js index 52b1fb7327..1ee9eec64e 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js @@ -16,19 +16,32 @@ function GrPluginEndpoints() { this._endpoints = {}; + this._callbacks = {}; } + GrPluginEndpoints.prototype.onNewEndpoint = function(endpoint, callback) { + if (!this._callbacks[endpoint]) { + this._callbacks[endpoint] = []; + } + this._callbacks[endpoint].push(callback); + }; + GrPluginEndpoints.prototype.registerModule = function(plugin, endpoint, type, - moduleName) { + moduleName, domHook) { if (!this._endpoints[endpoint]) { this._endpoints[endpoint] = []; } - this._endpoints[endpoint].push({ + const moduleInfo = { moduleName, plugin, pluginUrl: plugin._url, type, - }); + domHook, + }; + this._endpoints[endpoint].push(moduleInfo); + if (Gerrit._arePluginsLoaded() && this._callbacks[endpoint]) { + this._callbacks[endpoint].forEach(callback => callback(moduleInfo)); + } }; /** @@ -44,6 +57,7 @@ * plugin: Plugin, * pluginUrl: String, * type: EndpointType, + * domHook: !Object * }>} */ GrPluginEndpoints.prototype.getDetails = function(name, opt_options) { diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html index 2c1f4e9dbd..a61cdc814d 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html @@ -29,16 +29,21 @@ limitations under the License. let instance; let pluginFoo; let pluginBar; + let domHook; setup(() => { sandbox = sinon.sandbox.create(); + domHook = {}; instance = new GrPluginEndpoints(); Gerrit.install(p => { pluginFoo = p; }, '0.1', 'http://test.com/plugins/testplugin/static/foo.html'); - instance.registerModule(pluginFoo, 'a-place', 'decorate', 'foo-module'); + instance.registerModule( + pluginFoo, 'a-place', 'decorate', 'foo-module', domHook); Gerrit.install(p => { pluginBar = p; }, '0.1', 'http://test.com/plugins/testplugin/static/bar.html'); - instance.registerModule(pluginBar, 'a-place', 'style', 'bar-module'); + instance.registerModule( + pluginBar, 'a-place', 'style', 'bar-module', domHook); + sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true); }); teardown(() => { @@ -52,12 +57,14 @@ limitations under the License. plugin: pluginFoo, pluginUrl: pluginFoo._url, type: 'decorate', + domHook, }, { moduleName: 'bar-module', plugin: pluginBar, pluginUrl: pluginBar._url, type: 'style', + domHook, }, ]); }); @@ -69,6 +76,7 @@ limitations under the License. plugin: pluginBar, pluginUrl: pluginBar._url, type: 'style', + domHook, }, ]); }); @@ -82,6 +90,7 @@ limitations under the License. plugin: pluginFoo, pluginUrl: pluginFoo._url, type: 'decorate', + domHook, }, ]); }); @@ -95,5 +104,19 @@ limitations under the License. assert.deepEqual( instance.getPlugins('a-place'), [pluginFoo._url, pluginBar._url]); }); + + test('onNewEndpoint', () => { + const newModuleStub = sandbox.stub(); + instance.onNewEndpoint('a-place', newModuleStub); + instance.registerModule( + pluginFoo, 'a-place', 'replace', 'zaz-module', domHook); + assert.deepEqual(newModuleStub.lastCall.args[0], { + moduleName: 'zaz-module', + plugin: pluginFoo, + pluginUrl: pluginFoo._url, + type: 'replace', + domHook, + }); + }); }); 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 d1b94171a5..6c3db84dcd 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 @@ -55,8 +55,7 @@ window.$wnd = window; function Plugin(opt_url) { - this._generatedHookNames = []; - this._domHooks = new GrDomHooks(this); + this._domHooks = new GrDomHooksManager(this); if (!opt_url) { console.warn('Plugin not being loaded from /plugins base path.', @@ -93,11 +92,22 @@ }; Plugin.prototype.registerCustomComponent = function( - endpointName, moduleName, opt_options) { + endpointName, opt_moduleName, opt_options) { const type = opt_options && opt_options.replace ? EndpointType.REPLACE : EndpointType.DECORATE; + const hook = this._domHooks.getDomHook(endpointName, opt_moduleName); + const moduleName = opt_moduleName || hook.getModuleName(); Gerrit._endpoints.registerModule( - this, endpointName, type, moduleName); + this, endpointName, type, moduleName, hook); + return hook.getPublicAPI(); + }; + + /** + * Returns instance of DOM hook API for endpoint. Creates a placeholder + * element for the first call. + */ + Plugin.prototype.hook = function(endpointName, opt_options) { + return this.registerCustomComponent(endpointName, undefined, opt_options); }; Plugin.prototype.getServerInfo = function() { @@ -166,14 +176,6 @@ return new GrAttributeHelper(element); }; - Plugin.prototype.getDomHook = function(endpointName, opt_options) { - const hook = this._domHooks.getDomHook(endpointName); - const moduleName = hook.getModuleName(); - const type = opt_options && opt_options.type || EndpointType.DECORATE; - Gerrit._endpoints.registerModule(this, endpointName, type, moduleName); - return hook; - }; - const Gerrit = window.Gerrit || {}; // Number of plugins to initialize, -1 means 'not yet known'. From 2f3ee89d02fea8b30c62ef2d81fbc783cf792890 Mon Sep 17 00:00:00 2001 From: Viktar Donich Date: Fri, 4 Aug 2017 09:48:19 -0700 Subject: [PATCH 2/2] Plugin popup() api Changes to JS API: - popups have backdrop and is centered - plugin.popup() takes a string custom element name - plugin.popup() returns an API for closing and reopening the popup - plugin.deprecated.popup() takes Element (similar to GWT Plugin JS API) Recommended usage: ``` js Gerrit.install(function(plugin) { const popup = plugin.popup('my-plugin-popup-simple');; // ... work popup.close(); // ... more work popup.open(); }); ``` ``` html ``` Change-Id: Icb3f83d35f3c60915f12b77bc8a7d548d50d5695 --- plugins/replication | 2 +- polygerrit-ui/app/elements/gr-app.html | 2 + .../gr-popup-interface/gr-plugin-popup.html | 28 +++++ .../gr-popup-interface/gr-plugin-popup.js | 28 +++++ .../gr-plugin-popup_test.html | 67 +++++++++++ .../gr-popup-interface.html | 23 ++++ .../gr-popup-interface/gr-popup-interface.js | 71 +++++++++++ .../gr-popup-interface_test.html | 112 ++++++++++++++++++ .../gr-js-api-interface.html | 1 + .../gr-js-api-interface_test.html | 30 +++++ .../gr-js-api-interface/gr-public-js-api.js | 22 ++++ .../app/template_test_srcs/template_test.js | 6 +- polygerrit-ui/app/test/index.html | 2 + 13 files changed, 391 insertions(+), 3 deletions(-) create mode 100644 polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.html create mode 100644 polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js create mode 100644 polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html create mode 100644 polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html create mode 100644 polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js create mode 100644 polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html diff --git a/plugins/replication b/plugins/replication index 297b749038..643d635a45 160000 --- a/plugins/replication +++ b/plugins/replication @@ -1 +1 @@ -Subproject commit 297b749038153527291b43cb08b162eb475adcd7 +Subproject commit 643d635a4502e2a2df6cb02edade88bad3fd953a diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html index defbe8a405..406a4a75d1 100644 --- a/polygerrit-ui/app/elements/gr-app.html +++ b/polygerrit-ui/app/elements/gr-app.html @@ -46,6 +46,7 @@ limitations under the License. + @@ -201,6 +202,7 @@ limitations under the License. on-close="_handleRegistrationDialogClose"> + diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.html new file mode 100644 index 0000000000..3ccb3fd914 --- /dev/null +++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.html @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js new file mode 100644 index 0000000000..8286eaeae2 --- /dev/null +++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js @@ -0,0 +1,28 @@ +// Copyright (C) 2017 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'; + Polymer({ + is: 'gr-plugin-popup', + get opened() { + return this.$.overlay.opened; + }, + open() { + return this.$.overlay.open(); + }, + close() { + this.$.overlay.close(); + }, + }); +})(window); diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html new file mode 100644 index 0000000000..2dbf96d96e --- /dev/null +++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html @@ -0,0 +1,67 @@ + + + + +gr-plugin-popup + + + + + + + + + + + + + diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html new file mode 100644 index 0000000000..6bf37de43a --- /dev/null +++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js new file mode 100644 index 0000000000..e62e882079 --- /dev/null +++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js @@ -0,0 +1,71 @@ +// Copyright (C) 2017 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'; + + /** + * Plugin popup API. + * Provides method for opening and closing popups from plugin. + * opt_moduleName is a name of custom element that will be automatically + * inserted on popup opening. + * @param {!Object} plugin + * @param {opt_moduleName=} string + */ + function GrPopupInterface(plugin, opt_moduleName) { + this.plugin = plugin; + this._openingPromise = null; + this._popup = null; + this._moduleName = opt_moduleName || null; + } + + GrPopupInterface.prototype._getElement = function() { + return Polymer.dom(this._popup); + }; + + /** + * Opens the popup, inserts it into DOM over current UI. + * Creates the popup if not previously created. Creates popup content element, + * if it was provided with constructor. + * @returns {!Promise} + */ + GrPopupInterface.prototype.open = function() { + if (!this._openingPromise) { + this._openingPromise = + this.plugin.hook('plugin-overlay').getLastAttached() + .then(hookEl => { + const popup = document.createElement('gr-plugin-popup'); + if (this._moduleName) { + const el = Polymer.dom(popup).appendChild( + document.createElement(this._moduleName)); + el.plugin = this.plugin; + } + this._popup = Polymer.dom(hookEl).appendChild(popup); + Polymer.dom.flush(); + return this._popup.open().then(() => this); + }); + } + return this._openingPromise; + }; + + /** + * Hides the popup. + */ + GrPopupInterface.prototype.close = function() { + if (!this._popup) { return; } + this._popup.close(); + this._openingPromise = null; + }; + + window.GrPopupInterface = GrPopupInterface; +})(window); diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html new file mode 100644 index 0000000000..7d9dd28ff8 --- /dev/null +++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html @@ -0,0 +1,112 @@ + + + + +gr-popup-interface + + + + + + + + + + + + + + + + + + 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 f6e2b64591..53f889fddd 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 @@ -19,6 +19,7 @@ limitations under the License. + diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html index ca0f3722aa..7c9033edae 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html @@ -338,5 +338,35 @@ limitations under the License. 'http://test.com/r/plugins/testplugin/static/test.js'); }); }); + + suite('popup', () => { + test('popup(element) is deprecated', () => { + assert.throws(() => { + plugin.popup(document.createElement('div')); + }); + }); + + test('popup(moduleName) creates popup with component', () => { + const openStub = sandbox.stub(); + sandbox.stub(window, 'GrPopupInterface').returns({ + open: openStub, + }); + plugin.popup('some-name'); + assert.isTrue(openStub.calledOnce); + assert.isTrue(GrPopupInterface.calledWith(plugin, 'some-name')); + }); + + test('deprecated.popup(element) creates popup with element', () => { + const el = document.createElement('div'); + el.textContent = 'some text here'; + const openStub = sandbox.stub(GrPopupInterface.prototype, 'open'); + openStub.returns(Promise.resolve({ + _getElement() { + return document.createElement('div'); + }})); + plugin.deprecated.popup(el); + assert.isTrue(openStub.calledOnce); + }); + }); }); 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 6c3db84dcd..f1d607c6f5 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 @@ -76,6 +76,10 @@ return; } this._name = pathname.split('/')[2]; + + this.deprecated = { + popup: deprecatedAPI.popup.bind(this), + }; } Plugin._sharedAPIElement = document.createElement('gr-js-api-interface'); @@ -176,6 +180,24 @@ return new GrAttributeHelper(element); }; + Plugin.prototype.popup = function(moduleName) { + if (typeof moduleName !== 'string') { + throw new Error('deprecated, use deprecated.popup'); + } + const api = new GrPopupInterface(this, moduleName); + return api.open(); + }; + + const deprecatedAPI = {}; + deprecatedAPI.popup = function(el) { + console.warn('plugin.deprecated.popup() is deprecated!'); + if (!el) { + throw new Error('Popup contents not found'); + } + const api = new GrPopupInterface(this); + api.open().then(api => api._getElement().appendChild(el)); + }; + const Gerrit = window.Gerrit || {}; // Number of plugins to initialize, -1 means 'not yet known'. diff --git a/polygerrit-ui/app/template_test_srcs/template_test.js b/polygerrit-ui/app/template_test_srcs/template_test.js index 138d0ea052..dffcaf9c84 100644 --- a/polygerrit-ui/app/template_test_srcs/template_test.js +++ b/polygerrit-ui/app/template_test_srcs/template_test.js @@ -27,13 +27,15 @@ const EXTERN_NAMES = [ 'GrGerritAuth', 'GrLinkTextParser', 'GrPluginEndpoints', + 'GrPopupInterface', 'GrRangeNormalizer', 'GrReporting', 'GrReviewerUpdatesParser', 'GrThemeApi', 'moment', 'page', - 'util']; + 'util', +]; fs.readdir('./polygerrit-ui/temp/behaviors/', (err, data) => { if (err) { @@ -102,4 +104,4 @@ fs.readdir('./polygerrit-ui/temp/behaviors/', (err, data) => { process.exit(1); } }); -}); \ No newline at end of file +}); diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html index 41f5fc671e..912b0fff5a 100644 --- a/polygerrit-ui/app/test/index.html +++ b/polygerrit-ui/app/test/index.html @@ -103,6 +103,8 @@ limitations under the License. 'plugins/gr-attribute-helper/gr-attribute-helper_test.html', 'plugins/gr-external-style/gr-external-style_test.html', 'plugins/gr-plugin-host/gr-plugin-host_test.html', + 'plugins/gr-popup-interface/gr-plugin-popup_test.html', + 'plugins/gr-popup-interface/gr-popup-interface_test.html', 'settings/gr-account-info/gr-account-info_test.html', 'settings/gr-change-table-editor/gr-change-table-editor_test.html', 'settings/gr-email-editor/gr-email-editor_test.html',