Events that occur in a RichTextArea now go through DOM#dispatchEvent(), so all of the DOM event methods are available.  I fixed a bunch of bugs on multiple browsers related to setting focus immediately after attaching the RichTextArea.

Patch by: jlabanca
Review by: jgw
Issue: 3133, 3176, 3503



git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@5742 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/user/RichText.gwt.xml b/user/src/com/google/gwt/user/RichText.gwt.xml
index 76dba69..a2edcd1 100644
--- a/user/src/com/google/gwt/user/RichText.gwt.xml
+++ b/user/src/com/google/gwt/user/RichText.gwt.xml
@@ -34,10 +34,15 @@
     class="com.google.gwt.user.client.ui.impl.RichTextAreaImplMozilla">
     <when-type-is
       class="com.google.gwt.user.client.ui.impl.RichTextAreaImpl" />
-    <any>
-      <when-property-is name="user.agent" value="gecko1_8" />
-      <when-property-is name="user.agent" value="gecko" />
-    </any>
+    <when-property-is name="user.agent" value="gecko1_8" />
+  </replace-with>
+
+  <!-- Old Mozilla-specific implementation -->
+  <replace-with
+    class="com.google.gwt.user.client.ui.impl.RichTextAreaImplOldMozilla">
+    <when-type-is
+      class="com.google.gwt.user.client.ui.impl.RichTextAreaImpl" />
+    <when-property-is name="user.agent" value="gecko" />
   </replace-with>
 
   <!-- Safari-specific implementation -->
diff --git a/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplIE6.java b/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplIE6.java
index 338359a..4c171f8 100644
--- a/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplIE6.java
+++ b/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplIE6.java
@@ -33,7 +33,7 @@
   @Override
   public native void initElement() /*-{
     var _this = this;
-    _this.@com.google.gwt.user.client.ui.impl.RichTextAreaImplStandard::initializing = true;
+    _this.@com.google.gwt.user.client.ui.impl.RichTextAreaImplStandard::onElementInitializing()();
 
     setTimeout(function() {
       if (_this.@com.google.gwt.user.client.ui.impl.RichTextAreaImplStandard::initializing == false) {
@@ -96,7 +96,7 @@
         // Weird: this code has the context of the script frame, but we need the
         // event from the edit iframe's window.
         var evt = elem.contentWindow.event;
-        elem.__listener.@com.google.gwt.user.client.ui.Widget::onBrowserEvent(Lcom/google/gwt/user/client/Event;)(evt);
+        @com.google.gwt.user.client.DOM::dispatchEvent(Lcom/google/gwt/user/client/Event;Lcom/google/gwt/user/client/Element;Lcom/google/gwt/user/client/EventListener;)(evt, elem, elem.__listener);
       }
     };
 
diff --git a/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplMozilla.java b/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplMozilla.java
index a43219e..d5f8c23 100644
--- a/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplMozilla.java
+++ b/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplMozilla.java
@@ -20,22 +20,26 @@
  */
 public class RichTextAreaImplMozilla extends RichTextAreaImplStandard {
 
+  /**
+   * Indicates that the RichTextArea has never received focus after
+   * initialization.
+   */
+  boolean isFirstFocus;
+
   @Override
   public native void initElement() /*-{
     // Mozilla doesn't allow designMode to be set reliably until the iframe is
     // fully loaded.
     var _this = this;
     var iframe = _this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem;
-    _this.@com.google.gwt.user.client.ui.impl.RichTextAreaImplStandard::initializing = true;
+    _this.@com.google.gwt.user.client.ui.impl.RichTextAreaImplStandard::onElementInitializing()();
+    _this.@com.google.gwt.user.client.ui.impl.RichTextAreaImplMozilla::isFirstFocus = true;
 
     iframe.onload = function() {
       // Some Mozillae have the nasty habit of calling onload again when you set
       // designMode, so let's avoid doing it more than once.
       iframe.onload = null;
 
-      // Send notification that the iframe has finished loading.
-      _this.@com.google.gwt.user.client.ui.impl.RichTextAreaImplStandard::onElementInitialized()();
-
       // Don't set designMode until the RTA is targeted by an event. This is
       // necessary because editing won't work on Mozilla if the iframe is
       // *hidden, but attached*. Waiting for an event gets around this issue.
@@ -47,17 +51,16 @@
         iframe.contentWindow.onmouseover = null;
         iframe.contentWindow.document.designMode = 'On';
       };
-      
+
       // Issue 1441: we also need to catch the onmouseover event because focus
       // occurs after mouse down, so the cursor will not appear until the user
       // clicks twice, making the RichTextArea look uneditable. Catching the
       // mouseover event allows us to set design mode earlier. The focus event
       // is still needed to handle tab selection.
-      iframe.contentWindow.onmouseover = function() {
-        iframe.contentWindow.onfocus = null;
-        iframe.contentWindow.onmouseover = null;
-        iframe.contentWindow.document.designMode = 'On';
-      };
+      iframe.contentWindow.onmouseover = iframe.contentWindow.onfocus;
+
+      // Send notification that the iframe has finished loading.
+      _this.@com.google.gwt.user.client.ui.impl.RichTextAreaImplStandard::onElementInitialized()();
     };
   }-*/;
 
@@ -67,4 +70,32 @@
     // does what we actually want.
     execCommand("HiliteColor", color);
   }
