Ensures that onload events are fired for images even if the browser fires the onload event synchronously while the image is not attached.

Patch by: jlabanca
Review by: jgw (Partial TBR)



git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@7092 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/user/client/impl/DOMImplOpera.java b/user/src/com/google/gwt/user/client/impl/DOMImplOpera.java
index 5fe9b26..5b02b1c 100644
--- a/user/src/com/google/gwt/user/client/impl/DOMImplOpera.java
+++ b/user/src/com/google/gwt/user/client/impl/DOMImplOpera.java
@@ -59,7 +59,7 @@
     elem.onscroll      = (bits & 0x04000) ?
         @com.google.gwt.user.client.impl.DOMImplStandard::dispatchEvent : null;
     elem.onload        = (bits & 0x08000) ?
-        @com.google.gwt.user.client.impl.DOMImplStandard::dispatchEvent : null;
+        @com.google.gwt.user.client.impl.DOMImplStandard::dispatchUnhandledEvent : null;
     elem.onerror       = (bits & 0x10000) ?
         @com.google.gwt.user.client.impl.DOMImplStandard::dispatchEvent : null;
     elem.onmousewheel  = (bits & 0x20000) ?
diff --git a/user/src/com/google/gwt/user/client/impl/DOMImplStandard.java b/user/src/com/google/gwt/user/client/impl/DOMImplStandard.java
index 4160aab..414e4c3 100644
--- a/user/src/com/google/gwt/user/client/impl/DOMImplStandard.java
+++ b/user/src/com/google/gwt/user/client/impl/DOMImplStandard.java
@@ -38,6 +38,9 @@
   @SuppressWarnings("unused")
   private static JavaScriptObject dispatchEvent;
 
+  @SuppressWarnings("unused")
+  private static JavaScriptObject dispatchUnhandledEvent;
+
   @Override
   public Element eventGetFromElement(Event evt) {
     if (evt.getType().equals("mouseover")) {
@@ -165,6 +168,11 @@
       return true;
     });
 
