Re-submit: Iterating over all attributed when realizing PotentialElements.

Review at http://gwt-code-reviews.appspot.com/1573804

Review by: rjrjr@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@10709 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/user/client/ui/PotentialElement.java b/user/src/com/google/gwt/user/client/ui/PotentialElement.java
index f8abf74..83d06d9 100644
--- a/user/src/com/google/gwt/user/client/ui/PotentialElement.java
+++ b/user/src/com/google/gwt/user/client/ui/PotentialElement.java
@@ -17,6 +17,8 @@
 
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.builder.shared.HtmlElementBuilder;
+import com.google.gwt.dom.builder.shared.HtmlBuilderFactory;
 
 /**
  * EXPERIMENTAL and subject to change. Do not use this in production code.
@@ -37,6 +39,10 @@
  */
 public class PotentialElement extends Element {
 
+  static {
+    declareShim();
+  }
+
   public static PotentialElement as(Element e) {
     assert isPotential(e);
     return (PotentialElement) e;
@@ -58,34 +64,34 @@
    * that is needed.
    */
   public static native PotentialElement build(UIObject o, String tagName) /*-{
-    return @com.google.gwt.dom.client.Element::as(Lcom/google/gwt/core/client/JavaScriptObject;)({
-      className: '',
-      clientHeight: 0,
-      clientWidth: 0,
-      dir: '',
-      getAttribute: function(name, value) {
-        return this[name];
-      },
-      href: '',
-      id: '',
-      lang: '',
-      // should be @com.google.gwt.dom.client.Node.ELEMENT_MODE, but the compiler
-      // doesn't like that.
-      nodeType: 1,
-      removeAttribute: function(name, value) {
-        this[name] = undefined;
-      },
-      setAttribute: function(name, value) {
-        this[name] = value;
-      },
-      src: '',
-      style: {},
-      tagName: tagName,
-      __gwt_resolve: @com.google.gwt.user.client.ui.PotentialElement::buildResolveCallback(Lcom/google/gwt/user/client/ui/UIObject;)(o),
-      title: ''
-    });
+    var el = new $wnd.GwtPotentialElementShim();
+    el.tagName = tagName;
+    el.__gwt_resolve = @com.google.gwt.user.client.ui.PotentialElement::buildResolveCallback(Lcom/google/gwt/user/client/ui/UIObject;)(o);
+    return @com.google.gwt.dom.client.Element::as(Lcom/google/gwt/core/client/JavaScriptObject;)(el);
   }-*/;
 
+  /**
+   * Creates an {@link HtmlElementBuilder} instance inheriting all attributes
+   * set for the given PotentialElement.
+   *
+   * @param potentialElement assumed to be a PotentialElement, used as basis for
+   *     the builder
+   * @return a propertly configured {@link HtmlElementBuilder} instance
+   */
+  public static HtmlElementBuilder createBuilderFor(Element potentialElement) {
+    PotentialElement el = PotentialElement.as(potentialElement);
+    HtmlElementBuilder builder = HtmlBuilderFactory.get().trustedCreate(
+        el.getTagName());
+    el.mergeInto(builder);
+    return builder;
+  }
+
+  /**
+   * Tests whether a given {@link JavaScriptObject} represents a PotentialElement.
+   *
+   * @param o the {@link JavaScriptObject} to be tested
+   * @return true if the given object is a PotentialElement instance
+   */
   public static native boolean isPotential(JavaScriptObject o) /*-{
     try {
       return (!!o) &&  (!!o.__gwt_resolve);
@@ -117,6 +123,35 @@
     throw "A PotentialElement cannot be resolved twice.";
   }-*/;
 
+  private static final native void declareShim() /*-{
+    var shim = function() {};
+    shim.prototype = {
+      className: '',
+      clientHeight: 0,
+      clientWidth: 0,
+      dir: '',
+      getAttribute: function(name, value) {
+        return this[name];
+      },
+      href: '',
+      id: '',
+      lang: '',
+      // should be @com.google.gwt.dom.client.Node.ELEMENT_MODE, but the compiler
+      // doesn't like that.
+      nodeType: 1,
+      removeAttribute: function(name, value) {
+        this[name] = undefined;
+      },
+      setAttribute: function(name, value) {
+        this[name] = value;
+      },
+      src: '',
+      style: {},
+      title: ''
+    };
+    $wnd.GwtPotentialElementShim = shim;
+  }-*/;
+
   protected PotentialElement() {
   }
 
@@ -125,6 +160,54 @@
   }-*/;
 
   /**
+   * Copy only the fields that have actually changed from the values in the shim
+   * prototype. Do this by severing the __proto__ link, allowing us to iterate
+   * only on the fields set in this specific instance.
+   */
+  private native void mergeInto(HtmlElementBuilder builder) /*-{
+    var savedProto = this.__proto__;
+    var tagName = this.tagName;
+    var gwtResolve = this.__gwt_resolve;
+    var className = this.className;
+
+    try {
+      this.__proto__ = null;
+      this.tagName = null;
+      this.__gwt_resolve = null;
+
+      // className needs special treatment because the actual HTML attribute is
+      // called "class" and not "className".
+      if (this.className) {
+        builder.@com.google.gwt.dom.builder.shared.ElementBuilder::className(Ljava/lang/String;)(
+            this.className);
+        this.className = null;
+      }
+
+      // Iterate over all attributes, and copy them to the ElementBuilder.
+      // TODO(rdcastro): Deal with the "style" attribute.
+      for (attr in this) {
+        if (!this[attr]) {
+          continue;
+        }
+        if (typeof this[attr] == 'number') {
+          builder.@com.google.gwt.dom.builder.shared.ElementBuilder::attribute(Ljava/lang/String;I)(
+              attr, this[attr]);
+        } else if (typeof this[attr] == 'string') {
+          builder.@com.google.gwt.dom.builder.shared.ElementBuilder::attribute(Ljava/lang/String;Ljava/lang/String;)(
+              attr, this[attr]);
+        }
+      }
+    } finally {
+      this.__proto__ = savedProto;
+      if (className) {
+        this.className = className;
+      }
+      this.__gwt_resolve = gwtResolve;
+      this.tagName = tagName;
+    }
+  }-*/;
+
+  /**
    * Calls the <code>__gwt_resolve</code> method on the underlying
    * JavaScript object if it exists. On objects created via {@link #build}, this
    * method is a call to the {@link UIObject#resolvePotentialElement} method
diff --git a/user/src/com/google/gwt/user/client/ui/RenderablePanel.java b/user/src/com/google/gwt/user/client/ui/RenderablePanel.java
index 777e289..747c049 100644
--- a/user/src/com/google/gwt/user/client/ui/RenderablePanel.java
+++ b/user/src/com/google/gwt/user/client/ui/RenderablePanel.java
@@ -15,8 +15,7 @@
  */
 package com.google.gwt.user.client.ui;
 
-import com.google.gwt.dom.builder.shared.HtmlDivBuilder;
-import com.google.gwt.dom.builder.shared.HtmlBuilderFactory;
+import com.google.gwt.dom.builder.shared.HtmlElementBuilder;
 import com.google.gwt.dom.client.Document;
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.safehtml.shared.SafeHtml;
@@ -189,18 +188,12 @@
 
   @Override
   public SafeHtml render(RenderableStamper stamper) {
-    String styleName = getStyleName();
+    HtmlElementBuilder builder = PotentialElement.createBuilderFor(getElement());
+    stamper.stamp(builder);
+    builder.html(getInnerHtml()).end();
 
-    HtmlDivBuilder divBuilder = HtmlBuilderFactory.get()
-        .createDivBuilder();
-    if (styleName != null) {
-      divBuilder.className(styleName);
-      styleName = null;
-    }
-    stamper.stamp(divBuilder);
-    divBuilder.html(getInnerHtml()).end();
-
-    return divBuilder.asSafeHtml();
+    SafeHtml returnValue = builder.asSafeHtml();
+    return returnValue;
   }
 
   @Override
@@ -236,6 +229,8 @@
    * that may have been added to the panel.
    */
   private void buildAndInitDivContainer() {
+    // TODO(rdcastro): Use the same technique as in render() above.
+
     // Build the div that'll container the panel's HTML.
     Element element = Document.get().createDivElement();
     element.setInnerHTML(getInnerHtml().asString());