+
+  /**
+   * Firefox will not display the caret the first time a RichTextArea is
+   * programmatically focused, so we need to focus, blur, and refocus the
+   * RichTextArea. This only needs to be done the first time after the
+   * RichTextArea is initialized. See issue 3503.
+   */
+  protected native void setFirstFocusImpl() /*-{
+    var elem = this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem;
+    var wnd = elem.contentWindow;
+    wnd.removeEventListener('focus', elem.__gwt_focusHandler, true);
+    wnd.removeEventListener('blur', elem.__gwt_blurHandler, true);
+    wnd.focus();
+    wnd.blur();
+    wnd.addEventListener('focus', elem.__gwt_focusHandler, true);
+    wnd.addEventListener('blur', elem.__gwt_blurHandler, true);
+    wnd.focus();
+  }-*/;
+
+  @Override
+  protected void setFocusImpl(boolean focused) {
+    if (isFirstFocus) {
+      isFirstFocus = false;
+      setFirstFocusImpl();
+    } else {
+      super.setFocusImpl(focused);
+    }
+  }
 }
diff --git a/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplOldMozilla.java b/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplOldMozilla.java
new file mode 100644
index 0000000..a5def16
--- /dev/null
+++ b/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplOldMozilla.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2009 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.user.client.ui.impl;
+
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DeferredCommand;
+
+/**
+ * Old Mozilla-specific implementation of rich-text editing.
+ */
+public class RichTextAreaImplOldMozilla extends RichTextAreaImplMozilla {
+  /**
+   * The content window cannot be focused immediately after the content window
+   * has been loaded, so we need to wait for an additional deferred command.
+   */
+  @Override
+  protected void onElementInitialized() {
+    DeferredCommand.addCommand(new Command() {
+      public void execute() {
+        RichTextAreaImplOldMozilla.super.onElementInitialized();
+      }
+    });
+  }
+
+  /**
+   * Old Mozilla does not support blur on the content window of an iframe.
+   */
+  @Override
+  protected void setFirstFocusImpl() {
+    setFocusImpl(true);
+  }
+
+  @Override
+  protected void setFocusImpl(boolean focused) {
+    if (focused) {
+      super.setFocusImpl(focused);
+    }
+  }
+}
diff --git a/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplOpera.java b/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplOpera.java
index 2b4d045..3aec890 100644
--- a/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplOpera.java
+++ b/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplOpera.java
@@ -28,7 +28,7 @@
   }
 
   @Override
