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 `
` and the div is returned. + +.Signature +[source,javascript] +---- +Gerrit.html(htmlText, options, wantElements); +---- + +* htmlText: string of HTML to be parsed. A new unattached `
` 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( + 'Hello {name}!', + {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('Close', 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( + '
Name:
' + + '
Age:
' + + '', + { + 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() { + } +}