summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorShawn Pearce <sop@google.com>2014-03-08 00:15:16 +0000
committerGerrit Code Review <noreply-gerritcodereview@google.com>2014-03-08 00:15:17 +0000
commit2b0a393f7fe8e947d8f5a24fa91ad66a316778d9 (patch)
tree15da329c559b0984c19e6527b1924b0af812706c
parent8c392f5fa457608d5411bb0743a58da66f4e37f6 (diff)
parent210b5395aa0f4af6d5b7dfacb926326e95868038 (diff)
Merge changes Ic29a4ec0,Ic40487d3
* changes: Make it easy to construct CSS and HTML from JavaScript plugins Wrap long JSNI in ApiGlue
-rw-r--r--Documentation/js-api.txt130
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java79
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/api/HtmlTemplate.java157
3 files changed, 348 insertions, 18 deletions
diff --git a/Documentation/js-api.txt b/Documentation/js-api.txt
index 9447fe2d2d..d41eab5809 100644
--- a/Documentation/js-api.txt
+++ b/Documentation/js-api.txt
@@ -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
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()
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 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()
Registers a new plugin by invoking the supplied initialization
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java
index 9767ec49ea..4fa467ab51 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java
@@ -27,6 +27,7 @@ public class ApiGlue {
public static void init() {
init0();
ActionContext.init();
+ HtmlTemplate.init();
Plugin.init();
addHistoryHook();
}
@@ -44,9 +45,13 @@ public class ApiGlue {
project_actions: {},
getPluginName: @com.google.gerrit.client.api.ApiGlue::getPluginName(),
+ injectCss: @com.google.gwt.dom.client.StyleInjector::inject(Ljava/lang/String;),
install: function (f) {
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)},
_getPluginByUrl: function(u) {
@@ -80,26 +85,64 @@ public class ApiGlue {
return serverUrl;
},
- _api: function(u) {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){
- 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);
- 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);
+ _api: function(u) {
+ return @com.google.gerrit.client.rpc.RestApi::new(Ljava/lang/String;)(u);
},
- put: function(u,i,b){
- if(b){
- 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);
- else
- @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)
+ 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) {
+ 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);
+ } 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) {
+ if (b) {
+ 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);
+ } else {
+ @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)},
- 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)},
+ '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);
+ },
+ 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);
+ },
};
}-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/HtmlTemplate.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/HtmlTemplate.java
new file mode 100644
index 0000000000..95757abff2
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/HtmlTemplate.java
@@ -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() {
+ }
+}