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
This commit is contained in:
Shawn Pearce
2013-12-07 22:41:36 -08:00
parent df3f94cf36
commit 210b5395aa
3 changed files with 289 additions and 0 deletions

View File

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

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