+    @com.google.gwt.user.client.impl.DOMImplStandard::dispatchUnhandledEvent = $entry(function(evt) {
+      this.__gwtLastUnhandledEvent = evt.type;
+      @com.google.gwt.user.client.impl.DOMImplStandard::dispatchEvent.call(this, evt);
+    });
+
     @com.google.gwt.user.client.impl.DOMImplStandard::dispatchEvent = $entry(function(evt) {
       var listener, curElem = this;
       while (curElem && !(listener = curElem.__listener)) {
@@ -231,7 +239,7 @@
     if (chMask & 0x04000) elem.onscroll      = (bits & 0x04000) ?
         @com.google.gwt.user.client.impl.DOMImplStandard::dispatchEvent : null;
     if (chMask & 0x08000) elem.onload        = (bits & 0x08000) ?
-        @com.google.gwt.user.client.impl.DOMImplStandard::dispatchEvent : null;
+        @com.google.gwt.user.client.impl.DOMImplStandard::dispatchUnhandledEvent : null;
     if (chMask & 0x10000) elem.onerror       = (bits & 0x10000) ?
         @com.google.gwt.user.client.impl.DOMImplStandard::dispatchEvent : null;
     if (chMask & 0x20000) elem.onmousewheel  = (bits & 0x20000) ? 
diff --git a/user/src/com/google/gwt/user/client/impl/DOMImplTrident.java b/user/src/com/google/gwt/user/client/impl/DOMImplTrident.java
index 19ea536..8f90472 100644
--- a/user/src/com/google/gwt/user/client/impl/DOMImplTrident.java
+++ b/user/src/com/google/gwt/user/client/impl/DOMImplTrident.java
@@ -31,6 +31,9 @@
   @SuppressWarnings("unused")
   private static JavaScriptObject dispatchDblClickEvent;
 
+  @SuppressWarnings("unused")
+  private static JavaScriptObject dispatchUnhandledEvent;
+
   /**
    * Let every GWT app on the page preview the current event. If any app cancels
    * the event, the event will be canceled for all apps.
@@ -151,6 +154,11 @@
       }
     });
 
+    @com.google.gwt.user.client.impl.DOMImplTrident::dispatchUnhandledEvent = $entry(function() {
+      this.__gwtLastUnhandledEvent = $wnd.event.type;
+      @com.google.gwt.user.client.impl.DOMImplTrident::dispatchEvent.call(this);
+    });
+
     // We need to create these delegate functions to fix up the 'this' context.
     // Normally, 'this' is the firing element, but this is only true for
     // 'onclick = ...' event handlers, not for handlers setup via attachEvent().
@@ -243,7 +251,7 @@
     if (chMask & 0x04000) elem.onscroll      = (bits & 0x04000) ?
         @com.google.gwt.user.client.impl.DOMImplTrident::dispatchEvent : null;
     if (chMask & 0x08000) elem.onload        = (bits & 0x08000) ?
-        @com.google.gwt.user.client.impl.DOMImplTrident::dispatchEvent : null;
+        @com.google.gwt.user.client.impl.DOMImplTrident::dispatchUnhandledEvent : null;
     if (chMask & 0x10000) elem.onerror       = (bits & 0x10000) ?
         @com.google.gwt.user.client.impl.DOMImplTrident::dispatchEvent : null;
     if (chMask & 0x20000) elem.onmousewheel  = (bits & 0x20000) ? 
diff --git a/user/src/com/google/gwt/user/client/ui/Image.java b/user/src/com/google/gwt/user/client/ui/Image.java
index dd95468..0035d6a 100644
--- a/user/src/com/google/gwt/user/client/ui/Image.java
+++ b/user/src/com/google/gwt/user/client/ui/Image.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,6 +19,7 @@
 import com.google.gwt.dom.client.Document;
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.dom.client.ImageElement;
+import com.google.gwt.dom.client.NativeEvent;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.ErrorEvent;
@@ -43,6 +44,8 @@
 import com.google.gwt.event.dom.client.MouseWheelHandler;
 import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwt.resources.client.ImageResource;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DeferredCommand;
 import com.google.gwt.user.client.Event;
 import com.google.gwt.user.client.ui.impl.ClippedImageImpl;
 
@@ -57,16 +60,20 @@
  * image is constructed, and how it is transformed after construction. Methods
  * will operate differently depending on the mode that the image is in. These
  * differences are detailed in the documentation for each method.
- *
+ * 
  * <p>
  * If an image transitions between clipped mode and unclipped mode, any
  * {@link Element}-specific attributes added by the user (including style
  * attributes, style names, and style modifiers), except for event listeners,
  * will be lost.
  * </p>
- *
- * <h3>CSS Style Rules</h3> <ul class="css"> <li>.gwt-Image { }</li> </ul>
- *
+ * 
+ * <h3>CSS Style Rules</h3>
+ * <dl>
+ * <dt>.gwt-Image</dt>
+ * </dd>The outer element</dd>
+ * </dl>
+ * 
  * Tranformations between clipped and unclipped state will result in a loss of
  * any style names that were set/added; the only style names that are preserved
  * are those that are mentioned in the static CSS style rules. Due to
@@ -75,7 +82,7 @@
  * expected when an image is in clipped mode. These limitations can usually be
  * easily worked around by encapsulating the image in a container widget that
  * can itself be styled.
- *
+ * 
  * <p>
  * <h3>Example</h3>
  * {@example com.google.gwt.examples.ImageExample}
@@ -87,6 +94,12 @@
     HasAllMouseHandlers, SourcesMouseEvents {
 
   /**
+   * The attribute that is set when an image fires a native load or error event
+   * before it is attached.
+   */
+  private static final String UNHANDLED_EVENT_ATTR = "__gwtLastUnhandledEvent";
+
+  /**
    * Implementation of behaviors associated with the clipped state of an image.
    */
   private static class ClippedState extends State {
@@ -119,6 +132,11 @@
     }
 
     @Override
+    public ImageElement getImageElement(Image image) {
+      return impl.getImgElement(image).cast();
+    }
+
+    @Override
     public int getOriginLeft() {
       return left;
     }
@@ -140,7 +158,10 @@
 
     @Override
     public void setUrl(Image image, String url) {
-      image.changeState(new UnclippedState(image, url));
+      image.changeState(new UnclippedState(image));
+      // Need to make sure we change the state before an onload event can fire,
+      // or handlers will be fired while we are in the old state.
+      image.setUrl(url);
     }
 
     @Override
@@ -156,7 +177,7 @@
         this.height = height;
 
         impl.adjust(image.getElement(), url, left, top, width, height);
-        impl.fireSyntheticLoadEvent(image);
+        fireSyntheticLoadEvent(image);
       }
     }
 
@@ -177,7 +198,7 @@
         this.height = height;
 
         impl.adjust(image.getElement(), url, left, top, width, height);
-        impl.fireSyntheticLoadEvent(image);
+        fireSyntheticLoadEvent(image);
       }
     }
 
@@ -196,6 +217,8 @@
 
     public abstract int getHeight(Image image);
 
+    public abstract ImageElement getImageElement(Image image);
+
     public abstract int getOriginLeft();
 
     public abstract int getOriginTop();
@@ -204,6 +227,16 @@
 
     public abstract int getWidth(Image image);
 
+    public void onLoad(Image image) {
+      // If an onload event fired while the image wasn't attached, we need to
+      // synthesize one now.
+      String unhandledEvent = getImageElement(image).getPropertyString(
+          UNHANDLED_EVENT_ATTR);
+      if ("load".equals(unhandledEvent)) {
+        fireSyntheticLoadEvent(image);
+      }
+    }
+
     public abstract void setUrl(Image image, String url);
 
     public abstract void setUrlAndVisibleRect(Image image, String url,
@@ -212,6 +245,27 @@
     public abstract void setVisibleRect(Image image, int left, int top,
         int width, int height);
 
+    /**
+     * We need to synthesize a load event in case the image loads synchronously,
+     * before our handlers can be attached.
+     * 
+     * @param image the image on which to dispatch the event
+     */
+    protected void fireSyntheticLoadEvent(final Image image) {
+      /*
+       * We use a deferred command here to simulate the native version of the
+       * event as closely as possible. In the native event case, it is unlikely
+       * that a second load event would occur while you are in the load event
+       * handler.
+       */
+      DeferredCommand.addCommand(new Command() {
+        public void execute() {
+          NativeEvent evt = Document.get().createLoadEvent();
+          getImageElement(image).dispatchEvent(evt);
+        }
+      });
+    }
+
     // This method is used only by unit tests.
     protected abstract String getStateName();
   }
