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:
Shawn Pearce
2014-03-08 00:15:16 +00:00
committed by Gerrit Code Review
3 changed files with 348 additions and 18 deletions

View File

@@ -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

View File

@@ -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);
},
}; };
}-*/; }-*/;

View File

@@ -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() {
}
}