Merge changes Ic29a4ec0,Ic40487d3
* changes: Make it easy to construct CSS and HTML from JavaScript plugins Wrap long JSNI in ApiGlue
This commit is contained in:
@@ -549,6 +549,28 @@ The `Gerrit` object is the only symbol provided into the global
|
|||||||
namespace by Gerrit Code Review. All top-level functions can be
|
namespace by Gerrit Code Review. All top-level functions can be
|
||||||
accessed through this name.
|
accessed through this name.
|
||||||
|
|
||||||
|
[[Gerrit_css]]
|
||||||
|
Gerrit.css()
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
Creates a new unique CSS class and injects it into the document.
|
||||||
|
The name of the class is returned and can be used by the plugin.
|
||||||
|
See link:#Gerrit_html[`Gerrit.html()`] for an easy way to use
|
||||||
|
generated class names.
|
||||||
|
|
||||||
|
Classes created with this function should be created once at install
|
||||||
|
time and reused throughout the plugin. Repeatedly creating the same
|
||||||
|
class will explode the global stylesheet.
|
||||||
|
|
||||||
|
.Signature
|
||||||
|
[source,javascript]
|
||||||
|
----
|
||||||
|
Gerrit.install(function(self)) {
|
||||||
|
var style = {
|
||||||
|
name: Gerrit.css('background: #fff; color: #000;'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
----
|
||||||
|
|
||||||
[[Gerrit_delete]]
|
[[Gerrit_delete]]
|
||||||
=== Gerrit.delete()
|
=== Gerrit.delete()
|
||||||
Issues a DELETE REST API request to the Gerrit server. For plugin
|
Issues a DELETE REST API request to the Gerrit server. For plugin
|
||||||
@@ -626,6 +648,114 @@ If the URL passed matches `http://...`, `https://...`, or `//...`
|
|||||||
the current browser window will navigate to the non-Gerrit URL.
|
the current browser window will navigate to the non-Gerrit URL.
|
||||||
The user can return to Gerrit with the back button.
|
The user can return to Gerrit with the back button.
|
||||||
|
|
||||||
|
[[Gerrit_html]]
|
||||||
|
Gerrit.html()
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
Parses an HTML fragment after performing template replacements. If
|
||||||
|
the HTML has a single root element or node that node is returned,
|
||||||
|
otherwise it is wrapped inside a `<div>` and the div is returned.
|
||||||
|
|
||||||
|
.Signature
|
||||||
|
[source,javascript]
|
||||||
|
----
|
||||||
|
Gerrit.html(htmlText, options, wantElements);
|
||||||
|
----
|
||||||
|
|
||||||
|
* htmlText: string of HTML to be parsed. A new unattached `<div>` is
|
||||||
|
created in the browser's document and the innerHTML property is
|
||||||
|
assigned to the passed string, after performing replacements. If
|
||||||
|
the div has exactly one child, that child will be returned instead
|
||||||
|
of the div.
|
||||||
|
|
||||||
|
* options: optional object reference supplying replacements for any
|
||||||
|
`{name}` references in htmlText. Navigation through objects is
|
||||||
|
supported permitting `{style.bar}` to be replaced with `"foo"` if
|
||||||
|
options was `{style: {bar: "foo"}}`. Value replacements are HTML
|
||||||
|
escaped before being inserted into the document fragment.
|
||||||
|
|
||||||
|
* wantElements: if options is given and wantElements is also true
|
||||||
|
an object consisting of `{root: parsedElement, elements: {...}}` is
|
||||||
|
returned instead of the parsed element. The elements object contains
|
||||||
|
a property for each element using `id={name}` in htmlText.
|
||||||
|
|
||||||
|
.Example
|
||||||
|
[source,javascript]
|
||||||
|
----
|
||||||
|
var style = {bar: Gerrit.css('background: yellow')};
|
||||||
|
Gerrit.html(
|
||||||
|
'<span class="{style.bar}">Hello {name}!</span>',
|
||||||
|
{style: style, name: "World"});
|
||||||
|
----
|
||||||
|
|
||||||
|
Event handlers can be automatically attached to elements referenced
|
||||||
|
through an attribute id. Object navigation is not supported for ids,
|
||||||
|
and the parser strips the id attribute before returning the result.
|
||||||
|
Handler functions must begin with `on` and be a function to be
|
||||||
|
installed on the element. This approach is useful for onclick and
|
||||||
|
other handlers that do not want to create circular references that
|
||||||
|
will eventually leak browser memory.
|
||||||
|
|
||||||
|
.Example
|
||||||
|
[source,javascript]
|
||||||
|
----
|
||||||
|
var options = {
|
||||||
|
link: {
|
||||||
|
onclick: function(e) { window.close() },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Gerrit.html('<a href="javascript:;" id="{link}">Close</a>', options);
|
||||||
|
----
|
||||||
|
|
||||||
|
When using options to install handlers care must be taken to not
|
||||||
|
accidentally include the returned element into the event handler's
|
||||||
|
closure. This is why options is built before calling `Gerrit.html()`
|
||||||
|
and not inline as a shown above with "Hello World".
|
||||||
|
|
||||||
|
DOM nodes can optionally be returned, allowing handlers to access the
|
||||||
|
elements identified by `id={name}` at a later point in time.
|
||||||
|
|
||||||
|
.Example
|
||||||
|
[source,javascript]
|
||||||
|
----
|
||||||
|
var w = Gerrit.html(
|
||||||
|
'<div>Name: <input type="text" id="{name}"></div>'
|
||||||
|
+ '<div>Age: <input type="text" id="{age}"></div>'
|
||||||
|
+ '<button id="{submit}"><div>Save</div></button>',
|
||||||
|
{
|
||||||
|
submit: {
|
||||||
|
onclick: function(s) {
|
||||||
|
var e = w.elements;
|
||||||
|
window.alert(e.name.value + " is " + e.age.value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, true);
|
||||||
|
----
|
||||||
|
|
||||||
|
To prevent memory leaks `w.root` and `w.elements` should be set to
|
||||||
|
null when the elements are no longer necessary. Screens can use
|
||||||
|
link:#screen_onUnload[screen.onUnload()] to define a callback function
|
||||||
|
to perform this cleanup:
|
||||||
|
|
||||||
|
[source,javascript]
|
||||||
|
----
|
||||||
|
var w = Gerrit.html(...);
|
||||||
|
screen.body.appendElement(w.root);
|
||||||
|
screen.onUnload(function() { w.clear() });
|
||||||
|
----
|
||||||
|
|
||||||
|
[[Gerrit_injectCss]]
|
||||||
|
Gerrit.injectCss()
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
Injects CSS rules into the document by appending onto the end of the
|
||||||
|
existing rule list. CSS rules are global to the entire application
|
||||||
|
and must be manually scoped by each plugin. For an automatic scoping
|
||||||
|
alternative see link:#Gerrit_css[`css()`].
|
||||||
|
|
||||||
|
[source,javascript]
|
||||||
|
----
|
||||||
|
Gerrit.injectCss('.myplugin_bg {background: #000}');
|
||||||
|
----
|
||||||
|
|
||||||
[[Gerrit_install]]
|
[[Gerrit_install]]
|
||||||
=== Gerrit.install()
|
=== Gerrit.install()
|
||||||
Registers a new plugin by invoking the supplied initialization
|
Registers a new plugin by invoking the supplied initialization
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ public class ApiGlue {
|
|||||||
public static void init() {
|
public static void init() {
|
||||||
init0();
|
init0();
|
||||||
ActionContext.init();
|
ActionContext.init();
|
||||||
|
HtmlTemplate.init();
|
||||||
Plugin.init();
|
Plugin.init();
|
||||||
addHistoryHook();
|
addHistoryHook();
|
||||||
}
|
}
|
||||||
@@ -44,9 +45,13 @@ public class ApiGlue {
|
|||||||
project_actions: {},
|
project_actions: {},
|
||||||
|
|
||||||
getPluginName: @com.google.gerrit.client.api.ApiGlue::getPluginName(),
|
getPluginName: @com.google.gerrit.client.api.ApiGlue::getPluginName(),
|
||||||
|
injectCss: @com.google.gwt.dom.client.StyleInjector::inject(Ljava/lang/String;),
|
||||||
install: function (f) {
|
install: function (f) {
|
||||||
var p = this._getPluginByUrl(@com.google.gerrit.client.api.PluginName::getCallerUrl()());
|
var p = this._getPluginByUrl(@com.google.gerrit.client.api.PluginName::getCallerUrl()());
|
||||||
@com.google.gerrit.client.api.ApiGlue::install(Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gerrit/client/api/Plugin;)(f,p);
|
@com.google.gerrit.client.api.ApiGlue::install(
|
||||||
|
Lcom/google/gwt/core/client/JavaScriptObject;
|
||||||
|
Lcom/google/gerrit/client/api/Plugin;)
|
||||||
|
(f,p);
|
||||||
},
|
},
|
||||||
installGwt: function(u){return this._getPluginByUrl(u)},
|
installGwt: function(u){return this._getPluginByUrl(u)},
|
||||||
_getPluginByUrl: function(u) {
|
_getPluginByUrl: function(u) {
|
||||||
@@ -80,26 +85,64 @@ public class ApiGlue {
|
|||||||
return serverUrl;
|
return serverUrl;
|
||||||
},
|
},
|
||||||
|
|
||||||
_api: function(u) {return @com.google.gerrit.client.rpc.RestApi::new(Ljava/lang/String;)(u)},
|
_api: function(u) {
|
||||||
get: function(u,b){@com.google.gerrit.client.api.ActionContext::get(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),b)},
|
return @com.google.gerrit.client.rpc.RestApi::new(Ljava/lang/String;)(u);
|
||||||
|
},
|
||||||
|
get: function(u,b) {
|
||||||
|
@com.google.gerrit.client.api.ActionContext::get(
|
||||||
|
Lcom/google/gerrit/client/rpc/RestApi;
|
||||||
|
Lcom/google/gwt/core/client/JavaScriptObject;)
|
||||||
|
(this._api(u), b);
|
||||||
|
},
|
||||||
post: function(u,i,b) {
|
post: function(u,i,b) {
|
||||||
if (typeof i=='string')
|
if (typeof i == 'string') {
|
||||||
@com.google.gerrit.client.api.ActionContext::post(Lcom/google/gerrit/client/rpc/RestApi;Ljava/lang/String;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),i,b);
|
@com.google.gerrit.client.api.ActionContext::post(
|
||||||
else
|
Lcom/google/gerrit/client/rpc/RestApi;
|
||||||
@com.google.gerrit.client.api.ActionContext::post(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),i,b);
|
Ljava/lang/String;
|
||||||
|
Lcom/google/gwt/core/client/JavaScriptObject;)
|
||||||
|
(this._api(u), i, b);
|
||||||
|
} else {
|
||||||
|
@com.google.gerrit.client.api.ActionContext::post(
|
||||||
|
Lcom/google/gerrit/client/rpc/RestApi;
|
||||||
|
Lcom/google/gwt/core/client/JavaScriptObject;
|
||||||
|
Lcom/google/gwt/core/client/JavaScriptObject;)
|
||||||
|
(this._api(u), i, b);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
put: function(u,i,b) {
|
put: function(u,i,b) {
|
||||||
if (b) {
|
if (b) {
|
||||||
if(typeof i=='string')
|
if (typeof i == 'string') {
|
||||||
@com.google.gerrit.client.api.ActionContext::put(Lcom/google/gerrit/client/rpc/RestApi;Ljava/lang/String;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),i,b);
|
@com.google.gerrit.client.api.ActionContext::put(
|
||||||
else
|
Lcom/google/gerrit/client/rpc/RestApi;
|
||||||
@com.google.gerrit.client.api.ActionContext::put(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),i,b);
|
Ljava/lang/String;
|
||||||
|
Lcom/google/gwt/core/client/JavaScriptObject;)
|
||||||
|
(this._api(u), i, b);
|
||||||
} else {
|
} else {
|
||||||
@com.google.gerrit.client.api.ActionContext::put(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),i)
|
@com.google.gerrit.client.api.ActionContext::put(
|
||||||
|
Lcom/google/gerrit/client/rpc/RestApi;
|
||||||
|
Lcom/google/gwt/core/client/JavaScriptObject;
|
||||||
|
Lcom/google/gwt/core/client/JavaScriptObject;)
|
||||||
|
(this._api(u), i, b);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
@com.google.gerrit.client.api.ActionContext::put(
|
||||||
|
Lcom/google/gerrit/client/rpc/RestApi;
|
||||||
|
Lcom/google/gwt/core/client/JavaScriptObject;)
|
||||||
|
(this._api(u), i);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'delete': function(u,b){@com.google.gerrit.client.api.ActionContext::delete(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),b)},
|
'delete': function(u,b) {
|
||||||
del: function(u,b){@com.google.gerrit.client.api.ActionContext::delete(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),b)},
|
@com.google.gerrit.client.api.ActionContext::delete(
|
||||||
|
Lcom/google/gerrit/client/rpc/RestApi;
|
||||||
|
Lcom/google/gwt/core/client/JavaScriptObject;)
|
||||||
|
(this._api(u), b);
|
||||||
|
},
|
||||||
|
del: function(u,b) {
|
||||||
|
@com.google.gerrit.client.api.ActionContext::delete(
|
||||||
|
Lcom/google/gerrit/client/rpc/RestApi;
|
||||||
|
Lcom/google/gwt/core/client/JavaScriptObject;)
|
||||||
|
(this._api(u), b);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}-*/;
|
}-*/;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
// Copyright (C) 2013 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.
|
||||||
|
|
||||||
|
package com.google.gerrit.client.api;
|
||||||
|
|
||||||
|
import com.google.gwt.core.client.JavaScriptObject;
|
||||||
|
import com.google.gwt.dom.client.Document;
|
||||||
|
import com.google.gwt.dom.client.Element;
|
||||||
|
import com.google.gwt.dom.client.Node;
|
||||||
|
import com.google.gwt.dom.client.StyleInjector;
|
||||||
|
import com.google.gwt.user.client.DOM;
|
||||||
|
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
|
||||||
|
|
||||||
|
final class HtmlTemplate {
|
||||||
|
static native void init() /*-{
|
||||||
|
var ElementSet = function(r,e) {
|
||||||
|
this.root = r;
|
||||||
|
this.elements = e;
|
||||||
|
};
|
||||||
|
ElementSet.prototype = {
|
||||||
|
clear: function() {
|
||||||
|
this.root = null;
|
||||||
|
this.elements = null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
$wnd.Gerrit.css = @com.google.gerrit.client.api.HtmlTemplate::css(Ljava/lang/String;);
|
||||||
|
$wnd.Gerrit.html = function(h,r,w) {
|
||||||
|
var i = {};
|
||||||
|
if (r) {
|
||||||
|
h = h.replace(
|
||||||
|
/\sid=['"]\{([a-z_][a-z0-9_]*)\}['"]|\{([a-z0-9._-]+)\}/gi,
|
||||||
|
function(m,a,b) {
|
||||||
|
if (a)
|
||||||
|
return @com.google.gerrit.client.api.HtmlTemplate::id(
|
||||||
|
Lcom/google/gerrit/client/api/HtmlTemplate$IdMap;
|
||||||
|
Ljava/lang/String;)
|
||||||
|
(i,a);
|
||||||
|
return @com.google.gerrit.client.api.HtmlTemplate::html(
|
||||||
|
Lcom/google/gerrit/client/api/HtmlTemplate$ReplacementMap;
|
||||||
|
Ljava/lang/String;)
|
||||||
|
(r,b);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var e = @com.google.gerrit.client.api.HtmlTemplate::parseHtml(
|
||||||
|
Ljava/lang/String;Lcom/google/gerrit/client/api/HtmlTemplate$IdMap;
|
||||||
|
Lcom/google/gerrit/client/api/HtmlTemplate$ReplacementMap;
|
||||||
|
Z)
|
||||||
|
(h,i,r,!!w);
|
||||||
|
return w ? new ElementSet(e,i) : e;
|
||||||
|
};
|
||||||
|
}-*/;
|
||||||
|
|
||||||
|
private static final String css(String css) {
|
||||||
|
String name = DOM.createUniqueId();
|
||||||
|
StyleInjector.inject("." + name + "{" + css + "}");
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String id(IdMap idMap, String key) {
|
||||||
|
String id = DOM.createUniqueId();
|
||||||
|
idMap.put(id, key);
|
||||||
|
return " id='" + id + "'";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String html(ReplacementMap opts, String id) {
|
||||||
|
int d = id.indexOf('.');
|
||||||
|
if (0 < d) {
|
||||||
|
String name = id.substring(0, d);
|
||||||
|
String rest = id.substring(d + 1);
|
||||||
|
return html(opts.map(name), rest);
|
||||||
|
}
|
||||||
|
return new SafeHtmlBuilder().append(opts.str(id)).asString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Node parseHtml(
|
||||||
|
String html,
|
||||||
|
IdMap ids,
|
||||||
|
ReplacementMap opts,
|
||||||
|
boolean wantElements) {
|
||||||
|
Element div = Document.get().createDivElement();
|
||||||
|
div.setInnerHTML(html);
|
||||||
|
if (!ids.isEmpty()) {
|
||||||
|
attachHandlers(div, ids, opts, wantElements);
|
||||||
|
}
|
||||||
|
if (div.getChildCount() == 1) {
|
||||||
|
return div.getFirstChild();
|
||||||
|
}
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void attachHandlers(
|
||||||
|
Element e,
|
||||||
|
IdMap ids,
|
||||||
|
ReplacementMap opts,
|
||||||
|
boolean wantElements) {
|
||||||
|
if (e.getId() != null) {
|
||||||
|
String key = ids.get(e.getId());
|
||||||
|
if (key != null) {
|
||||||
|
ids.remove(e.getId());
|
||||||
|
if (wantElements) {
|
||||||
|
ids.put(key, e);
|
||||||
|
}
|
||||||
|
e.setId(null);
|
||||||
|
opts.map(key).attachHandlers(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Element c = e.getFirstChildElement(); c != null;) {
|
||||||
|
attachHandlers(c, ids, opts, wantElements);
|
||||||
|
c = c.getNextSiblingElement();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ReplacementMap extends JavaScriptObject {
|
||||||
|
final native ReplacementMap map(String n) /*-{ return this[n] }-*/;
|
||||||
|
final native String str(String n) /*-{ return ''+this[n] }-*/;
|
||||||
|
final native void attachHandlers(Element e) /*-{
|
||||||
|
for (var k in this) {
|
||||||
|
var f = this[k];
|
||||||
|
if (k.substring(0, 2) == 'on' && typeof f == 'function')
|
||||||
|
e[k] = f;
|
||||||
|
}
|
||||||
|
}-*/;
|
||||||
|
|
||||||
|
protected ReplacementMap() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class IdMap extends JavaScriptObject {
|
||||||
|
final native String get(String i) /*-{ return this[i] }-*/;
|
||||||
|
final native void remove(String i) /*-{ delete this[i] }-*/;
|
||||||
|
final native void put(String i, String k) /*-{ this[i] = k }-*/;
|
||||||
|
final native void put(String k, Element e) /*-{ this[k] = e }-*/;
|
||||||
|
final native boolean isEmpty() /*-{
|
||||||
|
for (var i in this)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}-*/;
|
||||||
|
|
||||||
|
protected IdMap() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private HtmlTemplate() {
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user