@@ -248,7 +302,12 @@
 
     @Override
     public int getHeight(Image image) {
-      return image.getImageElement().getHeight();
+      return getImageElement(image).getHeight();
+    }
+
+    @Override
+    public ImageElement getImageElement(Image image) {
+      return image.getElement().cast();
     }
 
     @Override
@@ -263,17 +322,18 @@
 
     @Override
     public String getUrl(Image image) {
-      return image.getImageElement().getSrc();
+      return getImageElement(image).getSrc();
     }
 
     @Override
     public int getWidth(Image image) {
-      return image.getImageElement().getWidth();
+      return getImageElement(image).getWidth();
     }
 
     @Override
     public void setUrl(Image image, String url) {
-      image.getImageElement().setSrc(url);
+      image.clearUnhandledEvent();
+      getImageElement(image).setSrc(url);
     }
 
     @Override
@@ -305,7 +365,7 @@
 
   /**
    * Causes the browser to pre-fetch the image at a given URL.
-   *
+   * 
    * @param url the URL of the image to be prefetched
    */
   public static void prefetch(String url) {
@@ -316,11 +376,11 @@
 
   /**
    * Creates a Image widget that wraps an existing &lt;img&gt; element.
-   *
+   * 
    * This element must already be attached to the document. If the element is
    * removed from the document, you must call
    * {@link RootPanel#detachNow(Widget)}.
-   *
+   * 
    * @param element the element to be wrapped
    */
   public static Image wrap(Element element) {
@@ -349,18 +409,18 @@
 
   /**
    * Creates an image whose size and content are defined by an ImageResource.
-   *
+   * 
    * @param resource the ImageResource to be displayed
    */
   public Image(ImageResource resource) {
-    this(resource.getURL(), resource.getLeft(),
-         resource.getTop(), resource.getWidth(), resource.getHeight());
+    this(resource.getURL(), resource.getLeft(), resource.getTop(),
+        resource.getWidth(), resource.getHeight());
   }
 
   /**
    * Creates an image with a specified URL. The load event will be fired once
    * the image at the given URL has been retrieved by the browser.
-   *
+   * 
    * @param url the URL of the image to be displayed
    */
   public Image(String url) {
@@ -377,7 +437,7 @@
    * the width and height are specified explicitly by the user, this behavior
    * will not cause problems with retrieving the width and height of a clipped
    * image in a load event handler.
-   *
+   * 
    * @param url the URL of the image to be displayed
    * @param left the horizontal co-ordinate of the upper-left vertex of the
    *          visibility rectangle
@@ -394,7 +454,7 @@
   /**
    * This constructor may be used by subclasses to explicitly use an existing
    * element. This element must be an &lt;img&gt; element.
-   *
+   * 
    * @param element the element to be used
    */
   protected Image(Element element) {
@@ -436,9 +496,9 @@
   }
 
   /**
-   * @deprecated Use {@link #addMouseOverHandler} {@link
-   * #addMouseMoveHandler}, {@link #addMouseDownHandler}, {@link
-   * #addMouseUpHandler} and {@link #addMouseOutHandler} instead
+   * @deprecated Use {@link #addMouseOverHandler} {@link #addMouseMoveHandler},
+   *             {@link #addMouseDownHandler}, {@link #addMouseUpHandler} and
+   *             {@link #addMouseOutHandler} instead
    */
   @Deprecated
   public void addMouseListener(MouseListener listener) {
@@ -477,7 +537,7 @@
    * Gets the height of the image. When the image is in the unclipped state, the
    * height of the image is not known until the image has been loaded (i.e. load
    * event has been fired for the image).
-   *
+   * 
    * @return the height of the image, or 0 if the height is unknown
    */
   public int getHeight() {
@@ -489,7 +549,7 @@
    * visibility rectangle. If the image is in the unclipped state, then the
    * visibility rectangle is assumed to be the rectangle which encompasses the
    * entire image, which has an upper-left vertex of (0,0).
-   *
+   * 
    * @return the horizontal co-ordinate of the upper-left vertex of the image's
    *         visibility rectangle
    */
@@ -502,7 +562,7 @@
    * visibility rectangle. If the image is in the unclipped state, then the
    * visibility rectangle is assumed to be the rectangle which encompasses the
    * entire image, which has an upper-left vertex of (0,0).
-   *
+   * 
    * @return the vertical co-ordinate of the upper-left vertex of the image's
    *         visibility rectangle
    */
@@ -514,7 +574,7 @@
    * Gets the URL of the image. The URL that is returned is not necessarily the
    * URL that was passed in by the user. It may have been transformed to an
    * absolute URL.
-   *
+   * 
    * @return the image URL
    */
   public String getUrl() {
@@ -525,16 +585,27 @@
    * Gets the width of the image. When the image is in the unclipped state, the
    * width of the image is not known until the image has been loaded (i.e. load
    * event has been fired for the image).
-   *
+   * 
    * @return the width of the image, or 0 if the width is unknown
    */
   public int getWidth() {
     return state.getWidth(this);
   }
 
+  @Override
+  public void onBrowserEvent(Event event) {
+    // We have to clear the unhandled event before firing handlers because the
+    // handlers could trigger onLoad, which would refire the event.
+    if (event.getTypeInt() == Event.ONLOAD) {
+      clearUnhandledEvent();
+    }
+
+    super.onBrowserEvent(event);
+  }
+
   /**
-   * @deprecated Use the {@link HandlerRegistration#removeHandler} method on
-   * the object returned by {@link #addClickHandler} instead
+   * @deprecated Use the {@link HandlerRegistration#removeHandler} method on the
+   *             object returned by {@link #addClickHandler} instead
    */
   @Deprecated
   public void removeClickListener(ClickListener listener) {
@@ -542,8 +613,8 @@
   }
 
   /**
-   * @deprecated Use the {@link HandlerRegistration#removeHandler}
-   * method on the object returned by an add*Handler method instead
+   * @deprecated Use the {@link HandlerRegistration#removeHandler} method on the
+   *             object returned by an add*Handler method instead
    */
   @Deprecated
   public void removeLoadListener(LoadListener listener) {
@@ -551,8 +622,8 @@
   }
 
   /**
-   * @deprecated Use the {@link HandlerRegistration#removeHandler}
-   * method on the object returned by an add*Handler method instead
+   * @deprecated Use the {@link HandlerRegistration#removeHandler} method on the
+   *             object returned by an add*Handler method instead
    */
   @Deprecated
   public void removeMouseListener(MouseListener listener) {
@@ -560,9 +631,8 @@
   }
 
   /**
-   * @deprecated Use the {@link HandlerRegistration#removeHandler}
-   * method on the object returned by {@link #addMouseWheelHandler}
-   * instead
+   * @deprecated Use the {@link HandlerRegistration#removeHandler} method on the
+   *             object returned by {@link #addMouseWheelHandler} instead
    */
   @Deprecated
   public void removeMouseWheelListener(MouseWheelListener listener) {
@@ -576,7 +646,7 @@
    * image's current url or current visibility rectangle co-ordinates. If the
    * image is currently in the unclipped state, a call to this method will cause
    * a transition to the clipped state.
-   *
+   * 
    * @param resource the ImageResource to display
    */
   public void setResource(ImageResource resource) {
@@ -589,7 +659,7 @@
    * state, a call to this method will cause a transition of the image to the
    * unclipped state. Regardless of whether or not the image is in the clipped
    * or unclipped state, a load event will be fired.
-   *
+   * 
    * @param url the image URL
    */
   public void setUrl(String url) {
@@ -603,7 +673,7 @@
    * visibility rectangle co-ordinates. If the image is currently in the
    * unclipped state, a call to this method will cause a transition to the
    * clipped state.
-   *
+   * 
    * @param url the image URL
    * @param left the horizontal coordinate of the upper-left vertex of the
    *          visibility rectangle
@@ -626,7 +696,7 @@
    * is in the unclipped state, a call to this method will cause a transition of
    * the image to the clipped state. This transition will cause a load event to
    * fire.
-   *
+   * 
    * @param left the horizontal coordinate of the upper-left vertex of the
    *          visibility rectangle
    * @param top the vertical coordinate of the upper-left vertex of the
@@ -638,11 +708,25 @@
     state.setVisibleRect(this, left, top, width, height);
   }
 
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+
+    // Issue 863: the state may need to fire a synthetic event if the native
+    // onload event fired while the image was detached.
+    state.onLoad(this);
+  }
+
   private void changeState(State newState) {
     state = newState;
   }
 
-  private ImageElement getImageElement() {
-    return getElement().cast();
+  /**
+   * Clear the unhandled event.
+   */
+  private void clearUnhandledEvent() {
+    if (state != null) {
+      state.getImageElement(this).setPropertyString(UNHANDLED_EVENT_ATTR, "");
+    }
   }
 }
diff --git a/user/src/com/google/gwt/user/client/ui/impl/ClippedImageImpl.java b/user/src/com/google/gwt/user/client/ui/impl/ClippedImageImpl.java
index 4e78056..9d112af 100644
--- a/user/src/com/google/gwt/user/client/ui/impl/ClippedImageImpl.java
+++ b/user/src/com/google/gwt/user/client/ui/impl/ClippedImageImpl.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
@@ -18,22 +18,19 @@
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.dom.client.Document;
 import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.NativeEvent;
-import com.google.gwt.user.client.Command;
-import com.google.gwt.user.client.DeferredCommand;
 import com.google.gwt.user.client.ui.Image;
 
 /**
  * Uses a combination of a clear image and a background image to clip all except
  * a desired portion of an underlying image.
- *
+ * 
  * Do not use this class - it is used for implementation only, and its methods
  * may change in the future.
  */
 public class ClippedImageImpl {
 
   public void adjust(Element img, String url, int left, int top, int width,
-                     int height) {
+      int height) {
     String style = "url(" + url + ") no-repeat " + (-left + "px ")
         + (-top + "px");
     img.getStyle().setProperty("background", style);
@@ -42,7 +39,7 @@
   }
 
   public Element createStructure(String url, int left, int top, int width,
-                                 int height) {
+      int height) {
     Element tmp = Document.get().createSpanElement();
     tmp.setInnerHTML(getHTML(url, left, top, width, height));
     return tmp.getFirstChildElement();
@@ -53,31 +50,15 @@
         + "px; background: url(" + url + ") no-repeat " + (-left + "px ")
         + (-top + "px");
 
-    String clippedImgHtml = "<img src='" + GWT.getModuleBaseURL() +
-        "clear.cache.gif' style='" + style + "' border='0'>";
+    String clippedImgHtml = "<img "
+        + "onload='this.__gwtLastUnhandledEvent=\"load\";' src='"
+        + GWT.getModuleBaseURL() + "clear.cache.gif' style='" + style
+        + "' border='0'>";
 
     return clippedImgHtml;
   }
 
-  public void fireSyntheticLoadEvent(final Image image) {
-    /*
-     * We need to synthesize a load event, because the native events that are
-     * fired would correspond to the loading of clear.cache.gif, which is
-     * incorrect. A native event would not even fire in Internet Explorer,
-     * because the root element is a wrapper element around the <img> element.
-     * Since we are synthesizing a load event, we do not need to sink the
-     * onload event.
-     * 
-     * We use a deferred command here to simulate the native version of the
-     * load event as closely as possible. In the native event case, it is
-     * unlikely that a second load event would occur while you are in the load
-     * event handler.
-     */
-    DeferredCommand.addCommand(new Command() {
-      public void execute() {
-        NativeEvent evt = Document.get().createLoadEvent();
-        image.getElement().dispatchEvent(evt);
-      }
-    });
+  public Element getImgElement(Image image) {
+    return image.getElement();
   }
 }
diff --git a/user/src/com/google/gwt/user/client/ui/impl/ClippedImageImplIE6.java b/user/src/com/google/gwt/user/client/ui/impl/ClippedImageImplIE6.java
index a06a5ce..9341f34 100644
--- a/user/src/com/google/gwt/user/client/ui/impl/ClippedImageImplIE6.java
+++ b/user/src/com/google/gwt/user/client/ui/impl/ClippedImageImplIE6.java
@@ -16,11 +16,7 @@
 package com.google.gwt.user.client.ui.impl;
 
 import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.Document;
 import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.NativeEvent;
-import com.google.gwt.user.client.Command;
-import com.google.gwt.user.client.DeferredCommand;
 import com.google.gwt.user.client.Event;
 import com.google.gwt.user.client.ui.Image;
 
@@ -113,25 +109,6 @@
     return clipper;
   }
 
-  public void fireSyntheticLoadEvent(final Image image) {
-    if (!isIE6) {
-      super.fireSyntheticLoadEvent(image);
-      return;
-    }
-    
-    // This is the same as the superclass' implementation, except that it
-    // explicitly checks for the 'clipper' element, and dispatches the event
-    // on the img (you can't dispatch events on the clipper).
-    DeferredCommand.addCommand(new Command() {
-      public void execute() {
-        NativeEvent evt = Document.get().createLoadEvent();
-        Element clipper = image.getElement();
-        Element img = clipper.getFirstChildElement();
-        img.dispatchEvent(evt);
-      }
-    });
-  }
-
   @Override
   public String getHTML(String url, int left, int top, int width, int height) {
     if (!isIE6) {
@@ -158,7 +135,7 @@
      */
     String clippedImgHtml = "<gwt:clipper style=\""
         + clipperStyle
-        + "\"><img src='"
+        + "\"><img onload='this.__gwtLastUnhandledEvent=\"load\";' src='"
         + moduleBaseUrlProtocol
         + "' onerror='if(window.__gwt_transparentImgHandler)window.__gwt_transparentImgHandler(this);else this.src=\"" + GWT.getModuleBaseURL() + "clear.cache.gif\"' style=\""
         + imgStyle + "\" width=" + (left + width) + " height=" + (top + height)
@@ -166,4 +143,12 @@
 
     return clippedImgHtml;
   }
+
+  @Override
+  public Element getImgElement(Image image) {
+    if (!isIE6) {
+      return super.getImgElement(image);
+    }
+    return image.getElement().getFirstChildElement();
+  }
 }
diff --git a/user/test/com/google/gwt/resources/client/ImageResourceTest.java b/user/test/com/google/gwt/resources/client/ImageResourceTest.java
index 9da158a..1f4e3c8 100644
--- a/user/test/com/google/gwt/resources/client/ImageResourceTest.java
+++ b/user/test/com/google/gwt/resources/client/ImageResourceTest.java
@@ -92,13 +92,7 @@
     assertFalse(a.getURL().equals(r.i16x16().getURL()));
   }
 
-  /**
-   * Issue 863: Image.onload event does not fire on Internet Explorer when image
-   * is in cache.
-   * 
-   * TODO(jlabanca): Reenable this test after fixing the issue.
-   */
-  public void disabledTestDedup() {
+  public void testDedup() {
     Resources r = GWT.create(Resources.class);
 
     ImageResource a = r.i64x64();
diff --git a/user/test/com/google/gwt/uibinder/test/client/UiBinderTest.java b/user/test/com/google/gwt/uibinder/test/client/UiBinderTest.java
index 623a29b..6ff73ff 100644
--- a/user/test/com/google/gwt/uibinder/test/client/UiBinderTest.java
+++ b/user/test/com/google/gwt/uibinder/test/client/UiBinderTest.java
@@ -41,6 +41,7 @@
 /**
  * Functional test of UiBinder.
  */
+@DoNotRunWith({Platform.HtmlUnit})
 public class UiBinderTest extends GWTTestCase {
   private WidgetBasedUi widgetUi;
   private DomBasedUi domUi;
diff --git a/user/test/com/google/gwt/user/client/ui/ImageTest.java b/user/test/com/google/gwt/user/client/ui/ImageTest.java
index 42ac527..f3e05fe 100644
--- a/user/test/com/google/gwt/user/client/ui/ImageTest.java
+++ b/user/test/com/google/gwt/user/client/ui/ImageTest.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
@@ -27,12 +27,13 @@
 import com.google.gwt.junit.client.GWTTestCase;
 import com.google.gwt.resources.client.ClientBundle;
 import com.google.gwt.resources.client.ImageResource;
+import com.google.gwt.user.client.Timer;
 
 /**
  * Tests for the Image widget. Images in both clipped mode and unclipped mode
  * are tested, along with the transitions between the two modes.
  */
-@SuppressWarnings("deprecation")
+@DoNotRunWith({Platform.HtmlUnit})
 public class ImageTest extends GWTTestCase {
   interface Bundle extends ClientBundle {
     ImageResource prettyPiccy();
@@ -50,6 +51,24 @@
     }
   }
 
+  private abstract static class TestLoadHandler implements LoadHandler {
+    private boolean finished = false;
+
+    /**
+     * Mark the test as finished.
+     */
+    public void finish() {
+      finished = true;
+    }
+
+    /**
+     * @return true if the test has finished
+     */
+    public boolean isFinished() {
+      return finished;
+    }
+  }
+
   @Deprecated
   private abstract static class TestLoadListener implements LoadListener {
     private boolean finished = false;
@@ -82,7 +101,7 @@
    * Helper method that allows us to 'peek' at the private <code>state</code>
    * field in the Image object, and call the <code>state.getStateName()</code>
    * method.
-   *
+   * 
    * @param image The image instance
    * @return "unclipped" if image is in the unclipped state, or "clipped" if the
    *         image is in the clipped state
@@ -96,25 +115,24 @@
 
   private int firedLoad;
 
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.user.UserTest";
+  }
+
   /**
    * Tests the transition from the clipped state to the unclipped state.
-   *
-   * Disabled because of issue #863 & #864. It fails intermittently in linux
-   * hosted mode tests.
    */
-  public void disabledTestChangeClippedImageToUnclipped() {
+  public void testChangeClippedImageToUnclipped() {
     final Image image = new Image("counting-forwards.png", 12, 13, 8, 8);
     assertEquals("clipped", getCurrentImageStateName(image));
 
     delayTestFinish(5000);
-    image.addLoadListener(new LoadListener() {
+    image.addErrorHandler(new TestErrorHandler(image));
+    image.addLoadHandler(new LoadHandler() {
       private int onLoadEventCount = 0;
 
-      public void onError(Widget sender) {
-        fail("The image " + ((Image) sender).getUrl() + " failed to load.");
-      }
-
-      public void onLoad(Widget sender) {
+      public void onLoad(LoadEvent event) {
         ++onLoadEventCount;
         if (onLoadEventCount == 1) { // Set the url after the first image loads
           image.setUrl("counting-forwards.png");
@@ -134,22 +152,17 @@
 
   /**
    * Tests the transition from the unclipped state to the clipped state.
-   *
-   * Disabled because of issue #863.
    */
-  public void disabledTestChangeImageToClipped() {
+  public void testChangeImageToClipped() {
     final Image image = new Image("counting-forwards.png");
     assertEquals("unclipped", getCurrentImageStateName(image));
 
     delayTestFinish(5000);
-    image.addLoadListener(new LoadListener() {
+    image.addErrorHandler(new TestErrorHandler(image));
+    image.addLoadHandler(new LoadHandler() {
       private int onLoadEventCount = 0;
 
-      public void onError(Widget sender) {
-        fail("The image " + ((Image) sender).getUrl() + " failed to load.");
-      }
-
-      public void onLoad(Widget sender) {
+      public void onLoad(LoadEvent event) {
         if (getCurrentImageStateName(image).equals("unclipped")) {
           image.setVisibleRect(12, 13, 8, 8);
         }
@@ -170,21 +183,16 @@
 
   /**
    * Tests the creation of an image in unclipped mode.
-   *
-   * Disabled because of issue #863 & #864.
    */
-  public void disabledTestCreateImage() {
+  public void testCreateImage() {
     final Image image = new Image("counting-forwards.png");
 
     delayTestFinish(5000);
-    image.addLoadListener(new LoadListener() {
+    image.addErrorHandler(new TestErrorHandler(image));
+    image.addLoadHandler(new LoadHandler() {
       private int onLoadEventCount = 0;
 
-      public void onError(Widget sender) {
-        fail("The image " + ((Image) sender).getUrl() + " failed to load.");
-      }
-
-      public void onLoad(Widget sender) {
+      public void onLoad(LoadEvent event) {
         if (++onLoadEventCount == 1) {
           assertEquals(32, image.getWidth());
           assertEquals(32, image.getHeight());
@@ -200,24 +208,40 @@
   }
 
   /**
+   * Tests the creation of an image that does not exist.
+   */
+  public void testCreateImageWithError() {
+    final Image image = new Image("imageDoesNotExist.png");
+
+    delayTestFinish(5000);
+    image.addErrorHandler(new ErrorHandler() {
+      public void onError(ErrorEvent event) {
+        finishTest();
+      }
+    });
+    image.addLoadHandler(new LoadHandler() {
+      public void onLoad(LoadEvent event) {
+        fail("The image " + image.getUrl() + " should have failed to load.");
+      }
+    });
+
+    RootPanel.get().add(image);
+  }
+
+  /**
    * Tests the firing of onload events when
    * {@link com.google.gwt.user.client.ui.Image#setUrl(String)} is called on an
    * unclipped image.
-   *
-   * Disabled because of issue #863
    */
-  public void disabledTestSetUrlAndLoadEventsOnUnclippedImage() {
+  public void testSetUrlAndLoadEventsOnUnclippedImage() {
     final Image image = new Image();
 
     delayTestFinish(5000);
-    image.addLoadListener(new LoadListener() {
+    image.addErrorHandler(new TestErrorHandler(image));
+    image.addLoadHandler(new LoadHandler() {
       private int onLoadEventCount = 0;
 
-      public void onError(Widget sender) {
-        fail("The image " + ((Image) sender).getUrl() + " failed to load.");
-      }
-
-      public void onLoad(Widget sender) {
+      public void onLoad(LoadEvent event) {
         if (++onLoadEventCount == 2) {
           finishTest();
         } else {
@@ -232,23 +256,19 @@
 
   /**
    * Tests the behavior of
-   * <code>setUrlAndVisibleRect(String, int, int, int, int)</code> method on
-   * an unclipped image, which causes a state transition to the clipped state.
-   *
-   * Disabled because of issue #863.
+   * <code>setUrlAndVisibleRect(String, int, int, int, int)</code> method on an
+   * unclipped image, which causes a state transition to the clipped state.
    */
-  public void disabledTestSetUrlAndVisibleRectOnUnclippedImage() {
+  public void testSetUrlAndVisibleRectOnUnclippedImage() {
     final Image image = new Image("counting-backwards.png");
+    assertEquals("unclipped", getCurrentImageStateName(image));
 
     delayTestFinish(5000);
-    image.addLoadListener(new LoadListener() {
+    image.addErrorHandler(new TestErrorHandler(image));
+    image.addLoadHandler(new LoadHandler() {
       private int onLoadEventCount = 0;
 
-      public void onError(Widget sender) {
-        fail("The image " + ((Image) sender).getUrl() + " failed to load.");
-      }
-
-      public void onLoad(Widget sender) {
+      public void onLoad(LoadEvent event) {
         if (getCurrentImageStateName(image).equals("unclipped")) {
           image.setUrlAndVisibleRect("counting-forwards.png", 0, 16, 16, 16);
         }
@@ -265,19 +285,13 @@
     });
 
     RootPanel.get().add(image);
-    assertEquals("unclipped", getCurrentImageStateName(image));
-  }
-
-  @Override
-  public String getModuleName() {
-    return "com.google.gwt.user.UserTest";
   }
 
   /**
    * Tests the creation of an image in clipped mode.
    */
-  @DoNotRunWith({Platform.HtmlUnit})
-  public void disabledTestCreateClippedImage() {
+  @SuppressWarnings("deprecation")
+  public void testCreateClippedImage() {
     final Image image = new Image("counting-forwards.png", 16, 16, 16, 16);
 
     delayTestFinish(5000);
@@ -343,6 +357,83 @@
     assertEquals(1, firedError);
   }
 
+  /**
+   * Verify that detaching and reattaching an image in a handler does not fire a
+   * second onload event.
+   */
+  public void testNoEventOnReattachInHandler() {
+    final Image image = new Image("counting-forwards.png");
+
+    delayTestFinish(5000);
+    image.addErrorHandler(new TestErrorHandler(image));
+    image.addLoadHandler(new LoadHandler() {
+      private int onLoadEventCount = 0;
+
+      public void onLoad(LoadEvent event) {
+        if (++onLoadEventCount == 1) {
+          RootPanel.get().remove(image);
+          RootPanel.get().add(image);
+          // The extra onLoad would will fire synchronously before finishTest().
+          finishTest();
+        } else {
+          fail("onLoad fired on reattach.");
+        }
+      }
+    });
+
+    RootPanel.get().add(image);
+  }
+
+  public void testOneEventOnly() {
+    final Image image = new Image("counting-forwards.png");
+
+    final TestLoadHandler loadHandler = new TestLoadHandler() {
+      public void onLoad(LoadEvent event) {
+        if (isFinished()) {
+          fail("LoadHandler fired multiple times.");
+        }
+        finish();
+      }
+    };
+    delayTestFinish(6000);
+    new Timer() {
+      @Override
+      public void run() {
+        assertTrue(loadHandler.isFinished());
+        finishTest();
+      }
+    }.schedule(5000);
+    image.addErrorHandler(new TestErrorHandler(image));
+    image.addLoadHandler(loadHandler);
+
+    RootPanel.get().add(image);
+  }
+
+  public void testOneEventOnlyClippedImage() {
+    final Image image = new Image("counting-forwards.png", 12, 13, 8, 8);
+
+    final TestLoadHandler loadHandler = new TestLoadHandler() {
+      public void onLoad(LoadEvent event) {
+        if (isFinished()) {
+          fail("LoadHandler fired multiple times.");
+        }
+        finish();
+      }
+    };
+    delayTestFinish(6000);
+    new Timer() {
+      @Override
+      public void run() {
+        assertTrue(loadHandler.isFinished());
+        finishTest();
+      }
+    }.schedule(5000);
+    image.addErrorHandler(new TestErrorHandler(image));
+    image.addLoadHandler(loadHandler);
+
+    RootPanel.get().add(image);
+  }
+  
   public void testResourceConstructor() {
     Bundle b = GWT.create(Bundle.class);
     Image image = new Image(b.prettyPiccy());
@@ -361,8 +452,8 @@
    * {@link com.google.gwt.user.client.ui.Image#setUrlAndVisibleRect(String,int,int,int,int)}
    * on a clipped image.
    */
-  @DoNotRunWith({Platform.HtmlUnit})
-  public void disabledTestSetUrlAndVisibleRectOnClippedImage() {
+  @SuppressWarnings("deprecation")
+  public void testSetUrlAndVisibleRectOnClippedImage() {
     final Image image = new Image("counting-backwards.png", 12, 12, 12, 12);
     delayTestFinish(5000);
 
@@ -412,8 +503,8 @@
    * {@link com.google.gwt.user.client.ui.Image#setVisibleRect(int,int,int,int)}
    * on a clipped image.
    */
-  @DoNotRunWith({Platform.HtmlUnit})
-  public void disabledTestSetVisibleRectAndLoadEventsOnClippedImage() {
+  @SuppressWarnings("deprecation")
+  public void testSetVisibleRectAndLoadEventsOnClippedImage() {
     final Image image = new Image("counting-backwards.png", 16, 16, 16, 16);
 
     delayTestFinish(5000);
diff --git a/user/test/com/google/gwt/user/client/ui/TreeItemTest.java b/user/test/com/google/gwt/user/client/ui/TreeItemTest.java
index 8e037d8..679f4c2 100644
--- a/user/test/com/google/gwt/user/client/ui/TreeItemTest.java
+++ b/user/test/com/google/gwt/user/client/ui/TreeItemTest.java
@@ -15,6 +15,8 @@
  */
 package com.google.gwt.user.client.ui;
 
+import com.google.gwt.junit.DoNotRunWith;
+import com.google.gwt.junit.Platform;
 import com.google.gwt.junit.client.GWTTestCase;
 
 /**
@@ -39,6 +41,7 @@
     assertEquals("Test", widget.getText());
   }
 
+  @DoNotRunWith({Platform.HtmlUnit})
   public void testSetWidgetNullWithError() {
     // Create a widget that will throw an exception onUnload.
     BadWidget badWidget = new BadWidget();
diff --git a/user/test/com/google/gwt/user/client/ui/TreeTest.java b/user/test/com/google/gwt/user/client/ui/TreeTest.java
index b581cf9..f6a520f 100644
--- a/user/test/com/google/gwt/user/client/ui/TreeTest.java
+++ b/user/test/com/google/gwt/user/client/ui/TreeTest.java
@@ -15,6 +15,8 @@
  */
 package com.google.gwt.user.client.ui;
 
+import com.google.gwt.junit.DoNotRunWith;
+import com.google.gwt.junit.Platform;
 import com.google.gwt.junit.client.GWTTestCase;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Element;
@@ -37,6 +39,7 @@
     return "com.google.gwt.user.DebugTest";
   }
 
+  @DoNotRunWith({Platform.HtmlUnit})
   public void testAttachDetachOrder() {
     HasWidgetsTester.testAll(new Tree(), new Adder(), true);
   }