Re-working @UiField and Widget replacement to use DOM walking rather then IDs.
Microbenchmarks show ~10% speedup in Firefox/Webkit browsers. AdWords saw little change on Firefox and potential 3% improvement in startup time on chrome.
Some additional constraints on UiBinders:
+ To simplify handling of paragraph, we disallow block-level fields and inside of <p> elements.
+ TD tags MUST be withing a TR to simplify DOM walking.
Review at http://gwt-code-reviews.appspot.com/241801
Review by: rjrjr@google.com
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@7816 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/tools/api-checker/config/gwt20_21userApi.conf b/tools/api-checker/config/gwt20_21userApi.conf
index 1959dcd..399a5fb 100644
--- a/tools/api-checker/config/gwt20_21userApi.conf
+++ b/tools/api-checker/config/gwt20_21userApi.conf
@@ -43,6 +43,7 @@
:com/google/gwt/rpc/client/impl/ClientWriterFactory.java\
:com/google/gwt/rpc/client/impl/EscapeUtil.java\
:com/google/gwt/rpc/linker/*.java\
+:com/google/gwt/uibinder/client/UiBinderUtil.java\
:com/google/gwt/uibinder/attributeparsers/*.java\
:com/google/gwt/uibinder/elementparsers/*.java\
:com/google/gwt/uibinder/testing/*.java\
diff --git a/user/src/com/google/gwt/dom/client/DOMImpl.java b/user/src/com/google/gwt/dom/client/DOMImpl.java
index dc61ea2..da3117a 100644
--- a/user/src/com/google/gwt/dom/client/DOMImpl.java
+++ b/user/src/com/google/gwt/dom/client/DOMImpl.java
@@ -240,6 +240,23 @@
return node.nodeType;
}-*/;
+ /**
+ * Get the child of a parent ignoring all text nodes that might be children of the parent.
+ * Returns null if the index is out of bounds.
+ */
+ public Node getNonTextChild(Node parent, int index) {
+ int i = 0;
+ for (Node child = parent.getFirstChild(); child != null; child = child.getNextSibling()) {
+ if (child.getNodeType() == Node.ELEMENT_NODE) {
+ if (i == index) {
+ return child.cast();
+ }
+ i++;
+ }
+ }
+ return null;
+ }
+
public native Element getParentElement(Node node) /*-{
var parent = node.parentNode;
if (!parent || parent.nodeType != 1) {
@@ -361,4 +378,6 @@
public native String toString(Element elem) /*-{
return elem.outerHTML;
}-*/;
+
+
}
diff --git a/user/src/com/google/gwt/dom/client/DOMImplIE6.java b/user/src/com/google/gwt/dom/client/DOMImplIE6.java
index 843f931..2f75ce9 100644
--- a/user/src/com/google/gwt/dom/client/DOMImplIE6.java
+++ b/user/src/com/google/gwt/dom/client/DOMImplIE6.java
@@ -24,6 +24,8 @@
private static boolean isIE6;
private static boolean isIE6Detected;
+
+
/**
* Check if the browser is IE6 or IE7.
*
@@ -84,6 +86,11 @@
/ getZoomMultiple(doc) + doc.getScrollTop());
}
+ @Override
+ public native Node getNonTextChild(Node parent, int index) /*-{
+ return parent.children[index];
+ }-*/;
+
@Override
public int getScrollLeft(Element elem) {
if (isRTL(elem)) {
diff --git a/user/src/com/google/gwt/dom/client/Element.java b/user/src/com/google/gwt/dom/client/Element.java
index d3b3692..b157a94 100644
--- a/user/src/com/google/gwt/dom/client/Element.java
+++ b/user/src/com/google/gwt/dom/client/Element.java
@@ -719,4 +719,6 @@
// on some browsers.
this.title = title || '';
}-*/;
+
+
}
diff --git a/user/src/com/google/gwt/dom/client/Node.java b/user/src/com/google/gwt/dom/client/Node.java
index 44ff0c8..aea2ee1 100644
--- a/user/src/com/google/gwt/dom/client/Node.java
+++ b/user/src/com/google/gwt/dom/client/Node.java
@@ -168,6 +168,16 @@
}-*/;
/**
+ * Get the child by index ignoring all text node children.
+ * Returns null if the index is out of bounds.
+ *
+ * @param index The child index ignoring text node children
+ */
+ public final Node getNonTextChild(int index) {
+ return DOMImpl.impl.getNonTextChild(this, index);
+ }
+
+ /**
* The Document object associated with this node. This is also the
* {@link Document} object used to create new nodes.
*/
@@ -183,7 +193,7 @@
public final Element getParentElement() {
return DOMImpl.impl.getParentElement(this);
}
-
+
/**
* The parent of this node. All nodes except Document may have a parent.
* However, if a node has just been created and not yet added to the tree, or
diff --git a/user/src/com/google/gwt/uibinder/client/UiBinderUtil.java b/user/src/com/google/gwt/uibinder/client/UiBinderUtil.java
index 4b8c1ed..b6487f3 100644
--- a/user/src/com/google/gwt/uibinder/client/UiBinderUtil.java
+++ b/user/src/com/google/gwt/uibinder/client/UiBinderUtil.java
@@ -18,67 +18,14 @@
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.user.client.ui.RootPanel;
-import com.google.gwt.user.client.ui.UIObject;
/**
* Static helper methods used by UiBinder. These methods are likely to move,
* so please don't use them for non-UiBinder code.
*/
public class UiBinderUtil {
- /**
- * Temporary attachment record that keeps track of where an element was
- * before attachment. Use the detach method to put things back.
- *
- */
- public static class TempAttachment {
- private final Element element;
- private final Element origParent;
- private final Element origSibling;
-
- private TempAttachment(Element origParent, Element origSibling,
- Element element) {
- this.origParent = origParent;
- this.origSibling = origSibling;
- this.element = element;
- }
-
- /**
- * Restore to previous DOM state before attachment.
- */
- public void detach() {
- // Put the panel's element back where it was.
- if (origParent != null) {
- origParent.insertBefore(element, origSibling);
- } else {
- orphan(element);
- }
- }
- }
private static Element hiddenDiv;
-
- /**
- * Attaches the element to the dom temporarily. Keeps track of where it is
- * attached so that things can be put back latter.
- *
- * @return attachment record which can be used for reverting back to previous
- * DOM state
- */
- public static TempAttachment attachToDom(Element element) {
- // TODO(rjrjr) This is copied from HTMLPanel. Reconcile
- ensureHiddenDiv();
-
- // Hang on to the panel's original parent and sibling elements so that it
- // can be replaced.
- Element origParent = element.getParentElement();
- Element origSibling = element.getNextSiblingElement();
-
- // Attach the panel's element to the hidden div.
- hiddenDiv.appendChild(element);
-
- return new TempAttachment(origParent, origSibling, element);
- }
public static Element fromHtml(String html) {
ensureHiddenDiv();
@@ -87,13 +34,57 @@
orphan(newbie);
return newbie;
}
-
+
+ public static Node getChild(Node node, int child) {
+ return node.getChild(child);
+ }
+
+ public static Node getNonTextChild(Node node, int child) {
+ return node.getNonTextChild(child);
+ }
+
+ public static Node getTableChild(Node node, int child) {
+ // If the table, has a tbody inside...
+ Element table = (Element)node;
+ Element firstChild = table.getNonTextChild(0).cast();
+ if ("tbody".equalsIgnoreCase(firstChild.getTagName())) {
+ return firstChild.getNonTextChild(child);
+ } else {
+ return table.getNonTextChild(child);
+ }
+ }
+
+ public static native Element lookupNodeByTreeIndicies(Element parent, String query,
+ String xpath) /*-{
+ if (parent.querySelector) {
+ return parent.querySelector(query);
+ } else {
+ return parent.ownerDocument.evaluate(
+ xpath, parent, null, XPathResult.ANY_TYPE, null).iterateNext();
+ }
+ }-*/;
+
+ public static native Element lookupNodeByTreeIndiciesIE(Element parent, int[] indicies) /*-{
+ var currentNode = parent;
+ for(var i = 0; i < indicies.length; i = i + 1) {
+ currentNode = currentNode.children[indicies[i]];
+ }
+ return currentNode;
+}-*/;
+
+ public static native Element lookupNodeByTreeIndiciesUsingQuery(Element parent, String query) /*-{
+ return parent.querySelector(query);
+ }-*/;
+
+ public static native Element lookupNodeByTreeIndiciesUsingXpath(Element parent, String xpath) /*-{
+ return parent.ownerDocument.evaluate(
+ xpath, parent, null, XPathResult.ANY_TYPE, null).iterateNext();
+ }-*/;
+
private static void ensureHiddenDiv() {
// If the hidden DIV has not been created, create it.
if (hiddenDiv == null) {
- hiddenDiv = Document.get().createDivElement();
- UIObject.setVisible(hiddenDiv, false);
- RootPanel.getBodyElement().appendChild(hiddenDiv);
+ hiddenDiv = Document.get().createDivElement();
}
}
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/CustomButtonParser.java b/user/src/com/google/gwt/uibinder/elementparsers/CustomButtonParser.java
index 5fade12..774e8c7 100644
--- a/user/src/com/google/gwt/uibinder/elementparsers/CustomButtonParser.java
+++ b/user/src/com/google/gwt/uibinder/elementparsers/CustomButtonParser.java
@@ -17,6 +17,7 @@
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.uibinder.rebind.DomCursor;
import com.google.gwt.uibinder.rebind.UiBinderWriter;
import com.google.gwt.uibinder.rebind.XMLElement;
import com.google.gwt.uibinder.rebind.XMLElement.Interpreter;
@@ -67,7 +68,9 @@
HtmlInterpreter interpreter = HtmlInterpreter.newInterpreterForUiObject(
writer, fieldName);
- String innerHtml = child.consumeInnerHtml(interpreter);
+ DomCursor cursor = writer.beginDomSection(fieldName + ".getElement()");
+ String innerHtml = child.consumeInnerHtml(interpreter, cursor);
+ writer.endDomSection();
if (innerHtml.length() > 0) {
writer.addStatement("%s.%s().setHTML(\"%s\");", fieldName,
faceNameGetter(faceName), innerHtml);
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/DialogBoxParser.java b/user/src/com/google/gwt/uibinder/elementparsers/DialogBoxParser.java
index 00dfd26..36e2d54 100644
--- a/user/src/com/google/gwt/uibinder/elementparsers/DialogBoxParser.java
+++ b/user/src/com/google/gwt/uibinder/elementparsers/DialogBoxParser.java
@@ -17,6 +17,7 @@
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.uibinder.rebind.DomCursor;
import com.google.gwt.uibinder.rebind.UiBinderWriter;
import com.google.gwt.uibinder.rebind.XMLElement;
import com.google.gwt.user.client.ui.DialogBox;
@@ -40,7 +41,9 @@
HtmlInterpreter interpreter = HtmlInterpreter.newInterpreterForUiObject(
writer, fieldName);
- caption = child.consumeInnerHtml(interpreter);
+ DomCursor cursor = writer.beginDomSection(fieldName + ".getElement()");
+ caption = child.consumeInnerHtml(interpreter, cursor);
+ writer.endDomSection();
} else {
if (body != null) {
writer.die("In %s, may have only one widget, but found %s and %s",
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/DomElementParser.java b/user/src/com/google/gwt/uibinder/elementparsers/DomElementParser.java
index 95d9745..0bdc543 100644
--- a/user/src/com/google/gwt/uibinder/elementparsers/DomElementParser.java
+++ b/user/src/com/google/gwt/uibinder/elementparsers/DomElementParser.java
@@ -17,6 +17,7 @@
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.uibinder.rebind.DomCursor;
import com.google.gwt.uibinder.rebind.UiBinderWriter;
import com.google.gwt.uibinder.rebind.XMLElement;
@@ -35,10 +36,12 @@
interpreter.interpretElement(elem);
- writer.beginAttachedSection(fieldName);
- String html = elem.consumeOpeningTag() + elem.consumeInnerHtml(interpreter)
+ DomCursor cursor = writer.beginDomSection(fieldName);
+
+ String html = elem.consumeOpeningTag() + elem.consumeInnerHtml(interpreter,
+ cursor)
+ elem.getClosingTag();
- writer.endAttachedSection();
+ writer.endDomSection();
writer.setFieldInitializer(fieldName, String.format(
"(%1$s) UiBinderUtil.fromHtml(\"%2$s\")",
type.getQualifiedSourceName(), html));
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/FieldInterpreter.java b/user/src/com/google/gwt/uibinder/elementparsers/FieldInterpreter.java
index 8c2f3c1..71a6d18 100644
--- a/user/src/com/google/gwt/uibinder/elementparsers/FieldInterpreter.java
+++ b/user/src/com/google/gwt/uibinder/elementparsers/FieldInterpreter.java
@@ -37,17 +37,8 @@
throws UnableToCompleteException {
String fieldName = writer.declareFieldIfNeeded(elem);
if (fieldName != null) {
- String token = writer.declareDomField(fieldName, element);
-
- if (elem.hasAttribute("id")) {
- writer.die(String.format(
- "Cannot declare id=\"%s\" and %s=\"%s\" on the same element",
- elem.consumeRawAttribute("id"), writer.getUiFieldAttributeName(),
- fieldName));
- }
-
- elem.setAttribute("id", token);
- }
+ writer.declareDomField(fieldName, elem.getLocalName());
+ }
/*
* Return null because we don't want to replace the dom element with any
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/HTMLPanelParser.java b/user/src/com/google/gwt/uibinder/elementparsers/HTMLPanelParser.java
index 83d65f7..e8d0991 100644
--- a/user/src/com/google/gwt/uibinder/elementparsers/HTMLPanelParser.java
+++ b/user/src/com/google/gwt/uibinder/elementparsers/HTMLPanelParser.java
@@ -18,6 +18,7 @@
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.uibinder.elementparsers.HtmlMessageInterpreter.PlaceholderInterpreterProvider;
+import com.google.gwt.uibinder.rebind.DomCursor;
import com.google.gwt.uibinder.rebind.UiBinderWriter;
import com.google.gwt.uibinder.rebind.XMLElement;
import com.google.gwt.uibinder.rebind.messages.MessageWriter;
@@ -45,10 +46,10 @@
*/
HtmlInterpreter htmlInterpreter = makeHtmlInterpreter(fieldName, writer);
- writer.beginAttachedSection(fieldName + ".getElement()");
+ DomCursor cursor = writer.beginDomSection(fieldName + ".getElement()");
String html = elem.consumeInnerHtml(InterpreterPipe.newPipe(
- widgetInterpreter, htmlInterpreter));
- writer.endAttachedSection();
+ widgetInterpreter, htmlInterpreter), cursor);
+ writer.endDomSection();
/*
* HTMLPanel has no no-arg ctor, so we have to generate our own, using the
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/HasHTMLParser.java b/user/src/com/google/gwt/uibinder/elementparsers/HasHTMLParser.java
index 601c03d..80cdda9 100644
--- a/user/src/com/google/gwt/uibinder/elementparsers/HasHTMLParser.java
+++ b/user/src/com/google/gwt/uibinder/elementparsers/HasHTMLParser.java
@@ -17,6 +17,7 @@
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.uibinder.rebind.DomCursor;
import com.google.gwt.uibinder.rebind.UiBinderWriter;
import com.google.gwt.uibinder.rebind.XMLElement;
@@ -28,11 +29,12 @@
public void parse(XMLElement elem, String fieldName, JClassType type,
UiBinderWriter writer) throws UnableToCompleteException {
+ writer.addInitComment("HasHtmlParser.parse");
HtmlInterpreter interpreter =
HtmlInterpreter.newInterpreterForUiObject(writer, fieldName);
- writer.beginAttachedSection(fieldName + ".getElement()");
- String html = elem.consumeInnerHtml(interpreter);
- writer.endAttachedSection();
+ DomCursor cursor = writer.beginDomSection(fieldName + ".getElement()");
+ String html = elem.consumeInnerHtml(interpreter, cursor);
+ writer.endDomSection();
// TODO(jgw): throw an error if there's a conflicting 'html' attribute.
if (html.trim().length() > 0) {
writer.genStringPropertySet(fieldName, "HTML", html);
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/HtmlMessageInterpreter.java b/user/src/com/google/gwt/uibinder/elementparsers/HtmlMessageInterpreter.java
index f21ac6d..24555fb 100644
--- a/user/src/com/google/gwt/uibinder/elementparsers/HtmlMessageInterpreter.java
+++ b/user/src/com/google/gwt/uibinder/elementparsers/HtmlMessageInterpreter.java
@@ -73,7 +73,8 @@
}
MessageWriter message = messages.newMessage(elem);
- message.setDefaultMessage(elem.consumeInnerHtml(phiProvider.get(message)));
+ message.setDefaultMessage(elem.consumeInnerHtml(phiProvider.get(message),
+ uiWriter.getCurrentDomCursor()));
return uiWriter.tokenForExpression(messages.declareMessage(message));
}
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/HtmlPlaceholderInterpreter.java b/user/src/com/google/gwt/uibinder/elementparsers/HtmlPlaceholderInterpreter.java
index 0819505..3966680 100644
--- a/user/src/com/google/gwt/uibinder/elementparsers/HtmlPlaceholderInterpreter.java
+++ b/user/src/com/google/gwt/uibinder/elementparsers/HtmlPlaceholderInterpreter.java
@@ -16,6 +16,7 @@
package com.google.gwt.uibinder.elementparsers;
import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.uibinder.rebind.DomCursor;
import com.google.gwt.uibinder.rebind.UiBinderWriter;
import com.google.gwt.uibinder.rebind.XMLElement;
import com.google.gwt.uibinder.rebind.messages.MessageWriter;
@@ -64,12 +65,15 @@
* This recursive innerHtml call has already been escaped. Hide it in a
* token to avoid double escaping
*/
- String body = tokenator.nextToken(elem.consumeInnerHtml(this));
-
+ DomCursor currentDomCursor = uiWriter.getCurrentDomCursor();
+ String body = tokenator.nextToken(elem.consumeInnerHtml(this,
+ currentDomCursor));
+
String closeTag = elem.getClosingTag();
String closePlaceholder =
nextPlaceholder(name + "End", closeTag, closeTag);
+ currentDomCursor.advanceChild();
return openPlaceholder + body + closePlaceholder;
}
@@ -79,7 +83,8 @@
@Override
protected String consumePlaceholderInnards(XMLElement elem)
throws UnableToCompleteException {
- return elem.consumeInnerHtml(fieldAndComputed);
+ return elem.consumeInnerHtml(fieldAndComputed,
+ uiWriter.getCurrentDomCursor());
}
/**
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParser.java b/user/src/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParser.java
index 7feb8ad..43384ad 100644
--- a/user/src/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParser.java
+++ b/user/src/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParser.java
@@ -19,6 +19,7 @@
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JEnumType;
import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.uibinder.rebind.DomCursor;
import com.google.gwt.uibinder.rebind.UiBinderWriter;
import com.google.gwt.uibinder.rebind.XMLElement;
import com.google.gwt.user.client.ui.StackLayoutPanel;
@@ -75,7 +76,9 @@
HtmlInterpreter htmlInt = HtmlInterpreter.newInterpreterForUiObject(
writer, fieldName);
String size = children.header.consumeRequiredDoubleAttribute("size");
- String html = children.header.consumeInnerHtml(htmlInt);
+ DomCursor cursor = writer.beginDomSection(fieldName + ".getElement()");
+ String html = children.header.consumeInnerHtml(htmlInt, cursor);
+ writer.endDomSection();
writer.addStatement("%s.add(%s, \"%s\", true, %s);", fieldName,
childFieldName, html, size);
} else if (children.customHeader != null) {
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParser.java b/user/src/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParser.java
index a6880a9..cb3aceb 100644
--- a/user/src/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParser.java
+++ b/user/src/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParser.java
@@ -19,6 +19,7 @@
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JEnumType;
import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.uibinder.rebind.DomCursor;
import com.google.gwt.uibinder.rebind.UiBinderWriter;
import com.google.gwt.uibinder.rebind.XMLElement;
import com.google.gwt.user.client.ui.TabLayoutPanel;
@@ -80,7 +81,9 @@
if (children.header != null) {
HtmlInterpreter htmlInt = HtmlInterpreter.newInterpreterForUiObject(
writer, fieldName);
- String html = children.header.consumeInnerHtml(htmlInt);
+ DomCursor cursor = writer.beginDomSection(fieldName + ".getElement()");
+ String html = children.header.consumeInnerHtml(htmlInt, cursor);
+ writer.endDomSection();
writer.addStatement("%s.add(%s, \"%s\", true);", fieldName,
childFieldName, html);
} else if (children.customHeader != null) {
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/TabPanelParser.java b/user/src/com/google/gwt/uibinder/elementparsers/TabPanelParser.java
index 627512b..45902b8 100644
--- a/user/src/com/google/gwt/uibinder/elementparsers/TabPanelParser.java
+++ b/user/src/com/google/gwt/uibinder/elementparsers/TabPanelParser.java
@@ -17,6 +17,7 @@
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.uibinder.rebind.DomCursor;
import com.google.gwt.uibinder.rebind.UiBinderWriter;
import com.google.gwt.uibinder.rebind.XMLElement;
@@ -59,7 +60,9 @@
if (tabChild.getLocalName().equals(TAG_TABHTML)) {
HtmlInterpreter interpreter = HtmlInterpreter.newInterpreterForUiObject(
writer, fieldName);
- tabHTML = tabChild.consumeInnerHtml(interpreter);
+ DomCursor cursor = writer.beginDomSection(fieldName + ".getElement()");
+ tabHTML = tabChild.consumeInnerHtml(interpreter, cursor);
+ writer.endDomSection();
} else {
if (childFieldName != null) {
writer.die("%s may only have a single child widget", child);
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/WidgetInterpreter.java b/user/src/com/google/gwt/uibinder/elementparsers/WidgetInterpreter.java
index 686cb2a..6edc8e4 100644
--- a/user/src/com/google/gwt/uibinder/elementparsers/WidgetInterpreter.java
+++ b/user/src/com/google/gwt/uibinder/elementparsers/WidgetInterpreter.java
@@ -68,30 +68,26 @@
public String interpretElement(XMLElement elem)
throws UnableToCompleteException {
- if (uiWriter.isWidgetElement(elem)) {
- // Allocate a local variable to hold the dom id for this widget. Note
- // that idHolder is a local variable reference, not a string id. We
- // have to generate the ids at runtime, not compile time, or else
- // we'll reuse ids for any template rendered more than once.
- String idHolder = uiWriter.declareDomIdHolder();
- String childField = uiWriter.parseElementToField(elem);
- uiWriter.ensureFieldAttached(fieldName);
+ if (uiWriter.isWidgetElement(elem)) {
- String elementPointer = idHolder + "Element";
+ String tag = getLegalPlaceholderTag(elem);
+
+ String childField = uiWriter.parseElementToField(elem);
+
+ String elementPointer = "element" + uiWriter.getUniqueId();
uiWriter.addInitStatement(
- "com.google.gwt.user.client.Element %s = " +
- "com.google.gwt.dom.client.Document.get().getElementById(%s).cast();",
- elementPointer, idHolder);
- // Delay replacing the placeholders with the widgets until after
- // detachment so as not to end up attaching the widget to the DOM
- // unnecessarily
- uiWriter.addDetachStatement(
+ "com.google.gwt.user.client.Element %s = %s;",
+ elementPointer, uiWriter.getDomAccessExpression(elementPointer, tag));
+
+ uiWriter.addInitStatement(
"%1$s.addAndReplaceElement(%2$s, %3$s);",
fieldName, childField, elementPointer);
+
+ // Increment DOM cursor based on the tag we are adding.
+ uiWriter.getCurrentDomCursor().advanceChild();
// Create an element to hold the widget.
- String tag = getLegalPlaceholderTag(elem);
- return "<" + tag + " id='\" + " + idHolder + " + \"'></" + tag + ">";
+ return "<" + tag + "></" + tag + ">";
}
return null;
}
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/WidgetPlaceholderInterpreter.java b/user/src/com/google/gwt/uibinder/elementparsers/WidgetPlaceholderInterpreter.java
index 4080258..23a65a1 100644
--- a/user/src/com/google/gwt/uibinder/elementparsers/WidgetPlaceholderInterpreter.java
+++ b/user/src/com/google/gwt/uibinder/elementparsers/WidgetPlaceholderInterpreter.java
@@ -18,6 +18,7 @@
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
+import com.google.gwt.uibinder.rebind.DomCursor;
import com.google.gwt.uibinder.rebind.UiBinderWriter;
import com.google.gwt.uibinder.rebind.XMLElement;
import com.google.gwt.uibinder.rebind.messages.MessageWriter;
@@ -61,10 +62,10 @@
private int serial = 0;
private final String ancestorExpression;
private final String fieldName;
- private final Map<String, XMLElement> idToWidgetElement =
- new HashMap<String, XMLElement>();
- private final Set<String> idIsHasHTML = new HashSet<String>();
- private final Set<String> idIsHasText = new HashSet<String>();
+ private final Map<String, XMLElement> elementHolderToWidgetElement =
+ new HashMap<String, XMLElement>();
+ private final Set<String> elementIsHasHTML = new HashSet<String>();
+ private final Set<String> elementIsHasText = new HashSet<String>();
WidgetPlaceholderInterpreter(String fieldName, UiBinderWriter writer,
MessageWriter message, String ancestorExpression) {
@@ -81,6 +82,7 @@
return super.interpretElement(elem);
}
+ uiWriter.addInitComment("WidgetPlaceholderInterpreter.interpretElement");
JClassType type = uiWriter.findFieldType(elem);
TypeOracle oracle = uiWriter.getOracle();
@@ -90,39 +92,46 @@
name = "widget" + (++serial);
}
- String idHolder = uiWriter.declareDomIdHolder();
- idToWidgetElement.put(idHolder, elem);
+ DomCursor cursor = uiWriter.getCurrentDomCursor();
+ String elementHolder = cursor.getAccessExpression();
+ // We are going to generate another element right here, so we advance the child pointer.
+ cursor.advanceChild();
+ elementHolderToWidgetElement.put(elementHolder, elem);
if (oracle.findType(HasHTML.class.getName()).isAssignableFrom(type)) {
- return handleHasHTMLPlaceholder(elem, name, idHolder);
+ return handleHasHTMLPlaceholder(elem, name, elementHolder);
}
if (oracle.findType(HasText.class.getName()).isAssignableFrom(type)) {
- return handleHasTextPlaceholder(elem, name, idHolder);
+ return handleHasTextPlaceholder(elem, name, elementHolder);
}
- return handleOpaqueWidgetPlaceholder(name, idHolder);
+ return handleOpaqueWidgetPlaceholder(name);
}
+
/**
* Called by {@link XMLElement#consumeInnerHtml} after all elements
* have been handed to {@link #interpretElement}.
*/
@Override
public String postProcess(String consumed) throws UnableToCompleteException {
- for (String idHolder : idToWidgetElement.keySet()) {
- XMLElement childElem = idToWidgetElement.get(idHolder);
+ for (Map.Entry<String, XMLElement> entry : elementHolderToWidgetElement.entrySet()) {
+ String element = entry.getKey();
+ XMLElement childElem = entry.getValue();
String childField = uiWriter.parseElementToField(childElem);
- genSetWidgetTextCall(idHolder, childField);
- uiWriter.addInitStatement("%1$s.addAndReplaceElement(%2$s, %3$s);",
- fieldName, childField, idHolder);
+ genSetWidgetTextCall(element, childField);
+
+ uiWriter.addInitStatement("%1$s.addAndReplaceElement(%2$s, " +
+ "(com.google.gwt.user.client.Element)%3$s);",
+ fieldName, childField, element);
}
/*
* We get used recursively, so this will be called again. Empty the map
* or else we'll re-register things.
*/
- idToWidgetElement.clear();
+ elementHolderToWidgetElement.clear();
return super.postProcess(consumed);
}
@@ -132,34 +141,36 @@
return closePlaceholder;
}
- private String genOpenTag(String name, String idHolder) {
- String openTag = String.format("<span id='\" + %s + \"'>", idHolder);
+ private String genOpenTag(String name) {
+ String openTag = "<span>";
String openPlaceholder =
nextPlaceholder(name + "Begin", "<span>", openTag);
return openPlaceholder;
}
- private void genSetWidgetTextCall(String idHolder, String childField) {
- if (idIsHasText.contains(idHolder)) {
+ private void genSetWidgetTextCall(String elementHolder, String childField) {
+ if (elementIsHasText.contains(elementHolder)) {
uiWriter.addInitStatement(
- "%s.setText(%s.getElementById(%s).getInnerText());", childField,
- fieldName, idHolder);
+ "%s.setText(((com.google.gwt.user.client.Element)%s).getInnerText());",
+ childField, elementHolder);
}
- if (idIsHasHTML.contains(idHolder)) {
+ if (elementIsHasHTML.contains(elementHolder)) {
uiWriter.addInitStatement(
- "%s.setHTML(%s.getElementById(%s).getInnerHTML());", childField,
- fieldName, idHolder);
+ "%s.setHTML(((com.google.gwt.user.client.Element)%s).getInnerHTML());",
+ childField, elementHolder);
}
}
private String handleHasHTMLPlaceholder(XMLElement elem, String name,
- String idHolder) throws UnableToCompleteException {
- idIsHasHTML.add(idHolder);
- String openPlaceholder = genOpenTag(name, idHolder);
+ String elementHolder) throws UnableToCompleteException {
+ elementIsHasHTML.add(elementHolder);
+ String openPlaceholder = genOpenTag(name);
+ DomCursor cursor = uiWriter.beginDomSection(elementHolder);
String body =
elem.consumeInnerHtml(new HtmlPlaceholderInterpreter(uiWriter,
- message, ancestorExpression));
+ message, ancestorExpression), cursor);
+ uiWriter.endDomSection();
String bodyToken = tokenator.nextToken(body);
String closePlaceholder = genCloseTag(name);
@@ -167,9 +178,9 @@
}
private String handleHasTextPlaceholder(XMLElement elem, String name,
- String idHolder) throws UnableToCompleteException {
- idIsHasText.add(idHolder);
- String openPlaceholder = genOpenTag(name, idHolder);
+ String elementHolder) throws UnableToCompleteException {
+ elementIsHasText.add(elementHolder);
+ String openPlaceholder = genOpenTag(name);
String body =
elem.consumeInnerText(new TextPlaceholderInterpreter(uiWriter,
@@ -180,8 +191,8 @@
return openPlaceholder + bodyToken + closePlaceholder;
}
- private String handleOpaqueWidgetPlaceholder(String name, String idHolder) {
- String tag = String.format("<span id='\" + %s + \"'></span>", idHolder);
+ private String handleOpaqueWidgetPlaceholder(String name) {
+ String tag = "<span></span>";
String placeholder = nextPlaceholder(name, "<span></span>", tag);
return placeholder;
}
diff --git a/user/src/com/google/gwt/uibinder/rebind/DomCursor.java b/user/src/com/google/gwt/uibinder/rebind/DomCursor.java
new file mode 100644
index 0000000..76e3f00
--- /dev/null
+++ b/user/src/com/google/gwt/uibinder/rebind/DomCursor.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright 2008 Google Inc.
+ *
+ * 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.gwt.uibinder.rebind;
+
+import com.google.gwt.core.ext.UnableToCompleteException;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * DOM Cursor keeps track of a given path in the DOM. This is useful for
+ * plucking out nodes from a DOM tree.
+ */
+public class DomCursor {
+
+ private static class PathComponent {
+
+ private int childIndex;
+ private boolean isTableWithoutTbody;
+
+ public PathComponent(XMLElement element) {
+ this.childIndex = 0;
+ this.isTableWithoutTbody = "table".equalsIgnoreCase(element.getLocalName());
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof PathComponent)) return false;
+ PathComponent other = (PathComponent) obj;
+ if (childIndex != other.childIndex) return false;
+ if (isTableWithoutTbody != other.isTableWithoutTbody) return false;
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + childIndex;
+ result = prime * result + (isTableWithoutTbody ? 1231 : 1237);
+ return result;
+ }
+
+ public void incrementIndex() {
+ childIndex++;
+ }
+ }
+
+ private class ParagraphTracking {
+ private LinkedList<Boolean> paragraphUnSafeNodes = new LinkedList<Boolean>();
+
+ public void beginUnsafeTag() {
+ paragraphUnSafeNodes.addLast(true);
+ }
+
+ public void endUnsafeTag() {
+ paragraphUnSafeNodes.removeLast();
+ }
+
+ public boolean isSafeForField() {
+ return paragraphUnSafeNodes.isEmpty();
+ }
+ }
+
+ // Tags that cause P tags to end weirdly violating XHTML
+ // http://dev.w3.org/html5/spec/Overview.html#parsing-main-inbody
+ private static final Set<String> PARAGRAPH_UNSAFE_NODES = new HashSet<String>(Arrays.asList(
+ new String[] {"address", "article", "aside", "blockquote", "center", "details", "dir", "div",
+ "dl", "fieldset", "figure", "footer", "header", "hgroup", "menu", "nav", "ol", "p", "section",
+ "ul"}));
+ private static final Set<String> TABLE_SECTIONS = new HashSet<String>(Arrays.asList(new String[] {
+ "thead", "tbody", "tfoot"}));
+ private static final String NON_TEXT_LOOKUP_METHOD = "UiBinderUtil.getNonTextChild";
+ private static final String STANDARD_LOOKUP_METHOD = "UiBinderUtil.getChild";
+ private static final String TABLE_NON_TEXT_LOOKUP_METHOD = "UiBinderUtil.getTableChild";
+
+ // Cache of path to a local variable that contains that.
+ private Map<LinkedList<PathComponent>, String> domPathCache =
+ new HashMap<LinkedList<PathComponent>, String>();
+ private LinkedList<ParagraphTracking> paragraphs = new LinkedList<ParagraphTracking>();
+ private final String parent;
+ private LinkedList<PathComponent> pathComponents = new LinkedList<PathComponent>();
+ private boolean preserveWhitespaceNodes = true;
+ private boolean walkingTextChildNodes = false;
+ private final UiBinderWriter writer;
+
+ public DomCursor(String parent, UiBinderWriter writer) {
+ this.parent = parent;
+ this.writer = writer;
+ }
+
+ public void advanceChild() {
+ pathComponents.getLast().incrementIndex();
+ writer.addInitComment("advance DomCursor %s", this.toString());
+ }
+
+ public void advanceChildForWhitespaceText() {
+ if (preserveWhitespaceNodes) {
+ advanceTextChild();
+ }
+ }
+
+ public void advanceTextChild() {
+ if (walkingTextChildNodes) {
+ advanceChild();
+ }
+ }
+
+ /**
+ * Finish visiting this subtree of the DOM.
+ */
+ public void finishChild(XMLElement elem) {
+ if (!writer.getMessages().isMessage(elem) &&
+ !isPlaceholderElement(elem)) {
+ pathComponents.removeLast();
+ writer.addInitComment("finish DomCursor %s", this.toString());
+ String tag = elem.getLocalName();
+ if ("p".equalsIgnoreCase(tag)) {
+ endParagraph();
+ }
+ if (isInsideParagraph() && isUnsafeParagraphTag(tag)) {
+ paragraphs.getLast().endUnsafeTag();
+ }
+ }
+ }
+
+
+ public String getAccessExpression() throws UnableToCompleteException {
+ return getAccessExpression(null, null);
+ }
+
+ /**
+ * Returns a Java expression for referencing the given node.
+ *
+ * @param localVar Optional variable that will be used to cache the result of the expression.
+ * If there is no applicable local variable, callers can pass null.
+ *
+ * @param tag Optional tag name of the tag that would be referenced. This can be null.
+ * @return Java access expression.
+ * @throws UnableToCompleteException
+ */
+ public String getAccessExpression(String localVar, String tag)
+ throws UnableToCompleteException {
+ // P tags cause all sorts of problems with hierarchy as the HTML spec has lots of weird
+ // semantics for block elements inside of P tags.
+ if (!safeForExpression()) {
+ writer.die("UiBinder no longer allows certain addressable " +
+ "elements inside of <p> tags because of browser " +
+ "inconsistency, consider using DIV instead");
+ }
+
+ String result = getDomWalkAccessExpression(tag);
+ if (localVar != null) {
+ domPathCache.put(new LinkedList<PathComponent>(pathComponents), localVar);
+ return result;
+ }
+
+ String varName = "intermediate" + writer.getUniqueId();
+ writer.addInitStatement("com.google.gwt.dom.client.Node %s = %s;", varName, result);
+ domPathCache.put(new LinkedList<PathComponent>(pathComponents), varName);
+ return varName;
+ }
+
+ /**
+ * Visit a child subtree of the DOM.
+ * @throws UnableToCompleteException
+ */
+ public void visitChild(XMLElement elem) throws UnableToCompleteException {
+ if (!writer.getMessages().isMessage(elem) &&
+ !isPlaceholderElement(elem)) {
+ // If we do see an actual tbody in the uibinder, we should stop trying to account for the
+ // automatic one that the browser will insert.
+ String tag = elem.getLocalName();
+ if ("tbody".equalsIgnoreCase(tag)) {
+ pathComponents.getLast().isTableWithoutTbody = false;
+ }
+ if ("td".equalsIgnoreCase(tag) && !"tr".equalsIgnoreCase(elem.getParent().getLocalName())) {
+ writer.die("TD tags must be inside of a TR tag");
+ }
+ pathComponents.addLast(new PathComponent(elem));
+ writer.addInitComment("visit DomCursor %s", this.toString());
+
+ if (isInsideParagraph() && isUnsafeParagraphTag(tag)) {
+ paragraphs.getLast().beginUnsafeTag();
+ }
+ if ("p".equalsIgnoreCase(tag)) {
+ beginParagraph();
+ }
+ }
+ }
+
+ private void beginParagraph() {
+ paragraphs.addLast(new ParagraphTracking());
+ }
+
+ private void endParagraph() {
+ paragraphs.removeLast();
+ }
+
+ private String getDomWalkAccessExpression(String tag) {
+ writer.addInitComment("DomWalkAccess %s", this.toString());
+
+ // First look and see if we have any part of the path in our variable cache.
+ int end = pathComponents.size();
+ String var = null;
+ for (; end > 0; --end) {
+ var = domPathCache.get(pathComponents.subList(0, end));
+ if (var != null) {
+ break;
+ }
+ }
+
+ // Next, do the remaining DOM walking
+ StringBuilder builder = new StringBuilder();
+ if (var != null) {
+ builder.append(var);
+ } else {
+ builder.append(parent);
+ }
+ for (int i = end; i < pathComponents.size(); ++i) {
+ PathComponent component = pathComponents.get(i);
+ if (i < (pathComponents.size() - 1)) {
+ // For partial paths, create an intermediate variable that can be reused
+ // by other elements that need to walk.
+ String varName = "intermediate" + writer.getUniqueId();
+ writer.addInitStatement("com.google.gwt.dom.client.Node %s = %s(%s, %d);",
+ varName, getLookupMethod(component, tag), builder.toString(),
+ component.childIndex);
+ domPathCache.put(new LinkedList<PathComponent>(pathComponents.subList(0, i + 1)), varName);
+ builder = new StringBuilder(varName);
+ } else {
+ builder.insert(0, getLookupMethod(component, tag) + "(");
+ builder.append(", ").append(component.childIndex).append(")");
+ }
+ }
+ builder.append(".cast()");
+ return builder.toString();
+ }
+
+ private String getLookupMethod(PathComponent component, String tag) {
+ if (walkingTextChildNodes) {
+ return STANDARD_LOOKUP_METHOD;
+ }
+ if (component.isTableWithoutTbody && !isValidDirectTableChild(tag)) {
+ return TABLE_NON_TEXT_LOOKUP_METHOD;
+ }
+ return NON_TEXT_LOOKUP_METHOD;
+ }
+
+ private String getQuery() {
+ StringBuilder query = new StringBuilder();
+
+ for (PathComponent component : pathComponents) {
+ if (query.length() > 0) {
+ query.append(" > ");
+ }
+ query.append(":nth-child(").append(component.childIndex + 1).append(")");
+ }
+ return query.toString();
+ }
+
+ /**
+ * Get an access expression using XPATH or CSS query. This is currently disabled as it isn't
+ * as fast as walking by hand.
+ * @return
+ */
+ private String getQueryAccessExpression() {
+ StringBuilder builder = new StringBuilder("UiBinderUtil.lookupNodeByTreeIndicies(");
+ builder.append(parent);
+ builder.append(",\"");
+ builder.append(getQuery());
+ builder.append("\",\"");
+ builder.append(getXpath());
+ builder.append("\").cast()");
+ return builder.toString();
+ }
+
+ private String getXpath() {
+ StringBuilder xpath = new StringBuilder();
+ for (PathComponent component : pathComponents) {
+ xpath.append("/*[").append(component.childIndex + 1).append("]");
+ }
+ return xpath.toString();
+ }
+
+ private boolean isInsideParagraph() {
+ return !paragraphs.isEmpty();
+ }
+
+ private boolean isPlaceholderElement(XMLElement elem) {
+ return "ph".equalsIgnoreCase(elem.getLocalName());
+ }
+
+ private boolean isUnsafeParagraphTag(String tag) {
+ return PARAGRAPH_UNSAFE_NODES.contains(tag.toLowerCase());
+ }
+
+ private boolean isValidDirectTableChild(String tag) {
+ return tag != null && TABLE_SECTIONS.contains(tag.toLowerCase());
+ }
+
+ private boolean safeForExpression() {
+ for (ParagraphTracking tracking : paragraphs) {
+ if (!tracking.isSafeForField()) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/user/src/com/google/gwt/uibinder/rebind/GetInnerHtmlVisitor.java b/user/src/com/google/gwt/uibinder/rebind/GetInnerHtmlVisitor.java
index 4eae5dd..5fc6d17 100644
--- a/user/src/com/google/gwt/uibinder/rebind/GetInnerHtmlVisitor.java
+++ b/user/src/com/google/gwt/uibinder/rebind/GetInnerHtmlVisitor.java
@@ -1,12 +1,12 @@
/*
* Copyright 2008 Google Inc.
- *
+ *
* 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
@@ -19,38 +19,62 @@
import com.google.gwt.uibinder.rebind.XMLElement.Interpreter;
import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.Text;
class GetInnerHtmlVisitor extends GetEscapedInnerTextVisitor {
/**
* Recursively gathers an HTML representation of the children of the given
- * Elem, and stuffs it into the given StringBuffer. Applies the interpreter to
+ * Element, and stuffs it into the given StringBuffer. Applies the interpreter to
* each descendant, and uses the writer to report errors.
*/
public static void getEscapedInnerHtml(Element elem, StringBuffer buffer,
- Interpreter<String> interpreter, XMLElementProvider writer)
- throws UnableToCompleteException {
- new ChildWalker().accept(elem, new GetInnerHtmlVisitor(buffer, interpreter,
- writer));
+ Interpreter<String> interpreter, XMLElementProvider writer, DomCursor domCursor,
+ MortalLogger logger)
+ throws UnableToCompleteException {
+ XMLElement xmlElement = writer.get(elem);
+ domCursor.visitChild(xmlElement);
+ new ChildWalker().accept(elem, new GetInnerHtmlVisitor(buffer, interpreter, writer, domCursor,
+ logger));
+ domCursor.finishChild(xmlElement);
}
+
+ private DomCursor domCursor;
+ private final MortalLogger logger;
- private GetInnerHtmlVisitor(StringBuffer buffer,
- Interpreter<String> interpreter, XMLElementProvider writer) {
+ private GetInnerHtmlVisitor(StringBuffer buffer, Interpreter<String> interpreter,
+ XMLElementProvider writer, DomCursor domCursor, MortalLogger logger) {
super(buffer, interpreter, writer);
+ this.domCursor = domCursor;
+ this.logger = logger;
}
-
+
@Override
public void visitElement(Element elem) throws UnableToCompleteException {
XMLElement xmlElement = elementProvider.get(elem);
String replacement = interpreter.interpretElement(xmlElement);
+
if (replacement != null) {
buffer.append(replacement);
return;
}
- // TODO(jgw): Ditch the closing tag when there are no children.
- buffer.append(xmlElement.consumeOpeningTag());
- getEscapedInnerHtml(elem, buffer, interpreter, elementProvider);
- buffer.append(xmlElement.getClosingTag());
+ Node parent = elem.getParentNode();
+ buffer.append(xmlElement.consumeOpeningTag());
+ getEscapedInnerHtml(elem, buffer, interpreter, elementProvider, domCursor, logger);
+ buffer.append(xmlElement.getClosingTag());
+ domCursor.advanceChild();
+ }
+
+ @Override
+ public void visitText(Text t) {
+ int startLength = buffer.length();
+ super.visitText(t);
+ if (buffer.length() != startLength && !buffer.toString().matches("^\\s*$")) {
+ if (buffer.toString().substring(startLength).matches("^\\s*$")) {
+ domCursor.advanceTextChild();
+ }
+ }
}
}
diff --git a/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java b/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java
index a013d27..a7ba90a 100644
--- a/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java
+++ b/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java
@@ -105,7 +105,7 @@
text = text.replaceAll(">", ">");
if (!preserveWhitespace) {
- text = text.replaceAll("\\s+", " ");
+ text = text.replaceAll("\\s+", " ");
}
return escapeTextForJavaStringLiteral(text);
@@ -201,22 +201,13 @@
private String gwtPrefix;
private String rendered;
-
+
/**
* Stack of element variable names that have been attached.
*/
- private final LinkedList<String> attachSectionElements = new LinkedList<String>();
- /**
- * Maps from field element name to the temporary attach record variable name.
- */
- private final Map<String, String> attachedVars = new HashMap<String, String>();
- private int nextAttachVar = 0;
-
- /**
- * Stack of statements to be executed after we detach the current attach
- * section.
- */
- private final LinkedList<List<String>> detachStatementsStack = new LinkedList<List<String>>();
+ private final LinkedList<DomCursor> domSectionElements =
+ new LinkedList<DomCursor>();
+
private final AttributeParsers attributeParsers;
private final BundleAttributeParsers bundleParsers;
@@ -267,18 +258,13 @@
}
/**
- * Add a statement to be executed right after the current attached element is
- * detached. This is useful for doing things that might be expensive while the
- * element is attached to the DOM.
- *
+ * Adds a comment in the current init stream. Useful for debugging generated code.
* @param format
- * @param args
- * @see #beginAttachedSection(String)
+ * @param params
*/
- public void addDetachStatement(String format, Object... args) {
- detachStatementsStack.getFirst().add(String.format(format, args));
+ public void addInitComment(String format, Object... params) {
+ addInitStatement("// " + format, params);
}
-
/**
* Add a statement to be run after everything has been instantiated, in the
* style of {@link String#format}.
@@ -296,50 +282,35 @@
}
/**
- * Begin a section where a new attachable element is being parsed--that is,
- * one that will be constructed as a big innerHTML string, and then briefly
- * attached to the dom to allow fields accessing its to be filled (at the
+ * Begin a section where a new DOM tree is being parsed--that is,
+ * one that will be constructed as a big innerHTML string, and then walked to
+ * allow fields accessing its to be filled (at the
* moment, HasHTMLParser, HTMLPanelParser, and DomElementParser.).
* <p>
- * Succeeding calls made to {@link #ensureAttached} and
- * {@link #ensureFieldAttached} must refer to children of this element, until
- * {@link #endAttachedSection} is called.
*
* @param element Java expression for the generated code that will return the
- * dom element to be attached.
+ * DOM element to be attached.
*/
- public void beginAttachedSection(String element) {
- attachSectionElements.addFirst(element);
- detachStatementsStack.addFirst(new ArrayList<String>());
+ public DomCursor beginDomSection(String element) {
+ DomCursor cursor = new DomCursor(element, this);
+ domSectionElements.addFirst(cursor);
+ addInitComment("writer.beginAttachedSection %s", cursor.toString());
+ return cursor;
}
/**
- * Declare a field that will hold an Element instance. Returns a token that
- * the caller must set as the id attribute of that element in whatever
- * innerHTML expression will reproduce it at runtime.
- * <P>
- * In the generated code, this token will be replaced by an expression to
- * generate a unique dom id at runtime. Further code will be generated to be
- * run after widgets are instantiated, to use that dom id in a getElementById
- * call and assign the Element instance to its field.
+ * Declare a field that will hold an Element instance.
*
* @param fieldName The name of the field being declared
- * @param parentElementExpression an expression to be evaluated at runtime,
- * which will return an Element that is an ancestor of this one
- * (needed by the getElementById call mentioned above).
+ * @param tag The tag name of the DOM element that is being referenced.
+ * @throws UnableToCompleteException
*/
- public String declareDomField(String fieldName, String parentElementExpression)
- throws UnableToCompleteException {
- ensureAttached(parentElementExpression);
- String name = declareDomIdHolder();
+ public void declareDomField(String fieldName, String tag) throws UnableToCompleteException {
setFieldInitializer(fieldName, "null");
- addInitStatement(
- "%s = com.google.gwt.dom.client.Document.get().getElementById(%s).cast();",
- fieldName, name);
- addInitStatement("%s.removeAttribute(\"id\");", fieldName);
- return tokenForExpression(name);
+ addInitStatement("%s = %s;", fieldName,
+ domSectionElements.getFirst().getAccessExpression(fieldName, tag));
}
-
+
/**
* Declare a variable that will be filled at runtime with a unique id, safe
* for use as a dom element's id attribute.
@@ -347,13 +318,15 @@
* @return that variable's name.
*/
public String declareDomIdHolder() throws UnableToCompleteException {
- String domHolderName = "domId" + domId++;
+ String domHolderName = "domId" + getUniqueId();
FieldWriter domField = fieldManager.registerField(
oracle.findType(String.class.getName()), domHolderName);
domField.setInitializer("com.google.gwt.dom.client.Document.get().createUniqueId()");
return domHolderName;
}
+
+
/**
* Declares a field of the given type name, returning the name of the declared
* field. If the element has a field or id attribute, use its value.
@@ -427,48 +400,11 @@
* End the current attachable section. This will detach the element if it was
* ever attached and execute any detach statements.
*
- * @see #beginAttachedSection(String)
+ * @see #beginDomSection(String)
*/
- public void endAttachedSection() {
- String elementVar = attachSectionElements.removeFirst();
- List<String> detachStatements = detachStatementsStack.removeFirst();
- if (attachedVars.containsKey(elementVar)) {
- String attachedVar = attachedVars.remove(elementVar);
- addInitStatement("%s.detach();", attachedVar);
- for (String statement : detachStatements) {
- addInitStatement(statement);
- }
- }
- }
-
- /**
- * Ensure that the specified element is attached to the DOM.
- *
- * @param element variable name of element to be attached
- * @see #beginAttachedSection(String)
- */
- public void ensureAttached(String element) {
- String attachSectionElement = attachSectionElements.getFirst();
- if (!attachedVars.containsKey(attachSectionElement)) {
- String attachedVar = "attachRecord" + nextAttachVar;
- addInitStatement(
- "UiBinderUtil.TempAttachment %s = UiBinderUtil.attachToDom(%s);",
- attachedVar, attachSectionElement);
- attachedVars.put(attachSectionElement, attachedVar);
- nextAttachVar++;
- }
- }
-
- /**
- * Ensure that the specified field is attached to the DOM. The field must hold
- * an object that responds to Element getElement(). Convenience wrapper for
- * {@link ensureAttached}<code>(field + ".getElement()")</code>.
- *
- * @param field variable name of the field to be attached
- * @see #beginAttachedSection(String)
- */
- public void ensureFieldAttached(String field) {
- ensureAttached(field + ".getElement()");
+ public void endDomSection() {
+ DomCursor cursor = domSectionElements.removeFirst();
+ addInitComment("writer.endAttachedSection %s", cursor.toString());
}
/**
@@ -536,14 +472,35 @@
public ImplicitClientBundle getBundleClass() {
return bundleClass;
}
+
+ /**
+ * Get the current cursor for the DOM. This can be used for accessing expressions, or walking
+ * subtrees.
+ */
+ public DomCursor getCurrentDomCursor() {
+ return domSectionElements.getFirst();
+ }
/**
+ * Returns an expression for accessing the current position in the DOM.
+ * @param varName optional variable where the result of this expression will be stored for
+ * cacheing
+ * @param tag optional name of the HTML tag we are using the expression for.
+ * @throws UnableToCompleteException
+ */
+ public String getDomAccessExpression(String varName, String tag)
+ throws UnableToCompleteException {
+ addInitComment("getDomAccessExpression %s", domSectionElements.getFirst().toString());
+ return domSectionElements.getFirst().getAccessExpression(varName, tag);
+ }
+
+ /**
* @return The logger, at least until we get get it handed off to parsers via
* constructor args.
*/
public MortalLogger getLogger() {
return logger;
- }
+ }
/**
* Get the {@link MessagesWriter} for this UI, generating it if necessary.
@@ -567,6 +524,10 @@
return gwtPrefix + ":field";
}
+ public int getUniqueId() {
+ return domId++;
+ }
+
public boolean isBinderElement(XMLElement elem) {
String uri = elem.getNamespaceUri();
return uri != null && UiBinderGenerator.BINDER_URI.equals(uri);
@@ -723,15 +684,11 @@
*
* @throws UnableToCompleteException
*/
- private void ensureAttachmentCleanedUp() throws UnableToCompleteException {
- if (!attachSectionElements.isEmpty()) {
+ private void ensureCursorCleanedUp() {
+ if (!domSectionElements.isEmpty()) {
throw new IllegalStateException("Attachments not cleaned up: "
- + attachSectionElements);
- }
- if (!detachStatementsStack.isEmpty()) {
- throw new IllegalStateException("Detach not cleaned up: "
- + detachStatementsStack);
- }
+ + domSectionElements);
+ }
}
/**
@@ -897,7 +854,7 @@
new PrintWriter(stringWriter));
writeBinder(niceWriter, rootField);
- ensureAttachmentCleanedUp();
+ ensureCursorCleanedUp();
return stringWriter.toString();
}
diff --git a/user/src/com/google/gwt/uibinder/rebind/XMLElement.java b/user/src/com/google/gwt/uibinder/rebind/XMLElement.java
index b9b9377..ae1c1db 100644
--- a/user/src/com/google/gwt/uibinder/rebind/XMLElement.java
+++ b/user/src/com/google/gwt/uibinder/rebind/XMLElement.java
@@ -379,13 +379,15 @@
* @param interpreter Called for each element, expected to return a string
* replacement for it, or null if it should be left as is
*/
- public String consumeInnerHtml(Interpreter<String> interpreter)
+ public String consumeInnerHtml(Interpreter<String> interpreter,
+ DomCursor domCursor)
throws UnableToCompleteException {
if (interpreter == null) {
throw new NullPointerException("interpreter must not be null");
}
StringBuffer buf = new StringBuffer();
- GetInnerHtmlVisitor.getEscapedInnerHtml(elem, buf, interpreter, provider);
+ GetInnerHtmlVisitor.getEscapedInnerHtml(elem, buf, interpreter, provider,
+ domCursor, logger);
clearChildren(elem);
return buf.toString().trim();
@@ -395,9 +397,11 @@
* Refines {@link #consumeInnerHtml(Interpreter)} to handle
* PostProcessingInterpreter.
*/
- public String consumeInnerHtml(PostProcessingInterpreter<String> interpreter)
+ public String consumeInnerHtml(PostProcessingInterpreter<String> interpreter,
+ DomCursor domCursor)
throws UnableToCompleteException {
- String html = consumeInnerHtml((Interpreter<String>) interpreter);
+ String html = consumeInnerHtml((Interpreter<String>) interpreter,
+ domCursor);
return interpreter.postProcess(html);
}
diff --git a/user/test/com/google/gwt/dom/client/ElementTest.java b/user/test/com/google/gwt/dom/client/ElementTest.java
index 5806456..cb7dfc6 100644
--- a/user/test/com/google/gwt/dom/client/ElementTest.java
+++ b/user/test/com/google/gwt/dom/client/ElementTest.java
@@ -286,6 +286,23 @@
assertEquals("foo", nodes.getItem(0).getInnerText());
assertEquals("bar", nodes.getItem(1).getInnerText());
}
+
+ public void testGetNonTextElement() {
+ DivElement div = Document.get().createDivElement();
+ Text text1 = Document.get().createTextNode("my text");
+ DivElement innerDiv = Document.get().createDivElement();
+ Text text2 = Document.get().createTextNode(" ");
+ SpanElement span = Document.get().createSpanElement();
+ Text text3 = Document.get().createTextNode("my text2");
+ div.appendChild(text1);
+ div.appendChild(innerDiv);
+ div.appendChild(text2);
+ div.appendChild(span);
+ div.appendChild(text3);
+
+ assertEquals(innerDiv, div.getNonTextChild(0));
+ assertEquals(span, div.getNonTextChild(1));
+ }
public void testHasAttribute() {
DivElement div = Document.get().createDivElement();
diff --git a/user/test/com/google/gwt/uibinder/UiBinderJreSuite.java b/user/test/com/google/gwt/uibinder/UiBinderJreSuite.java
index e828dba..58e834c 100644
--- a/user/test/com/google/gwt/uibinder/UiBinderJreSuite.java
+++ b/user/test/com/google/gwt/uibinder/UiBinderJreSuite.java
@@ -31,6 +31,7 @@
import com.google.gwt.uibinder.elementparsers.StackLayoutPanelParserTest;
import com.google.gwt.uibinder.elementparsers.TabLayoutPanelParserTest;
import com.google.gwt.uibinder.elementparsers.UIObjectParserTest;
+import com.google.gwt.uibinder.rebind.DomCursorTest;
import com.google.gwt.uibinder.rebind.FieldWriterOfGeneratedCssResourceTest;
import com.google.gwt.uibinder.rebind.GwtResourceEntityResolverTest;
import com.google.gwt.uibinder.rebind.HandlerEvaluatorTest;
@@ -51,6 +52,7 @@
TestSuite suite = new TestSuite("UiBinder tests that require the JRE");
// rebind
+ suite.addTestSuite(DomCursorTest.class);
suite.addTestSuite(FieldWriterOfGeneratedCssResourceTest.class);
suite.addTestSuite(GwtResourceEntityResolverTest.class);
suite.addTestSuite(HandlerEvaluatorTest.class);
diff --git a/user/test/com/google/gwt/uibinder/client/UiBinderUtilTest.java b/user/test/com/google/gwt/uibinder/client/UiBinderUtilTest.java
index 4eded61..1fbfe60 100644
--- a/user/test/com/google/gwt/uibinder/client/UiBinderUtilTest.java
+++ b/user/test/com/google/gwt/uibinder/client/UiBinderUtilTest.java
@@ -97,10 +97,10 @@
}
private void findAndAssertTextBeforeFirstChild(Element div, String id,
- String firstText) {
- UiBinderUtil.TempAttachment t = UiBinderUtil.attachToDom(div);
+ String firstText) {
+ Document.get().getBody().appendChild(div);
Element child = Document.get().getElementById(id);
- t.detach();
+ Document.get().getBody().removeChild(div);
assertStartsWith(child.getInnerHTML(), firstText + "<");
}
@@ -146,9 +146,9 @@
findAndAssertTextBeforeFirstChild(div, ableId, ableText);
findAndAssertTextBeforeFirstChild(div, bakerId, bakerText);
findAndAssertTextBeforeFirstChild(div, charlieId, charlieText);
- UiBinderUtil.TempAttachment t = UiBinderUtil.attachToDom(div);
+ Document.get().getBody().appendChild(div);
Element e = Document.get().getElementById(deltaId);
- t.detach();
+ Document.get().getBody().removeChild(div);
assertEquals(deltaText, e.getInnerText());
} finally {
// tearDown isn't reliable enough, e.g. doesn't fire when exceptions
diff --git a/user/test/com/google/gwt/uibinder/rebind/DomCursorTest.java b/user/test/com/google/gwt/uibinder/rebind/DomCursorTest.java
new file mode 100644
index 0000000..ccc4bbd
--- /dev/null
+++ b/user/test/com/google/gwt/uibinder/rebind/DomCursorTest.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2008 Google Inc.
+ *
+ * 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.gwt.uibinder.rebind;
+
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.uibinder.rebind.messages.MessagesWriter;
+
+import junit.framework.TestCase;
+
+import org.easymock.EasyMock;
+import org.easymock.IAnswer;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+
+/**
+ * Test for DomCuror.
+ */
+public class DomCursorTest extends TestCase {
+
+ private static final String PARENT = "parent";
+
+ private UiBinderWriter writer;
+ private DomCursor cursor;
+
+ @Override
+ public void setUp() {
+ writer = org.easymock.classextension.EasyMock.createMock(
+ UiBinderWriter.class);
+ org.easymock.classextension.EasyMock.expect(writer.getUniqueId()).andStubAnswer(new IAnswer<Integer>() {
+ private int nextId = 1;
+ @Override
+ public Integer answer() throws Throwable {
+ return nextId++;
+ }
+ });
+ MessagesWriter message = new MessagesWriter("ui", null, "", "", "");
+ org.easymock.classextension.EasyMock.expect(writer.getMessages()).andStubReturn(message);
+ writer.addInitComment((String) EasyMock.notNull(), EasyMock.notNull());
+ org.easymock.classextension.EasyMock.expectLastCall().asStub();
+ cursor = new DomCursor(PARENT, writer);
+ }
+
+ public void testAccessExpressions() throws Exception {
+ verifyInitAssignment(writer, "UiBinderUtil.getNonTextChild(parent, 0).cast()", 1);
+ verifyInitAssignment(writer, "UiBinderUtil.getNonTextChild(parent, 1).cast()", 2);
+ verifyInitAssignment(writer,
+ "UiBinderUtil.getNonTextChild(intermediate2, 0).cast()", 3);
+ verifyInitAssignment(writer, "UiBinderUtil.getNonTextChild(parent, 2).cast()", 4);
+ org.easymock.classextension.EasyMock.replay(writer);
+
+ // parent
+ cursor.visitChild(makeElement("div"));
+ // parent's first child
+ assertEquals(intermediate(1), cursor.getAccessExpression());
+
+ cursor.advanceChild();
+ assertEquals(intermediate(2), cursor.getAccessExpression());
+ // intermediate 2, parent's second child
+ XMLElement span = makeElement("span");
+ cursor.visitChild(span);
+ // intermediate 3, intermediate 2's first child
+ assertEquals(intermediate(3), cursor.getAccessExpression());
+ cursor.finishChild(span);
+
+ cursor.advanceChild();
+ // intermediate 4, parent's third child
+ assertEquals(intermediate(4), cursor.getAccessExpression());
+
+ org.easymock.classextension.EasyMock.verify(writer);
+ }
+
+ public void testParagraphsWith() throws Exception {
+ writer.die((String) EasyMock.anyObject());
+ org.easymock.classextension.EasyMock.expectLastCall().andThrow(new UnableToCompleteException());
+ org.easymock.classextension.EasyMock.replay(writer);
+
+ cursor.visitChild(makeElement("p"));
+ XMLElement span = makeElement("span");
+ cursor.visitChild(span);
+ cursor.finishChild(span);
+ cursor.visitChild(makeElement("div"));
+
+ try {
+ cursor.getAccessExpression();
+ fail("Expected exception about block elements inside paragraphs");
+ } catch (Exception e) {
+ // Expected
+ }
+ org.easymock.classextension.EasyMock.verify(writer);
+ }
+
+ public void testTables() throws UnableToCompleteException {
+
+ verifyInitAssignment(writer, intermediate(1), "UiBinderUtil.getNonTextChild", "parent", 0);
+ verifyInitAssignment(writer, "UiBinderUtil.getTableChild(intermediate1, 0).cast()", 2);
+ verifyInitAssignment(writer, "UiBinderUtil.getNonTextChild(intermediate2, 0).cast()", 3);
+ verifyInitAssignment(writer, intermediate(4), "UiBinderUtil.getNonTextChild", "parent", 1);
+ verifyInitAssignment(writer, "UiBinderUtil.getNonTextChild(intermediate4, 0).cast()", 5);
+ verifyInitAssignment(writer, "UiBinderUtil.getNonTextChild(intermediate5, 0).cast()", 6);
+
+ org.easymock.classextension.EasyMock.replay(writer);
+ XMLElement div = makeElement("div");
+ cursor.visitChild(div);
+
+ XMLElement table1 = makeElement("table");
+ cursor.visitChild(table1);
+ assertEquals(intermediate(2), cursor.getAccessExpression());
+
+ XMLElement tr = makeElement("tr");
+ cursor.visitChild(tr);
+ assertEquals(intermediate(3), cursor.getAccessExpression());
+
+ cursor.finishChild(tr);
+ cursor.finishChild(table1);
+ cursor.advanceChild();
+ XMLElement table2 = makeElement("table");
+ cursor.visitChild(table2);
+
+ XMLElement tbody = makeElement("tbody");
+ cursor.visitChild(tbody);
+ cursor.finishChild(tbody);
+ assertEquals(intermediate(5), cursor.getAccessExpression());
+
+ XMLElement tr2 = makeElement("tr");
+ cursor.visitChild(tr2);
+ assertEquals(intermediate(6), cursor.getAccessExpression());
+ cursor.finishChild(tr2);
+
+ XMLElement td = makeElement("td");
+ try {
+ cursor.visitChild(td);
+ fail("Expected exception about tds inside tables without trs");
+ } catch (Exception e) {
+ // expected
+ }
+ org.easymock.classextension.EasyMock.verify(writer);
+ }
+
+ private String intermediate(int count) {
+ return "intermediate" + count;
+ }
+
+ private XMLElement makeElement(String tag) {
+ NamedNodeMap attributes = EasyMock.createNiceMock(NamedNodeMap.class);
+ Element element = EasyMock.createNiceMock(Element.class);
+ EasyMock.expect(element.getLocalName()).andStubReturn(tag);
+ EasyMock.expect(element.getTagName()).andStubReturn(tag);
+ EasyMock.expect(element.getAttributes()).andStubReturn(attributes);
+ EasyMock.replay(element, attributes);
+ return new XMLElement(element, null, null, null, null, null);
+ }
+
+ private void verifyInitAssignment(UiBinderWriter writer, String expr, int intermediateCount) {
+ writer.addInitStatement("com.google.gwt.dom.client.Node %s = %s;",
+ "intermediate" + intermediateCount, expr);
+ }
+
+ private void verifyInitAssignment(UiBinderWriter writer, String var, String method, String parent,
+ int index) {
+ writer.addInitStatement("com.google.gwt.dom.client.Node %s = %s(%s, %d);", var, method, parent,
+ index);
+ }
+}