-  public native void setFocus(boolean focused) /*-{
+  protected native void setFocusImpl(boolean focused) /*-{
     // Opera needs the *iframe* focused, not its window.
     if (focused) {
       this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.focus();
diff --git a/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplSafari.java b/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplSafari.java
index 7d94ad6..c2e091b 100644
--- a/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplSafari.java
+++ b/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplSafari.java
@@ -39,7 +39,7 @@
 
     elem.__gwt_handler = function(evt) {
       if (elem.__listener) {
-        elem.__listener.@com.google.gwt.user.client.ui.Widget::onBrowserEvent(Lcom/google/gwt/user/client/Event;)(evt);
+        @com.google.gwt.user.client.DOM::dispatchEvent(Lcom/google/gwt/user/client/Event;Lcom/google/gwt/user/client/Element;Lcom/google/gwt/user/client/EventListener;)(evt, elem, elem.__listener);
       }
     };
 
@@ -57,15 +57,15 @@
     // doesn't work on the iframe element (at least not for focus/blur). Don't
     // dispatch through the normal handler method, as some of the querying we do
     // there interferes with focus.
-    elem.onfocus = function(evt) {
+    wnd.onfocus = function(evt) {
       if (elem.__listener) {
-        elem.__listener.@com.google.gwt.user.client.ui.Widget::onBrowserEvent(Lcom/google/gwt/user/client/Event;)(evt);
+        @com.google.gwt.user.client.DOM::dispatchEvent(Lcom/google/gwt/user/client/Event;Lcom/google/gwt/user/client/Element;Lcom/google/gwt/user/client/EventListener;)(evt, elem, elem.__listener);
       }
     };
 
-    elem.onblur = function(evt) {
+    wnd.onblur = function(evt) {
       if (elem.__listener) {
-        elem.__listener.@com.google.gwt.user.client.ui.Widget::onBrowserEvent(Lcom/google/gwt/user/client/Event;)(evt);
+        @com.google.gwt.user.client.DOM::dispatchEvent(Lcom/google/gwt/user/client/Event;Lcom/google/gwt/user/client/Element;Lcom/google/gwt/user/client/EventListener;)(evt, elem, elem.__listener);
       }
     };
   }-*/;
diff --git a/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplStandard.java b/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplStandard.java
index d4e99d1..5001f68 100644
--- a/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplStandard.java
+++ b/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplStandard.java
@@ -52,6 +52,11 @@
   protected boolean initializing;
 
   /**
+   * Indicates that the text area should be focused as soon as it is loaded.
+   */
+  private boolean isPendingFocus;
+
+  /**
    * True when the element has been attached.
    */
   private boolean isReady;
@@ -89,7 +94,7 @@
     // the iframe becomes attached to the DOM. Any non-zero timeout will do
     // just fine.
     var _this = this;
-    _this.@com.google.gwt.user.client.ui.impl.RichTextAreaImplStandard::initializing = true;
+    _this.@com.google.gwt.user.client.ui.impl.RichTextAreaImplStandard::onElementInitializing()();
     setTimeout(function() {
       // Turn on design mode.
       _this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.contentWindow.document.designMode = 'On';
@@ -172,13 +177,15 @@
   }
 
   @Override
-  public native void setFocus(boolean focused) /*-{
-    if (focused) {
-      this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.contentWindow.focus();
+  public void setFocus(boolean focused) {
+    if (initializing) {
+      // Issue 3503: if we focus before the iframe is in design mode, the text
+      // caret will not appear.
+      isPendingFocus = focused;
     } else {
-      this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.contentWindow.blur();
-    } 
-  }-*/;
+      setFocusImpl(focused);
+    }
+  }
 
   public void setFontName(String name) {
     execCommand("FontName", name);
@@ -288,7 +295,7 @@
 
     elem.__gwt_handler = function(evt) {
       if (elem.__listener) {
-        elem.__listener.@com.google.gwt.user.client.ui.Widget::onBrowserEvent(Lcom/google/gwt/user/client/Event;)(evt);
+        @com.google.gwt.user.client.DOM::dispatchEvent(Lcom/google/gwt/user/client/Event;Lcom/google/gwt/user/client/Element;Lcom/google/gwt/user/client/EventListener;)(evt, elem, elem.__listener);
       }
     };
 
@@ -341,8 +348,27 @@
       setHTMLImpl(DOM.getInnerHTML(beforeInitPlaceholder));
       beforeInitPlaceholder = null;
     }
+
+    // Focus on the element now that it is initialized
+    if (isPendingFocus) {
+      isPendingFocus = false;
+      setFocus(true);
+    }
   }
 
+  protected void onElementInitializing() {
+    initializing = true;
+    isPendingFocus = false;
+  }
+
+  protected native void setFocusImpl(boolean focused) /*-{
+    if (focused) {
+      this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.contentWindow.focus();
+    } else {
+      this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.contentWindow.blur();
+    }
+  }-*/;
+
   protected native void setHTMLImpl(String html) /*-{
     this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.contentWindow.document.body.innerHTML = html;
   }-*/;
