Adds some assertions to RichTextArea to prevent users from using the formatters before the RichTextArea is initialized.

Patch by: jlabanca
Review by: jgw
Issue: 2749



git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@5637 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/user/client/ui/RichTextArea.java b/user/src/com/google/gwt/user/client/ui/RichTextArea.java
index 3bd17a9..b31780c 100644
--- a/user/src/com/google/gwt/user/client/ui/RichTextArea.java
+++ b/user/src/com/google/gwt/user/client/ui/RichTextArea.java
@@ -43,10 +43,19 @@
     SourcesMouseEvents, HasAllMouseHandlers {
 
   /**
+   * <p>
    * This interface is used to access basic formatting options, when available.
    * If the implementation supports basic formatting, then
    * {@link RichTextArea#getBasicFormatter()} will return an instance of this
    * class.
+   * </p>
+   * <p>
+   * The formatter will format the user selected text in the
+   * {@link RichTextArea}.  As a result, it will only work reliably if the
+   * {@link RichTextArea} is attached, visible to on the page, and has been
+   * focused at least once.  If you just want to initialize the content of
+   * the {@link RichTextArea}, use {@link RichTextArea#setHTML(String)} instead. 
+   * </p>
    */
   public interface BasicFormatter {
 
@@ -166,10 +175,19 @@
   }
 
   /**
+   * <p>
    * This interface is used to access full formatting options, when available.
    * If the implementation supports full formatting, then
    * {@link RichTextArea#getExtendedFormatter()} will return an instance of this
    * class.
+   * </p>
+   * <p>
+   * The formatter will format the user selected text in the
+   * {@link RichTextArea}.  As a result, it will only work reliably if the
+   * {@link RichTextArea} is attached, visible to on the page, and has been
+   * focused at least once.  If you just want to initialize the content of
+   * the {@link RichTextArea}, use {@link RichTextArea#setHTML(String)} instead. 
+   * </p>
    */
   public interface ExtendedFormatter extends BasicFormatter {
 
@@ -363,7 +381,9 @@
   }
 
   /**
-   * Gets the basic rich text formatting interface.
+   * Gets the basic rich text formatting interface.  Note that formatting can
+   * only be done when the {@link RichTextArea} is attached, visible on the
+   * page, and has been focused by the user.
    * 
    * @return <code>null</code> if basic formatting is not supported
    */
@@ -375,7 +395,9 @@
   }
 
   /**
-   * Gets the full rich text formatting interface.
+   * Gets the full rich text formatting interface.  Note that formatting can
+   * only be done when the {@link RichTextArea} is attached, visible on the
+   * page, and has been focused by the user.
    * 
    * @return <code>null</code> if full formatting is not supported
    */
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 fe9e8cf..338359a 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
@@ -142,9 +142,4 @@
       elem.contentWindow.onblur = null;
     }
   }-*/;
-  
-  @Override
-  boolean isRichEditingActive(Element elem) {
-    return true;
-  }
 }
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 4d2389b..1dd081a 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
@@ -15,6 +15,7 @@
  */
 package com.google.gwt.user.client.ui.impl;
 
+import com.google.gwt.core.client.JavaScriptException;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Element;
 import com.google.gwt.user.client.ui.RichTextArea;
