summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorShawn Pearce <sop@google.com>2013-12-07 22:41:36 -0800
committerShawn Pearce <sop@google.com>2014-03-06 22:07:54 -0800
commit210b5395aa0f4af6d5b7dfacb926326e95868038 (patch)
treeca68fc2be131933b306572366d20d77a8a0e8b9f
parentdf3f94cf36ad2d418f3125c3cb43e9e58c95d775 (diff)
Make it easy to construct CSS and HTML from JavaScript plugins
Scoping CSS rules across plugins can be slightly tricky, as the namespace is global for the entire browser window. Allow plugins to create unique names using Gerrit.css() and then use those inside of HTML with class="{style.foo}" style replacements in the Gerrit.html() function. This style of development makes native HTML and CSS more natural to use inside of a plugin's JavaScript, and reduces the risks of conflicting CSS rules with core Gerrit or another plugin. Event handler registration is also supported, making it easier to attach functions to handle onclick for buttons and anchors. Handler registration avoids circular references between the function's environment and the DOM node, ensuring garbage collection works. Change-Id: Ic29a4ec0c15eedef4f4ce72031193f1896742dc5
-rw-r--r--Documentation/js-api.txt130
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java2
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/api/HtmlTemplate.java157
3 files changed, 289 insertions, 0 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 73cf498a4d..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,6 +45,7 @@ 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(
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() {
+ }
+}