diff --git a/user/test/com/google/gwt/user/client/ui/RichTextAreaTest.java b/user/test/com/google/gwt/user/client/ui/RichTextAreaTest.java
index fc4ecd0..a57fb70 100644
--- a/user/test/com/google/gwt/user/client/ui/RichTextAreaTest.java
+++ b/user/test/com/google/gwt/user/client/ui/RichTextAreaTest.java
@@ -16,10 +16,21 @@
 package com.google.gwt.user.client.ui;
 
 import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Document;
+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.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
 import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
 import com.google.gwt.user.client.Timer;
 import com.google.gwt.user.client.ui.RichTextArea.BasicFormatter;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * Tests the {@link RichTextArea} widget.
  */
@@ -57,6 +68,57 @@
     }.schedule(500);
   }
 
+  public void testBlurAfterAttach() {
+    final RichTextArea rta = new RichTextArea();
+    final List<String> actual = new ArrayList<String>();
+    rta.addFocusHandler(new FocusHandler() {
+      public void onFocus(FocusEvent event) {
+        actual.add("test");
+      }
+    });
+    RootPanel.get().add(rta);
+    rta.setFocus(true);
+    rta.setFocus(false);
+
+    // This has to be done on a timer because the rta can take some time to
+    // finish initializing (on some browsers).
+    this.delayTestFinish(1000);
+    new Timer() {
+      @Override
+      public void run() {
+        assertEquals(0, actual.size());
+        RootPanel.get().remove(rta);
+        finishTest();
+      }
+    }.schedule(500);
+  }
+
+  public void testFocusAfterAttach() {
+    final RichTextArea rta = new RichTextArea();
+    final List<String> actual = new ArrayList<String>();
+    rta.addFocusHandler(new FocusHandler() {
+      public void onFocus(FocusEvent event) {
+        actual.add("test");
+      }
+    });
+    RootPanel.get().add(rta);
+    rta.setFocus(true);
+
+    // This has to be done on a timer because the rta can take some time to
+    // finish initializing (on some browsers).
+    this.delayTestFinish(1000);
+    new Timer() {
+      @Override
+      public void run() {
+        // IE focuses automatically, resulting in an extra event, so all we can
+        // test is that we got at least one focus.
+        assertTrue(actual.size() > 0);
+        RootPanel.get().remove(rta);
+        finishTest();
+      }
+    }.schedule(500);
+  }
+
   /**
    * Test that adding and removing an RTA before initialization completes
    * doesn't throw an exception.
@@ -149,6 +211,37 @@
   }
 
   /**
+   * Test that events are dispatched correctly to handlers.
+   */
+  public void testEventDispatch() {
+    final RichTextArea rta = new RichTextArea();
+    RootPanel.get().add(rta);
+
+    final List<String> actual = new ArrayList<String>();
+    rta.addClickHandler(new ClickHandler() {
+      public void onClick(ClickEvent event) {
+        assertNotNull(Event.getCurrentEvent());
+        actual.add("test");
+      }
+    });
+
+    // Fire a click event after the iframe is available
+    delayTestFinish(1000);
+    new Timer() {
+      @Override
+      public void run() {
+        assertEquals(0, actual.size());
+        NativeEvent event = getDocument(rta).createClickEvent(0, 0, 0, 0, 0,
+            false, false, false, false);
+        getBodyElement(rta).dispatchEvent(event);
+        assertEquals(1, actual.size());
+        RootPanel.get().remove(rta);
+        finishTest();
+      }
+    }.schedule(500);
+  }
+
+  /**
    * Test that a delayed set of HTML is reflected. Some platforms have timing
    * subtleties that need to be tested.
    */
@@ -221,4 +314,29 @@
     }.schedule(200);
     delayTestFinish(1000);
   }
+
+  /**
+   * Get the body element from a RichTextArea.
+   * 
+   * @param rta the {@link RichTextArea}
+   * @return the body element
+   */
+  private Element getBodyElement(RichTextArea rta) {
+    return getDocument(rta).getBody().cast();
+  }
+
+  /**
+   * Get the iframe's Document. This is useful for creating events, which must
+   * be created in the iframe's document to work correctly.
+   * 
+   * @param rta the {@link RichTextArea}
+   * @return the document element
+   */
+  private Document getDocument(RichTextArea rta) {
+    return getDocumentImpl(rta.getElement());
+  }
+
+  private native Document getDocumentImpl(Element iframe) /*-{
+      return iframe.contentWindow.document;
+    }-*/;
 }