Provide JS API to plugin to replace DOM endpoint content
Allows plugins to replace parts of PolyGerrit with plugin-provided custom elements that use HTML/CSS for purposes of theming. Also adds an intent-based API method for replacing logo and title only: <dom-module id="chromium-style"> <script> Gerrit.install(function(plugin) { plugin.theme().setHeaderLogoAndTitle( plugin.url('/static/my_logo.png'), 'My Title'); }); </script> </dom-module> Advanced example: plugin replacing contents of header title section. <dom-module id="my-style"> <script> Gerrit.install(function(plugin) { plugin.registerCustomComponent( 'header-title', 'my-header', {replace: true}); }); </script> </dom-module> <dom-module id="my-header"> <template> <style> /* styles go here */ </style> <span> <img src=""> <span>My Custom Title</span> </span> </template> <script> Polymer({is: 'my-header'}); </script> </dom-module> Change-Id: I4c5410ba84891db864f1a5ab733291c1481f4496
This commit is contained in:
parent
b89eb17931
commit
8d36970418
@ -17,6 +17,7 @@ limitations under the License.
|
||||
|
||||
<link rel="import" href="../../../behaviors/docs-url-behavior/docs-url-behavior.html">
|
||||
<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
|
||||
<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
|
||||
<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
|
||||
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
|
||||
<link rel="import" href="../gr-account-dropdown/gr-account-dropdown.html">
|
||||
@ -40,7 +41,8 @@ limitations under the License.
|
||||
.bigTitle:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.bigTitle::before {
|
||||
/* TODO (viktard): Clean-up after chromium-style migrates to component. */
|
||||
.titleText::before {
|
||||
background-image: var(--header-icon);
|
||||
background-size: var(--header-icon-size) var(--header-icon-size);
|
||||
content: "";
|
||||
@ -50,7 +52,7 @@ limitations under the License.
|
||||
vertical-align: text-bottom;
|
||||
width: var(--header-icon-size);
|
||||
}
|
||||
.bigTitle::after {
|
||||
.titleText::after {
|
||||
content: var(--header-title-content);
|
||||
}
|
||||
ul {
|
||||
@ -131,7 +133,11 @@ limitations under the License.
|
||||
}
|
||||
</style>
|
||||
<nav>
|
||||
<a href$="[[_computeRelativeURL('/')]]" class="bigTitle"></a>
|
||||
<a href$="[[_computeRelativeURL('/')]]" class="bigTitle">
|
||||
<gr-endpoint-decorator name="header-title">
|
||||
<span class="titleText"></span>
|
||||
</gr-endpoint-decorator>
|
||||
</a>
|
||||
<ul class="links">
|
||||
<template is="dom-repeat" items="[[_links]]" as="linkGroup">
|
||||
<li class$="[[linkGroup.class]]">
|
||||
|
@ -27,21 +27,35 @@
|
||||
});
|
||||
},
|
||||
|
||||
_initPluginDomHook(name, plugin) {
|
||||
_initDecoration(name, plugin) {
|
||||
const el = document.createElement(name);
|
||||
el.plugin = plugin;
|
||||
el.content = this.getContentChildren()[0];
|
||||
return Polymer.dom(this.root).appendChild(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);
|
||||
},
|
||||
|
||||
ready() {
|
||||
Gerrit.awaitPluginsLoaded().then(() => Promise.all(
|
||||
Gerrit._getPluginsForEndpoint(this.name).map(
|
||||
pluginUrl => this._import(pluginUrl)))
|
||||
).then(() => {
|
||||
const modulesData = Gerrit._getEndpointDetails(this.name);
|
||||
for (const {moduleName, plugin} of modulesData) {
|
||||
this._initPluginDomHook(moduleName, plugin);
|
||||
for (const {moduleName, plugin, type} of modulesData) {
|
||||
switch (type) {
|
||||
case 'decorate':
|
||||
this._initDecoration(moduleName, plugin);
|
||||
break;
|
||||
case 'replace':
|
||||
this._initReplacement(moduleName, plugin);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
@ -42,13 +42,15 @@ limitations under the License.
|
||||
Gerrit.install(p => {
|
||||
plugin = p;
|
||||
plugin.registerCustomComponent('foo', 'some-module');
|
||||
plugin.registerCustomComponent('foo', 'other-module', {replace: true});
|
||||
}, '0.1', 'http://some/plugin/url.html');
|
||||
|
||||
sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve());
|
||||
|
||||
element = fixture('basic');
|
||||
sandbox.stub(element, '_initPluginDomHook');
|
||||
sandbox.stub(element, 'importHref', (url, resolve) => { resolve(); });
|
||||
sandbox.stub(element, '_initDecoration');
|
||||
sandbox.stub(element, '_initReplacement');
|
||||
sandbox.stub(element, 'importHref', (url, resolve) => resolve());
|
||||
|
||||
flush(done);
|
||||
});
|
||||
@ -62,9 +64,14 @@ limitations under the License.
|
||||
element.importHref.calledWith(new URL('http://some/plugin/url.html')));
|
||||
});
|
||||
|
||||
test('inits plugin-provided dom hook', () => {
|
||||
test('inits decoration dom hook', () => {
|
||||
assert.isTrue(
|
||||
element._initPluginDomHook.calledWith('some-module', plugin));
|
||||
element._initDecoration.calledWith('some-module', plugin));
|
||||
});
|
||||
|
||||
test('inits replacement dom hook', () => {
|
||||
assert.isTrue(
|
||||
element._initReplacement.calledWith('other-module', plugin));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
@ -0,0 +1,45 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
<link rel="import" href="../../../bower_components/polymer/polymer.html">
|
||||
|
||||
<dom-module id="gr-custom-plugin-header">
|
||||
<template>
|
||||
<style>
|
||||
img {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.title {
|
||||
margin-left: .25em;
|
||||
}
|
||||
</style>
|
||||
<span>
|
||||
<img src="[[logoUrl]]" hidden$="[[!logoUrl]]">
|
||||
<span class="title">[[title]]</span>
|
||||
</span>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
is: 'gr-custom-plugin-header',
|
||||
properties: {
|
||||
logoUrl: String,
|
||||
title: String,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</dom-module>
|
@ -0,0 +1,23 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
<link rel="import" href="../../../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
|
||||
<link rel="import" href="gr-custom-plugin-header.html">
|
||||
|
||||
<dom-module id="gr-theme-api">
|
||||
<script src="gr-theme-api.js"></script>
|
||||
</dom-module>
|
@ -0,0 +1,34 @@
|
||||
// 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';
|
||||
|
||||
// Prevent redefinition.
|
||||
if (window.GrThemeApi) { return; }
|
||||
|
||||
function GrThemeApi(plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
GrThemeApi.prototype.setHeaderLogoAndTitle = function(logoUrl, title) {
|
||||
this.plugin.getDomHook('header-title', {replace: true}).then(element => {
|
||||
const customHeader = document.createElement('gr-custom-plugin-header');
|
||||
customHeader.logoUrl = logoUrl;
|
||||
customHeader.title = title;
|
||||
element.appendChild(customHeader);
|
||||
});
|
||||
};
|
||||
|
||||
window.GrThemeApi = GrThemeApi;
|
||||
})(window);
|
@ -0,0 +1,81 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
|
||||
<title>gr-theme-api</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-endpoint-decorator/gr-endpoint-decorator.html">
|
||||
<link rel="import" href="gr-theme-api.html">
|
||||
|
||||
<script>void(0);</script>
|
||||
|
||||
<test-fixture id="header-title">
|
||||
<template>
|
||||
<gr-endpoint-decorator name="header-title">
|
||||
<span class="titleText"></span>
|
||||
</gr-endpoint-decorator>
|
||||
</template>
|
||||
</test-fixture>
|
||||
|
||||
<script>
|
||||
suite('gr-theme-api tests', () => {
|
||||
let sandbox;
|
||||
let theme;
|
||||
|
||||
setup(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
let plugin;
|
||||
Gerrit.install(p => { plugin = p; }, '0.1',
|
||||
'http://test.com/plugins/testplugin/static/test.js');
|
||||
theme = plugin.theme();
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
theme = null;
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
test('exists', () => {
|
||||
assert.isOk(theme);
|
||||
});
|
||||
|
||||
suite('header-title', () => {
|
||||
let customHeader;
|
||||
|
||||
setup(() => {
|
||||
fixture('header-title');
|
||||
stub('gr-custom-plugin-header', {
|
||||
ready() { customHeader = this; },
|
||||
});
|
||||
Gerrit._resolveAllPluginsLoaded();
|
||||
});
|
||||
|
||||
test('sets logo and title', done => {
|
||||
theme.setHeaderLogoAndTitle('foo.jpg', 'bar');
|
||||
flush(() => {
|
||||
assert.isNotNull(customHeader);
|
||||
assert.equal(customHeader.logoUrl, 'foo.jpg');
|
||||
assert.equal(customHeader.title, 'bar');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||
|
||||
<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
|
||||
<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
|
||||
<link rel="import" href="../../plugins/gr-theme-api/gr-theme-api.html">
|
||||
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
|
||||
<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
|
||||
|
||||
|
@ -34,9 +34,20 @@
|
||||
|
||||
const API_VERSION = '0.1';
|
||||
|
||||
/**
|
||||
* Plugin-provided custom components can affect content in extension
|
||||
* points using one of following methods:
|
||||
* - DECORATE: custom component is set with `content` attribute and may
|
||||
* decorate (e.g. style) DOM element.
|
||||
* - REPLACE: contents of extension point are replaced with the custom
|
||||
* component.
|
||||
* - STYLE: custom component is a shared styles module that is inserted
|
||||
* into the extension point.
|
||||
*/
|
||||
const EndpointType = {
|
||||
DECORATE: 'decorate',
|
||||
REPLACE: 'replace',
|
||||
STYLE: 'style',
|
||||
DOM_DECORATION: 'dom',
|
||||
};
|
||||
|
||||
// GWT JSNI uses $wnd to refer to window.
|
||||
@ -75,11 +86,12 @@
|
||||
endpointName, EndpointType.STYLE, moduleName);
|
||||
};
|
||||
|
||||
Plugin.prototype.registerCustomComponent =
|
||||
function(endpointName, moduleName) {
|
||||
this._registerEndpointModule(
|
||||
endpointName, EndpointType.DOM_DECORATION, moduleName);
|
||||
};
|
||||
Plugin.prototype.registerCustomComponent = function(endpointName, moduleName,
|
||||
opt_options) {
|
||||
const type = opt_options && opt_options.replace ?
|
||||
EndpointType.REPLACE : EndpointType.DECORATE;
|
||||
this._registerEndpointModule(endpointName, type, moduleName);
|
||||
};
|
||||
|
||||
Plugin.prototype._registerEndpointModule = function(endpoint, type, module) {
|
||||
const endpoints = Gerrit._endpoints;
|
||||
@ -146,6 +158,10 @@
|
||||
Plugin._sharedAPIElement.Element.REPLY_DIALOG));
|
||||
};
|
||||
|
||||
Plugin.prototype.theme = function() {
|
||||
return new GrThemeApi(this);
|
||||
};
|
||||
|
||||
Plugin.prototype._getGeneratedHookName = function(endpointName) {
|
||||
if (!this._generatedHookNames[endpointName]) {
|
||||
this._generatedHookNames[endpointName] =
|
||||
@ -154,7 +170,7 @@
|
||||
return this._generatedHookNames[endpointName];
|
||||
};
|
||||
|
||||
Plugin.prototype.getDomHook = function(endpointName) {
|
||||
Plugin.prototype.getDomHook = function(endpointName, opt_options) {
|
||||
const hookName = this._getGeneratedHookName(endpointName);
|
||||
if (!this._hooks[hookName]) {
|
||||
this._hooks[hookName] = new Promise((resolve, reject) => {
|
||||
@ -168,7 +184,7 @@
|
||||
resolve(this);
|
||||
},
|
||||
});
|
||||
this.registerCustomComponent(endpointName, hookName);
|
||||
this.registerCustomComponent(endpointName, hookName, opt_options);
|
||||
});
|
||||
}
|
||||
return this._hooks[hookName];
|
||||
|
@ -56,13 +56,21 @@ func main() {
|
||||
http.Handle("/plugins/", http.StripPrefix("/plugins/",
|
||||
http.FileServer(http.Dir(*plugins))))
|
||||
log.Println("Local plugins from", *plugins)
|
||||
} else {
|
||||
http.HandleFunc("/plugins/", handleRESTProxy)
|
||||
}
|
||||
log.Println("Serving on port", *port)
|
||||
log.Fatal(http.ListenAndServe(*port, &server{}))
|
||||
}
|
||||
|
||||
func handleRESTProxy(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if strings.HasSuffix(r.URL.Path, ".html") {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
} else if strings.HasSuffix(r.URL.Path, ".css") {
|
||||
w.Header().Set("Content-Type", "text/css")
|
||||
} else {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
}
|
||||
req := &http.Request{
|
||||
Method: "GET",
|
||||
URL: &url.URL{
|
||||
|
Loading…
Reference in New Issue
Block a user