@@ -28,6 +29,13 @@
     RichTextArea.BasicFormatter, RichTextArea.ExtendedFormatter {
 
   /**
+   * The message displayed when the formatter is used before the RichTextArea
+   * is initialized.
+   */
+  private static final String INACTIVE_MESSAGE = "RichTextArea formatters " +
+      "cannot be used until the RichTextArea is attached and focused.";
+
+  /**
    * Holds a cached copy of any user setHTML/setText actions until the real
    * text area is fully initialized.  Becomes <code>null</code> after init.
    */
@@ -41,6 +49,11 @@
    */
   protected boolean initializing;
 
+  /**
+   * True when the element has been attached.
+   */
+  private boolean isReady;
+
   @Override
   public native Element createElement() /*-{
     return $doc.createElement('iframe');
@@ -247,6 +260,8 @@
 
   @Override
   public void uninitElement() {
+    isReady = false;
+
     // Issue 1897: initElement uses a timeout, so its possible to call this
     // method after calling initElement, but before the event system is in
     // place.
@@ -325,6 +340,7 @@
       return;
     }
     initializing = false;
+    isReady = true;
 
     super.onElementInitialized();
 
@@ -366,11 +382,17 @@
   }-*/;
 
   void execCommand(String cmd, String param) {
-    if (isRichEditingActive(elem)) {
+    assert isReady : INACTIVE_MESSAGE;
+    if (isReady) {
       // When executing a command, focus the iframe first, since some commands
       // don't take properly when it's not focused.
       setFocus(true);
-      execCommandAssumingFocus(cmd, param);
+      try {
+        execCommandAssumingFocus(cmd, param);
+      } catch (JavaScriptException e) {
+        // In mozilla, editing throws a JS exception if the iframe is
+        // *hidden, but attached*.
+      }
     }
   }
 
@@ -378,19 +400,18 @@
     this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.contentWindow.document.execCommand(cmd, false, param);
   }-*/;
 
-  native boolean isRichEditingActive(Element e) /*-{
-    return ((e.contentWindow.document.designMode).toUpperCase()) == 'ON';
-  }-*/;
-
   boolean queryCommandState(String cmd) {
-    if (isRichEditingActive(elem)) {
+    if (isReady) {
       // When executing a command, focus the iframe first, since some commands
       // don't take properly when it's not focused.
       setFocus(true);
-      return queryCommandStateAssumingFocus(cmd);
-    } else {
-      return false;
+      try {
+        return queryCommandStateAssumingFocus(cmd);
+      } catch (JavaScriptException e) { 
+        return false;
+      }
     }
+    return false;
   }
 
   native boolean queryCommandStateAssumingFocus(String cmd) /*-{
@@ -398,10 +419,17 @@
   }-*/;
 
   String queryCommandValue(String cmd) {
-    // When executing a command, focus the iframe first, since some commands
-    // don't take properly when it's not focused.
-    setFocus(true);
-    return queryCommandValueAssumingFocus(cmd);
+    if (isReady) {
+      // When executing a command, focus the iframe first, since some commands
+      // don't take properly when it's not focused.
+      setFocus(true);
+      try {
+        return queryCommandValueAssumingFocus(cmd);
+      } catch (JavaScriptException e) {
+        return "";
+      }
+    }
+    return "";
   }
 
   native String queryCommandValueAssumingFocus(String cmd) /*-{
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 9a55823..fc4ecd0 100644
--- a/user/test/com/google/gwt/user/client/ui/RichTextAreaTest.java
+++ b/user/test/com/google/gwt/user/client/ui/RichTextAreaTest.java
@@ -15,8 +15,10 @@
  */
 package com.google.gwt.user.client.ui;
 
+import com.google.gwt.core.client.GWT;
 import com.google.gwt.junit.client.GWTTestCase;
 import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.ui.RichTextArea.BasicFormatter;
 
 /**
  * Tests the {@link RichTextArea} widget.
@@ -65,6 +67,87 @@
     RootPanel.get().remove(richTextArea);
   }
 
+  public void testFormatAfterAttach() {
+    final RichTextArea area = new RichTextArea();
+    BasicFormatter formatter = area.getBasicFormatter();
+    RootPanel.get().add(area);
+    if (formatter != null) {
+      try {
+        formatter.toggleBold();
+        if (!GWT.isScript()) {
+          fail("Expected AssertionError");
+        }
+      } catch (AssertionError e) {
+        // Expected because the iframe is not initialized
+        return;
+      }
+      if (!GWT.isScript()) {
+        fail("Expected AssertionError");
+      }
+    }
+  }
+
+  public void testFormatAfterInitialize() {
+    final RichTextArea area = new RichTextArea();
+    RootPanel.get().add(area);
+
+    // 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() {
+        BasicFormatter formatter = area.getBasicFormatter();
+        if (formatter != null) {
+          formatter.toggleBold();
+        }
+        RootPanel.get().remove(area);
+        finishTest();
+      }
+    }.schedule(500);
+  }
+
+  public void testFormatBeforeAttach() {
+    final RichTextArea area = new RichTextArea();
+    BasicFormatter formatter = area.getBasicFormatter();
+    if (formatter != null) {
+      try {
+        formatter.toggleBold();
+        if (!GWT.isScript()) {
+          fail("Expected AssertionError");
+        }
+      } catch (AssertionError e) {
+        // expected
+        return;
+      }
+      if (!GWT.isScript()) {
+        fail("Expected AssertionError");
+      }
+    }
+  }
+
+  public void testFormatWhenHidden() {
+    final RichTextArea area = new RichTextArea();
+    RootPanel.get().add(area);
+
+    // 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() {
+        area.setVisible(false);
+        BasicFormatter formatter = area.getBasicFormatter();
+        if (formatter != null) {
+          // This won't work on some browsers, but it should return quietly.
+          formatter.toggleBold();
+        }
+        RootPanel.get().remove(area);
+        finishTest();
+      }
+    }.schedule(500);
+  }
+
   /**
    * Test that a delayed set of HTML is reflected. Some platforms have timing
    * subtleties that need to be tested.